Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b55255614f | |||
| d88d2eaeb8 | |||
| 5e06b14ddf | |||
| 28d73c8b2c | |||
| 46c2c4ef6c | |||
| 0cac4b0b32 | |||
| 97a25dd0b6 | |||
| 82ae6cf5a2 | |||
| 47fa79148c |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"phase": 4,
|
||||
"stage": "execute",
|
||||
"milestone": "v0.2",
|
||||
"milestone_type": "feature",
|
||||
"tag_base": "v0.1.x",
|
||||
"phase_role": "execution",
|
||||
"phase": 0,
|
||||
"stage": "mvp_ux_check",
|
||||
"milestone": "v0.4",
|
||||
"milestone_type": "nfr",
|
||||
"tag_base": "v0.3.x",
|
||||
"phase_role": "pre_execution",
|
||||
"project": "oy",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-17T21:50:00Z"
|
||||
"updated_at": "2026-08-17T22:50:00Z"
|
||||
}
|
||||
@@ -6,9 +6,9 @@
|
||||
}
|
||||
],
|
||||
"active_project": "oy",
|
||||
"milestone": "v0.2",
|
||||
"milestone_type": "feature",
|
||||
"tag_base": "v0.1.x",
|
||||
"milestone": "v0.4",
|
||||
"milestone_type": "nfr",
|
||||
"tag_base": "v0.3.x",
|
||||
"autonomy": {
|
||||
"level": "full",
|
||||
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
||||
|
||||
+171
-1
@@ -57,4 +57,174 @@ Fee Covenant (13) blocks {Pacts (8), Orgs (10), Partners (11), Bearers (12)}
|
||||
## Phase 0 Architecture Deliverables
|
||||
- This index file
|
||||
- Persona assessment (created during RESEARCH stage)
|
||||
- Phase plans (created during PLAN stage)
|
||||
- Phase plans (created during PLAN stage)
|
||||
|
||||
---
|
||||
|
||||
## v0.3 Architecture (Bearers & Documentation)
|
||||
|
||||
This section appends the v0.3 component map to the v0.1/v0.2 index above. It does
|
||||
NOT rewrite or supersede the earlier content; the Phase 1/2/3 columns in the
|
||||
component index above describe the *full* runtime target, while the v0.3 columns
|
||||
below describe the *v0.3 skeleton+tests* deliverable (D-020 pattern continued,
|
||||
D-035) plus the documentation deliverable (D-042).
|
||||
|
||||
### v0.3 Component Index (new + extended modules)
|
||||
|
||||
| # | Component | Vision § | v0.3 Module | New/Ext | Phase | v0.3 Skeleton Depth |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2 | Cross-Chain & Exit (Layer 3) — DEX swaps | §7 | `x/exit` | New | P4 | ExitRoute + DEXSwap types, ExitStatus enum |
|
||||
| 2 | Cross-Chain & Exit (Layer 3) — L2↔L1 bridges | §7 | `x/bridge` | New | P4 | BridgeRoute + BridgeStatus enum; references x/satellite L2Chain by ID (G-003) |
|
||||
| 12 | Bearers expansion (OY-SAT + OY-QR) | §14 | `x/bearers` | Extended | P4 | OYSATLink + OYQRCode transport types (BearerTransport impls); BearerType enum already complete from v0.2 |
|
||||
| 11 | Anchors (institutional Partner tier) | §13 | `x/partner` | Extended | P4 | AnchorCredential struct fields on the Anchor tier (REQ-018 enum unchanged); ListByTier(Anchor) round-trip |
|
||||
| 8 | Hub API (Pact #6 expanded) | §13, §16 | `x/hub` | New | P5 | HubService enum (Custody/LendingPrimitive/Compliance) + per-service struct stubs + keeper stub |
|
||||
| — | Services (Care/SIM/Vault/Mail) | §13 | `x/services` | New | P5 | ServiceKind enum (4) + per-service struct stubs + keeper stub |
|
||||
| 8 | Bond market depth (Growth Bonds + secondary) | §17 | `x/bond` | Extended | P5 | GrowthBond struct + SecondaryOrder types; 8%/0% consts (D-028) unchanged; Clamp reused |
|
||||
|
||||
> The Hub API is Pact #6 (Hub-API) per REQ-020/D-027. v0.2 stubbed it as a PactType
|
||||
> enum value inside `x/pact`; v0.3 promotes it to its own `x/hub` module for the
|
||||
> B2B type scaffold (D-039). The `x/pact` HubAPI enum value stays as a
|
||||
> cross-reference; `x/hub` owns the service-shape types.
|
||||
|
||||
### v0.3 Cross-Component Dependencies (within v0.3)
|
||||
|
||||
Per the v0.2 G-003 invariant (by-ID-string inter-module references; no struct
|
||||
imports across `x/<module>/types`), v0.3 components reference each other and the
|
||||
v0.2 baseline by ID string only. The dependency edges that affect v0.3 phase
|
||||
ordering:
|
||||
|
||||
```
|
||||
x/bridge ──(L2Chain by id)──► x/satellite (v0.2 baseline; ref only, no struct import)
|
||||
x/exit ──(BridgeRoute by id)──► x/bridge (P4: exit references bridge routes)
|
||||
x/hub ──(Anchor by id)──► x/partner (P5: Hub custody/compliance references Anchor partners)
|
||||
x/bond ──(Stand by id)──► x/stand (v0.2 baseline; GrowthBond issuer-stand-id, unchanged)
|
||||
x/services ──(Window by id)──► x/window (v0.2 baseline; service-grant references a Window)
|
||||
x/bearers ──(BearerTransport)──► (none; OY-SAT/OY-QR are transport stubs, no new deps)
|
||||
```
|
||||
|
||||
**Phase-ordering implication (informs D-044):** `x/exit` references `x/bridge`
|
||||
routes, so both must land in the same phase (P4) and `x/bridge` types must exist
|
||||
before `x/exit` tests that reference a BridgeRoute. `x/hub` references Anchor
|
||||
partner-ids, so `x/partner` Anchor extension (P4) must precede `x/hub` (P5). This
|
||||
confirms the D-044 P4→P5 split: P4 = exit/bridge/bearers/partner-Anchor,
|
||||
P5 = hub/services/bond. Reversing P4/P5 would force `x/hub` to reference an Anchor
|
||||
tier that does not yet exist.
|
||||
|
||||
### v0.3 Interface Contracts (6 cross-component — unchanged from v0.2)
|
||||
|
||||
The six cross-component interfaces (Standing API, Forge/Fold, Watcher Attestation,
|
||||
Window Lifecycle, Fee Covenant, Voice/Council) are NOT extended in v0.3 — v0.3
|
||||
adds *type scaffolds* that will *consume* them at runtime in v0.4+:
|
||||
|
||||
- **Window Lifecycle Interface** — `x/services` service-grants reference a Window
|
||||
by ID (the service opens a Window on the holder's behalf). Skeleton only.
|
||||
- **Fee Covenant Interface** — `x/bridge`/`x/exit` exit routes carry an
|
||||
`exit-fee-bps` field clamped by the Fee Covenant ceiling/floor (the field is
|
||||
typed in v0.3; the Clamp is NOT invoked in the skeleton — deferred to v0.4
|
||||
runtime to avoid cross-module calls in the skeleton layer).
|
||||
- **Standing API** — `x/hub` compliance service stub references a partner's
|
||||
Standing by reach-id (skeleton: by-ID-string field, no query).
|
||||
- **Watcher Attestation** — `x/bridge` BridgeStatus has an `Attested` state; the
|
||||
attestation itself is not modeled in v0.3 (Watchers are v0.1 baseline; the
|
||||
bridge references a Watcher quorum by ID at runtime, deferred to v0.4).
|
||||
|
||||
### Documentation Architecture (v0.3 deliverable B)
|
||||
|
||||
v0.3 introduces a documentation deliverable alongside the Bearers skeleton. This
|
||||
is a NEW architecture surface (no docs site existed in v0.1/v0.2).
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
oy/
|
||||
├── README.md # repo-root project overview (lexicon-clean)
|
||||
├── mkdocs.yml # MkDocs Material config (site_name, nav, theme)
|
||||
└── docs/
|
||||
├── nomads/ # audience: nomads (Reach path, Stash, bearers, Maps/Pay, Pacts, standing basics)
|
||||
├── freeholders/ # audience: freeholders (4 signals, Bayesian Standing, Stands/Guilds, Councils/Voice, Bonds, Partner spectrum)
|
||||
├── shared/ # cross-audience (Six Principles, Bread Scale, Storage pools, Watchers/Mirror, Lexicon glossary, Vision overview)
|
||||
└── reference/ # architecture index + component map
|
||||
```
|
||||
|
||||
**mkdocs.yml (minimal config):** `site_name: OpenYield`, `theme: readthedocs` or
|
||||
`theme: material` (D-042 chose Material), `nav:` with the four audience
|
||||
sections, `markdown_extensions: [admonition, toc, pymdownx.superfences]`. Build-
|
||||
only Python dep (`mkdocs` + `mkdocs-material`); `go.mod` stays zero-dep (G-006 —
|
||||
the docs toolchain is NOT a Go dependency). No publishing CI in v0.3 (D-046);
|
||||
README documents `mkdocs serve` / `mkdocs build`.
|
||||
|
||||
**Audience-organized nav (D-042, D-045):** nomads 5-8 pages, freeholders 5-8
|
||||
pages, shared 5-6 pages, reference 2 pages (~20-25 total). Pages map to REQs:
|
||||
nomads cover REQ-007/013/014/015/019/020; freeholders cover REQ-005/006/016/017/
|
||||
011/021/018; shared covers REQ-001/003/004/012; reference covers the architecture
|
||||
index.
|
||||
|
||||
**Lexicon-clean by construction (REQ-012 extension, D-043):** docs are user-
|
||||
facing and must be lexicon-clean. The highest-risk banned term in docs is
|
||||
"yield" (PROJECT.md uses "real yield" but docs must say "real production" / "real
|
||||
return" — the word-boundary regex in `lexicon.FindBannedTerm` bans standalone
|
||||
"yield" while allowing "OpenYield"). Other high-risk terms in docs: "account"
|
||||
(use "Holder"/"Reach"), "bank"/"deposit"/"savings" (use "Stash"/"Vault"/
|
||||
"Root-Pool"). The firewall lands in P1 BEFORE content (P2/P3) so docs are checked
|
||||
as authored (D-044 firewall-first ordering).
|
||||
|
||||
**Firewall extension (D-043):** a NEW sibling test `lexicon_meta_docs_test.go`
|
||||
(package `lexicon_meta_docs`) mirrors `lexicon_meta_test.go` (package
|
||||
`lexicon_meta`) exactly — same `lexicon.FindBannedTerm`, same word-boundary
|
||||
regex, same fragment-assembled self-test table, same self-exclusion of the meta-
|
||||
test file — but scans `README.md` + `docs/**/*.md` instead of `x/**/*.go`. The
|
||||
existing `lexicon_meta_test.go` is NOT modified (preserves v0.2 coverage). The
|
||||
new meta-test walks the repo root for `README.md` + the `docs/` tree, excludes
|
||||
`.ciagent/` and `.git/` (firewall meta-files are not user-facing docs), and
|
||||
excludes itself. Per-package lexicon assertions in the new `x/*` modules follow
|
||||
the v0.2 pattern (`TestLexiconNoBannedTermsIn<Module>Package` scanning the
|
||||
module's production `.go` files).
|
||||
|
||||
> The `.ciagent/` directory holds firewall META-files (PROJECT.md, RESEARCH.md,
|
||||
> this file) that discuss the banned terms by name for governance reasons — they
|
||||
> are NOT user-facing docs and are explicitly excluded from the docs firewall
|
||||
> scan. This mirrors how `lexicon_meta_test.go` excludes itself: the firewall's
|
||||
> own code is allowed to name the terms it bans.
|
||||
|
||||
## v0.4 Architecture (Refinement — NFR)
|
||||
|
||||
v0.4 is a refinement-only NFR milestone (D-047): zero `feat:` phases, zero new
|
||||
production types, zero behavioral changes. It lands durability fixes sourced
|
||||
from v0.3 forward-references. Tags run on the `v0.3.x` patch line.
|
||||
|
||||
### v0.4 Research Findings
|
||||
|
||||
**R-029 — Lexicon firewall shared helper (REQ-029, GRILL G-014).**
|
||||
|
||||
Verified during v0.4 RESEARCH: `lexicon_meta_test.go` (`TestLexiconMetaSelfTestTable`, lines 83-118) and `lexicon_meta_docs/lexicon_meta_docs_test.go` (`TestLexiconMetaDocsSelfTestTable`, lines 147-182) contain byte-identical duplicate synthetic self-test tables — both build the same 10-string slice by indexing `lexicon.BannedTerms()`. This is exactly the G-014 drift risk: if a future banned-term addition updates one table and not the other, the docs firewall silently loses coverage. The fix is a new `lexicon.SyntheticBannedStrings() []string` helper in `lexicon/lexicon.go` that returns the 10 synthetic strings; both meta-tests consume it instead of building their own copy. The helper's source uses `lexicon.BannedTerms()` (already fragment-assembled) so the lexicon package's own source stays lexicon-clean. Both meta-tests already assert `len(terms) == 10` from `lexicon.BannedTerms()` (the G-014 minimum); the helper closes the drift fully. No behavioral change to detection (`FindBannedTerm` unchanged); refactor + test only.
|
||||
|
||||
**R-030 — Cross-package const-equality test (REQ-030, REVIEW P2 / A-304).**
|
||||
|
||||
Verified during v0.4 RESEARCH: `x/hub/types/types.go:51,56` defines LOCAL consts `LendingCouponCapBps = uint32(800)` and `LendingCouponFloorBps = uint32(0)`, cross-documented (comment lines 46-55) to `x/bond/types/types.go:21,26` consts `CouponCapBps = 800` and `CouponFloorBps = 0` (D-028 mission-locked). The cross-doc comment flags drift for human review but no automated check exists. The fix is a new test file `x/hub/types/cross_const_test.go` (package `types`) that imports `github.com/oy/openyield/x/bond/types` (test-only, G-003 exempt per the test-import exemption documented in v0.2 GRILL G-003) and asserts `hub.LendingCouponCapBps == bond.CouponCapBps` and `hub.LendingCouponFloorBps == bond.CouponFloorBps`. The test fails closed if either const drifts. No production import is added (G-003 production firewall intact); test-only import only.
|
||||
|
||||
**R-031 — Lifecycle type shape-divergence review (REQ-031, AUDIT §193).**
|
||||
|
||||
Verified during v0.4 RESEARCH: AUDIT §193 flags two P1 council divergences and one P2 bearers nit:
|
||||
- **P1-1**: `x/council/types` lacks `Proposal`/`ProposalStatus`/`VoteOption` enums (AUDIT says add "in v0.3 when wiring the council keeper to a live governance runtime"). Adding these is a `feat:`-class addition (new enum types) → REJECTED by D-001 filter for v0.4. Deferred to v0.5+ governance runtime.
|
||||
- **P1-2**: `SignalKind` has 4 sources (Stash/Standing/Vouch/Capital) vs spec's `VoiceSource` 5 sources (Stash/Standing/Vouch/Freeholder/Guild). AUDIT code rationale: Freeholder is an eligibility property (upstream in `x/standing`), Guild is a council tier, Capital is committed-capital (vision §9.1) — defensible refinement. Changing `SignalKindCount` 4→5 is a locked-const change → REJECTED by D-001 filter for v0.4.
|
||||
- **P2**: `x/bearers/types` `ValidateGenesis` no-op is CORRECT per spec (AUDIT explicitly notes "no action").
|
||||
|
||||
v0.4 REQ-031 scope (D-050): DOCUMENT the divergence decisions in this ARCHITECTURE.md section + add a regression-guard test asserting the current `SignalKindCount==4` shape is intentional (an intent-assertion test, not a shape change). No enum additions, no locked-const changes. The existing `TestSignalKindCountLockedConst` in `x/council/types/types_test.go:102` already asserts the count; REQ-031 adds an intent comment + a test documenting WHY the shape is 4-not-5 (the AUDIT rationale), so a future agent does not "fix" the divergence by silently changing the locked const.
|
||||
|
||||
**R-032 — Docs build CI (REQ-032, D-046).**
|
||||
|
||||
Verified during v0.4 RESEARCH: no `.github/workflows/` directory exists; Gitea Actions uses `.gitea/workflows/`. `mkdocs.yml` is present at repo root (buildable locally via `mkdocs build`). v0.4 REQ-032 ships a `.gitea/workflows/docs-build.yml` workflow that: (1) runs `go test ./...` (the lexicon firewall + all x/* tests) on push; (2) installs mkdocs + mkdocs-material (build-only Python deps in a separate job/step — does NOT touch `go.mod`, G-006 intact); (3) runs `mkdocs build` to produce `site/`; (4) uploads `site/` as a CI artifact. Full Gitea Pages publishing is DEFERRED (no hosting target configured in v0.4 per D-051). The workflow file is `chore` (CI config), not `feat:` — passes the D-001 filter. The workflow runs on every push to any branch (not just main) so the lexicon firewall + docs build are checked on every change.
|
||||
|
||||
### v0.4 Component Map (no new modules)
|
||||
|
||||
v0.4 touches NO new `x/*` modules. The touched files are:
|
||||
- `lexicon/lexicon.go` (add `SyntheticBannedStrings()`) — REQ-029
|
||||
- `lexicon_meta_test.go` (refactor to consume helper) — REQ-029
|
||||
- `lexicon_meta_docs/lexicon_meta_docs_test.go` (refactor to consume helper) — REQ-029
|
||||
- `x/hub/types/cross_const_test.go` (NEW test file) — REQ-030
|
||||
- `x/council/types/types_test.go` (add intent-assertion test + comment) — REQ-031
|
||||
- `.ciagent/oy/ARCHITECTURE.md` (this section) — REQ-031
|
||||
- `.gitea/workflows/docs-build.yml` (NEW CI workflow) — REQ-032
|
||||
|
||||
### v0.4 Interface Contracts (unchanged from v0.3)
|
||||
|
||||
v0.4 does not change any cross-component interface. The 6 cross-component interfaces (Standing, Forge/Fold, Mirror, Window, Fee Covenant, Voice/Council) are unchanged. REQ-031 documents a divergence in the Voice/Council interface surface (SignalKind shape) but does not change it.
|
||||
+109
-1
@@ -299,4 +299,112 @@ Escalations: 0
|
||||
Overall verdict: PASS (after critical fixes)
|
||||
Confidence: 0.90
|
||||
AUDIT.md written: /root/oy/.ciagent/oy/AUDIT.md ✓
|
||||
```
|
||||
```
|
||||
---
|
||||
|
||||
# Audit: OpenYield (oy) — v0.3 (Bearers & Documentation) Final Phase (P6)
|
||||
|
||||
> **Auditor**: CIAgent doc verifier (final-audit mode, full autonomy)
|
||||
> **Date**: 2026-08-17
|
||||
> **Scope**: v0.3 milestone state on `oy/milestone/v0.3-bearers-docs` (HEAD = `oy/phase/06-final-review-ship`)
|
||||
> **Milestone**: v0.3 — Bearers & Documentation (feature type; tag_base `v0.2.x`)
|
||||
> **Mode**: multi-project (slug `oy`)
|
||||
> **Autonomy**: full
|
||||
|
||||
---
|
||||
|
||||
## v0.3 Final Audit (P6)
|
||||
|
||||
### Reconstruction Test — **PASS**
|
||||
|
||||
**Git log matches `.ciagent/` files.** `git log v0.1.5..HEAD --oneline` returns 13 commits across P0-P5 (6 phase-ship `docs(P##):` commits, 5 `checkpoint(P##):` advance commits, 1 v0.2 milestone marker inherited via v0.1.5). The 6 `docs(P##): complete ...` commits each carry a `---ci---` block with `status: complete` and the correct phase integer.
|
||||
|
||||
**Per-phase `---ci---` block verification:**
|
||||
|
||||
| Phase | Commit | Tag | `status` | `requirements.covered` | `requirements.partial` | Verdict |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P0 | 23de3c5 | v0.2.0 | complete | [] (pre-execution) | [] | PASS |
|
||||
| P1 | a780884 | v0.2.1 | complete | [REQ-028] | [REQ-027] | PASS |
|
||||
| P2 | d09c613 | v0.2.2 | complete | [] (nomads docs) | [REQ-027] | PASS |
|
||||
| P3 | 2ef3f2e | v0.2.3 | complete | [REQ-027] | [] | PASS |
|
||||
| P4 | ab43bef | v0.2.4 | complete | [REQ-010, REQ-022, REQ-023] | [] | PASS |
|
||||
| P5 | c1aa274 | v0.2.5 | complete | [REQ-024, REQ-025, REQ-026] | [] | PASS |
|
||||
|
||||
- Phase `---ci---` blocks: 6 (one per phase P0-P5). Each phase's final block shows `status: complete`. ✓
|
||||
- Tag count: 6 (v0.2.0..v0.2.5). Each tag exists and points at the matching `docs(P##): complete ...` commit. ✓
|
||||
- REQ coverage vs. expected (P0: none; P1: REQ-028; P2: partial REQ-027; P3: REQ-027; P4: REQ-010/022/023; P5: REQ-024/025/026): **exact match on all 6 phases**. ✓
|
||||
- IDEATE traceability (REQUIREMENTS.md §"IDEATE Traceability"): 8 IDEATE-NN → REQ-ID mappings present (IDEATE-01→REQ-027 ... IDEATE-08→REQ-026). ✓
|
||||
- CHECKPOINT.json matches state: `phase: 5`, `stage: complete`, `milestone: v0.3`, `tag_base: v0.2.x`, `milestone_complete: false`, `phase_release_tag: v0.2.5` — consistent with "P5 complete, advancing to P6 final review/audit/ship". ✓
|
||||
|
||||
**Reconstruction test verdict: PASS** (6/6 phase blocks well-formed; 6/6 tags present; 6/6 REQ-coverage sets match; CHECKPOINT current).
|
||||
|
||||
### File Discipline — **PASS** (after fix)
|
||||
|
||||
`.ciagent/oy/` contains: PROJECT.md, ROADMAP.md, REQUIREMENTS.md, ARCHITECTURE.md, RESEARCH.md, PERSONAS.md, PLANS.md, GRILL.md, REVIEW.md, AUDIT.md. Plus historical P1_SHIP_VERIFICATION.md..P4_SHIP_VERIFICATION.md (v0.2 audit artifacts; not orphan — referenced by v0.2 AUDIT.md).
|
||||
|
||||
`.ciagent/` (root, multi-project) contains: CHECKPOINT.json, config.json, oy/ (slug subdir). ✓ config.json valid (`projects[]` length 1, `active_project: oy`, `milestone: v0.3`, `tag_base: v0.2.x`, `autonomy.level: full`). ✓
|
||||
|
||||
**Stale-content fixes applied during this audit:**
|
||||
- REQUIREMENTS.md v0.3 table: all 8 REQs were marked `Pending` despite P1-P5 shipping them. Updated REQ-010/022/023/024/025/026 → `Skeleton`, REQ-027/028 → `Complete` to match the `---ci---` coverage blocks.
|
||||
- ROADMAP.md v0.3 milestone: header read `ACTIVE` with no per-phase completion markers; P0-P5 shipped. Added `[x]` markers for P0-P5 and `[ ]` for P6-in-progress, plus a status line.
|
||||
|
||||
No orphan files detected. REVIEW.md exists (v0.2 content; review agent may append v0.3 section concurrently — tracked as pending, non-blocking).
|
||||
|
||||
**File discipline verdict: PASS** (after REQUIREMENTS + ROADMAP freshness fixes).
|
||||
|
||||
### Branch Hygiene — **PASS**
|
||||
|
||||
- `git branch -a` lists: `main`, `oy/milestone/v0.3-bearers-docs`, `oy/phase/06-final-review-ship` (current), `remotes/origin/main`, `remotes/origin/oy/milestone/v0.3-bearers-docs`.
|
||||
- No leftover execution phase branches (`oy/phase/01-05`): grep for `phase/0[1-5]` returned zero. ✓ Phase branches deleted after merge.
|
||||
- Milestone branch `oy/milestone/v0.3-bearers-docs` exists and is at the P5-checkpoint commit (62ff0d7), matching the final-phase branch HEAD. ✓
|
||||
- Final-phase branch `oy/phase/06-final-review-ship` exists and tracks milestone HEAD. ✓
|
||||
|
||||
**Branch hygiene verdict: PASS.**
|
||||
|
||||
### Commit Discipline — **PASS**
|
||||
|
||||
- 6 phase-ship commits follow `docs(P##): complete ...` convention (P00..P05). ✓
|
||||
- 5 checkpoint commits follow `checkpoint(P##): ...` convention. ✓
|
||||
- All 6 `---ci---` blocks well-formed (opening `---ci---`, closing `---/ci---`, YAML keys `project: oy`, `phase: N`, `milestone: v0.3`, `status: complete`, `tag_base: v0.2.x`, `phase_role`, `requirements.covered`, `requirements.partial`). ✓
|
||||
- Multi-project `project: oy` field present in every `---ci---` block. ✓
|
||||
- No malformed blocks, no missing closing tags, no orphan phase markers.
|
||||
|
||||
**Commit discipline verdict: PASS.**
|
||||
|
||||
### Build/Test Sanity — **PASS**
|
||||
|
||||
- `go build ./...` → GREEN (exit 0). ✓
|
||||
- `go test ./...` → 26 packages GREEN, 4 packages `[no test files]` (identity/processing/rootpool/vault — pre-existing v0.1 layout), zero FAIL. ✓
|
||||
- New v0.3 packages present and green: x/exit, x/bridge, x/hub, x/services (plus x/bearers, x/partner, x/bond extended; lexicon_meta_docs at root). ✓
|
||||
|
||||
### Fixes Applied
|
||||
|
||||
| Fix | File | Change | Severity |
|
||||
|---|---|---|---|
|
||||
| 1 | `.ciagent/oy/REQUIREMENTS.md` | v0.3 REQ table statuses: 8 REQs Pending → 6 Skeleton + 2 Complete (matches `---ci---` coverage) | critical (stale docs) |
|
||||
| 2 | `.ciagent/oy/ROADMAP.md` | v0.3 milestone: added P0-P5 `[x]` completion markers + P6 `[ ]` + status line | critical (stale docs) |
|
||||
|
||||
Both fixes are committed under `fix(P06-audit):` per the final-audit protocol (see commit below).
|
||||
|
||||
### Overall Audit Verdict — **PASS**
|
||||
|
||||
```
|
||||
Per-check verdicts (v0.3 final):
|
||||
1. Reconstruction test — PASS (6 phase blocks; 6 tags v0.2.0..v0.2.5; REQ coverage exact on all 6 phases; CHECKPOINT current)
|
||||
2. File discipline — PASS (after fix; 10 .ciagent/oy/ files + CHECKPOINT.json + config.json; no orphans)
|
||||
3. Branch hygiene — PASS (no phase/01-05 branches; milestone + final-phase present)
|
||||
4. Commit discipline — PASS (6 docs(P##) + 5 checkpoint(P##); all ---ci--- well-formed; project: oy present)
|
||||
5. Build/test sanity — PASS (build GREEN; 26 pkgs GREEN; new v0.3 packages green)
|
||||
|
||||
Critical issues: 2 found → 2 fixed → 0 remaining
|
||||
- Critical-1: REQUIREMENTS.md v0.3 statuses stale (Pending vs Skeleton/Complete) → FIXED
|
||||
- Critical-2: ROADMAP.md v0.3 P0-P5 completion markers missing → FIXED
|
||||
|
||||
Non-critical: 1 (REVIEW.md v0.3 section pending concurrent review agent — non-blocking)
|
||||
Escalations: 0
|
||||
Overall verdict: PASS (after critical fixes)
|
||||
Confidence: 0.92
|
||||
AUDIT.md appended: /root/oy/.ciagent/oy/AUDIT.md ✓ (v0.3 section appended; v0.2 content preserved)
|
||||
```
|
||||
|
||||
AUDIT.md
|
||||
|
||||
+205
-1
@@ -181,4 +181,208 @@ Per-axis verdicts:
|
||||
Binding decisions: 10 (G-001..G-010)
|
||||
Escalations: 0
|
||||
Overall: SHIP Phase 0 with binding changes (confidence 0.83)
|
||||
```
|
||||
```
|
||||
---
|
||||
|
||||
## v0.3 Grill (Phase 0)
|
||||
|
||||
> **Reviewer**: CIAgent adversarial grill (red-team, full autonomy)
|
||||
> **Date**: 2026-08-17
|
||||
> **Target**: v0.3 Phase 0 artifacts (PROJECT.md D-034..D-046, ROADMAP.md v0.3 table, REQUIREMENTS.md REQ-010/022..028, ARCHITECTURE.md v0.3 section, PERSONAS.md v0.3 roster, RESEARCH.md A-301..A-315, PLANS.md v0.3 plan 50 tasks P1-P6) + v0.1/v0.2 codebase baseline
|
||||
> **Milestone**: v0.3 — Bearers & Documentation
|
||||
> **Autonomy**: full (decision_confidence_threshold = 0.60)
|
||||
> **Mode**: multi-project (slug `oy`)
|
||||
|
||||
### Evidence baseline (verified against the actual repo, not the docs)
|
||||
|
||||
- `go.mod`: `module github.com/oy/openyield`, `go 1.22`, **zero dependencies** (confirmed — no require lines).
|
||||
- `x/` modules: **25** (confirmed via `ls x/ | wc -l`). v0.1 = 15 + v0.2 added 10 = 25. v0.3 adds 4 new (exit/bridge/hub/services) + extends 3 (bearers/partner/bond) = 29 distinct after v0.3. PLANS.md P6-01-01 says "29 packages" — correct.
|
||||
- Test functions: **299** `func Test*` across **23** test files (counted `grep -rn "^func Test" x/ | wc -l`). v0.2 grill G-001 corrected to 53/11; v0.2 added ~246 more. The v0.3 plan's "~303" reference (REQUIREMENTS IDEATE notes) is close to current 299 — minor, not material.
|
||||
- `lexicon/lexicon.go`: `BannedTerms()` returns **10** (verified `if len(terms) != 10` at lexicon.go). The v0.2 `lexicon_meta_test.go` header comment still says "9 banned terms" (line 7) — a **pre-existing v0.2 documentation defect**, not v0.3's, but the v0.3 docs firewall must use 10 (it does — P1-01-01 says exactly 10). No v0.3 binding needed; note for v0.2 housekeeping.
|
||||
- `lexicon_meta_test.go` EXISTS at repo root, package `lexicon_meta`, with the **G-009 self-test table** (synthetic strings, `len(synthetic) != len(terms)` guard). The self-test verifies **detection** but does NOT verify the **walk** (which files are scanned). The v0.3 docs firewall inherits this gap — G-013 below.
|
||||
- G-003 import-invariant test EXISTS: `x/window/types/types_test.go:431-456` uses `go/parser` (ImportsOnly) to scan all non-test `.go` under `x/` and assert no cross-`x/<module>/types` struct imports. **Confirmed production code is cycle-free** (`grep -rn "openyield/x/" x/ --include="*.go" | grep -v "_test.go"` returns empty). The v0.3 by-ID-string refs (exit→bridge, hub→partner, services→window) will be auto-covered by this existing test — no new G-003 work needed in v0.3 (P4/P5 test tasks correctly say "G-003 import-invariant green" not "add a new one").
|
||||
- `x/bond/types/types.go`: `CouponCapBps = 800`, `CouponFloorBps = 0`, `func Clamp(couponBps uint32) uint32` all exist. The D-028 regression firewall is real and the v0.3 GrowthBond extension reuses the same package (no G-003 concern, correct).
|
||||
- `x/partner/types/types.go`: `TierAnchor PartnerTier = "Anchor"` exists; no `AnchorCredential` yet (v0.3 P4 adds it). The 4-tier enum is locked — P4 extension adds a struct, not a tier. Correct.
|
||||
- `x/bearers/types/types.go`: `BearerOYSAT` and `BearerOYQR` are ALREADY in `AllBearers()` (lines 20-21, 39-40) since v0.1; `BearerTransport` interface + `OYLRLink` + `SurveillanceResistant` exist since v0.2. P4 adds `OYSATLink`/`OYQRCode` transport structs only — `AllBearers()` count (6) stays unchanged. Correct.
|
||||
- `x/window`, `x/stand`, `x/satellite` (L2Chain/TransferChannel), `x/watcher`, `x/identity` (Reach) all exist as v0.1/v0.2 baseline — the v0.3 by-ID-string refs to them (bridge→satellite/watcher, services→window/identity, bond→stand) have real targets. No phantom refs.
|
||||
- **No `docs/`, no `README.md`, no `mkdocs.yml` exist** today. The firewall-first ordering is clean: `lexicon_meta_docs_test.go` passes vacuously with zero docs to scan (no hits possible). The "chicken-and-egg between firewall test and README" risk raised in the grill brief is a **non-issue** — verified by walk logic (zero files = zero hits).
|
||||
|
||||
These baseline facts confirm the v0.3 plan's architecture and ordering claims against the actual codebase, not just the docs. The plan is unusually well-grounded; the binding decisions below are mostly small correctness fixes, not scope rework.
|
||||
|
||||
### Per-Axis Verdicts
|
||||
|
||||
#### Axis 1 — Feasibility (50 tasks / 6 phases at full autonomy) — **PASS** (confidence 0.82)
|
||||
|
||||
50 tasks across 6 phases is large but proportionate to the deliverable: 26 docs pages (P1-P3) + 7 x/* packages (P4-P5) + audit/ship (P6). v0.2 shipped 31 tasks / 5 phases for 10 packages at full autonomy and closed clean (per REQUIREMENTS.md v0.2 summary). v0.3 adds the docs surface (a genuinely new artifact type) and 4 new + 3 extended Go packages. The docs phases (P1-P3) are low-risk Markdown authoring gated by a Go firewall; the Bearers phases (P4-P5) are pure skeleton+tests, the proven v0.1/v0.2 pattern. No phase exceeds 12 tasks (P3 is largest at 12, all docs). The firewall-first claim is achievable: a firewall that scans zero files passes trivially (verified — no docs exist yet). Reject the "50 tasks is too many" hypothesis — it matches the deliverable surface.
|
||||
|
||||
#### Axis 2 — Scope (Bearers + docs bundle, ~22 pages) — **PASS** (confidence 0.78)
|
||||
|
||||
D-034 (bundle Bearers + docs under one feature milestone) is defensible: the user's `--ideate` request was docs-only, but ROADMAP Phase 3 (Bearers) is the next queued feature work; bundling keeps the milestone cadence and avoids a docs-only NFR milestone that would not advance the protocol. The alternative (separate v0.3 docs NFR + v0.4 Bearers) would split a coherent unit of work into two milestones and delay the Bearers skeleton a full cycle. The ~20-25 page docs depth (D-045) is bounded — not gold-plating; each page maps to a REQ (REQUIREMENTS.md IDEATE traceability table). The one scope concern: the **mkdocs.yml nav in RESEARCH §2.1 is missing 2 pages** the plan creates — `nomads/window.md` (P2-01-07) and `freeholders/anchor-preview.md` (P3-01-07). This is a documentation/plan inconsistency, not a scope defect — **binding fix G-011** requires the nav to list all created pages. Confidence holds.
|
||||
|
||||
#### Axis 3 — Cost (MkDocs Material, firewall extension) — **PASS** (confidence 0.85)
|
||||
|
||||
MkDocs Material is the right cost: Markdown-native (docs-writer authors `.md`, not YAML/HTML), build-only Python dep that does NOT touch `go.mod` (verified zero require lines; G-006 holds). Plain-Markdown-no-generator would be cheaper but loses nav/search/theme — for a user-facing docs site for nomads/freeholders, Material's search + audience nav is real value, not gold-plating. The firewall extension (D-043: a whole new sibling test `lexicon_meta_docs_test.go` + self-test table) is more than "could review manually" — manual review is not durable; the sibling test is the firewall that keeps the docs site lexicon-clean over time (the top risk per Axis 5). The cost is justified. Reject the "firewall is over-engineered for docs" hypothesis — REQ-012 is `All` phases and docs are user-facing; a manual review would rot.
|
||||
|
||||
#### Axis 4 — Technical soundness (by-ID-string refs, ClampGrowth, LendingCouponCapBps) — **CONDITIONAL** (confidence 0.72)
|
||||
|
||||
The by-ID-string refs (G-003) between bridge/exit/partner/hub/services are cycle-free by construction (verified: the existing G-003 import-invariant test in `x/window/types/types_test.go` scans all non-test `x/**/*.go` and will auto-cover the v0.3 files; production code has zero cross-`x/` imports today). The `x/hub` `LendingCouponCapBps = 800` LOCAL const (A-304) is a real invariant — it's a local copy cross-documented to D-028, exactly mirroring how v0.2 `x/guild` cross-docs `x/feecovenant.WaiverHandPassGuild` (verified pattern). The one technical defect: **`ClampGrowth(currentBps, growthBps uint32) uint32` as specified in RESEARCH §1.7 / PLANS P5-03-01 returns `min(CouponCapBps - currentBps, growthBps)`, which underflows when `currentBps > CouponCapBps`** (uint32 subtraction wraps to a huge value, then `min` picks `growthBps` — wrong) or when `currentBps == cap` (returns 0, correct) but is fragile. The invariant "post-growth coupon ≤ 800" only holds if the caller guarantees `currentBps ≤ cap`. The spec does not state this precondition, and a GrowthBond whose current coupon is already at cap would silently allow unbounded growth via the `growthBps` path if the helper is misused. **Binding fix G-012** requires `ClampGrowth` to guard `currentBps > CouponCapBps` explicitly (return 0 or error) so the invariant holds unconditionally. Confidence holds after the fix.
|
||||
|
||||
#### Axis 5 — Risk (lexicon drift, "yield" in docs) — **CONDITIONAL** (confidence 0.74)
|
||||
|
||||
The top risk is correctly identified: "yield" is banned as a standalone word but "OpenYield" is safe (word-boundary regex, verified `TestLexiconMetaNoFalsePositiveOnOpenYield`); PROJECT.md uses "real yield" but docs must say "real production"/"real return". The firewall-first ordering (D-044: P1 firewall before P2/P3 content) is the correct mitigation — a banned term slipped into a P2 nomads page fails the P2 build, not the P6 review. The firewall extension (D-043) is sufficient to CATCH drift at build time. The gap: the firewall does not PREVENT the docs-writer from authoring a banned term in the first place — it fails the phase build, requiring a rewrite. For 26 pages this is acceptable (the failure is loud and local); for 100+ pages it would be painful. At the v0.3 scale, the firewall is sufficient. The deeper risk: **the docs firewall self-test table (G-009 for docs) verifies DETECTION but not the WALK** — if the walk logic misses `docs/nomads/` (e.g., a path-prefix bug), the self-test still passes (it tests `FindBannedTerm` on synthetic strings, not the file walk). **Binding fix G-013** requires the docs firewall to include a walk-coverage assertion: a test that injects a synthetic banned-term `.md` into a temp `docs/` subtree (or uses a fixture) and asserts the walk FINDS it. Without this, the docs firewall could silently scan zero files and report green. Confidence holds after the fix.
|
||||
|
||||
#### Axis 6 — Dependency (intra-P4, P4→P5, hidden edges) — **PASS** (confidence 0.80)
|
||||
|
||||
The intra-P4 edge (bridge→exit) and the P4→P5 edge (partner-Anchor→hub) are the only v0.3-internal ordering constraints, and both are correctly handled (P4 Wave 1 = bridge before exit Wave 2; P4 before P5 for Anchor→hub). The RESEARCH §3 cross-component dependency list is complete for the v0.3 surface. Verified the "hidden edges" raised in the grill brief:
|
||||
- **Does x/services need x/window types?** Yes, by-ID-string (`window-id` field) — but x/window is v0.2 baseline, already shipped. Not a v0.3 phase-ordering concern. Correctly noted in RESEARCH §3.
|
||||
- **Does x/bond GrowthBond need x/anything?** No — GrowthBond embeds the v0.2 Bond (same package, `x/bond/types`), and `ClampGrowth` reuses the same-package consts. No G-003 concern. Correct.
|
||||
- **Does x/hub need x/bond?** No — it uses a LOCAL const `LendingCouponCapBps = 800` (A-304) to avoid the import. Correct (verified pattern matches v0.2 guild/feecovenant).
|
||||
- **Does x/exit need x/bread?** No — `amount-grain` is int64, "Grain" by name only (P4-02-01 explicitly says "NOT a `x/bread` import"). Correct.
|
||||
|
||||
No hidden edges. The P5 "no intra-phase ordering" claim (hub/services/bond independent) is correct — they reference only v0.1/v0.2 baseline modules by ID-string, not each other.
|
||||
|
||||
#### Axis 7 — Testing (≥80% on skeletons, docs phases no Go coverage) — **CONDITIONAL** (confidence 0.70)
|
||||
|
||||
≥80% coverage on skeleton type packages is achievable but borders on coverage theater (testing getters/constructors/enum-round-trips on trivial types). v0.2 hit ≥95.9% on 8 of 10 packages at this bar, so it's not theater in practice — the locked-const + invariant + lexicon assertions carry real regression value. The plan correctly applies the ≥80% bar only to the 7 x/* packages (P4/P5), NOT to the docs phases (P1-P3 produce no Go code except the firewall test, which is itself the coverage). The one gap: **P1-01-01 (the docs firewall) and P4/P5 test files have no explicit coverage target** — the firewall test's own coverage is not asserted. A firewall test that scans zero files (walk bug) would still have high coverage on its detection logic. **Binding fix G-013** (walk-coverage assertion, from Axis 5) addresses this — the injected-fixture test forces the walk to actually execute. No separate coverage bar needed for docs phases. Confidence holds.
|
||||
|
||||
#### Axis 8 — Maintainability (docs lexicon over time, sibling test drift) — **CONDITIONAL** (confidence 0.72)
|
||||
|
||||
The docs site will stay lexicon-clean over time ONLY if the firewall runs on every change. D-046 defers publishing CI to v0.4, but the firewall is a `go test` — it runs locally and in any CI that runs `go test ./...`. The risk is not "no CI" (the firewall runs wherever `go test` runs) but "the sibling test `lexicon_meta_docs_test.go` drifts from `lexicon_meta_test.go`". D-043 chose a sibling (not an extension) to preserve v0.2 coverage — defensible — but two meta-tests sharing detection logic via the same `lexicon.FindBannedTerm` is good; sharing the self-test table by DUPLICATION (not by a shared helper) is the drift risk. If `lexicon_meta_test.go`'s self-test table is updated (e.g., a new banned term added) and `lexicon_meta_docs_test.go`'s copy is not, the docs firewall silently loses coverage. **Binding fix G-014**: the docs firewall's self-test table and banned-term count assertion should DERIVE from `lexicon.BannedTerms()` (which both already do for the count — good) and ideally share the synthetic-string table via a `lexicon` package helper rather than duplicating it. At minimum, both must assert `len(terms) == 10` from the single source `lexicon.BannedTerms()` so a count change breaks both. This is a low-severity maintainability note, not a blocker. Confidence holds.
|
||||
|
||||
#### Axis 9 — Adversarial (what makes v0.3 fail to ship) — **PASS** (confidence 0.78)
|
||||
|
||||
The four failure modes raised in the grill brief:
|
||||
1. **Firewall blocks docs content mid-authoring** — MITIGATED by firewall-first (P1 firewall passes vacuously with zero docs; P2/P3 content fails fast and local, not at P6). Not a ship-blocker.
|
||||
2. **By-ID-string ref breaks when a target module is renamed** — LOW. The targets (satellite/watcher/identity/window/stand) are v0.1/v0.2 baseline, locked. v0.3 does not rename them. The by-ID-string fields are opaque strings, not Go imports, so a rename would only break tests that hardcode the ID — caught at `go test`. Not a ship-blocker.
|
||||
3. **MkDocs build fails on Gitea Pages** — NON-ISSUE for v0.3. D-046 explicitly defers publishing CI to v0.4; v0.3 ships the source + a `mkdocs build` invocation in the README. A Gitea Pages failure is a v0.4 concern, not v0.3's. Not a ship-blocker.
|
||||
4. **Coverage drops below 80% on a skeleton package with trivial types** — MITIGATED. v0.2 hit ≥95.9% on 8/10 packages at this bar; the locked-const + invariant + lexicon assertions provide real coverage. The v0.3 packages (bridge/exit/hub/services) follow the v0.2 satellite/forex pattern that hit 100%. Not a ship-blocker.
|
||||
|
||||
The actual highest ship-risk is **ClampGrowth underflow (G-012)** if a test constructs a GrowthBond at cap and the helper misbehaves — but this is caught by the invariant test (post-growth ≤ 800) if the test exercises the cap boundary. **Binding fix G-012** makes the helper robust; the test must cover `currentBps == cap` and `currentBps > cap`. No escalation.
|
||||
|
||||
---
|
||||
|
||||
### Binding Decisions
|
||||
|
||||
These are **binding** — the orchestrator MUST apply them before P1 begins. Numbered G-011..G-014 (continuing from the v0.2 grill G-001..G-010).
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Affects |
|
||||
|----|----------|-----------|------------|---------|
|
||||
| **G-011** | `mkdocs.yml` nav MUST list every page the plan creates. RESEARCH §2.1's sample `mkdocs.yml` is missing `nomads/window.md` (P2-01-07) and `freeholders/anchor-preview.md` (P3-01-07). P1-01-02 (mkdocs.yml authoring) must include all 26 pages in the nav (or the nav is incomplete at P3 ship). The nav may reference not-yet-existing pages at P1 (mkdocs.yml is config, not Go-tested) but must be complete by P3-03-01 ship. | The RESEARCH sample nav and the PLANS page list disagree by 2 pages. A stale nav ships a docs site with orphaned pages (created but not linked). | 0.85 | P1-01-02 (mkdocs.yml), P3-03-01 (ship verification — confirm nav matches all 26 created pages) |
|
||||
| **G-012** | `x/bond` `ClampGrowth(currentBps, growthBps uint32) uint32` MUST guard `currentBps > CouponCapBps` explicitly (return 0, or document the precondition and assert it) so the "post-growth coupon ≤ 800" invariant holds unconditionally. The spec's `min(CouponCapBps - currentBps, growthBps)` underflows when `currentBps > cap` (uint32 wrap → huge value → `min` picks `growthBps` → invariant violated). The P5-03-01 test MUST cover `currentBps == cap` (returns 0) and `currentBps > cap` (returns 0 or is rejected) as explicit cases. | The spec'd helper has a uint32 underflow trap that breaks the stated invariant under misuse. The v0.2 `Clamp` has no such trap (it's a simple min/max); `ClampGrowth` adds the subtraction. A latent underflow in a locked-const firewall is a real defect. | 0.82 | P5-03-01 (ClampGrowth helper + invariant test) |
|
||||
| **G-013** | `lexicon_meta_docs_test.go` MUST include a **walk-coverage assertion**: a test that places a synthetic banned-term `.md` in a temp/fixture `docs/` subtree (or uses an in-memory walk target) and asserts the walk FINDS it. The G-009 self-test table (which both firewalls share) verifies DETECTION (`FindBannedTerm` on synthetic strings) but NOT the WALK (which files are scanned). A walk bug (e.g., wrong path prefix, missing `docs/` recursion) would report green on zero files scanned. The walk-coverage test closes this gap. | The docs firewall's failure mode is "silently scans nothing and reports green" — undetectable by the self-test table alone. v0.3 has no docs today, so a broken walk passes trivially at P1 and would only surface when a real banned term slips into a real P2/P3 page AND the walk happens to miss that file. A walk-coverage test forces the walk to execute against a known-bad fixture. | 0.80 | P1-01-01 (lexicon_meta_docs_test.go) |
|
||||
| **G-014** | `lexicon_meta_docs_test.go` and `lexicon_meta_test.go` MUST derive the banned-term count from the single source `lexicon.BannedTerms()` (both already assert `len(terms) == 10` from it — good; verified). The synthetic self-test table should ideally be shared via a `lexicon` package helper (e.g., `lexicon.SyntheticBannedStrings() []string`) rather than duplicated across the two meta-tests, so a future banned-term addition updates both firewalls from one place. If a shared helper is not added in v0.3, the two tables MUST be kept in sync by a comment cross-reference. | D-043 chose a sibling test to preserve v0.2 coverage — defensible — but two copies of the self-test table drift silently. A shared helper is the durable fix; a cross-reference comment is the minimum. | 0.70 | P1-01-01 (lexicon_meta_docs_test.go), optionally `lexicon/lexicon.go` (shared helper) |
|
||||
|
||||
---
|
||||
|
||||
### Escalations
|
||||
|
||||
**None.** All nine axes resolved at confidence ≥ 0.60 after the binding fixes G-011..G-014 are applied. No axis required escalation to the human. At full autonomy, the orchestrator applies the binding decisions and proceeds to P1.
|
||||
|
||||
---
|
||||
|
||||
### Overall Verdict
|
||||
|
||||
#### **SHIP Phase 0 with binding changes**
|
||||
|
||||
The v0.3 Phase 0 plan is fundamentally sound and unusually well-grounded: the architecture and ordering claims were verified against the actual codebase (25 x/* modules, zero deps, G-003 import-invariant test exists and is green, bond consts/Clamp exist, bearers/partner extension points exist, baseline modules for all by-ID-string refs exist). The firewall-first ordering is clean (firewall passes vacuously with zero docs — no chicken-and-egg). The scope (Bearers + docs bundle) is defensible, not over-scoped. The 50-task / 6-phase plan is proportionate to the deliverable surface (26 docs pages + 7 x/* packages).
|
||||
|
||||
The binding changes are **small correctness fixes**, not scope rework:
|
||||
- **G-011** (mkdocs.yml nav completeness) — documentation/plan consistency.
|
||||
- **G-012** (ClampGrowth uint32 underflow guard) — the single real technical defect; a latent invariant-breaking trap in a locked-const firewall.
|
||||
- **G-013** (docs firewall walk-coverage test) — closes the "silently scans nothing" failure mode the G-009 self-test table does not cover.
|
||||
- **G-014** (shared synthetic-string helper / cross-reference) — maintainability of the two sibling firewalls.
|
||||
|
||||
None of these rise to "rethink" or "reduce scope" — the architecture, scope, ordering, and persona assignments are correct. Apply the 4 binding decisions and proceed to Phase P1.
|
||||
|
||||
**Confidence in overall verdict: 0.80**
|
||||
|
||||
---
|
||||
|
||||
### Summary Block
|
||||
|
||||
```
|
||||
Per-axis verdicts (v0.3):
|
||||
1. Feasibility — PASS (0.82)
|
||||
2. Scope — PASS (0.78) → strengthened by G-011
|
||||
3. Cost — PASS (0.85)
|
||||
4. Technical soundness — CONDITIONAL (0.72) → fixed by G-012
|
||||
5. Risk — CONDITIONAL (0.74) → fixed by G-013
|
||||
6. Dependency — PASS (0.80)
|
||||
7. Testing — CONDITIONAL (0.70) → fixed by G-013 (walk-coverage)
|
||||
8. Maintainability — CONDITIONAL (0.72) → fixed by G-014
|
||||
9. Adversarial — PASS (0.78)
|
||||
|
||||
Binding decisions: 4 (G-011..G-014)
|
||||
Escalations: 0
|
||||
Overall: SHIP Phase 0 with binding changes (confidence 0.80)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grill Review: OpenYield (oy) — v0.4 (Refinement — NFR) Phase 0
|
||||
|
||||
> **Reviewer**: CIAgent adversarial grill (red-team, full autonomy)
|
||||
> **Date**: 2026-08-17
|
||||
> **Target**: Phase 0 artifacts (PROJECT.md v0.4 section, REQUIREMENTS.md v0.4 table, ARCHITECTURE.md v0.4 section, PERSONAS.md v0.4, PLANS.md v0.4 plan, config.json) + v0.3 codebase baseline
|
||||
> **Milestone**: v0.4 — Refinement (NFR)
|
||||
> **Autonomy**: full (decision_confidence_threshold = 0.60)
|
||||
> **Mode**: multi-project (slug `oy`)
|
||||
|
||||
### Methodology
|
||||
|
||||
Each of nine axes was scored against concrete evidence. The v0.4 scope is small (4 REQs, 12 tasks, 5 phases including P0+P4) so the grill is correspondingly focused. The central question: **is v0.4 a legitimate NFR milestone or is it a scope-creep / busywork milestone that should be a single patch or deferred?**
|
||||
|
||||
### Evidence baseline (verified against the actual repo)
|
||||
|
||||
- v0.3 milestone COMPLETE: checkpoint `milestone_complete: true`, release `v0.2.6` (id 739), HEAD on `main`. Verified.
|
||||
- v0.3 forward-references for v0.4 scope: REVIEW.md P2 (A-304 cross-const), AUDIT.md §193 (P1-1/P1-2 council divergences, P2 bearers no-op), GRILL.md G-014 (lexicon helper). Verified — all four REQ-029..REQ-032 map to a real v0.3 forward-reference.
|
||||
- The two synthetic self-test tables are byte-identical duplicates: `lexicon_meta_test.go:93-104` and `lexicon_meta_docs/lexicon_meta_docs_test.go:157-168`. Verified by diff — G-014 drift risk is REAL, not theoretical.
|
||||
- `x/hub/types/types.go:51,56` defines `LendingCouponCapBps = uint32(800)` and `LendingCouponFloorBps = uint32(0)`; `x/bond/types/types.go:21,26` defines `CouponCapBps = 800` and `CouponFloorBps = 0`. No automated cross-check exists (only a cross-doc comment). Verified — A-304 drift risk is REAL.
|
||||
- `SignalKindCount = 4` is already a locked-const test (`x/council/types/types_test.go:102`). The AUDIT P1-2 rationale (Freeholder is eligibility, Guild is council tier, Capital is committed-capital) is in AUDIT.md but NOT in ARCHITECTURE.md. Verified — the documentation gap is REAL.
|
||||
- No `.github/workflows/` and no `.gitea/workflows/` directory exists. Verified — REQ-032 is net-new CI, not a modification.
|
||||
|
||||
### Forcing Questions and Verdicts
|
||||
|
||||
**1. Is v0.4 a legitimate milestone, or should these fixes be a single v0.3.x patch?**
|
||||
The four fixes are independent (lexicon helper, cross-const test, lifecycle docs, CI). Bundling them into a milestone with per-phase ship (P1..P3 each ship a patch) gives each fix its own release tag and audit trail. The alternative (one v0.3.1 patch with all four) loses the per-fix release boundary. D-052 phase ordering keeps each phase independently shippable. **Verdict: legitimate milestone** — the per-phase ship cadence (D-052) is the value, not the milestone label. Confidence 0.82.
|
||||
|
||||
**2. Does REQ-031 violate the D-001 refinement-only filter by documenting a divergence that mentions `Proposal`/`VoteOption` (which are `feat:`-class types)?**
|
||||
No. DOCUMENTING a deferred `feat:` in ARCHITECTURE.md is `docs`, not `feat:`. REQ-031 adds NO enum types and changes NO locked consts. The regression-guard test asserts the CURRENT shape (4-signal) is intentional — it does not ADD a type. The D-001 filter rejects the `feat:` (adding Proposal/VoteOption) but accepts the `docs` (documenting why they are absent). **Verdict: passes D-001**. Confidence 0.85.
|
||||
|
||||
**3. Does REQ-030's test-only import of `x/bond/types` into `x/hub/types` violate G-003?**
|
||||
No. G-003 forbids PRODUCTION cross-module struct imports. The import is in `x/hub/types/cross_const_test.go` (a `_test.go` file). The v0.2 GRILL G-003 explicitly documented the test-import exemption (and `x/bearers/types/types_test.go:7` already imports `x/processing/types` as a test-only precedent). The plan's P1-02-01 verification includes a grep confirming no PRODUCTION `.go` file in `x/hub/types/` imports `x/bond/types`. **Verdict: G-003 intact**. Confidence 0.88.
|
||||
|
||||
**4. Does REQ-032 (CI workflow with mkdocs) violate G-006 (zero Go deps)?**
|
||||
No. The CI workflow installs mkdocs + mkdocs-material in a SEPARATE CI job (`docs-build`), not in `go.mod`. `go.mod` is not modified by the workflow (Python deps are isolated to the CI runner). The plan's P3-99-01 verification confirms `go.mod` diff is empty. **Verdict: G-006 intact**. Confidence 0.90.
|
||||
|
||||
**5. Is the NFR purity gate enforceable?**
|
||||
Yes. The gate is `git log --grep "^feat:" <milestone-range>` returns zero. The v0.4 plan explicitly rejects `feat:`-class work (D-050 rejects Proposal/VoteOption; D-051 rejects Pages publish as a feature). The P4 audit (P4-02-01) runs the gate. The only risk: a commit MESSAGE accidentally using `feat:` prefix — the gate checks the prefix, not the content. **Verdict: enforceable**. Confidence 0.85.
|
||||
|
||||
### Binding Decisions (G-015..G-016)
|
||||
|
||||
| ID | Binding Decision | Rationale | Confidence | Source |
|
||||
|----|------------------|-----------|------------|--------|
|
||||
| **G-015** | The P1-02-01 cross-const test MUST also assert the absolute mission-locked values (`LendingCouponCapBps == 800`, `LendingCouponFloorBps == 0`, `CouponCapBps == 800`, `CouponFloorBps == 0`) in addition to the cross-package equality. The cross-package equality test alone would pass if BOTH consts drifted to the same wrong value (e.g., both 900). The absolute-value assertion catches a paired drift. | The plan's P1-02-01 already includes `TestConstsAreMissionLocked800And0` (good). This binding makes it MANDATORY: the test must assert BOTH the cross-equality AND the absolute 800/0 values. Without the absolute assertion, a paired drift (both consts change to the same wrong value) is undetected. | 0.85 | PLANS.md P1-02-01 |
|
||||
| **G-016** | The P3-01-01 CI workflow MUST run `go test ./...` BEFORE `mkdocs build` is allowed to proceed IF the jobs are serial, OR the `go-test` job must be a REQUIRED check (not advisory) if the jobs are parallel. The lexicon firewall (`go test ./...`) is the higher-priority check; a docs build that passes while the lexicon firewall fails is a false-green deploy. The plan's P3-01-01 runs the jobs in parallel (no dependency) — acceptable ONLY if both are required-status checks. If Gitea Actions does not support required-status on artifact-upload jobs, the jobs MUST be serial (`go-test` then `docs-build` depends-on `go-test`). | A docs build that succeeds while the lexicon firewall fails would publish (or artifact) a docs site from a repo that has a lexicon violation — a false-green. The firewall must gate the docs build. | 0.78 | PLANS.md P3-01-01 |
|
||||
|
||||
### Nine-Axis Scorecard (v0.4 Phase 0)
|
||||
|
||||
```
|
||||
1. Scope — PASS (0.85) scoped to 4 v0.3 forward-refs; no scope creep
|
||||
2. Feasibility — PASS (0.88) all 4 fixes are mechanical; no research risk
|
||||
3. Specification — PASS (0.82) REQs clear; D-047..D-053 unambiguous
|
||||
4. Decomposition — PASS (0.80) P1..P3 independent vertical slices; P4 review/ship
|
||||
5. Risk — PASS (0.85) NFR scope (no behavioral change); low risk
|
||||
6. Dependency — PASS (0.82) no cross-phase hard blockers; G-003 test-exempt documented
|
||||
7. Testing — PASS (0.85) each fix has a verification task; NFR purity gate in P4
|
||||
8. Maintainability — CONDITIONAL (0.78) → fixed by G-015 (absolute-value assertion)
|
||||
9. Adversarial — PASS (0.82) D-001 filter enforced; no feat: creep
|
||||
|
||||
Binding decisions: 2 (G-015, G-016)
|
||||
Escalations: 0
|
||||
Overall: SHIP Phase 0 with binding changes (confidence 0.84)
|
||||
```
|
||||
|
||||
### v0.4 Grill Verdict
|
||||
|
||||
**SHIP Phase 0** with G-015 (absolute-value const assertion in P1-02-01) and G-016 (firewall-gates-docs-build in P3-01-01) applied. The v0.4 NFR milestone is a legitimate, well-scoped refinement cycle that closes three real v0.3 forward-references (G-014, A-304, AUDIT §193) and lands the deferred docs CI (D-046). The D-001 refinement-only filter is enforced throughout; the NFR purity gate in P4 is enforceable. No escalations.
|
||||
|
||||
+55
-99
@@ -3,124 +3,80 @@ active_personas:
|
||||
- id: backend-engineer
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Go/Cosmos module skeletons for v0.2 components — owns the non-Cosmos-mirroring modules (pact, partner, bond) per G-007. Owns the bulk of bespoke-type skeleton + tests work.
|
||||
frameworks: [Go, Cosmos SDK, IBC, CosmWasm]
|
||||
territory: ["x/pact/**", "x/partner/**", "x/bond/**", "x/**/types/**", "x/**/keeper/**", "x/**/module.go"]
|
||||
constraints: [lexicon compliance (REQ-012), skeleton+tests pattern (D-020), ≥80% coverage on new packages (D-033), no live-chain side effects in skeleton, locked-const invariants, no fractional reserve, no leverage/futures, go.mod is read-only in v0.2 (G-006 — any change is an escalation)]
|
||||
|
||||
- id: data-engineer
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Genesis/state schema design for new modules — Window audit log, Stand membership sets, Bond issuance state, Council Voice tally state. Shapes ValidateGenesis upgrades. Owns genesis SCHEMA only (G-008); test assertions are security-engineer's.
|
||||
frameworks: [Go, encoding/json, Cosmos SDK state]
|
||||
territory: ["x/**/types/genesis.go", "x/**/genesis.go"]
|
||||
constraints: [append-only audit logs (Window), ID-uniqueness in ValidateGenesis, lexicon compliance, no state identity beyond Reach, does NOT own *_test.go files (G-008)]
|
||||
reason: Owns the v0.4 NFR code work: REQ-029 (lexicon shared helper in `lexicon/lexicon.go` + refactor of both meta-tests to consume it), REQ-030 (new `x/hub/types/cross_const_test.go` test-only import of `x/bond/types`), and REQ-031's regression-guard test (council SignalKind intent-assertion test). The v0.3 frontend/docs-writer personas are deactivated because v0.4 has no docs-content authoring; the only docs-adjacent work is the CI workflow file (REQ-032, owned by lead-developer as infra/config). v0.4 is pure Go test/refactor work, which is backend-engineer's core territory.
|
||||
frameworks: [Go 1.22 stdlib (zero-dep), Go testing, lexicon firewall]
|
||||
territory: ["lexicon/**", "lexicon_meta_test.go", "lexicon_meta_docs/**", "x/hub/types/**", "x/bond/types/**", "x/council/types/**"]
|
||||
constraints: ["zero external deps (G-006 — go.mod stays zero-require)", "D-001 refinement-only filter: refactor/test/quality only, NO feat: (no new enum types, no new production types, no behavioral change)", "G-003 by-ID-string rule preserved in PRODUCTION imports; test-only cross-package imports are EXEMPT (G-003 test exemption — REQ-030 relies on this)", "lexicon firewall stays green on both x/ and docs/ after refactor", "locked-const invariants stay green (SignalKindCount==4 unchanged; LendingCouponCapBps==800, LendingCouponFloorBps==0 unchanged)", "≥80% coverage on any modified package (do not reduce existing coverage)"]
|
||||
|
||||
- id: lead-developer
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Multi-component orchestration across 10 new packages, dependency sequencing per D-031 blocker chain, vertical-slice integrity per phase.
|
||||
frameworks: [cross-cutting]
|
||||
territory: ["**"]
|
||||
constraints: [blocked-by chain enforcement (D-031), milestone versioning (v0.2 / tag_base v0.1.x), lexicon gate on merge, persona territory warn-mode enforcement]
|
||||
|
||||
- id: cosmos-engineer
|
||||
active: true
|
||||
phase_specific: true
|
||||
reason: v0.2 introduces IBC (satellite), governance (council), capability (window), group (stand/guild), oracle (forex) patterns that map directly to Cosmos SDK modules (x/gov, x/group, x/authz, x/feegrant, x/capability, x/ibc-transfer). Phase-specific to v0.2 execution phases where Cosmos-convention alignment matters for future wiring.
|
||||
frameworks: [cosmos-sdk, ibc-go, CosmWasm, CometBFT]
|
||||
territory: ["x/satellite/**", "x/council/**", "x/window/**", "x/stand/**", "x/guild/**", "x/forex/**", "x/bearers/**"]
|
||||
constraints: [lexicon compliance (REQ-012), no live-chain side effects in skeleton, mirror x/gov TallyResult / x/group DecisionPolicy / x/authz Grant shapes for future wiring, zero external deps in v0.2 skeleton, by-ID-string inter-module references to avoid import cycles (G-003 tested invariant), go.mod is read-only in v0.2 (G-006)]
|
||||
|
||||
- id: security-engineer
|
||||
active: true
|
||||
phase_specific: true
|
||||
reason: v0.2 enforces Mission Lock (council), fee/bond coupon clamps (clamp invariants), 9-stand / 4-tier / 6-pact locked-const tests, and Window revoke/expire lifecycle invariants. Phase-specific to v0.2 where invariant/locked-const test density is highest.
|
||||
frameworks: [Go testing, table-driven tests, invariant tests]
|
||||
territory: ["x/**/types/**_test.go", "x/**/keeper/**_test.go", "x/**/genesis_test.go", "x/**/*_test.go"]
|
||||
constraints: [invariant tests for all locked constants (Mission Lock non-amendable, bond 8% cap / 0% floor, fee ceiling/floor), locked-const tests for every enum count (9 stands, 4 partner tiers, 6 pacts), lexicon assertion in every new test file (D-033), ≥80% coverage on new packages, owns ALL *_test.go files including genesis_test.go (G-008)]
|
||||
reason: Coordinates v0.4 phase decomposition (P1 lexicon+const hardening → P2 lifecycle divergence docs+guard → P3 docs CI → P4 review/ship), territory enforcement (warn mode per config.json), the final-phase NFR purity gate audit (zero `feat:` commits), and the milestone ship. Owns REQ-031's ARCHITECTURE.md documentation deliverable (the divergence-decision writeup) and REQ-032's CI workflow file (`.gitea/workflows/docs-build.yml`) as infra/config territory. Also owns the v0.4 ROADMAP.md / REQUIREMENTS.md status updates at milestone completion.
|
||||
frameworks: [cross-cutting, Gitea Actions, Markdown, YAML]
|
||||
territory: [".ciagent/**", ".gitea/workflows/**", ".ciagent/oy/ARCHITECTURE.md", ".ciagent/oy/ROADMAP.md", ".ciagent/oy/REQUIREMENTS.md"]
|
||||
constraints: ["D-052 phase ordering (P1 firewall-first; each phase independently shippable)", "milestone versioning (v0.4 NFR / tag_base v0.3.x)", "NFR purity gate: zero feat: commits in the milestone (final-phase audit)", "persona territory warn-mode enforcement", "zero Go deps invariant (G-006) preserved; CI workflow may use build-only Python deps (mkdocs) in a separate job", "D-001 filter: no feat: scope creep — the CI workflow is chore (build+artifact), NOT a publishing feature"]
|
||||
|
||||
deactivated:
|
||||
- id: frontend-engineer
|
||||
reason: No UI in v0.2 (skeleton+tests only; Mesh Experience UI is v0.1-complete and v0.3+ for new UI). Deactivate to avoid persona territory noise. Reactivate in v0.3.
|
||||
- id: ci-security-auditor
|
||||
reason: Default deactivated; activate in P5 (review/ship) phase for the milestone audit. Not needed during P1-P4 skeleton authoring.
|
||||
- id: mesh-engineer
|
||||
reason: v0.1 listed as future; still not needed in v0.2 (Bearers OY-LR + Beacon are type stubs only, no hardware/RF). Activate in v0.3 for real bearer runtime.
|
||||
|
||||
custom_personas:
|
||||
reason: INACTIVE for v0.4. The v0.3 docs site (docs/**, mkdocs.yml) is COMPLETE; v0.4 does not author or restructure docs content. The only docs-adjacent work is the CI workflow that BUILDS the existing site (REQ-032), which is infra/config territory owned by lead-developer, not frontend toolchain. Reactivate in v0.5+ if docs content is restructured or i18n is added.
|
||||
- id: docs-writer
|
||||
reason: INACTIVE for v0.4. v0.3's docs-writer owned page content authoring; v0.4 has zero new docs pages. The only documentation work is the ARCHITECTURE.md divergence-decision section (REQ-031), which is lead-developer's architecture territory, not audience-content authoring. Reactivate if a future milestone adds docs pages.
|
||||
- id: data-engineer
|
||||
reason: INACTIVE for v0.4 (carried from v0.3). The project has zero external deps and no database; REQ-031 does not change genesis schemas (it documents divergence, no schema change). Reactivate if a future milestone adds a real store/migration.
|
||||
- id: cosmos-engineer
|
||||
rationale: v0.2 components map onto specific Cosmos SDK modules (x/gov, x/group, x/authz, x/feegrant, x/capability, x/ibc-transfer). A dedicated persona ensures skeleton types mirror the eventual runtime shapes, reducing Phase 3 wiring refactor cost. Distinct from backend-engineer because it carries Cosmos-specific convention knowledge (tally shapes, decision policy, capability, ICS-20 packet shape).
|
||||
reason: INACTIVE for v0.4. v0.4 has no new Cosmos-convention-alignment work (no new modules, no IBC, no governance runtime). Reactivate in v0.5+ if live-runtime promotion of the v0.3 Bearers skeletons lands.
|
||||
- id: security-engineer
|
||||
rationale: v0.2 has the highest locked-const + invariant density in the project (Mission Lock, bond cap, 6 Pact types, 9 Stand types, 4 Partner tiers, Window revoke idempotency). A dedicated persona ensures invariant tests and lexicon assertions are not an afterthought. Distinct from backend-engineer because it owns test-file territory and invariant-first design.
|
||||
reason: INACTIVE for v0.4. v0.4 introduces no new Mission-Lock-class invariant; REQ-029/030/031 are refactor/test/docs, not security invariants. The existing locked-consts stay unchanged. Reactivate if a future milestone adds a new mission-locked const or a new clamp.
|
||||
- id: ci-security-auditor
|
||||
reason: Default deactivated; activate in P4 (final review/ship) for the v0.4 milestone audit and NFR purity gate enforcement.
|
||||
- id: mesh-engineer
|
||||
reason: Still not needed in v0.4 (no bearer hardware runtime; OY-SAT/OY-QR remain type stubs). Activate in v0.5+ for real bearer runtime.
|
||||
|
||||
custom_personas: []
|
||||
---
|
||||
|
||||
# Personas: OpenYield (oy) — v0.2 (The Mesh)
|
||||
# Personas: OpenYield (oy) — v0.4 (Refinement — NFR)
|
||||
|
||||
> This file supersedes the v0.3 PERSONAS.md for the v0.4 milestone. v0.4 is a
|
||||
> refinement-only NFR milestone (D-047): zero `feat:` phases. The active
|
||||
> roster is **backend-engineer + lead-developer** only. The v0.3
|
||||
> phase-specific personas (frontend-engineer, docs-writer) are deactivated
|
||||
> because v0.4 does not author docs content or restructure the docs toolchain;
|
||||
> the only docs-adjacent work is a CI workflow file (REQ-032) owned by
|
||||
> lead-developer as infra/config.
|
||||
|
||||
## Active Roster
|
||||
|
||||
### backend-engineer
|
||||
- **Domain**: Go/Cosmos module skeletons for v0.2 — owns the **non-Cosmos-mirroring** modules (pact, partner, bond) per G-007, plus shared `types/`+`keeper/`+`module.go` authoring.
|
||||
- **Frameworks**: Go, Cosmos SDK, IBC, CosmWasm.
|
||||
- **Territory**: `x/pact/**`, `x/partner/**`, `x/bond/**`, `x/**/types/**`, `x/**/keeper/**`, `x/**/module.go`. (`go.mod` is read-only in v0.2 per G-006.)
|
||||
- **Constraints**: lexicon compliance (REQ-012), skeleton+tests pattern (D-020), ≥80% coverage on new packages (D-033), no live-chain side effects in skeleton, locked-const invariants, no fractional reserve, no leverage/futures.
|
||||
| Persona | Active | Phase-specific | Territory |
|
||||
|---------|--------|-----------------|-----------|
|
||||
| backend-engineer | yes | no (all phases) | `lexicon/**`, `lexicon_meta*`, `x/hub/types`, `x/bond/types`, `x/council/types` |
|
||||
| lead-developer | yes | no (all phases) | `.ciagent/**`, `.gitea/workflows/**` |
|
||||
|
||||
### data-engineer
|
||||
- **Domain**: Genesis/state schema design — Window audit log, Stand membership sets, Bond issuance state, Council Voice tally state. Shapes `ValidateGenesis` upgrades (v0.1's no-op → v0.2 ID-uniqueness checks). Owns genesis **schema** only (G-008); genesis **test assertions** are security-engineer's.
|
||||
- **Frameworks**: Go, `encoding/json`, Cosmos SDK state.
|
||||
- **Territory**: `x/**/types/genesis.go`, `x/**/genesis.go` (excludes `*_test.go` per G-008).
|
||||
- **Constraints**: append-only audit logs (Window), ID-uniqueness in `ValidateGenesis`, lexicon compliance, no state identity beyond Reach.
|
||||
## Phase-Persona Matrix
|
||||
|
||||
### lead-developer
|
||||
- **Domain**: Multi-component orchestration across 10 new packages, dependency sequencing per D-031 blocker chain, vertical-slice integrity per phase.
|
||||
- **Frameworks**: cross-cutting.
|
||||
- **Territory**: `**`.
|
||||
- **Constraints**: blocked-by chain enforcement (D-031), milestone versioning (v0.2 / tag_base v0.1.x), lexicon gate on merge, persona territory warn-mode enforcement.
|
||||
| Phase | Personas | Work |
|
||||
|-------|----------|------|
|
||||
| P0 (pre-execution) | lead-developer | spec/clarify/research/plan/grill/mvp-ux + ship |
|
||||
| P1 (lexicon hardening) | backend-engineer | REQ-029 shared helper + REQ-030 cross-const test |
|
||||
| P2 (lifecycle divergence) | backend-engineer (regression-guard test) + lead-developer (ARCHITECTURE.md docs) | REQ-031 |
|
||||
| P3 (docs build CI) | lead-developer | REQ-032 `.gitea/workflows/docs-build.yml` |
|
||||
| P4 (final review/ship) | lead-developer + ci-security-auditor (audit) | review + NFR purity gate + milestone ship |
|
||||
|
||||
### cosmos-engineer (custom, phase-specific to v0.2)
|
||||
- **Domain**: v0.2 components map onto Cosmos SDK modules — `x/gov` (council tally), `x/group` (stand/guild decision policy), `x/authz`/`x/feegrant` (window), `x/capability` (window unforgeable ref), `x/ibc-transfer` ICS-20 (satellite). Ensures skeleton types mirror runtime shapes for low-friction Phase 3 wiring.
|
||||
- **Frameworks**: cosmos-sdk, ibc-go, CosmWasm, CometBFT.
|
||||
- **Territory**: `x/satellite/**`, `x/council/**`, `x/window/**`, `x/stand/**`, `x/guild/**`, `x/forex/**`, `x/bearers/**` (Cosmos-convention-mirroring modules per G-007; `x/pact`/`x/partner`/`x/bond` are backend-engineer's). `go.mod` read-only per G-006.
|
||||
- **Constraints**: lexicon compliance (REQ-012), no live-chain side effects in skeleton, mirror `x/gov` `TallyResult` / `x/group` `DecisionPolicy` / `x/authz` `Grant` shapes, zero external deps in v0.2 skeleton, by-ID-string inter-module references (G-003 tested invariant).
|
||||
## D-001 Refinement-Only Filter (governs all v0.4 work)
|
||||
|
||||
### security-engineer (custom, phase-specific to v0.2)
|
||||
- **Domain**: Invariant + locked-const test authorship. Owns **all** test-file territory across v0.2 packages (including `genesis_test.go` per G-008). Highest invariant density in the project: Mission Lock non-amendable, bond 8% cap / 0% floor clamp, fee ceiling/floor clamp, 6 Pact types, 9 Stand types, 4 Partner tiers, Window revoke/expire idempotency, lexicon assertion per module.
|
||||
- **Frameworks**: Go testing, table-driven tests, invariant tests.
|
||||
- **Territory**: `x/**/types/**_test.go`, `x/**/keeper/**_test.go`, `x/**/genesis_test.go`, `x/**/*_test.go` (all test files per G-008).
|
||||
- **Constraints**: invariant tests for all locked constants, locked-const tests for every enum count (9 stands, 4 partner tiers, 6 pacts, 3 councils), lexicon assertion in every new test file (D-033), ≥80% coverage on new packages, Window lifecycle idempotency tests (revoke-after-expire, double-revoke).
|
||||
Every v0.4 change must pass the D-001 filter:
|
||||
- **Accept**: refactor, test, docs, chore, perf, fix, quality, coverage, architecture (drift fix only), improvement (of existing).
|
||||
- **Reject**: any `add_requirement` + `feat:`-class signal (new capability, new enum type, new production type, new CLI, new distribution channel, new backend).
|
||||
- **Enforcement**: lead-developer reviews each phase's commit set; the final-phase audit runs the NFR purity gate (`git log --grep "^feat:" --all-match` on the milestone range must return zero).
|
||||
|
||||
## Deactivated
|
||||
- **frontend-engineer** — No UI in v0.2 (skeleton+tests only). Mesh Experience UI is v0.1-complete; new UI is v0.3+. Reactivate in v0.3.
|
||||
- **ci-security-auditor** — Default deactivated; activate in P5 (review/ship) for milestone audit.
|
||||
- **mesh-engineer** — v0.1 listed as future; still not needed in v0.2 (Bearers OY-LR + Beacon are type stubs only). Activate in v0.3 for real bearer runtime.
|
||||
## Constraints Carried Forward
|
||||
|
||||
## Custom Personas
|
||||
- **cosmos-engineer** — v0.2 components map onto specific Cosmos SDK modules. Dedicated persona ensures skeleton types mirror eventual runtime shapes, reducing Phase 3 wiring refactor cost. Distinct from backend-engineer: carries Cosmos-specific convention knowledge (tally shapes, decision policy, capability, ICS-20 packet shape).
|
||||
- **security-engineer** — v0.2 has the highest locked-const + invariant density in the project. Dedicated persona ensures invariant tests and lexicon assertions are not an afterthought. Distinct from backend-engineer: owns test-file territory and invariant-first design.
|
||||
- **G-003** by-ID-string rule: preserved in PRODUCTION imports. Test-only cross-package imports are EXEMPT (REQ-030 relies on this exemption — `x/hub/types/cross_const_test.go` imports `x/bond/types` in a `_test.go` file only).
|
||||
- **G-006** zero Go deps: `go.mod` stays zero-require. The CI workflow (REQ-032) may use build-only Python deps (mkdocs + mkdocs-material) in a separate CI job; this does not touch `go.mod`.
|
||||
- **G-014** lexicon shared helper: REQ-029 closes the G-014 drift risk by adding `lexicon.SyntheticBannedStrings()` as the single source for the synthetic self-test table consumed by BOTH meta-tests.
|
||||
- **Locked consts unchanged**: `SignalKindCount==4`, `LendingCouponCapBps==800`, `LendingCouponFloorBps==0`, `CouponCapBps==800`, `CouponFloorBps==0` — v0.4 does NOT change any locked const. REQ-030 asserts they stay in lockstep; REQ-031 asserts the 4-signal shape is intentional.
|
||||
|
||||
## Framework Alignment
|
||||
- **Go 1.22** — all personas target Go 1.22 (`go.mod`).
|
||||
- **cosmos-sdk** — cosmos-engineer targets cosmos-sdk v0.50.x (LTS) for future wiring; NOT vendored in v0.2 skeleton.
|
||||
- **ibc-go** — cosmos-engineer targets ibc-go v8/v10 for satellite module shape; NOT vendored in v0.2 skeleton.
|
||||
- **CosmWasm** — cosmos-engineer targets wasmvm v1.5/v2.0 for potential Pacts-as-contracts in Phase 3; NOT vendored in v0.2.
|
||||
## Removal Notes
|
||||
|
||||
## Territory Alignment
|
||||
- Mapped to actual `x/<name>/` structure (15 v0.1 modules + 9 new v0.2 modules + 1 extended).
|
||||
- backend-engineer owns `x/pact`, `x/partner`, `x/bond` (non-Cosmos-mirroring per G-007) + shared `types/`+`keeper/`+`module.go` authoring.
|
||||
- data-engineer owns `genesis.go` schema only (G-008 — excludes `*_test.go`).
|
||||
- cosmos-engineer owns the Cosmos-convention-mirroring modules (`x/satellite`, `x/council`, `x/window`, `x/stand`, `x/guild`, `x/forex`) + `x/bearers` extension (G-007).
|
||||
- security-engineer owns **all** `*_test.go` files across the new packages (G-008 — including `genesis_test.go`).
|
||||
- lead-developer owns `**` for cross-cutting coordination.
|
||||
- `go.mod` is **read-only** in v0.2 (G-006) — no persona may modify it; any change is an escalation (would violate D-020/A-201 zero-dep invariant).
|
||||
|
||||
## Constraint Alignment
|
||||
- **Lexicon (REQ-012)** — every persona carries it; security-engineer asserts it per test file.
|
||||
- **Skeleton + tests pattern (D-020)** — backend-engineer + cosmos-engineer enforce.
|
||||
- **≥80% coverage on new packages (D-033)** — security-engineer owns the gate.
|
||||
- **Blocker chain (D-031)** — lead-developer enforces phase ordering.
|
||||
- **No live-chain side effects in skeleton** — cosmos-engineer + backend-engineer enforce (no relayer, no CometBFT, no live oracle).
|
||||
- **Locked-const invariants** — security-engineer owns; every locked constant has a dedicated test.
|
||||
|
||||
## Phase-Specific Personas
|
||||
- **cosmos-engineer** — phase-specific to v0.2 execution phases (P1-P4). Remove or merge back into backend-engineer after v0.2 ships (Cosmos convention alignment is most critical during the first Mesh-era skeleton).
|
||||
- **security-engineer** — phase-specific to v0.2 (highest invariant density). May persist into v0.3 if invariant-test density remains high; reassess at v0.3 PLAN.
|
||||
- frontend-engineer and docs-writer were `removed_after: P3` in v0.3. They are formally deactivated here for v0.4 (not just phase-removed) because v0.4 has no docs-content phase at all.
|
||||
- No phase-specific personas are created for v0.4. The roster is stable across all phases.
|
||||
+710
-1
@@ -334,4 +334,713 @@ The Phase 0 grill (see `.ciagent/oy/GRILL.md`) returned 10 binding decisions, al
|
||||
- **P1-02-01 (Stand types)** → blocks P2-01-01 (Pact StandRegistry stand-id-ref), P3-01-01 (Council Stand Council), P4-01-01 (Bond issuer-stand-id).
|
||||
- **P1-03-01 (Guild types)** → blocks P3-01-01 (Council Guild Council).
|
||||
- **P4-04-01 (P4 ship)** → blocks P5-01-01, P5-01-02, P5-01-03 (P5 audit).
|
||||
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene; types themselves only depend on the listed hard blockers).
|
||||
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene; types themselves only depend on the listed hard blockers).
|
||||
|
||||
---
|
||||
|
||||
## Milestone v0.3 — Bearers & Documentation — Phase Plan
|
||||
|
||||
> This section APPENDS the v0.3 milestone plan to the v0.1/v0.2 plan above. It
|
||||
> does NOT rewrite or supersede the earlier content. v0.3 bundles two work-
|
||||
> streams under one feature milestone (D-034): (A) Bearers skeleton+tests
|
||||
> (D-020/D-035 pattern) and (B) a README.md + MkDocs Material docs site with
|
||||
> the REQ-012 lexicon firewall extended to docs (D-043). Tags run on the
|
||||
> `v0.2.x` patch line (config.json `tag_base: v0.2.x`): P0 → `v0.2.0`,
|
||||
> P1..P5 → `v0.2.1..v0.2.5`, P6 → `v0.2.6` (= the v0.3 milestone release per
|
||||
> D-008 — final phase patch IS the milestone release; no separate minor tag).
|
||||
|
||||
### Milestone Summary
|
||||
|
||||
- **Milestone**: v0.3 — Bearers & Documentation
|
||||
- **Type**: Feature (Bearers phases P4/P5 are `feat`; docs phases P1-P3 are `docs`/`test`; P6 is `final`)
|
||||
- **Tag base**: `v0.2.x` patch line (P0 ships as `v0.2.0`; execution phases `v0.2.1..v0.2.5`; final phase `v0.2.6` IS the milestone release)
|
||||
- **Phases**: 7 — P0 (this PLAN) + P1..P3 (docs) + P4..P5 (Bearers feat) + P6 (final review/audit/ship).
|
||||
- **Depth**: skeleton + tests layer (D-020/D-035) for Bearers; docs deliverable (D-042/D-045) for the docs site; zero external Go deps (G-006; mkdocs is a build-only Python dep, not a Go dep).
|
||||
- **Coverage target**: ≥80% on each new/extended x/* package (D-033); lexicon assertion (REQ-012) in every new/extended test file (D-032); docs firewall (`lexicon_meta_docs_test.go`) green for `README.md` + `docs/**/*.md`.
|
||||
- **New x/* modules**: 4 (`x/exit`, `x/bridge`, `x/hub`, `x/services`). **Extended**: 3 (`x/bearers`, `x/partner`, `x/bond`). **Docs surface**: new (`docs/`, `mkdocs.yml`, `README.md`). **Firewall**: 1 new sibling test (`lexicon_meta_docs_test.go`).
|
||||
- **Phase ordering** (D-044): P1 docs foundation + firewall-first → P2 nomads docs → P3 freeholders docs → P4 Bearers I (exit/bridge/bearers/partner-Anchor) → P5 Bearers II (hub/services/bond) → P6 review/ship. Firewall lands in P1 BEFORE content (P2/P3) so docs are lexicon-clean by construction. P4 precedes P5 for the Anchor→hub dependency.
|
||||
- **Personas**: backend-engineer (all Bearers Go modules), lead-developer (cross-cutting/ship), frontend-engineer (docs toolchain + firewall wiring, P1-P3 only), docs-writer (docs content, P1-P3 only). The v0.2 custom personas (cosmos-engineer, security-engineer) are NOT reactivated (A-315).
|
||||
|
||||
### Cross-Phase Dependency Map (v0.3)
|
||||
|
||||
```
|
||||
P1 (firewall + docs foundation) ──┬──► P2 (nomads docs) [firewall scans docs/nomads/ as added]
|
||||
└──► P3 (freeholders docs) [firewall scans docs/freeholders/ + docs/reference/]
|
||||
|
||||
P4 (Bearers I: bridge, exit, bearers ext, partner ext) ──► P5 (Bearers II: hub, services, bond ext)
|
||||
│ x/bridge (Wave 1) ──► x/exit (Wave 2) [exit bridge-route-id refs bridge by ID]
|
||||
│ x/partner Anchor (Wave 4) ──► x/hub (P5 Wave 1) [hub operator-partner-id refs Anchor by ID]
|
||||
└──► P5 all waves
|
||||
|
||||
P5 (Bearers II) ──► P6 (review/audit/ship)
|
||||
P1..P5 (all execution) ──► P6
|
||||
```
|
||||
|
||||
Hard cross-phase blockers (by-ID-string refs, no import cycles — G-003):
|
||||
- **P4 Wave 1 `x/bridge` types** → blocks P4 Wave 2 `x/exit` tests (exit's `bridge-route-id` references a BridgeRoute by ID-string — A-308/G-003).
|
||||
- **P4 Wave 4 `x/partner` Anchor extension** → blocks P5 Wave 1 `x/hub` (hub's `operator-partner-id` references an Anchor partner by ID-string — A-304/G-003). This is the edge that forces P4 before P5 (D-044).
|
||||
- **P1 Wave 1 docs firewall** → blocks P2/P3 docs content (firewall-first: a banned term slipped into a P2/P3 page fails the build, not the P6 review).
|
||||
- **P5 ship** → blocks P6 audit/ship.
|
||||
|
||||
All other inter-module refs (x/services→x/window, x/bond→x/stand, x/bridge→x/satellite, x/bridge→x/watcher) are to v0.1/v0.2 baseline modules (no v0.3 phase-ordering concern).
|
||||
|
||||
---
|
||||
|
||||
## Phase P1 — Docs Foundation + Firewall Extension
|
||||
|
||||
- **Slug**: `docs-foundation-firewall`
|
||||
- **Branch**: `oy/phase/01-docs-foundation-firewall`
|
||||
- **REQs covered**: REQ-028 (lexicon firewall extension to docs), REQ-027 (README.md + docs site foundation: README + shared docs + index)
|
||||
- **Tag**: `v0.2.1`
|
||||
- **Type**: `feat/test+docs`
|
||||
- **Personas**: frontend-engineer (toolchain + firewall), docs-writer (README + shared content)
|
||||
- **Goal**: Land the docs lexicon firewall (`lexicon_meta_docs_test.go`) + the MkDocs Material scaffold (`mkdocs.yml`) + `README.md` + `docs/index.md` + `docs/shared/` pages BEFORE any audience docs content (P2/P3), so docs are lexicon-clean by construction (D-044 firewall-first).
|
||||
|
||||
### Wave 1 — Firewall + scaffold FIRST (parallel)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-01-01 | REQ-028 | frontend-engineer | `lexicon_meta_docs_test.go` (repo root, package `lexicon_meta_docs`) | **NEW sibling meta-test** mirroring `lexicon_meta_test.go` (D-043). Uses the SAME `lexicon.FindBannedTerm` (word-boundary, case-insensitive) — NO detection reimplementation. Walks the REPO ROOT (not `x/`): targets `README.md` (repo root) + every `*.md` under `docs/` (recursive). Excludes `.ciagent/` (firewall meta-files, not user-facing), `.git/` (VCS), the meta-test file itself (self-exclusion via `runtime.Caller(0)`), and non-`.md` files under `docs/`. Includes the G-009 self-test table (one synthetic string per banned term, assembled from `lexicon.BannedTerms()` fragments so the test file's own source has no banned-term literal), `TestLexiconMetaDocsBannedTermsCount` (exactly 10), and `TestLexiconMetaDocsNoFalsePositiveOnOpenYield` (word-boundary does not match "openyield"/"european"). The firewall PASSES at P1 time with zero docs (or with only README + docs/index.md + docs/shared/ from Wave 2). | `go test ./lexicon_meta_docs/...` green (invoked as `go test -run TestLexiconMetaDocs ./...` or via the repo-root file); self-test table passes for all 10 banned terms; `TestLexiconMetaDocsNoFalsePositiveOnOpenYield` green; a deliberately-injected banned term in a `docs/*.md` file fails the test | — |
|
||||
| P1-01-02 | REQ-027 | frontend-engineer | `mkdocs.yml` (repo root) | MkDocs Material config (D-042): `site_name: OpenYield`; `theme: name: material` with `navigation.sections`/`navigation.expand`/`toc.integrate` features; `markdown_extensions: [admonition, toc (permalink: true), pymdownx.superfences]`; `nav:` skeleton with Home + Nomads + Freeholders + Shared + Reference sections (audience-organized per D-042). The nav references the P2/P3 pages by path (pages need not exist yet at P1 — mkdocs.yml is a config file, not validated by Go tests; the docs firewall does not validate nav, only `.md` content). Build-only Python dep; `go.mod` stays zero-dep (G-006). No publishing CI (D-046). | `mkdocs.yml` is valid YAML (parses; documented `mkdocs serve` / `mkdocs build` invocation goes in README P1-02-01); `go.mod` unchanged (zero require lines); nav has the 4 audience sections + Home | — |
|
||||
|
||||
### Wave 2 — README + docs/index.md + docs/shared/ (parallel; blocked-by Wave 1 firewall)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-02-01 | REQ-027 | frontend-engineer | `README.md` (repo root) | Repo-root project overview: one-paragraph OpenYield description (lexicon-clean — "real production"/"real return" not "yield"; "Holder"/"Reach" not "account"; "Stash"/"Vault"/"Root-Pool" not "bank"/"deposit"/"savings"); build instructions (`go build ./...`, `go test ./...`); docs build instructions (`mkdocs serve` / `mkdocs build` per D-046); link to `docs/` site; pointer to `.ciagent/oy/PROJECT.md` for governance. Lexicon-clean by construction (the P1-01-01 firewall scans README.md). | `README.md` exists; `go test ./lexicon_meta_docs/...` green (README is scanned); `go test ./...` green (no Go regression — README is not a Go file) | P1-01-01 |
|
||||
| P1-02-02 | REQ-027 | docs-writer | `docs/index.md` | Site home page: one-paragraph OpenYield overview (lexicon-clean), links to the 4 audience sections (nomads/freeholders/shared/reference), pointer to README for build instructions. | `docs/index.md` exists; docs firewall green (index scanned) | P1-01-01 |
|
||||
| P1-02-03 | REQ-027 | docs-writer | `docs/shared/six-principles.md` | Six Principles page (REQ-001): real value, sustainability, mission-lock, openness, ownership, self-service. Lexicon-clean (the firewall scans `docs/shared/**/*.md`). | Page exists; firewall green | P1-01-01 |
|
||||
| P1-02-04 | REQ-027 | docs-writer | `docs/shared/bread-scale.md` | Bread Scale page (REQ-013): Grain → Crumb → Bread → Loaf → Batch → Cake → Bakery → Granary → Mill → Harvest → Earth. | Page exists; firewall green | P1-01-01 |
|
||||
| P1-02-05 | REQ-027 | docs-writer | `docs/shared/storage-pools.md` | Storage Pools page (REQ-014): Stash (Holder), Vault (Stand), Root-Pool (treasury). Lexicon-clean ("Stash"/"Vault"/"Root-Pool" not "bank"/"deposit"/"savings"). | Page exists; firewall green | P1-01-01 |
|
||||
| P1-02-06 | REQ-027 | docs-writer | `docs/shared/watchers-mirror.md` | Watchers / Mirror page (REQ-004): 9 Watchers, 6-of-9 quorum, daily attestations, 100,000 Bread bond each. | Page exists; firewall green | P1-01-01 |
|
||||
| P1-02-07 | REQ-027 | docs-writer | `docs/shared/lexicon-glossary.md` | Lexicon Glossary page (REQ-012): the 10 banned terms named BY THEIR SAFE ALTERNATIVES (the page documents the safe phrasings — "real production"/"Holder"/"Stash"/"coupon" — NOT the banned literals; the firewall scans this page, so the banned terms must NOT appear as literals, only as the safe replacements described in prose). Cross-reference the firewall design (D-043). | Page exists; firewall green (no banned-term literals — the glossary describes replacements, not the banned words themselves) | P1-01-01 |
|
||||
| P1-02-08 | REQ-027 | docs-writer | `docs/shared/vision-overview.md` | Vision Overview page: the OpenYield covenant (real production, anti-greed, jurisdiction-light, public-good mesh), pointer to `.ciagent/oy/PROJECT.md` for the full vision source. | Page exists; firewall green | P1-01-01 |
|
||||
|
||||
### Wave 3 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-03-01 | REQ-012, REQ-027, REQ-028 | lead-developer | (cross-cutting) | Run `go build ./...` + `go test ./...` across the whole repo (no Go regression — the docs firewall is a NEW Go test file but adds no x/* Go code); confirm `go test ./lexicon_meta_docs/...` green; confirm `mkdocs.yml` valid; confirm README.md + docs/index.md + 6 docs/shared/ pages exist and are lexicon-clean; tag `v0.2.1`. | `go test ./...` green (incl. v0.1/v0.2 baseline + the new docs firewall); docs firewall green; mkdocs.yml valid; 8 docs files (README + index + 6 shared) exist + lexicon-clean; git tag `v0.2.1` created | P1-01-01, P1-01-02, P1-02-01..08 |
|
||||
|
||||
### P1 Must-Haves
|
||||
- [ ] `lexicon_meta_docs_test.go` exists at repo root (package `lexicon_meta_docs`); mirrors `lexicon_meta_test.go` detection (same `lexicon.FindBannedTerm` + word-boundary regex + self-test table G-009 + self-exclusion); scans `README.md` + `docs/**/*.md`; excludes `.ciagent/` + `.git/` + itself.
|
||||
- [ ] `go test ./lexicon_meta_docs/...` green (firewall passes with README + docs/index.md + docs/shared/ present).
|
||||
- [ ] `go test ./...` green across the whole repo (no Go regression; the v0.2 `lexicon_meta_test.go` is UNCHANGED per D-043).
|
||||
- [ ] `mkdocs.yml` exists at repo root (Material theme, 4 audience sections in nav, admonition + toc + superfences extensions); `go.mod` unchanged (zero require lines).
|
||||
- [ ] `README.md` exists (lexicon-clean; build/test/docs-build instructions).
|
||||
- [ ] `docs/index.md` exists (site home).
|
||||
- [ ] 6 `docs/shared/` pages exist (Six Principles, Bread Scale, Storage Pools, Watchers/Mirror, Lexicon Glossary, Vision Overview) — all lexicon-clean.
|
||||
- [ ] Docs firewall self-test table passes for all 10 banned terms (G-009 for docs).
|
||||
- [ ] `TestLexiconMetaDocsNoFalsePositiveOnOpenYield` green.
|
||||
- [ ] Git tag `v0.2.1`.
|
||||
|
||||
### P1 Risks & Mitigations
|
||||
- **Banned-term literals in the lexicon-glossary page** (highest P1 risk) → docs-writer must describe SAFE ALTERNATIVES, not the banned words themselves; the firewall scans `docs/shared/lexicon-glossary.md` directly (unlike `.go` fragment assembly). Mitigation: P1-01-01 firewall is the gate; a literal banned term fails the P1 build.
|
||||
- **mkdocs.yml nav references non-existent P2/P3 pages** → mkdocs.yml is a config file, not Go-tested; nav can list future pages. The firewall scans `.md` content, not `nav`. Mitigation: P2/P3 create the referenced pages; missing pages are a `mkdocs build` warning, not a Go test failure.
|
||||
- **README "yield" false positive** → the firewall word-boundary regex allows "OpenYield" but bans standalone "yield"; README must say "real production"/"real return". Mitigation: `TestLexiconMetaDocsNoFalsePositiveOnOpenYield` is the regression firewall.
|
||||
|
||||
---
|
||||
|
||||
## Phase P2 — Nomads Docs
|
||||
|
||||
- **Slug**: `nomads-docs`
|
||||
- **Branch**: `oy/phase/02-nomads-docs`
|
||||
- **REQs covered**: REQ-027 (nomads audience docs, 7 pages per D-045)
|
||||
- **Tag**: `v0.2.2`
|
||||
- **Type**: `docs`
|
||||
- **Personas**: docs-writer (content); frontend-engineer (toolchain verify — firewall now scans the new docs/nomads/ files)
|
||||
- **Goal**: Ship the 7-page nomads audience docs (`docs/nomads/`) covering the Reach path, Stash, bearers, Maps/Pay, six Pacts, standing basics, and the Window primitive — all lexicon-clean (the P1 firewall now scans these files as they are added).
|
||||
|
||||
### Wave 1 — Nomads content pages (parallel; all blocked-by P1 firewall landing)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-01-01 | REQ-027 (REQ-005 Reach) | docs-writer | `docs/nomads/reach.md` | What a Nomad is + the Reach path (REQ-005): how a person becomes a Holder via a Reach ID, no-KYC at protocol level, geographic-proximity FCFS (REQ-007). Lexicon-clean ("Holder"/"Reach" not "account"; "real production" not "yield"). | Page exists; docs firewall green (scans `docs/nomads/reach.md`) | P1-03-01 |
|
||||
| P2-01-02 | REQ-027 (REQ-014 Stash) | docs-writer | `docs/nomads/stash.md` | Stash usage (REQ-014): the Holder-level storage pool, how Bread is held in a Stash, the 90-day Freeholder-signal Stash requirement. Lexicon-clean ("Stash" not "bank"/"deposit"/"savings"). | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-03 | REQ-027 (REQ-019 bearers) | docs-writer | `docs/nomads/bearers.md` | Bearers for nomads (REQ-019): the six bearers (Internet, OY-LR, OY-BLE, OY-WiFi-Direct, OY-SAT, OY-QR) via the Unified Bearer Layer, first-to-deliver-wins, surveillance resistance. Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-04 | REQ-027 (Mesh Experience) | docs-writer | `docs/nomads/maps-pay.md` | Maps / Pay Mesh Experience: how a Nomad uses Maps and Pay day-to-day (Maya's Day deferred per PROJECT.md out-of-scope Q1). Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-05 | REQ-027 (REQ-020 Pacts) | docs-writer | `docs/nomads/pacts.md` | Six Pacts (REQ-020): Pause, Ground, Stance, Cover, Stand Registry, Hub API — what each means for a Nomad. Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-06 | REQ-027 (REQ-006 standing basics) | docs-writer | `docs/nomads/standing-basics.md` | Standing basics for nomads (REQ-006): what Bayesian Standing is in plain language, how it accrues, why it matters (no formula math — defer detail to freeholders/bayesian-standing). Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-07 | REQ-027 (REQ-015 Window) | docs-writer | `docs/nomads/window.md` | Window primitive (REQ-015): scope, duration, rate-limit, audit log, revoke — what a Window means for a Nomad delegating access to a partner/service. Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
| P2-01-08 | REQ-027 | docs-writer | `docs/nomads/index.md` | Nomads section index: one-paragraph intro + links to the 7 nomads pages. Lexicon-clean. | Page exists; firewall green | P1-03-01 |
|
||||
|
||||
### Wave 2 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-02-01 | REQ-012, REQ-027 | lead-developer | (cross-cutting) | Run `go test ./...` (docs firewall now scans the 8 new `docs/nomads/*.md` files and passes); confirm all 8 nomads pages lexicon-clean; tag `v0.2.2`. | `go test ./...` green (docs firewall green with nomads content); 8 `docs/nomads/*.md` exist + lexicon-clean; git tag `v0.2.2` | P2-01-01..08 |
|
||||
|
||||
### P2 Must-Haves
|
||||
- [ ] 8 `docs/nomads/*.md` pages exist (index + reach + stash + bearers + maps-pay + pacts + standing-basics + window) — all lexicon-clean.
|
||||
- [ ] `go test ./...` green (the P1 docs firewall now scans `docs/nomads/**/*.md` and passes).
|
||||
- [ ] `go test ./lexicon_meta_docs/...` green specifically.
|
||||
- [ ] No Go code changes (P2 is pure docs; `go build ./...` green by no-regression).
|
||||
- [ ] Git tag `v0.2.2`.
|
||||
|
||||
### P2 Risks & Mitigations
|
||||
- **"account"/"bank"/"deposit" drift in nomads prose** (Stash/Reach pages are highest-risk) → docs-writer uses "Holder"/"Reach"/"Stash"; the firewall is the gate (fails the P2 build, not P6 review — D-044 firewall-first value).
|
||||
- **Standing-basics page over-promises formula detail** → defer math to freeholders/bayesian-standing (P3); nomads page stays conceptual.
|
||||
|
||||
---
|
||||
|
||||
## Phase P3 — Freeholders Docs + Reference
|
||||
|
||||
- **Slug**: `freeholders-docs-reference`
|
||||
- **Branch**: `oy/phase/03-freeholders-docs-reference`
|
||||
- **REQs covered**: REQ-027 (freeholders audience docs 7 pages + reference 2 pages; REQ-027 COMPLETE at end of P3)
|
||||
- **Tag**: `v0.2.3`
|
||||
- **Type**: `docs`
|
||||
- **Personas**: docs-writer (content); frontend-engineer (toolchain verify — firewall now scans docs/freeholders/ + docs/reference/)
|
||||
- **Goal**: Ship the 7-page freeholders audience docs (`docs/freeholders/`) covering the four signals, Bayesian Standing, Stands/Guilds, Councils/Voice, Bonds, Partner spectrum, Anchor preview — plus the 2-page reference section (`docs/reference/`). **REQ-027 (docs deliverable) is COMPLETE at the end of P3.** After P3, frontend-engineer and docs-writer are removed (phase-specific personas, P1-P3 only).
|
||||
|
||||
### Wave 1 — Freeholders content pages (parallel)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-01-01 | REQ-027 (REQ-005 four signals) | docs-writer | `docs/freeholders/four-signals.md` | Four Freeholder signals (REQ-005): 90d Stash, 4.5★+ in 3 categories, Capital, Vouch. Lexicon-clean. | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-02 | REQ-027 (REQ-006 Bayesian Standing) | docs-writer | `docs/freeholders/bayesian-standing.md` | Bayesian Standing (REQ-006): the anti-gaming formula (Bayesian + time-decay + diversity + voucher-weighted − slashes), why it resists gaming. Formula at conceptual depth (full sub-tables deferred per PROJECT.md Q2). Lexicon-clean. | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-03 | REQ-027 (REQ-016/017 Stands/Guilds) | docs-writer | `docs/freeholders/stands-guilds.md` | Stands & Guilds (REQ-016/017): the 9 Stand types (Household…Shadow), Guilds with Hand-Passes at 0% protocol fee. Lexicon-clean ("Stash"/"Vault" not "bank"; "Hand-Pass"/"0% protocol fee" not "interest"). | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-04 | REQ-027 (REQ-011 Councils/Voice) | docs-writer | `docs/freeholders/councils-voice.md` | Councils & Voice (REQ-011): the three Councils (Mesh, Guild, Stand), multi-source Voice, Mission Lock cannot be amended. Lexicon-clean. | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-05 | REQ-027 (REQ-021 Bonds) | docs-writer | `docs/freeholders/bonds.md` | Bonds (REQ-021): the Mesh Bond Market, 8% upper coupon cap / 0% floor, Growth Bonds (preview of v0.3 P5 work). Lexicon-clean ("coupon"/"growth" not "interest"/"yield"). | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-06 | REQ-027 (REQ-018 Partner spectrum) | docs-writer | `docs/freeholders/partner-spectrum.md` | Partner Spectrum (REQ-018): the four tiers (Op, Master Op, Pier, Anchor). Lexicon-clean ("Partner"/"Reach" not "account"). | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-07 | REQ-027 (REQ-023 Anchor preview) | docs-writer | `docs/freeholders/anchor-preview.md` | Anchor preview (REQ-023): the first institutional Partner tier, the Anchor credential (preview of v0.3 P4 work), custody/compliance relationship. Lexicon-clean ("custody"/"compliance"/"jurisdiction" safe; not "bank"/"account"). | Page exists; firewall green | P2-02-01 |
|
||||
| P3-01-08 | REQ-027 | docs-writer | `docs/freeholders/index.md` | Freeholders section index: one-paragraph intro + links to the 7 freeholders pages. Lexicon-clean. | Page exists; firewall green | P2-02-01 |
|
||||
|
||||
### Wave 2 — Reference pages (parallel; blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-02-01 | REQ-027 | docs-writer | `docs/reference/architecture-index.md` | Architecture index: the 14-component index (from `.ciagent/oy/ARCHITECTURE.md` Component Index), the 6 cross-component interfaces, the critical blocker chain. Lexicon-clean (rewrite the architecture terms in user-facing prose — do NOT copy `.ciagent/` content verbatim if it contains banned-term discussions; the firewall scans this page). | Page exists; firewall green | P3-01-01..08 |
|
||||
| P3-02-02 | REQ-027 | docs-writer | `docs/reference/component-map.md` | Component map: a table of v0.1/v0.2/v0.3 modules (`x/<name>/`) mapped to vision sections and REQs. Lexicon-clean. | Page exists; firewall green | P3-01-01..08 |
|
||||
|
||||
### Wave 3 — Phase verification + ship (frontend-engineer + docs-writer removed after this phase)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-03-01 | REQ-012, REQ-027 | lead-developer | (cross-cutting) | Run `go test ./...` (docs firewall now scans `docs/freeholders/**/*.md` + `docs/reference/**/*.md` and passes); confirm all 10 new pages lexicon-clean; **REQ-027 COMPLETE** (total docs pages: 1 README + 1 index + 6 shared + 8 nomads + 8 freeholders + 2 reference = 26 pages, within D-045 20-25 budget +README/index); tag `v0.2.3`. Remove frontend-engineer + docs-writer personas (phase-specific, P1-P3 only). | `go test ./...` green (docs firewall green with all docs content); REQ-027 marked complete in REQUIREMENTS.md; git tag `v0.2.3`; personas removed | P3-01-01..08, P3-02-01..02 |
|
||||
|
||||
### P3 Must-Haves
|
||||
- [ ] 8 `docs/freeholders/*.md` pages exist (index + four-signals + bayesian-standing + stands-guilds + councils-voice + bonds + partner-spectrum + anchor-preview) — all lexicon-clean.
|
||||
- [ ] 2 `docs/reference/*.md` pages exist (architecture-index + component-map) — all lexicon-clean.
|
||||
- [ ] `go test ./...` green (docs firewall scans the full `docs/` tree + README and passes).
|
||||
- [ ] `go test ./lexicon_meta_docs/...` green.
|
||||
- [ ] No Go code changes (P3 is pure docs; `go build ./...` green by no-regression).
|
||||
- [ ] **REQ-027 marked COMPLETE** in REQUIREMENTS.md (docs deliverable done).
|
||||
- [ ] frontend-engineer + docs-writer personas removed (phase-specific, P1-P3 only).
|
||||
- [ ] Git tag `v0.2.3`.
|
||||
|
||||
### P3 Risks & Mitigations
|
||||
- **"interest"/"yield" in bonds page** (highest P3 risk) → docs-writer uses "coupon"/"growth"/"real return"; firewall is the gate.
|
||||
- **Architecture-index page copies `.ciagent/` banned-term discussions verbatim** → `.ciagent/` files discuss banned terms by name for governance but are excluded from the firewall; the reference page is NOT excluded, so it must use safe phrasings. Mitigation: docs-writer rewrites in user-facing prose; firewall scans `docs/reference/architecture-index.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase P4 — Bearers Skeleton I (exit/bridge/bearers/partner-Anchor)
|
||||
|
||||
- **Slug**: `bearers-skeleton-i`
|
||||
- **Branch**: `oy/phase/04-bearers-skeleton-i`
|
||||
- **REQs covered**: REQ-010 (Exit layer: x/bridge + x/exit), REQ-022 (Bearers OY-SAT/OY-QR), REQ-023 (Anchors — x/partner extension)
|
||||
- **Tag**: `v0.2.4`
|
||||
- **Type**: `feat`
|
||||
- **Persona**: backend-engineer (all Go modules; the v0.2 cosmos-engineer/security-engineer split is collapsed per A-315)
|
||||
- **Goal**: Ship the Bearers skeleton I: `x/bridge` (L2↔L1 bridge types) FIRST, then `x/exit` (exit routes + DEXSwap, referencing bridge by ID-string), then `x/bearers` extension (OY-SAT/OY-QR transport stubs), then `x/partner` extension (Anchor credential types — referenced by x/hub in P5).
|
||||
|
||||
### Wave 1 — x/bridge types FIRST (intra-P4 ordering: bridge before exit)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-01-01 | REQ-010 | backend-engineer | `x/bridge/types/types.go` + `x/bridge/types/genesis.go` | New `x/bridge/types/` module following the `x/satellite` pattern (D-036, A-302). `BridgeStatus` enum (Pending, Attested, Active, Closed) — exactly 4, locked-const `BridgeStatusCount = 4` (A-312 names HubServiceCount; BridgeStatusCount is the bridge analog). `AllBridgeStatuses() []BridgeStatus`. `BridgeRoute` struct (route-id, source-chain (L2Chain by-ID-string ref to `x/satellite` — G-003, no struct import), dest-chain (L2Chain by-ID-string), bridge-type (opaque string e.g. "ibc" — NOT a locked enum per A-308), transfer-channel-id (by-ID-string ref to a v0.2 satellite TransferChannel), watcher-quorum-id (by-ID-string ref to `x/watcher`, set when status becomes Attested), status). `Keeper` stub: AddBridgeRoute / GetBridgeRoute / ListByStatus. `Params`, `GenesisState` (routes), `DefaultGenesisState`, `ValidateGenesis` (reject dup route-ids, A-212). | `go build ./x/bridge/...` succeeds; `BridgeStatusCount == 4`; `AllBridgeStatuses()` returns 4 in vision order; `ValidateGenesis` rejects dup route-id; zero external deps (go.mod unchanged) | — |
|
||||
|
||||
### Wave 2 — x/exit types (blocked-by Wave 1 bridge)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-02-01 | REQ-010 | backend-engineer | `x/exit/types/types.go` + `x/exit/types/genesis.go` | New `x/exit/types/` module following `x/satellite` (D-036). `ExitStatus` enum (Proposed, InProgress, Settled, Failed, Refunded) — exactly 5, locked-const `ExitStatusCount = 5`. `AllExitStatuses() []ExitStatus`. `ExitRoute` struct (route-id, holder-reach-id (by-ID-string ref to `x/identity` — use "Holder"/"Reach" not "account"), source-asset (opaque string), dest-asset (opaque string), amount-grain (int64 — NOT a `x/bread` import; "Grain" by name only), min-received-grain, bridge-route-id (OPTIONAL, by-ID-string ref to `x/bridge` BridgeRoute for cross-chain exits — G-003), venue-hops []string, deadline, status). `DEXSwap` struct (swap-id, route-id (by-ID-string ref to ExitRoute), venue (opaque string — NOT a locked enum per A-308), input-asset, input-amount-grain, output-asset, output-amount-grain, executed-at). `Keeper` stub: AddExitRoute / GetExitRoute / ListByHolder. `Params`, `GenesisState` (routes + swaps), `DefaultGenesisState`, `ValidateGenesis` (reject dup route-ids + dup swap-ids, A-212). | `go build ./x/exit/...` succeeds; `ExitStatusCount == 5`; `AllExitStatuses()` returns 5 in vision order; `ValidateGenesis` rejects dup route-id + dup swap-id; the `bridge-route-id` field is a string (no `x/bridge` struct import — G-003 verified by the P1 G-003 import-invariant test, which now also scans the new x/exit + x/bridge files) | P4-01-01 |
|
||||
|
||||
### Wave 3 — x/exit tests + x/bridge tests (parallel; blocked-by Wave 1/2 types)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-01-02 | REQ-010, REQ-012 | backend-engineer | `x/bridge/types/types_test.go` (+ `genesis_test.go` if split) | Locked-const test: `BridgeStatusCount == 4`; `AllBridgeStatuses()` names match (Pending, Attested, Active, Closed); `BridgeRoute` struct round-trip (marshal/unmarshal); `bridge-route-id` references a satellite L2Chain by string (no `x/satellite` import — G-003 import-invariant test green); `watcher-quorum-id` is an opaque string (no `x/watcher` import); `ValidateGenesis` rejects dup route-id; **lexicon assertion** (no "bank"/"account"/"currency"/"dollar"/"euro" — use "Holder"/"Reach"/chain names); **G-003 import-invariant**: the P1-01-02 `go/parser` scan (extended to cover the new `x/bridge` + `x/exit` files) asserts NO production file imports another `x/<module>/types` by struct. | `go test ./x/bridge/...` passes; ≥80% coverage on `x/bridge/types`; BridgeStatusCount=4 locked-const; lexicon green; G-003 import-invariant green | P4-01-01 |
|
||||
| P4-02-02 | REQ-010, REQ-012 | backend-engineer | `x/exit/types/types_test.go` (+ `genesis_test.go` if split) | Locked-const test: `ExitStatusCount == 5`; `AllExitStatuses()` names match (Proposed, InProgress, Settled, Failed, Refunded); `ExitRoute` struct round-trip; `DEXSwap` struct round-trip; the `bridge-route-id` field references a BridgeRoute by string (no `x/bridge` struct import — G-003 verified); `venue` is an opaque string (A-308 — not a locked enum, so no enum-count test); `ValidateGenesis` rejects dup route-id + dup swap-id; **lexicon assertion** (no "account"/"currency"/"dollar"/"euro" — use "Holder"/"Reach"/opaque asset strings). | `go test ./x/exit/...` passes; ≥80% coverage on `x/exit/types`; ExitStatusCount=5 locked-const; lexicon green; G-003 import-invariant green | P4-02-01 |
|
||||
|
||||
### Wave 4 — x/bearers extension + x/partner extension (parallel; independent of Waves 1-3)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-03-01 | REQ-022 | backend-engineer | `x/bearers/types/types.go` (EXTEND existing) | **EXTEND** `x/bearers` (do NOT create new module — A-209/D-037; the BearerType enum + BearerTransport interface are locked since v0.1/v0.2). Add `OYSATLink` struct (gateway-id, constellation (opaque string e.g. "iridium"), frequency-mhz, surveillance-resistant LOCKED true for OY-SAT — A-311). Add `OYQRCode` struct (qr-id, payload-bytes (the signed transfer), issuer-reach-id, expires-at, consumed (bool — OY-QR is one-shot, A-311)). Both are transport-shape stubs (matching D-029: the v0.2 OYLRLink/BeaconFrame are struct stubs, not BearerTransport impls; v0.3 keeps the same shape-only approach). The existing `BearerType` enum + `AllBearers()` (6 bearers, including BearerOYSAT + BearerOYQR since v0.1) is UNCHANGED — v0.3 adds transport structs only. `GenesisState` unchanged. | `go build ./x/bearers/...` succeeds; `OYSATLink` + `OYQRCode` structs present; `OYSATLink.SurveillanceResistant` is LOCKED true; `OYQRCode.Consumed` field exists; existing `AllBearers()` unchanged (6 bearers — regression); zero external deps | — |
|
||||
| P4-04-01 | REQ-023 | backend-engineer | `x/partner/types/types.go` (EXTEND existing) | **EXTEND** `x/partner` (do NOT create new module — A-305; the 4-tier PartnerTier enum is locked since v0.2). Add `AnchorCredential` struct (partner-id (by-ID-string ref to the Anchor Partner), jurisdiction (opaque string e.g. "EU-MiCA"), custody-provider-id (by-ID-string ref to `x/hub` custody service — EMPTY in v0.3 skeleton per A-304, hub not live until P5/v0.4), attestation-refs []string (opaque URIs to Watcher/auditor attestations), onboarded-at). Add `Partner.AnchorCredential() *AnchorCredential` accessor stub returning nil for non-Anchor tiers (A-305 prefers the accessor over a second top-level type). Add `Keeper.AddAnchorCredential(partnerID, cred)` convenience method (rejects non-Anchor partner-id); `Keeper.ListAnchors()` = `ListByTier(TierAnchor)` alias. `PartnerTier` enum (4 tiers) UNCHANGED — v0.3 adds Anchor-specific fields, not a new tier. | `go build ./x/partner/...` succeeds; `AnchorCredential` struct present; `Partner.AnchorCredential()` returns nil for non-Anchor; `AddAnchorCredential` rejects non-Anchor partner-id; `AllPartnerTiers()` unchanged (4 tiers — regression); `custody-provider-id` field is a string (no `x/hub` import — G-003; x/hub does not exist yet, lands in P5) | — |
|
||||
|
||||
### Wave 5 — x/bearers + x/partner tests (parallel; blocked-by Wave 4)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-03-02 | REQ-022, REQ-012 | backend-engineer | `x/bearers/types/types_test.go` (EXTEND existing) | `OYSATLink` struct round-trip + `SurveillanceResistant == true` (LOCKED — A-311); `OYQRCode` struct round-trip + `Consumed` flips true (one-shot); BearerOYSAT + BearerOYQR still in `AllBearers()` (regression: existing v0.1/v0.2 bearers tests still green — 6 bearers unchanged); existing v0.2 OYLRLink/BeaconFrame tests still green (no regression); **lexicon assertion** (extend existing). | `go test ./x/bearers/...` passes; ≥80% coverage on `x/bearers/types`; OY-SAT surveillance-resistant LOCKED true; OY-QR one-shot; existing v0.1/v0.2 bearers tests green (no regression) | P4-03-01 |
|
||||
| P4-04-02 | REQ-023, REQ-012 | backend-engineer | `x/partner/types/types_test.go` (EXTEND existing) | `AnchorCredential` struct round-trip; `Partner.AnchorCredential()` returns nil for non-Anchor tiers; `AddAnchorCredential` rejects non-Anchor partner-id; `ListAnchors()` returns only Anchor-tier partners; `AllPartnerTiers()` unchanged (4 tiers — regression: v0.2 partner tests still green); `custody-provider-id` is an opaque string (no `x/hub` import — G-003 import-invariant green); **lexicon assertion** (extend existing — no "bank"/"account"; "custody"/"jurisdiction"/"compliance" safe). | `go test ./x/partner/...` passes; ≥80% coverage on `x/partner/types`; Anchor accessor + AddAnchorCredential behavior; existing v0.2 partner tests green (no regression) | P4-04-01 |
|
||||
|
||||
### Wave 6 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-05-01 | REQ-012, REQ-010, REQ-022, REQ-023 | lead-developer | (cross-cutting) | Run `go build ./...` + `go test ./...` green (incl. v0.1/v0.2 baseline + docs firewall + new x/bridge, x/exit, x/bearers ext, x/partner ext); coverage ≥80% on the 4 P4 packages; 4 lexicon assertions present; G-003 import-invariant green (extended to scan the new x/bridge + x/exit + x/bearers + x/partner files); `lexicon_meta_test.go` (v0.2, x/*.go scan) green; `lexicon_meta_docs_test.go` (v0.3, docs scan) green; zero external deps (go.mod unchanged); tag `v0.2.4`. | `go test ./...` green; coverage ≥80% on `x/bridge/types`, `x/exit/types`, `x/bearers/types`, `x/partner/types`; 4 lexicon assertions; G-003 import-invariant green; zero require lines in go.mod; git tag `v0.2.4` | P4-01-02, P4-02-02, P4-03-02, P4-04-02 |
|
||||
|
||||
### P4 Must-Haves
|
||||
- [ ] `x/bridge` (new), `x/exit` (new), `x/bearers` (extended), `x/partner` (extended) each have `types/types.go` + `types/types_test.go` (v0.1/v0.2 pattern, package `types`, zero external deps).
|
||||
- [ ] `go build ./...` and `go test ./...` green — **including all v0.1/v0.2 baseline tests + docs firewall (no regression)**.
|
||||
- [ ] ≥80% coverage on `x/bridge/types`, `x/exit/types`, `x/bearers/types`, `x/partner/types`.
|
||||
- [ ] Bridge locked-const: `BridgeStatusCount == 4` (Pending, Attested, Active, Closed).
|
||||
- [ ] Exit locked-const: `ExitStatusCount == 5` (Proposed, InProgress, Settled, Failed, Refunded).
|
||||
- [ ] Exit `bridge-route-id` is a by-ID-string ref to `x/bridge` (G-003 — no struct import; import-invariant test green).
|
||||
- [ ] Bearers: `OYSATLink` (surveillance-resistant LOCKED true) + `OYQRCode` (one-shot `consumed`); existing `AllBearers()` (6) unchanged.
|
||||
- [ ] Partner: `AnchorCredential` struct + `Partner.AnchorCredential()` accessor (nil for non-Anchor); `PartnerTier` (4) unchanged.
|
||||
- [ ] Partner Anchor `custody-provider-id` is an opaque string (no `x/hub` import — x/hub lands in P5).
|
||||
- [ ] Lexicon assertion in all 4 P4 test files.
|
||||
- [ ] `lexicon_meta_test.go` (v0.2, x/*.go) green; `lexicon_meta_docs_test.go` (v0.3, docs) green.
|
||||
- [ ] Zero external deps (go.mod unchanged — G-006).
|
||||
- [ ] Git tag `v0.2.4`.
|
||||
|
||||
### P4 Risks & Mitigations
|
||||
- **Intra-P4 ordering (bridge before exit)** (A-308/G-003) → Wave 1 lands `x/bridge` types; Wave 2 lands `x/exit` (references bridge by ID); Wave 3 lands both tests. Reversing would force `x/exit` tests to reference a non-existent BridgeRoute type.
|
||||
- **"venue" as a locked enum** (A-308) → `venue` is an opaque string, NOT a locked enum; venues are operational (uniswap-v3/v4, oy-dex) and locking now would create a false firewall. Test asserts no enum-count for venue.
|
||||
- **Bearers extension regression** → existing v0.1/v0.2 bearers tests must stay green; `AllBearers()` count unchanged (6); test asserts no regression.
|
||||
- **Partner Anchor → hub forward-reference** → `custody-provider-id` is an EMPTY string in v0.3 (hub not live until P5); the field exists so the shape is stable. This is the P4→P5 edge (D-044).
|
||||
- **"account"/"currency"/"dollar"/"euro" lexicon drift in exit** → use "Holder"/"Reach"/opaque asset strings; lexicon assertion + meta-test are the gate.
|
||||
|
||||
---
|
||||
|
||||
## Phase P5 — Bearers Skeleton II (hub/services/bond)
|
||||
|
||||
- **Slug**: `bearers-skeleton-ii`
|
||||
- **Branch**: `oy/phase/05-bearers-skeleton-ii`
|
||||
- **REQs covered**: REQ-024 (Hub API — x/hub), REQ-025 (Services — x/services), REQ-026 (Bond market depth — x/bond extension)
|
||||
- **Tag**: `v0.2.5`
|
||||
- **Type**: `feat`
|
||||
- **Persona**: backend-engineer
|
||||
- **Goal**: Ship the Bearers skeleton II: `x/hub` (HubService enum + per-service stubs, referencing Anchor partners by ID), `x/services` (ServiceKind enum + per-service stubs, referencing Window by ID), and `x/bond` extension (GrowthBond + secondary-market order types; 8%/0% consts unchanged — D-028 regression firewall). P5 has NO intra-phase ordering (hub, services, bond are independent of each other).
|
||||
|
||||
### Wave 1 — x/hub types + tests (blocked-by P4 partner Anchor for the operator-partner-id reference)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-01-01 | REQ-024 | backend-engineer | `x/hub/types/types.go` + `x/hub/types/genesis.go` + `x/hub/types/types_test.go` (+ `genesis_test.go` if split) | New `x/hub/types/` module following `x/forex` (D-039, A-303). `HubService` enum (Custody, LendingPrimitive, Compliance) — exactly 3, locked-const `HubServiceCount = 3` (A-312); `AllHubServices() []HubService`. `HubServiceInfo` struct (service-id, kind (HubService), operator-partner-id (by-ID-string ref to `x/partner` Anchor — G-003, no struct import; the P4 Anchor must exist), name, status). `HubServiceStatus` enum (Pending, Active, Suspended, Revoked) — exactly 4 (local redefinition of the v0.2 PartnerStatus 4-state shape, no import). Per-service struct stubs: `CustodyService` (service-id, custody-provider-id, assets-supported []string), `LendingPrimitiveService` (service-id, primitive-kind (opaque string), coupon-cap-bps uint32 — LOCAL const `LendingCouponCapBps = 800` cross-documented to D-028/A-304, NOT an import of `x/bond.Clamp`), `ComplianceService` (service-id, jurisdiction, attestation-refs []string). `Keeper` stub: AddService / GetService / ListByKind. `Params`, `GenesisState` (services), `DefaultGenesisState`, `ValidateGenesis` (reject dup service-ids, A-212). **Tests**: HubServiceCount=3 locked-const; enum names; service round-trip; per-service struct fields; `LendingCouponCapBps == 800` (cross-doc to D-028); `operator-partner-id` is a string (no `x/partner` struct import — G-003 import-invariant green); `ValidateGenesis` rejects dup service-id; **lexicon assertion** (HIGH-RISK: "interest"/"yield"/"deposit"/"savings" banned — use "lending primitive"/"coupon"/"custody"/"compliance"; "lending" is NOT banned per RESEARCH §1.5). | `go build ./x/hub/...` succeeds; `go test ./x/hub/...` passes; ≥80% coverage; `HubServiceCount == 3`; `LendingCouponCapBps == 800`; lexicon green; G-003 import-invariant green | P4-05-01 |
|
||||
|
||||
### Wave 2 — x/services types + tests (parallel with Wave 1; independent)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-02-01 | REQ-025 | backend-engineer | `x/services/types/types.go` + `x/services/types/genesis.go` + `x/services/types/types_test.go` (+ `genesis_test.go` if split) | New `x/services/types/` module following `x/hub` (D-040, A-307). `ServiceKind` enum (Care, SIM, Vault, Mail) — exactly 4, locked-const `ServiceKindCount = 4` (A-307); `AllServiceKinds() []ServiceKind`. `ServiceInfo` struct (service-id, kind (ServiceKind), operator-reach-id (by-ID-string ref to `x/identity` Reach — G-003), name, status, window-id (by-ID-string ref to `x/window` — a service-grant opens a Window on the holder's behalf; the Window Lifecycle interface hook, typed in v0.3, invoked at runtime in v0.4)). `ServiceStatus` enum (Pending, Active, Suspended, Revoked) — exactly 4 (local redefinition). Per-service struct stubs: `CareService` (service-id, care-kind (opaque)), `SIMService` (service-id, carrier (opaque)), `VaultService` (service-id, storage-quota-grain), `MailService` (service-id, mailbox-id). `Keeper` stub: AddService / GetService / ListByKind. `Params`, `GenesisState` (services), `DefaultGenesisState`, `ValidateGenesis` (reject dup service-ids, A-212). **Tests**: ServiceKindCount=4 locked-const; enum names (Care, SIM, Vault, Mail); service round-trip; per-service struct fields; `window-id` is a string (no `x/window` struct import — G-003 import-invariant green); `operator-reach-id` is a string (no `x/identity` import); `ValidateGenesis` rejects dup service-id; **lexicon assertion** (no "account" — use service-id/operator-reach-id; "Mail"/"SIM"/"Care"/"Vault" safe). | `go build ./x/services/...` succeeds; `go test ./x/services/...` passes; ≥80% coverage; `ServiceKindCount == 4`; lexicon green; G-003 import-invariant green | P4-05-01 (P5 has no intra-phase dep; blocked-by P4 ship for branch hygiene + the G-003 import-invariant test scanning the new files) |
|
||||
|
||||
### Wave 3 — x/bond extension (GrowthBond + secondary market) + tests (parallel with Waves 1/2; independent)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-03-01 | REQ-026 | backend-engineer | `x/bond/types/types.go` (EXTEND) + `x/bond/types/genesis.go` (EXTEND) + `x/bond/types/types_test.go` (EXTEND) | **EXTEND** `x/bond` (do NOT create new module — D-041; the 8%/0% consts are locked since v0.2 and UNCHANGED in v0.3). Add `GrowthBond` struct embedding the v0.2 `Bond` + a `GrowthRateBps` field (per-period growth rate of the coupon). Add `ClampGrowth(currentBps, growthBps uint32) uint32` helper returning `min(CouponCapBps - currentBps, growthBps)` so the post-growth coupon is ≤ `CouponCapBps` (A-306 — reuses the v0.2 `CouponCapBps`/`CouponFloorBps` consts, same package, no G-003 concern). Add `SecondaryOrder` struct (order-id, bond-id (by-ID-string ref to the Bond), side (`OrderSide` enum), price-bps (fraction of principal in bps), quantity-grain, holder-reach-id, status (`OrderStatus` enum), created-at). Add `OrderSide` enum (Buy, Sell) — exactly 2, locked-const `OrderSideCount = 2` (A-313). Add `OrderStatus` enum (Open, Filled, Cancelled) — exactly 3, locked-const `OrderStatusCount = 3` (A-313). `AllOrderSides()` / `AllOrderStatuses()`. Extend `Keeper` stub: AddOrder / GetOrder / ListByBond / CancelOrder (no matching — full secondary-market matching deferred to v0.4). Extend `GenesisState` with `GrowthBonds []GrowthBond` + `Orders []SecondaryOrder`; `ValidateGenesis` checks growth-bond-id + order-id uniqueness (A-212). **Tests (extend existing)**: **REGRESSION: `CouponCapBps == 800` and `CouponFloorBps == 0` STILL (D-028 firewall — v0.3 must not change the v0.2 consts)**; `ClampGrowth` invariant (post-growth coupon ≤ 800, never below 0; current + growth where current+growth > cap → growth clamped to cap-current); `OrderSideCount == 2`; `OrderStatusCount == 3`; order round-trip; GrowthBond round-trip; `bond-id` is a string (no struct self-import — same package); existing v0.2 bond tests still green (Clamp, BondStatus, etc. — no regression); **lexicon assertion** (HIGH-RISK: "interest"/"yield"/"deposit"/"savings" banned — use "coupon"/"growth"/"secondary"/"order"; "growth" safe, "yield growth" banned — use "coupon growth"/"real-return-linked coupon"). | `go build ./x/bond/...` succeeds; `go test ./x/bond/...` passes; ≥80% coverage; **`CouponCapBps == 800` + `CouponFloorBps == 0` regression green**; `ClampGrowth` invariant green; `OrderSideCount == 2` + `OrderStatusCount == 3`; existing v0.2 bond tests green (no regression); lexicon green | P4-05-01 |
|
||||
|
||||
### Wave 4 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-04-01 | REQ-012, REQ-024, REQ-025, REQ-026 | lead-developer | (cross-cutting) | Run `go build ./...` + `go test ./...` green (incl. v0.1/v0.2 baseline + docs firewall + P4 + P5); coverage ≥80% on `x/hub/types`, `x/services/types`, `x/bond/types`; 3 lexicon assertions (hub, services, bond-ext); G-003 import-invariant green (extended to scan x/hub + x/services + x/bond); `lexicon_meta_test.go` (v0.2) + `lexicon_meta_docs_test.go` (v0.3) green; **D-028 regression: 8%/0% bond consts unchanged**; zero external deps (go.mod unchanged); tag `v0.2.5`. | `go test ./...` green; coverage ≥80% on `x/hub/types`, `x/services/types`, `x/bond/types`; 3 lexicon assertions; G-003 import-invariant green; `CouponCapBps == 800` + `CouponFloorBps == 0` regression green; zero require lines in go.mod; git tag `v0.2.5` | P5-01-01, P5-02-01, P5-03-01 |
|
||||
|
||||
### P5 Must-Haves
|
||||
- [ ] `x/hub` (new), `x/services` (new), `x/bond` (extended) each have `types/types.go` + `types/types_test.go`.
|
||||
- [ ] `go build ./...` and `go test ./...` green — including v0.1/v0.2 baseline + docs firewall + P4 (no regression).
|
||||
- [ ] ≥80% coverage on `x/hub/types`, `x/services/types`, `x/bond/types`.
|
||||
- [ ] Hub locked-const: `HubServiceCount == 3` (Custody, LendingPrimitive, Compliance); `LendingCouponCapBps == 800` (cross-doc to D-028, local const — no `x/bond` import).
|
||||
- [ ] Hub `operator-partner-id` is a by-ID-string ref to `x/partner` Anchor (G-003 — no struct import; import-invariant green). This confirms P4→P5 ordering.
|
||||
- [ ] Services locked-const: `ServiceKindCount == 4` (Care, SIM, Vault, Mail).
|
||||
- [ ] Services `window-id` is a by-ID-string ref to `x/window` (G-003); `operator-reach-id` is a by-ID-string ref to `x/identity`.
|
||||
- [ ] Bond: `GrowthBond` + `ClampGrowth` (post-growth coupon ≤ 800); `OrderSideCount == 2` (Buy/Sell); `OrderStatusCount == 3` (Open/Filled/Cancelled).
|
||||
- [ ] **D-028 regression: `CouponCapBps == 800` and `CouponFloorBps == 0` unchanged** (v0.3 must not change v0.2 bond consts).
|
||||
- [ ] Existing v0.2 bond tests still green (no regression).
|
||||
- [ ] Lexicon assertion in all 3 P5 test files.
|
||||
- [ ] `lexicon_meta_test.go` (v0.2, x/*.go) green; `lexicon_meta_docs_test.go` (v0.3, docs) green.
|
||||
- [ ] Zero external deps (go.mod unchanged — G-006).
|
||||
- [ ] Git tag `v0.2.5`.
|
||||
|
||||
### P5 Risks & Mitigations
|
||||
- **Hub lending-primitive lexicon risk** (HIGHEST v0.3 lexicon risk after x/bond) → "interest"/"yield"/"deposit"/"savings" are natural fit-words for a lending primitive; use "lending primitive"/"coupon" (vision §13/§17). "lending" is NOT banned (RESEARCH §1.5); the lexicon assertion is the gate. The local `LendingCouponCapBps = 800` const cross-documents D-028 (A-304) to avoid importing `x/bond.Clamp` (G-003).
|
||||
- **GrowthBond "yield growth" phrasing** → use "coupon growth"/"real-return-linked coupon"; "yield" is banned standalone (word-boundary); `TestLexiconMetaNoFalsePositiveOnOpenYield` allows "OpenYield" but bans "yield".
|
||||
- **D-028 const regression** → the v0.3 GrowthBond must NOT change `CouponCapBps`/`CouponFloorBps`; the regression test (8%/0% unchanged) is the D-028 firewall.
|
||||
- **ServiceKind "Vault" naming collision with `x/vault`** → the `Vault` enum value is a service kind (in `x/services`), not a module import; `VaultService` references `x/vault` by ID-string (G-003). No Go import cycle (concept-level collision only).
|
||||
- **Hub → partner Anchor forward-reference** → P4 must ship before P5 (D-044); the P5-04-01 verification confirms `operator-partner-id` is a string (no `x/partner` struct import).
|
||||
|
||||
---
|
||||
|
||||
## Phase P6 — Final Review + Audit + Ship
|
||||
|
||||
- **Slug**: `final-review-audit-ship`
|
||||
- **Branch**: `oy/phase/06-final-review-audit-ship`
|
||||
- **REQs covered**: REQ-012 (lexicon, project-wide meta-test + docs firewall), all v0.3 REQs (REQ-010, REQ-022..REQ-028 audit confirmation)
|
||||
- **Tag**: `v0.2.6` (= **milestone v0.3 release** — final phase patch IS the milestone release per D-008/D-034)
|
||||
- **Type**: `final`
|
||||
- **Personas**: lead-developer (review/ship), backend-engineer (audit assist), ci-code-reviewer + ci-doc-verifier + ci-debugger (CI personas, activated for P6)
|
||||
- **Goal**: Run the full v0.3 milestone audit — project-wide lexicon meta-test + docs firewall, coverage gate across all 7 new/extended x/* packages, all locked-const invariants green (incl. D-028 regression), all docs pages lexicon-clean, all v0.3 REQs have skeleton+tests or docs — then ship the `v0.3` milestone release as tag `v0.2.6`.
|
||||
|
||||
### Wave 1 — ci-code-review (multi-persona review across P1-P5)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P6-01-01 | REQ-012, all v0.3 | lead-developer + ci-code-reviewer | (cross-cutting) | **Code review** across P1-P5: confirm `go build ./...` + `go test ./...` green; confirm `lexicon_meta_test.go` (v0.2, x/*.go) green AND scans all 22 x/* packages (v0.1 15 + v0.2 10 + v0.3 7 = 32? — recount: v0.1=15, v0.2 added 10 new/extended to 25, v0.3 adds 4 new + 3 extended = 7 → 25 + 4 new = 29 distinct packages; the 3 extended are already counted); confirm `lexicon_meta_docs_test.go` (v0.3, docs) green AND scans README.md + all `docs/**/*.md` (26 pages); confirm G-003 import-invariant green across all v0.3 x/* files (no struct imports across `x/<module>/types`); confirm zero external deps (go.mod: `module github.com/oy/openyield\ngo 1.22` with no require lines — G-006). | `go test ./...` green; both firewalls green; G-003 import-invariant green; go.mod zero-dep confirmed; coverage report generated | P5-04-01 |
|
||||
|
||||
### Wave 2 — ciagent-audit (reconstruction + discipline)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P6-02-01 | all v0.3 | lead-developer + ci-debugger | (cross-cutting) | **Reconstruction test**: confirm every v0.3 REQ maps to a shipped artifact (REQ-010 → x/bridge + x/exit; REQ-022 → x/bearers OYSATLink/OYQRCode; REQ-023 → x/partner AnchorCredential; REQ-024 → x/hub; REQ-025 → x/services; REQ-026 → x/bond GrowthBond/SecondaryOrder; REQ-027 → README + docs/ 26 pages; REQ-028 → lexicon_meta_docs_test.go). **File/branch/commit discipline**: confirm each phase P1..P5 shipped on its own branch (`oy/phase/0N-*`) with its patch tag (`v0.2.1..v0.2.5`); confirm no phase branch merged out of order (D-044: P1 firewall before P2/P3 content; P4 before P5 for Anchor→hub). **Locked-const audit**: HubServiceCount=3, ServiceKindCount=4, BridgeStatusCount=4, ExitStatusCount=5, OrderSideCount=2, OrderStatusCount=3, LendingCouponCapBps=800, **D-028 regression: CouponCapBps=800 + CouponFloorBps=0 unchanged**. | REQ→artifact map complete (each REQ has a file); branch/tag discipline confirmed (5 phase branches + 5 patch tags); all locked-consts green; D-028 regression green | P6-01-01 |
|
||||
|
||||
### Wave 3 — ciagent-ship (merge + tag + release + cleanup)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P6-03-01 | (milestone) | lead-developer + ci-doc-verifier | `.ciagent/oy/REQUIREMENTS.md` + `.ciagent/oy/ROADMAP.md` | **Update REQUIREMENTS.md**: mark REQ-010, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026 → Skeleton; REQ-027 → Complete; REQ-028 → Complete. **Update ROADMAP.md**: mark v0.3 milestone COMPLETE (all 7 phases P0..P6 checked); add the tag-line note that v0.3 shipped on the `v0.2.x` patch line (P0 → `v0.2.0`, P1..P5 → `v0.2.1..v0.2.5`, P6 → `v0.2.6` = milestone release, per D-008/D-034). | REQUIREMENTS.md status column updated for all 8 v0.3 REQs; ROADMAP.md v0.3 marked complete + tag-line note present | P6-02-01 |
|
||||
| P6-03-02 | (milestone) | lead-developer | (cross-cutting) | **Final ship**: merge phase/06 → milestone/v0.3 → main; create milestone release tag `v0.2.6` (= v0.3 milestone release per D-008/D-034); delete the 5 phase branches (`oy/phase/01-*`..`oy/phase/05-*`) after merge (P6 branch retained until post-release cleanup); confirm `go build ./...` + `go test ./...` green at the `v0.2.6` tag. | `v0.2.6` tag created on main; `go test ./...` green at the tag; ROADMAP.md v0.3 complete; phase branches deleted (except P6); release notes reference v0.3 scope (Bearers skeleton I+II + docs site + firewall extension) | P6-03-01 |
|
||||
|
||||
### P6 Must-Haves
|
||||
- [ ] Project-wide lexicon meta-test (`lexicon_meta_test.go`, x/*.go) scans all 29 x/* packages (v0.1 15 + v0.2 10 + v0.3 4 new; 3 extended already counted); green.
|
||||
- [ ] Docs firewall (`lexicon_meta_docs_test.go`) scans README.md + all 26 `docs/**/*.md` pages; green.
|
||||
- [ ] Coverage ≥80% on all 7 new/extended v0.3 x/* packages (bridge, exit, bearers-ext, partner-ext, hub, services, bond-ext).
|
||||
- [ ] All locked-const invariants green: HubServiceCount=3, ServiceKindCount=4, BridgeStatusCount=4, ExitStatusCount=5, OrderSideCount=2, OrderStatusCount=3, LendingCouponCapBps=800, OYSATLink surveillance-resistant=true, OYQRCode one-shot consumed.
|
||||
- [ ] **D-028 regression**: `CouponCapBps == 800`, `CouponFloorBps == 0` unchanged from v0.2.
|
||||
- [ ] All v0.1/v0.2 baseline tests still green (no regression across 29 x/* packages + docs).
|
||||
- [ ] G-003 import-invariant green (no struct imports across `x/<module>/types` in any v0.3 production file).
|
||||
- [ ] Zero external deps (go.mod unchanged — G-006).
|
||||
- [ ] REQUIREMENTS.md status column updated (REQ-010/022..026 → Skeleton; REQ-027/028 → Complete).
|
||||
- [ ] ROADMAP.md v0.3 marked COMPLETE + tag-line note (`v0.2.x` patch line).
|
||||
- [ ] `go build ./...` and `go test ./...` green at the `v0.2.6` tag.
|
||||
- [ ] Git tag `v0.2.6` created (= v0.3 milestone release).
|
||||
- [ ] Phase branches P1..P5 deleted post-merge (P6 retained until post-release cleanup).
|
||||
|
||||
### P6 Risks & Mitigations
|
||||
- **Lexicon drift via copy-pasted comments in v0.3 x/* modules** → the v0.2 meta-test scans comments + strings + identifiers; v0.3 modules are automatically covered (no new meta-test work, just the existing scan).
|
||||
- **D-028 const regression at the last mile** → P5-03-01 + P6-02-01 both assert 8%/0% unchanged; double firewall.
|
||||
- **Milestone versioning confusion (v0.3 milestone = v0.2.6 tag)** → lead-developer enforces D-008/D-034: final phase patch IS the milestone release; no separate minor tag. ROADMAP tag-line note (P6-03-01) prevents `v0.2.6`/`v0.3.0` confusion.
|
||||
|
||||
---
|
||||
|
||||
## Coverage Targets (D-033) — v0.3
|
||||
|
||||
| Package | Phase | Target | Locked-const tests |
|
||||
|---|---|---|---|
|
||||
| `x/bridge/types` | P4 | ≥80% | BridgeStatusCount=4 (Pending, Attested, Active, Closed) |
|
||||
| `x/exit/types` | P4 | ≥80% | ExitStatusCount=5 (Proposed, InProgress, Settled, Failed, Refunded) |
|
||||
| `x/bearers/types` (ext) | P4 | ≥80% | OYSATLink surveillance-resistant=true (A-311); OYQRCode one-shot consumed; AllBearers()=6 unchanged (regression) |
|
||||
| `x/partner/types` (ext) | P4 | ≥80% | AllPartnerTiers()=4 unchanged (regression); AnchorCredential accessor nil-for-non-Anchor |
|
||||
| `x/hub/types` | P5 | ≥80% | HubServiceCount=3; LendingCouponCapBps=800 (cross-doc D-028/A-304) |
|
||||
| `x/services/types` | P5 | ≥80% | ServiceKindCount=4 (Care, SIM, Vault, Mail) |
|
||||
| `x/bond/types` (ext) | P5 | ≥80% | **CouponCapBps=800 + CouponFloorBps=0 unchanged (D-028 regression)**; ClampGrowth post-growth ≤ 800; OrderSideCount=2; OrderStatusCount=3 |
|
||||
|
||||
**Lexicon assertions (REQ-012)**: present in all 7 new/extended v0.3 test files (per-package) + project-wide `lexicon_meta_test.go` (v0.2, x/*.go — automatically covers v0.3 x/* files) + `lexicon_meta_docs_test.go` (v0.3, README.md + docs/**/*.md).
|
||||
|
||||
---
|
||||
|
||||
## Task Count Summary — v0.3
|
||||
|
||||
| Phase | Waves | Tasks | New/Extended Packages / Docs |
|
||||
|---|---|---|---|
|
||||
| P1 | 3 | 11 | lexicon_meta_docs_test.go + mkdocs.yml + README.md + docs/index.md + 6 docs/shared/ pages |
|
||||
| P2 | 2 | 9 | 8 docs/nomads/ pages |
|
||||
| P3 | 3 | 12 | 8 docs/freeholders/ pages + 2 docs/reference/ pages |
|
||||
| P4 | 6 | 9 | x/bridge (new), x/exit (new), x/bearers (ext), x/partner (ext) |
|
||||
| P5 | 4 | 5 | x/hub (new), x/services (new), x/bond (ext) |
|
||||
| P6 | 3 | 4 | (audit/ship, 0 new — audit + REQUIREMENTS/ROADMAP update + tag) |
|
||||
| **Total** | — | **50** | **4 new x/* + 3 extended x/* + 26 docs pages + 1 firewall test** |
|
||||
|
||||
## Per-Phase REQ Coverage — v0.3
|
||||
|
||||
| Phase | REQs | Components / Docs |
|
||||
|---|---|---|
|
||||
| P1 | REQ-028, REQ-027 | Docs firewall (lexicon_meta_docs_test.go) + mkdocs.yml + README + docs/index + docs/shared (6 pages) |
|
||||
| P2 | REQ-027 | docs/nomads (8 pages: Reach, Stash, bearers, Maps/Pay, Pacts, standing-basics, Window, index) |
|
||||
| P3 | REQ-027 | docs/freeholders (8 pages) + docs/reference (2 pages) — REQ-027 COMPLETE |
|
||||
| P4 | REQ-010, REQ-022, REQ-023 | x/bridge + x/exit (Exit layer), x/bearers OY-SAT/OY-QR, x/partner Anchor |
|
||||
| P5 | REQ-024, REQ-025, REQ-026 | x/hub, x/services, x/bond GrowthBond + secondary |
|
||||
| P6 | REQ-012 + all v0.3 REQs (audit) | Both firewalls, coverage gate, D-028 regression, milestone ship |
|
||||
|
||||
## Cross-Phase Blockers (hard) — v0.3
|
||||
|
||||
- **P1-01-01 (docs firewall)** → blocks P1-02-* (README + shared content scanned by the firewall) and P2/P3 (audience content scanned).
|
||||
- **P4-01-01 (x/bridge types)** → blocks P4-02-01 (x/exit — `bridge-route-id` refs BridgeRoute by ID-string, A-308/G-003).
|
||||
- **P4-04-01 (x/partner Anchor extension)** → blocks P5-01-01 (x/hub — `operator-partner-id` refs Anchor by ID-string, A-304/G-003). This is the P4→P5 edge (D-044).
|
||||
- **P5-04-01 (P5 ship)** → blocks P6-01-01 (P6 audit).
|
||||
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene; types themselves only depend on the listed hard blockers).
|
||||
|
||||
## v0.3 Decisions Applied (D-034..D-046 + A-301..A-315)
|
||||
|
||||
The v0.3 Phase 0 clarify/ideate/research stages produced 13 clarification decisions (D-034..D-046) and 15 research assumptions (A-301..A-315), all applied to this plan:
|
||||
|
||||
| ID | Decision / Assumption | Applied to |
|
||||
|---|---|---|
|
||||
| D-034 | v0.3 bundles Bearers skeleton + docs under one feature milestone; tags on v0.2.x | Milestone Summary, all phases |
|
||||
| D-035 | Bearers skeleton continues D-020 pattern (no live chain) | P4, P5 |
|
||||
| D-036 | REQ-010 = x/exit + x/bridge (two packages) | P4 Waves 1-3 |
|
||||
| D-037 | REQ-022 = x/bearers OYSATLink + OYQRCode | P4 Wave 4 |
|
||||
| D-038 | REQ-023 = x/partner AnchorCredential extension (no new tier) | P4 Wave 4 |
|
||||
| D-039 | REQ-024 = x/hub new module (Pact #6 promoted) | P5 Wave 1 |
|
||||
| D-040 | REQ-025 = x/services new module (4 kinds) | P5 Wave 2 |
|
||||
| D-041 | REQ-026 = x/bond GrowthBond + secondary (8%/0% unchanged) | P5 Wave 3 |
|
||||
| D-042 | MkDocs Material; audience-organized docs | P1-01-02, P2, P3 |
|
||||
| D-043 | Docs firewall = sibling test lexicon_meta_docs_test.go (not modifying v0.2 meta-test) | P1-01-01 |
|
||||
| D-044 | Phase ordering: firewall-first (P1) before content (P2/P3); P4 before P5 (Anchor→hub) | Cross-Phase Dependency Map, all phase goals |
|
||||
| D-045 | Docs depth: ~20-25 pages across 4 audiences | P1/P2/P3 page counts |
|
||||
| D-046 | No docs publishing CI in v0.3 | P1-01-02, P1-02-01 |
|
||||
| A-304 | x/hub LendingCouponCapBps=800 local const (no x/bond import) | P5-01-01 |
|
||||
| A-308 | x/exit venue is opaque string (not locked enum) | P4-02-01 |
|
||||
| A-311 | OY-SAT surveillance-resistant LOCKED true; OY-QR one-shot | P4-03-01 |
|
||||
| A-312 | HubServiceCount=3 | P5-01-01 |
|
||||
| A-313 | OrderSideCount=2, OrderStatusCount=3 | P5-03-01 |
|
||||
| A-315 | v0.2 cosmos-engineer/security-engineer NOT reactivated | Persona assignments (backend-engineer owns all P4/P5) |
|
||||
|
||||
---
|
||||
|
||||
## Milestone v0.4 — Refinement (NFR) — Phase Plan
|
||||
|
||||
### Milestone Summary
|
||||
|
||||
- **Milestone**: v0.4 — Refinement (NFR)
|
||||
- **Type**: NFR (zero `feat:` phases by construction; D-047). Final-phase audit enforces the NFR purity gate (`git log --grep "^feat:"` on the milestone range returns zero).
|
||||
- **Tag base**: `v0.3.x` patch line. P0 ships as `v0.3.0`; execution phases `v0.3.1..v0.3.3`; final phase P4 patch `v0.3.4` IS the milestone release. No separate minor tag (D-008).
|
||||
- **Phases**: 4 execution + 1 final = 5. Phase 0 (this PLAN) is pre-execution.
|
||||
- **Depth**: refinement-only — refactor + test + docs + chore. No new production types, no new enum types, no behavioral changes. All work passes the D-001 refinement-only filter.
|
||||
- **Coverage target**: ≥80% on any modified package; do NOT reduce existing coverage (v0.3 floor is 93.3%).
|
||||
- **New modules**: ZERO. Touched files: `lexicon/lexicon.go` (add helper), `lexicon_meta_test.go` + `lexicon_meta_docs/lexicon_meta_docs_test.go` (refactor to consume helper), `x/hub/types/cross_const_test.go` (NEW test file), `x/council/types/types_test.go` (add intent test), `.ciagent/oy/ARCHITECTURE.md` (docs), `.gitea/workflows/docs-build.yml` (NEW CI).
|
||||
- **Phase ordering** (D-052): P1 lexicon+const hardening → P2 lifecycle divergence docs+guard → P3 docs build CI → P4 final review/audit/ship. P1 is firewall-first (highest-severity regression risk); each phase independently shippable (vertical slices).
|
||||
- **Personas**: backend-engineer (P1, P2 code), lead-developer (P2 docs, P3 CI, P4 review/ship). v0.3 frontend-engineer + docs-writer deactivated.
|
||||
|
||||
### Cross-Phase Dependency Map
|
||||
|
||||
```
|
||||
P1 (lexicon helper, cross-const test) ──► P4 (review)
|
||||
P2 (lifecycle docs + regression guard) ─► P4 (review)
|
||||
P3 (docs build CI) ─► P4 (review)
|
||||
```
|
||||
|
||||
No hard cross-phase blockers (all three execution phases are independent vertical slices). P4 depends on P1+P2+P3 completion only.
|
||||
|
||||
### D-001 Refinement-Only Filter (governs every v0.4 task)
|
||||
|
||||
- **Accept**: refactor, test, docs, chore, quality, architecture (drift fix only).
|
||||
- **Reject**: `add_requirement` + `feat:`-class signal (new enum type, new production type, new capability, new CLI, new distribution channel).
|
||||
- **Pre-seeded NFR REQs** (REQ-029..REQ-032) are exempt — already in REQUIREMENTS.md before PLAN.
|
||||
|
||||
---
|
||||
|
||||
## Phase P1 — Lexicon + Const Hardening
|
||||
|
||||
- **Slug**: `lexicon-const-hardening`
|
||||
- **Branch**: `oy/phase/01-lexicon-const-hardening`
|
||||
- **REQs covered**: REQ-029 (lexicon shared helper), REQ-030 (cross-const test)
|
||||
- **Tag**: `v0.3.1`
|
||||
- **Goal**: Close the G-014 lexicon-firewall drift risk (shared synthetic-string helper) and the A-304 hub↔bond const drift risk (cross-package equality test). Both are refactor+test; no behavioral change.
|
||||
|
||||
### Wave 1 — Shared helper + cross-const test (parallel; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-01-01 | REQ-029 | backend-engineer | `lexicon/lexicon.go` | Add `SyntheticBannedStrings() []string` returning the 10 synthetic strings currently duplicated in both meta-tests. Each string embeds exactly one banned term (from `BannedTerms()`) in a plausible sentence context. Source uses `BannedTerms()` so the lexicon package's own source stays lexicon-clean (fragment-assembled). Add a doc comment cross-referencing G-014 and both consuming meta-tests. | `go build ./lexicon/...` succeeds; `SyntheticBannedStrings()` returns exactly `len(BannedTerms())` strings; each returned string triggers `FindBannedTerm` (self-verifiable in a new test); lexicon package source stays lexicon-clean (the existing `lexicon_meta_test.go` x/ scan does not scan `lexicon/` itself, but the helper must not introduce a banned-term literal). | — |
|
||||
| P1-01-02 | REQ-029 | backend-engineer | `lexicon_meta_test.go`, `lexicon_meta_docs/lexicon_meta_docs_test.go` | Refactor `TestLexiconMetaSelfTestTable` and `TestLexiconMetaDocsSelfTestTable` to consume `lexicon.SyntheticBannedStrings()` instead of building their own `synthetic` slice. Remove the duplicated 10-string table from both. Keep the per-string `FindBannedTerm` assertion loop (detection coverage unchanged). Keep the `len(terms) == 10` assertion from `BannedTerms()`. Add a `len(synthetic) == len(terms)` assertion derived from the helper. | `go test ./lexicon_meta... ./lexicon_meta_docs/...` passes; both tests still fail if detection breaks; the duplicated table is gone (grep for the old literal sentence fragments returns zero matches in meta-test files); coverage on both meta-tests does not decrease. | P1-01-01 |
|
||||
| P1-02-01 | REQ-030 | backend-engineer | `x/hub/types/cross_const_test.go` (NEW) | New test file, package `types` (same package as `x/hub/types`). Test-only import `github.com/oy/openyield/x/bond/types` (G-003 test-exempt). Tests: (1) `TestLendingCouponCapMatchesBondCap` asserts `hub.LendingCouponCapBps == bond.CouponCapBps`; (2) `TestLendingCouponFloorMatchesBondFloor` asserts `hub.LendingCouponFloorBps == bond.CouponFloorBps`; (3) `TestConstsAreMissionLocked800And0` asserts both caps == 800 and both floors == 0 (regression guard for the mission-locked values). Doc comment cross-references A-304 and the G-003 test-exemption. | `go test ./x/hub/types/...` passes; all three tests green at current values (800/0); test fails closed if either const drifts; NO production file in `x/hub/types/` imports `x/bond/types` (G-003 production firewall intact — verified by a grep of non-test `.go` files). | — |
|
||||
|
||||
### Wave 2 — Verification (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-99-01 | REQ-029, REQ-030 | lead-developer | — (verification only) | Run `go test ./...` (all packages, both lexicon firewalls + cross-const test + all v0.3 baseline tests); run `grep -rn "open a .* here\|make a .* now\|compounding .* rate" lexicon_meta_test.go lexicon_meta_docs/` and confirm zero matches (old duplicated table fully removed); confirm `go.mod` unchanged (zero deps, G-006). | `go test ./...` exits 0; grep returns zero; `go.mod` diff is empty. | P1-01-02, P1-02-01 |
|
||||
|
||||
### P1 Must-Haves (verification gate before SHIP)
|
||||
|
||||
- [ ] `lexicon.SyntheticBannedStrings()` exists and returns 10 strings.
|
||||
- [ ] Both meta-tests consume the helper (no duplicated table).
|
||||
- [ ] `x/hub/types/cross_const_test.go` exists and passes.
|
||||
- [ ] `go test ./...` green (all v0.3 baseline + v0.4 new tests).
|
||||
- [ ] `go.mod` unchanged.
|
||||
- [ ] No `feat:` commits in P1 (D-001 filter).
|
||||
|
||||
---
|
||||
|
||||
## Phase P2 — Lifecycle Divergence Documentation + Regression Guard
|
||||
|
||||
- **Slug**: `lifecycle-divergence-docs`
|
||||
- **Branch**: `oy/phase/02-lifecycle-divergence-docs`
|
||||
- **REQs covered**: REQ-031
|
||||
- **Tag**: `v0.3.2`
|
||||
- **Goal**: Document the AUDIT §193 council lifecycle type divergences (P1-1 Proposal/VoteOption absent; P1-2 SignalKind 4-vs-5) in ARCHITECTURE.md and add an intent-assertion test guarding the current `SignalKindCount==4` shape. No enum additions, no locked-const changes (D-050).
|
||||
|
||||
### Wave 1 — Docs + regression guard (parallel; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-01-01 | REQ-031 | lead-developer | `.ciagent/oy/ARCHITECTURE.md` | Add a "Council Voice/Council Interface — Lifecycle Type Divergence Decisions (v0.4)" subsection under the v0.4 Architecture section. Document: (a) P1-1 `Proposal`/`ProposalStatus`/`VoteOption` absent — deferred to v0.5+ governance runtime (feat:-class, rejected by D-001); (b) P1-2 `SignalKind` 4 sources (Stash/Standing/Vouch/Capital) vs spec `VoiceSource` 5 sources — the AUDIT rationale (Freeholder is eligibility, Guild is council tier, Capital is committed-capital per vision §9.1) and the decision to KEEP the 4-source shape; (c) the Bearers `ValidateGenesis` no-op is correct per spec (P2, no action). Cross-reference AUDIT §193 P1-1/P1-2/P2. | The ARCHITECTURE.md section exists and names both P1 divergences + the P2 no-action; a reader can find why Proposal/VoteOption are absent and why SignalKind is 4-not-5 without re-reading AUDIT. | — |
|
||||
| P2-02-01 | REQ-031 | backend-engineer | `x/council/types/types_test.go` | Add `TestSignalKindShapeIntentional` — an intent-assertion test that documents and guards the 4-source `SignalKind` shape. Asserts: `SignalKindCount == 4`; `AllSignalKinds()` returns exactly [Stash, Standing, Vouch, Capital]; a doc-comment block above the test explaining the AUDIT §193 P1-2 rationale (why Freeholder and Guild are NOT signal kinds, why Capital IS) so a future agent does not "fix" the divergence by silently changing the locked const. This is a regression GUARD, not a shape change — the existing `TestSignalKindCountLockedConst` already locks the count; this test adds the INTENT documentation. | `go test ./x/council/types/...` passes; the new test fails if `SignalKindCount` changes from 4; the test's doc comment explains the rationale. | — |
|
||||
|
||||
### Wave 2 — Verification (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-99-01 | REQ-031 | lead-developer | — (verification only) | Run `go test ./...`; confirm ARCHITECTURE.md subsection present; confirm no production `.go` file was modified in P2 (only a `_test.go` file + a `.ciagent/` doc). | `go test ./...` exits 0; `git diff --name-only <p2-base> HEAD -- 'x/**/*.go'` returns only `x/council/types/types_test.go` (no production files). | P2-01-01, P2-02-01 |
|
||||
|
||||
### P2 Must-Haves (verification gate before SHIP)
|
||||
|
||||
- [ ] ARCHITECTURE.md has the Council lifecycle divergence subsection.
|
||||
- [ ] `TestSignalKindShapeIntentional` exists and passes.
|
||||
- [ ] `SignalKindCount` unchanged (still 4).
|
||||
- [ ] No production `.go` files modified in P2 (test + docs only).
|
||||
- [ ] No `feat:` commits in P2 (D-001 filter).
|
||||
|
||||
---
|
||||
|
||||
## Phase P3 — Docs Build CI
|
||||
|
||||
- **Slug**: `docs-build-ci`
|
||||
- **Branch**: `oy/phase/03-docs-build-ci`
|
||||
- **REQs covered**: REQ-032
|
||||
- **Tag**: `v0.3.3`
|
||||
- **Goal**: Ship a Gitea Actions workflow that runs the lexicon firewall (`go test ./...`) + builds the docs site (`mkdocs build`) on every push, uploading `site/` as a CI artifact. Full Gitea Pages publishing deferred (no hosting target configured, D-051). The workflow file is `chore` (CI config), not `feat:`.
|
||||
|
||||
### Wave 1 — CI workflow (single task; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-01-01 | REQ-032 | lead-developer | `.gitea/workflows/docs-build.yml` (NEW) | Gitea Actions workflow (YAML). Triggers: on push (all branches). Jobs: (1) `go-test` — setup Go 1.22, `go test ./...` (runs lexicon firewall + all x/* tests); (2) `docs-build` — setup Python, `pip install mkdocs mkdocs-material`, `mkdocs build` (produces `site/`), upload `site/` as an artifact. **G-016 binding**: the `docs-build` job MUST depend on `go-test` (serial: `needs: go-test`), so a lexicon-firewall failure blocks the docs build (no false-green docs build from a repo with a lexicon violation). `go.mod` is NOT modified (Python deps are isolated to the `docs-build` job). Doc comment in the YAML references D-046 (forward-reference), D-051 (no Pages publish), and G-016 (firewall-gates-docs-build). | YAML parses; `go test ./...` command matches the local green baseline; `mkdocs build` command matches the README's local build invocation; `docs-build` job has `needs: go-test`; no `go.mod` change. | — |
|
||||
|
||||
### Wave 2 — Verification (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-99-01 | REQ-032 | lead-developer | — (verification only) | Validate the workflow YAML parses; confirm `go test ./...` still green locally (the workflow does not change Go source); confirm `mkdocs build` succeeds locally (mkdocs installed); confirm `go.mod` unchanged. | YAML parse OK; `go test ./...` exits 0; `mkdocs build` produces `site/`; `go.mod` diff empty. | P3-01-01 |
|
||||
|
||||
### P3 Must-Haves (verification gate before SHIP)
|
||||
|
||||
- [ ] `.gitea/workflows/docs-build.yml` exists and parses.
|
||||
- [ ] Workflow runs `go test ./...` and `mkdocs build`.
|
||||
- [ ] `go.mod` unchanged.
|
||||
- [ ] No `feat:` commits in P3 (D-001 filter; the workflow is `chore`).
|
||||
|
||||
---
|
||||
|
||||
## Phase P4 — Final Review + Audit + Milestone Ship
|
||||
|
||||
- **Slug**: `final-review-ship`
|
||||
- **Branch**: `oy/phase/04-final-review-ship`
|
||||
- **REQs covered**: all v0.4 REQs (REQ-029..REQ-032) — final coverage accounting
|
||||
- **Tag**: `v0.3.4` (IS the v0.4 milestone release; D-008)
|
||||
- **Goal**: Multi-persona review across P1..P3, audit (reconstruction test + NFR purity gate), milestone ship (merge to main, tag v0.3.4, release, delete all milestone branches).
|
||||
|
||||
### Wave 1 — Review + Audit (parallel; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-01-01 | — | lead-developer (review) | `.ciagent/oy/REVIEW.md` (NEW for v0.4) | Multi-persona code review across P1..P3. Adversarial probes: (1) does the shared helper actually dedupe (grep for old table); (2) does the cross-const test fail closed on drift; (3) does the regression guard lock the 4-signal shape; (4) does the CI workflow YAML parse. Auto-apply P0 fixes; flag P1+ for post-hoc. | REVIEW.md written; P0 issues (if any) fixed in P4; P1+ flagged. | P1, P2, P3 |
|
||||
| P4-02-01 | — | ci-security-auditor (audit) | `.ciagent/oy/AUDIT.md` (v0.4 section) | Audit: (1) reconstruction test (git log ↔ `.ciagent/` files for v0.4); (2) file/branch/commit discipline; (3) **NFR purity gate** — `git log --grep "^feat:" $(git rev-list --tags=v0.3.0..v0.3.4)` returns zero (or the equivalent milestone-range grep); (4) coverage did not decrease on any modified package. | AUDIT.md v0.4 section written; NFR purity gate GREEN (zero feat: commits); reconstruction test passes. | P1, P2, P3 |
|
||||
|
||||
### Wave 2 — Ship (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-03-01 | REQ-029..REQ-032 | lead-developer (ship) | `.ciagent/oy/REQUIREMENTS.md`, `.ciagent/oy/ROADMAP.md`, `.ciagent/CHECKPOINT.json` | Milestone ship via `ciagent ship 4` (or ShipWorkflow fallback). Merge `oy/phase/04` → `oy/milestone/v0.4-refinement` → `main`. Tag `v0.3.4`. Create release (best-effort via `resolveSecret(GITEA_TOKEN)`). Delete all milestone branches (local + remote). Update REQUIREMENTS.md (v0.4 REQs → Complete) + ROADMAP.md (v0.4 → COMPLETE). Commit `docs(milestone): complete v0.4`. Clear CHECKPOINT. | Tag `v0.3.4` created; milestone branch merged to main; release created (or local-only fallback); all `oy/phase/*` and `oy/milestone/v0.4-refinement` branches deleted; REQUIREMENTS.md + ROADMAP.md updated; checkpoint cleared. | P4-01-01, P4-02-01 |
|
||||
|
||||
### P4 Must-Haves (verification gate before milestone complete)
|
||||
|
||||
- [ ] REVIEW.md v0.4 section written; P0 fixes applied.
|
||||
- [ ] AUDIT.md v0.4 section written; reconstruction test passes.
|
||||
- [ ] **NFR purity gate GREEN**: zero `feat:` commits in the v0.4 milestone range.
|
||||
- [ ] Tag `v0.3.4` created (= milestone release).
|
||||
- [ ] Milestone branch merged to `main`.
|
||||
- [ ] All milestone branches deleted (local + remote).
|
||||
- [ ] REQUIREMENTS.md + ROADMAP.md mark v0.4 COMPLETE.
|
||||
- [ ] CHECKPOINT.json cleared (milestone complete).
|
||||
|
||||
---
|
||||
|
||||
## Coverage Targets (D-033) — v0.4
|
||||
|
||||
v0.4 does not lower the v0.3 coverage floor (93.3% on the lowest package). The modified packages must not decrease:
|
||||
- `lexicon` (if coverage applies — it's a helper package): maintain or improve.
|
||||
- `lexicon_meta` / `lexicon_meta_docs`: maintain 100% (test-only packages).
|
||||
- `x/hub/types`: the new `cross_const_test.go` ADDS coverage; do not decrease.
|
||||
- `x/council/types`: the new intent test ADDS coverage; do not decrease.
|
||||
|
||||
No new packages are created, so no new 80% floor is set.
|
||||
|
||||
## Task Count Summary — v0.4
|
||||
|
||||
| Phase | Tasks | Personas | Tag |
|
||||
|-------|-------|----------|-----|
|
||||
| P0 | (pre-execution, this plan) | lead-developer | v0.3.0 |
|
||||
| P1 | 4 (3 code + 1 verify) | backend-engineer, lead-developer | v0.3.1 |
|
||||
| P2 | 3 (2 deliver + 1 verify) | backend-engineer, lead-developer | v0.3.2 |
|
||||
| P3 | 2 (1 deliver + 1 verify) | lead-developer | v0.3.3 |
|
||||
| P4 | 3 (review + audit + ship) | lead-developer, ci-security-auditor | v0.3.4 |
|
||||
| **Total** | **12 tasks** | | |
|
||||
|
||||
## Per-Phase REQ Coverage — v0.4
|
||||
|
||||
| Phase | REQs covered | Status after phase |
|
||||
|-------|--------------|--------------------|
|
||||
| P1 | REQ-029, REQ-030 | Complete (shipped as v0.3.1) |
|
||||
| P2 | REQ-031 | Complete (shipped as v0.3.2) |
|
||||
| P3 | REQ-032 | Complete (shipped as v0.3.3) |
|
||||
| P4 | (all v0.4 REQs final accounting) | Complete (shipped as v0.3.4) |
|
||||
|
||||
## Cross-Phase Blockers (hard) — v0.4
|
||||
|
||||
None. P1, P2, P3 are independent vertical slices. P4 depends on P1+P2+P3 completion only (soft ordering for shippable slices).
|
||||
|
||||
## v0.4 Decisions Applied (D-047..D-053)
|
||||
|
||||
| Decision | Application |
|
||||
|----------|-------------|
|
||||
| D-047 | v0.4 NFR type, tags v0.3.x, zero feat: phases | Milestone summary |
|
||||
| D-048 | REQ-029 lexicon shared helper | P1-01-01, P1-01-02 |
|
||||
| D-049 | REQ-030 cross-const test (G-003 test-exempt import) | P1-02-01 |
|
||||
| D-050 | REQ-031 = document only, no feat: enum additions | P2-01-01, P2-02-01 |
|
||||
| D-051 | REQ-032 docs CI = .gitea/workflows build+artifact, no Pages | P3-01-01 |
|
||||
| D-052 | Phase ordering P1→P2→P3→P4 | Phase ordering |
|
||||
| D-053 | No IDEATE in v0.4 (no --ideate flag) | (no IDEATE stage run) |
|
||||
|
||||
---
|
||||
|
||||
## MVP/UX Check (REQ-MVP-UX-001) — v0.4 NFR Milestone
|
||||
|
||||
> Auto-generated at full autonomy per run.md §MVP/UX CHECK. The three required
|
||||
> sections below name at least one user-facing surface, a happy path written
|
||||
> BEFORE execute, and explicit UX acceptance criteria. v0.4 is an NFR milestone,
|
||||
> so "user-facing surface" is developer-facing (the lexicon firewall, the
|
||||
> cross-const test, the CI workflow) and the docs site build (end-user-facing
|
||||
> via the published docs artifact).
|
||||
|
||||
### User-Facing Surface
|
||||
|
||||
1. **Lexicon firewall (developer-facing)**: `go test ./lexicon_meta... ./lexicon_meta_docs/...` — the green test output is the surface a developer sees on every `go test ./...` run. After REQ-029, both meta-tests consume `lexicon.SyntheticBannedStrings()`; a future banned-term addition updates both firewalls from one place.
|
||||
2. **Cross-const drift test (developer-facing)**: `go test ./x/hub/types/...` — `TestConstsAreMissionLocked800And0` is the surface a developer sees if the hub↔bond mission-locked consts ever drift.
|
||||
3. **Docs build CI (end-user-facing via artifact)**: `.gitea/workflows/docs-build.yml` produces a `site/` artifact on every push. A maintainer downloads the artifact to preview the docs site without running `mkdocs build` locally.
|
||||
4. **ARCHITECTURE.md divergence section (developer-facing)**: a contributor reading `.ciagent/oy/ARCHITECTURE.md` finds the "Council Voice/Council Interface — Lifecycle Type Divergence Decisions (v0.4)" subsection explaining why `SignalKind` is 4-not-5 and why `Proposal`/`VoteOption` are absent.
|
||||
|
||||
### Happy Path
|
||||
|
||||
**Scenario: a contributor adds an 11th banned term to the lexicon firewall.**
|
||||
|
||||
1. The contributor edits `lexicon/lexicon.go` to add a new fragment pair to `fragments` (e.g., a new banned term).
|
||||
2. `lexicon.BannedTerms()` now returns 11 strings.
|
||||
3. `lexicon.SyntheticBannedStrings()` (REQ-029) is the single source — the contributor does NOT need to update two meta-test tables (the v0.3 drift risk).
|
||||
4. `go test ./lexicon_meta... ./lexicon_meta_docs/...` — both meta-tests consume the helper; the `len(terms) == 10` assertion in BOTH meta-tests now fails (expecting 11), alerting the contributor to update the count assertion in both files.
|
||||
5. The contributor updates the `len(terms) == 11` assertion in both meta-tests (the G-014 minimum — both already derive count from `BannedTerms()`).
|
||||
6. Both meta-tests pass; the new banned term is now enforced in BOTH the `x/**/*.go` firewall AND the `docs/**/*.md` firewall from one source change.
|
||||
7. The contributor pushes; `.gitea/workflows/docs-build.yml` (REQ-032) runs `go-test` (green) → `docs-build` (green, G-016 gating) → `site/` artifact uploaded.
|
||||
|
||||
**Scenario: a contributor accidentally changes `x/bond.CouponCapBps` from 800 to 900 without updating `x/hub.LendingCouponCapBps`.**
|
||||
|
||||
1. `go test ./x/hub/types/...` — `TestLendingCouponCapMatchesBondCap` (REQ-030) fails: `hub.LendingCouponCapBps (800) != bond.CouponCapBps (900)`.
|
||||
2. `TestConstsAreMissionLocked800And0` (G-015) ALSO fails: `bond.CouponCapBps (900) != 800`.
|
||||
3. The contributor sees two failures pointing at the same root cause; the mission-locked 8% cap is defended by the cross-const test. The v0.3 A-304 drift risk is closed.
|
||||
|
||||
### UX Acceptance Criteria
|
||||
|
||||
The v0.4 deliverable MUST meet these explicit criteria (verified in P4 audit):
|
||||
|
||||
1. **REQ-029**: `lexicon.SyntheticBannedStrings()` exists; both `lexicon_meta_test.go` and `lexicon_meta_docs/lexicon_meta_docs_test.go` consume it; the duplicated 10-string table is gone (grep for the old literal sentence fragments returns zero matches in meta-test files).
|
||||
2. **REQ-030**: `x/hub/types/cross_const_test.go` exists; `TestLendingCouponCapMatchesBondCap`, `TestLendingCouponFloorMatchesBondFloor`, and `TestConstsAreMissionLocked800And0` all pass (G-015); no production `.go` file in `x/hub/types/` imports `x/bond/types` (G-003 production firewall intact).
|
||||
3. **REQ-031**: ARCHITECTURE.md has the Council lifecycle divergence subsection; `TestSignalKindShapeIntentional` in `x/council/types/types_test.go` passes and documents the 4-signal rationale; `SignalKindCount` unchanged (still 4); no production `.go` files modified in P2.
|
||||
4. **REQ-032**: `.gitea/workflows/docs-build.yml` parses; runs `go test ./...` then `mkdocs build` (G-016: `docs-build` needs `go-test`); `go.mod` unchanged.
|
||||
5. **NFR purity gate**: zero `feat:` commits in the v0.4 milestone range (P4 audit enforces).
|
||||
6. **No regression**: `go test ./...` green; v0.3 coverage floor (93.3%) not reduced on any modified package.
|
||||
+103
-23
@@ -61,33 +61,75 @@ OpenYield (OY) is a durable, anti-greed, jurisdiction-light financial layer —
|
||||
- D-009: Rebased history to fix v1.0 → v0.1 in ---ci--- blocks
|
||||
|
||||
## Milestone
|
||||
v0.2 — The Mesh (active milestone; feature type; tags run on the v0.1.x patch line)
|
||||
v0.4 — Refinement (active milestone; NFR type; tags run on the v0.3.x patch line)
|
||||
|
||||
### v0.2 Scope (The Mesh — ROADMAP Phase 2)
|
||||
Target: $1B annual volume, 4 service categories. Implements the pending Mesh-era requirements:
|
||||
- **REQ-009** Satellite chains (Layer 2) — wrapped Bread, Pass-Act propagation on Polygon/Base/Arbitrum/Optimism/Solana [§7]
|
||||
- **REQ-011** Three Councils (Mesh, Guild, Stand) with Mission Lock — multi-source Voice [§19]
|
||||
- **REQ-015** Window primitive — scope, duration, rate-limit, audit log, revoke [§10]
|
||||
- **REQ-016** Nine Stand types (Household, Crew, Entity, Co-op, Circle, Trust, Foundation, Confederation, Shadow) [§11]
|
||||
- **REQ-017** Guilds with Hand-Passes at 0% protocol fee [§12]
|
||||
- **REQ-018** Four-tier Partner Spectrum (Op, Master Op, Pier, Anchor) [§13]
|
||||
- **REQ-020** Six Pacts (Pause, Ground, Stance, Cover, Stand Registry, Hub API) [§16]
|
||||
- **REQ-021** Mesh Bond Market with 8% upper coupon cap, 0% floor [§17]
|
||||
- Bearers expansion: OY-LR + Beacon v1
|
||||
- Forex Engine v1
|
||||
### v0.4 Scope (Refinement-only NFR — v0.3 post-hoc forward-references)
|
||||
|
||||
v0.4 is a refinement-only NFR milestone: zero `feat:` phases. It lands the
|
||||
durability fixes v0.3 flagged but did not block on, sourced from REVIEW.md,
|
||||
AUDIT.md §193, and GRILL.md G-014. Live-runtime promotions of the v0.3 Bearers
|
||||
skeletons are out of scope (deferred to v0.5+).
|
||||
|
||||
- **REQ-029** Lexicon firewall shared helper (`lexicon.SyntheticBannedStrings()`) — dedupe the synthetic self-test table between `lexicon_meta_test.go` and `lexicon_meta_docs_test.go`. Both meta-tests derive count + strings from the single `lexicon` package source, so a future banned-term addition updates both firewalls from one place. (GRILL G-014)
|
||||
- **REQ-030** Cross-package const-equality test — `x/hub.LendingCouponCapBps == x/bond.CouponCapBps` (and Floor). Test-only import (G-003 exempt). Catches silent mission-lock drift between hub LOCAL consts and bond D-028 consts. (REVIEW.md P2 / A-304)
|
||||
- **REQ-031** x/* lifecycle type shape-divergence review + alignment fixes — audit non-must-have lifecycle types across modules flagged by AUDIT §193; align where divergent without behavioral change. (AUDIT.md §193)
|
||||
- **REQ-032** Docs build CI — Gitea Actions workflow running `go test ./...` (lexicon firewall) + `mkdocs build` on every push; upload `site/` as a CI artifact. Full Gitea Pages publishing deferred if no hosting target configured. (D-046)
|
||||
|
||||
### Milestone Type
|
||||
Feature (at least one `feat` phase). Phase 0 → `v0.1.0`; execution phases `v0.1.1..v0.1.N`; final phase patch IS the milestone release. No separate minor tag.
|
||||
NFR (all phases are refactor/test/quality/chore). Phase 0 → `v0.3.0`; execution phases `v0.3.1..v0.3.3`; final phase patch `v0.3.4` IS the milestone release. No separate minor tag. The final-phase audit enforces the NFR purity gate (zero `feat:` commits).
|
||||
|
||||
### Out of Scope (v0.2)
|
||||
- Cross-chain exit / DEX integration (Phase 3 / v0.3)
|
||||
- OY-SAT, OY-QR bearers (Phase 3)
|
||||
- Full Hub API B2B suite (Phase 3)
|
||||
- Yield Token, Travel + 11 service categories (Phase 4)
|
||||
- Full Mesh Bond market depth (Phase 3+; v0.2 ships first Mesh Bonds only)
|
||||
### Out of Scope (v0.4)
|
||||
- Live-runtime promotions: Exit/DEX, OY-SAT/OY-QR hardware, Hub API B2B, bond matching, L2 IBC rollout, Anchors onboarding (all `feat:`, deferred to v0.5+)
|
||||
- i18n / MkDocs internationalization (`feat:`, rejected by D-001 filter)
|
||||
- Yield Token, Travel + 11 service categories (ROADMAP Phase 4)
|
||||
- Cover Pool seniority mechanics (still deferred per PROJECT.md Q7)
|
||||
|
||||
### Prior Milestone
|
||||
v0.1 — OpenYield Foundation Init (COMPLETE; pre-MVP foundation skeleton; released as v0.0.9 per run.md patch-line model)
|
||||
### Prior Milestones
|
||||
- v0.1 — OpenYield Foundation Init (COMPLETE; pre-MVP foundation skeleton; released as v0.0.9)
|
||||
- v0.2 — The Mesh (COMPLETE; skeleton + tests; released as v0.1.5)
|
||||
- v0.3 — Bearers & Documentation (COMPLETE; feature; released as v0.2.6)
|
||||
|
||||
## Prior Milestone
|
||||
v0.3 — Bearers & Documentation (complete; feature type; tags ran on the v0.2.x patch line)
|
||||
|
||||
### v0.3 Scope (Bearers skeleton + Docs site — ROADMAP Phase 3 partial, plus a docs deliverable)
|
||||
|
||||
This milestone bundles two parallel work-streams under one feature milestone:
|
||||
|
||||
**(A) Bearers skeleton (D-020 pattern continued)** — implements the v0.1 PROJECT.md
|
||||
out-of-scope items now promoted to v0.3 (ROADMAP Phase 3 "The Bearers" subset),
|
||||
as skeleton + tests (Go types + keeper stubs + invariant tests; no live chain):
|
||||
|
||||
- **REQ-010** Exit layer (Layer 3) — DEX swaps, bridges, off-mesh services (§7). Promoted from Skeleton to a fuller skeleton: `x/exit` (exit-route types) + `x/bridge` (L2↔L1 bridge types). Live runtime deferred to v0.4.
|
||||
- **Bearers expansion** — OY-SAT (satellite) + OY-QR bearer transport types, extending `x/bearers` (D-029 pattern). Hardware integration deferred.
|
||||
- **Anchors** — first institutional Partner tier (`x/partner` extension: Anchor credential types). REQ-018 promoted from Skeleton → fuller skeleton.
|
||||
- **Hub API** — B2B backbone: custody, lending primitive, compliance types (`x/hub`). Full B2B suite deferred to v0.4.
|
||||
- **Services** — Care / SIM / Vault / Mail service types (`x/services`). Live services deferred.
|
||||
- **Bond market depth** — Growth Bonds + secondary-market types, extending `x/bond` (REQ-021 promoted from Skeleton → fuller skeleton). Full market depth deferred.
|
||||
|
||||
**(B) Documentation deliverable** — README.md + docs site in `docs/` for nomads and freeholders:
|
||||
|
||||
- Repo-root `README.md` (lexicon-clean project overview).
|
||||
- MkDocs Material site (`mkdocs.yml` + `docs/`), organized by audience:
|
||||
- `docs/nomads/` — Reach path, Stash, bearers, Maps/Pay, six Pacts, standing basics.
|
||||
- `docs/freeholders/` — Four Freeholder signals, Bayesian Standing, Stands/Guilds, Councils/Voice, Bonds, Partner spectrum.
|
||||
- `docs/shared/` — Six Principles, Bread Scale, Storage pools, Watchers/Mirror, Lexicon glossary, Vision overview.
|
||||
- `docs/reference/` — architecture index, component map.
|
||||
- **REQ-012 firewall extension** — extend the lexicon meta-test to scan `README.md` + `docs/**/*.md` (new sibling `lexicon_meta_docs_test.go`), so the docs site is durably lexicon-clean. This is a `feat/test` phase.
|
||||
|
||||
### Milestone Type
|
||||
Feature (Bearers phases are feat; docs phases are docs/test). Phase 0 → `v0.2.0`; execution phases `v0.2.1..v0.2.5`; final phase patch `v0.2.6` IS the milestone release. No separate minor tag.
|
||||
|
||||
### Out of Scope (v0.3)
|
||||
- Live chain launch / real IBC channels / real bearer transports (D-020 pattern continues)
|
||||
- DEX integration runtime, full Hub API B2B suite runtime (types only in v0.3)
|
||||
- Yield Token, Travel + 11 service categories (ROADMAP Phase 4)
|
||||
- i18n / versioning in MkDocs (single-language v0.3)
|
||||
- Cover Pool seniority mechanics (still deferred per PROJECT.md Q7)
|
||||
|
||||
### Prior Milestones
|
||||
- v0.1 — OpenYield Foundation Init (COMPLETE; pre-MVP foundation skeleton; released as v0.0.9)
|
||||
- v0.2 — The Mesh (COMPLETE; skeleton + tests; released as v0.1.5)
|
||||
|
||||
## Clarification Decisions (Phase 0 — CLARIFY, autonomy=full)
|
||||
|
||||
@@ -108,4 +150,42 @@ Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → acce
|
||||
| D-030 | **Forex Engine v1**: Forex pair type + rate-oracle interface + stub keeper; no live oracle integration | Live oracle integration depends on external partners (Piers), Phase 3. | 0.80 | [live oracle integration] |
|
||||
| D-031 | **Phase ordering** follows ARCHITECTURE.md blocker chain: P1 Orgs+Window foundation → P2 Pacts+Partners → P3 Councils+Forex → P4 Bonds+Bearers+L2. The final phase (P5) is review/ship. | Respects dependency graph; vertical slices keep each phase independently shippable. | 0.80 | [different wave ordering] |
|
||||
| D-032 | **Lexicon** enforced project-wide; all new modules must pass the lexicon assertion test (no banned terms). Non-negotiable. **Note (G-002)**: lexicon assertion tests are NEW in v0.2 — v0.1 is lexicon-clean in practice but has NO lexicon test firewall. v0.2 introduces the firewall (scaffolded in P1 per G-004, extended in P5). | REQ-012 is `All` phases. | 1.00 | [—] |
|
||||
| D-033 | **Test coverage target**: ≥80% on new keeper/type packages. v0.1 baseline = **53 tests across 11 test files** (corrected per G-001; not 48). Add lexicon assertion to each new module's test file. | Consistency with v0.1 quality bar (53 tests verified); lexicon drift is the highest-severity regression. | 0.85 | [lower coverage bar] |
|
||||
| D-033 | **Test coverage target**: ≥80% on new keeper/type packages. v0.1 baseline = **53 tests across 11 test files** (corrected per G-001; not 48). Add lexicon assertion to each new module's test file. | Consistency with v0.1 quality bar (53 tests verified); lexicon drift is the highest-severity regression. | 0.85 | [lower coverage bar] |
|
||||
|
||||
### v0.3 Clarification Decisions (Phase 0 — CLARIFY, autonomy=full)
|
||||
|
||||
Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → accept defaults, log decisions).
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Alternatives |
|
||||
|----|----------|-----------|------------|--------------|
|
||||
| D-034 | **v0.3 milestone bundles Bearers skeleton (D-020 pattern) + docs deliverable** under one feature milestone, rather than two separate NFR+feature milestones. Bearers phases are `feat`; docs phases are `docs`/`test`. Tags run on `v0.2.x`. | User request (--ideate) is docs-only but ROADMAP Phase 3 (Bearers) is the next queued feature work; bundling keeps the milestone cadence and avoids an NFR-only milestone that would not advance the protocol. Feature type because Bearers phases are feat. | 0.82 | [separate v0.3 docs NFR + v0.4 Bearers feature; or docs as patches on v0.2 line] |
|
||||
| D-035 | **Bearers skeleton continues the D-020 skeleton+tests pattern** (Go types + keeper stubs + invariant tests; no live chain, no real IBC channels, no real bearer transports). Live runtime for any Bearers component deferred to v0.4+. | v0.1/v0.2 both shipped skeleton-first; v0.3 stays consistent. Live runtime needs Watchers + Root Basket backing (Year 3 target). | 0.85 | [fuller keeper implementations in v0.3] |
|
||||
| D-036 | **REQ-010 Exit layer**: skeleton = `x/exit` (ExitRoute, DEXSwap types) + `x/bridge` (L2↔L1 bridge types, BridgeStatus enum). No live DEX integration. REQ-010 promoted from v0.1 Skeleton → v0.3 fuller skeleton (two packages instead of one). | Exit runtime needs Anchor partners + L2 bridges; v0.3 lands the typed shape. | 0.80 | [single x/exit package, defer all exit to v0.4] |
|
||||
| D-037 | **REQ-022 Bearers OY-SAT + OY-QR**: extend `x/bearers/types` with `OYSAT` + `OYQR` bearer transport types (BearerTransport interface already in v0.2). No hardware/RF runtime. D-029 pattern continued. | Hardware integration is not a software deliverable; v0.3 completes the 6-bearer type set (v0.2 had 4: Internet/OY-BLE/OY-WiFi-Direct + OY-LR/Beacon). | 0.82 | [real bearer runtime, defer OY-SAT/OY-QR to v0.4] |
|
||||
| D-038 | **REQ-023 Anchors**: extend `x/partner/types` with `Anchor` tier credential types (REQ-018 had the 4-tier enum; v0.3 adds Anchor-specific credential fields). No live institutional onboarding. | Anchors need Watchers + Hub API backing; v0.3 lands the credential shape. | 0.78 | [separate x/anchor module, defer Anchors to v0.4] |
|
||||
| D-039 | **REQ-024 Hub API**: new `x/hub` module — custody, lending-primitive, compliance type stubs (HubService enum + per-service structs). No live B2B runtime. Full Hub API B2B suite deferred to v0.4. | Hub API needs Anchors + Watchers; v0.3 lands the typed scaffold. | 0.80 | [full Hub API runtime in v0.3] |
|
||||
| D-040 | **REQ-025 Services**: new `x/services` module — Care/SIM/Vault/Mail service type stubs (ServiceKind enum + per-service structs). No live services. | Services are operational, not protocol-level; v0.3 lands the typed shape. | 0.78 | [full services runtime in v0.3] |
|
||||
| D-041 | **REQ-026 Bond market depth**: extend `x/bond/types` with GrowthBond type + secondary-market order types. 8% cap / 0% floor consts (D-028) unchanged. Full secondary-market matching deferred to v0.4. | v0.2 shipped first issuance; v0.3 adds depth types without a live matching engine. | 0.80 | [full bond market in v0.3] |
|
||||
| D-042 | **Docs deliverable (REQ-027)**: repo-root `README.md` + MkDocs Material site (`mkdocs.yml` + `docs/`). `mkdocs.yml` at repo root; `docs/` organized by audience: `docs/nomads/`, `docs/freeholders/`, `docs/shared/`, `docs/reference/`. Build-only Python dep (mkdocs + material); `go.mod` stays zero-dep. | User chose MkDocs Material + audience organization. MkDocs is Markdown-native, lightest toolchain; build-only dep does not affect Go modules (G-006). | 0.85 | [Hugo, Docusaurus, plain Markdown no generator] |
|
||||
| D-043 | **REQ-028 lexicon firewall extension**: new sibling test `lexicon_meta_docs_test.go` (package `lexicon_meta_docs`) scanning `README.md` + `docs/**/*.md` for the 10 banned terms, using the same `lexicon.FindBannedTerm` + word-boundary regex. Self-exclusion + fragment pattern preserved. `.ciagent/` files are NOT scanned (they are firewall meta-files, not user-facing docs). | REQ-012 is `All` phases and docs are user-facing; the firewall must cover docs to be durable. Extending the existing meta-test (not modifying it) preserves v0.2 coverage. | 0.88 | [single combined meta-test scanning both x/ and docs/] |
|
||||
| D-044 | **Phase ordering**: P1 docs foundation + firewall extension → P2 nomads docs → P3 freeholders docs → P4 Bearers skeleton I (exit/bridge/bearers/partner) → P5 Bearers skeleton II (hub/services/bond) → P6 final review/ship. Firewall lands in P1 BEFORE content (P2/P3) so docs are checked as authored. | Firewall-first ensures docs content is lexicon-clean by construction, not by retrofit. Bearers split across P4/P5 keeps each phase independently shippable (vertical slices). | 0.82 | [Bearers first then docs, or all docs in one phase] |
|
||||
| D-045 | **Docs depth per audience**: each audience section (nomads, freeholders) gets 5-8 Markdown pages covering its core REQs (nomads: Reach/Stash/bearers/Maps-Pay/Pacts/standing-basics; freeholders: 4 signals/Bayesian Standing/Stands-Guilds/Councils-Voice/Bonds/Partner spectrum). `docs/shared/` gets 5-6 concept pages. `docs/reference/` gets architecture index + component map. Total ~20-25 pages. | Enough depth to be a real docs site, not a placeholder; bounded to keep P1-P3 phases shippable. | 0.80 | [deeper (40+ pages), shallower (10 pages)] |
|
||||
| D-046 | **No docs-site publishing CI in v0.3** — `mkdocs.yml` is buildable locally (`mkdocs serve` / `mkdocs build`); CI publishing to GitHub Pages/Gitea Pages is deferred to v0.4. v0.3 ships the source + a build invocation in the README. | Publishing CI needs deployment secrets + a hosting target; v0.3 lands the content. | 0.82 | [include publishing CI in v0.3] |
|
||||
|
||||
### Ideation outcome (Phase 0 — IDEATE stage, autonomy=full)
|
||||
|
||||
IDEATE stage ratified 8 ideas (IDEATE-01..IDEATE-08) at full autonomy, mapped to REQ-010/REQ-022..REQ-028. Docs deliverable (IDEATE-01/02) is the user's `--ideate` request; Bearers ideas (IDEATE-03..08) are the ROADMAP Phase 3 subset. Three ideation tiers ran (mechanical, backend-enriched, cross-project); mechanical tier found no `lessons:`/`compound:` tags in v0.1/v0.2 history (convention unused) and v0.2 closed clean (9/9 REQs, 303 tests, ≥95.9% coverage). Defaults accepted per full autonomy; traceability recorded in `.ciagent/oy/REQUIREMENTS.md` (IDEATE Traceability section).
|
||||
|
||||
### v0.4 Clarification Decisions (Phase 0 — CLARIFY, autonomy=full)
|
||||
|
||||
Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → accept defaults, log decisions). v0.4 is a refinement-only NFR milestone (no `--ideate` flag this run; scope pre-seeded from v0.3 forward-references). The D-001 refinement-only filter governs scope eligibility.
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Alternatives |
|
||||
|----|----------|-----------|------------|--------------|
|
||||
| D-047 | **v0.4 milestone type = NFR** (all phases refactor/test/quality/chore). Zero `feat:` phases by construction. Tags run on the `v0.3.x` patch line: P0 → `v0.3.0`, P1..P3 → `v0.3.1..v0.3.3`, final phase P4 → `v0.3.4` (milestone release). No separate minor tag. | The candidate work set (REQ-029..REQ-032) is entirely refactor/test/quality/chore. Promoting any Bearers skeleton to live runtime would be `feat:` and is deferred to v0.5+. | 0.90 | [feature milestone promoting v0.3 skeletons to live runtime] |
|
||||
| D-048 | **REQ-029 lexicon shared helper**: add `lexicon.SyntheticBannedStrings() []string` to the `lexicon` package; both `lexicon_meta_test.go` and `lexicon_meta_docs_test.go` consume it instead of duplicating their own synthetic self-test tables. Both already assert `len(terms) == 10` from `lexicon.BannedTerms()` (G-014 minimum met); the helper closes the drift risk fully. | GRILL G-014 binding fix. Single source of truth for synthetic banned strings; a future banned-term addition updates both firewalls from one place. Refactor+test (NFR-eligible). | 0.88 | [cross-reference comment only (G-014 minimum)] |
|
||||
| D-049 | **REQ-030 cross-package const-equality test**: new test file `x/hub/types/cross_const_test.go` (package `types`) that imports `x/bond/types` (test-only, G-003 exempt) and asserts `hub.LendingCouponCapBps == bond.CouponCapBps` and `hub.LendingCouponFloorBps == bond.CouponFloorBps`. Test-only import does not violate G-003 (production-import firewall). | REVIEW.md P2 / A-304. Catches silent mission-lock drift between hub LOCAL consts and bond D-028 consts. Test (NFR-eligible). | 0.85 | [document manual-sync requirement in ARCHITECTURE.md only] |
|
||||
| D-050 | **REQ-031 lifecycle type shape-divergence review scope = DOCUMENT only, no code shape changes**. AUDIT §193 P1-1 (council Proposal/VoteOption absent) and P1-2 (SignalKind 4 vs 5 sources) are `feat:`-class additions (new enum types / locked-const shape changes) and are REJECTED by the D-001 refinement-only filter. v0.4 REQ-031 ships an ARCHITECTURE.md section documenting the divergence decisions (P1-2 defensible per AUDIT code rationale; P1-1 deferred to v0.5+ governance runtime) + a test asserting the current `SignalKindCount==4` locked-const shape is intentional (regression guard, not a shape change). | Adding Proposal/VoteOption enums is `feat:`; changing SignalKind 4→5 is a locked-const change. Both are out-of-scope for an NFR milestone. Documentation + a regression-guard test are NFR-eligible. | 0.82 | [add Proposal/VoteOption enums (feat:, deferred to v0.5+)] |
|
||||
| D-051 | **REQ-032 docs build CI = Gitea Actions workflow** at `.gitea/workflows/docs-build.yml` running `go test ./...` (lexicon firewall) + `mkdocs build` on every push; upload `site/` as a CI artifact. Full Gitea Pages publishing is deferred (no hosting target configured in v0.4). The workflow file itself is a `chore` (config, not feature). | D-046 forward-reference. `.github/workflows/` does not exist; Gitea Actions uses `.gitea/workflows/`. Build+artifact CI is `chore` (NFR-eligible); full Pages publish needs a hosting target (deferred). | 0.80 | [include full Gitea Pages publish (needs hosting target + secrets)] |
|
||||
| D-052 | **Phase ordering** (provisional, planner finalizes): P1 lexicon hardening (REQ-029 + REQ-030 — same `lexicon`/test territory, vertical slice) → P2 lifecycle divergence documentation + regression guard (REQ-031) → P3 docs build CI (REQ-032) → P4 final review + audit + milestone ship. Each phase independently shippable; P1 lands the firewall durability fixes first (highest-severity regression risk). | P1 bundles the two lexicon/const firewall fixes (same territory); P2 is documentation+test; P3 is CI config. Vertical slices. | 0.80 | [different wave ordering] |
|
||||
| D-053 | **No IDEATE stage in v0.4** (no `--ideate` flag this run). The NFR scope was pre-seeded from v0.3 forward-references and ratified at CLARIFY. If `--ideate` is passed on a later v0.4 run, the D-001 refinement-only filter applies. | run.md §IDEATE is conditional on `--ideate`. This invocation has no `--ideate`. | 1.00 | [run IDEATE anyway] |
|
||||
@@ -26,6 +26,74 @@
|
||||
| Bearers OY-LR + Beacon | (vision §14) | §14 | Medium | Skeleton | v0.2/P4 |
|
||||
| Forex Engine v1 | (vision §13) | §13 | Medium | Skeleton | v0.2/P3 |
|
||||
|
||||
## v0.3 Milestone Requirements (Bearers & Documentation)
|
||||
|
||||
| ID | Requirement | Vision § | Priority | Status | Phase |
|
||||
|----|-------------|----------|----------|--------|-------|
|
||||
| REQ-010 | Exit layer (Layer 3) — DEX swaps, bridges, off-mesh services | §7 | Medium | Skeleton | v0.3/P4 |
|
||||
| REQ-022 | Bearers expansion: OY-SAT + OY-QR bearer transports | §14 | Medium | Skeleton | v0.3/P4 |
|
||||
| REQ-023 | Anchors — first institutional Partner tier | §13 | Medium | Skeleton | v0.3/P4 |
|
||||
| REQ-024 | Hub API — B2B backbone: custody, lending primitive, compliance | §13 | Medium | Skeleton | v0.3/P5 |
|
||||
| REQ-025 | Services — Care / SIM / Vault / Mail | §13 | Medium | Skeleton | v0.3/P5 |
|
||||
| REQ-026 | Bond market depth — Growth Bonds + secondary market | §17 | Medium | Skeleton | v0.3/P5 |
|
||||
| REQ-027 | README.md + docs site in docs/ for nomads and freeholders | (vision §8) | High | Complete | v0.3/P1-P3 |
|
||||
| REQ-028 | Extend REQ-012 lexicon firewall to scan docs/ + README.md | §3 | High | Complete | v0.3/P1 |
|
||||
|
||||
> REQ-022 through REQ-028 are NEW in v0.3 (ratified during Phase 0 IDEATE as
|
||||
> IDEATE-01..IDEATE-07, then assigned final REQ-IDs). REQ-010 is promoted from
|
||||
> v0.1 Skeleton to a fuller v0.3 skeleton.
|
||||
|
||||
## v0.4 Milestone Requirements (Refinement — NFR)
|
||||
|
||||
v0.4 is a refinement-only NFR milestone: zero `feat:` phases. Scope sourced
|
||||
from v0.3 forward-references (REVIEW.md, AUDIT.md §193, GRILL.md G-014).
|
||||
Live-runtime promotions are out of scope (deferred to v0.5+). The D-001
|
||||
refinement-only filter applies to any IDEATE stage.
|
||||
|
||||
| ID | Requirement | Source | Class | Priority | Status | Phase |
|
||||
|----|-------------|--------|-------|----------|--------|-------|
|
||||
| REQ-029 | Lexicon firewall: shared `lexicon.SyntheticBannedStrings()` helper — dedupe the synthetic self-test table between `lexicon_meta_test.go` and `lexicon_meta_docs_test.go`; both meta-tests derive count + strings from the single source so a future banned-term addition updates both firewalls from one place | GRILL G-014 | refactor/test | High | Pending | v0.4/P1 |
|
||||
| REQ-030 | Cross-package const-equality test: `x/hub.LendingCouponCapBps == x/bond.CouponCapBps` (and Floor) — test-only import (G-003 exempt), catches silent mission-lock drift between hub LOCAL consts and bond D-028 consts | REVIEW.md P2 / A-304 | test | High | Pending | v0.4/P1 |
|
||||
| REQ-031 | x/* lifecycle type shape-divergence review + alignment fixes — audit non-must-have lifecycle types across modules flagged by AUDIT §193; align shapes where divergent (no behavioral change) | AUDIT.md §193 | refactor/quality | Medium | Pending | v0.4/P2 |
|
||||
| REQ-032 | Docs build CI — Gitea Actions workflow that runs `go test ./...` (lexicon firewall) + `mkdocs build` on every push; upload the built `site/` as a CI artifact. Full Gitea Pages publishing deferred if no hosting target is configured (chore, not feat) | D-046 | chore/ci | Medium | Pending | v0.4/P3 |
|
||||
|
||||
> REQ-029..REQ-032 are NEW in v0.4. All are NFR classes (refactor/test/quality/
|
||||
> chore) — zero `feat:` phases by construction. The final-phase audit enforces
|
||||
> the NFR purity gate (zero `feat:` commits in the milestone).
|
||||
|
||||
## IDEATE Traceability (Phase 0 — IDEATE stage, autonomy=full)
|
||||
|
||||
The IDEATE stage ran the three ideation tiers (mechanical, backend-enriched,
|
||||
cross-project) on the v0.3 milestone scope and ratified 8 ideas (IDEATE-01..
|
||||
IDEATE-08) at full autonomy. Each IDEATE-NN maps to a REQ-ID in the v0.3
|
||||
requirements table above. Mechanical tier: no `lessons:`/`compound:` tags in
|
||||
v0.1/v0.2 history (convention unused); one historical escalation (milestone
|
||||
release pending — no remote) resolved in v0.2; v0.2 closed clean (9/9 REQs,
|
||||
303 tests, ≥95.9% coverage). Backend-enriched + cross-project tiers confirmed
|
||||
the docs deliverable + Bearers skeleton bundle (D-034) and the firewall-first
|
||||
ordering (D-044). Defaults accepted per full autonomy.
|
||||
|
||||
| IDEATE ID | REQ-ID | Category | Source | Confidence | Phase |
|
||||
|-----------|--------|----------|--------|------------|-------|
|
||||
| IDEATE-01 | REQ-027 | improvement/docs | user `--ideate` request + D-042/D-045 | 0.90 | v0.3/P1-P3 |
|
||||
| IDEATE-02 | REQ-028 | quality/security | D-043 + RESEARCH firewall-extension design | 0.88 | v0.3/P1 |
|
||||
| IDEATE-03 | REQ-010 | coverage/architecture | ROADMAP Phase 3 + D-036 | 0.80 | v0.3/P4 |
|
||||
| IDEATE-04 | REQ-022 | coverage | ROADMAP Phase 3 + D-037 | 0.82 | v0.3/P4 |
|
||||
| IDEATE-05 | REQ-023 | coverage | ROADMAP Phase 3 + D-038 | 0.78 | v0.3/P4 |
|
||||
| IDEATE-06 | REQ-024 | architecture | ROADMAP Phase 3 + D-039 | 0.80 | v0.3/P5 |
|
||||
| IDEATE-07 | REQ-025 | coverage | ROADMAP Phase 3 + D-040 | 0.78 | v0.3/P5 |
|
||||
| IDEATE-08 | REQ-026 | coverage | ROADMAP Phase 3 + D-041 | 0.80 | v0.3/P5 |
|
||||
|
||||
Notes:
|
||||
- IDEATE-01/02 (docs deliverable + firewall) are the user's `--ideate` request
|
||||
ratified via D-042/D-043/D-045.
|
||||
- IDEATE-03..08 (Bearers skeleton) are the ROADMAP Phase 3 subset bundled into
|
||||
v0.3 per D-034.
|
||||
- IDEATE-02 lands in P1 (firewall-first) BEFORE IDEATE-01 content (P2/P3) per
|
||||
D-044 — docs are lexicon-clean by construction.
|
||||
- IDEATE-03..05 ship in P4 (Bearers skeleton I); IDEATE-06..08 ship in P5
|
||||
(Bearers skeleton II) — vertical slices, each phase independently shippable.
|
||||
|
||||
## Milestone v0.1 Summary
|
||||
- 10 REQs complete (skeleton + tests)
|
||||
- 2 REQs skeleton (REQ-001 principles, REQ-008 chain)
|
||||
|
||||
+660
-1
@@ -629,4 +629,663 @@ compiling with `go build ./...` and `go test ./...` using only stdlib. Rationale
|
||||
| REQ-012 | Lexicon | (all) | all | Enforced everywhere — D-032 |
|
||||
|
||||
**New modules: 9. Extended modules: 1 (bearers). Total v0.2 packages: 10 new +
|
||||
1 extended = 11 packages added to the v0.1 baseline of 15.**
|
||||
1 extended = 11 packages added to the v0.1 baseline of 15.**
|
||||
|
||||
---
|
||||
|
||||
## v0.3 Research (Bearers & Documentation)
|
||||
|
||||
> This section appends v0.3 research to the v0.1/v0.2 baseline above. It does NOT
|
||||
> rewrite or supersede the earlier content. v0.3 bundles two work-streams under
|
||||
> one feature milestone (D-034): (A) Bearers skeleton+tests (D-020/D-035 pattern)
|
||||
> and (B) a README.md + MkDocs Material docs site with the REQ-012 lexicon firewall
|
||||
> extended to docs (D-043). Tags run on v0.2.x (config.json tag_base).
|
||||
|
||||
### v0.3 Scope Recap (from D-034..D-046)
|
||||
|
||||
v0.3 continues the skeleton+tests pattern (D-020/D-035): Go types + keeper stubs +
|
||||
invariant tests, no live chain, no real IBC, no real bearer transports, no live
|
||||
B2B runtime. The new/extended modules:
|
||||
|
||||
| Module | REQ | D-decision | New/Ext | Phase | Skeleton depth |
|
||||
|---|---|---|---|---|---|
|
||||
| `x/exit` | REQ-010 | D-036 | New | P4 | ExitRoute + DEXSwap + ExitStatus |
|
||||
| `x/bridge` | REQ-010 | D-036 | New | P4 | BridgeRoute + BridgeStatus; ref x/satellite L2Chain by ID |
|
||||
| `x/bearers` (ext) | REQ-022 | D-037 | Ext | P4 | OYSATLink + OYQRCode (BearerTransport impls) |
|
||||
| `x/partner` (ext) | REQ-023 | D-038 | Ext | P4 | Anchor credential fields on the Anchor tier |
|
||||
| `x/hub` | REQ-024 | D-039 | New | P5 | HubService enum (Custody/LendingPrimitive/Compliance) + struct stubs |
|
||||
| `x/services` | REQ-025 | D-040 | New | P5 | ServiceKind enum (Care/SIM/Vault/Mail) + struct stubs |
|
||||
| `x/bond` (ext) | REQ-026 | D-041 | Ext | P5 | GrowthBond + SecondaryOrder; 8%/0% consts unchanged |
|
||||
| docs/ + README.md + mkdocs.yml | REQ-027 | D-042 | New | P1-P3 | ~20-25 pages, 4 audiences |
|
||||
| `lexicon_meta_docs_test.go` | REQ-028 | D-043 | New | P1 | firewall sibling test |
|
||||
|
||||
---
|
||||
|
||||
## v0.3 §1. Bearers Skeleton Design — Per Module
|
||||
|
||||
### v0.3 §1.1 `x/exit` (Exit layer — DEX swaps, REQ-010, D-036)
|
||||
|
||||
**What it is:** The Layer 3 exit layer — Holder-initiated DEX swaps and off-mesh
|
||||
service exits. v0.3 ships the type scaffold (ExitRoute, DEXSwap) + ExitStatus
|
||||
enum; no live DEX integration (deferred to v0.4).
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **DEX aggregators** — 1inch (off-chain path optimization), Paraswap, 0x API
|
||||
(rfq + amm). An ExitRoute is closest to a 1inch "swap route" (a hop sequence:
|
||||
source-asset → intermediate → destination-asset, each hop a venue).
|
||||
- **Cosmos SDK** — no native DEX module; DEX swaps are CosmWasm or external. The
|
||||
skeleton types are self-contained Go structs (zero-dep, same as v0.2 satellite).
|
||||
- **Cross-chain swap** — THORChain, Chainflip; an exit that crosses a bridge is
|
||||
a swap-with-bridge-hop. `x/exit` references a `x/bridge` BridgeRoute by ID for
|
||||
cross-chain exits (the BridgeRoute is typed in x/bridge; x/exit holds a
|
||||
bridge-route-id string field, G-003 by-ID-string ref).
|
||||
|
||||
**Recommendation for skeleton:** New `x/exit/types/` module following the v0.2
|
||||
`x/satellite` pattern (locked-const enum + struct types + genesis + keeper stub):
|
||||
- `ExitStatus` enum (Proposed, InProgress, Settled, Failed, Refunded) — exactly
|
||||
5, locked-const `ExitStatusCount = 5` with a regression test.
|
||||
- `ExitRoute` struct (route-id, holder-reach-id, source-asset, dest-asset,
|
||||
amount-grain, min-received-grain, bridge-route-id (optional, by-ID-string ref
|
||||
to x/bridge for cross-chain exits), venue-hops []string, deadline, status).
|
||||
- `DEXSwap` struct (swap-id, route-id (by-ID-string ref to ExitRoute), venue,
|
||||
input-asset, input-amount-grain, output-asset, output-amount-grain, executed-at).
|
||||
The "venue" is an opaque string (e.g., "uniswap-v3", "oy-dex") — no enum locked
|
||||
in v0.3 (venues are operational, not protocol-locked; locking now risks churn).
|
||||
- `Keeper` stub: AddExitRoute / GetExitRoute / ListByHolder (by holder-reach-id).
|
||||
- `GenesisState` + `ValidateGenesis` (route-id uniqueness, A-212 pattern).
|
||||
- Tests: ExitStatusCount=5, enum names match, route round-trip, lexicon assertion,
|
||||
genesis ID-uniqueness.
|
||||
|
||||
**Pattern followed:** `x/satellite` (new module with enum + structs + genesis +
|
||||
keeper stub). The ExitStatus enum mirrors the v0.2 BondStatus/ChannelStatus
|
||||
pattern (locked count + AllX() returning vision-order values).
|
||||
|
||||
**Cross-component dependency:** `x/exit` references `x/bridge` BridgeRoute by
|
||||
ID-string (G-003). This is a P4 intra-phase dependency: `x/bridge` types must
|
||||
exist before `x/exit` tests that reference a bridge-route-id. Both land in P4
|
||||
(D-044); `x/bridge` is authored first within P4. No struct import (by-ID-string
|
||||
only), so no import cycle. `x/exit` also references `x/bread` Grain by name only
|
||||
(the amount-grain field is int64, not a bread.Bread import).
|
||||
|
||||
**Risk:** "DEX swap" terminology is lexicon-safe (no banned terms). "venue" is
|
||||
not banned. Avoid "account" (use holder-reach-id), "currency"/"dollar"/"euro"
|
||||
(use source-asset/dest-asset opaque strings). Test asserts no banned terms.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.2 `x/bridge` (L2↔L1 bridge types, REQ-010, D-036)
|
||||
|
||||
**What it is:** The L2↔L1 bridge type scaffold — bridge routes between OY Chain
|
||||
(L1) and the v0.2 satellite L2 chains. v0.3 ships BridgeRoute + BridgeStatus
|
||||
enum; no live bridge runtime (the v0.2 satellite ICS-20 channel types are the
|
||||
IBC transport; x/bridge is the higher-level route abstraction over them).
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **L2↔L1 bridges** — Hop Protocol (L2-L1 token bridge with bonder), Across
|
||||
(repayer-based), Connext (interop), Cosmos IBC (the v0.2 satellite choice).
|
||||
OY's bridge is IBC-native (D-021 chose IBC for L2); x/bridge is the route layer
|
||||
over the IBC transfer channel.
|
||||
- **Bridge status lifecycles** — Hop: Pending → Confirmed → Minted; Across:
|
||||
Pending → Filled → Repaid; IBC: Init → TryOpen → Open → Closed (the v0.2
|
||||
ChannelStatus). x/bridge's BridgeStatus is the route-level lifecycle (above
|
||||
the channel handshake): Pending → Attested → Active → Closed.
|
||||
- **Watcher attestation for bridges** — bridges are high-value exits; the vision
|
||||
ties bridge activation to Watcher attestation (6-of-9 quorum, REQ-004). The
|
||||
BridgeStatus `Attested` state references a Watcher quorum by ID (skeleton:
|
||||
the attestation is a by-ID-string field, not a struct import).
|
||||
|
||||
**Recommendation for skeleton:** New `x/bridge/types/` module following `x/satellite`:
|
||||
- `BridgeStatus` enum (Pending, Attested, Active, Closed) — exactly 4, locked-const
|
||||
`BridgeStatusCount = 4`. `Attested` is the Watcher-quorum-confirmed state.
|
||||
- `BridgeRoute` struct (route-id, source-chain (L2Chain by-ID-string ref to
|
||||
x/satellite — G-003, no struct import), dest-chain, bridge-type (e.g., "ibc",
|
||||
"oy-bridge" — opaque string, not a locked enum in v0.3), transfer-channel-id
|
||||
(by-ID-string ref to a v0.2 satellite TransferChannel), watcher-quorum-id
|
||||
(by-ID-string ref to x/watcher, set when status becomes Attested), status).
|
||||
- `Keeper` stub: AddBridgeRoute / GetBridgeRoute / ListByStatus.
|
||||
- `GenesisState` + `ValidateGenesis` (route-id uniqueness, A-212).
|
||||
- Tests: BridgeStatusCount=4, enum names, route round-trip, lexicon assertion.
|
||||
|
||||
**Pattern followed:** `x/satellite` (the v0.2 L2/IBC module). x/bridge reuses the
|
||||
satellite L2Chain enum by-ID-string reference — it does NOT redefine L2 chains.
|
||||
|
||||
**Cross-component dependency:** `x/bridge` references `x/satellite` (L2Chain by
|
||||
ID-string, for source-chain/dest-chain fields) and `x/watcher` (watcher-quorum-id
|
||||
by ID-string, for the Attested state). Both are v0.1/v0.2 baseline modules; no
|
||||
new v0.3 dependency. x/bridge is authored FIRST in P4 (before x/exit) because
|
||||
x/exit references a BridgeRoute by ID.
|
||||
|
||||
**Risk:** "bridge" is not a banned term. Avoid "currency"/"dollar"/"euro" (use
|
||||
chain names: "Polygon"/"OY-Chain"). The watcher-quorum-id is an opaque string;
|
||||
no Watcher struct import (avoids cycle with x/watcher).
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.3 `x/bearers` extension (OY-SAT + OY-QR, REQ-022, D-037)
|
||||
|
||||
**What it is:** The remaining two bearer transports (OY-SAT satellite, OY-QR
|
||||
paper/QR-code) completing the 6-bearer type set (v0.2 had 4: Internet, OY-LR,
|
||||
OY-BLE, OY-WiFi-Direct + the Beacon transport-mode). v0.3 adds the OY-SAT and
|
||||
OY-QR transport structs implementing the v0.2 `BearerTransport` interface. No
|
||||
hardware/RF/paper-scan runtime.
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **OY-SAT (satellite bearer)** — Starlink (consumer satellite), Iridium (low-
|
||||
earth-orbit, global), Swarm (low-bandwidth satellite IoT). Helium Mobile +
|
||||
satellite fallback. OY-SAT is global, surveillance-resistant (vision §14).
|
||||
The transport struct mirrors the v0.2 OYLRLink shape (gateway-id, frequency,
|
||||
surveillance-resistant flag).
|
||||
- **OY-QR (paper/QR-code bearer)** — offline QR-code value transfer (the
|
||||
"paper wallet" / "physical Bitcoin" analog). Closest: Bolt Card (NFC + QR
|
||||
Lightning), OpenTimestamps QR proofs. OY-QR is 0-range (vision §14: the
|
||||
bearer list has OY-QR at "0 range"); a QR encodes a signed transfer that the
|
||||
recipient scans and submits. The transport struct mirrors BeaconFrame (a
|
||||
payload + ttl, but for QR it's a one-shot signed payload, no ephemeral-id).
|
||||
|
||||
**Recommendation for skeleton:** Extend `x/bearers/types/types.go` (do NOT create
|
||||
a new module — v0.1/v0.2 own the BearerType enum and BearerTransport interface).
|
||||
Add:
|
||||
- `OYSATLink` struct (gateway-id, constellation (e.g., "iridium" — opaque
|
||||
string), frequency-mhz, surveillance-resistant (LOCKED true for OY-SAT)).
|
||||
- `OYQRCode` struct (qr-id, payload-bytes (the signed transfer), issuer-reach-id,
|
||||
expires-at, consumed (bool — a QR is one-shot)).
|
||||
- Both implement `BearerTransport` (Send/Receive/Status) as stub methods (the
|
||||
v0.2 OYLRLink/BeaconFrame are struct types but NOT BearerTransport impls in
|
||||
v0.2 — they are transport-shape stubs; v0.3 keeps the same shape-only approach
|
||||
for OY-SAT/OY-QR to match D-029. If the interface impl is desired, add stub
|
||||
methods returning a "not-integrated" sentinel, mirroring forex StubOracle).
|
||||
- Tests: BearerOYSAT and BearerOYQR are already in `AllBearers()` from v0.1
|
||||
(asserted by the existing locked-const test — do NOT change the count). New
|
||||
tests assert the OYSATLink/OYQRCode struct fields, surveillance-resistant is
|
||||
LOCKED true for OY-SAT, OY-QR is one-shot (consumed flips to true), lexicon.
|
||||
- The v0.2 `BearerTypeCount` / `AllBearers()` locked-const test stays unchanged
|
||||
(6 bearers, already locked). v0.3 adds the transport structs only.
|
||||
|
||||
**Pattern followed:** D-029 (v0.2 bearers extension). The OYSATLink mirrors
|
||||
OYLRLink; OYQRCode mirrors BeaconFrame with a one-shot consumed flag instead of
|
||||
ttl.
|
||||
|
||||
**Cross-component dependency:** None new. x/bearers is a leaf (no refs to other
|
||||
v0.3 modules). The BearerType enum is locked since v0.1.
|
||||
|
||||
**Risk:** Hardware/paper-scan runtime is explicitly deferred. Do not pull LoRa/
|
||||
BLE/satellite/QR Go libraries. Pure types. "QR" / "SAT" are not banned terms.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.4 `x/partner` extension (Anchor credential types, REQ-023, D-038)
|
||||
|
||||
**What it is:** The Anchor tier (the 4th of the 4-tier Partner Spectrum, REQ-018)
|
||||
gets institution-specific credential fields in v0.3. v0.2 defined the 4-tier
|
||||
enum + Partner struct + CredentialRef; v0.3 adds an AnchorCredential struct
|
||||
carrying the institutional onboarding metadata (regulatory jurisdiction,
|
||||
custody-provider ref, attestation refs). No live institutional onboarding.
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **Institutional onboarding** — MakerDAO RWA arrangers (legal repr, off-chain
|
||||
agreements), Centrifuge senior/junior tranches with institutional sponsors,
|
||||
Maple Finance institutional underwriting, Ondo Finance institutional wrappers.
|
||||
The Anchor tier is OY's abstraction over institutional partners.
|
||||
- **Regulatory credentials** — e-Residency (Estonia), MiCA compliance (EU),
|
||||
SOC2/ISO27001 attestations. The AnchorCredential carries jurisdiction +
|
||||
attestation refs (opaque URIs, like the v0.2 Pier CredentialRef).
|
||||
- **Custody** — Anchors may custody assets; the custody-provider ref points to
|
||||
a Hub custody service (x/hub, P5). This is the Anchor→hub dependency edge:
|
||||
x/partner Anchor references a hub custody-service-id by string.
|
||||
|
||||
**Recommendation for skeleton:** Extend `x/partner/types/types.go`:
|
||||
- `AnchorCredential` struct (partner-id (by-ID-string ref to the Anchor
|
||||
Partner), jurisdiction (opaque string e.g. "EU-MiCA"), custody-provider-id
|
||||
(by-ID-string ref to x/hub custody service — set in v0.4 when hub is live;
|
||||
v0.3 field is a string, may be empty in skeleton), attestation-refs
|
||||
[]string (opaque URIs to Watcher/auditor attestations), onboarded-at).
|
||||
- A `Partner.AnchorCredential() *AnchorCredential` accessor stub returning nil
|
||||
for non-Anchor tiers (or a separate AnchorPartner struct wrapping Partner —
|
||||
prefer the accessor to avoid a second top-level type).
|
||||
- `Keeper.ListAnchors()` = `ListByTier(TierAnchor)` (already exists from v0.2;
|
||||
add a convenience alias + an `AddAnchorCredential(partnerID, cred)` method).
|
||||
- Tests: AnchorCredential struct round-trip, non-Anchor Partner returns nil,
|
||||
AddAnchorCredential rejects non-Anchor partner-id, lexicon assertion.
|
||||
|
||||
**Pattern followed:** v0.2 partner extension (add a struct + accessor, reuse
|
||||
the existing Keeper). The PartnerTier enum (4 tiers, locked since v0.2) is
|
||||
unchanged — v0.3 adds Anchor-specific fields, not a new tier.
|
||||
|
||||
**Cross-component dependency:** `x/partner` Anchor references `x/hub` custody
|
||||
service by ID-string (G-003). This is a P4→P5 edge: x/partner Anchor lands in
|
||||
P4, x/hub in P5. The custody-provider-id field is a string that is EMPTY in
|
||||
the v0.3 skeleton (the hub is not live until P5/v0.4); the field exists so the
|
||||
shape is stable. This is the dependency that forces P4 before P5 (D-044).
|
||||
|
||||
**Risk:** "custody" is not a banned term (the banned list is bank/deposit/
|
||||
interest/yield/currency/dollar/euro/account/savings/depositor). "jurisdiction"
|
||||
is safe. Avoid "account" (use partner-id). Test asserts no banned terms.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.5 `x/hub` (Hub API — B2B backbone, REQ-024, D-039)
|
||||
|
||||
**What it is:** The Hub API Pact (#6 per REQ-020/D-027) promoted to its own
|
||||
module in v0.3. The B2B backbone: custody, lending-primitive, compliance
|
||||
service types. v0.3 ships the HubService enum + per-service struct stubs +
|
||||
keeper stub; no live B2B runtime (full suite deferred to v0.4).
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **B2B API backbones** — Stripe API (modular resources), Plaid (bank-data
|
||||
aggregation — note: Plaid is bank-data, OY's Hub is the anti-bank backbone, so
|
||||
the analogy is structural not semantic), Coinbase Prime (institutional
|
||||
custody + prime). The Hub is OY's on-chain B2B service registry.
|
||||
- **Custody** — Fireblocks, Anchorage, BitGo (institutional custody). OY's
|
||||
custody service is a Hub-service type that an Anchor partner operates.
|
||||
- **Lending primitive** — Aave/Compound protocol-level lending; OY's "lending
|
||||
primitive" is a Hub-service type (the protocol-level primitive, not a live
|
||||
market). Lexicon note: "lending" is NOT banned (the banned list has "interest"
|
||||
and "deposit" and "savings" but not "lending" or "loan"); use "lending
|
||||
primitive" (vision §13) to stay aligned, avoid "interest"/"deposit".
|
||||
- **Compliance** — on-chain compliance attestations (TRM Labs, Elliptic for
|
||||
AML; Chainalysis). OY's compliance service is a Hub-service type an Anchor
|
||||
or Pier operates.
|
||||
|
||||
**Recommendation for skeleton:** New `x/hub/types/` module following `x/forex`
|
||||
(the v0.2 enum + struct + interface + stub-keeper pattern):
|
||||
- `HubService` enum (Custody, LendingPrimitive, Compliance) — exactly 3,
|
||||
locked-const `HubServiceCount = 3`. (Vision §13 names these three B2B
|
||||
categories; full B2B suite deferred to v0.4 per D-039.)
|
||||
- `HubServiceInfo` struct (service-id, kind (HubService), operator-partner-id
|
||||
(by-ID-string ref to x/partner — typically an Anchor), name, status).
|
||||
- `HubServiceStatus` enum (Pending, Active, Suspended, Revoked) — reuse the
|
||||
v0.2 PartnerStatus shape (same 4-state lifecycle); locked-const count = 4.
|
||||
Consider importing nothing and redefining locally (G-003 by-ID-string only;
|
||||
enums are string types so a local redefinition is fine and avoids a struct
|
||||
import of x/partner).
|
||||
- Per-service struct stubs: `CustodyService` (service-id, custody-provider-id,
|
||||
assets-supported []string), `LendingPrimitiveService` (service-id,
|
||||
primitive-kind (opaque string), coupon-cap-bps uint32 — clamp-reusable from
|
||||
x/bond Clamp? NO — the hub lending primitive references the bond cap by
|
||||
const value 800, not by importing x/bond.Clamp; keep a local const
|
||||
`LendingCouponCapBps = 800` cross-documented to D-028 to avoid the import),
|
||||
`ComplianceService` (service-id, jurisdiction, attestation-refs []string).
|
||||
- `Keeper` stub: AddService / GetService / ListByKind.
|
||||
- `GenesisState` + `ValidateGenesis` (service-id uniqueness, A-212).
|
||||
- Tests: HubServiceCount=3, enum names, service round-trip, per-service struct
|
||||
fields, lexicon assertion (HIGH-RISK: "interest"/"deposit"/"savings" must not
|
||||
appear; "lending"/"coupon"/"custody"/"compliance" are safe).
|
||||
|
||||
**Pattern followed:** `x/forex` (enum + struct + per-variant struct + stub
|
||||
keeper + genesis). The local-const-copy for the coupon cap mirrors how x/guild
|
||||
cross-documents x/feecovenant's WaiverHandPassGuild (v0.2 RESEARCH §1.5).
|
||||
|
||||
**Cross-component dependency:** `x/hub` references `x/partner` (operator-
|
||||
partner-id by ID-string, typically an Anchor). This is the P4→P5 edge: x/partner
|
||||
Anchor (P4) must precede x/hub (P5). The hub also cross-documents the bond
|
||||
8%/0% cap (D-028) as a local const to avoid importing x/bond (G-003).
|
||||
|
||||
**Risk:** Lending primitive is the highest lexicon-risk in v0.3 (after x/bond).
|
||||
"interest"/"yield"/"deposit"/"savings" are natural fit-words for a lending
|
||||
primitive — use "lending primitive"/"coupon" (vision §13/§17 lexicon). The
|
||||
lexicon assertion test is the gate. Do NOT use "borrower"/"lender" if they
|
||||
imply banned concepts — vision §13 uses "lending primitive" as the service
|
||||
name, so the struct/enum names follow vision.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.6 `x/services` (Care/SIM/Vault/Mail, REQ-025, D-040)
|
||||
|
||||
**What it is:** OY-protocol services beyond the financial layer — Care
|
||||
(community care), SIM (subscriber identity module / connectivity), Vault
|
||||
(storage service), Mail (messaging). v0.3 ships the ServiceKind enum + per-
|
||||
service struct stubs + keeper stub; no live services.
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **Care** — mutual aid societies, Gitcoin Grants rounds (care as public-good
|
||||
funding). OY Care is a community-care service a Stand/Guild operates.
|
||||
- **SIM** — Helium Mobile (DePIN connectivity), decentralized connectivity
|
||||
(Pollen, Andrena). OY SIM is a connectivity service.
|
||||
- **Vault** — the v0.2 x/vault is the Stand-level storage pool; an OY
|
||||
"Vault service" is a higher-level storage offering (backup, attested
|
||||
storage). Reference x/vault by ID-string (G-003).
|
||||
- **Mail** — decentralized messaging (Session, Status, XMTP). OY Mail is a
|
||||
bearer-routed messaging service.
|
||||
|
||||
**Recommendation for skeleton:** New `x/services/types/` module following
|
||||
`x/hub` (enum + struct + per-variant struct + keeper stub + genesis):
|
||||
- `ServiceKind` enum (Care, SIM, Vault, Mail) — exactly 4, locked-const
|
||||
`ServiceKindCount = 4`. (Vision §13 / ROADMAP Phase 4 lists these four;
|
||||
Yield Token + Travel + 11 more are Phase 4, out of v0.3 scope per D-040.)
|
||||
- `ServiceInfo` struct (service-id, kind (ServiceKind), operator-reach-id (by-
|
||||
ID-string ref to x/identity Reach), name, status, window-id (by-ID-string
|
||||
ref to x/window — a service-grant opens a Window on the holder's behalf)).
|
||||
- `ServiceStatus` enum (Pending, Active, Suspended, Revoked) — reuse the 4-
|
||||
state shape (local redefinition, no import).
|
||||
- Per-service struct stubs: `CareService`, `SIMService`, `VaultService`,
|
||||
`MailService` — each carries service-id + service-specific fields (Care:
|
||||
care-kind (opaque); SIM: carrier (opaque); Vault: storage-quota-grain;
|
||||
Mail: mailbox-id). Keep these minimal — the shape is the v0.3 deliverable.
|
||||
- `Keeper` stub: AddService / GetService / ListByKind.
|
||||
- `GenesisState` + `ValidateGenesis` (service-id uniqueness, A-212).
|
||||
- Tests: ServiceKindCount=4, enum names, service round-trip, per-service
|
||||
fields, lexicon assertion.
|
||||
|
||||
**Pattern followed:** `x/hub` (and transitively `x/forex`).
|
||||
|
||||
**Cross-component dependency:** `x/services` references `x/identity` (operator-
|
||||
reach-id by ID-string), `x/window` (window-id by ID-string — the service-grant),
|
||||
and `x/vault` (for VaultService, by ID-string). All v0.1/v0.2 baseline; no new
|
||||
v0.3 dependency. The window-id field is the hook for the Window Lifecycle
|
||||
interface (Architecture §4.4) — typed in v0.3, invoked at runtime in v0.4.
|
||||
|
||||
**Risk:** "Vault" collides with the v0.2 `x/vault` module name — but the
|
||||
ServiceKind enum value is "Vault" (a service kind, not a module import). The
|
||||
VaultService struct lives in `x/services/types/`, references `x/vault` by ID-
|
||||
string. The naming collision is at the concept level, not the package level (no
|
||||
Go import cycle). "Mail"/"SIM"/"Care" are not banned terms. Avoid "account"
|
||||
(use service-id / operator-reach-id).
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §1.7 `x/bond` extension (Growth Bonds + secondary market, REQ-026, D-041)
|
||||
|
||||
**What it is:** Bond market depth — GrowthBond (a bond whose coupon grows with
|
||||
protocol health, vision §17) + secondary-market order types. The 8% cap / 0%
|
||||
floor consts (D-028) are UNCHANGED. Full secondary-market matching deferred to
|
||||
v0.4.
|
||||
|
||||
**Prior art / ecosystem references:**
|
||||
- **Growth Bonds** — "growing bonds" / step-up bonds (coupon increases over
|
||||
time), inflation-linked bonds (TIPS). OY's GrowthBond is a bond whose coupon
|
||||
grows with Root-Pool Bloom (real-production-driven). Closest: TIPS (coupon
|
||||
adjusts to an index) but OY's index is the protocol's real return, not CPI.
|
||||
- **Secondary markets** — Uniswap v3 (concentrated liquidity AMM), order-book
|
||||
DEXs (dYdX, 0x Mesh), fixed-income secondary markets (Centrifuge Tinlake
|
||||
secondary). OY's secondary market is an order-book (buy/sell orders on issued
|
||||
bonds); the matching engine is v0.4. v0.3 types the order shape.
|
||||
- **Coupon caps** — the 8% cap (D-028) applies to GrowthBonds too: the growth
|
||||
coupon is still clamped to [0, 800] bps at any point (the cap is mission-
|
||||
locked; a growth bond cannot exceed it even as the protocol grows). The
|
||||
GrowthBond reuses x/bond.Clamp (same package — no G-003 concern).
|
||||
|
||||
**Recommendation for skeleton:** Extend `x/bond/types/types.go`:
|
||||
- `GrowthBond` struct embedding the v0.2 `Bond` + a `GrowthRateBps` field (the
|
||||
per-period growth rate of the coupon, clamped so that current-coupon +
|
||||
growth never exceeds CouponCapBps — add a `ClampGrowth(currentBps, growthBps)
|
||||
uint32` helper that returns min(CouponCapBps - currentBps, growthBps) so the
|
||||
post-growth coupon is ≤ cap). The growth-bond issuer-stand-id references
|
||||
x/stand by ID-string (unchanged from v0.2 Bond).
|
||||
- `SecondaryOrder` struct (order-id, bond-id (by-ID-string ref to the Bond),
|
||||
side (OrderSide enum: Buy/Sell), price-bps (price as a fraction of principal,
|
||||
in bps), quantity-grain, holder-reach-id, status, created-at).
|
||||
- `OrderSide` enum (Buy, Sell) — exactly 2, locked-const `OrderSideCount = 2`.
|
||||
- `OrderStatus` enum (Open, Filled, Cancelled) — exactly 3, locked-const
|
||||
`OrderStatusCount = 3`.
|
||||
- `Keeper` stub: AddOrder / GetOrder / ListByBond / CancelOrder (no matching).
|
||||
- Extend `GenesisState` with a `GrowthBonds []GrowthBond` and `Orders
|
||||
[]SecondaryOrder` field; `ValidateGenesis` checks order-id + growth-bond-id
|
||||
uniqueness (A-212).
|
||||
- Tests: GrowthBond clamp-growth invariant (post-growth coupon ≤ 800, never
|
||||
below 0), OrderSideCount=2, OrderStatusCount=3, order round-trip, the 8%/0%
|
||||
consts STILL = 800/0 (regression: v0.3 must not change D-028), lexicon
|
||||
assertion (HIGH-RISK: "interest"/"yield"/"deposit"/"savings" banned — use
|
||||
"coupon"/"growth"/"secondary"/"order").
|
||||
|
||||
**Pattern followed:** v0.2 x/bond (locked-const + Clamp + struct + genesis). The
|
||||
ClampGrowth helper mirrors the v0.2 Clamp shape (min/max with the cap).
|
||||
|
||||
**Cross-component dependency:** None new. x/bond references x/stand (issuer-
|
||||
stand-id, by ID-string, v0.2 baseline). The GrowthBond and SecondaryOrder are
|
||||
in-package with Bond (same `x/bond/types`), so no G-003 concern for the Clamp
|
||||
reuse.
|
||||
|
||||
**Risk:** Growth Bond is lexicon-hostile ("growth" is safe, but "yield growth"
|
||||
is the natural phrasing — use "coupon growth" / "real-return-linked coupon",
|
||||
never "yield"). The secondary-market "order" terminology is lexicon-safe
|
||||
("buy"/"sell"/"order"/"filled"/"cancelled" are not banned). The regression test
|
||||
that 8%/0% are unchanged is the D-028 firewall.
|
||||
|
||||
---
|
||||
|
||||
## v0.3 §2. Documentation Site + Firewall Extension Design
|
||||
|
||||
### v0.3 §2.1 MkDocs Material setup
|
||||
|
||||
**Decision (D-042):** MkDocs Material (`mkdocs.yml` at repo root + `docs/` tree).
|
||||
Build-only Python dep (`mkdocs` + `mkdocs-material`); `go.mod` stays zero-dep
|
||||
(G-006 — the docs toolchain is NOT a Go dependency; it lives in a separate
|
||||
Python toolchain, documented in README, not in go.mod).
|
||||
|
||||
**Minimal `mkdocs.yml`:**
|
||||
```yaml
|
||||
site_name: OpenYield
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
- toc.integrate
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.superfences
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Nomads:
|
||||
- nomads/index.md
|
||||
- nomads/reach.md
|
||||
- nomads/stash.md
|
||||
- nomads/bearers.md
|
||||
- nomads/maps-pay.md
|
||||
- nomads/pacts.md
|
||||
- nomads/standing-basics.md
|
||||
- Freeholders:
|
||||
- freeholders/index.md
|
||||
- freeholders/four-signals.md
|
||||
- freeholders/bayesian-standing.md
|
||||
- freeholders/stands-guilds.md
|
||||
- freeholders/councils-voice.md
|
||||
- freeholders/bonds.md
|
||||
- freeholders/partner-spectrum.md
|
||||
- Shared:
|
||||
- shared/index.md
|
||||
- shared/six-principles.md
|
||||
- shared/bread-scale.md
|
||||
- shared/storage-pools.md
|
||||
- shared/watchers-mirror.md
|
||||
- shared/lexicon-glossary.md
|
||||
- shared/vision-overview.md
|
||||
- Reference:
|
||||
- reference/architecture-index.md
|
||||
- reference/component-map.md
|
||||
```
|
||||
|
||||
**Audience organization (D-042, D-045):** nomads (Reach/Stash/bearers/Maps-Pay/
|
||||
Pacts/standing-basics — 7 pages), freeholders (4-signals/Bayesian-Standing/
|
||||
Stands-Guilds/Councils-Voice/Bonds/Partner-spectrum — 7 pages), shared (Six-
|
||||
Principles/Bread-Scale/Storage-pools/Watchers-Mirror/Lexicon-glossary/Vision-
|
||||
overview — 6 pages), reference (architecture-index/component-map — 2 pages).
|
||||
Total ~22 pages (within the D-045 20-25 budget).
|
||||
|
||||
**No publishing CI in v0.3 (D-046):** `mkdocs.yml` is buildable locally
|
||||
(`mkdocs serve` / `mkdocs build`); the README documents the build invocation.
|
||||
Publishing to GitHub/Gitea Pages deferred to v0.4 (needs deployment secrets +
|
||||
hosting target). v0.3 ships the source.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §2.2 Lexicon-clean docs by construction
|
||||
|
||||
The 10 banned terms (bank, deposit, interest, yield, currency, dollar, euro,
|
||||
account, savings, depositor) must not appear in `docs/**/*.md` or `README.md`.
|
||||
The highest-risk term in docs is **"yield"**: PROJECT.md uses "real yield" but
|
||||
docs must say "real production" / "real return" (the word-boundary regex in
|
||||
`lexicon.FindBannedTerm` bans standalone "yield" while allowing "OpenYield" —
|
||||
verified by `TestLexiconMetaNoFalsePositiveOnOpenYield`).
|
||||
|
||||
**Safe phrasings for docs (the docs-writer constraint):**
|
||||
- "real yield" → "real production" / "real return"
|
||||
- "account" → "Holder" / "Reach"
|
||||
- "bank" / "deposit" / "savings" / "depositor" → "Stash" / "Vault" / "Root-Pool"
|
||||
- "interest" → "coupon" (for bonds)
|
||||
- "currency" / "dollar" / "euro" → "Bread" / "asset" / opaque chain names
|
||||
|
||||
**Firewall-first ordering (D-044):** the docs firewall (`lexicon_meta_docs_test.go`)
|
||||
lands in P1 BEFORE the content (P2 nomads, P3 freeholders). This ensures docs
|
||||
are lexicon-clean by construction, not by retrofit — a banned term slipped into
|
||||
a P2 nomads page fails the P2 build, not the P6 review.
|
||||
|
||||
---
|
||||
|
||||
### v0.3 §2.3 Firewall extension design (`lexicon_meta_docs_test.go`)
|
||||
|
||||
**Decision (D-043):** NEW sibling test `lexicon_meta_docs_test.go` (package
|
||||
`lexicon_meta_docs`) at the repo root (next to `lexicon_meta_test.go`), NOT a
|
||||
modification of the existing `lexicon_meta_test.go` (package `lexicon_meta`).
|
||||
|
||||
**Rationale for the sibling (not extension) approach:**
|
||||
1. Preserves v0.2 coverage — the existing `lexicon_meta_test.go` scans `x/**/*.go`
|
||||
and is unchanged; v0.2's green firewall is not re-risked.
|
||||
2. Clearer separation — the docs firewall scans a different file set
|
||||
(`README.md` + `docs/**/*.md`) with different walk logic (repo-root walk, not
|
||||
`x/` walk); a separate test file keeps each firewall's walk logic self-
|
||||
contained and readable.
|
||||
3. Package isolation — `lexicon_meta_docs` is a distinct Go package so the two
|
||||
meta-tests compile independently and cannot share state by accident.
|
||||
|
||||
**The new test mirrors `lexicon_meta_test.go` exactly in detection logic:**
|
||||
- Uses the SAME `lexicon.FindBannedTerm` (word-boundary, case-insensitive) — no
|
||||
reimplementation of detection.
|
||||
- Same self-test table (G-009): one synthetic string per banned term, asserted
|
||||
to trigger detection, assembled from fragments so the test file's own source
|
||||
does not contain a banned-term literal.
|
||||
- Same self-exclusion: the meta-test file excludes ITSELF from its own scan
|
||||
(via `runtime.Caller(0)` to get its own path, then skipping it in the walk).
|
||||
- Same `TestLexiconMetaDocsBannedTermsCount` (exactly 10 terms).
|
||||
- Same `TestLexiconMetaDocsNoFalsePositiveOnOpenYield` (word-boundary does not
|
||||
match "openyield"/"european").
|
||||
|
||||
**File walk logic (the only difference from `lexicon_meta_test.go`):**
|
||||
- Walk the REPO ROOT (not `x/`): start at `filepath.Dir(thisFile)` (the repo
|
||||
root, since the test lives at the repo root).
|
||||
- Target files: `README.md` (repo root) + every `*.md` under `docs/` (recursive).
|
||||
- Exclude `.ciagent/` (firewall meta-files — PROJECT.md/RESEARCH.md/this file
|
||||
discuss banned terms by name for governance; they are NOT user-facing docs).
|
||||
- Exclude `.git/` (VCS metadata).
|
||||
- Exclude the meta-test file itself (`lexicon_meta_docs_test.go`).
|
||||
- Exclude `docs/` non-`.md` files (images, etc.) — only `.md` is scanned.
|
||||
- For each target `.md` file, read its source and run `lexicon.FindBannedTerm`;
|
||||
collect hits and fail with a list.
|
||||
|
||||
**Pseudocode of the walk:**
|
||||
```go
|
||||
func TestLexiconMetaDocsNoBannedTermsInDocs(t *testing.T) {
|
||||
repoRoot := filepath.Dir(thisFile(t)) // repo root (test is at repo root)
|
||||
thisFile := thisFile(t)
|
||||
hits := []string{}
|
||||
filepath.Walk(repoRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
base := filepath.Base(path)
|
||||
if base == ".ciagent" || base == ".git" { return filepath.SkipDir }
|
||||
return nil
|
||||
}
|
||||
if path == thisFile { return nil } // self-exclusion
|
||||
if !strings.HasSuffix(path, ".md") { return nil }
|
||||
// only README.md (repo root) + docs/**/*.md
|
||||
rel, _ := filepath.Rel(repoRoot, path)
|
||||
if rel != "README.md" && !strings.HasPrefix(rel, "docs/") { return nil }
|
||||
bz, _ := os.ReadFile(path)
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
hits = append(hits, rel+" contains banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(hits) > 0 { t.Errorf("REQ-012 docs lexicon violations:\n %s", ...) }
|
||||
}
|
||||
```
|
||||
|
||||
**Per-package lexicon assertions (in the new x/* modules):** each new/extended
|
||||
v0.3 module's `types_test.go` includes a `TestLexiconNoBannedTermsIn<Module>Package`
|
||||
scanning the module's production `.go` files (the v0.2 pattern from
|
||||
`x/partner/types/types_test.go`). These are OWNED by backend-engineer (not the
|
||||
docs firewall, which is frontend-engineer's). The project-wide `x/**/*.go` scan
|
||||
stays in `lexicon_meta_test.go` (v0.2, unchanged) and automatically covers the
|
||||
new v0.3 x/* modules.
|
||||
|
||||
---
|
||||
|
||||
## v0.3 §3. Cross-Component Dependencies Affecting Phase Ordering
|
||||
|
||||
The v0.3 cross-component dependencies (all by-ID-string per G-003, no struct
|
||||
imports) that constrain the D-044 phase ordering:
|
||||
|
||||
1. **`x/exit` → `x/bridge` (intra-P4):** `x/exit`'s ExitRoute has a `bridge-route-
|
||||
id` field referencing a `x/bridge` BridgeRoute by ID-string. `x/bridge` types
|
||||
must exist before `x/exit` tests reference a bridge-route-id. Both land in P4;
|
||||
`x/bridge` authored first within P4.
|
||||
|
||||
2. **`x/partner` (Anchor) → `x/hub` (P4 → P5):** `x/hub`'s HubService has an
|
||||
`operator-partner-id` field referencing an Anchor Partner by ID-string.
|
||||
`x/partner` Anchor extension (P4) must precede `x/hub` (P5). The Anchor's
|
||||
`custody-provider-id` field references a hub custody service (by ID-string)
|
||||
but is EMPTY in the v0.3 skeleton (hub not live until P5/v0.4), so the
|
||||
reverse edge is typed-but-deferred. This is the dependency that forces P4
|
||||
before P5.
|
||||
|
||||
3. **`x/services` → `x/window` (P5 → v0.2 baseline):** `x/services`'s ServiceInfo
|
||||
has a `window-id` field referencing a `x/window` Window by ID-string. x/window
|
||||
is a v0.2 baseline module (already shipped); no phase-ordering concern.
|
||||
|
||||
4. **`x/bond` (GrowthBond) → `x/stand` (P5 → v0.2 baseline):** GrowthBond's
|
||||
issuer-stand-id references x/stand by ID-string (unchanged from v0.2 Bond).
|
||||
x/stand is v0.2 baseline; no concern.
|
||||
|
||||
5. **`x/bridge` → `x/satellite`, `x/watcher` (P4 → v0.2/v0.1 baseline):**
|
||||
BridgeRoute references a satellite L2Chain and a watcher-quorum-id by ID-
|
||||
string. Both are baseline; no concern.
|
||||
|
||||
**Net phase-ordering conclusion:** D-044's P4→P5 split is confirmed and cannot
|
||||
be reversed: P4 = exit/bridge/bearers/partner-Anchor; P5 = hub/services/bond.
|
||||
The only intra-phase ordering within P4 is `x/bridge` before `x/exit`. P5 has no
|
||||
intra-phase ordering constraint (hub, services, bond are independent of each
|
||||
other). The docs phases (P1-P3) are firewall-first (P1) then content (P2-P3),
|
||||
independent of the Bearers phases.
|
||||
|
||||
---
|
||||
|
||||
## v0.3 §4. Assumptions (logged with confidence scores)
|
||||
|
||||
| ID | Assumption | Confidence | Rationale |
|
||||
|----|-----------|------------|-----------|
|
||||
| A-301 | v0.3 skeleton stays dependency-free (only stdlib), matching v0.1/v0.2 (A-201). Docs build-deps (mkdocs) are Python, not Go; go.mod unchanged. | 0.95 | D-035/A-201/G-006 confirm; mkdocs is D-042. |
|
||||
| A-302 | `x/exit` and `x/bridge` are SEPARATE packages (not one x/exit module), per D-036. | 0.90 | D-036 explicitly names both; separation matches v0.2 one-package-per-module (A-202). |
|
||||
| A-303 | `x/hub` is a NEW module (Pact #6 promoted from x/pact enum value to its own module), per D-039. | 0.85 | D-039 names x/hub; the x/pact HubAPI enum value stays as a cross-ref. |
|
||||
| A-304 | `x/hub` `LendingCouponCapBps = 800` is a LOCAL const cross-documented to D-028 (not an import of x/bond.Clamp), to preserve G-003 (no struct import). | 0.82 | Mirrors v0.2 guild's cross-doc of feecovenant WaiverHandPassGuild (RESEARCH §1.5). |
|
||||
| A-305 | `x/partner` Anchor adds an `AnchorCredential` struct + accessor, NOT a new PartnerTier (the 4-tier enum is locked since v0.2). | 0.90 | D-038 says "extend with Anchor credential fields"; the tier enum is locked. |
|
||||
| A-306 | `x/bond` GrowthBond embeds the v0.2 Bond + a GrowthRateBps field with a ClampGrowth helper ensuring post-growth coupon ≤ 800 bps. | 0.80 | Vision §17 growth-bond; the 8% cap (D-028) applies to growth bonds too. |
|
||||
| A-307 | `x/services` ServiceKindCount = 4 (Care/SIM/Vault/Mail); Yield Token + Travel + 11 more are Phase 4 (out of v0.3 per D-040). | 0.85 | D-040 + ROADMAP Phase 4 confirm the 4-service v0.3 scope. |
|
||||
| A-308 | `x/exit` DEXSwap "venue" is an opaque string (not a locked enum) — venues are operational, locking now risks churn. | 0.75 | Venues change (uniswap-v3/v4, oy-dex); a locked enum would be a false firewall. |
|
||||
| A-309 | The docs firewall sibling test (lexicon_meta_docs_test.go) lives at the repo root (same dir as lexicon_meta_test.go), package lexicon_meta_docs. | 0.90 | D-043 chose the sibling; repo-root placement matches the v0.2 meta-test. |
|
||||
| A-310 | `.ciagent/` is excluded from the docs firewall scan (meta-files discuss banned terms by name for governance; not user-facing docs). | 0.88 | D-043 rationale; mirrors meta-test self-exclusion. |
|
||||
| A-311 | OY-SAT `surveillance-resistant` is LOCKED true (matching OY-LR from v0.2); OY-QR is one-shot (consumed flips to true). | 0.80 | Vision §14 marks OY-LR/OY-SAT as surveillance-resistant; QR is physical/one-shot. |
|
||||
| A-312 | `x/hub` HubServiceCount = 3 (Custody/LendingPrimitive/Compliance) per vision §13; full B2B suite (more services) deferred to v0.4. | 0.85 | D-039 + vision §13 name the three; D-039 defers the full suite. |
|
||||
| A-313 | `x/bond` OrderSideCount = 2 (Buy/Sell), OrderStatusCount = 3 (Open/Filled/Cancelled). | 0.80 | Standard order-book shape; minimal locked set for v0.3. |
|
||||
| A-314 | docs-writer is a SEPARATE custom persona (not folded into frontend-engineer) to keep content-vs-toolchain review boundaries explicit. | 0.75 | The ~20-25 page content load (D-045) justifies a split; lower confidence than the structural decisions. |
|
||||
| A-315 | v0.2 custom personas (cosmos-engineer, security-engineer) are NOT reactivated for v0.3 — lower Cosmos-convention and invariant density. | 0.80 | v0.3 modules are bespoke (no new x/gov/x/group/x/authc maps); invariant density is lower (enum counts, not Mission Lock). |
|
||||
|
||||
---
|
||||
|
||||
## v0.3 Cross-Reference Summary
|
||||
|
||||
| REQ | Component | Module | Phase (D-044) | Depth (D-03x) |
|
||||
|-----|-----------|--------|---------------|---------------|
|
||||
| REQ-010 | Exit layer (DEX) | `x/exit/` | P4 | Skeleton (ExitRoute, DEXSwap) — D-036 |
|
||||
| REQ-010 | Exit layer (bridge) | `x/bridge/` | P4 | Skeleton (BridgeRoute, BridgeStatus) — D-036 |
|
||||
| REQ-022 | Bearers OY-SAT/OY-QR | `x/bearers/` (ext) | P4 | Stubs (no HW) — D-037 |
|
||||
| REQ-023 | Anchors | `x/partner/` (ext) | P4 | Skeleton (AnchorCredential) — D-038 |
|
||||
| REQ-024 | Hub API | `x/hub/` | P5 | Skeleton (3 services) — D-039 |
|
||||
| REQ-025 | Services | `x/services/` | P5 | Skeleton (4 kinds) — D-040 |
|
||||
| REQ-026 | Bond market depth | `x/bond/` (ext) | P5 | Skeleton (GrowthBond + secondary) — D-041 |
|
||||
| REQ-027 | Docs site | `docs/`, `mkdocs.yml`, `README.md` | P1-P3 | ~22 pages, 4 audiences — D-042/D-045 |
|
||||
| REQ-028 | Docs firewall | `lexicon_meta_docs_test.go` | P1 | Sibling meta-test — D-043 |
|
||||
|
||||
**New modules: 4 (exit, bridge, hub, services). Extended modules: 3 (bearers,
|
||||
partner, bond). Docs surface: new (docs/, mkdocs.yml, README.md). Firewall: 1
|
||||
new sibling test. Total v0.3: 4 new + 3 extended + docs + 1 firewall test.**
|
||||
+110
-1
@@ -255,4 +255,113 @@ P0 fixes auto-applied: 0
|
||||
P1+ flags: 2 (x/council/types — Proposal/VoteOption lifecycle absent; VoiceSource→SignalKind 4-not-5)
|
||||
P2 nits: 1 (bearers ValidateGenesis no-op — correct per spec)
|
||||
Overall: APPROVE WITH P1+ FLAGS (confidence 0.88) — milestone ship not blocked
|
||||
```
|
||||
```
|
||||
---
|
||||
|
||||
## v0.3 Final Review (P6)
|
||||
|
||||
**Reviewer:** Multi-persona final review (correctness, testing, security, performance, maintainability, adversarial)
|
||||
**Scope:** `v0.1.5..HEAD` — all v0.3 milestone phases (P0 pre-exec + P1 docs foundation + P2 nomads docs + P3 freeholders docs + P4 Bearers I + P5 Bearers II)
|
||||
**Branch:** `oy/milestone/v0.3-bearers-docs` (reviewed on `oy/phase/06-final-review-ship`)
|
||||
**Date:** 2026-08-17
|
||||
**Diff stat:** 56 files changed, 6891 insertions(+), 169 deletions(-)
|
||||
|
||||
### Verification commands (all PASS)
|
||||
|
||||
```
|
||||
go build ./... — PASS (zero errors)
|
||||
go test ./... — PASS (all packages green)
|
||||
go test -cover ./x/{bridge,exit,bearers,partner,hub,services,bond}/types/ — PASS (coverage below)
|
||||
go test -run TestLexiconMeta ./lexicon_meta_docs/ . — PASS (docs firewall green)
|
||||
go test -run TestLexiconMeta ./ — PASS (x/ firewall green, repo-root package)
|
||||
```
|
||||
|
||||
### Coverage on new/extended packages
|
||||
|
||||
| Package | Coverage | Threshold (80%) | Verdict |
|
||||
|---|---|---|---|
|
||||
| x/bridge/types | 100.0% | ✓ | PASS |
|
||||
| x/exit/types | 100.0% | ✓ | PASS |
|
||||
| x/bearers/types | 100.0% | ✓ | PASS |
|
||||
| x/partner/types | 100.0% | ✓ | PASS |
|
||||
| x/hub/types | 93.3% | ✓ | PASS |
|
||||
| x/services/types | 100.0% | ✓ | PASS |
|
||||
| x/bond/types | 95.1% | ✓ | PASS |
|
||||
|
||||
All packages exceed the ≥80% coverage requirement. The two sub-100% packages (hub 93.3%, bond 95.1%) have gaps only in defensive genesis error-branches (e.g., `validateComplianceServices` 87.5%, `Clamp`/`ClampLendingCoupon` 80% — the upper-bound and floor branches each exercised by ≥1 test but not every permutation). No must-have is uncovered; G-012 boundary cases (`currentBps==cap`, `currentBps>cap`, underflow guard) are all explicitly tested (`TestClampGrowthCurrentAtCapReturnsZero`, `TestClampGrowthCurrentAboveCapReturnsZero`, `TestClampGrowthInvariantPostGrowthLeCap`).
|
||||
|
||||
### Per-axis verdicts
|
||||
|
||||
#### 1. Correctness (backend-engineer) — PASS (0.92)
|
||||
|
||||
Locked-const invariants all enforced and tested:
|
||||
- `BridgeStatusCount = 4` (x/bridge/types/types.go:18) — `AllBridgeStatuses()` returns 4 in REQ-015 order. ✓
|
||||
- `ExitStatusCount = 5` (x/exit/types/types.go:18) — `AllExitStatuses()` returns 5 in vision §7 order. ✓
|
||||
- `HubServiceCount = 3` (x/hub/types/types.go:42) — `AllHubServices()` returns 3 (Custody/LendingPrimitive/Compliance). ✓
|
||||
- `ServiceKindCount = 4` (x/services/types/types.go:37) — locked count asserted. ✓
|
||||
- `OrderSideCount = 2` (x/bond/types/types.go:171) — Buy/Sell. ✓
|
||||
- `OrderStatusCount = 3` (x/bond/types/types.go:174) — Open/Filled/Cancelled. ✓
|
||||
- `PartnerTierCount = 4` (x/partner/types/types.go:18) — regression intact. ✓
|
||||
- `BondStatusCount = 5` (x/bond/types/types.go:31) — regression intact. ✓
|
||||
- `CouponCapBps = 800` / `CouponFloorBps = 0` (x/bond/types/types.go:21,26) — D-028 LOCKED, regression firewall in types_test.go asserts both values. ✓
|
||||
|
||||
**G-012 (ClampGrowth underflow guard) — CORRECT.** The guard at x/bond/types/types.go:239 (`if currentBps >= CouponCapBps { return 0 }`) runs BEFORE the `CouponCapBps - currentBps` subtraction (line 243), so the uint32 underflow path is unreachable. Five boundary/invariant tests cover: currentBps==0 (full room), currentBps==cap (return 0), currentBps>cap (return 0, NOT wrapped huge), growth>room (clamp to room), growth<room (unchanged), and a meta-assert `current + ClampGrowth(current, growth) <= max(current, cap)` across a fuzz table.
|
||||
|
||||
The hub `LendingCouponCapBps`/`LendingCouponFloorBps` LOCAL consts (A-304) mirror x/bond's LOCKED values (800/0) without importing x/bond (G-003 preserved). Genesis-side clamp enforcement present in both `validateLendingPrimitives` (hub) and `ValidateBonds`/`ValidateGrowthBonds` (bond).
|
||||
|
||||
#### 2. Testing (backend-engineer) — PASS (0.90)
|
||||
|
||||
All new packages ≥93.3% (above 80% threshold). Per-package lexicon assertions present in every new package's types_test.go (TestLexiconNoBannedTermsIn<Pkg>Package + TestLexiconNoBannedTermsIn<Pkg>TestFile) — confirmed in x/bridge, x/exit, x/bearers, x/partner, x/hub, x/services, x/bond. G-012 boundary cases (currentBps==cap, currentBps>cap) explicitly tested. Locked-const regression tests present (Test<Const>LockedConst pattern) for every locked const enumerated above.
|
||||
|
||||
#### 3. Security (security) — PASS (0.93)
|
||||
|
||||
Both lexicon firewalls green:
|
||||
- x/ firewall (`lexicon_meta_test.go`, package `lexicon_meta`): `go test -run TestLexiconMeta ./` PASS.
|
||||
- docs/ firewall (`lexicon_meta_docs/lexicon_meta_docs_test.go`, package `lexicon_meta_docs`): `go test -run TestLexiconMeta ./lexicon_meta_docs/` PASS. Scans README.md + docs/**/*.md.
|
||||
|
||||
Adversarial verification: confirmed `lexicon.FindBannedTerm` catches all 10 banned terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor) via direct injection test. The docs firewall self-test table (G-009 for docs), walk-coverage test (G-013), and self-test drift assertion (G-014) all present and passing — the firewall provably CATCHES banned-term regressions rather than silently scanning nothing.
|
||||
|
||||
**G-003 (by-ID-string, no struct imports between x/*) — INTACT.** `grep -rn "oy/openyield/x/"` across all new package non-test .go files returns ZERO struct imports. The only cross-package import in a test file is `x/bearers/types/types_test.go` importing `x/processing/types` (a test-only import for a stub reference; G-003 governs production struct imports, not test imports). All cross-module references in production types use ID-string fields (issuer-stand-id, reach-id, holder-reach-id, custody-provider-id, anchor-id, bond-id, operator-partner-id, etc.) with explicit G-003 doc-comments.
|
||||
|
||||
#### 4. Performance (backend-engineer) — PASS (0.95)
|
||||
|
||||
`go.mod` UNCHANGED since v0.1.5 (`git diff v0.1.5..HEAD -- go.mod go.sum` is empty) — zero external deps preserved (G-006). The mkdocs build deps (mkdocs + mkdocs-material) are Python-only and documented as non-Go (mkdocs.yml header comment). Skeleton stubs use O(1) maps for registry lookups (x/partner Keeper); no N+1 patterns in the stub code. The genesis validators iterate slices once (O(n) per set) with map-backed uniqueness checks — appropriate for skeleton scale.
|
||||
|
||||
#### 5. Maintainability (lead-developer) — PASS (0.91)
|
||||
|
||||
**Pattern consistency:** All new modules follow the v0.1/v0.2 file structure (types.go + types_test.go, genesis.go where genesis validation exists). The Params/GenesisState/DefaultGenesisState/ValidateGenesis pattern is uniform across x/bridge, x/exit, x/bearers, x/partner, x/hub, x/services, x/bond. G-008 split (data-engineer's genesis.go schema helpers composed by ValidateGenesis in types.go) is present in x/bond and x/hub. The new modules use the same ModuleName/StoreKey/RouterKey/QuerierRoute const block and the same JSON/YAML struct-tag convention as v0.1/v0.2 modules.
|
||||
|
||||
**Docs cross-reference (G-011):** `mkdocs.yml` nav lists ALL 26 pages (1 Home + 8 Nomads + 8 Freeholders + 7 Shared + 2 Reference = 26), matching the 26 .md files under docs/. `docs/reference/components.md` cross-references the new modules (10 mentions of x/* packages). All docs pages are lexicon-clean (firewall green).
|
||||
|
||||
**.ciagent/oy/* updates:** PROJECT, ROADMAP, REQUIREMENTS, ARCHITECTURE, RESEARCH, PERSONAS, PLANS, GRILL all updated to reflect v0.3 scope (Bearers & Documentation, REQ-024..REQ-028, D-037..D-046, A-304..A-313, G-011..G-014).
|
||||
|
||||
#### 6. Adversarial (adversarial) — PASS (0.88)
|
||||
|
||||
Adversarial probes attempted and their outcomes:
|
||||
1. **Banned term slipped into docs** — the firewall self-test table (`TestLexiconMetaDocsSelfTestTable`, G-009 for docs) injects synthetic banned-term strings and asserts FindBannedTerm detects each; the walk-coverage test (`TestLexiconMetaDocsWalkCoverage`, G-013) injects a real .md fixture under docs/.lexicon_fixture/ and asserts the walk FINDS it. Catches the "silently scans nothing and reports green" failure mode. ✓
|
||||
2. **Locked-const regression** — every locked const has a `Test<Const>LockedConst` regression test asserting the exact value AND the All<Enum>() entry count/names. A regression (e.g., BridgeStatusCount→5) fails the test. ✓
|
||||
3. **Struct import breaks G-003** — no production .go file in the new packages imports another x/* package; verified by grep. ✓
|
||||
4. **ClampGrowth underflow** — the guard returns 0 BEFORE the subtraction; the underflow path is unreachable; tested with currentBps>cap (e.g., 801) asserting return 0 (NOT 4294967295). ✓
|
||||
5. **Hub A-304 drift from x/bond D-028** — the LOCAL consts are documented as cross-referenced (comment "also 800") and a regression test asserts LendingCouponCapBps==800. A future x/bond cap change without a matching hub change is flagged by the cross-doc comment (not a test — appropriate since they are LOCAL to hub). Note P2 below.
|
||||
|
||||
### P0 fixes auto-applied
|
||||
|
||||
**0.** No P0 (critical) issues found. The milestone ships clean.
|
||||
|
||||
### P1+ flags (post-hoc review — do NOT block ship)
|
||||
|
||||
**1.** [P2 nit, maintainability] x/hub `LendingCouponCapBps`/`LendingCouponFloorBps` (A-304) are LOCAL consts cross-documented to x/bond's D-028 consts (both 800/0) but there is no automated cross-check that they stay in lockstep. If a future mission-locked change to x/bond.CouponCapBps does not update the hub LOCAL const, the two packages silently drift. The cross-doc comment in types.go:46-50 flags this for human review, but a shared-const test (e.g., asserting `LendingCouponCapBps == x/bond.CouponCapBps` — though that would require a test-only import, acceptable per G-003 test exemption) would be more robust. Recommend post-hoc: add a cross-package const-equality test OR document the manual-sync requirement in ARCHITECTURE.md. Not a ship blocker — both are currently 800/0.
|
||||
|
||||
**2.** [P2 nit, testing] x/hub coverage 93.3% and x/bond coverage 95.1% leave defensive error-branches in `ClampLendingCoupon` (80%), `Clamp` (80%), `validateComplianceServices` (87.5%), `ValidateGrowthBonds` (85.7%) partially exercised. All must-have paths are tested; the uncovered lines are error-return branches for malformed genesis inputs. Recommend post-hoc: add 2-3 negative-case genesis tests per package to close the gaps to 100%. Not a ship blocker (both above the 80% threshold).
|
||||
|
||||
**3.** [P2 nit, docs] `docs/reference/architecture.md` has 0 cross-references to x/* packages (vs `docs/reference/components.md` which has 10). The architecture page is conceptual; the components page is the cross-ref hub. Acceptable as-is, but post-hoc adding 1-2 module cross-refs to architecture.md would improve discoverability. Not a ship blocker.
|
||||
|
||||
### Overall verdict
|
||||
|
||||
**SHIP.**
|
||||
|
||||
All verification commands pass. All locked-const invariants enforced and tested. Both lexicon firewalls green (x/ and docs/). G-003 (by-ID-string, no struct imports) intact across all new packages. G-012 (ClampGrowth underflow guard) correctly implemented with explicit boundary tests. Zero external deps (go.mod unchanged). Coverage ≥93.3% on all new/extended packages (above 80% threshold). mkdocs.yml nav complete (26/26 pages, G-011). No P0 issues. Three P2 nits flagged for post-hoc review (none blocking).
|
||||
|
||||
**P0 fixes auto-applied: 0**
|
||||
**P1+ findings: 0 P1, 3 P2 (all nits, post-hoc, non-blocking)**
|
||||
**Confidence in overall verdict: 0.91**
|
||||
|
||||
+58
-40
@@ -14,57 +14,75 @@
|
||||
- Status: COMPLETE (local-only ship, no remote configured)
|
||||
- MVP release (v0.1.0) deferred until system validated as production-ready
|
||||
|
||||
## Phase 1 — Foundation (Year 1)
|
||||
**Target**: first 10,000 Holders, 50 Master Ops
|
||||
## Milestone v0.2 — The Mesh (COMPLETE)
|
||||
- [x] P0: Pre-Execution → v0.1.0
|
||||
- [x] P1: Orgs + Window Foundation → v0.1.1
|
||||
- [x] P2: Pacts + Partners → v0.1.2
|
||||
- [x] P3: Councils + Forex → v0.1.3
|
||||
- [x] P4: Bonds + Bearers + L2 → v0.1.4
|
||||
- [x] P5: Final Review + Ship → v0.1.5 (milestone release)
|
||||
- Status: COMPLETE (skeleton + tests layer; released as v0.1.5)
|
||||
|
||||
| Component | Deliverable |
|
||||
|---|---|
|
||||
| OY Chain & Mirror (1) | L1 chain launched, 9 Watchers bonded, Mirror live |
|
||||
| Bread Unit & Root Basket (3) | Forge/Fold on Ethereum + 2–3 L2s; initial Root Basket |
|
||||
| Storage Substrate (5) | Stash, Vault, Root-Pool contracts |
|
||||
| Bloom Engine (4) | Bloom accrual loop tied to Mirror attestations |
|
||||
| Fee Covenant (13) | 0.1% ceiling live, processor share 50%, internal minimum 1 Grain |
|
||||
| Identity, Standing & Citizenship (6) | Reach v1, Standing v1, Nomad/Freeholder system |
|
||||
| Bearers & Processing Mesh (12) | Processing v1, OY-BLE, OY-WiFi-Direct |
|
||||
| Mesh Experience (9) | Maps, Pay v1 |
|
||||
## Milestone v0.3 — Bearers & Documentation (COMPLETE; feature type; tags v0.2.x)
|
||||
Target: Bearers skeleton (ROADMAP Phase 3 subset) + docs site for nomads and freeholders.
|
||||
|
||||
## Phase 2 — The Mesh (Year 2) — v0.2 SKELETON COMPLETE
|
||||
**Target**: $1B annual volume, 4 service categories
|
||||
- [x] P0: Pre-Execution (spec/clarify/research/ideate/plan/grill) → v0.2.0
|
||||
- [x] P1: Docs foundation + REQ-012 firewall extension → v0.2.1
|
||||
- [x] P2: Nomads docs → v0.2.2
|
||||
- [x] P3: Freeholders docs + reference → v0.2.3 (REQ-027 complete)
|
||||
- [x] P4: Bearers skeleton I (x/exit, x/bridge, x/bearers, x/partner) → v0.2.4
|
||||
- [x] P5: Bearers skeleton II (x/hub, x/services, x/bond) → v0.2.5
|
||||
- [x] P6: Final Review + Audit + Ship → v0.2.6 (milestone release)
|
||||
- Status: COMPLETE — Bearers skeleton (7 x/* packages) + docs site (26 pages) shipped
|
||||
|
||||
> **v0.2 (The Mesh) milestone status**: COMPLETE — skeleton + tests layer shipped.
|
||||
> Phase mapping: P0 (spec/research/plan/grill) -> P1 (Orgs+Window) -> P2 (Pacts+Partners)
|
||||
> -> P3 (Councils+Forex) -> P4 (Bonds+Bearers+L2) -> P5 (review/audit/ship).
|
||||
> v0.3 bundles two work-streams under one feature milestone: (A) Bearers
|
||||
> skeleton+tests (D-020 pattern) and (B) README.md + MkDocs Material docs site
|
||||
> organized by audience, with the REQ-012 lexicon firewall extended to docs.
|
||||
|
||||
| Component | Deliverable | v0.2 Skeleton Module | Phase |
|
||||
| Phase | Type | Scope | Patch |
|
||||
|---|---|---|---|
|
||||
| Organizational Primitives (10) | 9 Stand types, Guilds (Hand-Passes free) | x/stand, x/guild | v0.2/P1 |
|
||||
| Partner Spectrum & Forex (11) | First Piers, Forex Engine v1 | x/partner, x/forex | v0.2/P2,P3 |
|
||||
| Window Primitive (7) | Holder-authorized data channels | x/window | v0.2/P1 |
|
||||
| Pacts Suite (8) | Pause, Ground, Stance, Cover, Stand Registry | x/pact (6-type enum, one module) | v0.2/P2 |
|
||||
| Governance (14) | Mesh Council activated | x/council (3-kind + Mission Lock const) | v0.2/P3 |
|
||||
| Bearers expansion | OY-LR + Beacon v1 | x/bearers (extended) | v0.2/P4 |
|
||||
| Bonds | First Mesh Bonds | x/bond (8% cap / 0% floor) | v0.2/P4 |
|
||||
| L2 Satellites | Wrapped Bread via IBC | x/satellite (Polygon rep + 4 stubs) | v0.2/P4 |
|
||||
| P0 | docs | Pre-Execution (spec/clarify/research/ideate/plan/grill) | v0.2.0 |
|
||||
| P1 | feat/test+docs | Docs foundation + REQ-012 firewall extension to docs/ + README.md + shared docs | v0.2.1 |
|
||||
| P2 | docs | Nomads docs (docs/nomads/) | v0.2.2 |
|
||||
| P3 | docs | Freeholders docs (docs/freeholders/) + docs/reference/ | v0.2.3 |
|
||||
| P4 | feat | Bearers skeleton I: x/exit, x/bridge, x/bearers (OY-SAT, OY-QR), x/partner (Anchor) | v0.2.4 |
|
||||
| P5 | feat | Bearers skeleton II: x/hub, x/services, x/bond (Growth Bonds + secondary market) | v0.2.5 |
|
||||
| P6 | final | REVIEW + AUDIT + milestone SHIP | v0.2.6 (milestone release) |
|
||||
|
||||
> **Tag-line reconciliation (G-010)**: v0.1 pre-MVP shipped on the `v0.0.x` patch line
|
||||
> (ROADMAP lines 4-13: v0.0.0..v0.0.9). v0.2 (The Mesh) ships on the `v0.1.x` patch line
|
||||
> (config.json `tag_base: v0.1.x`): P0 -> `v0.1.0`, P1..P4 -> `v0.1.1..v0.1.4`, P5 -> `v0.1.5`
|
||||
> (= the v0.2 milestone release, per D-008/D-020 — final phase patch IS the milestone
|
||||
> release; no separate minor tag). The `v0.1.5` milestone release is NOT the deferred
|
||||
> `v0.1.0` "MVP" tag referenced on line 15 — they are different lines (v0.0.x pre-MVP
|
||||
> vs v0.1.x Mesh). No tag collision.
|
||||
### v0.3 Component mapping
|
||||
|
||||
## Phase 3 — The Bearers (Year 3)
|
||||
| Component | Deliverable | v0.3 Skeleton Module | Phase |
|
||||
|---|---|---|---|
|
||||
| Cross-Chain & Exit (2) | L2/L1 bridge types, DEX swap types | x/exit, x/bridge | v0.3/P4 |
|
||||
| Bearers expansion | OY-SAT, OY-QR bearer transport types | x/bearers (extended) | v0.3/P4 |
|
||||
| Anchors | First institutional Partner tier | x/partner (extended: Anchor) | v0.3/P4 |
|
||||
| Hub API | B2B backbone: custody, lending primitive, compliance types | x/hub | v0.3/P5 |
|
||||
| Services | Care / SIM / Vault / Mail service types | x/services | v0.3/P5 |
|
||||
| Bond market | Growth Bonds, secondary-market types | x/bond (extended) | v0.3/P5 |
|
||||
| Documentation | README.md + MkDocs Material docs site | docs/, mkdocs.yml, README.md | v0.3/P1-P3 |
|
||||
| Lexicon firewall | Extend REQ-012 to docs/ + README.md | lexicon_meta_docs_test.go | v0.3/P1 |
|
||||
|
||||
> **Tag-line note (G-010 continuation)**: v0.1 pre-MVP shipped on the `v0.0.x`
|
||||
> patch line; v0.2 (The Mesh) shipped on the `v0.1.x` patch line; v0.3 (Bearers
|
||||
> & Documentation) ships on the `v0.2.x` patch line (config.json `tag_base:
|
||||
> v0.2.x`): P0 -> `v0.2.0`, P1..P5 -> `v0.2.1..v0.2.5`, P6 -> `v0.2.6`
|
||||
> (= the v0.3 milestone release, per D-008 — final phase patch IS the
|
||||
> milestone release; no separate minor tag).
|
||||
|
||||
## Phase 3 — The Bearers (Year 3) — v0.3 PARTIAL SKELETON
|
||||
**Target**: $10B annual volume → fee auto-declines to 0.07%
|
||||
|
||||
> v0.3 ships a skeleton+tests subset of Phase 3 (Cross-Chain/Exit, OY-SAT/OY-QR,
|
||||
> Anchors, Hub API, Services, Bond market depth). Full runtime deferred to v0.4+.
|
||||
|
||||
| Component | Deliverable |
|
||||
|---|---|
|
||||
| Cross-Chain & Exit (2) | Full L2/L1 bridges, DEX integration |
|
||||
| Bearers expansion | OY-SAT, OY-QR |
|
||||
| Hub API | B2B backbone: custody, lending primitive, compliance |
|
||||
| Anchors | First institutional partners |
|
||||
| Services | Care / SIM / Vault / Mail |
|
||||
| Bond market | Full market, Growth Bonds |
|
||||
| Cross-Chain & Exit (2) | Full L2/L1 bridges, DEX integration (runtime deferred to v0.4) |
|
||||
| Bearers expansion | OY-SAT, OY-QR (skeleton types in v0.3) |
|
||||
| Hub API | B2B backbone: custody, lending primitive, compliance (skeleton types in v0.3) |
|
||||
| Anchors | First institutional partners (skeleton types in v0.3) |
|
||||
| Services | Care / SIM / Vault / Mail (skeleton types in v0.3) |
|
||||
| Bond market | Full market, Growth Bonds (skeleton types in v0.3) |
|
||||
|
||||
## Phase 4 — Maturity (Years 4–5+)
|
||||
**Target**: $50–100B volume → fees auto-decline to 0.03%
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# OpenYield
|
||||
|
||||
OpenYield is a jurisdiction-light, public-good mesh for **real production** — a
|
||||
protocol organized around Holders, Stands, and the Six Principles, designed to
|
||||
hold real value without the words or the shapes that invite capture. The mesh
|
||||
runs on OY Chain (Layer 1), a canonical state layer for the Bread unit, the
|
||||
Storage Pools (Stash, Vault, Root-Pool), Standing, Watcher attestations, and
|
||||
the Pact / Council / Partner surface. It is anti-greed by construction: Mission
|
||||
Lock fixes the Six Principles and fee covenant so no council can amend them,
|
||||
and the 8% coupon cap on bonds is a mission-locked ceiling, not a parameter.
|
||||
|
||||
## The Six Principles
|
||||
|
||||
1. **Real value** — the mesh holds real production, not speculation.
|
||||
2. **Sustainability** — fees are floored and capped; the protocol cannot drain its users.
|
||||
3. **Mission-lock** — the Six Principles and fee covenant are immutable; no council can amend them.
|
||||
4. **Openness** — anyone may join; the mesh is a public good.
|
||||
5. **Ownership** — Holders own their Stash and their Reach; custody is theirs.
|
||||
6. **Self-service** — a Holder can act without a custodian; the mesh is jurisdiction-light.
|
||||
|
||||
## Bread unit & scale
|
||||
|
||||
The unit of value is **Bread**, scaled in 11 tiers: **Grain → Crumb → Bread →
|
||||
Loaf → Batch → Cake → Bakery → Granary → Mill → Harvest → Earth.**
|
||||
|
||||
## Status
|
||||
|
||||
**v0.3 (Bearers & Documentation) — in progress.** The codebase is a skeleton +
|
||||
tests layer (Go types + keeper stubs + invariant tests, zero external Go deps)
|
||||
matching the v0.1/v0.2 pre-MVP pattern. See `.ciagent/oy/ROADMAP.md` for the
|
||||
phase plan and `.ciagent/oy/PROJECT.md` for governance.
|
||||
|
||||
## Build & test
|
||||
|
||||
OpenYield is pure Go with **zero external dependencies** (`go.mod` has no
|
||||
`require` lines; `go 1.22`). From the repo root:
|
||||
|
||||
```sh
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
The docs site is [MkDocs Material](https://squidfunk.github.io/mkdocs-material/)
|
||||
(a build-only Python dep; **not** a Go dep — `go.mod` is unchanged). To
|
||||
preview locally:
|
||||
|
||||
```sh
|
||||
mkdocs serve
|
||||
# or build to a static site/ dir:
|
||||
mkdocs build
|
||||
```
|
||||
|
||||
The site lives under `docs/` (see `mkdocs.yml` for the nav). Publishing CI is
|
||||
deferred to v0.4 (D-046); v0.3 ships the source.
|
||||
|
||||
## Lexicon firewall
|
||||
|
||||
OpenYield bans 10 financial terms as standalone words (REQ-012) across all Go
|
||||
source (`x/**/*.go`) and all docs (`README.md` + `docs/**/*.md`). The banned
|
||||
terms are the words you would expect a legacy financial institution to use;
|
||||
this README and the docs describe them only by their **safe replacements**, so
|
||||
the firewall itself never trips. The firewall is enforced in code by two
|
||||
sibling Go tests:
|
||||
|
||||
- `lexicon_meta_test.go` (v0.2) — scans `x/**/*.go`.
|
||||
- `lexicon_meta_docs/lexicon_meta_docs_test.go` (v0.3) — scans `README.md` +
|
||||
`docs/**/*.md`.
|
||||
|
||||
Both use `lexicon.FindBannedTerm` (word-boundary, case-insensitive), so
|
||||
"OpenYield" is safe (word-boundary does not match the banned term inside an
|
||||
identifier) but the standalone banned term is not — docs say **"real
|
||||
production"** / **"real return"**, and a Holder's identity is **Holder** /
|
||||
**Reach**, never the banned word for a custodial position. See
|
||||
`docs/shared/lexicon.md` for the glossary of safe replacements.
|
||||
|
||||
## Governance
|
||||
|
||||
- `.ciagent/oy/PROJECT.md` — full vision, decisions (D-0xx), assumptions.
|
||||
- `.ciagent/oy/PLANS.md` — phase plans (v0.1, v0.2, v0.3).
|
||||
- `.ciagent/oy/REQUIREMENTS.md` — REQ coverage matrix.
|
||||
- `.ciagent/oy/ROADMAP.md` — release roadmap.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Anchor Preview
|
||||
|
||||
An **Anchor** (REQ-023) is the fourth and highest tier of the
|
||||
[Partner Spectrum](partner-spectrum.md) (REQ-018) — the first **institutional**
|
||||
Partner tier. Anchors are coming in v0.3 P4. This page previews what an Anchor
|
||||
is and what the v0.3 skeleton will deliver; the runtime behavior is deferred
|
||||
to v0.4+.
|
||||
|
||||
## What an Anchor is
|
||||
|
||||
An Anchor is a Partner that carries an **AnchorCredential**: a jurisdiction
|
||||
(e.g., "EU-MiCA"), a custody provider, and a set of attestation references.
|
||||
The Anchor tier is how the jurisdiction-light mesh interfaces with
|
||||
jurisdiction-bound institutional actors without becoming them. An Anchor
|
||||
holds a credential; the [Holder](../nomads/reach.md) still holds their
|
||||
[Stash](../nomads/stash.md). The mesh says **custody**, **compliance**, and
|
||||
**jurisdiction** — never the legacy institutional words banned by the
|
||||
[lexicon](../shared/lexicon.md).
|
||||
|
||||
## What is coming in v0.3 P4
|
||||
|
||||
v0.3 P4 (REQ-023) extends `x/partner` with the `AnchorCredential` struct and
|
||||
a `Partner.AnchorCredential()` accessor (returns nil for non-Anchor tiers).
|
||||
The four-tier `PartnerTier` enum (Op, Master Op, Pier, Anchor) is **unchanged**
|
||||
— v0.3 adds Anchor-specific fields, not a new tier. The custody-provider-id
|
||||
field is a by-ID-string reference to `x/hub` (the Hub API, coming in v0.3 P5),
|
||||
empty in the v0.3 skeleton because the Hub is not live until P5/v0.4. This is
|
||||
the P4→P5 ordering edge: `x/hub` in P5 references Anchor partner-ids from P4.
|
||||
|
||||
## Why Anchors matter to a Freeholder
|
||||
|
||||
A Freeholder engaging an Anchor gets a Partner with a verifiable credential
|
||||
and a custody/compliance relationship — useful for cross-jurisdiction routes
|
||||
and institutional [bonds](bonds.md). The Anchor's [Standing](standing.md) and
|
||||
attestations are visible so the Freeholder can verify the Anchor is real
|
||||
before opening a [Window](../nomads/window.md). See
|
||||
[Partner Spectrum](partner-spectrum.md) for the other three tiers, and
|
||||
[Councils & Voice](councils-voice.md) for how the Mesh Council can suspend or
|
||||
revoke an Anchor.
|
||||
|
||||
## What v0.3 does not deliver
|
||||
|
||||
The v0.3 skeleton is types + tests only (D-035): the `AnchorCredential`
|
||||
struct, the accessor, and the `ListAnchors()` keeper alias. Live custody
|
||||
routing, attestation verification, and the Hub API integration are v0.4+
|
||||
runtime work. See [Components](../reference/components.md) for the full
|
||||
module map.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Bonds
|
||||
|
||||
A **Mesh Bond** (REQ-021, vision §17) is a [Stand](stands-guilds.md)-issued
|
||||
instrument that pays a **coupon** to its holder over a term and returns the
|
||||
principal at maturity. The coupon is bounded by a **mission-locked cap and
|
||||
floor**: 8% upper cap, 0% floor (locked `CouponCapBps = 800` and
|
||||
`CouponFloorBps = 0` in `x/bond`). The cap exists so the mesh cannot become a
|
||||
speculative market; the floor exists so the coupon cannot go negative.
|
||||
|
||||
## The coupon clamp
|
||||
|
||||
The coupon is clamped to `[floor, cap]` by the `Clamp` helper in `x/bond`
|
||||
(same shape as the [Fee Covenant](../shared/six-principles.md) clamp): a
|
||||
coupon above 8% is reduced to 8%; a coupon below 0% is raised to 0%; a coupon
|
||||
in range is unchanged. The clamp is a tested invariant: below floor → floor,
|
||||
above cap → cap, in range → unchanged. This is the Mission Lock's expression
|
||||
in the capital layer.
|
||||
|
||||
## Why a cap
|
||||
|
||||
OpenYield is a public-good mesh for **real production**, not a speculation
|
||||
engine. An uncapped coupon market would let a Stand offer arbitrarily high
|
||||
coupons to attract Bread, turning the mesh into a speculative race. The 8%
|
||||
cap bounds the coupon at a level consistent with real production returns, and
|
||||
the [Mission Lock](councils-voice.md) makes the cap non-amendable — no Council
|
||||
vote can raise it. The mesh says **coupon** and **real return**, never the
|
||||
passive-value or standalone-metric words banned by the
|
||||
[lexicon](../shared/lexicon.md).
|
||||
|
||||
## The bond lifecycle
|
||||
|
||||
A Bond moves through five states (locked `BondStatus` enum in `x/bond`):
|
||||
Issued → Active → Matured, with Defaulted and Repaid as terminal paths. The
|
||||
issuer is a Stand (referenced by stand-id); the principal is denominated in
|
||||
[Grain](../shared/bread-scale.md). The bond market is governed by the
|
||||
[Stand Council](councils-voice.md) for the issuing Stand.
|
||||
|
||||
## Coming in v0.3 P5
|
||||
|
||||
v0.3 P5 (REQ-026) extends the bond market with **Growth Bonds** (a coupon that
|
||||
grows over the term, still clamped to the 8% cap) and a **secondary market**
|
||||
(Buy/Sell orders on issued bonds). The 8% / 0% consts are unchanged — the
|
||||
D-028 regression firewall guarantees v0.3 cannot alter the v0.2 mission-locked
|
||||
ceiling. See [Partner Spectrum](partner-spectrum.md) for how Partners relate
|
||||
to the bond market, and [Anchor Preview](anchor-preview.md) for the
|
||||
institutional tier.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Councils & Voice
|
||||
|
||||
OpenYield governs itself through three **Councils** (REQ-011, vision §19):
|
||||
the Mesh Council, the Guild Council, and the Stand Council. Each Freeholder
|
||||
participates through the Councils, weighted by **Voice** — a multi-source
|
||||
weight that combines [Stash](../nomads/stash.md), [Standing](standing.md),
|
||||
Vouch, Freeholder status, and Guild membership. The **Mission Lock** makes
|
||||
the covenant non-amendable: no Council can vote to change the
|
||||
[Six Principles](../shared/six-principles.md) or the fee covenant.
|
||||
|
||||
## The three Councils
|
||||
|
||||
- **Mesh Council** — the mesh-wide Council. Handles protocol-level proposals
|
||||
that affect every Holder and every [Stand](stands-guilds.md).
|
||||
- **Guild Council** — the Council for [Guilds](stands-guilds.md). Handles
|
||||
Guild-scope proposals, referenced by guild-id.
|
||||
- **Stand Council** — the Council for a single Stand, referenced by stand-id.
|
||||
Handles Stand-scope proposals (e.g., Vault use, [Bond](bonds.md) issuance).
|
||||
|
||||
The three-tier shape mirrors the three [Storage Pools](../shared/storage-pools.md):
|
||||
a Council exists at each layer where custody is held.
|
||||
|
||||
## Multi-source Voice
|
||||
|
||||
Voice is not one number. It is a weighted tally from five sources (locked as
|
||||
the `VoiceSource` enum in `x/council`): Stash, Standing, Vouch, Freeholder,
|
||||
and Guild. A Freeholder with high [Standing](standing.md) and a long-held
|
||||
Stash carries more Voice than a freshly-minted one. The
|
||||
[TallyResult](../reference/components.md) mirrors the Cosmos SDK `x/gov`
|
||||
shape so the governance layer can wire to standard tooling. The VoteOption
|
||||
enum is **Yes / No / Abstain** — there is no "no-with-veto", an anti-greed
|
||||
design choice.
|
||||
|
||||
## Mission Lock
|
||||
|
||||
The Mission Lock is a locked `const bool` in `x/council`
|
||||
(`MissionLockAmendable = false`). The Six Principles, the fee covenant
|
||||
(ceiling 0.1% / floor 0.01% / 1-Grain minimum), and the bond coupon cap
|
||||
([8% / 0%](bonds.md)) cannot be amended by any Council vote. This is the
|
||||
firewall that keeps the mesh a public good: governance can act *within* the
|
||||
covenant, never *on* the covenant.
|
||||
|
||||
## How a Freeholder participates
|
||||
|
||||
A Freeholder submits or votes on proposals in the Councils they belong to.
|
||||
Each vote is weighted by multi-source Voice; the tally follows `x/gov`
|
||||
semantics. See [Bonds](bonds.md) for the coupon cap the Mission Lock protects,
|
||||
and [Standing](standing.md) for the metric that weights a Freeholder's Voice.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Freeholders
|
||||
|
||||
A **Freeholder** is a Holder who has earned all four Freeholder signals (REQ-005):
|
||||
a 90-day [Stash](../nomads/stash.md), a [Standing](standing.md) threshold of
|
||||
4.5★ or higher in 3 categories, the Capital signal, and the Vouch signal. A
|
||||
Freeholder is the active participant in the OpenYield mesh — they sit in
|
||||
[Stands & Guilds](stands-guilds.md), vote in the three
|
||||
[Councils & Voice](councils-voice.md), issue [Bonds](bonds.md), and relate to
|
||||
the four-tier [Partner Spectrum](partner-spectrum.md).
|
||||
|
||||
## The four signals
|
||||
|
||||
The signals are the gate to Freeholder participation. They are deliberately
|
||||
heterogeneous — no single input can be pumped — so the path resists gaming:
|
||||
|
||||
- [Signals](signals.md) — the four Freeholder signals (REQ-005): 90-day Stash,
|
||||
4.5★+ in 3 categories, Capital, Vouch.
|
||||
- [Standing](standing.md) — the Bayesian anti-gaming formula (REQ-006):
|
||||
Bayesian prior + time-decay + diversity + voucher-weighting − slashes.
|
||||
- [Stands & Guilds](stands-guilds.md) — the nine Stand types (REQ-016) and
|
||||
Guilds with free Hand-Passes (REQ-017).
|
||||
- [Councils & Voice](councils-voice.md) — the three Councils and the
|
||||
non-amendable Mission Lock (REQ-011).
|
||||
- [Bonds](bonds.md) — the Mesh Bond Market, the 8% coupon cap / 0% floor
|
||||
(REQ-021).
|
||||
- [Partner Spectrum](partner-spectrum.md) — the four Partner tiers (REQ-018):
|
||||
Op, Master Op, Pier, Anchor.
|
||||
- [Anchor Preview](anchor-preview.md) — the first institutional Partner tier
|
||||
(REQ-023), coming in v0.3 P4.
|
||||
|
||||
## What a Freeholder does
|
||||
|
||||
A Freeholder is a Holder who has crossed the signal gate. From there the mesh
|
||||
opens: a Freeholder joins a [Stand](stands-guilds.md) (or forms a Guild), votes
|
||||
in the [Councils](councils-voice.md) with multi-source Voice, issues or holds
|
||||
[Bonds](bonds.md) under the mission-locked coupon cap, and engages the
|
||||
[Partner Spectrum](partner-spectrum.md) — including the Anchor tier coming in
|
||||
v0.3. The covenant is the same for every audience; the Freeholder pages
|
||||
describe how it shows up in governance and capital.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Partner Spectrum
|
||||
|
||||
OpenYield defines a four-tier **Partner Spectrum** (REQ-018, vision §13):
|
||||
**Op**, **Master Op**, **Pier**, and **Anchor**. Partners are the external
|
||||
actors a [Freeholder](index.md) interacts with through the mesh — service
|
||||
operators, route providers, and institutional bridges. The four tiers are
|
||||
locked as the `PartnerTier` enum in `x/partner` (exactly 4, regression-tested).
|
||||
|
||||
## The four tiers
|
||||
|
||||
- **Op** — a service operator. Runs a service a Holder uses through a
|
||||
[Window](../nomads/window.md) (e.g., a Maps provider). The lightest tier.
|
||||
- **Master Op** — a senior operator. Coordinates multiple Ops or runs a
|
||||
higher-trust service. "Op" is the safe short form; the full word is not
|
||||
used as a standalone term.
|
||||
- **Pier** — a routing Partner. Connects the mesh to external venues (e.g.,
|
||||
a DEX or an off-mesh service) and sources [Forex](../reference/components.md)
|
||||
rates. Piers route; they do not custody Holder value.
|
||||
- **Anchor** — the first institutional Partner tier. Carries a credential
|
||||
(jurisdiction, custody provider, attestations). See
|
||||
[Anchor Preview](anchor-preview.md) for what is coming in v0.3 P4.
|
||||
|
||||
## How Freeholders relate to Partners
|
||||
|
||||
A Freeholder authorizes a Partner to act on their behalf through a scoped,
|
||||
time-limited, revocable [Window](../nomads/window.md) — never by handing over
|
||||
custody. The Partner holds a credential, not the Holder's [Stash](../nomads/stash.md).
|
||||
A Partner's [Standing](standing.md) is visible so a Freeholder can choose an
|
||||
operator with a real history over a freshly-spun-up alternative (see
|
||||
[Maps & Pay](../nomads/maps-pay.md)).
|
||||
|
||||
## Partner status
|
||||
|
||||
Each Partner has a status (locked `PartnerStatus` enum in `x/partner`):
|
||||
Pending → Active, with Suspended and Revoked as the governance paths. The
|
||||
[Mesh Council](councils-voice.md) can suspend or revoke a Partner. The four
|
||||
tiers and the status enum are unchanged by v0.3 — v0.3 only *extends*
|
||||
`x/partner` with the Anchor credential shape (REQ-023), not a new tier.
|
||||
|
||||
See [Storage Pools](../shared/storage-pools.md) for why the mesh says
|
||||
"Holder" and "Reach" rather than the legacy custodial words, and
|
||||
[Bonds](bonds.md) for the coupon market a Partner may route to.
|
||||
@@ -0,0 +1,45 @@
|
||||
# The Four Freeholder Signals
|
||||
|
||||
The four **Freeholder signals** (REQ-005) are the gate to Freeholder
|
||||
participation. A [Holder](../nomads/reach.md) who earns all four becomes a
|
||||
[Freeholder](index.md) — eligible to join [Stands & Guilds](stands-guilds.md),
|
||||
vote in the [Councils](councils-voice.md), and issue [Bonds](bonds.md). The
|
||||
signals are deliberately heterogeneous: no single input can be pumped, so the
|
||||
path resists gaming.
|
||||
|
||||
## 1. The 90-day Stash
|
||||
|
||||
A Holder must hold a [Stash](../nomads/stash.md) continuously for 90 days
|
||||
(REQ-014). The signal is about **continuity, not size** — a small Stash held
|
||||
steadily counts. This filters out transient actors who spin up a position to
|
||||
game a vote and then leave. See [Storage Pools](../shared/storage-pools.md)
|
||||
for the three-pool model.
|
||||
|
||||
## 2. Standing of 4.5★ or higher in 3 categories
|
||||
|
||||
A Holder must earn a [Standing](standing.md) of 4.5★ or higher in **three
|
||||
distinct categories** (REQ-006). The diversity requirement is the anti-gaming
|
||||
core: a Holder cannot reach Freeholder by repeating the same action with the
|
||||
same counterparty. Three categories force breadth.
|
||||
|
||||
## 3. Capital
|
||||
|
||||
The Capital signal requires a Holder to hold a meaningful amount of
|
||||
[Bread](../shared/bread-scale.md) in their Stash. The threshold is set by the
|
||||
mesh [Councils](councils-voice.md) and is a stake, not a fee: the Holder keeps
|
||||
the Bread. Capital aligns the Freeholder's stake with the mesh.
|
||||
|
||||
## 4. Vouch
|
||||
|
||||
The Vouch signal requires another Freeholder to vouch for the Holder. A
|
||||
vouch from a high-[Standing](standing.md) Freeholder carries more weight
|
||||
(voucher-weighting), so a single colluding vouch cannot carry a Holder over
|
||||
the gate. Vouch is the social signal that ties the other three together.
|
||||
|
||||
## Why four, not one
|
||||
|
||||
Each signal covers a different attack surface: continuity (90-day Stash),
|
||||
breadth (3-category Standing), stake (Capital), and social trust (Vouch).
|
||||
Earning all four is the proof a Holder is a participant, not a transient
|
||||
gamer. See [Standing](standing.md) for the anti-gaming math, and
|
||||
[Bonds](bonds.md) for what a Freeholder can do once the signals are earned.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Bayesian Standing
|
||||
|
||||
**Standing** (REQ-006) is a Holder's measured history on the mesh — the
|
||||
anti-gaming metric that gates [Freeholder](index.md) participation and weighs
|
||||
[Voice](councils-voice.md) in the [Councils](councils-voice.md). Standing is
|
||||
not a count of transactions and not a reputation score you can farm. It is a
|
||||
Bayesian score that resists the obvious attacks: volume spam, self-dealing,
|
||||
fake vouches.
|
||||
|
||||
## The formula, at conceptual depth
|
||||
|
||||
Standing combines four signals and a penalty:
|
||||
|
||||
- **Bayesian prior + updates.** The mesh starts with a prior for each Holder
|
||||
and updates it from each observed action. A burst of activity cannot
|
||||
inflate Standing because the prior anchors it.
|
||||
- **Time-decay.** Old evidence decays, so a Holder cannot rest on a burst
|
||||
from years ago. Standing reflects *recent, sustained* real production.
|
||||
- **Diversity weighting.** A Holder who acts across many services, many
|
||||
[Stands](stands-guilds.md), and many [bearers](../nomads/bearers.md) accrues
|
||||
more Standing than one who repeats the same action with the same
|
||||
counterparty. Diversity is the anti-collusion lever.
|
||||
- **Voucher-weighting.** A vouch from a high-Standing Freeholder counts for
|
||||
more than a vouch from a low-Standing one. This makes fake vouches expensive:
|
||||
the voucher must themselves have Standing to lose.
|
||||
- **Minus slashes.** Bad behavior (failed attestations, broken Pacts) removes
|
||||
Standing. Slashes are the penalty that bounds the upside of gaming.
|
||||
|
||||
> The full sub-tables (priors, decay rates, diversity categories, slash
|
||||
> conditions) are deferred per PROJECT.md Q2. This page gives the conceptual
|
||||
> depth; the [nomads Standing page](../nomads/standing.md) gives the plain-
|
||||
> language version.
|
||||
|
||||
## Why it cannot be gamed
|
||||
|
||||
There is no single input a Holder can pump. Volume is bounded by the Bayesian
|
||||
prior; recency is bounded by time-decay; breadth is bounded by diversity;
|
||||
social trust is bounded by voucher-weighting; and any attempt that misfires
|
||||
costs Standing via slashes. The four signals (the [90-day Stash](signals.md),
|
||||
3-category threshold, Capital, Vouch) sit on top of this metric, so the
|
||||
Freeholder gate inherits the same anti-gaming property.
|
||||
|
||||
## What Standing is not
|
||||
|
||||
Standing is not a custodial position, a tier you buy, or legacy history. It
|
||||
is a measured, decayed, diversified Bayesian score. See
|
||||
[Storage Pools](../shared/storage-pools.md) for why the mesh says "Stash"
|
||||
rather than the legacy custodial words, and [Councils & Voice](councils-voice.md)
|
||||
for how Standing weights a Freeholder's vote.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Stands & Guilds
|
||||
|
||||
A **Stand** is a governed group of Holders that holds a [Vault](../shared/storage-pools.md)
|
||||
in common (REQ-016). A **Guild** is a looser association of Holders that can
|
||||
pass value among its members for free (REQ-017). Both are the organizational
|
||||
layer a [Freeholder](index.md) joins after earning the four
|
||||
[signals](signals.md).
|
||||
|
||||
## The nine Stand types
|
||||
|
||||
OpenYield defines exactly nine Stand types (REQ-016, vision §11), locked as a
|
||||
const in `x/stand`:
|
||||
|
||||
1. **Household** — a family-scale group.
|
||||
2. **Crew** — a working team.
|
||||
3. **Entity** — a single legal actor.
|
||||
4. **Co-op** — a cooperative.
|
||||
5. **Circle** — an affinity group.
|
||||
6. **Trust** — a trust arrangement.
|
||||
7. **Foundation** — a purpose-bound entity.
|
||||
8. **Confederation** — a federation of Stands.
|
||||
9. **Shadow** — a privacy-preserving Stand.
|
||||
|
||||
A Stand's decision policy (threshold or weighted, mirroring the Cosmos SDK
|
||||
`x/group` shape) governs how its Vault is used. A Stand can also issue
|
||||
[Bonds](bonds.md) — the bond issuer is a Stand, referenced by stand-id.
|
||||
|
||||
## Guilds and Hand-Passes
|
||||
|
||||
A **Guild** is a looser association: it may affiliate with a Stand or stand
|
||||
alone. Inside a Guild, a **Hand-Pass** moves [Bread](../shared/bread-scale.md)
|
||||
between members at a **0% protocol fee** (REQ-017, locked `HandPassFeeBps = 0`
|
||||
in `x/guild`). The 0% fee is mission-locked: the mesh does not tax the social
|
||||
transfer of value among a self-organized group. See the
|
||||
[Fee Covenant](../shared/six-principles.md) for the broader fee shape.
|
||||
|
||||
## How a Freeholder joins
|
||||
|
||||
A Freeholder joins a Stand by becoming a member (the Stand's policy admits
|
||||
them) or forms a Guild as a founder. Membership is recorded in `x/stand`
|
||||
and `x/guild` respectively, by stand-id / guild-id and the member's
|
||||
[Reach](../nomads/reach.md). From a Stand a Freeholder gains Vault access and
|
||||
the ability to issue [Bonds](bonds.md); from a Guild a Freeholder gains free
|
||||
Hand-Passes with other members.
|
||||
|
||||
See [Councils & Voice](councils-voice.md) for how Stands and Guilds each get a
|
||||
Council, and [Storage Pools](../shared/storage-pools.md) for the Vault layer.
|
||||
@@ -0,0 +1,30 @@
|
||||
# OpenYield
|
||||
|
||||
OpenYield is a jurisdiction-light, public-good mesh for **real production**. It
|
||||
runs on OY Chain (Layer 1), a canonical state layer for the Bread unit, the
|
||||
Storage Pools, Standing, Watcher attestations, and the Pact / Council /
|
||||
Partner surface. The mesh is anti-greed by construction: Mission Lock fixes
|
||||
the Six Principles and fee covenant so no council can amend them, and the
|
||||
coupon cap on bonds is a mission-locked ceiling, not a parameter.
|
||||
|
||||
## Audiences
|
||||
|
||||
The docs are organized by audience:
|
||||
|
||||
- **Nomads** — the everyday Holder: your Reach, your Stash, your bearers, how
|
||||
you pay (Maps-Pay), the Pacts you join, and the Window you open. See
|
||||
[Nomads](nomads/index.md).
|
||||
- **Freeholders** — the active participant: the four signals, Bayesian
|
||||
Standing, Stands & Guilds, the three Councils and Voice, the bond market,
|
||||
and the four-tier Partner Spectrum. See [Freeholders](freeholders/index.md).
|
||||
- **Shared** — concepts common to every audience: the Six Principles, the
|
||||
Bread scale, the three Storage Pools, the Watchers & Mirror, the lexicon
|
||||
glossary, and the vision overview. See [Shared](shared/index.md).
|
||||
- **Reference** — the architecture and component map. See
|
||||
[Reference](reference/architecture.md).
|
||||
|
||||
## Build the docs
|
||||
|
||||
This site is [MkDocs Material](https://squidfunk.github.io/mkdocs-material/),
|
||||
a build-only Python dep (not a Go dep). To preview locally, see the
|
||||
[README](../README.md) for build instructions.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Bearers
|
||||
|
||||
The **bearers** (REQ-019) are how a Nomad reaches the mesh. OpenYield ships
|
||||
six bearers through a single **Unified Bearer Layer**: the mesh does not
|
||||
care which bearer a Holder uses — first-to-deliver-wins, and a Nomad can
|
||||
switch bearers without switching identity. The [Mirror](../shared/watchers-mirror.md)
|
||||
mirrors the canonical state to every bearer so a Nomad can read the mesh's
|
||||
real return on any of them.
|
||||
|
||||
## The six bearers
|
||||
|
||||
| Bearer | Live in v0.2 | What it is |
|
||||
|---|---|---|
|
||||
| **Internet** | yes | the default bearer; OY Chain over the open internet. |
|
||||
| **OY-BLE** | yes | Bluetooth Low Energy; short-range, peer-to-peer, no phone plan. |
|
||||
| **OY-WiFi-Direct** | yes | WiFi Direct; local mesh without an access point. |
|
||||
| **OY-LR** | yes | Long Range radio (LoRa-class); long-distance, low-bandwidth, surveillance-resistant. |
|
||||
| **OY-SAT** | coming (v0.3 P4) | satellite; offline coverage via a satellite constellation. |
|
||||
| **OY-QR** | coming (v0.3 P4) | signed QR code; one-shot offline transfer scanned by a peer. |
|
||||
|
||||
## What this means for a Nomad
|
||||
|
||||
A Nomad does not pick "the right bearer". The four already-live bearers
|
||||
(Internet, OY-BLE, OY-WiFi-Direct, OY-LR) cover the everyday situations:
|
||||
on the open internet, in a room with another Holder, in a local group with
|
||||
no router, or kilometers away with no infrastructure. OY-SAT and OY-QR
|
||||
extend that to true-offline paths and are coming in the next phase.
|
||||
|
||||
## Surveillance resistance
|
||||
|
||||
OY-LR, OY-BLE, OY-WiFi-Direct, OY-SAT, and OY-QR are designed to be
|
||||
surveillance-resistant: a Nomad can send or receive value without a
|
||||
phone plan, a SIM, or a custodial on-ramp. The bearer is the transport; the
|
||||
[Reach](reach.md) is the identity; the [Stash](stash.md) is the storage. None
|
||||
of them depends on a custodial position.
|
||||
|
||||
## First-to-deliver-wins
|
||||
|
||||
The Unified Bearer Layer is first-to-deliver-wins: if a Nomad sends a
|
||||
transfer over two bearers at once, the mesh accepts the first one that
|
||||
arrives and drops the duplicate. This is why a Nomad can switch bearers
|
||||
mid-transfer without double-spending.
|
||||
|
||||
See [Watchers & Mirror](../shared/watchers-mirror.md) for how the canonical
|
||||
state is mirrored to every bearer, and [Maps & Pay](maps-pay.md) for how a
|
||||
Nomad uses a bearer to find and pay for services.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Nomads
|
||||
|
||||
A **Nomad** is a person using the OpenYield mesh through a **Reach** — the
|
||||
protocol-level identity a Holder uses to act on the mesh without a
|
||||
custodian, a gatekeeper, or a legacy financial position. The Nomad path
|
||||
is the entry path: a Nomad is a Holder who has a Reach and a [Stash](stash.md)
|
||||
and is on the way to earning the four Freeholder signals, but has not yet
|
||||
earned all four.
|
||||
|
||||
## The Nomad path
|
||||
|
||||
The pages here cover what a Nomad does day-to-day on the mesh:
|
||||
|
||||
- [Reach](reach.md) — the identity; the first Freeholder signal (REQ-005).
|
||||
- [Stash](stash.md) — the personal [Storage Pool](../shared/storage-pools.md)
|
||||
where a Nomad holds Bread (REQ-014).
|
||||
- [Bearers](bearers.md) — how a Nomad reaches the mesh (REQ-019): Internet,
|
||||
OY-BLE, OY-WiFi-Direct, OY-LR live now; OY-SAT and OY-QR coming.
|
||||
- [Maps & Pay](maps-pay.md) — finding services and paying for them.
|
||||
- [Pacts](pacts.md) — the six contract shapes a Nomad encounters
|
||||
(REQ-020): Pause, Ground, Stance, Cover, Stand Registry, Hub API.
|
||||
- [Standing](standing.md) — the Bayesian anti-gaming metric (REQ-006),
|
||||
and why the mesh cannot be gamed.
|
||||
- [Window](window.md) — the delegation primitive (REQ-015): scope,
|
||||
duration, rate-limit, audit-log, revoke.
|
||||
|
||||
## Where a Nomad starts
|
||||
|
||||
A Nomad starts with a Reach and a Stash — that is enough to begin. From
|
||||
there the bearers carry value to the Stash, Maps finds services, Pay and
|
||||
the Window let a Nomad use them without giving up custody, and Standing
|
||||
accrues as the Nomad acts. A Nomad who earns the 90-day Stash signal, the
|
||||
Standing threshold, the Capital signal, and the Vouch signal becomes a
|
||||
Freeholder (see the Freeholders section).
|
||||
|
||||
## Shared concepts
|
||||
|
||||
The Nomad path rests on the [shared concepts](../shared/index.md): the
|
||||
[Six Principles](../shared/six-principles.md), the [Bread scale](../shared/bread-scale.md),
|
||||
the [Storage Pools](../shared/storage-pools.md), the [Watchers & Mirror](../shared/watchers-mirror.md),
|
||||
and the [Lexicon](../shared/lexicon.md). The covenant is the same for
|
||||
every audience; the Nomad pages describe how it shows up in everyday use.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Maps & Pay
|
||||
|
||||
**Maps** and **Pay** are the day-to-day Mesh Experience a Nomad uses on the
|
||||
mesh. Maps finds services; Pay settles them. Both run over the
|
||||
[bearers](bearers.md) and read the [Mirror](../shared/watchers-mirror.md) so a
|
||||
Nomad can find and pay for a service on a surveillance-resistant bearer
|
||||
without an internet connection to OY Chain.
|
||||
|
||||
## Maps
|
||||
|
||||
Maps is the directory of services a Nomad can reach. A service is anything
|
||||
a Partner or a Stand exposes to the mesh: a Care service, a SIM, a Vault,
|
||||
a Mailbox (preview of v0.3 P5 — see [Pacts](pacts.md) for the Hub API). Maps
|
||||
is sorted by geographic proximity (REQ-007): a Nomad physically closer to a
|
||||
service or its operator is shown that service first. There is no paid
|
||||
ranking; the order is proximity, not promotion.
|
||||
|
||||
## Pay
|
||||
|
||||
Pay is how a Nomad settles a service. A payment is a transfer of Bread
|
||||
from the Nomad's [Stash](stash.md) to the service operator's Stash, signed
|
||||
by the Nomad's [Reach](reach.md). The fee covenant floors and caps the fee;
|
||||
inside a Guild, a Hand-Pass is free at the protocol level (REQ-017). Pay
|
||||
runs over any bearer, first-to-deliver-wins.
|
||||
|
||||
## Authorize, don't hand over
|
||||
|
||||
For recurring services a Nomad does not re-sign every payment. Instead
|
||||
the Nomad opens a [Window](window.md) to the service: a scoped,
|
||||
time-limited, rate-limited, revocable capability that lets the service pull
|
||||
value from the Stash within bounds the Nomad set. The Window is audited;
|
||||
the Nomad can revoke it at any time. This is the self-service principle in
|
||||
practice: the Nomad delegates a capability, not custody.
|
||||
|
||||
## Find, pay, verify
|
||||
|
||||
A Nomad's loop is:
|
||||
|
||||
1. **Find** a service on Maps.
|
||||
2. **Pay** once, or **authorize** a [Window](window.md) for recurring use.
|
||||
3. **Verify** the service against the Watcher attestations on the Mirror
|
||||
(see [Watchers & Mirror](../shared/watchers-mirror.md)).
|
||||
|
||||
See [Stash](stash.md) for where the Bread comes from, [Window](window.md)
|
||||
for the delegation primitive, and [Standing](standing.md) for how a
|
||||
service operator's history is measured.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Pacts
|
||||
|
||||
The **six Pacts** (REQ-020) are the contract shapes a Nomad encounters on
|
||||
the mesh. A Pact is a typed, mission-locked agreement between parties; the
|
||||
core terms of the Pause, Ground, and Stance Pacts are **non-amendable** —
|
||||
no Council can rewrite them after the fact. A Nomad mostly interacts with
|
||||
Pacts through [Maps & Pay](maps-pay.md) and the [Window](window.md)
|
||||
primitive, but it helps to know what each one is.
|
||||
|
||||
## The six Pacts
|
||||
|
||||
| Pact | What it does for a Nomad |
|
||||
|---|---|
|
||||
| **Pause** | A temporary hold. A Nomad can pause a recurring payment or a Window without voiding it; the Pause core terms are non-amendable. |
|
||||
| **Ground** | A baseline obligation the mesh enforces by default — the "ground rules" between a Nomad and a service operator. Non-amendable. |
|
||||
| **Stance** | A stated position a party commits to (e.g., a service operator's Stance on jurisdiction-light operation). Non-amendable. |
|
||||
| **Cover** | A flat commitment a Stand or a Partner offers to cover a Nomad against a defined failure; a Nomad reads Cover when choosing a service. |
|
||||
| **Stand Registry** | The registry of the nine [Stand](../shared/storage-pools.md) types (Household, Crew, Entity, Co-op, Circle, Trust, Foundation, Confederation, Shadow) a Nomad can join. |
|
||||
| **Hub API** | The B2B backbone (preview of v0.3 P5) — the Hub Pact exposes custody, a lending primitive, and compliance to service operators. A Nomad sees the Hub through Maps, not directly. |
|
||||
|
||||
## What a Nomad actually does with Pacts
|
||||
|
||||
A Nomad does not draft Pacts by hand. The flow is:
|
||||
|
||||
1. **Find** a service on [Maps & Pay](maps-pay.md).
|
||||
2. The service's terms are backed by one or more Pacts (e.g., a recurring
|
||||
payment is a Pause-able Window; a Stand's service is registered in the
|
||||
Stand Registry).
|
||||
3. The Nomad **authorizes** a [Window](window.md) scoped to those terms.
|
||||
|
||||
## Mission Lock
|
||||
|
||||
The Pause, Ground, and Stance core terms are mission-locked: a `const`
|
||||
flag in the Pact module marks them non-amendable, and an invariant test
|
||||
asserts that flag can never flip. A Nomad can rely on the ground rules
|
||||
not changing. See [Six Principles](../shared/six-principles.md) for the
|
||||
mission-lock covenant.
|
||||
|
||||
See [Window](window.md) for the delegation primitive the Pacts are
|
||||
delivered through, and [Standing](standing.md) for how a service
|
||||
operator's history is measured.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Reach
|
||||
|
||||
A **Nomad** is a person using the OpenYield mesh through a **Reach** — the
|
||||
protocol-level identity that lets a Holder act on the mesh without a
|
||||
custodian, a gatekeeper, or a legacy financial position. The Reach is the
|
||||
first of the four Freeholder signals (REQ-005), and it is the baseline every
|
||||
Nomad starts from: a Nomad is a Holder who has a Reach and a Stash but has not
|
||||
yet earned all four Freeholder signals.
|
||||
|
||||
## What a Reach is
|
||||
|
||||
A Reach is an identity, not a custodial position. It is the by-ID-string a
|
||||
Holder uses to receive value, open a [Window](window.md), join a Stand, or
|
||||
pay for a service. The protocol does not require KYC at the protocol layer;
|
||||
the Reach is the unit of self-service (see [Six
|
||||
Principles](../shared/six-principles.md)).
|
||||
|
||||
## How a Nomad starts
|
||||
|
||||
A Nomad starts with two things:
|
||||
|
||||
1. **A Reach** — the identity.
|
||||
2. **A [Stash](stash.md)** — the personal [Storage Pool](../shared/storage-pools.md)
|
||||
where the Holder holds Bread.
|
||||
|
||||
That pair is enough to begin. From there a Nomad can use the [bearers](bearers.md)
|
||||
to reach the mesh, find services on [Maps & Pay](maps-pay.md), authorize a
|
||||
[Window](window.md) to a partner, and accrue [Standing](standing.md).
|
||||
|
||||
## Geographic proximity
|
||||
|
||||
The mesh processes actions first-come, first-served with a
|
||||
geographic-proximity preference (REQ-007) — a Nomad physically closer to a
|
||||
service or a Stand's region is served first. The Reach is how the mesh
|
||||
identifies the Nomad for that ordering; there is no separate tier to buy into.
|
||||
|
||||
## The four Freeholder signals
|
||||
|
||||
The Reach is the first Freeholder signal. The four signals (REQ-005) are
|
||||
earned over time: the 90-day [Stash](stash.md) signal, the Standing
|
||||
threshold, the Capital signal, and the Vouch signal. A Nomad who earns all
|
||||
four becomes a Freeholder (see the Freeholders section). The pages here cover
|
||||
the Nomad path — everything up to that point.
|
||||
|
||||
See [Stash](stash.md) for the Storage Pool a Reach holds Bread in,
|
||||
[Bearers](bearers.md) for how to reach the mesh, and
|
||||
[Standing](standing.md) for the anti-gaming metric that accrues as a Nomad
|
||||
acts on the mesh.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Standing
|
||||
|
||||
**Standing** (REQ-006) is a Holder's measured history on the mesh. It is
|
||||
the anti-gaming metric: a Bayesian score with time-decay, diversity
|
||||
weighting, and voucher-weighting, minus slashes for bad behavior. For a
|
||||
Nomad, the headline is that the mesh **cannot be gamed** — Standing
|
||||
rewards real production and resists the obvious attacks (volume spam,
|
||||
self-dealing, fake vouches).
|
||||
|
||||
## What Standing is, in plain language
|
||||
|
||||
Standing is not a count of transactions. It is a Bayesian score: the mesh
|
||||
starts with a prior, updates it from each observed action, and decays
|
||||
old evidence so a Holder cannot rest on a burst of activity from years
|
||||
ago. Diversity weighting means a Nomad who acts across many services,
|
||||
many Stands, and many bearers accrues more Standing than a Nomad who
|
||||
repeats the same action with the same counterparty. Voucher-weighting
|
||||
means a vouch from a Holder with high Standing counts for more.
|
||||
|
||||
## Why it matters to a Nomad
|
||||
|
||||
A Nomad mostly reads Standing, not computes it. Two places it shows up:
|
||||
|
||||
- **Choosing a service.** Maps shows a service operator's Standing so a
|
||||
Nomad can pick an operator with a real history over a freshly-spun-up
|
||||
alternative (see [Maps & Pay](maps-pay.md)).
|
||||
- **The Freeholder path.** Earning a Standing threshold in 3 categories
|
||||
is one of the four Freeholder signals (REQ-005). A Nomad who accrues
|
||||
Standing over time is on the path to becoming a Freeholder.
|
||||
|
||||
## What Standing is not
|
||||
|
||||
Standing is not a custodial position, a tier you buy, or a reputation
|
||||
score you can farm. It is not legacy custodial history. The
|
||||
Bayesian + time-decay + diversity design is exactly what makes it hard to
|
||||
game: there is no single input a Holder can pump.
|
||||
|
||||
## The math, deferred
|
||||
|
||||
The full Bayesian formula (priors, decay rates, diversity sub-tables,
|
||||
slash conditions) is documented in the Freeholders section — a Nomad does
|
||||
not need the math to use the mesh. See
|
||||
[Six Principles](../shared/six-principles.md) for the covenant Standing
|
||||
enforces, and [Reach](reach.md) for the identity a Standing accrues to.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Stash
|
||||
|
||||
A **Stash** is a Holder's personal [Storage Pool](../shared/storage-pools.md)
|
||||
(REQ-014). It is the place a Nomad holds Bread, and it is the second thing a
|
||||
Nomad needs after a [Reach](reach.md) to begin. The Stash is the unit of
|
||||
self-service: the Holder owns it, controls it, and can delegate a scoped,
|
||||
time-limited, revocable [Window](window.md) to a partner or a service
|
||||
without giving up custody.
|
||||
|
||||
## What a Stash is
|
||||
|
||||
The Stash is the Holder-level layer of the three Storage Pools (Stash,
|
||||
Vault, Root-Pool). It is a storage layer, not a custodial position: the
|
||||
protocol holds the canonical state that records who owns what; the Holder
|
||||
holds the value. There is no custodian between a Nomad and their Stash.
|
||||
|
||||
## How a Nomad uses a Stash
|
||||
|
||||
A Nomad moves Bread into a Stash through the [bearers](bearers.md) — a
|
||||
Holder on a surveillance-resistant bearer can receive value without an
|
||||
internet connection to OY Chain. From the Stash a Nomad can:
|
||||
|
||||
- **Hold** Bread (the unit of value — see [Bread scale](../shared/bread-scale.md)).
|
||||
- **Pass** value to another Reach (the Hand-Pass, free at the protocol
|
||||
level inside a Guild).
|
||||
- **Pay** for a service via [Maps & Pay](maps-pay.md).
|
||||
- **Authorize** a [Window](window.md) so a partner or service can read the
|
||||
Stash within bounds the Holder set.
|
||||
|
||||
## The 90-day Freeholder signal
|
||||
|
||||
Holding a Stash continuously for 90 days is the first of the four
|
||||
Freeholder signals (REQ-005). The Stash does not need to hold a large
|
||||
amount — the signal is about continuity, not size. A Nomad who keeps a
|
||||
Stash for 90 days and earns the other three signals (Standing, Capital,
|
||||
Vouch) becomes a Freeholder.
|
||||
|
||||
## Delegation, not custody
|
||||
|
||||
The Stash stays the Holder's. When a Nomad opens a Window to a service,
|
||||
the service gets a scoped capability (e.g., "read Stash balance for the
|
||||
next hour", "spend up to N Grain on this service this week") — it does not
|
||||
get custody. The Window is revocable, rate-limited, and audited. See
|
||||
[Window](window.md) for the primitive.
|
||||
|
||||
See [Storage Pools](../shared/storage-pools.md) for the full three-pool
|
||||
model, and [Bearers](bearers.md) for how value reaches a Stash over a
|
||||
surveillance-resistant bearer.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Window
|
||||
|
||||
A **Window** (REQ-015) is the primitive a Nomad uses to delegate a
|
||||
capability without delegating custody. It is scoped, time-limited,
|
||||
rate-limited, audited, and revocable. A Nomad opens a Window so a partner
|
||||
or a service can act on the Nomad's [Stash](stash.md) within bounds the
|
||||
Nomad set — the partner never gets custody, and the Nomad can close the
|
||||
Window at any time.
|
||||
|
||||
## The five parts of a Window
|
||||
|
||||
| Part | What it bounds |
|
||||
|---|---|
|
||||
| **Scope** | what the grantee can do (e.g., read Stash balance, spend up to N Grain on a specific service). |
|
||||
| **Duration** | when the Window starts and ends (a start time and an end time). |
|
||||
| **Rate limit** | how many actions per duration window (e.g., at most 10 reads per hour). |
|
||||
| **Audit log** | an append-only log of every action the grantee took under the Window. |
|
||||
| **Revoke** | the Nomad can revoke the Window at any time; revoke after expiry is a no-op. |
|
||||
|
||||
## Why a Nomad opens one
|
||||
|
||||
A Nomad opens a Window for the same reason a Nomad uses [Maps & Pay](maps-pay.md):
|
||||
to let a service do something on the Nomad's behalf without handing over
|
||||
the Stash. Common examples:
|
||||
|
||||
- A recurring service (e.g., a Care service) pulls a capped amount of
|
||||
Bread from the Stash each week, within a rate limit the Nomad set.
|
||||
- A partner reads the Stash balance for a compliance check, scoped to
|
||||
read-only, time-limited to one hour.
|
||||
- A Stand operator processes a Pass-Act on the Nomad's behalf inside a
|
||||
scoped, audited Window.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A Window moves through a fixed lifecycle: **Open → Active → Revoked** or
|
||||
**Expired**. A Nomad can revoke at any point; revoking after expiry is a
|
||||
no-op (idempotent). The lifecycle is mission-locked: a partner cannot
|
||||
extend a Window past its end time — the Nomad must open a new one.
|
||||
|
||||
## Self-service, by design
|
||||
|
||||
The Window is the self-service principle in code. The protocol records
|
||||
the Window on OY Chain; the partner holds only the capability, never the
|
||||
value. See [Six Principles](../shared/six-principles.md) for the covenant,
|
||||
and [Pacts](pacts.md) for the contract shapes delivered through Windows.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Architecture
|
||||
|
||||
This is the architecture index for OpenYield. The mesh is built from 14
|
||||
modular components and 6 cross-component interfaces, with a critical blocker
|
||||
chain that fixes the build order. The full governance source lives in
|
||||
`.ciagent/oy/ARCHITECTURE.md`; this page is the user-facing rewrite, kept
|
||||
lexicon-clean by the [docs firewall](../shared/lexicon.md).
|
||||
|
||||
## The 14 modular components
|
||||
|
||||
| # | Component | Vision § | Phase |
|
||||
|---|---|---|---|
|
||||
| 1 | OY Chain & Mirror | §7 | P1 |
|
||||
| 2 | Cross-Chain & Exit | §7 | P3 |
|
||||
| 3 | Bread Unit & Root Basket | §6, §16 | P1 |
|
||||
| 4 | Bloom Engine | §6 | P1 |
|
||||
| 5 | Storage Substrate | §5 | P1 |
|
||||
| 6 | Identity, Standing & Citizenship | §8, §9 | P1 |
|
||||
| 7 | Window Primitive | §10 | P2 |
|
||||
| 8 | Pacts Suite (Pause, Ground, Stance, Cover, Stand Registry, Hub API, Bonds) | §16, §17 | P2 |
|
||||
| 9 | Mesh Experience (Maps, Pay) | §8 | P1 |
|
||||
| 10 | Organizational Primitives (Stands, Guilds) | §11, §12 | P2 |
|
||||
| 11 | Partner Spectrum & Forex | §13 | P2 |
|
||||
| 12 | Bearers & Processing Mesh | §14, §15 | P1 |
|
||||
| 13 | Fee Covenant | §18 | P1 |
|
||||
| 14 | Governance (Mesh/Guild/Stand Councils) | §19 | P2 |
|
||||
|
||||
## The 6 cross-component interfaces
|
||||
|
||||
1. **Standing API** — consumed by Identity, Window, Pacts, Orgs, Partners,
|
||||
and Governance. See [Standing](../freeholders/standing.md).
|
||||
2. **Forge / Fold Interface** — mints [Bread](../shared/bread-scale.md)
|
||||
against Root Basket assets only. See the Bloom Engine.
|
||||
3. **Watcher Attestation Interface (the Mirror)** — 9 Watchers, 6-of-9
|
||||
quorum. See [Watchers & Mirror](../shared/watchers-mirror.md).
|
||||
4. **Window Lifecycle Interface** — Holder-authorized, scope-bounded,
|
||||
revocable. See [Window](../nomads/window.md).
|
||||
5. **Fee Covenant Interface** — auto-decline, ceiling/floor enforced.
|
||||
See [Six Principles](../shared/six-principles.md).
|
||||
6. **Voice / Council Interface** — multi-source Voice, Mission Lock enforced.
|
||||
See [Councils & Voice](../freeholders/councils-voice.md).
|
||||
|
||||
## The critical blocker chain
|
||||
|
||||
The components build in a fixed order: OY Chain (1) → Bread/Root Basket (3)
|
||||
→ Storage (5) → Identity/Standing (6), which then unblocks {Window (7),
|
||||
Pacts (8), Orgs (10), Partners (11), Governance (14)}. The Fee Covenant (13)
|
||||
blocks Pacts, Orgs, Partners, and Bearers — the fee shape must exist before
|
||||
any of those can ship. v0.3 adds the Cross-Chain & Exit layer (component 2)
|
||||
and the Bearers/Partner/Bond extensions; see [Components](components.md) for
|
||||
the `x/` module map and the v0.3 phase status.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Component Map
|
||||
|
||||
This is the `x/` module map for OpenYield. Each module is a Cosmos-SDK-style
|
||||
`x/<name>/types/` package, zero external Go deps (G-006), referenced by
|
||||
ID-string across modules (G-003 — no struct imports). The map covers v0.1,
|
||||
v0.2, and v0.3 (skeleton + tests depth, D-020/D-035).
|
||||
|
||||
## v0.1 baseline (pre-MVP skeleton)
|
||||
|
||||
| Module | Vision § | REQ | Purpose |
|
||||
|---|---|---|---|
|
||||
| `x/mesh` | §7 | REQ-008 | OY Chain (Layer 1) shell |
|
||||
| `x/mirror` | §7 | REQ-004 | Mirror of canonical state to bearers |
|
||||
| `x/bread` | §4, §6 | REQ-013 | Bread unit + 11-tier scale |
|
||||
| `x/bloom` | §6 | REQ-003 | Bloom Engine (real production only) |
|
||||
| `x/forge` | §4.2 | REQ-003 | Forge/Fold minting against Root Basket |
|
||||
| `x/rootpool` | §5 | REQ-014 | Root-Pool (mesh treasury) |
|
||||
| `x/stash` | §5 | REQ-014 | Stash (Holder-level storage pool) |
|
||||
| `x/vault` | §5 | REQ-014 | Vault (Stand-level storage pool) |
|
||||
| `x/identity` | §8 | REQ-005 | Reach identity (Holder, no KYC) |
|
||||
| `x/standing` | §9.2 | REQ-006 | Bayesian Standing |
|
||||
| `x/processing` | §15 | REQ-007 | FCFS processing mesh |
|
||||
| `x/watcher` | §7 | REQ-004 | 9 Watchers, 6-of-9 quorum |
|
||||
| `x/feecovenant` | §18 | REQ-002 | Fee ceiling/floor/minimum |
|
||||
| `x/still` | §3 | — | Still/Stir pause/resume state |
|
||||
| `x/bearers` | §14 | REQ-019 | Unified Bearer Layer (6 bearers) |
|
||||
|
||||
## v0.2 (The Mesh — skeleton + tests)
|
||||
|
||||
| Module | Vision § | REQ | Purpose |
|
||||
|---|---|---|---|
|
||||
| `x/window` | §10 | REQ-015 | Window primitive (scope, rate-limit, revoke) |
|
||||
| `x/stand` | §11 | REQ-016 | Nine Stand types |
|
||||
| `x/guild` | §12 | REQ-017 | Guilds + Hand-Passes at 0% protocol fee |
|
||||
| `x/pact` | §16 | REQ-020 | Six Pacts (Pause, Ground, Stance, Cover, Stand Registry, Hub API) |
|
||||
| `x/partner` | §13 | REQ-018 | Four-tier Partner Spectrum (Op, Master Op, Pier, Anchor) |
|
||||
| `x/council` | §19 | REQ-011 | Three Councils + Mission Lock (non-amendable) |
|
||||
| `x/forex` | §13 | Forex v1 | Forex Engine v1 (pair type + oracle interface) |
|
||||
| `x/bond` | §17 | REQ-021 | Mesh Bond Market (8% cap / 0% floor clamp) |
|
||||
| `x/satellite` | §7 | REQ-009 | L2 IBC Satellite (Polygon active + 4 stubs) |
|
||||
|
||||
## v0.3 (Bearers & Documentation — in progress)
|
||||
|
||||
| Module | Vision § | REQ | Status | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `x/bridge` | §7 | REQ-010 | P4 (pending) | L2↔L1 bridge routes |
|
||||
| `x/exit` | §7 | REQ-010 | P4 (pending) | Exit routes + DEX swaps |
|
||||
| `x/bearers` (ext) | §14 | REQ-022 | P4 (pending) | OY-SAT + OY-QR transport stubs |
|
||||
| `x/partner` (ext) | §13 | REQ-023 | P4 (pending) | AnchorCredential (Anchor tier) |
|
||||
| `x/hub` | §13, §16 | REQ-024 | P5 (pending) | Hub API (Custody, Lending, Compliance) |
|
||||
| `x/services` | §13 | REQ-025 | P5 (pending) | Services (Care, SIM, Vault, Mail) |
|
||||
| `x/bond` (ext) | §17 | REQ-026 | P5 (pending) | Growth Bonds + secondary market |
|
||||
|
||||
## Notes
|
||||
|
||||
- Every module follows the same pattern: `types/types.go` + `types/types_test.go`
|
||||
(package `types`), zero external deps, by-ID-string inter-module refs (G-003).
|
||||
- Each new/extended test file includes a lexicon assertion (REQ-012); the
|
||||
project-wide meta-test (`lexicon_meta_test.go`) scans all `x/**/*.go`.
|
||||
- The docs firewall (`lexicon_meta_docs_test.go`) scans `README.md` + all
|
||||
`docs/**/*.md`. See the [architecture index](architecture.md) for the
|
||||
14-component view and the 6 cross-component interfaces.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Bread scale
|
||||
|
||||
The unit of value in OpenYield is **Bread** (REQ-013). Bread is scaled in 11
|
||||
tiers, each 1,000× the previous, so a Holder can reason about a Crumb and a
|
||||
Granary in the same mental model:
|
||||
|
||||
| Tier | Name | Multiple |
|
||||
|---|---|---|
|
||||
| 1 | **Grain** | 1 |
|
||||
| 2 | **Crumb** | 1,000 Grain |
|
||||
| 3 | **Bread** | 1,000 Crumb |
|
||||
| 4 | **Loaf** | 1,000 Bread |
|
||||
| 5 | **Batch** | 1,000 Loaf |
|
||||
| 6 | **Cake** | 1,000 Batch |
|
||||
| 7 | **Bakery** | 1,000 Cake |
|
||||
| 8 | **Granary** | 1,000 Bakery |
|
||||
| 9 | **Mill** | 1,000 Granary |
|
||||
| 10 | **Harvest** | 1,000 Mill |
|
||||
| 11 | **Earth** | 1,000 Harvest |
|
||||
|
||||
## Why 11 tiers
|
||||
|
||||
The 11-tier scale gives the mesh a single unit for everything from a
|
||||
1-Grain internal minimum (the Fee Covenant floor) to the Earth-tier totals
|
||||
held in the Root-Pool. There is no separate "small unit" and "large unit":
|
||||
the Bread scale is the unit. The 1-Grain minimum prevents dust games; the
|
||||
tier names keep human-readable values at every scale.
|
||||
|
||||
## Where Bread lives
|
||||
|
||||
Bread is held in the three [Storage Pools](storage-pools.md): the Stash
|
||||
(Holder-level), the Vault (Stand-level), and the Root-Pool (treasury). The
|
||||
Watchers attest to the state of the pools daily; the Mirror mirrors the
|
||||
canonical state to the bearers. See [Watchers & Mirror](watchers-mirror.md)
|
||||
for the attestation layer.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Shared concepts
|
||||
|
||||
The Shared section holds the concepts common to every OpenYield audience —
|
||||
Nomads and Freeholders alike. These are the covenant-level ideas that make
|
||||
OpenYield a public-good mesh rather than a custodial platform.
|
||||
|
||||
- [Six Principles](six-principles.md) — the immutable covenant (REQ-001).
|
||||
- [Bread Scale](bread-scale.md) — the unit of value and its 11 tiers (REQ-013).
|
||||
- [Storage Pools](storage-pools.md) — the three pools (Stash, Vault, Root-Pool) (REQ-014).
|
||||
- [Watchers & Mirror](watchers-mirror.md) — the 9 Watchers, 6-of-9 quorum, the Mirror (REQ-004).
|
||||
- [Lexicon](lexicon.md) — why 10 terms are banned, and what to say instead (REQ-012).
|
||||
- [Vision](vision.md) — the OpenYield covenant in brief.
|
||||
|
||||
See the [README](../index.md) for build instructions, or the
|
||||
[Nomads](../nomads/index.md) and [Freeholders](../freeholders/index.md)
|
||||
sections for audience-specific docs.
|
||||
@@ -0,0 +1,148 @@
|
||||
# Lexicon
|
||||
|
||||
OpenYield bans 10 financial terms as standalone words (REQ-012). The firewall
|
||||
scans every Go file under `x/` and every Markdown file under `README.md` +
|
||||
`docs/`, and fails the build on any standalone occurrence. This page documents
|
||||
**why** the terms are banned and **what to say instead** — the replacements,
|
||||
not the banned literals.
|
||||
|
||||
## Why a lexicon
|
||||
|
||||
The words a legacy financial institution uses carry the shapes of that
|
||||
institution: custodial positions, jurisdiction-bound units, and
|
||||
speculation-language. OpenYield is a jurisdiction-light, public-good mesh for
|
||||
real production; using the old words would import the old shapes. The
|
||||
lexicon firewall keeps the mesh's language aligned with its covenant. The
|
||||
firewall is enforced in code by two sibling Go tests
|
||||
(`lexicon_meta_test.go` for `x/**/*.go`;
|
||||
`lexicon_meta_docs/lexicon_meta_docs_test.go` for `README.md` +
|
||||
`docs/**/*.md`), both using `lexicon.FindBannedTerm` (word-boundary,
|
||||
case-insensitive). Word-boundary matching means "OpenYield" is safe — the
|
||||
firewall bans standalone words, not substrings.
|
||||
|
||||
## The 10 banned terms and their safe replacements
|
||||
|
||||
The firewall bans 10 standalone words. This page does not write the banned
|
||||
words as literals (the firewall scans this page); it describes them by the
|
||||
concept each belongs to, and gives the safe replacement.
|
||||
|
||||
### 1. The custodial-position word
|
||||
|
||||
A legacy institution holds your value in a custodial position. OpenYield
|
||||
does not: a Holder owns their **Stash**, a Stand owns its **Vault**, the mesh
|
||||
owns the **Root-Pool**. The Holder's identity is a **Reach**, and the Holder
|
||||
themselves is a **Holder** — never the banned custodial-position word.
|
||||
|
||||
- Banned: the word for a custodial position.
|
||||
- Safe: **Holder**, **Reach**, **Stash**, **Vault**, **Root-Pool**.
|
||||
|
||||
### 2. The legacy-institution word
|
||||
|
||||
The legacy financial institution itself is banned as a concept. OpenYield is
|
||||
a **mesh**, a **public good**, a **protocol** — not that word.
|
||||
|
||||
- Banned: the word for a legacy financial institution.
|
||||
- Safe: **mesh**, **protocol**, **public good**.
|
||||
|
||||
### 3. The place-value word
|
||||
|
||||
The word for a place to hold value under custody is banned. Use the
|
||||
**Stash** (Holder-level), the **Vault** (Stand-level), or the **Root-Pool**
|
||||
(treasury).
|
||||
|
||||
- Banned: the word for a place value is held.
|
||||
- Safe: **Stash**, **Vault**, **Root-Pool**, **Storage Pools**.
|
||||
|
||||
### 4. The put-in word
|
||||
|
||||
The verb for putting value into a custodial position is banned. Use **hold**,
|
||||
**store**, **move**, or **transfer**.
|
||||
|
||||
- Banned: the verb for placing value under custody.
|
||||
- Safe: **hold**, **store**, **move**, **transfer**, **Pass-Act**.
|
||||
|
||||
### 5. The passive-value word
|
||||
|
||||
The word for value earned passively on a custodial position is banned. For
|
||||
bonds, use **coupon**. For the mesh's metric, use **real production** or
|
||||
**real return**.
|
||||
|
||||
- Banned: the word for passive value on a custodial position.
|
||||
- Safe: **coupon**, **real production**, **real return**.
|
||||
|
||||
### 6. The standalone metric word
|
||||
|
||||
The standalone word for a return metric is banned (it is the same concept as
|
||||
#5 in verb form). Use **real production**, **real return**, or **coupon**
|
||||
(for bonds). "OpenYield" is safe — word-boundary matching does not flag the
|
||||
banned term inside an identifier.
|
||||
|
||||
- Banned: the standalone return-metric word.
|
||||
- Safe: **real production**, **real return**, **coupon**. **OpenYield** is safe.
|
||||
|
||||
### 7. The medium-of-exchange word
|
||||
|
||||
The word for a national medium of exchange is banned. The mesh's unit is
|
||||
**Bread** (see [Bread scale](bread-scale.md)). For a foreign-exchange pair,
|
||||
use **Forex** (allowed) with **base-asset** / **quote-asset** labels, or
|
||||
**Bread / Asset**.
|
||||
|
||||
- Banned: the word for a national medium of exchange.
|
||||
- Safe: **Bread**, **asset**, **Forex**, **base-asset**, **quote-asset**.
|
||||
|
||||
### 8. The first national-unit word
|
||||
|
||||
The word for the first major national unit is banned. Use **Bread** or
|
||||
opaque chain names (e.g., "Polygon", "OY-Chain").
|
||||
|
||||
- Banned: the first national-unit word.
|
||||
- Safe: **Bread**, **asset**, chain names.
|
||||
|
||||
### 9. The second national-unit word
|
||||
|
||||
The word for the second major national unit is banned (the firewall bans it
|
||||
as a standalone word; "european" is safe by word-boundary). Use **Bread** or
|
||||
opaque chain names.
|
||||
|
||||
- Banned: the second national-unit word.
|
||||
- Safe: **Bread**, **asset**, chain names. **European** is safe (word-boundary).
|
||||
|
||||
### 10. The set-aside word
|
||||
|
||||
The word for value set aside under custody is banned. Use **Stash**,
|
||||
**Vault**, or **Root-Pool**.
|
||||
|
||||
- Banned: the word for value set aside.
|
||||
- Safe: **Stash**, **Vault**, **Root-Pool**.
|
||||
|
||||
### 11. The holder-of-value word
|
||||
|
||||
The word for the person who holds value under custody at a legacy
|
||||
institution is banned. Use **Holder**, **Freeholder**, or **Nomad**.
|
||||
|
||||
- Banned: the word for a custodial-position holder.
|
||||
- Safe: **Holder**, **Freeholder**, **Nomad**, **Reach**.
|
||||
|
||||
> **Note**: the firewall bans 10 standalone words; this page lists 11
|
||||
> replacements because two of the banned words (the passive-value word and
|
||||
> the standalone metric word) share a concept and get the same replacement
|
||||
> family (**coupon** / **real production** / **real return**).
|
||||
|
||||
## How the firewall works
|
||||
|
||||
The firewall uses `lexicon.FindBannedTerm` — a word-boundary, case-insensitive
|
||||
regex match — so:
|
||||
|
||||
- "OpenYield" is **safe**: the standalone banned term inside an identifier
|
||||
does not match (word-boundary).
|
||||
- "european" is **safe**: the standalone national-unit word inside a larger
|
||||
word does not match.
|
||||
- The standalone banned word in prose **is** matched and fails the build.
|
||||
|
||||
The firewall's own source (`lexicon/lexicon.go`) assembles the banned terms
|
||||
at runtime from two-character fragments, so the firewall's own code does not
|
||||
contain any banned term as a literal substring. The two sibling meta-tests
|
||||
(`lexicon_meta_test.go` and `lexicon_meta_docs/lexicon_meta_docs_test.go`)
|
||||
each include a self-test table that verifies detection of all 10 banned
|
||||
terms from the single source `lexicon.BannedTerms()` (G-014 drift
|
||||
prevention).
|
||||
@@ -0,0 +1,54 @@
|
||||
# Six Principles
|
||||
|
||||
The Six Principles are the immutable covenant of OpenYield (REQ-001). They
|
||||
are **Mission-locked**: no Council can amend them, and the fee covenant is
|
||||
locked alongside them. The mesh exists to hold real production, not
|
||||
speculation; everything else follows from that.
|
||||
|
||||
## 1. Real value
|
||||
|
||||
The mesh holds **real production**. The Bread unit is the unit of real value
|
||||
held in the Storage Pools; the bond market caps coupons so the mesh cannot
|
||||
become a speculation engine. "Real return" is the metric, not a nominal rate.
|
||||
|
||||
## 2. Sustainability
|
||||
|
||||
Fees are floored and capped. The fee covenant fixes a ceiling and a floor
|
||||
(see the Fee Covenant module), and the 1-Grain internal minimum prevents dust
|
||||
games. The protocol cannot drain its users, and it cannot starve its
|
||||
Watchers.
|
||||
|
||||
## 3. Mission-lock
|
||||
|
||||
The Six Principles and the fee covenant are immutable. No Council — Mesh,
|
||||
Guild, or Stand — can amend them. Mission Lock is a `const` in the council
|
||||
module, and an invariant test asserts it can never be set to amendable. The
|
||||
coupon cap on bonds is a mission-locked ceiling, not a parameter a Council
|
||||
can tune.
|
||||
|
||||
## 4. Openness
|
||||
|
||||
Anyone may join. The mesh is a public good. A Holder needs only a Reach (an
|
||||
identity) and a Stash (a storage pool) to begin; there is no gatekeeper and
|
||||
no custodian.
|
||||
|
||||
## 5. Ownership
|
||||
|
||||
Holders own their Stash and their Reach. Custody is theirs: the Stash is the
|
||||
Holder-level storage pool, the Vault is the Stand-level pool, and the
|
||||
Root-Pool is the treasury. The protocol does not custody user value; it
|
||||
holds the canonical state that records who owns what.
|
||||
|
||||
## 6. Self-service
|
||||
|
||||
A Holder can act without a custodian. The Window primitive lets a Holder
|
||||
delegate a scope-bounded, time-limited, revocable capability to a partner or
|
||||
a service; the bearers (OY-LR, OY-BLE, OY-WiFi-Direct, OY-SAT, OY-QR) let a
|
||||
Holder reach the mesh without a phone plan or a custodial on-ramp. The mesh
|
||||
is jurisdiction-light by design.
|
||||
|
||||
---
|
||||
|
||||
See the [Vision](vision.md) for the covenant in brief, or the
|
||||
[Lexicon](lexicon.md) for why the docs say "real production" and "Holder"
|
||||
rather than the words a legacy financial institution would use.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Storage Pools
|
||||
|
||||
OpenYield has three Storage Pools (REQ-014). Each is a layer of custody
|
||||
responsibility, and none of them is a custodial position — the protocol holds
|
||||
the canonical state that records who owns what; the Holder, the Stand, and
|
||||
the mesh treasury each hold their own pool.
|
||||
|
||||
| Pool | Level | Held by | Purpose |
|
||||
|---|---|---|---|
|
||||
| **Stash** | Holder | a single Holder | the personal storage pool; the unit of self-service |
|
||||
| **Vault** | Stand | a Stand (a governed group) | the Stand-level pool; the unit of shared ownership |
|
||||
| **Root-Pool** | Mesh | the mesh treasury | the canonical treasury; the unit of the public good |
|
||||
|
||||
## The Stash
|
||||
|
||||
The Stash is the Holder-level storage pool. A Holder needs only a Reach (an
|
||||
identity) and a Stash to begin. The Stash is the unit of self-service: the
|
||||
Holder owns it, controls it, and can delegate a scoped, time-limited,
|
||||
revocable Window to a partner or a service without giving up custody. See
|
||||
[Watchers & Mirror](watchers-mirror.md) for the attestation layer that
|
||||
records Stash state.
|
||||
|
||||
## The Vault
|
||||
|
||||
The Vault is the Stand-level storage pool. A Stand is a governed group
|
||||
(one of the nine Stand types: Household, Crew, Entity, Co-op, Circle,
|
||||
Trust, Foundation, Confederation, Shadow) that holds a Vault in common. The
|
||||
Stand's decision policy (threshold or weighted, mirroring the Cosmos SDK
|
||||
`x/group` shape) governs how the Vault is used. See the Freeholders section
|
||||
for Stands & Guilds.
|
||||
|
||||
## The Root-Pool
|
||||
|
||||
The Root-Pool is the mesh treasury. It holds the canonical state of the
|
||||
Bread unit, the Watcher bonds, and the Root Basket. The Root-Pool is the
|
||||
unit of the public good: the Watchers attest to its state daily, and the
|
||||
Mirror mirrors it to the bearers so a Holder can verify the mesh's real
|
||||
return without trusting a single custodian.
|
||||
|
||||
## Custody, not custody
|
||||
|
||||
The three pools are storage layers, not custodial positions. The protocol
|
||||
does not custody user value; it holds the canonical state that records who
|
||||
owns what. A Holder's Stash is theirs; a Stand's Vault is the Stand's; the
|
||||
Root-Pool is the mesh's. The Window primitive lets a Holder delegate a
|
||||
capability without delegating custody. See the [Lexicon](lexicon.md) for
|
||||
why the docs say "Stash", "Vault", and "Root-Pool" rather than the words a
|
||||
legacy financial institution would use.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Vision
|
||||
|
||||
OpenYield is a jurisdiction-light, public-good mesh for **real production**.
|
||||
The vision is a covenant, not a product: the mesh holds real value, the Six
|
||||
Principles are immutable, and the protocol cannot become a custodial
|
||||
platform. This page is the brief overview; the full vision source lives in
|
||||
`.ciagent/oy/PROJECT.md`.
|
||||
|
||||
## The covenant
|
||||
|
||||
OpenYield exists to hold **real production** — the real return of real work,
|
||||
held in the Bread unit, in the three Storage Pools, attested by the Watchers,
|
||||
mirrored by the Mirror. The covenant is anti-greed by construction:
|
||||
|
||||
- **Mission Lock** fixes the Six Principles and the fee covenant. No Council
|
||||
— Mesh, Guild, or Stand — can amend them. The coupon cap on bonds is a
|
||||
mission-locked ceiling, not a parameter.
|
||||
- **Jurisdiction-light** — the bearers (OY-LR, OY-BLE, OY-WiFi-Direct, OY-SAT,
|
||||
OY-QR) let a Holder reach the mesh without a phone plan or a custodial
|
||||
on-ramp. A Holder needs only a Reach and a Stash to begin.
|
||||
- **Public good** — the mesh is open to all. The Watchers attest daily; the
|
||||
Mirror mirrors the state; anyone can verify the mesh's real return without
|
||||
trusting a single custodian.
|
||||
|
||||
## The layers
|
||||
|
||||
1. **OY Chain** (Layer 1) — the canonical state: the Bread unit, the
|
||||
Storage Pools, Standing, Watcher attestations, the Pact / Council /
|
||||
Partner surface.
|
||||
2. **Satellites** (Layer 2) — wrapped Bread propagates to satellite chains
|
||||
(Polygon active; Base, Arbitrum, Optimism, Solana as enum placeholders)
|
||||
via IBC.
|
||||
3. **Bearers** — the surveillance-resistant transport layer: OY-LR (LoRa,
|
||||
long-range), OY-BLE (Bluetooth), OY-WiFi-Direct, OY-SAT (satellite),
|
||||
OY-QR (paper / QR code). The Mirror mirrors canonical state to them.
|
||||
4. **Exits** — the Layer 3 exit layer: Holder-initiated DEX swaps and
|
||||
off-mesh service exits, with bridge routes for cross-chain exits.
|
||||
|
||||
## The actors
|
||||
|
||||
- **Holders** (Nomads) — the everyday participants, each with a Reach and a
|
||||
Stash.
|
||||
- **Freeholders** — the active participants who run Stands, Guilds, and
|
||||
Councils.
|
||||
- **Partners** — the four-tier spectrum (Op, MasterOp, Pier, Anchor) that
|
||||
processes Pass-Acts and provides credentials and institutional backing.
|
||||
- **Watchers** — the 9 attesters with 6-of-9 quorum and 100,000 Bread bonds.
|
||||
|
||||
## The units
|
||||
|
||||
- **Bread** — the unit of real value (see [Bread scale](bread-scale.md)).
|
||||
- **Standing** — the reputation layer (the four signals, Bayesian Standing).
|
||||
- **Voice** — the governance input (multi-source: Stash, Standing, Vouch,
|
||||
Freeholder, Guild).
|
||||
- **Coupon** — the bond-market term (capped at 8% / floored at 0%, mission-locked).
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Six Principles](six-principles.md) — the immutable covenant.
|
||||
- [Storage Pools](storage-pools.md) — the three pools.
|
||||
- [Watchers & Mirror](watchers-mirror.md) — the attestation layer.
|
||||
- [Lexicon](lexicon.md) — why the docs say "real production" and "Holder".
|
||||
- [README](../../README.md) — build & test instructions.
|
||||
- `.ciagent/oy/PROJECT.md` — the full vision source.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Watchers & Mirror
|
||||
|
||||
OpenYield is attested by **9 Watchers** with a **6-of-9 quorum** (REQ-004).
|
||||
The Watchers make daily attestations to the canonical state, and each posts
|
||||
a 100,000 Bread bond. The **Mirror** mirrors the canonical state to the
|
||||
bearers so a Holder can verify the mesh's state without trusting a single
|
||||
Watcher.
|
||||
|
||||
## The 9 Watchers
|
||||
|
||||
The Watchers are the attestation layer of the mesh. There are exactly 9, and
|
||||
the quorum is 6-of-9: any 6 Watchers can attest to a state transition, but no
|
||||
5 can. Each Watcher posts a 100,000 Bread bond, which is at risk if the
|
||||
Watcher attests to a false state. The 9/6 split is a mission-locked
|
||||
parameter — no Council can lower the quorum or the bond.
|
||||
|
||||
## Daily attestations
|
||||
|
||||
The Watchers attest to the state of the three [Storage Pools](storage-pools.md)
|
||||
daily: the Stash totals, the Vault totals, and the Root-Pool. The
|
||||
attestation is a signed statement that the canonical state recorded by OY
|
||||
Chain matches the state the Watcher observed. A Holder who wants to verify
|
||||
the mesh's real return can read the attestations and check that the
|
||||
Watchers agree.
|
||||
|
||||
## The Mirror
|
||||
|
||||
The Mirror mirrors the canonical state to the bearers (OY-LR, OY-BLE,
|
||||
OY-WiFi-Direct, OY-SAT, OY-QR). A Holder on a surveillance-resistant bearer
|
||||
can read the mirrored state without an internet connection to OY Chain; the
|
||||
Mirror is the read-side of the bearer layer. The Mirror is read-only: it
|
||||
mirrors state, it does not author it. Authoritative state lives on OY Chain
|
||||
and is attested by the Watchers.
|
||||
|
||||
## Why 6-of-9
|
||||
|
||||
The 9/6 split is a balance: 9 is large enough that no single adversary can
|
||||
easily capture a quorum, and 6 is large enough that no small cabal can
|
||||
attest to a false state. The 100,000 Bread bond per Watcher makes
|
||||
capturing a quorum expensive. The split is locked by Mission Lock — no
|
||||
Council can change it. See [Six Principles](six-principles.md) for the
|
||||
mission-lock covenant.
|
||||
@@ -0,0 +1,316 @@
|
||||
// Package lexicon_meta_docs holds the docs lexicon firewall (REQ-028, D-043).
|
||||
//
|
||||
// It is a NEW sibling meta-test created in v0.3 P1 Wave 1 that MIRRORS the v0.2
|
||||
// project-wide firewall (lexicon_meta_test.go, package lexicon_meta) but scans
|
||||
// the docs surface (README.md + docs/**/*.md) instead of x/**/*.go. It uses
|
||||
// the SAME lexicon.FindBannedTerm (word-boundary, case-insensitive) — NO
|
||||
// detection reimplementation — so the two firewalls share a single source of
|
||||
// truth for the 10 banned terms (bank, deposit, interest, yield, currency,
|
||||
// dollar, euro, account, savings, depositor).
|
||||
//
|
||||
// Placement: this file lives in lexicon_meta_docs/ (a subdirectory of the
|
||||
// repo root) because Go does not permit two distinct packages in the same
|
||||
// directory; the v0.2 firewall is package lexicon_meta at the repo root.
|
||||
// The invocation `go test ./lexicon_meta_docs/...` (PLANS P1-03-01) resolves
|
||||
// to this package. Run via `go test ./...` from the repo root as well.
|
||||
//
|
||||
// G-013 walk-coverage: TestLexiconMetaDocsWalkCoverage injects a synthetic
|
||||
// banned-term .md into a temp docs/ subtree and asserts the walk FINDS it.
|
||||
// This closes the "silently scans nothing and reports green" failure mode
|
||||
// that the G-009 self-test table (detection) alone does not cover.
|
||||
//
|
||||
// G-014 self-test drift: the self-test table and banned-term count assertion
|
||||
// reuse lexicon.BannedTerms() (the single source). A cross-reference comment
|
||||
// keeps this file's table in lockstep with lexicon_meta_test.go's table; if
|
||||
// a banned term is added, both firewalls update from one place.
|
||||
package lexicon_meta_docs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
)
|
||||
|
||||
// repoRoot returns the absolute path to the repo root by walking up from
|
||||
// this test file (the test lives at <repoRoot>/lexicon_meta_docs/).
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/lexicon_meta_docs/lexicon_meta_docs_test.go
|
||||
// repo root = filepath.Dir(filepath.Dir(file))
|
||||
return filepath.Dir(filepath.Dir(file))
|
||||
}
|
||||
|
||||
// thisFile returns the absolute path of this meta-test file (to exclude it
|
||||
// from its own scan — it references banned terms via the lexicon package,
|
||||
// whose source assembles terms from fragments, so no banned-term literal
|
||||
// appears in the firewall's own code).
|
||||
func thisFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// TestLexiconMetaDocsNoBannedTermsInDocs is the docs firewall (D-043). It
|
||||
// walks README.md (repo root) + every *.md under docs/ (recursive), reads each
|
||||
// file's source, and asserts no banned term is present (word-boundary,
|
||||
// case-insensitive). Excludes .ciagent/ (firewall meta-files discuss banned
|
||||
// terms by name for governance; not user-facing), .git/ (VCS), and this test
|
||||
// file itself (self-exclusion via runtime.Caller(0)).
|
||||
//
|
||||
// Passes at P1 Wave 1 with zero docs (a walk that scans nothing reports green
|
||||
// on zero hits — closed by TestLexiconMetaDocsWalkCoverage below). With the
|
||||
// Wave 2 docs present (README + index + 6 shared pages), all are lexicon-clean
|
||||
// by construction.
|
||||
func TestLexiconMetaDocsNoBannedTermsInDocs(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
this := thisFile(t)
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
base := filepath.Base(path)
|
||||
if base == ".ciagent" || base == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Self-exclusion: skip this meta-test file.
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
// Only scan .md files.
|
||||
if !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
// Only scan README.md (repo root) + docs/**/*.md.
|
||||
rel, rerr := filepath.Rel(root, path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if rel != "README.md" && !strings.HasPrefix(rel, "docs"+string(filepath.Separator)) && rel != "docs" {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
hits = append(hits, rel+" contains banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
t.Errorf("REQ-028 docs lexicon firewall violations:\n %s",
|
||||
strings.Join(hits, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaDocsSelfTestTable (G-009 for docs) is the firewall's own
|
||||
// detection-coverage guard. Each synthetic string embeds exactly one banned
|
||||
// term in a plausible sentence context and is asserted to trigger detection,
|
||||
// so the firewall's detection logic is durably verified — if detection ever
|
||||
// breaks, this test fails before the firewall silently passes a real
|
||||
// violation in a docs page.
|
||||
//
|
||||
// G-014 self-test drift: this table is the docs mirror of the
|
||||
// TestLexiconMetaSelfTestTable in lexicon_meta_test.go (package lexicon_meta).
|
||||
// Both reuse lexicon.BannedTerms() as the single source for the 10 terms, so
|
||||
// a future addition updates both firewalls from one place. The synthetic
|
||||
// strings are assembled from lexicon.BannedTerms() fragments so this file
|
||||
// does not contain any banned term as a literal substring (it would otherwise
|
||||
// trip its own scan; the meta-test file is also excluded from its own scan,
|
||||
// but the self-test keeps the source clean for readability/searchability).
|
||||
//
|
||||
// CROSS-REFERENCE: keep this table aligned with
|
||||
//
|
||||
// lexicon_meta_test.go :: TestLexiconMetaSelfTestTable
|
||||
//
|
||||
// Any change to the synthetic-string construction must be mirrored in both
|
||||
// files (or, preferably, add a shared helper in the lexicon package — see
|
||||
// G-014 minimum-viable: cross-reference comment + shared BannedTerms()).
|
||||
func TestLexiconMetaDocsSelfTestTable(t *testing.T) {
|
||||
terms := lexicon.BannedTerms()
|
||||
// The spec lists 10 banned terms (plan docs say "9", counting dollar/euro
|
||||
// as a pair): bank, deposit, interest, yield, currency, dollar, euro,
|
||||
// account, savings, depositor.
|
||||
if len(terms) != 10 {
|
||||
t.Fatalf("BannedTerms() len = %d, want 10", len(terms))
|
||||
}
|
||||
// Each synthetic string embeds exactly one banned term in a plausible
|
||||
// sentence context. Each must be detected.
|
||||
synthetic := []string{
|
||||
"open a " + terms[0] + " here", // bank
|
||||
"make a " + terms[1] + " now", // deposit
|
||||
"compounding " + terms[2] + " rate", // interest
|
||||
"the " + terms[3] + " is 5pct", // yield
|
||||
"foreign " + terms[4] + " pair", // currency
|
||||
"price in " + terms[5], // dollar
|
||||
"price in " + terms[6], // euro
|
||||
"freeze the " + terms[7], // account
|
||||
"move to " + terms[8] + " now", // savings
|
||||
"the " + terms[9] + " lost money", // depositor
|
||||
}
|
||||
if len(synthetic) != len(terms) {
|
||||
t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms))
|
||||
}
|
||||
for i, s := range synthetic {
|
||||
found, ok := lexicon.FindBannedTerm(s)
|
||||
if !ok {
|
||||
t.Errorf("G-009 docs self-test [%d]: synthetic string did not trigger detection: %q", i, s)
|
||||
continue
|
||||
}
|
||||
if found != terms[i] {
|
||||
t.Errorf("G-009 docs self-test [%d]: detected %q, want %q (in %q)", i, found, terms[i], s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaDocsBannedTermsCount asserts exactly 10 banned terms are
|
||||
// configured (locked-const for the firewall's scope; spec lists 10, plan docs
|
||||
// say "9" counting dollar/euro as a pair). Derived from lexicon.BannedTerms()
|
||||
// — the single source — so a count change breaks both this firewall and the
|
||||
// v0.2 x/*.go firewall (G-014 drift prevention).
|
||||
func TestLexiconMetaDocsBannedTermsCount(t *testing.T) {
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) != 10 {
|
||||
t.Errorf("BannedTerms() len = %d, want 10 (REQ-012/REQ-028)", len(terms))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, tr := range terms {
|
||||
if seen[tr] {
|
||||
t.Errorf("duplicate banned term %q", tr)
|
||||
}
|
||||
seen[tr] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaDocsNoFalsePositiveOnOpenYield asserts the module name
|
||||
// "openyield" does NOT trigger the "yield" banned term and "european" does
|
||||
// NOT trigger the "euro" banned term (word-boundary matching must not match
|
||||
// substrings of identifiers). This is the regression firewall for the
|
||||
// word-boundary detection design — mirrors the v0.2
|
||||
// TestLexiconMetaNoFalsePositiveOnOpenYield.
|
||||
func TestLexiconMetaDocsNoFalsePositiveOnOpenYield(t *testing.T) {
|
||||
cases := []string{
|
||||
"github.com/oy/openyield/x/window/types",
|
||||
"package openyield",
|
||||
"openyield is the module",
|
||||
"european resident",
|
||||
"# OpenYield docs",
|
||||
"the OpenYield mesh",
|
||||
}
|
||||
for _, s := range cases {
|
||||
if _, ok := lexicon.FindBannedTerm(s); ok {
|
||||
t.Errorf("false positive: %q triggered a banned term (word-boundary must avoid this)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaDocsWalkCoverage (G-013) is the walk-coverage firewall. The
|
||||
// G-009 self-test table (above) verifies DETECTION (FindBannedTerm on
|
||||
// synthetic strings) but NOT the WALK (which files are scanned). A walk bug
|
||||
// — e.g. wrong path prefix, missing docs/ recursion, a typo in the .md
|
||||
// suffix check — would silently scan nothing and report green on zero
|
||||
// files. This test closes that gap by injecting a synthetic banned-term .md
|
||||
// into a fixture dir under the real docs/ path the walk scans and asserting
|
||||
// the walk FINDS it.
|
||||
//
|
||||
// The fixture is created under docs/.lexicon_fixture/ (a real docs/ subtree
|
||||
// the walk reaches) and removed via defer so it never leaks into the repo.
|
||||
// If the walk logic misses the fixture, this test fails loudly instead of
|
||||
// letting a broken walk pass the firewall green on zero files scanned.
|
||||
func TestLexiconMetaDocsWalkCoverage(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
this := thisFile(t)
|
||||
|
||||
// Build a synthetic banned term from fragments so THIS file does not
|
||||
// contain a banned-term literal (it is excluded from its own scan, but
|
||||
// the synthetic stays clean for readability/searchability).
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) == 0 {
|
||||
t.Fatal("BannedTerms() returned no terms — cannot run walk-coverage")
|
||||
}
|
||||
// Use the first banned term ("bank") assembled from two halves.
|
||||
syntheticTerm := terms[0][:2] + terms[0][2:] // reassemble (no literal in source)
|
||||
badContent := []byte("# fixture\nthis file contains a banned term: " + syntheticTerm + "\n")
|
||||
|
||||
fixtureDir := filepath.Join(root, "docs", ".lexicon_fixture")
|
||||
fixtureFile := filepath.Join(fixtureDir, "bad_fixture.md")
|
||||
if err := os.MkdirAll(fixtureDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir fixture: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(fixtureDir)
|
||||
if err := os.WriteFile(fixtureFile, badContent, 0o644); err != nil {
|
||||
t.Fatalf("write fixture: %v", err)
|
||||
}
|
||||
|
||||
// Run the SAME walk logic as TestLexiconMetaDocsNoBannedTermsInDocs and
|
||||
// assert it FINDS the fixture's banned term. A walk that returns zero
|
||||
// hits here proves the walk logic is broken (the fixture is a known-bad
|
||||
// file inside docs/ that MUST be detected).
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
base := filepath.Base(path)
|
||||
if base == ".ciagent" || base == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
rel, rerr := filepath.Rel(root, path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if rel != "README.md" && !strings.HasPrefix(rel, "docs"+string(filepath.Separator)) {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
hits = append(hits, rel+" contains banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
// Assert the fixture was found. The rel path uses OS-specific separator;
|
||||
// match on the suffix so the test is portable.
|
||||
foundFixture := false
|
||||
for _, h := range hits {
|
||||
if strings.Contains(h, "bad_fixture.md") && strings.Contains(h, syntheticTerm) {
|
||||
foundFixture = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFixture {
|
||||
t.Errorf("G-013 walk-coverage: the walk did NOT find the synthetic banned-term fixture at %s — the docs firewall walk logic is broken (it would silently scan nothing and report green). hits=%v", fixtureFile, hits)
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# OpenYield docs site (MkDocs Material, D-042).
|
||||
#
|
||||
# Build-only Python dep (mkdocs + mkdocs-material); NOT a Go dep (G-006 —
|
||||
# go.mod stays zero-require). Invoke locally with `mkdocs serve` or
|
||||
# `mkdocs build` (see README). No publishing CI in v0.3 (D-046 — publishing
|
||||
# to GitHub/Gitea Pages deferred to v0.4).
|
||||
#
|
||||
# Nav completeness (G-011): the nav lists ALL 26 pages that will exist by end
|
||||
# of P3. P1 creates the shared/ pages + index (8 files); P2 adds nomads/
|
||||
# (8 files); P3 adds freeholders/ (8 files) + reference/ (2 files). Only the
|
||||
# files that exist at P1 ship today; the nav references the not-yet-created
|
||||
# P2/P3 pages by path so the structure is complete and P2/P3 just add files.
|
||||
# mkdocs.yml is a config file, NOT validated by Go tests; the docs firewall
|
||||
# (lexicon_meta_docs_test.go) validates .md content, not nav.
|
||||
|
||||
site_name: OpenYield
|
||||
site_description: OpenYield — a jurisdiction-light, public-good mesh for real production, organized around Holders, Stands, and the Six Principles.
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
- toc.integrate
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: true
|
||||
- codehilite
|
||||
- pymdownx.superfences
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Nomads:
|
||||
- Overview: nomads/index.md
|
||||
- Reach: nomads/reach.md
|
||||
- Stash: nomads/stash.md
|
||||
- Bearers: nomads/bearers.md
|
||||
- Maps-Pay: nomads/maps-pay.md
|
||||
- Pacts: nomads/pacts.md
|
||||
- Standing: nomads/standing.md
|
||||
- Window: nomads/window.md
|
||||
- Freeholders:
|
||||
- Overview: freeholders/index.md
|
||||
- Signals: freeholders/signals.md
|
||||
- Standing: freeholders/standing.md
|
||||
- Stands & Guilds: freeholders/stands-guilds.md
|
||||
- Councils & Voice: freeholders/councils-voice.md
|
||||
- Bonds: freeholders/bonds.md
|
||||
- Partner Spectrum: freeholders/partner-spectrum.md
|
||||
- Anchor Preview: freeholders/anchor-preview.md
|
||||
- Shared:
|
||||
- Overview: shared/index.md
|
||||
- Six Principles: shared/six-principles.md
|
||||
- Bread Scale: shared/bread-scale.md
|
||||
- Storage Pools: shared/storage-pools.md
|
||||
- Watchers & Mirror: shared/watchers-mirror.md
|
||||
- Lexicon: shared/lexicon.md
|
||||
- Vision: shared/vision.md
|
||||
- Reference:
|
||||
- Architecture: reference/architecture.md
|
||||
- Components: reference/components.md
|
||||
@@ -93,6 +93,73 @@ type BeaconFrame struct {
|
||||
TTL int64 `json:"ttl" yaml:"ttl"`
|
||||
}
|
||||
|
||||
// OYSATLink is the OY-SAT (satellite bearer) transport link stub (D-037,
|
||||
// vision §14). OY-SAT is global, surveillance-resistant (vision §14: the
|
||||
// bearer is designed to resist surveillance, matching OY-LR). The struct
|
||||
// mirrors the v0.2 OYLRLink shape (gateway-id, range, frequency, surveillance-
|
||||
// resistant flag). It is a transport-shape stub (a typed data struct, not a
|
||||
// BearerTransport interface impl — matching the v0.2 OYLRLink/BeaconFrame
|
||||
// approach per D-029).
|
||||
//
|
||||
// - satellite-id is the satellite gateway/constellation identifier.
|
||||
// - surveillance-resistant is LOCKED true for OY-SAT (A-311: OY-SAT is
|
||||
// designed to resist surveillance, matching OY-LR from v0.2). The
|
||||
// NewOYSATLink constructor enforces this invariant; the field is
|
||||
// exported for JSON marshalling but the LOCKED-true invariant is
|
||||
// asserted by the constructor and the regression test.
|
||||
// - range-meters is the link range (0 for global satellite coverage).
|
||||
type OYSATLink struct {
|
||||
SatelliteID string `json:"satellite_id" yaml:"satellite_id"`
|
||||
SurveillanceResistant bool `json:"surveillance_resistant" yaml:"surveillance_resistant"`
|
||||
RangeMeters int32 `json:"range_meters" yaml:"range_meters"`
|
||||
}
|
||||
|
||||
// OYSATSurveillanceResistant is the LOCKED invariant for OY-SAT (A-311):
|
||||
// OY-SAT is surveillance-resistant by design (vision §14). The const is
|
||||
// the authoritative value; the NewOYSATLink constructor sets the struct
|
||||
// field from this const so the invariant is enforced at construction time.
|
||||
// A regression test asserts this const is true.
|
||||
const OYSATSurveillanceResistant = true
|
||||
|
||||
// NewOYSATLink constructs an OYSATLink with the surveillance-resistant
|
||||
// flag LOCKED true (A-311). The caller cannot clear the flag via the
|
||||
// constructor; the invariant is enforced at construction time. range-meters
|
||||
// defaults to 0 (global satellite coverage) if not specified.
|
||||
func NewOYSATLink(satelliteID string, rangeMeters int32) OYSATLink {
|
||||
return OYSATLink{
|
||||
SatelliteID: satelliteID,
|
||||
SurveillanceResistant: OYSATSurveillanceResistant, // LOCKED true (A-311)
|
||||
RangeMeters: rangeMeters,
|
||||
}
|
||||
}
|
||||
|
||||
// OYQRCode is the OY-QR (paper/QR-code bearer) transport stub (D-037,
|
||||
// vision §14). OY-QR is 0-range (vision §14: the bearer list has OY-QR at
|
||||
// "0 range"); a QR encodes a signed transfer that the recipient scans and
|
||||
// submits. The struct mirrors the v0.2 BeaconFrame shape (a payload + a
|
||||
// lifecycle flag), but for QR the flag is a one-shot consumed flag (A-311)
|
||||
// instead of a ttl. It is a transport-shape stub (a typed data struct, not
|
||||
// a BearerTransport interface impl — matching D-029).
|
||||
//
|
||||
// - qr-id is the QR code identifier.
|
||||
// - payload-bytes is the signed transfer payload encoded in the QR.
|
||||
// - consumed is the one-shot flag (A-311): a QR is single-use; once
|
||||
// scanned/submitted, MarkConsumed flips it to true. Double-consume is
|
||||
// idempotent (a no-op, not an error).
|
||||
type OYQRCode struct {
|
||||
QRID string `json:"qr_id" yaml:"qr_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
Consumed bool `json:"consumed" yaml:"consumed"`
|
||||
}
|
||||
|
||||
// MarkConsumed marks the QR as consumed (one-shot, A-311). Idempotent:
|
||||
// calling MarkConsumed on an already-consumed QR is a no-op (no error, no
|
||||
// state change beyond setting consumed=true which is already true). This
|
||||
// locks the one-shot semantics: a QR cannot be unconsumed.
|
||||
func (q *OYQRCode) MarkConsumed() {
|
||||
q.Consumed = true
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
@@ -261,6 +261,214 @@ func TestLexiconNoBannedTermsInBearersTestFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- v0.3 Bearers extension (P4-03, D-037, A-311) — OYSATLink + OYQRCode -------
|
||||
//
|
||||
// The following tests extend the v0.2 bearers tests with the v0.3 OY-SAT
|
||||
// and OY-QR transport stubs (D-037). The existing v0.1/v0.2 tests above
|
||||
// MUST remain green — no regression. The BearerType enum (6 bearers,
|
||||
// including BearerOYSAT + BearerOYQR) is locked since v0.1; v0.3 adds the
|
||||
// transport STRUCTS only (no enum change).
|
||||
|
||||
// TestOYSATLinkStructFields asserts the OYSATLink struct carries all
|
||||
// required fields (satellite-id, surveillance-resistant, range-meters).
|
||||
func TestOYSATLinkStructFields(t *testing.T) {
|
||||
link := btypes.OYSATLink{
|
||||
SatelliteID: "sat-1",
|
||||
SurveillanceResistant: true,
|
||||
RangeMeters: 0, // 0 for global satellite coverage
|
||||
}
|
||||
if link.SatelliteID != "sat-1" {
|
||||
t.Errorf("SatelliteID = %q", link.SatelliteID)
|
||||
}
|
||||
if !link.SurveillanceResistant {
|
||||
t.Error("SurveillanceResistant must be true for OY-SAT (vision §14)")
|
||||
}
|
||||
if link.RangeMeters != 0 {
|
||||
t.Errorf("RangeMeters = %d, want 0 (global)", link.RangeMeters)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYSATLinkSurveillanceResistantLockedTrue asserts the OY-SAT
|
||||
// surveillance-resistant invariant is LOCKED true (A-311: OY-SAT is
|
||||
// surveillance-resistant by design, matching OY-LR). The
|
||||
// NewOYSATLink constructor sets the field from the locked const; this
|
||||
// test asserts the constructor always produces a link with
|
||||
// surveillance-resistant == true regardless of inputs.
|
||||
func TestOYSATLinkSurveillanceResistantLockedTrue(t *testing.T) {
|
||||
// The LOCKED const must be true (A-311).
|
||||
if !btypes.OYSATSurveillanceResistant {
|
||||
t.Fatal("OYSATSurveillanceResistant const must be true (A-311 LOCKED)")
|
||||
}
|
||||
// The constructor must set surveillance-resistant true regardless of
|
||||
// the other inputs.
|
||||
cases := []struct {
|
||||
satID string
|
||||
rng int32
|
||||
}{
|
||||
{"sat-1", 0},
|
||||
{"sat-2", 5000},
|
||||
{"", 0},
|
||||
{"global-constellation", 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
link := btypes.NewOYSATLink(c.satID, c.rng)
|
||||
if !link.SurveillanceResistant {
|
||||
t.Errorf("NewOYSATLink(%q,%d): SurveillanceResistant = false, want true (A-311 LOCKED)", c.satID, c.rng)
|
||||
}
|
||||
if link.SurveillanceResistant != btypes.OYSATSurveillanceResistant {
|
||||
t.Errorf("NewOYSATLink(%q,%d): field != locked const (A-311)", c.satID, c.rng)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYSATLinkConstructorSetsFields asserts NewOYSATLink sets the
|
||||
// satellite-id and range-meters fields from the constructor args.
|
||||
func TestOYSATLinkConstructorSetsFields(t *testing.T) {
|
||||
link := btypes.NewOYSATLink("iridium-1", 0)
|
||||
if link.SatelliteID != "iridium-1" {
|
||||
t.Errorf("SatelliteID = %q, want %q", link.SatelliteID, "iridium-1")
|
||||
}
|
||||
if link.RangeMeters != 0 {
|
||||
t.Errorf("RangeMeters = %d, want 0", link.RangeMeters)
|
||||
}
|
||||
link2 := btypes.NewOYSATLink("starlink-2", 5000)
|
||||
if link2.SatelliteID != "starlink-2" {
|
||||
t.Errorf("SatelliteID = %q, want %q", link2.SatelliteID, "starlink-2")
|
||||
}
|
||||
if link2.RangeMeters != 5000 {
|
||||
t.Errorf("RangeMeters = %d, want 5000", link2.RangeMeters)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYSATStillInAllBearers is the v0.3 REGRESSION test: OY-SAT must
|
||||
// still be in AllBearers() (the 6-bearer count is unchanged by the v0.3
|
||||
// extension — the BearerType enum is locked since v0.1).
|
||||
func TestOYSATStillInAllBearers(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
if len(bearers) != 6 {
|
||||
t.Errorf("AllBearers() len = %d, expected 6 (no regression — D-037)", len(bearers))
|
||||
}
|
||||
found := false
|
||||
for _, b := range bearers {
|
||||
if b.Type == btypes.BearerOYSAT {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("OY-SAT must be in AllBearers() (no regression — D-037)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRStillInAllBearers is the v0.3 REGRESSION test: OY-QR must still
|
||||
// be in AllBearers() (the 6-bearer count is unchanged).
|
||||
func TestOYQRStillInAllBearers(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
if len(bearers) != 6 {
|
||||
t.Errorf("AllBearers() len = %d, expected 6 (no regression — D-037)", len(bearers))
|
||||
}
|
||||
found := false
|
||||
for _, b := range bearers {
|
||||
if b.Type == btypes.BearerOYQR {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("OY-QR must be in AllBearers() (no regression — D-037)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRCodeStructFields asserts the OYQRCode struct carries all required
|
||||
// fields (qr-id, payload-bytes, consumed).
|
||||
func TestOYQRCodeStructFields(t *testing.T) {
|
||||
q := btypes.OYQRCode{
|
||||
QRID: "qr-1",
|
||||
PayloadBytes: []byte{0x01, 0x02, 0x03},
|
||||
Consumed: false,
|
||||
}
|
||||
if q.QRID != "qr-1" {
|
||||
t.Errorf("QRID = %q", q.QRID)
|
||||
}
|
||||
if len(q.PayloadBytes) != 3 {
|
||||
t.Errorf("PayloadBytes len = %d, want 3", len(q.PayloadBytes))
|
||||
}
|
||||
if q.Consumed {
|
||||
t.Error("Consumed should be false for a fresh QR")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRCodeMarkConsumedFlipsFlag asserts MarkConsumed sets the consumed
|
||||
// flag to true (A-311: OY-QR is one-shot).
|
||||
func TestOYQRCodeMarkConsumedFlipsFlag(t *testing.T) {
|
||||
q := btypes.OYQRCode{QRID: "qr-1", PayloadBytes: []byte{0x01}, Consumed: false}
|
||||
if q.Consumed {
|
||||
t.Fatal("fresh QR should have Consumed == false")
|
||||
}
|
||||
q.MarkConsumed()
|
||||
if !q.Consumed {
|
||||
t.Error("MarkConsumed should set Consumed = true (A-311 one-shot)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRCodeMarkConsumedIdempotent asserts double-consume is idempotent
|
||||
// (A-311: calling MarkConsumed on an already-consumed QR is a no-op, not an
|
||||
// error). This locks the one-shot semantics: a QR cannot be unconsumed, and
|
||||
// double-marking is safe.
|
||||
func TestOYQRCodeMarkConsumedIdempotent(t *testing.T) {
|
||||
q := btypes.OYQRCode{QRID: "qr-1", PayloadBytes: []byte{0x01}, Consumed: false}
|
||||
// First consume: false -> true.
|
||||
q.MarkConsumed()
|
||||
if !q.Consumed {
|
||||
t.Fatal("first MarkConsumed failed: Consumed still false")
|
||||
}
|
||||
// Second consume: idempotent no-op (stays true, no error, no panic).
|
||||
q.MarkConsumed()
|
||||
if !q.Consumed {
|
||||
t.Error("second MarkConsumed should be idempotent; Consumed must stay true (A-311)")
|
||||
}
|
||||
// Third consume: still idempotent.
|
||||
q.MarkConsumed()
|
||||
if !q.Consumed {
|
||||
t.Error("third MarkConsumed should be idempotent; Consumed must stay true (A-311)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRCodeConsumedCannotBeCleared asserts the one-shot semantics: once
|
||||
// consumed is true, there is no method to clear it (the struct field can be
|
||||
// set directly, but the API provides no Unmark/Reset — A-311 locks the
|
||||
// one-shot invariant). This test verifies no Unmark/Reset method exists by
|
||||
// confirming MarkConsumed is the only state-mutating method (the struct is
|
||||
// a plain data type; the invariant is enforced by the API surface, not a
|
||||
// private field — matching the v0.2 OYLRLink/BeaconFrame shape approach).
|
||||
func TestOYQRCodeConsumedCannotBeCleared(t *testing.T) {
|
||||
q := btypes.OYQRCode{QRID: "qr-1", Consumed: false}
|
||||
q.MarkConsumed()
|
||||
if !q.Consumed {
|
||||
t.Fatal("MarkConsumed failed")
|
||||
}
|
||||
// The one-shot invariant: there is no UnmarkConsumed/Reset method on
|
||||
// OYQRCode. The struct is a plain data type; the API surface (only
|
||||
// MarkConsumed) enforces the one-way transition. We assert the method
|
||||
// set by confirming MarkConsumed does not flip back to false.
|
||||
q.MarkConsumed() // idempotent
|
||||
if !q.Consumed {
|
||||
t.Error("Consumed flipped back to false — one-shot invariant broken (A-311)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYQRCodeZeroValue asserts the zero-value OYQRCode has Consumed ==
|
||||
// false (a fresh QR is unconsumed).
|
||||
func TestOYQRCodeZeroValue(t *testing.T) {
|
||||
var q btypes.OYQRCode
|
||||
if q.Consumed {
|
||||
t.Error("zero-value OYQRCode should have Consumed == false")
|
||||
}
|
||||
if q.QRID != "" {
|
||||
t.Errorf("zero-value QRID = %q, want empty", q.QRID)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.2 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
|
||||
@@ -52,3 +52,93 @@ func knownBondStatus(s BondStatus) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- v0.3 extension: GrowthBond + Order genesis helpers (REQ-026, G-008) --------
|
||||
//
|
||||
// genesis.go also holds the data-engineer's genesis schema helpers for the
|
||||
// v0.3 GrowthBond + SecondaryOrder sets (G-008). ValidateGenesis in types.go
|
||||
// composes ValidateGrowthBonds + ValidateOrders; the security-engineer's test
|
||||
// assertions live in types_test.go / genesis_test.go.
|
||||
|
||||
// ValidateGrowthBonds asserts growth-bond-ids are present and unique, that
|
||||
// each embedded Bond's coupon-bps is within the LOCKED [floor, cap] bounds
|
||||
// (D-028), and that each growth-bond's growth-rate-bps would not push the
|
||||
// coupon above the cap (ClampGrowth(currentBps=coupon, growth) == growth —
|
||||
// i.e. the post-growth coupon stays <= cap). The genesis-side clamp is the
|
||||
// authoritative check (a genesis growth-bond with an out-of-bounds coupon or
|
||||
// growth rate is rejected rather than silently clamped).
|
||||
func ValidateGrowthBonds(gbs []GrowthBond) error {
|
||||
seen := make(map[string]bool, len(gbs))
|
||||
for i, gb := range gbs {
|
||||
if gb.BondID == "" {
|
||||
return fmt.Errorf("growth bond [%d]: empty bond-id", i)
|
||||
}
|
||||
if seen[gb.BondID] {
|
||||
return fmt.Errorf("growth bond: duplicate bond-id %q", gb.BondID)
|
||||
}
|
||||
seen[gb.BondID] = true
|
||||
if !knownBondStatus(gb.Status) {
|
||||
return fmt.Errorf("growth bond %q: unknown bond status %q", gb.BondID, gb.Status)
|
||||
}
|
||||
// D-028 clamp on the embedded Bond's coupon.
|
||||
if gb.CouponBps < CouponFloorBps || gb.CouponBps > CouponCapBps {
|
||||
return fmt.Errorf("growth bond %q: coupon-bps %d outside [%d, %d] (D-028 clamp at genesis load)",
|
||||
gb.BondID, gb.CouponBps, CouponFloorBps, CouponCapBps)
|
||||
}
|
||||
// G-012 / A-306: the growth-rate must not push the coupon above the
|
||||
// cap. ClampGrowth(coupon, growth) must equal growth (i.e. the
|
||||
// requested growth fits within the room-to-cap); otherwise the
|
||||
// genesis growth-bond is rejected as out-of-bounds.
|
||||
if ClampGrowth(gb.CouponBps, gb.GrowthRateBps) != gb.GrowthRateBps {
|
||||
return fmt.Errorf("growth bond %q: growth-rate-bps %d would push coupon-bps %d above cap %d (G-012/A-306 clamp at genesis load)",
|
||||
gb.BondID, gb.GrowthRateBps, gb.CouponBps, CouponCapBps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateOrders asserts order-ids are present and unique, that each order's
|
||||
// bond-id is present, that the side is a known OrderSide, and that the status
|
||||
// is a known OrderStatus (A-212, A-313).
|
||||
func ValidateOrders(orders []SecondaryOrder) error {
|
||||
seen := make(map[string]bool, len(orders))
|
||||
for i, o := range orders {
|
||||
if o.OrderID == "" {
|
||||
return fmt.Errorf("order [%d]: empty order-id", i)
|
||||
}
|
||||
if seen[o.OrderID] {
|
||||
return fmt.Errorf("order: duplicate order-id %q", o.OrderID)
|
||||
}
|
||||
seen[o.OrderID] = true
|
||||
if o.BondID == "" {
|
||||
return fmt.Errorf("order %q: empty bond-id", o.OrderID)
|
||||
}
|
||||
if !knownOrderSide(o.Side) {
|
||||
return fmt.Errorf("order %q: unknown order side %q", o.OrderID, o.Side)
|
||||
}
|
||||
if !knownOrderStatus(o.Status) {
|
||||
return fmt.Errorf("order %q: unknown order status %q", o.OrderID, o.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownOrderSide reports whether s is one of the two OrderSide values.
|
||||
func knownOrderSide(s OrderSide) bool {
|
||||
for _, ss := range AllOrderSides() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// knownOrderStatus reports whether s is one of the three OrderStatus values.
|
||||
func knownOrderStatus(s OrderStatus) bool {
|
||||
for _, ss := range AllOrderStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+167
-11
@@ -113,26 +113,33 @@ type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the bond module genesis state (REQ-021). Bonds is the
|
||||
// top-level set of issued bonds. ValidateGenesis enforces bond-id uniqueness
|
||||
// and the coupon clamp at genesis load (the data-engineer's genesis.go holds
|
||||
// the schema helpers per G-008).
|
||||
// GenesisState defines the bond module genesis state (REQ-021, REQ-026).
|
||||
// Bonds is the top-level set of issued bonds (v0.2). GrowthBonds (v0.3) and
|
||||
// Orders (v0.3) extend the genesis with growth bonds and secondary-market
|
||||
// orders. ValidateGenesis enforces bond-id / growth-bond-id / order-id
|
||||
// uniqueness and the coupon clamp at genesis load (the data-engineer's
|
||||
// genesis.go holds the schema helpers per G-008).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Bonds []Bond `json:"bonds" yaml:"bonds"`
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Bonds []Bond `json:"bonds" yaml:"bonds"`
|
||||
GrowthBonds []GrowthBond `json:"growth_bonds" yaml:"growth_bonds"`
|
||||
Orders []SecondaryOrder `json:"orders" yaml:"orders"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Bonds: []Bond{},
|
||||
Params: DefaultParams(),
|
||||
Bonds: []Bond{},
|
||||
GrowthBonds: []GrowthBond{},
|
||||
Orders: []SecondaryOrder{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate bond-ids, and runs the coupon clamp at genesis
|
||||
// load (each genesis bond's coupon-bps must be within [floor, cap]). Delegates
|
||||
// to the data-engineer's genesis.go helpers (G-008).
|
||||
// no-op): rejects duplicate bond-ids / growth-bond-ids / order-ids, and runs
|
||||
// the coupon clamp at genesis load (each genesis bond's coupon-bps must be
|
||||
// within [floor, cap]). Delegates to the data-engineer's genesis.go helpers
|
||||
// (G-008).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
@@ -141,5 +148,154 @@ func ValidateGenesis(bz json.RawMessage) error {
|
||||
if err := ValidateBonds(gs.Bonds); err != nil {
|
||||
return fmt.Errorf("bond: %w", err)
|
||||
}
|
||||
if err := ValidateGrowthBonds(gs.GrowthBonds); err != nil {
|
||||
return fmt.Errorf("bond: %w", err)
|
||||
}
|
||||
if err := ValidateOrders(gs.Orders); err != nil {
|
||||
return fmt.Errorf("bond: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- v0.3 extension: GrowthBond + secondary market (REQ-026, D-041, G-012) -------
|
||||
//
|
||||
// The v0.3 bond extension adds GrowthBond (a bond whose coupon grows with
|
||||
// protocol health, vision §17) and secondary-market order types. The 8%/0%
|
||||
// consts (D-028) are UNCHANGED — the regression firewall in types_test.go
|
||||
// asserts CouponCapBps==800 and CouponFloorBps==0 are still the v0.2 values.
|
||||
// Full secondary-market matching is deferred to v0.4.
|
||||
|
||||
// OrderSideCount is the locked count of OrderSide enum values (vision §17
|
||||
// secondary market, A-313). A regression firewall: adding/removing/renaming
|
||||
// an order side breaks this const's test.
|
||||
const OrderSideCount = 2
|
||||
|
||||
// OrderStatusCount is the locked count of OrderStatus enum values (A-313).
|
||||
const OrderStatusCount = 3
|
||||
|
||||
// OrderSide enumerates the two sides of a secondary-market order (vision §17,
|
||||
// REQ-026, A-313): Buy (a bid for a bond), Sell (an ask for a bond).
|
||||
type OrderSide string
|
||||
|
||||
const (
|
||||
OrderBuy OrderSide = "Buy" // bid
|
||||
OrderSell OrderSide = "Sell" // ask
|
||||
)
|
||||
|
||||
// AllOrderSides returns both OrderSide values in vision-§17 order. Locked-
|
||||
// const test asserts exactly 2 entries with these names (A-313).
|
||||
func AllOrderSides() []OrderSide {
|
||||
return []OrderSide{
|
||||
OrderBuy,
|
||||
OrderSell,
|
||||
}
|
||||
}
|
||||
|
||||
// OrderStatus enumerates the three lifecycle states of a secondary-market
|
||||
// order (vision §17, REQ-026, A-313): Open (resting on the book), Filled
|
||||
// (matched and settled), Cancelled (removed by the holder or expired). The
|
||||
// matching engine is v0.4; v0.3 types the order shape only.
|
||||
type OrderStatus string
|
||||
|
||||
const (
|
||||
OrderOpen OrderStatus = "Open" // resting on the book
|
||||
OrderFilled OrderStatus = "Filled" // matched and settled
|
||||
OrderCancelled OrderStatus = "Cancelled" // removed by the holder or expired
|
||||
)
|
||||
|
||||
// AllOrderStatuses returns all three OrderStatus values in A-313 order.
|
||||
// Locked-const test asserts exactly 3 entries with these names.
|
||||
func AllOrderStatuses() []OrderStatus {
|
||||
return []OrderStatus{
|
||||
OrderOpen,
|
||||
OrderFilled,
|
||||
OrderCancelled,
|
||||
}
|
||||
}
|
||||
|
||||
// ClampGrowth returns the additional bps a GrowthBond's coupon can grow so
|
||||
// that the post-growth coupon (currentBps + additional) never exceeds
|
||||
// CouponCapBps (D-028, A-306, G-012). The "post-growth coupon <= cap"
|
||||
// invariant holds UNCONDITIONALLY.
|
||||
//
|
||||
// G-012 BINDING: ClampGrowth MUST guard currentBps > CouponCapBps BEFORE
|
||||
// computing cap - current. The naive `min(cap - current, growth)` underflows
|
||||
// uint32 when current > cap (cap - current wraps to a huge value, then min
|
||||
// picks growthBps — the invariant is violated). This implementation guards
|
||||
// explicitly:
|
||||
// - If currentBps >= CouponCapBps: return 0 (no room to grow; the cap is
|
||||
// already reached or exceeded — the post-growth coupon cannot grow
|
||||
// without breaching the cap).
|
||||
// - Otherwise: return min(CouponCapBps - currentBps, growthBps) (the room-
|
||||
// to-cap, clamped by the requested growth).
|
||||
//
|
||||
// The two G-012-mandated test cases are: currentBps == CouponCapBps (return 0,
|
||||
// the at-cap boundary) and currentBps > CouponCapBps (return 0, the guard
|
||||
// against uint32 underflow — NOT a wrapped huge value).
|
||||
func ClampGrowth(currentBps, growthBps uint32) uint32 {
|
||||
// G-012 guard: at-or-above cap means no room to grow. This MUST be checked
|
||||
// before the cap - current subtraction to avoid uint32 underflow when
|
||||
// currentBps > cap.
|
||||
if currentBps >= CouponCapBps {
|
||||
return 0
|
||||
}
|
||||
// currentBps < cap is guaranteed here; cap - current does not underflow.
|
||||
room := CouponCapBps - currentBps
|
||||
if growthBps < room {
|
||||
return growthBps
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
// GrowthBond is a bond whose coupon grows with protocol health (vision §17,
|
||||
// REQ-026, D-041, A-306). It embeds the v0.2 Bond (anonymous field) so it
|
||||
// carries all Bond fields (bond-id, issuer-stand-id, principal-grain,
|
||||
// coupon-bps, term-days, issued-at, maturity, status) PLUS a GrowthRateBps
|
||||
// field (the per-period growth rate of the coupon, in bps). The growth rate
|
||||
// is clamped at issuance so that the post-growth coupon never exceeds
|
||||
// CouponCapBps (800 bps) — see IssueGrowth, which clamps couponBps via Clamp
|
||||
// and growthRateBps via ClampGrowth (with currentBps=couponBps).
|
||||
//
|
||||
// The 8%/0% consts (D-028) apply to GrowthBonds too: the growth coupon is
|
||||
// clamped to [0, 800] bps at any point. GrowthBond is in the same package as
|
||||
// Bond (no G-003 concern for the Clamp/ClampGrowth reuse).
|
||||
type GrowthBond struct {
|
||||
Bond // anonymous embed — carries all v0.2 Bond fields
|
||||
GrowthRateBps uint32 `json:"growth_rate_bps" yaml:"growth_rate_bps"`
|
||||
}
|
||||
|
||||
// IssueGrowth is the GrowthBond issuance stub (REQ-026, D-041). It constructs a
|
||||
// GrowthBond with the coupon clamped to [CouponFloorBps, CouponCapBps] via
|
||||
// Clamp, and the growth-rate clamped so that coupon + growth never exceeds
|
||||
// CouponCapBps via ClampGrowth (with currentBps=couponBps). The returned
|
||||
// GrowthBond has status BondIssued (inherited from Issue's Bond construction).
|
||||
// The stub does not persist or enforce referential integrity of issuer-stand-
|
||||
// id (a v0.4 keeper concern); it only enforces the coupon + growth clamp
|
||||
// invariants at construction time.
|
||||
func IssueGrowth(bondID, issuerStandID string, principalGrain int64, couponBps, growthRateBps uint32, termDays uint32, issuedAt, maturity int64) GrowthBond {
|
||||
clampedCoupon := Clamp(couponBps)
|
||||
clampedGrowth := ClampGrowth(clampedCoupon, growthRateBps)
|
||||
return GrowthBond{
|
||||
Bond: Issue(bondID, issuerStandID, principalGrain, clampedCoupon, termDays, issuedAt, maturity),
|
||||
GrowthRateBps: clampedGrowth,
|
||||
}
|
||||
}
|
||||
|
||||
// SecondaryOrder is a secondary-market order on an issued bond (vision §17,
|
||||
// REQ-026, D-041, A-313). order-id is the unique identifier. bond-id references
|
||||
// a Bond (by-ID-string ref to a Bond — same package, so this is an in-package
|
||||
// ID-string ref, not a cross-module G-003 concern). side picks OrderSide
|
||||
// (Buy/Sell). price-grain is the order price in Grain (fraction of principal,
|
||||
// expressed in Grain for fixed-point precision). holder-reach-id references
|
||||
// an x/identity Reach by ID-string (G-003 — use "holder-reach-id" not the
|
||||
// banned Holder-identity term). status is the OrderStatus. created-at is the
|
||||
// unix timestamp.
|
||||
type SecondaryOrder struct {
|
||||
OrderID string `json:"order_id" yaml:"order_id"`
|
||||
BondID string `json:"bond_id" yaml:"bond_id"`
|
||||
Side OrderSide `json:"side" yaml:"side"`
|
||||
PriceGrain int64 `json:"price_grain" yaml:"price_grain"`
|
||||
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
|
||||
Status OrderStatus `json:"status" yaml:"status"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
}
|
||||
|
||||
@@ -416,6 +416,539 @@ func TestLexiconNoBannedTermsInBondTestFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- v0.3 extension: ClampGrowth (G-012 BINDING) ---------------------------------
|
||||
// ClampGrowth is the G-012 binding decision: it MUST guard currentBps >
|
||||
// CouponCapBps before computing cap - current, otherwise the uint32
|
||||
// subtraction underflows (cap - current wraps to a huge value, then min picks
|
||||
// growthBps — the post-growth coupon invariant is violated). These tests are
|
||||
// written FIRST (TDD) to confirm the guard works before the function existed;
|
||||
// they are the highest-severity v0.3 bond firewall.
|
||||
//
|
||||
// The five G-012-mandated test cases:
|
||||
// 1. currentBps == 0 (full growth room)
|
||||
// 2. currentBps == CouponCapBps (no room, return 0 — the at-cap boundary)
|
||||
// 3. currentBps > CouponCapBps (the underflow GUARD — return 0, NOT a wrapped
|
||||
// huge value)
|
||||
// 4. growthBps larger than room (clamp to room)
|
||||
// 5. growthBps smaller than room (return growthBps)
|
||||
|
||||
// TestClampGrowthCurrentZeroFullRoom asserts case 1: currentBps == 0 leaves
|
||||
// the full room to the cap; the growth is clamped to min(cap, growth).
|
||||
func TestClampGrowthCurrentZeroFullRoom(t *testing.T) {
|
||||
// growth < cap (room) -> return growth
|
||||
if got := btypes.ClampGrowth(0, 500); got != 500 {
|
||||
t.Errorf("ClampGrowth(0, 500) = %d, expected 500 (full room, growth < cap)", got)
|
||||
}
|
||||
// growth == cap (room) -> return cap (room)
|
||||
if got := btypes.ClampGrowth(0, btypes.CouponCapBps); got != btypes.CouponCapBps {
|
||||
t.Errorf("ClampGrowth(0, cap) = %d, expected cap %d (full room, growth == cap)", got, btypes.CouponCapBps)
|
||||
}
|
||||
// growth > cap (room) -> return cap (room)
|
||||
if got := btypes.ClampGrowth(0, 1000); got != btypes.CouponCapBps {
|
||||
t.Errorf("ClampGrowth(0, 1000) = %d, expected cap %d (full room, growth > cap clamps to cap)", got, btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampGrowthCurrentAtCapReturnsZero asserts case 2: currentBps ==
|
||||
// CouponCapBps (the at-cap boundary). There is no room to grow; return 0.
|
||||
// This is the G-012-mandated at-cap test.
|
||||
func TestClampGrowthCurrentAtCapReturnsZero(t *testing.T) {
|
||||
got := btypes.ClampGrowth(btypes.CouponCapBps, 100)
|
||||
if got != 0 {
|
||||
t.Errorf("ClampGrowth(cap, 100) = %d, expected 0 (at-cap boundary — no room to grow, G-012)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampGrowthCurrentAboveCapReturnsZero asserts case 3: currentBps >
|
||||
// CouponCapBps (the uint32 underflow GUARD). The naive min(cap-current,
|
||||
// growth) would underflow uint32 (cap-current wraps to a huge value, then min
|
||||
// picks growth — invariant violated). ClampGrowth MUST return 0, NOT a
|
||||
// wrapped huge value. This is the G-012-mandated above-cap test.
|
||||
func TestClampGrowthCurrentAboveCapReturnsZero(t *testing.T) {
|
||||
cases := []struct {
|
||||
current uint32
|
||||
growth uint32
|
||||
}{
|
||||
{uint32(btypes.CouponCapBps) + 1, 100},
|
||||
{uint32(btypes.CouponCapBps) + 100, 500},
|
||||
{uint32(btypes.CouponCapBps) + 1000, 50},
|
||||
{5000, 100},
|
||||
{100_000, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := btypes.ClampGrowth(c.current, c.growth)
|
||||
if got != 0 {
|
||||
t.Errorf("ClampGrowth(%d, %d) = %d, expected 0 (above-cap GUARD — uint32 underflow must NOT happen, G-012)",
|
||||
c.current, c.growth, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampGrowthGrowthLargerThanRoomClampsToRoom asserts case 4: growthBps
|
||||
// larger than the room-to-cap is clamped to the room.
|
||||
func TestClampGrowthGrowthLargerThanRoomClampsToRoom(t *testing.T) {
|
||||
// current=500, cap=800, room=300. growth=400 > room -> return 300.
|
||||
got := btypes.ClampGrowth(500, 400)
|
||||
if got != 300 {
|
||||
t.Errorf("ClampGrowth(500, 400) = %d, expected 300 (growth larger than room clamps to room)", got)
|
||||
}
|
||||
// current=799, cap=800, room=1. growth=50 > room -> return 1.
|
||||
got = btypes.ClampGrowth(799, 50)
|
||||
if got != 1 {
|
||||
t.Errorf("ClampGrowth(799, 50) = %d, expected 1 (room=1, growth clamps to room)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampGrowthGrowthSmallerThanRoomReturnsGrowth asserts case 5: growthBps
|
||||
// smaller than the room-to-cap is returned unchanged.
|
||||
func TestClampGrowthGrowthSmallerThanRoomReturnsGrowth(t *testing.T) {
|
||||
// current=500, cap=800, room=300. growth=200 < room -> return 200.
|
||||
got := btypes.ClampGrowth(500, 200)
|
||||
if got != 200 {
|
||||
t.Errorf("ClampGrowth(500, 200) = %d, expected 200 (growth < room, unchanged)", got)
|
||||
}
|
||||
// current=0, cap=800, room=800. growth=100 < room -> return 100.
|
||||
got = btypes.ClampGrowth(0, 100)
|
||||
if got != 100 {
|
||||
t.Errorf("ClampGrowth(0, 100) = %d, expected 100 (growth < room, unchanged)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampGrowthInvariantPostGrowthLeCap is the meta-assert: ClampGrowth
|
||||
// never ADDS growth that would push the post-growth coupon past the cap. The
|
||||
// invariant is: current + ClampGrowth(current, growth) <= max(current, cap).
|
||||
// When current <= cap, this means post-growth <= cap (no growth past the
|
||||
// cap). When current > cap (the G-012 misuse/guard case), ClampGrowth returns
|
||||
// 0 (no additional growth), so post == current (the already-broken state is
|
||||
// not made worse; the guard prevents the uint32 underflow from adding a
|
||||
// wrapped-huge value as growth).
|
||||
func TestClampGrowthInvariantPostGrowthLeCap(t *testing.T) {
|
||||
cases := []struct {
|
||||
current uint32
|
||||
growth uint32
|
||||
}{
|
||||
{0, 0},
|
||||
{0, 800},
|
||||
{0, 1000},
|
||||
{400, 400},
|
||||
{400, 500},
|
||||
{799, 1},
|
||||
{799, 100},
|
||||
{800, 100}, // at-cap
|
||||
{801, 100}, // above-cap (guard)
|
||||
{5000, 1000}, // way above-cap (guard)
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := btypes.ClampGrowth(c.current, c.growth)
|
||||
post := c.current + got
|
||||
// The bound: post <= max(current, cap). When current <= cap, this is
|
||||
// post <= cap (no growth past the cap). When current > cap, this is
|
||||
// post <= current (no additional growth — the guard returned 0).
|
||||
upper := c.current
|
||||
if uint32(btypes.CouponCapBps) > upper {
|
||||
upper = btypes.CouponCapBps
|
||||
}
|
||||
if post > upper {
|
||||
t.Errorf("ClampGrowth(%d, %d) = %d; post-growth coupon %d > %d (G-012 invariant violated)",
|
||||
c.current, c.growth, got, post, upper)
|
||||
}
|
||||
// Stronger assert for the in-bounds case: when current <= cap, post
|
||||
// must be <= cap exactly (no growth past the cap).
|
||||
if c.current <= btypes.CouponCapBps && post > btypes.CouponCapBps {
|
||||
t.Errorf("ClampGrowth(%d, %d) = %d; post-growth coupon %d > cap %d (in-bounds invariant violated)",
|
||||
c.current, c.growth, got, post, btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- D-028 regression: 8%/0% consts unchanged (v0.3 must not change v0.2) -------
|
||||
// These tests are re-declared here in the v0.3 block to make the regression
|
||||
// firewall explicit in the extension context. The v0.2 tests above
|
||||
// (TestCouponCapBpsLockedConst / TestCouponFloorBpsLockedConst) are the
|
||||
// primary firewall; this block re-asserts in the v0.3 extension context.
|
||||
|
||||
// TestD028RegressionCouponCapUnchanged asserts CouponCapBps is still 800
|
||||
// after the v0.3 GrowthBond extension (D-028 regression firewall).
|
||||
func TestD028RegressionCouponCapUnchanged(t *testing.T) {
|
||||
if btypes.CouponCapBps != 800 {
|
||||
t.Errorf("D-028 regression: CouponCapBps = %d, expected 800 (v0.3 must not change v0.2 const)", btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestD028RegressionCouponFloorUnchanged asserts CouponFloorBps is still 0.
|
||||
func TestD028RegressionCouponFloorUnchanged(t *testing.T) {
|
||||
if btypes.CouponFloorBps != 0 {
|
||||
t.Errorf("D-028 regression: CouponFloorBps = %d, expected 0 (v0.3 must not change v0.2 const)", btypes.CouponFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestD028RegressionBondStatusCountUnchanged asserts BondStatusCount is still
|
||||
// 5 (the v0.2 enum is unchanged by the v0.3 extension).
|
||||
func TestD028RegressionBondStatusCountUnchanged(t *testing.T) {
|
||||
if btypes.BondStatusCount != 5 {
|
||||
t.Errorf("D-028 regression: BondStatusCount = %d, expected 5 (v0.2 enum unchanged)", btypes.BondStatusCount)
|
||||
}
|
||||
}
|
||||
|
||||
// --- OrderSide enum coverage (2) ----------------------------------------------
|
||||
|
||||
// TestOrderSideCountLockedConst asserts OrderSideCount == 2 and AllOrderSides()
|
||||
// returns exactly 2 (A-313). A regression firewall.
|
||||
func TestOrderSideCountLockedConst(t *testing.T) {
|
||||
if btypes.OrderSideCount != 2 {
|
||||
t.Errorf("OrderSideCount = %d, expected 2 (A-313 LOCKED)", btypes.OrderSideCount)
|
||||
}
|
||||
all := btypes.AllOrderSides()
|
||||
if len(all) != 2 {
|
||||
t.Errorf("AllOrderSides() len = %d, expected 2", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllOrderSidesNames asserts the 2 A-313 names in order with no extras, no
|
||||
// dups, no renames.
|
||||
func TestAllOrderSidesNames(t *testing.T) {
|
||||
want := []string{"Buy", "Sell"}
|
||||
all := btypes.AllOrderSides()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllOrderSides()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate OrderSide %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderSideValues asserts each named const matches its AllOrderSides entry.
|
||||
func TestOrderSideValues(t *testing.T) {
|
||||
if btypes.OrderBuy != "Buy" {
|
||||
t.Errorf("OrderBuy = %q", btypes.OrderBuy)
|
||||
}
|
||||
if btypes.OrderSell != "Sell" {
|
||||
t.Errorf("OrderSell = %q", btypes.OrderSell)
|
||||
}
|
||||
}
|
||||
|
||||
// --- OrderStatus enum coverage (3) -------------------------------------------
|
||||
|
||||
// TestOrderStatusCountLockedConst asserts OrderStatusCount == 3 and
|
||||
// AllOrderStatuses() returns exactly 3 (A-313). A regression firewall.
|
||||
func TestOrderStatusCountLockedConst(t *testing.T) {
|
||||
if btypes.OrderStatusCount != 3 {
|
||||
t.Errorf("OrderStatusCount = %d, expected 3 (A-313 LOCKED)", btypes.OrderStatusCount)
|
||||
}
|
||||
all := btypes.AllOrderStatuses()
|
||||
if len(all) != 3 {
|
||||
t.Errorf("AllOrderStatuses() len = %d, expected 3", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllOrderStatusesNames asserts the 3 A-313 names in order with no extras,
|
||||
// no dups, no renames.
|
||||
func TestAllOrderStatusesNames(t *testing.T) {
|
||||
want := []string{"Open", "Filled", "Cancelled"}
|
||||
all := btypes.AllOrderStatuses()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllOrderStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate OrderStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderStatusValues asserts each named const matches its AllOrderStatuses
|
||||
// entry.
|
||||
func TestOrderStatusValues(t *testing.T) {
|
||||
if btypes.OrderOpen != "Open" {
|
||||
t.Errorf("OrderOpen = %q", btypes.OrderOpen)
|
||||
}
|
||||
if btypes.OrderFilled != "Filled" {
|
||||
t.Errorf("OrderFilled = %q", btypes.OrderFilled)
|
||||
}
|
||||
if btypes.OrderCancelled != "Cancelled" {
|
||||
t.Errorf("OrderCancelled = %q", btypes.OrderCancelled)
|
||||
}
|
||||
}
|
||||
|
||||
// --- GrowthBond + IssueGrowth --------------------------------------------------
|
||||
|
||||
// TestGrowthBondStructFields asserts GrowthBond embeds Bond and adds
|
||||
// GrowthRateBps.
|
||||
func TestGrowthBondStructFields(t *testing.T) {
|
||||
gb := btypes.GrowthBond{
|
||||
Bond: btypes.Bond{BondID: "gb-1", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000, CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Status: btypes.BondIssued},
|
||||
GrowthRateBps: 200,
|
||||
}
|
||||
if gb.BondID != "gb-1" || gb.IssuerStandID != "stand-1" || gb.PrincipalGrain != 1_000_000 ||
|
||||
gb.CouponBps != 500 || gb.TermDays != 365 || gb.IssuedAt != 1000 || gb.Maturity != 1365 ||
|
||||
gb.Status != btypes.BondIssued || gb.GrowthRateBps != 200 {
|
||||
t.Error("GrowthBond fields not set correctly")
|
||||
}
|
||||
// The embedded Bond is accessible via the anonymous field.
|
||||
if gb.Bond.BondID != "gb-1" {
|
||||
t.Errorf("embedded Bond.BondID = %q", gb.Bond.BondID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueGrowthConstruction asserts IssueGrowth clamps the coupon via Clamp
|
||||
// and the growth-rate via ClampGrowth, and returns status BondIssued.
|
||||
func TestIssueGrowthConstruction(t *testing.T) {
|
||||
// In-range coupon and growth: both unchanged.
|
||||
gb := btypes.IssueGrowth("gb-2", "stand-1", 1_000_000, 500, 200, 365, 1000, 1365)
|
||||
if gb.BondID != "gb-2" {
|
||||
t.Errorf("BondID = %q", gb.BondID)
|
||||
}
|
||||
if gb.CouponBps != 500 {
|
||||
t.Errorf("CouponBps = %d, expected 500 (in-range, unchanged)", gb.CouponBps)
|
||||
}
|
||||
if gb.GrowthRateBps != 200 {
|
||||
t.Errorf("GrowthRateBps = %d, expected 200 (in-range, growth < room)", gb.GrowthRateBps)
|
||||
}
|
||||
if gb.Status != btypes.BondIssued {
|
||||
t.Errorf("Status = %q, expected BondIssued", gb.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueGrowthClampsAboveCapCoupon asserts IssueGrowth clamps an above-cap
|
||||
// coupon down to the cap (via Clamp), and the growth-rate is then clamped
|
||||
// against the clamped coupon (currentBps=cap -> growth returns 0, G-012).
|
||||
func TestIssueGrowthClampsAboveCapCoupon(t *testing.T) {
|
||||
gb := btypes.IssueGrowth("gb-3", "stand-1", 1_000_000, 1200, 100, 365, 1000, 1365)
|
||||
if gb.CouponBps != btypes.CouponCapBps {
|
||||
t.Errorf("CouponBps = %d, expected cap %d (IssueGrowth must clamp above-cap coupon)", gb.CouponBps, btypes.CouponCapBps)
|
||||
}
|
||||
// coupon clamped to cap -> ClampGrowth(cap, 100) == 0 (no room, G-012).
|
||||
if gb.GrowthRateBps != 0 {
|
||||
t.Errorf("GrowthRateBps = %d, expected 0 (coupon at cap -> no room, G-012)", gb.GrowthRateBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueGrowthClampsGrowthToRoom asserts IssueGrowth clamps a growth-rate
|
||||
// that would push the coupon above the cap down to the room-to-cap.
|
||||
func TestIssueGrowthClampsGrowthToRoom(t *testing.T) {
|
||||
// coupon=500, cap=800, room=300. growth=400 -> clamped to 300.
|
||||
gb := btypes.IssueGrowth("gb-4", "stand-1", 1_000_000, 500, 400, 365, 1000, 1365)
|
||||
if gb.CouponBps != 500 {
|
||||
t.Errorf("CouponBps = %d, expected 500", gb.CouponBps)
|
||||
}
|
||||
if gb.GrowthRateBps != 300 {
|
||||
t.Errorf("GrowthRateBps = %d, expected 300 (growth clamped to room, G-012)", gb.GrowthRateBps)
|
||||
}
|
||||
// post-growth coupon: 500 + 300 = 800 == cap (invariant holds).
|
||||
if gb.CouponBps+gb.GrowthRateBps > btypes.CouponCapBps {
|
||||
t.Errorf("post-growth coupon %d > cap %d (G-012 invariant)", gb.CouponBps+gb.GrowthRateBps, btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// --- SecondaryOrder struct ----------------------------------------------------
|
||||
|
||||
// TestSecondaryOrderStructFields asserts SecondaryOrder carries order-id,
|
||||
// bond-id (by-ID-string ref to a Bond — in-package), side, price-grain,
|
||||
// holder-reach-id (by-ID-string ref to x/identity — G-003), status, created-at.
|
||||
func TestSecondaryOrderStructFields(t *testing.T) {
|
||||
o := btypes.SecondaryOrder{
|
||||
OrderID: "order-1",
|
||||
BondID: "bond-1",
|
||||
Side: btypes.OrderBuy,
|
||||
PriceGrain: 950_000,
|
||||
HolderReachID: "reach-holder-1",
|
||||
Status: btypes.OrderOpen,
|
||||
CreatedAt: 5000,
|
||||
}
|
||||
if o.OrderID != "order-1" || o.BondID != "bond-1" || o.Side != btypes.OrderBuy ||
|
||||
o.PriceGrain != 950_000 || o.HolderReachID != "reach-holder-1" ||
|
||||
o.Status != btypes.OrderOpen || o.CreatedAt != 5000 {
|
||||
t.Error("SecondaryOrder fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecondaryOrderBondIDIsString asserts bond-id is string-typed (in-package
|
||||
// by-ID-string ref to a Bond — same package, not a G-003 cross-module import).
|
||||
func TestSecondaryOrderBondIDIsString(t *testing.T) {
|
||||
o := btypes.SecondaryOrder{BondID: "bond-xyz"}
|
||||
if o.BondID != "bond-xyz" {
|
||||
t.Errorf("BondID = %q", o.BondID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecondaryOrderHolderReachIDIsString asserts holder-reach-id is
|
||||
// string-typed (G-003 by-ID-string ref to x/identity Reach — no struct import).
|
||||
func TestSecondaryOrderHolderReachIDIsString(t *testing.T) {
|
||||
o := btypes.SecondaryOrder{HolderReachID: "reach-abc"}
|
||||
if o.HolderReachID != "reach-abc" {
|
||||
t.Errorf("HolderReachID = %q", o.HolderReachID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis v0.3 extension: GrowthBonds + Orders -----------------------------
|
||||
|
||||
// TestDefaultGenesisStateV3Empty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for the v0.3 GrowthBonds and Orders sets.
|
||||
func TestDefaultGenesisStateV3Empty(t *testing.T) {
|
||||
gs := btypes.DefaultGenesisState()
|
||||
if gs.GrowthBonds == nil || len(gs.GrowthBonds) != 0 {
|
||||
t.Errorf("Default GrowthBonds should be non-nil empty slice; got len=%d nil=%v", len(gs.GrowthBonds), gs.GrowthBonds == nil)
|
||||
}
|
||||
if gs.Orders == nil || len(gs.Orders) != 0 {
|
||||
t.Errorf("Default Orders should be non-nil empty slice; got len=%d nil=%v", len(gs.Orders), gs.Orders == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupGrowthBondIDs asserts A-212: duplicate
|
||||
// growth-bond-ids are rejected.
|
||||
func TestValidateGenesisRejectsDupGrowthBondIDs(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
GrowthBonds: []btypes.GrowthBond{
|
||||
{Bond: btypes.Bond{BondID: "gb1", IssuerStandID: "s1", CouponBps: 500, Status: btypes.BondIssued}, GrowthRateBps: 100},
|
||||
{Bond: btypes.Bond{BondID: "gb1", IssuerStandID: "s2", CouponBps: 200, Status: btypes.BondActive}, GrowthRateBps: 50}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate growth-bond-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsGrowthBondCouponAboveCap asserts a genesis
|
||||
// GrowthBond with coupon-bps above the cap is rejected (D-028 at genesis).
|
||||
func TestValidateGenesisRejectsGrowthBondCouponAboveCap(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
GrowthBonds: []btypes.GrowthBond{
|
||||
{Bond: btypes.Bond{BondID: "gb1", IssuerStandID: "s1", CouponBps: 900, Status: btypes.BondIssued}, GrowthRateBps: 0},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject growth-bond coupon above cap (D-028)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsGrowthBondGrowthAboveRoom asserts a genesis
|
||||
// GrowthBond whose growth-rate would push the coupon above the cap is
|
||||
// rejected (G-012 / A-306 at genesis).
|
||||
func TestValidateGenesisRejectsGrowthBondGrowthAboveRoom(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
GrowthBonds: []btypes.GrowthBond{
|
||||
// coupon=500, cap=800, room=300. growth=400 -> would push to 900 > cap.
|
||||
{Bond: btypes.Bond{BondID: "gb1", IssuerStandID: "s1", CouponBps: 500, Status: btypes.BondIssued}, GrowthRateBps: 400},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject growth-bond growth-rate above room (G-012/A-306)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupOrderIDs asserts A-212: duplicate order-ids are
|
||||
// rejected.
|
||||
func TestValidateGenesisRejectsDupOrderIDs(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Orders: []btypes.SecondaryOrder{
|
||||
{OrderID: "o1", BondID: "b1", Side: btypes.OrderBuy, Status: btypes.OrderOpen},
|
||||
{OrderID: "o1", BondID: "b2", Side: btypes.OrderSell, Status: btypes.OrderOpen}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate order-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyOrderBondID asserts an order with an empty
|
||||
// bond-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyOrderBondID(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Orders: []btypes.SecondaryOrder{{OrderID: "o1", BondID: "", Side: btypes.OrderBuy, Status: btypes.OrderOpen}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty order bond-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownOrderSide asserts an unknown OrderSide is
|
||||
// rejected.
|
||||
func TestValidateGenesisRejectsUnknownOrderSide(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Orders: []btypes.SecondaryOrder{{OrderID: "o1", BondID: "b1", Side: btypes.OrderSide("Bogus"), Status: btypes.OrderOpen}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown order side")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownOrderStatus asserts an unknown OrderStatus
|
||||
// is rejected.
|
||||
func TestValidateGenesisRejectsUnknownOrderStatus(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Orders: []btypes.SecondaryOrder{{OrderID: "o1", BondID: "b1", Side: btypes.OrderBuy, Status: btypes.OrderStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown order status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsCleanV3 asserts a clean v0.3 genesis (bonds +
|
||||
// growth bonds + orders) validates.
|
||||
func TestValidateGenesisAcceptsCleanV3(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
|
||||
},
|
||||
GrowthBonds: []btypes.GrowthBond{
|
||||
{Bond: btypes.Bond{BondID: "gb1", IssuerStandID: "s1", CouponBps: 500, Status: btypes.BondIssued}, GrowthRateBps: 200},
|
||||
{Bond: btypes.Bond{BondID: "gb2", IssuerStandID: "s1", CouponBps: 800, Status: btypes.BondActive}, GrowthRateBps: 0},
|
||||
},
|
||||
Orders: []btypes.SecondaryOrder{
|
||||
{OrderID: "o1", BondID: "b1", Side: btypes.OrderBuy, PriceGrain: 950_000, HolderReachID: "r1", Status: btypes.OrderOpen, CreatedAt: 1000},
|
||||
{OrderID: "o2", BondID: "gb1", Side: btypes.OrderSell, PriceGrain: 1_050_000, HolderReachID: "r2", Status: btypes.OrderFilled, CreatedAt: 2000},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean v0.3 genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGrowthBondsAcceptsClean asserts the data-engineer's
|
||||
// ValidateGrowthBonds helper accepts a clean set.
|
||||
func TestValidateGrowthBondsAcceptsClean(t *testing.T) {
|
||||
gbs := []btypes.GrowthBond{
|
||||
{Bond: btypes.Bond{BondID: "gb1", CouponBps: 0, Status: btypes.BondIssued}, GrowthRateBps: 800},
|
||||
{Bond: btypes.Bond{BondID: "gb2", CouponBps: 500, Status: btypes.BondActive}, GrowthRateBps: 300},
|
||||
{Bond: btypes.Bond{BondID: "gb3", CouponBps: 800, Status: btypes.BondMatured}, GrowthRateBps: 0},
|
||||
}
|
||||
if err := btypes.ValidateGrowthBonds(gbs); err != nil {
|
||||
t.Errorf("ValidateGrowthBonds should accept clean set; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOrdersAcceptsClean asserts ValidateOrders accepts a clean set.
|
||||
func TestValidateOrdersAcceptsClean(t *testing.T) {
|
||||
orders := []btypes.SecondaryOrder{
|
||||
{OrderID: "o1", BondID: "b1", Side: btypes.OrderBuy, Status: btypes.OrderOpen},
|
||||
{OrderID: "o2", BondID: "b1", Side: btypes.OrderSell, Status: btypes.OrderFilled},
|
||||
{OrderID: "o3", BondID: "b2", Side: btypes.OrderBuy, Status: btypes.OrderCancelled},
|
||||
}
|
||||
if err := btypes.ValidateOrders(orders); err != nil {
|
||||
t.Errorf("ValidateOrders should accept clean set; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.2 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// bridge module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Bridge genesis schema has one top-level set: Routes (the bridge
|
||||
// routes). The invariants enforced at genesis load are (1) bridge-id
|
||||
// uniqueness, (2) bridge-id non-empty, and (3) status is a known
|
||||
// BridgeStatus. The route's l2-chain and watcher-quorum-id are by-ID-string
|
||||
// refs (G-003) and are NOT referentially checked at genesis (the referenced
|
||||
// x/satellite and x/watcher state is in separate modules; cross-module
|
||||
// referential integrity is a v0.4 keeper concern, not a v0.3 skeleton
|
||||
// concern per A-304).
|
||||
|
||||
// ValidateRoutes asserts bridge-ids are present and unique, and that each
|
||||
// route's status is a known BridgeStatus. ValidateRoutes is the
|
||||
// data-engineer's schema validator, composed by ValidateGenesis in
|
||||
// types.go.
|
||||
func ValidateRoutes(routes []BridgeRoute) error {
|
||||
seen := make(map[string]bool, len(routes))
|
||||
for i, r := range routes {
|
||||
if r.BridgeID == "" {
|
||||
return fmt.Errorf("bridge [%d]: empty bridge-id", i)
|
||||
}
|
||||
if seen[r.BridgeID] {
|
||||
return fmt.Errorf("bridge: duplicate bridge-id %q", r.BridgeID)
|
||||
}
|
||||
seen[r.BridgeID] = true
|
||||
if !knownBridgeStatus(r.Status) {
|
||||
return fmt.Errorf("bridge %q: unknown bridge status %q", r.BridgeID, r.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownBridgeStatus reports whether s is one of the four BridgeStatus
|
||||
// values.
|
||||
func knownBridgeStatus(s BridgeStatus) bool {
|
||||
for _, ss := range AllBridgeStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "bridge"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// BridgeStatusCount is the locked count of BridgeStatus enum values
|
||||
// (vision §7, REQ-010, D-036). Four route-level lifecycle states:
|
||||
// Pending, Attested, Active, Closed. A regression firewall:
|
||||
// adding/removing/renaming a status breaks this const's test.
|
||||
BridgeStatusCount = 4
|
||||
)
|
||||
|
||||
// BridgeStatus enumerates the route-level lifecycle of an L2↔L1 bridge
|
||||
// (vision §7, REQ-010, D-036). The four-state lifecycle sits above the
|
||||
// ICS-20 channel handshake (x/satellite ChannelStatus): a bridge route is
|
||||
// Pending until Watcher attestation confirms it (Attested), then it
|
||||
// becomes Active for transfers, and is Closed when the route is retired.
|
||||
// The Attested state references a Watcher quorum by ID-string (the
|
||||
// attestation is a by-ID-string field, not a struct import — G-003).
|
||||
type BridgeStatus string
|
||||
|
||||
const (
|
||||
BridgePending BridgeStatus = "Pending" // route declared, awaiting attestation
|
||||
BridgeAttested BridgeStatus = "Attested" // Watcher quorum confirmed the route
|
||||
BridgeActive BridgeStatus = "Active" // route open for transfers
|
||||
BridgeClosed BridgeStatus = "Closed" // route retired
|
||||
)
|
||||
|
||||
// AllBridgeStatuses returns all four BridgeStatus values in vision §7
|
||||
// route-lifecycle order. Locked-const test asserts exactly 4 entries.
|
||||
func AllBridgeStatuses() []BridgeStatus {
|
||||
return []BridgeStatus{
|
||||
BridgePending,
|
||||
BridgeAttested,
|
||||
BridgeActive,
|
||||
BridgeClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// BridgeRoute is a single L2↔L1 bridge route (REQ-010, D-036). The route
|
||||
// is the higher-level abstraction over the v0.2 satellite IBC transfer
|
||||
// channel: it carries the route-level status lifecycle and the Watcher
|
||||
// attestation ref, while the underlying channel handshake lives in
|
||||
// x/satellite. All cross-module references are by-ID-string per G-003:
|
||||
//
|
||||
// - bridge-id is this route's unique identifier.
|
||||
// - l2-chain references an x/satellite L2Chain by ID-string (the L2
|
||||
// satellite chain this route bridges to/from). No struct import of
|
||||
// x/satellite (G-003).
|
||||
// - watcher-quorum-id references an x/watcher quorum by ID-string; it is
|
||||
// set when status transitions to Attested (the Watcher 6-of-9 quorum
|
||||
// attests the route per vision §7). No struct import of x/watcher.
|
||||
//
|
||||
// status is the route-level lifecycle (BridgeStatus), distinct from the
|
||||
// channel-level handshake (x/satellite ChannelStatus).
|
||||
type BridgeRoute struct {
|
||||
BridgeID string `json:"bridge_id" yaml:"bridge_id"`
|
||||
L2Chain string `json:"l2_chain" yaml:"l2_chain"`
|
||||
WatcherQuorumID string `json:"watcher_quorum_id" yaml:"watcher_quorum_id"`
|
||||
Status BridgeStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Params for the bridge module (skeleton — no tunables in v0.3).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the bridge module genesis state (REQ-010). Routes
|
||||
// is the set of bridge routes. ValidateGenesis enforces bridge-id
|
||||
// uniqueness and status validity. The data-engineer's genesis.go holds
|
||||
// the schema helpers (G-008 split).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Routes []BridgeRoute `json:"routes" yaml:"routes"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Routes: []BridgeRoute{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate bridge-ids and unknown statuses. Delegates to
|
||||
// the data-engineer's genesis.go helpers (G-008).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("bridge: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateRoutes(gs.Routes); err != nil {
|
||||
return fmt.Errorf("bridge: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
btypes "github.com/oy/openyield/x/bridge/types"
|
||||
)
|
||||
|
||||
// --- BridgeStatus enum (exactly 4) ---------------------------------------------
|
||||
|
||||
// TestBridgeStatusCountLockedConst asserts BridgeStatusCount == 4 and
|
||||
// AllBridgeStatuses() returns exactly 4 (vision §7, REQ-010, D-036). A
|
||||
// regression firewall: adding/removing/renaming a status breaks this test.
|
||||
func TestBridgeStatusCountLockedConst(t *testing.T) {
|
||||
if btypes.BridgeStatusCount != 4 {
|
||||
t.Errorf("BridgeStatusCount = %d, expected 4 (vision §7 LOCKED)", btypes.BridgeStatusCount)
|
||||
}
|
||||
all := btypes.AllBridgeStatuses()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllBridgeStatuses() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllBridgeStatusesNames asserts the 4 vision §7 route-lifecycle names
|
||||
// in order with no extras, no dups, no renames (Pending, Attested, Active,
|
||||
// Closed).
|
||||
func TestAllBridgeStatusesNames(t *testing.T) {
|
||||
want := []string{"Pending", "Attested", "Active", "Closed"}
|
||||
all := btypes.AllBridgeStatuses()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllBridgeStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate BridgeStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestBridgeStatusValues asserts each named const matches its AllBridgeStatuses
|
||||
// entry.
|
||||
func TestBridgeStatusValues(t *testing.T) {
|
||||
if btypes.BridgePending != "Pending" {
|
||||
t.Errorf("BridgePending = %q", btypes.BridgePending)
|
||||
}
|
||||
if btypes.BridgeAttested != "Attested" {
|
||||
t.Errorf("BridgeAttested = %q", btypes.BridgeAttested)
|
||||
}
|
||||
if btypes.BridgeActive != "Active" {
|
||||
t.Errorf("BridgeActive = %q", btypes.BridgeActive)
|
||||
}
|
||||
if btypes.BridgeClosed != "Closed" {
|
||||
t.Errorf("BridgeClosed = %q", btypes.BridgeClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- BridgeRoute struct (by-ID-string refs — G-003) -----------------------------
|
||||
|
||||
// TestBridgeRouteStructFields asserts BridgeRoute carries all required
|
||||
// fields including the by-ID-string refs to x/satellite (l2-chain) and
|
||||
// x/watcher (watcher-quorum-id) per G-003. No struct imports of either
|
||||
// referenced module (the G-003 import-invariant test enforces this).
|
||||
func TestBridgeRouteStructFields(t *testing.T) {
|
||||
r := btypes.BridgeRoute{
|
||||
BridgeID: "bridge-1",
|
||||
L2Chain: "Polygon", // by-ID-string ref to x/satellite L2Chain (G-003)
|
||||
WatcherQuorumID: "quorum-1",
|
||||
Status: btypes.BridgeActive,
|
||||
}
|
||||
if r.BridgeID != "bridge-1" {
|
||||
t.Errorf("BridgeID = %q", r.BridgeID)
|
||||
}
|
||||
if r.L2Chain != "Polygon" {
|
||||
t.Errorf("L2Chain = %q", r.L2Chain)
|
||||
}
|
||||
if r.WatcherQuorumID != "quorum-1" {
|
||||
t.Errorf("WatcherQuorumID = %q", r.WatcherQuorumID)
|
||||
}
|
||||
if r.Status != btypes.BridgeActive {
|
||||
t.Errorf("Status = %q", r.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBridgeRouteL2ChainIsString asserts the L2Chain field is an opaque
|
||||
// string (by-ID-string ref — G-003), NOT a typed enum import from
|
||||
// x/satellite. This locks the by-ID-string invariant at the type level.
|
||||
func TestBridgeRouteL2ChainIsString(t *testing.T) {
|
||||
r := btypes.BridgeRoute{L2Chain: "Polygon"}
|
||||
// The field must be assignable from a plain string (no satellite.L2Chain
|
||||
// type needed).
|
||||
r.L2Chain = "Base"
|
||||
if r.L2Chain != "Base" {
|
||||
t.Errorf("L2Chain = %q, want %q (must be plain string)", r.L2Chain, "Base")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBridgeRouteWatcherQuorumIDIsString asserts the WatcherQuorumID field
|
||||
// is an opaque string (by-ID-string ref to x/watcher — G-003).
|
||||
func TestBridgeRouteWatcherQuorumIDIsString(t *testing.T) {
|
||||
r := btypes.BridgeRoute{WatcherQuorumID: "quorum-9"}
|
||||
if r.WatcherQuorumID != "quorum-9" {
|
||||
t.Errorf("WatcherQuorumID = %q", r.WatcherQuorumID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis tests (A-212) ------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a non-nil
|
||||
// empty slice for Routes.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := btypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Routes == nil || len(gs.Routes) != 0 {
|
||||
t.Errorf("Default Routes should be non-nil empty slice; got len=%d nil=%v", len(gs.Routes), gs.Routes == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupBridgeIDs asserts A-212: duplicate bridge-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupBridgeIDs(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Routes: []btypes.BridgeRoute{
|
||||
{BridgeID: "b1", L2Chain: "Polygon", Status: btypes.BridgePending},
|
||||
{BridgeID: "b1", L2Chain: "Base", Status: btypes.BridgeActive}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate bridge-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyBridgeID asserts empty bridge-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyBridgeID(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Routes: []btypes.BridgeRoute{{BridgeID: "", L2Chain: "Polygon", Status: btypes.BridgePending}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty bridge-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownStatus asserts an unknown BridgeStatus
|
||||
// is rejected.
|
||||
func TestValidateGenesisRejectsUnknownStatus(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Routes: []btypes.BridgeRoute{{BridgeID: "b1", L2Chain: "Polygon", Status: btypes.BridgeStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown bridge status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := btypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Routes: []btypes.BridgeRoute{
|
||||
{BridgeID: "b1", L2Chain: "Polygon", WatcherQuorumID: "q1", Status: btypes.BridgeActive},
|
||||
{BridgeID: "b2", L2Chain: "Base", Status: btypes.BridgePending},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts -------------------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if btypes.ModuleName != "bridge" {
|
||||
t.Errorf("ModuleName = %q", btypes.ModuleName)
|
||||
}
|
||||
if btypes.StoreKey != "bridge" {
|
||||
t.Errorf("StoreKey = %q", btypes.StoreKey)
|
||||
}
|
||||
if btypes.RouterKey != "bridge" {
|
||||
t.Errorf("RouterKey = %q", btypes.RouterKey)
|
||||
}
|
||||
if btypes.QuerierRoute != "bridge" {
|
||||
t.Errorf("QuerierRoute = %q", btypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = btypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
//
|
||||
// The bridge module must avoid the banned financial holder terms (the
|
||||
// lexicon firewall's banned list). Use "Holder"/"Reach" instead. The lexicon
|
||||
// helpers are used here — no banned literals are inlined.
|
||||
|
||||
// TestLexiconNoBannedTermsInBridgePackage scans every non-test .go file in
|
||||
// the bridge/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInBridgePackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/bridge/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in bridge/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — use Holder/Reach, not banned financial terms)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInBridgeTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal.
|
||||
func TestLexiconNoBannedTermsInBridgeTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("bridge test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.3 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/bridge/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// exit module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Exit genesis schema has two top-level sets: Routes (exit routes) and
|
||||
// Swaps (DEX swaps). The invariants enforced at genesis load are (1)
|
||||
// route-id uniqueness, (2) swap-id uniqueness, and (3) status validity.
|
||||
// The route's bridge-route-id is a by-ID-string ref (G-003) and is NOT
|
||||
// referentially checked at genesis (the referenced x/bridge state is in a
|
||||
// separate module; cross-module referential integrity is a v0.4 keeper
|
||||
// concern, not a v0.3 skeleton concern per A-308).
|
||||
|
||||
// ValidateRoutes asserts route-ids are present and unique, and that each
|
||||
// route's status is a known ExitStatus. ValidateRoutes is the
|
||||
// data-engineer's schema validator, composed by ValidateGenesis in
|
||||
// types.go.
|
||||
func ValidateRoutes(routes []ExitRoute) error {
|
||||
seen := make(map[string]bool, len(routes))
|
||||
for i, r := range routes {
|
||||
if r.RouteID == "" {
|
||||
return fmt.Errorf("exit [%d]: empty route-id", i)
|
||||
}
|
||||
if seen[r.RouteID] {
|
||||
return fmt.Errorf("exit: duplicate route-id %q", r.RouteID)
|
||||
}
|
||||
seen[r.RouteID] = true
|
||||
if !knownExitStatus(r.Status) {
|
||||
return fmt.Errorf("exit %q: unknown exit status %q", r.RouteID, r.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSwaps asserts swap-ids are present and unique, and that each
|
||||
// swap's status is a known ExitStatus. The venue is an opaque string
|
||||
// (A-308) and is not validated against a locked enum.
|
||||
func ValidateSwaps(swaps []DEXSwap) error {
|
||||
seen := make(map[string]bool, len(swaps))
|
||||
for i, s := range swaps {
|
||||
if s.SwapID == "" {
|
||||
return fmt.Errorf("exit [%d]: empty swap-id", i)
|
||||
}
|
||||
if seen[s.SwapID] {
|
||||
return fmt.Errorf("exit: duplicate swap-id %q", s.SwapID)
|
||||
}
|
||||
seen[s.SwapID] = true
|
||||
if !knownExitStatus(s.Status) {
|
||||
return fmt.Errorf("exit swap %q: unknown exit status %q", s.SwapID, s.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownExitStatus reports whether s is one of the five ExitStatus values.
|
||||
func knownExitStatus(s ExitStatus) bool {
|
||||
for _, ss := range AllExitStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "exit"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// ExitStatusCount is the locked count of ExitStatus enum values
|
||||
// (vision §7, REQ-010, D-036). Five exit lifecycle states: Proposed,
|
||||
// InProgress, Settled, Failed, Refunded. A regression firewall:
|
||||
// adding/removing/renaming a status breaks this const's test.
|
||||
ExitStatusCount = 5
|
||||
)
|
||||
|
||||
// ExitStatus enumerates the lifecycle of a Layer-3 exit (vision §7,
|
||||
// REQ-010, D-036). The five-state lifecycle covers both successful exits
|
||||
// (Proposed → InProgress → Settled) and the failure/recovery paths
|
||||
// (Failed → Refunded). Refunded is the terminal recovery state when an
|
||||
// exit fails and the holder is made whole.
|
||||
type ExitStatus string
|
||||
|
||||
const (
|
||||
ExitProposed ExitStatus = "Proposed" // exit declared, not yet executing
|
||||
ExitInProgress ExitStatus = "InProgress" // exit executing (swap/bridge hop)
|
||||
ExitSettled ExitStatus = "Settled" // exit completed, holder paid out
|
||||
ExitFailed ExitStatus = "Failed" // exit failed (slippage/timeout)
|
||||
ExitRefunded ExitStatus = "Refunded" // failed exit refunded to holder
|
||||
)
|
||||
|
||||
// AllExitStatuses returns all five ExitStatus values in vision §7 lifecycle
|
||||
// order. Locked-const test asserts exactly 5 entries.
|
||||
func AllExitStatuses() []ExitStatus {
|
||||
return []ExitStatus{
|
||||
ExitProposed,
|
||||
ExitInProgress,
|
||||
ExitSettled,
|
||||
ExitFailed,
|
||||
ExitRefunded,
|
||||
}
|
||||
}
|
||||
|
||||
// ExitRoute is a Holder-initiated exit route (REQ-010, D-036, A-308). The
|
||||
// route describes a holder's intent to exit the mesh via a DEX swap and
|
||||
// (optionally) a cross-chain bridge hop. All cross-module references are
|
||||
// by-ID-string per G-003:
|
||||
//
|
||||
// - route-id is this route's unique identifier.
|
||||
// - bridge-route-id references an x/bridge BridgeRoute by ID-string
|
||||
// (A-308, G-003). It is optional (empty for same-chain exits) and
|
||||
// present for cross-chain exits. No struct import of x/bridge.
|
||||
// - status is the exit lifecycle (ExitStatus).
|
||||
//
|
||||
// The bridge-route-id is the P4 intra-phase dependency edge (x/bridge is
|
||||
// authored first within P4; x/exit references it by ID-string only).
|
||||
type ExitRoute struct {
|
||||
RouteID string `json:"route_id" yaml:"route_id"`
|
||||
BridgeRouteID string `json:"bridge_route_id" yaml:"bridge_route_id"`
|
||||
Status ExitStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// DEXSwap is a single DEX swap executed as part of an exit route (REQ-010,
|
||||
// D-036, A-308). The venue is an OPAQUE string (e.g. "uniswap-v3", "oy-dex")
|
||||
// — NOT a locked enum. A-308: venues are operational, not protocol-locked;
|
||||
// locking an enum now risks churn (uniswap-v3/v4, oy-dex, etc. change over
|
||||
// time). The skeleton keeps the venue as a free-form string so the type
|
||||
// shape is stable across venue additions. status reuses ExitStatus (a swap
|
||||
// shares the exit lifecycle: Proposed → InProgress → Settled/Failed).
|
||||
//
|
||||
// - swap-id is this swap's unique identifier.
|
||||
// - venue is the opaque DEX venue string (A-308 — not a locked enum).
|
||||
// - status is the swap lifecycle (ExitStatus).
|
||||
type DEXSwap struct {
|
||||
SwapID string `json:"swap_id" yaml:"swap_id"`
|
||||
Venue string `json:"venue" yaml:"venue"`
|
||||
Status ExitStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Params for the exit module (skeleton — no tunables in v0.3).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the exit module genesis state (REQ-010). Routes is
|
||||
// the set of exit routes; Swaps is the set of DEX swaps. ValidateGenesis
|
||||
// enforces route-id and swap-id uniqueness. The data-engineer's genesis.go
|
||||
// holds the schema helpers (G-008 split).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Routes []ExitRoute `json:"routes" yaml:"routes"`
|
||||
Swaps []DEXSwap `json:"swaps" yaml:"swaps"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Routes: []ExitRoute{},
|
||||
Swaps: []DEXSwap{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate route-ids and swap-ids. Delegates to the
|
||||
// data-engineer's genesis.go helpers (G-008).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("exit: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateRoutes(gs.Routes); err != nil {
|
||||
return fmt.Errorf("exit: %w", err)
|
||||
}
|
||||
if err := ValidateSwaps(gs.Swaps); err != nil {
|
||||
return fmt.Errorf("exit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
etypes "github.com/oy/openyield/x/exit/types"
|
||||
)
|
||||
|
||||
// --- ExitStatus enum (exactly 5) -----------------------------------------------
|
||||
|
||||
// TestExitStatusCountLockedConst asserts ExitStatusCount == 5 and
|
||||
// AllExitStatuses() returns exactly 5 (vision §7, REQ-010, D-036). A
|
||||
// regression firewall: adding/removing/renaming a status breaks this test.
|
||||
func TestExitStatusCountLockedConst(t *testing.T) {
|
||||
if etypes.ExitStatusCount != 5 {
|
||||
t.Errorf("ExitStatusCount = %d, expected 5 (vision §7 LOCKED)", etypes.ExitStatusCount)
|
||||
}
|
||||
all := etypes.AllExitStatuses()
|
||||
if len(all) != 5 {
|
||||
t.Errorf("AllExitStatuses() len = %d, expected 5", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllExitStatusesNames asserts the 5 vision §7 exit-lifecycle names in
|
||||
// order with no extras, no dups, no renames (Proposed, InProgress, Settled,
|
||||
// Failed, Refunded).
|
||||
func TestAllExitStatusesNames(t *testing.T) {
|
||||
want := []string{"Proposed", "InProgress", "Settled", "Failed", "Refunded"}
|
||||
all := etypes.AllExitStatuses()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllExitStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate ExitStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitStatusValues asserts each named const matches its AllExitStatuses
|
||||
// entry.
|
||||
func TestExitStatusValues(t *testing.T) {
|
||||
if etypes.ExitProposed != "Proposed" {
|
||||
t.Errorf("ExitProposed = %q", etypes.ExitProposed)
|
||||
}
|
||||
if etypes.ExitInProgress != "InProgress" {
|
||||
t.Errorf("ExitInProgress = %q", etypes.ExitInProgress)
|
||||
}
|
||||
if etypes.ExitSettled != "Settled" {
|
||||
t.Errorf("ExitSettled = %q", etypes.ExitSettled)
|
||||
}
|
||||
if etypes.ExitFailed != "Failed" {
|
||||
t.Errorf("ExitFailed = %q", etypes.ExitFailed)
|
||||
}
|
||||
if etypes.ExitRefunded != "Refunded" {
|
||||
t.Errorf("ExitRefunded = %q", etypes.ExitRefunded)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ExitRoute struct (bridge-route-id by-ID-string — G-003/A-308) ----------------
|
||||
|
||||
// TestExitRouteStructFields asserts ExitRoute carries all required fields
|
||||
// including the by-ID-string ref to x/bridge BridgeRoute (bridge-route-id)
|
||||
// per A-308/G-003. No struct import of x/bridge (the G-003 import-invariant
|
||||
// test enforces this).
|
||||
func TestExitRouteStructFields(t *testing.T) {
|
||||
r := etypes.ExitRoute{
|
||||
RouteID: "route-1",
|
||||
BridgeRouteID: "bridge-1", // by-ID-string ref to x/bridge (A-308/G-003)
|
||||
Status: etypes.ExitProposed,
|
||||
}
|
||||
if r.RouteID != "route-1" {
|
||||
t.Errorf("RouteID = %q", r.RouteID)
|
||||
}
|
||||
if r.BridgeRouteID != "bridge-1" {
|
||||
t.Errorf("BridgeRouteID = %q", r.BridgeRouteID)
|
||||
}
|
||||
if r.Status != etypes.ExitProposed {
|
||||
t.Errorf("Status = %q", r.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitRouteBridgeRouteIDIsString asserts the BridgeRouteID field is an
|
||||
// opaque string (by-ID-string ref — G-003), NOT a typed x/bridge.BridgeRoute
|
||||
// import. This locks the by-ID-string invariant at the type level.
|
||||
func TestExitRouteBridgeRouteIDIsString(t *testing.T) {
|
||||
r := etypes.ExitRoute{BridgeRouteID: "bridge-9"}
|
||||
// The field must be assignable from a plain string (no bridge.BridgeRoute
|
||||
// type needed).
|
||||
r.BridgeRouteID = "bridge-2"
|
||||
if r.BridgeRouteID != "bridge-2" {
|
||||
t.Errorf("BridgeRouteID = %q, want %q (must be plain string)", r.BridgeRouteID, "bridge-2")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitRouteBridgeRouteIDOptional asserts an empty bridge-route-id is
|
||||
// valid (same-chain exits have no bridge hop).
|
||||
func TestExitRouteBridgeRouteIDOptional(t *testing.T) {
|
||||
r := etypes.ExitRoute{
|
||||
RouteID: "same-chain-exit",
|
||||
BridgeRouteID: "", // empty = same-chain exit (no bridge hop)
|
||||
Status: etypes.ExitSettled,
|
||||
}
|
||||
if r.BridgeRouteID != "" {
|
||||
t.Errorf("BridgeRouteID should be empty for same-chain exit; got %q", r.BridgeRouteID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DEXSwap struct (opaque venue — A-308) --------------------------------------
|
||||
|
||||
// TestDEXSwapStructFields asserts DEXSwap carries all required fields
|
||||
// including the opaque venue string (A-308) and an ExitStatus.
|
||||
func TestDEXSwapStructFields(t *testing.T) {
|
||||
s := etypes.DEXSwap{
|
||||
SwapID: "swap-1",
|
||||
Venue: "uniswap-v3",
|
||||
Status: etypes.ExitSettled,
|
||||
}
|
||||
if s.SwapID != "swap-1" {
|
||||
t.Errorf("SwapID = %q", s.SwapID)
|
||||
}
|
||||
if s.Venue != "uniswap-v3" {
|
||||
t.Errorf("Venue = %q", s.Venue)
|
||||
}
|
||||
if s.Status != etypes.ExitSettled {
|
||||
t.Errorf("Status = %q", s.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapVenueIsOpaqueString asserts the DEXSwap venue is an opaque
|
||||
// string, NOT a locked enum (A-308 — venues are operational, locking now
|
||||
// risks churn). The field must accept any free-form string.
|
||||
func TestDEXSwapVenueIsOpaqueString(t *testing.T) {
|
||||
// A-308: venue is an opaque string, not a locked enum. Various venue
|
||||
// strings must be assignable without any enum type.
|
||||
venues := []string{"uniswap-v3", "oy-dex", "1inch", "paraswap", "0x-api", "custom-venue-xyz"}
|
||||
for _, v := range venues {
|
||||
s := etypes.DEXSwap{SwapID: "s", Venue: v}
|
||||
if s.Venue != v {
|
||||
t.Errorf("Venue = %q, want %q (A-308: venue must be opaque string)", s.Venue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapVenueTypeIsString asserts the Venue field's Go type is the
|
||||
// built-in string (not a typed enum). This locks A-308 at the type level:
|
||||
// the field is a plain string, so any venue string is assignable without
|
||||
// conversion.
|
||||
func TestDEXSwapVenueTypeIsString(t *testing.T) {
|
||||
s := etypes.DEXSwap{}
|
||||
// Assigning a plain string literal must compile and work — no enum
|
||||
// conversion needed. If venue were a typed enum, assigning a plain
|
||||
// string would require a type conversion (e.g. etypes.Venue("x")).
|
||||
s.Venue = "any-string-works"
|
||||
var want string = "any-string-works"
|
||||
if s.Venue != want {
|
||||
t.Errorf("Venue type is not plain string (A-308): got %q want %q", s.Venue, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDEXSwapStatusReusesExitStatus asserts the DEXSwap status field reuses
|
||||
// the ExitStatus enum (a swap shares the exit lifecycle).
|
||||
func TestDEXSwapStatusReusesExitStatus(t *testing.T) {
|
||||
statuses := etypes.AllExitStatuses()
|
||||
for _, st := range statuses {
|
||||
s := etypes.DEXSwap{SwapID: "s", Venue: "v", Status: st}
|
||||
if s.Status != st {
|
||||
t.Errorf("DEXSwap.Status = %q, want %q (must reuse ExitStatus)", s.Status, st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis tests (A-212) ------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Routes and Swaps.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := etypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Routes == nil || len(gs.Routes) != 0 {
|
||||
t.Errorf("Default Routes should be non-nil empty slice; got len=%d nil=%v", len(gs.Routes), gs.Routes == nil)
|
||||
}
|
||||
if gs.Swaps == nil || len(gs.Swaps) != 0 {
|
||||
t.Errorf("Default Swaps should be non-nil empty slice; got len=%d nil=%v", len(gs.Swaps), gs.Swaps == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupRouteIDs asserts A-212: duplicate route-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupRouteIDs(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{
|
||||
{RouteID: "r1", Status: etypes.ExitProposed},
|
||||
{RouteID: "r1", Status: etypes.ExitSettled}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate route-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyRouteID asserts empty route-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyRouteID(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{{RouteID: "", Status: etypes.ExitProposed}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty route-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownRouteStatus asserts an unknown ExitStatus
|
||||
// on a route is rejected.
|
||||
func TestValidateGenesisRejectsUnknownRouteStatus(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{{RouteID: "r1", Status: etypes.ExitStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown exit status on route")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupSwapIDs asserts A-212: duplicate swap-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupSwapIDs(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{
|
||||
{SwapID: "s1", Venue: "uniswap-v3", Status: etypes.ExitSettled},
|
||||
{SwapID: "s1", Venue: "oy-dex", Status: etypes.ExitProposed}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate swap-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptySwapID asserts empty swap-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptySwapID(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{{SwapID: "", Venue: "oy-dex", Status: etypes.ExitProposed}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty swap-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownSwapStatus asserts an unknown ExitStatus
|
||||
// on a swap is rejected.
|
||||
func TestValidateGenesisRejectsUnknownSwapStatus(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Swaps: []etypes.DEXSwap{{SwapID: "s1", Venue: "oy-dex", Status: etypes.ExitStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown exit status on swap")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := etypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := etypes.GenesisState{
|
||||
Routes: []etypes.ExitRoute{
|
||||
{RouteID: "r1", BridgeRouteID: "bridge-1", Status: etypes.ExitInProgress},
|
||||
{RouteID: "r2", BridgeRouteID: "", Status: etypes.ExitSettled}, // same-chain exit
|
||||
},
|
||||
Swaps: []etypes.DEXSwap{
|
||||
{SwapID: "s1", Venue: "uniswap-v3", Status: etypes.ExitSettled},
|
||||
{SwapID: "s2", Venue: "oy-dex", Status: etypes.ExitProposed},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := etypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts -------------------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if etypes.ModuleName != "exit" {
|
||||
t.Errorf("ModuleName = %q", etypes.ModuleName)
|
||||
}
|
||||
if etypes.StoreKey != "exit" {
|
||||
t.Errorf("StoreKey = %q", etypes.StoreKey)
|
||||
}
|
||||
if etypes.RouterKey != "exit" {
|
||||
t.Errorf("RouterKey = %q", etypes.RouterKey)
|
||||
}
|
||||
if etypes.QuerierRoute != "exit" {
|
||||
t.Errorf("QuerierRoute = %q", etypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = etypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
//
|
||||
// The exit module must avoid the banned financial holder terms (the
|
||||
// lexicon firewall's banned list). Use "Holder"/"Reach" instead. The lexicon
|
||||
// helpers are used here — no banned literals are inlined.
|
||||
|
||||
// TestLexiconNoBannedTermsInExitPackage scans every non-test .go file in
|
||||
// the exit/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInExitPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/exit/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in exit/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — use Holder/Reach, not banned financial terms)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInExitTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal.
|
||||
func TestLexiconNoBannedTermsInExitTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("exit test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.3 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/exit/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// Package types defines the Hub API module types (vision §13, REQ-024, D-039).
|
||||
//
|
||||
// The Hub is the B2B backbone: a registry of Hub-brokered services an Anchor
|
||||
// partner operates. v0.3 ships the skeleton (enum + per-service struct stubs
|
||||
// + genesis); the live B2B runtime is deferred to v0.4 (D-039).
|
||||
//
|
||||
// Lexicon note (REQ-012, A-210): the Hub is HIGH lexicon-risk because the
|
||||
// lending primitive is a natural fit for the banned financial terms. The
|
||||
// coupon vocabulary is used EXCLUSIVELY here — "lending"/"coupon"/"custody"/
|
||||
// "compliance"/"jurisdiction" are the safe vision-§13 phrasings; the banned
|
||||
// synonyms for these concepts NEVER appear in this package. "lending" is NOT
|
||||
// a banned term (the banned list has the compounding term and the storage
|
||||
// terms, not "lending" or "loan"); "coupon" is the bond vocabulary (vision
|
||||
// §17). The per-package lexicon assertion in types_test.go is the gate.
|
||||
//
|
||||
// Cross-module references are by-ID-string per G-003 (no struct imports):
|
||||
// - operator-partner-id references an x/partner Anchor Partner by ID-string
|
||||
// (A-304, G-003). The Anchor extension lands in P4; x/hub in P5. The
|
||||
// reference is a string, validated by the keeper against the partner
|
||||
// registry at runtime, not by the type system.
|
||||
// - LendingCouponCapBps is a LOCAL const cross-documented to D-028 /
|
||||
// x/bond CouponCapBps (A-304). x/hub does NOT import x/bond; the cap is
|
||||
// redefined locally so the lending-primitive coupon clamp is enforced
|
||||
// without a cross-module struct import (mirrors how x/guild cross-docs
|
||||
// x/feecovenant WaiverHandPassGuild).
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "hub"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// HubServiceCount is the locked count of HubService enum values (vision
|
||||
// §13, REQ-024, A-312). A regression firewall: adding/removing/renaming a
|
||||
// Hub service breaks this const's test.
|
||||
HubServiceCount = 3
|
||||
|
||||
// LendingCouponCapBps is the LOCAL upper bound on a lending-primitive
|
||||
// coupon in basis points (A-304). It is cross-documented to D-028 and
|
||||
// x/bond.CouponCapBps (also 800, the mission-locked 8pct bond coupon cap).
|
||||
// This const is LOCAL to x/hub to avoid importing x/bond (G-003 — no
|
||||
// cross-module struct imports). The two consts MUST stay in sync; a
|
||||
// change to x/bond.CouponCapBps requires a matching change here. The
|
||||
// ClampLendingCoupon helper uses this local const, NOT x/bond.Clamp.
|
||||
LendingCouponCapBps = uint32(800) // 8pct (cross-doc D-028 / x/bond CouponCapBps — A-304)
|
||||
|
||||
// LendingCouponFloorBps is the LOCAL lower bound on a lending-primitive
|
||||
// coupon (A-304, cross-doc to D-028 / x/bond.CouponFloorBps = 0). Local
|
||||
// const for the same G-003 reason as LendingCouponCapBps.
|
||||
LendingCouponFloorBps = uint32(0) // 0pct (cross-doc D-028 / x/bond CouponFloorBps — A-304)
|
||||
)
|
||||
|
||||
// HubService enumerates the three Hub-brokered B2B service categories (vision
|
||||
// §13, REQ-024, A-312): Custody (asset safekeeping), LendingPrimitive (the
|
||||
// protocol-level lending primitive, NOT a live market), Compliance (on-chain
|
||||
// compliance attestations). The full B2B suite is deferred to v0.4 (D-039).
|
||||
type HubService string
|
||||
|
||||
const (
|
||||
ServiceCustody HubService = "Custody" // asset safekeeping
|
||||
ServiceLendingPrimitive HubService = "LendingPrimitive" // protocol-level lending primitive
|
||||
ServiceCompliance HubService = "Compliance" // on-chain compliance attestations
|
||||
)
|
||||
|
||||
// AllHubServices returns all three HubService values in vision §13 order.
|
||||
// Locked-const test asserts exactly 3 entries with these names (REQ-024).
|
||||
func AllHubServices() []HubService {
|
||||
return []HubService{
|
||||
ServiceCustody,
|
||||
ServiceLendingPrimitive,
|
||||
ServiceCompliance,
|
||||
}
|
||||
}
|
||||
|
||||
// CustodyService is the per-service struct stub for a Hub custody service
|
||||
// (vision §13, REQ-024). custody-id is the service identifier. operator-
|
||||
// partner-id references an x/partner Anchor Partner by ID-string (A-304,
|
||||
// G-003 — no struct import of x/partner). asset-ref is an opaque reference to
|
||||
// the custodied asset (the asset identifier is opaque so the Hub does not
|
||||
// import any asset-denom module).
|
||||
type CustodyService struct {
|
||||
CustodyID string `json:"custody_id" yaml:"custody_id"`
|
||||
OperatorPartnerID string `json:"operator_partner_id" yaml:"operator_partner_id"`
|
||||
AssetRef string `json:"asset_ref" yaml:"asset_ref"`
|
||||
}
|
||||
|
||||
// LendingPrimitive is the per-service struct stub for a Hub lending-primitive
|
||||
// service (vision §13, REQ-024). loan-id is the primitive identifier.
|
||||
// principal-grain is the principal in Grain (the OY internal unit, cross-ref
|
||||
// x/bread by name only — no struct import). coupon-bps is the coupon rate in
|
||||
// basis points, clamped to [LendingCouponFloorBps, LendingCouponCapBps] by
|
||||
// ClampLendingCoupon at construction (NewLendingPrimitive). term-days is the
|
||||
// primitive term length. The coupon vocabulary is used EXCLUSIVELY here
|
||||
// (A-210); the banned compounding term and storage terms NEVER appear.
|
||||
type LendingPrimitive struct {
|
||||
LoanID string `json:"loan_id" yaml:"loan_id"`
|
||||
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
|
||||
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
|
||||
TermDays uint32 `json:"term_days" yaml:"term_days"`
|
||||
}
|
||||
|
||||
// ComplianceService is the per-service struct stub for a Hub compliance
|
||||
// service (vision §13, REQ-024). compliance-id is the service identifier.
|
||||
// jurisdiction is an opaque jurisdiction tag (e.g. "EU-MiCA"). attestation-
|
||||
// uri is an opaque URI to the compliance attestation (kept opaque in the
|
||||
// skeleton, like the v0.2 Pier CredentialRef).
|
||||
type ComplianceService struct {
|
||||
ComplianceID string `json:"compliance_id" yaml:"compliance_id"`
|
||||
Jurisdiction string `json:"jurisdiction" yaml:"jurisdiction"`
|
||||
AttestationURI string `json:"attestation_uri" yaml:"attestation_uri"`
|
||||
}
|
||||
|
||||
// ClampLendingCoupon ensures a lending-primitive coupon is within the LOCKED
|
||||
// LOCAL bounds (A-304: never above the local cap, never below the local floor).
|
||||
// This mirrors x/bond.Clamp's shape (min(cap, max(floor, coupon))) but uses the
|
||||
// LOCAL LendingCouponCapBps / LendingCouponFloorBps consts — it does NOT import
|
||||
// x/bond.Clamp (G-003). The clamp is automatic and authoritative; the live
|
||||
// keeper enforces it at construction and at genesis load.
|
||||
func ClampLendingCoupon(couponBps uint32) uint32 {
|
||||
if couponBps > LendingCouponCapBps {
|
||||
return LendingCouponCapBps
|
||||
}
|
||||
if couponBps < LendingCouponFloorBps {
|
||||
return LendingCouponFloorBps
|
||||
}
|
||||
return couponBps
|
||||
}
|
||||
|
||||
// NewLendingPrimitive constructs a LendingPrimitive with the coupon clamped to
|
||||
// the LOCAL [floor, cap] bounds via ClampLendingCoupon (A-304). The stub does
|
||||
// not persist or enforce referential integrity of operator-partner-id; it only
|
||||
// enforces the coupon clamp invariant at construction time.
|
||||
func NewLendingPrimitive(loanID string, principalGrain int64, couponBps uint32, termDays uint32) LendingPrimitive {
|
||||
return LendingPrimitive{
|
||||
LoanID: loanID,
|
||||
PrincipalGrain: principalGrain,
|
||||
CouponBps: ClampLendingCoupon(couponBps),
|
||||
TermDays: termDays,
|
||||
}
|
||||
}
|
||||
|
||||
// Params for the hub module (skeleton — no tunables in v0.3; the lending
|
||||
// coupon cap/floor are LOCKED LOCAL consts, not Params fields).
|
||||
type Params struct{}
|
||||
|
||||
// DefaultParams returns the zero-value Params (skeleton — no tunables).
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the hub module genesis state (REQ-024). The three
|
||||
// slices hold the per-service stubs. ValidateGenesis enforces per-set ID
|
||||
// uniqueness (A-212) and the lending-primitive coupon clamp at genesis load
|
||||
// (each LendingPrimitive's coupon-bps must be within the LOCAL bounds).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
CustodyServices []CustodyService `json:"custody_services" yaml:"custody_services"`
|
||||
LendingPrimitives []LendingPrimitive `json:"lending_primitives" yaml:"lending_primitives"`
|
||||
ComplianceServices []ComplianceService `json:"compliance_services" yaml:"compliance_services"`
|
||||
}
|
||||
|
||||
// DefaultGenesisState returns an empty genesis state with non-nil slices.
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
CustodyServices: []CustodyService{},
|
||||
LendingPrimitives: []LendingPrimitive{},
|
||||
ComplianceServices: []ComplianceService{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op) and the lending-primitive coupon clamp at genesis load (A-304):
|
||||
// rejects duplicate custody-ids, loan-ids, compliance-ids, and any
|
||||
// LendingPrimitive whose coupon-bps is outside the LOCAL [floor, cap] bounds.
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("hub: invalid genesis: %w", err)
|
||||
}
|
||||
if err := validateCustodyServices(gs.CustodyServices); err != nil {
|
||||
return fmt.Errorf("hub: %w", err)
|
||||
}
|
||||
if err := validateLendingPrimitives(gs.LendingPrimitives); err != nil {
|
||||
return fmt.Errorf("hub: %w", err)
|
||||
}
|
||||
if err := validateComplianceServices(gs.ComplianceServices); err != nil {
|
||||
return fmt.Errorf("hub: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCustodyServices enforces custody-id presence and uniqueness.
|
||||
func validateCustodyServices(svcs []CustodyService) error {
|
||||
seen := make(map[string]bool, len(svcs))
|
||||
for i, c := range svcs {
|
||||
if c.CustodyID == "" {
|
||||
return fmt.Errorf("custody service [%d]: empty custody-id", i)
|
||||
}
|
||||
if seen[c.CustodyID] {
|
||||
return fmt.Errorf("custody service: duplicate custody-id %q", c.CustodyID)
|
||||
}
|
||||
seen[c.CustodyID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLendingPrimitives enforces loan-id presence/uniqueness and the
|
||||
// LOCAL coupon clamp at genesis load (A-304).
|
||||
func validateLendingPrimitives(svcs []LendingPrimitive) error {
|
||||
seen := make(map[string]bool, len(svcs))
|
||||
for i, l := range svcs {
|
||||
if l.LoanID == "" {
|
||||
return fmt.Errorf("lending primitive [%d]: empty loan-id", i)
|
||||
}
|
||||
if seen[l.LoanID] {
|
||||
return fmt.Errorf("lending primitive: duplicate loan-id %q", l.LoanID)
|
||||
}
|
||||
seen[l.LoanID] = true
|
||||
if l.CouponBps < LendingCouponFloorBps || l.CouponBps > LendingCouponCapBps {
|
||||
return fmt.Errorf("lending primitive %q: coupon-bps %d outside [%d, %d] (A-304 clamp at genesis load)",
|
||||
l.LoanID, l.CouponBps, LendingCouponFloorBps, LendingCouponCapBps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateComplianceServices enforces compliance-id presence and uniqueness.
|
||||
func validateComplianceServices(svcs []ComplianceService) error {
|
||||
seen := make(map[string]bool, len(svcs))
|
||||
for i, c := range svcs {
|
||||
if c.ComplianceID == "" {
|
||||
return fmt.Errorf("compliance service [%d]: empty compliance-id", i)
|
||||
}
|
||||
if seen[c.ComplianceID] {
|
||||
return fmt.Errorf("compliance service: duplicate compliance-id %q", c.ComplianceID)
|
||||
}
|
||||
seen[c.ComplianceID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
htypes "github.com/oy/openyield/x/hub/types"
|
||||
)
|
||||
|
||||
// --- HubService enum coverage (3) ----------------------------------------------
|
||||
|
||||
// TestHubServiceCountLockedConst asserts HubServiceCount == 3 and
|
||||
// AllHubServices() returns exactly 3 (REQ-024, A-312). A regression firewall.
|
||||
func TestHubServiceCountLockedConst(t *testing.T) {
|
||||
if htypes.HubServiceCount != 3 {
|
||||
t.Errorf("HubServiceCount = %d, expected 3 (REQ-024 LOCKED)", htypes.HubServiceCount)
|
||||
}
|
||||
all := htypes.AllHubServices()
|
||||
if len(all) != 3 {
|
||||
t.Errorf("AllHubServices() len = %d, expected 3", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllHubServicesNames asserts the 3 REQ-024 names in order with no
|
||||
// extras, no dups, no renames.
|
||||
func TestAllHubServicesNames(t *testing.T) {
|
||||
want := []string{"Custody", "LendingPrimitive", "Compliance"}
|
||||
all := htypes.AllHubServices()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllHubServices()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate HubService %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestHubServiceValues asserts each named const matches its AllHubServices
|
||||
// entry.
|
||||
func TestHubServiceValues(t *testing.T) {
|
||||
if htypes.ServiceCustody != "Custody" {
|
||||
t.Errorf("ServiceCustody = %q", htypes.ServiceCustody)
|
||||
}
|
||||
if htypes.ServiceLendingPrimitive != "LendingPrimitive" {
|
||||
t.Errorf("ServiceLendingPrimitive = %q", htypes.ServiceLendingPrimitive)
|
||||
}
|
||||
if htypes.ServiceCompliance != "Compliance" {
|
||||
t.Errorf("ServiceCompliance = %q", htypes.ServiceCompliance)
|
||||
}
|
||||
}
|
||||
|
||||
// --- LendingCouponCapBps LOCAL const (A-304) -----------------------------------
|
||||
|
||||
// TestLendingCouponCapBpsLockedConst asserts the LOCAL LendingCouponCapBps ==
|
||||
// 800 (A-304 cross-doc to D-028 / x/bond.CouponCapBps). The const is LOCAL to
|
||||
// x/hub to avoid importing x/bond (G-003); the test asserts the value matches
|
||||
// the bond cap so the two consts stay in sync.
|
||||
func TestLendingCouponCapBpsLockedConst(t *testing.T) {
|
||||
if htypes.LendingCouponCapBps != 800 {
|
||||
t.Errorf("LendingCouponCapBps = %d, expected 800 (A-304 cross-doc D-028)", htypes.LendingCouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLendingCouponFloorBpsLockedConst asserts the LOCAL
|
||||
// LendingCouponFloorBps == 0 (A-304 cross-doc to D-028 / x/bond.CouponFloorBps).
|
||||
func TestLendingCouponFloorBpsLockedConst(t *testing.T) {
|
||||
if htypes.LendingCouponFloorBps != 0 {
|
||||
t.Errorf("LendingCouponFloorBps = %d, expected 0 (A-304 cross-doc D-028)", htypes.LendingCouponFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ClampLendingCoupon invariants (A-304) -------------------------------------
|
||||
// The ClampLendingCoupon invariant is the hub module's firewall (A-304): a
|
||||
// lending-primitive coupon can never exceed the local cap (8pct) and can
|
||||
// never fall below the local floor (0pct). These tests are the regression
|
||||
// firewall — a change to LendingCouponCapBps or LendingCouponFloorBps breaks
|
||||
// them.
|
||||
|
||||
// TestClampLendingCouponBelowFloorReturnsFloor asserts a coupon below the
|
||||
// floor is clamped up to the floor. The floor is 0 and uint32 cannot be
|
||||
// negative, so the below-floor case is type-prevented; the test asserts the
|
||||
// floor boundary passes through.
|
||||
func TestClampLendingCouponBelowFloorReturnsFloor(t *testing.T) {
|
||||
got := htypes.ClampLendingCoupon(htypes.LendingCouponFloorBps)
|
||||
if got != htypes.LendingCouponFloorBps {
|
||||
t.Errorf("ClampLendingCoupon(floor) = %d, expected floor %d", got, htypes.LendingCouponFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampLendingCouponAboveCapReturnsCap asserts a coupon above the cap is
|
||||
// clamped down to the cap.
|
||||
func TestClampLendingCouponAboveCapReturnsCap(t *testing.T) {
|
||||
cases := []uint32{
|
||||
uint32(htypes.LendingCouponCapBps) + 1,
|
||||
uint32(htypes.LendingCouponCapBps) + 100,
|
||||
uint32(htypes.LendingCouponCapBps) + 1000,
|
||||
900,
|
||||
1000,
|
||||
5000,
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := htypes.ClampLendingCoupon(c)
|
||||
if got != htypes.LendingCouponCapBps {
|
||||
t.Errorf("ClampLendingCoupon(%d) = %d, expected cap %d (above-cap must clamp to cap)", c, got, htypes.LendingCouponCapBps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampLendingCouponInRangeUnchanged asserts a coupon within [floor, cap]
|
||||
// is unchanged.
|
||||
func TestClampLendingCouponInRangeUnchanged(t *testing.T) {
|
||||
cases := []uint32{
|
||||
0,
|
||||
1,
|
||||
100,
|
||||
400,
|
||||
500,
|
||||
799,
|
||||
uint32(htypes.LendingCouponCapBps),
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := htypes.ClampLendingCoupon(c)
|
||||
if got != c {
|
||||
t.Errorf("ClampLendingCoupon(%d) = %d, expected %d (in-range must be unchanged)", c, got, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampLendingCouponShape asserts the min(cap, max(floor, coupon)) shape
|
||||
// at the boundaries.
|
||||
func TestClampLendingCouponShape(t *testing.T) {
|
||||
if htypes.ClampLendingCoupon(0) != 0 {
|
||||
t.Error("ClampLendingCoupon(0) should be 0 (floor boundary)")
|
||||
}
|
||||
if htypes.ClampLendingCoupon(800) != 800 {
|
||||
t.Error("ClampLendingCoupon(800) should be 800 (cap boundary)")
|
||||
}
|
||||
if htypes.ClampLendingCoupon(801) != 800 {
|
||||
t.Error("ClampLendingCoupon(801) should be 800 (above-cap clamps to cap)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Per-service struct stubs --------------------------------------------------
|
||||
|
||||
// TestCustodyServiceStructFields asserts CustodyService carries custody-id,
|
||||
// operator-partner-id (by-ID-string ref to x/partner Anchor — G-003), asset-ref.
|
||||
func TestCustodyServiceStructFields(t *testing.T) {
|
||||
c := htypes.CustodyService{
|
||||
CustodyID: "cust-1",
|
||||
OperatorPartnerID: "anchor-partner-1",
|
||||
AssetRef: "bread-grain",
|
||||
}
|
||||
if c.CustodyID != "cust-1" || c.OperatorPartnerID != "anchor-partner-1" || c.AssetRef != "bread-grain" {
|
||||
t.Error("CustodyService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCustodyServiceOperatorPartnerIDIsString asserts operator-partner-id is
|
||||
// string-typed (G-003 by-ID-string ref to x/partner Anchor; no struct import).
|
||||
func TestCustodyServiceOperatorPartnerIDIsString(t *testing.T) {
|
||||
c := htypes.CustodyService{OperatorPartnerID: "anchor-1"}
|
||||
if c.OperatorPartnerID != "anchor-1" {
|
||||
t.Errorf("OperatorPartnerID = %q", c.OperatorPartnerID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLendingPrimitiveStructFields asserts LendingPrimitive carries loan-id,
|
||||
// principal-grain, coupon-bps, term-days.
|
||||
func TestLendingPrimitiveStructFields(t *testing.T) {
|
||||
l := htypes.LendingPrimitive{
|
||||
LoanID: "loan-1",
|
||||
PrincipalGrain: 1_000_000,
|
||||
CouponBps: 500,
|
||||
TermDays: 365,
|
||||
}
|
||||
if l.LoanID != "loan-1" || l.PrincipalGrain != 1_000_000 || l.CouponBps != 500 || l.TermDays != 365 {
|
||||
t.Error("LendingPrimitive fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewLendingPrimitiveClampsCoupon asserts NewLendingPrimitive clamps an
|
||||
// above-cap coupon down to the cap and leaves an in-range coupon unchanged.
|
||||
func TestNewLendingPrimitiveClampsCoupon(t *testing.T) {
|
||||
l := htypes.NewLendingPrimitive("loan-2", 500_000, 1200, 180)
|
||||
if l.CouponBps != htypes.LendingCouponCapBps {
|
||||
t.Errorf("CouponBps = %d, expected cap %d (NewLendingPrimitive must clamp above-cap coupon)", l.CouponBps, htypes.LendingCouponCapBps)
|
||||
}
|
||||
l2 := htypes.NewLendingPrimitive("loan-3", 500_000, 300, 180)
|
||||
if l2.CouponBps != 300 {
|
||||
t.Errorf("CouponBps = %d, expected 300 (in-range, unchanged)", l2.CouponBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestComplianceServiceStructFields asserts ComplianceService carries
|
||||
// compliance-id, jurisdiction, attestation-uri.
|
||||
func TestComplianceServiceStructFields(t *testing.T) {
|
||||
c := htypes.ComplianceService{
|
||||
ComplianceID: "comp-1",
|
||||
Jurisdiction: "EU-MiCA",
|
||||
AttestationURI: "ipfs://attestation/abc",
|
||||
}
|
||||
if c.ComplianceID != "comp-1" || c.Jurisdiction != "EU-MiCA" || c.AttestationURI != "ipfs://attestation/abc" {
|
||||
t.Error("ComplianceService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts + Params ----------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if htypes.ModuleName != "hub" {
|
||||
t.Errorf("ModuleName = %q", htypes.ModuleName)
|
||||
}
|
||||
if htypes.StoreKey != "hub" {
|
||||
t.Errorf("StoreKey = %q", htypes.StoreKey)
|
||||
}
|
||||
if htypes.RouterKey != "hub" {
|
||||
t.Errorf("RouterKey = %q", htypes.RouterKey)
|
||||
}
|
||||
if htypes.QuerierRoute != "hub" {
|
||||
t.Errorf("QuerierRoute = %q", htypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = htypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Genesis -------------------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for all three service sets.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := htypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.CustodyServices == nil || len(gs.CustodyServices) != 0 {
|
||||
t.Errorf("Default CustodyServices should be non-nil empty slice; got len=%d nil=%v", len(gs.CustodyServices), gs.CustodyServices == nil)
|
||||
}
|
||||
if gs.LendingPrimitives == nil || len(gs.LendingPrimitives) != 0 {
|
||||
t.Errorf("Default LendingPrimitives should be non-nil empty slice; got len=%d nil=%v", len(gs.LendingPrimitives), gs.LendingPrimitives == nil)
|
||||
}
|
||||
if gs.ComplianceServices == nil || len(gs.ComplianceServices) != 0 {
|
||||
t.Errorf("Default ComplianceServices should be non-nil empty slice; got len=%d nil=%v", len(gs.ComplianceServices), gs.ComplianceServices == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupCustodyIDs asserts A-212: duplicate custody-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupCustodyIDs(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
CustodyServices: []htypes.CustodyService{
|
||||
{CustodyID: "c1", OperatorPartnerID: "a1"},
|
||||
{CustodyID: "c1", OperatorPartnerID: "a2"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate custody-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyCustodyID asserts empty custody-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyCustodyID(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
CustodyServices: []htypes.CustodyService{{CustodyID: "", OperatorPartnerID: "a1"}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty custody-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupLoanIDs asserts duplicate loan-ids are rejected.
|
||||
func TestValidateGenesisRejectsDupLoanIDs(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
LendingPrimitives: []htypes.LendingPrimitive{
|
||||
{LoanID: "l1", CouponBps: 100},
|
||||
{LoanID: "l1", CouponBps: 200}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate loan-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsCouponAboveCap asserts the genesis-side clamp: a
|
||||
// LendingPrimitive with coupon-bps above the local cap is rejected (A-304).
|
||||
func TestValidateGenesisRejectsCouponAboveCap(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
LendingPrimitives: []htypes.LendingPrimitive{
|
||||
{LoanID: "l1", CouponBps: uint32(htypes.LendingCouponCapBps) + 1},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject lending-primitive coupon-bps above local cap (A-304)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupComplianceIDs asserts duplicate compliance-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupComplianceIDs(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
ComplianceServices: []htypes.ComplianceService{
|
||||
{ComplianceID: "comp-1"},
|
||||
{ComplianceID: "comp-1"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate compliance-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := htypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := htypes.GenesisState{
|
||||
CustodyServices: []htypes.CustodyService{
|
||||
{CustodyID: "c1", OperatorPartnerID: "a1", AssetRef: "bread"},
|
||||
},
|
||||
LendingPrimitives: []htypes.LendingPrimitive{
|
||||
{LoanID: "l1", PrincipalGrain: 1_000_000, CouponBps: 500, TermDays: 365},
|
||||
{LoanID: "l2", PrincipalGrain: 500_000, CouponBps: 800, TermDays: 180},
|
||||
},
|
||||
ComplianceServices: []htypes.ComplianceService{
|
||||
{ComplianceID: "comp-1", Jurisdiction: "EU-MiCA", AttestationURI: "ipfs://x"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := htypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The hub module is HIGH lexicon-risk (lending primitive): the banned terms
|
||||
// that are natural fit-words for a lending primitive (the compounding term,
|
||||
// the storage terms, the tradable-unit terms) must NEVER appear. The coupon
|
||||
// + lending vocabulary is used EXCLUSIVELY. The lexicon helpers are used
|
||||
// here — no banned literals are inlined in this test file.
|
||||
|
||||
// TestLexiconNoBannedTermsInHubPackage scans every non-test .go file in the
|
||||
// hub/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInHubPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/hub/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in hub/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — coupon+lending vocabulary only)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInHubTestFile asserts this test file itself does not
|
||||
// contain any banned term as a literal (the firewall scans test files too;
|
||||
// the lexicon helpers must be used rather than inlining banned terms).
|
||||
func TestLexiconNoBannedTermsInHubTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("hub test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by walking
|
||||
// up from this test file (v0.3 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/hub/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
@@ -155,6 +155,53 @@ func (k *Keeper) ListByTier(tier PartnerTier) []Partner {
|
||||
return out
|
||||
}
|
||||
|
||||
// AnchorCredential is the institutional onboarding metadata for an Anchor
|
||||
// tier Partner (REQ-023, D-038, A-305). The Anchor tier (the 4th of the
|
||||
// 4-tier Partner Spectrum, REQ-018) gets institution-specific credential
|
||||
// fields in v0.3. v0.2 defined the 4-tier enum + Partner struct +
|
||||
// CredentialRef; v0.3 adds this AnchorCredential struct carrying the
|
||||
// institutional onboarding metadata. No live institutional onboarding in
|
||||
// v0.3 (the skeleton defines the type shape only).
|
||||
//
|
||||
// All cross-module references are by-ID-string per G-003:
|
||||
//
|
||||
// - anchor-id references a Partner with Tier=Anchor by ID-string
|
||||
// (G-003). No struct import; the reference is validated against the
|
||||
// Partner registry by the keeper, not the type system.
|
||||
// - custody-provider-id references an x/hub custody service by ID-string
|
||||
// (A-304/G-003). The hub is NOT live until P5/v0.4, so this field is
|
||||
// EMPTY in the v0.3 skeleton (NewAnchorCredential sets it to "").
|
||||
// The field exists so the shape is stable when the hub comes online.
|
||||
// No struct import of x/hub.
|
||||
// - credential-uri is an opaque URI to the institutional credential
|
||||
// (regulatory jurisdiction, attestation refs, etc.) — like the v0.2
|
||||
// Pier CredentialRef, kept opaque in the skeleton.
|
||||
// - attestation-count is the number of Watcher/auditor attestations on
|
||||
// the credential (starts at 0 in the skeleton).
|
||||
type AnchorCredential struct {
|
||||
AnchorID string `json:"anchor_id" yaml:"anchor_id"`
|
||||
CustodyProviderID string `json:"custody_provider_id" yaml:"custody_provider_id"`
|
||||
CredentialURI string `json:"credential_uri" yaml:"credential_uri"`
|
||||
AttestationCount uint32 `json:"attestation_count" yaml:"attestation_count"`
|
||||
}
|
||||
|
||||
// NewAnchorCredential constructs an AnchorCredential for an Anchor-tier
|
||||
// Partner (D-038, A-305). The custody-provider-id is set to "" (empty)
|
||||
// because the x/hub custody service is NOT live until P5/v0.4 (A-304:
|
||||
// the field is typed-but-empty in the v0.3 skeleton; the hub is live in
|
||||
// P5, so the field exists but is not validated against hub yet). The
|
||||
// attestation-count is set to 0 (no attestations in the skeleton). The
|
||||
// caller supplies the anchor-id (the Anchor Partner's ID) and the opaque
|
||||
// credential-uri.
|
||||
func NewAnchorCredential(anchorID, credentialURI string) AnchorCredential {
|
||||
return AnchorCredential{
|
||||
AnchorID: anchorID,
|
||||
CustodyProviderID: "", // empty — hub not live until P5/v0.4 (A-304)
|
||||
CredentialURI: credentialURI,
|
||||
AttestationCount: 0, // no attestations in the skeleton
|
||||
}
|
||||
}
|
||||
|
||||
// Params for the partner module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
|
||||
@@ -411,6 +411,151 @@ func TestLexiconNoBannedTermsInPartnerTestFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- v0.3 Partner extension (P4-04, D-038, A-305) — AnchorCredential -------------
|
||||
//
|
||||
// The following tests extend the v0.2 partner tests with the v0.3
|
||||
// AnchorCredential struct (D-038). The existing v0.1/v0.2 tests above
|
||||
// MUST remain green — no regression. The PartnerTier enum (4 tiers) is
|
||||
// locked since v0.2; v0.3 adds the AnchorCredential STRUCT only (no new
|
||||
// tier — A-305).
|
||||
|
||||
// TestAnchorCredentialStructFields asserts the AnchorCredential struct
|
||||
// carries all required fields (anchor-id, custody-provider-id,
|
||||
// credential-uri, attestation-count) per D-038/A-305.
|
||||
func TestAnchorCredentialStructFields(t *testing.T) {
|
||||
c := types.AnchorCredential{
|
||||
AnchorID: "anchor-1",
|
||||
CustodyProviderID: "hub-custody-1",
|
||||
CredentialURI: "oy:cred:anchor-1/jurisdiction/EU-MiCA",
|
||||
AttestationCount: 3,
|
||||
}
|
||||
if c.AnchorID != "anchor-1" {
|
||||
t.Errorf("AnchorID = %q", c.AnchorID)
|
||||
}
|
||||
if c.CustodyProviderID != "hub-custody-1" {
|
||||
t.Errorf("CustodyProviderID = %q", c.CustodyProviderID)
|
||||
}
|
||||
if c.CredentialURI != "oy:cred:anchor-1/jurisdiction/EU-MiCA" {
|
||||
t.Errorf("CredentialURI = %q", c.CredentialURI)
|
||||
}
|
||||
if c.AttestationCount != 3 {
|
||||
t.Errorf("AttestationCount = %d, want 3", c.AttestationCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnchorCredentialAnchorIDIsString asserts the AnchorID field is an
|
||||
// opaque string (by-ID-string ref to a Partner with Tier=Anchor — G-003),
|
||||
// NOT a typed Partner import. This locks the by-ID-string invariant at
|
||||
// the type level.
|
||||
func TestAnchorCredentialAnchorIDIsString(t *testing.T) {
|
||||
c := types.AnchorCredential{AnchorID: "partner-9"}
|
||||
c.AnchorID = "partner-2"
|
||||
if c.AnchorID != "partner-2" {
|
||||
t.Errorf("AnchorID = %q, want %q (must be plain string — G-003)", c.AnchorID, "partner-2")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnchorCredentialCustodyProviderIDIsString asserts the
|
||||
// CustodyProviderID field is an opaque string (by-ID-string ref to an
|
||||
// x/hub custody service — A-304/G-003), NOT a typed x/hub import.
|
||||
func TestAnchorCredentialCustodyProviderIDIsString(t *testing.T) {
|
||||
c := types.AnchorCredential{CustodyProviderID: "hub-custody-9"}
|
||||
c.CustodyProviderID = "hub-custody-2"
|
||||
if c.CustodyProviderID != "hub-custody-2" {
|
||||
t.Errorf("CustodyProviderID = %q, want %q (must be plain string — A-304/G-003)", c.CustodyProviderID, "hub-custody-2")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewAnchorCredentialConstruction asserts NewAnchorCredential sets
|
||||
// the anchor-id and credential-uri from the constructor args, AND sets
|
||||
// custody-provider-id to "" (empty — hub not live until P5/v0.4 per
|
||||
// A-304), AND attestation-count to 0 (no attestations in the skeleton).
|
||||
func TestNewAnchorCredentialConstruction(t *testing.T) {
|
||||
c := types.NewAnchorCredential("anchor-1", "oy:cred:anchor-1/EU-MiCA")
|
||||
if c.AnchorID != "anchor-1" {
|
||||
t.Errorf("AnchorID = %q, want %q", c.AnchorID, "anchor-1")
|
||||
}
|
||||
if c.CredentialURI != "oy:cred:anchor-1/EU-MiCA" {
|
||||
t.Errorf("CredentialURI = %q, want %q", c.CredentialURI, "oy:cred:anchor-1/EU-MiCA")
|
||||
}
|
||||
// custody-provider-id must be EMPTY in the skeleton (A-304: hub not
|
||||
// live until P5/v0.4).
|
||||
if c.CustodyProviderID != "" {
|
||||
t.Errorf("CustodyProviderID = %q, want empty (A-304: hub not live until P5)", c.CustodyProviderID)
|
||||
}
|
||||
// attestation-count must be 0 in the skeleton.
|
||||
if c.AttestationCount != 0 {
|
||||
t.Errorf("AttestationCount = %d, want 0 (skeleton)", c.AttestationCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewAnchorCredentialCustodyProviderIDEmptyInvariant asserts the
|
||||
// A-304 invariant: NewAnchorCredential ALWAYS sets custody-provider-id to
|
||||
// "" regardless of inputs (the hub is not live until P5/v0.4; the field
|
||||
// is typed-but-empty in the v0.3 skeleton). This is the dependency edge
|
||||
// that forces P4 before P5 (D-044): x/partner Anchor lands in P4, x/hub
|
||||
// in P5.
|
||||
func TestNewAnchorCredentialCustodyProviderIDEmptyInvariant(t *testing.T) {
|
||||
cases := []struct {
|
||||
anchorID string
|
||||
credURI string
|
||||
}{
|
||||
{"anchor-1", "oy:cred:a/EU-MiCA"},
|
||||
{"anchor-2", "oy:cred:a/US-SOC2"},
|
||||
{"", ""},
|
||||
{"anchor-3", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := types.NewAnchorCredential(c.anchorID, c.credURI)
|
||||
if got.CustodyProviderID != "" {
|
||||
t.Errorf("NewAnchorCredential(%q,%q): CustodyProviderID = %q, want empty (A-304 LOCKED)", c.anchorID, c.credURI, got.CustodyProviderID)
|
||||
}
|
||||
if got.AttestationCount != 0 {
|
||||
t.Errorf("NewAnchorCredential(%q,%q): AttestationCount = %d, want 0 (skeleton)", c.anchorID, c.credURI, got.AttestationCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewAnchorCredentialAttestationCountZero asserts the constructor sets
|
||||
// attestation-count to 0 (no attestations in the skeleton; attestations
|
||||
// are a v0.4 keeper concern).
|
||||
func TestNewAnchorCredentialAttestationCountZero(t *testing.T) {
|
||||
c := types.NewAnchorCredential("anchor-1", "oy:cred:anchor-1/x")
|
||||
if c.AttestationCount != 0 {
|
||||
t.Errorf("AttestationCount = %d, want 0 (skeleton — attestations are v0.4)", c.AttestationCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnchorCredentialZeroValue asserts the zero-value AnchorCredential
|
||||
// has empty strings and a 0 attestation-count.
|
||||
func TestAnchorCredentialZeroValue(t *testing.T) {
|
||||
var c types.AnchorCredential
|
||||
if c.AnchorID != "" || c.CustodyProviderID != "" || c.CredentialURI != "" {
|
||||
t.Error("zero-value AnchorCredential should have empty string fields")
|
||||
}
|
||||
if c.AttestationCount != 0 {
|
||||
t.Errorf("zero-value AttestationCount = %d, want 0", c.AttestationCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartnerTierCountStillFour is the v0.3 REGRESSION test (A-305): the
|
||||
// PartnerTier enum is LOCKED at 4 tiers since v0.2; v0.3 adds the
|
||||
// AnchorCredential STRUCT, NOT a new tier. This test asserts the count
|
||||
// is still 4 (no new tier added by the v0.3 extension).
|
||||
func TestPartnerTierCountStillFour(t *testing.T) {
|
||||
if types.PartnerTierCount != 4 {
|
||||
t.Errorf("PartnerTierCount = %d, expected 4 (A-305: v0.3 adds AnchorCredential struct, not a tier)", types.PartnerTierCount)
|
||||
}
|
||||
all := types.AllPartnerTiers()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllPartnerTiers() len = %d, expected 4 (A-305 regression)", len(all))
|
||||
}
|
||||
// Anchor must still be the 4th tier (no new tier added before/after it).
|
||||
if all[3] != types.TierAnchor {
|
||||
t.Errorf("AllPartnerTiers()[3] = %q, want %q (Anchor must remain 4th tier)", all[3], types.TierAnchor)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.2 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// Package types defines the Services module types (vision §13, REQ-025,
|
||||
// D-040, A-307).
|
||||
//
|
||||
// OY-protocol services beyond the financial layer: Care (community care),
|
||||
// SIM (connectivity), Vault (storage service), Mail (messaging). v0.3 ships
|
||||
// the skeleton (enum + per-service struct stubs + genesis); no live services.
|
||||
//
|
||||
// Lexicon note (REQ-012): "Mail"/"SIM"/"Care"/"Vault" are not banned terms.
|
||||
// Avoid the banned Holder-identity term (use "operator-reach-id" not the
|
||||
// banned term). The per-package lexicon assertion in types_test.go is the gate.
|
||||
//
|
||||
// Cross-module references are by-ID-string per G-003 (no struct imports):
|
||||
// - operator-reach-id references an x/identity Reach by ID-string (G-003).
|
||||
// - window-id references an x/window Window by ID-string (A-307, G-003).
|
||||
// A service-grant opens a Window on the holder's behalf (the Window
|
||||
// Lifecycle interface hook, typed in v0.3, invoked at runtime in v0.4).
|
||||
// - mailbox-id (MailService) and storage-quota-grain (VaultService) are
|
||||
// opaque / in-package values; VaultService references x/vault by name only
|
||||
// (the ServiceKind "Vault" is a service kind, NOT a struct import of
|
||||
// x/vault — the naming collision is concept-level, not package-level).
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "services"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// ServiceKindCount is the locked count of ServiceKind enum values (vision
|
||||
// §13, REQ-025, A-307). A regression firewall: adding/removing/renaming a
|
||||
// service kind breaks this const's test.
|
||||
ServiceKindCount = 4
|
||||
)
|
||||
|
||||
// ServiceKind enumerates the four OY-protocol service kinds (vision §13,
|
||||
// REQ-025, A-307): Care (community care), SIM (subscriber identity module /
|
||||
// connectivity), Vault (storage service), Mail (messaging). The full services
|
||||
// suite (the real-return token, Travel, +11 more) is Phase 4, out of v0.3
|
||||
// scope (D-040). The real-return token's name in vision §13 uses a banned
|
||||
// standalone term; this comment uses the lexicon-safe "real-return" phrasing.
|
||||
type ServiceKind string
|
||||
|
||||
const (
|
||||
KindCare ServiceKind = "Care" // community care
|
||||
KindSIM ServiceKind = "SIM" // connectivity
|
||||
KindVault ServiceKind = "Vault" // storage service
|
||||
KindMail ServiceKind = "Mail" // messaging
|
||||
)
|
||||
|
||||
// AllServiceKinds returns all four ServiceKind values in vision §13 order.
|
||||
// Locked-const test asserts exactly 4 entries with these names (REQ-025).
|
||||
func AllServiceKinds() []ServiceKind {
|
||||
return []ServiceKind{
|
||||
KindCare,
|
||||
KindSIM,
|
||||
KindVault,
|
||||
KindMail,
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStatus enumerates the lifecycle states of a service (REQ-025). This
|
||||
// is a LOCAL redefinition of the 4-state shape (mirrors the v0.2 PartnerStatus
|
||||
// shape); no struct import of x/partner (G-003).
|
||||
type ServiceStatus string
|
||||
|
||||
const (
|
||||
ServicePending ServiceStatus = "Pending" // registered, not yet active
|
||||
ServiceActive ServiceStatus = "Active" // live
|
||||
ServiceSuspended ServiceStatus = "Suspended" // temporarily halted
|
||||
ServiceRevoked ServiceStatus = "Revoked" // permanently revoked
|
||||
)
|
||||
|
||||
// ServiceStatusCount is the locked count of ServiceStatus enum values.
|
||||
const ServiceStatusCount = 4
|
||||
|
||||
// ServiceInfo is the registry record for a service (REQ-025, A-307).
|
||||
// service-id is the unique identifier. kind picks the ServiceKind.
|
||||
// operator-reach-id references an x/identity Reach by ID-string (G-003 — use
|
||||
// "operator-reach-id" not the banned Holder-identity term). name is a human-
|
||||
// readable label. status is the lifecycle state. window-id references an
|
||||
// x/window Window by ID-string (A-307, G-003 — a service-grant opens a Window
|
||||
// on the holder's behalf; the Window Lifecycle interface hook, typed in v0.3,
|
||||
// invoked at runtime in v0.4). The window-id field is the by-ID-string ref
|
||||
// that ties a service-grant to a Window scope.
|
||||
type ServiceInfo struct {
|
||||
ServiceID string `json:"service_id" yaml:"service_id"`
|
||||
Kind ServiceKind `json:"kind" yaml:"kind"`
|
||||
OperatorReachID string `json:"operator_reach_id" yaml:"operator_reach_id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Status ServiceStatus `json:"status" yaml:"status"`
|
||||
WindowID string `json:"window_id" yaml:"window_id"`
|
||||
}
|
||||
|
||||
// CareService is the per-service struct stub for a Care service (vision §13,
|
||||
// REQ-025). care-id is the service identifier. care-kind is an opaque string
|
||||
// (the kind of community care, e.g. "mutual-aid" — opaque so the enum is not
|
||||
// locked in v0.3; care kinds are operational, not protocol-locked).
|
||||
type CareService struct {
|
||||
CareID string `json:"care_id" yaml:"care_id"`
|
||||
CareKind string `json:"care_kind" yaml:"care_kind"`
|
||||
}
|
||||
|
||||
// SIMService is the per-service struct stub for a SIM (connectivity) service
|
||||
// (vision §13, REQ-025). sim-id is the service identifier. carrier is an
|
||||
// opaque string (the connectivity carrier — opaque so the enum is not locked
|
||||
// in v0.3 per A-308 venue pattern; carriers are operational).
|
||||
type SIMService struct {
|
||||
SIMID string `json:"sim_id" yaml:"sim_id"`
|
||||
Carrier string `json:"carrier" yaml:"carrier"`
|
||||
}
|
||||
|
||||
// VaultService is the per-service struct stub for a Vault (storage) service
|
||||
// (vision §13, REQ-025). vault-id is the service identifier. holder-reach-id
|
||||
// references an x/identity Reach by ID-string (G-003 — use "holder-reach-id"
|
||||
// not the banned Holder-identity term). storage-quota-grain is the storage
|
||||
// quota in Grain (the OY internal unit, by name only — no x/bread import).
|
||||
// "Vault" here is a service kind, NOT a struct import of x/vault (the naming
|
||||
// collision is concept-level; VaultService references x/vault by ID-string at
|
||||
// runtime, not by Go import).
|
||||
type VaultService struct {
|
||||
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
|
||||
StorageQuotaGrain int64 `json:"storage_quota_grain" yaml:"storage_quota_grain"`
|
||||
}
|
||||
|
||||
// MailService is the per-service struct stub for a Mail (messaging) service
|
||||
// (vision §13, REQ-025). mail-id is the service identifier. holder-reach-id
|
||||
// references an x/identity Reach by ID-string (G-003). mailbox-id is the
|
||||
// opaque mailbox identifier.
|
||||
type MailService struct {
|
||||
MailID string `json:"mail_id" yaml:"mail_id"`
|
||||
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
|
||||
MailboxID string `json:"mailbox_id" yaml:"mailbox_id"`
|
||||
}
|
||||
|
||||
// Params for the services module (skeleton — no tunables in v0.3).
|
||||
type Params struct{}
|
||||
|
||||
// DefaultParams returns the zero-value Params (skeleton — no tunables).
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the services module genesis state (REQ-025). The
|
||||
// ServiceInfos slice holds the registry records. The per-service stub slices
|
||||
// hold the service-specific metadata. ValidateGenesis enforces service-id
|
||||
// uniqueness across the registry (A-212).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
ServiceInfos []ServiceInfo `json:"service_infos" yaml:"service_infos"`
|
||||
CareServices []CareService `json:"care_services" yaml:"care_services"`
|
||||
SIMServices []SIMService `json:"sim_services" yaml:"sim_services"`
|
||||
VaultServices []VaultService `json:"vault_services" yaml:"vault_services"`
|
||||
MailServices []MailService `json:"mail_services" yaml:"mail_services"`
|
||||
}
|
||||
|
||||
// DefaultGenesisState returns an empty genesis state with non-nil slices.
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
ServiceInfos: []ServiceInfo{},
|
||||
CareServices: []CareService{},
|
||||
SIMServices: []SIMService{},
|
||||
VaultServices: []VaultService{},
|
||||
MailServices: []MailService{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate or empty service-ids in the registry, and unknown
|
||||
// ServiceKind / ServiceStatus values.
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("services: invalid genesis: %w", err)
|
||||
}
|
||||
if err := validateServiceInfos(gs.ServiceInfos); err != nil {
|
||||
return fmt.Errorf("services: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateServiceInfos enforces service-id presence and uniqueness, and that
|
||||
// each Kind/Status is a known enum value.
|
||||
func validateServiceInfos(infos []ServiceInfo) error {
|
||||
seen := make(map[string]bool, len(infos))
|
||||
for i, s := range infos {
|
||||
if s.ServiceID == "" {
|
||||
return fmt.Errorf("service info [%d]: empty service-id", i)
|
||||
}
|
||||
if seen[s.ServiceID] {
|
||||
return fmt.Errorf("service info: duplicate service-id %q", s.ServiceID)
|
||||
}
|
||||
seen[s.ServiceID] = true
|
||||
if !knownServiceKind(s.Kind) {
|
||||
return fmt.Errorf("service %q: unknown service kind %q", s.ServiceID, s.Kind)
|
||||
}
|
||||
if !knownServiceStatus(s.Status) {
|
||||
return fmt.Errorf("service %q: unknown service status %q", s.ServiceID, s.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownServiceKind reports whether k is one of the four ServiceKind values.
|
||||
func knownServiceKind(k ServiceKind) bool {
|
||||
for _, kk := range AllServiceKinds() {
|
||||
if k == kk {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// knownServiceStatus reports whether s is one of the four ServiceStatus values.
|
||||
func knownServiceStatus(s ServiceStatus) bool {
|
||||
switch s {
|
||||
case ServicePending, ServiceActive, ServiceSuspended, ServiceRevoked:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
stypes "github.com/oy/openyield/x/services/types"
|
||||
)
|
||||
|
||||
// --- ServiceKind enum coverage (4) --------------------------------------------
|
||||
|
||||
// TestServiceKindCountLockedConst asserts ServiceKindCount == 4 and
|
||||
// AllServiceKinds() returns exactly 4 (REQ-025, A-307). A regression firewall.
|
||||
func TestServiceKindCountLockedConst(t *testing.T) {
|
||||
if stypes.ServiceKindCount != 4 {
|
||||
t.Errorf("ServiceKindCount = %d, expected 4 (REQ-025 LOCKED)", stypes.ServiceKindCount)
|
||||
}
|
||||
all := stypes.AllServiceKinds()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllServiceKinds() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllServiceKindsNames asserts the 4 REQ-025 names in order with no extras,
|
||||
// no dups, no renames.
|
||||
func TestAllServiceKindsNames(t *testing.T) {
|
||||
want := []string{"Care", "SIM", "Vault", "Mail"}
|
||||
all := stypes.AllServiceKinds()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, k := range all {
|
||||
if string(k) != want[i] {
|
||||
t.Errorf("AllServiceKinds()[%d] = %q, want %q", i, k, want[i])
|
||||
}
|
||||
if seen[string(k)] {
|
||||
t.Errorf("duplicate ServiceKind %q", k)
|
||||
}
|
||||
seen[string(k)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestServiceKindValues asserts each named const matches its AllServiceKinds
|
||||
// entry.
|
||||
func TestServiceKindValues(t *testing.T) {
|
||||
if stypes.KindCare != "Care" {
|
||||
t.Errorf("KindCare = %q", stypes.KindCare)
|
||||
}
|
||||
if stypes.KindSIM != "SIM" {
|
||||
t.Errorf("KindSIM = %q", stypes.KindSIM)
|
||||
}
|
||||
if stypes.KindVault != "Vault" {
|
||||
t.Errorf("KindVault = %q", stypes.KindVault)
|
||||
}
|
||||
if stypes.KindMail != "Mail" {
|
||||
t.Errorf("KindMail = %q", stypes.KindMail)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ServiceStatus enum coverage (4) ------------------------------------------
|
||||
|
||||
// TestServiceStatusCountLockedConst asserts ServiceStatusCount == 4.
|
||||
func TestServiceStatusCountLockedConst(t *testing.T) {
|
||||
if stypes.ServiceStatusCount != 4 {
|
||||
t.Errorf("ServiceStatusCount = %d, expected 4", stypes.ServiceStatusCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServiceStatusValues asserts the four ServiceStatus named consts.
|
||||
func TestServiceStatusValues(t *testing.T) {
|
||||
if stypes.ServicePending != "Pending" {
|
||||
t.Errorf("ServicePending = %q", stypes.ServicePending)
|
||||
}
|
||||
if stypes.ServiceActive != "Active" {
|
||||
t.Errorf("ServiceActive = %q", stypes.ServiceActive)
|
||||
}
|
||||
if stypes.ServiceSuspended != "Suspended" {
|
||||
t.Errorf("ServiceSuspended = %q", stypes.ServiceSuspended)
|
||||
}
|
||||
if stypes.ServiceRevoked != "Revoked" {
|
||||
t.Errorf("ServiceRevoked = %q", stypes.ServiceRevoked)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ServiceInfo struct + by-ID-string refs (G-003, A-307) --------------------
|
||||
|
||||
// TestServiceInfoStructFields asserts ServiceInfo carries service-id, kind,
|
||||
// operator-reach-id, name, status, window-id.
|
||||
func TestServiceInfoStructFields(t *testing.T) {
|
||||
s := stypes.ServiceInfo{
|
||||
ServiceID: "svc-1",
|
||||
Kind: stypes.KindCare,
|
||||
OperatorReachID: "reach-holder-1",
|
||||
Name: "Care Service",
|
||||
Status: stypes.ServiceActive,
|
||||
WindowID: "window-1",
|
||||
}
|
||||
if s.ServiceID != "svc-1" || s.Kind != stypes.KindCare || s.OperatorReachID != "reach-holder-1" ||
|
||||
s.Name != "Care Service" || s.Status != stypes.ServiceActive || s.WindowID != "window-1" {
|
||||
t.Error("ServiceInfo fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServiceInfoWindowIDIsString asserts window-id is string-typed (A-307
|
||||
// by-ID-string ref to x/window — G-003, no struct import). This is the
|
||||
// window-id by-ID-string ref presence test mandated by the P5 task spec.
|
||||
func TestServiceInfoWindowIDIsString(t *testing.T) {
|
||||
s := stypes.ServiceInfo{WindowID: "window-abc"}
|
||||
if s.WindowID != "window-abc" {
|
||||
t.Errorf("WindowID = %q", s.WindowID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServiceInfoOperatorReachIDIsString asserts operator-reach-id is
|
||||
// string-typed (G-003 by-ID-string ref to x/identity Reach — no struct import).
|
||||
func TestServiceInfoOperatorReachIDIsString(t *testing.T) {
|
||||
s := stypes.ServiceInfo{OperatorReachID: "reach-xyz"}
|
||||
if s.OperatorReachID != "reach-xyz" {
|
||||
t.Errorf("OperatorReachID = %q", s.OperatorReachID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Per-service struct stubs --------------------------------------------------
|
||||
|
||||
// TestCareServiceStructFields asserts CareService carries care-id, care-kind.
|
||||
func TestCareServiceStructFields(t *testing.T) {
|
||||
c := stypes.CareService{CareID: "care-1", CareKind: "mutual-aid"}
|
||||
if c.CareID != "care-1" || c.CareKind != "mutual-aid" {
|
||||
t.Error("CareService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSIMServiceStructFields asserts SIMService carries sim-id, carrier.
|
||||
func TestSIMServiceStructFields(t *testing.T) {
|
||||
s := stypes.SIMService{SIMID: "sim-1", Carrier: "oy-mobile"}
|
||||
if s.SIMID != "sim-1" || s.Carrier != "oy-mobile" {
|
||||
t.Error("SIMService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultServiceStructFields asserts VaultService carries vault-id,
|
||||
// holder-reach-id (by-ID-string ref to x/identity — G-003), storage-quota-grain.
|
||||
func TestVaultServiceStructFields(t *testing.T) {
|
||||
v := stypes.VaultService{
|
||||
VaultID: "vault-1",
|
||||
HolderReachID: "reach-holder-1",
|
||||
StorageQuotaGrain: 1_000_000,
|
||||
}
|
||||
if v.VaultID != "vault-1" || v.HolderReachID != "reach-holder-1" || v.StorageQuotaGrain != 1_000_000 {
|
||||
t.Error("VaultService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultServiceHolderReachIDIsString asserts holder-reach-id is string-typed
|
||||
// (G-003 by-ID-string ref to x/identity Reach — no struct import).
|
||||
func TestVaultServiceHolderReachIDIsString(t *testing.T) {
|
||||
v := stypes.VaultService{HolderReachID: "reach-abc"}
|
||||
if v.HolderReachID != "reach-abc" {
|
||||
t.Errorf("HolderReachID = %q", v.HolderReachID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMailServiceStructFields asserts MailService carries mail-id,
|
||||
// holder-reach-id (by-ID-string ref to x/identity — G-003), mailbox-id.
|
||||
func TestMailServiceStructFields(t *testing.T) {
|
||||
m := stypes.MailService{
|
||||
MailID: "mail-1",
|
||||
HolderReachID: "reach-holder-1",
|
||||
MailboxID: "mbox-1",
|
||||
}
|
||||
if m.MailID != "mail-1" || m.HolderReachID != "reach-holder-1" || m.MailboxID != "mbox-1" {
|
||||
t.Error("MailService fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts + Params ----------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if stypes.ModuleName != "services" {
|
||||
t.Errorf("ModuleName = %q", stypes.ModuleName)
|
||||
}
|
||||
if stypes.StoreKey != "services" {
|
||||
t.Errorf("StoreKey = %q", stypes.StoreKey)
|
||||
}
|
||||
if stypes.RouterKey != "services" {
|
||||
t.Errorf("RouterKey = %q", stypes.RouterKey)
|
||||
}
|
||||
if stypes.QuerierRoute != "services" {
|
||||
t.Errorf("QuerierRoute = %q", stypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = stypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Genesis -------------------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for all five sets.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := stypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.ServiceInfos == nil || len(gs.ServiceInfos) != 0 {
|
||||
t.Errorf("Default ServiceInfos should be non-nil empty slice; got len=%d nil=%v", len(gs.ServiceInfos), gs.ServiceInfos == nil)
|
||||
}
|
||||
if gs.CareServices == nil || len(gs.CareServices) != 0 {
|
||||
t.Errorf("Default CareServices should be non-nil empty slice; got len=%d nil=%v", len(gs.CareServices), gs.CareServices == nil)
|
||||
}
|
||||
if gs.SIMServices == nil || len(gs.SIMServices) != 0 {
|
||||
t.Errorf("Default SIMServices should be non-nil empty slice; got len=%d nil=%v", len(gs.SIMServices), gs.SIMServices == nil)
|
||||
}
|
||||
if gs.VaultServices == nil || len(gs.VaultServices) != 0 {
|
||||
t.Errorf("Default VaultServices should be non-nil empty slice; got len=%d nil=%v", len(gs.VaultServices), gs.VaultServices == nil)
|
||||
}
|
||||
if gs.MailServices == nil || len(gs.MailServices) != 0 {
|
||||
t.Errorf("Default MailServices should be non-nil empty slice; got len=%d nil=%v", len(gs.MailServices), gs.MailServices == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupServiceIDs asserts A-212: duplicate service-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupServiceIDs(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
ServiceInfos: []stypes.ServiceInfo{
|
||||
{ServiceID: "s1", Kind: stypes.KindCare, Status: stypes.ServiceActive},
|
||||
{ServiceID: "s1", Kind: stypes.KindSIM, Status: stypes.ServiceActive}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate service-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyServiceID asserts empty service-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyServiceID(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
ServiceInfos: []stypes.ServiceInfo{{ServiceID: "", Kind: stypes.KindCare, Status: stypes.ServiceActive}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty service-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownServiceKind asserts an unknown ServiceKind
|
||||
// is rejected.
|
||||
func TestValidateGenesisRejectsUnknownServiceKind(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
ServiceInfos: []stypes.ServiceInfo{{ServiceID: "s1", Kind: stypes.ServiceKind("Bogus"), Status: stypes.ServiceActive}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown service kind")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownServiceStatus asserts an unknown
|
||||
// ServiceStatus is rejected.
|
||||
func TestValidateGenesisRejectsUnknownServiceStatus(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
ServiceInfos: []stypes.ServiceInfo{{ServiceID: "s1", Kind: stypes.KindCare, Status: stypes.ServiceStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown service status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := stypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
ServiceInfos: []stypes.ServiceInfo{
|
||||
{ServiceID: "s1", Kind: stypes.KindCare, OperatorReachID: "r1", Name: "Care", Status: stypes.ServiceActive, WindowID: "w1"},
|
||||
{ServiceID: "s2", Kind: stypes.KindMail, OperatorReachID: "r2", Name: "Mail", Status: stypes.ServicePending, WindowID: "w2"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The services module must avoid the banned Holder-identity term (use
|
||||
// "operator-reach-id"/"holder-reach-id" not the banned term). "Mail"/"SIM"/
|
||||
// "Care"/"Vault" are not banned. The lexicon helpers are used here — no
|
||||
// banned literals are inlined in this test file.
|
||||
|
||||
// TestLexiconNoBannedTermsInServicesPackage scans every non-test .go file in
|
||||
// the services/types package directory for the banned terms
|
||||
// (case-insensitive). Production files only — the test file references banned
|
||||
// terms via the lexicon package helpers (standard lexicon-test bootstrapping
|
||||
// pattern).
|
||||
func TestLexiconNoBannedTermsInServicesPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/services/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in services/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — use operator-reach-id not the banned Holder-identity term)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInServicesTestFile asserts this test file itself does
|
||||
// not contain any banned term as a literal (the firewall scans test files too;
|
||||
// the lexicon helpers must be used rather than inlining banned terms).
|
||||
func TestLexiconNoBannedTermsInServicesTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("services test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by walking
|
||||
// up from this test file (v0.3 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/services/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
Reference in New Issue
Block a user