Compare commits

..

16 Commits

Author SHA1 Message Date
cloudinit-bot 63042285e8 docs(P04): complete Bonds+Bearers+L2 phase
---ci---
project: oy
phase: 4
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-021, REQ-009]
  partial: []
---/ci---

Phase 4 (Bonds+Bearers+L2) complete. 3 packages: x/bond (8% cap/0% floor Clamp,
REQ-021), x/bearers EXTENDED (BearerTransport + OYLRLink + BeaconFrame, D-029),
x/satellite (5-chain L2 enum + ICS-20 v1 Packet, REQ-009). 303 tests total (264 prev
+ 39 new). Coverage: bond 96.8%, bearers 100%, satellite 100%. AllBearers() still 6
(no v0.1 regression). Lexicon + G-003 invariants green. Tagged v0.1.4.
2026-08-17 21:35:17 +00:00
cloudinit-bot c7f7391774 docs(P03): complete Councils+Forex phase
---ci---
project: oy
phase: 3
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-011]
  partial: [REQ-009]
---/ci---

Phase 3 (Councils+Forex) complete. 2 new modules: x/council (3-kind Council, Mission Lock
const false, Voice/SignalKind/TallyResult), x/forex (ForexPair Bread/Asset labels, RateOracle
interface, 4 OracleKind). 264 tests total (207 prev + 57 new). Coverage: x/council 96.4%,
x/forex 100%. Lexicon + G-003 invariants green. Tagged v0.1.3.
2026-08-17 21:30:05 +00:00
cloudinit-bot 0fefd88668 docs(P02): complete Pacts+Partners phase
---ci---
project: oy
phase: 2
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-020, REQ-018]
  partial: []
---/ci---

Phase 2 (Pacts+Partners) complete. 2 new modules: x/pact (6-PactType enum + Mission Lock,
G-005 one module not six), x/partner (4-tier Partner Spectrum). 207 tests total (143 prev
+ 64 new). Coverage: x/pact 95.9%, x/partner 100%. Lexicon + G-003 invariants green.
Tagged v0.1.2.
2026-08-17 21:23:29 +00:00
cloudinit-bot 93a8a3b311 docs(P01): complete Orgs+Window foundation phase
---ci---
project: oy
phase: 1
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-015, REQ-016, REQ-017, REQ-012]
  partial: []
---/ci---

Phase 1 (Orgs+Window foundation) complete. 3 new modules (x/window, x/stand, x/guild)
+ lexicon meta-test scaffolding (G-004). 143 tests total (53 v0.1 baseline + 90 new), 100%
coverage on new packages. Window = fullest primitive (lifecycle Open->Active->Revoked->
Expired, rate-limit, append-only audit log). 9-type Stand enum. Guild Hand-Pass @ 0% fee.
G-003 by-ID-string import invariant test green. G-004/G-009 lexicon meta-test + self-test
table green. Tagged v0.1.1.
2026-08-17 21:18:35 +00:00
cloudinit-bot 3e762f648d docs(P00): complete pre-execution phase
---ci---
project: oy
phase: 0
milestone: v0.2
status: complete
requirements:
  covered: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021]
  partial: []
---/ci---

Phase 0 (pre-execution) complete: SPECIFY -> CLARIFY -> RESEARCH -> PLAN -> GRILL -> SHIP.
v0.2 (The Mesh) milestone established. 14 clarification decisions (D-020..D-033),
domain research + persona roster, 31-task vertical-slice plan, 10 grill binding
decisions (G-001..G-010) applied. Tagged v0.1.0.
2026-08-17 21:01:39 +00:00
cloudinit-bot 289c499a6d chore(init): reinitialize config.json — add personas/sessions/parallelization
---ci---
project: oy
phase: 9
milestone: v0.1
status: complete
---/ci---

Config audit against init.md requirements:
- All required fields present and valid (projects, active_project, autonomy, release, secrets, ship)
- Added optional fields for run.md compatibility:
  - personas.enabled=true, territory_enforcement=warn
  - sessions.session_isolation=branch
  - parallelization.enabled=false, max_concurrent_agents=1
- Forge bootstrapped (gitea/git.cloudinit.dev/oy/openyield)
- Secrets: GITEA_TOKEN in .ciagent/.env.secrets (0600, gitignored)
- All 7 .ciagent/oy/ files present (PROJECT, ARCHITECTURE, ROADMAP, REQUIREMENTS, PLANS, RESEARCH, PERSONAS)
- JSON validated
2026-08-17 20:44:02 +00:00
cloudinit-bot bc15516eea chore(forge): bootstrap Gitea access — org, repo, remote, release
---ci---
project: oy
milestone: v0.1
status: complete
---/ci---

Forge bootstrap complete:
- Organization 'oy' created on git.cloudinit.dev
- Repository 'oy/openyield' created (public, default branch: main)
- Git remote 'origin' configured (HTTPS with credential helper)
- GITEA_TOKEN stored in .ciagent/.env.secrets (mode 0600, gitignored)
- main branch + 10 tags (v0.0.0-v0.0.9) pushed to origin
- Release v0.1 (Pre-MVP Foundation) created on Gitea (tag v0.0.9)
- config.json updated: release.bootstrapped=true, secrets scopes, ship config
2026-08-17 20:42:45 +00:00
CIAgent 41344eb78b docs(ship): update ROADMAP — milestone v0.1 complete
---ci---
project: oy
milestone: v0.1
status: complete
---/ci---

ROADMAP.md updated: all 10 phases marked complete.
Milestone v0.1 status: COMPLETE (local-only).
MVP release (v0.1.0) reserved for future validation.
2026-08-17 20:38:24 +00:00
CIAgent 42641f8483 escalation(milestone): release pending — no remote configured
---ci---
project: oy
milestone: v0.1
status: complete
escalation:
  type: release_pending
  resolution: auto
  reason: no git remote configured; tag+merge succeeded
  forge: gitea (git.cloudinit.dev/oy/openyield)
  fallback: local-only ship complete
---/ci---

Milestone v0.1 ship complete (local-only).
Tag v0.0.9 created. main branch created.
Release creation deferred — no remote to push to.
Pipeline does not halt (release_blocking=false).
2026-08-17 20:38:15 +00:00
CIAgent 1beff09cff docs(milestone): complete OpenYield v0.1 (pre-MVP foundation)
---ci---
project: oy
phase: 0
milestone: v0.1
status: complete
requirements:
  covered: [REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-012, REQ-013, REQ-014, REQ-019]
  partial: [REQ-001, REQ-008, REQ-010]
  pending: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021]
---/ci---

Milestone v0.1 complete. Pre-MVP foundation skeleton.
10 patches (v0.0.0 - v0.0.9). 48 tests. 11 modules.
No MVP release tag (v0.1.0 reserved for MVP).
Next milestone: v0.2 (The Mesh).
2026-08-17 20:38:03 +00:00
CIAgent db61fb1f6c docs(milestone): complete OpenYield v0.1
---ci---
project: oy
phase: 9
milestone: v0.1
status: complete
requirements:
  covered: [REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-012, REQ-013, REQ-014, REQ-019]
  partial: [REQ-001, REQ-008, REQ-010]
  pending: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021]
---/ci---

Milestone v0.1 complete. Pre-MVP foundation skeleton.
10 patches (v0.0.0 - v0.0.9). 48 tests. 11 modules.
Next milestone: v0.2 (The Mesh).
2026-08-17 20:02:22 +00:00
CIAgent 82245f98f7 ship(P09): merge final review into milestone v0.1
---ci---
project: oy
phase: 9
milestone: v0.1
status: complete
ship: v0.0.9
requirements:
  covered: [REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-012, REQ-013, REQ-014, REQ-019]
  partial: [REQ-001, REQ-008, REQ-010]
  pending: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021]
---/ci---

Milestone v0.1 complete (pre-MVP development).
48 tests passing. 11 modules. Lexicon compliant.
Next milestone: v0.2 (The Mesh — Window, Pacts, Orgs, Partners, Governance).
2026-08-17 20:02:07 +00:00
CIAgent e6a7634262 review(P09): final review + audit + lexicon fix
---ci---
project: oy
phase: 9
milestone: v0.1
status: execute
---/ci---

Final review across all phases:
- 48 unit tests passing across 11 modules
- go vet PASS, go build PASS
- Lexicon fix: 'yield' -> 'production' in bloom module (§3 compliance)
- Full lexicon check: CLEAN (no bank/deposit/interest/yield/currency/dollar/euro/account/savings/depositor)
- 10 REQs complete, 2 skeleton, 9 pending (future milestones)
- All locked constants verified: §7 (6-of-9, 100k bond), §18 (0.1pct/0.01pct/1 Grain), §9.1 (90d/4.5/3cats), §9.2 (Bayesian formula), §15 (FCFS)
- Branch hygiene: all phase branches deleted, tags v0.0.0-v0.0.8 created
- Milestone v0.1 ready for ship
2026-08-17 20:02:01 +00:00
CIAgent 11b1585913 ship(P08): merge Mesh Experience into milestone v0.1
---ci---
project: oy
phase: 8
milestone: v0.1
status: complete
ship: v0.0.8
---/ci---
2026-08-17 20:01:14 +00:00
CIAgent b6f041b5af verify(P08): all tests pass
---ci---
project: oy
phase: 8
milestone: v0.1
status: verify
---/ci---
2026-08-17 20:01:14 +00:00
CIAgent c0bd9eedf5 feat(P08): Mesh Experience — Maps, Pay, Four Faces
---ci---
project: oy
phase: 8
milestone: v0.1
status: execute
---/ci---

Phase 8 (Component 9):
- x/mesh: Four Faces (Reach, Standing, Maps, Pay), MapEntry, PayAct, Exit Layer
- Maya's Day placeholder (deferred to component doc, Q1)
- 4 tests passing
2026-08-17 20:01:14 +00:00
48 changed files with 8592 additions and 349 deletions
+6 -3
View File
@@ -1,8 +1,11 @@
{
"phase": 2,
"phase": 4,
"stage": "execute",
"milestone": "v0.1",
"milestone": "v0.2",
"milestone_type": "feature",
"tag_base": "v0.1.x",
"phase_role": "execution",
"project": "oy",
"attempts": 0,
"updated_at": "2026-08-17T00:00:00Z"
"updated_at": "2026-08-17T21:50:00Z"
}
+29 -1
View File
@@ -6,6 +6,9 @@
}
],
"active_project": "oy",
"milestone": "v0.2",
"milestone_type": "feature",
"tag_base": "v0.1.x",
"autonomy": {
"level": "full",
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
@@ -17,6 +20,31 @@
"forge": "gitea",
"base_url": "git.cloudinit.dev",
"owner": "oy",
"repo": "openyield"
"repo": "openyield",
"remote": "origin",
"bootstrapped": true
},
"secrets": {
"scopes": {
"gitea": ["GITEA_TOKEN"]
},
"env_file": ".ciagent/.env.secrets"
},
"ship": {
"per_phase": true,
"allow_skip": false,
"release_blocking": false,
"max_release_retries": 3
},
"personas": {
"enabled": true,
"territory_enforcement": "warn"
},
"sessions": {
"session_isolation": "branch"
},
"parallelization": {
"enabled": false,
"max_concurrent_agents": 1
}
}
+184
View File
@@ -0,0 +1,184 @@
# Grill Review: OpenYield (oy) — v0.2 (The Mesh) Phase 0
> **Reviewer**: CIAgent adversarial grill (red-team, full autonomy)
> **Date**: 2026-08-17
> **Target**: Phase 0 artifacts (PROJECT.md, REQUIREMENTS.md, ARCHITECTURE.md, RESEARCH.md, PERSONAS.md, PLANS.md, config.json) + v0.1 codebase baseline
> **Milestone**: v0.2 — The Mesh
> **Autonomy**: full (decision_confidence_threshold = 0.60)
> **Mode**: multi-project (slug `oy`)
---
## Methodology
Each of nine axes was scored by gathering concrete evidence (commit hash, file:line, REQ-ID, D-decision, A-assumption). Missing or contradictory evidence is itself a finding. Forcing questions were answered from the artifacts; where confidence ≥ 0.60 a binding verdict was reached. No escalations were required — all axes resolved at ≥ 0.60 confidence after binding fixes are applied.
### Evidence baseline (verified against the actual repo, not the docs)
- v0.1 codebase: **15 modules** under `x/` (confirmed via glob `x/**/types/types.go`).
- `go.mod`: `module github.com/oy/openyield`, `go 1.22`, **zero dependencies** (confirmed).
- Test functions: **53** (counted via `grep -rh "^func Test" x/`). PROJECT.md D-033 says "48-test baseline"; RESEARCH.md §2 says "53-test baseline pattern". **Inconsistent — both refer to v0.1.**
- Test files: **11** `*_test.go` (not 15 — modules `identity`, `processing`, `rootpool`, `vault` have **no test files**).
- **Zero lexicon assertion tests** exist in v0.1 (`grep -rli lexicon x/` returns empty). D-032 claims lexicon is "carried from v0.1" — but v0.1 never had a lexicon test. The claim is aspirational, not historical.
- **Zero `genesis.go` files** exist; all `ValidateGenesis` are no-ops in `types.go` (e.g. `feecovenant/types/types.go:122` returns `nil` unconditionally).
- **No project-wide lexicon meta-test** exists.
- `x/bearers/types/types_test.go` already imports `x/processing/types` cross-module (line 7) — an existing precedent for cross-module test imports, but it is a *test* import, not a production import cycle.
- Current lexicon cleanliness: `grep -rni "bank\|account" x/ --include="*.go"` returns **zero hits** — v0.1 is lexicon-clean in practice, but the firewall is nonexistent.
These baseline facts drive several binding decisions below. The plan's narrative ("matching v0.1's 48-test baseline", "lexicon carried from v0.1") is **partly fictional** — v0.1 has no lexicon test and the test count is misstated in two places. This does not block v0.2; it means the lexicon firewall is *new* work in v0.2, not inherited work.
---
## Per-Axis Verdicts
### Axis 1 — Feasibility (skeleton + tests, zero deps) — **PASS** (confidence 0.85)
The v0.2 scope (Go types + keeper stubs + unit tests, no live chain) is genuinely achievable with zero external deps. v0.1 proved the pattern: 15 modules compile under `go 1.22` with only `encoding/json`. RESEARCH.md §4 is explicit and correct that pulling `cosmos-sdk`/`ibc-go` would force a 1000+ module tree and break the fast test loop. The ICS-20 packet shape is self-contained Go structs (A-215), the Forex oracle is a Go interface (no impl), and bearers are pure types (no LoRa/BLE libs). No axis-1 risk reaches the escalation threshold.
The one caveat: the Window "fuller" implementation (D-023) is the most ambitious skeleton and rate-limit semantics are unstated (A-206, conf 0.65). But the plan correctly scopes the skeleton to a simple counter (`actionsConsumed` vs `maxActions`), which is trivially feasible. Confidence holds.
### Axis 2 — Scope (over/under, D-020..D-033 defensibility) — **CONDITIONAL** (confidence 0.72)
D-020..D-033 are individually defensible and well-reasoned. The skeleton-first choice (D-020, conf 0.85), one-chain IBC representative (D-021), Mission Lock as const (D-022), Window-fuller (D-023), and the deferred items (Cover seniority Q7, Pier credential routing Q5, Solana IBC) all correctly defer operational/Phase-3 concerns. The scope is *not* over-scoped for a skeleton milestone.
However: (a) D-033's claim of "matching v0.1's 48-test baseline" is factually wrong — v0.1 has 53 tests and no lexicon tests (see baseline). (b) D-032's claim that lexicon is "carried from v0.1" is false — v0.1 has no lexicon test. These are documentation defects, not scope defects, but they mislead future phases about what is inherited vs. new. **Binding fix G-002** corrects the narrative; the scope itself is sound.
### Axis 3 — Cost / Effort (30 tasks / 5 phases) — **PASS** (confidence 0.80)
30 tasks across 5 phases for 10 new/extended packages is proportionate. Each package gets a types task + a test task + (for 3 of them) a genesis task, plus 5 cross-cutting lead-developer ship tasks. No phase exceeds 8 tasks; the largest (P4) has 8 tasks for 3 packages which is reasonable. There is **no bloat**: there are no "research", "spike", or "documentation" tasks masquerading as execution; every task produces a concrete `.go` file or a verifiable gate. The 5 lead-developer verification tasks (one per phase) are the correct overhead for vertical-slice shippability. Reject the "too many tasks" hypothesis.
Minor: P5 has 4 tasks for pure audit/ship, which is light but appropriate (no new code). No binding change.
### Axis 4 — Architecture (module layout, blocker chain, import cycles) — **PASS** (confidence 0.82)
The 9-new + 1-extended layout maps cleanly to the blocker chain in ARCHITECTURE.md §5.2. D-031's phase ordering (P1 Orgs+Window → P2 Pacts+Partners → P3 Councils+Forex → P4 Bonds+Bearers+L2 → P5 ship) respects the dependency graph: Window is a leaf (P1) so P2/P3/P4 reference it by `WindowRef` id-string; Stand (P1) blocks Pact StandRegistry (P2), Council Stand-Council (P3), and Bond issuer (P4); Guild (P1) blocks Council Guild-Council (P3). A-203 (by-ID-string refs) is the correct cycle-avoidance strategy and is consistently applied in RESEARCH.md §3's interface diagram.
One concern: RESEARCH.md §3 says `x/window` "may be imported by Pacts/Partners/Orgs if needed (but skeleton keeps it by-ref too)" — this hedge could permit a future struct import. The plan's task specs (P2-01-01, P2-02-01) correctly use `window-id-ref` strings, so the skeleton is safe. **Binding fix G-003** makes the by-ref rule a hard invariant test so the hedge cannot silently become an import cycle.
### Axis 5 — Risk (15 assumptions, hidden traps) — **CONDITIONAL** (confidence 0.70)
The 15 assumptions (A-201..A-215) are mostly sound and well-scored. The highest-confidence (A-201 dep-free 0.95, A-202 one-module-per-component 0.90, A-209 bearers-extend 0.90, A-215 no-deps-vendored 0.90, A-210 lexicon-coupon 1.00) are correct. The lower-confidence assumptions are correctly *flagged* for v0.3 revisit (A-206 rate-limit 0.65, A-213 Shadow Stand 0.60, A-214 spread cap 0.70).
Three hidden traps:
1. **Lexicon firewall is new, not inherited** (see baseline). The plan assumes v0.1 had a lexicon test; it did not. The P5 meta-test (P5-01-01) is therefore load-bearing for the *entire* project's lexicon integrity, not just v0.2. This raises its criticality — **binding fix G-004** moves the lexicon meta-test earlier (into P1) as a scaffolding test that fails-empty initially, so drift is caught from the first phase, not at P5 when 10 packages already exist.
2. **`x/bearers/types/types_test.go` already cross-imports `x/processing/types`** (line 7). This is a *test* import, not a production import cycle, but it means the bearers test package is already coupled. When P4 extends `x/bearers`, the existing test must stay green (P4-02-02 acknowledges this). No binding change — the plan already flags the regression risk — but it is a latent coupling the grill records.
3. **A-207 (one `x/pact` module with PactType enum, conf 0.80) contradicts D-027's wording** ("separate keeper modules"). RESEARCH.md §1.7 explicitly reconciles this ("one module with a PactType enum satisfies 'separate execute entries' without 6 dirs"), but the contradiction between the D-decision text and the chosen implementation is a documentation smell. **Binding fix G-005** clarifies D-027's intent. Confidence holds; no escalation.
### Axis 6 — Requirement Coverage (all v0.2 REQs, gaps, double-counting) — **PASS** (confidence 0.84)
Cross-referencing RESEARCH.md §6 (Cross-Reference Summary) against PLANS.md per-phase REQ coverage and REQUIREMENTS.md:
| REQ | Component | Module | Phase | Depth | Covered? |
|-----|-----------|--------|-------|-------|----------|
| REQ-009 | L2 IBC | x/satellite | P4 | Skeleton (Polygon rep) D-021 | ✅ P4-03-01/02 |
| REQ-011 | Councils | x/council | P3 | Skeleton + Mission Lock D-022 | ✅ P3-01-01/02/03 |
| REQ-015 | Window | x/window | P1 | Fuller D-023 | ✅ P1-01-01/02/03 |
| REQ-016 | 9 Stands | x/stand | P1 | Skeleton D-024 | ✅ P1-02-01/02/03 |
| REQ-017 | Guilds | x/guild | P1 | Skeleton D-025 | ✅ P1-03-01/02 |
| REQ-018 | Partners | x/partner | P2 | Skeleton D-026 | ✅ P2-02-01/02 |
| REQ-020 | 6 Pacts | x/pact | P2 | Skeleton D-027 | ✅ P2-01-01/02 |
| REQ-021 | Bonds | x/bond | P4 | Skeleton D-028 | ✅ P4-01-01/02/03 |
| Bearers | OY-LR+Beacon | x/bearers (ext) | P4 | Stubs D-029 | ✅ P4-02-01/02 |
| Forex | Forex v1 | x/forex | P3 | Stub D-030 | ✅ P3-02-01/02 |
| REQ-012 | Lexicon | (all) | all | D-032 | ✅ per-module + P5 meta |
Every v0.2 REQ maps to at least one types task + one test task. No REQ is double-counted across phases. REQ-012 (lexicon) is correctly cross-cutting. No gaps. The coverage is complete and non-overlapping. The only caveat (G-004) is *when* the lexicon meta-test lands, not *whether* it covers REQ-012.
### Axis 7 — Persona Assignment (territory globs, conflicts) — **CONDITIONAL** (confidence 0.74)
Persona→task assignments are coherent with territory globs:
- backend-engineer (`x/**/types/**`, `x/**/keeper/**`, `x/**/module.go`, `go.mod`) → P2-01-01, P2-02-01, P4-01-01 (pact/partner/bond types). ✅
- cosmos-engineer (`x/{satellite,council,window,stand,guild,partner,pact,bond,forex,bearers}/**`, `go.mod`) → P1-01-01, P1-02-01, P1-03-01, P3-01-01, P3-02-01, P4-02-01, P4-03-01. ✅
- data-engineer (`x/**/types/genesis*.go`, `x/**/genesis*.go`) → P1-01-03, P1-02-03, P3-01-03, P4-01-03. ✅
- security-engineer (`x/**/types/**_test.go`, `x/**/keeper/**_test.go`, `x/**/genesis_test.go`) → all `*_test.go` tasks. ✅
- lead-developer (`**`) → all wave-3 ship tasks. ✅
**Territory conflicts (warn mode, non-blocking but recorded):**
1. `go.mod` is claimed by **both backend-engineer and cosmos-engineer** (and referenced in 5 persona entries). Since v0.2 adds **zero** deps (A-201), `go.mod` should not change in v0.2. **Binding fix G-006**: no persona touches `go.mod` in v0.2; if a dep is needed, it is an escalation (would violate A-201).
2. `x/partner/**`, `x/pact/**`, `x/bond/**` are claimed by **both cosmos-engineer and backend-engineer** (cosmos-engineer's explicit list; backend-engineer's `x/**/types/**` glob). The plan assigns P2-01-01 (pact) and P2-02-01 (partner) and P4-01-01 (bond) to **backend-engineer**, but cosmos-engineer's territory list includes these same paths. This is a genuine overlap. Since territory enforcement is `warn` (config.json), it does not block, but it is ambiguous ownership. **Binding fix G-007** clarifies: cosmos-engineer owns the *Cosmos-convention-shaped* modules (window, stand, guild, council, satellite, forex, bearers-ext); backend-engineer owns the *non-Cosmos-mirroring* modules (pact, partner, bond). This matches the actual task assignments and removes the glob overlap.
3. security-engineer's `x/**/genesis_test.go` territory overlaps data-engineer's `x/**/genesis*.go` for the test file. The plan assigns genesis *test* logic to data-engineer's genesis tasks (e.g. P1-01-03 says "audit-log ordering test green"). Ambiguous who writes `genesis_test.go`. **Binding fix G-008**: genesis *schema* (genesis.go) = data-engineer; genesis *test assertions* (`*_test.go`) = security-engineer. The plan's task verbiage already implies this; G-008 makes it explicit.
No conflict is severe enough to block at warn mode. Confidence holds after fixes.
### Axis 8 — Testability (must-haves verifiable, invariants concrete) — **PASS** (confidence 0.83)
Every must-have in the plan has a concrete verification:
- Locked-const tests are concrete: 9 Stands (P1-02-02), 4 Partner tiers (P2-02-02), 6 Pacts (P2-01-02), 3 Councils (P3-01-02), Mission Lock `== false` (P3-01-02), Bond cap 800 / floor 0 + clamp invariant (P4-01-02), Guild fee 0 (P1-03-02), 5 L2 chains (P4-03-02), ICS-20 packet field names (P4-03-02). Each is a single-const-change-breaks-a-test firewall. ✅
- Invariant tests are concrete: Window lifecycle Open→Active→Revoked→Expired, revoke-after-expire no-op, double-revoke idempotent-or-error (P1-01-02); bond clamp below→floor / above→cap / in-range→unchanged (P4-01-02); Mission Lock non-amendable (P3-01-02). ✅
- Lexicon assertions: per-module (10 files) + P5 meta-test. ✅ (with G-004 moving meta-test earlier)
- Coverage: ≥80% per package, table-driven on every exported const/struct/method (D-033). Achievable for low-cyclomatic-complexity skeletons. ✅
One gap: P5-01-01's lexicon meta-test verification says "a deliberately-injected banned term in any new file fails the meta-test (verified by a temporary spike)". A "temporary spike" is not a durable test. **Binding fix G-009**: the meta-test must include a *self-test* — a table of synthetic strings (one per banned term) asserted to be detected, so the meta-test is durably verified without manual spikes. Confidence holds.
### Axis 9 — Phase Ordering & Shippability (patches, v0.1.5 milestone) — **PASS** (confidence 0.81)
Each phase is independently shippable as a patch on the `v0.1.x` line:
- P1 → `v0.1.1` (window, stand, guild): ships 3 foundation modules; `go test ./...` green; no upstream blocker. ✅
- P2 → `v0.1.2` (pact, partner): ships 2 modules referencing P1 by-id-string; blocked-by P1 types only. ✅
- P3 → `v0.1.3` (council, forex): ships 2 modules; council blocked-by P1 stand+guild (by-id); forex independent. ✅
- P4 → `v0.1.4` (bond, bearers-ext, satellite): ships 3 packages; bond blocked-by P1 stand; bearers/satellite independent; v0.1 baseline regression-gated. ✅
- P5 → `v0.1.5` (audit + ship): final phase patch IS the milestone release per D-008/D-020. ROADMAP.md Phase 2 checkbox updated. ✅
The tag arithmetic is correct: P0 → `v0.1.0` (already shipped per ROADMAP "MVP deferred"), P1..P4 → `v0.1.1..v0.1.4`, P5 → `v0.1.5` = v0.2 milestone. No separate minor tag (D-008). The blocker chain is honored: all cross-phase hard blockers (P1-01-01, P1-02-01, P1-03-01) are in P1 Wave 1, so P2/P3/P4 Wave 1 can start once P1 types land (the plan's "soft ordering for branch hygiene" note is correct — types only depend on listed hard blockers).
One observation: ROADMAP.md still shows v0.1 phases as `v0.0.0..v0.0.9` (lines 4-13) and says "MVP release (v0.1.0) deferred". PROJECT.md says v0.1 was "released as v0.0.9 per run.md patch-line model" (line 90). The v0.1.x tag base for v0.2 (config.json `tag_base: v0.1.x`, PLANS.md line 7) is internally consistent but the ROADMAP's v0.0.x history is a separate patch line. This is a documentation reconciliation item, not a blocker. **Binding fix G-010**: P5-01-03 (REQ-coverage audit) must also reconcile ROADMAP.md's v0.0.x → v0.1.x tag-line note so the milestone release (`v0.1.5`) is not confused with the v0.0.x pre-MVP line.
---
## Binding Decisions
These are **binding** — the orchestrator MUST apply them before P1 begins. Numbered G-001..G-010.
| ID | Decision | Rationale | Confidence | Affects |
|----|----------|-----------|------------|---------|
| **G-001** | Correct the v0.1 baseline test-count claim. PROJECT.md D-033 says "48-test baseline"; RESEARCH.md §2 says "53-test baseline pattern". Actual count is **53** `func Test*` across 11 files. Standardize on "53 tests across 11 test files (v0.1 baseline)". | Both docs cite v0.1 test counts and disagree with each other; the actual count (verified `grep -rh "^func Test" x/ \| wc -l` = 53) is the source of truth. Future phases must not cite a phantom 48. | 0.95 | PROJECT.md D-033, RESEARCH.md §2 |
| **G-002** | Correct the lexicon-inheritance narrative. D-032 says lexicon is "carried from v0.1"; D-033 says "matching v0.1's 48-test baseline" re lexicon. **v0.1 has zero lexicon tests** (verified: `grep -rli lexicon x/` empty). State explicitly: "Lexicon assertion tests are NEW in v0.2; v0.1 is lexicon-clean in practice but has no test firewall. v0.2 introduces the firewall." | The current narrative implies inherited work that does not exist, understating v0.2's new-work scope and the criticality of the P5 meta-test. | 0.92 | PROJECT.md D-032/D-033, RESEARCH.md §2 |
| **G-003** | Add a hard invariant test in P1-01-02 (Window test file) asserting that no v0.2 production (non-test) `.go` file under `x/` imports another `x/<module>/types` package by struct — i.e. enforce A-203 (by-ID-string refs) as a *tested* invariant, not just a convention. Use `go/ast` or `go/parser` to scan imports, OR a simpler package-import grep test. | RESEARCH.md §3 hedges that Window "may be imported by Pacts/Partners/Orgs if needed". A hedge in a design doc can silently become an import cycle. Make the rule a test that fails on violation. | 0.78 | P1-01-02, RESEARCH.md §3 |
| **G-004** | Introduce the project-wide lexicon meta-test scaffolding in **P1 Wave 3** (new task P1-04-02, lead-developer), not P5. The P1 version scans `x/**/*.go` for the 9 banned terms and passes (v0.1 is clean). It fails-empty initially for the 3 new P1 packages only if drift occurs. P5-01-01 then *extends* the already-existing meta-test to cover all 10 packages rather than creating it from scratch at audit time. | Per G-002, the lexicon firewall is new in v0.2. Creating it only at P5 means drift in P1-P4 is undetected until audit, when 10 packages already exist and remediation is expensive. A P1 scaffolding meta-test catches drift from the first phase. This is the single most material fix. | 0.88 | PLANS.md P1, P5-01-01 |
| **G-005** | Clarify D-027 wording: "six Pacts as **one `x/pact` module** with a `PactType` enum + six per-type execute-entry structs (A-207), NOT six micro-modules." Update the D-027 decision text in PROJECT.md to remove the "separate keeper modules" ambiguity that contradicts A-207. | D-027 says "separate keeper modules"; A-207 and RESEARCH §1.7 say one module. The contradiction is a documentation smell that could cause a future agent to spawn 6 dirs. | 0.85 | PROJECT.md D-027 |
| **G-006** | No persona may modify `go.mod` in v0.2. v0.2 adds zero external dependencies (A-201, conf 0.95). Any `go.mod` change in P1-P5 is an immediate **escalation** (would violate D-020/A-201). Remove `go.mod` from active persona territories for v0.2, or add a constraint: "go.mod is read-only in v0.2; any change triggers escalation." | `go.mod` is claimed by both backend-engineer and cosmos-engineer (5 mentions). Since it must not change, the overlap is moot but ambiguous. Making it explicitly read-only removes the ambiguity and protects the zero-dep invariant. | 0.90 | PERSONAS.md, config.json |
| **G-007** | Disambiguate `x/pact`, `x/partner`, `x/bond` ownership. **backend-engineer** owns these three (non-Cosmos-mirroring modules: pact = custom commitment types, partner = custom registry, bond = custom clamp). **cosmos-engineer** owns `x/window`, `x/stand`, `x/guild`, `x/council`, `x/satellite`, `x/forex`, `x/bearers` (Cosmos-convention-mirroring: x/authz, x/group, x/gov, x/ibc-transfer, x/capability shapes). Update cosmos-engineer's territory list in PERSONAS.md to remove `x/pact`, `x/partner`, `x/bond`. | Both personas' territory globs overlap on these three paths. The actual PLANS.md task assignments (P2-01-01, P2-02-01, P4-01-01 → backend-engineer) already resolve it operationally, but the globs in PERSONAS.md still conflict. Align the globs with the assignments. | 0.82 | PERSONAS.md |
| **G-008** | Genesis file ownership split: **data-engineer** owns `genesis.go` / genesis *schema* (the `GenesisState` struct, `ValidateGenesis` body). **security-engineer** owns `*_test.go` including genesis *test assertions* (`genesis_test.go` if separate, or the genesis assertions within `types_test.go`). Update PERSONAS.md so security-engineer's territory explicitly includes `x/**/genesis_test.go` and data-engineer's explicitly excludes `*_test.go`. | data-engineer's `x/**/genesis*.go` glob currently matches `genesis_test.go`, overlapping security-engineer's `x/**/genesis_test.go`. Plan tasks (e.g. P1-01-03) say "test green" but the test-author persona is security-engineer. Clarify schema vs test ownership. | 0.80 | PERSONAS.md, PLANS.md genesis tasks |
| **G-009** | P5-01-01 lexicon meta-test must include a **self-test table**: 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 the meta-test's detection. This durably verifies the meta-test detects all 9 banned terms without a manual "temporary spike". | P5-01-01's verification relies on "a temporary spike" to confirm detection — a manual step that won't be repeated. A self-test table makes the meta-test's coverage itself tested. | 0.86 | PLANS.md P5-01-01 |
| **G-010** | P5-01-03 (REQ-coverage audit) must additionally reconcile the ROADMAP.md tag-line narrative: add a one-line note that v0.1 pre-MVP shipped on the `v0.0.x` line (ROADMAP lines 4-13) and v0.2 ships on the `v0.1.x` line (config `tag_base`), so the `v0.1.5` milestone release is not confused with a v0.1.0 "MVP" tag. | ROADMAP.md says "MVP release (v0.1.0) deferred" while v0.2's P0 is `v0.1.0` per PLANS.md. The two `v0.1.0` references mean different things. A reconciliation note prevents a tag collision or confusion at ship time. | 0.75 | PLANS.md P5-01-03, ROADMAP.md |
---
## Escalations
**None.** All nine axes resolved at confidence ≥ 0.60 after the binding fixes G-001..G-010 are applied. No axis required escalation to the human. At full autonomy, the orchestrator applies the binding decisions and proceeds to P1.
---
## Overall Verdict
### **SHIP Phase 0 with binding changes**
The v0.2 Phase 0 plan is fundamentally sound: the skeleton+tests scope is feasible, zero-dep, well-decomposed, REQ-complete, and properly ordered along the blocker chain. The plan is **not** over-scoped, **not** unfeasible, and **not** token-bloated — 30 tasks for 10 packages is proportionate.
The binding changes are **documentation/test-firewall corrections**, not scope rework. The most material is **G-004** (move the lexicon meta-test scaffolding from P5 to P1): the plan's single biggest hidden risk is that it treats the lexicon firewall as inherited from v0.1 when it is in fact new work, and creates it only at the final audit phase. Catching drift from P1 is cheap; catching it at P5 after 10 packages exist is expensive. G-001/G-002 correct the factual errors in the baseline narrative. G-003/G-005/G-006/G-007/G-008/G-009/G-010 close smaller ambiguities (import-cycle invariant, D-027 wording, go.mod read-only, persona territory overlaps, meta-test self-coverage, tag-line reconciliation).
None of these rise to "rethink" — the architecture, scope, and ordering are correct. Apply the 10 binding decisions and proceed to Phase P1.
**Confidence in overall verdict: 0.83**
---
## Summary Block
```
Per-axis verdicts:
1. Feasibility — PASS (0.85)
2. Scope — CONDITIONAL (0.72) → fixed by G-001, G-002
3. Cost/Effort — PASS (0.80)
4. Architecture — PASS (0.82) → strengthened by G-003
5. Risk — CONDITIONAL (0.70) → fixed by G-004, G-005
6. Req Coverage — PASS (0.84)
7. Persona Assignment — CONDITIONAL (0.74) → fixed by G-006, G-007, G-008
8. Testability — PASS (0.83) → strengthened by G-009
9. Phase/Ship — PASS (0.81) → strengthened by G-010
Binding decisions: 10 (G-001..G-010)
Escalations: 0
Overall: SHIP Phase 0 with binding changes (confidence 0.83)
```
+71
View File
@@ -0,0 +1,71 @@
# P1 — Orgs + Window Foundation — Ship Verification
Phase 1 of v0.2 (The Mesh). Branch: `oy/phase/01-orgs-window-foundation`.
This file is the lead-developer's P1-04-01 ship-verification report. The
executor agent runs the build/test/cover checks and reports results; the
orchestrator handles the merge/tag/push (`v0.1.1`).
## Tasks shipped (8)
| Task ID | Commit | Deliverable |
|---|---|---|
| P1-01-01 | `81db3e6` | Window types — Window/Scope/RateLimit/AuditEntry + lifecycle (REQ-015) |
| P1-02-01 | `0be6331` | Stand types — 9-type enum + Stand/Membership/StandPolicy (REQ-016) |
| P1-03-01 | `dbdc17e` | Guild types — Guild + HandPass @ 0% (REQ-017) |
| P1-01-02 | `0e72c64` | Window tests — lifecycle/idempotency/lexicon/G-003 (REQ-015) |
| P1-01-03 | `2e0ffec` | Window genesis audit-log schema tests (REQ-015) |
| P1-02-02 | `82d5bca` | Stand tests — 9-type locked-const + enum/lexicon (REQ-016) |
| P1-02-03 | `e24d7bc` | Stand genesis schema — membership-set invariants (REQ-016) |
| P1-03-02 | `e0832bd` | Guild tests — HandPassFeeBps=0 invariant + lexicon (REQ-017) |
| P1-04-02 | `e36b26d` | lexicon meta-test scaffolding — project-wide firewall (REQ-012, G-004/G-009) |
## Verification results
### `go build ./...`
GREEN. All 19 packages (15 v0.1 baseline + 3 new P1 + lexicon) compile with
zero external deps (only stdlib `encoding/json`, `fmt`, `regexp`, `strings`,
`go/parser`, `go/token`, `os`, `path/filepath`, `runtime`).
### `go test ./...`
GREEN. 143 tests across the repo; v0.1 baseline (53 tests) unchanged — no
regression. New: window (41 tests), stand (28), guild (17), lexicon meta (4).
### Coverage (`go test -cover`)
| Package | Coverage | Target |
|---|---|---|
| `x/window/types` | 100.0% | ≥80% |
| `x/stand/types` | 100.0% | ≥80% |
| `x/guild/types` | 100.0% | ≥80% |
### P1 Must-Haves checklist
- [x] `x/window`, `x/stand`, `x/guild` each have `types/types.go` + `types_test.go` (v0.1 pattern, package `types`, zero external deps).
- [x] `go build ./...` and `go test ./...` green across the whole repo.
- [x] ≥80% coverage on `x/window/types`, `x/stand/types`, `x/guild/types` (all 100%).
- [x] Window lifecycle tests: Open→Active→Revoked→Expired; revoke-after-expire no-op; double-revoke idempotent.
- [x] Stand locked-const: exactly 9 types with vision §11 names.
- [x] Guild `HandPassFeeBps == 0` invariant test.
- [x] Lexicon assertion in all 3 new test files.
- [x] `ValidateGenesis` performs ID-uniqueness checks (A-212 upgrade from v0.1 no-op).
- [x] Project-wide lexicon meta-test (G-004) scans all `x/**/*.go`; self-test table (G-009) detects all 10 banned terms.
- [x] G-003 by-ID-string import invariant test passes (zero cross-module struct imports in production code under x/).
- [ ] Git tag `v0.1.1` — NOT created by executor; orchestrator ships the phase.
## Deviations
- **Banned-terms count**: spec says "9 banned terms" but enumerates 10
(dollar AND euro are distinct terms, not a single pair). Implemented 10 to
match the enumerated list; documented in `lexicon/lexicon.go` and the
meta-test. The firewall scope is the enumerated list, not the count label.
- **genesis.go placement**: P1-01-03's `genesis.go` (ValidateAuditLogs) was
authored in P1-01-01 so `types.go` compiles (types.go references
ValidateAuditLogs). P1-01-03 adds `genesis_test.go` (the security-engineer's
assertions, G-008 split). Same content, just split across the two commits
for the persona boundary.
- **Word-boundary lexicon matching**: substring matching would false-positive
on "openyield" (matches "yield"). Implemented word-boundary regex matching
in `lexicon.FindBannedTerm`; documented and tested with a
no-false-positive test.
## Hand-off
Orchestrator: merge `oy/phase/01-orgs-window-foundation` and tag `v0.1.1`.
Executor did not merge/tag/push per instructions.
+124
View File
@@ -0,0 +1,124 @@
# P2 Ship Verification — v0.2 Phase 2 (Pacts + Partners)
**Branch**: `oy/phase/02-pacts-partners`
**Phase**: P2 — Pacts + Partners (REQ-020, REQ-018)
**Tag target**: `v0.1.2` (orchestrator ships; executor does NOT merge/tag/push)
**Date**: 2026-08-17
## Summary
Phase 2 ships two new Mesh modules — `x/pact` (6-Pact enum with Mission-Lock
invariant) and `x/partner` (4-tier Partner Spectrum with registry keeper stub)
— both consuming Window + Stand refs from P1 by-ID-string (G-003). All five
P2 tasks executed atomically with per-task commits. Build green, tests green,
coverage ≥80% on both new packages, lexicon firewall green.
## Must-Haves (from PLANS.md P2 Must-Haves)
| Must-Have | Status | Evidence |
|---|---|---|
| `x/pact`, `x/partner` each have `types/types.go` + `types/types_test.go` | ✅ | 4 files created (pact: types.go+types_test.go+genesis.go; partner: types.go+types_test.go) |
| `go build ./...` and `go test ./...` green | ✅ | `go build ./...` → build OK; `go test ./...` → all ok (20 packages with tests) |
| ≥80% coverage on `x/pact/types`, `x/partner/types` | ✅ | pact 95.9%, partner 100.0% |
| Pact locked-const: exactly 6 types (vision §16 names) | ✅ | `PactTypeCount == 6`, `AllPactTypes()` returns Pause/Ground/Stance/Cover/StandRegistry/HubAPI; `TestPactTypeCountLockedConst` + `TestAllPactTypesNames` |
| Partner locked-const: exactly 4 tiers (Op, MasterOp, Pier, Anchor) | ✅ | `PartnerTierCount == 4`, `AllPartnerTiers()`; `TestPartnerTierCountLockedConst` + `TestAllPartnerTiersNames` |
| Mission-Lock invariant: Pause/Ground/Stance core terms non-amendable | ✅ | `MissionLockAmendable == false` const + per-type `AmendableCoreTermsPause/Ground/Stance == false` consts; `TestMissionLockAmendableConstFalse` + `TestMissionLockCoreTermsNonAmendable` (highest-severity regression firewall) |
| Lexicon assertion in both new test files | ✅ | `TestLexiconNoBannedTermsInPactPackage` + `TestLexiconNoBannedTermsInPactTestFile`; `TestLexiconNoBannedTermsInPartnerPackage` + `TestLexiconNoBannedTermsInPartnerTestFile` |
| `ValidateGenesis` ID-uniqueness checks | ✅ | pact rejects dup/empty pact-ids + unknown types; partner rejects dup/empty partner-ids (A-212 upgrade) |
| Git tag `v0.1.2` | ⏸ DEFERRED | Orchestrator ships (executor does NOT tag/merge/push per instructions) |
## Tasks Committed (5)
| Task | Commit | Description |
|---|---|---|
| P2-01-01 | `d00d51d` | pact types — 6 PactType enum, Mission-Lock invariant, execute stubs |
| P2-02-01 | `f74e4ae` | partner types — 4-tier Partner Spectrum, registry keeper stub |
| P2-01-02 | `c050e52` | pact types tests — locked-const, Mission-Lock, execute stubs, lexicon |
| P2-01-03 | `76d5f5d` | pact genesis schema — ValidatePacts rejects dup ids, Mission-Lock check |
| P2-02-02 | `363b367` | partner types tests — locked-const, registry, ListByTier, lexicon |
## Build / Test / Coverage Results
### `go build ./...`
```
build OK
```
### `go test ./... -count=1`
- 20 packages with tests, all `ok` (no FAILs)
- Total test count: **207** (up from 143 baseline → +64 new tests across pact + partner)
- Packages with no test files: lexicon, x/identity/types, x/processing/types, x/rootpool/types, x/vault/types (unchanged from baseline)
### `go test -cover ./x/pact/types/... ./x/partner/types/...`
| Package | Coverage | Target | Pass |
|---|---|---|---|
| `x/pact/types` | **95.9%** | ≥80% | ✅ |
| `x/partner/types` | **100.0%** | ≥80% | ✅ |
### Lexicon meta-test (`go test -run TestLexiconMeta .`)
- `TestLexiconMetaNoBannedTermsInX` — PASS (scans all `x/**/*.go` production + test for 10 banned terms)
- `TestLexiconMetaSelfTestTable` — PASS (G-009 self-test table for all 10 banned terms)
- `TestLexiconMetaBannedTermsCount` — PASS
- `TestLexiconMetaNoFalsePositiveOnOpenYield` — PASS (word-boundary matcher, "openyield" not flagged)
### G-003 by-ID-string invariant (`go test -run TestG003 ./x/window/...`)
- `TestG003NoCrossModuleStructImportsInProduction` — PASS (no production `.go` file under `x/` imports a foreign `x/<module>/types` package; pact + partner conform — refs are by-ID-string)
## Module Details
### x/pact (REQ-020, A-207: ONE module with enum)
- **PactType enum**: Pause, Ground, Stance, Cover, StandRegistry, HubAPI — exactly 6 (vision §16)
- **PactStatus enum**: Proposed, Active, Fulfilled, Voided
- **Pact struct**: id, type, parties ([]string Reach IDs), terms ([]byte), status, execute-msg-ref, window-id-ref (string, G-003), stand-id-ref (string, G-003)
- **Per-type Execute* stubs**: ExecutePause/Ground/Stance/Cover/StandRegistry/HubAPI — each transitions Proposed→Active, guards on type + status; ExecuteStandRegistry requires non-empty stand-id-ref
- **Mission-Lock invariant**: `MissionLockAmendable` const bool false + per-type `AmendableCoreTermsPause/Ground/Stance` const flags false; Cover/StandRegistry/HubAPI amendable. `MissionLockAmendableCoreTerms(type)` helper
- **AllPactTypes()** returns exactly 6 in vision §16 order
- **Genesis**: `GenesisState{Pacts []Pact}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty pact-ids, unknown types, bad JSON); data-engineer's `ValidatePacts` + `MissionLockCheck` wired into the genesis load path (G-008)
### x/partner (REQ-018, D-026)
- **PartnerTier enum**: Op, MasterOp, Pier, Anchor — exactly 4 (vision §13). "Op" not "operator" — lexicon-clean per RESEARCH §1.6
- **PartnerStatus enum**: Pending, Active, Suspended, Revoked
- **CredentialType enum**: Eresidency, Biometric, Vouch, Custom
- **CredentialRef struct**: provider-id, credential-type, ref-uri (opaque URI; Pier credential routing deferred per Q5)
- **Partner struct**: id, tier, name, reach-id (string, G-003), region, credential-ref, status
- **Registry keeper stub**: `Keeper` with `NewKeeper`, `AddPartner`, `GetPartner`, `ListPartners`, `ListByTier` (in-memory, mutex-safe)
- **AllPartnerTiers()** returns exactly 4 in vision §13 order
- **Genesis**: `GenesisState{Partners []Partner}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty partner-ids, bad JSON)
## Deviation: genesis.go created in P2-01-01 alongside types.go
The plan ordered genesis.go as task P2-01-03 (after the test task P2-01-02),
but `types.go` references `ValidatePacts` (the genesis helper) and the build
must be green after each per-task commit. I therefore created `genesis.go`
with `ValidatePacts` + `MissionLockCheck` in P2-01-01, and P2-01-03 then
extended it (wiring `MissionLockCheck` INTO `ValidatePacts` so the genesis
load path enforces the Mission-Lock check alongside id-uniqueness) and
committed the extension as the P2-01-03 deliverable. Both tasks are
individually committed; the deviation is structural only (genesis helper
landed in the types task to keep the build green, then was refined in the
genesis task). No semantic change to the plan's deliverables.
## Lexicon Compliance Notes
- **No banned literals** in any new `x/**/*.go` file (production or test). The 10 banned terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor) are referenced only via the `lexicon` package helpers (`lexicon.FindBannedTerm`, `lexicon.BannedTerms`) in test files.
- **Partner module** uses "Op"/"MasterOp"/"Pier"/"Anchor" (not "operator", which implies a banned financial term per RESEARCH §1.6). Verified lexicon-clean.
- **Pact module** avoids "account" — uses "Holder"/"Reach" conventions. The term "parties" ([]string of Reach IDs) is used for the Pact's participating Reach IDs.
- **Self-bootstrapping**: each test file has a `TestLexiconNoBannedTermsIn*TestFile` self-check that asserts the test file itself contains no banned literals (the lexicon helpers must be used, not inline strings).
- **Project-wide meta-test** (`lexicon_meta_test.go`) scans ALL `x/**/*.go` including the new pact + partner files — PASS.
## Pre-existing LSP noise (not P2 scope)
The LSP reports errors in `x/watcher/` files (cosmos-sdk/codec imports) and
`go.mod` (version "v2.0.1" invalid). These are **pre-existing** and **not in
P2 scope** — `x/watcher` is a v0.1 module with stale cosmos-sdk references
that are not part of the v0.2 skeleton (the v0.2 skeleton is zero-deps;
`go build ./...` succeeds because the watcher files are excluded from the
build path or compile cleanly via `go build`). `go build ./...` and
`go test ./...` both PASS, confirming the LSP noise does not affect the
build.
## Orchestrator Handoff
- **Do NOT merge/tag/push** — executor leaves the branch `oy/phase/02-pacts-partners` with 5 commits for the orchestrator to ship as tag `v0.1.2`.
- All P2 must-haves pass except the git tag (deferred to orchestrator per instructions).
- No regressions: all v0.1 baseline tests (143) + all v0.2-P1 tests + 64 new P2 tests = 207 total, all green.
+154
View File
@@ -0,0 +1,154 @@
# P3 Ship Verification — v0.2 Phase 3 (Councils + Forex)
**Branch**: `oy/phase/03-councils-forex`
**Phase**: P3 — Councils + Forex (REQ-011, Forex v1)
**Tag target**: `v0.1.3` (orchestrator ships; executor does NOT merge/tag/push)
**Date**: 2026-08-17
## Summary
Phase 3 ships two new Mesh modules — `x/council` (3-Council enum
Mesh/Guild/Stand with Mission Lock as a `const bool` + Voice/SignalKind/
TallyResult types mirroring `x/gov`) and `x/forex` (Forex Engine v1 stub:
ForexPair with lexicon-clean "Bread/Asset" labels + RateOracle interface +
StubOracle + 4-OracleKind enum) — both referencing x/stand and x/guild
by-ID-string (G-003). All six P3 tasks executed atomically with per-task
commits. Build green, tests green, coverage ≥80% on both new packages,
lexicon firewall green (forex is the highest lexicon-risk module per
RESEARCH §1.10 — verified clean), Mission Lock invariant green.
## Must-Haves (from PLANS.md P3 Must-Haves)
| Must-Have | Status | Evidence |
|---|---|---|
| `x/council`, `x/forex` each have `types/types.go` + `types/types_test.go` | ✅ | 6 files created (council: types.go+types_test.go+genesis.go; forex: types.go+types_test.go+genesis.go) |
| `go build ./...` and `go test ./...` green | ✅ | `go build ./...` → BUILD OK; `go test ./... -count=1` → all 22 packages ok (0 FAIL) |
| ≥80% coverage on `x/council/types`, `x/forex/types` | ✅ | council 96.4%, forex 100.0% |
| Council locked-const: exactly 3 types (Mesh, Guild, Stand) | ✅ | `CouncilKindCount == 3`, `AllCouncilKinds()` returns MeshCouncil/GuildCouncil/StandCouncil; `TestCouncilKindCountLockedConst` + `TestAllCouncilKindsNames` |
| **Mission Lock invariant**: `MissionLockAmendable == false`, test asserts non-amendable (highest-severity) | ✅ | `MissionLockAmendable` const bool false; `TestMissionLockAmendableConstFalse` + `TestMissionLockAmendableCannotBeSetTrue` (const is the firewall — cannot be reassigned) |
| `TallyResult` shape mirrors `x/gov` (A-204) for future wiring | ✅ | Fields yes/no/abstain/nowithveto/total/quorum_met; JSON tags verified in `TestTallyResultStructShape`; NoWithVeto always 0 (anti-greed, no veto option) |
| `VoteOption` has no "no-with-veto" (anti-greed) | ✅ | N/A — council uses `TallyResult` with NoWithVeto locked to 0 (no separate VoteOption enum; the TallyResult field is the parity-with-x-gov shape with the anti-greed invariant); `TestTallyResultNoWithVetoAlwaysZero` |
| Forex pair labels lexicon-clean (no banned tradable-unit terms); `RateOracle` interface compiles | ✅ | ForexPair uses `base_asset`/`quote_asset` JSON tags (A-208 "Bread/Asset"); `TestForexPairStructFields` + `TestForexPairLabelsLexiconClean`; `RateOracle` interface compiles (`TestRateOracleInterfaceCompiles` + `TestStubOracleSatisfiesInterface`) |
| Lexicon assertion in both new test files | ✅ | `TestLexiconNoBannedTermsInCouncilPackage` + `TestLexiconNoBannedTermsInCouncilTestFile`; `TestLexiconNoBannedTermsInForexPackage` + `TestLexiconNoBannedTermsInForexTestFile` |
| `ValidateGenesis` ID-uniqueness + referential integrity (Council) | ✅ | council rejects dup/empty council-ids + dup/empty voice-ids + unknown kinds/signals + Stand Council without stand-id-ref + Guild Council without guild-id-ref + Voice with unknown council-id (referential integrity P3-01-03); forex rejects dup/empty pair-ids + dup/empty provider-ids + empty base/quote-asset + unknown oracle-kind (A-212) |
| Git tag `v0.1.3` | ⏸ DEFERRED | Orchestrator ships (executor does NOT tag/merge/push per instructions) |
## Tasks Committed (6)
| Task | Commit | Description |
|---|---|---|
| P3-01-01 | `81708bd` | council types — 3 CouncilKind enum, Mission Lock const, Voice/SignalKind/TallyResult |
| P3-02-01 | `73aa90f` | forex types — ForexPair (Bread/Asset labels), RateOracle iface, 4 OracleKind enum, StubOracle |
| P3-01-02 | `02d02c8` | council types tests — locked-const, Mission Lock invariant, SignalKind, TallyResult, lexicon |
| P3-01-03 | `7804fdb` | council genesis schema — Voice tally referential integrity, Mission Lock check |
| P3-02-02 | `94eeca6` | forex types tests — OracleKind enum, RateOracle iface, StubOracle sentinel, lexicon (highest risk) |
| P3-02-03 | `a7567e2` | forex genesis schema — ValidatePairs/ValidateProviders, dup-id rejection |
## Build / Test / Coverage Results
### `go build ./...`
```
BUILD OK
```
### `go test ./... -count=1`
- 22 packages with tests, all `ok` (0 FAILs)
- Total test count: **264** (up from 207 baseline → +57 new tests across council + forex)
- Packages with no test files: lexicon, x/identity/types, x/processing/types, x/rootpool/types, x/vault/types (unchanged from baseline)
### `go test -cover ./x/council/types/... ./x/forex/types/...`
| Package | Coverage | Target | Pass |
|---|---|---|---|
| `x/council/types` | **96.4%** | ≥80% | ✅ |
| `x/forex/types` | **100.0%** | ≥80% | ✅ |
### Lexicon meta-test (`go test -run TestLexiconMeta .`)
- `TestLexiconMetaNoBannedTermsInX` — PASS (scans all `x/**/*.go` production + test for 10 banned terms; council + forex files clean)
- `TestLexiconMetaSelfTestTable` — PASS (G-009 self-test table for all 10 banned terms)
- `TestLexiconMetaBannedTermsCount` — PASS
- `TestLexiconMetaNoFalsePositiveOnOpenYield` — PASS (word-boundary matcher, "openyield" not flagged)
### G-003 by-ID-string invariant (`go test -run TestG003 ./x/window/...`)
- `TestG003NoCrossModuleStructImportsInProduction` — PASS (no production `.go` file under `x/` imports a foreign `x/<module>/types` package; council references x/stand + x/guild by-ID-string; forex has no cross-module refs)
## Module Details
### x/council (REQ-011, D-022)
- **CouncilKind enum**: MeshCouncil, GuildCouncil, StandCouncil — exactly 3 (REQ-011)
- **Council struct**: id, kind, stand-id-ref (optional, by-ID-string to x/stand — P1-02-01), guild-id-ref (optional, by-ID-string to x/guild — P1-03-01), members ([]CouncilMember), voice-threshold
- **CouncilMember struct**: reach-id (lexicon-clean holder identifier — NOT the banned financial holder term), voice-weight, joined-at
- **Voice struct**: id, council-id, proposer-reach, signal-kind, target-ref, tally, timestamp
- **SignalKind enum**: Stash, Standing, Vouch, Capital — exactly 4 (the four Freeholder signals, cross-ref v0.1 REQ-005 / vision §9.1 x/standing FreeholderSignals)
- **TallyResult struct**: yes, no, abstain, nowithveto (always 0 — anti-greed), total, quorum-met — mirrors x/gov shape (A-204)
- **Mission Lock invariant**: `MissionLockAmendable` const bool false — the highest-severity regression firewall; the const can NEVER be set true (compile-time const)
- **Genesis**: `GenesisState{Councils, Voices, Params}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty council-ids, dup/empty voice-ids, unknown kinds/signals, Stand Council without stand-id-ref, Guild Council without guild-id-ref, Voice with unknown council-id [referential integrity]); data-engineer's `ValidateCouncils` + `ValidateVoices` + `MissionLockCheck` wired into the genesis load path (G-008)
### x/forex (Forex v1, D-030)
- **ForexPair struct**: id, base-asset, quote-asset, decimals — uses "Bread/Asset" style labels (A-208), NOT the banned financial tradable-unit terms (lexicon-hostile per RESEARCH §1.10)
- **RateOracle Go interface**: `GetRate(pairID) (rate uint64, timestamp int64, err error)` — no impl in v0.2 (Phase 3 wires Piers)
- **OracleProvider struct**: id, name, kind
- **OracleKind enum**: Chainlink, Pyth, UMA, Internal — exactly 4 (Forex v1)
- **SpotRate struct**: pair-id, rate, timestamp, provider-id (by-ID-string refs per G-003)
- **StubOracle**: stub keeper; `GetRate` returns sentinel `ErrOracleNotIntegrated` ("forex oracle not integrated (Phase 3 wires Piers)")
- **SpreadCapBps**: const 0 (A-214 documented placeholder; test asserts ≥0; v0.3 may set a positive cap)
- **Genesis**: `GenesisState{Pairs, Providers, Params}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty pair-ids, dup/empty provider-ids, empty base/quote-asset, unknown oracle-kind); data-engineer's `ValidatePairs` + `ValidateProviders` (G-008)
## Deviation: genesis.go created in Wave 1 alongside types.go (P3-01-01 / P3-02-01)
The plan ordered genesis.go as tasks P3-01-03 and P3-02-03 (after the test
tasks P3-01-02 and P3-02-02), but `types.go` references `ValidateCouncils`/
`ValidateVoices` (council) and `ValidatePairs`/`ValidateProviders` (forex)
— the genesis helpers — and the build must be green after each per-task
commit. I therefore created `genesis.go` with the Validate* helpers in the
Wave 1 types tasks (P3-01-01 and P3-02-01), and the Wave 2 genesis tasks
(P3-01-03 and P3-02-03) then refined the doc/comments to make the
deliverable explicit and committed the refinement. This matches the P2
deviation pattern (documented in P2_SHIP_VERIFICATION.md). All four tasks
are individually committed; the deviation is structural only (genesis
helper landed in the types task to keep the build green, then was refined
in the genesis task). No semantic change to the plan's deliverables.
## Lexicon Compliance Notes (Forex is highest risk per RESEARCH §1.10)
- **No banned literals** in any new `x/council/**/*.go` or `x/forex/**/*.go`
file (production or test). The 10 banned terms (bank, deposit, interest,
yield, currency, dollar, euro, account, savings, depositor) are
referenced only via the `lexicon` package helpers
(`lexicon.FindBannedTerm`, `lexicon.BannedTerms`) in test files.
- **Council module** uses "reach-id"/"voice-holder"/"proposer-reach"
(NOT the banned financial holder term — the lexicon-clean holder
identifier per RESEARCH §2). Comments deliberately avoid the banned term
even in "NOT <banned-term>" form (the word-boundary matcher would flag it).
- **Forex module** uses "Forex" (allowed — vision §13 names it; NOT in the
banned list), "base-asset"/"quote-asset" (A-208 — NOT the banned
tradable-unit terms), "Bread"/"Asset" sample labels (A-208). The banned
financial terms for tradable units (the three lexicon-hostile terms
per RESEARCH §1.10) NEVER appear in source. "fx" is borderline but
avoided (the module name is "forex" not "fx").
- **Self-bootstrapping**: each test file has a
`TestLexiconNoBannedTermsIn*TestFile` self-check that asserts the test
file itself contains no banned literals (the lexicon helpers must be
used, not inline strings).
- **Project-wide meta-test** (`lexicon_meta_test.go`) scans ALL
`x/**/*.go` including the new council + forex files — PASS.
## Pre-existing LSP noise (not P3 scope)
The LSP reports errors in `x/watcher/` files (cosmos-sdk/codec imports) and
`go.mod` (version "v2.0.1" invalid). These are **pre-existing** and **not
in P3 scope** — `x/watcher` is a v0.1 module with stale cosmos-sdk
references that are not part of the v0.2 skeleton (the v0.2 skeleton is
zero-deps; `go build ./...` succeeds because the watcher files are
excluded from the build path or compile cleanly via `go build`).
`go build ./...` and `go test ./...` both PASS, confirming the LSP noise
does not affect the build. (Same note as P1/P2 ship verification.)
## Orchestrator Handoff
- **Do NOT merge/tag/push** — executor leaves the branch
`oy/phase/03-councils-forex` with 6 commits for the orchestrator to ship
as tag `v0.1.3`.
- All P3 must-haves pass except the git tag (deferred to orchestrator per
instructions).
- No regressions: all v0.1 baseline tests + all v0.2-P1 tests + all v0.2-P2
tests + 57 new P3 tests = 264 total, all green.
+113
View File
@@ -0,0 +1,113 @@
# Phase P4 — Bonds + Bearers + L2 — Ship Verification
> Milestone **v0.2 (The Mesh)** — Phase 4 (P4 — Bonds+Bearers+L2).
> Branch: `oy/phase/04-bonds-bearers-l2`.
> Tag: **NOT created** (per executor instructions — do NOT merge/tag/push).
## Verification Summary
| Check | Result |
|---|---|
| `go build ./...` | ✅ green |
| `go test ./...` | ✅ green (303 PASS, 0 FAIL across 21 packages with tests) |
| `go test -cover ./x/bond/types/...` | ✅ 96.8% (≥80%) |
| `go test -cover ./x/bearers/types/...` | ✅ 100.0% (≥80%) |
| `go test -cover ./x/satellite/types/...` | ✅ 100.0% (≥80%) |
| Existing v0.1 tests (no regression) | ✅ all green (15+10=25 packages incl. 4 no-test) |
| Lexicon meta-test (`TestLexiconMetaNoBannedTermsInX`) | ✅ green |
| Bond lexicon (A-210 coupon-only) | ✅ green (`TestLexiconNoBannedTermsInBondPackage`) |
| Satellite lexicon (Holder/Reach, not banned terms) | ✅ green (`TestLexiconNoBannedTermsInSatellitePackage`) |
| Bearers extension lexicon | ✅ green (`TestLexiconNoBannedTermsInBearersPackage`) |
| AllBearers() == 6 (no regression) | ✅ green (`TestBearerCount`, `TestOYLRStillInAllBearers`) |
| Git tag `v0.1.4` | ⛔ NOT created (per executor instructions — do NOT tag/push) |
## Tasks Executed (8/8 committed)
| Task | File(s) | Commit | Persona |
|---|---|---|---|
| P4-01-01 | `x/bond/types/types.go`, `x/bond/types/genesis.go` | `242ebcc` | backend-engineer |
| P4-02-01 | `x/bearers/types/types.go` (extended) | `0727219` | cosmos-engineer |
| P4-03-01 | `x/satellite/types/types.go`, `x/satellite/types/genesis.go` | `0979015` | cosmos-engineer |
| P4-01-02 | `x/bond/types/types_test.go` | `70f1ddf` | security-engineer |
| P4-01-03 | `x/bond/types/genesis_test.go` (genesis.go committed in 01-01) | `e18c323` | data-engineer |
| P4-02-02 | `x/bearers/types/types_test.go` (extended) | `faf0508` | security-engineer |
| P4-03-02 | `x/satellite/types/types_test.go` | `9ee2d11` | security-engineer |
| P4-04-01 | `.ciagent/oy/P4_SHIP_VERIFICATION.md` | (this commit) | lead-developer |
## Must-Haves (P4 checklist)
- [x] `x/bond` (new), `x/bearers` (extended), `x/satellite` (new) each have `types/types.go` + `types/types_test.go`.
- [x] `go build ./...` and `go test ./...` green — including all v0.1 baseline tests (no regression).
- [x] ≥80% coverage on `x/bond/types` (96.8%), `x/bearers/types` (100%), `x/satellite/types` (100%).
- [x] Bond clamp invariant: `CouponCapBps == 800`, `CouponFloorBps == 0`; clamp below→floor, above→cap, in-range→unchanged.
- [x] Bond lexicon: "coupon" exclusively, no banned terms (A-210).
- [x] Bearers: `BearerTransport` interface compiles; `OYLRLink` + `BeaconFrame` stubs; existing `AllBearers()` (6) unchanged.
- [x] Satellite: `L2Chain` exactly 5 (Polygon active + 4 stubs); `Packet` pinned to ICS-20 v1 shape; zero external deps.
- [x] Lexicon assertion in all 3 test files (bond, bearers-ext, satellite).
- [x] `ValidateGenesis` ID-uniqueness (all 3) + genesis clamp (Bond).
- [ ] Git tag `v0.1.4` — ⛔ NOT created (executor instructed NOT to merge/tag/push).
## Deliverable Detail
### P4-01-01 — Bond types (backend-engineer, REQ-021, D-028)
- `CouponCapBps = 800` (8%), `CouponFloorBps = 0` (0%) — LOCKED `const`.
- `Bond` struct: id, issuer-stand-id (by-ID-string ref to x/stand per G-003), principal-grain, coupon-bps, term-days, issued-at, maturity, status.
- `BondStatus` enum (5): Issued, Active, Matured, Defaulted, Repaid.
- `Issue(...)` stub: constructs Bond with coupon clamped, status BondIssued.
- `Clamp(couponBps)` mirrors `x/feecovenant` Clamp shape: `min(cap, max(floor, coupon))`.
- `AllBondStatuses()` returns 5.
- `DefaultParams`, `GenesisState` (bonds), `DefaultGenesisState`, `ValidateGenesis` (rejects dup bond-ids).
### P4-02-01 — Bearers extension (cosmos-engineer, D-029, A-209)
- EXTENDED existing `x/bearers/types/types.go` (NOT a new module).
- `BearerTransport` Go interface: `Send`, `Receive`, `Status` — no impl.
- `OYLRLink` struct: gateway-id, range-meters, frequency-mhz, surveillance-resistant=true.
- `BeaconFrame` struct: beacon-id, ephemeral-id, payload-bytes, ttl.
- PRESERVED existing `BearerType` enum + `AllBearers()` (OY-LR still in the 6).
- `DefaultParams`/`GenesisState` unchanged (no break).
### P4-03-01 — Satellite types (cosmos-engineer, REQ-009, D-021, A-215)
- `L2Chain` enum (5): Polygon active; Base, Arbitrum, Optimism, Solana StatusPending (D-021).
- `TransferChannel` struct: port-id, channel-id, counterparty, status.
- `ChannelStatus` enum (4): Init, TryOpen, Open, Closed (ICS-20 handshake).
- `WrappedBreadDenom` struct: denom, trace-path (IBC trace encoding).
- `Packet` stub struct: sequence, source-port, source-channel, dest-port, dest-channel, data, timeout-height, timeout-timestamp (ICS-20 v1 shape).
- NO ibc-go import (zero external deps — A-201).
- `AllL2Chains()` returns 5; `AllChannelStatuses()` returns 4.
- `DefaultParams`, `GenesisState` (channels + denoms), `DefaultGenesisState`, `ValidateGenesis` (rejects dup channel-ids + dup denoms).
### P4-01-02 — Bond tests (security-engineer, REQ-021)
- Clamp invariant tests: below floor → floor, above cap → cap, in range → unchanged.
- `CouponCapBps == 800` locked-const; `CouponFloorBps == 0` locked-const.
- `BondStatus` enum coverage (5); `Issue` stub callable + clamps above cap.
- `ValidateGenesis` rejects dup bond-id, unknown status, coupon above cap.
- Lexicon assertion (lexicon helpers, no banned literals — A-210 coupon-only).
### P4-01-03 — Bond genesis (data-engineer, REQ-021)
- `ValidateBonds` enforces coupon-bps within [floor, cap] at genesis load (D-028 clamp).
- `genesis_test.go`: boundary tests (at floor, at cap, just above cap, just below cap).
### P4-02-02 — Bearers tests extension (security-engineer, D-029)
- `BearerTransport` interface signature test (stub impl satisfies it).
- `OYLRLink` non-empty + surveillance-resistant == true; `BeaconFrame` non-empty + ttl > 0.
- OY-LR still in AllBearers() (REGRESSION: existing v0.1 tests pass).
- Lexicon assertion (extends existing test file).
### P4-03-02 — Satellite tests (security-engineer, REQ-009)
- `L2Chain` exactly 5 (Polygon + 4 stubs); Polygon only active (D-021).
- `ChannelStatus` coverage (4); `Packet` fields match ICS-20 v1 (JSON tags).
- `WrappedBreadDenom` trace-path encoding; `ValidateGenesis` rejects dup channel-id + dup denom.
- Lexicon assertion (no banned terms — use Holder/Reach).
### P4-04-01 — Phase ship verification (lead-developer)
- This document. Full build/test/coverage verification.
## Test Counts
- **Total `--- PASS`: 303** (leaf tests; some names repeat across packages).
- **Total `--- FAIL`: 0**.
- **Packages with tests: 21** (4 packages have no test files: identity, processing, rootpool, vault — same as v0.1 baseline).
## Notes
- The bond `genesis.go` was created in P4-01-01's commit (needed for `go build``ValidateBonds` is referenced by `ValidateGenesis` in types.go). P4-01-03 adds the dedicated `genesis_test.go` clamp assertions and owns the data-engineer's genesis-schema deliverable.
- Pre-existing LSP errors in `x/watcher/` (cosmos-sdk imports not vendored) are unchanged and do not affect `go build ./...` or `go test ./...` (the watcher module builds under the v0.1 baseline; these are stale LSP diagnostics, not build errors).
- No merge, no tag, no push performed (per executor instructions).
+96 -42
View File
@@ -2,71 +2,125 @@
active_personas:
- id: backend-engineer
active: true
reason: OY Chain core, CosmWasm contracts, Mirror attestations, Fee Covenant enforcement
frameworks: [Rust, CosmWasm, Cosmos SDK, CometBFT, IBC]
territory: ["crates/**", "contracts/**", "chain/**"]
constraints: [mission-lock enforcement, FCFS, no leverage/futures, no fractional reserve]
phase_specific: false
reason: Go/Cosmos module skeletons for v0.2 components — owns the non-Cosmos-mirroring modules (pact, partner, bond) per G-007. Owns the bulk of bespoke-type skeleton + tests work.
frameworks: [Go, Cosmos SDK, IBC, CosmWasm]
territory: ["x/pact/**", "x/partner/**", "x/bond/**", "x/**/types/**", "x/**/keeper/**", "x/**/module.go"]
constraints: [lexicon compliance (REQ-012), skeleton+tests pattern (D-020), ≥80% coverage on new packages (D-033), no live-chain side effects in skeleton, locked-const invariants, no fractional reserve, no leverage/futures, go.mod is read-only in v0.2 (G-006 — any change is an escalation)]
- id: data-engineer
active: true
reason: Storage substrate (Stash/Vault/Root-Pool), indexing, audit logs, Window data channels
frameworks: [Rust, SQLx, PostgreSQL, Subsquid]
territory: ["storage/**", "indexer/**", "migrations/**"]
constraints: [Holder data sovereignty, audit log immutability, revocable access]
- id: frontend-engineer
active: true
reason: OY mesh app, Maps UI, Pay flows, Window management, Maya's Day integration
frameworks: [React Native, Expo, TypeScript, Reanimated]
territory: ["app/**", "components/**", "screens/**"]
constraints: [self-service default, no KYC gates, offline-first]
phase_specific: false
reason: Genesis/state schema design for new modules — Window audit log, Stand membership sets, Bond issuance state, Council Voice tally state. Shapes ValidateGenesis upgrades. Owns genesis SCHEMA only (G-008); test assertions are security-engineer's.
frameworks: [Go, encoding/json, Cosmos SDK state]
territory: ["x/**/types/genesis.go", "x/**/genesis.go"]
constraints: [append-only audit logs (Window), ID-uniqueness in ValidateGenesis, lexicon compliance, no state identity beyond Reach, does NOT own *_test.go files (G-008)]
- id: lead-developer
active: true
reason: Multi-component orchestration, dependency sequencing, persona coordination
phase_specific: false
reason: Multi-component orchestration across 10 new packages, dependency sequencing per D-031 blocker chain, vertical-slice integrity per phase.
frameworks: [cross-cutting]
territory: ["**"]
constraints: [blocked-by chain enforcement, milestone versioning]
constraints: [blocked-by chain enforcement (D-031), milestone versioning (v0.2 / tag_base v0.1.x), lexicon gate on merge, persona territory warn-mode enforcement]
- id: cosmos-engineer
active: true
phase_specific: true
reason: v0.2 introduces IBC (satellite), governance (council), capability (window), group (stand/guild), oracle (forex) patterns that map directly to Cosmos SDK modules (x/gov, x/group, x/authz, x/feegrant, x/capability, x/ibc-transfer). Phase-specific to v0.2 execution phases where Cosmos-convention alignment matters for future wiring.
frameworks: [cosmos-sdk, ibc-go, CosmWasm, CometBFT]
territory: ["x/satellite/**", "x/council/**", "x/window/**", "x/stand/**", "x/guild/**", "x/forex/**", "x/bearers/**"]
constraints: [lexicon compliance (REQ-012), no live-chain side effects in skeleton, mirror x/gov TallyResult / x/group DecisionPolicy / x/authz Grant shapes for future wiring, zero external deps in v0.2 skeleton, by-ID-string inter-module references to avoid import cycles (G-003 tested invariant), go.mod is read-only in v0.2 (G-006)]
- id: security-engineer
active: true
phase_specific: true
reason: v0.2 enforces Mission Lock (council), fee/bond coupon clamps (clamp invariants), 9-stand / 4-tier / 6-pact locked-const tests, and Window revoke/expire lifecycle invariants. Phase-specific to v0.2 where invariant/locked-const test density is highest.
frameworks: [Go testing, table-driven tests, invariant tests]
territory: ["x/**/types/**_test.go", "x/**/keeper/**_test.go", "x/**/genesis_test.go", "x/**/*_test.go"]
constraints: [invariant tests for all locked constants (Mission Lock non-amendable, bond 8% cap / 0% floor, fee ceiling/floor), locked-const tests for every enum count (9 stands, 4 partner tiers, 6 pacts), lexicon assertion in every new test file (D-033), ≥80% coverage on new packages, owns ALL *_test.go files including genesis_test.go (G-008)]
phase_specific: []
deactivated:
- id: frontend-engineer
reason: No UI in v0.2 (skeleton+tests only; Mesh Experience UI is v0.1-complete and v0.3+ for new UI). Deactivate to avoid persona territory noise. Reactivate in v0.3.
- id: ci-security-auditor
reason: Default deactivated; activate per-phase for security audits
custom_personas: []
reason: Default deactivated; activate in P5 (review/ship) phase for the milestone audit. Not needed during P1-P4 skeleton authoring.
- id: mesh-engineer
reason: v0.1 listed as future; still not needed in v0.2 (Bearers OY-LR + Beacon are type stubs only, no hardware/RF). Activate in v0.3 for real bearer runtime.
custom_personas:
- id: cosmos-engineer
rationale: v0.2 components map onto specific Cosmos SDK modules (x/gov, x/group, x/authz, x/feegrant, x/capability, x/ibc-transfer). A dedicated persona ensures skeleton types mirror the eventual runtime shapes, reducing Phase 3 wiring refactor cost. Distinct from backend-engineer because it carries Cosmos-specific convention knowledge (tally shapes, decision policy, capability, ICS-20 packet shape).
- id: security-engineer
rationale: v0.2 has the highest locked-const + invariant density in the project (Mission Lock, bond cap, 6 Pact types, 9 Stand types, 4 Partner tiers, Window revoke idempotency). A dedicated persona ensures invariant tests and lexicon assertions are not an afterthought. Distinct from backend-engineer because it owns test-file territory and invariant-first design.
---
# Personas: OpenYield (oy)
# Personas: OpenYield (oy) — v0.2 (The Mesh)
## Active Roster
### backend-engineer
- **Domain**: OY Chain, CosmWasm contracts, Mirror, Fee Covenant.
- **Frameworks**: Rust, CosmWasm, Cosmos SDK, CometBFT, IBC.
- **Territory**: `crates/**`, `contracts/**`, `chain/**`.
- **Constraints**: mission-lock enforcement, FCFS, no leverage/futures, no fractional reserve.
- **Domain**: Go/Cosmos module skeletons for v0.2 — owns the **non-Cosmos-mirroring** modules (pact, partner, bond) per G-007, plus shared `types/`+`keeper/`+`module.go` authoring.
- **Frameworks**: Go, Cosmos SDK, IBC, CosmWasm.
- **Territory**: `x/pact/**`, `x/partner/**`, `x/bond/**`, `x/**/types/**`, `x/**/keeper/**`, `x/**/module.go`. (`go.mod` is read-only in v0.2 per G-006.)
- **Constraints**: lexicon compliance (REQ-012), skeleton+tests pattern (D-020), ≥80% coverage on new packages (D-033), no live-chain side effects in skeleton, locked-const invariants, no fractional reserve, no leverage/futures.
### data-engineer
- **Domain**: Storage substrate, indexing, audit logs, Window data channels.
- **Frameworks**: Rust, SQLx, PostgreSQL, Subsquid.
- **Territory**: `storage/**`, `indexer/**`, `migrations/**`.
- **Constraints**: Holder data sovereignty, audit log immutability, revocable access.
### frontend-engineer
- **Domain**: OY mesh app, Maps UI, Pay flows, Window management.
- **Frameworks**: React Native, Expo, TypeScript, Reanimated.
- **Territory**: `app/**`, `components/**`, `screens/**`.
- **Constraints**: self-service default, no KYC gates, offline-first.
- **Domain**: Genesis/state schema design — Window audit log, Stand membership sets, Bond issuance state, Council Voice tally state. Shapes `ValidateGenesis` upgrades (v0.1's no-op → v0.2 ID-uniqueness checks). Owns genesis **schema** only (G-008); genesis **test assertions** are security-engineer's.
- **Frameworks**: Go, `encoding/json`, Cosmos SDK state.
- **Territory**: `x/**/types/genesis.go`, `x/**/genesis.go` (excludes `*_test.go` per G-008).
- **Constraints**: append-only audit logs (Window), ID-uniqueness in `ValidateGenesis`, lexicon compliance, no state identity beyond Reach.
### lead-developer
- **Domain**: Multi-component orchestration, dependency sequencing.
- **Domain**: Multi-component orchestration across 10 new packages, dependency sequencing per D-031 blocker chain, vertical-slice integrity per phase.
- **Frameworks**: cross-cutting.
- **Territory**: `**`.
- **Constraints**: blocked-by chain enforcement, milestone versioning.
- **Constraints**: blocked-by chain enforcement (D-031), milestone versioning (v0.2 / tag_base v0.1.x), lexicon gate on merge, persona territory warn-mode enforcement.
## Phase-Specific
None at Phase 0. Phase 1 will add:
- security-engineer (for Watcher slashing logic, signature verification, smart contract audits)
- mesh-engineer (for LoRa/BLE/SAT bearer implementations)
### cosmos-engineer (custom, phase-specific to v0.2)
- **Domain**: v0.2 components map onto Cosmos SDK modules — `x/gov` (council tally), `x/group` (stand/guild decision policy), `x/authz`/`x/feegrant` (window), `x/capability` (window unforgeable ref), `x/ibc-transfer` ICS-20 (satellite). Ensures skeleton types mirror runtime shapes for low-friction Phase 3 wiring.
- **Frameworks**: cosmos-sdk, ibc-go, CosmWasm, CometBFT.
- **Territory**: `x/satellite/**`, `x/council/**`, `x/window/**`, `x/stand/**`, `x/guild/**`, `x/forex/**`, `x/bearers/**` (Cosmos-convention-mirroring modules per G-007; `x/pact`/`x/partner`/`x/bond` are backend-engineer's). `go.mod` read-only per G-006.
- **Constraints**: lexicon compliance (REQ-012), no live-chain side effects in skeleton, mirror `x/gov` `TallyResult` / `x/group` `DecisionPolicy` / `x/authz` `Grant` shapes, zero external deps in v0.2 skeleton, by-ID-string inter-module references (G-003 tested invariant).
### security-engineer (custom, phase-specific to v0.2)
- **Domain**: Invariant + locked-const test authorship. Owns **all** test-file territory across v0.2 packages (including `genesis_test.go` per G-008). Highest invariant density in the project: Mission Lock non-amendable, bond 8% cap / 0% floor clamp, fee ceiling/floor clamp, 6 Pact types, 9 Stand types, 4 Partner tiers, Window revoke/expire idempotency, lexicon assertion per module.
- **Frameworks**: Go testing, table-driven tests, invariant tests.
- **Territory**: `x/**/types/**_test.go`, `x/**/keeper/**_test.go`, `x/**/genesis_test.go`, `x/**/*_test.go` (all test files per G-008).
- **Constraints**: invariant tests for all locked constants, locked-const tests for every enum count (9 stands, 4 partner tiers, 6 pacts, 3 councils), lexicon assertion in every new test file (D-033), ≥80% coverage on new packages, Window lifecycle idempotency tests (revoke-after-expire, double-revoke).
## Deactivated
- **frontend-engineer** — No UI in v0.2 (skeleton+tests only). Mesh Experience UI is v0.1-complete; new UI is v0.3+. Reactivate in v0.3.
- **ci-security-auditor** — Default deactivated; activate in P5 (review/ship) for milestone audit.
- **mesh-engineer** — v0.1 listed as future; still not needed in v0.2 (Bearers OY-LR + Beacon are type stubs only). Activate in v0.3 for real bearer runtime.
## Custom Personas
None at Phase 0.
- **cosmos-engineer** — v0.2 components map onto specific Cosmos SDK modules. Dedicated persona ensures skeleton types mirror eventual runtime shapes, reducing Phase 3 wiring refactor cost. Distinct from backend-engineer: carries Cosmos-specific convention knowledge (tally shapes, decision policy, capability, ICS-20 packet shape).
- **security-engineer** — v0.2 has the highest locked-const + invariant density in the project. Dedicated persona ensures invariant tests and lexicon assertions are not an afterthought. Distinct from backend-engineer: owns test-file territory and invariant-first design.
## Framework Alignment
- **Go 1.22** — all personas target Go 1.22 (`go.mod`).
- **cosmos-sdk** — cosmos-engineer targets cosmos-sdk v0.50.x (LTS) for future wiring; NOT vendored in v0.2 skeleton.
- **ibc-go** — cosmos-engineer targets ibc-go v8/v10 for satellite module shape; NOT vendored in v0.2 skeleton.
- **CosmWasm** — cosmos-engineer targets wasmvm v1.5/v2.0 for potential Pacts-as-contracts in Phase 3; NOT vendored in v0.2.
## Territory Alignment
- Mapped to actual `x/<name>/` structure (15 v0.1 modules + 9 new v0.2 modules + 1 extended).
- backend-engineer owns `x/pact`, `x/partner`, `x/bond` (non-Cosmos-mirroring per G-007) + shared `types/`+`keeper/`+`module.go` authoring.
- data-engineer owns `genesis.go` schema only (G-008 — excludes `*_test.go`).
- cosmos-engineer owns the Cosmos-convention-mirroring modules (`x/satellite`, `x/council`, `x/window`, `x/stand`, `x/guild`, `x/forex`) + `x/bearers` extension (G-007).
- security-engineer owns **all** `*_test.go` files across the new packages (G-008 — including `genesis_test.go`).
- lead-developer owns `**` for cross-cutting coordination.
- `go.mod` is **read-only** in v0.2 (G-006) — no persona may modify it; any change is an escalation (would violate D-020/A-201 zero-dep invariant).
## Constraint Alignment
- **Lexicon (REQ-012)** — every persona carries it; security-engineer asserts it per test file.
- **Skeleton + tests pattern (D-020)** — backend-engineer + cosmos-engineer enforce.
- **≥80% coverage on new packages (D-033)** — security-engineer owns the gate.
- **Blocker chain (D-031)** — lead-developer enforces phase ordering.
- **No live-chain side effects in skeleton** — cosmos-engineer + backend-engineer enforce (no relayer, no CometBFT, no live oracle).
- **Locked-const invariants** — security-engineer owns; every locked constant has a dedicated test.
## Phase-Specific Personas
- **cosmos-engineer** — phase-specific to v0.2 execution phases (P1-P4). Remove or merge back into backend-engineer after v0.2 ships (Cosmos convention alignment is most critical during the first Mesh-era skeleton).
- **security-engineer** — phase-specific to v0.2 (highest invariant density). May persist into v0.3 if invariant-test density remains high; reassess at v0.3 PLAN.
+321 -197
View File
@@ -1,213 +1,337 @@
# Phase Plans: OpenYield (oy) — Milestone v0.1 (Pre-MVP)
# Plans: OpenYield (oy) — v0.2 (The Mesh)
## Milestone Versioning
## Milestone Summary
- **Milestone**: v0.1 (development; stays v0.1 until MVP ships as v0.1.0)
- **Patch line**: v0.0.x (previous minor)
- **Phase 0 tag**: v0.0.0
- **P1-P8 tags**: v0.0.1 - v0.0.8
- **P9 (final review)**: v0.0.9 — milestone complete, no MVP release yet
- **MVP release** (future milestone): v0.1.0
- **Milestone**: v0.2 — The Mesh
- **Type**: Feature (≥1 `feat` phase)
- **Tag base**: `v0.1.x` patch line (P0 ships as `v0.1.0`; execution phases `v0.1.1..v0.1.4`; final phase `v0.1.5` IS the milestone release)
- **Phases**: 5 — P1..P4 (execution) + P5 (final review/audit/ship). Phase 0 (PLAN) is in progress.
- **Depth**: skeleton + tests layer (D-020) — Go types + keeper stubs + unit tests, zero external deps, matching v0.1's pre-MVP pattern.
- **Coverage target**: ≥80% on each new package (D-033); lexicon assertion (REQ-012) in every new module's test file (D-032).
- **New modules**: 9 (`x/satellite`, `x/council`, `x/window`, `x/stand`, `x/guild`, `x/partner`, `x/pact`, `x/bond`, `x/forex`). **Extended**: 1 (`x/bearers`). Total v0.2 packages: 10 new/extended + v0.1 baseline of 15.
- **Phase ordering** (D-031): P1 Orgs+Window foundation → P2 Pacts+Partners → P3 Councils+Forex → P4 Bonds+Bearers+L2 → P5 review/ship. Respects ARCHITECTURE.md blocker chain.
- **Personas**: backend-engineer (bespoke types), cosmos-engineer (Cosmos-convention types), data-engineer (genesis validation), security-engineer (all test files, invariants, lexicon), lead-developer (cross-cutting verification/ship).
## Phase Breakdown — Task-Level Slices
### Phase 0 — Pre-Execution (COMPLETE)
- SPECIFY → CLARIFY → RESEARCH → PLAN → GRILL
- Tag: v0.0.0
- Ships: .ciagent/ reference files, 21 REQ-IDs, persona assessment
### Phase 1 — OY Chain & Mirror (Component 1)
- **Wave**: Core Spine
- **REQs**: REQ-004 (9 Watchers, 6-of-9 quorum), REQ-008 (OY Chain)
- **Deliverables**:
- Cosmos SDK app-chain skeleton (CometBFT consensus)
- Watcher bond module (min 100,000 Bread bond)
- Attestation quorum module (6-of-9, daily epoch, geographic diversity rule)
- Mirror: reserve ratio publication, NAV computation
- Still/Stir (pause/resume) module
- **Blocked by**: Phase 0 ship
- **Owner**: backend-engineer
- **Tag**: v0.0.1
- **Demo**: Chain launches, 9 Watchers bond, daily attestation quorum reached, reserve ratio published
### Phase 2 — Bread Unit & Root Basket (Component 3)
- **Wave**: Core Spine
- **REQs**: REQ-003 (Bloom from real yield), REQ-013 (Bread unit), REQ-009 (Satellites)
- **Deliverables**:
- Forge/Fold CosmWasm contracts (mint against deposited Root Basket assets only)
- Bread unit scale (Grain → Crumb → Bread → Loaf → Batch → Cake → Bakery → Granary → Mill → Harvest → Earth)
- Root Basket config (Treasuries ~35%, IG corporate ~25%, gold ~20%, Bitcoin ~10%, other RWA ~10%)
- Ethereum bridge stub (wrapped Bread on Ethereum)
- Satellite chain stubs (Polygon, Base, Arbitrum)
- **Blocked by**: Phase 1
- **Owner**: backend-engineer
- **Tag**: v0.0.2
- **Demo**: Deposit RWA asset → Forge mints Bread → Fold burns Bread → returns asset. NAV per Bread published.
### Phase 3 — Storage Substrate (Component 5)
- **Wave**: Core Spine
- **REQs**: REQ-014 (3 pools), REQ-015 (Window data source)
- **Deliverables**:
- Stash contract (per Holder, sovereign)
- Vault contract (per Stand, governed)
- Root-Pool contract (treasury, attested by Watchers)
- Storage indexer (Subsquid + GraphQL)
- Audit log primitive (for Window)
- **Blocked by**: Phase 1, Phase 2
- **Owner**: data-engineer
- **Tag**: v0.0.3
- **Demo**: Holder creates Stash, receives Bread, balance queryable. Root-Pool visible to all Holders.
### Phase 4 — Bloom Engine (Component 4)
- **Wave**: Foundation Layer
- **REQs**: REQ-003 (Bloom from real yield)
- **Deliverables**:
- Bloom accrual loop (tied to Mirror attestations)
- Pro-rata distribution to every Grain in every Stash
- Target ~4-5%/year (honest expectation, not promise)
- Bloom Booster Bucket (higher-yield strategy)
- **Blocked by**: Phase 2, Phase 3
- **Owner**: backend-engineer
- **Tag**: v0.0.4
- **Demo**: Bread in Stash accrues Bloom automatically. Holder sees Bloom balance grow.
### Phase 5 — Fee Covenant (Component 13)
- **Wave**: Foundation Layer
- **REQs**: REQ-002 (fee ceiling/floor)
- **Deliverables**:
- Fee Covenant smart contract (0.1% ceiling, 0.01% floor, 1-Grain internal minimum)
- Auto-decline schedule (Genesis 0.1% → $10B 0.07% → $50B 0.05% → $200B 0.03% → $500B 0.02% → $1T 0.01%)
- Processor share (50% → 30% → 20% → 10%)
- Waiver logic (Hand-Passes, Crew-internal, Household-internal, Cover Calls, Trust, Foundation, Circle)
- clamp() function (never > ceiling, never < floor)
- **Blocked by**: Phase 3
- **Owner**: backend-engineer
- **Tag**: v0.0.5
- **Demo**: Pass-Act executes, fee computed correctly, waived passes cost only 1 Grain, auto-decline verified.
### Phase 6 — Identity, Standing & Citizenship (Component 6)
- **Wave**: Foundation Layer
- **REQs**: REQ-005 (Four Freeholder signals), REQ-006 (Standing formula), REQ-012 (Lexicon)
- **Deliverables**:
- Reach v1 (mesh-native identity, cryptographic)
- Standing v1 (Bayesian + time-decay + diversity + voucher-weighted slashes)
- Freeholder transition (4 signals: 90d Stash, 4.5★+ in 3 cats, Capital, Vouch)
- Vouching with skin-in-the-game
- Slashing (Crack, FraudulentCoverCall, InactivityTimeout)
- Wayfarer's Record attestation
- Two-Layer Passport routing stub
- **Blocked by**: Phase 1, Phase 3
- **Owner**: backend-engineer + data-engineer
- **Tag**: v0.0.6
- **Demo**: Holder accumulates Standing, meets 4 signals, becomes Freeholder automatically. Vouching + slashing verified.
### Phase 7 — Bearers & Processing Mesh (Component 12)
- **Wave**: User-Facing
- **REQs**: REQ-007 (FCFS), REQ-019 (6 bearers)
- **Deliverables**:
- FCFS processing mesh (not fee-auctioned)
- Geographic proximity wins (closest processor to recipient)
- Light client (~30 MB app, ~1-3% battery/day)
- OY-BLE bearer (10-100m)
- OY-WiFi-Direct bearer (50-200m)
- Unified Bearer Layer (parallel尝试, first-to-deliver wins, RFC 5050 DTN principles)
- Internet bearer (fallback)
- **Blocked by**: Phase 1, Phase 2, Phase 3, Phase 5
- **Owner**: backend-engineer + frontend-engineer
- **Tag**: v0.0.7
- **Demo**: Two phones transfer Bread via BLE. Processing mesh routes via FCFS. Light client runs.
### Phase 8 — Mesh Experience (Component 9)
- **Wave**: User-Facing
- **REQs**: REQ-010 (Exit layer), Maya's Day placeholder
- **Deliverables**:
- Maps v1 (discovery — where you are, what's around)
- Pay v1 (settlement — how value flows)
- React Native app (Expo, offline-first)
- Exit layer stub (DEX swaps, bridges)
- Maya's Day placeholder UI (full narrative deferred to component doc)
- **Blocked by**: Phase 6, Phase 7, Phase 5
- **Owner**: frontend-engineer
- **Tag**: v0.0.8
- **Demo**: Holder opens app, sees Maps, sends Bread via Pay, views Standing, sees Mirror ratio.
### Phase 9 — Final Review + Audit + Milestone Ship
- **Deliverables**:
- Multi-persona code review (all phases P1-P8)
- P0 fixes auto-applied, P1+ flagged for post-hoc
- Project health audit (reconstruction test, .ciagent discipline, branch hygiene)
- Milestone ship: merge phase/09 → milestone/v0.1-oy → main
- Tag v0.0.9 (final patch in v0.1 development line)
- Update REQUIREMENTS.md (mark milestone requirements complete)
- Update ROADMAP.md (mark milestone complete)
- Delete all milestone branches (tags preserve history)
- **Blocked by**: Phase 1-8 all shipped
- **Owner**: lead-developer
- **Tag**: v0.0.9
- **Note**: v0.1 milestone complete. MVP NOT released. Next milestone (v0.2) builds toward MVP at v0.1.0.
## Dependency Graph
### Cross-Phase Dependency Map
```
P0 (done) ──► P1 (Chain & Mirror)
P2 (Bread & Root Basket)
P3 (Storage)
┌────────┼────────┐
▼ ▼ ▼
P4 (Bloom) P5 (Fee) P6 (Identity)
│ │
└────┬───┘
P7 (Bearers)
P8 (Mesh UI)
P9 (Final)
P1 (Window, Stand, Guild) ──┬──► P2 (Pact, Partner) [Window refs + Stand refs]
├──► P3 (Council, Forex) [Stand + Guild refs; Forex independent]
└──► P4 (Bond, Bearers, L2) [Stand refs for Bond; Bearers/L2 independent]
P2 ──► P3 (Partner/Pier feeds Forex oracle interface)
P3 ──► P4 (Council governance over Bond market; not a hard type-dep, soft ordering)
P4 ──► P5 (all execution phases complete before review/ship)
```
## Parallelization Opportunities
Hard cross-phase blockers (by-ID-string refs, no import cycles):
- P2 Pact `StandRegistry` references `x/stand` StandRef (P1-02-01 must land first).
- P2 Partner `Window`-scoped actions reference `x/window` WindowRef (P1-01-01 must land first).
- P3 Council `Stand` Council references `x/stand` + `x/guild` (P1-02-01, P1-03-01 must land first).
- P3 Forex oracle interface is consumed by Piers (P2 Partner) — soft ordering, no type dep.
- P4 Bond issuer references `x/stand` StandRef (P1-02-01 must land first).
After P3 ships:
- P4 (Bloom), P5 (Fee), P6 (Identity) can run in parallel (all depend only on P1-P3)
- P5 depends only on P3; P6 depends on P1+P3; P4 depends on P2+P3
---
After P5+P6 ship:
- P7 (Bearers) depends on P1+P2+P3+P5
- P8 (Mesh UI) depends on P5+P6+P7
## Phase P1 — Orgs + Window Foundation
## Vertical Slice Integrity
- **Slug**: `orgs-window-foundation`
- **Branch**: `oy/phase/01-orgs-window-foundation`
- **REQs covered**: REQ-015 (Window — *fullest* per D-023), REQ-016 (9 Stands), REQ-017 (Guilds + Hand-Pass 0%)
- **Tag**: `v0.1.1`
- **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.
| After Phase | Demoable End State |
|---|---|
| P1 | Chain runs, Watchers attest, Mirror live |
| P2 | Bread minted against RWA, NAV published |
| P3 | Holder has Stash, Root-Pool visible |
| P4 | Bloom accrues to Stash automatically |
| P5 | Fees enforced, waivers work, auto-decline |
| P6 | Freeholder transition computed, vouching works |
| P7 | Transact via BLE/WiFi-Direct, FCFS processing |
| P8 | Full app: Maps, Pay, Standing, Mirror ratio |
| P9 | Milestone v0.1 complete, ready for v0.2 |
### Wave 1 — Foundation module types (parallel; no internal deps)
## Phase 2+ (Future Milestones)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P1-01-01 | REQ-015 | cosmos-engineer | `x/window/types/types.go` | `Window` struct (id, grantor-holder, grantee, scope, start, end, rate-limit, revoked, audit-log-refs); `ScopeKind` enum (ReadStash, ReadStanding, ProcessPassActForStand, …); `Scope` struct (kind, resource-id); `RateLimit` struct (max-actions, per-duration-seconds, actions-consumed); `AuditEntry` struct (timestamp, action, result, granter-ref); `WindowStatus` enum (Open, Active, Revoked, Expired); `Revoke()` stub; `DefaultParams`, `GenesisState`, `DefaultGenesisState`, `ValidateGenesis` (reject dup window-ids); consts `ModuleName`/`StoreKey`/`RouterKey`/`QuerierRoute` | `go build ./x/window/...` succeeds; `Window` struct fields match RESEARCH §1.3; `ValidateGenesis` returns error on dup window-ids (A-212 upgrade) | — |
| P1-02-01 | REQ-016 | cosmos-engineer | `x/stand/types/types.go` | `StandType` enum (Household, Crew, Entity, Coop, Circle, Trust, Foundation, Confederation, Shadow) — exactly 9; `AllStandTypes() []StandType`; `Stand` struct (id, type, name, vault-id, admin-reach, created-at, member-count); `Membership` struct (stand-id, reach-id, joined-at, role); `StandRole` enum (Member, Admin, Observer); `StandPolicy` stub (threshold/weighted — mirror `x/group` DecisionPolicy, A-205); `DefaultParams`, `GenesisState` (stands + memberships), `DefaultGenesisState`, `ValidateGenesis` (reject dup stand-ids, dup member-reach within a stand) | `go build ./x/stand/...` succeeds; `AllStandTypes()` returns exactly 9 entries with vision §11 names; `ValidateGenesis` rejects dup stand-id and dup member-reach | — |
| P1-03-01 | REQ-017 | cosmos-engineer | `x/guild/types/types.go` | `Guild` struct (id, name, founder-reach, created-at, stand-affiliation-id optional); `HandPass` struct (pass-id, guild-id, issuer-reach, recipient-reach, amount-grain, timestamp, fee-grain-always-0); `HandPassFeeBps = 0` locked `const` (cross-doc to feecovenant `WaiverHandPassGuild`); `IssueHandPass(...)` stub; `DefaultParams`, `GenesisState` (guilds + hand-passes), `DefaultGenesisState`, `ValidateGenesis` (reject dup guild-ids, dup pass-ids) | `go build ./x/guild/...` succeeds; `HandPassFeeBps == 0`; `ValidateGenesis` rejects dup guild-id | — |
### Milestone v0.2 — The Mesh
- Window, Pacts, Orgs, Partners, Governance, OY-LR, Bonds
- Target: $1B annual volume (simulation)
### Wave 2 — Tests + genesis validation (parallel; blocked-by Wave 1)
### Milestone v0.3 — The Bearers
- Cross-Chain, OY-SAT, OY-QR, Hub API, Anchors, full bond market
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P1-01-02 | REQ-015 | security-engineer | `x/window/types/types_test.go` | 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 | P1-03-01 |
### Milestone v0.4 — Maturity
- Cross-chain expansion, Confederation, Yield Token
### Wave 3 — Phase verification + lexicon meta-test scaffolding + ship
### MVP Release — v0.1.0
- After v0.4 milestone completes and system is validated as production-ready
- First public release to real Holders
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P1-04-01 | REQ-012, D-033 | lead-developer | (cross-cutting) | 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 | P1-01-02, P1-01-03, P1-02-02, P1-02-03, P1-03-02, P1-04-02 |
| P1-04-02 | REQ-012 | security-engineer | `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`.
- [ ] Window lifecycle tests: Open→Active→Revoked→Expired; revoke-after-expire no-op; double-revoke handled.
- [ ] 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.
- **`x/group` DecisionPolicy shape drift** (A-205) → `StandPolicy` mirrors `threshold` + `weighted` fields; cosmos-engineer reviews.
---
## Phase P2 — Pacts + Partners
- **Slug**: `pacts-partners`
- **Branch**: `oy/phase/02-pacts-partners`
- **REQs covered**: REQ-020 (6 Pacts), REQ-018 (4-tier Partner Spectrum)
- **Tag**: `v0.1.2`
- **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.
### Wave 1 — Module types (parallel; blocked-by P1 foundation)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P2-01-01 | REQ-020 | backend-engineer | `x/pact/types/types.go` | `PactType` enum (Pause, Ground, Stance, Cover, StandRegistry, HubAPI) — exactly 6 (A-207); `Pact` struct (id, type, parties, terms-bytes, status, execute-msg-ref, window-id-ref, stand-id-ref); `PactStatus` enum (Proposed, Active, Fulfilled, Voided); per-type `Execute*` stub methods (`ExecutePause`, `ExecuteGround`, `ExecuteStance`, `ExecuteCover`, `ExecuteStandRegistry`, `ExecuteHubAPI`); Mission-Lock invariant: Pause/Ground/Stance core terms non-amendable (const flag); `DefaultParams`, `GenesisState` (pacts), `DefaultGenesisState`, `ValidateGenesis` (reject dup pact-ids) | `go build ./x/pact/...` succeeds; `AllPactTypes()` returns exactly 6 with vision §16 names; `ValidateGenesis` rejects dup pact-id | P1-01-01, P1-02-01 |
| P2-02-01 | REQ-018 | backend-engineer | `x/partner/types/types.go` | `PartnerTier` enum (Op, MasterOp, Pier, Anchor) — exactly 4 (D-026); `Partner` struct (id, tier, name, reach-id, region, credential-ref, status); `CredentialRef` struct (provider-id, credential-type, ref-uri — opaque URI, Pier routing deferred per Q5); `PartnerStatus` enum (Pending, Active, Suspended, Revoked); registry keeper stub: `AddPartner`, `GetPartner`, `ListPartners`, `ListByTier`; `DefaultParams`, `GenesisState` (partners), `DefaultGenesisState`, `ValidateGenesis` (reject dup partner-ids) | `go build ./x/partner/...` succeeds; `AllPartnerTiers()` returns exactly 4 with vision §13 names; `ValidateGenesis` rejects dup partner-id | P1-01-01 |
### Wave 2 — Tests (parallel; blocked-by Wave 1)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P2-01-02 | REQ-020 | security-engineer | `x/pact/types/types_test.go` | Locked-const test: `AllPactTypes()` returns exactly 6; names match vision §16; no extras/dups; Mission-Lock invariant test (Pause/Ground/Stance `AmendableCoreTerms == false`); `PactStatus` enum coverage; `Execute*` stubs callable; `ValidateGenesis` rejects dup pact-id; **lexicon assertion** (no "bank"/"account" etc.) | `go test ./x/pact/...` passes; ≥80% coverage; 6-type locked-const test breaks on add/remove/rename; Mission Lock invariant green | P2-01-01 |
| P2-02-02 | REQ-018 | security-engineer | `x/partner/types/types_test.go` | Locked-const test: `AllPartnerTiers()` returns exactly 4 (Op, MasterOp, Pier, Anchor); tier names lexicon-clean (no "operator" spelled out if banned — verify "Op" is safe per vision §13); `PartnerStatus` enum coverage; registry stubs: `AddPartner`/`GetPartner`/`ListByTier` round-trip; `ValidateGenesis` rejects dup partner-id; **lexicon assertion** | `go test ./x/partner/...` passes; ≥80% coverage; 4-tier locked-const test breaks on change | P2-02-01 |
### Wave 3 — Phase verification + ship
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P2-03-01 | REQ-012, D-033 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; P2 packages ≥80% coverage; 2 lexicon assertions present; tag `v0.1.2` | `go test ./...` green; coverage ≥80% on `x/pact/types`, `x/partner/types`; git tag `v0.1.2` | P2-01-02, P2-02-02 |
### P2 Must-Haves
- [ ] `x/pact`, `x/partner` each have `types/types.go` + `types/types_test.go`.
- [ ] `go build ./...` and `go test ./...` green.
- [ ] ≥80% coverage on `x/pact/types`, `x/partner/types`.
- [ ] Pact locked-const: exactly 6 types (vision §16 names).
- [ ] Partner locked-const: exactly 4 tiers (Op, MasterOp, Pier, Anchor).
- [ ] Mission-Lock invariant: Pause/Ground/Stance core terms non-amendable.
- [ ] Lexicon assertion in both new test files.
- [ ] `ValidateGenesis` ID-uniqueness checks.
- [ ] Git tag `v0.1.2`.
### P2 Risks & Mitigations
- **Cover Pool seniority deferred** (Q7) → Cover Pact is a flat commitment type with no seniority fields; test asserts no seniority fields exist.
- **"Master Op" lexicon risk** → security-engineer's lexicon assertion scans partner-type strings; "Op" verified safe per vision §13.
- **6 micro-modules vs 1 module** (A-207) → one `x/pact` module with `PactType` enum + per-type execute stubs; matches v0.1 one-module-per-component pattern.
---
## Phase P3 — Councils + Forex
- **Slug**: `councils-forex`
- **Branch**: `oy/phase/03-councils-forex`
- **REQs covered**: REQ-011 (3 Councils + Mission Lock), Forex Engine v1
- **Tag**: `v0.1.3`
- **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.
### Wave 1 — Module types (parallel; blocked-by P1 + P2)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P3-01-01 | REQ-011 | cosmos-engineer | `x/council/types/types.go` | `CouncilType` enum (Mesh, Guild, Stand) — exactly 3; `Proposal` struct (id, council, proposer-reach, submit-time, voting-period, status); `ProposalStatus` enum (Pending, Active, Succeeded, Failed, Executed) — mirror OZ/Governor + `x/gov`; `VoteOption` enum (Yes, No, Abstain — no "no-with-veto", anti-greed); `TallyResult` struct (yes, no, abstain, total-voice, quorum-met) — mirror `x/gov` `TallyResult` (A-204); `VoiceSource` enum (Stash, Standing, Vouch, Freeholder, Guild) — multi-source weighting inputs; `MissionLockAmendable = false` locked `const bool` (A-211); Stand Council references `x/stand` by stand-id-string; Guild Council references `x/guild` by guild-id-string (no struct imports); `DefaultParams`, `GenesisState` (proposals + tallies), `DefaultGenesisState`, `ValidateGenesis` (reject dup proposal-ids) | `go build ./x/council/...` succeeds; `AllCouncilTypes()` returns exactly 3; `MissionLockAmendable == false`; `TallyResult` fields match `x/gov` shape | P1-02-01, P1-03-01 |
| P3-02-01 | Forex v1 | cosmos-engineer | `x/forex/types/types.go` | `ForexPair` struct (base-asset, quote-asset — NOT "currency"/"dollar"/"euro"; use "Bread/Asset" style labels, A-208); `RateOracle` Go interface (`GetRate(pair) (rate, timestamp, err)` — no impl); `ForexRate` struct (pair, rate, source, timestamp, confidence); `SpreadCapBps` const (locked; placeholder 0 with documented TBD, A-214 — test asserts ≥0); stub keeper `GetRate` returns sentinel "not-integrated"; `DefaultParams`, `GenesisState` (pairs + rates), `DefaultGenesisState`, `ValidateGenesis` (reject dup pair-ids) | `go build ./x/forex/...` succeeds; `RateOracle` interface compiles; `SpreadCapBps >= 0`; no banned terms in pair-label fields | — (Forex independent; soft order after P2 Partner for oracle-consumer narrative) |
### Wave 2 — Tests + genesis (parallel; blocked-by Wave 1)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P3-01-02 | REQ-011 | security-engineer | `x/council/types/types_test.go` | Locked-const test: `AllCouncilTypes()` returns exactly 3 (Mesh, Guild, Stand); **Mission Lock invariant**: `MissionLockAmendable == false` and a test asserting it can never be set true (the regression firewall); `ProposalStatus` enum coverage (5 states); `VoteOption` enum coverage (3 options, no veto); `TallyResult` struct shape matches `x/gov`; `VoiceSource` enum coverage (5 sources); `ValidateGenesis` rejects dup proposal-id; **lexicon assertion** (no "account" — use "Holder"/"Reach") | `go test ./x/council/...` passes; ≥80% coverage; Mission Lock invariant test is the highest-severity regression firewall | P3-01-01 |
| P3-01-03 | REQ-011 | data-engineer | `x/council/types/genesis.go` | Voice tally genesis shape: proposals + tallies in `GenesisState`; `ValidateGenesis` enforces tally-proposal referential integrity (tally.proposal-id exists) | `go test ./x/council/...` passes; referential integrity test green | P3-01-01 |
| P3-02-02 | Forex v1 | security-engineer | `x/forex/types/types_test.go` | `ForexPair` struct fields lexicon-clean (assert no "currency"/"dollar"/"euro"/"fx" in field names or sample labels); `RateOracle` interface signature compiles + a stub impl satisfies it; `SpreadCapBps >= 0`; stub keeper `GetRate` returns sentinel; `ValidateGenesis` rejects dup pair-id; **lexicon assertion** (highest lexicon-risk module per RESEARCH §1.10) | `go test ./x/forex/...` passes; ≥80% coverage; lexicon assertion scans pair-label sample data | P3-02-01 |
### Wave 3 — Phase verification + ship
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P3-03-01 | REQ-012, D-033 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; P3 packages ≥80% coverage; 2 lexicon assertions; Mission Lock invariant green; tag `v0.1.3` | `go test ./...` green; coverage ≥80% on `x/council/types`, `x/forex/types`; Mission Lock invariant passes; git tag `v0.1.3` | P3-01-02, P3-01-03, P3-02-02 |
### P3 Must-Haves
- [ ] `x/council`, `x/forex` each have `types/types.go` + `types/types_test.go`.
- [ ] `go build ./...` and `go test ./...` green.
- [ ] ≥80% coverage on `x/council/types`, `x/forex/types`.
- [ ] Council locked-const: exactly 3 types (Mesh, Guild, Stand).
- [ ] **Mission Lock invariant**: `MissionLockAmendable == false`, test asserts non-amendable (highest-severity).
- [ ] `TallyResult` shape mirrors `x/gov` (A-204) for future wiring.
- [ ] `VoteOption` has no "no-with-veto" (anti-greed).
- [ ] Forex pair labels lexicon-clean (no "currency"/"dollar"/"euro"); `RateOracle` interface compiles.
- [ ] Lexicon assertion in both new test files.
- [ ] `ValidateGenesis` ID-uniqueness + referential integrity (Council).
- [ ] Git tag `v0.1.3`.
### P3 Risks & Mitigations
- **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`.
---
## Phase P4 — Bonds + Bearers + L2
- **Slug**: `bonds-bearers-l2`
- **Branch**: `oy/phase/04-bonds-bearers-l2`
- **REQs covered**: REQ-021 (Mesh Bond Market 8% cap), REQ-009 (L2 IBC Satellite — Polygon rep + 4 stubs), Bearers OY-LR + Beacon v1 (D-029)
- **Tag**: `v0.1.4`
- **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.
### Wave 1 — Module types (parallel; Bond blocked-by P1 Stand, Bearers/L2 independent)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P4-01-01 | REQ-021 | backend-engineer | `x/bond/types/types.go` | `CouponCapBps = 800` (8%) and `CouponFloorBps = 0` (0%) locked `const` (D-028); `Bond` struct (id, issuer-stand-id, principal-grain, coupon-bps, term-days, issued-at, maturity, status); `BondStatus` enum (Issued, Active, Matured, Defaulted, Repaid); `Issue(...)` stub; `Clamp(couponBps)` returns `min(cap, max(floor, coupon))` — same shape as Fee Covenant's `Clamp`; issuer-stand-id references `x/stand` by id-string; `DefaultParams`, `GenesisState` (bonds), `DefaultGenesisState`, `ValidateGenesis` (reject dup bond-ids) | `go build ./x/bond/...` succeeds; `CouponCapBps == 800`; `CouponFloorBps == 0`; `Clamp` matches Fee Covenant shape; `ValidateGenesis` rejects dup bond-id | P1-02-01 |
| P4-02-01 | D-029 | cosmos-engineer | `x/bearers/types/types.go` (extend existing) | **EXTEND** existing `x/bearers` (do NOT create new module — A-209); add `BearerTransport` Go interface (`Send`, `Receive`, `Status` — no impl); `OYLRLink` struct (gateway-id, range-meters, frequency-mhz, surveillance-resistant=true); `BeaconFrame` struct (beacon-id, ephemeral-id, payload-bytes, ttl); preserve existing `BearerType` enum + `AllBearers()` (OY-LR already present); `DefaultParams`/`GenesisState` unchanged | `go build ./x/bearers/...` succeeds; `BearerTransport` interface compiles; `OYLRLink` + `BeaconFrame` structs present; existing `AllBearers()` unchanged (6 bearers) | — |
| P4-03-01 | REQ-009 | cosmos-engineer | `x/satellite/types/types.go` | `L2Chain` enum (Polygon active; Base, Arbitrum, Optimism, Solana as `StatusPending` per D-021); `TransferChannel` struct (port-id, channel-id, counterparty, status); `ChannelStatus` enum (Init, TryOpen, Open, Closed) — mirror ICS-20 handshake; `WrappedBreadDenom` struct (denom, trace-path) encoding IBC trace; `Packet` stub struct (sequence, source-port, source-channel, dest-port, dest-channel, data, timeout-height, timeout-timestamp) — pinned to ICS-20 v1 shape (A-215); no ibc-go import (zero deps); `DefaultParams`, `GenesisState` (channels + denoms), `DefaultGenesisState`, `ValidateGenesis` (reject dup channel-ids) | `go build ./x/satellite/...` succeeds; `L2Chain` enum has 5 entries (Polygon + 4 stubs); `Packet` struct fields match ICS-20 v1; zero external deps confirmed | — |
### Wave 2 — Tests + genesis (parallel; blocked-by Wave 1)
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|---|---|---|---|---|---|---|
| P4-01-02 | REQ-021 | security-engineer | `x/bond/types/types_test.go` | **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 clamp invariant: `CouponCapBps == 800`, `CouponFloorBps == 0`; clamp below→floor, above→cap, in-range→unchanged.
- [ ] Bond lexicon: "coupon" exclusively, no "interest"/"yield" (A-210).
- [ ] Bearers: `BearerTransport` interface compiles; `OYLRLink` + `BeaconFrame` stubs; existing `AllBearers()` (6) unchanged.
- [ ] Satellite: `L2Chain` exactly 5 (Polygon active + 4 stubs); `Packet` pinned to ICS-20 v1 shape; zero external deps.
- [ ] Lexicon assertion in all 3 test files.
- [ ] `ValidateGenesis` ID-uniqueness (all 3) + genesis clamp (Bond).
- [ ] Git tag `v0.1.4`.
### P4 Risks & Mitigations
- **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.
---
## Phase P5 — Final Review + Audit + Ship
- **Slug**: `final-review-audit-ship`
- **Branch**: `oy/phase/05-final-review-audit-ship`
- **REQs covered**: REQ-012 (lexicon, project-wide meta-test), all v0.2 REQs (audit confirmation)
- **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 | P4-04-01 |
| P5-01-03 | REQ-009..021 | lead-developer | `.ciagent/oy/PLANS.md` (this file, checklist update) + `.ciagent/oy/REQUIREMENTS.md` + `.ciagent/oy/ROADMAP.md` | 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.
---
## Coverage Targets (D-033)
| Package | Phase | Target | Locked-const tests |
|---|---|---|---|
| `x/window/types` | P1 | ≥80% | WindowStatus count, lifecycle idempotency |
| `x/stand/types` | P1 | ≥80% | 9 Stand types (exactly) |
| `x/guild/types` | P1 | ≥80% | `HandPassFeeBps == 0` |
| `x/pact/types` | P2 | ≥80% | 6 Pact types (exactly), Mission Lock non-amendable |
| `x/partner/types` | P2 | ≥80% | 4 Partner tiers (exactly) |
| `x/council/types` | P3 | ≥80% | 3 Councils (exactly), `MissionLockAmendable == false` |
| `x/forex/types` | P3 | ≥80% | `SpreadCapBps >= 0`, lexicon-clean labels |
| `x/bond/types` | P4 | ≥80% | `CouponCapBps == 800`, `CouponFloorBps == 0`, clamp invariant |
| `x/bearers/types` | P4 | ≥80% | `BearerTransport` interface, OY-LR in `AllBearers()` (regression) |
| `x/satellite/types` | P4 | ≥80% | 5 L2 chains (Polygon active + 4 stubs), ICS-20 Packet shape |
**Lexicon assertion (REQ-012)**: present in every new module's `types_test.go` (10 files) + project-wide meta-test in P5.
---
## Task Count Summary
| Phase | Waves | Tasks | New/Extended Packages |
|---|---|---|---|
| P1 | 3 | 8 | window, stand, guild (3) + lexicon meta-test scaffolding (G-004) |
| P2 | 3 | 5 | pact, partner (2) |
| P3 | 3 | 6 | council, forex (2) |
| P4 | 3 | 8 | bond, bearers (ext), satellite (3) |
| P5 | 2 | 4 | (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:
| ID | Decision | Applied to |
|---|---|---|
| G-001 | Correct v0.1 baseline test count: 53 tests / 11 files (not 48) | PROJECT.md D-033, RESEARCH.md |
| G-002 | Lexicon assertion tests are NEW in v0.2 (v0.1 has zero lexicon tests); firewall is new work, not inherited | PROJECT.md D-032/D-033, RESEARCH.md, PLANS.md P1-04-02 |
| G-003 | By-ID-string inter-module refs (A-203) enforced as a TESTED invariant in P1-01-02 (go/parser scan, no cross-module struct imports in production code) | PLANS.md P1-01-02, RESEARCH.md §3 |
| G-004 | Lexicon meta-test scaffolding moved from P5 to P1 Wave 3 (new task P1-04-02); P5-01-01 EXTENDS it rather than creating it | PLANS.md P1 Wave 3, P5-01-01 |
| G-005 | D-027 clarified: one `x/pact` module with `PactType` enum + 6 per-type execute-entry structs (A-207), NOT six micro-modules | PROJECT.md D-027 |
| G-006 | `go.mod` is read-only in v0.2 (zero deps, A-201); any change is an escalation | PERSONAS.md (all personas) |
| G-007 | `x/pact`/`x/partner`/`x/bond` owned by backend-engineer; `x/window`/`x/stand`/`x/guild`/`x/council`/`x/satellite`/`x/forex`/`x/bearers` owned by cosmos-engineer | PERSONAS.md |
| G-008 | Genesis schema (`genesis.go`) = data-engineer; genesis test assertions (`*_test.go` incl `genesis_test.go`) = security-engineer | PERSONAS.md |
| G-009 | P5-01-01 lexicon meta-test includes a self-test table (synthetic string per banned term) so detection coverage is durably verified | PLANS.md P1-04-02, P5-01-01 |
| G-010 | P5-01-03 reconciles ROADMAP.md tag-line narrative (v0.0.x vs v0.1.x) to prevent `v0.1.5`/`v0.1.0` confusion | PLANS.md P5-01-03 |
## Per-Phase REQ Coverage
| Phase | REQs | Components |
|---|---|---|
| P1 | REQ-015, REQ-016, REQ-017 | Window (fullest), 9 Stands, Guilds + Hand-Pass 0% |
| P2 | REQ-020, REQ-018 | 6 Pacts, 4-tier Partner Spectrum |
| P3 | REQ-011, Forex v1 | 3 Councils + Mission Lock, Forex Engine v1 stub |
| P4 | REQ-021, REQ-009, Bearers | Mesh Bond 8% cap, L2 IBC (Polygon rep), OY-LR + Beacon |
| P5 | REQ-012 + all v0.2 REQs (audit) | Lexicon meta-test, coverage gate, milestone ship |
## Cross-Phase Blockers (hard)
- **P1-01-01 (Window types)** → blocks P2-02-01 (Partner Window-scoped actions), P2-01-01 (Pact window-id-ref).
- **P1-02-01 (Stand types)** → blocks P2-01-01 (Pact StandRegistry stand-id-ref), P3-01-01 (Council Stand Council), P4-01-01 (Bond issuer-stand-id).
- **P1-03-01 (Guild types)** → blocks P3-01-01 (Council Guild Council).
- **P4-04-01 (P4 ship)** → blocks P5-01-01, P5-01-02, P5-01-03 (P5 audit).
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene; types themselves only depend on the listed hard blockers).
+48 -1
View File
@@ -61,4 +61,51 @@ OpenYield (OY) is a durable, anti-greed, jurisdiction-light financial layer —
- D-009: Rebased history to fix v1.0 → v0.1 in ---ci--- blocks
## Milestone
v0.1OpenYield Foundation Init (pre-MVP development milestone; remains v0.1 until MVP ships as v0.1.0)
v0.2The Mesh (active milestone; feature type; tags run on the v0.1.x patch line)
### v0.2 Scope (The Mesh — ROADMAP Phase 2)
Target: $1B annual volume, 4 service categories. Implements the pending Mesh-era requirements:
- **REQ-009** Satellite chains (Layer 2) — wrapped Bread, Pass-Act propagation on Polygon/Base/Arbitrum/Optimism/Solana [§7]
- **REQ-011** Three Councils (Mesh, Guild, Stand) with Mission Lock — multi-source Voice [§19]
- **REQ-015** Window primitive — scope, duration, rate-limit, audit log, revoke [§10]
- **REQ-016** Nine Stand types (Household, Crew, Entity, Co-op, Circle, Trust, Foundation, Confederation, Shadow) [§11]
- **REQ-017** Guilds with Hand-Passes at 0% protocol fee [§12]
- **REQ-018** Four-tier Partner Spectrum (Op, Master Op, Pier, Anchor) [§13]
- **REQ-020** Six Pacts (Pause, Ground, Stance, Cover, Stand Registry, Hub API) [§16]
- **REQ-021** Mesh Bond Market with 8% upper coupon cap, 0% floor [§17]
- Bearers expansion: OY-LR + Beacon v1
- Forex Engine v1
### Milestone Type
Feature (at least one `feat` phase). Phase 0 → `v0.1.0`; execution phases `v0.1.1..v0.1.N`; final phase patch IS the milestone release. No separate minor tag.
### Out of Scope (v0.2)
- Cross-chain exit / DEX integration (Phase 3 / v0.3)
- OY-SAT, OY-QR bearers (Phase 3)
- Full Hub API B2B suite (Phase 3)
- Yield Token, Travel + 11 service categories (Phase 4)
- Full Mesh Bond market depth (Phase 3+; v0.2 ships first Mesh Bonds only)
### Prior Milestone
v0.1 — OpenYield Foundation Init (COMPLETE; pre-MVP foundation skeleton; released as v0.0.9 per run.md patch-line model)
## Clarification Decisions (Phase 0 — CLARIFY, autonomy=full)
Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → accept defaults, log decisions).
| ID | Decision | Rationale | Confidence | Alternatives |
|----|----------|-----------|------------|--------------|
| D-020 | v0.2 ships a **skeleton + tests** layer (Go types + keeper stubs + unit tests) for each Mesh-era component, matching v0.1's pre-MVP approach; no live chain launch in v0.2 | v0.1 established the skeleton-first pattern; ROADMAP "Year 2" targets are aspirational, not v0.2 deliverables. Consistency with v0.1 reduces risk. | 0.85 | [full Go implementations, live Cosmos chain launch] |
| D-021 | **REQ-009 L2 satellites**: skeleton = IBC light-client + transfer-channel types for ONE L2 (Polygon) as representative; remaining 4 chains stubbed as enum placeholders | Full 5-chain IBC rollout is Phase 3 scope; v0.2 proves the pattern with one chain. | 0.80 | [all 5 chains in v0.2, defer all L2 to v0.3] |
| D-022 | **REQ-011 Three Councils**: skeleton = 3 module stubs (mesh/guild/stand council keeper + Voice tally types), Mission Lock enforced as a const; no live governance in v0.2 | Governance activation needs Holders (Year 2 target); v0.2 lands the typed scaffold + Mission Lock invariant tests. | 0.82 | [full governance runtime, defer to v0.4] |
| D-023 | **REQ-015 Window**: full primitive — scope/duration/rate-limit/audit-log/revoke types + keeper + lifecycle tests. This is a leaf component with no upstream blocker, so it can be more complete. | Window is self-contained and required by Pacts/Orgs/Partners; a fuller implementation unblocks v0.3. | 0.75 | [skeleton-only Window] |
| D-024 | **REQ-016 Nine Stands**: enum (9 named types) + Stand keeper + membership-set types; no Stand-internal economics in v0.2 | Stand economics (revenue distribution, bonding) is Phase 3+. | 0.80 | [full Stand economics] |
| D-025 | **REQ-017 Guilds**: Guild keeper + Hand-Pass type at 0% protocol fee (locked const) + issuance tests; no Guild-internal task queue | Guild task management is operational, not protocol-level. | 0.78 | [full Guild ops runtime] |
| D-026 | **REQ-018 Partner Spectrum**: 4-tier enum (Op/MasterOp/Pier/Anchor) + Partner registry keeper + credential-ref types; Pier credential routing (e-Residency, biometrics) deferred per PROJECT.md out-of-scope Q5 | Credential routing was explicitly deferred in v0.1 PROJECT.md. | 0.85 | [include credential routing now] |
| D-027 | **REQ-020 Six Pacts**: **one `x/pact` module** with a `PactType` enum (Pause/Ground/Stance/Cover/Stand-Registry/Hub-API) + six per-type execute-entry structs (per A-207), NOT six micro-modules. Each execute-entry has invariant tests; Cover Pool seniority deferred per Q7 | Cover Pool seniority is explicitly out-of-scope (PROJECT.md Q7); one module with enum satisfies "separate execute entries" without 6 dirs | 0.80 | [full Cover Pool mechanics, six separate modules] |
| D-028 | **REQ-021 Mesh Bonds**: Bond market keeper + 8% cap / 0% floor consts + issuance tests; full secondary-market depth deferred to Phase 3 | ROADMAP Phase 2 says "First Mesh Bonds" — first issuance, not full market. | 0.82 | [full bond market in v0.2] |
| D-029 | **Bearers OY-LR + Beacon v1**: skeleton bearer-interface types + OY-LR (long-range) + Beacon transport stubs; no hardware integration | Hardware/RF integration is not a v0.2 software deliverable. | 0.85 | [real bearer runtime, defer all bearers] |
| D-030 | **Forex Engine v1**: Forex pair type + rate-oracle interface + stub keeper; no live oracle integration | Live oracle integration depends on external partners (Piers), Phase 3. | 0.80 | [live oracle integration] |
| D-031 | **Phase ordering** follows ARCHITECTURE.md blocker chain: P1 Orgs+Window foundation → P2 Pacts+Partners → P3 Councils+Forex → P4 Bonds+Bearers+L2. The final phase (P5) is review/ship. | Respects dependency graph; vertical slices keep each phase independently shippable. | 0.80 | [different wave ordering] |
| D-032 | **Lexicon** enforced project-wide; all new modules must pass the lexicon assertion test (no banned terms). Non-negotiable. **Note (G-002)**: lexicon assertion tests are NEW in v0.2 — v0.1 is lexicon-clean in practice but has NO lexicon test firewall. v0.2 introduces the firewall (scaffolded in P1 per G-004, extended in P5). | REQ-012 is `All` phases. | 1.00 | [—] |
| D-033 | **Test coverage target**: ≥80% on new keeper/type packages. v0.1 baseline = **53 tests across 11 test files** (corrected per G-001; not 48). Add lexicon assertion to each new module's test file. | Consistency with v0.1 quality bar (53 tests verified); lexicon drift is the highest-severity regression. | 0.85 | [lower coverage bar] |
+31 -23
View File
@@ -1,25 +1,33 @@
# Requirements: OpenYield (oy)
| ID | Requirement | Vision § | Priority | Status |
|----|-------------|----------|----------|--------|
| REQ-001 | Enforce Six Principles | §2 | High | Pending |
| REQ-002 | Fee ceiling 0.1% / floor 0.01% / 1-Grain internal minimum | §18 | High | Pending |
| REQ-003 | Bloom from real yield only (Root Basket composition) | §6 | High | Pending |
| REQ-004 | 9 Watchers, 6-of-9 quorum | §7 | High | Pending |
| REQ-005 | Four Freeholder signals | §9.1 | High | Pending |
| REQ-006 | Standing anti-gaming formula | §9.2 | High | Pending |
| REQ-007 | FCFS processing | §15 | High | Pending |
| REQ-008 | OY Chain (Layer 1) | §7 | High | Pending |
| REQ-009 | Satellite chains (Layer 2) | §7 | Medium | Pending |
| REQ-010 | Exit layer (Layer 3) | §7 | Medium | Pending |
| REQ-011 | Three Councils with Mission Lock | §19 | High | Pending |
| REQ-012 | Lexicon compliance | §3 | High | Pending |
| REQ-013 | Bread unit with scale | §4 | High | Pending |
| REQ-014 | Three pools of storage | §5 | High | Pending |
| REQ-015 | Window primitive | §10 | High | Pending |
| REQ-016 | Nine Stand types | §11 | Medium | Pending |
| REQ-017 | Guilds with free Hand-Passes | §12 | Medium | Pending |
| REQ-018 | Four-tier Partner Spectrum | §13 | Medium | Pending |
| REQ-019 | Six bearers via Unified Bearer Layer | §14 | Medium | Pending |
| REQ-020 | Six Pacts | §16 | Medium | Pending |
| REQ-021 | Mesh Bond Market with 8% cap | §17 | Medium | Pending |
| ID | Requirement | Vision § | Priority | Status | Phase |
|----|-------------|----------|----------|--------|-------|
| REQ-001 | Enforce Six Principles | §2 | High | Skeleton | P0 |
| REQ-002 | Fee ceiling 0.1pct / floor 0.01pct / 1-Grain internal minimum | §18 | High | Complete | P5 |
| REQ-003 | Bloom from real production only (Root Basket composition) | §6 | High | Complete | P2,P4 |
| REQ-004 | 9 Watchers, 6-of-9 quorum | §7 | High | Complete | P1 |
| REQ-005 | Four Freeholder signals | §9.1 | High | Complete | P3,P6 |
| REQ-006 | Standing anti-gaming formula | §9.2 | High | Complete | P6 |
| REQ-007 | FCFS processing | §15 | High | Complete | P7 |
| REQ-008 | OY Chain (Layer 1) | §7 | High | Skeleton | P1 |
| REQ-009 | Satellite chains (Layer 2) | §7 | Medium | Pending | Future |
| REQ-010 | Exit layer (Layer 3) | §7 | Medium | Skeleton | P8 |
| REQ-011 | Three Councils with Mission Lock | §19 | High | Pending | Future |
| REQ-012 | Lexicon compliance | §3 | High | Complete | All |
| REQ-013 | Bread unit with scale | §4 | High | Complete | P2 |
| REQ-014 | Three pools of storage | §5 | High | Complete | P3 |
| REQ-015 | Window primitive | §10 | High | Pending | Future |
| REQ-016 | Nine Stand types | §11 | Medium | Pending | Future |
| REQ-017 | Guilds with free Hand-Passes | §12 | Medium | Pending | Future |
| REQ-018 | Four-tier Partner Spectrum | §13 | Medium | Pending | Future |
| REQ-019 | Six bearers via Unified Bearer Layer | §14 | Medium | Complete | P7 |
| REQ-020 | Six Pacts | §16 | Medium | Pending | Future |
| REQ-021 | Mesh Bond Market with 8pct cap | §17 | Medium | Pending | Future |
## Milestone v0.1 Summary
- 10 REQs complete (skeleton + tests)
- 2 REQs skeleton (REQ-001 principles, REQ-008 chain)
- 9 REQs pending (future milestones v0.2-v0.4)
- All locked constants verified by tests
- Lexicon fully compliant
- 48 unit tests passing across 11 modules
+615 -71
View File
@@ -1,88 +1,632 @@
# Research: OpenYield (oy) — Phase 0
# Research: OpenYield (oy) — Phase 0 (v0.2 — The Mesh)
## Domain Ecosystem
> Builds on v0.1 RESEARCH (stablecoin/RWA/mesh/reputation ecosystem survey). v0.1 prior art
> (DAI, MakerDAO RWA, Ondo, Maple, Centrifuge, Helium, Nodle, Kleros, Gitcoin Passport,
> LayerZero, Wormhole, Axelar, IBC) is not duplicated here. This file focuses on the **v0.2
> Mesh-era components**: REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020,
> REQ-021, plus Bearers OY-LR/Beacon and Forex Engine v1.
**Category**: Public-good financial infrastructure / real-yield stablecoin mesh.
## v0.2 Scope Recap (from D-020..D-033)
**Prior Art**:
- **Stablecoins**: USDC/USDT (centralized), DAI (crypto-collateralized, no real yield), Terra/UST (failed algorithmic).
- **RWA protocols**: MakerDAO RWA, Ondo Finance, Maple, Centrifuge, TrueFi.
- **Mesh networks**: Helium (IoT), Nodle (Bluetooth), FOAM (proof of location).
- **Reputation systems**: Kleros (dispute resolution), Gitcoin Passport (sybil resistance), Lens Protocol (social graph).
- **DAOs**: Aragon, DAOstack, Compound, Uniswap governance.
- **Cross-chain**: LayerZero, Wormhole, Axelar, IBC (Cosmos native).
v0.2 ships a **skeleton + tests** layer (Go types + keeper stubs + unit tests) for each
Mesh-era component, matching v0.1's pre-MVP approach. No live chain launch. Lexicon
(REQ-012) enforced everywhere. ≥80% coverage on new packages. Mission Lock and locked
constants enforced as `const` and verified by invariant tests.
**Gap OpenYield Fills**:
- No existing protocol combines real-yield stablecoin + mesh networking + reputation-based citizenship + public-good covenant.
- DAI lacks real yield (crypto-collateralized). MakerDAO RWA vaults exist but are DAO-controlled, not Holder-sovereign.
- Helium/Nodle cover physical mesh but not financial sovereignty.
- No protocol enforces a mission-lock covenant at the smart-contract level.
> **Lexicon firewall note (G-002)**: v0.1 is lexicon-clean in practice but has **zero**
> lexicon test files (verified: `grep -rli lexicon x/` is empty). The lexicon assertion
> tests are NEW in v0.2 — scaffolded as a project-wide meta-test in P1 Wave 3 (G-004),
> extended to all 10 packages in P5. Do not assume inherited lexicon test coverage.
> **v0.1 baseline (G-001)**: 53 tests across 11 test files (not 48).
## Architectural Pattern: Three-Layer Mesh
---
1. **Sovereign app-chain** (Layer 1) — Cosmos SDK / Polygon CDK / OP Stack.
- Pattern: dYdX v3 (Cosmos app-chain), Canto (Cosmos L1), Lens (zkSync).
2. **Satellite chains** (Layer 2) — wrapped asset propagation.
- Pattern: Wormhole-wrapped assets, LayerZero OFT, Circle CCTP.
3. **Exit layer** — DEX aggregation.
- Pattern: 1inch, Matcha, LI.FI.
## 1. Domain Research — Per-Component
## Mirror Pattern (Reserve Attestation)
### 1.1 REQ-009 — Layer 2 IBC Satellites (Polygon representative)
- **Pattern**: Paxos (NYDFS-regulated attestations), Tether (third-party attestations), MakerDAO PSM.
- **OpenYield difference**: 9 decentralized Watchers (vs centralized custodian), 6-of-9 quorum, slashing bonds, geographic diversity.
- **Implementation**: Cosmos SDK modules with cross-chain attestation queries; off-chain auditor signs daily snapshots; on-chain NAV updates gated by quorum.
**What it is:** Wrapped Bread propagation from OY Chain (L1) to satellite chains (L2) via
IBC, enabling Pass-Act (transfer) cross-chain. D-021 scopes v0.2 to ONE representative
chain (Polygon) with IBC light-client + transfer-channel types; the other four chains
(Base, Arbitrum, Optimism, Solana) are enum placeholders.
## Bayesian Reputation
**Prior art / ecosystem references:**
- **Cosmos IBC (ibc-go)** — the canonical cross-chain protocol. Transport layer (TAO)
provides clients, connections, channels; application layer (ICS-20 fungible transfer,
ICS-27 interchain accounts). IBC-Go v10 introduced **IBC v2**, a streamlined redesign
organized around Clients, Router, and Applications — client pairs replace multi-step
connection/channel handshakes, packets carry typed Payloads, timestamp-only timeouts
(universal across heterogeneous chains like EVM). IBC v2 is particularly suited to
EVM connectivity (IBC Eureka for Cosmos↔Ethereum).
- **ICS-20 (fungible token transfer)** — the standard application module for wrapped
asset transfer. Tokens are escrowed on the source chain and minted as vouchers on the
destination chain; the denom path encodes the trace (`transfer/channel-N/<orig-denom>`).
- **Polygon ↔ Cosmos IBC** — Polygon is an EVM chain; connecting it to a Cosmos chain
requires either an EVM-compatible light client (e.g., a Polygon PoS client or a ZK
client) or IBC Eureka. For a skeleton, the pattern is to define a client-type enum and
channel handshake state types without live relayer logic.
- **Alternative bridges** (noted but not used in skeleton): LayerZero OFT, Wormhole
NTT/Portal, Circle CCTP. OpenYield chose IBC-native (v0.1 RESEARCH) for sovereignty +
security; Wormhole/LayerZero remain Phase 3 fallbacks for chains lacking IBC clients.
- **Non-cosmos chains (Solana)** — Solana lacks native IBC; would need a grandpa/light
client wrapper or a 3rd-party bridge. Scoped out to Phase 3 per D-021.
- **Pattern**: PageRank (Google), EigenTrust (P2P), Kleros court reputation.
- **OpenYield formula**: Time-decayed Bayesian average with prior mean 4.0, prior weight 10, decay buckets [6mo:100%, 12mo:50%, 24mo:25%, 24mo+:0%], diversity bonus (3:+.05, 4:+.10, 5+:+0.15), voucher weight (Freeholder 1.5x).
- **Anti-gaming**: Vouchers have skin-in-the-game; vouching cascades are slashed.
**Recommendation for skeleton:** Define a `satellite` module with:
- `L2Chain` enum (Polygon active; Base/Arbitrum/Optimism/Solana as `StatusPending`).
- `TransferChannel` type (port-id, channel-id, counterparty, status) mirroring ICS-20
handshake states (Init, TryOpen, Open, Closed).
- `WrappedBreadDenom` type encoding the IBC trace path.
- A `Packet` stub type (sequence, source-port, source-channel, dest-port, dest-channel,
data, timeout-height, timeout-timestamp) matching ibc-go v4/v8 channel packet shape.
- No live ibc-go import in skeleton (deps not vendored); types are self-contained Go
structs so the skeleton compiles under `go 1.22` with zero external deps, same as v0.1.
## Tech Stack Recommendations
**Risk:** If the skeleton's packet shape diverges from the eventual ibc-go version chosen,
refactor cost is small (types-only). Pin the packet shape to ICS-20 v1 (stable, widely
implemented) to minimize churn.
| Layer | Recommendation | Rationale |
|---|---|---|
| OY Chain | Cosmos SDK + CometBFT | App-chain sovereignty, IBC for satellites, Rust performance |
| Smart contracts | CosmWasm (Rust) | Type-safe, auditable, IBC-native |
| Satellite bridges | IBC + Wormhole | Cosmos-native + EVM coverage |
| Light client | CosmJS / gRPC-web | Browser-compatible, ~30 MB app size |
| Mesh transport | libp2p + custom DTN | Delay-tolerant, carrier-agnostic |
| LoRa firmware | Rust on Embassy | Embedded, low-power |
| Off-chain indexer | Subsquid + GraphQL | Historical queries, audit logs |
| Frontend | React Native + Expo | Cross-platform (iOS/Android) |
---
## Risks Catalog (19 from §20)
### 1.2 REQ-011 — Three Councils (Mesh, Guild, Stand) with Mission Lock
1. Smart contract bugs → formal verification (Certora, KFramework), bug bounties, gradual rollout.
2. RWA venue failure → diversify across 5+ venues, insurance fund.
3. Eye manipulation → multi-source oracles (Chainlink + Pyth + UMA).
4. Anchor-Break (depeg) → over-collateralization, circuit breakers.
5. Processor centralization → random rotation, geographic diversity scoring.
6. Watcher compromise → slashing, fork recovery, geographic/org diversity.
7. Sovereign reserve dependency → multi-jurisdiction reserves.
8. Bank failure → segregated accounts, multiple banking partners.
9. Fiat collapse → Root Basket includes gold + Bitcoin as hedge.
10. Adoption risk → UX-first, Maya's Day integration narrative.
11. Governance capture → multi-source Voice, Freeholder auto-elevation.
12. RWA liquidity risk → short-duration Treasuries, liquid asset tiers.
13. Partner capture → self-service ceiling, Window revoke, partner markup cap.
14. Bond default → over-collateralization, seniority rules, Cover Pool.
15. Cross-chain bridge failure → IBC native, rate-limited bridges, canonical contract.
16. Sybil attacks on Standing → Bayesian prior, vouching requirements.
17. Window abuse → scope enforcement, rate-limits, audit log.
18. Forex Engine venue risk → multi-venue execution, spread caps.
19. Pier regulatory capture → pay-to-play model, Holder sovereignty preserved.
**What it is:** Three governance councils — Mesh Council (whole-mesh), Guild Council
(guild-level), Stand Council (Stand-level) — each using multi-source Voice. Mission Lock
(the Six Principles + fee covenant + no-amend covenant) cannot be amended by any council.
D-022 scopes v0.2 to 3 keeper stubs + Voice tally types + Mission Lock as a `const`.
## Open Questions (deferred to component docs)
**Prior art / ecosystem references:**
- **OpenZeppelin Governor** — modular on-chain governance: Governor + GovernorVotes +
GovernorVotesQuorumFraction + GovernorCountingSimple + GovernorTimelockControl. Key
patterns borrowed: (a) quorum as a fraction of supply, (b) snapshot-based voting power
(ERC20Votes / EIP-6372 clock) to prevent double-voting, (c) timelock between pass and
execution, (d) proposal lifecycle (Pending → Active → Succeeded → Queued → Executed).
OpenZeppelin's modular inheritance avoids forking GovernorAlpha/Bravo.
- **Compound Governor (Bravo)** — the original GovernorAlpha/Bravo; OpenZeppelin is
compatible-but-modular. OY's three-council structure is a **federated** variant: three
Governor instances with different voter bases and scopes, not one.
- **MakerDAO governance** — multi-stakeholder (MKR holders + governance facade), executive
voting + polling, spell-based execution. Relevant for "multi-source Voice" (different
signal sources weighted differently).
- **Cosmos SDK `x/gov`** — the Cosmos-native governance module: proposals, voting period,
deposit, tally (yes/no/no-with-veto/abstain), quorum + threshold. This is the closest
on-chain pattern to OY's Cosmos-based stack; a Cosmos app-chain would naturally use
`x/gov` semantics per council. For skeleton, model the tally types on `x/gov`'s
`TallyResult` and `VoteOption`.
- **Multi-source Voice** — not standard in OZ/Compound/Cosmos gov. Closest analog: **Quadratic
Voting** (Gitcoin, DemocraciDAO) and **conviction voting** (1Hive Gardens, Tecommons)
where voting power accrues over time. OY's multi-source Voice (Standing, Stash, Vouch,
etc.) is a custom weighted-tally; skeleton defines the weight source enum + tally struct
but not the live aggregation.
| # | Question | Owning Component | Resolution |
**Recommendation for skeleton:** Define a `council` module (or three sub-packages
`meshcouncil`, `guildcouncil`, `standcouncil`) with:
- `CouncilType` enum (Mesh, Guild, Stand).
- `Proposal` type (id, council, proposer, submit-time, voting-period, status).
- `ProposalStatus` enum mirroring OZ/Governor lifecycle: Pending → Active → Succeeded →
Failed → Executed.
- `VoteOption` enum (Yes, No, Abstain — no "no-with-veto" since OY covenant disallows
adversarial veto games per anti-greed principle).
- `TallyResult` struct (yes, no, abstain, total-voice, quorum-met).
- `VoiceSource` enum (Stash, Standing, Vouch, Freeholder, Guild) — the multi-source
weighting inputs.
- `MissionLockAmendable = false` as a `const bool`; an invariant test asserts it is never
set true (Mission Lock cannot be amended).
**Risk:** Multi-source Voice weighting math is complex and unstated in the vision at
formula level — keep skeleton to tally *structure*, not the weighting algorithm. Flag
the algorithm as a v0.3+ decision.
---
### 1.3 REQ-015 — Window Primitive (fuller implementation per D-023)
**What it is:** A Window is a Holder-authorized, scope-bounded, time-limited, revocable
delegation of access (to a partner, a service, a Stand). Properties: scope, duration,
rate-limit, audit log, revoke. D-023 makes this the *fullest* v0.2 component because it's
a leaf with no upstream blocker and is required by Pacts/Orgs/Partners.
**Prior art / ecosystem references:**
- **Capability-based security** — Object-capability model (ocaps): a capability is an
unforgeable token granting the bearer the right to perform an action. Window is an
ocap with scope + expiry + revocation. Refs: Miller ("Robust Composition", 2006),
Caja, Agoric.
- **OAuth 2.0 scopes & refresh tokens** — closest web analog: scope strings, token
expiry, refresh, revocation endpoint. Window's "rate-limit" is OAuth's missing piece.
- **Cosmos SDK `x/capability`** — Cosmos's scoped-capability module (used by IBC to bind
ports to modules). A capability is an unforgeable pointer; modules claim/release. Window
extends this with Holder-authorized scope + duration + rate-limit + audit log.
- **Macaroons (Google)** — caveat-bound bearer tokens: a macaroon is a token with added
caveats (scope, time, etc.) that constrain authority; third-party caveats allow
delegation chains. Window's scope + duration + revoke maps cleanly to macaroon caveats.
- **Audit log patterns** — append-only event log (event sourcing). Cosmos SDK `x/feegrant`
and `x/authz` are the closest Cosmos analogs: `x/authz` grants one account the right to
execute messages on behalf of another, with expiry + revoke; `x/feegrant` grants
fee-allowance with spend limits. Window is a generalization of `x/authz` + `x/feegrant`
+ scope + rate-limit + audit-log.
**Recommendation for skeleton (fuller):** Define a `window` module with:
- `Window` struct (id, grantor-holder, grantee, scope, start, end, rate-limit-per-window,
revoked, audit-log-refs).
- `Scope` type — a structured scope (what data/actions the window opens): e.g., read-Stash,
read-Standing, process-Pass-Act-for-Stand, etc. Use a `ScopeKind` enum + free-form
`ResourceID` string.
- `RateLimit` struct (max-actions, per-duration-seconds, actions-consumed).
- `AuditEntry` struct (timestamp, action, result, granter-ref) — append-only.
- Lifecycle: `Open → Active → Revoked → Expired`. A `Revoke()` method stub.
- Full lifecycle tests: open, rate-limit-consume, revoke, expire, revoke-after-expire
(no-op), double-revoke (idempotent or error — test both).
- `DefaultGenesis` with empty windows; `ValidateGenesis` checks no overlapping window-ids.
**Risk:** "Rate-limit" semantics (token bucket vs sliding window vs fixed window) is
unstated. Use a simple counter (`actionsConsumed` vs `maxActions`) for skeleton; note
the algorithm choice as a low-confidence assumption.
---
### 1.4 REQ-016 — Nine Stand Types
**What it is:** Nine organizational forms (Stands): Household, Crew, Entity, Co-op,
Circle, Trust, Foundation, Confederation, Shadow. Each is a governed group holding a
Vault. D-024 scopes v0.2 to an enum + Stand keeper + membership-set types; no Stand
economics in v0.2.
**Prior art / ecosystem references:**
- **DAO organizational forms** — Aragon (MiniMeToken + Voting apps, organization templates),
DAOstack (Genesis), MolochDAO (ragequit, guild shares, shares-as-membership). Moloch's
"guild" + "member shares" pattern is the closest to OY's "Stand with Vault + members".
- **SubDAO / Org primitives** — Cosmos SDK has no first-class "organization" module; the
closest is `x/group` (a group of members with a policy — weighted/threshold decision
policy). `x/group` supports: member addresses, admin, policy (threshold or weighted),
proposal + vote + tally + execution. This is the *exact* pattern for a Stand: a group
with a decision policy governing a Vault.
- **Lens Protocol (social graph)** — follows/followers as membership edges; useful for
Stand membership-set modeling (set of Reach IDs, not addresses).
- **Safe (Gnosis Safe) modules** — modular Safe with custom modules for spending policy;
OY Stand = group + Vault + policy, very close to Safe + SpendingLimitModule.
- **Nine named types** — no protocol has exactly nine; this is OY-specific
(taxonomy-driven). Skeleton encodes them as a fixed enum with a test asserting exactly
9 entries (locked-const test).
**Recommendation for skeleton:** Define a `stand` module with:
- `StandType` enum (9 values, exactly), plus `AllStandTypes() []StandType` returning all 9.
- `Stand` struct (id, type, name, vault-id, admin-reach, created-at, member-count).
- `Membership` struct (stand-id, reach-id, joined-at, role — e.g., Member, Admin,
Observer).
- `StandPolicy` stub (threshold or weighted — mirror `x/group` decision policy).
- Genesis with empty stands; `ValidateGenesis` checks no dup stand-ids, member-reach
unique within a stand.
- Test: exactly 9 types, each name matches vision §11, no extras, no dups.
**Risk:** "Shadow" Stand is described as adversarial/temporary in the vision — its
semantics differ from the other 8. Skeleton treats all 9 uniformly; flag the Shadow
behavioral split as a v0.3 design question.
---
### 1.5 REQ-017 — Guilds + Hand-Pass (0% protocol fee)
**What it is:** Guilds are task-oriented collectives; a Hand-Pass is a free
(0% protocol fee) Pass-Act issued by a Guild. D-025 scopes v0.2 to Guild keeper +
Hand-Pass type at locked 0% fee + issuance tests; no Guild task queue.
**Prior art / ecosystem references:**
- **Gitcoin Grants / Bounties** — task-oriented guilds paying contributors; closest to
"Guild issues Hand-Passes for tasks".
- **Dework / Lens work-nets** — on-chain task marketplaces.
- **Fee waivers in existing protocols** — MakerDAO PSM fees are tunable per-pair;
Uniswap protocol fee switch (0..25 bps, off by default). OY's 0% is *locked* (not
tunable) — closer to a smart-contract covenant than a parameter.
- **Cosmos SDK fee grants (`x/feegrant`)** — fee allowances are grants; a 0-fee Hand-Pass
is conceptually a "always-free grant" but the *protocol* fee is the covenant, not a
per-tx grant. The Fee Covenant module (v0.1, REQ-002) already encodes
`WaiverHandPassGuild` as a 0-fee waiver reason (see `x/feecovenant/types/types.go`).
v0.2's Guild module *references* that waiver, doesn't redefine the fee.
**Recommendation for skeleton:** Define a `guild` module with:
- `Guild` struct (id, name, founder-reach, created-at, stand-affiliation optional).
- `HandPass` struct (pass-id, guild-id, issuer-reach, recipient-reach, amount-grain,
timestamp, fee-grain-always-0).
- `HandPassFeeBps = 0` as a locked `const` (cross-referenced to feecovenant
`WaiverHandPassGuild`).
- `IssueHandPass` stub method.
- Tests: HandPass fee is 0, Guild has a Stand affiliation or standalone, issuer must be
guild member (skeleton: type-level only, no enforcement).
- A lexicon assertion test in the guild test file.
**Risk:** Guild-internal task management (queue, claims, completion) is operational, not
protocol — correctly out of scope. Keep skeleton to the issuance type.
---
### 1.6 REQ-018 — Partner Spectrum (4-tier)
**What it is:** Four partner tiers: Op (operator, processes Pass-Acts), Master Op
(senior operator), Pier (credential/identity provider, e-Residency/biometrics), Anchor
(institutional). D-026 scopes v0.2 to 4-tier enum + Partner registry keeper +
credential-ref types; Pier credential routing deferred per PROJECT.md Q5.
**Prior art / ecosystem references:**
- **Layered partner programs** — Visa (Acquirer → Issuer → Network), Stripe (Atlas
partners), AWS Partner Network (Registered → Select → Advanced → Premier). OY's 4-tier
mirrors these tiered-partner spectra.
- **Credential providers** — ceramic.network (DIDs + verifiable credentials),
EBSI (European Blockchain Services Infrastructure), Anonybit (biometric ID), Estonia
e-Residency. Piers are OY's abstraction over these.
- **Cosmos SDK `x/authz` / `x/group`** — a Partner is an authorized actor (like authz
grantee) for a Window-scoped action. A Partner registry is a typed address book with
tier metadata; closest Cosmos analog is a lightweight `x/params`-backed registry or a
dedicated module.
- **Anchor (institutional)** — closest to MakerDAO RWA arrangers or Centrifuge tiers
(senior/junior tranches with institutional sponsors).
**Recommendation for skeleton:** Define a `partner` module with:
- `PartnerTier` enum (Op, MasterOp, Pier, Anchor) — exactly 4, locked test.
- `Partner` struct (id, tier, name, reach-id, region, credential-ref, status).
- `CredentialRef` struct (provider-id, credential-type enum, ref-uri) — Pier routing
deferred, so the ref is an opaque URI for now.
- `PartnerStatus` enum (Pending, Active, Suspended, Revoked).
- Registry keeper stub: add/get/list/by-tier.
- Tests: exactly 4 tiers, tier names match, no lexicon drift, partner-by-tier lookup.
**Risk:** "Master Op" naming — must stay lexicon-clean (no "operator" if it implies bank
terms; "Op" is fine per vision §13). Verify no banned terms in partner-type strings.
---
### 1.7 REQ-020 — Six Pacts
**What it is:** Six Pacts (commitment types): Pause, Ground, Stance, Cover,
Stand-Registry, Hub-API. D-027 scopes v0.2 to 6 Pact types as separate keeper modules
with execute-entry + invariant tests; Cover Pool seniority deferred per Q7.
**Prior art / ecosystem references:**
- **Pacts / commitments** — closest analog: **CosmWasm contracts as commitments** (each
Pact is a contract with an `execute` entry point). For a Go skeleton, model each as a
module with an `Execute(msg)` stub.
- **Pause (circuit breaker)** — OpenZeppelin `Pausable`, Compound's Pause guardian,
Cosmos SDK `x/crisis`. OY v0.1 already has `x/still` (Still/Stir pause/resume); Pause
Pact is the Pact-level wrapper around Still.
- **Ground** — closest to "earth-anchored" / collateral lock; Compound's collateral
factor, MakerDAO vault collateral lock. For skeleton: a `Ground` Pact = lock Bread
against a real-world commitment.
- **Stance** — public-position / attestation Pact; closest to EAS (Ethereum Attestation
Service) attestations or Kleros court rulings. Skeleton: a typed signed-attestation
record.
- **Cover** — insurance-like commitment; closest to **Nexus Mutual**, **InsurAce**,
**Bridge Mutual**, and OY's own "Cover Pool" (vision §16). Seniority/tranches deferred
per Q7; skeleton: a Cover commitment type with amount + beneficiary + trigger-condition
stub.
- **Stand Registry** — a Pact that registers a Stand into the canonical OY Chain
registry; ties to REQ-016 Stand module.
- **Hub API** — B2B backbone Pact; closest to Stripe API / Plaid API as on-chain
commitments. Full Hub API suite deferred to Phase 3; v0.2 = stub type only.
**Recommendation for skeleton:** Define a `pact` module with sub-files per Pact, OR six
tiny modules `x/pactpause`, `x/pactground`, `x/pactstance`, `x/pactcover`,
`x/pactstandregistry`, `x/pacthubapi`. Recommend **one `pact` module with a `PactType`
enum + per-type execute-entry structs** to match v0.1's one-module-per-component pattern
(avoids 6 micro-modules):
- `PactType` enum (Pause, Ground, Stance, Cover, StandRegistry, HubAPI) — exactly 6.
- `Pact` struct (id, type, parties, terms-bytes, status, execute-msg-ref).
- `PactStatus` enum (Proposed, Active, Fulfilled, Voided).
- Per-type `Execute*` stub methods.
- Tests: exactly 6 types, invariant that Mission-Lock Pacts (Pause/Ground/Stance) can be
invoked but not amended in their core terms.
**Risk:** Cover Pool seniority math (Q7) is deferred — keep the Cover Pact to a flat
commitment type with no seniority fields.
---
### 1.8 REQ-021 — Mesh Bond Market (8% cap)
**What it is:** A bond market where Stands/Guilds issue bonds (coupon-bearing
commitments) with an 8% upper coupon cap and 0% floor. D-028 scopes v0.2 to Bond market
keeper + 8%/0% consts + issuance tests; full secondary-market depth deferred to Phase 3.
**Prior art / ecosystem references:**
- **On-chain bond markets** — MakerDAO RWA vaults (centrifuge/tinlake senior-tranche),
Maple Finance (term loans), TrueFi (unsecured loans with credit ratings), Centrifuge
(Tinlake drop/tin tranches). OY's "8% coupon cap" is a *protocol-level* ceiling, not a
per-loan rate — closer to a usury cap enforced at the covenant layer.
- **Coupon caps / usury laws** — real-world analog: state usury laws cap interest rates;
DeFi analog: Aave rate caps (borrow cap, supply cap), Compound's per-market collateral
factor. OY's 8% is *mission-locked* (not adjustable by governance) — closer to
Compound's immutable risk params or MakerDAO's debt ceiling floors.
- **Bond structures** — fixed-coupon, zero-coupon, amortizing. v0.2 skeleton: fixed-coupon
with cap/floor only.
- **Cosmos SDK** — no native bond module; bonds are custom CosmWasm or a Go module. For
skeleton: a Go module mirroring the v0.1 `x/feecovenant` locked-const pattern.
**Recommendation for skeleton:** Define a `bond` module with:
- `CouponCapBps = 800` (8%) and `CouponFloorBps = 0` (0%) as locked `const`.
- `Bond` struct (id, issuer-stand-id, principal-grain, coupon-bps, term-days, issued-at,
maturity, status).
- `BondStatus` enum (Issued, Active, Matured, Defaulted, Repaid).
- `Issue(...)` stub; `Clamp(couponBps)` returns min(cap, max(floor, coupon)) — same shape
as Fee Covenant's `Clamp`.
- Tests: cap=800, floor=0, clamp invariant (below floor → floor, above cap → cap, in
range → unchanged), lexicon.
**Risk:** "interest" / "yield" are banned lexicon terms — the bond module must use
"coupon" (vision §17 lexicon-compliant) consistently. Test asserts no banned terms in
the bond package.
---
### 1.9 Bearers — OY-LR + Beacon v1 (stubs per D-029)
**What it is:** Two new bearer transports: OY-LR (LoRa, long-range 2-10km) and OY-Beacon
(transport beacon). v0.1 already defined the `BearerType` enum including `BearerOYLR`;
v0.2 adds the bearer-interface + transport stubs. No hardware integration.
**Prior art / ecosystem references:**
- **LoRaWAN** — Long Range (LoRa) low-power wide-area network: star-of-stars topology,
gateways relay to network server. Helium is the canonical crypto-LoRa project
(incentivized coverage). OY-LR is surveillance-resistant (vision §14) — differs from
Helium's public coverage model.
- **Delay-Tolerant Networking (DTN)** — RFC 5050 Bundle Protocol; v0.1 already references
this for the Unified Bearer Layer. OY-LR + Beacon fit DTN (store-and-forward).
- **Bluetooth Beacons** — iBeacon/Eddystone (advertising packets, no connection); OY
Beacon is a transport-mode beacon (presence + small payload), closest to Eddystone-EID
(ephemeral identifier).
- **Mesh routing** — B.A.T.M.A.N., OLSR, AODV. OY uses first-to-deliver-wins (already in
v0.1 `UnifiedBearerLayer`).
**Recommendation for skeleton:** Extend the existing `x/bearers` module (do NOT create a
new module — v0.1 owns the BearerType enum). Add:
- `BearerTransport` interface stub (Send, Receive, Status) — a Go interface, not impl.
- `OYLRLink` struct (gateway-id, range-meters, frequency-mhz, surveillance-resistant=true).
- `BeaconFrame` struct (beacon-id, ephemeral-id, payload-bytes, ttl).
- Tests: OY-LR is in `AllBearers()` (already asserted in v0.1), transport interface
signature, Beacon frame non-empty, lexicon.
- Keep all under `x/bearers/types/` to match the v0.1 one-package-per-module convention.
**Risk:** Hardware/RF integration is explicitly deferred — do not pull in any LoRa or
BLE Go libraries. Pure types.
---
### 1.10 Forex Engine v1 (stub per D-030)
**What it is:** A Forex pair type + rate-oracle interface + stub keeper; no live oracle
integration (depends on Piers, Phase 3).
**Prior art / ecosystem references:**
- **Oracle patterns** — Chainlink (aggregated off-chain reports, commit-reveal),
Pyth Network (low-latency pull-based, Herodotus/pythnet), UMA (optimistic oracle with
dispute window), API3 (first-party Airnodes). For Forex specifically: Chainlink FX
feeds, Pyth FX pairs. OY's Forex Engine is a *consumer* of these (via Piers), not a
producer.
- **DEX aggregator oracle** — Uniswap TWAP, Compound's Open Oracle.
- **Cosmos SDK oracle modules** — `x/oracle` (Terra classic — failed, cautionary), Band
Protocol (Cosmos-native oracle). For skeleton: define an oracle *interface*, not an
integration.
- **Forex-specific** — multi-venue execution with spread caps (vision §18 risk #18); OY
caps spreads, doesn't trust a single venue.
**Recommendation for skeleton:** Define a `forex` module with:
- `ForexPair` struct (base, quote — but NOT "currency" (banned); use "base-asset" /
"quote-asset" or vision-lexicon pair labels like "Bread/Asset").
- `RateOracle` Go interface (GetRate(pair) (rate, timestamp, err)) — no implementation.
- `ForexRate` struct (pair, rate, source, timestamp, confidence).
- `SpreadCapBps` const (locked) — defer exact value to a v0.3 decision; skeleton sets a
placeholder 0 (or a documented TBD const with a test that it's ≥0).
- Stub keeper: `GetRate` returns a sentinel "not-integrated" rate.
- Tests: pair struct, oracle interface compiles, lexicon (no "currency"/"dollar").
**Risk:** Forex terminology is *lexicon-hostile* ("currency", "dollar", "euro", "fx" all
banned or borderline). Use "Forex Engine" (allowed, vision §13 names it) but pair
labels must avoid banned terms. "Forex" itself is not in the banned list (the banned
list is: bank, deposit, interest, yield, currency, dollar/euro, account, savings,
depositor). Test asserts pair labels are lexicon-clean.
---
## 2. Pitfalls & Risks
### Integration pitfalls
- **Window ↔ Pacts ↔ Partners ↔ Orgs coupling:** Window is consumed by Pacts, Orgs,
Partners, Governance (Architecture §4.4). Skeleton must define a stable `WindowRef`
type early (P1) so downstream modules reference it without import cycles. Recommend
Window ships first in the blocker-chain ordering (D-031 confirms this).
- **Council ↔ Stand ↔ Guild identity:** Stand Council governs a Stand; Guild Council
governs a Guild; a Guild may affiliate with a Stand. Avoid circular imports: keep
`council` referencing `stand`/`guild` types via interface or by-ID strings, not direct
struct imports.
- **IBCPacket type vs ibc-go:** If the skeleton's packet type diverges from the real
ibc-go packet, the v0.3 integration requires a refactor. Pin to ICS-20 v1 shape.
### Cosmos SDK gotchas
- **No deps vendored yet** (`go.mod` is bare `go 1.22`). v0.2 skeleton must stay
dependency-free (only stdlib + `encoding/json`), exactly as v0.1 did. Pulling ibc-go
or cosmos-sdk into `go.mod` would force a large dependency tree and break the
"skeleton + tests compiles with zero external deps" property.
- **`x/gov` tally types:** if we later wire councils to `x/gov`, the tally shape must
match `gov.TallyResult`. Skeleton should mirror `x/gov` field names to ease the swap.
- **`x/group` decision policy:** Stand/Guild policies should mirror `group.DecisionPolicy`
(threshold + weighted) so a future `x/group` integration is mechanical.
- **`x/authz` / `x/feegrant`:** Window should mirror `authz.Grant` (granter, grantee,
msg-type, expiry) so a future `x/authz`-backed implementation is a thin wrapper.
- **Module name collisions:** Cosmos SDK has `x/gov`, `x/group`, `x/bank` (BANNED name —
never use), `x/auth`. OY uses custom module names (`bread`, `still`, `mirror`,
`feecovenant`) — keep this pattern; do not name any v0.2 module `bank` or `account`.
### Lexicon-compliance risks (highest severity per D-033)
- **Banned terms:** bank, deposit, interest, yield, currency, dollar/euro, account,
savings, depositor. The bond module is the highest-risk ("interest"/"yield" are natural
fit-words for coupons — use "coupon" only). Forex is the second-highest ("currency",
"dollar", "euro"). Council/Governance is lower-risk but "account" must be avoided
(use "Holder"/"Reach").
- **Drift vector:** copy-pasting from prior-art docs (OZ Governor, Compound) into
comments will import banned terms. Every new module's test file must include a lexicon
assertion (D-033). A project-wide lexicon test (scanning all `x/**/*.go` for banned
terms) is recommended as a meta-test added in the review phase.
- **Test coverage:** D-033 mandates ≥80% on new keeper/type packages. The skeleton-only
modules have low cyclomatic complexity, so 80% is achievable with table-driven tests on
every exported const, struct, and method (matching v0.1's 53-test baseline pattern).
### Test-coverage risks
- **Locked-const tests are the regression firewall:** every locked constant (8% cap, 0%
floor, 6 Pact types, 9 Stand types, 4 Partner tiers, Mission Lock non-amendable) must
have a dedicated test. A single const change must break a test.
- **Invariant tests:** Mission Lock (council cannot amend), fee clamp (never > ceiling,
never < floor), bond clamp (never > cap, never < floor) — these are invariant tests,
not just equality tests.
- **Genesis validation:** every new module's `ValidateGenesis` should at least reject
duplicate IDs; v0.1's `ValidateGenesis` returns `nil` unconditionally — upgrade the v0.2
modules to actually validate (small improvement, low risk).
---
## 3. Architecture Recommendations for v0.2
### Module layout under `x/`
Following v0.1's one-package-per-module convention (`x/<name>/types/types.go` +
`types_test.go`), v0.2 adds these new modules (mapped to REQs and D-decisions):
| New module | REQ | D-decision | Status |
|---|---|---|---|
| Q1 | Maya's Day integration spec | 9 Mesh Experience | Component doc in P1 |
| Q2 | Standing sub-tables (per-category, buckets) | 6 Identity | Component doc in P1 |
| Q3 | Full risk mitigations | Distributed | In research above; expanded in component docs |
| Q4 | Phase-by-phase deliverables | All | ROADMAP.md |
| Q5 | Pier credential routing | 6 Identity, 11 Partners | Component docs in P2 |
| Q6 | Experimental bond forms | 8 Pacts (Bonds) | Phase 4+, deferred |
| Q7 | Cover Pool seniority math | 8 Pacts (Cover) | Component doc in P2 |
| Q8 | Processor share tier volumes | 12 Bearers, 13 Fee | Component docs in P1 |
| `x/satellite/` | REQ-009 | D-021 | New — L2 IBC types (Polygon rep + 4 stubs) |
| `x/council/` | REQ-011 | D-022 | New — 3 sub-councils in one module (Mesh/Guild/Stand) |
| `x/window/` | REQ-015 | D-023 | New — **fullest** v0.2 module (leaf, no blocker) |
| `x/stand/` | REQ-016 | D-024 | New — 9-type enum + keeper stub |
| `x/guild/` | REQ-017 | D-025 | New — Guild + HandPass @ 0% |
| `x/partner/` | REQ-018 | D-026 | New — 4-tier Partner registry |
| `x/pact/` | REQ-020 | D-027 | New — 6 Pact types in one module |
| `x/bond/` | REQ-021 | D-028 | New — Mesh Bond market with 8% cap |
| `x/forex/` | (Forex v1) | D-030 | New — Forex pair + oracle interface stub |
**Existing modules extended (not new):**
| Existing module | Extension | D-decision |
|---|---|---|
| `x/bearers/` | Add `BearerTransport` interface + OY-LR + Beacon types | D-029 |
**Existing modules referenced (not modified):**
- `x/feecovenant/` — Guild HandPass references `WaiverHandPassGuild` (already a 0-fee
waiver reason in v0.1). Do NOT modify feecovenant; guild's `HandPassFeeBps = 0` const
cross-documents the covenant.
- `x/vault/` — Stand references `vault.Vault` by ID string (avoid import cycle).
- `x/stash/` — Window references `stash.Stash` by holder-id string.
- `x/standing/` — Council VoiceSource references standing by reach-id string.
- `x/identity/` — Reach IDs used everywhere as string keys.
### Interface contracts between new modules
To avoid import cycles, new modules communicate **by-ID-strings**, not by importing each
other's struct types. **This is a hard invariant (G-003)**: no v0.2 production (non-test)
`.go` file under `x/` may import another `x/<module>/types` package by struct — enforced
by a tested invariant in P1-01-02 (Window test file) using `go/parser` or a package-import
grep test. The earlier "Window may be imported by Pacts/Partners/Orgs if needed" hedge is
**retracted** (G-003): the skeleton keeps ALL inter-module refs by-ID-string, including
Window, to keep the invariant uniform and testable.
```
x/window ──(WindowRef by id)──► x/pact, x/partner, x/stand, x/council
x/stand ──(StandRef by id)───► x/council (Stand Council), x/guild (affiliation)
x/guild ──(GuildRef by id)───► x/council (Guild Council)
x/forex ──(Oracle interface)──► (none in v0.2; Piers in Phase 3)
x/bond ──(BondRef by id)────► x/stand (issuer), x/pact (Cover)
x/satellite ──(Packet types)──► (none in v0.2; relayer in Phase 3)
x/bearers ──(BearerTransport)──► (none in v0.2; processing mesh in Phase 3)
```
### Blocker-chain ordering (per D-031)
D-031 fixes the phase ordering. v0.2 execution phases (P1..P5) follow the
ARCHITECTURE.md blocker chain:
```
P1: Orgs + Window foundation
└─ x/stand (REQ-016), x/guild (REQ-017), x/window (REQ-015)
(Window is leaf — can be built first within P1)
P2: Pacts + Partners
└─ x/pact (REQ-020), x/partner (REQ-018)
(depend on Window + Stand from P1)
P3: Councils + Forex
└─ x/council (REQ-011), x/forex (Forex v1)
(Council depends on Stand + Guild + Standing; Forex is independent)
P4: Bonds + Bearers + L2
└─ x/bond (REQ-021), x/bearers ext (D-029), x/satellite (REQ-009)
(Bonds depend on Stand; Bearers extend v0.1; L2 is independent)
P5: Review + ship (milestone v0.2)
```
Each phase is independently shippable (vertical slice integrity, same as v0.1).
---
## 4. Dependency Notes
### Current state
- `go.mod`: `module github.com/oy/openyield`, `go 1.22`, **zero dependencies**.
- v0.1 compiled all 15 modules with only stdlib (`encoding/json`). No Cosmos SDK, no
ibc-go, no CosmWasm vendored.
### Recommendation for v0.2 skeleton
**Keep zero external dependencies.** Every v0.2 module must be pure-Go types + tests
compiling with `go build ./...` and `go test ./...` using only stdlib. Rationale:
1. The skeleton's value is in the *typed contract* (structs, consts, invariants), not
in wiring to live Cosmos SDK / ibc-go.
2. Pulling `cosmos-sdk` or `ibc-go` would force a 1000+ module dependency tree, break
the fast `go test` feedback loop, and require go version bumps beyond 1.22.
3. v0.1 established this pattern successfully (53 tests, 11 files, zero deps).
### Future (Phase 3+) dependency targets — researched but NOT added in v0.2
- **Cosmos SDK** — v0.50.x (LTS) or v0.47.x (the long-term-support line many chains still
use). v0.50+ uses CometBFT (separated from tendermint). For go 1.22, cosmos-sdk v0.50.x
is compatible (go 1.21+). Defer the choice to the Phase 3 chain-launch decision.
- **ibc-go** — v8.x (for cosmos-sdk v0.50) or v7.x (for cosmos-sdk v0.47). ibc-go v10
introduces IBC v2 (streamlined, EVM-friendly) — attractive for Polygon but only if
cosmos-sdk v0.50+ is chosen. Defer.
- **CosmWasm** — `wasmvm` v1.5.x or v2.0.x (for Pacts potentially as CosmWasm contracts
in Phase 3). For v0.2 skeleton, Pacts are Go types — no wasmvm.
- **CometBFT** — v0.37.x / v1.0 (consensus; only needed for live chain, not skeleton).
### Minimal set for v0.2
**None.** Zero new dependencies. The skeleton is self-contained.
---
## 5. Assumptions (logged with confidence scores; not flagged for human validation)
| ID | Assumption | Confidence | Rationale |
|----|-----------|------------|-----------|
| A-201 | v0.2 skeleton stays dependency-free (only stdlib), matching v0.1. | 0.95 | v0.1 proved this works; D-020 confirms skeleton-first. |
| A-202 | One module per component (`x/<name>/types/`) is the v0.2 convention; no multi-package modules. | 0.90 | v0.1 has 15 modules all following this; consistency is low-risk. |
| A-203 | Inter-module references use by-ID strings (not struct imports) to avoid cycles. | 0.85 | Cosmos SDK modules also decouple this way; import cycles are a real risk given the coupling graph. |
| A-204 | Council tally types mirror Cosmos SDK `x/gov` `TallyResult` shape for future wiring. | 0.75 | `x/gov` is the most likely runtime; matching field names eases Phase 3. |
| A-205 | Stand/Guild decision policy mirrors `x/group` `DecisionPolicy` (threshold/weighted). | 0.75 | Same future-wiring rationale as A-204. |
| A-206 | Window rate-limit uses a simple counter (`actionsConsumed` vs `maxActions`), not a token bucket. | 0.65 | Algorithm unstated in vision; counter is simplest; revisit in v0.3. |
| A-207 | Six Pacts live in one `x/pact` module (not six micro-modules) for v0.1-pattern consistency. | 0.80 | D-027 says "separate keeper modules" — one module with a PactType enum satisfies "separate execute entries" without 6 dirs. |
| A-208 | Forex pair labels use "Bread/Asset" style (not "currency"/"dollar") to stay lexicon-clean. | 0.80 | Lexicon is non-negotiable (D-032); "Forex" itself is allowed (vision §13). |
| A-209 | Bearers OY-LR + Beacon extend the existing `x/bearers` module (no new module). | 0.90 | v0.1 owns the `BearerType` enum; D-029 says "stubs", not "new module". |
| A-210 | Bond coupon uses "coupon" terminology exclusively; "interest"/"yield" banned. | 1.00 | Lexicon (REQ-012) is non-negotiable. |
| A-211 | Mission Lock is a `const bool MissionLockAmendable = false` in council module. | 0.85 | Simplest invariant-testable form; vision §19 says Mission Lock cannot be amended. |
| A-212 | `ValidateGenesis` in v0.2 modules performs basic ID-uniqueness checks (upgrading v0.1's no-op). | 0.80 | Small improvement, low risk, raises skeleton quality. |
| A-213 | Shadow Stand is treated uniformly with the other 8 in v0.2 (no special behavioral split). | 0.60 | Vision implies Shadow is adversarial/temporary; defer the split to v0.3 design. Low confidence — flag for v0.3 revisit. |
| A-214 | Forex `SpreadCapBps` exact value deferred to v0.3; skeleton uses a documented placeholder const. | 0.70 | Vision §18 risk #18 mentions spread caps but no locked value. |
| A-215 | ibc-go / cosmos-sdk / CosmWasm versions are NOT pinned in v0.2 (deps not vendored); choice deferred to Phase 3 chain launch. | 0.90 | D-020 says no live chain in v0.2. |
---
## Cross-Reference Summary
| REQ | Component | Module | Phase (D-031) | Depth (D-02x) |
|-----|-----------|--------|---------------|---------------|
| REQ-009 | L2 IBC | `x/satellite/` | P4 | Skeleton (Polygon rep) — D-021 |
| REQ-011 | Councils | `x/council/` | P3 | Skeleton (3 keepers, Mission Lock const) — D-022 |
| REQ-015 | Window | `x/window/` | P1 | **Fuller** (leaf, no blocker) — D-023 |
| REQ-016 | Stands | `x/stand/` | P1 | Skeleton (9-type enum) — D-024 |
| REQ-017 | Guilds | `x/guild/` | P1 | Skeleton (HandPass 0%) — D-025 |
| REQ-018 | Partners | `x/partner/` | P2 | Skeleton (4-tier enum) — D-026 |
| REQ-020 | Pacts | `x/pact/` | P2 | Skeleton (6 types, execute stubs) — D-027 |
| REQ-021 | Bonds | `x/bond/` | P4 | Skeleton (8% cap, issuance) — D-028 |
| Bearers | OY-LR + Beacon | `x/bearers/` (ext) | P4 | Stubs (no HW) — D-029 |
| Forex | Forex v1 | `x/forex/` | P3 | Stub (oracle interface) — D-030 |
| REQ-012 | Lexicon | (all) | all | Enforced everywhere — D-032 |
**New modules: 9. Extended modules: 1 (bearers). Total v0.2 packages: 10 new +
1 extended = 11 packages added to the v0.1 baseline of 15.**
+13 -8
View File
@@ -1,13 +1,18 @@
# Roadmap: OpenYield (oy)
## Phase 0 — Pre-Execution (Current)
- [x] Initialize CIAgent project (init workflow)
- [ ] SPECIFY — validate specification
- [ ] CLARIFY — resolve ambiguities (autonomy: full → auto-decide defaults)
- [ ] RESEARCH — domain research + persona assessment
- [ ] PLAN — create phase plans
- [ ] GRILL — adversarial review
- [ ] Ship phase 0
## Milestone v0.1 — Pre-MVP Foundation (COMPLETE)
- [x] P0: Pre-Execution (spec/research/plan/grill) → v0.0.0
- [x] P1: OY Chain & Mirror → v0.0.1
- [x] P2: Bread & Root Basket → v0.0.2
- [x] P3: Storage Substrate → v0.0.3
- [x] P4: Bloom Engine → v0.0.4
- [x] P5: Fee Covenant → v0.0.5
- [x] P6: Identity/Standing → v0.0.6
- [x] P7: Bearers/Processing → v0.0.7
- [x] P8: Mesh Experience → v0.0.8
- [x] P9: Final Review → v0.0.9
- Status: COMPLETE (local-only ship, no remote configured)
- MVP release (v0.1.0) deferred until system validated as production-ready
## Phase 1 — Foundation (Year 1)
**Target**: first 10,000 Holders, 50 Master Ops
+87
View File
@@ -0,0 +1,87 @@
// Package lexicon holds the project-wide lexicon firewall (REQ-012).
//
// The 9 banned financial terms must never appear in any production or test
// .go file under x/. This package exposes the banned-terms list and detection
// helpers; the terms themselves are assembled at runtime from two-character
// fragments so that the SOURCE of this package does not contain any banned
// term as a literal substring. This is the standard lexicon-test bootstrapping
// pattern: the firewall's own code must not trip the firewall.
//
// The lexicon firewall is NEW in v0.2 (G-002): v0.1 is lexicon-clean in
// practice but has zero lexicon tests. The project-wide meta-test in
// P1-04-02 (lexicon_meta_test.go) is the durable firewall; per-package
// lexicon assertions in each new module's types_test.go scan the module's
// production files.
package lexicon
import (
"regexp"
"strings"
)
// term is a banned term assembled from two halves so the source file does
// not contain the literal banned word.
type term struct {
a, b string
}
// fragments holds the 9 banned terms as (a, b) halves. Neither half alone
// is a banned term, and concatenation produces the banned term at runtime.
var fragments = []term{
{"ba", "nk"}, // bank
{"depo", "sit"}, // deposit
{"intere", "st"}, // interest
{"yie", "ld"}, // yield
{"curre", "ncy"}, // currency
{"dol", "lar"}, // dollar
{"eu", "ro"}, // euro
{"acco", "unt"}, // account
{"savin", "gs"}, // savings
{"deposito", "r"}, // depositor
}
// BannedTerms returns the banned financial terms (REQ-012). The spec lists
// 10 terms (often described as "9" in plan docs, counting dollar/euro as a
// pair): bank, deposit, interest, yield, currency, dollar, euro, account,
// savings, depositor. The terms are assembled at runtime from fragments so
// this package's source does not contain any banned term as a literal
// substring.
func BannedTerms() []string {
out := make([]string, len(fragments))
for i, t := range fragments {
out[i] = t.a + t.b
}
return out
}
// bannedTermRegexes are the compiled word-boundary regexes for the 9 banned
// terms. Word boundaries prevent false positives like "openyield" matching
// "yield" or "european" matching "euro" — the firewall bans the words as
// concepts, not as arbitrary substrings. The regexes are case-insensitive.
var bannedTermRegexes = func() []*regexp.Regexp {
terms := BannedTerms()
out := make([]*regexp.Regexp, len(terms))
for i, t := range terms {
out[i] = regexp.MustCompile(`\b` + regexp.QuoteMeta(t) + `\b`)
}
return out
}()
// FindBannedTerm returns the first banned term found in s (case-insensitive,
// word-boundary match) and true, or "" and false if none. Used by the
// project-wide meta-test (P1-04-02) and the per-package lexicon assertions.
func FindBannedTerm(s string) (string, bool) {
lower := strings.ToLower(s)
terms := BannedTerms()
for i, re := range bannedTermRegexes {
if re.MatchString(lower) {
return terms[i], true
}
}
return "", false
}
// ContainsBannedTerm is an alias for FindBannedTerm kept for compatibility.
func ContainsBannedTerm(s string) (string, bool) {
return FindBannedTerm(s)
}
+177
View File
@@ -0,0 +1,177 @@
// Package lexicon_meta holds the project-wide lexicon firewall meta-test
// (REQ-012, G-004, G-009). It is the durable firewall created in v0.2 P1
// Wave 3; P5-01-01 EXTENDS it rather than recreating it.
//
// The meta-test scans every .go file under x/ (production + test) for the 9
// banned financial terms and fails on any hit. It includes a self-test table
// (G-009) of synthetic strings — one per banned term — asserted to each
// trigger detection, so the meta-test's own detection coverage is durably
// verified without manual spikes.
//
// The meta-test file itself is excluded from the scan (it must reference the
// banned terms via the shared lexicon package, whose source assembles terms
// from fragments so no banned term appears as a literal substring anywhere
// in the firewall's own code — the standard lexicon-test bootstrapping
// pattern).
package lexicon_meta
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
)
// TestLexiconMetaNoBannedTermsInX is the project-wide firewall (G-004).
// It walks every .go file under x/ (production + test), reads its source,
// and asserts no banned term is present (word-boundary, case-insensitive).
// The meta-test file itself is excluded (it is the firewall's own code and
// references the banned terms via the lexicon package, whose source uses
// fragments).
//
// Passes at P1: the v0.1 baseline (15 modules) plus the 3 new P1 modules
// (window, stand, guild) are all lexicon-clean.
func TestLexiconMetaNoBannedTermsInX(t *testing.T) {
xRoot := repoXRoot(t)
thisFile := thisFile(t)
hits := []string{}
err := filepath.Walk(xRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".go") {
return nil
}
// Exclude the meta-test file itself (the firewall's own code).
if path == thisFile {
return nil
}
bz, rerr := os.ReadFile(path)
if rerr != nil {
return rerr
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
rel, _ := filepath.Rel(xRoot, path)
hits = append(hits, rel+" contains banned term "+found)
}
return nil
})
if err != nil {
t.Fatalf("walk: %v", err)
}
if len(hits) > 0 {
t.Errorf("REQ-012 lexicon firewall violations:\n %s",
strings.Join(hits, "\n "))
}
}
// TestLexiconMetaSelfTestTable (G-009) is the meta-test's own coverage
// firewall. Each synthetic string is asserted to trigger detection so the
// firewall's detection logic is durably verified — if detection ever breaks,
// this test fails before the firewall silently passes a real violation.
//
// The synthetic strings are assembled from fragments so this file does not
// contain any banned term as a literal substring (it would otherwise trip
// its own scan; the meta-test file is also excluded from the scan, but the
// self-test keeps the source clean for readability/searchability).
func TestLexiconMetaSelfTestTable(t *testing.T) {
terms := lexicon.BannedTerms()
// The spec lists 10 banned terms (plan docs say "9", counting dollar/euro
// as a pair): bank, deposit, interest, yield, currency, dollar, euro,
// account, savings, depositor.
if len(terms) != 10 {
t.Fatalf("BannedTerms() len = %d, want 10", len(terms))
}
// Each synthetic string embeds exactly one banned term in a plausible
// sentence context. Each must be detected.
synthetic := []string{
"open a " + terms[0] + " here", // bank
"make a " + terms[1] + " now", // deposit
"compounding " + terms[2] + " rate", // interest
"the " + terms[3] + " is 5pct", // yield
"foreign " + terms[4] + " pair", // currency
"price in " + terms[5], // dollar
"price in " + terms[6], // euro
"freeze the " + terms[7], // account
"move to " + terms[8] + " now", // savings
"the " + terms[9] + " lost money", // depositor
}
if len(synthetic) != len(terms) {
t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms))
}
for i, s := range synthetic {
found, ok := lexicon.FindBannedTerm(s)
if !ok {
t.Errorf("G-009 self-test [%d]: synthetic string did not trigger detection: %q", i, s)
continue
}
if found != terms[i] {
t.Errorf("G-009 self-test [%d]: detected %q, want %q (in %q)", i, found, terms[i], s)
}
}
}
// TestLexiconMetaBannedTermsCount asserts exactly 10 banned terms are
// configured (locked-const for the firewall's scope; spec lists 10, plan docs
// say "9" counting dollar/euro as a pair).
func TestLexiconMetaBannedTermsCount(t *testing.T) {
terms := lexicon.BannedTerms()
if len(terms) != 10 {
t.Errorf("BannedTerms() len = %d, want 10 (REQ-012)", len(terms))
}
seen := map[string]bool{}
for _, tr := range terms {
if seen[tr] {
t.Errorf("duplicate banned term %q", tr)
}
seen[tr] = true
}
}
// TestLexiconMetaNoFalsePositiveOnOpenYield asserts the module name
// "openyield" does NOT trigger the "yield" banned term (word-boundary
// matching must not match substrings of identifiers). This is the
// regression firewall for the word-boundary detection design.
func TestLexiconMetaNoFalsePositiveOnOpenYield(t *testing.T) {
cases := []string{
"github.com/oy/openyield/x/window/types",
"package openyield",
"openyield is the module",
"european resident",
}
for _, s := range cases {
if _, ok := lexicon.FindBannedTerm(s); ok {
t.Errorf("false positive: %q triggered a banned term (word-boundary must avoid this)", s)
}
}
}
// repoXRoot returns the absolute path to the repo's x/ directory by walking
// up from this test file.
func repoXRoot(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/lexicon_meta_test.go -> repo root is its dir; x/ is repo/x
repoRoot := filepath.Dir(file)
return filepath.Join(repoRoot, "x")
}
// thisFile returns the absolute path of this meta-test file (to exclude it
// from its own scan).
func thisFile(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return file
}
+45
View File
@@ -48,6 +48,51 @@ type UnifiedBearerLayer struct {
FirstToDeliver bool `json:"first_to_deliver" yaml:"first_to_deliver"`
}
// BearerTransport is the transport interface for a bearer (D-029, vision
// §14). A bearer implementation provides Send (dispatch a payload), Receive
// (accept an inbound payload), and Status (report the bearer's current
// reachability). This is a Go interface stub — no implementation is provided
// in v0.2; the OY-LR and Beacon transports are typed stubs only (no
// hardware/RF integration per D-029). The interface is the v0.2 hook for the
// Phase 3 processing-mesh runtime.
type BearerTransport interface {
// Send dispatches a payload via the bearer. Returns an error if the
// bearer cannot accept the payload. The stub implementations do not
// actually transmit; the interface contract is the v0.2 deliverable.
Send(payload []byte) error
// Receive accepts an inbound payload from the bearer. Returns the
// payload and an error if the bearer has no inbound payload.
Receive() ([]byte, error)
// Status reports the bearer's current reachability (true = reachable).
Status() bool
}
// OYLRLink is the OY-LR (LoRa, long-range 2-10km) transport link stub (D-029,
// vision §14). OY-LR is surveillance-resistant (vision §14: differs from
// Helium's public-coverage model). gateway-id is the LoRa gateway
// identifier; range-meters is the link range (2-10km); frequency-mhz is the
// operating frequency; surveillance-resistant is LOCKED true for OY-LR (the
// bearer is designed to resist surveillance).
type OYLRLink struct {
GatewayID string `json:"gateway_id" yaml:"gateway_id"`
RangeMeters int32 `json:"range_meters" yaml:"range_meters"`
FrequencyMHz uint32 `json:"frequency_mhz" yaml:"frequency_mhz"`
SurveillanceResistant bool `json:"surveillance_resistant" yaml:"surveillance_resistant"`
}
// BeaconFrame is the OY-Beacon transport-mode beacon frame stub (D-029,
// vision §14). A beacon is a transport-mode beacon (presence + small
// payload), closest to Eddystone-EID (ephemeral identifier). beacon-id is
// the beacon identifier; ephemeral-id is the rotating ephemeral identifier;
// payload-bytes is the small payload; ttl is the time-to-live in seconds
// (must be > 0 for a valid frame).
type BeaconFrame struct {
BeaconID string `json:"beacon_id" yaml:"beacon_id"`
EphemeralID string `json:"ephemeral_id" yaml:"ephemeral_id"`
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
TTL int64 `json:"ttl" yaml:"ttl"`
}
type Params struct{}
func DefaultParams() Params { return Params{} }
+218
View File
@@ -1,8 +1,13 @@
package types_test
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
btypes "github.com/oy/openyield/x/bearers/types"
ptypes "github.com/oy/openyield/x/processing/types"
)
@@ -56,3 +61,216 @@ func TestEmptyProcessorSelection(t *testing.T) {
t.Error("Empty processor list should return nil")
}
}
// --- v0.2 Bearers extension (P4-02-02, D-029) -----------------------------------
// The following tests extend the existing v0.1 bearers tests with the v0.2
// BearerTransport interface, OYLRLink, and BeaconFrame stubs (D-029). The
// existing v0.1 tests above (TestBearerCount, TestSurveillanceResistantBearers,
// TestProcessingModeFCFS, TestLightClientSize, TestProcessorSelectionByProximity,
// TestEmptyProcessorSelection) MUST remain green — no regression.
// TestOYLRStillInAllBearers is the REGRESSION test (D-029): OY-LR must still
// be in AllBearers() (the 6-bearer count is unchanged by the v0.2 extension).
func TestOYLRStillInAllBearers(t *testing.T) {
bearers := btypes.AllBearers()
if len(bearers) != 6 {
t.Errorf("AllBearers() len = %d, expected 6 (no regression — D-029)", len(bearers))
}
found := false
for _, b := range bearers {
if b.Type == btypes.BearerOYLR {
found = true
break
}
}
if !found {
t.Error("OY-LR must still be in AllBearers() (no regression — D-029)")
}
}
// TestBearerTransportInterfaceSignature asserts the BearerTransport
// interface is satisfiable by a stub implementation (D-029). The interface
// has three methods: Send, Receive, Status — no implementation is provided
// in v0.2; this test verifies the interface compiles and a stub satisfies it.
func TestBearerTransportInterfaceSignature(t *testing.T) {
// stubTransport is a minimal stub that satisfies BearerTransport.
var _ btypes.BearerTransport = stubTransport{}
}
// stubTransport is a minimal stub implementation of BearerTransport for the
// interface-signature test. It does not actually transmit (no hardware/RF
// integration per D-029); it exists only to verify the interface compiles.
type stubTransport struct{}
func (stubTransport) Send(payload []byte) error { return nil }
func (stubTransport) Receive() ([]byte, error) { return nil, nil }
func (stubTransport) Status() bool { return true }
// TestBearerTransportInterfaceMethods asserts the interface methods have the
// expected signatures by invoking them on the stub.
func TestBearerTransportInterfaceMethods(t *testing.T) {
s := stubTransport{}
if err := s.Send([]byte("hi")); err != nil {
t.Errorf("Send returned error: %v", err)
}
if _, err := s.Receive(); err != nil {
t.Errorf("Receive returned error: %v", err)
}
if !s.Status() {
t.Error("Status should return true for the stub")
}
}
// TestOYLRLinkStructNonEmpty asserts the OYLRLink struct is non-empty when
// populated, and that surveillance-resistant is true (OY-LR is designed to
// resist surveillance — vision §14).
func TestOYLRLinkStructNonEmpty(t *testing.T) {
link := btypes.OYLRLink{
GatewayID: "gw-1",
RangeMeters: 10000,
FrequencyMHz: 915,
SurveillanceResistant: true,
}
if link.GatewayID != "gw-1" {
t.Errorf("GatewayID = %q", link.GatewayID)
}
if link.RangeMeters != 10000 {
t.Errorf("RangeMeters = %d", link.RangeMeters)
}
if link.FrequencyMHz != 915 {
t.Errorf("FrequencyMHz = %d", link.FrequencyMHz)
}
if !link.SurveillanceResistant {
t.Error("SurveillanceResistant must be true for OY-LR (vision §14)")
}
}
// TestOYLRLinkSurveillanceResistantTrue asserts the OYLRLink's surveillance-
// resistant flag is the locked design property (OY-LR is surveillance-
// resistant per vision §14). The zero-value is false; the constructor pattern
// must set it true. This test asserts a populated link has it true.
func TestOYLRLinkSurveillanceResistantTrue(t *testing.T) {
link := btypes.OYLRLink{SurveillanceResistant: true}
if !link.SurveillanceResistant {
t.Error("OYLRLink.SurveillanceResistant must be true for OY-LR (§14)")
}
}
// TestBeaconFrameStructNonEmpty asserts the BeaconFrame struct is non-empty
// when populated, and that ttl > 0 for a valid frame.
func TestBeaconFrameStructNonEmpty(t *testing.T) {
frame := btypes.BeaconFrame{
BeaconID: "beacon-1",
EphemeralID: "eph-abc",
PayloadBytes: []byte{0x01, 0x02},
TTL: 300,
}
if frame.BeaconID != "beacon-1" {
t.Errorf("BeaconID = %q", frame.BeaconID)
}
if frame.EphemeralID != "eph-abc" {
t.Errorf("EphemeralID = %q", frame.EphemeralID)
}
if len(frame.PayloadBytes) != 2 {
t.Errorf("PayloadBytes len = %d", len(frame.PayloadBytes))
}
if frame.TTL <= 0 {
t.Errorf("TTL = %d, must be > 0 for a valid frame", frame.TTL)
}
}
// TestBeaconFrameTTLPositive asserts a valid BeaconFrame has TTL > 0.
func TestBeaconFrameTTLPositive(t *testing.T) {
cases := []int64{1, 60, 300, 3600}
for _, ttl := range cases {
f := btypes.BeaconFrame{TTL: ttl}
if f.TTL <= 0 {
t.Errorf("TTL = %d, must be > 0", f.TTL)
}
}
}
// TestDefaultGenesisStateUnchanged asserts DefaultGenesisState is unchanged
// by the v0.2 extension (no regression — the v0.1 GenesisState shape is
// preserved).
func TestDefaultGenesisStateUnchanged(t *testing.T) {
gs := btypes.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
}
// TestValidateGenesisUnchanged asserts ValidateGenesis is unchanged (no
// regression — v0.1 returned nil unconditionally; the extension preserves
// this).
func TestValidateGenesisUnchanged(t *testing.T) {
if err := btypes.ValidateGenesis(nil); err != nil {
t.Errorf("ValidateGenesis should return nil (no regression); got: %v", err)
}
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// The bearers extension must not introduce banned terms. The lexicon helpers
// are used here — no banned literals are inlined in this test file.
// TestLexiconNoBannedTermsInBearersPackage scans every non-test .go file in
// the bearers/types package directory for the banned terms (case-insensitive).
// Production files only — the test file references banned terms via the
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
func TestLexiconNoBannedTermsInBearersPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/bearers/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in bearers/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — D-029 extension)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInBearersTestFile asserts this test file itself does
// not contain any banned term as a literal (the firewall scans test files
// too; the lexicon helpers must be used rather than inlining banned terms).
func TestLexiconNoBannedTermsInBearersTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("bearers test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/bearers/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+3 -3
View File
@@ -36,7 +36,7 @@ type BloomBoosterBucket struct {
UnlockHeight int64 `json:"unlock_height" yaml:"unlock_height"`
}
// BloomSource defines where Bloom originates (§6: only from real yield)
// BloomSource defines where Bloom originates (§6: only from real production)
type BloomSource string
const (
@@ -46,10 +46,10 @@ const (
BloomFromRwaCashflow BloomSource = "RwaCashflow"
)
// MissionLockBloom: Bloom originates ONLY from real yield (§6)
// MissionLockBloom: Bloom originates ONLY from real production (§6)
// No synthetic Bloom. No protocol-printed Bloom.
// This is a Mission Lock — no Council vote can change it.
const MissionLockBloom = "Bloom originates only from real yield. No synthetic Bloom. No protocol-printed Bloom."
const MissionLockBloom = "Bloom originates only from real production. No synthetic Bloom. No protocol-printed Bloom."
type Params struct {
TargetRateBps uint32 `json:"target_rate_bps" yaml:"target_rate_bps"`
+54
View File
@@ -0,0 +1,54 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the bond
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
// the security-engineer's test assertions live in types_test.go.
//
// The Bond genesis schema has one top-level set: Bonds (the issued bonds).
// The invariants enforced at genesis load are (1) bond-id uniqueness, and
// (2) the coupon clamp — each genesis bond's coupon-bps must be within
// [CouponFloorBps, CouponCapBps]. The clamp invariant is the highest-severity
// bond firewall (D-028): a genesis bond with a coupon above the cap or below
// the floor is rejected at genesis load.
// ValidateBonds asserts bond-ids are present and unique, that each bond's
// status is a known BondStatus, and that each bond's coupon-bps is within
// the LOCKED bounds [CouponFloorBps, CouponCapBps] (the genesis-side clamp
// enforcement — D-028). ValidateBonds is the data-engineer's schema
// validator, composed by ValidateGenesis in types.go.
func ValidateBonds(bonds []Bond) error {
seen := make(map[string]bool, len(bonds))
for i, b := range bonds {
if b.BondID == "" {
return fmt.Errorf("bond [%d]: empty bond-id", i)
}
if seen[b.BondID] {
return fmt.Errorf("bond: duplicate bond-id %q", b.BondID)
}
seen[b.BondID] = true
if !knownBondStatus(b.Status) {
return fmt.Errorf("bond %q: unknown bond status %q", b.BondID, b.Status)
}
// Genesis-side clamp enforcement (D-028): a genesis bond's coupon
// must be within the LOCKED [floor, cap] bounds. A bond with an
// out-of-bounds coupon is rejected at genesis load rather than
// silently clamped — the genesis schema is authoritative.
if b.CouponBps < CouponFloorBps || b.CouponBps > CouponCapBps {
return fmt.Errorf("bond %q: coupon-bps %d outside [%d, %d] (D-028 clamp at genesis load)",
b.BondID, b.CouponBps, CouponFloorBps, CouponCapBps)
}
}
return nil
}
// knownBondStatus reports whether s is one of the five BondStatus values.
func knownBondStatus(s BondStatus) bool {
for _, ss := range AllBondStatuses() {
if s == ss {
return true
}
}
return false
}
+97
View File
@@ -0,0 +1,97 @@
package types_test
import (
"encoding/json"
"testing"
btypes "github.com/oy/openyield/x/bond/types"
)
// genesis_test.go holds the security-engineer's genesis-clamp test assertions
// for the bond module (G-008 — security-engineer owns ALL *_test.go files,
// including genesis_test.go). These tests focus on the data-engineer's
// genesis schema clamp enforcement (P4-01-03): ValidateGenesis rejects any
// genesis bond whose coupon-bps is outside the LOCKED [floor, cap] bounds.
// The clamp invariant (D-028) is the highest-severity bond firewall; the
// genesis load is the first enforcement point.
// TestGenesisClampRejectsAboveCapForManyBonds asserts that multiple bonds,
// each with a coupon above the cap, are all rejected. The genesis clamp
// applies per-bond (not just the first).
func TestGenesisClampRejectsAboveCapForManyBonds(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{
{BondID: "b1", IssuerStandID: "s1", CouponBps: 801, Status: btypes.BondIssued},
{BondID: "b2", IssuerStandID: "s1", CouponBps: 900, Status: btypes.BondActive},
{BondID: "b3", IssuerStandID: "s1", CouponBps: 5000, Status: btypes.BondMatured},
},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject bonds with coupon-bps above cap")
}
}
// TestGenesisClampAcceptsAtBounds asserts bonds at the floor (0) and cap (800)
// are accepted at genesis load (boundary inclusive).
func TestGenesisClampAcceptsAtBounds(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{
{BondID: "b-floor", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued},
{BondID: "b-cap", IssuerStandID: "s1", CouponBps: 800, Status: btypes.BondIssued},
},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept bonds at floor (0) and cap (800); got: %v", err)
}
}
// TestGenesisClampRejectsJustAboveCap asserts a coupon 1 bps above the cap is
// rejected (off-by-one regression firewall).
func TestGenesisClampRejectsJustAboveCap(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 801, Status: btypes.BondIssued}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject coupon-bps == 801 (just above cap 800)")
}
}
// TestGenesisClampAcceptsJustBelowCap asserts a coupon 1 bps below the cap is
// accepted.
func TestGenesisClampAcceptsJustBelowCap(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 799, Status: btypes.BondIssued}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept coupon-bps == 799 (just below cap); got: %v", err)
}
}
// TestGenesisValidateBondsRejectsDup asserts the data-engineer's ValidateBonds
// helper rejects duplicate bond-ids.
func TestGenesisValidateBondsRejectsDup(t *testing.T) {
bonds := []btypes.Bond{
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
{BondID: "b1", IssuerStandID: "s2", CouponBps: 200, Status: btypes.BondActive},
}
if err := btypes.ValidateBonds(bonds); err == nil {
t.Error("ValidateBonds should reject duplicate bond-ids")
}
}
// TestGenesisValidateBondsAcceptsClean asserts ValidateBonds accepts a clean
// set of bonds.
func TestGenesisValidateBondsAcceptsClean(t *testing.T) {
bonds := []btypes.Bond{
{BondID: "b1", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued},
{BondID: "b2", IssuerStandID: "s1", CouponBps: 500, Status: btypes.BondActive},
{BondID: "b3", IssuerStandID: "s2", CouponBps: 800, Status: btypes.BondMatured},
}
if err := btypes.ValidateBonds(bonds); err != nil {
t.Errorf("ValidateBonds should accept clean bonds; got: %v", err)
}
}
+145
View File
@@ -0,0 +1,145 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "bond"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// CouponCapBps is the upper bound on a bond coupon in basis points
// (vision §17, REQ-021, D-028). Mission-locked at 8pct (800 bps); no
// Council vote can change it. The bond module is the highest lexicon-risk
// package (A-210): the coupon vocabulary is used EXCLUSIVELY here — the
// banned financial terms that are natural coupon-synonyms are NEVER used
// in this package. The security-engineer's lexicon assertion in
// types_test.go is the firewall gate.
CouponCapBps = 800 // 8pct (cap, LOCKED — D-028)
// CouponFloorBps is the lower bound on a bond coupon in basis points
// (vision §17, REQ-021, D-028). Mission-locked at 0pct (0 bps); no
// Council vote can change it.
CouponFloorBps = 0 // 0pct (floor, LOCKED — D-028)
// BondStatusCount is the locked count of BondStatus enum values (vision
// §17, REQ-021). A regression firewall: adding/removing/renaming a bond
// status breaks this const's test.
BondStatusCount = 5
)
// BondStatus enumerates the bond lifecycle states (vision §17, REQ-021).
// The five statuses mirror a fixed-coupon commitment lifecycle: Issued
// (created), Active (in good standing), Matured (term reached), Defaulted
// (covenant breach), Repaid (principal returned).
type BondStatus string
const (
BondIssued BondStatus = "Issued" // created, not yet active
BondActive BondStatus = "Active" // in good standing
BondMatured BondStatus = "Matured" // term reached
BondDefaulted BondStatus = "Defaulted" // covenant breach
BondRepaid BondStatus = "Repaid" // principal returned
)
// AllBondStatuses returns all five BondStatus values in REQ-021 lifecycle
// order. Locked-const test asserts exactly 5 entries with these names.
func AllBondStatuses() []BondStatus {
return []BondStatus{
BondIssued,
BondActive,
BondMatured,
BondDefaulted,
BondRepaid,
}
}
// Bond is a fixed-coupon commitment issued by a Stand (vision §17, REQ-021).
// issuer-stand-id references x/stand by ID string (G-003 by-ID-string ref —
// P1-02-01 stand-id-ref; no struct import of x/stand). principal-grain is the
// principal in Grain (the OY internal unit, cross-ref x/bread). coupon-bps is
// the coupon rate in basis points, clamped to [CouponFloorBps, CouponCapBps]
// by Clamp at issuance and at genesis load. term-days is the term length.
// issued-at and maturity are unix timestamps. status is the lifecycle state.
type Bond struct {
BondID string `json:"bond_id" yaml:"bond_id"`
IssuerStandID string `json:"issuer_stand_id" yaml:"issuer_stand_id"`
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
TermDays uint32 `json:"term_days" yaml:"term_days"`
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
Maturity int64 `json:"maturity" yaml:"maturity"`
Status BondStatus `json:"status" yaml:"status"`
}
// Issue is the bond issuance stub (REQ-021, D-028). It constructs a Bond with
// the coupon clamped to [CouponFloorBps, CouponCapBps]. The stub does not
// persist or enforce referential integrity of issuer-stand-id (that is a
// v0.3 keeper concern); it only enforces the coupon clamp invariant at
// construction time. The returned Bond has status BondIssued.
func Issue(bondID, issuerStandID string, principalGrain int64, couponBps uint32, termDays uint32, issuedAt, maturity int64) Bond {
return Bond{
BondID: bondID,
IssuerStandID: issuerStandID,
PrincipalGrain: principalGrain,
CouponBps: Clamp(couponBps),
TermDays: termDays,
IssuedAt: issuedAt,
Maturity: maturity,
Status: BondIssued,
}
}
// Clamp ensures a coupon is within the LOCKED bounds (vision §17, REQ-021,
// D-028: never above the cap, never below the floor). This is automatic and
// authoritative; no Council vote can change it. The shape mirrors
// x/feecovenant's Clamp exactly (min(cap, max(floor, coupon))).
func Clamp(couponBps uint32) uint32 {
if couponBps > CouponCapBps {
return CouponCapBps
}
if couponBps < CouponFloorBps {
return CouponFloorBps
}
return couponBps
}
// Params for the bond module (skeleton — no tunables in v0.2; the cap and
// floor are LOCKED consts, not Params fields).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the bond module genesis state (REQ-021). Bonds is the
// top-level set of issued bonds. ValidateGenesis enforces bond-id uniqueness
// and the coupon clamp at genesis load (the data-engineer's genesis.go holds
// the schema helpers per G-008).
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Bonds []Bond `json:"bonds" yaml:"bonds"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Bonds: []Bond{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate bond-ids, and runs the coupon clamp at genesis
// load (each genesis bond's coupon-bps must be within [floor, cap]). Delegates
// to the data-engineer's genesis.go helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("bond: invalid genesis: %w", err)
}
if err := ValidateBonds(gs.Bonds); err != nil {
return fmt.Errorf("bond: %w", err)
}
return nil
}
+431
View File
@@ -0,0 +1,431 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
btypes "github.com/oy/openyield/x/bond/types"
)
// --- Clamp invariant tests (highest-severity for bond) --------------------------
// The Clamp invariant is the bond module's firewall (D-028): a bond coupon
// can never exceed the cap (8pct) and can never fall below the floor (0pct).
// These tests are the regression firewall — a change to CouponCapBps or
// CouponFloorBps breaks them.
// TestCouponCapBpsLockedConst asserts CouponCapBps == 800 (8pct, D-028 LOCKED).
// A regression firewall: changing the cap breaks this test.
func TestCouponCapBpsLockedConst(t *testing.T) {
if btypes.CouponCapBps != 800 {
t.Errorf("CouponCapBps = %d, expected 800 (8pct — D-028 LOCKED)", btypes.CouponCapBps)
}
}
// TestCouponFloorBpsLockedConst asserts CouponFloorBps == 0 (0pct, D-028 LOCKED).
// A regression firewall: changing the floor breaks this test.
func TestCouponFloorBpsLockedConst(t *testing.T) {
if btypes.CouponFloorBps != 0 {
t.Errorf("CouponFloorBps = %d, expected 0 (0pct — D-028 LOCKED)", btypes.CouponFloorBps)
}
}
// TestClampBelowFloorReturnsFloor asserts a coupon below the floor is clamped
// up to the floor.
func TestClampBelowFloorReturnsFloor(t *testing.T) {
// Negative coupons are not representable (uint32); the only "below floor"
// case is impossible since the floor is 0 and the type is uint32. The test
// asserts the floor value itself passes through (the in-range boundary).
// A future floor > 0 would make this test assert negative-clamping; the
// current floor == 0 means the below-floor case is type-prevented.
got := btypes.Clamp(btypes.CouponFloorBps)
if got != btypes.CouponFloorBps {
t.Errorf("Clamp(floor) = %d, expected floor %d", got, btypes.CouponFloorBps)
}
}
// TestClampAboveCapReturnsCap asserts a coupon above the cap is clamped down
// to the cap.
func TestClampAboveCapReturnsCap(t *testing.T) {
cases := []uint32{
uint32(btypes.CouponCapBps) + 1,
uint32(btypes.CouponCapBps) + 100,
uint32(btypes.CouponCapBps) + 1000,
900,
1000,
5000,
}
for _, c := range cases {
got := btypes.Clamp(c)
if got != btypes.CouponCapBps {
t.Errorf("Clamp(%d) = %d, expected cap %d (above-cap must clamp to cap)", c, got, btypes.CouponCapBps)
}
}
}
// TestClampInRangeUnchanged asserts a coupon within [floor, cap] is unchanged.
func TestClampInRangeUnchanged(t *testing.T) {
cases := []uint32{
0,
1,
100,
400,
500,
799,
uint32(btypes.CouponCapBps),
}
for _, c := range cases {
got := btypes.Clamp(c)
if got != c {
t.Errorf("Clamp(%d) = %d, expected %d (in-range must be unchanged)", c, got, c)
}
}
}
// TestClampMatchesFeeCovenantShape asserts the bond Clamp has the same shape
// as x/feecovenant's Clamp: min(cap, max(floor, coupon)). The test verifies
// the boundary semantics rather than importing feecovenant (no cross-module
// struct imports per G-003, though cross-module const access is allowed).
func TestClampMatchesFeeCovenantShape(t *testing.T) {
// The shape is min(cap, max(floor, coupon)). For floor=0 and cap=800:
// min(800, max(0, coupon))
// In-range passes through; above-cap clamps to cap; below-floor clamps to
// floor (here, floor=0, so type-prevented for uint32).
if btypes.Clamp(0) != 0 {
t.Error("Clamp(0) should be 0 (floor boundary)")
}
if btypes.Clamp(800) != 800 {
t.Error("Clamp(800) should be 800 (cap boundary)")
}
if btypes.Clamp(801) != 800 {
t.Error("Clamp(801) should be 800 (above-cap clamps to cap)")
}
}
// TestClampInvariantBreaksIfCapChanges is the regression-firewall meta-assert:
// if CouponCapBps were changed, the above-cap test would break. This test
// documents the invariant: Clamp(above-cap) == cap, for the current cap.
func TestClampInvariantBreaksIfCapChanges(t *testing.T) {
above := uint32(btypes.CouponCapBps) + 50
if btypes.Clamp(above) != btypes.CouponCapBps {
t.Errorf("Clamp(%d) = %d, expected CouponCapBps %d (invariant: above-cap clamps to cap)", above, btypes.Clamp(above), btypes.CouponCapBps)
}
}
// --- BondStatus enum coverage (5) ----------------------------------------------
// TestBondStatusCountLockedConst asserts BondStatusCount == 5 and
// AllBondStatuses() returns exactly 5 (REQ-021). A regression firewall.
func TestBondStatusCountLockedConst(t *testing.T) {
if btypes.BondStatusCount != 5 {
t.Errorf("BondStatusCount = %d, expected 5 (REQ-021 LOCKED)", btypes.BondStatusCount)
}
all := btypes.AllBondStatuses()
if len(all) != 5 {
t.Errorf("AllBondStatuses() len = %d, expected 5", len(all))
}
}
// TestAllBondStatusesNames asserts the 5 REQ-021 names in order with no
// extras, no dups, no renames.
func TestAllBondStatusesNames(t *testing.T) {
want := []string{"Issued", "Active", "Matured", "Defaulted", "Repaid"}
all := btypes.AllBondStatuses()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllBondStatuses()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate BondStatus %q", s)
}
seen[string(s)] = true
}
}
// TestBondStatusValues asserts each named const matches its AllBondStatuses
// entry.
func TestBondStatusValues(t *testing.T) {
if btypes.BondIssued != "Issued" {
t.Errorf("BondIssued = %q", btypes.BondIssued)
}
if btypes.BondActive != "Active" {
t.Errorf("BondActive = %q", btypes.BondActive)
}
if btypes.BondMatured != "Matured" {
t.Errorf("BondMatured = %q", btypes.BondMatured)
}
if btypes.BondDefaulted != "Defaulted" {
t.Errorf("BondDefaulted = %q", btypes.BondDefaulted)
}
if btypes.BondRepaid != "Repaid" {
t.Errorf("BondRepaid = %q", btypes.BondRepaid)
}
}
// --- Issue stub callable -------------------------------------------------------
// TestIssueStubCallable asserts the Issue stub is callable and returns a
// Bond with the coupon clamped and status BondIssued.
func TestIssueStubCallable(t *testing.T) {
b := btypes.Issue("bond-1", "stand-abc", 1_000_000, 500, 365, 1000, 1365)
if b.BondID != "bond-1" {
t.Errorf("BondID = %q", b.BondID)
}
if b.IssuerStandID != "stand-abc" {
t.Errorf("IssuerStandID = %q", b.IssuerStandID)
}
if b.PrincipalGrain != 1_000_000 {
t.Errorf("PrincipalGrain = %d", b.PrincipalGrain)
}
if b.CouponBps != 500 {
t.Errorf("CouponBps = %d, expected 500 (in-range, unchanged)", b.CouponBps)
}
if b.TermDays != 365 {
t.Errorf("TermDays = %d", b.TermDays)
}
if b.IssuedAt != 1000 || b.Maturity != 1365 {
t.Errorf("IssuedAt=%d Maturity=%d", b.IssuedAt, b.Maturity)
}
if b.Status != btypes.BondIssued {
t.Errorf("Status = %q, expected Issued", b.Status)
}
}
// TestIssueStubClampsAboveCap asserts the Issue stub clamps an above-cap
// coupon down to the cap.
func TestIssueStubClampsAboveCap(t *testing.T) {
b := btypes.Issue("bond-2", "stand-abc", 1_000_000, 1200, 365, 1000, 1365)
if b.CouponBps != btypes.CouponCapBps {
t.Errorf("CouponBps = %d, expected cap %d (Issue must clamp above-cap coupon)", b.CouponBps, btypes.CouponCapBps)
}
}
// --- Bond struct fields --------------------------------------------------------
// TestBondStructFields asserts the Bond struct carries all required fields
// including the by-ID-string ref to x/stand (issuer-stand-id per G-003).
func TestBondStructFields(t *testing.T) {
b := btypes.Bond{
BondID: "bond-3",
IssuerStandID: "stand-xyz",
PrincipalGrain: 500_000,
CouponBps: 300,
TermDays: 180,
IssuedAt: 2000,
Maturity: 2180,
Status: btypes.BondActive,
}
if b.BondID != "bond-3" || b.IssuerStandID != "stand-xyz" || b.PrincipalGrain != 500_000 ||
b.CouponBps != 300 || b.TermDays != 180 || b.IssuedAt != 2000 || b.Maturity != 2180 ||
b.Status != btypes.BondActive {
t.Error("Bond fields not set correctly")
}
}
// TestBondIssuerStandIDIsString asserts issuer-stand-id is string-typed
// (G-003 by-ID-string ref to x/stand; no struct import).
func TestBondIssuerStandIDIsString(t *testing.T) {
b := btypes.Bond{IssuerStandID: "stand-abc"}
if b.IssuerStandID != "stand-abc" {
t.Errorf("IssuerStandID = %q", b.IssuerStandID)
}
}
// --- Genesis -------------------------------------------------------------------
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slice for Bonds.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := btypes.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Bonds == nil || len(gs.Bonds) != 0 {
t.Errorf("Default Bonds should be non-nil empty slice; got len=%d nil=%v", len(gs.Bonds), gs.Bonds == nil)
}
}
// TestValidateGenesisRejectsDupBondIDs asserts A-212: duplicate bond-ids are
// rejected.
func TestValidateGenesisRejectsDupBondIDs(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
{BondID: "b1", IssuerStandID: "s2", CouponBps: 200, Status: btypes.BondActive}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate bond-ids")
}
}
// TestValidateGenesisRejectsEmptyBondID asserts empty bond-id is rejected.
func TestValidateGenesisRejectsEmptyBondID(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty bond-id")
}
}
// TestValidateGenesisRejectsUnknownBondStatus asserts an unknown BondStatus
// is rejected.
func TestValidateGenesisRejectsUnknownBondStatus(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondStatus("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown bond status")
}
}
// TestValidateGenesisRejectsCouponAboveCap asserts the genesis-side clamp: a
// genesis bond with coupon-bps above the cap is rejected (D-028).
func TestValidateGenesisRejectsCouponAboveCap(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: uint32(btypes.CouponCapBps) + 1, Status: btypes.BondIssued}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject coupon-bps above cap (D-028 clamp at genesis load)")
}
}
// TestValidateGenesisRejectsCouponBelowFloor asserts the genesis-side clamp:
// a genesis bond with coupon-bps below the floor is rejected (D-028).
func TestValidateGenesisRejectsCouponBelowFloor(t *testing.T) {
// Floor is 0; a uint32 cannot be below 0, so this test asserts the
// boundary: coupon-bps == 0 (the floor) is accepted. The below-floor case
// is type-prevented. We assert the floor boundary passes.
gs := btypes.GenesisState{
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued}},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept coupon-bps == floor (0); got: %v", err)
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := btypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := btypes.GenesisState{
Bonds: []btypes.Bond{
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
{BondID: "b2", IssuerStandID: "s1", CouponBps: 800, Status: btypes.BondActive},
},
}
bz, _ := json.Marshal(gs)
if err := btypes.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// --- Module consts -------------------------------------------------------------
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if btypes.ModuleName != "bond" {
t.Errorf("ModuleName = %q", btypes.ModuleName)
}
if btypes.StoreKey != "bond" {
t.Errorf("StoreKey = %q", btypes.StoreKey)
}
if btypes.RouterKey != "bond" {
t.Errorf("RouterKey = %q", btypes.RouterKey)
}
if btypes.QuerierRoute != "bond" {
t.Errorf("QuerierRoute = %q", btypes.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = btypes.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// The bond module is the HIGHEST lexicon-risk package (A-210): the banned
// terms that are natural coupon-synonyms ("intere"+"st", "yie"+"ld") must
// NEVER appear. The coupon vocabulary is used EXCLUSIVELY. The lexicon
// helpers are used here — no banned literals are inlined in this test file.
// TestLexiconNoBannedTermsInBondPackage scans every non-test .go file in the
// bond/types package directory for the banned terms (case-insensitive).
// Production files only — the test file references banned terms via the
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
func TestLexiconNoBannedTermsInBondPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/bond/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in bond/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — A-210 coupon-only vocabulary)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInBondTestFile asserts this test file itself does
// not contain any banned term as a literal (the firewall scans test files
// too; the lexicon helpers must be used rather than inlining banned terms).
func TestLexiconNoBannedTermsInBondTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("bond test file contains banned term %q — use lexicon helpers, not literals (A-210)", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/bond/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+123
View File
@@ -0,0 +1,123 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the
// council module (G-008 split). ValidateGenesis in types.go composes these
// helpers; the security-engineer's test assertions live in types_test.go.
//
// The Council genesis schema has two top-level sets: Councils (the three
// governance councils — Mesh/Guild/Stand) and Voices (the Voice-tally
// set). The invariants enforced at genesis load are (1) council-id
// uniqueness, (2) voice-id uniqueness, (3) referential integrity (each
// Voice's council-id references an existing Council), and (4) the
// Mission-Lock check (the global MissionLockAmendable const bool is the
// firewall — this helper is the genesis-side echo).
// ValidateCouncils asserts council-ids are present and unique, and that
// each Council's kind is a known CouncilKind. A Stand Council must populate
// stand-id-ref (by-ID-string ref to x/stand); a Guild Council must populate
// guild-id-ref (by-ID-string ref to x/guild). A Mesh Council leaves both
// refs empty. ValidateCouncils is the data-engineer's schema validator,
// composed by ValidateGenesis in types.go.
func ValidateCouncils(councils []Council) error {
seen := make(map[string]bool, len(councils))
for i, c := range councils {
if c.CouncilID == "" {
return fmt.Errorf("council [%d]: empty council-id", i)
}
if seen[c.CouncilID] {
return fmt.Errorf("council: duplicate council-id %q", c.CouncilID)
}
seen[c.CouncilID] = true
if !knownCouncilKind(c.Kind) {
return fmt.Errorf("council %q: unknown council kind %q", c.CouncilID, c.Kind)
}
// A Stand Council must reference a Stand by-ID-string (P1-02-01 ref).
if c.Kind == CouncilStand && c.StandIDRef == "" {
return fmt.Errorf("council %q: Stand Council missing stand-id-ref", c.CouncilID)
}
// A Guild Council must reference a Guild by-ID-string (P1-03-01 ref).
if c.Kind == CouncilGuild && c.GuildIDRef == "" {
return fmt.Errorf("council %q: Guild Council missing guild-id-ref", c.CouncilID)
}
}
if err := MissionLockCheck(councils); err != nil {
return err
}
return nil
}
// ValidateVoices asserts voice-ids are present and unique, and that each
// Voice's council-id references an existing Council in the genesis set
// (referential integrity — the P3-01-03 deliverable: each Voice tally's
// council-id must resolve to a genesis Council). signal-kind must be a
// known SignalKind (the four Freeholder signals, cross-ref REQ-005). The
// referential-integrity check is the data-engineer's genesis invariant: a
// Voice tally pointing at a non-existent Council is rejected at genesis
// load (no orphan tallies).
func ValidateVoices(voices []Voice, councils []Council) error {
councilIDs := make(map[string]bool, len(councils))
for _, c := range councils {
councilIDs[c.CouncilID] = true
}
seen := make(map[string]bool, len(voices))
for i, v := range voices {
if v.VoiceID == "" {
return fmt.Errorf("voice [%d]: empty voice-id", i)
}
if seen[v.VoiceID] {
return fmt.Errorf("voice: duplicate voice-id %q", v.VoiceID)
}
seen[v.VoiceID] = true
if !councilIDs[v.CouncilID] {
return fmt.Errorf("voice %q: council-id %q does not reference an existing council", v.VoiceID, v.CouncilID)
}
if !knownSignalKind(v.SignalKind) {
return fmt.Errorf("voice %q: unknown signal-kind %q", v.VoiceID, v.SignalKind)
}
}
return nil
}
// knownCouncilKind reports whether k is one of the three CouncilKind values.
func knownCouncilKind(k CouncilKind) bool {
for _, kk := range AllCouncilKinds() {
if k == kk {
return true
}
}
return false
}
// knownSignalKind reports whether s is one of the four SignalKind values.
func knownSignalKind(s SignalKind) bool {
for _, kk := range AllSignalKinds() {
if s == kk {
return true
}
}
return false
}
// MissionLockCheck asserts the Mission-Lock invariant on a slice of
// Councils (vision §19, REQ-011). Because MissionLockAmendable is a compile-
// time const bool == false, this check always passes — it exists as the
// data-engineer's genesis-side assertion that the Mission-Lock firewall is
// intact. If the const ever flipped to true (which the test suite rejects),
// the genesis load would surface it here. The helper is the genesis hook
// for v0.3 keeper logic to extend with live per-council Mission-Lock
// enforcement.
func MissionLockCheck(councils []Council) error {
// The global MissionLockAmendable const is the firewall: if it were ever
// flipped to true (which the test suite rejects), the genesis load would
// surface it here. The per-council loop is the hook for v0.3 live logic.
if MissionLockAmendable {
return fmt.Errorf("council: Mission Lock amendable (MissionLockAmendable == true) — firewall breach")
}
for range councils {
// No per-council runtime data to verify in the skeleton — the const
// is the source of truth. The loop preserves the hook point.
}
return nil
}
+187
View File
@@ -0,0 +1,187 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "council"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// CouncilKindCount is the locked count of CouncilKind enum values
// (vision §13 / REQ-011). A regression firewall: adding/removing/renaming
// a Council kind breaks this const's test.
CouncilKindCount = 3
// MissionLockAmendable is the Mission-Lock invariant (vision §19, REQ-011):
// the Six Principles + Fee Covenant + no-amend covenant can NEVER be
// amended by any council. This is a locked const bool — the highest-
// severity regression firewall in the council module. The const can
// NEVER be set true; the test asserts it is false and that no code path
// can flip it (the compile-time const is the firewall, not runtime data).
MissionLockAmendable = false
// SignalKindCount is the locked count of SignalKind enum values — the
// four Freeholder signals (vision §9.1 / REQ-005) plus Capital (REQ-011
// multi-source Voice). Cross-ref v0.1 x/standing FreeholderSignals.
SignalKindCount = 4
)
// CouncilKind enumerates the three governance councils (vision §13, REQ-011):
// Mesh Council (whole-mesh), Guild Council (guild-level), Stand Council
// (Stand-level). Each uses multi-source Voice. Mission Lock (the Six
// Principles + fee covenant + no-amend covenant) cannot be amended by any
// council — enforced by the compile-time MissionLockAmendable const bool.
type CouncilKind string
const (
CouncilMesh CouncilKind = "MeshCouncil" // whole-mesh council
CouncilGuild CouncilKind = "GuildCouncil" // guild-level council
CouncilStand CouncilKind = "StandCouncil" // Stand-level council
)
// AllCouncilKinds returns all three CouncilKind values in REQ-011 order.
// Locked-const test asserts exactly 3 entries with these names (REQ-011).
func AllCouncilKinds() []CouncilKind {
return []CouncilKind{
CouncilMesh,
CouncilGuild,
CouncilStand,
}
}
// Council is one of three governance councils (REQ-011). kind picks the
// tier (Mesh/Guild/Stand). stand-id-ref references x/stand by ID string
// (optional — only Stand Councils populate it; P1-02-01 by-ID-string ref).
// guild-id-ref references x/guild by ID string (optional — only Guild
// Councils populate it; P1-03-01 by-ID-string ref). Both refs are by-ID-
// string per G-003 (no struct imports of x/stand or x/guild). members is
// the voice-holder set; voice-threshold is the tally pass threshold.
type Council struct {
CouncilID string `json:"council_id" yaml:"council_id"`
Kind CouncilKind `json:"kind" yaml:"kind"`
StandIDRef string `json:"stand_id_ref,omitempty" yaml:"stand_id_ref,omitempty"`
GuildIDRef string `json:"guild_id_ref,omitempty" yaml:"guild_id_ref,omitempty"`
Members []CouncilMember `json:"members" yaml:"members"`
VoiceThreshold uint32 `json:"voice_threshold" yaml:"voice_threshold"`
}
// CouncilMember is a voice-holder in a Council (REQ-011). reach-id
// references x/identity Reach by string (G-003 — the lexicon-clean holder
// identifier; the banned financial holder term is NOT used here). voice-
// weight is the member's Voice weight in the tally; joined-at is the join
// timestamp.
type CouncilMember struct {
ReachID string `json:"reach_id" yaml:"reach_id"`
VoiceWeight uint32 `json:"voice_weight" yaml:"voice_weight"`
JoinedAt int64 `json:"joined_at" yaml:"joined_at"`
}
// Voice is a single Voice signal cast on a Council proposal (REQ-011).
// council-id references the Council by ID string (G-003). proposer-reach
// references x/identity Reach by string (lexicon-clean holder identifier;
// the banned financial holder term is NOT used).
// signal-kind picks the multi-source Voice input (Stash/Standing/Vouch/
// Capital — the four Freeholder signals, cross-ref v0.1 REQ-005
// FreeholderSignals). target-ref is the proposal/option the Voice targets
// (opaque string ref). tally is the running tally result; timestamp is the
// cast time.
type Voice struct {
VoiceID string `json:"voice_id" yaml:"voice_id"`
CouncilID string `json:"council_id" yaml:"council_id"`
ProposerReach string `json:"proposer_reach" yaml:"proposer_reach"`
SignalKind SignalKind `json:"signal_kind" yaml:"signal_kind"`
TargetRef string `json:"target_ref" yaml:"target_ref"`
Tally TallyResult `json:"tally" yaml:"tally"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
}
// SignalKind enumerates the multi-source Voice inputs (REQ-011). The four
// Freeholder signals (vision §9.1 / REQ-005, cross-ref x/standing
// FreeholderSignals): Stash, Standing, Vouch, Capital. No "Freeholder"
// SignalKind — the four signals are the inputs a Freeholder-eligible Reach
// casts; the eligibility is upstream (x/standing). Capital is the committed-
// capital signal (vision §9.1 committed_capital).
type SignalKind string
const (
SignalStash SignalKind = "Stash" // Stash-maturity signal (vision §9.1)
SignalStanding SignalKind = "Standing" // multi-domain Standing signal (§9.1)
SignalVouch SignalKind = "Vouch" // community endorsement / Vouch (§9.1)
SignalCapital SignalKind = "Capital" // committed-capital signal (§9.1)
)
// AllSignalKinds returns all four SignalKind values in REQ-005 / vision §9.1
// order. Locked-const test asserts exactly 4 entries (cross-ref v0.1
// x/standing FreeholderSignals: StashMaturity, MultiDomainStanding,
// CommittedCapital, CommunityEndorsement — the four signals map to
// Stash/Standing/Capital/Vouch here).
func AllSignalKinds() []SignalKind {
return []SignalKind{
SignalStash,
SignalStanding,
SignalVouch,
SignalCapital,
}
}
// TallyResult mirrors Cosmos SDK x/gov TallyResult shape (A-204) for
// future wiring of Council governance to x/gov. Fields: yes, no, abstain
// (no "no-with-veto" — anti-greed, vision §19), nowithveto (kept as a
// zero-locked field for x/gov shape parity — always 0 in OY since the
// VoteOption enum has no veto option), total (total Voice cast). The
// quorum-met flag is the tally pass indicator. The field names (yes, no,
// abstain) match x/gov exactly so a future x/gov wiring is mechanical.
type TallyResult struct {
Yes uint64 `json:"yes" yaml:"yes"`
No uint64 `json:"no" yaml:"no"`
Abstain uint64 `json:"abstain" yaml:"abstain"`
NoWithVeto uint64 `json:"nowithveto" yaml:"nowithveto"` // always 0 — no veto option (anti-greed)
Total uint64 `json:"total" yaml:"total"`
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
}
// Params for the council module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the council module genesis state (REQ-011).
// Councils is the top-level set of three Council kinds; Voices is the
// Voice-tally set. ValidateGenesis enforces council-id uniqueness,
// voice-id uniqueness, and the Mission-Lock check (the const firewall echo).
// The data-engineer's genesis.go holds the schema helpers (G-008).
type GenesisState struct {
Councils []Council `json:"councils" yaml:"councils"`
Voices []Voice `json:"voices" yaml:"voices"`
Params Params `json:"params" yaml:"params"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Councils: []Council{},
Voices: []Voice{},
Params: DefaultParams(),
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate council-ids and duplicate voice-ids, and runs
// the Mission-Lock check. Delegates to the data-engineer's genesis.go
// helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("council: invalid genesis: %w", err)
}
if err := ValidateCouncils(gs.Councils); err != nil {
return fmt.Errorf("council: %w", err)
}
if err := ValidateVoices(gs.Voices, gs.Councils); err != nil {
return fmt.Errorf("council: %w", err)
}
return nil
}
+506
View File
@@ -0,0 +1,506 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/council/types"
)
// TestCouncilKindCountLockedConst asserts CouncilKindCount is exactly 3
// and AllCouncilKinds() returns exactly 3 (REQ-011). A regression firewall:
// adding/removing/renaming a Council kind breaks this test.
func TestCouncilKindCountLockedConst(t *testing.T) {
if types.CouncilKindCount != 3 {
t.Errorf("CouncilKindCount = %d, expected 3 (REQ-011 LOCKED)", types.CouncilKindCount)
}
all := types.AllCouncilKinds()
if len(all) != 3 {
t.Errorf("AllCouncilKinds() len = %d, expected 3", len(all))
}
}
// TestAllCouncilKindsNames asserts the 3 REQ-011 names in order with no
// extras, no dups, no renames.
func TestAllCouncilKindsNames(t *testing.T) {
want := []string{"MeshCouncil", "GuildCouncil", "StandCouncil"}
all := types.AllCouncilKinds()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, k := range all {
if string(k) != want[i] {
t.Errorf("AllCouncilKinds()[%d] = %q, want %q", i, k, want[i])
}
if seen[string(k)] {
t.Errorf("duplicate CouncilKind %q", k)
}
seen[string(k)] = true
}
}
// TestCouncilKindValues asserts each named const matches its AllCouncilKinds
// entry.
func TestCouncilKindValues(t *testing.T) {
if types.CouncilMesh != "MeshCouncil" {
t.Errorf("CouncilMesh = %q", types.CouncilMesh)
}
if types.CouncilGuild != "GuildCouncil" {
t.Errorf("CouncilGuild = %q", types.CouncilGuild)
}
if types.CouncilStand != "StandCouncil" {
t.Errorf("CouncilStand = %q", types.CouncilStand)
}
}
// TestMissionLockAmendableConstFalse asserts the global Mission-Lock const
// is false (vision §19, REQ-011): the Mission Lock can NEVER be amended.
// This is the highest-severity regression firewall for the council module.
// The const can NEVER be set true; this test is the firewall that breaks if
// anyone flips the const.
func TestMissionLockAmendableConstFalse(t *testing.T) {
if types.MissionLockAmendable != false {
t.Fatalf("MissionLockAmendable = %v, expected false (Mission Lock non-amendable — vision §19)", types.MissionLockAmendable)
}
// Re-assert via a bool-typed comparison so the test fails to compile if
// the const is ever changed to a non-bool type (defence in depth).
var isFalse bool = types.MissionLockAmendable == false
if !isFalse {
t.Fatal("MissionLockAmendable must equal false")
}
}
// TestMissionLockAmendableCannotBeSetTrue asserts the const cannot be set
// true — it is a compile-time const, not a runtime variable. The test
// constructs an expression that would fail to compile if the const were a
// mutable var (the const-ness is the firewall). This is the regression
// firewall the spec mandates: "a test asserting it can never be set true".
func TestMissionLockAmendableCannotBeSetTrue(t *testing.T) {
// The const is declared as `const MissionLockAmendable = false`. Go
// consts cannot be reassigned at runtime. The test below would be a
// compile error if it tried to assign to the const:
// types.MissionLockAmendable = true // cannot assign to const
// So the firewall IS the compile-time const-ness. We assert the value
// is false and the type is bool (so a future change to a string or int
// would break the typed comparison above). The regression guard is that
// any PR flipping the const to true breaks TestMissionLockAmendableConstFalse
// AND any PR changing it to a var breaks the `const` declaration (Go
// compiler rejects assignment to a var-typed const in other code paths).
if types.MissionLockAmendable {
t.Fatal("MissionLockAmendable must be false; the const is the firewall — flipping it to true is a Mission Lock breach")
}
}
// TestSignalKindCountLockedConst asserts SignalKindCount is exactly 4
// (the four Freeholder signals, cross-ref v0.1 REQ-005 / vision §9.1).
func TestSignalKindCountLockedConst(t *testing.T) {
if types.SignalKindCount != 4 {
t.Errorf("SignalKindCount = %d, expected 4 (REQ-005 four Freeholder signals)", types.SignalKindCount)
}
all := types.AllSignalKinds()
if len(all) != 4 {
t.Errorf("AllSignalKinds() len = %d, expected 4", len(all))
}
}
// TestAllSignalKindsNames asserts the 4 signal names (Stash, Standing,
// Vouch, Capital) cross-ref v0.1 x/standing FreeholderSignals (StashMaturity,
// MultiDomainStanding, CommunityEndorsement, CommittedCapital).
func TestAllSignalKindsNames(t *testing.T) {
want := []string{"Stash", "Standing", "Vouch", "Capital"}
all := types.AllSignalKinds()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllSignalKinds()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate SignalKind %q", s)
}
seen[string(s)] = true
}
}
// TestSignalKindValues asserts each named const matches its AllSignalKinds
// entry.
func TestSignalKindValues(t *testing.T) {
if types.SignalStash != "Stash" {
t.Errorf("SignalStash = %q", types.SignalStash)
}
if types.SignalStanding != "Standing" {
t.Errorf("SignalStanding = %q", types.SignalStanding)
}
if types.SignalVouch != "Vouch" {
t.Errorf("SignalVouch = %q", types.SignalVouch)
}
if types.SignalCapital != "Capital" {
t.Errorf("SignalCapital = %q", types.SignalCapital)
}
}
// TestTallyResultStructShape asserts TallyResult mirrors x/gov shape (A-204):
// fields yes, no, abstain, nowithveto, total, quorum_met. The no-with-veto
// field is kept for x/gov parity but always 0 (OY has no veto option —
// anti-greed, vision §19). The test asserts the field names via JSON tags
// and that NoWithVeto is zero by default.
func TestTallyResultStructShape(t *testing.T) {
tr := types.TallyResult{
Yes: 10,
No: 3,
Abstain: 1,
NoWithVeto: 0, // always 0 — no veto option
Total: 14,
QuorumMet: true,
}
if tr.Yes != 10 || tr.No != 3 || tr.Abstain != 1 || tr.NoWithVeto != 0 ||
tr.Total != 14 || tr.QuorumMet != true {
t.Error("TallyResult fields not set correctly")
}
// x/gov field-name parity: marshal and check JSON tags.
bz, err := json.Marshal(tr)
if err != nil {
t.Fatalf("marshal: %v", err)
}
js := string(bz)
for _, tag := range []string{`"yes"`, `"no"`, `"abstain"`, `"nowithveto"`, `"total"`, `"quorum_met"`} {
if !strings.Contains(js, tag) {
t.Errorf("TallyResult JSON missing tag %s (x/gov shape parity A-204)", tag)
}
}
}
// TestTallyResultNoWithVetoAlwaysZero asserts the default TallyResult has
// NoWithVeto == 0 (the anti-greed invariant — no veto option in OY).
func TestTallyResultNoWithVetoAlwaysZero(t *testing.T) {
var tr types.TallyResult
if tr.NoWithVeto != 0 {
t.Errorf("default TallyResult.NoWithVeto = %d, expected 0 (no veto option — anti-greed)", tr.NoWithVeto)
}
}
// TestCouncilStructFields asserts Council carries all required fields
// including the by-ID-string refs (stand-id-ref, guild-id-ref per G-003).
func TestCouncilStructFields(t *testing.T) {
c := types.Council{
CouncilID: "c1",
Kind: types.CouncilStand,
StandIDRef: "stand-xyz",
GuildIDRef: "",
Members: []types.CouncilMember{{ReachID: "reach:a", VoiceWeight: 5, JoinedAt: 100}},
VoiceThreshold: 3,
}
if c.CouncilID != "c1" || c.Kind != types.CouncilStand || c.StandIDRef != "stand-xyz" ||
c.GuildIDRef != "" || len(c.Members) != 1 || c.VoiceThreshold != 3 {
t.Error("Council fields not set correctly")
}
}
// TestCouncilStructRefsAreStrings asserts stand-id-ref and guild-id-ref are
// string-typed (G-003 by-ID-string invariant; the G-003 import invariant is
// enforced project-wide by P1-01-02's go/parser scan, so this test only
// asserts the field types at the struct level, not cross-module imports).
func TestCouncilStructRefsAreStrings(t *testing.T) {
c := types.Council{StandIDRef: "stand-abc", GuildIDRef: "guild-def"}
if c.StandIDRef != "stand-abc" {
t.Errorf("StandIDRef = %q", c.StandIDRef)
}
if c.GuildIDRef != "guild-def" {
t.Errorf("GuildIDRef = %q", c.GuildIDRef)
}
}
// TestCouncilMemberStructFields asserts CouncilMember uses reach-id (NOT
// the banned financial holder term — lexicon-clean).
func TestCouncilMemberStructFields(t *testing.T) {
m := types.CouncilMember{ReachID: "reach:a", VoiceWeight: 7, JoinedAt: 200}
if m.ReachID != "reach:a" || m.VoiceWeight != 7 || m.JoinedAt != 200 {
t.Error("CouncilMember fields not set correctly")
}
}
// TestVoiceStructFields asserts Voice carries all required fields.
func TestVoiceStructFields(t *testing.T) {
v := types.Voice{
VoiceID: "v1",
CouncilID: "c1",
ProposerReach: "reach:prop",
SignalKind: types.SignalStash,
TargetRef: "proposal:p1",
Tally: types.TallyResult{Yes: 1, Total: 1, QuorumMet: true},
Timestamp: 999,
}
if v.VoiceID != "v1" || v.CouncilID != "c1" || v.ProposerReach != "reach:prop" ||
v.SignalKind != types.SignalStash || v.TargetRef != "proposal:p1" ||
v.Tally.Yes != 1 || v.Tally.Total != 1 || v.Tally.QuorumMet != true || v.Timestamp != 999 {
t.Error("Voice fields not set correctly")
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Councils and Voices.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Councils == nil || len(gs.Councils) != 0 {
t.Errorf("Default Councils should be non-nil empty slice; got len=%d nil=%v", len(gs.Councils), gs.Councils == nil)
}
if gs.Voices == nil || len(gs.Voices) != 0 {
t.Errorf("Default Voices should be non-nil empty slice; got len=%d nil=%v", len(gs.Voices), gs.Voices == nil)
}
}
// TestValidateGenesisRejectsDupCouncilIDs asserts A-212: duplicate
// council-ids are rejected.
func TestValidateGenesisRejectsDupCouncilIDs(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{
{CouncilID: "c1", Kind: types.CouncilMesh},
{CouncilID: "c1", Kind: types.CouncilGuild, GuildIDRef: "g1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate council-ids")
}
}
// TestValidateGenesisRejectsDupVoiceIDs asserts A-212: duplicate voice-ids
// are rejected.
func TestValidateGenesisRejectsDupVoiceIDs(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
Voices: []types.Voice{
{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalStash},
{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalVouch}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate voice-ids")
}
}
// TestValidateGenesisRejectsEmptyCouncilID asserts empty council-id is
// rejected.
func TestValidateGenesisRejectsEmptyCouncilID(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "", Kind: types.CouncilMesh}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty council-id")
}
}
// TestValidateGenesisRejectsEmptyVoiceID asserts empty voice-id is rejected.
func TestValidateGenesisRejectsEmptyVoiceID(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
Voices: []types.Voice{{VoiceID: "", CouncilID: "c1", SignalKind: types.SignalStash}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty voice-id")
}
}
// TestValidateGenesisRejectsUnknownCouncilKind asserts an unknown
// CouncilKind is rejected (data-engineer schema validation).
func TestValidateGenesisRejectsUnknownCouncilKind(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilKind("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown council kind")
}
}
// TestValidateGenesisRejectsUnknownSignalKind asserts an unknown SignalKind
// is rejected.
func TestValidateGenesisRejectsUnknownSignalKind(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
Voices: []types.Voice{{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalKind("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown signal-kind")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{
{CouncilID: "cm", Kind: types.CouncilMesh},
{CouncilID: "cg", Kind: types.CouncilGuild, GuildIDRef: "g1"},
{CouncilID: "cs", Kind: types.CouncilStand, StandIDRef: "s1"},
},
Voices: []types.Voice{
{VoiceID: "v1", CouncilID: "cm", SignalKind: types.SignalStash},
{VoiceID: "v2", CouncilID: "cs", SignalKind: types.SignalCapital},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestValidateGenesisRejectsStandCouncilWithoutStandIDRef asserts a Stand
// Council without stand-id-ref is rejected (by-ID-string ref to x/stand).
func TestValidateGenesisRejectsStandCouncilWithoutStandIDRef(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "cs", Kind: types.CouncilStand, StandIDRef: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject Stand Council without stand-id-ref")
}
}
// TestValidateGenesisRejectsGuildCouncilWithoutGuildIDRef asserts a Guild
// Council without guild-id-ref is rejected (by-ID-string ref to x/guild).
func TestValidateGenesisRejectsGuildCouncilWithoutGuildIDRef(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "cg", Kind: types.CouncilGuild, GuildIDRef: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject Guild Council without guild-id-ref")
}
}
// TestValidateGenesisRejectsVoiceWithUnknownCouncil asserts referential
// integrity: a Voice whose council-id does not reference an existing
// Council is rejected (P3-01-03 deliverable).
func TestValidateGenesisRejectsVoiceWithUnknownCouncil(t *testing.T) {
gs := types.GenesisState{
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
Voices: []types.Voice{{VoiceID: "v1", CouncilID: "no-such-council", SignalKind: types.SignalStash}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject Voice with unknown council-id (referential integrity)")
}
}
// TestMissionLockCheckIsNoOp asserts the genesis-side MissionLockCheck helper
// is a no-op (the const is the true firewall). It must return nil for any
// slice of Councils.
func TestMissionLockCheckIsNoOp(t *testing.T) {
councils := []types.Council{
{CouncilID: "c1", Kind: types.CouncilMesh},
{CouncilID: "c2", Kind: types.CouncilGuild, GuildIDRef: "g1"},
{CouncilID: "c3", Kind: types.CouncilStand, StandIDRef: "s1"},
}
if err := types.MissionLockCheck(councils); err != nil {
t.Errorf("MissionLockCheck should be a no-op (const is the firewall), got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "council" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "council" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "council" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "council" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInCouncilPackage scans every non-test .go file in
// the council/types package directory for the 9 banned terms
// (case-insensitive). Production files only — the test file references
// banned terms via the lexicon package helpers (standard lexicon-test
// bootstrapping pattern; no banned literals are inlined in this test file).
func TestLexiconNoBannedTermsInCouncilPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/council/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in council/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInCouncilTestFile asserts this test file itself
// does not contain any banned term as a literal (the firewall scans test
// files too; the lexicon helpers must be used rather than inlining banned
// terms). This is the self-bootstrapping check.
func TestLexiconNoBannedTermsInCouncilTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("council test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/council/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+71
View File
@@ -0,0 +1,71 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the
// forex module (G-008 split). ValidateGenesis in types.go composes these
// helpers; the security-engineer's test assertions live in types_test.go.
//
// The Forex genesis schema has two top-level sets: Pairs (the tradable
// ForexPairs) and Providers (the oracle-provider registry). The
// invariants enforced at genesis load are (1) pair-id uniqueness and
// (2) provider-id uniqueness (A-212 upgrade from v0.1's no-op). The
// lexicon firewall is the highest-severity constraint for this module
// (RESEARCH §1.10): the data-engineer's schema uses "base-asset"/"quote-
// asset" field names (A-208 "Bread/Asset" labels) and never the banned
// financial terms for tradable units.
// ValidatePairs asserts pair-ids are present and unique, and that the
// base-asset / quote-asset labels are non-empty (the lexicon-clean "Bread/
// Asset" labels per A-208 — the schema trusts the labels are lexicon-clean
// because the production code never inlines a banned term; the project-wide
// meta-test in lexicon_meta_test.go is the durable firewall). This is the
// P3-02-03 data-engineer schema validator composed by ValidateGenesis.
func ValidatePairs(pairs []ForexPair) error {
seen := make(map[string]bool, len(pairs))
for i, p := range pairs {
if p.PairID == "" {
return fmt.Errorf("forex pair [%d]: empty pair-id", i)
}
if seen[p.PairID] {
return fmt.Errorf("forex: duplicate pair-id %q", p.PairID)
}
seen[p.PairID] = true
if p.BaseAsset == "" {
return fmt.Errorf("forex pair %q: empty base-asset", p.PairID)
}
if p.QuoteAsset == "" {
return fmt.Errorf("forex pair %q: empty quote-asset", p.PairID)
}
}
return nil
}
// ValidateProviders asserts provider-ids are present and unique, and that
// each provider's kind is a known OracleKind.
func ValidateProviders(providers []OracleProvider) error {
seen := make(map[string]bool, len(providers))
for i, p := range providers {
if p.ProviderID == "" {
return fmt.Errorf("forex provider [%d]: empty provider-id", i)
}
if seen[p.ProviderID] {
return fmt.Errorf("forex: duplicate provider-id %q", p.ProviderID)
}
seen[p.ProviderID] = true
if !knownOracleKind(p.Kind) {
return fmt.Errorf("forex provider %q: unknown oracle kind %q", p.ProviderID, p.Kind)
}
}
return nil
}
// knownOracleKind reports whether k is one of the four OracleKind values.
func knownOracleKind(k OracleKind) bool {
for _, kk := range AllOracleKinds() {
if k == kk {
return true
}
}
return false
}
+155
View File
@@ -0,0 +1,155 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "forex"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// SpreadCapBps is the LOCKED spread cap for Forex rates (vision §18
// risk #18, A-214). The exact value is deferred to a v0.3 decision; the
// skeleton sets a documented placeholder of 0 (≥0 invariant). The test
// asserts SpreadCapBps >= 0. A v0.3+ governance decision may set a
// positive cap; the placeholder is the locked skeleton value.
SpreadCapBps = 0
// OracleKindCount is the locked count of OracleKind enum values
// (vision §13 / Forex v1). A regression firewall: adding/removing/
// renaming an Oracle kind breaks this const's test.
OracleKindCount = 4
// ErrOracleNotIntegrated is the sentinel error returned by the stub
// keeper GetRate when no live oracle is wired (skeleton — Phase 3
// wires Piers as the oracle consumer). The sentinel is the "not-
// integrated" marker the spec mandates.
ErrOracleNotIntegrated = "forex oracle not integrated (Phase 3 wires Piers)"
)
// ForexPair is a tradable pair in the Forex Engine v1 (vision §13, Forex v1).
// base-asset / quote-asset use "Bread/Asset" style labels (A-208) — NOT the
// banned financial terms for tradable units (which are lexicon-hostile per
// RESEARCH §1.10). "Forex" itself is allowed (vision §13 names it). The
// pair is a (base, quote) tuple of asset labels plus a decimals precision.
// The labels are opaque strings (e.g. "Bread"/"Asset") so downstream modules
// reference pairs by ID without importing banned terms.
type ForexPair struct {
PairID string `json:"pair_id" yaml:"pair_id"`
BaseAsset string `json:"base_asset" yaml:"base_asset"`
QuoteAsset string `json:"quote_asset" yaml:"quote_asset"`
Decimals uint32 `json:"decimals" yaml:"decimals"`
}
// RateOracle is the Go interface a Forex rate oracle must satisfy (Forex v1).
// GetRate returns the current rate for a pair-id (as a fixed-point uint64),
// the timestamp of the rate (block/unix time), and an error if the oracle
// is unavailable or the pair-id is unknown. The interface has no impl in
// v0.2 (skeleton — Phase 3 wires Piers as the oracle consumer per the
// soft-ordering note in PLANS.md cross-phase map).
type RateOracle interface {
GetRate(pairID string) (rate uint64, timestamp int64, err error)
}
// OracleKind enumerates the supported oracle providers (Forex v1).
// Chainlink (aggregated off-chain reports), Pyth (low-latency pull-based),
// UMA (optimistic oracle with dispute window), Internal (a protocol-internal
// rate source — e.g. a DEX TWAP). The skeleton defines the enum only; no
// live integration.
type OracleKind string
const (
OracleChainlink OracleKind = "Chainlink"
OraclePyth OracleKind = "Pyth"
OracleUMA OracleKind = "UMA"
OracleInternal OracleKind = "Internal"
)
// AllOracleKinds returns all four OracleKind values in Forex v1 order.
// Locked-const test asserts exactly 4 entries with these names.
func AllOracleKinds() []OracleKind {
return []OracleKind{
OracleChainlink,
OraclePyth,
OracleUMA,
OracleInternal,
}
}
// OracleProvider is a registered oracle provider in the Forex Engine
// (Forex v1). id is the provider's unique identifier; name is a human-
// readable label; kind picks the OracleKind (Chainlink/Pyth/UMA/Internal).
type OracleProvider struct {
ProviderID string `json:"provider_id" yaml:"provider_id"`
Name string `json:"name" yaml:"name"`
Kind OracleKind `json:"kind" yaml:"kind"`
}
// SpotRate is a single spot-rate observation for a ForexPair (Forex v1).
// pair-id references the ForexPair by ID string (G-003); rate is the fixed-
// point uint64 rate; timestamp is the observation time; provider-id
// references the OracleProvider by ID string (G-003).
type SpotRate struct {
PairID string `json:"pair_id" yaml:"pair_id"`
Rate uint64 `json:"rate" yaml:"rate"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
ProviderID string `json:"provider_id" yaml:"provider_id"`
}
// StubOracle is the stub keeper for the Forex Engine (Forex v1). GetRate
// returns the sentinel ErrOracleNotIntegrated for any pair-id (the skeleton
// is not wired to a live oracle — Phase 3 wires Piers). The stub satisfies
// the RateOracle interface so the interface compiles and a stub impl is
// callable from tests.
type StubOracle struct{}
// GetRate returns the sentinel "not-integrated" rate for any pair-id.
// The skeleton never returns a live rate; Phase 3 wires the real keeper.
func (StubOracle) GetRate(pairID string) (uint64, int64, error) {
_ = pairID
return 0, 0, fmt.Errorf("%s", ErrOracleNotIntegrated)
}
// Params for the forex module (skeleton — no tunables in v0.2; SpreadCapBps
// is the locked const, not a tunable param).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the forex module genesis state (Forex v1).
// Pairs is the top-level set of ForexPairs; Providers is the oracle-provider
// registry. ValidateGenesis enforces pair-id uniqueness and provider-id
// uniqueness. The data-engineer's genesis.go holds the schema helpers (G-008).
type GenesisState struct {
Pairs []ForexPair `json:"pairs" yaml:"pairs"`
Providers []OracleProvider `json:"providers" yaml:"providers"`
Params Params `json:"params" yaml:"params"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Pairs: []ForexPair{},
Providers: []OracleProvider{},
Params: DefaultParams(),
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate pair-ids and duplicate provider-ids. Delegates
// to the data-engineer's genesis.go helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("forex: invalid genesis: %w", err)
}
if err := ValidatePairs(gs.Pairs); err != nil {
return fmt.Errorf("forex: %w", err)
}
if err := ValidateProviders(gs.Providers); err != nil {
return fmt.Errorf("forex: %w", err)
}
return nil
}
+421
View File
@@ -0,0 +1,421 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/forex/types"
)
// TestOracleKindCountLockedConst asserts OracleKindCount is exactly 4 and
// AllOracleKinds() returns exactly 4 (Forex v1). A regression firewall:
// adding/removing/renaming an Oracle kind breaks this test.
func TestOracleKindCountLockedConst(t *testing.T) {
if types.OracleKindCount != 4 {
t.Errorf("OracleKindCount = %d, expected 4 (Forex v1 LOCKED)", types.OracleKindCount)
}
all := types.AllOracleKinds()
if len(all) != 4 {
t.Errorf("AllOracleKinds() len = %d, expected 4", len(all))
}
}
// TestAllOracleKindsNames asserts the 4 oracle-kind names in order with no
// extras, no dups, no renames.
func TestAllOracleKindsNames(t *testing.T) {
want := []string{"Chainlink", "Pyth", "UMA", "Internal"}
all := types.AllOracleKinds()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, k := range all {
if string(k) != want[i] {
t.Errorf("AllOracleKinds()[%d] = %q, want %q", i, k, want[i])
}
if seen[string(k)] {
t.Errorf("duplicate OracleKind %q", k)
}
seen[string(k)] = true
}
}
// TestOracleKindValues asserts each named const matches its AllOracleKinds
// entry.
func TestOracleKindValues(t *testing.T) {
if types.OracleChainlink != "Chainlink" {
t.Errorf("OracleChainlink = %q", types.OracleChainlink)
}
if types.OraclePyth != "Pyth" {
t.Errorf("OraclePyth = %q", types.OraclePyth)
}
if types.OracleUMA != "UMA" {
t.Errorf("OracleUMA = %q", types.OracleUMA)
}
if types.OracleInternal != "Internal" {
t.Errorf("OracleInternal = %q", types.OracleInternal)
}
}
// TestSpreadCapBpsNonNegative asserts SpreadCapBps >= 0 (A-214: the exact
// value is deferred to v0.3; the skeleton uses a documented placeholder of
// 0; the test asserts the invariant is non-negative).
func TestSpreadCapBpsNonNegative(t *testing.T) {
if types.SpreadCapBps < 0 {
t.Errorf("SpreadCapBps = %d, expected >= 0 (A-214)", types.SpreadCapBps)
}
// The skeleton placeholder is exactly 0 (documented TBD per A-214).
if types.SpreadCapBps != 0 {
t.Logf("SpreadCapBps = %d (skeleton placeholder is 0; v0.3 may set a positive cap)", types.SpreadCapBps)
}
}
// TestForexPairStructFields asserts ForexPair uses base-asset / quote-asset
// field names (A-208 "Bread/Asset" labels) — NOT the banned financial terms
// for tradable units (lexicon-hostile per RESEARCH §1.10). The test asserts
// the field names via JSON tags and constructs a sample pair with lexicon-
// clean labels.
func TestForexPairStructFields(t *testing.T) {
p := types.ForexPair{
PairID: "pair-1",
BaseAsset: "Bread",
QuoteAsset: "Asset",
Decimals: 8,
}
if p.PairID != "pair-1" || p.BaseAsset != "Bread" || p.QuoteAsset != "Asset" || p.Decimals != 8 {
t.Error("ForexPair fields not set correctly")
}
// Assert the JSON tags are "base_asset"/"quote_asset" (NOT the banned
// tradable-unit terms). This is the lexicon shape invariant.
bz, err := json.Marshal(p)
if err != nil {
t.Fatalf("marshal: %v", err)
}
js := string(bz)
if !strings.Contains(js, `"base_asset"`) {
t.Error("ForexPair JSON missing base_asset tag (A-208)")
}
if !strings.Contains(js, `"quote_asset"`) {
t.Error("ForexPair JSON missing quote_asset tag (A-208)")
}
}
// TestForexPairLabelsLexiconClean asserts the sample pair labels ("Bread"/
// "Asset") are lexicon-clean — the highest-severity check for the forex
// module (RESEARCH §1.10). The test scans the literal labels used in this
// test file AND the production types.go for any banned term.
func TestForexPairLabelsLexiconClean(t *testing.T) {
// Sample labels per A-208.
labels := []string{"Bread", "Asset", "base_asset", "quote_asset", "BaseAsset", "QuoteAsset"}
for _, l := range labels {
if found, ok := lexicon.FindBannedTerm(l); ok {
t.Errorf("label %q contains banned term %q (A-208 lexicon-clean labels)", l, found)
}
}
}
// TestRateOracleInterfaceCompiles asserts the RateOracle interface signature
// compiles and a stub impl satisfies it. This is the interface-shape
// regression firewall: GetRate(pairID) (rate uint64, timestamp int64, err error).
func TestRateOracleInterfaceCompiles(t *testing.T) {
var oracle types.RateOracle = types.StubOracle{}
if oracle == nil {
t.Fatal("StubOracle should be non-nil")
}
// The interface method must be callable.
_, _, err := oracle.GetRate("pair-1")
if err == nil {
t.Error("StubOracle.GetRate should return the not-integrated sentinel error")
}
}
// TestStubOracleGetRateSentinel asserts the stub keeper GetRate returns the
// sentinel "not-integrated" error for any pair-id (Forex v1 stub; Phase 3
// wires Piers as the oracle consumer).
func TestStubOracleGetRateSentinel(t *testing.T) {
stub := types.StubOracle{}
rate, ts, err := stub.GetRate("any-pair-id")
if err == nil {
t.Fatal("StubOracle.GetRate should error (not integrated)")
}
if !strings.Contains(err.Error(), "not integrated") {
t.Errorf("StubOracle.GetRate error = %q, want sentinel containing 'not integrated'", err.Error())
}
if rate != 0 {
t.Errorf("StubOracle.GetRate rate = %d, expected 0 (sentinel)", rate)
}
if ts != 0 {
t.Errorf("StubOracle.GetRate timestamp = %d, expected 0 (sentinel)", ts)
}
}
// TestStubOracleSatisfiesInterface asserts StubOracle satisfies the
// RateOracle interface at compile time (var _ types.RateOracle = StubOracle{}
// would be a compile error if the interface drifted).
func TestStubOracleSatisfiesInterface(t *testing.T) {
var _ types.RateOracle = types.StubOracle{}
}
// TestOracleProviderStructFields asserts OracleProvider carries id, name,
// kind.
func TestOracleProviderStructFields(t *testing.T) {
p := types.OracleProvider{
ProviderID: "op-1",
Name: "Chainlink FX",
Kind: types.OracleChainlink,
}
if p.ProviderID != "op-1" || p.Name != "Chainlink FX" || p.Kind != types.OracleChainlink {
t.Error("OracleProvider fields not set correctly")
}
}
// TestSpotRateStructFields asserts SpotRate carries pair-id, rate, timestamp,
// provider-id (by-ID-string ref per G-003).
func TestSpotRateStructFields(t *testing.T) {
sr := types.SpotRate{
PairID: "pair-1",
Rate: 100000000,
Timestamp: 1700000000,
ProviderID: "op-1",
}
if sr.PairID != "pair-1" || sr.Rate != 100000000 || sr.Timestamp != 1700000000 || sr.ProviderID != "op-1" {
t.Error("SpotRate fields not set correctly")
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Pairs and Providers.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Pairs == nil || len(gs.Pairs) != 0 {
t.Errorf("Default Pairs should be non-nil empty slice; got len=%d nil=%v", len(gs.Pairs), gs.Pairs == nil)
}
if gs.Providers == nil || len(gs.Providers) != 0 {
t.Errorf("Default Providers should be non-nil empty slice; got len=%d nil=%v", len(gs.Providers), gs.Providers == nil)
}
}
// TestValidateGenesisRejectsDupPairIDs asserts A-212: duplicate pair-ids
// are rejected.
func TestValidateGenesisRejectsDupPairIDs(t *testing.T) {
gs := types.GenesisState{
Pairs: []types.ForexPair{
{PairID: "p1", BaseAsset: "Bread", QuoteAsset: "Asset"},
{PairID: "p1", BaseAsset: "Bread", QuoteAsset: "Asset"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate pair-ids")
}
}
// TestValidateGenesisRejectsDupProviderIDs asserts A-212: duplicate
// provider-ids are rejected.
func TestValidateGenesisRejectsDupProviderIDs(t *testing.T) {
gs := types.GenesisState{
Providers: []types.OracleProvider{
{ProviderID: "op1", Name: "A", Kind: types.OracleChainlink},
{ProviderID: "op1", Name: "B", Kind: types.OraclePyth}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate provider-ids")
}
}
// TestValidateGenesisRejectsEmptyPairID asserts empty pair-id is rejected.
func TestValidateGenesisRejectsEmptyPairID(t *testing.T) {
gs := types.GenesisState{
Pairs: []types.ForexPair{{PairID: "", BaseAsset: "Bread", QuoteAsset: "Asset"}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty pair-id")
}
}
// TestValidateGenesisRejectsEmptyProviderID asserts empty provider-id is
// rejected.
func TestValidateGenesisRejectsEmptyProviderID(t *testing.T) {
gs := types.GenesisState{
Providers: []types.OracleProvider{{ProviderID: "", Name: "A", Kind: types.OracleChainlink}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty provider-id")
}
}
// TestValidateGenesisRejectsEmptyBaseAsset asserts empty base-asset is
// rejected (the lexicon-clean label must be present).
func TestValidateGenesisRejectsEmptyBaseAsset(t *testing.T) {
gs := types.GenesisState{
Pairs: []types.ForexPair{{PairID: "p1", BaseAsset: "", QuoteAsset: "Asset"}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty base-asset")
}
}
// TestValidateGenesisRejectsEmptyQuoteAsset asserts empty quote-asset is
// rejected.
func TestValidateGenesisRejectsEmptyQuoteAsset(t *testing.T) {
gs := types.GenesisState{
Pairs: []types.ForexPair{{PairID: "p1", BaseAsset: "Bread", QuoteAsset: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty quote-asset")
}
}
// TestValidateGenesisRejectsUnknownOracleKind asserts an unknown OracleKind
// is rejected.
func TestValidateGenesisRejectsUnknownOracleKind(t *testing.T) {
gs := types.GenesisState{
Providers: []types.OracleProvider{{ProviderID: "op1", Name: "A", Kind: types.OracleKind("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown oracle kind")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Pairs: []types.ForexPair{
{PairID: "p1", BaseAsset: "Bread", QuoteAsset: "Asset", Decimals: 8},
{PairID: "p2", BaseAsset: "Bread", QuoteAsset: "Other", Decimals: 6},
},
Providers: []types.OracleProvider{
{ProviderID: "op1", Name: "Chainlink FX", Kind: types.OracleChainlink},
{ProviderID: "op2", Name: "Pyth FX", Kind: types.OraclePyth},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "forex" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "forex" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "forex" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "forex" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// TestErrOracleNotIntegratedSentinel asserts the sentinel error string is
// non-empty and mentions "not integrated".
func TestErrOracleNotIntegratedSentinel(t *testing.T) {
if types.ErrOracleNotIntegrated == "" {
t.Error("ErrOracleNotIntegrated sentinel is empty")
}
if !strings.Contains(types.ErrOracleNotIntegrated, "not integrated") {
t.Errorf("ErrOracleNotIntegrated = %q, want substring 'not integrated'", types.ErrOracleNotIntegrated)
}
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
//
// The forex module is the HIGHEST lexicon-risk module per RESEARCH §1.10
// (the banned financial terms for tradable units are "natural" fit-words
// for Forex). The lexicon assertion scans production files AND the test
// file itself; sample pair-label data ("Bread"/"Asset") is asserted clean.
// TestLexiconNoBannedTermsInForexPackage scans every non-test .go file in
// the forex/types package directory for the 9 banned terms
// (case-insensitive). Production files only — the test file references
// banned terms via the lexicon package helpers (standard lexicon-test
// bootstrapping pattern; no banned literals are inlined in this test file).
func TestLexiconNoBannedTermsInForexPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/forex/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in forex/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — forex is highest risk)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInForexTestFile asserts this test file itself does
// not contain any banned term as a literal (the firewall scans test files
// too; the lexicon helpers must be used rather than inlining banned terms).
// This is the self-bootstrapping check.
func TestLexiconNoBannedTermsInForexTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("forex test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/forex/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+117
View File
@@ -0,0 +1,117 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "guild"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// HandPassFeeBps is the LOCKED protocol fee for a Hand-Pass: 0 bps (REQ-017).
// A Guild Hand-Pass is always free at the protocol layer. This is a covenant,
// not a tunable parameter — cross-referenced to feecovenant.WaiverHandPassGuild
// (v0.1 already encodes HandPassGuild as a 0-fee waiver reason). v0.2's Guild
// module references that waiver, doesn't redefine the fee.
HandPassFeeBps = 0
)
// Guild is a task-oriented collective (vision §16, REQ-017). A Guild may
// optionally affiliate with a Stand (stand-affiliation-id references x/stand
// by ID string — G-003 by-ID-string invariant). founder-reach references
// x/identity Reach by string.
type Guild struct {
GuildID string `json:"guild_id" yaml:"guild_id"`
Name string `json:"name" yaml:"name"`
FounderReach string `json:"founder_reach" yaml:"founder_reach"`
CreatedAt int64 `json:"created_at" yaml:"created_at"`
StandAffiliationID string `json:"stand_affiliation_id,omitempty" yaml:"stand_affiliation_id,omitempty"`
}
// HandPass is a free (0% protocol fee) Pass-Act issued by a Guild (REQ-017).
// FeeGrain is always 0 (HandPassFeeBps == 0 is the locked const covenant).
// issuer-reach / recipient-reach reference x/identity Reach by string (G-003).
type HandPass struct {
PassID string `json:"pass_id" yaml:"pass_id"`
GuildID string `json:"guild_id" yaml:"guild_id"`
IssuerReach string `json:"issuer_reach" yaml:"issuer_reach"`
RecipientReach string `json:"recipient_reach" yaml:"recipient_reach"`
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
FeeGrain int64 `json:"fee_grain" yaml:"fee_grain"` // always 0 (HandPassFeeBps == 0)
}
// IssueHandPass is a stub for issuing a Hand-Pass (REQ-017). The skeleton
// constructs a HandPass with FeeGrain = 0 (the locked covenant). Issuer
// type-level checks (issuer must be a guild member) are NOT enforced in
// the skeleton — flagged for v0.3 keeper logic.
func IssueHandPass(passID, guildID, issuerReach, recipientReach string, amountGrain int64, timestamp int64) HandPass {
return HandPass{
PassID: passID,
GuildID: guildID,
IssuerReach: issuerReach,
RecipientReach: recipientReach,
AmountGrain: amountGrain,
Timestamp: timestamp,
FeeGrain: 0, // HandPassFeeBps == 0 (locked covenant)
}
}
// Params for the guild module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the guild module genesis state (REQ-017).
// Guilds + HandPasses are the two top-level sets; ValidateGenesis enforces
// guild-id uniqueness and pass-id uniqueness.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Guilds []Guild `json:"guilds" yaml:"guilds"`
HandPasses []HandPass `json:"hand_passes" yaml:"hand_passes"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Guilds: []Guild{},
HandPasses: []HandPass{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate guild-ids and duplicate pass-ids. Also enforces
// the 0-fee covenant on genesis HandPasses (FeeGrain must be 0).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("guild: invalid genesis: %w", err)
}
seenGuild := make(map[string]bool, len(gs.Guilds))
for _, g := range gs.Guilds {
if g.GuildID == "" {
return fmt.Errorf("guild: empty guild-id")
}
if seenGuild[g.GuildID] {
return fmt.Errorf("guild: duplicate guild-id %q", g.GuildID)
}
seenGuild[g.GuildID] = true
}
seenPass := make(map[string]bool, len(gs.HandPasses))
for _, p := range gs.HandPasses {
if p.PassID == "" {
return fmt.Errorf("guild: empty pass-id")
}
if seenPass[p.PassID] {
return fmt.Errorf("guild: duplicate pass-id %q", p.PassID)
}
seenPass[p.PassID] = true
if p.FeeGrain != 0 {
return fmt.Errorf("guild: HandPass %q has non-zero FeeGrain (HandPassFeeBps == 0 covenant)", p.PassID)
}
}
return nil
}
+264
View File
@@ -0,0 +1,264 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/guild/types"
)
// TestHandPassFeeBpsLockedConst asserts the LOCKED 0-fee covenant (REQ-017).
// A Guild Hand-Pass is always free at the protocol layer. This is a
// regression firewall: changing HandPassFeeBps breaks this test.
func TestHandPassFeeBpsLockedConst(t *testing.T) {
if types.HandPassFeeBps != 0 {
t.Errorf("HandPassFeeBps = %d, expected 0 (REQ-017 LOCKED 0pct covenant)", types.HandPassFeeBps)
}
}
// TestIssueHandPassFeeAlwaysZero asserts IssueHandPass constructs a HandPass
// with FeeGrain = 0 (the locked covenant), regardless of the amount.
func TestIssueHandPassFeeAlwaysZero(t *testing.T) {
hp := types.IssueHandPass("p1", "g1", "reach:issuer", "reach:recipient", 10000, 1234)
if hp.FeeGrain != 0 {
t.Errorf("IssueHandPass FeeGrain = %d, expected 0 (HandPassFeeBps == 0)", hp.FeeGrain)
}
// Even a large amount has zero fee (0% covenant).
hp2 := types.IssueHandPass("p2", "g1", "reach:i", "reach:r", 1_000_000_000, 1234)
if hp2.FeeGrain != 0 {
t.Errorf("IssueHandPass FeeGrain (large amount) = %d, expected 0", hp2.FeeGrain)
}
}
// TestIssueHandPassFields asserts IssueHandPass populates all fields.
func TestIssueHandPassFields(t *testing.T) {
hp := types.IssueHandPass("p1", "g1", "reach:issuer", "reach:recipient", 5000, 1234)
if hp.PassID != "p1" || hp.GuildID != "g1" || hp.IssuerReach != "reach:issuer" ||
hp.RecipientReach != "reach:recipient" || hp.AmountGrain != 5000 ||
hp.Timestamp != 1234 || hp.FeeGrain != 0 {
t.Error("IssueHandPass fields not set correctly")
}
}
// TestHandPassStructFields asserts HandPass carries all required fields.
func TestHandPassStructFields(t *testing.T) {
hp := types.HandPass{
PassID: "p1",
GuildID: "g1",
IssuerReach: "reach:i",
RecipientReach: "reach:r",
AmountGrain: 100,
Timestamp: 200,
FeeGrain: 0,
}
if hp.PassID != "p1" || hp.GuildID != "g1" || hp.AmountGrain != 100 ||
hp.FeeGrain != 0 {
t.Error("HandPass fields not set correctly")
}
}
// TestGuildWithStandAffiliation asserts a Guild can affiliate with a Stand
// (stand-affiliation-id set).
func TestGuildWithStandAffiliation(t *testing.T) {
g := types.Guild{
GuildID: "g1",
Name: "Task Guild",
FounderReach: "reach:founder",
CreatedAt: 100,
StandAffiliationID: "s1",
}
if g.StandAffiliationID != "s1" {
t.Errorf("StandAffiliationID = %q, want %q", g.StandAffiliationID, "s1")
}
}
// TestGuildStandalone asserts a Guild can be standalone (no Stand affiliation).
func TestGuildStandalone(t *testing.T) {
g := types.Guild{
GuildID: "g2",
Name: "Loose Collective",
FounderReach: "reach:founder",
CreatedAt: 100,
}
if g.StandAffiliationID != "" {
t.Errorf("Standalone Guild StandAffiliationID = %q, want empty", g.StandAffiliationID)
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Guilds and HandPasses.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Guilds == nil || len(gs.Guilds) != 0 {
t.Errorf("Default Guilds should be non-nil empty slice")
}
if gs.HandPasses == nil || len(gs.HandPasses) != 0 {
t.Errorf("Default HandPasses should be non-nil empty slice")
}
}
// TestValidateGenesisRejectsDupGuildIDs asserts A-212: duplicate guild-ids
// are rejected.
func TestValidateGenesisRejectsDupGuildIDs(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{
{GuildID: "g1"},
{GuildID: "g1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate guild-ids")
}
}
// TestValidateGenesisRejectsDupPassIDs asserts A-212: duplicate pass-ids
// are rejected.
func TestValidateGenesisRejectsDupPassIDs(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{
{PassID: "p1"},
{PassID: "p1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate pass-ids")
}
}
// TestValidateGenesisRejectsNonZeroFeeGrain asserts the 0-fee covenant is
// enforced at genesis: any HandPass with non-zero FeeGrain is rejected.
func TestValidateGenesisRejectsNonZeroFeeGrain(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{
{PassID: "p1", FeeGrain: 1}, // violates 0-fee covenant
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject non-zero FeeGrain (0pct covenant)")
}
}
// TestValidateGenesisRejectsEmptyGuildID asserts empty guild-id is rejected.
func TestValidateGenesisRejectsEmptyGuildID(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{{GuildID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty guild-id")
}
}
// TestValidateGenesisRejectsEmptyPassID asserts empty pass-id is rejected.
func TestValidateGenesisRejectsEmptyPassID(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{{PassID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty pass-id")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{bad`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates,
// including a Guild with Stand affiliation and a standalone Guild.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{
{GuildID: "g1", StandAffiliationID: "s1"},
{GuildID: "g2"}, // standalone
},
HandPasses: []types.HandPass{
{PassID: "p1", GuildID: "g1", FeeGrain: 0},
{PassID: "p2", GuildID: "g2", FeeGrain: 0},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "guild" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "guild" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "guild" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "guild" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInGuildPackage scans every non-test .go file in
// the guild/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file contains the banned terms as the list
// of things to forbid (standard lexicon-test bootstrapping pattern).
func TestLexiconNoBannedTermsInGuildPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/guild/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in guild/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// packageDir resolves a Go import path to its filesystem directory.
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+79
View File
@@ -0,0 +1,79 @@
package types
import "encoding/json"
const (
ModuleName = "mesh"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
)
// Four Faces of the mesh experience (§8)
// Reach (who you are), Standing (who you can trust),
// Maps (where you are), Pay (how value flows)
type MeshFace string
const (
FaceReach MeshFace = "Reach"
FaceStanding MeshFace = "Standing"
FaceMaps MeshFace = "Maps"
FacePay MeshFace = "Pay"
)
// AllFaces returns the four mesh faces (§8)
func AllFaces() []MeshFace {
return []MeshFace{FaceReach, FaceStanding, FaceMaps, FacePay}
}
// MapEntry is a discovery entry (§8: Maps — where you are, what's around)
type MapEntry struct {
EntryID string `json:"entry_id" yaml:"entry_id"`
Name string `json:"name" yaml:"name"`
Latitude float64 `json:"latitude" yaml:"latitude"`
Longitude float64 `json:"longitude" yaml:"longitude"`
Category string `json:"category" yaml:"category"` // Op, Pier, service
HolderID string `json:"holder_id" yaml:"holder_id"`
IsOp bool `json:"is_op" yaml:"is_op"`
IsPier bool `json:"is_pier" yaml:"is_pier"`
}
// PayAct represents a Pass-Act (value flow, §8: Pay)
type PayAct struct {
PassID string `json:"pass_id" yaml:"pass_id"`
FromStashID string `json:"from_stash_id" yaml:"from_stash_id"`
ToStashID string `json:"to_stash_id" yaml:"to_stash_id"`
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
FeeGrain int64 `json:"fee_grain" yaml:"fee_grain"`
IsLongPass bool `json:"is_long_pass" yaml:"is_long_pass"` // remittance
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
}
// ExitLayerType defines Layer 3 exit methods (§7)
type ExitLayerType string
const (
ExitDEX ExitLayerType = "DEX"
ExitBridge ExitLayerType = "Bridge"
ExitOffMesh ExitLayerType = "OffMesh"
)
// MayasDayPlaceholder — full narrative deferred to component doc (§8, Q1)
// The mesh is one environment with four invisible primitives
// activating simultaneously. A Holder doesn't open apps — they live in the mesh.
const MayasDayPlaceholder = "Maya's Day narrative deferred to Mesh Experience component doc (Architecture Q1)"
type Params struct{}
func DefaultParams() Params { return Params{} }
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
MapEntries []MapEntry `json:"map_entries" yaml:"map_entries"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{Params: DefaultParams(), MapEntries: []MapEntry{}}
}
func ValidateGenesis(bz json.RawMessage) error { return nil }
+39
View File
@@ -0,0 +1,39 @@
package types_test
import (
"testing"
"github.com/oy/openyield/x/mesh/types"
)
func TestFourFaces(t *testing.T) {
faces := types.AllFaces()
if len(faces) != 4 {
t.Errorf("Expected 4 mesh faces (§8), got %d", len(faces))
}
}
func TestFaceNames(t *testing.T) {
expected := map[types.MeshFace]bool{
types.FaceReach: true, types.FaceStanding: true,
types.FaceMaps: true, types.FacePay: true,
}
for _, f := range types.AllFaces() {
if !expected[f] {
t.Errorf("Unexpected face: %s", f)
}
}
}
func TestExitLayerTypes(t *testing.T) {
exits := []types.ExitLayerType{types.ExitDEX, types.ExitBridge, types.ExitOffMesh}
if len(exits) != 3 {
t.Errorf("Expected 3 exit layer types (§7), got %d", len(exits))
}
}
func TestMayasDayPlaceholder(t *testing.T) {
if types.MayasDayPlaceholder == "" {
t.Error("Maya's Day placeholder should note deferral (§8, Q1)")
}
}
+80
View File
@@ -0,0 +1,80 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the pact
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
// the security-engineer's test assertions live in types_test.go.
//
// The Pact genesis schema is a single top-level set: Pacts. The two
// invariants enforced at genesis load are (1) pact-id uniqueness (A-212) and
// (2) the Mission-Lock check (the per-type AmendableCoreTerms flags for
// Pause/Ground/Stance must be false — the global MissionLockAmendable const
// bool is the firewall). The Mission-Lock is enforced by compile-time consts;
// the genesis-side MissionLockCheck is the data-engineer's hook that asserts
// the const firewall is intact whenever genesis Pacts are loaded (so a
// future change to the consts would surface here too).
// ValidatePacts asserts pact-ids are present and unique, and that each
// Pact's type is a known PactType. It also runs the Mission-Lock check
// (MissionLockCheck) so the genesis load path enforces both invariants.
// ValidatePacts is the data-engineer's schema validator, composed by
// ValidateGenesis in types.go.
func ValidatePacts(pacts []Pact) error {
seen := make(map[string]bool, len(pacts))
for i, p := range pacts {
if p.PactID == "" {
return fmt.Errorf("pact [%d]: empty pact-id", i)
}
if seen[p.PactID] {
return fmt.Errorf("pact: duplicate pact-id %q", p.PactID)
}
seen[p.PactID] = true
if !knownPactType(p.Type) {
return fmt.Errorf("pact %q: unknown pact type %q", p.PactID, p.Type)
}
}
if err := MissionLockCheck(pacts); err != nil {
return err
}
return nil
}
// knownPactType reports whether t is one of the six vision §16 PactType values.
func knownPactType(t PactType) bool {
for _, kt := range AllPactTypes() {
if t == kt {
return true
}
}
return false
}
// MissionLockCheck asserts the Mission-Lock invariant on a slice of Pacts:
// every Pause/Ground/Stance Pact must have its AmendableCoreTerms flag false.
// Because the flags are compile-time consts (AmendableCoreTermsPause/Ground/
// Stance == false) and the global MissionLockAmendable const is false, this
// check always passes — it exists as the data-engineer's genesis-side
// assertion that the Mission-Lock firewall is intact. If the consts ever
// changed to true, this check would still pass (the consts are the firewall,
// not runtime data); the test in types_test.go is the true regression guard.
// The helper is the genesis hook for v0.3 keeper logic to extend with live
// per-pact Mission-Lock enforcement.
func MissionLockCheck(pacts []Pact) error {
// The global MissionLockAmendable const is the firewall: if it were ever
// flipped to true (which the test suite rejects), the genesis load would
// surface it here. The per-pact loop echoes the invariant for each
// Mission-Locked Pact type so a future per-pact check has a hook point.
if MissionLockAmendable {
return fmt.Errorf("pact: Mission Lock amendable (MissionLockAmendable == true) — firewall breach")
}
for _, p := range pacts {
if !MissionLockAmendableCoreTerms(p.Type) {
// Non-amendable core terms: the const flags already guarantee this;
// the genesis check is the echo. No per-pact runtime data to verify
// in the skeleton — the const is the source of truth.
continue
}
}
return nil
}
+235
View File
@@ -0,0 +1,235 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "pact"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// PactTypeCount is the locked count of PactType enum values (vision §16).
// A regression firewall: adding/removing/renaming a Pact type breaks this
// const's test (REQ-020, A-207: ONE module with enum, not six micro-modules).
PactTypeCount = 6
// MissionLockAmendable is the Mission-Lock invariant: the core terms of
// Pause/Ground/Stance Pacts are non-amendable (vision §19). This is a
// locked const bool: it can NEVER be set true. The regression test asserts
// it is false and that the per-type AmendableCoreTerms flags for
// Pause/Ground/Stance are all false.
MissionLockAmendable = false
)
// PactType enumerates the six commitment types (vision §16, REQ-020).
// A-207: all six live in ONE x/pact module with a PactType enum + per-type
// execute-entry stubs (NOT six micro-modules).
type PactType string
const (
PactPause PactType = "Pause" // circuit-breaker commitment (wraps x/still)
PactGround PactType = "Ground" // earth-anchored collateral lock commitment
PactStance PactType = "Stance" // public-position / attestation commitment
PactCover PactType = "Cover" // insurance-like commitment (Cover Pool)
PactStandRegistry PactType = "StandRegistry" // registers a Stand into the canonical registry
PactHubAPI PactType = "HubAPI" // B2B backbone commitment
)
// AllPactTypes returns all six PactType values in vision §16 order.
// Locked-const test asserts exactly 6 entries with these names (REQ-020).
func AllPactTypes() []PactType {
return []PactType{
PactPause,
PactGround,
PactStance,
PactCover,
PactStandRegistry,
PactHubAPI,
}
}
// PactStatus enumerates the lifecycle states of a Pact (REQ-020).
type PactStatus string
const (
StatusProposed PactStatus = "Proposed" // pact created, not yet active
StatusActive PactStatus = "Active" // pact is live and binding
StatusFulfilled PactStatus = "Fulfilled" // pact completed successfully
StatusVoided PactStatus = "Voided" // pact voided (cancelled / breached)
)
// PactStatusCount is the locked count of PactStatus enum values.
const PactStatusCount = 4
// Pact is a commitment of one of six types (vision §16, REQ-020). Each Pact
// has a type, parties (Reach IDs by-ID-string per G-003), opaque terms-bytes,
// a status, and per-type execute-message ref. window-id-ref references
// x/window by ID string (G-003 by-ID-string invariant; P1-01-01 convention).
// stand-id-ref references x/stand by ID string (P1-02-01 convention); only
// StandRegistry Pacts populate it for non-empty, others leave it "".
type Pact struct {
PactID string `json:"pact_id" yaml:"pact_id"`
Type PactType `json:"type" yaml:"type"`
Parties []string `json:"parties" yaml:"parties"`
Terms []byte `json:"terms" yaml:"terms"`
Status PactStatus `json:"status" yaml:"status"`
ExecuteMsgRef string `json:"execute_msg_ref" yaml:"execute_msg_ref"`
WindowIDRef string `json:"window_id_ref" yaml:"window_id_ref"`
StandIDRef string `json:"stand_id_ref" yaml:"stand_id_ref"`
}
// MissionLockCoreTerms flags which Pact types have non-amendable core terms
// under the Mission Lock (vision §19). Pause/Ground/Stance core terms are
// non-amendable; the const flags below are the per-type invariant. The
// module-level MissionLockAmendable const bool is the global firewall.
const (
// AmendableCoreTermsPause is false: Pause Pact core terms are
// non-amendable under the Mission Lock.
AmendableCoreTermsPause = false
// AmendableCoreTermsGround is false: Ground Pact core terms are
// non-amendable under the Mission Lock.
AmendableCoreTermsGround = false
// AmendableCoreTermsStance is false: Stance Pact core terms are
// non-amendable under the Mission Lock.
AmendableCoreTermsStance = false
)
// MissionLockAmendableCoreTerms returns the per-type AmendableCoreTerms flag
// for a PactType. Pause/Ground/Stance return false (non-amendable); Cover,
// StandRegistry, HubAPI return true (amendable per the skeleton — these are
// not Mission-Locked). The Mission-Lock invariant test asserts the three
// core types return false.
func MissionLockAmendableCoreTerms(t PactType) bool {
switch t {
case PactPause:
return AmendableCoreTermsPause
case PactGround:
return AmendableCoreTermsGround
case PactStance:
return AmendableCoreTermsStance
default:
return true
}
}
// ExecutePause is the execute-entry stub for a Pause Pact (circuit-breaker).
// The skeleton returns the proposed status transition; v0.3 wires the live
// keeper that wraps x/still.
func (p *Pact) ExecutePause() error {
if p.Type != PactPause {
return fmt.Errorf("ExecutePause: pact %q is type %q, not Pause", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecutePause: pact %q status %q, not Proposed", p.PactID, p.Status)
}
p.Status = StatusActive
return nil
}
// ExecuteGround is the execute-entry stub for a Ground Pact
// (earth-anchored collateral lock).
func (p *Pact) ExecuteGround() error {
if p.Type != PactGround {
return fmt.Errorf("ExecuteGround: pact %q is type %q, not Ground", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecuteGround: pact %q status %q, not Proposed", p.PactID, p.Status)
}
p.Status = StatusActive
return nil
}
// ExecuteStance is the execute-entry stub for a Stance Pact
// (public-position / attestation).
func (p *Pact) ExecuteStance() error {
if p.Type != PactStance {
return fmt.Errorf("ExecuteStance: pact %q is type %q, not Stance", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecuteStance: pact %q status %q, not Proposed", p.PactID, p.Status)
}
p.Status = StatusActive
return nil
}
// ExecuteCover is the execute-entry stub for a Cover Pact (insurance-like).
// Cover Pool seniority is deferred per Q7 — the skeleton is a flat
// commitment type with no seniority fields.
func (p *Pact) ExecuteCover() error {
if p.Type != PactCover {
return fmt.Errorf("ExecuteCover: pact %q is type %q, not Cover", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecuteCover: pact %q status %q, not Proposed", p.PactID, p.Status)
}
p.Status = StatusActive
return nil
}
// ExecuteStandRegistry is the execute-entry stub for a StandRegistry Pact.
// stand-id-ref references x/stand by ID string (G-003); the skeleton activates
// the pact without a live keeper call.
func (p *Pact) ExecuteStandRegistry() error {
if p.Type != PactStandRegistry {
return fmt.Errorf("ExecuteStandRegistry: pact %q is type %q, not StandRegistry", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecuteStandRegistry: pact %q status %q, not Proposed", p.PactID, p.Status)
}
if p.StandIDRef == "" {
return fmt.Errorf("ExecuteStandRegistry: pact %q missing stand-id-ref", p.PactID)
}
p.Status = StatusActive
return nil
}
// ExecuteHubAPI is the execute-entry stub for a HubAPI Pact (B2B backbone).
// The full Hub API suite is deferred to Phase 3; v0.2 = stub type only.
func (p *Pact) ExecuteHubAPI() error {
if p.Type != PactHubAPI {
return fmt.Errorf("ExecuteHubAPI: pact %q is type %q, not HubAPI", p.PactID, p.Type)
}
if p.Status != StatusProposed {
return fmt.Errorf("ExecuteHubAPI: pact %q status %q, not Proposed", p.PactID, p.Status)
}
p.Status = StatusActive
return nil
}
// Params for the pact module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the pact module genesis state (REQ-020).
// Pacts is the top-level set; ValidateGenesis enforces pact-id uniqueness and
// the Mission-Lock check (Mission-Locked types' AmendableCoreTerms flags must
// be false). The data-engineer's genesis.go holds the schema helpers (G-008).
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Pacts []Pact `json:"pacts" yaml:"pacts"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Pacts: []Pact{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate pact-ids, and runs the Mission-Lock check on
// genesis Pacts. Delegates to the data-engineer's genesis.go helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("pact: invalid genesis: %w", err)
}
if err := ValidatePacts(gs.Pacts); err != nil {
return fmt.Errorf("pact: %w", err)
}
return nil
}
+449
View File
@@ -0,0 +1,449 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/pact/types"
)
// TestPactTypeCountLockedConst asserts PactTypeCount is exactly 6 and
// AllPactTypes() returns exactly 6 (vision §16, REQ-020, A-207). A regression
// firewall: adding/removing/renaming a Pact type breaks this test.
func TestPactTypeCountLockedConst(t *testing.T) {
if types.PactTypeCount != 6 {
t.Errorf("PactTypeCount = %d, expected 6 (vision §16 LOCKED)", types.PactTypeCount)
}
all := types.AllPactTypes()
if len(all) != 6 {
t.Errorf("AllPactTypes() len = %d, expected 6", len(all))
}
}
// TestAllPactTypesNames asserts the 6 vision §16 names in order with no
// extras, no dups, no renames.
func TestAllPactTypesNames(t *testing.T) {
want := []string{
"Pause", "Ground", "Stance", "Cover", "StandRegistry", "HubAPI",
}
all := types.AllPactTypes()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllPactTypes()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate PactType %q", s)
}
seen[string(s)] = true
}
}
// TestPactTypeValues asserts each named const matches its AllPactTypes entry.
func TestPactTypeValues(t *testing.T) {
if types.PactPause != "Pause" {
t.Errorf("PactPause = %q", types.PactPause)
}
if types.PactGround != "Ground" {
t.Errorf("PactGround = %q", types.PactGround)
}
if types.PactStance != "Stance" {
t.Errorf("PactStance = %q", types.PactStance)
}
if types.PactCover != "Cover" {
t.Errorf("PactCover = %q", types.PactCover)
}
if types.PactStandRegistry != "StandRegistry" {
t.Errorf("PactStandRegistry = %q", types.PactStandRegistry)
}
if types.PactHubAPI != "HubAPI" {
t.Errorf("PactHubAPI = %q", types.PactHubAPI)
}
}
// TestPactStatusCountLockedConst asserts PactStatusCount is exactly 4.
func TestPactStatusCountLockedConst(t *testing.T) {
if types.PactStatusCount != 4 {
t.Errorf("PactStatusCount = %d, expected 4", types.PactStatusCount)
}
}
// TestPactStatusEnumCoverage asserts all four PactStatus values are distinct
// and non-empty (REQ-020 lifecycle: Proposed, Active, Fulfilled, Voided).
func TestPactStatusEnumCoverage(t *testing.T) {
statuses := []types.PactStatus{
types.StatusProposed, types.StatusActive,
types.StatusFulfilled, types.StatusVoided,
}
if len(statuses) != 4 {
t.Errorf("expected 4 PactStatus consts, got %d", len(statuses))
}
seen := map[types.PactStatus]bool{}
for _, s := range statuses {
if s == "" {
t.Error("empty PactStatus")
}
if seen[s] {
t.Errorf("duplicate PactStatus %q", s)
}
seen[s] = true
}
}
// TestMissionLockAmendableConstFalse asserts the global Mission-Lock const
// is false (vision §19): the Mission Lock can NEVER be amended. This is the
// highest-severity regression firewall for the pact module.
func TestMissionLockAmendableConstFalse(t *testing.T) {
if types.MissionLockAmendable != false {
t.Fatalf("MissionLockAmendable = %v, expected false (Mission Lock non-amendable)", types.MissionLockAmendable)
}
}
// TestMissionLockCoreTermsNonAmendable asserts the per-type AmendableCoreTerms
// const flags for Pause/Ground/Stance are all false (Mission-Lock invariant).
// Cover/StandRegistry/HubAPI return true (amendable — not Mission-Locked).
func TestMissionLockCoreTermsNonAmendable(t *testing.T) {
// Pause/Ground/Stance core terms MUST be non-amendable.
if types.AmendableCoreTermsPause != false {
t.Error("AmendableCoreTermsPause must be false (Mission Lock)")
}
if types.AmendableCoreTermsGround != false {
t.Error("AmendableCoreTermsGround must be false (Mission Lock)")
}
if types.AmendableCoreTermsStance != false {
t.Error("AmendableCoreTermsStance must be false (Mission Lock)")
}
// The MissionLockAmendableCoreTerms helper echoes the const flags.
locked := []types.PactType{types.PactPause, types.PactGround, types.PactStance}
for _, pt := range locked {
if types.MissionLockAmendableCoreTerms(pt) != false {
t.Errorf("MissionLockAmendableCoreTerms(%q) = true, want false (Mission Lock)", pt)
}
}
// Cover/StandRegistry/HubAPI are amendable (not Mission-Locked).
amendable := []types.PactType{types.PactCover, types.PactStandRegistry, types.PactHubAPI}
for _, pt := range amendable {
if types.MissionLockAmendableCoreTerms(pt) != true {
t.Errorf("MissionLockAmendableCoreTerms(%q) = false, want true (amendable)", pt)
}
}
}
// TestPactStructFields asserts Pact carries all required fields including
// the by-ID-string refs (window-id-ref, stand-id-ref per G-003).
func TestPactStructFields(t *testing.T) {
p := types.Pact{
PactID: "p1",
Type: types.PactPause,
Parties: []string{"reach:a", "reach:b"},
Terms: []byte("terms-bytes"),
Status: types.StatusProposed,
ExecuteMsgRef: "msg:pause:1",
WindowIDRef: "w1",
StandIDRef: "s1",
}
if p.PactID != "p1" || p.Type != types.PactPause || len(p.Parties) != 2 ||
string(p.Terms) != "terms-bytes" || p.Status != types.StatusProposed ||
p.ExecuteMsgRef != "msg:pause:1" || p.WindowIDRef != "w1" || p.StandIDRef != "s1" {
t.Error("Pact fields not set correctly")
}
}
// TestPactStructRefsAreStrings asserts window-id-ref and stand-id-ref are
// string-typed (G-003 by-ID-string invariant; the G-003 import invariant is
// enforced project-wide by P1-01-02's go/parser scan, so this test only
// asserts the field types at the struct level, not cross-module imports).
func TestPactStructRefsAreStrings(t *testing.T) {
// Construct a Pact and confirm the ref fields hold plain strings —
// no struct imports of x/window or x/stand are needed.
p := types.Pact{WindowIDRef: "window-abc", StandIDRef: "stand-xyz"}
if p.WindowIDRef != "window-abc" {
t.Errorf("WindowIDRef = %q", p.WindowIDRef)
}
if p.StandIDRef != "stand-xyz" {
t.Errorf("StandIDRef = %q", p.StandIDRef)
}
}
// TestExecuteStubsCallable asserts each per-type Execute* stub is callable
// and transitions a Proposed Pact to Active (REQ-020).
func TestExecuteStubsCallable(t *testing.T) {
tt := []struct {
name string
pact types.Pact
execFn func(*types.Pact) error
}{
{"Pause", types.Pact{PactID: "p1", Type: types.PactPause, Status: types.StatusProposed}, (*types.Pact).ExecutePause},
{"Ground", types.Pact{PactID: "p2", Type: types.PactGround, Status: types.StatusProposed}, (*types.Pact).ExecuteGround},
{"Stance", types.Pact{PactID: "p3", Type: types.PactStance, Status: types.StatusProposed}, (*types.Pact).ExecuteStance},
{"Cover", types.Pact{PactID: "p4", Type: types.PactCover, Status: types.StatusProposed}, (*types.Pact).ExecuteCover},
{"HubAPI", types.Pact{PactID: "p6", Type: types.PactHubAPI, Status: types.StatusProposed}, (*types.Pact).ExecuteHubAPI},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
p := tc.pact
if err := tc.execFn(&p); err != nil {
t.Fatalf("Execute%s: %v", tc.name, err)
}
if p.Status != types.StatusActive {
t.Errorf("after Execute%s, status = %q, want Active", tc.name, p.Status)
}
})
}
}
// TestExecuteStandRegistryRequiresStandIDRef asserts ExecuteStandRegistry
// requires a non-empty stand-id-ref (the by-ID-string ref to x/stand).
func TestExecuteStandRegistryRequiresStandIDRef(t *testing.T) {
p := types.Pact{PactID: "p5", Type: types.PactStandRegistry, Status: types.StatusProposed, StandIDRef: ""}
if err := p.ExecuteStandRegistry(); err == nil {
t.Error("ExecuteStandRegistry should error on empty stand-id-ref")
}
p.StandIDRef = "s1"
if err := p.ExecuteStandRegistry(); err != nil {
t.Errorf("ExecuteStandRegistry with stand-id-ref: %v", err)
}
if p.Status != types.StatusActive {
t.Errorf("status = %q, want Active", p.Status)
}
}
// TestExecuteStubsRejectWrongType asserts each Execute* stub rejects a Pact
// of the wrong type (type guard).
func TestExecuteStubsRejectWrongType(t *testing.T) {
p := types.Pact{PactID: "p", Type: types.PactCover, Status: types.StatusProposed}
if err := p.ExecutePause(); err == nil {
t.Error("ExecutePause on a Cover pact should error")
}
if err := p.ExecuteGround(); err == nil {
t.Error("ExecuteGround on a Cover pact should error")
}
if err := p.ExecuteStance(); err == nil {
t.Error("ExecuteStance on a Cover pact should error")
}
if err := p.ExecuteStandRegistry(); err == nil {
t.Error("ExecuteStandRegistry on a Cover pact should error")
}
if err := p.ExecuteHubAPI(); err == nil {
t.Error("ExecuteHubAPI on a Cover pact should error")
}
// ExecuteCover should succeed (matches type).
if err := p.ExecuteCover(); err != nil {
t.Errorf("ExecuteCover on a Cover pact: %v", err)
}
}
// TestExecuteStubsRejectNonProposed asserts each Execute* stub rejects a
// Pact not in the Proposed status.
func TestExecuteStubsRejectNonProposed(t *testing.T) {
tt := []struct {
name string
pact types.Pact
execFn func(*types.Pact) error
}{
{"Pause-active", types.Pact{PactID: "p", Type: types.PactPause, Status: types.StatusActive}, (*types.Pact).ExecutePause},
{"Ground-fulfilled", types.Pact{PactID: "p", Type: types.PactGround, Status: types.StatusFulfilled}, (*types.Pact).ExecuteGround},
{"Stance-voided", types.Pact{PactID: "p", Type: types.PactStance, Status: types.StatusVoided}, (*types.Pact).ExecuteStance},
{"Cover-active", types.Pact{PactID: "p", Type: types.PactCover, Status: types.StatusActive}, (*types.Pact).ExecuteCover},
{"HubAPI-voided", types.Pact{PactID: "p", Type: types.PactHubAPI, Status: types.StatusVoided}, (*types.Pact).ExecuteHubAPI},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
p := tc.pact
if err := tc.execFn(&p); err == nil {
t.Errorf("Execute%s on %q-status pact should error", tc.name, p.Status)
}
})
}
}
// TestExecuteStandRegistryNonProposed asserts ExecuteStandRegistry rejects
// a non-Proposed StandRegistry pact even when stand-id-ref is set.
func TestExecuteStandRegistryNonProposed(t *testing.T) {
p := types.Pact{PactID: "p", Type: types.PactStandRegistry, Status: types.StatusActive, StandIDRef: "s1"}
if err := p.ExecuteStandRegistry(); err == nil {
t.Error("ExecuteStandRegistry on Active pact should error")
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a
// non-nil empty slice for Pacts.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Pacts == nil || len(gs.Pacts) != 0 {
t.Errorf("Default Pacts should be non-nil empty slice; got len=%d nil=%v", len(gs.Pacts), gs.Pacts == nil)
}
}
// TestValidateGenesisRejectsDupPactIDs asserts A-212: duplicate pact-ids
// are rejected (upgrade from v0.1's no-op ValidateGenesis).
func TestValidateGenesisRejectsDupPactIDs(t *testing.T) {
gs := types.GenesisState{
Pacts: []types.Pact{
{PactID: "p1", Type: types.PactPause},
{PactID: "p1", Type: types.PactGround}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate pact-ids")
}
}
// TestValidateGenesisRejectsEmptyPactID asserts empty pact-id is rejected.
func TestValidateGenesisRejectsEmptyPactID(t *testing.T) {
gs := types.GenesisState{
Pacts: []types.Pact{{PactID: "", Type: types.PactPause}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty pact-id")
}
}
// TestValidateGenesisRejectsUnknownType asserts an unknown PactType is
// rejected (data-engineer schema validation).
func TestValidateGenesisRejectsUnknownType(t *testing.T) {
gs := types.GenesisState{
Pacts: []types.Pact{{PactID: "p1", Type: types.PactType("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown pact type")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Pacts: []types.Pact{
{PactID: "p1", Type: types.PactPause, Status: types.StatusProposed},
{PactID: "p2", Type: types.PactCover, Status: types.StatusActive},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestMissionLockCheckIsNoOp asserts the genesis-side MissionLockCheck helper
// is a no-op (the const flags are the true firewall). It must return nil for
// any slice of Pacts — the Mission Lock is enforced at compile time by the
// const bools, not at genesis load.
func TestMissionLockCheckIsNoOp(t *testing.T) {
pacts := []types.Pact{
{PactID: "p1", Type: types.PactPause},
{PactID: "p2", Type: types.PactGround},
{PactID: "p3", Type: types.PactStance},
{PactID: "p4", Type: types.PactCover},
}
if err := types.MissionLockCheck(pacts); err != nil {
t.Errorf("MissionLockCheck should be a no-op (const flags are the firewall), got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "pact" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "pact" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "pact" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "pact" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInPactPackage scans every non-test .go file in
// the pact/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file references banned terms via the
// lexicon package helpers (standard lexicon-test bootstrapping pattern;
// no banned literals are inlined in this test file).
func TestLexiconNoBannedTermsInPactPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/pact/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in pact/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInPactTestFile asserts this test file itself does
// not contain any banned term as a literal (the firewall scans test files
// too; the lexicon helpers must be used rather than inlining banned terms).
// This is the self-bootstrapping check.
func TestLexiconNoBannedTermsInPactTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("pact test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/pact/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+195
View File
@@ -0,0 +1,195 @@
package types
import (
"encoding/json"
"fmt"
"sync"
)
const (
ModuleName = "partner"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// PartnerTierCount is the locked count of PartnerTier enum values
// (vision §13, D-026). A regression firewall: adding/removing/renaming a
// tier breaks this const's test (REQ-018).
PartnerTierCount = 4
)
// PartnerTier enumerates the four partner tiers (vision §13, REQ-018, D-026).
// Op processes Pass-Acts; MasterOp is a senior Op; Pier is a credential /
// identity provider (e-Residency/biometrics); Anchor is institutional.
// "Op" is used (not "operator" — the latter implies a banned financial term
// per RESEARCH §1.6; "Op" is vision-§13 lexicon-clean).
type PartnerTier string
const (
TierOp PartnerTier = "Op" // processes Pass-Acts
TierMasterOp PartnerTier = "MasterOp" // senior Op
TierPier PartnerTier = "Pier" // credential / identity provider
TierAnchor PartnerTier = "Anchor" // institutional
)
// AllPartnerTiers returns all four PartnerTier values in vision §13 order.
// Locked-const test asserts exactly 4 entries with these names (REQ-018).
func AllPartnerTiers() []PartnerTier {
return []PartnerTier{
TierOp,
TierMasterOp,
TierPier,
TierAnchor,
}
}
// PartnerStatus enumerates the lifecycle states of a Partner (REQ-018).
type PartnerStatus string
const (
StatusPending PartnerStatus = "Pending" // partner registered, not yet active
StatusActive PartnerStatus = "Active" // partner is live
StatusSuspended PartnerStatus = "Suspended" // partner temporarily halted
StatusRevoked PartnerStatus = "Revoked" // partner permanently revoked
)
// PartnerStatusCount is the locked count of PartnerStatus enum values.
const PartnerStatusCount = 4
// CredentialType enumerates the kinds of credentials a Pier can reference
// (REQ-018). The ref-uri is opaque; Pier credential routing is deferred per
// Q5 (v0.3 will wire the live routing). The skeleton defines the type enum
// so genesis / registry entries carry a typed credential kind.
type CredentialType string
const (
CredentialEresidency CredentialType = "Eresidency" // e-Residency-style identity
CredentialBiometric CredentialType = "Biometric" // biometric identity
CredentialVouch CredentialType = "Vouch" // vouch-based attestation
CredentialCustom CredentialType = "Custom" // opaque custom credential
)
// CredentialRef references an external credential provider (REQ-018).
// provider-id references a Partner (typically a Pier) by ID string
// (G-003 by-ID-string invariant). ref-uri is an opaque URI; Pier credential
// routing is deferred per Q5, so the skeleton keeps the ref opaque.
type CredentialRef struct {
ProviderID string `json:"provider_id" yaml:"provider_id"`
CredentialType CredentialType `json:"credential_type" yaml:"credential_type"`
RefURI string `json:"ref_uri" yaml:"ref_uri"`
}
// Partner is a registered actor on the Partner Spectrum (vision §13, REQ-018).
// reach-id references x/identity Reach by string (G-003 by-ID-string
// invariant). credential-ref references a credential provider (typically a
// Pier) by ID string. region is a free-form locale tag.
type Partner struct {
PartnerID string `json:"partner_id" yaml:"partner_id"`
Tier PartnerTier `json:"tier" yaml:"tier"`
Name string `json:"name" yaml:"name"`
ReachID string `json:"reach_id" yaml:"reach_id"`
Region string `json:"region" yaml:"region"`
CredentialRef CredentialRef `json:"credential_ref" yaml:"credential_ref"`
Status PartnerStatus `json:"status" yaml:"status"`
}
// Keeper is a registry keeper stub for Partners (REQ-018). The skeleton
// provides in-memory add/get/list/by-tier operations; v0.3 wires the live
// keeper backed by the store. It is safe for concurrent use (the live keeper
// will use the SDK store, which is single-threaded per-block; the stub uses
// a mutex so the skeleton's tests can exercise concurrent paths).
type Keeper struct {
mu sync.Mutex
partners map[string]Partner
}
// NewKeeper returns an empty registry keeper stub.
func NewKeeper() *Keeper {
return &Keeper{partners: make(map[string]Partner)}
}
// AddPartner registers a Partner by ID. Returns an error if the ID is empty
// or already registered.
func (k *Keeper) AddPartner(p Partner) error {
if p.PartnerID == "" {
return fmt.Errorf("partner: empty partner-id")
}
k.mu.Lock()
defer k.mu.Unlock()
if _, exists := k.partners[p.PartnerID]; exists {
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
}
k.partners[p.PartnerID] = p
return nil
}
// GetPartner returns a Partner by ID and true, or zero-value and false.
func (k *Keeper) GetPartner(id string) (Partner, bool) {
k.mu.Lock()
defer k.mu.Unlock()
p, ok := k.partners[id]
return p, ok
}
// ListPartners returns all registered Partners (unordered).
func (k *Keeper) ListPartners() []Partner {
k.mu.Lock()
defer k.mu.Unlock()
out := make([]Partner, 0, len(k.partners))
for _, p := range k.partners {
out = append(out, p)
}
return out
}
// ListByTier returns all registered Partners matching the given tier.
func (k *Keeper) ListByTier(tier PartnerTier) []Partner {
k.mu.Lock()
defer k.mu.Unlock()
out := []Partner{}
for _, p := range k.partners {
if p.Tier == tier {
out = append(out, p)
}
}
return out
}
// Params for the partner module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the partner module genesis state (REQ-018).
// Partners is the top-level set; ValidateGenesis enforces partner-id uniqueness.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Partners []Partner `json:"partners" yaml:"partners"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Partners: []Partner{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate partner-ids.
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("partner: invalid genesis: %w", err)
}
seen := make(map[string]bool, len(gs.Partners))
for _, p := range gs.Partners {
if p.PartnerID == "" {
return fmt.Errorf("partner: empty partner-id")
}
if seen[p.PartnerID] {
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
}
seen[p.PartnerID] = true
}
return nil
}
+426
View File
@@ -0,0 +1,426 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/partner/types"
)
// TestPartnerTierCountLockedConst asserts PartnerTierCount is exactly 4
// and AllPartnerTiers() returns exactly 4 (vision §13, REQ-018, D-026). A
// regression firewall: adding/removing/renaming a tier breaks this test.
func TestPartnerTierCountLockedConst(t *testing.T) {
if types.PartnerTierCount != 4 {
t.Errorf("PartnerTierCount = %d, expected 4 (vision §13 LOCKED)", types.PartnerTierCount)
}
all := types.AllPartnerTiers()
if len(all) != 4 {
t.Errorf("AllPartnerTiers() len = %d, expected 4", len(all))
}
}
// TestAllPartnerTiersNames asserts the 4 vision §13 names in order with no
// extras, no dups, no renames. "Op" (not "operator") per vision §13 — the
// latter implies a banned financial term per RESEARCH §1.6; "Op" is
// lexicon-clean.
func TestAllPartnerTiersNames(t *testing.T) {
want := []string{"Op", "MasterOp", "Pier", "Anchor"}
all := types.AllPartnerTiers()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllPartnerTiers()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate PartnerTier %q", s)
}
seen[string(s)] = true
}
}
// TestPartnerTierValues asserts each named const matches its AllPartnerTiers
// entry.
func TestPartnerTierValues(t *testing.T) {
if types.TierOp != "Op" {
t.Errorf("TierOp = %q", types.TierOp)
}
if types.TierMasterOp != "MasterOp" {
t.Errorf("TierMasterOp = %q", types.TierMasterOp)
}
if types.TierPier != "Pier" {
t.Errorf("TierPier = %q", types.TierPier)
}
if types.TierAnchor != "Anchor" {
t.Errorf("TierAnchor = %q", types.TierAnchor)
}
}
// TestPartnerStatusCountLockedConst asserts PartnerStatusCount is exactly 4.
func TestPartnerStatusCountLockedConst(t *testing.T) {
if types.PartnerStatusCount != 4 {
t.Errorf("PartnerStatusCount = %d, expected 4", types.PartnerStatusCount)
}
}
// TestPartnerStatusEnumCoverage asserts all four PartnerStatus values are
// distinct and non-empty (REQ-018 lifecycle: Pending, Active, Suspended, Revoked).
func TestPartnerStatusEnumCoverage(t *testing.T) {
statuses := []types.PartnerStatus{
types.StatusPending, types.StatusActive,
types.StatusSuspended, types.StatusRevoked,
}
if len(statuses) != 4 {
t.Errorf("expected 4 PartnerStatus consts, got %d", len(statuses))
}
seen := map[types.PartnerStatus]bool{}
for _, s := range statuses {
if s == "" {
t.Error("empty PartnerStatus")
}
if seen[s] {
t.Errorf("duplicate PartnerStatus %q", s)
}
seen[s] = true
}
}
// TestCredentialTypeEnumCoverage asserts the CredentialType values are
// distinct and non-empty (Pier credential routing deferred per Q5; the
// enum is the typed kind for genesis / registry entries).
func TestCredentialTypeEnumCoverage(t *testing.T) {
cts := []types.CredentialType{
types.CredentialEresidency, types.CredentialBiometric,
types.CredentialVouch, types.CredentialCustom,
}
if len(cts) != 4 {
t.Errorf("expected 4 CredentialType consts, got %d", len(cts))
}
seen := map[types.CredentialType]bool{}
for _, c := range cts {
if c == "" {
t.Error("empty CredentialType")
}
if seen[c] {
t.Errorf("duplicate CredentialType %q", c)
}
seen[c] = true
}
}
// TestCredentialRefStruct asserts CredentialRef carries all required fields
// (provider-id, credential-type, ref-uri — opaque URI).
func TestCredentialRefStruct(t *testing.T) {
c := types.CredentialRef{
ProviderID: "pier-1",
CredentialType: types.CredentialEresidency,
RefURI: "oy:cred:pier-1/eresidency/abc123",
}
if c.ProviderID != "pier-1" || c.CredentialType != types.CredentialEresidency ||
c.RefURI != "oy:cred:pier-1/eresidency/abc123" {
t.Error("CredentialRef fields not set correctly")
}
}
// TestPartnerStructFields asserts Partner carries all required fields
// including the by-ID-string reach-id (G-003).
func TestPartnerStructFields(t *testing.T) {
p := types.Partner{
PartnerID: "pt1",
Tier: types.TierPier,
Name: "Pier One",
ReachID: "reach:pier-1",
Region: "EU",
CredentialRef: types.CredentialRef{
ProviderID: "pier-1",
CredentialType: types.CredentialBiometric,
RefURI: "oy:cred:bio/x",
},
Status: types.StatusActive,
}
if p.PartnerID != "pt1" || p.Tier != types.TierPier || p.Name != "Pier One" ||
p.ReachID != "reach:pier-1" || p.Region != "EU" ||
p.CredentialRef.ProviderID != "pier-1" || p.Status != types.StatusActive {
t.Error("Partner fields not set correctly")
}
}
// --- Registry keeper stub tests (REQ-018) ---------------------------------------
// TestKeeperAddGetRoundTrip asserts AddPartner + GetPartner round-trips a
// Partner by ID.
func TestKeeperAddGetRoundTrip(t *testing.T) {
k := types.NewKeeper()
p := types.Partner{
PartnerID: "pt1",
Tier: types.TierOp,
Name: "Op One",
ReachID: "reach:op-1",
Status: types.StatusActive,
}
if err := k.AddPartner(p); err != nil {
t.Fatalf("AddPartner: %v", err)
}
got, ok := k.GetPartner("pt1")
if !ok {
t.Fatal("GetPartner: not found")
}
if got.PartnerID != "pt1" || got.Tier != types.TierOp {
t.Errorf("GetPartner returned wrong Partner: %+v", got)
}
}
// TestKeeperAddPartnerRejectsEmptyID asserts AddPartner rejects an empty id.
func TestKeeperAddPartnerRejectsEmptyID(t *testing.T) {
k := types.NewKeeper()
if err := k.AddPartner(types.Partner{PartnerID: ""}); err == nil {
t.Error("AddPartner should reject empty partner-id")
}
}
// TestKeeperAddPartnerRejectsDup asserts AddPartner rejects a duplicate id.
func TestKeeperAddPartnerRejectsDup(t *testing.T) {
k := types.NewKeeper()
p := types.Partner{PartnerID: "pt1", Tier: types.TierOp}
if err := k.AddPartner(p); err != nil {
t.Fatalf("first AddPartner: %v", err)
}
if err := k.AddPartner(p); err == nil {
t.Error("AddPartner should reject duplicate partner-id")
}
}
// TestKeeperGetPartnerMissing asserts GetPartner returns false for an
// unregistered id.
func TestKeeperGetPartnerMissing(t *testing.T) {
k := types.NewKeeper()
if _, ok := k.GetPartner("nope"); ok {
t.Error("GetPartner should return false for unregistered id")
}
}
// TestKeeperListPartners asserts ListPartners returns all registered Partners.
func TestKeeperListPartners(t *testing.T) {
k := types.NewKeeper()
_ = k.AddPartner(types.Partner{PartnerID: "a", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "b", Tier: types.TierAnchor})
list := k.ListPartners()
if len(list) != 2 {
t.Errorf("ListPartners len = %d, want 2", len(list))
}
}
// TestKeeperListPartnersEmpty asserts ListPartners on an empty keeper returns
// a non-nil empty slice (or a usable slice).
func TestKeeperListPartnersEmpty(t *testing.T) {
k := types.NewKeeper()
list := k.ListPartners()
if list == nil {
t.Fatal("ListPartners returned nil")
}
if len(list) != 0 {
t.Errorf("ListPartners len = %d, want 0", len(list))
}
}
// TestKeeperListByTier asserts ListByTier returns only Partners matching the
// given tier (REQ-018 round-trip).
func TestKeeperListByTier(t *testing.T) {
k := types.NewKeeper()
_ = k.AddPartner(types.Partner{PartnerID: "op1", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "op2", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "mop1", Tier: types.TierMasterOp})
_ = k.AddPartner(types.Partner{PartnerID: "pier1", Tier: types.TierPier})
_ = k.AddPartner(types.Partner{PartnerID: "anc1", Tier: types.TierAnchor})
tt := []struct {
tier types.PartnerTier
wantN int
}{
{types.TierOp, 2},
{types.TierMasterOp, 1},
{types.TierPier, 1},
{types.TierAnchor, 1},
}
for _, tc := range tt {
t.Run(string(tc.tier), func(t *testing.T) {
got := k.ListByTier(tc.tier)
if len(got) != tc.wantN {
t.Errorf("ListByTier(%q) len = %d, want %d", tc.tier, len(got), tc.wantN)
}
for _, p := range got {
if p.Tier != tc.tier {
t.Errorf("ListByTier(%q) returned Partner with tier %q", tc.tier, p.Tier)
}
}
})
}
}
// TestKeeperListByTierEmpty asserts ListByTier returns an empty (non-nil)
// slice when no Partners match.
func TestKeeperListByTierEmpty(t *testing.T) {
k := types.NewKeeper()
got := k.ListByTier(types.TierAnchor)
if got == nil {
t.Fatal("ListByTier returned nil")
}
if len(got) != 0 {
t.Errorf("ListByTier len = %d, want 0", len(got))
}
}
// --- Genesis tests (REQ-018, A-212) ----------------------------------------------
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a non-nil
// empty slice for Partners.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Partners == nil || len(gs.Partners) != 0 {
t.Errorf("Default Partners should be non-nil empty slice; got len=%d nil=%v", len(gs.Partners), gs.Partners == nil)
}
}
// TestValidateGenesisRejectsDupPartnerIDs asserts A-212: duplicate partner-ids
// are rejected.
func TestValidateGenesisRejectsDupPartnerIDs(t *testing.T) {
gs := types.GenesisState{
Partners: []types.Partner{
{PartnerID: "pt1", Tier: types.TierOp},
{PartnerID: "pt1", Tier: types.TierAnchor}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate partner-ids")
}
}
// TestValidateGenesisRejectsEmptyPartnerID asserts empty partner-id is rejected.
func TestValidateGenesisRejectsEmptyPartnerID(t *testing.T) {
gs := types.GenesisState{
Partners: []types.Partner{{PartnerID: "", Tier: types.TierOp}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty partner-id")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Partners: []types.Partner{
{PartnerID: "pt1", Tier: types.TierOp, Status: types.StatusActive},
{PartnerID: "pt2", Tier: types.TierPier, Status: types.StatusPending},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "partner" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "partner" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "partner" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "partner" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInPartnerPackage scans every non-test .go file in
// the partner/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file references banned terms via the
// lexicon package helpers (standard lexicon-test bootstrapping pattern; no
// banned literals are inlined in this test file).
func TestLexiconNoBannedTermsInPartnerPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/partner/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in partner/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInPartnerTestFile asserts this test file itself does
// not contain any banned term as a literal (the firewall scans test files
// too; the lexicon helpers must be used rather than inlining banned terms).
func TestLexiconNoBannedTermsInPartnerTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("partner test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/partner/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+59
View File
@@ -0,0 +1,59 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the
// satellite module (G-008 split). ValidateGenesis in types.go composes these
// helpers; the security-engineer's test assertions live in types_test.go.
//
// The Satellite genesis schema has two top-level sets: Channels (the IBC
// transfer channels between OY Chain and L2 satellites) and Denoms (the
// wrapped Bread denoms). The invariants enforced at genesis load are
// (1) channel-id uniqueness, (2) denom uniqueness, and (3) each channel's
// status is a known ChannelStatus.
// ValidateChannels asserts channel-ids are present and unique, and that
// each channel's status is a known ChannelStatus. ValidateChannels is the
// data-engineer's schema validator, composed by ValidateGenesis in types.go.
func ValidateChannels(channels []TransferChannel) error {
seen := make(map[string]bool, len(channels))
for i, c := range channels {
if c.ChannelID == "" {
return fmt.Errorf("channel [%d]: empty channel-id", i)
}
if seen[c.ChannelID] {
return fmt.Errorf("channel: duplicate channel-id %q", c.ChannelID)
}
seen[c.ChannelID] = true
if !knownChannelStatus(c.Status) {
return fmt.Errorf("channel %q: unknown channel status %q", c.ChannelID, c.Status)
}
}
return nil
}
// ValidateDenoms asserts denoms are present and unique. ValidateDenoms is
// the data-engineer's schema validator for the wrapped Bread denom set.
func ValidateDenoms(denoms []WrappedBreadDenom) error {
seen := make(map[string]bool, len(denoms))
for i, d := range denoms {
if d.Denom == "" {
return fmt.Errorf("denom [%d]: empty denom", i)
}
if seen[d.Denom] {
return fmt.Errorf("denom: duplicate denom %q", d.Denom)
}
seen[d.Denom] = true
}
return nil
}
// knownChannelStatus reports whether s is one of the four ChannelStatus values.
func knownChannelStatus(s ChannelStatus) bool {
for _, ss := range AllChannelStatuses() {
if s == ss {
return true
}
}
return false
}
+171
View File
@@ -0,0 +1,171 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "satellite"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// L2ChainCount is the locked count of L2Chain enum values (vision §10,
// REQ-009, D-021). Five L2 satellite chains: Polygon (the one active
// representative in v0.2) plus Base, Arbitrum, Optimism, Solana (four
// StatusPending enum placeholders). A regression firewall:
// adding/removing/renaming a chain breaks this const's test.
L2ChainCount = 5
// ChannelStatusCount is the locked count of ChannelStatus enum values
// (ICS-20 handshake): Init, TryOpen, Open, Closed. A regression firewall
// for the ICS-20 handshake shape (A-215).
ChannelStatusCount = 4
)
// L2Chain enumerates the L2 satellite chains (vision §10, REQ-009, D-021).
// Polygon is the one active representative in v0.2 (D-021 scopes v0.2 to ONE
// representative chain). Base, Arbitrum, Optimism, and Solana are
// StatusPending enum placeholders (the full 5-chain IBC rollout is Phase 3
// per D-021). Solana lacks native IBC (RESEARCH §1.1) and is stubbed as
// StatusPending — no Solana light-client logic in v0.2.
type L2Chain string
const (
ChainPolygon L2Chain = "Polygon" // active representative (D-021)
ChainBase L2Chain = "Base" // StatusPending placeholder
ChainArbitrum L2Chain = "Arbitrum" // StatusPending placeholder
ChainOptimism L2Chain = "Optimism" // StatusPending placeholder
ChainSolana L2Chain = "Solana" // StatusPending placeholder (no native IBC)
)
// ChainActivation is the activation state of an L2 chain (D-021): Active
// (Polygon in v0.2) or StatusPending (the four stubs).
type ChainActivation string
const (
ChainActive ChainActivation = "Active" // chain is live for IBC transfer
ChainStatusPending ChainActivation = "StatusPending" // chain is a placeholder (Phase 3 rollout)
)
// ChainInfo describes an L2 chain's properties (REQ-009, D-021).
type ChainInfo struct {
Chain L2Chain `json:"chain" yaml:"chain"`
Activation ChainActivation `json:"activation" yaml:"activation"`
}
// AllL2Chains returns all five L2Chain values (Polygon + 4 stubs) with their
// activation states (D-021). Locked-const test asserts exactly 5 entries.
// Polygon is the only ChainActive entry; the other four are StatusPending.
func AllL2Chains() []ChainInfo {
return []ChainInfo{
{ChainPolygon, ChainActive},
{ChainBase, ChainStatusPending},
{ChainArbitrum, ChainStatusPending},
{ChainOptimism, ChainStatusPending},
{ChainSolana, ChainStatusPending},
}
}
// ChannelStatus enumerates the ICS-20 channel handshake states (A-215):
// Init (channel initialized), TryOpen (counterparty trying to open), Open
// (channel established), Closed (channel closed). The four-state handshake
// mirrors ibc-go ICS-20 v1 channel state (stable, widely implemented).
type ChannelStatus string
const (
ChannelInit ChannelStatus = "Init" // channel initialized
ChannelTryOpen ChannelStatus = "TryOpen" // counterparty trying to open
ChannelOpen ChannelStatus = "Open" // channel established
ChannelClosed ChannelStatus = "Closed" // channel closed
)
// AllChannelStatuses returns all four ChannelStatus values in ICS-20
// handshake order. Locked-const test asserts exactly 4 entries.
func AllChannelStatuses() []ChannelStatus {
return []ChannelStatus{
ChannelInit,
ChannelTryOpen,
ChannelOpen,
ChannelClosed,
}
}
// TransferChannel is an IBC transfer channel between OY Chain (L1) and an L2
// satellite (REQ-009, A-215). port-id and channel-id are the ICS-20 port and
// channel identifiers (e.g. "transfer" / "channel-0"). counterparty is the
// counterparty port+channel on the L2. status is the handshake state.
type TransferChannel struct {
PortID string `json:"port_id" yaml:"port_id"`
ChannelID string `json:"channel_id" yaml:"channel_id"`
Counterparty string `json:"counterparty" yaml:"counterparty"`
Status ChannelStatus `json:"status" yaml:"status"`
}
// WrappedBreadDenom encodes an IBC-traced wrapped Bread denom (REQ-009,
// A-215). When Bread propagates from OY Chain (L1) to an L2 via IBC, the
// denom on the L2 is the original denom prefixed with the IBC trace path
// (e.g. "transfer/channel-0/bread"). denom is the full traced denom on the
// destination chain; trace-path is the IBC trace (the port/channel hops).
type WrappedBreadDenom struct {
Denom string `json:"denom" yaml:"denom"`
TracePath string `json:"trace_path" yaml:"trace_path"`
}
// Packet is the ICS-20 v1 packet shape stub (REQ-009, A-215). Pinned to the
// ICS-20 v1 channel packet shape (stable, widely implemented) to minimize
// churn if a different ibc-go version is chosen in Phase 3. Fields:
// sequence, source-port, source-channel, dest-port, dest-channel, data,
// timeout-height, timeout-timestamp. NO ibc-go import — zero external deps
// (A-201); the type is a self-contained Go struct.
type Packet struct {
Sequence uint64 `json:"sequence" yaml:"sequence"`
SourcePort string `json:"source_port" yaml:"source_port"`
SourceChannel string `json:"source_channel" yaml:"source_channel"`
DestPort string `json:"dest_port" yaml:"dest_port"`
DestChannel string `json:"dest_channel" yaml:"dest_channel"`
Data []byte `json:"data" yaml:"data"`
TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"`
TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"`
}
// Params for the satellite module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the satellite module genesis state (REQ-009).
// Channels is the set of IBC transfer channels; Denoms is the set of wrapped
// Bread denoms. ValidateGenesis enforces channel-id uniqueness and denom
// uniqueness. The data-engineer's genesis.go holds the schema helpers (G-008).
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Channels []TransferChannel `json:"channels" yaml:"channels"`
Denoms []WrappedBreadDenom `json:"denoms" yaml:"denoms"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Channels: []TransferChannel{},
Denoms: []WrappedBreadDenom{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate channel-ids and duplicate denoms. Delegates to
// the data-engineer's genesis.go helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("satellite: invalid genesis: %w", err)
}
if err := ValidateChannels(gs.Channels); err != nil {
return fmt.Errorf("satellite: %w", err)
}
if err := ValidateDenoms(gs.Denoms); err != nil {
return fmt.Errorf("satellite: %w", err)
}
return nil
}
+467
View File
@@ -0,0 +1,467 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
stypes "github.com/oy/openyield/x/satellite/types"
)
// --- L2Chain enum (exactly 5, Polygon active + 4 stubs) ------------------------
// TestL2ChainCountLockedConst asserts L2ChainCount == 5 and AllL2Chains()
// returns exactly 5 (REQ-009, D-021). A regression firewall.
func TestL2ChainCountLockedConst(t *testing.T) {
if stypes.L2ChainCount != 5 {
t.Errorf("L2ChainCount = %d, expected 5 (REQ-009, D-021 LOCKED)", stypes.L2ChainCount)
}
all := stypes.AllL2Chains()
if len(all) != 5 {
t.Errorf("AllL2Chains() len = %d, expected 5", len(all))
}
}
// TestAllL2ChainsNames asserts the 5 chain names in order with no extras, no
// dups, no renames (D-021: Polygon + Base/Arbitrum/Optimism/Solana).
func TestAllL2ChainsNames(t *testing.T) {
want := []string{"Polygon", "Base", "Arbitrum", "Optimism", "Solana"}
all := stypes.AllL2Chains()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, c := range all {
if string(c.Chain) != want[i] {
t.Errorf("AllL2Chains()[%d].Chain = %q, want %q", i, c.Chain, want[i])
}
if seen[string(c.Chain)] {
t.Errorf("duplicate L2Chain %q", c.Chain)
}
seen[string(c.Chain)] = true
}
}
// TestL2ChainValues asserts each named const matches its AllL2Chains entry.
func TestL2ChainValues(t *testing.T) {
if stypes.ChainPolygon != "Polygon" {
t.Errorf("ChainPolygon = %q", stypes.ChainPolygon)
}
if stypes.ChainBase != "Base" {
t.Errorf("ChainBase = %q", stypes.ChainBase)
}
if stypes.ChainArbitrum != "Arbitrum" {
t.Errorf("ChainArbitrum = %q", stypes.ChainArbitrum)
}
if stypes.ChainOptimism != "Optimism" {
t.Errorf("ChainOptimism = %q", stypes.ChainOptimism)
}
if stypes.ChainSolana != "Solana" {
t.Errorf("ChainSolana = %q", stypes.ChainSolana)
}
}
// TestPolygonOnlyActiveRep asserts Polygon is the only ChainActive entry in
// AllL2Chains (D-021: v0.2 scopes to ONE representative chain). The other
// four must be StatusPending.
func TestPolygonOnlyActiveRep(t *testing.T) {
all := stypes.AllL2Chains()
activeCount := 0
for _, c := range all {
if c.Activation == stypes.ChainActive {
activeCount++
if c.Chain != stypes.ChainPolygon {
t.Errorf("chain %q is active, expected only Polygon (D-021)", c.Chain)
}
}
if c.Activation == stypes.ChainStatusPending {
if c.Chain == stypes.ChainPolygon {
t.Error("Polygon must be active, not StatusPending (D-021)")
}
}
}
if activeCount != 1 {
t.Errorf("expected exactly 1 active chain (Polygon, D-021), got %d", activeCount)
}
}
// TestFourStubsAreStatusPending asserts Base, Arbitrum, Optimism, Solana are
// all StatusPending (D-021 — the 4 stubs).
func TestFourStubsAreStatusPending(t *testing.T) {
stubs := []stypes.L2Chain{stypes.ChainBase, stypes.ChainArbitrum, stypes.ChainOptimism, stypes.ChainSolana}
all := stypes.AllL2Chains()
activationByChain := map[string]stypes.ChainActivation{}
for _, c := range all {
activationByChain[string(c.Chain)] = c.Activation
}
for _, s := range stubs {
if activationByChain[string(s)] != stypes.ChainStatusPending {
t.Errorf("chain %q activation = %q, expected StatusPending (D-021)", s, activationByChain[string(s)])
}
}
}
// --- ChannelStatus enum (4 states) ---------------------------------------------
// TestChannelStatusCountLockedConst asserts ChannelStatusCount == 4 and
// AllChannelStatuses() returns exactly 4 (A-215 ICS-20 handshake).
func TestChannelStatusCountLockedConst(t *testing.T) {
if stypes.ChannelStatusCount != 4 {
t.Errorf("ChannelStatusCount = %d, expected 4 (A-215 ICS-20)", stypes.ChannelStatusCount)
}
all := stypes.AllChannelStatuses()
if len(all) != 4 {
t.Errorf("AllChannelStatuses() len = %d, expected 4", len(all))
}
}
// TestAllChannelStatusesNames asserts the 4 ICS-20 handshake names in order.
func TestAllChannelStatusesNames(t *testing.T) {
want := []string{"Init", "TryOpen", "Open", "Closed"}
all := stypes.AllChannelStatuses()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllChannelStatuses()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate ChannelStatus %q", s)
}
seen[string(s)] = true
}
}
// TestChannelStatusValues asserts each named const.
func TestChannelStatusValues(t *testing.T) {
if stypes.ChannelInit != "Init" {
t.Errorf("ChannelInit = %q", stypes.ChannelInit)
}
if stypes.ChannelTryOpen != "TryOpen" {
t.Errorf("ChannelTryOpen = %q", stypes.ChannelTryOpen)
}
if stypes.ChannelOpen != "Open" {
t.Errorf("ChannelOpen = %q", stypes.ChannelOpen)
}
if stypes.ChannelClosed != "Closed" {
t.Errorf("ChannelClosed = %q", stypes.ChannelClosed)
}
}
// --- Packet struct fields (ICS-20 v1 shape — A-215) ---------------------------
// TestPacketFieldsMatchICS20v1 asserts the Packet struct has exactly the 8
// ICS-20 v1 fields with the expected names. A-215 pins the packet shape to
// ICS-20 v1 to minimize churn. Cross-check field names via JSON tags.
func TestPacketFieldsMatchICS20v1(t *testing.T) {
p := stypes.Packet{
Sequence: 42,
SourcePort: "transfer",
SourceChannel: "channel-0",
DestPort: "transfer",
DestChannel: "channel-1",
Data: []byte("payload"),
TimeoutHeight: 1000,
TimeoutTimestamp: 9999999999,
}
if p.Sequence != 42 || p.SourcePort != "transfer" || p.SourceChannel != "channel-0" ||
p.DestPort != "transfer" || p.DestChannel != "channel-1" ||
len(p.Data) != 7 || p.TimeoutHeight != 1000 || p.TimeoutTimestamp != 9999999999 {
t.Error("Packet fields not set correctly")
}
// ICS-20 v1 field-name parity: marshal and check JSON tags.
bz, err := json.Marshal(p)
if err != nil {
t.Fatalf("marshal: %v", err)
}
js := string(bz)
wantTags := []string{
`"sequence"`, `"source_port"`, `"source_channel"`, `"dest_port"`,
`"dest_channel"`, `"data"`, `"timeout_height"`, `"timeout_timestamp"`,
}
for _, tag := range wantTags {
if !strings.Contains(js, tag) {
t.Errorf("Packet JSON missing tag %s (ICS-20 v1 shape parity A-215)", tag)
}
}
}
// TestPacketICS20v1FieldCount asserts the Packet struct has exactly 8 fields
// (the ICS-20 v1 shape). A regression firewall for packet-shape drift.
func TestPacketICS20v1FieldCount(t *testing.T) {
// The 8 ICS-20 v1 fields: sequence, source_port, source_channel,
// dest_port, dest_channel, data, timeout_height, timeout_timestamp.
// We verify by constructing a Packet with all 8 fields and asserting
// each is independently settable to a non-zero value.
p := stypes.Packet{
Sequence: 1,
SourcePort: "sp",
SourceChannel: "sc",
DestPort: "dp",
DestChannel: "dc",
Data: []byte{0x01},
TimeoutHeight: 1,
TimeoutTimestamp: 1,
}
if p.Sequence != 1 || p.SourcePort != "sp" || p.SourceChannel != "sc" ||
p.DestPort != "dp" || p.DestChannel != "dc" || len(p.Data) != 1 ||
p.TimeoutHeight != 1 || p.TimeoutTimestamp != 1 {
t.Error("Packet does not have all 8 ICS-20 v1 fields independently settable")
}
}
// --- WrappedBreadDenom trace-path encoding ------------------------------------
// TestWrappedBreadDenomStruct asserts the WrappedBreadDenom struct carries
// the denom and trace-path fields.
func TestWrappedBreadDenomStruct(t *testing.T) {
d := stypes.WrappedBreadDenom{
Denom: "transfer/channel-0/bread",
TracePath: "transfer/channel-0",
}
if d.Denom != "transfer/channel-0/bread" {
t.Errorf("Denom = %q", d.Denom)
}
if d.TracePath != "transfer/channel-0" {
t.Errorf("TracePath = %q", d.TracePath)
}
}
// TestWrappedBreadDenomTracePathEncoding asserts the IBC trace-path encoding
// (REQ-009): the denom is the trace-path + "/" + original-denom.
func TestWrappedBreadDenomTracePathEncoding(t *testing.T) {
cases := []struct {
trace string
orig string
}{
{"transfer/channel-0", "bread"},
{"transfer/channel-5", "bread"},
{"transfer/channel-0/transfer/channel-3", "bread"}, // multi-hop
}
for _, c := range cases {
full := c.trace + "/" + c.orig
d := stypes.WrappedBreadDenom{Denom: full, TracePath: c.trace}
if !strings.HasPrefix(d.Denom, d.TracePath) {
t.Errorf("denom %q must start with trace-path %q", d.Denom, d.TracePath)
}
if !strings.HasSuffix(d.Denom, c.orig) {
t.Errorf("denom %q must end with original denom %q", d.Denom, c.orig)
}
}
}
// --- TransferChannel ----------------------------------------------------------
// TestTransferChannelStruct asserts the TransferChannel struct carries all
// required fields.
func TestTransferChannelStruct(t *testing.T) {
ch := stypes.TransferChannel{
PortID: "transfer",
ChannelID: "channel-0",
Counterparty: "transfer/channel-0",
Status: stypes.ChannelOpen,
}
if ch.PortID != "transfer" || ch.ChannelID != "channel-0" ||
ch.Counterparty != "transfer/channel-0" || ch.Status != stypes.ChannelOpen {
t.Error("TransferChannel fields not set correctly")
}
}
// --- Genesis -------------------------------------------------------------------
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Channels and Denoms.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := stypes.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Channels == nil || len(gs.Channels) != 0 {
t.Errorf("Default Channels should be non-nil empty slice; got len=%d nil=%v", len(gs.Channels), gs.Channels == nil)
}
if gs.Denoms == nil || len(gs.Denoms) != 0 {
t.Errorf("Default Denoms should be non-nil empty slice; got len=%d nil=%v", len(gs.Denoms), gs.Denoms == nil)
}
}
// TestValidateGenesisRejectsDupChannelIDs asserts A-212: duplicate
// channel-ids are rejected.
func TestValidateGenesisRejectsDupChannelIDs(t *testing.T) {
gs := stypes.GenesisState{
Channels: []stypes.TransferChannel{
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelInit}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate channel-ids")
}
}
// TestValidateGenesisRejectsEmptyChannelID asserts empty channel-id is rejected.
func TestValidateGenesisRejectsEmptyChannelID(t *testing.T) {
gs := stypes.GenesisState{
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "", Status: stypes.ChannelInit}},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty channel-id")
}
}
// TestValidateGenesisRejectsUnknownChannelStatus asserts an unknown
// ChannelStatus is rejected.
func TestValidateGenesisRejectsUnknownChannelStatus(t *testing.T) {
gs := stypes.GenesisState{
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelStatus("Bogus")}},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject unknown channel status")
}
}
// TestValidateGenesisRejectsDupDenom asserts duplicate denoms are rejected.
func TestValidateGenesisRejectsDupDenom(t *testing.T) {
gs := stypes.GenesisState{
Denoms: []stypes.WrappedBreadDenom{
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate denoms")
}
}
// TestValidateGenesisRejectsEmptyDenom asserts empty denom is rejected.
func TestValidateGenesisRejectsEmptyDenom(t *testing.T) {
gs := stypes.GenesisState{
Denoms: []stypes.WrappedBreadDenom{{Denom: "", TracePath: "transfer/channel-0"}},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty denom")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := stypes.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := stypes.GenesisState{
Channels: []stypes.TransferChannel{
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
{PortID: "transfer", ChannelID: "channel-1", Status: stypes.ChannelInit},
},
Denoms: []stypes.WrappedBreadDenom{
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
},
}
bz, _ := json.Marshal(gs)
if err := stypes.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// --- Module consts -------------------------------------------------------------
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if stypes.ModuleName != "satellite" {
t.Errorf("ModuleName = %q", stypes.ModuleName)
}
if stypes.StoreKey != "satellite" {
t.Errorf("StoreKey = %q", stypes.StoreKey)
}
if stypes.RouterKey != "satellite" {
t.Errorf("RouterKey = %q", stypes.RouterKey)
}
if stypes.QuerierRoute != "satellite" {
t.Errorf("QuerierRoute = %q", stypes.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = stypes.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// The satellite module must avoid the banned financial holder terms (the
// lexicon firewall's banned list). Use "Holder"/"Reach" instead. The lexicon
// helpers are used here — no banned literals are inlined.
// TestLexiconNoBannedTermsInSatellitePackage scans every non-test .go file in
// the satellite/types package directory for the banned terms (case-
// insensitive). Production files only — the test file references banned
// terms via the lexicon package helpers.
func TestLexiconNoBannedTermsInSatellitePackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/satellite/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in satellite/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall — use Holder/Reach, not banned financial terms)", filepath.Base(f), found)
}
}
}
// TestLexiconNoBannedTermsInSatelliteTestFile asserts this test file itself
// does not contain any banned term as a literal.
func TestLexiconNoBannedTermsInSatelliteTestFile(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
bz, err := os.ReadFile(thisFile)
if err != nil {
t.Fatalf("read self: %v", err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Fatalf("satellite test file contains banned term %q — use lexicon helpers, not literals", found)
}
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file (v0.2 skeleton has zero external deps).
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/satellite/types/types_test.go -> repoRoot = .../oy (4 dirs up)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+49
View File
@@ -0,0 +1,49 @@
package types
import "fmt"
// genesis.go holds the data-engineer's genesis schema helpers for the stand
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
// the security-engineer's test assertions live in genesis_test.go.
//
// The Stand genesis schema is a membership-set: Stands (the organizational
// forms) + Memberships (the membership edges). The two top-level invariants
// are stand-id uniqueness and member-reach uniqueness within a stand
// (REQ-016, A-212 upgrade from v0.1's no-op ValidateGenesis).
// ValidateStands asserts stand-ids are present and unique.
func ValidateStands(stands []Stand) error {
seen := make(map[string]bool, len(stands))
for i, s := range stands {
if s.StandID == "" {
return fmt.Errorf("stand [%d]: empty stand-id", i)
}
if seen[s.StandID] {
return fmt.Errorf("stand: duplicate stand-id %q", s.StandID)
}
seen[s.StandID] = true
}
return nil
}
// ValidateMemberships asserts the membership-set invariant: the (stand-id,
// reach-id) pair is unique across the membership set — i.e. a reach can be
// a member of a stand at most once. The same reach MAY be a member of
// different stands (uniqueness is per-stand, not global).
func ValidateMemberships(memberships []Membership) error {
seen := make(map[string]bool, len(memberships))
for i, m := range memberships {
if m.StandID == "" {
return fmt.Errorf("membership [%d]: empty stand-id", i)
}
if m.ReachID == "" {
return fmt.Errorf("membership [%d]: empty reach-id", i)
}
key := m.StandID + "/" + m.ReachID
if seen[key] {
return fmt.Errorf("membership: duplicate member-reach %q in stand %q", m.ReachID, m.StandID)
}
seen[key] = true
}
return nil
}
+126
View File
@@ -0,0 +1,126 @@
package types_test
import (
"encoding/json"
"testing"
"github.com/oy/openyield/x/stand/types"
)
// genesis_test.go holds the security-engineer's test assertions for the
// data-engineer's genesis.go schema (G-008 split). The locked-const,
// enum-coverage, and lexicon assertions live in types_test.go.
// TestValidateStandsRejectsDup asserts ValidateStands rejects duplicate
// stand-ids (the membership-set's top-level invariant).
func TestValidateStandsRejectsDup(t *testing.T) {
stands := []types.Stand{
{StandID: "s1"},
{StandID: "s1"},
}
if err := types.ValidateStands(stands); err == nil {
t.Error("ValidateStands should reject duplicate stand-ids")
}
}
// TestValidateStandsRejectsEmpty asserts empty stand-id is rejected.
func TestValidateStandsRejectsEmpty(t *testing.T) {
stands := []types.Stand{{StandID: ""}}
if err := types.ValidateStands(stands); err == nil {
t.Error("ValidateStands should reject empty stand-id")
}
}
// TestValidateStandsAcceptsUnique asserts a clean stand set validates.
func TestValidateStandsAcceptsUnique(t *testing.T) {
stands := []types.Stand{{StandID: "s1"}, {StandID: "s2"}}
if err := types.ValidateStands(stands); err != nil {
t.Errorf("ValidateStands should accept unique ids, got: %v", err)
}
}
// TestValidateMembershipsRejectsDupWithinStand asserts the membership-set
// invariant: (stand-id, reach-id) pair must be unique.
func TestValidateMembershipsRejectsDupWithinStand(t *testing.T) {
m := []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s1", ReachID: "reach:a"}, // dup within stand
}
if err := types.ValidateMemberships(m); err == nil {
t.Error("ValidateMemberships should reject duplicate (stand-id, reach-id)")
}
}
// TestValidateMembershipsAcceptsSameReachDifferentStands asserts the same
// reach can join different stands (uniqueness is per-stand, not global).
func TestValidateMembershipsAcceptsSameReachDifferentStands(t *testing.T) {
m := []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s2", ReachID: "reach:a"}, // ok
}
if err := types.ValidateMemberships(m); err != nil {
t.Errorf("ValidateMemberships should accept same reach in different stands, got: %v", err)
}
}
// TestValidateMembershipsRejectsEmptyFields asserts empty stand-id or
// reach-id is rejected (every membership edge must be fully identified).
func TestValidateMembershipsRejectsEmptyFields(t *testing.T) {
cases := []struct {
name string
m []types.Membership
}{
{"empty stand-id", []types.Membership{{StandID: "", ReachID: "reach:a"}}},
{"empty reach-id", []types.Membership{{StandID: "s1", ReachID: ""}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if err := types.ValidateMemberships(tc.m); err == nil {
t.Error("ValidateMemberships should reject empty fields")
}
})
}
}
// TestValidateMembershipsEmptyOK asserts an empty membership set validates.
func TestValidateMembershipsEmptyOK(t *testing.T) {
if err := types.ValidateMemberships(nil); err != nil {
t.Errorf("ValidateMemberships(nil) should be nil, got: %v", err)
}
if err := types.ValidateMemberships([]types.Membership{}); err != nil {
t.Errorf("ValidateMemberships([]) should be nil, got: %v", err)
}
}
// TestValidateGenesisComposesBoth asserts ValidateGenesis composes both
// ValidateStands and ValidateMemberships.
func TestValidateGenesisComposesBoth(t *testing.T) {
// clean stands but dup membership — should fail
gs := types.GenesisState{
Stands: []types.Stand{{StandID: "s1"}},
Memberships: []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s1", ReachID: "reach:a"},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject dup membership even with clean stands")
}
}
// TestValidateGenesisClean asserts a fully clean genesis validates.
func TestValidateGenesisClean(t *testing.T) {
gs := types.GenesisState{
Stands: []types.Stand{{StandID: "s1"}, {StandID: "s2"}},
Memberships: []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s2", ReachID: "reach:a"},
{StandID: "s1", ReachID: "reach:b"},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
+136
View File
@@ -0,0 +1,136 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "stand"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// StandTypeCount is the locked count of StandType enum values (vision §11).
// A regression firewall: adding/removing/renaming a Stand type breaks this
// const's test.
StandTypeCount = 9
)
// StandType enumerates the nine organizational forms (vision §11, REQ-016).
// All nine are treated uniformly in v0.2 (A-213: the Shadow Stand behavioral
// split is deferred to v0.3 design).
type StandType string
const (
StandHousehold StandType = "Household"
StandCrew StandType = "Crew"
StandEntity StandType = "Entity"
StandCoop StandType = "Co-op"
StandCircle StandType = "Circle"
StandTrust StandType = "Trust"
StandFoundation StandType = "Foundation"
StandConfederation StandType = "Confederation"
StandShadow StandType = "Shadow"
)
// AllStandTypes returns all nine StandType values in vision §11 order.
// Locked-const test asserts exactly 9 entries with these names (REQ-016).
func AllStandTypes() []StandType {
return []StandType{
StandHousehold,
StandCrew,
StandEntity,
StandCoop,
StandCircle,
StandTrust,
StandFoundation,
StandConfederation,
StandShadow,
}
}
// Stand is a governed group holding a Vault (vision §11, REQ-016).
// Modeled on Cosmos SDK x/group (a group of members with a decision policy
// governing a Vault). admin-reach references a Reach ID (by-ID-string, G-003);
// vault-id references x/vault by ID string (no struct import).
type Stand struct {
StandID string `json:"stand_id" yaml:"stand_id"`
Type StandType `json:"type" yaml:"type"`
Name string `json:"name" yaml:"name"`
VaultID string `json:"vault_id" yaml:"vault_id"`
AdminReach string `json:"admin_reach" yaml:"admin_reach"`
CreatedAt int64 `json:"created_at" yaml:"created_at"`
MemberCount uint32 `json:"member_count" yaml:"member_count"`
}
// StandRole enumerates member roles within a Stand.
type StandRole string
const (
RoleMember StandRole = "Member"
RoleAdmin StandRole = "Admin"
RoleObserver StandRole = "Observer"
)
// Membership is a Stand membership edge (REQ-016). stand-id references
// x/stand by ID string; reach-id references x/identity Reach by string
// (G-003 by-ID-string invariant).
type Membership struct {
StandID string `json:"stand_id" yaml:"stand_id"`
ReachID string `json:"reach_id" yaml:"reach_id"`
JoinedAt int64 `json:"joined_at" yaml:"joined_at"`
Role StandRole `json:"role" yaml:"role"`
}
// StandPolicy is a stub for a Stand's decision policy (A-205).
// Mirrors x/group DecisionPolicy: threshold (N-of-M) OR weighted (sum of
// weights >= threshold). The skeleton does not enforce the policy; v0.3
// wires the live aggregation. Exactly one of Threshold/Weighted should be
// non-zero in the live object; the skeleton keeps both as fields for
// future-wiring symmetry with x/group.
type StandPolicy struct {
Threshold uint32 `json:"threshold" yaml:"threshold"`
Weighted bool `json:"weighted" yaml:"weighted"`
}
// Params for the stand module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the stand module genesis state (REQ-016).
// Stands + Memberships are the two top-level sets; ValidateGenesis enforces
// stand-id uniqueness and member-reach uniqueness within a stand.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Stands []Stand `json:"stands" yaml:"stands"`
Memberships []Membership `json:"memberships" yaml:"memberships"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Stands: []Stand{},
Memberships: []Membership{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate stand-ids and duplicate (stand-id, reach-id)
// membership pairs. The membership-set invariant is "a reach can be a
// member of a stand at most once; the same reach may join different stands".
// Validation is delegated to the data-engineer's genesis.go helpers (G-008).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("stand: invalid genesis: %w", err)
}
if err := ValidateStands(gs.Stands); err != nil {
return fmt.Errorf("stand: %w", err)
}
if err := ValidateMemberships(gs.Memberships); err != nil {
return fmt.Errorf("stand: %w", err)
}
return nil
}
+295
View File
@@ -0,0 +1,295 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/stand/types"
)
// TestStandTypeCountLockedConst asserts AllStandTypes() returns exactly 9
// (vision §11). A regression firewall: adding/removing/renaming a Stand type
// breaks this test (REQ-016).
func TestStandTypeCountLockedConst(t *testing.T) {
if types.StandTypeCount != 9 {
t.Errorf("StandTypeCount = %d, expected 9 (vision §11 LOCKED)", types.StandTypeCount)
}
all := types.AllStandTypes()
if len(all) != 9 {
t.Errorf("AllStandTypes() len = %d, expected 9", len(all))
}
}
// TestAllStandTypesNames asserts the 9 vision §11 names in order with no
// extras, no dups, no renames.
func TestAllStandTypesNames(t *testing.T) {
want := []string{
"Household", "Crew", "Entity", "Co-op", "Circle",
"Trust", "Foundation", "Confederation", "Shadow",
}
all := types.AllStandTypes()
if len(all) != len(want) {
t.Fatalf("len = %d, want %d", len(all), len(want))
}
seen := map[string]bool{}
for i, s := range all {
if string(s) != want[i] {
t.Errorf("AllStandTypes()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate StandType %q", s)
}
seen[string(s)] = true
}
}
// TestStandTypeValues asserts each named const matches its AllStandTypes entry.
func TestStandTypeValues(t *testing.T) {
if types.StandHousehold != "Household" {
t.Errorf("StandHousehold = %q", types.StandHousehold)
}
if types.StandCrew != "Crew" {
t.Errorf("StandCrew = %q", types.StandCrew)
}
if types.StandEntity != "Entity" {
t.Errorf("StandEntity = %q", types.StandEntity)
}
if types.StandCoop != "Co-op" {
t.Errorf("StandCoop = %q", types.StandCoop)
}
if types.StandCircle != "Circle" {
t.Errorf("StandCircle = %q", types.StandCircle)
}
if types.StandTrust != "Trust" {
t.Errorf("StandTrust = %q", types.StandTrust)
}
if types.StandFoundation != "Foundation" {
t.Errorf("StandFoundation = %q", types.StandFoundation)
}
if types.StandConfederation != "Confederation" {
t.Errorf("StandConfederation = %q", types.StandConfederation)
}
if types.StandShadow != "Shadow" {
t.Errorf("StandShadow = %q", types.StandShadow)
}
}
// TestStandRoleEnumCoverage asserts the three StandRole values.
func TestStandRoleEnumCoverage(t *testing.T) {
roles := []types.StandRole{types.RoleMember, types.RoleAdmin, types.RoleObserver}
if len(roles) != 3 {
t.Errorf("expected 3 StandRole consts, got %d", len(roles))
}
seen := map[types.StandRole]bool{}
for _, r := range roles {
if r == "" {
t.Error("empty StandRole")
}
if seen[r] {
t.Errorf("duplicate StandRole %q", r)
}
seen[r] = true
}
}
// TestStandStructFields asserts Stand carries all required fields.
func TestStandStructFields(t *testing.T) {
s := types.Stand{
StandID: "s1",
Type: types.StandHousehold,
Name: "Household A",
VaultID: "v1",
AdminReach: "reach:admin",
CreatedAt: 100,
MemberCount: 3,
}
if s.StandID != "s1" || s.Type != types.StandHousehold || s.Name != "Household A" ||
s.VaultID != "v1" || s.AdminReach != "reach:admin" || s.CreatedAt != 100 ||
s.MemberCount != 3 {
t.Error("Stand fields not set correctly")
}
}
// TestMembershipStructFields asserts Membership carries all required fields.
func TestMembershipStructFields(t *testing.T) {
m := types.Membership{
StandID: "s1",
ReachID: "reach:member",
JoinedAt: 200,
Role: types.RoleMember,
}
if m.StandID != "s1" || m.ReachID != "reach:member" || m.JoinedAt != 200 ||
m.Role != types.RoleMember {
t.Error("Membership fields not set correctly")
}
}
// TestStandPolicyStub asserts StandPolicy carries threshold + weighted fields
// (A-205 mirrors x/group DecisionPolicy).
func TestStandPolicyStub(t *testing.T) {
p := types.StandPolicy{Threshold: 5, Weighted: false}
if p.Threshold != 5 || p.Weighted != false {
t.Error("StandPolicy fields not set correctly")
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Stands and Memberships.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Stands == nil || len(gs.Stands) != 0 {
t.Errorf("Default Stands should be non-nil empty slice; got len=%d nil=%v", len(gs.Stands), gs.Stands == nil)
}
if gs.Memberships == nil || len(gs.Memberships) != 0 {
t.Errorf("Default Memberships should be non-nil empty slice; got len=%d nil=%v", len(gs.Memberships), gs.Memberships == nil)
}
}
// TestValidateGenesisRejectsDupStandIDs asserts A-212: duplicate stand-ids
// are rejected.
func TestValidateGenesisRejectsDupStandIDs(t *testing.T) {
gs := types.GenesisState{
Stands: []types.Stand{
{StandID: "s1"},
{StandID: "s1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate stand-ids")
}
}
// TestValidateGenesisRejectsDupMemberReach asserts A-212: duplicate
// (stand-id, reach-id) membership pairs are rejected.
func TestValidateGenesisRejectsDupMemberReach(t *testing.T) {
gs := types.GenesisState{
Memberships: []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s1", ReachID: "reach:a"}, // dup within same stand
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate member-reach within a stand")
}
}
// TestValidateGenesisAcceptsSameReachInDifferentStands asserts the same
// reach can be a member of two different stands (uniqueness is per-stand).
func TestValidateGenesisAcceptsSameReachInDifferentStands(t *testing.T) {
gs := types.GenesisState{
Memberships: []types.Membership{
{StandID: "s1", ReachID: "reach:a"},
{StandID: "s2", ReachID: "reach:a"}, // ok — different stand
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept same reach in different stands, got: %v", err)
}
}
// TestValidateGenesisRejectsEmptyStandID asserts empty stand-id is rejected.
func TestValidateGenesisRejectsEmptyStandID(t *testing.T) {
gs := types.GenesisState{
Stands: []types.Stand{{StandID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty stand-id")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{bad`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Stands: []types.Stand{{StandID: "s1"}, {StandID: "s2"}},
Memberships: []types.Membership{{StandID: "s1", ReachID: "reach:a"}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "stand" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "stand" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "stand" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "stand" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInStandPackage scans every non-test .go file in
// the stand/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file contains the banned terms as the list
// of things to forbid (standard lexicon-test bootstrapping pattern).
func TestLexiconNoBannedTermsInStandPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/stand/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in stand/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// packageDir resolves a Go import path to its filesystem directory.
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
+29
View File
@@ -0,0 +1,29 @@
package types
import "fmt"
// ValidateAuditLogs enforces the append-only audit-log invariants (REQ-015):
// 1. entry-ids are unique (no duplicate entry-id in the slice)
// 2. timestamps are non-decreasing (append-only ordering)
//
// This is the data-engineer's genesis schema (G-008); the test assertions live
// in types_test.go (security-engineer's territory). Called by ValidateGenesis
// in types.go.
func ValidateAuditLogs(logs []AuditEntry) error {
seen := make(map[string]bool, len(logs))
var lastTs int64 = -1
for i, e := range logs {
if e.EntryID == "" {
return fmt.Errorf("audit log [%d]: empty entry-id", i)
}
if seen[e.EntryID] {
return fmt.Errorf("audit log: duplicate entry-id %q", e.EntryID)
}
seen[e.EntryID] = true
if i > 0 && e.Timestamp < lastTs {
return fmt.Errorf("audit log: timestamps must be non-decreasing (entry %q)", e.EntryID)
}
lastTs = e.Timestamp
}
return nil
}
+139
View File
@@ -0,0 +1,139 @@
package types_test
import (
"encoding/json"
"testing"
"github.com/oy/openyield/x/window/types"
)
// genesis_test.go holds the security-engineer's test assertions for the
// data-engineer's genesis.go schema (G-008 split). The general lifecycle
// and lexicon tests live in types_test.go; this file focuses on the
// append-only audit-log genesis invariants (REQ-015, P1-01-03).
// TestGenesisAuditLogAppendOnlyShape asserts the GenesisState carries an
// AuditLogs slice and the empty default is non-nil.
func TestGenesisAuditLogAppendOnlyShape(t *testing.T) {
gs := types.DefaultGenesisState()
if gs.AuditLogs == nil {
t.Fatal("DefaultGenesisState.AuditLogs should be non-nil empty slice")
}
// GenesisState must round-trip through JSON with the audit_logs field.
bz, err := json.Marshal(gs)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var back types.GenesisState
if err := json.Unmarshal(bz, &back); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if back.AuditLogs == nil {
t.Error("unmarshalled AuditLogs should be non-nil")
}
}
// TestGenesisValidateAuditLogAppendOnlyOrdering is the data-engineer's
// genesis invariant: timestamps must be non-decreasing (append-only).
func TestGenesisValidateAuditLogAppendOnlyOrdering(t *testing.T) {
cases := []struct {
name string
logs []types.AuditEntry
wantErr bool
}{
{
name: "single entry ok",
logs: []types.AuditEntry{{EntryID: "e1", Timestamp: 100}},
wantErr: false,
},
{
name: "equal timestamps ok (append-only allows equal)",
logs: []types.AuditEntry{
{EntryID: "e1", Timestamp: 100},
{EntryID: "e2", Timestamp: 100},
},
wantErr: false,
},
{
name: "strictly increasing ok",
logs: []types.AuditEntry{
{EntryID: "e1", Timestamp: 100},
{EntryID: "e2", Timestamp: 200},
{EntryID: "e3", Timestamp: 300},
},
wantErr: false,
},
{
name: "decreasing rejected",
logs: []types.AuditEntry{
{EntryID: "e1", Timestamp: 300},
{EntryID: "e2", Timestamp: 100},
},
wantErr: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := types.ValidateAuditLogs(tc.logs)
if tc.wantErr && err == nil {
t.Error("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Errorf("expected nil, got: %v", err)
}
})
}
}
// TestGenesisValidateAuditLogNoDupEntryIDs is the data-engineer's genesis
// invariant: entry-ids must be unique.
func TestGenesisValidateAuditLogNoDupEntryIDs(t *testing.T) {
logs := []types.AuditEntry{
{EntryID: "e1", Timestamp: 100},
{EntryID: "e1", Timestamp: 200}, // dup id
}
if err := types.ValidateAuditLogs(logs); err == nil {
t.Error("ValidateAuditLogs should reject duplicate entry-ids")
}
}
// TestGenesisValidateAuditLogRejectsEmptyEntryID asserts the schema rejects
// empty entry-ids (every audit entry must be identifiable).
func TestGenesisValidateAuditLogRejectsEmptyEntryID(t *testing.T) {
logs := []types.AuditEntry{{EntryID: "", Timestamp: 100}}
if err := types.ValidateAuditLogs(logs); err == nil {
t.Error("ValidateAuditLogs should reject empty entry-id")
}
}
// TestGenesisValidateGenesisSurfacesAuditLogErrors asserts ValidateGenesis
// composes the audit-log validation into the full genesis validation.
func TestGenesisValidateGenesisSurfacesAuditLogErrors(t *testing.T) {
gs := types.GenesisState{
Windows: []types.Window{{WindowID: "w1"}},
AuditLogs: []types.AuditEntry{
{EntryID: "e1", Timestamp: 200},
{EntryID: "e2", Timestamp: 100}, // out of order
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should surface audit-log ordering error")
}
}
// TestGenesisValidateGenesisCleanAuditLog asserts a clean audit log passes
// full genesis validation.
func TestGenesisValidateGenesisCleanAuditLog(t *testing.T) {
gs := types.GenesisState{
Windows: []types.Window{{WindowID: "w1"}},
AuditLogs: []types.AuditEntry{
{EntryID: "e1", Timestamp: 100, Action: "open", Result: "ok", GranterRef: "reach:g"},
{EntryID: "e2", Timestamp: 200, Action: "revoke", Result: "ok", GranterRef: "reach:g"},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean audit log, got: %v", err)
}
}
+175
View File
@@ -0,0 +1,175 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "window"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
)
// ScopeKind enumerates the access scopes a Window can open (§4.4, REQ-015).
// A Window's scope is a structured (kind, resource-id) pair so downstream
// modules (Pacts, Partners, Orgs) reference the scope by value, not by
// importing this package's structs (G-003 by-ID-string invariant).
type ScopeKind string
const (
ScopeReadStash ScopeKind = "ReadStash" // read a Holder's Stash
ScopeReadStanding ScopeKind = "ReadStanding" // read a Reach's Standing
ScopeProcessPassActForStand ScopeKind = "ProcessPassActForStand" // process a Pass-Act on behalf of a Stand
)
// Scope is a structured scope pair: what the Window opens.
type Scope struct {
Kind ScopeKind `json:"kind" yaml:"kind"`
ResourceID string `json:"resource_id" yaml:"resource_id"`
}
// RateLimit caps the number of actions a Window permits (REQ-015).
// A-206: simple counter semantics (actionsConsumed vs maxActions); the
// rate-limit algorithm (token bucket vs sliding window) is deferred to v0.3.
type RateLimit struct {
MaxActions uint32 `json:"max_actions" yaml:"max_actions"`
PerDurationSeconds int64 `json:"per_duration_seconds" yaml:"per_duration_seconds"`
ActionsConsumed uint32 `json:"actions_consumed" yaml:"actions_consumed"`
}
// Consume increments actions-consumed by one. Returns true if the action was
// permitted (under the cap), false if the cap was reached (blocked).
// A-206: counter semantics — once actions-consumed == max-actions, further
// consumes are blocked until the window resets (v0.3 will define reset).
func (r *RateLimit) Consume() bool {
if r.ActionsConsumed >= r.MaxActions {
return false
}
r.ActionsConsumed++
return true
}
// AuditEntry is an append-only audit-log entry for a Window (REQ-015).
// Append-only ordering is enforced by ValidateGenesis (timestamps non-decreasing).
type AuditEntry struct {
EntryID string `json:"entry_id" yaml:"entry_id"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
Action string `json:"action" yaml:"action"`
Result string `json:"result" yaml:"result"`
GranterRef string `json:"granter_ref" yaml:"granter_ref"`
}
// WindowStatus enumerates the lifecycle states of a Window (REQ-015).
type WindowStatus string
const (
StatusOpen WindowStatus = "Open" // window created, not yet active
StatusActive WindowStatus = "Active" // window is live and consumable
StatusRevoked WindowStatus = "Revoked" // Holder revoked before expiry
StatusExpired WindowStatus = "Expired" // window end-time has passed
)
// WindowStatusCount is the locked count of WindowStatus enum values.
// A regression firewall: changing the lifecycle shape breaks this const's test.
const WindowStatusCount = 4
// Window is a Holder-authorized, scope-bounded, time-limited, revocable
// delegation of access (REQ-015). Modeled on x/authz Grant + x/feegrant
// FeeAllowance + ocap caveat-bound tokens (macaroons), with a rate-limit and
// append-only audit log.
type Window struct {
WindowID string `json:"window_id" yaml:"window_id"`
GrantorHolder string `json:"grantor_holder" yaml:"grantor_holder"`
Grantee string `json:"grantee" yaml:"grantee"`
Scope Scope `json:"scope" yaml:"scope"`
Start int64 `json:"start" yaml:"start"`
End int64 `json:"end" yaml:"end"`
RateLimit RateLimit `json:"rate_limit" yaml:"rate_limit"`
Revoked bool `json:"revoked" yaml:"revoked"`
Status WindowStatus `json:"status" yaml:"status"`
AuditLogRefs []string `json:"audit_log_refs" yaml:"audit_log_refs"`
}
// Revoke transitions a Window to the Revoked status (REQ-015).
// Revoke is idempotent: revoking an already-revoked window is a no-op
// (returns nil). Revoking an expired window is also a no-op (expired is
// a terminal state that wins over revoke). The audit-log entry for the
// revoke action is the caller's responsibility (skeleton stub).
func (w *Window) Revoke() error {
// Expired is terminal: revoke is a no-op on an expired window.
if w.Status == StatusExpired {
return nil
}
// Idempotent: revoking an already-revoked window is a no-op.
if w.Status == StatusRevoked {
return nil
}
w.Status = StatusRevoked
w.Revoked = true
return nil
}
// Expire transitions a Window to the Expired status. Used by the (future)
// keeper's end-block sweep when now > End. Expire is terminal: a later
// Revoke on an expired window is a no-op.
func (w *Window) Expire() {
w.Status = StatusExpired
}
// Activate transitions a Window from Open to Active (REQ-015 lifecycle).
// Only an Open window can be activated.
func (w *Window) Activate() error {
if w.Status != StatusOpen {
return fmt.Errorf("cannot activate window in status %q", w.Status)
}
w.Status = StatusActive
return nil
}
// Params for the window module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the window module genesis state (REQ-015).
// AuditLogs is the append-only audit-log slice; ValidateGenesis enforces
// non-decreasing timestamps + no dup entry-ids (data-engineer schema, G-008).
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Windows []Window `json:"windows" yaml:"windows"`
AuditLogs []AuditEntry `json:"audit_logs" yaml:"audit_logs"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Windows: []Window{},
AuditLogs: []AuditEntry{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate window-ids. Append-only audit-log ordering and
// entry-id uniqueness are enforced by genesis.go's ValidateAuditLogs.
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("window: invalid genesis: %w", err)
}
seen := make(map[string]bool, len(gs.Windows))
for _, w := range gs.Windows {
if w.WindowID == "" {
return fmt.Errorf("window: empty window-id")
}
if seen[w.WindowID] {
return fmt.Errorf("window: duplicate window-id %q", w.WindowID)
}
seen[w.WindowID] = true
}
if err := ValidateAuditLogs(gs.AuditLogs); err != nil {
return fmt.Errorf("window: %w", err)
}
return nil
}
+537
View File
@@ -0,0 +1,537 @@
package types_test
import (
"encoding/json"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/window/types"
)
// TestWindowStatusCountLockedConst asserts the WindowStatus enum count is
// exactly 4 (Open, Active, Revoked, Expired). A regression firewall: adding
// or removing a status breaks this test.
func TestWindowStatusCountLockedConst(t *testing.T) {
if types.WindowStatusCount != 4 {
t.Errorf("WindowStatusCount = %d, expected 4 (Open/Active/Revoked/Expired LOCKED)", types.WindowStatusCount)
}
statuses := []types.WindowStatus{
types.StatusOpen, types.StatusActive, types.StatusRevoked, types.StatusExpired,
}
if len(statuses) != 4 {
t.Errorf("expected 4 WindowStatus consts, got %d", len(statuses))
}
seen := map[types.WindowStatus]bool{}
for _, s := range statuses {
if seen[s] {
t.Errorf("duplicate WindowStatus %q", s)
}
seen[s] = true
}
}
// TestWindowLifecycleOpenActiveRevokedExpired walks the full lifecycle:
// Open → Active → Revoked → Expired (terminal).
func TestWindowLifecycleOpenActiveRevokedExpired(t *testing.T) {
w := types.Window{Status: types.StatusOpen}
if w.Status != types.StatusOpen {
t.Fatalf("expected Open, got %q", w.Status)
}
if err := w.Activate(); err != nil {
t.Fatalf("Activate: %v", err)
}
if w.Status != types.StatusActive {
t.Fatalf("expected Active, got %q", w.Status)
}
if err := w.Revoke(); err != nil {
t.Fatalf("Revoke: %v", err)
}
if w.Status != types.StatusRevoked {
t.Fatalf("expected Revoked, got %q", w.Status)
}
if !w.Revoked {
t.Fatal("Revoked flag should be true after Revoke()")
}
// Expire is terminal and is invoked by the keeper end-block sweep.
w.Expire()
// Note: once Revoked, Expire() sets Status to Expired — the lifecycle
// test exercises each transition; the terminal-wins-over-revoke invariant
// is tested separately (TestRevokeAfterExpireIsNoOp).
}
// TestRevokeTransitionsToRevoked asserts Revoke() on an Active window moves
// it to Revoked and sets the Revoked flag.
func TestRevokeTransitionsToRevoked(t *testing.T) {
w := types.Window{Status: types.StatusActive}
if err := w.Revoke(); err != nil {
t.Fatalf("Revoke on Active: %v", err)
}
if w.Status != types.StatusRevoked {
t.Errorf("expected Revoked, got %q", w.Status)
}
if !w.Revoked {
t.Error("Revoked flag should be true")
}
}
// TestRevokeAfterExpireIsNoOp asserts Expired is terminal: a Revoke() call
// on an Expired window is a no-op (status stays Expired, no error).
func TestRevokeAfterExpireIsNoOp(t *testing.T) {
w := types.Window{Status: types.StatusExpired}
if err := w.Revoke(); err != nil {
t.Fatalf("Revoke on Expired should be no-op, got error: %v", err)
}
if w.Status != types.StatusExpired {
t.Errorf("Revoke on Expired should not change status; got %q", w.Status)
}
}
// TestDoubleRevokeIdempotent asserts revoking an already-revoked window is
// idempotent (no error, status stays Revoked). The plan says "double-revoke
// is idempotent OR error (test both paths)" — the skeleton implements the
// idempotent path (returns nil); this test locks that behavior.
func TestDoubleRevokeIdempotent(t *testing.T) {
w := types.Window{Status: types.StatusActive}
_ = w.Revoke()
if w.Status != types.StatusRevoked {
t.Fatalf("first Revoke failed: %q", w.Status)
}
if err := w.Revoke(); err != nil {
t.Fatalf("second Revoke should be idempotent (no error), got: %v", err)
}
if w.Status != types.StatusRevoked {
t.Errorf("double-revoke should keep status Revoked; got %q", w.Status)
}
}
// TestActivateOnlyFromOpen asserts Activate rejects non-Open windows.
func TestActivateOnlyFromOpen(t *testing.T) {
w := types.Window{Status: types.StatusRevoked}
if err := w.Activate(); err == nil {
t.Error("Activate on Revoked should error")
}
w2 := types.Window{Status: types.StatusActive}
if err := w2.Activate(); err == nil {
t.Error("Activate on already-Active should error")
}
}
// TestRateLimitConsumeIncrementsAndBlocks asserts the A-206 counter
// semantics: each Consume() increments actions-consumed while under the
// cap, and blocks (returns false) once the cap is reached.
func TestRateLimitConsumeIncrementsAndBlocks(t *testing.T) {
tt := []struct {
name string
maxActions uint32
consumeN int
wantLast bool // expected return of the Nth consume
wantCount uint32
}{
{"under cap", 5, 3, true, 3},
{"exactly cap", 3, 3, true, 3},
{"at cap then block", 2, 3, false, 2}, // 3rd consume blocked
{"zero cap blocks all", 0, 1, false, 0},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
r := types.RateLimit{MaxActions: tc.maxActions}
var last bool
for i := 0; i < tc.consumeN; i++ {
last = r.Consume()
}
if last != tc.wantLast {
t.Errorf("last Consume() = %v, want %v", last, tc.wantLast)
}
if r.ActionsConsumed != tc.wantCount {
t.Errorf("ActionsConsumed = %d, want %d", r.ActionsConsumed, tc.wantCount)
}
})
}
}
// TestScopeKindEnumCoverage asserts all three ScopeKind values are distinct
// and non-empty (REQ-015 scope set).
func TestScopeKindEnumCoverage(t *testing.T) {
kinds := []types.ScopeKind{
types.ScopeReadStash, types.ScopeReadStanding, types.ScopeProcessPassActForStand,
}
if len(kinds) != 3 {
t.Errorf("expected 3 ScopeKind consts, got %d", len(kinds))
}
seen := map[types.ScopeKind]bool{}
for _, k := range kinds {
if k == "" {
t.Error("empty ScopeKind")
}
if seen[k] {
t.Errorf("duplicate ScopeKind %q", k)
}
seen[k] = true
}
}
// TestScopeStruct asserts Scope carries kind + resource-id.
func TestScopeStruct(t *testing.T) {
s := types.Scope{Kind: types.ScopeReadStash, ResourceID: "reach:abc"}
if s.Kind != types.ScopeReadStash {
t.Errorf("Kind = %q", s.Kind)
}
if s.ResourceID != "reach:abc" {
t.Errorf("ResourceID = %q", s.ResourceID)
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns empty
// slices (not nil) for Windows and AuditLogs.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if len(gs.Windows) != 0 {
t.Errorf("Default Windows len = %d, want 0", len(gs.Windows))
}
if gs.Windows == nil {
t.Error("Default Windows should be non-nil empty slice")
}
if len(gs.AuditLogs) != 0 {
t.Errorf("Default AuditLogs len = %d, want 0", len(gs.AuditLogs))
}
if gs.AuditLogs == nil {
t.Error("Default AuditLogs should be non-nil empty slice")
}
}
// TestValidateGenesisRejectsDupWindowIDs asserts A-212: duplicate window-ids
// are rejected (upgrade from v0.1's no-op ValidateGenesis).
func TestValidateGenesisRejectsDupWindowIDs(t *testing.T) {
gs := types.GenesisState{
Windows: []types.Window{
{WindowID: "w1"},
{WindowID: "w1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate window-ids")
}
}
// TestValidateGenesisAcceptsUniqueIDs asserts a clean genesis validates.
func TestValidateGenesisAcceptsUniqueIDs(t *testing.T) {
gs := types.GenesisState{
Windows: []types.Window{
{WindowID: "w1"},
{WindowID: "w2"},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept unique ids, got: %v", err)
}
}
// TestValidateGenesisRejectsEmptyWindowID asserts empty window-id is rejected.
func TestValidateGenesisRejectsEmptyWindowID(t *testing.T) {
gs := types.GenesisState{
Windows: []types.Window{{WindowID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty window-id")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestAuditLogAppendOnlyOrdering asserts ValidateAuditLogs rejects
// non-decreasing timestamps (append-only invariant, data-engineer schema).
func TestAuditLogAppendOnlyOrdering(t *testing.T) {
tt := []struct {
name string
logs []types.AuditEntry
wantErr bool
}{
{
name: "empty ok",
logs: []types.AuditEntry{},
},
{
name: "non-decreasing ok",
logs: []types.AuditEntry{
{EntryID: "a1", Timestamp: 100},
{EntryID: "a2", Timestamp: 100},
{EntryID: "a3", Timestamp: 200},
},
},
{
name: "decreasing rejected",
logs: []types.AuditEntry{
{EntryID: "a1", Timestamp: 200},
{EntryID: "a2", Timestamp: 100}, // out of order
},
wantErr: true,
},
{
name: "dup entry-id rejected",
logs: []types.AuditEntry{
{EntryID: "a1", Timestamp: 100},
{EntryID: "a1", Timestamp: 200}, // dup id
},
wantErr: true,
},
{
name: "empty entry-id rejected",
logs: []types.AuditEntry{
{EntryID: "", Timestamp: 100},
},
wantErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
err := types.ValidateAuditLogs(tc.logs)
if tc.wantErr && err == nil {
t.Error("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Errorf("expected nil, got: %v", err)
}
})
}
}
// TestValidateGenesisRejectsBadAuditLog asserts ValidateGenesis surfaces
// audit-log errors.
func TestValidateGenesisRejectsBadAuditLog(t *testing.T) {
gs := types.GenesisState{
AuditLogs: []types.AuditEntry{
{EntryID: "a1", Timestamp: 200},
{EntryID: "a2", Timestamp: 100}, // out of order
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject out-of-order audit logs")
}
}
// TestAuditEntryStruct asserts AuditEntry carries all required fields.
func TestAuditEntryStruct(t *testing.T) {
e := types.AuditEntry{
EntryID: "a1",
Timestamp: 100,
Action: "revoke",
Result: "ok",
GranterRef: "reach:granter",
}
if e.EntryID != "a1" || e.Timestamp != 100 || e.Action != "revoke" ||
e.Result != "ok" || e.GranterRef != "reach:granter" {
t.Error("AuditEntry fields not set correctly")
}
}
// TestWindowStructFields asserts Window carries all required fields.
func TestWindowStructFields(t *testing.T) {
w := types.Window{
WindowID: "w1",
GrantorHolder: "reach:grantor",
Grantee: "reach:grantee",
Scope: types.Scope{Kind: types.ScopeReadStash, ResourceID: "stash:1"},
Start: 100,
End: 200,
RateLimit: types.RateLimit{MaxActions: 5, PerDurationSeconds: 60},
Status: types.StatusOpen,
AuditLogRefs: []string{"a1", "a2"},
}
if w.WindowID != "w1" || w.GrantorHolder != "reach:grantor" ||
w.Grantee != "reach:grantee" || w.Start != 100 || w.End != 200 ||
w.Status != types.StatusOpen || len(w.AuditLogRefs) != 2 {
t.Error("Window fields not set correctly")
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "window" {
t.Errorf("ModuleName = %q, want %q", types.ModuleName, "window")
}
if types.StoreKey != "window" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "window" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "window" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
//
// The lexicon firewall scans the window package's .go files for the 9 banned
// terms. v0.1 is lexicon-clean in practice but has ZERO lexicon tests (G-002);
// this is the NEW v0.2 firewall. The project-wide meta-test in P1-04-02
// extends this to all x/**/*.go files.
// TestLexiconNoBannedTermsInWindowPackage scans every non-test .go file in
// the window/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file itself contains the banned terms as
// the list of things to forbid, which is the standard lexicon-test
// bootstrapping pattern. The project-wide meta-test (P1-04-02) scans all
// x/**/*.go (including tests) with self-exclusion.
func TestLexiconNoBannedTermsInWindowPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/window/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in window/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// --- G-003 by-ID-string import invariant -----------------------------------------
//
// A-203/G-003: no production (non-test) .go file under x/ may import another
// x/<module>/types package by struct (enforced as a TESTED invariant, not
// just a convention). The skeleton keeps ALL inter-module refs by-ID-string
// to avoid import cycles. This test scans every non-test .go file under x/
// using go/parser and asserts no import path matches
// github.com/oy/openyield/x/<other>/types.
// TestG003NoCrossModuleStructImportsInProduction scans every non-test .go
// file under x/ for imports of other x/<module>/types packages.
func TestG003NoCrossModuleStructImportsInProduction(t *testing.T) {
xRoot := repoXRoot(t)
fset := token.NewFileSet()
violations := []string{}
err := filepath.Walk(xRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".go") {
return nil
}
// Skip test files (G-003 is about production code only).
if strings.HasSuffix(path, "_test.go") {
return nil
}
// Parse imports only (no type checking needed).
f, perr := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
if perr != nil {
return perr
}
// Derive this file's own module to allow same-package imports.
ownTypesPkg := ownTypesImport(path)
for _, imp := range f.Imports {
ip := strings.Trim(imp.Path.Value, `"`)
// Allow a file to import its OWN types package (rare; e.g. an
// alias file). Block imports of OTHER x/<module>/types packages.
if isForeignTypesImport(ip) && ip != ownTypesPkg {
rel, _ := filepath.Rel(xRoot, path)
violations = append(violations, rel+" -> "+ip)
}
}
return nil
})
if err != nil {
t.Fatalf("walk: %v", err)
}
if len(violations) > 0 {
t.Errorf("G-003 violation: production files importing foreign x/<module>/types:\n %s",
strings.Join(violations, "\n "))
}
}
// isForeignTypesImport reports whether ip is an x/<module>/types import
// (the form that would create a cross-module struct dependency). It returns
// true only for imports matching github.com/oy/openyield/x/<anything>/types.
func isForeignTypesImport(ip string) bool {
const prefix = "github.com/oy/openyield/x/"
if !strings.HasPrefix(ip, prefix) {
return false
}
rest := strings.TrimPrefix(ip, prefix)
// x/<module>/types has exactly one "/" after the prefix and ends in /types.
// x/<module>/types/foo would be a sub-package (also blocked).
parts := strings.Split(rest, "/")
if len(parts) < 2 {
return false
}
return parts[len(parts)-1] == "types"
}
// ownTypesImport returns the x/<module>/types import path a file at the
// given path belongs to, or "" if the file is not under a types package.
func ownTypesImport(path string) string {
dir := filepath.Dir(path)
if filepath.Base(dir) != "types" {
return ""
}
module := filepath.Base(filepath.Dir(dir))
return "github.com/oy/openyield/x/" + module + "/types"
}
// packageDir resolves a Go import path to its filesystem directory by
// walking up from this test file. The v0.2 skeleton has zero external deps,
// so we use runtime.Caller rather than go/build (which would need GOPATH
// setup); the test file's own location anchors the resolution.
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/window/types/types_test.go
// repoRoot = .../oy (4 dirs up: types -> window -> x -> oy)
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
// repoXRoot returns the absolute path to the repo's x/ directory.
func repoXRoot(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
// file = .../oy/x/window/types/types_test.go -> x/ is 3 dirs up from file
return filepath.Dir(filepath.Dir(filepath.Dir(file)))
}