Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f7391774 | |||
| 0fefd88668 | |||
| 93a8a3b311 | |||
| 3e762f648d | |||
| 289c499a6d | |||
| bc15516eea | |||
| 41344eb78b | |||
| 42641f8483 | |||
| 1beff09cff | |||
| db61fb1f6c | |||
| 82245f98f7 | |||
| e6a7634262 | |||
| 11b1585913 | |||
| b6f041b5af | |||
| c0bd9eedf5 | |||
| 8132f6ecd4 | |||
| 414dda8b3e | |||
| 85af56e23e | |||
| dfbaef6bc4 | |||
| 6b23409da2 | |||
| 141119f9ca |
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"phase": 2,
|
||||
"phase": 3,
|
||||
"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:40:00Z"
|
||||
}
|
||||
+29
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+96
-42
@@ -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
@@ -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
@@ -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.1 — OpenYield Foundation Init (pre-MVP development milestone; remains v0.1 until MVP ships as v0.1.0)
|
||||
v0.2 — The 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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "bearers"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// BearerType defines the transport bearers (§14)
|
||||
type BearerType string
|
||||
|
||||
const (
|
||||
BearerInternet BearerType = "Internet" // Global, carrier-dependent
|
||||
BearerOYLR BearerType = "OY-LR" // LoRa, 2-10km, surveillance-resistant
|
||||
BearerOYBLE BearerType = "OY-BLE" // Bluetooth, 10-100m
|
||||
BearerOYWiFiDirect BearerType = "OY-WiFi-Direct" // 50-200m
|
||||
BearerOYSAT BearerType = "OY-SAT" // Satellite, global
|
||||
BearerOYQR BearerType = "OY-QR" // Paper, 0 range
|
||||
)
|
||||
|
||||
// BearerInfo describes a bearer's properties (§14)
|
||||
type BearerInfo struct {
|
||||
Type BearerType `json:"type" yaml:"type"`
|
||||
RangeMeters int32 `json:"range_meters" yaml:"range_meters"`
|
||||
CarrierDependent bool `json:"carrier_dependent" yaml:"carrier_dependent"`
|
||||
SurveillanceResistant bool `json:"surveillance_resistant" yaml:"surveillance_resistant"`
|
||||
}
|
||||
|
||||
// AllBearers returns all bearer types with their properties (§14)
|
||||
func AllBearers() []BearerInfo {
|
||||
return []BearerInfo{
|
||||
{BearerInternet, 0, true, false},
|
||||
{BearerOYLR, 10000, false, true},
|
||||
{BearerOYBLE, 100, false, true},
|
||||
{BearerOYWiFiDirect, 200, false, true},
|
||||
{BearerOYSAT, 0, false, true},
|
||||
{BearerOYQR, 0, false, true},
|
||||
}
|
||||
}
|
||||
|
||||
// UnifiedBearerLayer implements first-to-deliver-wins (§14)
|
||||
// RFC 5050 Bundle Protocol principles, delay-tolerant networking
|
||||
type UnifiedBearerLayer struct {
|
||||
ActiveBearers []BearerType `json:"active_bearers" yaml:"active_bearers"`
|
||||
FirstToDeliver bool `json:"first_to_deliver" yaml:"first_to_deliver"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,58 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
btypes "github.com/oy/openyield/x/bearers/types"
|
||||
ptypes "github.com/oy/openyield/x/processing/types"
|
||||
)
|
||||
|
||||
func TestBearerCount(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
if len(bearers) != 6 {
|
||||
t.Errorf("Expected 6 bearers (§14), got %d", len(bearers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSurveillanceResistantBearers(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
for _, b := range bearers {
|
||||
if b.Type == btypes.BearerInternet && b.SurveillanceResistant {
|
||||
t.Error("Internet bearer should NOT be surveillance-resistant (§14)")
|
||||
}
|
||||
if b.Type != btypes.BearerInternet && !b.SurveillanceResistant {
|
||||
t.Errorf("Bearer %s should be surveillance-resistant (§14)", b.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessingModeFCFS(t *testing.T) {
|
||||
if ptypes.ModeFCFS != "FCFS" {
|
||||
t.Error("Processing mode should be FCFS (§15 LOCKED)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightClientSize(t *testing.T) {
|
||||
if ptypes.LightClientSizeMB != 30 {
|
||||
t.Errorf("LightClientSize = %d, expected 30 MB (§15)", ptypes.LightClientSizeMB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessorSelectionByProximity(t *testing.T) {
|
||||
processors := []ptypes.Processor{
|
||||
{ProcessorID: "far", Latitude: 40.0, Longitude: 40.0},
|
||||
{ProcessorID: "close", Latitude: 10.0, Longitude: 10.0},
|
||||
{ProcessorID: "mid", Latitude: 20.0, Longitude: 20.0},
|
||||
}
|
||||
selected := ptypes.SelectProcessorByProximity(10.1, 10.1, processors)
|
||||
if selected == nil || selected.ProcessorID != "close" {
|
||||
t.Error("Should select closest processor (§15: geographic proximity wins)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyProcessorSelection(t *testing.T) {
|
||||
selected := ptypes.SelectProcessorByProximity(10.0, 10.0, []ptypes.Processor{})
|
||||
if selected != nil {
|
||||
t.Error("Empty processor list should return nil")
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "identity"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Reach is the mesh-native identity layer (§8, §9.6 Layer 1)
|
||||
// Cryptographic, virtual, no state identity
|
||||
type Reach struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
PublicKey string `json:"public_key" yaml:"public_key"`
|
||||
IsNomad bool `json:"is_nomad" yaml:"is_nomad"`
|
||||
IsFreeholder bool `json:"is_freeholder" yaml:"is_freeholder"`
|
||||
}
|
||||
|
||||
// CitizenshipTier (§9)
|
||||
type CitizenshipTier string
|
||||
|
||||
const (
|
||||
TierNomad CitizenshipTier = "Nomad" // any Holder (lifestyle tier)
|
||||
TierFreeholder CitizenshipTier = "Freeholder" // earned full recognition (sovereign tier)
|
||||
)
|
||||
|
||||
// TwoLayerPassport (§9.6)
|
||||
// Layer 1: Reach (mesh-native, virtual, cryptographic)
|
||||
// Layer 2: Pier-routed real-world credentials (e-Residency, biometric IDs)
|
||||
// OY does not issue passports; OY routes to Piers that do.
|
||||
type TwoLayerPassport struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Layer1Reach bool `json:"layer1_reach" yaml:"layer1_reach"`
|
||||
Layer2PierRouted bool `json:"layer2_pier_routed" yaml:"layer2_pier_routed"`
|
||||
PierCredentialRef string `json:"pier_credential_ref" yaml:"pier_credential_ref"`
|
||||
}
|
||||
|
||||
// WayfarersRecord is an optional Reach-signed attestation (§9.5)
|
||||
// Credential of belonging, not state identity
|
||||
type WayfarersRecord struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
FreeholderStatus bool `json:"freeholder_status" yaml:"freeholder_status"`
|
||||
StandingScore float64 `json:"standing_score" yaml:"standing_score"`
|
||||
GuildAffiliations []string `json:"guild_affiliations" yaml:"guild_affiliations"`
|
||||
BloomHistory string `json:"bloom_history" yaml:"bloom_history"`
|
||||
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Reaches []Reach `json:"reaches" yaml:"reaches"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Reaches: []Reach{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -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 }
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "processing"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// FCFS processing — NOT fee-auctioned (§15 LOCKED)
|
||||
// Geographic proximity wins (closest processor to recipient)
|
||||
// Anyone with a phone can process via light client (~30 MB app)
|
||||
|
||||
LightClientSizeMB = 30 // ~30 MB app
|
||||
BatteryPerDayActivePct = 3 // ~1-3pct battery/day active
|
||||
)
|
||||
|
||||
// ProcessingMode is FCFS (§15) — never fee-auctioned
|
||||
type ProcessingMode string
|
||||
|
||||
const (
|
||||
ModeFCFS ProcessingMode = "FCFS" // First-come-first-served, not fee-auctioned
|
||||
)
|
||||
|
||||
// Processor is a light client processing Pass-Acts (§15)
|
||||
type Processor struct {
|
||||
ProcessorID string `json:"processor_id" yaml:"processor_id"`
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Latitude float64 `json:"latitude" yaml:"latitude"`
|
||||
Longitude float64 `json:"longitude" yaml:"longitude"`
|
||||
ActiveSince int64 `json:"active_since" yaml:"active_since"`
|
||||
BlocksProcessed uint64 `json:"blocks_processed" yaml:"blocks_processed"`
|
||||
}
|
||||
|
||||
// ProcessingJob is a Pass-Act awaiting processing
|
||||
type ProcessingJob struct {
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
SenderLat float64 `json:"sender_lat" yaml:"sender_lat"`
|
||||
SenderLng float64 `json:"sender_lng" yaml:"sender_lng"`
|
||||
RecipientLat float64 `json:"recipient_lat" yaml:"recipient_lat"`
|
||||
RecipientLng float64 `json:"recipient_lng" yaml:"recipient_lng"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// SelectProcessorByProximity — geographic proximity wins (§15)
|
||||
// In FCFS mode, the closest processor to the recipient processes the job
|
||||
func SelectProcessorByProximity(recipientLat, recipientLng float64, processors []Processor) *Processor {
|
||||
if len(processors) == 0 {
|
||||
return nil
|
||||
}
|
||||
closest := &processors[0]
|
||||
minDist := haversine(recipientLat, recipientLng, closest.Latitude, closest.Longitude)
|
||||
for i := 1; i < len(processors); i++ {
|
||||
dist := haversine(recipientLat, recipientLng, processors[i].Latitude, processors[i].Longitude)
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
closest = &processors[i]
|
||||
}
|
||||
}
|
||||
return closest
|
||||
}
|
||||
|
||||
// haversine computes distance between two coordinates (simplified)
|
||||
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
dlat := lat2 - lat1
|
||||
dlng := lng2 - lng1
|
||||
return dlat*dlat + dlng*dlng // squared distance (sufficient for comparison)
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Processors []Processor `json:"processors" yaml:"processors"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Processors: []Processor{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "standing"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Standing formula parameters (§9.2 — LOCKED)
|
||||
PriorMean = 4.0 // Bayesian prior mean
|
||||
PriorWeight = 10 // Bayesian prior weight
|
||||
DecayBucket6mo = 1.0 // 6mo: 100pct
|
||||
DecayBucket12mo = 0.5 // 12mo: 50pct
|
||||
DecayBucket24mo = 0.25 // 24mo: 25pct
|
||||
DecayBucket24plus = 0.0 // 24mo+: 0pct
|
||||
|
||||
// Diversity bonus (§9.2)
|
||||
DiversityBonus3Cats = 0.05
|
||||
DiversityBonus4Cats = 0.10
|
||||
DiversityBonus5Cats = 0.15
|
||||
|
||||
// Voucher weights (§9.2)
|
||||
VoucherWeightFreeholder = 1.5
|
||||
VoucherWeight45Plus = 1.2
|
||||
VoucherWeight40To45 = 1.0
|
||||
VoucherWeightBelow40 = 0.5
|
||||
VoucherWeightBelow10Ratings = 0.3
|
||||
|
||||
// Minimum counterparties (§9.2)
|
||||
MinCounterpartiesValid = 3 // to be valid
|
||||
MinCounterpartiesFreeholderEligible = 10 // to be Freeholder-eligible
|
||||
MinCounterpartiesFreeholderStatus = 30 // across 3 categories for Freeholder status
|
||||
|
||||
// Freeholder Four Signals (§9.1 — LOCKED)
|
||||
FreeholderStashMaturityDays = 90 // 90 days Stash activity
|
||||
FreeholderStashMaxGapDays = 30 // no gap > 30 days
|
||||
FreeholderMinStandingScore = 4.5 // 4.5+ in at least 3 service categories
|
||||
FreeholderMinCategories = 3 // at least 3 service categories
|
||||
)
|
||||
|
||||
// Rating is a single rating event (§9.2)
|
||||
type Rating struct {
|
||||
RaterID string `json:"rater_id" yaml:"rater_id"`
|
||||
RateeID string `json:"ratee_id" yaml:"ratee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
Score float64 `json:"score" yaml:"score"` // 0.0-5.0
|
||||
Weight float64 `json:"weight" yaml:"weight"` // derived from rater Voucher_Weight
|
||||
TxRef string `json:"tx_ref" yaml:"tx_ref"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
DecayBucket uint8 `json:"decay_bucket" yaml:"decay_bucket"`
|
||||
}
|
||||
|
||||
// Vouch is a Freeholder vouch with skin-in-the-game (§9.1)
|
||||
type Vouch struct {
|
||||
VoucherID string `json:"voucher_id" yaml:"voucher_id"`
|
||||
VoucheeID string `json:"vouchee_id" yaml:"vouchee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` // voucher skin-in-the-game
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// Slash penalizes a Holder (§9.4)
|
||||
type Slash struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Amount float64 `json:"amount" yaml:"amount"`
|
||||
Reason string `json:"reason" yaml:"reason"` // Crack, FraudulentCoverCall, InactivityTimeout
|
||||
Attester string `json:"attester" yaml:"attester"` // Watcher ID
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// StandingBucket for display (§9.2)
|
||||
type StandingBucket string
|
||||
|
||||
const (
|
||||
BucketNew StandingBucket = "New" // <10 ratings
|
||||
BucketTrusted StandingBucket = "Trusted" // 4.0-4.4
|
||||
BucketPreferred StandingBucket = "Preferred" // 4.5-4.7
|
||||
BucketTop StandingBucket = "Top" // 4.8+
|
||||
BucketSlashed StandingBucket = "Slashed" // penalized
|
||||
)
|
||||
|
||||
// FreeholderSignals — all four must be present (§9.1 LOCKED)
|
||||
type FreeholderSignals struct {
|
||||
StashMaturity bool `json:"stash_maturity" yaml:"stash_maturity"`
|
||||
MultiDomainStanding bool `json:"multi_domain_standing" yaml:"multi_domain_standing"`
|
||||
CommittedCapital bool `json:"committed_capital" yaml:"committed_capital"`
|
||||
CommunityEndorsement bool `json:"community_endorsement" yaml:"community_endorsement"`
|
||||
}
|
||||
|
||||
// IsFreeholderEligible checks all four signals (§9.1)
|
||||
// No application, no committee, no form. All four must be present.
|
||||
func (s FreeholderSignals) IsFreeholderEligible() bool {
|
||||
return s.StashMaturity && s.MultiDomainStanding && s.CommittedCapital && s.CommunityEndorsement
|
||||
}
|
||||
|
||||
// ComputeDiversityBonus returns the bonus for category count (§9.2)
|
||||
func ComputeDiversityBonus(categoryCount int) float64 {
|
||||
switch {
|
||||
case categoryCount >= 5:
|
||||
return DiversityBonus5Cats
|
||||
case categoryCount == 4:
|
||||
return DiversityBonus4Cats
|
||||
case categoryCount == 3:
|
||||
return DiversityBonus3Cats
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// GetVoucherWeight returns the weight for a given rater profile (§9.2)
|
||||
func GetVoucherWeight(isFreeholder bool, standingScore float64, ratingCount int) float64 {
|
||||
if isFreeholder {
|
||||
return VoucherWeightFreeholder
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return VoucherWeightBelow10Ratings
|
||||
}
|
||||
if standingScore >= 4.5 {
|
||||
return VoucherWeight45Plus
|
||||
}
|
||||
if standingScore >= 4.0 {
|
||||
return VoucherWeight40To45
|
||||
}
|
||||
return VoucherWeightBelow40
|
||||
}
|
||||
|
||||
// GetStandingBucket returns the display bucket for a score (§9.2)
|
||||
func GetStandingBucket(score float64, ratingCount int, isSlashed bool) StandingBucket {
|
||||
if isSlashed {
|
||||
return BucketSlashed
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return BucketNew
|
||||
}
|
||||
switch {
|
||||
case score >= 4.8:
|
||||
return BucketTop
|
||||
case score >= 4.5:
|
||||
return BucketPreferred
|
||||
case score >= 4.0:
|
||||
return BucketTrusted
|
||||
}
|
||||
return BucketNew
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Ratings []Rating `json:"ratings" yaml:"ratings"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Ratings: []Rating{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,100 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/standing/types"
|
||||
)
|
||||
|
||||
func TestFreeholderSignalsAllPresent(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: true,
|
||||
}
|
||||
if !signals.IsFreeholderEligible() {
|
||||
t.Error("All four signals present should be Freeholder eligible (§9.1)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreeholderSignalsMissing(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: false, // missing
|
||||
}
|
||||
if signals.IsFreeholderEligible() {
|
||||
t.Error("Missing one signal should NOT be Freeholder eligible (§9.1: all four must be present)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiversityBonus(t *testing.T) {
|
||||
if types.ComputeDiversityBonus(3) != 0.05 {
|
||||
t.Error("3 categories should give +0.05 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(4) != 0.10 {
|
||||
t.Error("4 categories should give +0.10 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(5) != 0.15 {
|
||||
t.Error("5+ categories should give +0.15 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(2) != 0.0 {
|
||||
t.Error("2 categories should give 0 bonus (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoucherWeights(t *testing.T) {
|
||||
if types.GetVoucherWeight(true, 4.0, 100) != 1.5 {
|
||||
t.Error("Freeholder weight should be 1.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.6, 100) != 1.2 {
|
||||
t.Error("4.5+ with 1-2 cats should be 1.2x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.2, 100) != 1.0 {
|
||||
t.Error("4.0-4.5 should be 1.0x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 3.5, 100) != 0.5 {
|
||||
t.Error("Below 4.0 should be 0.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.0, 5) != 0.3 {
|
||||
t.Error("Below 10 ratings should be 0.3x (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandingBuckets(t *testing.T) {
|
||||
if types.GetStandingBucket(4.9, 100, false) != types.BucketTop {
|
||||
t.Error("4.8+ should be Top (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.6, 100, false) != types.BucketPreferred {
|
||||
t.Error("4.5-4.7 should be Preferred (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.2, 100, false) != types.BucketTrusted {
|
||||
t.Error("4.0-4.4 should be Trusted (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(3.5, 100, false) != types.BucketNew {
|
||||
t.Error("Below 4.0 should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 5, false) != types.BucketNew {
|
||||
t.Error("Below 10 ratings should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 100, true) != types.BucketSlashed {
|
||||
t.Error("Slashed should be Slashed bucket (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockedConstants(t *testing.T) {
|
||||
if types.FreeholderStashMaturityDays != 90 {
|
||||
t.Error("Stash maturity should be 90 days (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinStandingScore != 4.5 {
|
||||
t.Error("Min standing score should be 4.5 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinCategories != 3 {
|
||||
t.Error("Min categories should be 3 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.MinCounterpartiesFreeholderStatus != 30 {
|
||||
t.Error("Min counterparties for Freeholder status should be 30 (§9.2)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
Reference in New Issue
Block a user