Goal: Ship the Window leaf primitive (full lifecycle), the 9-type Stand enum + keeper stub, and the Guild + Hand-Pass @ 0% skeleton — the foundation every downstream Mesh module references.
Wave 1 — Foundation module types (parallel; no internal deps)
Table-driven tests: lifecycle Open→Active→Revoked→Expired; Revoke() transitions to Revoked; revoke-after-expire is no-op (idempotent); double-revoke is idempotent OR error (test both paths); RateLimit consume increments actions-consumed, blocks at max-actions; Scope kind enum coverage; DefaultGenesis empty; ValidateGenesis rejects dup window-ids; lexicon assertion (no banned terms in package); locked-const for WindowStatus count; by-ID-string import invariant (G-003): a test that scans all non-test .go files under x/ using go/parser or a package-import grep and asserts NO production file imports another x/<module>/types package by struct (enforces A-203 as a tested invariant, not just a convention)
go test ./x/window/... passes; ≥80% coverage on x/window/types; lexicon assertion green; lifecycle idempotency tests present; G-003 import-invariant test passes (zero cross-module struct imports in production code)
P1-01-01
P1-01-03
REQ-015
data-engineer
x/window/types/genesis.go (or extend types.go)
Append-only audit log genesis shape: AuditEntry slice in GenesisState, ValidateGenesis enforces append-only ordering (timestamps non-decreasing) + no dup entry-ids
go test ./x/window/... passes; audit-log ordering test green
P1-01-01
P1-02-02
REQ-016
security-engineer
x/stand/types/types_test.go
Locked-const test: AllStandTypes() returns exactly 9; each name matches vision §11 (Household, Crew, Entity, Co-op, Circle, Trust, Foundation, Confederation, Shadow) — no extras, no dups; StandRole enum coverage; ValidateGenesis rejects dup stand-id + dup member-reach; lexicon assertion
go test ./x/stand/... passes; ≥80% coverage; 9-type locked-const test breaks if a type is added/removed/renamed
P1-02-01
P1-02-03
REQ-016
data-engineer
x/stand/types/genesis.go
Membership-set genesis: stands + memberships in GenesisState, ValidateGenesis enforces member-reach uniqueness within a stand
go test ./x/stand/... passes; membership uniqueness test green
P1-02-01
P1-03-02
REQ-017
security-engineer
x/guild/types/types_test.go
Locked-const test: HandPassFeeBps == 0; HandPass fee field always 0; Guild with Stand affiliation OR standalone; issuer type-level (no enforcement in skeleton); ValidateGenesis rejects dup guild-id + dup pass-id; lexicon assertion
go test ./x/guild/... passes; ≥80% coverage; HandPassFeeBps == 0 invariant
Run go build ./... + go test ./... across whole repo; confirm P1 packages ≥80% coverage; confirm lexicon assertion present in all 3 new test files; tag v0.1.1
go test ./... green; go test -cover ./x/window/... ./x/stand/... ./x/guild/... ≥80% each; 3 lexicon assertions present; git tag v0.1.1 created
lexicon_meta_test.go (repo root, package lexicon or main) — NEW (G-004)
Project-wide lexicon meta-test scaffolding: scans all x/**/*.go (production + test) for the 9 banned terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor). Passes at P1 time (v0.1 baseline is clean). The meta-test is the durable firewall created in v0.2 (G-002: v0.1 has NO lexicon test). P5-01-01 will extend this scaffolding to assert all 10 new/extended packages, not create it from scratch. Includes a self-test table (G-009): a hardcoded slice of synthetic strings, one per banned term (e.g. "bank account", "interest yield", "currency dollar euro", "deposit savings depositor"), asserted to each trigger detection — so the meta-test's own coverage is durably verified without manual spikes.
go test ./lexicon_meta_test.go (or go test ./...) green; meta-test scans every .go under x/; self-test table passes for all 9 banned terms (each synthetic string is detected)
P1-01-01
P1 Must-Haves
x/window, x/stand, x/guild each have types/types.go + types/types_test.go (v0.1 pattern, package types, zero external deps).
go build ./... and go test ./... green across the whole repo.
≥80% coverage on x/window/types, x/stand/types, x/guild/types.
Stand locked-const: exactly 9 types with vision §11 names.
Guild HandPassFeeBps == 0 invariant test.
Lexicon assertion in all 3 new test files.
ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1 no-op).
Git tag v0.1.1.
P1 Risks & Mitigations
Rate-limit algorithm unstated (A-206, conf 0.65) → use simple counter (actionsConsumed vs maxActions); flag for v0.3 revisit. Test asserts counter semantics, not algorithm.
Shadow Stand behavioral split (A-213, conf 0.60) → treat all 9 uniformly in skeleton; test asserts uniform enum, no special-casing. Flag for v0.3 design.
Window ↔ downstream import cycles → Window ships first (Wave 1) so P2/P3/P4 reference by WindowRef id-string, not struct import.
Goal: Ship the 6-Pact module (Pause/Ground/Stance/Cover/StandRegistry/HubAPI) with execute-entry stubs + invariant tests, and the 4-tier Partner registry with credential-ref types — both consuming Window + Stand refs from P1.
Goal: Ship the 3-Council module (Mesh/Guild/Stand) with x/gov-shaped tally types + Mission Lock as a const bool, and the Forex Engine v1 stub (pair type + oracle interface + stub keeper) with lexicon-clean pair labels.
Multi-source Voice weighting math unstated (RESEARCH §1.2) → skeleton defines VoiceSource enum + TallyResult struct only; no aggregation algorithm. Flag weighting math as v0.3+ decision.
Forex lexicon hostility (highest risk) → security-engineer lexicon assertion scans pair-label sample data in tests, not just field names. "Forex" itself allowed (vision §13); "Bread/Asset" labels (A-208).
x/gov tally shape drift (A-204, conf 0.75) → cosmos-engineer mirrors TallyResult field names (yes/no/abstain/total); test asserts field names.
Council ↔ Stand/Guild import cycles → reference by id-string only; test asserts no struct imports of x/stand/x/guild.
Goal: Ship the Mesh Bond module with 8% cap / 0% floor clamp invariants, extend the x/bearers module with the BearerTransport interface + OY-LR + Beacon stubs, and ship the L2 satellite module with ICS-20-shaped packet types for Polygon (representative) + 4 enum-placeholder chains.
Clamp invariant tests (highest-severity for bond): below floor → floor; above cap → cap; in range → unchanged; CouponCapBps == 800 locked-const; CouponFloorBps == 0 locked-const; BondStatus enum coverage; Issue stub callable; ValidateGenesis rejects dup bond-id; lexicon assertion (no "interest"/"yield" — use "coupon" exclusively, A-210)
go test ./x/bond/... passes; ≥80% coverage; clamp invariant test breaks if cap/floor change; lexicon green (no banned terms)
P4-01-01
P4-01-03
REQ-021
data-engineer
x/bond/types/genesis.go
Bond issuance genesis: bonds in GenesisState; ValidateGenesis enforces coupon-bps within [floor, cap] for each genesis bond (clamp at genesis load)
go test ./x/bond/... passes; genesis clamp test green
P4-01-01
P4-02-02
D-029
security-engineer
x/bearers/types/types_test.go (extend existing)
BearerTransport interface signature test (a stub impl satisfies it); OYLRLink struct non-empty + surveillance-resistant == true; BeaconFrame struct non-empty + ttl > 0; OY-LR still in AllBearers() (regression: existing v0.1 test still passes); lexicon assertion (extend existing)
go test ./x/bearers/... passes; ≥80% coverage; existing v0.1 bearers tests still green (no regression)
P4-02-01
P4-03-02
REQ-009
security-engineer
x/satellite/types/types_test.go
L2Chain enum has exactly 5 entries (Polygon + Base/Arbitrum/Optimism/Solana); Polygon is the only active rep (D-021); ChannelStatus enum coverage (4 states); Packet struct fields match ICS-20 v1 (cross-check field names); WrappedBreadDenom trace-path encoding; ValidateGenesis rejects dup channel-id; lexicon assertion (no "bank"/"account" — use "Holder"/"Reach")
go test ./x/satellite/... passes; ≥80% coverage; 5-chain enum locked-const; ICS-20 shape test
P4-03-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P4-04-01
REQ-012, D-033
lead-developer
(cross-cutting)
go build ./... + go test ./... green; P4 packages ≥80% coverage; 3 lexicon assertions (bond, bearers-ext, satellite); bond clamp invariant green; tag v0.1.4
go test ./... green; coverage ≥80% on x/bond/types, x/bearers/types, x/satellite/types; existing v0.1 tests still green; git tag v0.1.4
P4-01-02, P4-01-03, P4-02-02, P4-03-02
P4 Must-Haves
x/bond (new), x/bearers (extended), x/satellite (new) each have types/types.go + types/types_test.go.
go build ./... and go test ./... green — including all v0.1 baseline tests (no regression).
≥80% coverage on x/bond/types, x/bearers/types, x/satellite/types.
Bond lexicon risk (RESEARCH §1.8, A-210 conf 1.00) → "coupon" exclusively; security-engineer lexicon assertion is the gate; "interest"/"yield" must never appear.
ICS-20 packet shape drift (A-215, RESEARCH §1.1) → pin to ICS-20 v1 (stable, widely implemented); test asserts field names; refactor cost is types-only if v0.3 picks a different ibc-go version.
Bearers extension regression → existing v0.1 bearers tests must stay green; AllBearers() count unchanged (6); test asserts no regression.
Full 5-chain IBC rollout is Phase 3 (D-021) → only Polygon is active; 4 chains are StatusPending enum placeholders; test asserts exactly 1 active.
Solana lacks native IBC (RESEARCH §1.1) → stubbed as StatusPending; no Solana light-client logic in v0.2.
Tag: v0.1.5 (= milestone v0.2 release — final phase patch IS the milestone release per D-008/D-020)
Goal: Run the full milestone audit — project-wide lexicon meta-test, coverage gate across all 10 new/extended packages, all locked-const invariants green, all REQs in v0.2 scope have skeleton+tests — then ship the v0.2 milestone release as tag v0.1.5.
Wave 1 — Audit + meta-tests (parallel)
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P5-01-01
REQ-012
security-engineer
lexicon_meta_test.go (extend the P1-04-02 scaffolding — G-004/G-009)
Extend the project-wide lexicon meta-test (created in P1-04-02 per G-004) to cover all 10 new/extended v0.2 packages: scans all x/**/*.go for banned terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor); fails on any hit. The self-test table (G-009) from P1-04-02 remains and asserts each of the 9 banned terms' synthetic string is detected. This is an EXTENSION, not creation — the meta-test has been green and guarding since P1.
go test ./... green; meta-test scans every .go under x/ (all 25 packages); self-test table passes for all 9 banned terms (G-009); a deliberately-injected banned term in any new file fails the meta-test
P4-04-01, P1-04-02
P5-01-02
D-033
security-engineer
(cross-cutting)
Coverage gate: go test -cover ./x/window/... ./x/stand/... ./x/guild/... ./x/pact/... ./x/partner/... ./x/council/... ./x/forex/... ./x/bond/... ./x/bearers/... ./x/satellite/... — all ≥80%; produce a coverage report
Coverage report shows ≥80% on all 10 new/extended packages; no package below threshold
REQ-coverage audit: confirm every v0.2 REQ (REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021, Bearers OY-LR/Beacon, Forex v1) has skeleton + tests; update REQUIREMENTS.md status column (Pending → Skeleton). ROADMAP tag-line reconciliation (G-010): add a one-line note to ROADMAP.md that v0.1 pre-MVP shipped on the v0.0.x patch line (ROADMAP lines 4-13) and v0.2 ships on the v0.1.x patch line (config tag_base), so the v0.1.5 milestone release is not confused with a hypothetical v0.1.0 "MVP" tag.
REQUIREMENTS.md status column updated for all v0.2 REQs; each REQ maps to a shipped module file; ROADMAP.md tag-line reconciliation note present
P4-04-01
Wave 2 — Ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P5-02-01
(milestone)
lead-developer
(cross-cutting)
Final ship: go build ./... + go test ./... green; all v0.1 baseline tests still green (no regression across 15 + 10 = 25 packages); create milestone release tag v0.1.5 (= v0.2 milestone release per D-008/D-020); update ROADMAP.md Phase 2 checkbox
go test ./... green; v0.1.5 tag created; ROADMAP.md Phase 2 marked complete; release notes reference v0.2 scope
P5-01-01, P5-01-02, P5-01-03
P5 Must-Haves
Project-wide lexicon meta-test scans all x/**/*.go for all 9 banned terms; green.
Coverage ≥80% on all 10 new/extended packages (window, stand, guild, pact, partner, council, forex, bond, bearers, satellite).
All locked-const invariants green: Window status count, 9 Stands, 4 Partner tiers, 6 Pacts, 3 Councils, Mission Lock non-amendable, Bond 8% cap / 0% floor clamp, Guild 0% fee, Forex spread cap ≥0, 5 L2 chains.
All v0.1 baseline tests still green (no regression across 25 packages).
REQUIREMENTS.md status column updated (all v0.2 REQs → Skeleton).
go build ./... and go test ./... green.
Git tag v0.1.5 created (= v0.2 milestone release).
ROADMAP.md Phase 2 marked complete.
P5 Risks & Mitigations
Lexicon drift via copy-pasted comments (RESEARCH §2) → meta-test scans comments + strings + identifiers; not just exported names.
Coverage cliff at integration boundaries → skeleton-only modules have low cyclomatic complexity, so 80% is achievable with table-driven tests on every exported const/struct/method (v0.1 baseline pattern).
Milestone versioning confusion (v0.2 milestone = v0.1.5 tag) → lead-developer enforces D-008/D-020: final phase patch IS the milestone release; no separate minor tag.
(audit/ship, 0 new — extends P1 lexicon meta-test)
Total
—
31
10 new/extended
Grill Binding Decisions Applied (G-001..G-010)
The Phase 0 grill (see .ciagent/oy/GRILL.md) returned 10 binding decisions, all applied to this plan and to PROJECT.md/RESEARCH.md/PERSONAS.md before Phase 0 ship:
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)
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).
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)
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).
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
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).
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
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).
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
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)
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.
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
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)
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)
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)
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
EXTENDx/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)
EXTENDx/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)
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.
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)
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
EXTENDx/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.
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).
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
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)
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.
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)
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).
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).
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).
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).
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.
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.
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
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.
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.
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.
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.
The contributor edits lexicon/lexicon.go to add a new fragment pair to fragments (e.g., a new banned term).
lexicon.BannedTerms() now returns 11 strings.
lexicon.SyntheticBannedStrings() (REQ-029) is the single source — the contributor does NOT need to update two meta-test tables (the v0.3 drift risk).
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.
The contributor updates the len(terms) == 11 assertion in both meta-tests (the G-014 minimum — both already derive count from BannedTerms()).
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.
Scenario: a contributor accidentally changes x/bond.CouponCapBps from 800 to 900 without updating x/hub.LendingCouponCapBps.
go test ./x/hub/types/... — TestLendingCouponCapMatchesBondCap (REQ-030) fails: hub.LendingCouponCapBps (800) != bond.CouponCapBps (900).
TestConstsAreMissionLocked800And0 (G-015) ALSO fails: bond.CouponCapBps (900) != 800.
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):
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).
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).
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.
REQ-032: .gitea/workflows/docs-build.yml parses; runs go test ./... then mkdocs build (G-016: docs-build needs go-test); go.mod unchanged.
NFR purity gate: zero feat: commits in the v0.4 milestone range (P4 audit enforces).
No regression: go test ./... green; v0.3 coverage floor (93.3%) not reduced on any modified package.
Plans: OpenYield (oy) — v0.5 (Bearers Runtime)
This section APPENDS the v0.5 milestone plan to the v0.1/v0.2/v0.3/v0.4
plans above. It does NOT rewrite or supersede the earlier content. v0.5 is
the first feature milestone to ship executable behavior beyond
invariant tests: the v0.3 Bearers skeletons (x/exit, x/bridge,
x/bearers, x/partner, x/hub, x/services, x/bond, plus the
cross-cutting x/council) are promoted from types + in-memory keeper
stubs + invariant tests to live keeper MsgServer message handlers +
simtest-grade end-to-end flows (D-054). This is NOT mainnet (D-020
continues to govern network deployment; D-054 ratifies runtime =
simtest-grade handlers, not live chain). Tags run on the v0.4.x patch
line (config.json tag_base: v0.4.x): P0 → v0.4.0; execution phases
v0.4.1..v0.4.7; final phase P8 → v0.4.8 IS the v0.5 milestone release
(D-008 — final phase patch IS the milestone release; no separate minor
tag). Branch names use NO oy/ prefix (single-project mode: only oy
exists; the slug prefix would be redundant — config projects[] length
is 1).
Milestone Summary
Milestone: v0.5 — Bearers Runtime
Type: Feature (all execution phases P1..P7 are feat; P8 is final)
Tag base: v0.4.x patch line (P0 → v0.4.0; execution P1..P7 → v0.4.1..v0.4.7; final P8 → v0.4.8 IS the v0.5 milestone release)
Phases: 8 — P1..P7 (execution) + P8 (final review/audit/ship). Phase 0 (this PLAN) is in progress.
Depth: runtime promotion (keeper MsgServer handlers + simtest-grade end-to-end flows, NOT mainnet — D-054/D-020 continues). The v0.3 skeleton types/ contracts are NOT amended; runtime adds behavior on top.
Coverage target: ≥80% on each runtime package (keeper + simtest; D-033 carries forward).
Modules promoted to runtime: 8 (x/exit, x/bridge, x/bearers, x/partner, x/hub, x/services, x/bond, x/council). New enum types: 3 in x/council (ProposalKind, ProposalStatus, VoteOption) + Proposal struct (AUDIT §193 P1-1). New interface: CustodyKeyring (D-058). New dep: cosmos-sdk v0.50.x + ibc-go v8.x (D-055, GRILL-ratified G-006 controlled exception).
G-003 firewall (survives runtime promotion): keepers use expected_keepers.go interface shims (ibc-go convention) for cross-module keeper calls — NO production struct imports across x/<module>/types. The P3→P4 hub dependency is broken this way (hub keeper INTERFACE exists in x/partner/types/expected_keepers.go in P3 territory; the hub keeper IMPL is wired in P4). Test-only cross-package imports remain exempt (the G-003 test exemption, used by REQ-030 in v0.4; simtest may import multiple x/*/keeper packages to wire shims).
G-006 controlled exception (D-055, GRILL-ratified):go.mod gains cosmos-sdk v0.50.x + ibc-go v8.x (A-504, planner confirms the exact pin). Scoped to runtime phases P1..P7; P0 + P8 stay dep-neutral where possible. types/ packages gain sdk.Msg imports for Msg* types (isolated in types/msg_*.go); invariant/lexicon tests stay stdlib-only and green.
The 4 planner-escalation items from RESEARCH §4 (low-confidence assumptions) are resolved here as provisional planner decisions D-062..D-065 (continuing the decision ID sequence from D-061). They are PROVISIONAL until the GRILL stage ratifies them.
ID
Decision
Rationale
Confidence
Alternatives
D-062
cosmos-sdk / ibc-go version pin = cosmos-sdk v0.50.x + ibc-go v8.x (resolves A-504). v0.50.x is the LTS line (go 1.22-compatible); ibc-go v8.x is the stable pairing for cosmos-sdk v0.50. ibc-go v10 (IBC v2 / Eureka) is the documented target pattern but a newer pin — defer to a later upgrade. Mark GRILL-confirmed.
v0.50.x + ibc-go v8.x is the stable, widely-deployed pairing (Osmosis, dYdX-v4 lineage); v10 IBC-v2 is attractive but newer and risks churn in a runtime-promotion milestone. The IBC v2 patterns are documented in RESEARCH but the v0.5 impl uses the v8 stable interfaces.
Bond CLOB match above 800 bps = REJECT (fails closed), NOT clamp-with-refund (resolves A-562). D-057 says "hard clamp on each match"; the runtime interpretation is reject-above-cap. A match whose implied coupon exceeds CouponCapBps=800 bps is REJECTED — the trade fails closed, the resting order stays, the incoming order rests or is cancelled. The mission-lock is a hard invariant (a usury violation), not a soft cap to be clamped with a refund path. Matches within [0, 800] bps use the v0.3 Clamp helper (clamp-within-band is safe — no refund needed since the value is already in-band).
Reject is simpler (no refund path) and is the mission-lock-true choice: a trade above the cap is a usury violation, not a clampable excess. The Fee Covenant Clamp shape (clamp, not reject) applies to ISSUANCE (a coupon field set by the issuer), but MATCHING is a market-determined price — a match above the cap is a violation, not an input to clamp. REJECT fails closed (the safer choice for the highest-severity locked const).
0.70
[clamp-with-refund (the match clears at 800, excess refunded to seller) — adds a refund path, softens the mission-lock]
D-064
MissionLockAmendment-Rejected ProposalKind rejected at ValidateBasic (resolves A-572). The MsgSubmitProposalValidateBasic REJECTS a proposal of kind MissionLockAmendment-Rejected — the message never reaches the handler. The const MissionLockAmendable = false (v0.2 locked) is the firewall; the ValidateBasic is the gate. The proposal is unproposable, not propose-then-fail. The v0.2 TestMissionLockAmendableFalse regression test stays green.
The const is the firewall; the ValidateBasic gate is the dual firewall. Rejecting at ValidateBasic is the cleanest: the message never enters the keeper, no state record is created, no event is emitted. Propose-then-fail (record Pending → auto-transition Failed with event) would document the rejection on-chain but creates a state record for an unproposable proposal — unnecessary state growth. The Mission-Lock-non-amendable design intent is "unproposable", not "propose-then-fail".
0.80
[propose-then-fail (record Pending, auto-transition Failed with "Mission Lock non-amendable" event) — documents the rejection on-chain but creates dead state]
D-065
Watcher Veto quorum default = 6 (resolves A-574), matching REQ-004's 6-of-9 Watcher quorum. A single Veto does NOT block (anti-greed, vision §19); the proposal transitions to Failed only if NoWithVeto >= WatcherVetoQuorum (a Params field, NOT a locked const — the v0.2 Params struct was empty, v0.5 P7 adds WatcherVetoQuorum defaulting to 6). The quorum is a param (governance-tunable in a future milestone) rather than a locked const, to allow adjustment without a locked-const amendment.
REQ-004 fixes the Watcher quorum at 6-of-9; the Veto quorum mirrors it (a Watcher-coordinated veto requires the same quorum as a Watcher attestation). A single Veto blocking would violate the anti-greed principle (vision §19 — no single-actor veto gate). Defaulting to 6 (not locking as a const) lets a future governance vote adjust the quorum without a Mission-Lock-class amendment (Veto quorum is NOT a Mission-Lock const; the distinction is documented in v0.4 ARCHITECTURE.md).
0.75
[lock WatcherVetoQuorum=6 as a const (over-rigid; prevents future tuning); default 9 (requires all Watchers, too high a bar for a veto)]
These 4 decisions are surfaced through the normal decision flow (planner →
GRILL ratification). They are NOT auto-decided (the autonomy threshold for
locked-const-shape and dep-pinning decisions is at the GRILL boundary per
PERSONAS.md lead-developer constraints). The GRILL stage may ratify, amend,
or reject them; if rejected, the planner re-resolves before the affected
phase lands (D-062 before P1; D-063 before P6; D-064/D-065 before P7).
Phase P1 — Exit + Bridge Runtime
Slug: exit-bridge-runtime
Branch: phase/01-exit-bridge-runtime
REQs covered: REQ-033 (Exit layer runtime — x/exit DEX swap routing + x/bridge L2↔L1 IBC packet handlers for the 5 locked L2 chains per D-059)
Tag: v0.4.1
Type: feat
Goal: Promote x/exit and x/bridge from v0.3 skeleton types to runtime: x/exit MsgServer (DEX swap routing handlers driving the ExitStatus lifecycle) + x/bridge MsgServer (IBC packet recv/ack/timeout for the 5 L2 chains, Solana via wormhole-adapter) + expected-keeper shims + simtest. This is the outermost edge (fewest internal deps); ships first per D-056.
New Msg* types implementing sdk.Msg: MsgAttestBridgeRoute (Watcher-quorum-driven transition Pending→Attested, references x/watcher by ID via WatcherKeeper expected-keeper shim), MsgActivateBridge, MsgCloseBridge. ValidateBasic (stateless: non-empty route-id, valid status transition target) + GetSigners. expected_keepers.go: WatcherKeeper interface (methods x/bridge handler calls — e.g., IsQuorumSigned(quorumID string, payload []byte) bool), BreadKeeper interface (MintWrappedBread, ReleaseWrappedBread by ID-string). NO struct import of x/watcher/types or x/bread/types (G-003 intact).
go build ./x/bridge/... succeeds; Msg* implement sdk.Msg (ValidateBasic + GetSigners); expected_keepers.go defines INTERFACES only (no struct imports); lexicon green on new files.
New Msg* types: MsgSubmitExitRoute (proposes an ExitRoute; ValidateBasic: non-empty holder-reach-id, source/dest-asset, amount > 0), MsgExecuteDEXSwap (executes the pre-computed venue-hops; ValidateBasic: non-empty route-id, route status == InProgress-or-Proposed), MsgRefundExit (on Failed; ValidateBasic: non-empty route-id, status == Failed). expected_keepers.go: BridgeKeeper interface (methods x/exit handler calls for cross-chain exits — e.g., GetBridgeRoute(routeID string) (status, bridgeType, err)). NO struct import of x/bridge/types (G-003 intact — the interface is defined in x/exit/types). bridge-route-id field stays a by-ID-string at the type level (G-003).
go build ./x/exit/... succeeds; Msg* implement sdk.Msg; expected_keepers.go defines BridgeKeeper INTERFACE (no struct import of x/bridge/types); lexicon green.
Simtest (in-memory sdk.Context + dbm in-memory store; no real IBC light clients — D-054). x/exit simtest: full ExitStatus lifecycle (Proposed→InProgress→Settled; Failed→Refunded); cross-chain exit invokes the BridgeKeeper shim (wired to the real x/bridge keeper in the test setup — G-003 test exemption); Fee Covenant clamp event asserted; replay rejection (duplicate MsgExecuteDEXSwap on a Settled route is a no-op error). x/bridge simtest: OnRecvPacket mints wrapped Bread (assert BreadKeeper.MintWrappedBread called); OnAcknowledgementPacket deletes the in-flight record (first ack) and rejects the second (REPLAY PROTECTION — CVE-class pitfall, A-513); OnTimeoutPacket refunds the escrow exactly once (TIMEOUT-REFUND — second timeout is a no-op); Solana branch verifies a stub guardian sig set (2-of-N); denom trace parser pinned to ICS-20 v1 transfer/channel-N/<denom>. Coverage ≥80% on x/exit/keeper + x/bridge/keeper.
go test ./x/exit/... ./x/bridge/... passes; simtest covers lifecycle + replay + timeout-refund; coverage ≥80% on both keeper packages; lexicon green; G-003 import-invariant green (no production struct imports across x//types).
P1-05-01
Wave 4 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P1-99-01
REQ-012, REQ-033
lead-developer
(cross-cutting)
go build ./... + go test ./... green (incl. all v0.1..v0.4 baseline + P1 runtime); coverage ≥80% on x/exit/keeper, x/bridge/keeper; lexicon firewall (lexicon_meta_test.go + lexicon_meta_docs_test.go) green on new keeper/ + msg_*.go + module.go files; G-003 import-invariant green; go.mod has cosmos-sdk + ibc-go (D-062 pin); tag v0.4.1.
go test ./... green; coverage ≥80% on both P1 keeper packages; both lexicon firewalls green; G-003 green; git tag v0.4.1 created.
IBC ack/timeout handling (CVE-class pitfall, A-513) → simtest MUST cover both replay (second ack rejected) and timeout-refund (exactly-once refund). security-engineer reviews the simtest.
Solana guardian sig set rotation → handler reads CURRENT set from state, not hardcoded; simtest uses a frozen stub set; rotation test deferred (D-054).
Denom trace drift → parser pinned to ICS-20 v1 transfer/channel-N/<denom> shape (v0.2 satellite); simtest asserts the trace parse.
cosmos-sdk version pin churn (D-062) → v0.50.x + ibc-go v8.x is the stable choice; GRILL ratifies before P1 ships.
Store-backed Keeper. MsgServer: session lifecycle handlers (Open→Active on first frame ack→Closed on last frame or ttl expiry→Revoked out-of-band; frames received on Closed/Revoked are REJECTED). MsgConsumeOYQR is the canonical one-shot handler: load QR → assert !consumed → assert expires-at > now → FLIP consumed=true (state write FIRST — A-521) → emit transfer effect via BreadKeeper shim → emit event → return. A replay finds consumed==true and returns error (idempotent reject, NOT double-effect). transport.go: store-backed BearerTransport impl (the keeper IS the transport in simtest; Send/Receive/Status backed by the store; no hardware/RF dep — D-054). module.go: AppModule + RegisterServices. Surveillance-resistant invariant: handler emits NO geolocation / sender physical location (the surveillance-resistant locked const is a runtime invariant).
go build ./x/bearers/... succeeds; session lifecycle handlers present; MsgConsumeOYQR flips consumed BEFORE the transfer effect; store-backed BearerTransport impl present; no hardware Go libraries imported; lexicon green.
P2-01-01
Wave 2 — Simtest
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P2-03-01
REQ-034
mesh-engineer + security-engineer
x/bearers/keeper/msg_server_simtest_test.go
Simtest: full session lifecycle (Open→Active→Closed; Open→Active→Revoked; rejected-frame-on-Closed/Revoked); OY-QR one-shot (consume flips consumed, transfer effect via BreadKeeper shim; REPLAY finds consumed==true and returns error — A-521); OY-SAT frame send/receive round-trip; surveillance-resistant NEGATIVE test (assert the event set contains NO geolocation fields — A-522); BearerTransport store-backed impl round-trip. Coverage ≥80% on x/bearers/keeper.
Goal: Promote x/partner Anchor tier from v0.3 skeleton to runtime: MsgIssueAnchorCredential / MsgOnboardAnchor / MsgSuspendAnchorCredential / MsgRevokeAnchorCredential + credential lifecycle (Pending→Onboarded→Suspended→Revoked) + simtest. Depends on bearers (P2) for transport. The P3→P4 hub dependency is broken by the HubKeeper expected-keeper shim (interface in P3, impl wired in P4).
New Msg* types: MsgIssueAnchorCredential (ValidateBasic: non-empty partner-id, partner must be Anchor tier — checked at handler via keeper, non-empty jurisdiction), MsgOnboardAnchor (ValidateBasic: non-empty partner-id, custody-provider-id, attestation-refs), MsgSuspendAnchorCredential, MsgRevokeAnchorCredential (ValidateBasic: non-empty partner-id). expected_keepers.go: WatcherKeeper interface (revocation authz — IsQuorumSigned(quorumID string, payload []byte) bool; 6-of-9 per REQ-004), HubKeeper interface (custody-provider-id validity — CustodyServiceExists(serviceID string) bool). NO struct import of x/watcher/types or x/hub/types (G-003 intact — the P3→P4 hub dep is broken here: the HubKeeper INTERFACE exists in P3 territory; the hub keeper IMPL is wired in P4).
go build ./x/partner/... succeeds; Msg* implement sdk.Msg; expected_keepers.go defines WatcherKeeper + HubKeeper INTERFACES (no struct imports); lexicon green.
Store-backed Keeper (replaces the v0.3 in-memory Keeper stub in types/types.go; the stub may stay as a test helper). MsgServer: IssueAnchorCredential (credential starts Pending; issuer must be Watcher-authorized via WatcherKeeper shim), OnboardAnchor (Pending→Onboarded; asserts custody-provider-id references a live hub custody service via HubKeeper shim; attestation-refs populated), SuspendAnchorCredential (Onboarded→Suspended), RevokeAnchorCredential (→Revoked; only Watcher quorum or issuing party — authz via WatcherKeeper shim). module.go: AppModule + RegisterServices.
go build ./x/partner/... succeeds; MsgServer methods present; HubKeeper shim used for custody-provider-id validity (no x/hub struct import — G-003); WatcherKeeper shim for revocation authz; lexicon green.
P3-01-01
Wave 2 — Simtest
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P3-03-01
REQ-035
backend-engineer + security-engineer
x/partner/keeper/msg_server_simtest_test.go
Simtest: full credential lifecycle (Pending→Onboarded→Suspended→Revoked); OnboardAnchor asserts HubKeeper.CustodyServiceExists (wired to a stub hub keeper in the test setup — G-003 test exemption; the real hub keeper lands in P4); post-revocation rejection (a downstream custody action on a revoked credential returns ErrCredentialRevoked); revocation authz via WatcherKeeper shim (6-of-9 quorum check); non-Anchor partner rejection on IssueAnchorCredential. Coverage ≥80% on x/partner/keeper.
go test ./x/partner/... passes; simtest covers lifecycle + post-revocation rejection + authz; coverage ≥80%; lexicon green; G-003 import-invariant green.
P3-02-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P3-99-01
REQ-012, REQ-035
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% on x/partner/keeper; lexicon firewalls green; G-003 green; PartnerTierCount=4 unchanged (regression); tag v0.4.3.
go test ./... green; coverage ≥80%; both lexicon firewalls green; G-003 green; PartnerTierCount=4 unchanged; git tag v0.4.3.
Goal: Promote x/hub from v0.3 skeleton to runtime: MsgRegisterCustodyService / MsgCustodyReceiveAsset / MsgCustodyReleaseAsset / MsgRecordLendingPrimitive (coupon clamp) / MsgRecordComplianceAttestation + CustodyKeyring interface (D-058) + in-memory memKeyring test impl + simtest. data-engineer phase-specific for custody state. Depends on anchors (P3) — the PartnerKeeper shim is wired to the real x/partner keeper.
Wave 1 — CustodyKeyring interface + memKeyring + custody state
In-memory test-only memKeyring impl (signs with a throwaway ed25519 key per assetID; supports rotation by swapping the keymap entry; NO real MPC/HSM — D-054/D-058). custody_state.go: custody asset records (assetID → custody entry + sig ref + key version); in-memory test store ONLY (SDK in-memory store is the substrate; no real database, no migration — D-054 simtest grade). Shape consistent with CustodyKeyring interface; supports rotation.
go build ./x/hub/... succeeds; memKeyring implements CustodyKeyring; custody state shape (assetID → entry + sig ref + key version) present; no real DB; lexicon green.
Store-backed Keeper (wraps the custody state from P4-02-01). MsgServer: RegisterCustodyService (operator must be Onboarded Anchor — checked via PartnerKeeper shim), CustodyReceiveAsset (delegates signing to CustodyKeyring; records custody entry + sig ref), CustodyReleaseAsset (COMPLIANCE-BEFORE-CUSTODY ordering — A-544: checks compliance status via ComplianceKeeper shim BEFORE the custody debit; authz: holder or authorized Window grantee), RecordLendingPrimitive (CLAMPS coupon to [LendingCouponFloorBps=0, LendingCouponCapBps=800] — runtime echo of D-028/REQ-030; emits clamp event for simtest), RecordComplianceAttestation (records attestation ref against partner). module.go: AppModule + RegisterServices.
go build ./x/hub/... succeeds; MsgServer methods present; compliance-before-custody ordering enforced (A-544); lending coupon clamp invoked at runtime; PartnerKeeper + ComplianceKeeper shims used (no struct imports); lexicon green.
Simtest: RegisterCustodyService with Onboarded Anchor (wired to real x/partner keeper — G-003 test exemption); CustodyReceiveAsset + CustodyReleaseAsset round-trip via memKeyring (sig recorded); CustodyReleaseAsset on a non-compliant partner REJECTED (compliance-before-custody — A-544); RecordLendingPrimitive coupon clamp event (coupon within [0, 800] bps; a coupon > 800 is clamped to 800 and the clamp event is emitted — A-543); CustodyKeyring rotation (swap keymap entry; Status reports the new active key version; a subsequent Sign uses the new key). Coverage ≥80% on x/hub/keeper.
go build ./... + go test ./... green; coverage ≥80% on x/hub/keeper; lexicon firewalls green; G-003 green; HubServiceCount=3 unchanged; LendingCouponCapBps=800 + LendingCouponFloorBps=0 unchanged (REQ-030 cross-const test stays green); tag v0.4.4. Remove data-engineer persona (P4 phase-specific).
go test ./... green; coverage ≥80%; both lexicon firewalls green; G-003 green; HubServiceCount=3 + LendingCouponCapBps=800/LendingCouponFloorBps=0 unchanged; REQ-030 cross-const test green; git tag v0.4.4; data-engineer removed.
Goal: Promote x/services from v0.3 skeleton to runtime: MsgRegisterService / MsgActivateService / MsgSuspendService / MsgRevokeService + per-kind handlers (MsgIssueCareGrant, MsgActivateSIM, MsgProvisionVault, MsgBindMailbox) + simtest. Depends on hub (P4). The window-id grant is checked on EVERY op (A-552 — revoked Window invalidates ongoing service ops).
Store-backed Keeper. MsgServer: RegisterService (operator-reach-id valid; window-id must reference an Active Window — checked via WindowKeeper shim), ActivateService, SuspendService, RevokeService (revocation requires Window grantor or Watcher quorum). Per-kind handlers: IssueCareGrant (Care), ActivateSIM (SIM), ProvisionVault (Vault, references x/vault by ID via VaultKeeper shim), BindMailbox (Mail). WINDOW-GRANT CHECKED ON EVERY OP (A-552 — a revoked Window invalidates the service; the handler checks WindowKeeper.GetWindowStatus before each op, not just registration). module.go: AppModule + RegisterServices.
go build ./x/services/... succeeds; MsgServer methods present; per-kind typed dispatch; window-grant checked on every op (A-552); WindowKeeper + VaultKeeper shims used (no struct imports); lexicon green.
P5-01-01
Wave 2 — Simtest
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P5-03-01
REQ-037
backend-engineer
x/services/keeper/msg_server_simtest_test.go
Simtest: full service lifecycle (Pending→Active→Suspended→Revoked); per-kind handler round-trips (Care/SIM/Vault/Mail); window-grant validity on EVERY op (a service registered against a Revoked Window is rejected; a service operating after its Window expired is rejected — A-552); VaultService provisioning via VaultKeeper shim (wired to real x/vault keeper in test setup — G-003 test exemption). Coverage ≥80% on x/services/keeper.
go test ./x/services/... passes; simtest covers lifecycle + per-kind + window-grant-on-every-op; coverage ≥80%; lexicon green; G-003 import-invariant green.
P5-02-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P5-99-01
REQ-012, REQ-037
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% on x/services/keeper; lexicon firewalls green; G-003 green; ServiceKindCount=4 unchanged; tag v0.4.5.
go test ./... green; coverage ≥80%; both lexicon firewalls green; G-003 green; ServiceKindCount=4 unchanged; git tag v0.4.5.
New Msg* types: MsgIssueBond (ValidateBasic: non-empty bond-id, issuer-stand-id, principal > 0, coupon-bps within [0, 800] — stateless clamp check), MsgIssueGrowthBond (ValidateBasic: same + growth-rate-bps), MsgTickGrowthBond (ValidateBasic: non-empty bond-id), MsgPlaceSecondaryOrder (ValidateBasic: non-empty order-id, bond-id, side ∈ {Buy, Sell}, price-bps, quantity > 0), MsgCancelSecondaryOrder (ValidateBasic: non-empty order-id), MsgMatchSecondaryOrder (ValidateBasic: non-empty incoming-order-id). expected_keepers.go: StandKeeper interface (GrowthBond issuer-stand-id validity — StandExists(standID string) bool). NO struct import of x/stand/types (G-003 intact).
go build ./x/bond/... succeeds; Msg* implement sdk.Msg; expected_keepers.go defines StandKeeper INTERFACE; lexicon green (no "interest"/"yield"/"deposit"/"savings" — use "coupon"/"growth"/"order"/"match").
Store-backed Keeper (resting book stored ordered by (price, sequence) for price-time priority FCFS — REQ-007). MsgServer: IssueBond (invokes v0.3 Clamp on coupon), IssueGrowthBond (Clamp + ClampGrowth), TickGrowthBond (applies growth, clamped), PlaceSecondaryOrder, CancelSecondaryOrder, MatchSecondaryOrder (CLOB match — clob.go: loads the resting book for the bond, matches the incoming order against the best opposing price until filled or the book is empty, writes Filled orders, emits a match event with the matched coupon CLAMPED to [0, 800] bps via v0.3 Clamp; per D-063, a match whose implied coupon EXCEEDS 800 bps is REJECTED — fails closed, the resting order stays, the incoming order rests or is cancelled; matches within [0, 800] use Clamp (in-band, no refund needed). PER-TX matching (dYdX-v4-shaped, no batch end-of-block matching in v0.5 simtest). Handler documented as NOT front-running-safe for mainnet (Year-3+ concern; simtest does NOT assert front-running safety — D-054). module.go: AppModule + RegisterServices. The 8%/0% consts are referenced directly (NOT a local copy) — A-563; the REQ-030 cross-const test stays green.
go build ./x/bond/... succeeds; MsgServer methods present; CLOB matching with price-time priority; per-match coupon clamp [0, 800] (A-562/D-063 REJECT above cap); consts referenced directly (A-563); no AMM (D-057/A-564); lexicon green.
P6-01-01
Wave 2 — Simtest
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P6-03-01
REQ-038
backend-engineer + security-engineer
x/bond/keeper/msg_server_simtest_test.go
Simtest: bond issuance (coupon clamped at issuance); GrowthBond issuance + tick (growth clamped); CLOB matching — full fill, partial fill + rest, no-match (order rests), cancel; PER-MATCH CLAMP: a match within [0, 800] bps clears (clamp event emitted); a match whose implied coupon EXCEEDS 800 bps is REJECTED (fails closed — D-063; the resting order stays, the incoming order rests); price-time priority FCFS (at the same price, the earlier resting order fills first — REQ-007); StandKeeper shim wired to real x/stand keeper in test setup (G-003 test exemption). Coverage ≥80% on x/bond/keeper.
go build ./... + go test ./... green; coverage ≥80% on x/bond/keeper; lexicon firewalls green; G-003 green; D-028 regression: CouponCapBps=800 + CouponFloorBps=0 unchanged; REQ-030 cross-const test green; tag v0.4.6.
go test ./... green; coverage ≥80%; both lexicon firewalls green; G-003 green; CouponCapBps=800/CouponFloorBps=0 unchanged; REQ-030 cross-const test green; git tag v0.4.6.
Lexicon firewall green (no "interest"/"yield"); G-003 import-invariant green.
Git tag v0.4.6.
P6 Risks & Mitigations
Per-match clamp reject-vs-clamp (A-562, resolved by D-063) → REJECT above cap (fails closed); simtest covers reject-above-cap.
CLOB front-running (out of scope for simtest — D-054) → handler documented as NOT front-running-safe for mainnet; simtest does NOT assert front-running safety.
Store-backed Keeper. MsgServer: SubmitProposal (validates kind — the MissionLockAmendment-Rejected kind never reaches here per D-064 ValidateBasic rejection; creates Proposal status=Pending), Vote (cast a Voice with a VoteOption; Veto requires Watcher authz via WatcherKeeper shim; vote on a non-Active proposal REJECTED; vote after voting-deadline REJECTED), TallyProposal (closes the voting deadline, computes the tally, transitions Succeeded/Failed; Veto semantics: a single Veto does NOT block — anti-greed, vision §19; the proposal transitions to Failed only if NoWithVeto >= WatcherVetoQuorum (default 6, D-065/A-574); the v0.2 TallyResult.NoWithVeto field (zero-locked in v0.2) is now populated by Watcher Vetos). Proposal EXECUTION (auto-executing a passed proposal) is NOT in v0.5 — the handler records the tally result but does NOT auto-execute (a v0.6+ concern). module.go: AppModule + RegisterServices.
go build ./x/council/... succeeds; MsgServer methods present; MissionLockAmendment-Rejected rejected at ValidateBasic (never reaches handler — D-064); Veto quorum-based (single Veto does NOT block — D-065); no auto-execution; lexicon green.
Simtest: full proposal lifecycle (Submit→Active→Vote→Tally→Succeeded/Failed); MissionLockAmendment-Rejected kind REJECTED at ValidateBasic (the message never reaches the handler — D-064/A-572; simtest asserts the MsgSubmitProposal with that kind fails ValidateBasic with a Mission-Lock error); Veto semantics (a single Veto does NOT block; NoWithVeto >= WatcherVetoQuorum (default 6) transitions to Failed — D-065/A-574; simtest covers single-Veto-no-block + quorum-Veto-fails); vote-on-non-Active REJECTED; tally-before-deadline REJECTED; Watcher authz for Veto via WatcherKeeper shim (wired to real x/watcher keeper in test setup — G-003 test exemption). types_test.go EXTEND: locked-const tests for ProposalKindCount=4, ProposalStatusCount=5, VoteOptionCount=4; MissionLockAmendable==false regression (v0.2 TestMissionLockAmendableFalse stays green); SignalKindCount==4 regression (v0.4 TestSignalKindShapeIntentional stays green). Coverage ≥80% on x/council/keeper + x/council/types.
go test ./x/council/... passes; simtest covers lifecycle + MissionLockAmendment-reject + Veto quorum + vote/tally rejections; coverage ≥80%; locked-const tests for new enums + Mission Lock + SignalKind regression; lexicon green; G-003 import-invariant green.
P7-02-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P7-99-01
REQ-012, REQ-039
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% on x/council/keeper + x/council/types; lexicon firewalls green; G-003 green; CouncilKindCount=3 unchanged; MissionLockAmendable=false unchanged (v0.2 regression green); SignalKindCount=4 unchanged (v0.4 regression green); new locked-consts: ProposalKindCount=4, ProposalStatusCount=5, VoteOptionCount=4; tag v0.4.7.
go test ./... green; coverage ≥80%; both lexicon firewalls green; G-003 green; all locked-consts green (unchanged + new); git tag v0.4.7.
P7-03-01
P7 Must-Haves
x/council has keeper/keeper.go + keeper/msg_server.go + types/msg_*.go + types/expected_keepers.go + module.go; types/types.go EXTENDED with Proposal/ProposalKind/ProposalStatus/VoteOption.
Mission Lock const firewall integrity (D-064/A-572) → ValidateBasic rejects MissionLockAmendment-Rejected kind; the const is the firewall, ValidateBasic is the gate; v0.2 regression test green.
Veto semantics (D-065/A-574) → single Veto does NOT block (anti-greed); quorum-based (default 6); simtest covers single-Veto-no-block + quorum-Veto-fails.
SignalKind 4-not-5 (A-573) → UNCHANGED; v0.4 regression-guard test green; expansion deferred to v0.6+ governance vote (not a Mission-Lock const; a distinct locked const).
Phase P8 — Final Review + Audit + Milestone Ship
Slug: final-review-audit-ship
Branch: phase/08-final-review-audit-ship
REQs covered: all v0.5 REQs (REQ-033..REQ-039) — final coverage accounting; no new REQs (covers post-hoc fixes from REVIEW/AUDIT)
Tag: v0.4.8 (IS the v0.5 milestone release; D-008)
Type: final
Personas: lead-developer (review/ship) + ci-security-auditor (ACTIVATED for the v0.5 milestone audit + feature purity gate)
Goal: Multi-persona review across P1..P7, audit (reconstruction test + feature purity gate: no breaking schema changes; locked-const firewall intact; G-003 production firewall intact; G-006 controlled exception GRILL-ratified), milestone ship (merge to main, tag v0.4.8 = v0.5 milestone release, release, delete all milestone branches).
Wave 1 — Review + Audit (parallel)
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P8-01-01
—
lead-developer (review)
.ciagent/oy/REVIEW.md (NEW for v0.5)
Multi-persona code review across P1..P7. Adversarial probes: (1) do the expected_keepers.go shims preserve G-003 (no production struct imports across x/<module>/types); (2) does the CLOB per-match clamp REJECT above 800 (D-063); (3) does MissionLockAmendment-Rejected get rejected at ValidateBasic (D-064); (4) does the OY-QR one-shot flip consumed BEFORE the transfer (A-521); (5) does compliance-before-custody ordering hold (A-544); (6) does the IBC replay/timeout protection mirror ibc-go (A-513). Auto-apply P0 fixes; flag P1+ for post-hoc.
Update REQUIREMENTS.md: mark REQ-033..REQ-039 → Complete (runtime shipped). Update ROADMAP.md: mark v0.5 milestone COMPLETE; add the tag-line note that v0.5 shipped on the v0.4.x patch line (P0 → v0.4.0, P1..P7 → v0.4.1..v0.4.7, P8 → v0.4.8 = milestone release, per D-008).
REQUIREMENTS.md status column updated for all 7 v0.5 REQs → Complete; ROADMAP.md v0.5 marked complete + tag-line note present.
P8-01-01, P8-02-01
P8-03-02
(milestone)
lead-developer
(cross-cutting)
Final ship: merge phase/08 → milestone/v0.5-bearers-runtime → main; create milestone release tag v0.4.8 (= v0.5 milestone release per D-008); delete the 8 phase branches (phase/01-*..phase/08-*) after merge; confirm go build ./... + go test ./... green at the v0.4.8 tag.
v0.4.8 tag created on main; go test ./... green at the tag; ROADMAP.md v0.5 complete; phase branches deleted; release notes reference v0.5 scope (8 modules promoted to runtime: exit, bridge, bearers, partner, hub, services, bond, council; cosmos-sdk + ibc-go deps added per D-055; CustodyKeyring interface D-058; CLOB matching D-057; AUDIT §193 P1-1 enums D-060).
P8-03-01
P8 Must-Haves
REVIEW.md v0.5 section written; P0 fixes applied.
AUDIT.md v0.5 section written; reconstruction test passes.
Feature purity gate GREEN: no breaking schema changes (v0.3 types/ contracts NOT amended); locked-const firewall intact (all v0.1..v0.4 consts unchanged; new P7 enums added per D-060); G-003 production firewall intact; G-006 controlled exception GRILL-ratified (D-055/D-062).
Coverage ≥80% on all 8 runtime keeper packages (exit, bridge, bearers, partner, hub, services, bond, council).
Both lexicon firewalls green (x//*.go incl. new keeper/module files + docs//*.md).
G-003 import-invariant green (no production struct imports across x//types; expected_keepers.go shims are interfaces).
REQUIREMENTS.md + ROADMAP.md mark v0.5 COMPLETE.
Tag v0.4.8 created (= v0.5 milestone release).
Milestone branch merged to main.
All 8 phase branches deleted (local + remote).
P8 Risks & Mitigations
Runtime promotion breaks v0.3 type contracts → the v0.3 types/ packages are NOT amended (runtime adds behavior on top); the feature purity gate verifies no struct field removal/enum rename.
G-006 dep exception churn → D-055/D-062 GRILL-ratified; the pin (cosmos-sdk v0.50.x + ibc-go v8.x) is the stable choice; the audit verifies the dep is scoped to runtime phases.
Milestone versioning confusion (v0.5 milestone = v0.4.8 tag) → lead-developer enforces D-008: final phase patch IS the milestone release; no separate minor tag. ROADMAP tag-line note (P8-03-01) prevents v0.4.8/v0.5.0 confusion.
Coverage Targets (D-033) — v0.5
Package
Phase
Target
Locked-const / invariant tests
x/exit/keeper
P1
≥80%
ExitStatusCount=5 (regression); cross-chain exit via BridgeKeeper shim; Fee Covenant clamp on exit-fee-bps
P1-01-01 (cosmos-sdk + ibc-go dep, D-062 GRILL) → blocks P1-02-01 and all subsequent runtime work (the dep must land before any Msg*/sdk.Msg/MsgServer compiles). GRILL ratifies D-062 before P1 ships.
P1-03-01 (x/bridge keeper) → blocks P1-05-01 (x/exit keeper — BridgeKeeper shim wired to the real x/bridge keeper in simtest).
P1-99-01 (P1 ship) → blocks P2-01-01 (bearers routes through exit; branch hygiene + G-003 import-invariant test scanning the new files).
P3-01-01 (x/partner expected-keepers incl. HubKeeper shim) → the P3→P4 hub dep is broken here (interface in P3; impl wired in P4). P4-04-01 wires the real x/hub keeper to the PartnerKeeper shim in x/hub's expected-keepers.
P3-99-01 (P3 ship) → blocks P4-01-01 (hub custody backs anchors; the PartnerKeeper shim in x/hub/types/expected_keepers.go is wired to the real x/partner keeper).
P4-99-01 (P4 ship) → blocks P5-01-01 (services sit on hub) and P6-01-01 (bond uses hub lending primitive — the cross-const test guards the shared 800/0 consts).
P5-99-01 (P5 ship) → blocks P6-99-01? No — P6 depends on P4 (hub lending), not P5. P5 and P6 are both blocked by P4; they could run in parallel if parallelization were enabled (config parallelization.enabled: false — serial).
The v0.5 Phase 0 clarify/research stages produced 8 clarification decisions (D-054..D-061) and 4 planner-escalation decisions (D-062..D-065, provisional until GRILL), applied to this plan:
ID
Decision
Applied to
D-054
Runtime = simtest-grade handlers, NOT mainnet
All P1..P7 simtest tasks; P8 audit
D-055
cosmos-sdk dep GRILL-approved (G-006 controlled exception)
P1-01-01 (go.mod); all runtime phases
D-056
Phase ordering P1 exit → P2 bearers → P3 anchors → P4 hub → P5 services → P6 bond → P7 council → P8 final
Cross-Phase Dependency Map; all phase goals
D-057
Bond CLOB matching (not AMM); 8%/0% per-match clamp
P6-02-01 (CLOB), P6-03-01 (simtest)
D-058
Hub custody = CustodyKeyring interface + memKeyring test impl (no real MPC)
Auto-generated at full autonomy per run.md §MVP/UX CHECK. v0.5 is a
feature milestone (runtime promotion); "user-facing surface" is
developer-facing (the MsgServer handlers, the simtest output, the
CustodyKeyring interface, the CLOB matching engine) and the protocol
semantics (Anchor credential lifecycle, council governance with the
Mission-Lock const firewall). No end-user UI changes (the docs site is
complete from v0.3; no new docs pages in v0.5).
User-Facing Surface
MsgServer handlers (developer-facing): each x/<module>/keeper/msg_server.go exposes one *Response, error method per Msg*. A developer invoking go test ./x/.../keeper/... sees the simtest exercise each handler against an in-memory sdk.Context. The green test output is the surface.
CustodyKeyring interface (developer-facing): x/hub/types/keyring.go defines the CustodyKeyring interface (Sign/Derive/Status); a custody vendor integration team implements it. The interface is the contract surface.
CLOB matching engine (developer-facing): x/bond/keeper/clob.go implements the price-time-priority CLOB; a developer invoking MsgMatchSecondaryOrder sees the resting book matched and the per-match coupon clamped (REJECT above 800 — D-063). The match event is the surface.
Council governance (protocol-facing): x/councilMsgSubmitProposal / MsgVote / MsgTallyProposal with the MissionLockAmendment-Rejected kind rejected at ValidateBasic (D-064) — a developer attempting to submit a Mission-Lock-amendment proposal sees a ValidateBasic error. The Watcher Veto quorum (default 6, D-065) is the anti-greed gate.
Happy Path
Scenario: a developer exercises the OY-QR one-shot bearer transfer end-to-end.
The developer writes a simtest in x/bearers/keeper/msg_server_simtest_test.go (or runs the existing one).
MsgIssueOYQR creates an OYQRCode with consumed=false, expires-at in the future, a payload, an issuer-reach-id.
MsgConsumeOYQR is invoked: the handler loads the QR, asserts !consumed, asserts expires-at > now, FLIPS consumed=true (state write FIRST — A-521), emits the transfer effect via the BreadKeeper shim, emits an event, returns.
A REPLAY (second MsgConsumeOYQR on the same qr-id) loads the QR, finds consumed==true, returns an error (idempotent reject — NOT double-effect).
The simtest asserts: the transfer effect happened exactly once; the replay returned an error; the event set contains NO geolocation fields (surveillance-resistant negative test — A-522).
The simtest is green; coverage on x/bearers/keeper ≥80%.
Scenario: a developer exercises the CLOB bond matching with the per-match clamp.
A resting Sell order is placed at a price implying a 750 bps coupon (within [0, 800] band).
A Buy order arrives matching the Sell; the match clears at 750 bps (within band — Clamp is a no-op); the match event asserts the coupon is 750.
A second resting Sell order is placed at a price implying a 900 bps coupon (above the 800 cap).
A Buy order arrives matching the second Sell; the match's implied coupon (900) EXCEEDS CouponCapBps=800; the match is REJECTED (fails closed — D-063/A-562); the resting Sell stays; the Buy rests or is cancelled.
The simtest asserts: the in-band match cleared; the above-cap match was rejected; the 8%/0% consts are unchanged (D-028 regression); the REQ-030 cross-const test (hub ↔ bond) is green.
The simtest is green; coverage on x/bond/keeper ≥80%.
Scenario: a developer attempts to submit a Mission-Lock-amendment proposal.
MsgSubmitProposal is constructed with kind = MissionLockAmendment-Rejected.
ValidateBasic runs (stateless gate): the kind is MissionLockAmendment-Rejected; ValidateBasic REJECTS the message with a Mission-Lock error (D-064/A-572 — the message never reaches the handler).
The handler is never invoked; no Proposal state record is created; no event is emitted.
The simtest asserts: ValidateBasic returned a Mission-Lock error; the keeper's Proposal store is empty; the v0.2 TestMissionLockAmendableFalse regression test is green (MissionLockAmendable==false unchanged).
The simtest is green; coverage on x/council/keeper + x/council/types ≥80%.
UX Acceptance Criteria
The v0.5 deliverable MUST meet these explicit criteria (verified in P8 audit):
REQ-033: x/exit + x/bridge each have keeper/msg_server.go + types/msg_*.go + types/expected_keepers.go + module.go; go test ./x/exit/... ./x/bridge/... passes; simtest covers ExitStatus lifecycle + IBC replay/timeout (A-513); coverage ≥80% on both keeper packages; G-003 intact (no x/bridge/types struct import in x/exit production code).
REQ-038: x/bond has keeper/msg_server.go + keeper/clob.go; CLOB price-time priority (REQ-007); per-match clamp REJECT above 800 (D-063/A-562); no AMM (D-057); CouponCapBps=800/CouponFloorBps=0 unchanged (D-028); REQ-030 cross-const test green; OrderSideCount=2/OrderStatusCount=3 unchanged.
REQ-039: x/council has keeper/msg_server.go + types/msg_*.go; Proposal/ProposalKind(4)/ProposalStatus(5)/VoteOption(4) enums added (D-060); MissionLockAmendment-Rejected rejected at ValidateBasic (D-064/A-572); Watcher Veto quorum default 6 (D-065/A-574); single-Veto-no-block (anti-greed); MissionLockAmendable=false unchanged (v0.2 regression green); SignalKindCount=4 unchanged (v0.4 regression green); CouncilKindCount=3 unchanged; no proposal auto-execution.
Feature purity gate (P8): no breaking schema changes (v0.3 types/ contracts NOT amended); locked-const firewall intact (all v0.1..v0.4 consts unchanged; new P7 enums per D-060); G-003 production firewall intact (expected_keepers.go are interfaces); G-006 controlled exception GRILL-ratified (D-055/D-062).
No regression: go test ./... green; v0.4 coverage floor (93.3% on x/hub/types, 96.4% on x/council/types) not reduced on the types/ packages; v0.1..v0.4 baseline tests green.
D-055/D-062 dep: go.mod has cosmos-sdk v0.50.x + ibc-go v8.x (GRILL-ratified); types/ packages gain sdk.Msg imports for Msg* (isolated in types/msg_*.go); invariant/lexicon tests stay stdlib-only and green.
v0.6 Plan (Nomad Web UI)
This section APPENDS the v0.6 milestone plan to the v0.1..v0.5 plans above.
It does NOT rewrite or supersede the earlier content. v0.6 is the project's
first UI milestone: a working prototype Web UI where a person can sign up
to be a Nomad (create a Reach + open a Stash) and exercise basic functionality
around (Reach, Stash) plus Window authorization, Standing progress, and Bloom
accrual. All data is generated test fixtures — no real blockchain (D-020
continues). Stack: Go html/template + HTMX (vendored, no node) + Go 1.22
net/http.ServeMux mock HTTP server in a new web/ dir that instantiates the
real x/*/types structs from in-memory fixtures (D-067). No keeper, no
Cosmos runtime, no app.go (none exists in the repo). Tags run on the
v0.5.x patch line (config.json tag_base: v0.5.x): P0 → v0.5.0;
execution phases P1..P5 → v0.5.1..v0.5.5; final phase P6 → v0.5.6 IS the
v0.6 milestone release (D-008 — final phase patch IS the milestone release;
no separate minor tag). Branch names use NO oy/ prefix (single-project mode:
only oy exists; the slug prefix would be redundant — config projects[]
length is 1, matching the v0.5 convention).
Milestone Summary
Milestone: v0.6 — Nomad Web UI
Type: Feature (P1..P5 are feat; REQ-045 is test co-shipped in P1; P6 is final)
Tag base: v0.5.x patch line (P0 → v0.5.0; execution P1..P5 → v0.5.1..v0.5.5; final P6 → v0.5.6 IS the v0.6 milestone release)
Phases: 7 — P1..P5 (execution) + P6 (final review/audit/ship). Phase 0 (this PLAN) is in progress.
Depth: UI prototype — Go html/template server-rendered HTML + HTMX progressive enhancement; in-memory mock store seeded from fixtures; instantiates real x/*/types structs (Reach, Stash, Window, FreeholderSignals, BloomRecord). No keeper, no Cosmos runtime, no app.go, no persistence (resets on restart).
Coverage target: ≥80% on each new web/ package (D-033 carries forward); the mock store + handlers have Go tests (go test ./web/...), not just manual browser checks. Lexicon firewall (REQ-012) extended to web/ via a new sibling meta-test (REQ-045).
New dirs: web/ (top-level; main.go, handlers/, store/, templates/, static/); lexicon_meta_web/ (sibling firewall test dir, mirrors lexicon_meta_docs/). Extended: none (x/ is NOT modified — web/ imports x/*/types as app-layer consumption per D-070, not a production cross-x/ import).
G-006 (zero Go deps): preserved. HTMX is a vendored static asset (web/static/htmx.min.js), NOT a go get. Go 1.22 net/http.ServeMux is stdlib. html/template is stdlib. go.mod is unchanged (no new require lines; the v0.5 cosmos-sdk/ibc-go deps stay but v0.6 adds nothing).
G-003 (production firewall): intact. web/ importing x/*/types is app-layer consumption (D-070), NOT a cross-x/ production import. No x/ module gains a production import of another x/ module's types via web/. The v0.2 G-003 import-invariant test scans x/**/*.go (unchanged scope); web/ is outside x/ so it is not scanned by that test — a NEW web/-scoped test confirms web/ only imports x/*/types (not x/*/keeper).
Phase ordering (D-072, finalized here): P1 web foundation + Reach signup + lexicon firewall (REQ-040 + REQ-045 — same web/ territory, vertical slice, firewall-first) → P2 Stash dashboard (REQ-041) → P3 Window authorization (REQ-042) → P4 Standing + Freeholder signals (REQ-043) → P5 Bloom accrual (REQ-044) → P6 final review/audit/ship. Each phase independently shippable (vertical slice); P1 lands the foundation + firewall first (lexicon-clean by construction — D-044 pattern).
Personas (from PERSONAS.md, reactivated for v0.6): frontend-engineer owns web/** (templates, static, handlers, main.go) — REACTIVATED for the first time (territory web/**); backend-engineer co-owns web/store/ (the mock store that imports x/*/types) + owns the lexicon firewall extension (REQ-045, lexicon_meta_web/ — mirrors lexicon_meta_docs/); lead-developer owns P0 + P6 + coordination. The v0.5 cosmos-engineer/security-engineer/mesh-engineer personas are NOT reactivated (no keeper/MsgServer/simtest work in v0.6 — UI mock only). ci-security-auditor activated in P6.
Cross-Phase Dependency Map (v0.6)
P1 (web foundation + Reach signup + lexicon firewall)
│ web/main.go + web/store/ + web/handlers/reach.go + web/templates/reach.html
│ lexicon_meta_web/lexicon_meta_web_test.go [firewall-first; scans web/ as content lands]
▼
P2 (Stash dashboard) [depends on Reach existing (signup creates Stash atomically, D-071)]
▼
P3 (Window authorization) [depends on Stash existing (Window scope references a Stash)]
▼
P4 (Standing + Freeholder signals) [depends on Reach existing (Standing is per-Reach)]
▼
P5 (Bloom accrual) [depends on Stash existing (BloomRecord is per-Stash)]
▼
P6 (final review/audit/ship)
Hard cross-phase blockers:
P1 lexicon firewall (lexicon_meta_web/) → blocks P2..P5 content (firewall-first: a banned term slipped into a P2..P5 template/JS/Go file fails the build, not the P6 review — D-044/D-069 pattern).
P1 web foundation (web/main.go + web/store/ + base templates) → blocks P2..P5 (each screen extends the base layout + uses the mock store).
P1 Reach signup (REQ-040) → blocks P2 (Stash dashboard needs a Stash, created atomically at signup per D-071), P4 (Standing is per-Reach).
P2 Stash dashboard (REQ-041) → blocks P3 (Window scope references a Stash), P5 (BloomRecord is per-Stash).
All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene).
All other refs are app-layer consumption of x/*/types (D-070): web/store/ imports x/identity/types, x/stash/types, x/window/types, x/standing/types, x/bread/types, x/bloom/types. No x/ module is modified.
D-070 / G-003 Boundary (app-layer consumption, NOT a cross-x/ import)
web/ is NOT an x/ module — it is the application layer (exactly as a future cmd/oyd would be). web/store/ importing x/identity/types.Reach is app-layer consumption of protocol types, the intended consumption direction. The G-003 firewall (no production struct imports across x/<module>/types) is intact: no x/ module gains a production import of another x/ module's types via web/. A new web/-scoped test (web/store/import_test.go) asserts web/ only imports x/*/types packages (NOT x/*/keeper — there is no keeper in v0.6; the mock store IS the data source).
Phase P1 — Web Foundation + Reach Signup + Lexicon Firewall (REQ-040, REQ-045) → v0.5.1
Slug: web-foundation-reach-firewall
Branch: phase/01-web-foundation-reach-firewall
REQs covered: REQ-040 (Nomad Reach signup Web UI), REQ-045 (lexicon firewall extension to web/)
Tag: v0.5.1
Type: feat+test
Goal: Ship the web foundation (Go 1.22 net/http.ServeMux mock server in web/, base templates, vendored HTMX, in-memory mock store importing x/*/types) + the "Create a Reach" signup form (POST) that atomically creates a Reach (IsNomad=true) + a Stash (D-071) + Reach list/detail views + the lexicon_meta_web/ firewall extension (REQ-045, firewall-first). After P1, the UI is runnable via go run ./web and a visitor can create a Reach.
Wave 1 — Lexicon firewall FIRST (parallel with Wave 2 foundation; no internal deps)
NEW sibling meta-test mirroring lexicon_meta_docs/lexicon_meta_docs_test.go (D-069). Uses the SAME lexicon.FindBannedTerm (word-boundary, case-insensitive) — NO detection reimplementation. Walks the repo root; targets web/templates/**/*.html + web/static/**/*.js + web/**/*.go (production + test). Excludes .ciagent/ (firewall meta-files), .git/ (VCS), the meta-test file itself (self-exclusion via runtime.Caller(0)), and non-.{html,js,go} files under web/. Includes the G-009 self-test table (consumes lexicon.SyntheticBannedStrings() — G-014 shared helper from REQ-029), TestLexiconMetaWebBannedTermsCount (exactly 10), TestLexiconMetaWebNoFalsePositiveOnOpenYield (word-boundary does not match "openyield"/"european"), and TestLexiconMetaWebWalkCoverage (G-013 — injects a synthetic banned-term file into web/templates/.lexicon_fixture/ and asserts the walk FINDS it). The firewall PASSES at P1 Wave 1 with zero web/ content (a walk that scans nothing reports green on zero hits — closed by the walk-coverage test). "Sign up" maps to "Create a Reach" — the word "account" is banned (REQ-012); the firewall enforces this on all UI strings.
go test ./lexicon_meta_web/... green (invoked as go test -run TestLexiconMetaWeb ./...); self-test table passes for all 10 banned terms; TestLexiconMetaWebNoFalsePositiveOnOpenYield green; TestLexiconMetaWebWalkCoverage finds the fixture; a deliberately-injected banned term in a web/templates/*.html file fails the test
—
Wave 2 — Web foundation: main.go + mock store + base templates + HTMX (parallel with Wave 1; no internal deps)
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P1-02-01
REQ-040, D-066, D-067, D-068
frontend-engineer
web/main.go, web/server.go
web/main.go: package main; func main() calling server.go's runServer(). web/server.go: Go 1.22 net/http.ServeMux (mux := http.NewServeMux(); Go 1.22 method-pattern routing mux.HandleFunc("GET /", ...)); serves web/static/ via http.FileServer (HTMX vendored); loads templates via html/template (template.ParseGlob("web/templates/*.html")); constructs the mock store (store.NewStore() from P1-03-01) and injects it into handlers. Listens on :8080 (env-overridable PORT). No external deps (stdlib only — G-006). No app.go, no Cosmos runtime.
go build ./web succeeds; go run ./web starts a server on :8080 (manual check: curl -s http://localhost:8080/ returns the home page); go.mod unchanged (zero new require lines); lexicon firewall green on web/main.go + web/server.go
Vendored HTMX 1.x minified JS (a single static asset — NOT a go get; G-006 preserved). style.css: minimal lexicon-clean CSS for the 5 screens (no banned terms in comments/class names). HTMX attributes used for progressive enhancement (form POST → swap).
web/static/htmx.min.js exists (vendored, not a go.mod entry); go build ./web succeeds; go run ./web serves /static/htmx.min.js (manual check: curl -s http://localhost:8080/static/htmx.min.js returns the JS); lexicon firewall green on web/static/**
—
P1-02-03
REQ-040
frontend-engineer
web/templates/base.html, web/templates/home.html
base.html: the shared layout (head, nav, {{block "content" .}}{{end}}, HTMX script tag, footer). Nav links to all 5 screens (Reach signup, Stash dashboard, Window authorization, Standing progress, Bloom accrual) — P2..P5 screens link to placeholder routes that P2..P5 fill in. home.html: site home with a one-paragraph OpenYield overview (lexicon-clean — "real production"/"Holder"/"Reach"/"Stash"; NOT "yield"/"account"/"bank") + links to the 5 screens. Lexicon-clean by construction (the P1-01-01 firewall scans these as they land).
web/templates/base.html + web/templates/home.html exist; go run ./web serves / (home renders); nav has 5 links; lexicon firewall green
P1-01-01, P1-02-01
Wave 3 — Mock store (imports x/*/types) + import-invariant test (blocked-by Wave 2)
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P1-03-01
REQ-040, D-067, D-070, D-071
backend-engineer
web/store/store.go, web/store/fixtures.go
web/store/store.go: in-memory mock store (type Store struct { mu sync.Mutex; reaches map[string]identitytypes.Reach; stashes map[string]stashtypes.Stash; ... }). NewStore() seeds from fixtures.go. Methods: CreateReach(holderID, publicKey string) (identitytypes.Reach, stashtypes.Stash, error) — atomic Reach + Stash creation per D-071: creates a Reach with IsNomad=true + a Stash with HolderID matching the Reach's HolderID and BalanceGrain seeded to a test value (e.g., 500000 Grain = 50 Bread per D-071 example); ListReaches() []identitytypes.Reach; GetReach(reachID string) (identitytypes.Reach, bool); GetStash(holderID string) (stashtypes.Stash, bool). Imports x/identity/types, x/stash/types (app-layer consumption — D-070; NOT a cross-x/ import). fixtures.go: seed data (a few pre-existing Reach/Stash pairs for the list view). All strings lexicon-clean ("Holder"/"Reach"/"Stash"; NOT "account"/"bank"/"deposit").
go build ./web/store succeeds; go test ./web/store passes (P1-03-02); CreateReach returns a Reach with IsNomad=true AND a Stash with matching HolderID (atomic — D-071); web/store imports only x/identity/types + x/stash/types (no x/*/keeper — verified by P1-03-03); lexicon firewall green
go test ./web/store passes; coverage ≥80% on web/store; atomic-create test asserts both Reach + Stash exist after one call; lexicon firewall green
P1-03-01
P1-03-03
REQ-040, D-070, G-003
backend-engineer
web/store/import_test.go
G-003 boundary test for web/: scans all non-test .go files under web/ using go/parser (or go/build import list) and asserts web/ imports ONLY x/*/types packages (NOT x/*/keeper — there is no keeper in v0.6). This is the app-layer-consumption invariant (D-070): web/ may consume protocol types but must not reach into keeper/state machinery. The existing v0.2 G-003 import-invariant test (scanning x/**/*.go) is UNCHANGED — web/ is outside x/ so it is not in that test's scope; this NEW test covers the web/ scope.
go test ./web/store passes; the import-invariant test asserts no x/*/keeper import in any web/ production file; x/*/types imports are allowed (D-070)
web/handlers/reach.go: ReachHandler struct holding *store.Store + *template.Template. Routes (Go 1.22 method patterns, wired in web/server.go): GET /reach → list view (ListReaches); GET /reach/{id} → detail view (GetReach + GetStash); GET /reach/new → "Create a Reach" form (lexicon-clean: "Create a Reach", NOT "Sign up for an account" — "account" is banned); POST /reach → form handler calling store.CreateReach (atomic Reach + Stash per D-071), redirect to the new Reach detail. HTMX: the form POST can be progressive-enhanced (hx-post="/reach" → swap). web/handlers/server.go: handler constructor + route registration helper.
go build ./web succeeds; go run ./web serves GET /reach (list), GET /reach/new (form), POST /reach (creates + redirects); manual browser check: fill the form → a Reach + Stash appear; lexicon firewall green on web/handlers/reach.go
reach_list.html: table of Reaches (ReachID, HolderID, IsNomad, IsFreeholder) + "Create a Reach" link. reach_detail.html: Reach fields + the associated Stash (BalanceGrain). reach_new.html: the "Create a Reach" form (HolderID + PublicKey inputs; submit POST /reach). All lexicon-clean ("Holder"/"Reach"/"Stash"; NOT "account"/"bank"/"deposit"). Extends base.html.
go run ./web renders all 3 Reach templates; manual browser check: form submission creates a Reach visible in the list; lexicon firewall green on all 3 templates
P1-04-01, P1-02-03
P1-04-03
REQ-040, D-033
frontend-engineer + backend-engineer
web/handlers/reach_test.go
Handler tests using httptest.NewRecorder + httptest.NewRequest (stdlib — no external test deps): GET /reach returns 200 + list HTML; GET /reach/{id} returns 200 + detail HTML for a seeded Reach; GET /reach/new returns 200 + form HTML; POST /reach with valid form creates a Reach + Stash atomically (assert both in the store) + redirects (302) to the detail; POST /reach with empty HolderID returns 400; the rendered HTML contains NO banned terms (assert via lexicon.FindBannedTerm on the response body — a per-handler lexicon check, complementing the file-scan firewall). Coverage ≥80% on web/handlers (P1 scope).
go test ./web/handlers passes; coverage ≥80% on web/handlers (P1 subset); atomic-create asserted via the store after POST; rendered-HTML lexicon check green
P1-04-01, P1-04-02
Wave 5 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P1-99-01
REQ-012, REQ-040, REQ-045
lead-developer
(cross-cutting)
go build ./... + go test ./... green (incl. all v0.1..v0.5 baseline + P1 web + the new lexicon_meta_web/ firewall); coverage ≥80% on web/store + web/handlers (P1 subset); lexicon_meta_web/ firewall green; lexicon_meta_test.go (v0.2, x/*.go) + lexicon_meta_docs_test.go (v0.3, docs) green (no regression); go.mod unchanged (G-006 — zero new require lines); go run ./web starts on :8080 (manual check); tag v0.5.1.
go test ./... green; coverage ≥80% on web/store + web/handlers; all 3 lexicon firewalls green; go.mod unchanged; go run ./web serves the home + Reach screens; git tag v0.5.1 created
go test ./lexicon_meta_web/... green (firewall passes with P1 web content).
go test ./... green across the whole repo (no regression; v0.2/v0.3 lexicon firewalls unchanged).
web/main.go + web/server.go exist; Go 1.22 net/http.ServeMux + html/template; go run ./web starts on :8080 with no external deps.
web/static/htmx.min.js vendored (NOT a go get); go.mod unchanged (G-006).
web/store/store.go imports x/identity/types + x/stash/types (D-070 app-layer consumption); CreateReach atomically creates a Reach (IsNomad=true) + a Stash (D-071); web/store/import_test.go asserts no x/*/keeper imports.
web/handlers/reach.go + 3 Reach templates exist; GET /reach (list), GET /reach/{id} (detail), GET /reach/new (form), POST /reach (atomic create + redirect).
"Create a Reach" labels used (NOT "Sign up for an account" — "account" is banned per REQ-012).
≥80% coverage on web/store + web/handlers (P1 subset).
Rendered-HTML lexicon check green (per-handler test asserts no banned terms in response body).
Git tag v0.5.1.
P1 Risks & Mitigations
"account" lexicon drift in signup form (highest P1 risk) → "Create a Reach" labels; the lexicon_meta_web/ firewall scans web/templates/reach_new.html; the per-handler rendered-HTML lexicon check (P1-04-03) catches a banned term in dynamic content. Mitigation: firewall-first (P1-01-01 lands before content).
HTMX vendored asset size / version → HTMX 1.x minified is ~14KB; vendored as a static file, not a go.mod entry. Pin the version in a comment in web/static/htmx.min.js header.
Atomic Reach + Stash creation race (D-071) → CreateReach holds the store mutex across both map writes; the concurrent-create test (P1-03-02) asserts no race.
G-003 boundary confusion → web/ importing x/*/types is app-layer consumption (D-070), NOT a cross-x/ import; the NEW web/store/import_test.go (P1-03-03) documents and enforces the boundary (only x/*/types, never x/*/keeper).
Goal: Ship the Stash dashboard screen: balance in Grain + Bread-scale conversion (using x/bread/types.BreadScaleAll() + GrainsPerBread=10000 per D-073 — code constants are the source of truth, NOT docs) + 90-day maturity progress bar (x/stash/types.StashActivity.IsMature, MaturityThresholdDays=90). Depends on P1 (a Stash exists, created atomically at signup per D-071).
Wave 1 — Store extensions + handler + template (parallel where possible; blocked-by P1 ship)
Extend the mock store: add GetStashActivity(stashID string) (stashtypes.StashActivity, bool) + seed StashActivity fixtures (some mature, some not — ActiveDays varying around MaturityThresholdDays=90; MaxGapDays varying around MaxGapForMaturity=30). Import x/stash/types (already imported in P1). The Bread-scale conversion is computed in the handler/template from x/bread/types.BreadScaleAll() + GrainsPerBread=10000 (D-073 — code constants, NOT docs/shared/bread-scale.md which is outdated).
go build ./web/store succeeds; GetStashActivity returns seeded activities; web/store still imports only x/*/types (P1-03-03 import-invariant green); lexicon firewall green
StashHandler struct. Route: GET /stash/{holderID} → dashboard. Loads Stash (balance BalanceGrain in Grain) + StashActivity from the store; computes Bread-scale conversion by calling x/bread/types.BreadScaleAll() (returns the 11-denomination table) and x/bread/types.GrainsPerBread (10000) to convert the Grain balance into Bread (and display the full scale table for context). Computes 90-day maturity progress: ActiveDays / MaturityThresholdDays (as a percentage; capped at 100%) + IsMature() boolean (stashtypes.StashActivity.IsMature() — ActiveDays >= 90 && MaxGapDays <= 30). Passes all to the template.
go build ./web succeeds; go run ./web serves GET /stash/{holderID} (200 + dashboard HTML); Bread-scale conversion uses BreadScaleAll() + GrainsPerBread=10000 (D-073 — verified by a test asserting the conversion matches the code constants); lexicon firewall green
P2-01-01, P1-02-03
P2-02-02
REQ-041
frontend-engineer
web/templates/stash.html
Stash dashboard template: balance in Grain + Bread-scale conversion table (all 11 denominations from BreadScaleAll() — Grain, Crumb, Bread, Loaf, Batch, Cake, Bakery, Granary, Mill, Harvest, Earth) + 90-day maturity progress bar (CSS width = ActiveDays/90 * 100%) + IsMature badge (green if mature, amber if not). Extends base.html. Lexicon-clean ("Stash"/"Grain"/"Bread"/"maturity"; NOT "bank"/"deposit"/"savings"/"interest").
go run ./web renders the Stash dashboard; manual check: balance shows in Grain + Bread scale; progress bar reflects ActiveDays; lexicon firewall green on web/templates/stash.html
Handler test (httptest): GET /stash/{holderID} returns 200 + dashboard HTML; the rendered HTML shows the Grain balance + the Bread-scale conversion table (all 11 denominations) + the maturity progress bar. Bread-scale conversion correctness test: asserts the handler's conversion matches x/bread/types.BreadScaleAll() + GrainsPerBread=10000 (D-073 — e.g., 500000 Grain = 50 Bread; 100 Grain = 1 Crumb); a test that would FAIL if the handler used the outdated docs/shared/bread-scale.md values (1,000× ratios) instead of the code constants. Maturity progress test: a mature fixture (ActiveDays=90, MaxGapDays=10 → IsMature()==true, progress 100%) vs an immature fixture (ActiveDays=45, MaxGapDays=10 → IsMature()==false, progress 50%). Store test: GetStashActivity hit/miss. Rendered-HTML lexicon check (no banned terms in response body). Coverage ≥80% on web/handlers (P1+P2 cumulative).
go test ./web/... passes; Bread-scale conversion matches code constants (D-073); maturity progress matches IsMature(); coverage ≥80% on web/handlers (cumulative); rendered-HTML lexicon check green
P2-02-02, P2-01-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P2-99-01
REQ-012, REQ-041
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% on web/store + web/handlers (cumulative); lexicon_meta_web/ firewall green (now scans web/templates/stash.html + web/handlers/stash.go); go.mod unchanged; go run ./web serves the Stash dashboard; tag v0.5.2.
go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; Stash dashboard reachable from home; git tag v0.5.2
P2-03-01
P2 Must-Haves
web/handlers/stash.go + web/templates/stash.html exist; GET /stash/{holderID} renders the dashboard.
Balance shown in Grain + Bread-scale conversion using x/bread/types.BreadScaleAll() + GrainsPerBread=10000 (D-073 — code constants, NOT docs).
90-day maturity progress bar + IsMature badge using x/stash/types.StashActivity.IsMature() + MaturityThresholdDays=90 + MaxGapForMaturity=30.
Bread-scale conversion correctness test (would fail if docs values were used instead of code constants — D-073 regression guard).
≥80% coverage on web/store + web/handlers (cumulative).
Bread-scale source-of-truth drift (D-073) → the conversion correctness test asserts the handler uses BreadScaleAll() + GrainsPerBread=10000 (code constants); a test using the outdated docs/shared/bread-scale.md 1,000× ratios would fail. The doc-fix is a P1+ follow-up (NOT v0.6 scope — PROJECT.md out-of-scope).
Maturity progress > 100% → cap ActiveDays/90 at 100% in the template/handler; test the cap.
REQs covered: REQ-042 (Window authorization — open Window (scope+duration+rate-limit), lifecycle Open→Active→Revoked/Expired via Window.Activate/Revoke/Expire, audit log)
Tag: v0.5.3
Type: feat
Goal: Ship the Window authorization screen: a form to open a Window (scope + duration + rate-limit) delegating to a service, a lifecycle view (Open→Active→Revoked/Expired using x/window/types.Window.Activate/Revoke/Expire), and an audit log view (AuditEntry). Depends on P2 (Window scope references a Stash).
Extend the mock store: add windows map[string]windowtypes.Window + auditLogs map[string][]windowtypes.AuditEntry. Methods: OpenWindow(grantorHolder, grantee string, scope windowtypes.Scope, start, end int64, rateLimit windowtypes.RateLimit) (windowtypes.Window, error) (creates a Window status=Open + an initial AuditEntry); ActivateWindow(windowID string) error (calls Window.Activate() — Open→Active); RevokeWindow(windowID string) error (calls Window.Revoke() — idempotent; Revoked/Expired are no-ops); ExpireWindow(windowID string) (calls Window.Expire()); ListWindows(grantorHolder string) []windowtypes.Window; GetWindow(windowID string) (windowtypes.Window, bool); GetAuditLog(windowID string) []windowtypes.AuditEntry; AppendAuditEntry(windowID string, entry windowtypes.AuditEntry). Import x/window/types (app-layer — D-070). Seed fixtures: a few Windows in various lifecycle states (Open, Active, Revoked, Expired) + audit logs.
go build ./web/store succeeds; OpenWindow creates a Window status=Open; ActivateWindow transitions Open→Active; RevokeWindow transitions to Revoked (idempotent on already-Revoked/Expired); ExpireWindow transitions to Expired; web/store still imports only x/*/types (import-invariant green); lexicon firewall green
WindowHandler struct. Routes: GET /window → list of Windows for a holder; GET /window/{id} → detail (lifecycle state + scope + rate-limit + audit log); GET /window/new → "Open a Window" form (scope kind dropdown from ScopeKind enum: ReadStash/ReadStanding/ProcessPassActForStand; resource-id; grantee; duration start/end; rate-limit max-actions + per-duration); POST /window → OpenWindow + redirect to detail; POST /window/{id}/activate → ActivateWindow (HTMX swap); POST /window/{id}/revoke → RevokeWindow; POST /window/{id}/expire → ExpireWindow. Each lifecycle action appends an AuditEntry.
go build ./web succeeds; go run ./web serves the 4 Window routes; lifecycle transitions call Window.Activate/Revoke/Expire (verified by test); lexicon firewall green
window_list.html: table of Windows (WindowID, GrantorHolder, Grantee, Scope, Status) + "Open a Window" link. window_detail.html: Window fields + lifecycle state badge (Open=amber, Active=green, Revoked=red, Expired=grey) + Activate/Revoke/Expire buttons (HTMX hx-post) + audit log table (AuditEntry rows: timestamp, action, result, granter-ref). window_new.html: the "Open a Window" form. Extends base.html. Lexicon-clean ("Window"/"scope"/"Holder"/"Reach"; NOT "account"/"bank").
go run ./web renders all 3 Window templates; manual check: open a Window → activate → revoke → audit log shows the actions; lexicon firewall green on all 3 templates
Handler tests (httptest): GET /window (list 200); GET /window/{id} (detail 200 + lifecycle badge + audit log); GET /window/new (form 200); POST /window (open → 302 to detail; new Window status=Open + initial AuditEntry); POST /window/{id}/activate (Open→Active; AuditEntry appended); POST /window/{id}/revoke (→Revoked; idempotent — second revoke is a no-op, no new AuditEntry); POST /window/{id}/expire (→Expired; revoke-after-expire is a no-op). Lifecycle correctness test: asserts the handler calls Window.Activate/Revoke/Expire (the real x/window/types methods — not a reimplementation); Revoke() on an Expired window is a no-op (matches the v0.2 type contract). Store tests: OpenWindow/ActivateWindow/RevokeWindow/ExpireWindow/ListWindows/GetAuditLog round-trips. Rendered-HTML lexicon check. Coverage ≥80% on web/handlers (P1+P2+P3 cumulative).
go test ./web/... passes; lifecycle transitions match Window.Activate/Revoke/Expire; revoke-on-expired no-op; coverage ≥80% (cumulative); rendered-HTML lexicon check green
P3-02-02, P3-01-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P3-99-01
REQ-012, REQ-042
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; go run ./web serves the Window screen; tag v0.5.3.
go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; Window screen reachable from home; git tag v0.5.3
P3-03-01
P3 Must-Haves
web/handlers/window.go + 3 Window templates exist; GET /window (list), GET /window/{id} (detail + lifecycle + audit log), GET /window/new (form), POST /window (open), POST /window/{id}/activate|revoke|expire (lifecycle).
Lifecycle transitions call x/window/types.Window.Activate/Revoke/Expire (the real methods); Revoke() on Expired is a no-op (v0.2 type contract).
Audit log (AuditEntry) appended on each lifecycle action; displayed in the detail view.
Window form uses ScopeKind enum (ReadStash/ReadStanding/ProcessPassActForStand) + rate-limit fields.
≥80% coverage on web/store + web/handlers (cumulative).
Lifecycle state-machine divergence from the type contract → the handler MUST call Window.Activate/Revoke/Expire (not reimplement transitions); the lifecycle correctness test asserts the real methods are invoked.
Revoke-on-Expired no-op → the v0.2 Revoke() returns nil on Expired (terminal state wins); the test covers this edge.
REQs covered: REQ-043 (Standing + Freeholder signals progress — computed from mock Rating/Vouch/Slash using locked constants + GetStandingBucket/ComputeDiversityBonus/GetVoucherWeight; 4-signal progress via FreeholderSignals.IsFreeholderEligible)
Tag: v0.5.4
Type: feat
Goal: Ship the Standing + Freeholder signals progress screen: computed from mock Rating/Vouch/Slash records using the locked formula constants + GetStandingBucket/ComputeDiversityBonus/GetVoucherWeight; displays the 4-signal progress (FreeholderSignals.IsFreeholderEligible — StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement). Depends on P1 (Standing is per-Reach).
Extend the mock store: add ratings map[string][]standingtypes.Rating (per-ratee), vouches map[string][]standingtypes.Vouch, slashes map[string][]standingtypes.Slash. Methods: ListRatings(rateeID string) []standingtypes.Rating; ListVouches(voucheeID string) []standingtypes.Vouch; ListSlashes(reachID string) []standingtypes.Slash; ComputeStandingScore(reachID string) (float64, standingtypes.StandingBucket) — computes a simplified standing score from the mock Ratings using the locked constants (PriorMean=4.0, PriorWeight=10, decay buckets, ComputeDiversityBonus, GetVoucherWeight) + GetStandingBucket(score, ratingCount, isSlashed); ComputeFreeholderSignals(reachID string) standingtypes.FreeholderSignals — computes the 4 signals: StashMaturity (from StashActivity.IsMature() — P2 store method), MultiDomainStanding (score >= FreeholderMinStandingScore=4.5 in >= FreeholderMinCategories=3), CommittedCapital (mock: Stash balance >= a threshold), CommunityEndorsement (>= 1 Vouch). Import x/standing/types + x/stash/types (app-layer — D-070). Seed fixtures: a Reach with enough Ratings/Vouches to be Freeholder-eligible + one that is not.
go build ./web/store succeeds; ComputeStandingScore returns a score + bucket using the locked constants; ComputeFreeholderSignals returns 4 booleans; web/store imports only x/*/types (import-invariant green); lexicon firewall green
StandingHandler struct. Route: GET /standing/{reachID} → Standing + Freeholder signals progress. Loads Ratings/Vouches/Slashes from the store; calls ComputeStandingScore + ComputeFreeholderSignals; passes the score, bucket, 4 signals (each as a progress indicator), and the underlying records to the template.
go build ./web succeeds; go run ./web serves GET /standing/{reachID} (200 + progress HTML); the score is computed from the locked constants (verified by test); lexicon firewall green
P4-01-01, P1-02-03
P4-02-02
REQ-043
frontend-engineer
web/templates/standing.html
Standing + Freeholder signals template: standing score (float, 1 decimal) + StandingBucket badge (New/Trusted/Preferred/Top/Slashed) + 4-signal progress (StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement — each a check/cross + label) + Freeholder-eligible badge (green if IsFreeholderEligible(), grey if not) + underlying Ratings/Vouches/Slashes tables. Extends base.html. Lexicon-clean ("Standing"/"Freeholder"/"Vouch"/"Rating"; NOT "account"/"bank").
go run ./web renders the Standing screen; manual check: a seeded Freeholder-eligible Reach shows 4 checks + green badge; a non-eligible Reach shows crosses; lexicon firewall green on web/templates/standing.html
Handler test (httptest): GET /standing/{reachID} returns 200 + progress HTML; the rendered HTML shows the score + bucket + 4 signals + Freeholder-eligible badge. Standing score computed-from-locked-constants test: asserts ComputeStandingScore uses x/standing/types constants (PriorMean=4.0, PriorWeight=10, ComputeDiversityBonus, GetVoucherWeight, GetStandingBucket) — a test that would FAIL if the handler hardcoded a score instead of computing from the locked constants. Freeholder-eligible badge test: a seeded eligible Reach (IsFreeholderEligible()==true → green badge) vs a non-eligible Reach (a signal false → grey badge); asserts the badge reflects IsFreeholderEligible(). Store tests: ListRatings/ListVouches/ListSlashes/ComputeStandingScore/ComputeFreeholderSignals round-trips. Rendered-HTML lexicon check. Coverage ≥80% on web/handlers (cumulative P1..P4).
go test ./web/... passes; standing score computed from locked constants; Freeholder-eligible badge reflects IsFreeholderEligible(); coverage ≥80% (cumulative); rendered-HTML lexicon check green
P4-02-02, P4-01-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P4-99-01
REQ-012, REQ-043
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; go run ./web serves the Standing screen; tag v0.5.4.
go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; Standing screen reachable from home; git tag v0.5.4
P4-03-01
P4 Must-Haves
web/handlers/standing.go + web/templates/standing.html exist; GET /standing/{reachID} renders the progress.
Standing score computed from x/standing/types locked constants (PriorMean, PriorWeight, decay buckets, ComputeDiversityBonus, GetVoucherWeight, GetStandingBucket) — NOT hardcoded.
Standing formula oversimplification → the mock ComputeStandingScore is a SIMPLIFIED computation from the locked constants (not the full Bayesian formula — sub-tables deferred per PROJECT.md Q2); the test asserts it uses the locked constants, not that it matches a full oracle. Document the simplification in a code comment.
Freeholder-eligible badge divergence → the badge MUST reflect IsFreeholderEligible() (the real method); the test asserts the rendered badge matches the method output.
Goal: Ship the Bloom accrual screen: a per-Stash BloomRecord view (AccruedGrain, RateBasisPoints, LastAccrualBlock) computed from mock data; shows the 4.5% target rate (TargetBloomRateBasisPoints=450). Depends on P2 (BloomRecord is per-Stash).
BloomHandler struct. Route: GET /bloom/{stashID} → Bloom accrual view. Loads the BloomRecord from the store; reads x/bloom/types.TargetBloomRateBasisPoints=450 (4.5% target rate — D-073 code-constant source-of-truth) + MinBloomRateBasisPoints=400 + MaxBloomRateBasisPoints=500 + AccrualPeriodBlocks=144; passes the record + the target rate band to the template.
go build ./web succeeds; go run ./web serves GET /bloom/{stashID} (200 + accrual HTML); the target rate is read from x/bloom/types.TargetBloomRateBasisPoints (verified by test); lexicon firewall green
P5-01-01, P1-02-03
P5-02-02
REQ-044
frontend-engineer
web/templates/bloom.html
Bloom accrual template: BloomRecord fields (AccruedGrain, RateBasisPoints displayed as a percentage e.g. 4.5%, LastAccrualBlock) + the target rate band (4.0%–5.0%, target 4.5%) + a note that "Bloom originates only from real production" (the MissionLockBloom const, lexicon-clean). Extends base.html. Lexicon-clean ("Bloom"/"Grain"/"real production"; NOT "yield"/"interest"/"deposit").
go run ./web renders the Bloom screen; manual check: accrued Grain + 4.5% target rate displayed; lexicon firewall green on web/templates/bloom.html
Handler test (httptest): GET /bloom/{stashID} returns 200 + accrual HTML; the rendered HTML shows AccruedGrain + RateBasisPoints (as %) + LastAccrualBlock + the 4.5% target rate. Target-rate source-of-truth test: asserts the handler reads x/bloom/types.TargetBloomRateBasisPoints=450 (NOT a hardcoded 450 or a docs value — D-073 pattern); a test that would FAIL if the handler hardcoded the rate. Store tests: GetBloomRecord/ListBloomRecords round-trips. Rendered-HTML lexicon check (highest-risk screen for "yield"/"interest" drift — the test scans the response body). Coverage ≥80% on web/handlers (cumulative P1..P5).
go test ./web/... passes; target rate read from code constant (D-073); coverage ≥80% (cumulative); rendered-HTML lexicon check green (no "yield"/"interest")
P5-02-02, P5-01-01
Wave 3 — Phase verification + ship
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P5-99-01
REQ-012, REQ-044
lead-developer
(cross-cutting)
go build ./... + go test ./... green; coverage ≥80% (cumulative across all web/ packages); lexicon_meta_web/ green; go.mod unchanged; go run ./web serves the Bloom screen; tag v0.5.5.
go test ./... green; coverage ≥80% (cumulative); lexicon_meta_web/ green; go.mod unchanged; Bloom screen reachable from home; git tag v0.5.5
P5-03-01
P5 Must-Haves
web/handlers/bloom.go + web/templates/bloom.html exist; GET /bloom/{stashID} renders the accrual view.
4.5% target rate read from x/bloom/types.TargetBloomRateBasisPoints=450 (D-073 code-constant source-of-truth; NOT hardcoded).
Target-rate source-of-truth test (regression guard against hardcoding).
≥80% coverage on web/store + web/handlers (cumulative P1..P5).
lexicon_meta_web/ firewall green; rendered-HTML lexicon check green (no "yield"/"interest").
go.mod unchanged (G-006).
Git tag v0.5.5.
P5 Risks & Mitigations
"yield"/"interest" lexicon drift in Bloom prose (highest P5 risk — Bloom is conceptually close to "yield") → "Bloom"/"real production"/"accrual" labels; the lexicon_meta_web/ firewall scans web/templates/bloom.html; the rendered-HTML lexicon check (P5-03-01) scans the response body. "Bloom" is the vision lexicon (§6); "yield" is banned.
Target-rate hardcoding → the handler MUST read TargetBloomRateBasisPoints from x/bloom/types (D-073); the source-of-truth test asserts it.
REQs covered: all v0.6 REQs (REQ-040..REQ-045) — final coverage accounting; no new REQs (covers post-hoc fixes from REVIEW/AUDIT)
Tag: v0.5.6 (IS the v0.6 milestone release; D-008)
Type: final
Personas: lead-developer (review/ship) + ci-security-auditor (ACTIVATED for the v0.6 milestone audit + feature purity gate)
Goal: Multi-persona review across P1..P5, audit (reconstruction test + feature purity gate: no breaking schema changes; G-003 production firewall intact — web/ is app-layer, not an x/ module; G-006 go.mod unchanged — HTMX is a vendored static asset, not a Go dep; locked-const firewall intact — no x/ consts amended), milestone ship (merge to main, tag v0.5.6 = v0.6 milestone release, release, delete all milestone branches).
Wave 1 — Review + Audit (parallel)
Task ID
REQ
Persona
Files
Deliverable
Must-have verification
Blocked-by
P6-01-01
—
lead-developer (review)
.ciagent/oy/REVIEW.md (NEW for v0.6)
Multi-persona code review across P1..P5. Adversarial probes: (1) does go run ./web start with no external deps (G-006); (2) are all 5 screens reachable from the home page; (3) does the lexicon_meta_web/ firewall scan all web/templates/** + web/static/** + web/**/*.go; (4) does the Bread-scale conversion match x/bread/types code constants (D-073); (5) does the Standing score use the locked formula constants; (6) does the Freeholder-eligible badge reflect IsFreeholderEligible(); (7) do the Window lifecycle transitions call Window.Activate/Revoke/Expire; (8) are there any banned terms in any rendered page (rendered-HTML lexicon checks). Auto-apply P0 fixes; flag P1+ for post-hoc.
Audit: (1) reconstruction test (git log ↔ .ciagent/ files for v0.6; each REQ-040..REQ-045 maps to a shipped UI screen / firewall); (2) file/branch/commit discipline (6 phase branches phase/01-*..phase/06-*; 6 patch tags v0.5.1..v0.5.6; D-072 ordering respected — firewall-first P1 before content P2..P5); (3) feature purity gate: no breaking schema changes (no x/ module modified — web/ is new app-layer, not an x/ amendment); locked-const firewall intact (all v0.1..v0.5 consts unchanged — web/ does not touch x/ consts); G-003 production firewall intact (web/ imports only x/*/types per D-070; the web/store/import_test.go import-invariant green; no x/ module gains a production import of another x/ module's types via web/); G-006 go.mod unchanged (HTMX is a vendored static asset, NOT a go get — zero new require lines in v0.6); (4) coverage ≥80% on all web/ packages (web/store, web/handlers); (5) all 3 lexicon firewalls green (lexicon_meta_test.go x/*.go, lexicon_meta_docs_test.go docs, lexicon_meta_web/ web); (6) manual browser check: go run ./web → all 5 screens reachable + the happy path works end-to-end.
AUDIT.md v0.6 section written; feature purity gate GREEN (no breaking schema changes; locked-const firewall intact; G-003 intact — web/ is app-layer; G-006 go.mod unchanged); reconstruction test passes.
Update REQUIREMENTS.md: mark REQ-040..REQ-045 → Complete (UI shipped). Update ROADMAP.md: mark v0.6 milestone COMPLETE; add the tag-line note that v0.6 shipped on the v0.5.x patch line (P0 → v0.5.0, P1..P5 → v0.5.1..v0.5.5, P6 → v0.5.6 = milestone release, per D-008).
REQUIREMENTS.md status column updated for all 6 v0.6 REQs → Complete; ROADMAP.md v0.6 marked complete + tag-line note present.
P6-01-01, P6-02-01
P6-03-02
(milestone)
lead-developer
(cross-cutting)
Final ship: merge phase/06 → milestone/v0.6-nomad-web-ui → main; create milestone release tag v0.5.6 (= v0.6 milestone release per D-008); delete the 6 phase branches (phase/01-*..phase/06-*) after merge; confirm go build ./... + go test ./... green at the v0.5.6 tag; confirm go run ./web starts at the tag.
v0.5.6 tag created on main; go test ./... green at the tag; go run ./web starts at the tag; ROADMAP.md v0.6 complete; phase branches deleted; release notes reference v0.6 scope (5-screen Nomad Web UI: Reach signup, Stash dashboard, Window authorization, Standing + Freeholder signals, Bloom accrual; Go html/template + HTMX vendored; mock server over real x/*/types; lexicon firewall extended to web/).
P6-03-01
P6 Must-Haves
REVIEW.md v0.6 section written; P0 fixes applied.
AUDIT.md v0.6 section written; reconstruction test passes.
Feature purity gate GREEN: no breaking schema changes (no x/ module modified); locked-const firewall intact (all v0.1..v0.5 consts unchanged); G-003 production firewall intact (web/ imports only x/*/types per D-070; import-invariant green); G-006 go.mod unchanged (HTMX vendored, not a Go dep).
Coverage ≥80% on all web/ packages (web/store, web/handlers).
All 3 lexicon firewalls green (lexicon_meta_test.go x/*.go, lexicon_meta_docs_test.go docs, lexicon_meta_web/ web).
go run ./web starts at the v0.5.6 tag; all 5 screens reachable; happy path works end-to-end.
REQUIREMENTS.md + ROADMAP.md mark v0.6 COMPLETE.
Tag v0.5.6 created (= v0.6 milestone release).
Milestone branch merged to main.
All 6 phase branches deleted (local + remote).
P6 Risks & Mitigations
UI mock diverges from x/*/types contracts → the mock store instantiates the REAL x/*/types structs (D-067); the handler tests assert the conversions match the code constants (D-073 for Bread-scale, locked formula constants for Standing, TargetBloomRateBasisPoints for Bloom); divergence is caught by the tests, not just the audit.
G-006 dep exception confusion → v0.6 adds NO Go deps (HTMX is a vendored static asset); the v0.5 cosmos-sdk/ibc-go deps stay but v0.6 adds nothing; the audit verifies go.mod is unchanged across the v0.6 milestone range.
Milestone versioning confusion (v0.6 milestone = v0.5.6 tag) → lead-developer enforces D-008: final phase patch IS the milestone release; no separate minor tag. ROADMAP tag-line note (P6-03-01) prevents v0.5.6/v0.6.0 confusion.
Reach signup form + atomic create (P1); Stash dashboard + Bread-scale conversion correctness (P2, D-073); Window lifecycle via Window.Activate/Revoke/Expire (P3); Standing score from locked constants + Freeholder-eligible badge (P4); Bloom target-rate from code constant (P5); rendered-HTML lexicon checks (all phases)
lexicon_meta_web
P1
100% (test-only)
G-009 self-test via SyntheticBannedStrings(); G-013 walk-coverage; no-false-positive on "openyield"/"european"; scans web/templates/** + web/static/** + web/**/*.go
Lexicon assertions (REQ-012): the NEW lexicon_meta_web/ firewall (P1) scans all web/**/*.{html,js,go} for the 10 banned terms. Per-handler rendered-HTML lexicon checks (each phase's handler test) scan the HTTP response body for banned terms — catching dynamic content the file-scan firewall cannot see. The v0.2 lexicon_meta_test.go (x/*.go) + v0.3 lexicon_meta_docs_test.go (docs) are UNCHANGED (no regression).
P1-01-01 (lexicon firewall lexicon_meta_web/) → blocks P1-02-03 + P1-04-02 (templates scanned by the firewall) and P2..P5 (content scanned).
P1-02-01 (web foundation main.go/server.go) → blocks P1-03-01 (store used by handlers) and all subsequent handler/template tasks.
P1-03-01 (mock store) → blocks P1-04-01 (Reach handler uses the store) and P2..P5 (each screen extends the store).
P1-04-01 (Reach signup handler) → blocks P2 (Stash dashboard needs a Stash created at signup — D-071), P4 (Standing is per-Reach).
P2-99-01 (P2 ship) → blocks P3 (Window scope references a Stash), P5 (BloomRecord is per-Stash).
P3-99-01 (P3 ship) → blocks P4 (soft ordering for branch hygiene; Standing is per-Reach, not per-Window, but the cumulative test suite + branch hygiene enforce serial phases).
P4-99-01 (P4 ship) → blocks P5 (soft ordering).
P5-99-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).
v0.6 Decisions Applied (D-066..D-073)
The v0.6 Phase 0 clarify/ideate/research stages produced 8 clarification decisions (D-066..D-073), all applied to this plan:
ID
Decision
Applied to
D-066
Frontend stack = Go html/template + HTMX (vendored, no node); G-006 preserved
P1-02-01, P1-02-02 (web foundation + HTMX vendored); Milestone Summary (G-006)
D-067
Mock data layer = Go HTTP server in web/ instantiating real x/*/types structs; no keeper, no Cosmos runtime, no app.go
P1-02-01, P1-03-01 (mock store imports x/*/types); all handler tasks
D-068
UI code location = new top-level web/ dir (not an x/ module)
P1-02-01, P1-03-01 (web/ dir); Milestone Summary
D-069
Lexicon firewall extension to web/ = new sibling lexicon_meta_web/ (mirrors lexicon_meta_docs/); firewall-first (P1 before content)
P1-01-01 (firewall); all P2..P5 content scanned
D-070
G-003 boundary: web/ importing x/*/types is app-layer consumption, NOT a cross-x/ production import
P1-03-01, P1-03-03 (import-invariant test); all store tasks; P6 audit
D-071
"Sign up" = create a Reach + open a Stash atomically; UI labels it "Create a Reach" ("account" is banned)
P1-03-01 (atomic CreateReach), P1-04-01/02 ("Create a Reach" labels)
Web UI at http://localhost:8080 — runnable via go run ./web (single binary, no external deps, no node toolchain, no build step). The UI is a Go html/template server-rendered HTML layer with HTMX progressive enhancement (vendored web/static/htmx.min.js). Five screens, all reachable from the home page nav:
Reach signup (GET /reach/new → POST /reach): the "Create a Reach" form. A visitor signs up to be a Nomad by creating a Reach (atomically creates a Stash per D-071). Reach list (GET /reach) + detail (GET /reach/{id}).
Stash dashboard (GET /stash/{holderID}): balance in Grain + Bread-scale conversion (all 11 denominations from BreadScaleAll()) + 90-day maturity progress bar + IsMature badge.
Window authorization (GET /window/new → POST /window; GET /window/{id}): open a Window (scope + duration + rate-limit), lifecycle transitions (Activate/Revoke/Expire via HTMX buttons), audit log.
README quickstart: go run ./web → open http://localhost:8080. No make, no npm, no docker — just Go 1.22 stdlib + one vendored JS file. The README (updated in P1 or P6) documents the go run invocation + the 5 screen routes.
Happy Path
Scenario: a visitor signs up as a Nomad and exercises the full UI end-to-end (mock data, no chain, no real values).
The visitor opens http://localhost:8080/ (home page) in a browser. The home page shows a one-paragraph OpenYield overview + nav links to the 5 screens.
The visitor clicks "Create a Reach" → GET /reach/new renders the signup form (HolderID + PublicKey inputs; labeled "Create a Reach" — NOT "Sign up for an account").
The visitor fills the form and submits (POST /reach). The handler calls store.CreateReach which atomically creates a Reach (IsNomad=true) + a Stash (matching HolderID, BalanceGrain seeded to 500000 Grain = 50 Bread per D-071). The browser redirects to GET /reach/{id} (Reach detail showing the Reach + the associated Stash).
The visitor clicks the Stash link → GET /stash/{holderID} renders the Stash dashboard: balance 500000 Grain + the Bread-scale conversion table (50 Bread, 5 Loaves, 0.5 Batch, … from BreadScaleAll()) + a 90-day maturity progress bar (e.g., 45/90 days = 50% — the seeded StashActivity is immature) + an amber "Not Mature" badge (IsMature()==false because ActiveDays < 90).
The visitor clicks "Window authorization" → GET /window/new renders the "Open a Window" form (scope dropdown: ReadStash/ReadStanding/ProcessPassActForStand; grantee; duration; rate-limit). The visitor opens a Window delegating ReadStash to a service → POST /window creates the Window (status=Open) + an initial AuditEntry → redirects to GET /window/{id}.
On the Window detail page, the visitor clicks "Activate" (POST /window/{id}/activate via HTMX) → the Window transitions Open→Active (Window.Activate()); an AuditEntry is appended; the badge turns green. The visitor clicks "Revoke" → the Window transitions to Revoked (Window.Revoke()); the badge turns red; the audit log shows both actions.
The visitor clicks "Standing" → GET /standing/{reachID} renders the Standing + Freeholder signals progress: a standing score (computed from mock Ratings using the locked constants) + a StandingBucket badge + 4-signal progress (StashMaturity: cross — the Stash is immature; MultiDomainStanding: check; CommittedCapital: check; CommunityEndorsement: cross — no Vouch) + a grey "Not Freeholder-eligible" badge (IsFreeholderEligible()==false because 2 signals are false).
The visitor clicks "Bloom accrual" → GET /bloom/{stashID} renders the Bloom view: AccruedGrain (mock value) + RateBasisPoints 4.5% (from TargetBloomRateBasisPoints=450) + LastAccrualBlock + the target rate band (4.0%–5.0%) + the Mission Lock note ("Bloom originates only from real production").
End-to-end in-browser with mock data; no chain, no real values, no persistence (resets on restart). The whole flow took ~6 clicks across 5 screens.
UX Acceptance Criteria
The v0.6 deliverable MUST meet these explicit criteria (verified in P6 audit):
go run ./web starts without external deps: go.mod is unchanged across the v0.6 milestone range (G-006 — zero new require lines; HTMX is a vendored static asset at web/static/htmx.min.js, NOT a go get); go run ./web starts a server on :8080 using only Go 1.22 stdlib (net/http.ServeMux + html/template).
All 5 screens reachable from the home page: the home page nav (web/templates/base.html) links to Reach signup, Stash dashboard, Window authorization, Standing progress, Bloom accrual; each route returns 200 (verified by handler tests + manual browser check at http://localhost:8080).
Lexicon firewall green on all UI strings: the lexicon_meta_web/ firewall (P1-01-01) scans web/templates/**/*.html + web/static/**/*.js + web/**/*.go for the 10 banned terms and passes; per-handler rendered-HTML lexicon checks (each phase's handler test) scan the HTTP response body and pass; "Sign up" is labeled "Create a Reach" ("account" is banned per REQ-012).
Bread-scale conversion matches x/bread/types constants: the Stash dashboard conversion uses x/bread/types.BreadScaleAll() + GrainsPerBread=10000 (D-073 — code constants, NOT docs/shared/bread-scale.md); the Bread-scale conversion correctness test (P2-03-01) asserts the conversion matches the code constants and would FAIL if the outdated docs values (1,000× ratios) were used.
Standing score computed from locked formula constants: the Standing screen score uses x/standing/types constants (PriorMean=4.0, PriorWeight=10, decay buckets, ComputeDiversityBonus, GetVoucherWeight, GetStandingBucket) — NOT hardcoded; the standing-score computed-from-locked-constants test (P4-03-01) asserts this.
Freeholder-eligible badge reflects IsFreeholderEligible(): the Standing screen badge is green when FreeholderSignals.IsFreeholderEligible()==true and grey when false; the Freeholder-eligible badge test (P4-03-01) asserts the rendered badge matches the method output.
Window lifecycle transitions match Window.Activate/Revoke/Expire: the Window screen lifecycle buttons call the real x/window/types.Window.Activate/Revoke/Expire methods (NOT a reimplementation); Revoke() on an Expired window is a no-op (v0.2 type contract); the lifecycle correctness test (P3-03-01) asserts the real methods are invoked.
No banned terms in any rendered page: the per-handler rendered-HTML lexicon checks (P1-04-03, P2-03-01, P3-03-01, P4-03-01, P5-03-01) scan each screen's HTTP response body via lexicon.FindBannedTerm and pass; the lexicon_meta_web/ file-scan firewall passes on all web/**/*.{html,js,go} files.