Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5299b8dc64 | |||
| fdf5bd71ff | |||
| a70d6faa59 | |||
| 3c52aa1bb2 | |||
| be4c023340 | |||
| 29c5947fa5 | |||
| 68053238bd | |||
| c97e18fc1f | |||
| d42c624245 | |||
| 155a618d91 |
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"phase": 1,
|
||||
"stage": "complete",
|
||||
"milestone": "v0.6",
|
||||
"milestone": "v0.5",
|
||||
"milestone_type": "feature",
|
||||
"tag_base": "v0.5.x",
|
||||
"tag_base": "v0.4.x",
|
||||
"phase_role": "execution",
|
||||
"project": "oy",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-18T13:55:00Z",
|
||||
"milestone_complete": false,
|
||||
"requirements_covered": ["REQ-040", "REQ-045"]
|
||||
"updated_at": "2026-08-18T01:00:00Z",
|
||||
"phase_release_tag": "v0.4.1",
|
||||
"release_id": 754,
|
||||
"requirements_covered": ["REQ-033"]
|
||||
}
|
||||
@@ -6,9 +6,9 @@
|
||||
}
|
||||
],
|
||||
"active_project": "oy",
|
||||
"milestone": "v0.6",
|
||||
"milestone": "v0.5",
|
||||
"milestone_type": "feature",
|
||||
"tag_base": "v0.5.x",
|
||||
"tag_base": "v0.4.x",
|
||||
"autonomy": {
|
||||
"level": "full",
|
||||
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
|
||||
|
||||
@@ -513,346 +513,3 @@ The v0.4 (Refinement — NFR) milestone is **shippable**.
|
||||
| Elevation of Privilege | No privilege surface added; the lexicon helper is a pure function; the regression guard only asserts existing consts | Low | Accept |
|
||||
|
||||
No threat exceeds the low/accept threshold. No escalations. v0.4 hardens the mission-locked const firewall (REQ-030) and the lexicon firewall (REQ-029) without introducing any new attack surface.
|
||||
|
||||
---
|
||||
|
||||
# AUDIT: OpenYield (oy) — v0.5 (Bearers Runtime) Final Phase
|
||||
|
||||
> **Auditor**: CIAgent security auditor (ci-auditor, read-only on source; critical-fix mode for source + AUDIT.md only — ROADMAP/REQUIREMENTS/PROJECT/RESEARCH/ARCHITECTURE/PERSONAS/GRILL/PLANS/REVIEW are out-of-surface per run constraints)
|
||||
> **Date**: 2026-08-18
|
||||
> **Scope**: v0.5 milestone state on `milestone/v0.5-bearers-runtime` (HEAD = `phase/08-final-review-ship` @ `5d9ac3c`)
|
||||
> **Milestone**: v0.5 — Bearers Runtime (feature type; tag_base `v0.4.x`)
|
||||
> **Mode**: multi-project (slug `oy`; config `projects[]` length 1, `active_project: oy`)
|
||||
> **Autonomy**: full
|
||||
|
||||
---
|
||||
|
||||
## 1. Reconstruction Test (git log ↔ `.ciagent/` files) — **PASS** (with one discipline deviation, see §8)
|
||||
|
||||
### 1.1 Phase progression
|
||||
|
||||
`git log v0.3.4..HEAD --oneline` returns 11 v0.5-scope commits (P0..P7 phase-ship + 2 checkpoint + P8 verify), in order:
|
||||
|
||||
| Phase | Commit | Tag | Subject | `status` | `requirements.covered` | Verdict |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P0 | 155a618 | v0.4.0 | `Merge phase/00 into milestone/v0.5-bearers-runtime (P0 complete → v0.4.0)` | complete | [] | PASS |
|
||||
| P1 | c97e18f | v0.4.1 | `Merge phase/01 into milestone/v0.5-bearers-runtime (P1 complete → v0.4.1)` | complete | [REQ-033] | PASS |
|
||||
| — | 6805323 | — | `checkpoint(p1): v0.5 phase 1 complete → v0.4.1` | complete | (checkpoint) | PASS |
|
||||
| P2 | 29c5947 | v0.4.2 | `Merge phase/02 ...` | complete | [REQ-034] | PASS |
|
||||
| P3 | be4c023 | v0.4.3 | `Merge phase/03 ...` | complete | [REQ-035] | PASS |
|
||||
| P4 | 3c52aa1 | v0.4.4 | `Merge phase/04 ...` | complete | [REQ-036] | PASS |
|
||||
| P5 | a70d6fa | v0.4.5 | `Merge phase/05 ...` | complete | [REQ-037] | PASS |
|
||||
| P6 | fdf5bd7 | v0.4.6 | `Merge phase/06 ...` | complete | [REQ-038] | PASS |
|
||||
| P7 | 5299b8d | v0.4.7 | `Merge phase/07 ...` | complete | [REQ-039] | PASS |
|
||||
| P8 | 5d9ac3c | (v0.4.8 pending) | `verify(P8): v0.5 final code review ...` | verify | [REQ-033..039] | PASS |
|
||||
|
||||
**Each phase commit carries a well-formed `---ci---` block** (verified by extracting all 10 blocks):
|
||||
- `project: oy` present in every block ✓ (multi-project discipline observed)
|
||||
- `milestone: v0.5` present in every block ✓
|
||||
- `phase: N` integer correct (0..8) ✓
|
||||
- `status: complete` on P0..P7 ship commits; `status: verify` on the P8 review commit ✓
|
||||
- `requirements.covered` matches the expected REQ-per-phase map exactly (P1→REQ-033, P2→REQ-034, P3→REQ-035, P4→REQ-036, P5→REQ-037, P6→REQ-038, P7→REQ-039; P0 none; P8 all seven) ✓
|
||||
|
||||
**Tags v0.4.0..v0.4.7 exist and map to the correct phase-ship commits** (verified by `git tag -l | grep v0.4` + `git ls-remote --tags origin | grep v0.4`):
|
||||
```
|
||||
v0.4.0 -> 155a618 (P0) ✓
|
||||
v0.4.1 -> c97e18f (P1) ✓
|
||||
v0.4.2 -> 29c5947 (P2) ✓
|
||||
v0.4.3 -> be4c023 (P3) ✓
|
||||
v0.4.4 -> 3c52aa1 (P4) ✓
|
||||
v0.4.5 -> a70d6fa (P5) ✓
|
||||
v0.4.6 -> fdf5bd7 (P6) ✓
|
||||
v0.4.7 -> 5299b8d (P7) ✓
|
||||
v0.4.8 -> ABSENT (correct — final phase P8 creates it at ship)
|
||||
```
|
||||
All 8 tags pushed to `origin` (verified by `git ls-remote --tags origin`). The milestone release tag `v0.4.8` is NOT yet present — correctly deferred to the P8 ship step (P8-03-02).
|
||||
|
||||
**REQ coverage vs. expected (P1: REQ-033; P2: REQ-034; P3: REQ-035; P4: REQ-036; P5: REQ-037; P6: REQ-038; P7: REQ-039): exact match on all 7 execution phases.** REQ-033..REQ-039 (the v0.5 requirement set per REQUIREMENTS.md §"v0.5 Milestone Requirements") are all covered; no REQ is double-counted, no REQ is missing.
|
||||
|
||||
**CHECKPOINT.json** reflects: `phase: 1`, `stage: complete`, `milestone: v0.5`, `tag_base: v0.4.x`, `milestone_type: feature`, `phase_role: execution`, `project: oy`, `phase_release_tag: v0.4.1`, `release_id: 754`, `requirements_covered: [REQ-033]`. Valid JSON. NOTE: `phase: 1` reflects the last checkpoint-written execution phase (the checkpoint was last advanced at the P1 ship); P2..P7 did not write intermediate checkpoints (they used the `Merge phase/NN` squash pattern instead of `checkpoint(pN)` advance commits, except P0 and P1). This is a minor checkpoint-cadence deviation (P2..P7 checkpoint writes skipped), not a reconstruction blocker — the phase-ship `---ci---` blocks carry the authoritative phase/status/REQ state. The P8 ship step will advance the checkpoint to `phase: 8, stage: ship` (per P8-03-02).
|
||||
|
||||
**Reconstruction test verdict: PASS.** The git log + `---ci---` blocks + tags fully reconstruct the v0.5 phase progression, REQ coverage, and milestone state. A reader can reconstruct the entire v0.5 milestone from commit messages alone.
|
||||
|
||||
### 1.2 Commit structure observation (not a reconstruction failure)
|
||||
|
||||
The 8 `Merge phase/NN into milestone/v0.5-bearers-runtime ...` commits are **single-parent commits**, not true 2-parent merges (`git cat-file -p` shows one `parent` line each; `c97e18f^2` is undefined). The phase work was committed directly onto the milestone branch as squash commits labeled "Merge phase/NN". This diverges from the plan's documented branch model (PLANS.md says phases ship on separate `phase/NN-*` branches then merge), but the `---ci---` blocks, tags, and REQ coverage are all correct and reconstructable. See §4 (commit discipline) and §8 (critical issues) for the feature-purity-gate consequence.
|
||||
|
||||
---
|
||||
|
||||
## 2. `.ciagent` File Discipline — **PASS** (with one discipline deviation: ROADMAP v0.5 section absent — see §8)
|
||||
|
||||
**All 9 canonical files present in `.ciagent/oy/`:**
|
||||
|
||||
```
|
||||
ARCHITECTURE.md ✓ (v0.5 Runtime Architecture section appended)
|
||||
AUDIT.md ✓ (this section appended — v0.2/v0.3/v0.4 preserved)
|
||||
GRILL.md ✓ (v0.5 grill G-017..G-024 appended)
|
||||
PERSONAS.md ✓ (v0.5 roster appended)
|
||||
PLANS.md ✓ (v0.5 plan appended — 8 phases, 36 tasks)
|
||||
PROJECT.md ✓ (v0.5 scope/decisions D-054..D-065 appended)
|
||||
REQUIREMENTS.md ✓ (v0.5 table REQ-033..REQ-039 appended)
|
||||
RESEARCH.md ✓ (v0.5 research A-501..A-574 appended)
|
||||
REVIEW.md ✓ (v0.5 review appended — PASS, 5 P1+ flagged)
|
||||
ROADMAP.md ✗ (NO v0.5 milestone section — see §8 Critical-2)
|
||||
```
|
||||
|
||||
Plus historical artifacts: `P1_SHIP_VERIFICATION.md`..`P4_SHIP_VERIFICATION.md` (v0.2 phase-ship records; referenced by the v0.2 AUDIT.md; not orphan).
|
||||
|
||||
**config.json — valid JSON, all required settings correct:**
|
||||
|
||||
| Setting | Required | Actual | Verdict |
|
||||
|---|---|---|---|
|
||||
| `milestone_type` | `feature` | `feature` ✓ | PASS |
|
||||
| `tag_base` | `v0.4.x` | `v0.4.x` ✓ | PASS |
|
||||
| `ship.per_phase` | `true` | `true` ✓ | PASS |
|
||||
| `ship.allow_skip` | `false` | `false` ✓ | PASS |
|
||||
| `active_project` | `oy` | `oy` ✓ | PASS |
|
||||
| `projects[]` length | >0 (multi-project) | 1 (`oy`) ✓ | PASS |
|
||||
| `milestone` | `v0.5` | `v0.5` ✓ | PASS |
|
||||
|
||||
**Per-file v0.5 section presence:**
|
||||
- PLANS.md: v0.5 plan present (`# Plans: OpenYield (oy) — v0.5 (Bearers Runtime)` at line 1050; 8 phases P1..P7 + P8; task-count summary 36 tasks across 8 phases) ✓
|
||||
- GRILL.md: v0.5 grill present (G-017..G-024; 5 decision ratifications D-055, D-062, D-063, D-064, D-065; 8 binding fixes) ✓
|
||||
- REVIEW.md: v0.5 review present (PASS-WITH-FIXES → SHIP; 8 GRILL fixes landed; 0 P0; 5 P1+/P2 flagged) ✓
|
||||
- REQUIREMENTS.md: v0.5 table present (REQ-033..REQ-039, all class `feat`, status `pending` — correct per the brief: "will be marked Complete at milestone ship; for now they are the v0.5 requirements") ✓
|
||||
- ROADMAP.md: **NO v0.5 milestone section** (✗ — see §8 Critical-2). Prior milestones (v0.2, v0.3, v0.4) all had their sections added during P0 planning; v0.5 omitted this. The P8-03-01 ship task is responsible for adding it at ship, but the in-progress status should have been present during the milestone.
|
||||
|
||||
**No stale `.ciagent/` files** — all referenced files exist. No orphan files detected.
|
||||
|
||||
**File discipline verdict: PASS** (with one deviation documented in §8: ROADMAP.md v0.5 section absent — the ship step P8-03-01 must add it; the auditor is constrained from modifying ROADMAP.md per run constraints).
|
||||
|
||||
---
|
||||
|
||||
## 3. Branch Hygiene — **PASS**
|
||||
|
||||
| Check | Result | Verdict |
|
||||
|---|---|---|
|
||||
| `main` exists | at v0.4 milestone release (pre-v0.5) ✓ | PASS |
|
||||
| `main` is at v0.4 (pre-v0.5) | merge-base(main, milestone/v0.5) == main ✓ | PASS |
|
||||
| `milestone/v0.5-bearers-runtime` exists | local + `remotes/origin/milestone/v0.5-bearers-runtime` ✓ | PASS |
|
||||
| `milestone/v0.5-bearers-runtime` contains all P0-P7 work | 8 phase-ship commits P0-P7 + P8 verify ✓ | PASS |
|
||||
| `phase/08-final-review-ship` exists (current) | checked out, HEAD == `5d9ac3c` (verify(P8)) ✓ | PASS |
|
||||
| NO leftover execution phase branches | `git branch` lists only `main`, `milestone/v0.5-bearers-runtime`, `phase/08-final-review-ship` ✓ | PASS |
|
||||
|
||||
`git branch` returns exactly three local branches: `main`, `milestone/v0.5-bearers-runtime`, `phase/08-final-review-ship` (current). The execution phase branches `phase/01-exit-bridge-runtime`..`phase/07-council-governance-runtime` are NOT present locally — consistent with the single-parent squash model (§1.2): phase work was committed directly to the milestone branch, so there were no separate phase branches to delete. Only the final-phase branch `phase/08-final-review-ship` remains (the active phase). The P8 ship step (P8-03-02) will delete it post-merge.
|
||||
|
||||
**Branch hygiene verdict: PASS.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Commit Discipline — **PASS** (with one deviation: feature-purity-gate subject convention — see §8 Critical-1)
|
||||
|
||||
**`---ci---` block discipline:**
|
||||
- All 10 v0.5-scope commits (P0..P7 ship + P0/P1 checkpoint + P8 verify) carry `---ci---` blocks ✓
|
||||
- `project: oy` present in every block (multi-project discipline) ✓
|
||||
- `milestone: v0.5` present in every block ✓
|
||||
- `phase: N` correct integer (0..8) ✓
|
||||
- `status` field present and correct (`complete` on ships, `verify` on P8) ✓
|
||||
- `requirements.covered` present and correct on all phase-ship commits ✓
|
||||
- No malformed blocks, no missing closing `---/ci---` tags ✓
|
||||
|
||||
**Conventional commit subjects:**
|
||||
- P0..P7 ship commits: `Merge phase/NN into milestone/v0.5-bearers-runtime ...` (conventional-ish; uses the `Merge` prefix) ✓
|
||||
- Checkpoint commits: `checkpoint(p0): ...`, `checkpoint(p1): ...` (conventional) ✓
|
||||
- P8 verify commit: `verify(P8): v0.5 final code review ...` (conventional `verify` prefix) ✓
|
||||
- No `docs(PNN):` / `feat:` / `refactor:` / `chore:` subjects in the v0.5 range on the first-parent line (the phase work was squashed into the `Merge phase/NN` commits rather than committed as `feat(PNN): ...`)
|
||||
|
||||
**Feature purity gate (v0.5 is a FEATURE milestone — requires ≥1 `feat:` phase):**
|
||||
- **Substance gate: PASS.** v0.5 ships executable runtime behavior (8 modules promoted to live keeper MsgServer handlers + simtest; the cosmos-sdk v0.50.8 + ibc-go v8.2.1 dep D-055/D-062; CLOB matching D-057; CustodyKeyring D-058; Proposal/VoteOption enums D-060). This is unambiguously feature-class work, not NFR/refactor. The v0.3 `types/` contracts are NOT amended (runtime adds behavior on top — no breaking schema changes). The v0.5 P7 enums (`ProposalKindCount=4` / `ProposalStatusCount=5` / `VoteOptionCount=4`) are ADDITIVE (new types), not amendments to existing locked consts.
|
||||
- **Subject convention gate: DEVIATION.** `git log v0.3.4..HEAD --format="%s" | grep -E "^feat:"` returns ZERO matches. The phase work is committed under `Merge phase/NN` subjects (single-parent squash commits), not `feat(PNN): ...` subjects. The plan (PLANS.md Milestone Summary) says "all execution phases P1..P7 are `feat`" — the subject convention diverges from this. See §8 Critical-1 for the full analysis and disposition.
|
||||
- **No breaking schema changes: PASS.** Verified by reading the v0.3 `types/` files — the v0.5 runtime adds `keeper/`, `types/msg_*.go`, `types/expected_keepers.go`, `module.go` on top of the unchanged v0.3 `types/types.go` contracts. The locked-const firewall is intact (§6).
|
||||
|
||||
**G-003 production firewall intact across all new v0.5 code:**
|
||||
- `grep -rn "openyield/x/" x/*/types/*.go` (non-test, excluding `expected_keepers.go`) → ZERO cross-module struct imports (GREP_EXIT=1) ✓
|
||||
- The keeper files (`x/*/keeper/*.go`, `x/*/module.go`) import their OWN module's `types` package (intra-module, expected and correct — G-003 governs CROSS-module struct imports in `types/` packages, not a keeper importing its own module's types)
|
||||
- Cross-module keeper coupling is via `expected_keepers.go` interface shims (8 files, one per module: BridgeKeeper, HubKeeper, BreadKeeper, WatcherKeeper, StandKeeper, GuildKeeper, ComplianceKeeper, PartnerKeeper) — INTERFACES only, no struct imports ✓
|
||||
- The existing G-003 import-invariant test (`x/window/types/types_test.go:437` `TestG003NoCrossModuleStructImportsInProduction`) auto-covers the new v0.5 files and passes ✓
|
||||
|
||||
**Commit discipline verdict: PASS** (substance + `---ci---` blocks + conventional subjects all green; the `feat:` subject-convention deviation is documented in §8 Critical-1 as a non-blocking documentation defect — the substance is feature work; history is tagged/pushed and cannot be rewritten).
|
||||
|
||||
---
|
||||
|
||||
## 5. Test + Coverage Discipline — **PASS**
|
||||
|
||||
| Check | Command | Result | Verdict |
|
||||
|---|---|---|---|
|
||||
| Build | `go build ./...` | exit 0, GREEN | PASS |
|
||||
| Tests | `go test ./...` | exit 0, 34 packages `ok` (13 `[no test files]` — pre-existing v0.1 layout), zero FAIL | PASS |
|
||||
| Lexicon firewall (x/) | `go test -run TestLexiconMeta ./...` | GREEN (both firewalls: x/ + docs/) | PASS |
|
||||
| G-003 invariant | `go test -run TestG003NoCrossModuleStructImportsInProduction ./x/window/types/` | GREEN | PASS |
|
||||
| G-024 stdlib-only types tests | `grep -rln "cosmos-sdk\|sdk.Msg\|sdk.Context" x/*/types/*_test.go` | exit 1 (ZERO hits — invariant/lexicon tests remain stdlib-only) | PASS |
|
||||
|
||||
**Coverage on all 8 keeper packages (≥80% required, D-033; verified by `go test -cover`):**
|
||||
|
||||
| Package | Coverage | Verdict |
|
||||
|---|---|---|
|
||||
| `x/exit/keeper` | 85.0% | PASS |
|
||||
| `x/bridge/keeper` | 82.1% | PASS |
|
||||
| `x/bearers/keeper` | 91.2% | PASS |
|
||||
| `x/partner/keeper` | 87.6% | PASS |
|
||||
| `x/hub/keeper` | 90.0% | PASS |
|
||||
| `x/services/keeper` | 91.5% | PASS |
|
||||
| `x/bond/keeper` | 92.5% | PASS |
|
||||
| `x/council/keeper` | 90.3% | PASS |
|
||||
|
||||
All 8 keeper packages exceed the ≥80% target. Floor = 82.1% (`x/bridge/keeper`); ceiling = 92.5% (`x/bond/keeper`). D-033 satisfied with margin. The simtest files (`msg_server_simtest_test.go` in each keeper) exercise the MVP/UX flows: bridge IBC recv/ack/timeout, bearers OY-QR one-shot consume, partner anchor credential lifecycle, hub custody release + lending clamp, services lifecycle, bond CLOB match, council proposal/vote/tally.
|
||||
|
||||
**Test + coverage verdict: PASS.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Locked-Const Firewall — **PASS** (all v0.1..v0.5 consts verified unchanged in source)
|
||||
|
||||
Verified by direct `grep` of every const listed in the audit brief against the source files:
|
||||
|
||||
| Const | Expected | Source location | Actual | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| `ExitStatusCount` | 5 | `x/exit/types/types.go:18` | `= 5` ✓ | PASS |
|
||||
| `BridgeStatusCount` | 4 | `x/bridge/types/types.go:18` | `= 4` ✓ | PASS |
|
||||
| `BearerTypeCount` (via `AllBearers()`) | 6 | `x/bearers/types/types.go:36` | 6 bearers ✓ | PASS |
|
||||
| `OYSATLink.SurveillanceResistant` | true | `x/bearers/types/types.go:125` (`OYSATSurveillanceResistant = true` const; field set from const at line 134) | `= true` LOCKED ✓ | PASS |
|
||||
| `PartnerTierCount` | 4 | `x/partner/types/types.go:18` | `= 4` ✓ | PASS |
|
||||
| `AnchorCredentialStatusCount` (NEW v0.5, additive) | 4 | `x/partner/types/anchor_credential.go:62` | `= 4` ✓ | PASS |
|
||||
| `PartnerStatusCount` | 4 | `x/partner/types/types.go:57` | `= 4` ✓ | PASS |
|
||||
| `HubServiceCount` | 3 | `x/hub/types/types.go:42` | `= 3` ✓ | PASS |
|
||||
| `LendingCouponCapBps` | uint32(800) | `x/hub/types/types.go:51` | `= uint32(800)` ✓ | PASS |
|
||||
| `LendingCouponFloorBps` | uint32(0) | `x/hub/types/types.go:56` | `= uint32(0)` ✓ | PASS |
|
||||
| `ServiceKindCount` | 4 | `x/services/types/types.go:37` | `= 4` ✓ | PASS |
|
||||
| `CouponCapBps` | 800 | `x/bond/types/types.go:21` | `= 800` ✓ | PASS |
|
||||
| `CouponFloorBps` | 0 | `x/bond/types/types.go:26` | `= 0` ✓ | PASS |
|
||||
| `OrderSideCount` | 2 | `x/bond/types/types.go:171` | `= 2` ✓ | PASS |
|
||||
| `OrderStatusCount` | 3 | `x/bond/types/types.go:174` | `= 3` ✓ | PASS |
|
||||
| `CouncilKindCount` | 3 | `x/council/types/types.go:17` | `= 3` ✓ | PASS |
|
||||
| `SignalKindCount` | 4 | `x/council/types/types.go:30` | `= 4` ✓ | PASS |
|
||||
| `MissionLockAmendable` (council) | false | `x/council/types/types.go:25` | `= false` ✓ | PASS |
|
||||
| `MissionLockAmendable` (pact) | false | `x/pact/types/types.go:24` | `= false` ✓ | PASS |
|
||||
| `WatcherVetoQuorumDefault` (NEW v0.5, param-tunable NOT locked-const) | 6 | `x/council/types/types.go:60` | `= 6` (default; `Params.Validate` bounds [2,9] at lines 199-204 — G-020) ✓ | PASS |
|
||||
|
||||
**All v0.1..v0.4 locked-consts unchanged.** The v0.5 additions are ADDITIVE only:
|
||||
- `AnchorCredentialStatusCount = 4` (new in `x/partner/types/anchor_credential.go` — a new enum for the Anchor credential lifecycle; does not amend `PartnerTierCount` or `PartnerStatusCount`)
|
||||
- `WatcherVetoQuorumDefault = 6` (new in `x/council/types/types.go` — a DEFAULT for the `Params.WatcherVetoQuorum` field, NOT a locked const; G-020 bounds [2,9] enforced in `Params.Validate`; param-tunable per D-065)
|
||||
- P7 council governance enums (`ProposalKindCount` / `ProposalStatusCount` / `VoteOptionCount`) — new types added per D-060 (AUDIT §193 P1-1 closure); additive, no existing enum amended.
|
||||
|
||||
**Locked-const firewall verdict: PASS.** No v0.1..v0.4 locked-const was amended. The v0.5 additions are additive (feature purity gate substance: no breaking schema changes).
|
||||
|
||||
---
|
||||
|
||||
## 7. `go.mod` Discipline — **PASS** (G-006 controlled exception GRILL-ratified)
|
||||
|
||||
| Check | Expected | Actual | Verdict |
|
||||
|---|---|---|---|
|
||||
| `go` directive | 1.22 (G-018, not bumped) | `go 1.22` (go.mod line 3) ✓ | PASS |
|
||||
| cosmos-sdk pin | v0.50.8 (D-062) | `github.com/cosmos/cosmos-sdk v0.50.8` (direct require) ✓ | PASS |
|
||||
| ibc-go pin | v8.2.1 (D-062) | `github.com/cosmos/ibc-go/v8 v8.2.1` (direct require) ✓ | PASS |
|
||||
| G-006 controlled exception | GRILL-ratified (D-055/D-062) | D-055 ratifies the cosmos-sdk + ibc-go dep as the G-006 controlled exception; D-062 pins the versions; GRILL §1 "Decision Ratifications" confirms both ✓ | PASS |
|
||||
| G-018 hard build gate | `go build ./...` exits 0 under go 1.22 | `go build ./...` exit 0 ✓ | PASS |
|
||||
| Only deps added in v0.5 | cosmos-sdk + ibc-go (D-055) | The direct `require` block adds `cosmossdk.io/store v1.1.0`, `cosmos-sdk v0.50.8`, `ibc-go/modules/capability v1.0.0`, `ibc-go/v8 v8.2.1` — all part of the cosmos-sdk v0.50.x + ibc-go v8.x transitive tree (D-062 pin). No OTHER deps added outside this exception ✓ | PASS |
|
||||
|
||||
**`go.mod` discipline verdict: PASS.** The G-006 controlled exception (D-055/D-062) is the only dep addition in v0.5. The go directive remains 1.22 (G-018 hard gate green). The cosmos-sdk v0.50.8 + ibc-go v8.2.1 pins match D-062 exactly.
|
||||
|
||||
---
|
||||
|
||||
## 8. Critical Issues Found
|
||||
|
||||
**Initial critical issue count: 2.** Both are in surfaces the auditor is constrained from modifying (ROADMAP.md / REQUIREMENTS.md per run constraints; commit history per no-rewrite + no-tag constraints). Neither is a code/source defect — `go build ./...` + `go test ./...` are green, all locked-consts intact, G-003/G-018/G-024 firewalls green. Both are documentation/commit-hygiene defects flagged for the P8 ship step to address.
|
||||
|
||||
### Critical-1: Feature purity gate — zero `feat:` commit subjects in the v0.5 range
|
||||
|
||||
- **Spec**: PLANS.md v0.5 Milestone Summary — "Type: Feature (all execution phases P1..P7 are `feat`; P8 is `final`)". The feature purity gate requires ≥1 `feat:` commit subject.
|
||||
- **Pre-fix state**: `git log v0.3.4..HEAD --format="%s" | grep -E "^feat:"` returns ZERO matches. The 7 execution-phase work units (P1..P7) are committed as single-parent squash commits with subjects `Merge phase/NN into milestone/v0.5-bearers-runtime (PN complete → v0.4.N)`. None use the `feat:` conventional prefix.
|
||||
- **Impact**: A reader auditing commit subjects alone would NOT see the `feat:` signal that distinguishes a feature milestone from an NFR milestone. The v0.4 NFR purity gate used the subject-only check (`git log --format="%s" | grep -E "^feat:"` → zero = GREEN for NFR); applying the same check to v0.5 yields zero, which is GREEN for an NFR but RED for a feature. The substance IS feature work (8 modules promoted to runtime MsgServer handlers + simtest; cosmos-sdk dep; CLOB matching; CustodyKeyring; governance enums) — the subject convention diverges from the substance.
|
||||
- **Root cause**: The phase work was squashed directly onto the milestone branch as `Merge phase/NN` commits (single-parent, not true 2-parent merges — verified by `git cat-file -p`) rather than committed on separate `phase/NN-*` branches with `feat(PNN): ...` subjects then merged.
|
||||
- **Disposition**: **DOCUMENTED, NOT FIXED in this phase.** The history is tagged (v0.4.0..v0.4.7) and pushed to `origin`; rewriting it would require force-pushing tagged history, which violates the run constraints ("Do NOT tag, merge, or modify CHECKPOINT.json" + the git safety protocol forbids force-pushing tagged history). The auditor is read-only on commit history. **The substance of the feature purity gate is satisfied** (the work is unambiguously feature-class; no breaking schema changes; v0.5 P7 enums additive; locked-const firewall intact). The subject-convention deviation is a non-blocking documentation/commit-hygiene defect. **Recommendation for the orchestrator/ship step**: (a) the P8-03-02 ship commit (`docs(milestone): complete v0.5`) should reference the feature-class substance in its body; (b) future feature milestones should use `feat(PNN): ...` subjects on phase branches before merging, per the PLANS.md convention.
|
||||
|
||||
### Critical-2: ROADMAP.md has NO v0.5 milestone section
|
||||
|
||||
- **Spec**: PLANS.md P8-03-01 — "Update ROADMAP.md: mark v0.5 milestone COMPLETE; add the tag-line note that v0.5 shipped on the `v0.4.x` patch line". Prior milestones (v0.2, v0.3, v0.4) all had their ROADMAP sections added during P0 planning (in-progress status), then marked COMPLETE at ship.
|
||||
- **Pre-fix state**: `grep -n "v0.5\|Bearers Runtime" ROADMAP.md` returns ONE match (line 88: a forward-reference note "v0.3 Bearers skeletons are deferred to v0.5+"). There is NO `## Milestone v0.5 — Bearers Runtime` section. The ROADMAP jumps from `## Milestone v0.4 — Refinement (COMPLETE)` directly to `## Phase 3 — The Bearers (Year 3) — v0.3 PARTIAL SKELETON` (the vision narrative). A reader of ROADMAP.md cannot see that v0.5 is in progress or what it ships.
|
||||
- **Impact**: A reader cannot reconstruct the v0.5 milestone's existence, phase plan, or tag-line from ROADMAP.md alone (the reconstruction test in §1 relies on the git log + PLANS.md, not ROADMAP.md). This is a file-discipline defect — every prior milestone added its ROADMAP section during planning.
|
||||
- **Root cause**: The v0.5 P0 planning stage did not append a ROADMAP.md v0.5 section (unlike v0.2/v0.3/v0.4 P0 stages). The P8-03-01 ship task is responsible for adding it at ship, but the in-progress status should have been present during the milestone.
|
||||
- **Disposition**: **DOCUMENTED, NOT FIXED in this phase.** The run constraints explicitly forbid the auditor from modifying ROADMAP.md ("Do NOT touch PROJECT.md, REQUIREMENTS.md, ROADMAP.md, RESEARCH.md, ARCHITECTURE.md, PERSONAS.md, GRILL.md, PLANS.md, REVIEW.md — only AUDIT.md + source fixes"). **The P8-03-01 ship step MUST add the v0.5 ROADMAP section** (header `## Milestone v0.5 — Bearers Runtime (COMPLETE; feature type; tags v0.4.x)` with P0..P8 checkbox list + tag-line note + component-mapping table mirroring the v0.3/v0.4 section format) and mark it COMPLETE at ship.
|
||||
|
||||
**Post-fix verification**: N/A — neither critical issue is fixed in this phase (both are out-of-surface for the auditor). `go build ./...` + `go test ./...` re-confirmed GREEN after the AUDIT.md append (documentation-only change; no source touched). **Remaining critical issue count after this audit: 2** (both deferred to the P8 ship step, which owns ROADMAP.md/REQUIREMENTS.md updates and the ship commit).
|
||||
|
||||
---
|
||||
|
||||
## 9. P1+ Issues from REVIEW.md (acknowledged — 5 flagged for v0.6+)
|
||||
|
||||
The v0.5 REVIEW.md (§4) flagged 5 P1+/P2 issues for post-hoc review. None block the v0.5 ship (all are mainnet-readiness concerns for v0.6+, consistent with D-054 simtest grade). The audit acknowledges and endorses these flags:
|
||||
|
||||
| # | Severity | Issue | Disposition |
|
||||
|---|---|---|---|
|
||||
| 1 | P1 (security) | No proposal deposit/bond at v0.5 simtest grade — `MsgSubmitProposal` does not bond a deposit; a mainnet spammer could flood Pending proposals | Flag for v0.6+ (add deposit gate, standard `x/gov` pattern) — not a v0.5 blocker (simtest grade) |
|
||||
| 2 | P1 (adversarial) | CLOB per-tx matching is front-running-exposed at mainnet — no batch auction; tx-ordering advantage could sandwich | Flag for v0.6+ (evaluate batch auction / commit-reveal) — not a v0.5 blocker (simtest grade) |
|
||||
| 3 | P1 (maintainability) | Simtest does NOT exercise real IBC light-client verification — in-memory `sdk.Context` + stub keepers (G-022) | Flag for v0.6+ mainnet-readiness milestone — not a v0.5 blocker (D-054 simtest grade explicit) |
|
||||
| 4 | P2 (performance) | CLOB `restingBookForBond` is O(n) over all resting orders (loads `AllRestingOrders` then filters) | Flag for v0.6+ (prefix-key the book store by `BondID`) — not a v0.5 blocker (simtest depth) |
|
||||
| 5 | P2 (maintainability) | `emitMatchEventHook` package-level mutable var in `clob.go` — pragmatic split but a testability smell | Flag for v0.6+ (pass emitter as Keeper field / constructor injection) — not a v0.5 blocker (simtest is serial) |
|
||||
|
||||
All 5 are post-hoc, non-blocking, and consistent with the D-054 simtest-grade scope. The audit confirms REVIEW.md's verdict: **PASS-WITH-FIXES → SHIP** (all 8 GRILL fixes G-017..G-024 landed with evidence; 0 P0; 5 P1+/P2 flagged).
|
||||
|
||||
---
|
||||
|
||||
## 10. Overall Audit Verdict
|
||||
|
||||
### **PASS** (with 2 documentation/commit-hygiene issues deferred to the P8 ship step)
|
||||
|
||||
The v0.5 (Bearers Runtime) milestone is **shippable**. The 2 critical issues (§8) are both in surfaces the auditor cannot modify (ROADMAP.md per run constraints; tagged/pushed commit history per no-rewrite constraints) and both are the P8 ship step's responsibility (P8-03-01 adds the ROADMAP v0.5 section; P8-03-02 creates the `v0.4.8` ship commit). Neither is a code/source defect — the build is green, tests are green, all locked-consts are intact, all firewalls (G-003/G-018/G-024/lexicon) are green.
|
||||
|
||||
**Per-check summary:**
|
||||
|
||||
| # | Check | Verdict |
|
||||
|---|---|---|
|
||||
| 1 | Reconstruction test (git log ↔ .ciagent, tags, ---ci--- blocks, REQ coverage) | PASS |
|
||||
| 2 | .ciagent file discipline (9 canonical files + config.json; ROADMAP v0.5 section absent — §8 Critical-2) | PASS (with deviation) |
|
||||
| 3 | Branch hygiene (main, milestone, final-phase; no leftover phase branches) | PASS |
|
||||
| 4 | Commit discipline (---ci--- blocks + conventional subjects; feat: subject convention deviation — §8 Critical-1) | PASS (with deviation) |
|
||||
| 5 | Test + coverage discipline (build GREEN; 34 packages GREEN; 8 keepers 82.1%..92.5%) | PASS |
|
||||
| 6 | Locked-const firewall (all v0.1..v0.5 consts verified unchanged in source) | PASS |
|
||||
| 7 | go.mod discipline (go 1.22; cosmos-sdk v0.50.8 + ibc-go v8.2.1; G-006 exception GRILL-ratified) | PASS |
|
||||
|
||||
**Critical issues: 2 found → 0 fixed in this phase (both out-of-surface) → 2 deferred to P8 ship step.**
|
||||
- Critical-1 (feature purity gate `feat:` subject convention): DOCUMENTED — substance is feature-class; subject convention diverged; history tagged/pushed, cannot rewrite.
|
||||
- Critical-2 (ROADMAP.md v0.5 section absent): DOCUMENTED — P8-03-01 ship step must add it; auditor constrained from modifying ROADMAP.md.
|
||||
|
||||
**Non-critical: 5** (REVIEW.md P1+/P2 flags — all post-hoc, v0.6+, non-blocking).
|
||||
**Escalations: 0.**
|
||||
|
||||
**STRIDE security summary (per ci-auditor role, read-only):**
|
||||
|
||||
| Category | Finding | Severity | Disposition |
|
||||
|---|---|---|---|
|
||||
| Spoofing | No new auth surface added in v0.5 (runtime handlers use expected-keeper interface shims; no identity assertion logic); Anchor revocation authz via WatcherKeeper 6-of-9 quorum shim (REQ-004) | Low | Accept |
|
||||
| Tampering | Mission Lock const firewall intact (`MissionLockAmendable=false` in council + pact); D-064 adds `ValidateBasic` gate rejecting `MissionLockAmendment-Rejected` proposal kind (defence in depth — const + ValidateBasic + handler kind-switch); CLOB per-match REJECT above 800 bps (D-063 — fails closed, no usury violation); locked-const regression tests all green | Low (improved) | Accept |
|
||||
| Repudiation | All runtime handlers emit events after state mutation (state-machine ordering: ValidateBasic → keeper authz → state mutation → event emit); IBC in-flight records deleted on first ack (A-513 replay protection); OY-QR `consumed` flip is state-write-first (A-521) | Low | Accept |
|
||||
| Info Disclosure | OY-SAT surveillance-resistant invariant (`OYSATSurveillanceResistant=true` LOCKED); handler emits NO geolocation fields (negative test); no secrets in code; lexicon firewall green on all new runtime files | Low | Accept |
|
||||
| Denial of Service | No proposal deposit/bond at simtest grade (REVIEW P1-1 — flagged for v0.6+ mainnet); CLOB per-tx matching front-running-exposed (REVIEW P1-2 — flagged for v0.6+); simtest grade does not model mempool ordering (D-054) | Low (simtest grade; mainnet DoS surface is v0.6+) | Accept |
|
||||
| Elevation of Privilege | G-003 production firewall intact (zero cross-module struct imports; expected_keepers.go interfaces); WatcherVetoQuorum bounds [2,9] (G-020 — no single-Veto-block, no unsatisfiable quorum); MissionLockAmendment unproposable at ValidateBasic (D-064) | Low | Accept |
|
||||
|
||||
No threat exceeds the low/accept threshold. No escalations. The v0.5 runtime promotion introduces simtest-grade message handlers with no mainnet attack surface (D-054); all security-relevant invariants (Mission Lock, Bond Clamp, G-003 import firewall, surveillance-resistance, IBC replay/timeout) are compile-time consts + tested firewalls + simtest-verified handlers.
|
||||
|
||||
**Confidence in overall verdict: 0.88**
|
||||
|
||||
---
|
||||
|
||||
## Summary Block
|
||||
|
||||
```
|
||||
Per-check verdicts (v0.5 final):
|
||||
1. Reconstruction test — PASS (8 phase commits P0..P7 + P8 verify; ---ci--- blocks well-formed; tags v0.4.0..v0.4.7; v0.4.8 absent)
|
||||
2. .ciagent discipline — PASS (9 canonical files; config.json valid; ROADMAP v0.5 section absent — §8 Critical-2, deferred to ship)
|
||||
3. Branch hygiene — PASS (main + milestone + phase/08; no leftover phase branches; single-parent squash model)
|
||||
4. Commit discipline — PASS (all ---ci--- blocks well-formed; project: oy; feat: subject convention deviation — §8 Critical-1)
|
||||
5. Test + coverage — PASS (build GREEN; 34 pkgs GREEN; 8 keepers 82.1%..92.5%; lexicon + G-003 + G-024 green)
|
||||
6. Locked-const firewall — PASS (all v0.1..v0.5 consts verified unchanged in source; v0.5 additions additive)
|
||||
7. go.mod discipline — PASS (go 1.22; cosmos-sdk v0.50.8 + ibc-go v8.2.1; G-006 exception GRILL-ratified D-055/D-062)
|
||||
|
||||
Critical issues: 2 found → 0 fixed (out-of-surface) → 2 deferred to P8 ship step
|
||||
- Critical-1: feat: subject convention (feature purity gate substance PASS, subject deviation) → DOCUMENTED
|
||||
- Critical-2: ROADMAP.md v0.5 section absent (P8-03-01 ship step must add) → DOCUMENTED
|
||||
|
||||
Non-critical: 5 (REVIEW.md P1+/P2 — proposal deposit, CLOB front-running, simtest vs real IBC, CLOB O(n), emitMatchEventHook — all v0.6+, non-blocking)
|
||||
Escalations: 0
|
||||
Overall verdict: PASS (after P8 ship step addresses the 2 deferred documentation issues)
|
||||
Confidence: 0.88
|
||||
AUDIT.md appended: /root/oy/.ciagent/oy/AUDIT.md ✓ (v0.5 section appended; v0.2/v0.3/v0.4 content preserved)
|
||||
```
|
||||
|
||||
@@ -1093,441 +1093,3 @@ fixes:
|
||||
escalations: []
|
||||
---/ci---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v0.6 Grill (Nomad Web UI)
|
||||
|
||||
> **Reviewer**: CIAgent adversarial grill (red-team, full autonomy)
|
||||
> **Date**: 2026-08-18
|
||||
> **Target**: v0.6 Phase 0 artifacts (PROJECT.md D-066..D-073, REQUIREMENTS.md REQ-040..REQ-045 + IDEATE traceability, ROADMAP.md v0.6 phase table, PLANS.md v0.6 plan lines 1692-2152 incl. MVP/UX 3 sections, RESEARCH.md, PERSONAS.md frontend-engineer activation) + v0.1..v0.5 codebase baseline
|
||||
> **Milestone**: v0.6 — Nomad Web UI (feature type; tags run on the v0.5.x patch line)
|
||||
> **Autonomy**: full (decision_confidence_threshold = 0.60)
|
||||
> **Mode**: multi-project (slug `oy`)
|
||||
> **G-NNN sequence**: continues from G-024 (highest prior grill id). New fixes G-025..
|
||||
> **D-NNN sequence**: continues from D-073 (highest prior clarify id). New decisions D-078..
|
||||
|
||||
### Methodology
|
||||
|
||||
v0.6 is the project's first UI milestone. The grill assumes the plan is
|
||||
over-scoped, too costly, and quietly breaks G-006 (zero-dep) or G-003
|
||||
(production firewall) until evidence forces otherwise. Each of the nine
|
||||
review axes was scored against concrete evidence (commit hash, file:line,
|
||||
REQ-ID, D-decision, verified codebase state). Missing or contradictory
|
||||
evidence is itself a finding.
|
||||
|
||||
### Evidence baseline (verified against the actual repo, not the docs)
|
||||
|
||||
- `go.mod`: `module github.com/oy/openyield`, `go 1.22`, **cosmos-sdk v0.50.8
|
||||
+ ibc-go v8.2.1 already present** (the v0.5 D-055 controlled exception
|
||||
landed). v0.6 "go.mod unchanged" therefore means **zero NEW require lines
|
||||
on top of the v0.5 baseline**, NOT "go.mod is empty". This is a subtle but
|
||||
material audit-surface distinction — a naive "no new lines added in
|
||||
P1..P5" check would pass even if an indirect dep was bumped (a `go mod
|
||||
tidy` side-effect). **G-028** (below) forces the P6 audit to diff go.mod
|
||||
against the `v0.5.0` tag (the v0.6 P0 baseline), not just check "no new
|
||||
direct require lines".
|
||||
- `web/` directory: **does NOT exist** (verified — `ls web/` fails). v0.6 is
|
||||
greenfield UI; the plan's "new top-level `web/` dir" is accurate.
|
||||
- `lexicon_meta_web/`: **does NOT exist** (verified). The new firewall is
|
||||
net-new work, mirroring `lexicon_meta_docs/` (which exists and has the
|
||||
G-009 self-test + G-013 walk-coverage + self-exclusion via
|
||||
`runtime.Caller(0)` — all verified present).
|
||||
- `lexicon/lexicon.go`: `BannedTerms()`, `FindBannedTerm()`, and
|
||||
`SyntheticBannedStrings()` ALL exist (verified at lines 49, 73, 111). The
|
||||
G-014 shared helper from v0.4 REQ-029 landed. The new `lexicon_meta_web/`
|
||||
firewall has a proven template to mirror — no detection reimplementation
|
||||
risk.
|
||||
- `x/bread/types/types.go:13`: `GrainsPerBread = 10000` (verified). The docs
|
||||
table `docs/shared/bread-scale.md` says "each 1,000× the previous" (Crumb =
|
||||
1,000 Grain; Bread = 1,000 Crumb). **The code says Grain→Bread is 10,000×
|
||||
(GrainsPerBread=10000), NOT 1,000×.** D-073 (code constants are the source
|
||||
of truth, NOT docs) is CONFIRMED CORRECT — the docs are genuinely outdated.
|
||||
This is not a judgment call; it is a verified numeric contradiction. The
|
||||
doc-fix is correctly deferred (P1+ follow-up, not v0.6 scope — docs were a
|
||||
v0.3 deliverable).
|
||||
- `x/bloom/types/types.go:13-19`: `TargetBloomRateBasisPoints=450`,
|
||||
`MinBloomRateBasisPoints=400`, `MaxBloomRateBasisPoints=500`,
|
||||
`AccrualPeriodBlocks=144` — ALL exist (verified). P5's Bloom screen has
|
||||
real code constants to read.
|
||||
- `x/standing/types/types.go:12-13,39-40,94,99,112,129`: `PriorMean=4.0`,
|
||||
`PriorWeight=10`, `FreeholderMinStandingScore=4.5`,
|
||||
`FreeholderMinCategories=3`, `IsFreeholderEligible()`,
|
||||
`ComputeDiversityBonus()`, `GetVoucherWeight()`, `GetStandingBucket()` —
|
||||
ALL exist (verified). P4's Standing screen has the real locked-formula
|
||||
surface. The plan's "computed from locked constants, NOT hardcoded" claim
|
||||
is grounded.
|
||||
- `x/stash/types/types.go:19,23,32,35,38-39`: `BalanceGrain`, `StashActivity`,
|
||||
`MaturityThresholdDays=90`, `MaxGapForMaturity=30`, `IsMature()` — ALL
|
||||
exist (verified). P2's Stash dashboard has real maturity logic.
|
||||
- `x/window/types/types.go:19,22-24,36,56,100,117,123`: `ScopeKind` enum
|
||||
(ReadStash/ReadStanding/ProcessPassActForStand), `RateLimit`,
|
||||
`AuditEntry`, `Revoke()`, `Expire()`, `Activate()` — ALL exist (verified).
|
||||
P3's Window screen has the real lifecycle methods. The plan's "call
|
||||
`Window.Activate/Revoke/Expire`, NOT a reimplementation" is enforceable.
|
||||
- `x/identity/types/types.go:14-19`: `Reach` struct with `ReachID`,
|
||||
`HolderID`, `IsNomad` — exists (verified). P1's "Create a Reach" form has a
|
||||
real type to instantiate.
|
||||
- **G-003 import-invariant test** exists at `x/window/types/types_test.go`
|
||||
(uses `go/parser` ImportsOnly, scans `x/**/*.go`, prefix
|
||||
`github.com/oy/openyield/x/`). **Confirmed: `web/` is OUTSIDE `x/` so this
|
||||
test does NOT scan `web/`** — the plan's new `web/store/import_test.go`
|
||||
(P1-03-03) is the correct complement. BUT the plan's test only forbids
|
||||
`x/*/keeper` imports; `x/*/module` (the Cosmos runtime wiring) is also a
|
||||
production surface that `web/` must not reach into. **G-025** extends the
|
||||
forbidden-import set.
|
||||
- **No `app.go`, no `cmd/oyd`, no `main.go`** exists in the repo (verified
|
||||
— the plan's "no chain runtime exists" is accurate). v0.6's `web/main.go`
|
||||
is the project's first `main.go`. This is a genuine greenfield.
|
||||
|
||||
These baseline facts confirm the v0.6 plan's architecture claims against the
|
||||
actual codebase. The plan is well-grounded; the binding decisions below are
|
||||
correctness and verification hardening, not scope rework.
|
||||
|
||||
---
|
||||
|
||||
## 1. Per-Axis Adversarial Assessment
|
||||
|
||||
### Axis 1 — Feasibility (Go html/template + HTMX + stdlib ServeMux, no node) — **PASS** (confidence 0.85)
|
||||
|
||||
The stack is genuinely achievable with zero new Go deps. Go 1.22
|
||||
`net/http.ServeMux` supports method-pattern routing (`mux.HandleFunc("GET
|
||||
/reach", ...)` — verified Go 1.22 feature). `html/template` is stdlib with
|
||||
auto-escaping (XSS protection by default). HTMX 1.x minified is ~14KB
|
||||
served as a static asset via `http.FileServer` — no `go get`, no node, no
|
||||
build step. The mock-server-over-real-Go-types approach (D-067) is sound:
|
||||
all six referenced `x/*/types` packages exist and export the structs the UI
|
||||
surfaces (verified — Reach, Stash, StashActivity, Window, FreeholderSignals,
|
||||
BloomRecord all present with the methods the plan calls). No hidden blocker
|
||||
reaches the escalation threshold. The one feasibility risk is **HTMX
|
||||
version drift** (the vendored file has no version-pinned provenance in the
|
||||
plan beyond "HTMX 1.x") — minor, addressed by P1-02-02's "pin the version in
|
||||
a comment in the file header" mitigation.
|
||||
|
||||
### Axis 2 — Scope (5 screens + firewall + mock server in 5 execution phases) — **PASS** (confidence 0.80)
|
||||
|
||||
5 screens + 1 firewall + 1 mock server across 5 execution phases (P1..P5) +
|
||||
1 final phase (P6) is proportionate, not over-scoped. Each screen is a
|
||||
vertical slice (store extension + handler + template + tests) following the
|
||||
proven v0.2..v0.5 pattern. The Nomad happy path (Reach → Stash → Window →
|
||||
Standing → Bloom) is the correct ordering — each screen depends on the
|
||||
prior's data existing. No screen is gratuitous: all 5 map to a user-
|
||||
validated `--ideate` request and a REQ. The "working prototype" depth (mock
|
||||
data, no persistence, no auth) is the right scope for a first UI milestone
|
||||
— deferring auth/persistence to v0.7+ is correct (a UI with mock auth is a
|
||||
real prototype; a UI with broken half-auth is a worse prototype). Reject
|
||||
the "defer a screen" hypothesis — all 5 are on the Nomad critical path and
|
||||
each is one phase of work.
|
||||
|
||||
### Axis 3 — Cost (6 phases: P1..P5 + P6 final) — **PASS** (confidence 0.82)
|
||||
|
||||
33 tasks across 6 phases is proportionate. P1 is the largest (9 tasks —
|
||||
foundation + Reach + firewall, three parallel waves) because it lands the
|
||||
shared substrate; P2..P5 are uniform (5 tasks each — one screen per phase);
|
||||
P6 is light (4 tasks — review/audit/ship). Bundling P2+P3 (Stash + Window)
|
||||
into one phase would save one ship cycle but would couple two independent
|
||||
screens and break the vertical-slice shippability property (each phase
|
||||
ships a patch tag). The 6-phase structure is the right granularity — fewer
|
||||
phases would couple unrelated screens; more would be ceremony. No bloat:
|
||||
every task produces a concrete `.go`/`.html`/`.js` file or a verifiable
|
||||
gate. Reject the "too many phases" hypothesis.
|
||||
|
||||
### Axis 4 — G-006 (zero-dep): HTMX as vendored static asset — **PASS** (confidence 0.92)
|
||||
|
||||
A vendored JS file served over `http.FileServer` is NOT a Go module
|
||||
dependency — it is a static asset, exactly like `docs/images/` or the
|
||||
vendored CSS. The boundary is unambiguous: **`go.mod` require lines = Go
|
||||
deps; static files under `web/static/` = not Go deps.** HTMX has no Go
|
||||
import path; `go build ./web` does not resolve it; it is served byte-for-
|
||||
byte to the browser. G-006's intent (durability of the Go build, no
|
||||
external Go module tree) is fully preserved. The risk is not "HTMX is a
|
||||
dep" but "a future `go mod tidy` accidentally adds a Go dep" — **G-028**
|
||||
makes the P6 audit verify `go.mod` is byte-identical to the `v0.5.0` tag
|
||||
baseline (diff, not just "no new direct require lines"). Confidence holds.
|
||||
|
||||
### Axis 5 — G-003 (production firewall): web/ importing x/*/types — **CONDITIONAL** (confidence 0.78) → fixed by G-025
|
||||
|
||||
`web/` is NOT an `x/` module — it is the application layer (D-070), exactly
|
||||
as a future `cmd/oyd` would be. `web/store` importing `x/identity/types`
|
||||
is app-layer consumption of protocol types, the intended consumption
|
||||
direction. G-003's intent (no cross-module struct coupling INSIDE the
|
||||
protocol layer) is intact: no `x/` module gains a production import of
|
||||
another `x/` module's types via `web/`. The existing G-003 test
|
||||
(`x/window/types/types_test.go`) scans `x/**/*.go` and correctly does NOT
|
||||
scan `web/` (web/ is outside x/). The plan's new `web/store/import_test.go`
|
||||
(P1-03-03) is the right complement. **BUT** the plan's test only forbids
|
||||
`x/*/keeper` imports; `x/*/module` (the Cosmos `module.go` runtime wiring)
|
||||
is ALSO a production surface that `web/` must not reach into — importing
|
||||
`x/bond/module` would couple the UI to the runtime app-module graph,
|
||||
violating the "no keeper, no Cosmos runtime" boundary (D-067). **G-025**
|
||||
extends the forbidden-import set to `x/*/keeper` AND `x/*/module` (and
|
||||
any `x/*/` subpackage other than `types`). Confidence holds after the fix.
|
||||
|
||||
### Axis 6 — REQ-012 (lexicon firewall extension to web/) — **CONDITIONAL** (confidence 0.80) → fixed by G-026
|
||||
|
||||
The firewall extension is sound in structure: a new sibling
|
||||
`lexicon_meta_web/` mirroring `lexicon_meta_docs/` (same
|
||||
`lexicon.FindBannedTerm` + word-boundary, G-009 self-test via
|
||||
`SyntheticBannedStrings()`, G-013 walk-coverage, self-exclusion via
|
||||
`runtime.Caller(0)`). The firewall-first ordering (P1 before P2..P5
|
||||
content) is the proven D-044 pattern. "Create a Reach" (not "Sign up for
|
||||
an account") is the correct label — "account" is banned (REQ-012). The
|
||||
file-scan firewall catches banned terms in `web/templates/**`,
|
||||
`web/static/**`, and `web/**/*.go`. **BUT** the plan's `web/**/*.go` scan
|
||||
does not explicitly state whether it covers **string literals** (error
|
||||
messages, template-fragment strings) or only comments/identifiers. A banned
|
||||
term in a Go string literal (e.g., `errors.New("account not found")`) is
|
||||
user-facing if it surfaces in an error response — the file-scan catches it
|
||||
(the walk reads the file content, not just the AST), but the plan should
|
||||
make this explicit. The deeper gap: the **per-handler rendered-HTML lexicon
|
||||
check** (each phase's handler test scans the HTTP response body) covers the
|
||||
happy-path response, but **error responses** (400/500 pages) are a drift
|
||||
vector — a banned term in an error template or a dynamically-generated
|
||||
error message would not be caught by the happy-path rendered-HTML check.
|
||||
**G-026** requires the rendered-HTML lexicon check to cover at least one
|
||||
error response per handler (e.g., `POST /reach` with empty HolderID → 400
|
||||
response body scanned for banned terms). Confidence holds after the fix.
|
||||
|
||||
### Axis 7 — Bread-scale discrepancy (D-073) — **PASS** (confidence 0.90)
|
||||
|
||||
D-073 is CONFIRMED CORRECT against the actual codebase. The code
|
||||
(`x/bread/types/types.go:13` `GrainsPerBread = 10000`) and the docs
|
||||
(`docs/shared/bread-scale.md` "each 1,000× the previous") are in verified
|
||||
numeric contradiction: the docs say Grain→Crumb is 1,000× and Crumb→Bread
|
||||
is 1,000× (so Grain→Bread is 1,000,000×), while the code says
|
||||
Grain→Bread is 10,000×. The code constants are tested
|
||||
(`x/bread/types/types_test.go` asserts them); the docs are not. Using the
|
||||
code as the source of truth keeps the UI consistent with the protocol
|
||||
layer. The doc-fix is correctly deferred to a P1+ follow-up (not v0.6
|
||||
scope — docs were a v0.3 deliverable; this is a doc-drift fix, not a UI
|
||||
feature). The regression-guard test (P2-03-01 "Bread-scale conversion
|
||||
correctness test — would FAIL if the outdated docs values were used") is
|
||||
the right enforcement. No binding change — D-073 is ratified as-is.
|
||||
|
||||
### Axis 8 — Security (no auth, no sessions, in-memory store, XSS, injection) — **CONDITIONAL** (confidence 0.75) → fixed by G-027
|
||||
|
||||
For a "working prototype" milestone, no auth / no sessions / in-memory
|
||||
store is acceptable — it is explicitly out of scope (PROJECT.md v0.6 OOS)
|
||||
and documented. The XSS risk is LOW: `html/template` auto-escapes by
|
||||
default; the Reach form inputs (HolderID, PublicKey) are rendered through
|
||||
template actions (`{{.HolderID}}`) which escape HTML. The one injection
|
||||
risk the plan does not address: **ReachID/HolderID used as map keys in the
|
||||
mock store without validation.** The plan's P1-04-03 tests "POST /reach
|
||||
with empty HolderID returns 400" (good), but does not specify validation
|
||||
of the map key itself — a HolderID containing path separators (`/`),
|
||||
template syntax (`{{`), or very long strings could cause route confusion,
|
||||
template injection (if unescaped in a URL path), or memory exhaustion.
|
||||
`html/template` escapes on OUTPUT, but the map key is also used in URL
|
||||
construction (`GET /reach/{id}`) and route matching. **G-027** requires
|
||||
`CreateReach` to validate HolderID/PublicKey (non-empty, length-bounded,
|
||||
no path separators, no template syntax) before using them as map keys /
|
||||
URL path segments. This is not a production security gate (the mock store
|
||||
resets on restart); it is a prototype-robustness gate that prevents the
|
||||
happy path from breaking on adversarial input. Confidence holds after the
|
||||
fix.
|
||||
|
||||
### Axis 9 — Testability (≥80% on web/, HTMX fragment rendering, MVP/UX criteria) — **PASS** (confidence 0.82)
|
||||
|
||||
≥80% coverage on `web/store` + `web/handlers` is realistic with
|
||||
`httptest.NewRecorder` + `httptest.NewRequest` (stdlib — no external test
|
||||
deps, G-006 preserved). The handlers are thin (load from store → render
|
||||
template → write response), so coverage is achievable with table-driven
|
||||
tests per route + per lifecycle transition. HTMX fragment rendering is
|
||||
tested WITHOUT a browser: the handler tests assert the response body
|
||||
contains the expected HTML fragments (e.g., the Bread-scale conversion
|
||||
table, the maturity progress bar width) — the HTMX swap is a client-side
|
||||
concern, but the SERVER-SIDE fragment is testable via response-body
|
||||
assertions. The 8 MVP/UX acceptance criteria (PLANS.md lines 2143-2151)
|
||||
are all auto-verifiable EXCEPT criterion 2's "manual browser check at
|
||||
http://localhost:8080" and the happy-path end-to-end (PLANS.md Happy Path
|
||||
section) — these are manual checks, not auto-tests. This is acceptable for
|
||||
a UI milestone (the auto-tests verify the routes return 200 + correct
|
||||
HTML; the manual check verifies the browser renders them correctly), but
|
||||
the P6 audit must record the manual check as a separate verification
|
||||
step, not conflate it with the auto-test green. No binding change — the
|
||||
plan already distinguishes "handler tests" from "manual browser check"
|
||||
throughout.
|
||||
|
||||
---
|
||||
|
||||
## 2. Binding Decisions (D-078..D-081)
|
||||
|
||||
These are **binding** — the orchestrator MUST apply them before EXECUTE
|
||||
begins. Numbered D-078..D-081 (continuing from D-073).
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Binding fix (if any) |
|
||||
|----|----------|-----------|------------|----------------------|
|
||||
| **D-078** | **RATIFY G-006 boundary: HTMX vendored as `web/static/htmx.min.js` is G-006-compliant (static asset, not a Go dep); `go.mod` MUST stay unchanged across the v0.6 milestone range (zero new require lines on top of the v0.5 baseline).** The boundary is: `go.mod` require lines = Go deps; static files under `web/static/` = not Go deps. HTMX has no Go import path; `go build ./web` does not resolve it. | A vendored JS file served over `http.FileServer` is a static asset (like `docs/images/`), not a Go module dependency. Verified: `go.mod` already has cosmos-sdk from v0.5; v0.6 adds nothing. G-006 intent (durability of the Go build, no new external Go module tree) is fully preserved. | 0.92 | **G-028** — the P6 audit MUST diff `go.mod` against the `v0.5.0` tag (the v0.6 P0 baseline), not just check "no new direct require lines were added in P1..P5". A `go mod tidy` side-effect could bump an indirect dep without adding a direct require line. The diff must be EMPTY (or only the expected `// indirect` reordering with no version changes). | None — RATIFIED (with G-028 enforcement) |
|
||||
| **D-079** | **RATIFY G-003 boundary: `web/` importing `x/*/types` is app-layer consumption (D-070), NOT a cross-`x/` production import. The G-003 production firewall stays intact: no `x/` module gains a production import of another `x/` module's types via `web/`.** The existing G-003 test (`x/window/types/types_test.go`) scans `x/**/*.go` and correctly does NOT scan `web/` (web/ is outside x/). The new `web/store/import_test.go` (P1-03-03) is the correct `web/`-scoped complement. | `web/` is the application layer (like a future `cmd/oyd`), not an `x/` module. Consuming protocol types is the intended direction. Verified: the G-003 test prefix is `github.com/oy/openyield/x/` so `web/` imports are outside its scope by construction. | 0.85 | **G-025** — `web/store/import_test.go` MUST forbid imports of `x/*/keeper` AND `x/*/module` (and any `x/*/` subpackage other than `types`), not just `x/*/keeper`. The plan only mentions `keeper`; `module.go` is also a Cosmos runtime surface that `web/` must not reach into (D-067 "no keeper, no Cosmos runtime"). | None — RATIFIED (with G-025 enforcement) |
|
||||
| **D-080** | **RATIFY D-073: Bread-scale source of truth = `x/bread/types` code constants (`GrainsPerBread=10000`, `BreadScaleAll()`), NOT `docs/shared/bread-scale.md`.** The doc is verified outdated (docs say 1,000× ratios; code says 10,000× Grain→Bread). The doc-fix is a P1+ follow-up, NOT v0.6 scope (docs were a v0.3 deliverable; this is a doc-drift fix, not a UI feature). The P2-03-01 Bread-scale conversion correctness test (would FAIL if docs values were used) is the regression guard. | Verified numeric contradiction: `x/bread/types/types.go:13` `GrainsPerBread = 10000` vs `docs/shared/bread-scale.md` "each 1,000× the previous". Code constants are tested (`types_test.go`); docs are not. Using code keeps the UI consistent with the protocol layer. | 0.90 | None — the P2-03-01 regression-guard test is already in the plan. The doc-fix deferral is correct (P1+ follow-up, not v0.6). | None — RATIFIED |
|
||||
| **D-081** | **RATIFY the lexicon firewall extension to `web/` (REQ-045): new sibling `lexicon_meta_web/` mirroring `lexicon_meta_docs/` (same `lexicon.FindBannedTerm` + word-boundary + G-009 self-test via `SyntheticBannedStrings()` + G-013 walk-coverage + self-exclusion via `runtime.Caller(0)`). Firewall-first (P1 before P2..P5 content). "Create a Reach" is the correct label (not "Sign up for an account" — "account" is banned).** The file-scan firewall + per-handler rendered-HTML lexicon checks are the dual firewall (file-scan catches static content; rendered-HTML catches dynamic content). | REQ-012 is `All` phases and UI strings are user-facing. Verified: `lexicon.SyntheticBannedStrings()` exists (G-014 helper); `lexicon_meta_docs/` has the proven template (self-test + walk-coverage + self-exclusion). Firewall-first (D-044 pattern) catches drift at build time, not at P6 audit. | 0.88 | **G-026** — the per-handler rendered-HTML lexicon check MUST cover at least one ERROR response per handler (e.g., `POST /reach` with empty HolderID → 400 response body scanned for banned terms), not just the happy-path 200 response. Error messages and error templates are a drift vector the happy-path check misses. | None — RATIFIED (with G-026 enforcement) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Binding Fixes (G-025..G-028)
|
||||
|
||||
These are **binding** — the orchestrator MUST apply them before the
|
||||
affected phase ships. Numbered G-025..G-028 (continuing from G-024).
|
||||
|
||||
| ID | Binding Fix | Rationale | Confidence | Affects (phase / task) |
|
||||
|----|-------------|-----------|------------|------------------------|
|
||||
| **G-025** | **`web/store/import_test.go` (P1-03-03) MUST forbid imports of `x/*/keeper` AND `x/*/module` (and any `x/*/` subpackage other than `types`), not just `x/*/keeper`.** The plan's P1-03-03 only mentions `x/*/keeper`. `x/*/module` (the Cosmos `module.go` runtime wiring) is also a production surface that `web/` must not reach into — importing `x/bond/module` would couple the UI to the runtime app-module graph, violating D-067 ("no keeper, no Cosmos runtime, no `app.go`"). The import-invariant test should assert that every `github.com/oy/openyield/x/<module>/` import path in a `web/` production `.go` file ends in `/types` (i.e., only `x/<module>/types` is allowed; `x/<module>/keeper`, `x/<module>/module`, `x/<module>/simtest`, etc. are forbidden). | The plan's `web/store/import_test.go` only forbids `x/*/keeper`, leaving `x/*/module` (and other runtime subpackages) as an unguarded import path. D-067's "no Cosmos runtime" boundary is broader than just "no keeper". A single test asserting "only `x/*/types` imports from `web/`" closes the full boundary. | 0.82 | **P1-03-03** (`web/store/import_test.go`). Must land before P1 ships; carries through P2..P5. |
|
||||
| **G-026** | **The per-handler rendered-HTML lexicon check (P1-04-03, P2-03-01, P3-03-01, P4-03-01, P5-03-01) MUST scan at least one ERROR response body per handler, not just the happy-path 200 response.** For each handler, the test must include a case that triggers an error response (e.g., `POST /reach` with empty HolderID → 400; `GET /stash/{nonexistent}` → 404; `POST /window` with invalid scope → 400) and assert the error response body contains no banned terms via `lexicon.FindBannedTerm`. Error messages and error templates are a drift vector: a banned term in an error string (e.g., `"account not found"`, `"deposit failed"`) would not be caught by the happy-path rendered-HTML check. The file-scan firewall catches banned terms in template FILES, but a dynamically-generated error message (constructed in Go code, not a template file) is only caught by scanning the error response body. | The plan's rendered-HTML lexicon checks scan the happy-path 200 response. Error responses are a separate code path (different template, or a Go-constructed error string) that the happy-path check does not exercise. A banned term in an error message is user-facing and would pass the file-scan firewall (the term is in a Go string literal, not a template file) while failing the lexicon intent. Scanning one error response per handler closes this gap. | 0.80 | **P1-04-03, P2-03-01, P3-03-01, P4-03-01, P5-03-01** (rendered-HTML lexicon checks). Must land before each phase ships. |
|
||||
| **G-027** | **`web/store/store.go` `CreateReach` (P1-03-01) MUST validate `holderID` and `publicKey` before using them as map keys / URL path segments.** Validation: non-empty (already tested in P1-04-03), length-bounded (e.g., `len(holderID) <= 128`, `len(publicKey) <= 256`), no path separators (`/`, `\`), no template syntax (`{{`, `}}`). The handler test (P1-04-03) MUST include cases for each validation failure (empty → 400; too long → 400; contains `/` → 400; contains `{{` → 400). This is a prototype-robustness gate, not a production security gate — the mock store resets on restart, but an adversarial input should not break the happy path or cause route confusion (`GET /reach/{id}` with `id` containing `/` would mismatch the route). | The plan tests "empty HolderID returns 400" but does not specify validation of the map key / URL path segment beyond emptiness. `html/template` escapes on OUTPUT, but the HolderID is also used in URL construction (`GET /reach/{id}`) and route matching — a HolderID containing `/` would cause route confusion; a very long string would cause memory exhaustion in the in-memory map. Validation before map-key use is the standard prototype-robustness pattern. | 0.75 | **P1-03-01** (`web/store/store.go` `CreateReach` validation); **P1-04-03** (handler test cases for each validation failure). Must land before P1 ships. |
|
||||
| **G-028** | **The P6 audit (P6-02-01) MUST verify `go.mod` is byte-identical (or diff-empty) against the `v0.5.0` tag (the v0.6 P0 baseline), NOT just check "no new direct require lines were added in P1..P5".** The verification: `git diff v0.5.0..HEAD -- go.mod` must be EMPTY (or contain only `// indirect` reordering with no version changes). A `go mod tidy` side-effect in P1..P5 could bump an indirect dep version (e.g., `github.com/cosmos/cosmos-sdk` stays v0.50.8 but an indirect dep bumps from v1.2.3 to v1.2.4) without adding a direct require line — a naive "no new direct require lines" check would pass while `go.mod` changed. The diff-against-baseline check closes this. | `go.mod` already has the v0.5 cosmos-sdk + ibc-go deps (verified). v0.6 "go.mod unchanged" means unchanged FROM the v0.5 baseline, not "empty". The plan's P6-02-01 says "G-006 go.mod unchanged (HTMX is a vendored static asset, NOT a `go get` — zero new require lines in v0.6)" — "zero new require lines" is necessary but not sufficient; an indirect dep bump is a `go.mod` change that "zero new require lines" would miss. The diff-against-`v0.5.0` check is the complete enforcement. | 0.85 | **P6-02-01** (audit feature purity gate — G-006 verification). Must land before P6 ships (milestone release). |
|
||||
|
||||
---
|
||||
|
||||
## 4. Escalations
|
||||
|
||||
**None.** All nine axes resolved at confidence ≥ 0.60 after the binding
|
||||
fixes G-025..G-028 are applied. No axis required escalation to the human.
|
||||
At full autonomy, the orchestrator applies the binding decisions (D-078..
|
||||
D-081) and binding fixes (G-025..G-028) and proceeds to EXECUTE.
|
||||
|
||||
The single most material finding is **G-028** (the go.mod audit surface):
|
||||
because v0.5 already added cosmos-sdk, the v0.6 "go.mod unchanged" gate is
|
||||
subtler than "no new deps" — it must verify no indirect dep was bumped
|
||||
either. This is not an escalation (the fix is mechanical: diff go.mod
|
||||
against the v0.5.0 tag), but it is the finding most likely to cause a
|
||||
false-green P6 audit if not surfaced now.
|
||||
|
||||
---
|
||||
|
||||
## 5. Overall Verdict
|
||||
|
||||
### **SHIP Phase 0 WITH FIXES** (confidence 0.82)
|
||||
|
||||
The v0.6 Phase 0 plan is fundamentally sound and well-grounded: the
|
||||
Go `html/template` + HTMX + stdlib `ServeMux` stack is genuinely zero-new-
|
||||
dep (HTMX is a vendored static asset, not a Go dep — D-078); the mock-
|
||||
server-over-real-Go-types approach (D-067) is grounded in verified
|
||||
codebase facts (all six `x/*/types` packages exist with the structs and
|
||||
methods the UI surfaces); the 5-screen scope is proportionate (not over-
|
||||
scoped for a first UI milestone); the firewall-first ordering (D-069,
|
||||
P1 before content) is the proven D-044 pattern; D-073 (code constants
|
||||
over outdated docs) is verified correct against the actual numeric
|
||||
contradiction (`GrainsPerBread=10000` vs docs "1,000×").
|
||||
|
||||
The 4 decision ratifications (D-078..D-081) are all **RATIFIED**:
|
||||
- **D-078** (G-006 boundary — HTMX is a static asset) — RATIFY with G-028
|
||||
audit enforcement.
|
||||
- **D-079** (G-003 boundary — web/ is app-layer) — RATIFY with G-025
|
||||
import-invariant enforcement.
|
||||
- **D-080** (D-073 — code constants over docs) — RATIFY as-is (regression-
|
||||
guard test already in the plan).
|
||||
- **D-081** (lexicon firewall extension to web/) — RATIFY with G-026
|
||||
error-response lexicon enforcement.
|
||||
|
||||
The 4 binding fixes (G-025..G-028) are **correctness and verification
|
||||
hardening**, not scope rework:
|
||||
- **G-025** (import-invariant forbids `x/*/module` too) — closes the
|
||||
"no keeper but maybe module" gap in the plan's `web/store/import_test.go`.
|
||||
- **G-026** (rendered-HTML lexicon check covers error responses) — closes
|
||||
the error-message drift vector the happy-path check misses.
|
||||
- **G-027** (ReachID/HolderID validation before map-key use) — prototype-
|
||||
robustness gate; prevents route confusion and memory exhaustion on
|
||||
adversarial input.
|
||||
- **G-028** (go.mod diff against v0.5.0 baseline, not just "no new
|
||||
require lines") — closes the indirect-dep-bump false-green in the P6
|
||||
audit.
|
||||
|
||||
None of these rise to "RETHINK" or "REDUCE SCOPE" — the architecture,
|
||||
scope, ordering, and persona assignments are correct. The security posture
|
||||
(no auth, no sessions, in-memory store) is acceptable for a "working
|
||||
prototype" milestone and explicitly out of scope. Apply the 4 binding
|
||||
fixes and proceed to EXECUTE (P1).
|
||||
|
||||
**Confidence in overall verdict: 0.82**
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary Block
|
||||
|
||||
```
|
||||
Decision ratifications:
|
||||
D-078 (G-006 boundary — HTMX vendored static asset) — RATIFY (0.92)
|
||||
D-079 (G-003 boundary — web/ is app-layer, not x/) — RATIFY (0.85)
|
||||
D-080 (D-073 — code constants over outdated docs) — RATIFY (0.90)
|
||||
D-081 (lexicon firewall extension to web/) — RATIFY (0.88)
|
||||
|
||||
Nine-axis scorecard:
|
||||
1. Feasibility (html/template + HTMX + stdlib) — PASS (0.85)
|
||||
2. Scope (5 screens + firewall + mock, 5 phases) — PASS (0.80)
|
||||
3. Cost (6 phases, 33 tasks) — PASS (0.82)
|
||||
4. G-006 (HTMX vendored, not a Go dep) — PASS (0.92) → enforced by G-028
|
||||
5. G-003 (web/ app-layer consumption) — CONDITIONAL (0.78) → fixed by G-025
|
||||
6. REQ-012 (lexicon firewall extension to web/) — CONDITIONAL (0.80) → fixed by G-026
|
||||
7. Bread-scale (D-073 code constants) — PASS (0.90)
|
||||
8. Security (no auth, XSS, injection) — CONDITIONAL (0.75) → fixed by G-027
|
||||
9. Testability (≥80% web/, HTMX fragments, MVP) — PASS (0.82)
|
||||
|
||||
Feature purity gate: PASS WITH FIXES (G-025, G-028)
|
||||
|
||||
Binding fixes: 4 (G-025..G-028)
|
||||
G-025 — web/store/import_test.go forbids x/*/keeper AND x/*/module — before P1
|
||||
G-026 — rendered-HTML lexicon check covers error responses — before each phase
|
||||
G-027 — CreateReach validates HolderID/PublicKey before map-key — before P1
|
||||
G-028 — P6 audit diffs go.mod against v0.5.0 tag (not just no-new-lines) — before P6
|
||||
|
||||
Escalations: 0
|
||||
Overall: SHIP Phase 0 WITH FIXES (confidence 0.82)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. CI Commit Block (for the orchestrator)
|
||||
|
||||
```
|
||||
docs(P00): grill — v0.6 adversarial review (D-078+, G-025+)
|
||||
|
||||
---ci---
|
||||
project: oy
|
||||
phase: 0
|
||||
milestone: v0.6
|
||||
status: grill
|
||||
decisions:
|
||||
- id: D-078
|
||||
decision: RATIFY G-006 boundary — HTMX vendored as web/static/htmx.min.js is G-006-compliant (static asset, not Go dep); go.mod unchanged across v0.6
|
||||
rationale: vendored JS served over http.FileServer is not a Go module dep (no import path); go.mod already has cosmos-sdk from v0.5; v0.6 adds nothing; G-028 enforces diff-against-v0.5.0
|
||||
confidence: 0.92
|
||||
alternatives: [HTMX via go get (breaks G-006); node toolchain + React (breaks Go-only convention)]
|
||||
- id: D-079
|
||||
decision: RATIFY G-003 boundary — web/ importing x/*/types is app-layer consumption (D-070), not cross-x/ production import; G-003 firewall intact
|
||||
rationale: web/ is not an x/ module; existing G-003 test scans x/**/*.go (web/ outside scope by construction); new web/store/import_test.go is the web/-scoped complement; G-025 extends forbidden set to x/*/module
|
||||
confidence: 0.85
|
||||
alternatives: [treat web/ as x/ module (wrong — not protocol); forbid web/ from importing x/*/types (would force TS/JSON fixtures, losing locked-constant grounding)]
|
||||
- id: D-080
|
||||
decision: RATIFY D-073 — Bread-scale source of truth = x/bread/types code constants (GrainsPerBread=10000, BreadScaleAll()), NOT docs/shared/bread-scale.md (outdated: 1,000× vs code 10,000×)
|
||||
rationale: verified numeric contradiction (types.go:13 GrainsPerBread=10000 vs docs "1,000×"); code constants tested, docs not; doc-fix deferred to P1+ follow-up (not v0.6 scope); P2-03-01 regression-guard test enforces
|
||||
confidence: 0.90
|
||||
alternatives: [use docs values (wrong — not tested, disagrees with code); fix docs in v0.6 (out of scope — doc-drift fix, not UI feature)]
|
||||
- id: D-081
|
||||
decision: RATIFY lexicon firewall extension to web/ (REQ-045) — new sibling lexicon_meta_web/ mirroring lexicon_meta_docs/; firewall-first (P1 before content); "Create a Reach" label (not "account")
|
||||
rationale: REQ-012 is All phases; UI strings user-facing; SyntheticBannedStrings() helper exists (G-014); lexicon_meta_docs/ has proven template; G-026 extends rendered-HTML check to error responses
|
||||
confidence: 0.88
|
||||
alternatives: [skip firewall (REQ-012 is All phases); single combined meta-test (loses isolation)]
|
||||
fixes:
|
||||
- id: G-025
|
||||
fix: web/store/import_test.go MUST forbid x/*/keeper AND x/*/module (only x/*/types allowed from web/)
|
||||
affects: P1-03-03
|
||||
before_phase: P1
|
||||
confidence: 0.82
|
||||
- id: G-026
|
||||
fix: per-handler rendered-HTML lexicon check MUST scan at least one ERROR response body (not just happy-path 200)
|
||||
affects: P1-04-03, P2-03-01, P3-03-01, P4-03-01, P5-03-01
|
||||
before_phase: P1 (carries through P2..P5)
|
||||
confidence: 0.80
|
||||
- id: G-027
|
||||
fix: CreateReach MUST validate HolderID/PublicKey (non-empty, length-bounded, no path separators, no template syntax) before map-key use; handler test covers each validation failure
|
||||
affects: P1-03-01, P1-04-03
|
||||
before_phase: P1
|
||||
confidence: 0.75
|
||||
- id: G-028
|
||||
fix: P6 audit MUST diff go.mod against v0.5.0 tag (not just check "no new direct require lines") — catches indirect dep bumps
|
||||
affects: P6-02-01
|
||||
before_phase: P6
|
||||
confidence: 0.85
|
||||
escalations: []
|
||||
---/ci---
|
||||
```
|
||||
|
||||
+1
-465
@@ -1685,468 +1685,4 @@ The v0.5 deliverable MUST meet these explicit criteria (verified in P8 audit):
|
||||
7. **REQ-039**: `x/council` has `keeper/msg_server.go` + `types/msg_*.go`; `Proposal`/`ProposalKind`(4)/`ProposalStatus`(5)/`VoteOption`(4) enums added (D-060); `MissionLockAmendment-Rejected` rejected at `ValidateBasic` (D-064/A-572); Watcher Veto quorum default 6 (D-065/A-574); single-Veto-no-block (anti-greed); `MissionLockAmendable=false` unchanged (v0.2 regression green); `SignalKindCount=4` unchanged (v0.4 regression green); `CouncilKindCount=3` unchanged; no proposal auto-execution.
|
||||
8. **Feature purity gate (P8)**: no breaking schema changes (v0.3 `types/` contracts NOT amended); locked-const firewall intact (all v0.1..v0.4 consts unchanged; new P7 enums per D-060); G-003 production firewall intact (`expected_keepers.go` are interfaces); G-006 controlled exception GRILL-ratified (D-055/D-062).
|
||||
9. **No regression**: `go test ./...` green; v0.4 coverage floor (93.3% on `x/hub/types`, 96.4% on `x/council/types`) not reduced on the `types/` packages; v0.1..v0.4 baseline tests green.
|
||||
10. **D-055/D-062 dep**: `go.mod` has cosmos-sdk v0.50.x + ibc-go v8.x (GRILL-ratified); `types/` packages gain `sdk.Msg` imports for `Msg*` (isolated in `types/msg_*.go`); invariant/lexicon tests stay stdlib-only and green.
|
||||
|
||||
---
|
||||
|
||||
## v0.6 Plan (Nomad Web UI)
|
||||
|
||||
> This section APPENDS the v0.6 milestone plan to the v0.1..v0.5 plans above.
|
||||
> It does NOT rewrite or supersede the earlier content. v0.6 is the project's
|
||||
> first **UI** milestone: a working prototype Web UI where a person can sign up
|
||||
> to be a Nomad (create a Reach + open a Stash) and exercise basic functionality
|
||||
> around (Reach, Stash) plus Window authorization, Standing progress, and Bloom
|
||||
> accrual. All data is generated test fixtures — no real blockchain (D-020
|
||||
> continues). Stack: Go `html/template` + HTMX (vendored, no node) + Go 1.22
|
||||
> `net/http.ServeMux` mock HTTP server in a new `web/` dir that instantiates the
|
||||
> real `x/*/types` structs from in-memory fixtures (D-067). No keeper, no
|
||||
> Cosmos runtime, no `app.go` (none exists in the repo). Tags run on the
|
||||
> `v0.5.x` patch line (config.json `tag_base: v0.5.x`): P0 → `v0.5.0`;
|
||||
> execution phases P1..P5 → `v0.5.1..v0.5.5`; final phase P6 → `v0.5.6` IS the
|
||||
> v0.6 milestone release (D-008 — final phase patch IS the milestone release;
|
||||
> no separate minor tag). Branch names use NO `oy/` prefix (single-project mode:
|
||||
> only `oy` exists; the slug prefix would be redundant — config `projects[]`
|
||||
> length is 1, matching the v0.5 convention).
|
||||
|
||||
### Milestone Summary
|
||||
|
||||
- **Milestone**: v0.6 — Nomad Web UI
|
||||
- **Type**: Feature (P1..P5 are `feat`; REQ-045 is `test` co-shipped in P1; P6 is `final`)
|
||||
- **Tag base**: `v0.5.x` patch line (P0 → `v0.5.0`; execution P1..P5 → `v0.5.1..v0.5.5`; final P6 → `v0.5.6` IS the v0.6 milestone release)
|
||||
- **Phases**: 7 — P1..P5 (execution) + P6 (final review/audit/ship). Phase 0 (this PLAN) is in progress.
|
||||
- **Depth**: UI prototype — Go `html/template` server-rendered HTML + HTMX progressive enhancement; in-memory mock store seeded from fixtures; instantiates real `x/*/types` structs (Reach, Stash, Window, FreeholderSignals, BloomRecord). No keeper, no Cosmos runtime, no `app.go`, no persistence (resets on restart).
|
||||
- **Coverage target**: ≥80% on each new `web/` package (D-033 carries forward); the mock store + handlers have Go tests (`go test ./web/...`), not just manual browser checks. Lexicon firewall (REQ-012) extended to `web/` via a new sibling meta-test (REQ-045).
|
||||
- **New dirs**: `web/` (top-level; `main.go`, `handlers/`, `store/`, `templates/`, `static/`); `lexicon_meta_web/` (sibling firewall test dir, mirrors `lexicon_meta_docs/`). **Extended**: none (`x/` is NOT modified — `web/` imports `x/*/types` as app-layer consumption per D-070, not a production cross-`x/` import).
|
||||
- **G-006 (zero Go deps)**: preserved. HTMX is a vendored static asset (`web/static/htmx.min.js`), NOT a `go get`. Go 1.22 `net/http.ServeMux` is stdlib. `html/template` is stdlib. `go.mod` is unchanged (no new require lines; the v0.5 cosmos-sdk/ibc-go deps stay but v0.6 adds nothing).
|
||||
- **G-003 (production firewall)**: intact. `web/` importing `x/*/types` is app-layer consumption (D-070), NOT a cross-`x/` production import. No `x/` module gains a production import of another `x/` module's types via `web/`. The v0.2 G-003 import-invariant test scans `x/**/*.go` (unchanged scope); `web/` is outside `x/` so it is not scanned by that test — a NEW `web/`-scoped test confirms `web/` only imports `x/*/types` (not `x/*/keeper`).
|
||||
- **Phase ordering** (D-072, finalized here): P1 web foundation + Reach signup + lexicon firewall (REQ-040 + REQ-045 — same `web/` territory, vertical slice, firewall-first) → P2 Stash dashboard (REQ-041) → P3 Window authorization (REQ-042) → P4 Standing + Freeholder signals (REQ-043) → P5 Bloom accrual (REQ-044) → P6 final review/audit/ship. Each phase independently shippable (vertical slice); P1 lands the foundation + firewall first (lexicon-clean by construction — D-044 pattern).
|
||||
- **Personas** (from PERSONAS.md, reactivated for v0.6): **frontend-engineer** owns `web/**` (templates, static, handlers, `main.go`) — REACTIVATED for the first time (territory `web/**`); **backend-engineer** co-owns `web/store/` (the mock store that imports `x/*/types`) + owns the lexicon firewall extension (REQ-045, `lexicon_meta_web/` — mirrors `lexicon_meta_docs/`); **lead-developer** owns P0 + P6 + coordination. The v0.5 cosmos-engineer/security-engineer/mesh-engineer personas are NOT reactivated (no keeper/MsgServer/simtest work in v0.6 — UI mock only). ci-security-auditor activated in P6.
|
||||
|
||||
### Cross-Phase Dependency Map (v0.6)
|
||||
|
||||
```
|
||||
P1 (web foundation + Reach signup + lexicon firewall)
|
||||
│ web/main.go + web/store/ + web/handlers/reach.go + web/templates/reach.html
|
||||
│ lexicon_meta_web/lexicon_meta_web_test.go [firewall-first; scans web/ as content lands]
|
||||
▼
|
||||
P2 (Stash dashboard) [depends on Reach existing (signup creates Stash atomically, D-071)]
|
||||
▼
|
||||
P3 (Window authorization) [depends on Stash existing (Window scope references a Stash)]
|
||||
▼
|
||||
P4 (Standing + Freeholder signals) [depends on Reach existing (Standing is per-Reach)]
|
||||
▼
|
||||
P5 (Bloom accrual) [depends on Stash existing (BloomRecord is per-Stash)]
|
||||
▼
|
||||
P6 (final review/audit/ship)
|
||||
```
|
||||
|
||||
Hard cross-phase blockers:
|
||||
- **P1 lexicon firewall (`lexicon_meta_web/`)** → blocks P2..P5 content (firewall-first: a banned term slipped into a P2..P5 template/JS/Go file fails the build, not the P6 review — D-044/D-069 pattern).
|
||||
- **P1 web foundation (`web/main.go` + `web/store/` + base templates)** → blocks P2..P5 (each screen extends the base layout + uses the mock store).
|
||||
- **P1 Reach signup (REQ-040)** → blocks P2 (Stash dashboard needs a Stash, created atomically at signup per D-071), P4 (Standing is per-Reach).
|
||||
- **P2 Stash dashboard (REQ-041)** → blocks P3 (Window scope references a Stash), P5 (BloomRecord is per-Stash).
|
||||
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene).
|
||||
|
||||
All other refs are app-layer consumption of `x/*/types` (D-070): `web/store/` imports `x/identity/types`, `x/stash/types`, `x/window/types`, `x/standing/types`, `x/bread/types`, `x/bloom/types`. No `x/` module is modified.
|
||||
|
||||
### D-070 / G-003 Boundary (app-layer consumption, NOT a cross-`x/` import)
|
||||
|
||||
`web/` is NOT an `x/` module — it is the application layer (exactly as a future `cmd/oyd` would be). `web/store/` importing `x/identity/types.Reach` is app-layer consumption of protocol types, the intended consumption direction. The G-003 firewall (no production struct imports across `x/<module>/types`) is intact: no `x/` module gains a production import of another `x/` module's types via `web/`. A new `web/`-scoped test (`web/store/import_test.go`) asserts `web/` only imports `x/*/types` packages (NOT `x/*/keeper` — there is no keeper in v0.6; the mock store IS the data source).
|
||||
|
||||
---
|
||||
|
||||
## Phase P1 — Web Foundation + Reach Signup + Lexicon Firewall (REQ-040, REQ-045) → v0.5.1
|
||||
|
||||
- **Slug**: `web-foundation-reach-firewall`
|
||||
- **Branch**: `phase/01-web-foundation-reach-firewall`
|
||||
- **REQs covered**: REQ-040 (Nomad Reach signup Web UI), REQ-045 (lexicon firewall extension to `web/`)
|
||||
- **Tag**: `v0.5.1`
|
||||
- **Type**: `feat+test`
|
||||
- **Goal**: Ship the web foundation (Go 1.22 `net/http.ServeMux` mock server in `web/`, base templates, vendored HTMX, in-memory mock store importing `x/*/types`) + the "Create a Reach" signup form (POST) that atomically creates a Reach (`IsNomad=true`) + a Stash (D-071) + Reach list/detail views + the `lexicon_meta_web/` firewall extension (REQ-045, firewall-first). After P1, the UI is runnable via `go run ./web` and a visitor can create a Reach.
|
||||
|
||||
### Wave 1 — Lexicon firewall FIRST (parallel with Wave 2 foundation; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-01-01 | REQ-045, D-069 | backend-engineer | `lexicon_meta_web/lexicon_meta_web_test.go` (NEW; package `lexicon_meta_web`) | **NEW sibling meta-test** mirroring `lexicon_meta_docs/lexicon_meta_docs_test.go` (D-069). Uses the SAME `lexicon.FindBannedTerm` (word-boundary, case-insensitive) — NO detection reimplementation. Walks the repo root; targets `web/templates/**/*.html` + `web/static/**/*.js` + `web/**/*.go` (production + test). Excludes `.ciagent/` (firewall meta-files), `.git/` (VCS), the meta-test file itself (self-exclusion via `runtime.Caller(0)`), and non-`.{html,js,go}` files under `web/`. Includes the G-009 self-test table (consumes `lexicon.SyntheticBannedStrings()` — G-014 shared helper from REQ-029), `TestLexiconMetaWebBannedTermsCount` (exactly 10), `TestLexiconMetaWebNoFalsePositiveOnOpenYield` (word-boundary does not match "openyield"/"european"), and `TestLexiconMetaWebWalkCoverage` (G-013 — injects a synthetic banned-term file into `web/templates/.lexicon_fixture/` and asserts the walk FINDS it). The firewall PASSES at P1 Wave 1 with zero `web/` content (a walk that scans nothing reports green on zero hits — closed by the walk-coverage test). **"Sign up" maps to "Create a Reach"** — the word "account" is banned (REQ-012); the firewall enforces this on all UI strings. | `go test ./lexicon_meta_web/...` green (invoked as `go test -run TestLexiconMetaWeb ./...`); self-test table passes for all 10 banned terms; `TestLexiconMetaWebNoFalsePositiveOnOpenYield` green; `TestLexiconMetaWebWalkCoverage` finds the fixture; a deliberately-injected banned term in a `web/templates/*.html` file fails the test | — |
|
||||
|
||||
### Wave 2 — Web foundation: main.go + mock store + base templates + HTMX (parallel with Wave 1; no internal deps)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-02-01 | REQ-040, D-066, D-067, D-068 | frontend-engineer | `web/main.go`, `web/server.go` | `web/main.go`: `package main; func main()` calling `server.go`'s `runServer()`. `web/server.go`: Go 1.22 `net/http.ServeMux` (`mux := http.NewServeMux()`; Go 1.22 method-pattern routing `mux.HandleFunc("GET /", ...)`); serves `web/static/` via `http.FileServer` (HTMX vendored); loads templates via `html/template` (`template.ParseGlob("web/templates/*.html")`); constructs the mock store (`store.NewStore()` from P1-03-01) and injects it into handlers. Listens on `:8080` (env-overridable `PORT`). No external deps (stdlib only — G-006). No `app.go`, no Cosmos runtime. | `go build ./web` succeeds; `go run ./web` starts a server on `:8080` (manual check: `curl -s http://localhost:8080/` returns the home page); `go.mod` unchanged (zero new require lines); lexicon firewall green on `web/main.go` + `web/server.go` | — |
|
||||
| P1-02-02 | REQ-040, D-066 | frontend-engineer | `web/static/htmx.min.js` (vendored), `web/static/style.css` | Vendored HTMX 1.x minified JS (a single static asset — NOT a `go get`; G-006 preserved). `style.css`: minimal lexicon-clean CSS for the 5 screens (no banned terms in comments/class names). HTMX attributes used for progressive enhancement (form POST → swap). | `web/static/htmx.min.js` exists (vendored, not a go.mod entry); `go build ./web` succeeds; `go run ./web` serves `/static/htmx.min.js` (manual check: `curl -s http://localhost:8080/static/htmx.min.js` returns the JS); lexicon firewall green on `web/static/**` | — |
|
||||
| P1-02-03 | REQ-040 | frontend-engineer | `web/templates/base.html`, `web/templates/home.html` | `base.html`: the shared layout (head, nav, `{{block "content" .}}{{end}}`, HTMX script tag, footer). Nav links to all 5 screens (Reach signup, Stash dashboard, Window authorization, Standing progress, Bloom accrual) — P2..P5 screens link to placeholder routes that P2..P5 fill in. `home.html`: site home with a one-paragraph OpenYield overview (lexicon-clean — "real production"/"Holder"/"Reach"/"Stash"; NOT "yield"/"account"/"bank") + links to the 5 screens. Lexicon-clean by construction (the P1-01-01 firewall scans these as they land). | `web/templates/base.html` + `web/templates/home.html` exist; `go run ./web` serves `/` (home renders); nav has 5 links; lexicon firewall green | P1-01-01, P1-02-01 |
|
||||
|
||||
### Wave 3 — Mock store (imports x/*/types) + import-invariant test (blocked-by Wave 2)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-03-01 | REQ-040, D-067, D-070, D-071 | backend-engineer | `web/store/store.go`, `web/store/fixtures.go` | `web/store/store.go`: in-memory mock store (`type Store struct { mu sync.Mutex; reaches map[string]identitytypes.Reach; stashes map[string]stashtypes.Stash; ... }`). `NewStore()` seeds from `fixtures.go`. Methods: `CreateReach(holderID, publicKey string) (identitytypes.Reach, stashtypes.Stash, error)` — **atomic Reach + Stash creation per D-071**: creates a `Reach` with `IsNomad=true` + a `Stash` with `HolderID` matching the Reach's `HolderID` and `BalanceGrain` seeded to a test value (e.g., 500000 Grain = 50 Bread per D-071 example); `ListReaches() []identitytypes.Reach`; `GetReach(reachID string) (identitytypes.Reach, bool)`; `GetStash(holderID string) (stashtypes.Stash, bool)`. Imports `x/identity/types`, `x/stash/types` (app-layer consumption — D-070; NOT a cross-`x/` import). `fixtures.go`: seed data (a few pre-existing Reach/Stash pairs for the list view). All strings lexicon-clean ("Holder"/"Reach"/"Stash"; NOT "account"/"bank"/"deposit"). | `go build ./web/store` succeeds; `go test ./web/store` passes (P1-03-02); `CreateReach` returns a Reach with `IsNomad=true` AND a Stash with matching `HolderID` (atomic — D-071); `web/store` imports only `x/identity/types` + `x/stash/types` (no `x/*/keeper` — verified by P1-03-03); lexicon firewall green | P1-02-01 |
|
||||
| P1-03-02 | REQ-040, D-033 | backend-engineer | `web/store/store_test.go` | Table-driven tests: `CreateReach` atomicity (Reach `IsNomad=true` + Stash `HolderID` matches + `BalanceGrain` seeded); `ListReaches` returns seeded + created; `GetReach` hit/miss; `GetStash` hit/miss; concurrent `CreateReach` (mutex safety — two goroutines, distinct holder IDs, no race). Coverage ≥80% on `web/store`. | `go test ./web/store` passes; coverage ≥80% on `web/store`; atomic-create test asserts both Reach + Stash exist after one call; lexicon firewall green | P1-03-01 |
|
||||
| P1-03-03 | REQ-040, D-070, G-003 | backend-engineer | `web/store/import_test.go` | **G-003 boundary test for `web/`**: scans all non-test `.go` files under `web/` using `go/parser` (or `go/build` import list) and asserts `web/` imports ONLY `x/*/types` packages (NOT `x/*/keeper` — there is no keeper in v0.6). This is the app-layer-consumption invariant (D-070): `web/` may consume protocol types but must not reach into keeper/state machinery. The existing v0.2 G-003 import-invariant test (scanning `x/**/*.go`) is UNCHANGED — `web/` is outside `x/` so it is not in that test's scope; this NEW test covers the `web/` scope. | `go test ./web/store` passes; the import-invariant test asserts no `x/*/keeper` import in any `web/` production file; `x/*/types` imports are allowed (D-070) | P1-03-01 |
|
||||
|
||||
### Wave 4 — Reach signup handler + templates + tests (blocked-by Wave 3 store)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-04-01 | REQ-040, D-071 | frontend-engineer | `web/handlers/reach.go`, `web/handlers/server.go` (handler wiring) | `web/handlers/reach.go`: `ReachHandler` struct holding `*store.Store` + `*template.Template`. Routes (Go 1.22 method patterns, wired in `web/server.go`): `GET /reach` → list view (`ListReaches`); `GET /reach/{id}` → detail view (`GetReach` + `GetStash`); `GET /reach/new` → "Create a Reach" form (lexicon-clean: "Create a Reach", NOT "Sign up for an account" — "account" is banned); `POST /reach` → form handler calling `store.CreateReach` (atomic Reach + Stash per D-071), redirect to the new Reach detail. HTMX: the form POST can be progressive-enhanced (`hx-post="/reach"` → swap). `web/handlers/server.go`: handler constructor + route registration helper. | `go build ./web` succeeds; `go run ./web` serves `GET /reach` (list), `GET /reach/new` (form), `POST /reach` (creates + redirects); manual browser check: fill the form → a Reach + Stash appear; lexicon firewall green on `web/handlers/reach.go` | P1-03-01, P1-02-03 |
|
||||
| P1-04-02 | REQ-040 | frontend-engineer | `web/templates/reach_list.html`, `web/templates/reach_detail.html`, `web/templates/reach_new.html` | `reach_list.html`: table of Reaches (ReachID, HolderID, IsNomad, IsFreeholder) + "Create a Reach" link. `reach_detail.html`: Reach fields + the associated Stash (BalanceGrain). `reach_new.html`: the "Create a Reach" form (HolderID + PublicKey inputs; submit POST `/reach`). All lexicon-clean ("Holder"/"Reach"/"Stash"; NOT "account"/"bank"/"deposit"). Extends `base.html`. | `go run ./web` renders all 3 Reach templates; manual browser check: form submission creates a Reach visible in the list; lexicon firewall green on all 3 templates | P1-04-01, P1-02-03 |
|
||||
| P1-04-03 | REQ-040, D-033 | frontend-engineer + backend-engineer | `web/handlers/reach_test.go` | Handler tests using `httptest.NewRecorder` + `httptest.NewRequest` (stdlib — no external test deps): `GET /reach` returns 200 + list HTML; `GET /reach/{id}` returns 200 + detail HTML for a seeded Reach; `GET /reach/new` returns 200 + form HTML; `POST /reach` with valid form creates a Reach + Stash atomically (assert both in the store) + redirects (302) to the detail; `POST /reach` with empty HolderID returns 400; the rendered HTML contains NO banned terms (assert via `lexicon.FindBannedTerm` on the response body — a per-handler lexicon check, complementing the file-scan firewall). Coverage ≥80% on `web/handlers` (P1 scope). | `go test ./web/handlers` passes; coverage ≥80% on `web/handlers` (P1 subset); atomic-create asserted via the store after POST; rendered-HTML lexicon check green | P1-04-01, P1-04-02 |
|
||||
|
||||
### Wave 5 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P1-99-01 | REQ-012, REQ-040, REQ-045 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green (incl. all v0.1..v0.5 baseline + P1 web + the new `lexicon_meta_web/` firewall); coverage ≥80% on `web/store` + `web/handlers` (P1 subset); `lexicon_meta_web/` firewall green; `lexicon_meta_test.go` (v0.2, x/*.go) + `lexicon_meta_docs_test.go` (v0.3, docs) green (no regression); `go.mod` unchanged (G-006 — zero new require lines); `go run ./web` starts on `:8080` (manual check); tag `v0.5.1`. | `go test ./...` green; coverage ≥80% on `web/store` + `web/handlers`; all 3 lexicon firewalls green; `go.mod` unchanged; `go run ./web` serves the home + Reach screens; git tag `v0.5.1` created | P1-01-01, P1-04-03, P1-03-02, P1-03-03 |
|
||||
|
||||
### P1 Must-Haves
|
||||
- [ ] `lexicon_meta_web/lexicon_meta_web_test.go` exists (package `lexicon_meta_web`); mirrors `lexicon_meta_docs/` detection (same `lexicon.FindBannedTerm` + word-boundary + G-009 self-test via `lexicon.SyntheticBannedStrings()` + G-013 walk-coverage + self-exclusion); scans `web/templates/**/*.html` + `web/static/**/*.js` + `web/**/*.go`; excludes `.ciagent/` + `.git/` + itself.
|
||||
- [ ] `go test ./lexicon_meta_web/...` green (firewall passes with P1 web content).
|
||||
- [ ] `go test ./...` green across the whole repo (no regression; v0.2/v0.3 lexicon firewalls unchanged).
|
||||
- [ ] `web/main.go` + `web/server.go` exist; Go 1.22 `net/http.ServeMux` + `html/template`; `go run ./web` starts on `:8080` with no external deps.
|
||||
- [ ] `web/static/htmx.min.js` vendored (NOT a `go get`); `go.mod` unchanged (G-006).
|
||||
- [ ] `web/store/store.go` imports `x/identity/types` + `x/stash/types` (D-070 app-layer consumption); `CreateReach` atomically creates a Reach (`IsNomad=true`) + a Stash (D-071); `web/store/import_test.go` asserts no `x/*/keeper` imports.
|
||||
- [ ] `web/handlers/reach.go` + 3 Reach templates exist; `GET /reach` (list), `GET /reach/{id}` (detail), `GET /reach/new` (form), `POST /reach` (atomic create + redirect).
|
||||
- [ ] "Create a Reach" labels used (NOT "Sign up for an account" — "account" is banned per REQ-012).
|
||||
- [ ] ≥80% coverage on `web/store` + `web/handlers` (P1 subset).
|
||||
- [ ] Rendered-HTML lexicon check green (per-handler test asserts no banned terms in response body).
|
||||
- [ ] Git tag `v0.5.1`.
|
||||
|
||||
### P1 Risks & Mitigations
|
||||
- **"account" lexicon drift in signup form** (highest P1 risk) → "Create a Reach" labels; the `lexicon_meta_web/` firewall scans `web/templates/reach_new.html`; the per-handler rendered-HTML lexicon check (P1-04-03) catches a banned term in dynamic content. Mitigation: firewall-first (P1-01-01 lands before content).
|
||||
- **HTMX vendored asset size / version** → HTMX 1.x minified is ~14KB; vendored as a static file, not a go.mod entry. Pin the version in a comment in `web/static/htmx.min.js` header.
|
||||
- **Atomic Reach + Stash creation race (D-071)** → `CreateReach` holds the store mutex across both map writes; the concurrent-create test (P1-03-02) asserts no race.
|
||||
- **G-003 boundary confusion** → `web/` importing `x/*/types` is app-layer consumption (D-070), NOT a cross-`x/` import; the NEW `web/store/import_test.go` (P1-03-03) documents and enforces the boundary (only `x/*/types`, never `x/*/keeper`).
|
||||
|
||||
---
|
||||
|
||||
## Phase P2 — Stash Dashboard (REQ-041) → v0.5.2
|
||||
|
||||
- **Slug**: `stash-dashboard`
|
||||
- **Branch**: `phase/02-stash-dashboard`
|
||||
- **REQs covered**: REQ-041 (Stash dashboard — balance in Grain + Bread-scale conversion + 90-day maturity progress)
|
||||
- **Tag**: `v0.5.2`
|
||||
- **Type**: `feat`
|
||||
- **Goal**: Ship the Stash dashboard screen: balance in Grain + Bread-scale conversion (using `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000` per D-073 — code constants are the source of truth, NOT docs) + 90-day maturity progress bar (`x/stash/types.StashActivity.IsMature`, `MaturityThresholdDays=90`). Depends on P1 (a Stash exists, created atomically at signup per D-071).
|
||||
|
||||
### Wave 1 — Store extensions + handler + template (parallel where possible; blocked-by P1 ship)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-01-01 | REQ-041, D-073 | backend-engineer | `web/store/store.go` (EXTEND), `web/store/fixtures.go` (EXTEND) | Extend the mock store: add `GetStashActivity(stashID string) (stashtypes.StashActivity, bool)` + seed `StashActivity` fixtures (some mature, some not — `ActiveDays` varying around `MaturityThresholdDays=90`; `MaxGapDays` varying around `MaxGapForMaturity=30`). Import `x/stash/types` (already imported in P1). The Bread-scale conversion is computed in the handler/template from `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000` (D-073 — code constants, NOT `docs/shared/bread-scale.md` which is outdated). | `go build ./web/store` succeeds; `GetStashActivity` returns seeded activities; `web/store` still imports only `x/*/types` (P1-03-03 import-invariant green); lexicon firewall green | P1-99-01 |
|
||||
| P2-02-01 | REQ-041, D-073 | frontend-engineer | `web/handlers/stash.go`, `web/handlers/server.go` (EXTEND route wiring) | `StashHandler` struct. Route: `GET /stash/{holderID}` → dashboard. Loads `Stash` (balance `BalanceGrain` in Grain) + `StashActivity` from the store; computes Bread-scale conversion by calling `x/bread/types.BreadScaleAll()` (returns the 11-denomination table) and `x/bread/types.GrainsPerBread` (10000) to convert the Grain balance into Bread (and display the full scale table for context). Computes 90-day maturity progress: `ActiveDays / MaturityThresholdDays` (as a percentage; capped at 100%) + `IsMature()` boolean (`stashtypes.StashActivity.IsMature()` — `ActiveDays >= 90 && MaxGapDays <= 30`). Passes all to the template. | `go build ./web` succeeds; `go run ./web` serves `GET /stash/{holderID}` (200 + dashboard HTML); Bread-scale conversion uses `BreadScaleAll()` + `GrainsPerBread=10000` (D-073 — verified by a test asserting the conversion matches the code constants); lexicon firewall green | P2-01-01, P1-02-03 |
|
||||
| P2-02-02 | REQ-041 | frontend-engineer | `web/templates/stash.html` | Stash dashboard template: balance in Grain + Bread-scale conversion table (all 11 denominations from `BreadScaleAll()` — Grain, Crumb, Bread, Loaf, Batch, Cake, Bakery, Granary, Mill, Harvest, Earth) + 90-day maturity progress bar (CSS width = `ActiveDays/90 * 100%`) + `IsMature` badge (green if mature, amber if not). Extends `base.html`. Lexicon-clean ("Stash"/"Grain"/"Bread"/"maturity"; NOT "bank"/"deposit"/"savings"/"interest"). | `go run ./web` renders the Stash dashboard; manual check: balance shows in Grain + Bread scale; progress bar reflects `ActiveDays`; lexicon firewall green on `web/templates/stash.html` | P2-02-01, P1-02-03 |
|
||||
|
||||
### Wave 2 — Tests (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-03-01 | REQ-041, D-073, D-033 | frontend-engineer + backend-engineer | `web/handlers/stash_test.go`, `web/store/store_test.go` (EXTEND) | Handler test (`httptest`): `GET /stash/{holderID}` returns 200 + dashboard HTML; the rendered HTML shows the Grain balance + the Bread-scale conversion table (all 11 denominations) + the maturity progress bar. **Bread-scale conversion correctness test**: asserts the handler's conversion matches `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000` (D-073 — e.g., 500000 Grain = 50 Bread; 100 Grain = 1 Crumb); a test that would FAIL if the handler used the outdated `docs/shared/bread-scale.md` values (1,000× ratios) instead of the code constants. **Maturity progress test**: a mature fixture (`ActiveDays=90, MaxGapDays=10` → `IsMature()==true`, progress 100%) vs an immature fixture (`ActiveDays=45, MaxGapDays=10` → `IsMature()==false`, progress 50%). Store test: `GetStashActivity` hit/miss. Rendered-HTML lexicon check (no banned terms in response body). Coverage ≥80% on `web/handlers` (P1+P2 cumulative). | `go test ./web/...` passes; Bread-scale conversion matches code constants (D-073); maturity progress matches `IsMature()`; coverage ≥80% on `web/handlers` (cumulative); rendered-HTML lexicon check green | P2-02-02, P2-01-01 |
|
||||
|
||||
### Wave 3 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P2-99-01 | REQ-012, REQ-041 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; coverage ≥80% on `web/store` + `web/handlers` (cumulative); `lexicon_meta_web/` firewall green (now scans `web/templates/stash.html` + `web/handlers/stash.go`); `go.mod` unchanged; `go run ./web` serves the Stash dashboard; tag `v0.5.2`. | `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; Stash dashboard reachable from home; git tag `v0.5.2` | P2-03-01 |
|
||||
|
||||
### P2 Must-Haves
|
||||
- [ ] `web/handlers/stash.go` + `web/templates/stash.html` exist; `GET /stash/{holderID}` renders the dashboard.
|
||||
- [ ] Balance shown in Grain + Bread-scale conversion using `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000` (D-073 — code constants, NOT docs).
|
||||
- [ ] 90-day maturity progress bar + `IsMature` badge using `x/stash/types.StashActivity.IsMature()` + `MaturityThresholdDays=90` + `MaxGapForMaturity=30`.
|
||||
- [ ] Bread-scale conversion correctness test (would fail if docs values were used instead of code constants — D-073 regression guard).
|
||||
- [ ] ≥80% coverage on `web/store` + `web/handlers` (cumulative).
|
||||
- [ ] `lexicon_meta_web/` firewall green; rendered-HTML lexicon check green.
|
||||
- [ ] `go.mod` unchanged (G-006).
|
||||
- [ ] Git tag `v0.5.2`.
|
||||
|
||||
### P2 Risks & Mitigations
|
||||
- **Bread-scale source-of-truth drift (D-073)** → the conversion correctness test asserts the handler uses `BreadScaleAll()` + `GrainsPerBread=10000` (code constants); a test using the outdated `docs/shared/bread-scale.md` 1,000× ratios would fail. The doc-fix is a P1+ follow-up (NOT v0.6 scope — PROJECT.md out-of-scope).
|
||||
- **Maturity progress > 100%** → cap `ActiveDays/90` at 100% in the template/handler; test the cap.
|
||||
|
||||
---
|
||||
|
||||
## Phase P3 — Window Authorization (REQ-042) → v0.5.3
|
||||
|
||||
- **Slug**: `window-authorization`
|
||||
- **Branch**: `phase/03-window-authorization`
|
||||
- **REQs covered**: REQ-042 (Window authorization — open Window (scope+duration+rate-limit), lifecycle Open→Active→Revoked/Expired via `Window.Activate/Revoke/Expire`, audit log)
|
||||
- **Tag**: `v0.5.3`
|
||||
- **Type**: `feat`
|
||||
- **Goal**: Ship the Window authorization screen: a form to open a Window (scope + duration + rate-limit) delegating to a service, a lifecycle view (Open→Active→Revoked/Expired using `x/window/types.Window.Activate/Revoke/Expire`), and an audit log view (`AuditEntry`). Depends on P2 (Window scope references a Stash).
|
||||
|
||||
### Wave 1 — Store extensions + handler + template (blocked-by P2 ship)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-01-01 | REQ-042 | backend-engineer | `web/store/store.go` (EXTEND), `web/store/fixtures.go` (EXTEND) | Extend the mock store: add `windows map[string]windowtypes.Window` + `auditLogs map[string][]windowtypes.AuditEntry`. Methods: `OpenWindow(grantorHolder, grantee string, scope windowtypes.Scope, start, end int64, rateLimit windowtypes.RateLimit) (windowtypes.Window, error)` (creates a Window status=Open + an initial `AuditEntry`); `ActivateWindow(windowID string) error` (calls `Window.Activate()` — Open→Active); `RevokeWindow(windowID string) error` (calls `Window.Revoke()` — idempotent; Revoked/Expired are no-ops); `ExpireWindow(windowID string)` (calls `Window.Expire()`); `ListWindows(grantorHolder string) []windowtypes.Window`; `GetWindow(windowID string) (windowtypes.Window, bool)`; `GetAuditLog(windowID string) []windowtypes.AuditEntry`; `AppendAuditEntry(windowID string, entry windowtypes.AuditEntry)`. Import `x/window/types` (app-layer — D-070). Seed fixtures: a few Windows in various lifecycle states (Open, Active, Revoked, Expired) + audit logs. | `go build ./web/store` succeeds; `OpenWindow` creates a Window status=Open; `ActivateWindow` transitions Open→Active; `RevokeWindow` transitions to Revoked (idempotent on already-Revoked/Expired); `ExpireWindow` transitions to Expired; `web/store` still imports only `x/*/types` (import-invariant green); lexicon firewall green | P2-99-01 |
|
||||
| P3-02-01 | REQ-042 | frontend-engineer | `web/handlers/window.go`, `web/handlers/server.go` (EXTEND) | `WindowHandler` struct. Routes: `GET /window` → list of Windows for a holder; `GET /window/{id}` → detail (lifecycle state + scope + rate-limit + audit log); `GET /window/new` → "Open a Window" form (scope kind dropdown from `ScopeKind` enum: ReadStash/ReadStanding/ProcessPassActForStand; resource-id; grantee; duration start/end; rate-limit max-actions + per-duration); `POST /window` → `OpenWindow` + redirect to detail; `POST /window/{id}/activate` → `ActivateWindow` (HTMX swap); `POST /window/{id}/revoke` → `RevokeWindow`; `POST /window/{id}/expire` → `ExpireWindow`. Each lifecycle action appends an `AuditEntry`. | `go build ./web` succeeds; `go run ./web` serves the 4 Window routes; lifecycle transitions call `Window.Activate/Revoke/Expire` (verified by test); lexicon firewall green | P3-01-01, P1-02-03 |
|
||||
| P3-02-02 | REQ-042 | frontend-engineer | `web/templates/window_list.html`, `web/templates/window_detail.html`, `web/templates/window_new.html` | `window_list.html`: table of Windows (WindowID, GrantorHolder, Grantee, Scope, Status) + "Open a Window" link. `window_detail.html`: Window fields + lifecycle state badge (Open=amber, Active=green, Revoked=red, Expired=grey) + Activate/Revoke/Expire buttons (HTMX `hx-post`) + audit log table (`AuditEntry` rows: timestamp, action, result, granter-ref). `window_new.html`: the "Open a Window" form. Extends `base.html`. Lexicon-clean ("Window"/"scope"/"Holder"/"Reach"; NOT "account"/"bank"). | `go run ./web` renders all 3 Window templates; manual check: open a Window → activate → revoke → audit log shows the actions; lexicon firewall green on all 3 templates | P3-02-01, P1-02-03 |
|
||||
|
||||
### Wave 2 — Tests (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-03-01 | REQ-042, D-033 | frontend-engineer + backend-engineer | `web/handlers/window_test.go`, `web/store/store_test.go` (EXTEND) | Handler tests (`httptest`): `GET /window` (list 200); `GET /window/{id}` (detail 200 + lifecycle badge + audit log); `GET /window/new` (form 200); `POST /window` (open → 302 to detail; new Window status=Open + initial AuditEntry); `POST /window/{id}/activate` (Open→Active; AuditEntry appended); `POST /window/{id}/revoke` (→Revoked; idempotent — second revoke is a no-op, no new AuditEntry); `POST /window/{id}/expire` (→Expired; revoke-after-expire is a no-op). **Lifecycle correctness test**: asserts the handler calls `Window.Activate/Revoke/Expire` (the real `x/window/types` methods — not a reimplementation); `Revoke()` on an Expired window is a no-op (matches the v0.2 type contract). Store tests: `OpenWindow`/`ActivateWindow`/`RevokeWindow`/`ExpireWindow`/`ListWindows`/`GetAuditLog` round-trips. Rendered-HTML lexicon check. Coverage ≥80% on `web/handlers` (P1+P2+P3 cumulative). | `go test ./web/...` passes; lifecycle transitions match `Window.Activate/Revoke/Expire`; revoke-on-expired no-op; coverage ≥80% (cumulative); rendered-HTML lexicon check green | P3-02-02, P3-01-01 |
|
||||
|
||||
### Wave 3 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P3-99-01 | REQ-012, REQ-042 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; `go run ./web` serves the Window screen; tag `v0.5.3`. | `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; Window screen reachable from home; git tag `v0.5.3` | P3-03-01 |
|
||||
|
||||
### P3 Must-Haves
|
||||
- [ ] `web/handlers/window.go` + 3 Window templates exist; `GET /window` (list), `GET /window/{id}` (detail + lifecycle + audit log), `GET /window/new` (form), `POST /window` (open), `POST /window/{id}/activate|revoke|expire` (lifecycle).
|
||||
- [ ] Lifecycle transitions call `x/window/types.Window.Activate/Revoke/Expire` (the real methods); `Revoke()` on Expired is a no-op (v0.2 type contract).
|
||||
- [ ] Audit log (`AuditEntry`) appended on each lifecycle action; displayed in the detail view.
|
||||
- [ ] Window form uses `ScopeKind` enum (ReadStash/ReadStanding/ProcessPassActForStand) + rate-limit fields.
|
||||
- [ ] ≥80% coverage on `web/store` + `web/handlers` (cumulative).
|
||||
- [ ] `lexicon_meta_web/` firewall green; rendered-HTML lexicon check green.
|
||||
- [ ] `go.mod` unchanged (G-006).
|
||||
- [ ] Git tag `v0.5.3`.
|
||||
|
||||
### P3 Risks & Mitigations
|
||||
- **Lifecycle state-machine divergence from the type contract** → the handler MUST call `Window.Activate/Revoke/Expire` (not reimplement transitions); the lifecycle correctness test asserts the real methods are invoked.
|
||||
- **Revoke-on-Expired no-op** → the v0.2 `Revoke()` returns nil on Expired (terminal state wins); the test covers this edge.
|
||||
|
||||
---
|
||||
|
||||
## Phase P4 — Standing + Freeholder Signals (REQ-043) → v0.5.4
|
||||
|
||||
- **Slug**: `standing-freeholder-signals`
|
||||
- **Branch**: `phase/04-standing-freeholder-signals`
|
||||
- **REQs covered**: REQ-043 (Standing + Freeholder signals progress — computed from mock `Rating`/`Vouch`/`Slash` using locked constants + `GetStandingBucket`/`ComputeDiversityBonus`/`GetVoucherWeight`; 4-signal progress via `FreeholderSignals.IsFreeholderEligible`)
|
||||
- **Tag**: `v0.5.4`
|
||||
- **Type**: `feat`
|
||||
- **Goal**: Ship the Standing + Freeholder signals progress screen: computed from mock `Rating`/`Vouch`/`Slash` records using the locked formula constants + `GetStandingBucket`/`ComputeDiversityBonus`/`GetVoucherWeight`; displays the 4-signal progress (`FreeholderSignals.IsFreeholderEligible` — StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement). Depends on P1 (Standing is per-Reach).
|
||||
|
||||
### Wave 1 — Store extensions + handler + template (blocked-by P3 ship)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-01-01 | REQ-043 | backend-engineer | `web/store/store.go` (EXTEND), `web/store/fixtures.go` (EXTEND) | Extend the mock store: add `ratings map[string][]standingtypes.Rating` (per-ratee), `vouches map[string][]standingtypes.Vouch`, `slashes map[string][]standingtypes.Slash`. Methods: `ListRatings(rateeID string) []standingtypes.Rating`; `ListVouches(voucheeID string) []standingtypes.Vouch`; `ListSlashes(reachID string) []standingtypes.Slash`; `ComputeStandingScore(reachID string) (float64, standingtypes.StandingBucket)` — computes a simplified standing score from the mock Ratings using the locked constants (`PriorMean=4.0`, `PriorWeight=10`, decay buckets, `ComputeDiversityBonus`, `GetVoucherWeight`) + `GetStandingBucket(score, ratingCount, isSlashed)`; `ComputeFreeholderSignals(reachID string) standingtypes.FreeholderSignals` — computes the 4 signals: `StashMaturity` (from `StashActivity.IsMature()` — P2 store method), `MultiDomainStanding` (score >= `FreeholderMinStandingScore=4.5` in >= `FreeholderMinCategories=3`), `CommittedCapital` (mock: Stash balance >= a threshold), `CommunityEndorsement` (>= 1 Vouch). Import `x/standing/types` + `x/stash/types` (app-layer — D-070). Seed fixtures: a Reach with enough Ratings/Vouches to be Freeholder-eligible + one that is not. | `go build ./web/store` succeeds; `ComputeStandingScore` returns a score + bucket using the locked constants; `ComputeFreeholderSignals` returns 4 booleans; `web/store` imports only `x/*/types` (import-invariant green); lexicon firewall green | P3-99-01 |
|
||||
| P4-02-01 | REQ-043 | frontend-engineer | `web/handlers/standing.go`, `web/handlers/server.go` (EXTEND) | `StandingHandler` struct. Route: `GET /standing/{reachID}` → Standing + Freeholder signals progress. Loads Ratings/Vouches/Slashes from the store; calls `ComputeStandingScore` + `ComputeFreeholderSignals`; passes the score, bucket, 4 signals (each as a progress indicator), and the underlying records to the template. | `go build ./web` succeeds; `go run ./web` serves `GET /standing/{reachID}` (200 + progress HTML); the score is computed from the locked constants (verified by test); lexicon firewall green | P4-01-01, P1-02-03 |
|
||||
| P4-02-02 | REQ-043 | frontend-engineer | `web/templates/standing.html` | Standing + Freeholder signals template: standing score (float, 1 decimal) + `StandingBucket` badge (New/Trusted/Preferred/Top/Slashed) + 4-signal progress (StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement — each a check/cross + label) + Freeholder-eligible badge (green if `IsFreeholderEligible()`, grey if not) + underlying Ratings/Vouches/Slashes tables. Extends `base.html`. Lexicon-clean ("Standing"/"Freeholder"/"Vouch"/"Rating"; NOT "account"/"bank"). | `go run ./web` renders the Standing screen; manual check: a seeded Freeholder-eligible Reach shows 4 checks + green badge; a non-eligible Reach shows crosses; lexicon firewall green on `web/templates/standing.html` | P4-02-01, P1-02-03 |
|
||||
|
||||
### Wave 2 — Tests (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-03-01 | REQ-043, D-033 | frontend-engineer + backend-engineer | `web/handlers/standing_test.go`, `web/store/store_test.go` (EXTEND) | Handler test (`httptest`): `GET /standing/{reachID}` returns 200 + progress HTML; the rendered HTML shows the score + bucket + 4 signals + Freeholder-eligible badge. **Standing score computed-from-locked-constants test**: asserts `ComputeStandingScore` uses `x/standing/types` constants (`PriorMean=4.0`, `PriorWeight=10`, `ComputeDiversityBonus`, `GetVoucherWeight`, `GetStandingBucket`) — a test that would FAIL if the handler hardcoded a score instead of computing from the locked constants. **Freeholder-eligible badge test**: a seeded eligible Reach (`IsFreeholderEligible()==true` → green badge) vs a non-eligible Reach (a signal false → grey badge); asserts the badge reflects `IsFreeholderEligible()`. Store tests: `ListRatings`/`ListVouches`/`ListSlashes`/`ComputeStandingScore`/`ComputeFreeholderSignals` round-trips. Rendered-HTML lexicon check. Coverage ≥80% on `web/handlers` (cumulative P1..P4). | `go test ./web/...` passes; standing score computed from locked constants; Freeholder-eligible badge reflects `IsFreeholderEligible()`; coverage ≥80% (cumulative); rendered-HTML lexicon check green | P4-02-02, P4-01-01 |
|
||||
|
||||
### Wave 3 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P4-99-01 | REQ-012, REQ-043 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; `go run ./web` serves the Standing screen; tag `v0.5.4`. | `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; Standing screen reachable from home; git tag `v0.5.4` | P4-03-01 |
|
||||
|
||||
### P4 Must-Haves
|
||||
- [ ] `web/handlers/standing.go` + `web/templates/standing.html` exist; `GET /standing/{reachID}` renders the progress.
|
||||
- [ ] Standing score computed from `x/standing/types` locked constants (`PriorMean`, `PriorWeight`, decay buckets, `ComputeDiversityBonus`, `GetVoucherWeight`, `GetStandingBucket`) — NOT hardcoded.
|
||||
- [ ] 4-signal progress (StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement) displayed; Freeholder-eligible badge reflects `FreeholderSignals.IsFreeholderEligible()`.
|
||||
- [ ] Standing-score computed-from-locked-constants test (regression guard against hardcoding).
|
||||
- [ ] ≥80% coverage on `web/store` + `web/handlers` (cumulative).
|
||||
- [ ] `lexicon_meta_web/` firewall green; rendered-HTML lexicon check green.
|
||||
- [ ] `go.mod` unchanged (G-006).
|
||||
- [ ] Git tag `v0.5.4`.
|
||||
|
||||
### P4 Risks & Mitigations
|
||||
- **Standing formula oversimplification** → the mock `ComputeStandingScore` is a SIMPLIFIED computation from the locked constants (not the full Bayesian formula — sub-tables deferred per PROJECT.md Q2); the test asserts it uses the locked constants, not that it matches a full oracle. Document the simplification in a code comment.
|
||||
- **Freeholder-eligible badge divergence** → the badge MUST reflect `IsFreeholderEligible()` (the real method); the test asserts the rendered badge matches the method output.
|
||||
|
||||
---
|
||||
|
||||
## Phase P5 — Bloom Accrual (REQ-044) → v0.5.5
|
||||
|
||||
- **Slug**: `bloom-accrual`
|
||||
- **Branch**: `phase/05-bloom-accrual`
|
||||
- **REQs covered**: REQ-044 (Bloom accrual — per-Stash `BloomRecord` view: `AccruedGrain`, `RateBasisPoints`, `LastAccrualBlock`; 4.5% target rate)
|
||||
- **Tag**: `v0.5.5`
|
||||
- **Type**: `feat`
|
||||
- **Goal**: Ship the Bloom accrual screen: a per-Stash `BloomRecord` view (`AccruedGrain`, `RateBasisPoints`, `LastAccrualBlock`) computed from mock data; shows the 4.5% target rate (`TargetBloomRateBasisPoints=450`). Depends on P2 (BloomRecord is per-Stash).
|
||||
|
||||
### Wave 1 — Store extensions + handler + template (blocked-by P4 ship)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-01-01 | REQ-044 | backend-engineer | `web/store/store.go` (EXTEND), `web/store/fixtures.go` (EXTEND) | Extend the mock store: add `bloomRecords map[string]bloomtypes.BloomRecord` (keyed by StashID). Methods: `GetBloomRecord(stashID string) (bloomtypes.BloomRecord, bool)`; `ListBloomRecords(holderID string) []bloomtypes.BloomRecord` (via the holder's Stashes). Import `x/bloom/types` (app-layer — D-070). Seed fixtures: `BloomRecord` per Stash with `AccruedGrain` (mock value), `RateBasisPoints` (e.g., 450 = the target rate), `LastAccrualBlock` (mock block height). | `go build ./web/store` succeeds; `GetBloomRecord`/`ListBloomRecords` return seeded records; `web/store` imports only `x/*/types` (import-invariant green); lexicon firewall green | P4-99-01 |
|
||||
| P5-02-01 | REQ-044 | frontend-engineer | `web/handlers/bloom.go`, `web/handlers/server.go` (EXTEND) | `BloomHandler` struct. Route: `GET /bloom/{stashID}` → Bloom accrual view. Loads the `BloomRecord` from the store; reads `x/bloom/types.TargetBloomRateBasisPoints=450` (4.5% target rate — D-073 code-constant source-of-truth) + `MinBloomRateBasisPoints=400` + `MaxBloomRateBasisPoints=500` + `AccrualPeriodBlocks=144`; passes the record + the target rate band to the template. | `go build ./web` succeeds; `go run ./web` serves `GET /bloom/{stashID}` (200 + accrual HTML); the target rate is read from `x/bloom/types.TargetBloomRateBasisPoints` (verified by test); lexicon firewall green | P5-01-01, P1-02-03 |
|
||||
| P5-02-02 | REQ-044 | frontend-engineer | `web/templates/bloom.html` | Bloom accrual template: `BloomRecord` fields (`AccruedGrain`, `RateBasisPoints` displayed as a percentage e.g. 4.5%, `LastAccrualBlock`) + the target rate band (4.0%–5.0%, target 4.5%) + a note that "Bloom originates only from real production" (the `MissionLockBloom` const, lexicon-clean). Extends `base.html`. Lexicon-clean ("Bloom"/"Grain"/"real production"; NOT "yield"/"interest"/"deposit"). | `go run ./web` renders the Bloom screen; manual check: accrued Grain + 4.5% target rate displayed; lexicon firewall green on `web/templates/bloom.html` | P5-02-01, P1-02-03 |
|
||||
|
||||
### Wave 2 — Tests (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-03-01 | REQ-044, D-033 | frontend-engineer + backend-engineer | `web/handlers/bloom_test.go`, `web/store/store_test.go` (EXTEND) | Handler test (`httptest`): `GET /bloom/{stashID}` returns 200 + accrual HTML; the rendered HTML shows `AccruedGrain` + `RateBasisPoints` (as %) + `LastAccrualBlock` + the 4.5% target rate. **Target-rate source-of-truth test**: asserts the handler reads `x/bloom/types.TargetBloomRateBasisPoints=450` (NOT a hardcoded 450 or a docs value — D-073 pattern); a test that would FAIL if the handler hardcoded the rate. Store tests: `GetBloomRecord`/`ListBloomRecords` round-trips. Rendered-HTML lexicon check (highest-risk screen for "yield"/"interest" drift — the test scans the response body). Coverage ≥80% on `web/handlers` (cumulative P1..P5). | `go test ./web/...` passes; target rate read from code constant (D-073); coverage ≥80% (cumulative); rendered-HTML lexicon check green (no "yield"/"interest") | P5-02-02, P5-01-01 |
|
||||
|
||||
### Wave 3 — Phase verification + ship
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P5-99-01 | REQ-012, REQ-044 | lead-developer | (cross-cutting) | `go build ./...` + `go test ./...` green; coverage ≥80% (cumulative across all `web/` packages); `lexicon_meta_web/` green; `go.mod` unchanged; `go run ./web` serves the Bloom screen; tag `v0.5.5`. | `go test ./...` green; coverage ≥80% (cumulative); `lexicon_meta_web/` green; `go.mod` unchanged; Bloom screen reachable from home; git tag `v0.5.5` | P5-03-01 |
|
||||
|
||||
### P5 Must-Haves
|
||||
- [ ] `web/handlers/bloom.go` + `web/templates/bloom.html` exist; `GET /bloom/{stashID}` renders the accrual view.
|
||||
- [ ] `BloomRecord` fields displayed: `AccruedGrain`, `RateBasisPoints` (as %), `LastAccrualBlock`.
|
||||
- [ ] 4.5% target rate read from `x/bloom/types.TargetBloomRateBasisPoints=450` (D-073 code-constant source-of-truth; NOT hardcoded).
|
||||
- [ ] Target-rate source-of-truth test (regression guard against hardcoding).
|
||||
- [ ] ≥80% coverage on `web/store` + `web/handlers` (cumulative P1..P5).
|
||||
- [ ] `lexicon_meta_web/` firewall green; rendered-HTML lexicon check green (no "yield"/"interest").
|
||||
- [ ] `go.mod` unchanged (G-006).
|
||||
- [ ] Git tag `v0.5.5`.
|
||||
|
||||
### P5 Risks & Mitigations
|
||||
- **"yield"/"interest" lexicon drift in Bloom prose** (highest P5 risk — Bloom is conceptually close to "yield") → "Bloom"/"real production"/"accrual" labels; the `lexicon_meta_web/` firewall scans `web/templates/bloom.html`; the rendered-HTML lexicon check (P5-03-01) scans the response body. "Bloom" is the vision lexicon (§6); "yield" is banned.
|
||||
- **Target-rate hardcoding** → the handler MUST read `TargetBloomRateBasisPoints` from `x/bloom/types` (D-073); the source-of-truth test asserts it.
|
||||
|
||||
---
|
||||
|
||||
## Phase P6 — Final Review + Audit + Milestone Ship → v0.5.6
|
||||
|
||||
- **Slug**: `final-review-audit-ship`
|
||||
- **Branch**: `phase/06-final-review-audit-ship`
|
||||
- **REQs covered**: all v0.6 REQs (REQ-040..REQ-045) — final coverage accounting; no new REQs (covers post-hoc fixes from REVIEW/AUDIT)
|
||||
- **Tag**: `v0.5.6` (IS the v0.6 milestone release; D-008)
|
||||
- **Type**: `final`
|
||||
- **Personas**: lead-developer (review/ship) + ci-security-auditor (ACTIVATED for the v0.6 milestone audit + feature purity gate)
|
||||
- **Goal**: Multi-persona review across P1..P5, audit (reconstruction test + feature purity gate: no breaking schema changes; G-003 production firewall intact — `web/` is app-layer, not an `x/` module; G-006 go.mod unchanged — HTMX is a vendored static asset, not a Go dep; locked-const firewall intact — no `x/` consts amended), milestone ship (merge to main, tag `v0.5.6` = v0.6 milestone release, release, delete all milestone branches).
|
||||
|
||||
### Wave 1 — Review + Audit (parallel)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P6-01-01 | — | lead-developer (review) | `.ciagent/oy/REVIEW.md` (NEW for v0.6) | Multi-persona code review across P1..P5. Adversarial probes: (1) does `go run ./web` start with no external deps (G-006); (2) are all 5 screens reachable from the home page; (3) does the `lexicon_meta_web/` firewall scan all `web/templates/**` + `web/static/**` + `web/**/*.go`; (4) does the Bread-scale conversion match `x/bread/types` code constants (D-073); (5) does the Standing score use the locked formula constants; (6) does the Freeholder-eligible badge reflect `IsFreeholderEligible()`; (7) do the Window lifecycle transitions call `Window.Activate/Revoke/Expire`; (8) are there any banned terms in any rendered page (rendered-HTML lexicon checks). Auto-apply P0 fixes; flag P1+ for post-hoc. | REVIEW.md v0.6 section written; P0 issues (if any) fixed in P6; P1+ flagged. | P1..P5 |
|
||||
| P6-02-01 | — | ci-security-auditor (audit) | `.ciagent/oy/AUDIT.md` (v0.6 section) | Audit: (1) reconstruction test (git log ↔ `.ciagent/` files for v0.6; each REQ-040..REQ-045 maps to a shipped UI screen / firewall); (2) file/branch/commit discipline (6 phase branches `phase/01-*`..`phase/06-*`; 6 patch tags `v0.5.1`..`v0.5.6`; D-072 ordering respected — firewall-first P1 before content P2..P5); (3) **feature purity gate**: no breaking schema changes (no `x/` module modified — `web/` is new app-layer, not an `x/` amendment); locked-const firewall intact (all v0.1..v0.5 consts unchanged — `web/` does not touch `x/` consts); G-003 production firewall intact (`web/` imports only `x/*/types` per D-070; the `web/store/import_test.go` import-invariant green; no `x/` module gains a production import of another `x/` module's types via `web/`); G-006 go.mod unchanged (HTMX is a vendored static asset, NOT a `go get` — zero new require lines in v0.6); (4) coverage ≥80% on all `web/` packages (`web/store`, `web/handlers`); (5) all 3 lexicon firewalls green (`lexicon_meta_test.go` x/*.go, `lexicon_meta_docs_test.go` docs, `lexicon_meta_web/` web); (6) manual browser check: `go run ./web` → all 5 screens reachable + the happy path works end-to-end. | AUDIT.md v0.6 section written; feature purity gate GREEN (no breaking schema changes; locked-const firewall intact; G-003 intact — `web/` is app-layer; G-006 go.mod unchanged); reconstruction test passes. | P1..P5 |
|
||||
|
||||
### Wave 2 — Ship (blocked-by Wave 1)
|
||||
|
||||
| Task ID | REQ | Persona | Files | Deliverable | Must-have verification | Blocked-by |
|
||||
|---|---|---|---|---|---|---|
|
||||
| P6-03-01 | REQ-040..REQ-045 | lead-developer (ship) | `.ciagent/oy/REQUIREMENTS.md`, `.ciagent/oy/ROADMAP.md` | Update REQUIREMENTS.md: mark REQ-040..REQ-045 → Complete (UI shipped). Update ROADMAP.md: mark v0.6 milestone COMPLETE; add the tag-line note that v0.6 shipped on the `v0.5.x` patch line (P0 → `v0.5.0`, P1..P5 → `v0.5.1..v0.5.5`, P6 → `v0.5.6` = milestone release, per D-008). | REQUIREMENTS.md status column updated for all 6 v0.6 REQs → Complete; ROADMAP.md v0.6 marked complete + tag-line note present. | P6-01-01, P6-02-01 |
|
||||
| P6-03-02 | (milestone) | lead-developer | (cross-cutting) | Final ship: merge `phase/06` → `milestone/v0.6-nomad-web-ui` → `main`; create milestone release tag `v0.5.6` (= v0.6 milestone release per D-008); delete the 6 phase branches (`phase/01-*`..`phase/06-*`) after merge; confirm `go build ./...` + `go test ./...` green at the `v0.5.6` tag; confirm `go run ./web` starts at the tag. | `v0.5.6` tag created on main; `go test ./...` green at the tag; `go run ./web` starts at the tag; ROADMAP.md v0.6 complete; phase branches deleted; release notes reference v0.6 scope (5-screen Nomad Web UI: Reach signup, Stash dashboard, Window authorization, Standing + Freeholder signals, Bloom accrual; Go html/template + HTMX vendored; mock server over real x/*/types; lexicon firewall extended to web/). | P6-03-01 |
|
||||
|
||||
### P6 Must-Haves
|
||||
- [ ] REVIEW.md v0.6 section written; P0 fixes applied.
|
||||
- [ ] AUDIT.md v0.6 section written; reconstruction test passes.
|
||||
- [ ] **Feature purity gate GREEN**: no breaking schema changes (no `x/` module modified); locked-const firewall intact (all v0.1..v0.5 consts unchanged); G-003 production firewall intact (`web/` imports only `x/*/types` per D-070; import-invariant green); G-006 go.mod unchanged (HTMX vendored, not a Go dep).
|
||||
- [ ] Coverage ≥80% on all `web/` packages (`web/store`, `web/handlers`).
|
||||
- [ ] All 3 lexicon firewalls green (`lexicon_meta_test.go` x/*.go, `lexicon_meta_docs_test.go` docs, `lexicon_meta_web/` web).
|
||||
- [ ] `go run ./web` starts at the `v0.5.6` tag; all 5 screens reachable; happy path works end-to-end.
|
||||
- [ ] REQUIREMENTS.md + ROADMAP.md mark v0.6 COMPLETE.
|
||||
- [ ] Tag `v0.5.6` created (= v0.6 milestone release).
|
||||
- [ ] Milestone branch merged to `main`.
|
||||
- [ ] All 6 phase branches deleted (local + remote).
|
||||
|
||||
### P6 Risks & Mitigations
|
||||
- **UI mock diverges from `x/*/types` contracts** → the mock store instantiates the REAL `x/*/types` structs (D-067); the handler tests assert the conversions match the code constants (D-073 for Bread-scale, locked formula constants for Standing, `TargetBloomRateBasisPoints` for Bloom); divergence is caught by the tests, not just the audit.
|
||||
- **G-006 dep exception confusion** → v0.6 adds NO Go deps (HTMX is a vendored static asset); the v0.5 cosmos-sdk/ibc-go deps stay but v0.6 adds nothing; the audit verifies `go.mod` is unchanged across the v0.6 milestone range.
|
||||
- **Milestone versioning confusion (v0.6 milestone = v0.5.6 tag)** → lead-developer enforces D-008: final phase patch IS the milestone release; no separate minor tag. ROADMAP tag-line note (P6-03-01) prevents `v0.5.6`/`v0.6.0` confusion.
|
||||
|
||||
---
|
||||
|
||||
## Coverage Targets (D-033) — v0.6
|
||||
|
||||
| Package | Phase | Target | Key tests |
|
||||
|---|---|---|---|
|
||||
| `web/store` | P1 (created), P2..P5 (extended) | ≥80% | Atomic Reach+Stash create (D-071); `GetStashActivity`/`IsMature` (P2); `OpenWindow`/`ActivateWindow`/`RevokeWindow`/`ExpireWindow` lifecycle (P3); `ComputeStandingScore`/`ComputeFreeholderSignals` from locked constants (P4); `GetBloomRecord` (P5); import-invariant (only `x/*/types`, no `x/*/keeper` — D-070) |
|
||||
| `web/handlers` | P1 (created), P2..P5 (extended) | ≥80% | Reach signup form + atomic create (P1); Stash dashboard + Bread-scale conversion correctness (P2, D-073); Window lifecycle via `Window.Activate/Revoke/Expire` (P3); Standing score from locked constants + Freeholder-eligible badge (P4); Bloom target-rate from code constant (P5); rendered-HTML lexicon checks (all phases) |
|
||||
| `lexicon_meta_web` | P1 | 100% (test-only) | G-009 self-test via `SyntheticBannedStrings()`; G-013 walk-coverage; no-false-positive on "openyield"/"european"; scans `web/templates/**` + `web/static/**` + `web/**/*.go` |
|
||||
|
||||
**Lexicon assertions (REQ-012)**: the NEW `lexicon_meta_web/` firewall (P1) scans all `web/**/*.{html,js,go}` for the 10 banned terms. Per-handler rendered-HTML lexicon checks (each phase's handler test) scan the HTTP response body for banned terms — catching dynamic content the file-scan firewall cannot see. The v0.2 `lexicon_meta_test.go` (x/*.go) + v0.3 `lexicon_meta_docs_test.go` (docs) are UNCHANGED (no regression).
|
||||
|
||||
---
|
||||
|
||||
## Task Count Summary — v0.6
|
||||
|
||||
| Phase | Waves | Tasks | New/Extended |
|
||||
|---|---|---|---|
|
||||
| P1 | 5 | 9 | `lexicon_meta_web/` (new firewall); `web/main.go`, `web/server.go`, `web/static/`, `web/templates/base.html`+`home.html`, `web/store/`, `web/handlers/reach.go` + 3 Reach templates |
|
||||
| P2 | 3 | 5 | `web/handlers/stash.go` + `web/templates/stash.html` (Stash dashboard) |
|
||||
| P3 | 3 | 5 | `web/handlers/window.go` + 3 Window templates (Window authorization) |
|
||||
| P4 | 3 | 5 | `web/handlers/standing.go` + `web/templates/standing.html` (Standing + Freeholder signals) |
|
||||
| P5 | 3 | 5 | `web/handlers/bloom.go` + `web/templates/bloom.html` (Bloom accrual) |
|
||||
| P6 | 2 | 4 | (review + audit + ship; 0 new — audit + REQUIREMENTS/ROADMAP update + tag) |
|
||||
| **Total** | — | **33** | **`web/` new dir (5 screens) + `lexicon_meta_web/` new firewall; zero `x/` modifications** |
|
||||
|
||||
## Per-Phase REQ Coverage — v0.6
|
||||
|
||||
| Phase | REQs | Components |
|
||||
|---|---|---|
|
||||
| P1 | REQ-040, REQ-045 | Web foundation + Reach signup (mock server, base templates, HTMX, store, "Create a Reach" form/list/detail) + lexicon firewall extension to `web/` |
|
||||
| P2 | REQ-041 | Stash dashboard (balance in Grain + Bread-scale conversion via `BreadScaleAll()`/`GrainsPerBread` + 90-day maturity progress via `IsMature()`) |
|
||||
| P3 | REQ-042 | Window authorization (open/lifecycle/audit-log via `Window.Activate/Revoke/Expire`) |
|
||||
| P4 | REQ-043 | Standing + Freeholder signals (computed from mock Ratings/Vouches/Slashes using locked constants + `GetStandingBucket`/`ComputeDiversityBonus`/`GetVoucherWeight`; 4-signal `IsFreeholderEligible`) |
|
||||
| P5 | REQ-044 | Bloom accrual (per-Stash `BloomRecord` + 4.5% target rate from `TargetBloomRateBasisPoints=450`) |
|
||||
| P6 | all v0.6 REQs (audit) | Feature purity gate; G-003 (app-layer) + G-006 (go.mod unchanged) verification; milestone ship |
|
||||
|
||||
## Cross-Phase Blockers (hard) — v0.6
|
||||
|
||||
- **P1-01-01 (lexicon firewall `lexicon_meta_web/`)** → blocks P1-02-03 + P1-04-02 (templates scanned by the firewall) and P2..P5 (content scanned).
|
||||
- **P1-02-01 (web foundation `main.go`/`server.go`)** → blocks P1-03-01 (store used by handlers) and all subsequent handler/template tasks.
|
||||
- **P1-03-01 (mock store)** → blocks P1-04-01 (Reach handler uses the store) and P2..P5 (each screen extends the store).
|
||||
- **P1-04-01 (Reach signup handler)** → blocks P2 (Stash dashboard needs a Stash created at signup — D-071), P4 (Standing is per-Reach).
|
||||
- **P2-99-01 (P2 ship)** → blocks P3 (Window scope references a Stash), P5 (BloomRecord is per-Stash).
|
||||
- **P3-99-01 (P3 ship)** → blocks P4 (soft ordering for branch hygiene; Standing is per-Reach, not per-Window, but the cumulative test suite + branch hygiene enforce serial phases).
|
||||
- **P4-99-01 (P4 ship)** → blocks P5 (soft ordering).
|
||||
- **P5-99-01 (P5 ship)** → blocks P6-01-01 (P6 audit).
|
||||
- All P(N) phase-ship tasks block P(N+1) Wave 1 tasks (soft ordering for branch hygiene).
|
||||
|
||||
## v0.6 Decisions Applied (D-066..D-073)
|
||||
|
||||
The v0.6 Phase 0 clarify/ideate/research stages produced 8 clarification decisions (D-066..D-073), all applied to this plan:
|
||||
|
||||
| ID | Decision | Applied to |
|
||||
|---|---|---|
|
||||
| D-066 | Frontend stack = Go `html/template` + HTMX (vendored, no node); G-006 preserved | P1-02-01, P1-02-02 (web foundation + HTMX vendored); Milestone Summary (G-006) |
|
||||
| D-067 | Mock data layer = Go HTTP server in `web/` instantiating real `x/*/types` structs; no keeper, no Cosmos runtime, no `app.go` | P1-02-01, P1-03-01 (mock store imports `x/*/types`); all handler tasks |
|
||||
| D-068 | UI code location = new top-level `web/` dir (not an `x/` module) | P1-02-01, P1-03-01 (web/ dir); Milestone Summary |
|
||||
| D-069 | Lexicon firewall extension to `web/` = new sibling `lexicon_meta_web/` (mirrors `lexicon_meta_docs/`); firewall-first (P1 before content) | P1-01-01 (firewall); all P2..P5 content scanned |
|
||||
| D-070 | G-003 boundary: `web/` importing `x/*/types` is app-layer consumption, NOT a cross-`x/` production import | P1-03-01, P1-03-03 (import-invariant test); all store tasks; P6 audit |
|
||||
| D-071 | "Sign up" = create a Reach + open a Stash atomically; UI labels it "Create a Reach" ("account" is banned) | P1-03-01 (atomic `CreateReach`), P1-04-01/02 ("Create a Reach" labels) |
|
||||
| D-072 | Phase ordering: P1 foundation+Reach+firewall → P2 Stash → P3 Window → P4 Standing → P5 Bloom → P6 final | Cross-Phase Dependency Map; all phase goals |
|
||||
| D-073 | Bread-scale source of truth = `x/bread/types` code constants (`GrainsPerBread=10000`, `BreadScaleAll()`), NOT docs | P2-02-01, P2-03-01 (Bread-scale conversion correctness test); P5-02-01 (Bloom target rate from `TargetBloomRateBasisPoints=450`) |
|
||||
|
||||
---
|
||||
|
||||
## User-Facing Surface
|
||||
|
||||
**Web UI at `http://localhost:8080`** — runnable via `go run ./web` (single binary, no external deps, no node toolchain, no build step). The UI is a Go `html/template` server-rendered HTML layer with HTMX progressive enhancement (vendored `web/static/htmx.min.js`). Five screens, all reachable from the home page nav:
|
||||
|
||||
1. **Reach signup** (`GET /reach/new` → `POST /reach`): the "Create a Reach" form. A visitor signs up to be a Nomad by creating a Reach (atomically creates a Stash per D-071). Reach list (`GET /reach`) + detail (`GET /reach/{id}`).
|
||||
2. **Stash dashboard** (`GET /stash/{holderID}`): balance in Grain + Bread-scale conversion (all 11 denominations from `BreadScaleAll()`) + 90-day maturity progress bar + `IsMature` badge.
|
||||
3. **Window authorization** (`GET /window/new` → `POST /window`; `GET /window/{id}`): open a Window (scope + duration + rate-limit), lifecycle transitions (Activate/Revoke/Expire via HTMX buttons), audit log.
|
||||
4. **Standing + Freeholder signals** (`GET /standing/{reachID}`): standing score + bucket + 4-signal progress (StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement) + Freeholder-eligible badge.
|
||||
5. **Bloom accrual** (`GET /bloom/{stashID}`): per-Stash `BloomRecord` (`AccruedGrain`, `RateBasisPoints` as %, `LastAccrualBlock`) + 4.5% target rate band.
|
||||
|
||||
**README quickstart**: `go run ./web` → open `http://localhost:8080`. No `make`, no `npm`, no `docker` — just Go 1.22 stdlib + one vendored JS file. The README (updated in P1 or P6) documents the `go run` invocation + the 5 screen routes.
|
||||
|
||||
---
|
||||
|
||||
## Happy Path
|
||||
|
||||
**Scenario: a visitor signs up as a Nomad and exercises the full UI end-to-end (mock data, no chain, no real values).**
|
||||
|
||||
1. The visitor opens `http://localhost:8080/` (home page) in a browser. The home page shows a one-paragraph OpenYield overview + nav links to the 5 screens.
|
||||
2. The visitor clicks "Create a Reach" → `GET /reach/new` renders the signup form (HolderID + PublicKey inputs; labeled "Create a Reach" — NOT "Sign up for an account").
|
||||
3. The visitor fills the form and submits (`POST /reach`). The handler calls `store.CreateReach` which **atomically** creates a `Reach` (`IsNomad=true`) + a `Stash` (matching `HolderID`, `BalanceGrain` seeded to 500000 Grain = 50 Bread per D-071). The browser redirects to `GET /reach/{id}` (Reach detail showing the Reach + the associated Stash).
|
||||
4. The visitor clicks the Stash link → `GET /stash/{holderID}` renders the Stash dashboard: balance 500000 Grain + the Bread-scale conversion table (50 Bread, 5 Loaves, 0.5 Batch, … from `BreadScaleAll()`) + a 90-day maturity progress bar (e.g., 45/90 days = 50% — the seeded `StashActivity` is immature) + an amber "Not Mature" badge (`IsMature()==false` because `ActiveDays < 90`).
|
||||
5. The visitor clicks "Window authorization" → `GET /window/new` renders the "Open a Window" form (scope dropdown: ReadStash/ReadStanding/ProcessPassActForStand; grantee; duration; rate-limit). The visitor opens a Window delegating ReadStash to a service → `POST /window` creates the Window (status=Open) + an initial `AuditEntry` → redirects to `GET /window/{id}`.
|
||||
6. On the Window detail page, the visitor clicks "Activate" (`POST /window/{id}/activate` via HTMX) → the Window transitions Open→Active (`Window.Activate()`); an `AuditEntry` is appended; the badge turns green. The visitor clicks "Revoke" → the Window transitions to Revoked (`Window.Revoke()`); the badge turns red; the audit log shows both actions.
|
||||
7. The visitor clicks "Standing" → `GET /standing/{reachID}` renders the Standing + Freeholder signals progress: a standing score (computed from mock Ratings using the locked constants) + a `StandingBucket` badge + 4-signal progress (StashMaturity: cross — the Stash is immature; MultiDomainStanding: check; CommittedCapital: check; CommunityEndorsement: cross — no Vouch) + a grey "Not Freeholder-eligible" badge (`IsFreeholderEligible()==false` because 2 signals are false).
|
||||
8. The visitor clicks "Bloom accrual" → `GET /bloom/{stashID}` renders the Bloom view: `AccruedGrain` (mock value) + `RateBasisPoints` 4.5% (from `TargetBloomRateBasisPoints=450`) + `LastAccrualBlock` + the target rate band (4.0%–5.0%) + the Mission Lock note ("Bloom originates only from real production").
|
||||
9. End-to-end in-browser with mock data; no chain, no real values, no persistence (resets on restart). The whole flow took ~6 clicks across 5 screens.
|
||||
|
||||
---
|
||||
|
||||
## UX Acceptance Criteria
|
||||
|
||||
The v0.6 deliverable MUST meet these explicit criteria (verified in P6 audit):
|
||||
|
||||
1. **`go run ./web` starts without external deps**: `go.mod` is unchanged across the v0.6 milestone range (G-006 — zero new require lines; HTMX is a vendored static asset at `web/static/htmx.min.js`, NOT a `go get`); `go run ./web` starts a server on `:8080` using only Go 1.22 stdlib (`net/http.ServeMux` + `html/template`).
|
||||
2. **All 5 screens reachable from the home page**: the home page nav (`web/templates/base.html`) links to Reach signup, Stash dashboard, Window authorization, Standing progress, Bloom accrual; each route returns 200 (verified by handler tests + manual browser check at `http://localhost:8080`).
|
||||
3. **Lexicon firewall green on all UI strings**: the `lexicon_meta_web/` firewall (P1-01-01) scans `web/templates/**/*.html` + `web/static/**/*.js` + `web/**/*.go` for the 10 banned terms and passes; per-handler rendered-HTML lexicon checks (each phase's handler test) scan the HTTP response body and pass; "Sign up" is labeled "Create a Reach" ("account" is banned per REQ-012).
|
||||
4. **Bread-scale conversion matches `x/bread/types` constants**: the Stash dashboard conversion uses `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000` (D-073 — code constants, NOT `docs/shared/bread-scale.md`); the Bread-scale conversion correctness test (P2-03-01) asserts the conversion matches the code constants and would FAIL if the outdated docs values (1,000× ratios) were used.
|
||||
5. **Standing score computed from locked formula constants**: the Standing screen score uses `x/standing/types` constants (`PriorMean=4.0`, `PriorWeight=10`, decay buckets, `ComputeDiversityBonus`, `GetVoucherWeight`, `GetStandingBucket`) — NOT hardcoded; the standing-score computed-from-locked-constants test (P4-03-01) asserts this.
|
||||
6. **Freeholder-eligible badge reflects `IsFreeholderEligible()`**: the Standing screen badge is green when `FreeholderSignals.IsFreeholderEligible()==true` and grey when false; the Freeholder-eligible badge test (P4-03-01) asserts the rendered badge matches the method output.
|
||||
7. **Window lifecycle transitions match `Window.Activate/Revoke/Expire`**: the Window screen lifecycle buttons call the real `x/window/types.Window.Activate/Revoke/Expire` methods (NOT a reimplementation); `Revoke()` on an Expired window is a no-op (v0.2 type contract); the lifecycle correctness test (P3-03-01) asserts the real methods are invoked.
|
||||
8. **No banned terms in any rendered page**: the per-handler rendered-HTML lexicon checks (P1-04-03, P2-03-01, P3-03-01, P4-03-01, P5-03-01) scan each screen's HTTP response body via `lexicon.FindBannedTerm` and pass; the `lexicon_meta_web/` file-scan firewall passes on all `web/**/*.{html,js,go}` files.
|
||||
10. **D-055/D-062 dep**: `go.mod` has cosmos-sdk v0.50.x + ibc-go v8.x (GRILL-ratified); `types/` packages gain `sdk.Msg` imports for `Msg*` (isolated in `types/msg_*.go`); invariant/lexicon tests stay stdlib-only and green.
|
||||
+2
-65
@@ -61,55 +61,7 @@ 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.6 — Nomad Web UI (in progress; feature type; tags run on the v0.5.x patch line)
|
||||
|
||||
### v0.6 Scope (Nomad Web UI MVP — generated test data, no real chain)
|
||||
|
||||
v0.6 is the project's first UI milestone. It delivers a working prototype Web
|
||||
UI where a person can sign up to be a Nomad (create a Reach + open a Stash)
|
||||
and exercise basic functionality around the (Reach, Stash) components, plus
|
||||
Window authorization, Standing progress, and Bloom accrual views. All data is
|
||||
generated as test fixtures — there is no real blockchain interaction, no live
|
||||
chain launch, no real bearer transports (D-020 continues to govern network
|
||||
deployment). The UI is a greenfield Go `html/template` + HTMX layer served by a
|
||||
Go mock HTTP server that instantiates the real `x/*/types` structs (Reach,
|
||||
Stash, Window, FreeholderSignals, BloomRecord) populated from in-memory
|
||||
fixtures. No keeper, no Cosmos runtime, no `app.go` (none exists in the repo).
|
||||
|
||||
This milestone is the prerequisite for real-world MVP testing: it makes the
|
||||
Nomad path visible and exercisable in a browser. Wiring the UI to a real `oyd`
|
||||
daemon (once one exists) is deferred to v0.7+ (no `app.go`, `cmd/`, or `main.go`
|
||||
exists in the repo today).
|
||||
|
||||
- **REQ-040** Nomad Reach signup Web UI — Go HTTP mock server (`web/`) + "Create a Reach" form + Reach list/detail; grounds the UI in `x/identity/types.Reach`. "Sign up" maps to "Create a Reach" (the word "account" is banned per REQ-012).
|
||||
- **REQ-041** Stash dashboard Web UI — balance in Grain + Bread-scale conversion (using `x/bread/types.BreadScaleAll()`) + 90-day maturity progress bar (`x/stash/types.StashActivity.IsMature`).
|
||||
- **REQ-042** Window authorization Web UI — form to open a Window (scope + duration + rate-limit), lifecycle view (Open→Active→Revoked/Expired via `x/window/types.Window.Activate/Revoke/Expire`), audit log.
|
||||
- **REQ-043** Standing + Freeholder signals progress Web UI — computed from mock `Rating`/`Vouch`/`Slash` records using the locked constants + `GetStandingBucket`/`ComputeDiversityBonus`/`GetVoucherWeight`; 4-signal progress (`FreeholderSignals.IsFreeholderEligible`).
|
||||
- **REQ-044** Bloom accrual Web UI — per-Stash `BloomRecord` view (`AccruedGrain`, `RateBasisPoints`), computed from mock data; shows the 4.5% target rate.
|
||||
- **REQ-045** Extend REQ-012 lexicon firewall to scan `web/templates/**` + `web/static/**` (new `lexicon_meta_web_test.go`). Firewall-first: lands in P1 before content.
|
||||
|
||||
### Milestone Type
|
||||
Feature (all execution phases are `feat` except REQ-045 which is `test`). Phase 0 → `v0.5.0`; execution phases `v0.5.1..v0.5.5`; final phase patch `v0.5.6` IS the v0.6 milestone release. No separate minor tag. The final-phase audit enforces the feature purity gate (no breaking schema changes; G-003 production firewall intact; G-006 go.mod unchanged unless a runtime dep is GRILL-approved — HTMX is a vendored static asset, not a Go dep).
|
||||
|
||||
### Out of Scope (v0.6)
|
||||
- Real blockchain interaction / mainnet / IBC / real bearer transports (D-020 continues)
|
||||
- A real `oyd` daemon / `app.go` / `cmd/oyd` (no chain runtime exists; deferred to v0.7+)
|
||||
- Real Anchors onboarding / Hub API B2B / real custody (simtest/mock only)
|
||||
- Authentication / sessions / real key management (mock; a Reach is created by form submission, stored in-memory)
|
||||
- Persistence (mock store is in-memory; resets on restart)
|
||||
- i18n / multi-language UI
|
||||
- Real Standing oracle / real Bloom accrual engine (computed from fixtures using locked constants)
|
||||
- The 5 P1+ mainnet-readiness items deferred from v0.5 (governance spam deposit, CLOB front-running, real IBC simtest, CLOB perf, emitMatchEventHook testability) — those are v0.7+ mainnet-readiness, not UI work
|
||||
|
||||
### Prior Milestones
|
||||
- v0.1 — OpenYield Foundation Init (COMPLETE; pre-MVP foundation skeleton; released as v0.0.9)
|
||||
- v0.2 — The Mesh (COMPLETE; skeleton + tests; released as v0.1.5)
|
||||
- v0.3 — Bearers & Documentation (COMPLETE; feature; released as v0.2.6)
|
||||
- v0.4 — Refinement (COMPLETE; NFR; released as v0.3.4)
|
||||
- v0.5 — Bearers Runtime (COMPLETE; feature; released as v0.4.8)
|
||||
|
||||
## Prior Milestone
|
||||
v0.5 — Bearers Runtime (complete; feature type; tags ran on the v0.4.x patch line)
|
||||
v0.5 — Bearers Runtime (in progress; feature type; tags run on the v0.4.x patch line)
|
||||
|
||||
### v0.5 Scope (Live-runtime promotions of the v0.3 Bearers skeletons)
|
||||
|
||||
@@ -291,19 +243,4 @@ Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → acce
|
||||
| D-058 | **Hub custody model = key-share abstraction (MPC-via-interface, not a concrete HSM/MPC vendor).** `x/hub` custody handlers expose a `CustodyKeyring` interface with `Sign`/`Derive` methods; v0.5 ships an in-memory test-only implementation. Real MPC/HSM backing is deferred (operational, Year 3+). This keeps v0.5 dep-neutral w.r.t. custody vendors while landing the handler surface. | Custody key management is operational, not protocol-level. An interface + test impl lets runtime handlers be exercised in simtest without committing to a vendor. GRILL reviews the interface boundary. | 0.78 | [commit to a specific MPC vendor (premature); hand-roll shamir (out of scope)] |
|
||||
| D-059 | **IBC packet scope = the 5 L2 chains already in the v0.2 skeleton** (Polygon, Base, Arbitrum, Optimism, Solana per REQ-009/`x/satellite`). v0.5 `x/bridge` handlers implement IBC packet recv/ack for these 5 chains' `BridgeStatus` transitions. No new L2 chains in v0.5. Solana IBC uses the wormhole-style bridge adapter (already stubbed in `x/bridge` per D-021). | The 5 L2 chains are the locked-const set (REQ-009). Adding new chains is a Year-4 concern. Solana IBC was a v0.1 deferred item (D-021) now promoted. | 0.82 | [add 3+ new L2 chains (Year 4); defer Solana IBC again (stalls)] |
|
||||
| D-060 | **Council governance shape (AUDIT §193 P1-1)**: add `Proposal` and `VoteOption` enum types to `x/council/types` (currently absent per AUDIT). `ProposalKind` enum = {Stand, Guild, Mesh, MissionLockAmendment-Rejected} (Mission Lock non-amendable → the enum value exists but the handler rejects it; documents the non-amendability in code). `VoteOption` enum = {Yes, No, Abstain, Veto} (Veto = Watcher-only, quorum rule). SignalKind stays at 4 sources (P1-2 defensible per AUDIT; expansion deferred to v0.6+ governance vote). Mission Lock const firewall (G-003) intact. | AUDIT P1-1 flagged the absence as a divergence. Adding the enums is `feat:` (deferred from v0.4 by D-001). P1-2 (SignalKind 4→5) is a locked-const change rejected by the audit rationale, so it stays at 4. | 0.82 | [add SignalKind 5th source (locked-const change, rejected); defer Proposal/VoteOption again (stalls)] |
|
||||
| D-061 | **No IDEATE stage in v0.5** (no `--ideate` flag this run). The feature scope was pre-seeded from PROJECT.md v0.4 out-of-scope + AUDIT §193 P1-1 + D-050 and ratified at CLARIFY. The D-001 refinement-only filter does NOT apply (v0.5 is a feature milestone, not NFR). | run.md §IDEATE is conditional on `--ideate`. This invocation has no `--ideate`. | 1.00 | [run IDEATE anyway] |
|
||||
|
||||
## Clarification Decisions (Phase 0 v0.6 — CLARIFY, autonomy=full)
|
||||
|
||||
Auto-decided defaults logged per clarify workflow Step 4 (full autonomy → accept defaults, log decisions). `--ideate` flag IS present this run; v0.6 is the project's first UI milestone. The D-001 refinement-only filter does NOT apply (v0.6 is a feature milestone). User-validated stack choices recorded via interactive questions: Go `html/template` + HTMX (frontend stack), Go mock API server (mock data layer), all 5 screens (Reach signup, Stash dashboard, Window authorization, Standing + Freeholder signals, Bloom accrual), new top-level `web/` dir (UI code location).
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Alternatives |
|
||||
|----|----------|-----------|------------|--------------|
|
||||
| D-066 | **Frontend stack = Go `html/template` + HTMX.** HTMX is a single vendored JS file served as a static asset — no node toolchain, no `package.json`, no build step. Go `html/template` is stdlib. **G-006 (zero-dep) is preserved** — this is the decisive factor vs. a React/Svelte SPA. Sufficient for Reach/Stash/Window/Standing/Bloom screens (progressive enhancement over server-rendered HTML). Weakest for rich dashboards, but adequate for an MVP prototype. | User-validated. Project has a strong zero-dep ethos (G-006); v0.5 took a controlled G-006 exception for cosmos-sdk only after GRILL ratification. A node toolchain + `package.json` would be a far larger exception for a UI MVP that can be served by stdlib + one vendored JS file. | 0.88 | [React/Vite SPA (adds node toolchain, breaks Go-only convention); SvelteKit (same tradeoff); pure static HTML/CSS/vanilla JS (most fragile)] |
|
||||
| D-067 | **Mock data layer = Go HTTP server in `web/` instantiating real `x/*/types` structs.** New top-level `web/` dir holds `main.go`, HTTP handlers, an in-memory mock store seeded from fixtures, and `static/` + `templates/`. The mock store imports `x/identity/types`, `x/stash/types`, `x/window/types`, `x/standing/types`, `x/bread/types`, `x/bloom/types` and populates them with test values. **No keeper, no Cosmos runtime, no `app.go`** (none exists in the repo). This grounds the UI in the actual locked data shapes (Reach, Stash, Window, FreeholderSignals, BloomRecord) — the UI does not exercise a chain but does exercise the real Go type definitions. | User-validated. The four modules the Nomad UI primarily surfaces (identity, stash, window, standing) are types-only skeletons with no keeper/MsgServer. A Go mock server reuses the type definitions as the source of truth, so the UI is grounded in the locked constants (GrainsPerBread=10000, MaturityThresholdDays=90, standing formula) rather than mirroring them in TS/JSON. | 0.85 | [frontend-only TS/JSON fixtures (UI would not exercise any Go code path); Go mock API + in-memory keepers (more code, premature)] |
|
||||
| D-068 | **UI code location = new top-level `web/` dir.** Clean separation from `x/` protocol modules; does not touch the `go.mod` module path; does not pretend to be a Cosmos module. `web/` contains `main.go` (or `cmd/oyd-ui/main.go`), handlers, mock store, fixtures, `static/`, `templates/`. | User-validated. The Mesh Experience component is ROADMAP Phase 2, not a v0.6 deliverable; coupling the UI to Cosmos module conventions (a new `x/meshexperience`) is premature. A top-level `web/` dir matches the project's existing convention of non-`x/` top-level dirs (`docs/`, `lexicon/`, `lexicon_meta_docs/`). | 0.90 | [new `x/meshexperience` Cosmos module (couples UI to Cosmos conventions prematurely); `cmd/oyd-ui` + `web/` split (more files, clearer build)] |
|
||||
| D-069 | **Lexicon firewall extension to `web/`.** REQ-012 currently scans `x/` + `docs/`. A new sibling meta-test `lexicon_meta_web_test.go` (package `lexicon_meta_web`) scans `web/templates/**/*.html` + `web/static/**/*.js` for the 10 banned terms, using the same `lexicon.FindBannedTerm` + word-boundary regex. Self-exclusion + fragment pattern preserved. **"Sign up" maps to "Create a Reach"** — the word "account" is banned (REQ-012). Firewall-first: lands in P1 before content (P2..P5) so UI strings are lexicon-clean by construction. | REQ-012 is `All` phases and UI strings are user-facing; the firewall must cover the UI to be durable. Extending the existing meta-test pattern (not modifying it) preserves v0.2/v0.3 coverage. Firewall-first (D-044 pattern) ensures UI content is lexicon-clean by construction, not by retrofit. | 0.88 | [skip (REQ-012 is All phases); single combined meta-test scanning x/ + docs/ + web/ (loses isolation)] |
|
||||
| D-070 | **G-003 firewall scope: `web/` importing `x/*/types` is app-layer consumption, not a cross-`x/` production import.** G-003 (production import firewall) prohibits production struct imports across `x/<module>/types` packages. `web/` is not an `x/` module — it is the application layer that consumes protocol types, exactly as a future `cmd/oyd` would. The G-003 firewall stays intact: no `x/` module gains a production import of another `x/` module's types via `web/`. GRILL reviews this boundary. | G-003's intent is to prevent cross-module coupling inside the protocol layer. The application layer consuming types is the intended consumption direction. `web/` importing `x/identity/types` is no different from `cmd/oyd` importing it (when one exists). | 0.85 | [treat `web/` as an `x/` module (wrong — it is not protocol); forbid `web/` from importing `x/*/types` (would force TS/JSON fixtures, losing the grounding in locked constants)] |
|
||||
| D-071 | **"Sign up" = create a Reach + open a Stash atomically.** The Nomad entry path per `docs/nomads/reach.md` is "a Nomad starts with a Reach and a Stash". The signup form creates both atomically: a `Reach` with `IsNomad=true` + a `Stash` with `HolderID` matching the Reach's `HolderID` and `BalanceGrain` seeded to a test value (e.g., 500,000 Grain = 50 Bread). No KYC, no custodian (REQ-001 self-service principle). The UI labels this "Create a Reach" (lexicon-clean; "account" is banned). | The docs define the Nomad starting state as Reach + Stash. Creating only a Reach would leave the Nomad unable to view a Stash dashboard (P2) — the atomic creation matches the docs and makes the happy path contiguous. | 0.82 | [create Reach only, defer Stash creation to a separate flow (fractures the happy path); create Reach + Stash + Window all at signup (over-scope for an MVP)] |
|
||||
| D-072 | **Phase ordering** (provisional, planner finalizes): P1 Web foundation + Reach signup + lexicon firewall extension (REQ-040 + REQ-045 — same `web/` territory, vertical slice, firewall-first) → P2 Stash dashboard (REQ-041, depends on Reach existing) → P3 Window authorization (REQ-042, depends on Stash existing) → P4 Standing + Freeholder signals (REQ-043, depends on Reach existing) → P5 Bloom accrual (REQ-044, depends on Stash existing) → P6 final review + audit + milestone ship. Each phase independently shippable; P1 lands the foundation + firewall first (lexicon-clean by construction). | P1 bundles the web foundation + Reach signup + the firewall extension (same `web/` territory, vertical slice). P2..P5 each add one screen, ordered by the Nomad happy path (Reach → Stash → Window → Standing → Bloom). Vertical slices, each phase shippable. | 0.82 | [different wave ordering; bundle Stash + Window in one phase] |
|
||||
| D-073 | **Bread-scale source of truth = `x/bread/types` code constants, NOT `docs/shared/bread-scale.md`.** The code constants (`GrainsPerBread=10000`, `BreadScaleAll()` table) are the locked, tested values; the docs table is aspirational/outdated (states 1,000× ratios that do not match the code). The UI uses the code constants for all Bread-scale conversions. A doc-fix for `docs/shared/bread-scale.md` is flagged as a P1+ follow-up (not a v0.6 deliverable — docs were a v0.3 deliverable; this is a doc-drift fix, not a UI feature). | The code constants are tested (`x/bread/types/types_test.go` asserts them); the docs are not. Using the code as the source of truth keeps the UI consistent with the protocol layer. | 0.90 | [use the docs table (wrong — not tested, disagrees with code); fix the docs in v0.6 (out of scope — doc-drift fix, not a UI feature)] |
|
||||
| D-061 | **No IDEATE stage in v0.5** (no `--ideate` flag this run). The feature scope was pre-seeded from PROJECT.md v0.4 out-of-scope + AUDIT §193 P1-1 + D-050 and ratified at CLARIFY. The D-001 refinement-only filter does NOT apply (v0.5 is a feature milestone, not NFR). | run.md §IDEATE is conditional on `--ideate`. This invocation has no `--ideate`. | 1.00 | [run IDEATE anyway] |
|
||||
+32
-71
@@ -83,13 +83,13 @@ deferred items (D-050, PROJECT.md v0.4 out-of-scope, ROADMAP Phase 3 runtime).
|
||||
|
||||
| ID | Requirement | Source | Class | Priority | Status | Phase |
|
||||
|----|-------------|--------|-------|----------|--------|-------|
|
||||
| REQ-033 | Exit layer runtime — `x/exit` DEX swap routing + `x/bridge` L2↔L1 IBC packet handlers; promotes REQ-010 from skeleton → runtime (simtest-grade message handlers; live DEX/IBC channels deferred) | PROJECT.md v0.4 OOS / D-050 | feat | High | Complete | v0.5/P1 |
|
||||
| REQ-034 | Bearers transport runtime — OY-SAT + OY-QR bearer transport message handlers in `x/bearers` (extends REQ-019/REQ-022); session lifecycle in simtest (hardware integration deferred) | PROJECT.md v0.4 OOS | feat | Medium | Complete | v0.5/P2 |
|
||||
| REQ-035 | Anchors onboarding runtime — `x/partner` Anchor tier credential issuance + revocation handlers (extends REQ-018/REQ-023); credential lifecycle in simtest (real institutional onboarding deferred) | PROJECT.md v0.4 OOS | feat | Medium | Complete | v0.5/P3 |
|
||||
| REQ-036 | Hub API B2B runtime — `x/hub` custody, lending primitive, compliance message handlers; keeper handlers + simtest (real B2B suite deferred) | PROJECT.md v0.4 OOS | feat | High | Complete | v0.5/P4 |
|
||||
| REQ-037 | Services runtime — `x/services` Care / SIM / Vault / Mail service lifecycle handlers; runtime handlers + simtest (live service integrations deferred) | PROJECT.md v0.4 OOS | feat | Medium | Complete | v0.5/P5 |
|
||||
| REQ-038 | Bond market depth runtime — `x/bond` Growth Bonds + secondary-market matching handlers (extends REQ-021/REQ-026); matching engine + simtest (live market depth deferred) | PROJECT.md v0.4 OOS | feat | High | Complete | v0.5/P6 |
|
||||
| REQ-039 | Council governance runtime — `x/council` Proposal/VoteOption enum types (AUDIT §193 P1-1, deferred from v0.4) + Voice lifecycle handlers; governance message handlers + simtest (Mission Lock const firewall intact per G-003; SignalKind 4→5 expansion deferred to v0.6+) | AUDIT §193 P1-1 / D-050 | feat | Medium | Complete | v0.5/P7 |
|
||||
| REQ-033 | Exit layer runtime — `x/exit` DEX swap routing + `x/bridge` L2↔L1 IBC packet handlers; promotes REQ-010 from skeleton → runtime (simtest-grade message handlers; live DEX/IBC channels deferred) | PROJECT.md v0.4 OOS / D-050 | feat | High | pending | v0.5/P1 |
|
||||
| REQ-034 | Bearers transport runtime — OY-SAT + OY-QR bearer transport message handlers in `x/bearers` (extends REQ-019/REQ-022); session lifecycle in simtest (hardware integration deferred) | PROJECT.md v0.4 OOS | feat | Medium | pending | v0.5/P2 |
|
||||
| REQ-035 | Anchors onboarding runtime — `x/partner` Anchor tier credential issuance + revocation handlers (extends REQ-018/REQ-023); credential lifecycle in simtest (real institutional onboarding deferred) | PROJECT.md v0.4 OOS | feat | Medium | pending | v0.5/P3 |
|
||||
| REQ-036 | Hub API B2B runtime — `x/hub` custody, lending primitive, compliance message handlers; keeper handlers + simtest (real B2B suite deferred) | PROJECT.md v0.4 OOS | feat | High | pending | v0.5/P4 |
|
||||
| REQ-037 | Services runtime — `x/services` Care / SIM / Vault / Mail service lifecycle handlers; runtime handlers + simtest (live service integrations deferred) | PROJECT.md v0.4 OOS | feat | Medium | pending | v0.5/P5 |
|
||||
| REQ-038 | Bond market depth runtime — `x/bond` Growth Bonds + secondary-market matching handlers (extends REQ-021/REQ-026); matching engine + simtest (live market depth deferred) | PROJECT.md v0.4 OOS | feat | High | pending | v0.5/P6 |
|
||||
| REQ-039 | Council governance runtime — `x/council` Proposal/VoteOption enum types (AUDIT §193 P1-1, deferred from v0.4) + Voice lifecycle handlers; governance message handlers + simtest (Mission Lock const firewall intact per G-003; SignalKind 4→5 expansion deferred to v0.6+) | AUDIT §193 P1-1 / D-050 | feat | Medium | pending | v0.5/P7 |
|
||||
|
||||
> REQ-033..REQ-039 are NEW in v0.5. All are `feat`-class (runtime promotion
|
||||
> from skeleton). No breaking schema changes; locked-const firewall intact
|
||||
@@ -97,77 +97,38 @@ deferred items (D-050, PROJECT.md v0.4 out-of-scope, ROADMAP Phase 3 runtime).
|
||||
> purity gate (no breaking schema changes; G-003 production firewall intact;
|
||||
> G-006 go.mod unchanged unless a runtime dep is GRILL-approved).
|
||||
|
||||
## Milestone v0.5 Summary (Bearers Runtime — Feature) — COMPLETE
|
||||
|
||||
- 7 v0.5-scope REQs shipped as feature (runtime promotion from skeleton): REQ-033, REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039
|
||||
- 8 modules promoted to runtime (keeper MsgServer handlers + simtest-grade end-to-end flows): x/exit, x/bridge, x/bearers, x/partner, x/hub, x/services, x/bond, x/council
|
||||
- cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (D-055/D-062, G-006 controlled exception — scoped to runtime phases; types/ packages stay dep-free)
|
||||
- G-003 production firewall intact (expected_keepers.go interfaces; no production struct imports across x/<module>/types)
|
||||
- Locked-const firewall intact: all v0.1..v0.4 consts unchanged (ExitStatusCount=5, BridgeStatusCount=4, BearerTypeCount=6, OYSATLink.SurveillanceResistant=true, PartnerTierCount=4, HubServiceCount=3, LendingCouponCapBps=800, LendingCouponFloorBps=0, ServiceKindCount=4, CouponCapBps=800, CouponFloorBps=0, CouncilKindCount=3, SignalKindCount=4, MissionLockAmendable=false); v0.5 additions additive (AnchorCredentialStatusCount=4, WatcherVetoQuorum default 6 param-tunable)
|
||||
- 5 GRILL decisions ratified: D-055 (cosmos-sdk dep), D-062 (version pin), D-063 (bond CLOB REJECT above 800bps), D-064 (MissionLockAmendment reject-at-ValidateBasic), D-065 (Watcher Veto quorum default 6)
|
||||
- 8 binding fixes landed: G-017 (NoWithVeto test reconciled), G-018 (go 1.22 build gate), G-019 (ImpliedCoupon helper + boundary test), G-020 (WatcherVetoQuorum bounds [2,9]), G-021 (IBC replay ERROR), G-022 (baseline stubs documented), G-023 (keeper/msg_server.go ownership split), G-024 (types/ tests stdlib-only)
|
||||
- Coverage ≥80% on all 8 keeper packages: x/exit/keeper 85.0%, x/bridge/keeper 82.1%, x/bearers/keeper 91.2%, x/partner/keeper 87.6%, x/hub/keeper 90.0%, x/services/keeper 91.5%, x/bond/keeper 92.5%, x/council/keeper 90.3%
|
||||
- 34 packages green (no regression on v0.1..v0.4 baseline)
|
||||
- Tags: v0.4.0 (P0) -> v0.4.1 (P1) -> v0.4.2 (P2) -> v0.4.3 (P3) -> v0.4.4 (P4) -> v0.4.5 (P5) -> v0.4.6 (P6) -> v0.4.7 (P7) -> v0.4.8 (P8 = v0.5 milestone release)
|
||||
- Tag-line note: v0.5 (feature) ships on the v0.4.x patch line (config tag_base). The v0.4.8 milestone release IS the deliverable (D-008 — final phase patch IS the milestone release; no separate minor tag).
|
||||
- 5 P1+ issues flagged for v0.6+ mainnet-readiness (governance spam deposit, CLOB front-running/batch auction, real IBC light-client simtest, CLOB prefix-key perf, emitMatchEventHook testability)
|
||||
|
||||
## v0.6 Milestone Requirements (Nomad Web UI — Feature)
|
||||
|
||||
v0.6 is the project's first UI milestone. It delivers a working prototype Web
|
||||
UI where a person can sign up to be a Nomad (create a Reach + open a Stash)
|
||||
and exercise basic functionality around the (Reach, Stash) components, plus
|
||||
Window authorization, Standing progress, and Bloom accrual views. All data is
|
||||
generated as test fixtures — no real blockchain interaction (D-020 continues).
|
||||
The UI is a greenfield Go `html/template` + HTMX layer served by a Go mock HTTP
|
||||
server (`web/`) that instantiates the real `x/*/types` structs from in-memory
|
||||
fixtures. No keeper, no Cosmos runtime, no `app.go`.
|
||||
|
||||
| ID | Requirement | Source | Class | Priority | Status | Phase |
|
||||
|----|-------------|--------|-------|----------|--------|-------|
|
||||
| REQ-040 | Nomad Reach signup Web UI — Go HTTP mock server (`web/main.go`, Go 1.22 `net/http.ServeMux`, mock store) + "Create a Reach" form (POST) + Reach list/detail views; grounds the UI in `x/identity/types.Reach`. "Sign up" maps to "Create a Reach" (the word "account" is banned per REQ-012). Signup atomically creates a Reach (`IsNomad=true`) + a Stash (per D-071, Nomad starts with both). | user `--ideate` request + D-066/D-067/D-068/D-071 | feat | High | Pending | v0.6/P1 |
|
||||
| REQ-041 | Stash dashboard Web UI — balance in Grain + Bread-scale conversion (using `x/bread/types.BreadScaleAll()` + `GrainsPerBread=10000`, per D-073 code constants are the source of truth) + 90-day maturity progress bar (`x/stash/types.StashActivity.IsMature`, `MaturityThresholdDays=90`). | user `--ideate` request + D-073 | feat | High | Pending | v0.6/P2 |
|
||||
| REQ-042 | Window authorization Web UI — form to open a Window (scope + duration + rate-limit) delegating to a service, lifecycle view (Open→Active→Revoked/Expired via `x/window/types.Window.Activate/Revoke/Expire`), audit log (`AuditEntry`). | user `--ideate` request | feat | Medium | Pending | v0.6/P3 |
|
||||
| REQ-043 | Standing + Freeholder signals progress Web UI — computed from mock `Rating`/`Vouch`/`Slash` records using the locked constants + `GetStandingBucket`/`ComputeDiversityBonus`/`GetVoucherWeight`; 4-signal progress (`FreeholderSignals.IsFreeholderEligible` — StashMaturity, MultiDomainStanding, CommittedCapital, CommunityEndorsement). | user `--ideate` request | feat | Medium | Pending | v0.6/P4 |
|
||||
| REQ-044 | Bloom accrual Web UI — per-Stash `BloomRecord` view (`AccruedGrain`, `RateBasisPoints`, `LastAccrualBlock`), computed from mock data; shows the 4.5% target rate (`TargetBloomRateBasisPoints=450`). | user `--ideate` request | feat | Low | Pending | v0.6/P5 |
|
||||
| REQ-045 | Extend REQ-012 lexicon firewall to scan `web/templates/**` + `web/static/**` + `web/**/*.go` (new `lexicon_meta_web_test.go`, package `lexicon_meta_web`, subdir `lexicon_meta_web/`). Mirrors the `lexicon_meta_docs` pattern with G-013 walk-coverage + G-009 self-test + G-014 shared `SyntheticBannedStrings()`. Firewall-first: lands in P1 before content (P2..P5). | D-069 + RESEARCH D-075 | test/quality | High | Pending | v0.6/P1 |
|
||||
|
||||
> REQ-040..REQ-045 are NEW in v0.6. REQ-040..REQ-044 are `feat`-class (UI
|
||||
> screens); REQ-045 is `test` (lexicon firewall extension). No breaking schema
|
||||
> changes; G-003 production firewall intact (`web/` is app-layer, not an `x/`
|
||||
> module); G-006 go.mod unchanged (HTMX is a vendored static asset, not a Go
|
||||
> dep). The final-phase audit enforces the feature purity gate.
|
||||
|
||||
## IDEATE Traceability (Phase 0 v0.6 — IDEATE stage, autonomy=full)
|
||||
## IDEATE Traceability (Phase 0 — IDEATE stage, autonomy=full)
|
||||
|
||||
The IDEATE stage ran the three ideation tiers (mechanical, backend-enriched,
|
||||
cross-project) on the v0.6 milestone scope and ratified 6 ideas (IDEATE-09..
|
||||
IDEATE-14) at full autonomy. Each IDEATE-NN maps to a REQ-ID in the v0.6
|
||||
requirements table above. The user pre-validated the 5 screens + stack via
|
||||
interactive questions during CLARIFY (Go html/template + HTMX, Go mock API
|
||||
server, new `web/` dir, all 5 screens); IDEATE ratifies that validation.
|
||||
|
||||
Mechanical tier: v0.5 closed clean (7/7 REQs, 8 keeper packages ≥80% coverage,
|
||||
G-003/locked-const firewalls intact, 5 P1+ flagged for v0.7+ mainnet-readiness);
|
||||
no `lessons:`/`compound:` tags in v0.1..v0.5 history (convention unused).
|
||||
Backend-enriched tier: confirmed the mock-server-over-real-Go-types approach
|
||||
grounds the UI in the locked constants (D-067/D-073). Cross-project tier: no
|
||||
applicable cross-project patterns (this is the project's first UI; no prior UI
|
||||
conventions to inherit). Defaults accepted per full autonomy.
|
||||
cross-project) on the v0.3 milestone scope and ratified 8 ideas (IDEATE-01..
|
||||
IDEATE-08) at full autonomy. Each IDEATE-NN maps to a REQ-ID in the v0.3
|
||||
requirements table above. Mechanical tier: no `lessons:`/`compound:` tags in
|
||||
v0.1/v0.2 history (convention unused); one historical escalation (milestone
|
||||
release pending — no remote) resolved in v0.2; v0.2 closed clean (9/9 REQs,
|
||||
303 tests, ≥95.9% coverage). Backend-enriched + cross-project tiers confirmed
|
||||
the docs deliverable + Bearers skeleton bundle (D-034) and the firewall-first
|
||||
ordering (D-044). Defaults accepted per full autonomy.
|
||||
|
||||
| IDEATE ID | REQ-ID | Category | Source | Confidence | Phase |
|
||||
|-----------|--------|----------|--------|------------|-------|
|
||||
| IDEATE-09 | REQ-040 | feature/ui | user `--ideate` request + D-066/D-067/D-068/D-071 | 0.92 | v0.6/P1 |
|
||||
| IDEATE-10 | REQ-041 | feature/ui | user `--ideate` request + D-073 | 0.90 | v0.6/P2 |
|
||||
| IDEATE-11 | REQ-042 | feature/ui | user `--ideate` request | 0.85 | v0.6/P3 |
|
||||
| IDEATE-12 | REQ-043 | feature/ui | user `--ideate` request | 0.85 | v0.6/P4 |
|
||||
| IDEATE-13 | REQ-044 | feature/ui | user `--ideate` request | 0.80 | v0.6/P5 |
|
||||
| IDEATE-14 | REQ-045 | quality/security | D-069 + RESEARCH D-075 | 0.88 | v0.6/P1 |
|
||||
| IDEATE-01 | REQ-027 | improvement/docs | user `--ideate` request + D-042/D-045 | 0.90 | v0.3/P1-P3 |
|
||||
| IDEATE-02 | REQ-028 | quality/security | D-043 + RESEARCH firewall-extension design | 0.88 | v0.3/P1 |
|
||||
| IDEATE-03 | REQ-010 | coverage/architecture | ROADMAP Phase 3 + D-036 | 0.80 | v0.3/P4 |
|
||||
| IDEATE-04 | REQ-022 | coverage | ROADMAP Phase 3 + D-037 | 0.82 | v0.3/P4 |
|
||||
| IDEATE-05 | REQ-023 | coverage | ROADMAP Phase 3 + D-038 | 0.78 | v0.3/P4 |
|
||||
| IDEATE-06 | REQ-024 | architecture | ROADMAP Phase 3 + D-039 | 0.80 | v0.3/P5 |
|
||||
| IDEATE-07 | REQ-025 | coverage | ROADMAP Phase 3 + D-040 | 0.78 | v0.3/P5 |
|
||||
| IDEATE-08 | REQ-026 | coverage | ROADMAP Phase 3 + D-041 | 0.80 | v0.3/P5 |
|
||||
|
||||
Notes:
|
||||
- IDEATE-09/14 ship in P1 (web foundation + firewall-first, same `web/` territory — vertical slice).
|
||||
- IDEATE-10..13 ship in P2..P5 (one screen per phase, ordered by the Nomad happy path: Reach → Stash → Window → Standing → Bloom).
|
||||
- The D-001 refinement-only filter does NOT apply (v0.6 is a feature milestone, not NFR).
|
||||
- IDEATE-01/02 (docs deliverable + firewall) are the user's `--ideate` request
|
||||
ratified via D-042/D-043/D-045.
|
||||
- IDEATE-03..08 (Bearers skeleton) are the ROADMAP Phase 3 subset bundled into
|
||||
v0.3 per D-034.
|
||||
- IDEATE-02 lands in P1 (firewall-first) BEFORE IDEATE-01 content (P2/P3) per
|
||||
D-044 — docs are lexicon-clean by construction.
|
||||
- IDEATE-03..05 ship in P4 (Bearers skeleton I); IDEATE-06..08 ship in P5
|
||||
(Bearers skeleton II) — vertical slices, each phase independently shippable.
|
||||
|
||||
## Milestone v0.1 Summary
|
||||
- 10 REQs complete (skeleton + tests)
|
||||
|
||||
+1
-530
@@ -2194,533 +2194,4 @@ flow per the researcher role — NOT flagged `[ASSUMED]`):
|
||||
rejection is proposed. Confidence 0.80.
|
||||
4. **A-574** (Watcher Veto quorum value): default 6 proposed (matches
|
||||
REQ-004 6-of-9); the exact param value is a planner decision.
|
||||
Confidence 0.75.
|
||||
|
||||
---
|
||||
|
||||
## v0.6 Research (Nomad Web UI MVP)
|
||||
|
||||
> v0.6 is the project's first UI milestone (D-066..D-073, CLARIFY). Stack is
|
||||
> Go `html/template` + HTMX (vendored single JS file, no node toolchain —
|
||||
> G-006 zero-dep preserved). Mock data layer = Go HTTP server in new
|
||||
> top-level `web/` dir instantiating real `x/*/types` structs from in-memory
|
||||
> fixtures (no keeper, no Cosmos runtime, no `app.go`). 5 screens: Reach
|
||||
> signup, Stash dashboard, Window authorization, Standing + Freeholder
|
||||
> signals, Bloom accrual. This research grounds the UI in the live codebase
|
||||
> (read-only — no files modified).
|
||||
|
||||
### v0.6 §1. Go `html/template` + HTMX Architecture
|
||||
|
||||
**Server structure** — A Go `html/template` server for the mock UI follows
|
||||
the standard stdlib-only layout (matches D-068's `web/` dir decision):
|
||||
|
||||
```
|
||||
web/
|
||||
main.go # entrypoint: registers routes, serves static + templates
|
||||
handlers/ # one file per screen (reach.go, stash.go, window.go, ...)
|
||||
store/ # in-memory mock store (imports x/*/types, seeded from fixtures)
|
||||
templates/
|
||||
base.html # layout: <html><head>...<script htmx>...</head><body>{{template "content" .}}</body>
|
||||
reach_signup.html # {{define "content"}}...{{end}} per page
|
||||
stash_dashboard.html
|
||||
window_authorize.html
|
||||
standing_signals.html
|
||||
bloom_accrual.html
|
||||
fragments/ # HTMX swap targets (partial HTML, no base layout)
|
||||
static/
|
||||
htmx.min.js # vendored single JS file (G-006: no go get, no npm)
|
||||
style.css
|
||||
```
|
||||
|
||||
**Layout/base template pattern** — Go `html/template` supports template
|
||||
composition via `{{template "name" .}}` + `{{define "name"}}...{{end}}`. The
|
||||
conventional pattern: `base.html` defines the full HTML skeleton and a
|
||||
`{{template "content" .}}` slot; each page template uses
|
||||
`{{define "content"}}...{{end}}` to fill the slot. `template.ParseGlob` or
|
||||
`template.New("").ParseFiles` loads both base + page; `template.ExecuteTemplate`
|
||||
renders the page within the base. The base references the vendored HTMX via
|
||||
`<script src="/static/htmx.min.js"></script>` (served by `http.FileServer` or
|
||||
`http.ServeFile` from `web/static/`).
|
||||
|
||||
**HTMX vendoring (G-006-compliant)** — HTMX is a dependency-free, browser-
|
||||
oriented JS library: a single `htmx.min.js` file loaded via `<script>` tag.
|
||||
**No build step, no node, no npm, no `go get`** — confirmed from the official
|
||||
docs (htmx.org/docs/#installing: "There is no need for a build system to use
|
||||
it"). Vendoring = download `htmx.min.js` (current stable: **2.0.10**, per
|
||||
htmx.org quick-start script tag `htmx.org@2.0.10`) into `web/static/htmx.min.js`
|
||||
and serve it as a static asset. This preserves G-006 (zero-dep go.mod — HTMX is
|
||||
a static file, not a Go module dependency). The `go.mod` (go.mod:1-10) is
|
||||
unchanged by v0.6: no new `require` entries for the UI.
|
||||
|
||||
**HTMX progressive enhancement + Go handler contract** — HTMX generalizes
|
||||
HTML: any element can issue AJAX requests via `hx-get`/`hx-post`/etc.
|
||||
attributes, and the response HTML is swapped into a target element. The Go
|
||||
handler contract for HTMX:
|
||||
- **Initial GET (full page)**: handler renders the full page (base + content
|
||||
template). No-JS fallback works because forms still POST normally.
|
||||
- **HTMX request (fragment)**: HTMX sets the `HX-Request: true` header on
|
||||
AJAX requests. The Go handler checks `r.Header.Get("HX-Request") == "true"`
|
||||
and, if so, renders ONLY the content fragment (no base layout) — returning
|
||||
partial HTML for the swap. Non-HTMX requests get the full page.
|
||||
- **Forms**: `<form hx-post="/reaches" hx-target="#reach-list">` posts to a
|
||||
Go endpoint; the handler validates, mutates the in-memory store, and
|
||||
returns the updated fragment (e.g., the new Reach row) for HTMX to swap in.
|
||||
No-JS fallback: the form's native `action`/`method` attributes handle the
|
||||
POST, returning a full page redirect/render.
|
||||
- **`hx-boost="true"`**: wraps regular `<a>`/`<form>` in AJAX, degrading
|
||||
gracefully to native navigation if JS is disabled (progressive enhancement,
|
||||
confirmed by htmx docs).
|
||||
|
||||
**`html/template` auto-escaping (XSS)** — Go's `html/template` package
|
||||
contextually auto-escapes template actions based on their parsing context
|
||||
(HTML, attribute, CSS, JS, URL). `{{.ReachID}}` in an HTML context becomes
|
||||
`<script>...`-escaped; in a `href="..."` attribute context it's URL-
|
||||
encoded; in a `<script>` block it's JS-escaped. This prevents XSS in user-
|
||||
submitted content (e.g., the Reach signup form's `PublicKey` field) WITHOUT
|
||||
requiring explicit `template.HTMLEscapeString` calls. The auto-escaping is
|
||||
context-aware: `{{template "content" .}}` passes the data through, and each
|
||||
`{{.Field}}` action is escaped per its position. This is a stdlib guarantee
|
||||
(contrast with `text/template`, which does NOT escape — `html/template` must
|
||||
be used for any HTML output). The mock store's string fields (ReachID,
|
||||
PublicKey, etc.) are safe to render directly via `{{.Field}}`.
|
||||
|
||||
### v0.6 §2. Mock Server Data Model — Exact Struct Shapes + Import Paths
|
||||
|
||||
The mock store in `web/store/` instantiates real `x/*/types` structs. Module
|
||||
path is `github.com/oy/openyield` (go.mod:1). The 6 target modules and their
|
||||
exact struct shapes (verified by reading the source):
|
||||
|
||||
**1. Identity — `github.com/oy/openyield/x/identity/types`**
|
||||
(`x/identity/types/types.go:14-21`)
|
||||
```go
|
||||
type Reach struct {
|
||||
ReachID string `json:"reach_id"`
|
||||
HolderID string `json:"holder_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
PublicKey string `json:"public_key"`
|
||||
IsNomad bool `json:"is_nomad"`
|
||||
IsFreeholder bool `json:"is_freeholder"`
|
||||
}
|
||||
```
|
||||
Plus `CitizenshipTier` (`"Nomad"`/`"Freeholder"`, types.go:24-29) for the
|
||||
signup screen's tier selection. The UI's "Create a Reach" screen instantiates
|
||||
`Reach{ReachID:..., IsNomad:true, ...}` (lexicon-safe: "sign up" maps to
|
||||
"Create a Reach" per REQ-012 — "account" is banned).
|
||||
|
||||
**2. Stash — `github.com/oy/openyield/x/stash/types`**
|
||||
(`x/stash/types/types.go:14-21`)
|
||||
```go
|
||||
type Stash struct {
|
||||
HolderID string `json:"holder_id"`
|
||||
StashID string `json:"stash_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
LastActive int64 `json:"last_active"`
|
||||
BalanceGrain int64 `json:"balance_grain"`
|
||||
IsStill bool `json:"is_still"` // Still = Holder paused partner access
|
||||
}
|
||||
```
|
||||
Plus `StashActivity` (types.go:24-29) with `ActiveDays uint32`, `MaxGapDays
|
||||
uint32`, `LastActivityDay int64` — and the `IsMature()` helper (types.go:38-40)
|
||||
that checks `ActiveDays >= MaturityThresholdDays && MaxGapDays <=
|
||||
MaxGapForMaturity`. Locked consts: `MaturityThresholdDays = 90` (types.go:32),
|
||||
`MaxGapForMaturity = 30` (types.go:35). The Stash dashboard screen displays
|
||||
`BalanceGrain` (converted to Bread via `x/bread/types` — see §below) and the
|
||||
maturity progress (`ActiveDays/90`).
|
||||
|
||||
**3. Window — `github.com/oy/openyield/x/window/types`**
|
||||
(`x/window/types/types.go:82-93`)
|
||||
```go
|
||||
type Window struct {
|
||||
WindowID string `json:"window_id"`
|
||||
GrantorHolder string `json:"grantor_holder"`
|
||||
Grantee string `json:"grantee"`
|
||||
Scope Scope `json:"scope"`
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
RateLimit RateLimit `json:"rate_limit"`
|
||||
Revoked bool `json:"revoked"`
|
||||
Status WindowStatus `json:"status"`
|
||||
AuditLogRefs []string `json:"audit_log_refs"`
|
||||
}
|
||||
```
|
||||
Supporting types: `ScopeKind` (types.go:19-25: `ReadStash`/`ReadStanding`/
|
||||
`ProcessPassActForStand`), `Scope` (types.go:28-31: `Kind ScopeKind`,
|
||||
`ResourceID string`), `RateLimit` (types.go:36-40: `MaxActions uint32`,
|
||||
`PerDurationSeconds int64`, `ActionsConsumed uint32`), `AuditEntry`
|
||||
(types.go:56-62), `WindowStatus` (types.go:65-72: `Open`/`Active`/`Revoked`/
|
||||
`Expired`), `WindowStatusCount = 4` (types.go:76, locked). Methods the mock
|
||||
store can invoke to demonstrate lifecycle: `Window.Activate()` (types.go:123-
|
||||
129, Open→Active), `Window.Revoke()` (types.go:100-112, idempotent, Expired
|
||||
is terminal), `Window.Expire()` (types.go:117-119), `RateLimit.Consume()`
|
||||
(types.go:46-52, returns false when cap reached). The Window authorization
|
||||
screen lets a Holder grant a scoped, time-limited, rate-limited Window.
|
||||
|
||||
**4. Standing — `github.com/oy/openyield/x/standing/types`**
|
||||
(`x/standing/types/types.go:44-96`)
|
||||
```go
|
||||
type Rating struct {
|
||||
RaterID, RateeID, Category, TxRef string
|
||||
Score, Weight float64
|
||||
Timestamp int64
|
||||
DecayBucket uint8
|
||||
}
|
||||
type Vouch struct {
|
||||
VoucherID, VoucheeID, Category string
|
||||
BondAmount int64
|
||||
Timestamp int64
|
||||
}
|
||||
type Slash struct {
|
||||
ReachID, Reason, Attester string
|
||||
Amount float64
|
||||
Timestamp int64
|
||||
}
|
||||
type FreeholderSignals struct {
|
||||
StashMaturity bool
|
||||
MultiDomainStanding bool
|
||||
CommittedCapital bool
|
||||
CommunityEndorsement bool
|
||||
}
|
||||
```
|
||||
Helpers the UI invokes: `FreeholderSignals.IsFreeholderEligible()` (types.go:94-
|
||||
96, all four must be true), `GetStandingBucket(score, ratingCount, isSlashed)`
|
||||
(types.go:129-145, returns `New`/`Trusted`/`Preferred`/`Top`/`Slashed`),
|
||||
`ComputeDiversityBonus(categoryCount)` (types.go:99-109), `GetVoucherWeight(
|
||||
isFreeholder, standingScore, ratingCount)` (types.go:112-126). Locked formula
|
||||
consts (types.go:12-41): `PriorMean=4.0`, `PriorWeight=10`, decay buckets
|
||||
(`1.0`/`0.5`/`0.25`/`0.0`), diversity bonuses (`0.05`/`0.10`/`0.15`), voucher
|
||||
weights (`1.5`/`1.2`/`1.0`/`0.5`/`0.3`), min counterparties (`3`/`10`/`30`),
|
||||
Freeholder thresholds (`90` days, `30` max gap, `4.5` score, `3` categories).
|
||||
The Standing + Freeholder signals screen displays the 4 signals and the
|
||||
computed bucket.
|
||||
|
||||
**5. Bread — `github.com/oy/openyield/x/bread/types`**
|
||||
(`x/bread/types/types.go:13, 30-50`)
|
||||
```go
|
||||
const GrainsPerBread = 10000
|
||||
type BreadScale struct {
|
||||
Name string
|
||||
GrainValue int64
|
||||
}
|
||||
func BreadScaleAll() []BreadScale // returns 11 denominations
|
||||
```
|
||||
**Source of truth = code constants, NOT docs** (D-073). The 11-tier scale
|
||||
from `BreadScaleAll()` (types.go:37-49), each tier 100× the previous (NOT
|
||||
1,000× as the outdated `docs/shared/bread-scale.md:7-19` claims):
|
||||
| Tier | Name | GrainValue |
|
||||
|---|---|---|
|
||||
| 1 | Grain | 1 |
|
||||
| 2 | Crumb | 100 |
|
||||
| 3 | Bread | 10,000 |
|
||||
| 4 | Loaf | 100,000 |
|
||||
| 5 | Batch | 1,000,000 |
|
||||
| 6 | Cake | 10,000,000 |
|
||||
| 7 | Bakery | 100,000,000 |
|
||||
| 8 | Granary | 1,000,000,000 |
|
||||
| 9 | Mill | 10,000,000,000 |
|
||||
| 10 | Harvest | 100,000,000,000 |
|
||||
| 11 | Earth | 1,000,000,000,000 |
|
||||
|
||||
Verified by `x/bread/types/types_test.go:9-28` (asserts `GrainsPerBread ==
|
||||
10000` and the full `BreadScaleAll()` table). The UI's Stash dashboard and
|
||||
Bloom accrual screens use these code constants for all Grain↔Bread
|
||||
conversions. **Doc-drift finding**: `docs/shared/bread-scale.md:10-19` states
|
||||
Crumb=1,000 Grain and each tier is 1,000× the previous — this contradicts the
|
||||
code (Crumb=100, each tier 100×). D-073 flags a doc-fix as a P1+ follow-up
|
||||
(NOT a v0.6 deliverable); the UI uses the code constants.
|
||||
|
||||
**6. Bloom — `github.com/oy/openyield/x/bloom/types`**
|
||||
(`x/bloom/types/types.go:23-28`)
|
||||
```go
|
||||
type BloomRecord struct {
|
||||
StashID string `json:"stash_id"`
|
||||
AccruedGrain int64 `json:"accrued_grain"`
|
||||
LastAccrualBlock int64 `json:"last_accrual_block"`
|
||||
RateBasisPoints uint32 `json:"rate_basis_points"`
|
||||
}
|
||||
```
|
||||
Locked consts (types.go:13-19): `TargetBloomRateBasisPoints = 450` (4.5%),
|
||||
`MinBloomRateBasisPoints = 400`, `MaxBloomRateBasisPoints = 500`,
|
||||
`AccrualPeriodBlocks = 144`. The Bloom accrual screen displays a Stash's
|
||||
`BloomRecord` — `AccruedGrain` (in Bread-scale via `x/bread/types`), the
|
||||
rate in bps, and the accrual block. `MissionLockBloom` (types.go:52) is the
|
||||
locked const: "Bloom originates only from real production. No synthetic
|
||||
Bloom. No protocol-printed Bloom." — the UI surfaces this as a tooltip/
|
||||
explanatory note.
|
||||
|
||||
**G-003 scope confirmation** — `web/store/` importing `x/*/types` is app-
|
||||
layer consumption (the mock store reads type definitions to populate
|
||||
fixtures), NOT cross-`x/` production import. The G-003 production firewall
|
||||
governs keeper-to-keeper cross-module calls (use `expected_keepers.go`
|
||||
interface shims, not struct imports). `web/` is not a keeper; it imports
|
||||
types packages the same way `x/*/types/types_test.go` does (test-only
|
||||
exemption). The firewall is intact (D-067 rationale).
|
||||
|
||||
### v0.6 §3. Lexicon Firewall Extension — `lexicon_meta_web_test.go`
|
||||
|
||||
**Existing pattern to mirror** — Two meta-tests exist:
|
||||
1. `lexicon_meta_test.go` (repo root, package `lexicon_meta`) — scans
|
||||
`x/**/*.go` (lexicon_meta_test.go:37-72).
|
||||
2. `lexicon_meta_docs/lexicon_meta_docs_test.go` (subdir, package
|
||||
`lexicon_meta_docs`) — scans `README.md` + `docs/**/*.md`
|
||||
(lexicon_meta_docs_test.go:75-122).
|
||||
|
||||
Both share a single source of truth via `lexicon.SyntheticBannedStrings()`
|
||||
(lexicon.go:111-125, REQ-029 / G-014) and `lexicon.FindBannedTerm()`
|
||||
(lexicon.go:73-82, word-boundary case-insensitive regex match).
|
||||
|
||||
**Pattern for `lexicon_meta_web_test.go`** — A new sibling meta-test,
|
||||
mirroring the docs firewall:
|
||||
- **Location**: `lexicon_meta_web/lexicon_meta_web_test.go` (subdir, like
|
||||
`lexicon_meta_docs/` — Go forbids two packages in one dir).
|
||||
- **Package**: `lexicon_meta_web`.
|
||||
- **Scan targets**: `web/templates/**/*.html` + `web/static/**/*.js` (REQ-012
|
||||
extension for the UI surface). NOT `.go` files in `web/` (those are covered
|
||||
by the existing `lexicon_meta_test.go` x/ scan only if `web/` is under
|
||||
`x/` — it is NOT, so `web/**/*.go` must ALSO be scanned; the new test
|
||||
should scan `web/**/*.{html,js,go}` or a separate walk for `.go`).
|
||||
- **Self-exclusion**: via `runtime.Caller(0)` + `thisFile(t)` helper
|
||||
(lexicon_meta_test.go:160-167), the test file excludes itself from its own
|
||||
scan (it references banned terms via the `lexicon` package's fragment-
|
||||
assembled helpers, so no banned-term literal appears in the firewall's own
|
||||
code).
|
||||
- **Detection**: `lexicon.FindBannedTerm(string(bz))` — NO reimplementation.
|
||||
- **Walk-coverage (G-013)**: mirror `TestLexiconMetaDocsWalkCoverage`
|
||||
(lexicon_meta_docs_test.go:218-296) — inject a synthetic banned-term
|
||||
fixture into a temp `web/templates/.lexicon_fixture/` subtree and assert
|
||||
the walk FINDS it (closes the "silently scans nothing, reports green" gap).
|
||||
- **Self-test table (G-009)**: consume `lexicon.SyntheticBannedStrings()`
|
||||
(single source, G-014) — no duplicated table.
|
||||
- **Banned-terms count assertion**: `len(lexicon.BannedTerms()) == 10`
|
||||
(mirror lexicon_meta_test.go:113-125).
|
||||
- **False-positive regression**: `TestLexiconMetaNoFalsePositiveOnOpenYield`
|
||||
(lexicon_meta_test.go:131-143) — "openyield" must NOT trigger "yield".
|
||||
|
||||
**10 banned terms — verified from code** (`lexicon/lexicon.go:30-41`):
|
||||
`bank`, `deposit`, `interest`, `yield`, `currency`, `dollar`, `euro`,
|
||||
`account`, `savings`, `depositor`. Assembled at runtime from two-character
|
||||
fragments (e.g., `{"ba", "nk"}` → "bank") so the firewall's own source
|
||||
contains no banned-term literal. The spec lists 10; plan docs say "9"
|
||||
counting dollar/euro as a pair (lexicon.go:43-48, lexicon_meta_test.go:87-89).
|
||||
|
||||
**Lexicon-safe UI vocabulary** — "account" is banned → the Reach signup
|
||||
screen uses "Create a Reach" (not "Create an account"). "deposit"/"savings"
|
||||
banned → the Stash dashboard uses "Stash" / "balance" / "Grain" / "Bread".
|
||||
"interest"/"yield" banned → the Bloom screen uses "Bloom" / "accrual" /
|
||||
"rate". "bank"/"currency"/"dollar"/"euro" banned → Bread-scale only. The
|
||||
meta-test enforces this across all `web/templates/**` + `web/static/**`.
|
||||
|
||||
### v0.6 §4. HTTP Routing (stdlib — Go 1.22 `net/http.ServeMux`)
|
||||
|
||||
**Go version confirmed**: `go.mod:3` declares `go 1.22`. Go 1.22 enhanced
|
||||
`net/http.ServeMux` with method + path-pattern routing (no third-party
|
||||
router needed — G-006 preserved; `gorilla/mux` is a transitive cosmos-sdk
|
||||
dep at go.mod:75 but is NOT used by `web/`).
|
||||
|
||||
**Pattern** — Go 1.22 `ServeMux` supports `"METHOD /path/{param}"` patterns:
|
||||
```go
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /reaches/{id}", reachHandler) // path param
|
||||
mux.HandleFunc("POST /reaches", createReachHandler) // form submit
|
||||
mux.HandleFunc("GET /stashes/{id}", stashHandler)
|
||||
mux.HandleFunc("POST /windows/{id}/revoke", revokeWindowHandler)
|
||||
```
|
||||
Path-parameter extraction: `r.PathValue("id")` (replaces the legacy
|
||||
`r.URL.Query().Get()` hack). Method matching is exact: `"GET /reaches/{id}"`
|
||||
matches only GET; a POST to the same path 404s unless a `"POST /reaches"`
|
||||
handler is also registered. `{$}` suffix anchors the pattern (e.g.,
|
||||
`"GET /"` matches only the root, not `/foo`).
|
||||
|
||||
**Why stdlib, not gorilla/mux** — G-006 (zero-dep for the UI layer): the
|
||||
mock server is a standalone Go binary in `web/`, not a Cosmos module. It
|
||||
should not pull router deps that exist only because cosmos-sdk transitively
|
||||
requires them. Go 1.22's enhanced `ServeMux` covers all 5 screens' routing
|
||||
needs (GET for render, POST for form submit, path params for entity detail).
|
||||
The `gorilla/mux` at go.mod:75 is an indirect cosmos-sdk dep — `web/` does
|
||||
not import it.
|
||||
|
||||
**Static assets** — `mux.Handle("/static/", http.StripPrefix("/static/",
|
||||
http.FileServer(http.Dir("web/static"))))` serves the vendored `htmx.min.js`
|
||||
+ CSS. No middleware needed for the MVP.
|
||||
|
||||
### v0.6 §5. PERSONAS.md Update — frontend-engineer Activation
|
||||
|
||||
**Current state** — `.ciagent/oy/PERSONAS.md:52-56` lists `frontend-engineer`
|
||||
under `deactivated:` with reason "INACTIVE for v0.5. The v0.3 docs site is
|
||||
COMPLETE; v0.5 has no UI/docs-content work." This is the project's FIRST UI
|
||||
milestone — frontend-engineer must be **activated** for v0.6.
|
||||
|
||||
**YAML frontmatter format** (PERSONAS.md:1-61) — The file uses YAML frontmatter
|
||||
between `---` fences with this structure:
|
||||
```yaml
|
||||
---
|
||||
active_personas:
|
||||
- id: <persona-id>
|
||||
active: true
|
||||
phase_specific: false | true
|
||||
reason: <paragraph — why active, what they own>
|
||||
frameworks: [<list>]
|
||||
territory: [<glob list>]
|
||||
constraints: [<list of invariant strings>]
|
||||
|
||||
phase_specific_personas: # optional, for phase-scoped personas
|
||||
- id: ...
|
||||
...
|
||||
|
||||
deactivated:
|
||||
- id: <persona-id>
|
||||
reason: <paragraph — why inactive>
|
||||
|
||||
custom_personas: []
|
||||
---
|
||||
```
|
||||
Followed by a Markdown body (`# Personas: OpenYield (oy) — v0.6 ...`) with an
|
||||
Active Roster table, Phase-Persona Matrix, and Constraints Carried Forward
|
||||
sections (PERSONAS.md:63-133).
|
||||
|
||||
**Proposed frontend-engineer activation** (v0.6):
|
||||
- **Move** `frontend-engineer` from `deactivated:` (PERSONAS.md:52-54) to
|
||||
`active_personas:` (after the existing active personas).
|
||||
- **`active: true`**, **`phase_specific: false`** (spans all v0.6 phases —
|
||||
P1..P5 for the 5 screens + lexicon firewall).
|
||||
- **`reason`**: First UI milestone. Owns the Go `html/template` + HTMX mock
|
||||
Web UI in `web/` — 5 screens (Reach signup, Stash dashboard, Window
|
||||
authorization, Standing + Freeholder signals, Bloom accrual), the vendored
|
||||
HTMX static asset, the in-memory mock store's template rendering, and the
|
||||
`lexicon_meta_web_test.go` firewall extension. Co-owns the mock store's
|
||||
Go type integration with backend-engineer (the store imports `x/*/types`).
|
||||
- **`frameworks`**: `[Go 1.22, html/template (stdlib), HTMX 2.0.x (vendored JS), net/http.ServeMux (Go 1.22), Go testing]`. **NO node, NO React, NO
|
||||
package.json, NO build step.**
|
||||
- **`territory`**: `["web/templates/**", "web/static/**", "web/handlers/**",
|
||||
"web/store/**", "web/main.go", "lexicon_meta_web/lexicon_meta_web_test.go"]`.
|
||||
- **`constraints`**:
|
||||
- "G-006 zero-dep preserved — HTMX is a vendored static JS file
|
||||
(`web/static/htmx.min.js`), NOT a `go get` dependency; `go.mod` unchanged
|
||||
by v0.6 UI work; no node/npm/package.json toolchain"
|
||||
- "G-003 app-layer consumption — `web/store/` imports `x/*/types` to
|
||||
instantiate real structs (Reach, Stash, Window, FreeholderSignals,
|
||||
BloomRecord); this is app-layer consumption, NOT cross-`x/` production
|
||||
import; the production firewall (keeper-to-keeper shims) is intact"
|
||||
- "REQ-012 lexicon firewall extended — new `lexicon_meta_web_test.go`
|
||||
scans `web/templates/**` + `web/static/**`; 'account' is banned — Reach
|
||||
signup uses 'Create a Reach'; 'deposit'/'savings'/'interest'/'yield'/
|
||||
'bank'/'currency'/'dollar'/'euro'/'depositor' all banned"
|
||||
- "Bread-scale source of truth = `x/bread/types` code constants
|
||||
(`GrainsPerBread=10000`, `BreadScaleAll()` 11-tier table, each tier
|
||||
100× the previous), NOT `docs/shared/bread-scale.md` (which is outdated —
|
||||
states 1,000× ratios; D-073)"
|
||||
- "`html/template` auto-escaping prevents XSS in user-submitted content
|
||||
(Reach form input); no manual escaping needed"
|
||||
- "HTMX progressive enhancement — forms work without JS (native
|
||||
`action`/`method` fallback); `HX-Request` header distinguishes fragment
|
||||
vs full-page rendering in handlers"
|
||||
|
||||
**backend-engineer co-ownership note** — Add a note that backend-engineer
|
||||
co-owns the mock store's Go type integration (`web/store/` importing
|
||||
`x/*/types`), since backend-engineer owns the `x/` module types. The
|
||||
frontend-engineer owns the template/handler/HTMX layer; backend-engineer
|
||||
advises on the struct shapes and locked consts. This mirrors the v0.5
|
||||
cosmos-engineer advisory pattern.
|
||||
|
||||
**Other persona changes** — backend-engineer, lead-developer stay active
|
||||
(lead-developer owns `.ciagent/` updates + milestone ship). security-
|
||||
engineer, cosmos-engineer, mesh-engineer, data-engineer: **deactivate** for
|
||||
v0.6 (no runtime promotion, no IBC, no bearers, no custody store — v0.6 is
|
||||
UI-only, mock data, no chain). ci-security-auditor: default off, activate in
|
||||
the final phase for the feature purity gate. docs-writer: stays deactivated
|
||||
(the doc-drift fix for `bread-scale.md` is a P1+ follow-up, not a v0.6
|
||||
deliverable — D-073).
|
||||
|
||||
### v0.6 §6. ARCHITECTURE.md Update — Proposed Section Outline
|
||||
|
||||
**Current structure** (`.ciagent/oy/ARCHITECTURE.md`, 514 lines) — top-level
|
||||
sections by line:
|
||||
- `# Architecture: OpenYield (oy) — Phase 0 Index` (line 1)
|
||||
- `## Source` (3), `## Component Index` (7), `## Cross-Component Interfaces`
|
||||
(26), `## Critical Blocker Chain` (34), `## Non-Negotiables` (45), `##
|
||||
Phase 0 Architecture Deliverables` (57)
|
||||
- `## v0.3 Architecture (Bearers & Documentation)` (64) + subsections
|
||||
(72-186)
|
||||
- `## v0.4 Architecture (Refinement — NFR)` (188) + subsections (194-264)
|
||||
- `## v0.5 Runtime Architecture (Bearers Runtime)` (266) + subsections
|
||||
(277-514, the file's end)
|
||||
|
||||
**Insertion point** — A new `## v0.6 Architecture (Nomad Web UI MVP)`
|
||||
section appended AFTER the v0.5 section (after line 514, the file's current
|
||||
end). This matches the chronological append convention (v0.3 → v0.4 → v0.5
|
||||
→ v0.6).
|
||||
|
||||
**Proposed section outline** (do NOT write — report only):
|
||||
```
|
||||
## v0.6 Architecture (Nomad Web UI MVP)
|
||||
### v0.6 Scope Recap (from D-066..D-073)
|
||||
### v0.6 Component Map (new web/ dir — not a Cosmos module)
|
||||
- web/main.go (entrypoint, Go 1.22 net/http.ServeMux)
|
||||
- web/handlers/ (5 screen handlers + fragment vs full-page dispatch)
|
||||
- web/store/ (in-memory mock store, imports x/*/types)
|
||||
- web/templates/ (base.html layout + 5 page templates + fragments/)
|
||||
- web/static/ (vendored htmx.min.js 2.0.10 + style.css)
|
||||
### v0.6 Mock Data Layer (D-067 — real x/*/types structs, no keeper)
|
||||
- 6 imported type packages: identity, stash, window, standing, bread, bloom
|
||||
- Locked consts surfaced: GrainsPerBread=10000, MaturityThresholdDays=90,
|
||||
TargetBloomRateBasisPoints=450, standing formula constants
|
||||
- Bread-scale source = code constants (D-073), NOT docs/shared/bread-scale.md
|
||||
### v0.6 G-003 Firewall (app-layer consumption, not cross-x/ production)
|
||||
- web/store/ imports x/*/types (like x/*/types/types_test.go test exemption)
|
||||
- No keeper, no expected_keepers.go, no sdk.Msg — web/ is not a Cosmos module
|
||||
### v0.6 G-006 Zero-Dep (HTMX vendored as static asset, not a Go dep)
|
||||
- go.mod unchanged by v0.6
|
||||
- htmx.min.js is a static file served by http.FileServer, not a go get
|
||||
### v0.6 Lexicon Firewall Extension (REQ-012 — new lexicon_meta_web_test.go)
|
||||
- Scans web/templates/** + web/static/** (+ web/**/*.go)
|
||||
- 10 banned terms (account, bank, deposit, savings, interest, yield, ...)
|
||||
- "Create a Reach" not "Create an account"
|
||||
### v0.6 Interface Contracts (unchanged — UI is read-only mock, no new x/ interfaces)
|
||||
```
|
||||
|
||||
This outline mirrors the v0.5 section's subsection density (scope recap →
|
||||
component map → per-concern firewall/dep sections → interface contracts).
|
||||
|
||||
### v0.6 Cross-Reference Summary
|
||||
|
||||
| Concern | Source | Reference |
|
||||
|---|---|---|
|
||||
| Module path | go.mod:1 | `github.com/oy/openyield` |
|
||||
| Go version | go.mod:3 | `go 1.22` (ServeMux method patterns) |
|
||||
| Reach struct | x/identity/types/types.go:14-21 | 6 fields |
|
||||
| Stash struct | x/stash/types/types.go:14-21 | 6 fields + IsMature() helper |
|
||||
| Window struct | x/window/types/types.go:82-93 | 10 fields + Activate/Revoke/Expire |
|
||||
| FreeholderSignals | x/standing/types/types.go:85-90 | 4 bools + IsFreeholderEligible() |
|
||||
| BreadScaleAll | x/bread/types/types.go:36-50 | 11 tiers, 100× ratios (NOT 1,000×) |
|
||||
| BloomRecord | x/bloom/types/types.go:23-28 | 4 fields + TargetBloomRateBasisPoints=450 |
|
||||
| Banned terms | lexicon/lexicon.go:30-41 | 10 terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor) |
|
||||
| Meta-test pattern | lexicon_meta_test.go:37-72 | Walk + FindBannedTerm + self-exclusion |
|
||||
| Docs meta-test | lexicon_meta_docs/lexicon_meta_docs_test.go:75-122 | subdir pattern + G-013 walk-coverage |
|
||||
| HTMX version | htmx.org docs | 2.0.10 (stable, single JS file, no build) |
|
||||
| PERSONAS.md format | .ciagent/oy/PERSONAS.md:1-61 | YAML frontmatter + Markdown body |
|
||||
| ARCHITECTURE.md end | .ciagent/oy/ARCHITECTURE.md:514 | v0.6 section appends after |
|
||||
| D-067 (mock store) | PROJECT.md:303 | Go HTTP server, real x/*/types structs |
|
||||
| D-073 (bread-scale) | PROJECT.md:309 | Code constants, not docs (docs outdated) |
|
||||
|
||||
### v0.6 Assumptions (logged with confidence scores; not flagged for human validation)
|
||||
|
||||
1. **HTMX 2.0.10 is the version to vendor** — the htmx.org quick-start
|
||||
script tag pins `htmx.org@2.0.10`; v4 is in beta (Summer '26 target).
|
||||
2.0.10 is the current stable. Confidence 0.95.
|
||||
2. **`web/**/*.go` must be scanned by the lexicon firewall** — the existing
|
||||
`lexicon_meta_test.go` scans only `x/**/*.go`; `web/` is a new top-level
|
||||
dir NOT under `x/`. The new `lexicon_meta_web_test.go` should scan
|
||||
`web/**/*.{html,js,go}` (or a separate `.go` walk) to cover handler/
|
||||
store Go files. Confidence 0.85.
|
||||
3. **`html/template` auto-escaping is sufficient for the MVP** — the mock
|
||||
UI has no authenticated users and no persistent storage; user input
|
||||
(Reach signup form) is rendered via `{{.Field}}` which context-auto-
|
||||
escapes. No `template.JS`/`template.HTML` unsafe injection points needed
|
||||
for the MVP. Confidence 0.90.
|
||||
4. **Go 1.22 `ServeMux` covers all 5 screens' routing** — GET (render) +
|
||||
POST (form submit) + path params (`/reaches/{id}`) is the full routing
|
||||
surface; no middleware, no wildcard host matching needed. Confidence
|
||||
0.95.
|
||||
5. **`web/main.go` is the entrypoint (not `cmd/oyd-ui/main.go`)** — D-068
|
||||
says `web/` contains `main.go` (or `cmd/oyd-ui/main.go`); the simpler
|
||||
`web/main.go` matches the mock-server scope (single binary, no
|
||||
subcommands). Confidence 0.80.
|
||||
Confidence 0.75.
|
||||
@@ -421,346 +421,3 @@ All four REQs (REQ-029..REQ-032) delivered. The three real v0.3 forward-referenc
|
||||
**P0 fixes auto-applied: 0**
|
||||
**P1+ findings: 0 P1, 3 P2 (all nits, post-hoc, non-blocking)**
|
||||
**Confidence in overall verdict: 0.90**
|
||||
|
||||
---
|
||||
|
||||
# REVIEW: OpenYield (oy) — v0.5 (Bearers Runtime) Final Phase
|
||||
|
||||
> **Reviewer**: CIAgent multi-persona code review (correctness, testing, security, performance, maintainability, adversarial)
|
||||
> **Date**: 2026-08-18
|
||||
> **Branch**: `phase/08-final-review-ship` (off `milestone/v0.5-bearers-runtime`)
|
||||
> **Scope**: `4369b3e..HEAD` — all v0.5 execution work (P1..P7: x/exit + x/bridge, x/bearers, x/partner, x/hub, x/services, x/bond, x/council runtime promotion)
|
||||
> **Milestone**: v0.5 — Bearers Runtime (feature)
|
||||
> **Mode**: multi-project (slug `oy`)
|
||||
> **Autonomy**: full — P0 auto-applied; P1+ flagged for post-hoc review (do not block ship)
|
||||
|
||||
---
|
||||
|
||||
## Verification Commands Run
|
||||
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `go build ./...` | **GREEN** (exit 0) |
|
||||
| `go test ./...` | **GREEN** (all packages pass; 729 tests / 36 test files) |
|
||||
| `go test -cover ./x/{exit,bridge,bearers,partner,hub,services,bond,council}/keeper/...` | **ALL ≥80%** (range 82.1%–92.5%) |
|
||||
| `go test -run TestG003NoCrossModuleStructImportsInProduction ./x/window/types/` | **GREEN** (G-003 production firewall intact) |
|
||||
| `go test -run TestLexiconMeta ./...` | **GREEN** (both lexicon firewalls: x/ + docs/) |
|
||||
| `git log --format="%s" \| grep -E "^feat:"` | **non-empty** (P1..P7 are `feat` — feature milestone, correct) |
|
||||
| cross-module production import scan (sed-based, by-module) | **ZERO cross-module struct imports** (all `openyield/x/<m>/types` imports are intra-module) |
|
||||
| baseline keeper dirs (mirror/forge/still/watcher/bread) | **EMPTY** (G-022 — v0.1 keepers NOT promoted) |
|
||||
| `grep cosmos-sdk in x/*/types/*_test.go` | **ZERO HITS** (G-024 stdlib-only invariant/lexicon tests) |
|
||||
|
||||
### Coverage detail (8 keeper packages — D-054 simtest grade)
|
||||
|
||||
| Package | Coverage |
|
||||
|---|---|
|
||||
| x/exit/keeper | 85.0% |
|
||||
| x/bridge/keeper | 82.1% |
|
||||
| x/bearers/keeper | 91.2% |
|
||||
| x/partner/keeper | 87.6% |
|
||||
| x/hub/keeper | 90.0% |
|
||||
| x/services/keeper | 91.5% |
|
||||
| x/bond/keeper | 92.5% |
|
||||
| x/council/keeper | 90.3% |
|
||||
|
||||
All 8 keeper packages exceed the ≥80% target (D-033); floor is 82.1% (x/bridge).
|
||||
|
||||
---
|
||||
|
||||
## 1. Per-Lens Findings
|
||||
|
||||
### 1.1 Correctness (backend-engineer lens) — **PASS** (confidence 0.88)
|
||||
|
||||
**MsgServer state-machine ordering.** All 8 keepers follow the mandated ordering
|
||||
`ValidateBasic → keeper authz → state mutation → event emit`. Verified by reading
|
||||
the handler headers + bodies in `x/{exit,bridge,bearers,partner,hub,services,bond,
|
||||
council}/keeper/msg_server.go` — each handler's doc comment enumerates the ordering
|
||||
and the body implements it. No handler mutates state before `ValidateBasic`.
|
||||
|
||||
**IBC handlers (x/bridge).** `OnRecvPacket` writes the in-flight record after mint
|
||||
(A-513 replay protection). `OnAcknowledgementPacket` deletes the in-flight record on
|
||||
first ack and **returns ERROR** on a second ack (`ibc_module.go:314-316` — G-021 landed).
|
||||
`OnTimeoutPacket` refunds exactly-once via the `Refunded` flag guard (`ibc_module.go:
|
||||
349-353` — A-513 landed). The `Refunded` flip is state-write-first (A-521 idempotency
|
||||
pattern).
|
||||
|
||||
**CLOB matching (x/bond).** Price-time priority FCFS via `sortRestingBook` (sort by
|
||||
price, then sequence — `clob.go:229-249`). Per-match REJECT above 800 bps via
|
||||
`ImpliedCoupon > CouponCapBps` (`clob.go:174-179` — D-063 landed). The G-019
|
||||
`ImpliedCoupon` helper is the single formula used by both match and clamp
|
||||
(`clob.go:108-115`). Boundary unit test covers 800/801/799 bps
|
||||
(`msg_server_simtest_test.go:830+`).
|
||||
|
||||
**Council governance (x/council).** `MsgSubmitProposal.ValidateBasic` rejects the
|
||||
`MissionLockAmendment-Rejected` kind (`types_test.go:760-777` — D-064 landed). The
|
||||
handler ALSO rejects it at the kind-switch as defence-in-depth (`msg_server.go:141-
|
||||
144`). Veto quorum default 6 with `Params.Validate` bounds [2,9] (`types.go:194-204`
|
||||
— D-065 + G-020 landed). Single-Veto-no-block is enforced by the quorum rule.
|
||||
|
||||
**OY-QR one-shot (x/bearers).** `ConsumeOYQR` flips `consumed=true` BEFORE the
|
||||
`BreadKeeper.TransferGrain` effect (`msg_server.go:354-355` — A-521 landed). A replay
|
||||
finds `consumed==true` and returns an error (`msg_server.go:336-337`). A failed
|
||||
transfer rolls back the consumed flip (SDK store atomicity — `msg_server.go:365-369`).
|
||||
|
||||
**Compliance-before-custody (x/hub).** `CustodyReleaseAsset` consults the
|
||||
ComplianceKeeper shim's `IsCompliant` BEFORE releasing custody (`msg_server.go:220-
|
||||
226` — A-544 landed).
|
||||
|
||||
**Lending coupon clamp (x/hub).** `RecordLendingPrimitive` clamps the coupon to
|
||||
`[LendingCouponFloorBps=0, LendingCouponCapBps=800]` via `ClampLendingCoupon`
|
||||
(`msg_server.go:268+` — A-543 landed).
|
||||
|
||||
**Locked-const firewall verified** (all v0.1..v0.4 consts unchanged; v0.5 P7
|
||||
additive enums ProposalKindCount=4 / ProposalStatusCount=5 / VoteOptionCount=4):
|
||||
ExitStatusCount=5, BridgeStatusCount=4, BearerTypeCount=6 (via `len(AllBearers())`),
|
||||
OYSATLink.SurveillanceResistant=true, PartnerTierCount=4,
|
||||
AnchorCredentialStatusCount=4, PartnerStatusCount=4, HubServiceCount=3,
|
||||
LendingCouponCapBps=uint32(800), LendingCouponFloorBps=uint32(0), ServiceKindCount=4,
|
||||
CouponCapBps=800, CouponFloorBps=0, OrderSideCount=2, OrderStatusCount=3,
|
||||
CouncilKindCount=3, SignalKindCount=4, MissionLockAmendable=false (council + pact).
|
||||
All match the spec values.
|
||||
|
||||
### 1.2 Testing (security-engineer lens) — **PASS** (confidence 0.87)
|
||||
|
||||
**Coverage.** All 8 keeper packages ≥80% (82.1%..92.5% — verified by `go test -cover`).
|
||||
|
||||
**G-017 reconciliation.** `TestTallyResultNoWithVetoDefaultZero` (renamed from the
|
||||
v0.2 `TestTallyResultNoWithVetoAlwaysZero`) and the new
|
||||
`TestTallyResultNoWithVetoPopulatedByQuorum` BOTH pass
|
||||
(`x/council/types/types_test.go:255-304` — verified by running both with `go test
|
||||
-v`). The v0.2 regression protection is preserved (renamed + re-scoped, NOT
|
||||
deleted); the v0.5 populated case is covered by the new test.
|
||||
|
||||
**G-019 boundary test.** `TestImpliedCouponBoundary` covers price-bps 9200 (→800,
|
||||
at cap), 9199 (→801, REJECTED), 9201 (→799, in-band) at
|
||||
`x/bond/keeper/msg_server_simtest_test.go:830+`. Plus
|
||||
`TestImpliedCouponBoundaryAtCapClears` and `TestImpliedCouponBoundaryAboveCapRejected`
|
||||
exercise the full match path at the boundary.
|
||||
|
||||
**G-024 stdlib-only invariant tests.** `grep -rln "cosmos-sdk\|sdk.Msg\|sdk.Context"
|
||||
x/*/types/*_test.go` returns ZERO hits — invariant/lexicon tests remain stdlib-only.
|
||||
|
||||
**Replay/timeout/negative tests.** `x/bridge/keeper/msg_server_simtest_test.go`
|
||||
exercises the second-OnAck-ERROR (G-021) and timeout-refund-exactly-once (A-513).
|
||||
`x/bearers/keeper/msg_server_simtest_test.go` exercises the consumed-before-transfer
|
||||
(A-521) + replay reject. `x/council/keeper/msg_server_simtest_test.go` exercises the
|
||||
MissionLockAmendment reject + Veto quorum. All green.
|
||||
|
||||
**Simtest happy-path scenarios.** The 8 simtest files cover the MVP/UX flows: bridge
|
||||
ICS-20 recv/ack/timeout, bearers OY-QR consume, partner anchor credential issuance,
|
||||
hub custody release + lending, services lifecycle, bond CLOB match, council
|
||||
proposal/vote/tally.
|
||||
|
||||
### 1.3 Security (security-engineer lens) — **PASS** (confidence 0.86)
|
||||
|
||||
**G-003 production firewall.** A sed-based by-module scan of all non-test `.go`
|
||||
files under `x/` returns ZERO cross-module `openyield/x/<other>/{types,keeper}`
|
||||
imports. All cross-module coupling is via `expected_keepers.go` interfaces (8 files,
|
||||
one per module). The existing G-003 import-invariant test (`x/window/types/
|
||||
types_test.go:437`) auto-covers the new files and passes.
|
||||
|
||||
**Locked-consts unchanged.** Verified by direct `grep` of every const listed in the
|
||||
review brief (see §1.1). The v0.5 P7 enums are ADDITIVE (new types), not amendments
|
||||
— feature purity gate satisfied.
|
||||
|
||||
**G-018 go.mod go directive.** `go.mod` line 3: `go 1.22` (NOT bumped). The cosmos-sdk
|
||||
v0.50.8 + ibc-go v8.2.1 transitive tree builds under go 1.22 (G-018 hard gate green —
|
||||
`go build ./...` exits 0).
|
||||
|
||||
**G-020 WatcherVetoQuorum bounds.** `Params.Validate()` rejects `< 2` and `> 9`
|
||||
(`x/council/types/types.go:199-204`). The bounds [2,9] forbid single-Veto-block
|
||||
(min 2) and unsatisfiable quorum (max 9 — the Watcher set size per REQ-004).
|
||||
|
||||
**Lexicon firewall green.** `go test -run TestLexiconMeta ./...` green across all
|
||||
new runtime files (no banned terms in x/ or docs/).
|
||||
|
||||
**IBC denom trace parser pinned to ICS-20 v1.** `ValidateDenomTrace` +
|
||||
`ParseDenomTrace` (`x/bridge/keeper/ibc_module.go:101-132`) parse the
|
||||
`transfer/channel-N/<denom>` shape (ICS-20 v1). No IBC-v2/Eureka parsing.
|
||||
|
||||
### 1.4 Performance (backend-engineer lens) — **PASS** (confidence 0.80)
|
||||
|
||||
**CLOB matching.** `matchTaker` sorts the resting book once (`sortRestingBook`, O(n
|
||||
log n)) then walks in price-time order, stopping at the first non-crossing price
|
||||
(`clob.go:155-207`). No quadratic scan. The book load (`restingBookForBond`) is
|
||||
O(n) over resting orders for the bond; acceptable for simtest-grade depth. A
|
||||
production keeper would use prefix-key iteration; the simtest-grade `AllRestingOrders`
|
||||
+ filter is O(n) and adequate (no hot-path concern at simtest depth).
|
||||
|
||||
**Keeper stores.** All keepers use prefix-keyed store access (StoreKey + per-entity
|
||||
prefixes). No full-table scans in the handler hot paths (the CLOB book load is the
|
||||
only O(n) walk and it is bounded by resting orders for one bond).
|
||||
|
||||
**Hot-path allocations.** No obvious hot-path allocations in the match loop (the
|
||||
`filledOrderIDs` slice grows by append; the resting book is a single allocation).
|
||||
Acceptable for simtest grade.
|
||||
|
||||
### 1.5 Maintainability (lead-developer lens) — **PASS** (confidence 0.85)
|
||||
|
||||
**Code style consistency.** The 8 runtime modules follow the v0.1..v0.4 skeleton
|
||||
conventions: `types/` package owns structs/enums/consts/ValidateBasic; `keeper/`
|
||||
owns the MsgServer + state; `module.go` owns RegisterServices; `expected_keepers.go`
|
||||
owns the cross-module interface shims. Consistent across P1..P7.
|
||||
|
||||
**expected_keepers.go interface pattern.** All 8 modules ship a `types/
|
||||
expected_keepers.go` defining the cross-module keeper INTERFACES (BridgeKeeper,
|
||||
HubKeeper, BreadKeeper, WatcherKeeper, StandKeeper, GuildKeeper, ComplianceKeeper).
|
||||
No struct imports. The pattern is uniform.
|
||||
|
||||
**Commit discipline.** All 8 phase merges carry `---ci---` blocks (verified by `git
|
||||
log --grep "---ci---"`). The phase commits follow the `checkpoint(pN): v0.5 phase N
|
||||
complete → v0.4.N` pattern.
|
||||
|
||||
### 1.6 Adversarial (ci-security-auditor lens) — **PASS** (confidence 0.82)
|
||||
|
||||
**What would break at mainnet?**
|
||||
- **Simtest vs real IBC light clients.** The bridge simtest uses in-memory
|
||||
`sdk.Context` + stub BreadKeeper/WatcherKeeper (G-022 — baseline keepers remain
|
||||
empty stubs). Real IBC light-client verification (client state, consensus state,
|
||||
proofs) is NOT exercised — this is D-054 simtest grade, not mainnet. Mainnet
|
||||
rollout requires wiring real ibc-go light clients (out of v0.5 scope). FLAG for
|
||||
post-hoc (P1, maintainability) — the simtest does NOT prove IBC proof verification.
|
||||
- **Custody key rotation.** `x/hub/keeper/keyring_mem.go` is an in-memory keyring
|
||||
(D-058). Rotation is modelled via `Status` reporting active key version; no
|
||||
cross-block caching. Mainnet requires a real KMS-backed keyring (out of scope).
|
||||
- **CLOB front-running.** The CLOB is per-tx matching (no batch auction); a
|
||||
front-runner with tx-ordering advantage could sandwich. D-054 simtest grade does
|
||||
not model mempool ordering. FLAG for post-hoc (P1, adversarial) — a per-tx CLOB is
|
||||
front-running-exposed at mainnet; a batch auction or commit-reveal is a v0.6+
|
||||
design decision.
|
||||
- **Governance proposal spam.** `MsgSubmitProposal.ValidateBasic` checks fields +
|
||||
kind but does NOT bond a deposit. A spammer could flood Pending proposals. The
|
||||
keeper `SubmitProposal` does not charge a fee. FLAG for post-hoc (P1, security) —
|
||||
no proposal deposit/bond at v0.5 simtest grade; mainnet needs a deposit gate
|
||||
(standard x/gov pattern).
|
||||
|
||||
**Mission-Lock const firewall bypass?** `MsgSubmitProposal.ValidateBasic` rejects
|
||||
the `MissionLockAmendment-Rejected` kind (D-064). The handler ALSO rejects it at
|
||||
the kind-switch (`msg_server.go:141-144` — defence in depth). No custom message can
|
||||
reach the handler with that kind: the MsgServer registration
|
||||
(`x/council/module.go:49` `RegisterServices`) wires only `types.MsgServer`
|
||||
(scaffolding) + the backend-implemented handler bodies (G-023 ownership split). A
|
||||
custom message would need a new `Msg*` type + a new `ValidateBasic` — both are
|
||||
additive and would be caught at code review. The const `MissionLockAmendable=false`
|
||||
is the firewall; `ValidateBasic` + the handler switch are the gates. Sound.
|
||||
|
||||
**Double-spend via IBC replay?**
|
||||
- **Second OnAck (G-021).** Returns ERROR (not silent no-op) — `ibc_module.go:314-
|
||||
316`. The in-flight record is deleted on first ack; a second ack finds no record
|
||||
and errors. The relayer sees the failure. No double-mint (mint happens once on
|
||||
OnRecv; the ack path only deletes the in-flight record).
|
||||
- **Timeout refund (A-513).** `OnTimeoutPacket` refunds via the `Refunded` flag
|
||||
guard. A second timeout finds `Refunded==true` and no-ops (benign — the refund
|
||||
already happened). A timeout on an already-acked packet finds no in-flight record
|
||||
and no-ops (benign — the ack path already finalized). The `Refunded` flip is
|
||||
state-write-first. No double-refund. Sound.
|
||||
|
||||
---
|
||||
|
||||
## 2. GRILL Fix Verification (G-017..G-024)
|
||||
|
||||
| ID | Fix | Landed? | Evidence |
|
||||
|---|---|---|---|
|
||||
| **G-017** | NoWithVeto regression-test reconciliation (rename + new test) | ✅ LANDED | `x/council/types/types_test.go:255` `TestTallyResultNoWithVetoDefaultZero` (renamed); `:271` `TestTallyResultNoWithVetoPopulatedByQuorum` (new). Both pass (`go test -v`). |
|
||||
| **G-018** | cosmos-sdk dep is a HARD go 1.22 build gate | ✅ LANDED | `go.mod:3` `go 1.22` (NOT bumped); `go build ./...` exits 0 under go 1.22 with cosmos-sdk v0.50.8 + ibc-go v8.2.1 transitive tree. |
|
||||
| **G-019** | CLOB ImpliedCoupon helper + boundary test (800/801/799) | ✅ LANDED | `x/bond/keeper/clob.go:108` `ImpliedCoupon(priceBps, principalGrain)`; `msg_server_simtest_test.go:830` `TestImpliedCouponBoundary` covers 9200/9199/9201 → 800/801/799. |
|
||||
| **G-020** | WatcherVetoQuorum Params.Validate bounds [2,9] | ✅ LANDED | `x/council/types/types.go:199-204` rejects `< 2` and `> 9`; `types_test.go:735` `TestParamsValidateBounds` covers 0/1/2..9/10. |
|
||||
| **G-021** | IBC second OnAck returns ERROR (not silent no-op) | ✅ LANDED | `x/bridge/keeper/ibc_module.go:314-316` returns `fmt.Errorf("bridge: replay detected — no in-flight record ... (already acknowledged)")`. |
|
||||
| **G-022** | Baseline keeper stubs documented + not promoted | ✅ LANDED | `x/{mirror,forge,still,watcher,bread}/keeper/` dirs all EMPTY (verified by `ls`). Each runtime keeper's msg_server.go documents the "nil-shim behavior (simtest wiring)" pattern. |
|
||||
| **G-023** | keeper/msg_server.go ownership (cosmos scaffolds, backend implements) | ✅ LANDED | Structural check: `module.go` (cosmos scaffolding) wires RegisterServices; `keeper/msg_server.go` holds the backend-implemented handler bodies. Pattern consistent across all 8 modules. |
|
||||
| **G-024** | types/ invariant tests stdlib-only (no cosmos-sdk import) | ✅ LANDED | `grep -rln "cosmos-sdk\|sdk.Msg\|sdk.Context" x/*/types/*_test.go` returns ZERO hits. Invariant/lexicon tests remain stdlib-only; only `msg_*.go` imports `sdk.Msg`. |
|
||||
|
||||
**All 8 GRILL fixes (G-017..G-024) LANDED with evidence.**
|
||||
|
||||
---
|
||||
|
||||
## 3. P0 Issues (auto-applied in this phase)
|
||||
|
||||
**0.** No P0 (critical) issues found. The milestone ships clean:
|
||||
- `go build ./...` green
|
||||
- `go test ./...` green (729 tests)
|
||||
- All 8 GRILL fixes landed with evidence
|
||||
- All locked-consts unchanged
|
||||
- G-003 production firewall intact
|
||||
- G-018 go 1.22 build gate green
|
||||
- Coverage ≥80% on all 8 keeper packages
|
||||
|
||||
No source edits were required; no `fix(P8): ...` commits were created.
|
||||
|
||||
---
|
||||
|
||||
## 4. P1+ Issues (flagged for post-hoc review — do NOT block ship)
|
||||
|
||||
**1.** [P1, security] **No proposal deposit/bond at v0.5 simtest grade.** `MsgSubmitProposal.ValidateBasic` checks fields + kind but does NOT bond a deposit; the keeper does not charge a fee. A mainnet spammer could flood Pending proposals. Recommend post-hoc: add a deposit gate (standard `x/gov` pattern) in a v0.6+ milestone before mainnet. Not a v0.5 ship blocker (simtest grade does not model spam economics).
|
||||
|
||||
**2.** [P1, adversarial] **CLOB per-tx matching is front-running-exposed at mainnet.** The CLOB matches per-tx (no batch auction); a tx-ordering-advantaged actor could sandwich. D-054 simtest grade does not model mempool ordering. Recommend post-hoc: evaluate a batch auction or commit-reveal for v0.6+ before mainnet. Not a v0.5 ship blocker (simtest grade).
|
||||
|
||||
**3.** [P1, maintainability] **Simtest does NOT exercise real IBC light-client verification.** The bridge simtest uses in-memory `sdk.Context` + stub BreadKeeper/WatcherKeeper (G-022). Real IBC client state / consensus state / proof verification is NOT exercised. Mainnet rollout requires wiring real ibc-go light clients (out of v0.5 scope). Recommend post-hoc: a v0.6+ mainnet-readiness milestone exercises real light clients. Not a v0.5 ship blocker (D-054 simtest grade is explicit).
|
||||
|
||||
**4.** [P2, performance] **CLOB `restingBookForBond` is O(n) over all resting orders.** The keeper loads `AllRestingOrders` then filters by `BondID` in Go. Acceptable for simtest depth; a production keeper would use a prefix-keyed store iteration scoped to the bond. Recommend post-hoc: prefix-key the book store by `BondID` for mainnet depth. Not a v0.5 ship blocker.
|
||||
|
||||
**5.** [P2, maintainability] **`emitMatchEventHook` indirection in clob.go.** `clob.go` uses a package-level `var emitMatchEventHook func(...)` set by `msg_server.go` to avoid importing the sdk event package in `clob.go`. This is a pragmatic split but the package-level mutable var is a minor testability smell (a parallel test could race the hook). Recommend post-hoc: pass the event emitter as a Keeper field or a constructor injection. Not a v0.5 ship blocker (simtest is serial).
|
||||
|
||||
---
|
||||
|
||||
## 5. Overall Verdict
|
||||
|
||||
### **PASS-WITH-FIXES** → **SHIP** (all GRILL fixes landed; no P0; P1+ flagged for post-hoc)
|
||||
|
||||
The v0.5 (Bearers Runtime) milestone delivers the runtime promotion of 8 v0.3
|
||||
skeleton modules to live keeper `MsgServer` handlers + simtest-grade end-to-end
|
||||
flows (D-054). All 8 GRILL binding fixes (G-017..G-024) landed with evidence:
|
||||
|
||||
- G-017 NoWithVeto reconciliation: renamed + new test, both pass.
|
||||
- G-018 go 1.22 hard build gate: `go.mod` go directive unchanged; build green.
|
||||
- G-019 ImpliedCoupon helper + boundary test (800/801/799): landed + tested.
|
||||
- G-020 WatcherVetoQuorum Validate bounds [2,9]: landed + tested.
|
||||
- G-021 IBC second-OnAck ERROR (not silent no-op): landed.
|
||||
- G-022 baseline keeper stubs documented + NOT promoted: 5 v0.1 keeper dirs empty.
|
||||
- G-023 keeper/msg_server.go ownership split: cosmos scaffolds, backend implements.
|
||||
- G-024 types/ invariant tests stdlib-only: zero cosmos-sdk imports in types tests.
|
||||
|
||||
`go build ./...` + `go test ./...` green across all packages (729 tests). Coverage
|
||||
≥80% on all 8 keeper packages (82.1%..92.5%). All v0.1..v0.4 locked-consts unchanged;
|
||||
v0.5 P7 enums are additive (feature purity gate satisfied). G-003 production firewall
|
||||
intact (zero cross-module struct imports; expected_keepers.go interface pattern
|
||||
uniform). G-018 go 1.22 build gate green. Lexicon firewall green on all new files.
|
||||
|
||||
No P0 (critical) issues found — no source fixes applied. 5 P1+/P2 issues flagged for
|
||||
post-hoc review (proposal deposit gap, CLOB front-running, simtest vs real IBC light
|
||||
clients, CLOB book O(n) load, emitMatchEventHook indirection) — none block the v0.5
|
||||
ship (all are mainnet-readiness concerns for v0.6+, consistent with D-054 simtest
|
||||
grade).
|
||||
|
||||
**P0 fixes auto-applied: 0**
|
||||
**P1+ findings: 2 P1, 3 P2 (all post-hoc, non-blocking)**
|
||||
**Confidence in overall verdict: 0.86**
|
||||
|
||||
---
|
||||
|
||||
## Summary Block
|
||||
|
||||
```
|
||||
Per-lens verdicts (v0.5 final):
|
||||
1. Correctness — PASS (0.88)
|
||||
2. Testing — PASS (0.87)
|
||||
3. Security — PASS (0.86)
|
||||
4. Performance — PASS (0.80)
|
||||
5. Maintainability — PASS (0.85)
|
||||
6. Adversarial — PASS (0.82)
|
||||
|
||||
GRILL fix verification (G-017..G-024):
|
||||
G-017 NoWithVeto reconciliation — LANDED ✓
|
||||
G-018 go 1.22 hard build gate — LANDED ✓
|
||||
G-019 CLOB ImpliedCoupon helper + boundary — LANDED ✓
|
||||
G-020 WatcherVetoQuorum Validate bounds — LANDED ✓
|
||||
G-021 IBC second-OnAck ERROR — LANDED ✓
|
||||
G-022 baseline keeper stubs documented — LANDED ✓
|
||||
G-023 keeper/msg_server.go ownership split — LANDED ✓
|
||||
G-024 types/ invariant tests stdlib-only — LANDED ✓
|
||||
All 8 GRILL fixes LANDED.
|
||||
|
||||
P0 fixes auto-applied: 0
|
||||
P1+ flags for post-hoc review: 2 P1, 3 P2 (none blocking)
|
||||
Overall: PASS-WITH-FIXES → SHIP (confidence 0.86)
|
||||
```
|
||||
|
||||
@@ -110,120 +110,6 @@ docs build CI. Refinement-only NFR milestone: zero `feat:` phases.
|
||||
> `v0.3.1..v0.3.3`, P4 -> `v0.3.4` (= the v0.4 milestone release, per D-008 —
|
||||
> final phase patch IS the milestone release; no separate minor tag).
|
||||
|
||||
## Milestone v0.5 — Bearers Runtime (COMPLETE; feature type; tags v0.4.x)
|
||||
|
||||
Target: Promote the v0.3 Bearers skeletons from type+keeper-stub layers to
|
||||
live runtime behavior (keeper MsgServer handlers + simtest-grade end-to-end
|
||||
flows). NOT mainnet (D-020 pattern continues to govern network deployment);
|
||||
runtime = simtest-grade message handlers, not mainnet deployment.
|
||||
|
||||
- [x] P0: Pre-Execution (spec/clarify/research/plan/grill/mvp-ux) → v0.4.0
|
||||
- [x] P1: Exit + Bridge runtime (REQ-033) → v0.4.1
|
||||
- [x] P2: Bearers transport runtime (REQ-034) → v0.4.2
|
||||
- [x] P3: Anchors onboarding runtime (REQ-035) → v0.4.3
|
||||
- [x] P4: Hub API B2B runtime (REQ-036) → v0.4.4
|
||||
- [x] P5: Services runtime (REQ-037) → v0.4.5
|
||||
- [x] P6: Bond market runtime (REQ-038) → v0.4.6
|
||||
- [x] P7: Council governance runtime (REQ-039) → v0.4.7
|
||||
- [x] P8: Final Review + Audit + Ship → v0.4.8 (milestone release)
|
||||
- Status: COMPLETE — 7 runtime REQs shipped; all 8 keeper packages ≥80% coverage (82.1%..92.5%); G-003 firewall intact; locked-const firewall intact; cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (D-055/D-062, G-006 controlled exception); 5 GRILL decisions ratified (D-055/D-062/D-063/D-064/D-065); 8 binding fixes landed (G-017..G-024); 5 P1+ flagged for v0.6+ mainnet-readiness.
|
||||
|
||||
| Phase | Type | Scope | Patch |
|
||||
|---|---|---|---|
|
||||
| P0 | docs | Pre-Execution (spec/clarify/research/plan/grill/mvp-ux) | v0.4.0 |
|
||||
| P1 | feat | Exit + Bridge runtime: x/exit DEX swap routing + x/bridge IBC packet handlers (5 L2 chains) | v0.4.1 |
|
||||
| P2 | feat | Bearers transport runtime: OY-SAT + OY-QR message handlers + session lifecycle | v0.4.2 |
|
||||
| P3 | feat | Anchors onboarding runtime: x/partner Anchor credential lifecycle | v0.4.3 |
|
||||
| P4 | feat | Hub API B2B runtime: custody/lending/compliance + CustodyKeyring interface (D-058) | v0.4.4 |
|
||||
| P5 | feat | Services runtime: Care/SIM/Vault/Mail service lifecycle handlers | v0.4.5 |
|
||||
| P6 | feat | Bond market runtime: Growth Bonds + secondary-market CLOB matching (REJECT above 800bps D-063) | v0.4.6 |
|
||||
| P7 | feat | Council governance runtime: Proposal/VoteOption enums (AUDIT §193 P1-1) + MissionLockAmendment reject (D-064) | v0.4.7 |
|
||||
| P8 | final | REVIEW + AUDIT + milestone SHIP | v0.4.8 (milestone release) |
|
||||
|
||||
### v0.5 Component mapping
|
||||
|
||||
| Component | Deliverable | v0.5 Runtime Module | Phase |
|
||||
|---|---|---|---|
|
||||
| Exit layer (Layer 3) | DEX swap routing + cross-chain exit handlers | x/exit/keeper + x/exit/module.go | v0.5/P1 |
|
||||
| Bridge (L2↔L1) | IBC packet handlers (5 L2 chains, Solana wormhole-adapter) | x/bridge/keeper + x/bridge/module.go + ibc_module.go | v0.5/P1 |
|
||||
| Bearers expansion | OY-SAT + OY-QR message handlers + session lifecycle | x/bearers/keeper + x/bearers/module.go | v0.5/P2 |
|
||||
| Anchors | Anchor credential issuance/revocation runtime | x/partner/keeper + x/partner/module.go | v0.5/P3 |
|
||||
| Hub API | Custody/lending/compliance runtime + CustodyKeyring interface | x/hub/keeper + x/hub/module.go + keyring_mem.go | v0.5/P4 |
|
||||
| Services | Care/SIM/Vault/Mail service lifecycle runtime | x/services/keeper + x/services/module.go | v0.5/P5 |
|
||||
| Bond market | Growth Bonds + secondary-market CLOB matching engine | x/bond/keeper + x/bond/module.go + clob.go | v0.5/P6 |
|
||||
| Council governance | Proposal/VoteOption enums + governance message handlers | x/council/keeper + x/council/module.go | v0.5/P7 |
|
||||
|
||||
> **Tag-line note (G-010 continuation)**: v0.5 (feature) ships on the `v0.4.x`
|
||||
> patch line (config.json `tag_base: v0.4.x`): P0 -> `v0.4.0`, P1..P7 ->
|
||||
> `v0.4.1..v0.4.7`, P8 -> `v0.4.8` (= the v0.5 milestone release, per D-008 —
|
||||
> final phase patch IS the milestone release; no separate minor tag).
|
||||
|
||||
### v0.5 deferred to v0.6+ (P1+ from REVIEW.md)
|
||||
- P1 security: no proposal deposit/bond (governance spam gap — mainnet-readiness)
|
||||
- P1 adversarial: CLOB per-tx front-running exposure (batch auction is a v0.6+ design)
|
||||
- P1 maintainability: simtest doesn't exercise real IBC light-client verification
|
||||
- P2 performance: CLOB `restingBookForBond` O(n) load (prefix-key for mainnet)
|
||||
- P2 maintainability: `emitMatchEventHook` package-level mutable var (minor testability)
|
||||
- SignalKind 4→5 expansion (AUDIT §193 P1-2; deferred to v0.6+ governance vote)
|
||||
- Live chain launch / mainnet / real IBC channels / real bearer transports (D-020 continues)
|
||||
- Real institutional Anchors onboarding (credential lifecycle in simtest only)
|
||||
- Yield Token, Travel + 11 service categories (ROADMAP Phase 4 — Maturity)
|
||||
|
||||
## Milestone v0.6 — Nomad Web UI (IN PROGRESS; feature type; tags v0.5.x)
|
||||
|
||||
Target: The project's first UI milestone. A working prototype Web UI where a
|
||||
person can sign up to be a Nomad (create a Reach + open a Stash) and exercise
|
||||
basic functionality around the (Reach, Stash) components, plus Window
|
||||
authorization, Standing progress, and Bloom accrual views. All data is
|
||||
generated as test fixtures — no real blockchain interaction (D-020 continues).
|
||||
Greenfield Go `html/template` + HTMX layer served by a Go mock HTTP server
|
||||
(`web/`) that instantiates the real `x/*/types` structs from in-memory
|
||||
fixtures. No keeper, no Cosmos runtime, no `app.go` (none exists in the repo).
|
||||
|
||||
- [ ] P0: Pre-Execution (spec/clarify/research/ideate/plan/grill/mvp-ux) → v0.5.0
|
||||
- [ ] P1: Web foundation + Reach signup + lexicon firewall extension (REQ-040, REQ-045) → v0.5.1
|
||||
- [ ] P2: Stash dashboard (REQ-041) → v0.5.2
|
||||
- [ ] P3: Window authorization (REQ-042) → v0.5.3
|
||||
- [ ] P4: Standing + Freeholder signals (REQ-043) → v0.5.4
|
||||
- [ ] P5: Bloom accrual view (REQ-044) → v0.5.5
|
||||
- [ ] P6: Final Review + Audit + Ship → v0.5.6 (milestone release)
|
||||
|
||||
| Phase | Type | Scope | Patch |
|
||||
|---|---|---|---|
|
||||
| P0 | docs | Pre-Execution (spec/clarify/research/ideate/plan/grill/mvp-ux) | v0.5.0 |
|
||||
| P1 | feat+test | Web foundation + Reach signup (REQ-040) + lexicon firewall extension to web/ (REQ-045) | v0.5.1 |
|
||||
| P2 | feat | Stash dashboard: balance + Bread-scale conversion + 90-day maturity progress (REQ-041) | v0.5.2 |
|
||||
| P3 | feat | Window authorization: open/lifecycle/audit-log view (REQ-042) | v0.5.3 |
|
||||
| P4 | feat | Standing + Freeholder signals progress: computed from mock Ratings/Vouches/Slashes (REQ-043) | v0.5.4 |
|
||||
| P5 | feat | Bloom accrual view: per-Stash BloomRecord (REQ-044) | v0.5.5 |
|
||||
| P6 | final | REVIEW + AUDIT + milestone SHIP | v0.5.6 (milestone release) |
|
||||
|
||||
### v0.6 Component mapping
|
||||
|
||||
| Component | Deliverable | v0.6 Module | Phase |
|
||||
|---|---|---|---|
|
||||
| Web UI foundation | Go HTTP mock server + base templates + HTMX vendored | web/main.go, web/handlers/, web/store/, web/templates/, web/static/ | v0.6/P1 |
|
||||
| Reach signup | "Create a Reach" form + Reach list/detail | web/handlers/reach.go, web/templates/reach.html | v0.6/P1 |
|
||||
| Stash dashboard | Balance + Bread-scale + 90-day maturity | web/handlers/stash.go, web/templates/stash.html | v0.6/P2 |
|
||||
| Window authorization | Open/lifecycle/audit-log view | web/handlers/window.go, web/templates/window.html | v0.6/P3 |
|
||||
| Standing + Freeholder signals | Progress view from mock Ratings/Vouches/Slashes | web/handlers/standing.go, web/templates/standing.html | v0.6/P4 |
|
||||
| Bloom accrual | Per-Stash BloomRecord view | web/handlers/bloom.go, web/templates/bloom.html | v0.6/P5 |
|
||||
| Lexicon firewall | Extend REQ-012 to web/ | lexicon_meta_web/lexicon_meta_web_test.go | v0.6/P1 |
|
||||
|
||||
> **Tag-line note (G-010 continuation)**: v0.6 (feature) ships on the `v0.5.x`
|
||||
> patch line (config.json `tag_base: v0.5.x`): P0 -> `v0.5.0`, P1..P5 ->
|
||||
> `v0.5.1..v0.5.5`, P6 -> `v0.5.6` (= the v0.6 milestone release, per D-008 —
|
||||
> final phase patch IS the milestone release; no separate minor tag).
|
||||
|
||||
### v0.6 deferred to v0.7+
|
||||
- Real blockchain interaction / mainnet / IBC / real bearer transports (D-020 continues)
|
||||
- A real `oyd` daemon / `app.go` / `cmd/oyd` (no chain runtime exists; wiring the UI to a real daemon is v0.7+)
|
||||
- Authentication / sessions / real key management (mock; a Reach is created by form submission, stored in-memory)
|
||||
- Persistence (mock store is in-memory; resets on restart)
|
||||
- i18n / multi-language UI
|
||||
- The 5 P1+ mainnet-readiness items deferred from v0.5 (governance spam deposit, CLOB front-running, real IBC simtest, CLOB perf, emitMatchEventHook testability) — those are v0.7+ mainnet-readiness, not UI work
|
||||
- Bread-scale doc-fix (`docs/shared/bread-scale.md` is outdated vs code constants — P1+ follow-up, not v0.6 scope)
|
||||
|
||||
## Phase 3 — The Bearers (Year 3) — v0.3 PARTIAL SKELETON
|
||||
**Target**: $10B annual volume → fee auto-declines to 0.07%
|
||||
|
||||
|
||||
@@ -25,14 +25,10 @@ Loaf → Batch → Cake → Bakery → Granary → Mill → Harvest → Earth.**
|
||||
|
||||
## Status
|
||||
|
||||
**v0.6 (Nomad Web UI) — in progress.** v0.5 shipped the Bearers Runtime
|
||||
(simtest-grade keeper handlers for 8 x/ modules). v0.6 adds the project's
|
||||
first UI: a Go `html/template` + HTMX prototype Web UI in `web/` where a
|
||||
visitor can sign up to be a Nomad (create a Reach + open a Stash) and
|
||||
exercise basic functionality around Reach, Stash, Window, Standing, and
|
||||
Bloom. All data is generated test fixtures — no real chain. See
|
||||
`.ciagent/oy/ROADMAP.md` for the phase plan and `.ciagent/oy/PROJECT.md`
|
||||
for governance.
|
||||
**v0.3 (Bearers & Documentation) — in progress.** The codebase is a skeleton +
|
||||
tests layer (Go types + keeper stubs + invariant tests, zero external Go deps)
|
||||
matching the v0.1/v0.2 pre-MVP pattern. See `.ciagent/oy/ROADMAP.md` for the
|
||||
phase plan and `.ciagent/oy/PROJECT.md` for governance.
|
||||
|
||||
## Build & test
|
||||
|
||||
@@ -44,29 +40,6 @@ go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Web UI
|
||||
|
||||
The Nomad Web UI (v0.6) is a Go `html/template` server with HTMX progressive
|
||||
enhancement, served by a mock HTTP server in `web/` that instantiates the
|
||||
real `x/*/types` structs from in-memory fixtures. No node, no build step,
|
||||
no real chain. To run it:
|
||||
|
||||
```sh
|
||||
go run ./web
|
||||
# opens on http://localhost:8080 (PORT env var overridable)
|
||||
```
|
||||
|
||||
Five screens, all reachable from the home nav:
|
||||
|
||||
- `/reach` — create a Reach (sign up to be a Nomad) + Reach list/detail
|
||||
- `/stash/{holderID}` — Stash dashboard (Grain balance + Bread scale + 90-day maturity)
|
||||
- `/window` — Window authorization (open/lifecycle/audit log)
|
||||
- `/standing/{reachID}` — Standing + Freeholder signals progress
|
||||
- `/bloom/{stashID}` — Bloom accrual view
|
||||
|
||||
HTMX is a single vendored JS file (`web/static/htmx.min.js`), NOT a Go
|
||||
dependency — `go.mod` stays unchanged (G-006).
|
||||
|
||||
## Docs
|
||||
|
||||
The docs site is [MkDocs Material](https://squidfunk.github.io/mkdocs-material/)
|
||||
@@ -85,19 +58,17 @@ deferred to v0.4 (D-046); v0.3 ships the source.
|
||||
## Lexicon firewall
|
||||
|
||||
OpenYield bans 10 financial terms as standalone words (REQ-012) across all Go
|
||||
source (`x/**/*.go`), all docs (`README.md` + `docs/**/*.md`), and all web UI
|
||||
files (`web/**/*.{html,js,go}`). The banned terms are the words you would
|
||||
expect a legacy financial institution to use; this README and the docs describe
|
||||
them only by their **safe replacements**, so the firewall itself never trips.
|
||||
The firewall is enforced in code by three sibling Go tests:
|
||||
source (`x/**/*.go`) and all docs (`README.md` + `docs/**/*.md`). The banned
|
||||
terms are the words you would expect a legacy financial institution to use;
|
||||
this README and the docs describe them only by their **safe replacements**, so
|
||||
the firewall itself never trips. The firewall is enforced in code by two
|
||||
sibling Go tests:
|
||||
|
||||
- `lexicon_meta_test.go` (v0.2) — scans `x/**/*.go`.
|
||||
- `lexicon_meta_docs/lexicon_meta_docs_test.go` (v0.3) — scans `README.md` +
|
||||
`docs/**/*.md`.
|
||||
- `lexicon_meta_web/lexicon_meta_web_test.go` (v0.6) — scans
|
||||
`web/templates/**` + `web/static/**` + `web/**/*.go`.
|
||||
|
||||
All three use `lexicon.FindBannedTerm` (word-boundary, case-insensitive), so
|
||||
Both use `lexicon.FindBannedTerm` (word-boundary, case-insensitive), so
|
||||
"OpenYield" is safe (word-boundary does not match the banned term inside an
|
||||
identifier) but the standalone banned term is not — docs say **"real
|
||||
production"** / **"real return"**, and a Holder's identity is **Holder** /
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
// Package lexicon_meta_web holds the web lexicon firewall (REQ-045, D-069).
|
||||
//
|
||||
// It is a NEW sibling meta-test created in v0.6 P1 Wave 1 that MIRRORS the
|
||||
// v0.3 docs firewall (lexicon_meta_docs/lexicon_meta_docs_test.go, package
|
||||
// lexicon_meta_docs) but scans the web surface (web/templates/**/*.html +
|
||||
// web/static/**/*.js + web/**/*.go) instead of README.md + docs/**/*.md. It
|
||||
// uses the SAME lexicon.FindBannedTerm (word-boundary, case-insensitive) —
|
||||
// NO detection reimplementation — so the three firewalls (x/*.go, docs, web)
|
||||
// share a single source of truth for the 10 banned terms (bank, deposit,
|
||||
// interest, yield, currency, dollar, euro, account, savings, depositor).
|
||||
//
|
||||
// Placement: this file lives in lexicon_meta_web/ (a subdirectory of the
|
||||
// repo root) because Go does not permit two distinct packages in the same
|
||||
// directory; the v0.2 firewall is package lexicon_meta at the repo root and
|
||||
// the v0.3 firewall is package lexicon_meta_docs in lexicon_meta_docs/. The
|
||||
// invocation `go test ./lexicon_meta_web/...` (PLANS P1-01-01) resolves to
|
||||
// this package. Run via `go test ./...` from the repo root as well.
|
||||
//
|
||||
// G-013 walk-coverage: TestLexiconMetaWebWalkCoverage injects a synthetic
|
||||
// banned-term .html into a temp web/templates/ subtree and asserts the walk
|
||||
// FINDS it. This closes the "silently scans nothing and reports green"
|
||||
// failure mode that the G-009 self-test table (detection) alone does not
|
||||
// cover.
|
||||
//
|
||||
// G-014 self-test drift: the self-test table and banned-term count assertion
|
||||
// reuse lexicon.BannedTerms() (the single source). A cross-reference comment
|
||||
// keeps this file's table in lockstep with lexicon_meta_test.go's table and
|
||||
// lexicon_meta_docs_test.go's table; if a banned term is added, all three
|
||||
// firewalls update from one place.
|
||||
package lexicon_meta_web
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
)
|
||||
|
||||
// repoRoot returns the absolute path to the repo root by walking up from
|
||||
// this test file (the test lives at <repoRoot>/lexicon_meta_web/).
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/lexicon_meta_web/lexicon_meta_web_test.go
|
||||
// repo root = filepath.Dir(filepath.Dir(file))
|
||||
return filepath.Dir(filepath.Dir(file))
|
||||
}
|
||||
|
||||
// thisFile returns the absolute path of this meta-test file (to exclude it
|
||||
// from its own scan — it references banned terms via the lexicon package,
|
||||
// whose source assembles terms from fragments, so no banned-term literal
|
||||
// appears in the firewall's own code).
|
||||
func thisFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// isWebTarget reports whether path (relative to repo root) is a file the web
|
||||
// firewall scans: web/templates/**/*.html, web/static/**/*.js, and
|
||||
// web/**/*.go (production + test). Non-{html,js,go} files under web/ (e.g.
|
||||
// vendored binary assets) are skipped.
|
||||
func isWebTarget(rel string) bool {
|
||||
if !strings.HasPrefix(rel, "web"+string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(rel, ".html") || strings.HasSuffix(rel, ".js") || strings.HasSuffix(rel, ".go")
|
||||
}
|
||||
|
||||
// TestLexiconMetaWebNoBannedTermsInWeb is the web firewall (D-069). It walks
|
||||
// the repo root, targets web/templates/**/*.html + web/static/**/*.js +
|
||||
// web/**/*.go (production + test), reads each file's source, and asserts no
|
||||
// banned term is present (word-boundary, case-insensitive). Excludes
|
||||
// .ciagent/ (firewall meta-files discuss banned terms by name for
|
||||
// governance; not user-facing), .git/ (VCS), and this test file itself
|
||||
// (self-exclusion via runtime.Caller(0)).
|
||||
//
|
||||
// Passes at P1 Wave 1 with zero web content (a walk that scans nothing
|
||||
// reports green on zero hits — closed by TestLexiconMetaWebWalkCoverage
|
||||
// below). With the Wave 2..4 web content present (templates, static assets,
|
||||
// handlers, store), all are lexicon-clean by construction.
|
||||
func TestLexiconMetaWebNoBannedTermsInWeb(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
this := thisFile(t)
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
base := filepath.Base(path)
|
||||
if base == ".ciagent" || base == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Self-exclusion: skip this meta-test file.
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
rel, rerr := filepath.Rel(root, path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if !isWebTarget(rel) {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
hits = append(hits, rel+" contains banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
t.Errorf("REQ-045 web lexicon firewall violations:\n %s",
|
||||
strings.Join(hits, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaWebSelfTestTable (G-009 for web) is the firewall's own
|
||||
// detection-coverage guard. Each synthetic string embeds exactly one banned
|
||||
// term in a plausible sentence context and is asserted to trigger detection,
|
||||
// so the firewall's detection logic is durably verified — if detection ever
|
||||
// breaks, this test fails before the firewall silently passes a real
|
||||
// violation in a web template or handler.
|
||||
//
|
||||
// REQ-029 (GRILL G-014): the synthetic strings are sourced from
|
||||
// lexicon.SyntheticBannedStrings(), the single source of truth shared with
|
||||
// lexicon_meta_test.go :: TestLexiconMetaSelfTestTable and
|
||||
// lexicon_meta_docs_test.go :: TestLexiconMetaDocsSelfTestTable. Before
|
||||
// REQ-029, each meta-test DUPLICATED its own 10-string table (byte-identical),
|
||||
// creating a drift risk; the shared helper closes it. This file no longer
|
||||
// builds its own synthetic table — all three meta-tests consume the same
|
||||
// helper, so a future banned-term addition updates all firewalls from one
|
||||
// place.
|
||||
func TestLexiconMetaWebSelfTestTable(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))
|
||||
}
|
||||
// REQ-029: consume the shared synthetic-string helper (G-014 single source).
|
||||
synthetic := lexicon.SyntheticBannedStrings()
|
||||
if len(synthetic) != len(terms) {
|
||||
t.Fatalf("SyntheticBannedStrings() len = %d, want %d (must match BannedTerms())", len(synthetic), len(terms))
|
||||
}
|
||||
for i, s := range synthetic {
|
||||
found, ok := lexicon.FindBannedTerm(s)
|
||||
if !ok {
|
||||
t.Errorf("G-009 web self-test [%d]: synthetic string did not trigger detection: %q", i, s)
|
||||
continue
|
||||
}
|
||||
if found != terms[i] {
|
||||
t.Errorf("G-009 web self-test [%d]: detected %q, want %q (in %q)", i, found, terms[i], s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaWebBannedTermsCount asserts exactly 10 banned terms are
|
||||
// configured (locked-const for the firewall's scope; spec lists 10, plan docs
|
||||
// say "9" counting dollar/euro as a pair). Derived from lexicon.BannedTerms()
|
||||
// — the single source — so a count change breaks all three firewalls (x/*.go,
|
||||
// docs, web) (G-014 drift prevention).
|
||||
func TestLexiconMetaWebBannedTermsCount(t *testing.T) {
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) != 10 {
|
||||
t.Errorf("BannedTerms() len = %d, want 10 (REQ-012/REQ-045)", len(terms))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, tr := range terms {
|
||||
if seen[tr] {
|
||||
t.Errorf("duplicate banned term %q", tr)
|
||||
}
|
||||
seen[tr] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaWebNoFalsePositiveOnOpenYield asserts the module name
|
||||
// "openyield" does NOT trigger the "yield" banned term and "european" does
|
||||
// NOT trigger the "euro" banned term (word-boundary matching must not match
|
||||
// substrings of identifiers). This is the regression firewall for the
|
||||
// word-boundary detection design — mirrors the v0.2
|
||||
// TestLexiconMetaNoFalsePositiveOnOpenYield and the v0.3
|
||||
// TestLexiconMetaDocsNoFalsePositiveOnOpenYield.
|
||||
func TestLexiconMetaWebNoFalsePositiveOnOpenYield(t *testing.T) {
|
||||
cases := []string{
|
||||
"github.com/oy/openyield/x/window/types",
|
||||
"package openyield",
|
||||
"openyield is the module",
|
||||
"european resident",
|
||||
"# OpenYield web",
|
||||
"the OpenYield mesh",
|
||||
}
|
||||
for _, s := range cases {
|
||||
if _, ok := lexicon.FindBannedTerm(s); ok {
|
||||
t.Errorf("false positive: %q triggered a banned term (word-boundary must avoid this)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaWebWalkCoverage (G-013) is the walk-coverage firewall. The
|
||||
// G-009 self-test table (above) verifies DETECTION (FindBannedTerm on
|
||||
// synthetic strings) but NOT the WALK (which files are scanned). A walk bug
|
||||
// — e.g. wrong path prefix, missing web/ recursion, a typo in the .html
|
||||
// suffix check — would silently scan nothing and report green on zero
|
||||
// files. This test closes that gap by injecting a synthetic banned-term
|
||||
// .html into a fixture dir under the real web/templates/ path the walk scans
|
||||
// and asserting the walk FINDS it.
|
||||
//
|
||||
// The fixture is created under web/templates/.lexicon_fixture/ (a real
|
||||
// web/templates/ subtree the walk reaches) and removed via defer so it never
|
||||
// leaks into the repo. If the walk logic misses the fixture, this test fails
|
||||
// loudly instead of letting a broken walk pass the firewall green on zero
|
||||
// files scanned.
|
||||
func TestLexiconMetaWebWalkCoverage(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
this := thisFile(t)
|
||||
|
||||
// Build a synthetic banned term from fragments so THIS file does not
|
||||
// contain a banned-term literal (it is excluded from its own scan, but
|
||||
// the synthetic stays clean for readability/searchability).
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) == 0 {
|
||||
t.Fatal("BannedTerms() returned no terms — cannot run walk-coverage")
|
||||
}
|
||||
// Use the first banned term ("bank") assembled from two halves.
|
||||
syntheticTerm := terms[0][:2] + terms[0][2:] // reassemble (no literal in source)
|
||||
badContent := []byte("<!-- fixture -->\nthis file contains a banned term: " + syntheticTerm + "\n")
|
||||
|
||||
fixtureDir := filepath.Join(root, "web", "templates", ".lexicon_fixture")
|
||||
fixtureFile := filepath.Join(fixtureDir, "bad_fixture.html")
|
||||
if err := os.MkdirAll(fixtureDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir fixture: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(fixtureDir)
|
||||
if err := os.WriteFile(fixtureFile, badContent, 0o644); err != nil {
|
||||
t.Fatalf("write fixture: %v", err)
|
||||
}
|
||||
|
||||
// Run the SAME walk logic as TestLexiconMetaWebNoBannedTermsInWeb and
|
||||
// assert it FINDS the fixture's banned term. A walk that returns zero
|
||||
// hits here proves the walk logic is broken (the fixture is a known-bad
|
||||
// file inside web/templates/ that MUST be detected).
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
base := filepath.Base(path)
|
||||
if base == ".ciagent" || base == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
rel, rerr := filepath.Rel(root, path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if !isWebTarget(rel) {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
hits = append(hits, rel+" contains banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
// Assert the fixture was found. The rel path uses OS-specific separator;
|
||||
// match on the suffix so the test is portable.
|
||||
foundFixture := false
|
||||
for _, h := range hits {
|
||||
if strings.Contains(h, "bad_fixture.html") && strings.Contains(h, syntheticTerm) {
|
||||
foundFixture = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFixture {
|
||||
t.Errorf("G-013 walk-coverage: the walk did NOT find the synthetic banned-term fixture at %s — the web firewall walk logic is broken (it would silently scan nothing and report green). hits=%v", fixtureFile, hits)
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
identitytypes "github.com/oy/openyield/x/identity/types"
|
||||
stashtypes "github.com/oy/openyield/x/stash/types"
|
||||
)
|
||||
|
||||
// registerReach wires the Reach signup routes (REQ-040) into the mux.
|
||||
// Go 1.22 method-pattern routing: GET /reach (list), GET /reach/new (form),
|
||||
// POST /reach (atomic create + redirect per D-071), GET /reach/{id} (detail).
|
||||
func (s *Server) registerReach(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /reach", s.handleReachList)
|
||||
mux.HandleFunc("GET /reach/new", s.handleReachNew)
|
||||
mux.HandleFunc("POST /reach", s.handleReachCreate)
|
||||
mux.HandleFunc("GET /reach/{id}", s.handleReachDetail)
|
||||
}
|
||||
|
||||
// handleReachList renders all Reaches (seeded + created).
|
||||
func (s *Server) handleReachList(w http.ResponseWriter, r *http.Request) {
|
||||
reaches := s.Store.ListReaches()
|
||||
s.render(w, "reach_list.html", map[string]any{"Reaches": reaches})
|
||||
}
|
||||
|
||||
// handleReachNew renders the "Create a Reach" form. Lexicon-clean: "Create a
|
||||
// Reach", NOT a legacy custodial-position label (REQ-012 bans that word).
|
||||
func (s *Server) handleReachNew(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, "reach_new.html", nil)
|
||||
}
|
||||
|
||||
// handleReachCreate handles the POST from the "Create a Reach" form. Calls
|
||||
// store.CreateReach (atomic Reach + Stash per D-071). On validation error
|
||||
// (G-027) returns 400 with a lexicon-clean message; on duplicate returns 409.
|
||||
// On success redirects (302) to the new Reach detail page.
|
||||
func (s *Server) handleReachCreate(w http.ResponseWriter, r *http.Request) {
|
||||
holderID := r.FormValue("holder_id")
|
||||
publicKey := r.FormValue("public_key")
|
||||
reach, _, err := s.Store.CreateReach(holderID, publicKey)
|
||||
if err != nil {
|
||||
// G-026: rendered-HTML lexicon check scans error response bodies too;
|
||||
// keep the error message lexicon-clean (no banned terms).
|
||||
status := http.StatusBadRequest
|
||||
if isDuplicate(err) {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
http.Error(w, "Could not create a Reach: "+err.Error(), status)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/reach/"+reach.HolderID, http.StatusFound)
|
||||
}
|
||||
|
||||
// handleReachDetail renders one Reach + its associated Stash (BalanceGrain).
|
||||
func (s *Server) handleReachDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
reach, ok := s.Store.GetReach(id)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
stash, _ := s.Store.GetStash(id)
|
||||
s.render(w, "reach_detail.html", map[string]any{
|
||||
"Reach": reach,
|
||||
"Stash": stash,
|
||||
})
|
||||
}
|
||||
|
||||
// isDuplicate reports whether err is a duplicate-holder error from
|
||||
// store.CreateReach. Kept as a string match to avoid exporting store errors.
|
||||
func isDuplicate(err error) bool {
|
||||
return err != nil && contains(err.Error(), "already has a Reach")
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Compile-time assertions that the handlers use the real x/*/types structs
|
||||
// (D-067: the UI grounds in the real Go type definitions).
|
||||
var _ identitytypes.Reach
|
||||
var _ stashtypes.Stash
|
||||
@@ -1,185 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
"github.com/oy/openyield/web/store"
|
||||
)
|
||||
|
||||
// newTestServer builds a Server with a fresh store + templates parsed from
|
||||
// web/templates (relative to repo root via the handlers test working dir).
|
||||
func newTestServer(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
srv, err := New(store.NewStore(), "../../web/templates")
|
||||
if err != nil {
|
||||
t.Fatalf("new handlers server: %v", err)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
// assertNoBannedTerms checks the rendered response body for banned terms
|
||||
// (G-026: applies to BOTH 200 happy-path AND error response bodies).
|
||||
func assertNoBannedTerms(t *testing.T, body string) {
|
||||
t.Helper()
|
||||
if term, ok := lexicon.FindBannedTerm(body); ok {
|
||||
t.Errorf("rendered HTML contains banned term %q (REQ-012/G-026)", term)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachListReturnsSeededReaches(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/reach", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET /reach: status %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "holder-alia") {
|
||||
t.Errorf("GET /reach: body missing seeded reach holder-alia")
|
||||
}
|
||||
if !strings.Contains(body, "holder-bryn") {
|
||||
t.Errorf("GET /reach: body missing seeded reach holder-bryn")
|
||||
}
|
||||
assertNoBannedTerms(t, body)
|
||||
}
|
||||
|
||||
func TestReachNewReturnsForm(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/reach/new", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET /reach/new: status %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Create a Reach") {
|
||||
t.Errorf("GET /reach/new: body missing 'Create a Reach' label")
|
||||
}
|
||||
// The legacy custodial-position word is BANNED (REQ-012) — must not appear.
|
||||
// Check the full banned-terms list via the lexicon package (no literals in
|
||||
// source); FindBannedTerm does word-boundary matching so this is stricter
|
||||
// than a naive substring check.
|
||||
if term, ok := lexicon.FindBannedTerm(body); ok {
|
||||
t.Errorf("GET /reach/new: body contains banned word %q", term)
|
||||
}
|
||||
assertNoBannedTerms(t, body)
|
||||
}
|
||||
|
||||
func TestReachCreateValidRedirectsAndAtomicallyCreates(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/reach", strings.NewReader("holder_id=holder-new&public_key=pk-new"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("POST /reach valid: status %d, want 302 (Found)", rec.Code)
|
||||
}
|
||||
loc := rec.Header().Get("Location")
|
||||
if !strings.Contains(loc, "/reach/holder-new") {
|
||||
t.Errorf("POST /reach: Location %q, want redirect to /reach/holder-new", loc)
|
||||
}
|
||||
// D-071: atomic creation — both Reach + Stash must be present.
|
||||
reach, ok := srv.Store.GetReach("holder-new")
|
||||
if !ok {
|
||||
t.Fatalf("POST /reach: GetReach miss after create (atomicity broken)")
|
||||
}
|
||||
if !reach.IsNomad {
|
||||
t.Errorf("POST /reach: created Reach IsNomad=false, want true (D-071)")
|
||||
}
|
||||
stash, ok := srv.Store.GetStash("holder-new")
|
||||
if !ok {
|
||||
t.Fatalf("POST /reach: GetStash miss after create (atomicity broken — D-071)")
|
||||
}
|
||||
if stash.HolderID != reach.HolderID {
|
||||
t.Errorf("POST /reach: stash.HolderID %q != reach.HolderID %q (D-071)", stash.HolderID, reach.HolderID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReachCreateEmptyHolderIDReturns400(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/reach", strings.NewReader("holder_id=&public_key=pk"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("POST /reach empty holder: status %d, want 400", rec.Code)
|
||||
}
|
||||
// G-026: rendered-HTML lexicon check scans the ERROR response body too.
|
||||
assertNoBannedTerms(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestReachCreatePathSeparatorReturns400(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/reach", strings.NewReader("holder_id=h/x&public_key=pk"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("POST /reach path separator: status %d, want 400", rec.Code)
|
||||
}
|
||||
assertNoBannedTerms(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestReachCreateDuplicateReturns409(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/reach", strings.NewReader("holder_id=holder-alia&public_key=pk"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusConflict {
|
||||
t.Fatalf("POST /reach duplicate: status %d, want 409", rec.Code)
|
||||
}
|
||||
assertNoBannedTerms(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestReachDetailSeededReturnsReachAndStash(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/reach/holder-alia", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET /reach/holder-alia: status %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "reach-holder-alia") {
|
||||
t.Errorf("GET /reach/holder-alia: body missing reach-holder-alia")
|
||||
}
|
||||
if !strings.Contains(body, "stash-holder-alia") {
|
||||
t.Errorf("GET /reach/holder-alia: body missing associated stash-holder-alia")
|
||||
}
|
||||
if !strings.Contains(body, "Grain") {
|
||||
t.Errorf("GET /reach/holder-alia: body missing Stash balance in Grain")
|
||||
}
|
||||
assertNoBannedTerms(t, body)
|
||||
}
|
||||
|
||||
func TestReachDetailMissingReturns404(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.Register(mux)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/reach/nobody", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("GET /reach/nobody: status %d, want 404", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Package handlers holds the HTTP handlers for the OpenYield web UI screens.
|
||||
//
|
||||
// Each screen (Reach signup, Stash dashboard, Window authorization, Standing
|
||||
// progress, Bloom accrual) gets its own handler file. handlers/server.go wires
|
||||
// routes into the mux from web/server.go. Handlers render html/template
|
||||
// templates against the mock store (web/store). Lexicon-clean by construction
|
||||
// (REQ-012 / REQ-045): the lexicon_meta_web firewall scans these files.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/oy/openyield/web/store"
|
||||
)
|
||||
|
||||
// Server bundles the mock store + per-page templates + route registration.
|
||||
// Each screen handler is a method on Server so it shares the store + tmpl.
|
||||
//
|
||||
// Template loading: base.html is parsed once, then each page template is
|
||||
// parsed in a CLONE of the base set so the per-page "content" block does not
|
||||
// collide across pages (Go html/template shares the block namespace within
|
||||
// one set; cloning per page isolates each page's content block). This is the
|
||||
// standard Go template pattern for layouts + pages.
|
||||
type Server struct {
|
||||
Store *store.Store
|
||||
Pages map[string]*template.Template
|
||||
}
|
||||
|
||||
// New constructs a Server with the given store + per-page templates loaded
|
||||
// from templatesDir (the absolute or relative path to web/templates/).
|
||||
func New(s *store.Store, templatesDir string) (*Server, error) {
|
||||
basePath := filepath.Join(templatesDir, "base.html")
|
||||
base, err := template.ParseFiles(basePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse base: %w", err)
|
||||
}
|
||||
pages := map[string]*template.Template{}
|
||||
pageGlob := filepath.Join(templatesDir, "*.html")
|
||||
matches, err := filepath.Glob(pageGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob pages: %w", err)
|
||||
}
|
||||
for _, p := range matches {
|
||||
name := filepath.Base(p)
|
||||
if name == "base.html" {
|
||||
continue
|
||||
}
|
||||
clone, cerr := base.Clone()
|
||||
if cerr != nil {
|
||||
return nil, fmt.Errorf("clone for %s: %w", name, cerr)
|
||||
}
|
||||
pt, perr := clone.ParseFiles(p)
|
||||
if perr != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", name, perr)
|
||||
}
|
||||
pages[name] = pt
|
||||
}
|
||||
return &Server{Store: s, Pages: pages}, nil
|
||||
}
|
||||
|
||||
// Register wires all screen routes into the given mux (Go 1.22 method
|
||||
// patterns). Called by web/server.go after constructing the Server.
|
||||
func (s *Server) Register(mux *http.ServeMux) {
|
||||
s.registerReach(mux)
|
||||
// P2..P5 register their own routes (stash, window, standing, bloom).
|
||||
}
|
||||
|
||||
// render executes the named page template with the given data, writing HTML
|
||||
// to w. The page template invokes base.html and overrides the "content" block.
|
||||
func (s *Server) render(w http.ResponseWriter, name string, data any) {
|
||||
tmpl, ok := s.Pages[name]
|
||||
if !ok {
|
||||
http.Error(w, "template not found: "+name, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
|
||||
http.Error(w, "render error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderHome renders the home page (public so web/server.go can call it for
|
||||
// the "/" route which lives outside handlers.Register).
|
||||
func (s *Server) RenderHome(w http.ResponseWriter, data any) {
|
||||
s.render(w, "home.html", data)
|
||||
}
|
||||
|
||||
// templatesDir returns the default web/templates directory relative to the
|
||||
// working directory. Used by web/server.go when constructing via New().
|
||||
func DefaultTemplatesDir() string {
|
||||
dir, _ := os.Getwd()
|
||||
if filepath.Base(dir) == "web" || filepath.Base(dir) == "handlers" {
|
||||
return filepath.Join(dir, "templates")
|
||||
}
|
||||
return "web/templates"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
runServer()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/oy/openyield/web/handlers"
|
||||
"github.com/oy/openyield/web/store"
|
||||
)
|
||||
|
||||
func runServer() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
srv, err := handlers.New(store.NewStore(), "web/templates")
|
||||
if err != nil {
|
||||
log.Fatalf("init handlers: %v", err)
|
||||
}
|
||||
srv.Register(mux)
|
||||
|
||||
// Home page (rendered via the handlers' page machinery too).
|
||||
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
srv.RenderHome(w, nil)
|
||||
})
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||
|
||||
server := &http.Server{Addr: ":" + port, Handler: mux}
|
||||
log.Printf("OpenYield web on :%s", port)
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
Vendored
-5
File diff suppressed because one or more lines are too long
@@ -1,78 +0,0 @@
|
||||
/* style.css — OpenYield web UI minimal styling (lexicon-clean).
|
||||
No banned terms in comments or class names (REQ-012/REQ-045). */
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--panel: #161b22;
|
||||
--ink: #c9d1d9;
|
||||
--muted: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--line: #30363d;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
header.nav {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
background: var(--panel);
|
||||
}
|
||||
header.nav .brand { font-weight: 600; color: var(--ink); }
|
||||
header.nav a { color: var(--muted); }
|
||||
header.nav a:hover { color: var(--accent); }
|
||||
|
||||
main { max-width: 960px; margin: 2rem auto; padding: 0 1.5rem; }
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 1rem 1.5rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--line); }
|
||||
th { color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
|
||||
form .field { margin-bottom: 1rem; }
|
||||
form label { display: block; margin-bottom: 0.25rem; color: var(--muted); font-size: 0.9rem; }
|
||||
form input[type=text], form input[type=password] {
|
||||
width: 100%; max-width: 32rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
color: var(--ink);
|
||||
font-family: monospace;
|
||||
}
|
||||
button, .btn {
|
||||
background: var(--accent); color: #0d1117; border: none;
|
||||
padding: 0.5rem 1rem; border-radius: 4px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
button:hover, .btn:hover { opacity: 0.9; text-decoration: none; }
|
||||
|
||||
.error { color: #f85149; }
|
||||
.muted { color: var(--muted); }
|
||||
@@ -1,46 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
identitytypes "github.com/oy/openyield/x/identity/types"
|
||||
stashtypes "github.com/oy/openyield/x/stash/types"
|
||||
)
|
||||
|
||||
// seed populates the store with a few pre-existing Reach/Stash pairs for the
|
||||
// list view. All strings lexicon-clean ("Holder"/"Reach"/"Stash"; NOT the
|
||||
// banned financial terms). Two fixtures: one mature (90+ active days),
|
||||
// one immature (45 active days) so the Stash dashboard (P2) can show both
|
||||
// states.
|
||||
func (s *Store) seed() {
|
||||
now := time.Now().Unix()
|
||||
// Fixture 1: a mature Nomad (ActiveDays=92, MaxGapDays=10 -> IsMature()).
|
||||
seedOne(s, "holder-alia", "pk-alia-001", now, 920000, 92, 10)
|
||||
// Fixture 2: an immature Nomad (ActiveDays=45, MaxGapDays=5 -> not mature).
|
||||
seedOne(s, "holder-bryn", "pk-bryn-002", now, 410000, 45, 5)
|
||||
}
|
||||
|
||||
func seedOne(s *Store, holderID, pubKey string, now int64, balanceGrain int64, activeDays, maxGap uint32) {
|
||||
reachID := "reach-" + holderID
|
||||
stashID := "stash-" + holderID
|
||||
s.reaches[holderID] = identitytypes.Reach{
|
||||
ReachID: reachID,
|
||||
HolderID: holderID,
|
||||
CreatedAt: now - int64(activeDays)*86400,
|
||||
PublicKey: pubKey,
|
||||
IsNomad: true,
|
||||
}
|
||||
s.stashes[holderID] = stashtypes.Stash{
|
||||
HolderID: holderID,
|
||||
StashID: stashID,
|
||||
CreatedAt: now - int64(activeDays)*86400,
|
||||
LastActive: now,
|
||||
BalanceGrain: balanceGrain,
|
||||
}
|
||||
s.stashActivities[stashID] = stashtypes.StashActivity{
|
||||
StashID: stashID,
|
||||
ActiveDays: activeDays,
|
||||
MaxGapDays: maxGap,
|
||||
LastActivityDay: now,
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// import_test.go enforces the G-003/G-025 boundary for web/: web/ is the
|
||||
// application layer that consumes protocol types (D-070), NOT a cross-x/
|
||||
// production import. The invariant: every non-test .go file under web/ may
|
||||
// import github.com/oy/openyield/x/<module>/types packages (the app-layer
|
||||
// consumption direction), but MUST NOT import github.com/oy/openyield/
|
||||
// x/<module>/keeper OR github.com/oy/openyield/x/<module> (the module.go
|
||||
// packages — G-025 extends the original keeper-only check to also forbid
|
||||
// module.go, since those packages carry Cosmos runtime machinery the mock UI
|
||||
// must not reach into). This test uses go/parser (stdlib only — G-006) and
|
||||
// mirrors the x/window/types/types_test.go G-003 pattern, but with the
|
||||
// inverted rule: x/*/types is ALLOWED (app-layer consumption), x/*/keeper
|
||||
// and x/<module> (module.go) are FORBIDDEN.
|
||||
package store
|
||||
|
||||
import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestG025WebImportsOnlyTypesNotKeeperOrModule(t *testing.T) {
|
||||
webRoot := webRoot(t)
|
||||
fset := token.NewFileSet()
|
||||
violations := []string{}
|
||||
err := filepath.Walk(webRoot, 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-025 is about production code only).
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
f, perr := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
||||
if perr != nil {
|
||||
return perr
|
||||
}
|
||||
for _, imp := range f.Imports {
|
||||
ip := strings.Trim(imp.Path.Value, `"`)
|
||||
if isForbiddenXImport(ip) {
|
||||
rel, _ := filepath.Rel(webRoot, path)
|
||||
violations = append(violations, rel+" -> "+ip)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk web/: %v", err)
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
t.Errorf("G-025 violation: web/ production files importing forbidden x/ packages:\n %s",
|
||||
strings.Join(violations, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
// isForbiddenXImport reports whether ip is an x/<module>/keeper or a bare
|
||||
// x/<module> (module.go) import — both forbidden from web/ (G-025). The
|
||||
// x/<module>/types packages are ALLOWED (D-070 app-layer consumption).
|
||||
func isForbiddenXImport(ip string) bool {
|
||||
const prefix = "github.com/oy/openyield/x/"
|
||||
if !strings.HasPrefix(ip, prefix) {
|
||||
return false
|
||||
}
|
||||
rest := strings.TrimPrefix(ip, prefix)
|
||||
parts := strings.Split(rest, "/")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// x/<module> (module.go package) — forbidden (G-025).
|
||||
return true
|
||||
case 2:
|
||||
// x/<module>/types -> allowed (D-070). x/<module>/keeper -> forbidden.
|
||||
if parts[1] == "types" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
// x/<module>/<sub>/... — forbid anything other than types (e.g.
|
||||
// x/<module>/keeper/... sub-packages).
|
||||
if parts[1] == "types" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// webRoot returns the absolute path to the web/ directory by walking up
|
||||
// from this test file (web/store/import_test.go -> repoRoot/web).
|
||||
func webRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/web/store/import_test.go
|
||||
// repoRoot = filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||
// webRoot = repoRoot/web
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||
return filepath.Join(repoRoot, "web")
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Package store is the in-memory mock data layer for the OpenYield web UI.
|
||||
//
|
||||
// It instantiates the real x/*/types structs (Reach, Stash, StashActivity)
|
||||
// from in-memory fixtures and provides create/get/list methods. This is the
|
||||
// app-layer consumption of protocol types (D-070), NOT a cross-x/ production
|
||||
// import — web/ is NOT an x/ module. No keeper, no Cosmos runtime, no app.go
|
||||
// (G-003 boundary enforced by import_test.go / G-025).
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
identitytypes "github.com/oy/openyield/x/identity/types"
|
||||
stashtypes "github.com/oy/openyield/x/stash/types"
|
||||
)
|
||||
|
||||
// seedBalanceGrain is the test balance seeded to a new Stash at signup (D-071
|
||||
// example: 500000 Grain = 50 Bread per GrainsPerBread=10000).
|
||||
const seedBalanceGrain int64 = 500000
|
||||
|
||||
// Store is the in-memory mock store. All methods are goroutine-safe (mu).
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
reaches map[string]identitytypes.Reach
|
||||
stashes map[string]stashtypes.Stash
|
||||
stashActivities map[string]stashtypes.StashActivity
|
||||
}
|
||||
|
||||
// NewStore constructs a Store seeded from fixtures (fixtures.go).
|
||||
func NewStore() *Store {
|
||||
s := &Store{
|
||||
reaches: map[string]identitytypes.Reach{},
|
||||
stashes: map[string]stashtypes.Stash{},
|
||||
stashActivities: map[string]stashtypes.StashActivity{},
|
||||
}
|
||||
s.seed()
|
||||
return s
|
||||
}
|
||||
|
||||
// CreateReach atomically creates a Reach (IsNomad=true) + a Stash (D-071).
|
||||
// G-027: HolderID and PublicKey are validated (non-empty, <=128 bytes, no
|
||||
// path separators, no template syntax) before any map write. Returns the
|
||||
// created Reach + Stash.
|
||||
func (s *Store) CreateReach(holderID, publicKey string) (identitytypes.Reach, stashtypes.Stash, error) {
|
||||
if err := validateReachInput(holderID, publicKey); err != nil {
|
||||
return identitytypes.Reach{}, stashtypes.Stash{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, dup := s.reaches[holderID]; dup {
|
||||
return identitytypes.Reach{}, stashtypes.Stash{}, fmt.Errorf("holder %q already has a Reach", holderID)
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
reachID := "reach-" + holderID
|
||||
stashID := "stash-" + holderID
|
||||
reach := identitytypes.Reach{
|
||||
ReachID: reachID,
|
||||
HolderID: holderID,
|
||||
CreatedAt: now,
|
||||
PublicKey: publicKey,
|
||||
IsNomad: true,
|
||||
}
|
||||
stash := stashtypes.Stash{
|
||||
HolderID: holderID,
|
||||
StashID: stashID,
|
||||
CreatedAt: now,
|
||||
LastActive: now,
|
||||
BalanceGrain: seedBalanceGrain,
|
||||
}
|
||||
activity := stashtypes.StashActivity{
|
||||
StashID: stashID,
|
||||
ActiveDays: 1,
|
||||
MaxGapDays: 1,
|
||||
LastActivityDay: now,
|
||||
}
|
||||
s.reaches[holderID] = reach
|
||||
s.stashes[holderID] = stash
|
||||
s.stashActivities[stashID] = activity
|
||||
return reach, stash, nil
|
||||
}
|
||||
|
||||
// ListReaches returns all seeded + created Reaches.
|
||||
func (s *Store) ListReaches() []identitytypes.Reach {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]identitytypes.Reach, 0, len(s.reaches))
|
||||
for _, r := range s.reaches {
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetReach returns the Reach for a holderID (by HolderID, the stable key).
|
||||
func (s *Store) GetReach(holderID string) (identitytypes.Reach, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.reaches[holderID]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// GetStash returns the Stash for a holderID.
|
||||
func (s *Store) GetStash(holderID string) (stashtypes.Stash, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
st, ok := s.stashes[holderID]
|
||||
return st, ok
|
||||
}
|
||||
|
||||
// GetStashActivity returns the StashActivity for a stashID.
|
||||
func (s *Store) GetStashActivity(stashID string) (stashtypes.StashActivity, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
a, ok := s.stashActivities[stashID]
|
||||
return a, ok
|
||||
}
|
||||
|
||||
// validateReachInput enforces G-027: HolderID and PublicKey must be non-empty,
|
||||
// <=128 bytes, and contain no path separators or template syntax. This is a
|
||||
// prototype-robustness gate (the mock store uses holderID as a map key).
|
||||
func validateReachInput(holderID, publicKey string) error {
|
||||
if holderID == "" {
|
||||
return fmt.Errorf("holder id is required")
|
||||
}
|
||||
if len(holderID) > 128 {
|
||||
return fmt.Errorf("holder id too long (max 128)")
|
||||
}
|
||||
if strings.ContainsAny(holderID, "/\\") {
|
||||
return fmt.Errorf("holder id must not contain path separators")
|
||||
}
|
||||
if strings.Contains(holderID, "{{") {
|
||||
return fmt.Errorf("holder id must not contain template syntax")
|
||||
}
|
||||
if publicKey == "" {
|
||||
return fmt.Errorf("public key is required")
|
||||
}
|
||||
if len(publicKey) > 128 {
|
||||
return fmt.Errorf("public key too long (max 128)")
|
||||
}
|
||||
if strings.ContainsAny(publicKey, "/\\") {
|
||||
return fmt.Errorf("public key must not contain path separators")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
identitytypes "github.com/oy/openyield/x/identity/types"
|
||||
stashtypes "github.com/oy/openyield/x/stash/types"
|
||||
)
|
||||
|
||||
func TestNewStoreSeedsFixtures(t *testing.T) {
|
||||
s := NewStore()
|
||||
reaches := s.ListReaches()
|
||||
if len(reaches) < 2 {
|
||||
t.Fatalf("NewStore seeded %d reaches, want >=2", len(reaches))
|
||||
}
|
||||
// Both seeded reaches must be Nomads (IsNomad=true).
|
||||
for _, r := range reaches {
|
||||
if !r.IsNomad {
|
||||
t.Errorf("seeded reach %q: IsNomad=false, want true", r.HolderID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReachAtomicReachAndStash(t *testing.T) {
|
||||
s := NewStore()
|
||||
reach, stash, err := s.CreateReach("holder-test1", "pk-test1")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateReach: %v", err)
|
||||
}
|
||||
// D-071: Reach must be IsNomad=true.
|
||||
if !reach.IsNomad {
|
||||
t.Errorf("reach.IsNomad = false, want true (D-071)")
|
||||
}
|
||||
if reach.HolderID != "holder-test1" {
|
||||
t.Errorf("reach.HolderID = %q, want holder-test1", reach.HolderID)
|
||||
}
|
||||
// D-071: Stash must have matching HolderID + seeded BalanceGrain.
|
||||
if stash.HolderID != reach.HolderID {
|
||||
t.Errorf("stash.HolderID = %q, want %q (D-071 atomic)", stash.HolderID, reach.HolderID)
|
||||
}
|
||||
if stash.BalanceGrain != seedBalanceGrain {
|
||||
t.Errorf("stash.BalanceGrain = %d, want %d", stash.BalanceGrain, seedBalanceGrain)
|
||||
}
|
||||
// Both must be retrievable after the atomic call.
|
||||
if _, ok := s.GetReach("holder-test1"); !ok {
|
||||
t.Errorf("GetReach miss after CreateReach (atomicity broken)")
|
||||
}
|
||||
if _, ok := s.GetStash("holder-test1"); !ok {
|
||||
t.Errorf("GetStash miss after CreateReach (atomicity broken)")
|
||||
}
|
||||
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
||||
t.Errorf("GetStashActivity miss after CreateReach (atomicity broken)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReachDuplicateRejected(t *testing.T) {
|
||||
s := NewStore()
|
||||
if _, _, err := s.CreateReach("holder-alia", "pk-dupe"); err == nil {
|
||||
t.Errorf("CreateReach duplicate holder-alia: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReachValidationG027(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
holderID string
|
||||
publicKey string
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty holder", "", "pk", true},
|
||||
{"empty pubkey", "h", "", true},
|
||||
{"holder too long", stringOf('x', 129), "pk", true},
|
||||
{"pubkey too long", "h", stringOf('y', 129), true},
|
||||
{"holder with slash", "h/x", "pk", true},
|
||||
{"holder with backslash", "h\\x", "pk", true},
|
||||
{"holder with template syntax", "h{{", "pk", true},
|
||||
{"pubkey with slash", "h", "p/x", true},
|
||||
{"valid minimal", "h", "p", false},
|
||||
{"valid typical", "holder-oka", "pk-oka-7", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
s := NewStore()
|
||||
_, _, err := s.CreateReach(c.holderID, c.publicKey)
|
||||
if c.wantErr && err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
if !c.wantErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReachHitMiss(t *testing.T) {
|
||||
s := NewStore()
|
||||
if _, ok := s.GetReach("holder-alia"); !ok {
|
||||
t.Errorf("GetReach(holder-alia) miss, want hit (seeded)")
|
||||
}
|
||||
if _, ok := s.GetReach("nobody"); ok {
|
||||
t.Errorf("GetReach(nobody) hit, want miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStashHitMiss(t *testing.T) {
|
||||
s := NewStore()
|
||||
if _, ok := s.GetStash("holder-alia"); !ok {
|
||||
t.Errorf("GetStash(holder-alia) miss, want hit (seeded)")
|
||||
}
|
||||
if _, ok := s.GetStash("nobody"); ok {
|
||||
t.Errorf("GetStash(nobody) hit, want miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStashActivityHitMiss(t *testing.T) {
|
||||
s := NewStore()
|
||||
stash, ok := s.GetStash("holder-alia")
|
||||
if !ok {
|
||||
t.Fatal("seeded stash holder-alia missing")
|
||||
}
|
||||
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
||||
t.Errorf("GetStashActivity(%q) miss, want hit", stash.StashID)
|
||||
}
|
||||
if _, ok := s.GetStashActivity("stash-nobody"); ok {
|
||||
t.Errorf("GetStashActivity(stash-nobody) hit, want miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReachConcurrentNoRace(t *testing.T) {
|
||||
s := NewStore()
|
||||
const n = 50
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
holder := "holder-concurrent-" + itoa(i)
|
||||
_, _, _ = s.CreateReach(holder, "pk")
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
// All n concurrent creates with distinct holder IDs must be present.
|
||||
for i := 0; i < n; i++ {
|
||||
if _, ok := s.GetReach("holder-concurrent-" + itoa(i)); !ok {
|
||||
t.Errorf("concurrent reach %d missing after wg.Wait", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeededMatureVsImmature(t *testing.T) {
|
||||
s := NewStore()
|
||||
// holder-alia: ActiveDays=92, MaxGapDays=10 -> mature.
|
||||
aliaStash, ok := s.GetStash("holder-alia")
|
||||
if !ok {
|
||||
t.Fatal("seeded holder-alia missing")
|
||||
}
|
||||
aliaAct, ok := s.GetStashActivity(aliaStash.StashID)
|
||||
if !ok {
|
||||
t.Fatal("seeded alia activity missing")
|
||||
}
|
||||
if !aliaAct.IsMature() {
|
||||
t.Errorf("holder-alia IsMature=false, want true (ActiveDays=%d, MaxGap=%d)",
|
||||
aliaAct.ActiveDays, aliaAct.MaxGapDays)
|
||||
}
|
||||
// holder-bryn: ActiveDays=45, MaxGapDays=5 -> not mature.
|
||||
brynStash, ok := s.GetStash("holder-bryn")
|
||||
if !ok {
|
||||
t.Fatal("seeded holder-bryn missing")
|
||||
}
|
||||
brynAct, ok := s.GetStashActivity(brynStash.StashID)
|
||||
if !ok {
|
||||
t.Fatal("seeded bryn activity missing")
|
||||
}
|
||||
if brynAct.IsMature() {
|
||||
t.Errorf("holder-bryn IsMature=true, want false (ActiveDays=%d, MaxGap=%d)",
|
||||
brynAct.ActiveDays, brynAct.MaxGapDays)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time assertions that the types are the real x/*/types structs
|
||||
// (D-067: the mock store grounds the UI in the real Go type definitions).
|
||||
var _ identitytypes.Reach
|
||||
var _ stashtypes.Stash
|
||||
|
||||
// itoa is a tiny strconv.Itoa without the import (keeps store_test.go deps
|
||||
// to just sync + testing + the two x/*/types packages).
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
func stringOf(r rune, n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = byte(r)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{{define "base.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{block "title" .}}OpenYield{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<span class="brand">OpenYield</span>
|
||||
<a href="/">Home</a>
|
||||
<a href="/reach">Reach</a>
|
||||
<a href="/stash">Stash</a>
|
||||
<a href="/window">Window</a>
|
||||
<a href="/standing">Standing</a>
|
||||
<a href="/bloom">Bloom</a>
|
||||
</header>
|
||||
<main>
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
<footer>
|
||||
OpenYield — real production on the mesh. Reach, Stash, Window, Standing, Bloom.
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1,24 +0,0 @@
|
||||
{{define "title"}}OpenYield — real production on the mesh{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="panel">
|
||||
<h1>OpenYield</h1>
|
||||
<p>
|
||||
OpenYield is a mesh-native system for real production. A Holder creates a
|
||||
Reach to enter the mesh, holds a Stash of Grain, and authorizes Window
|
||||
access to partners. Standing accrues through honest participation, and
|
||||
Bloom rewards sustained contribution. No middleman holds your Stash.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>The five screens</h2>
|
||||
<ul>
|
||||
<li><a href="/reach">Reach</a> — create a Reach and view the mesh of Holders.</li>
|
||||
<li><a href="/stash">Stash</a> — your sovereign Grain Stash (P2).</li>
|
||||
<li><a href="/window">Window</a> — authorize partner access to your Stash (P3).</li>
|
||||
<li><a href="/standing">Standing</a> — track progress toward Freeholder standing (P4).</li>
|
||||
<li><a href="/bloom">Bloom</a> — accrued rewards for sustained contribution (P5).</li>
|
||||
</ul>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,31 +0,0 @@
|
||||
{{define "title"}}{{.Reach.ReachID}} — OpenYield{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="panel">
|
||||
<h1>{{.Reach.ReachID}}</h1>
|
||||
<table class="kv">
|
||||
<tr><th>Reach ID</th><td>{{.Reach.ReachID}}</td></tr>
|
||||
<tr><th>Holder ID</th><td>{{.Reach.HolderID}}</td></tr>
|
||||
<tr><th>Public Key</th><td><code>{{.Reach.PublicKey}}</code></td></tr>
|
||||
<tr><th>Created</th><td>{{.Reach.CreatedAt}}</td></tr>
|
||||
<tr><th>Nomad</th><td>{{if .Reach.IsNomad}}yes{{else}}no{{end}}</td></tr>
|
||||
<tr><th>Freeholder</th><td>{{if .Reach.IsFreeholder}}yes{{else}}no{{end}}</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{{if .Stash.StashID}}
|
||||
<section class="panel">
|
||||
<h2>Stash</h2>
|
||||
<table class="kv">
|
||||
<tr><th>Stash ID</th><td>{{.Stash.StashID}}</td></tr>
|
||||
<tr><th>Balance</th><td>{{.Stash.BalanceGrain}} Grain</td></tr>
|
||||
<tr><th>Created</th><td>{{.Stash.CreatedAt}}</td></tr>
|
||||
<tr><th>Last active</th><td>{{.Stash.LastActive}}</td></tr>
|
||||
<tr><th>Still</th><td>{{if .Stash.IsStill}}paused{{else}}active{{end}}</td></tr>
|
||||
</table>
|
||||
<p><a href="/stash/{{.Stash.HolderID}}">View Stash dashboard</a></p>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<p><a href="/reach">Back to Reach list</a></p>
|
||||
{{end}}
|
||||
@@ -1,35 +0,0 @@
|
||||
{{define "title"}}Reach — OpenYield{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="panel">
|
||||
<h1>Reach</h1>
|
||||
<p>A Reach is the mesh-native identity a Holder uses to act on the mesh
|
||||
without a custodian, a gatekeeper, or a legacy financial position. A Nomad
|
||||
is a Holder who has a Reach and a Stash and is on the way to earning the
|
||||
four Freeholder signals.</p>
|
||||
<p><a href="/reach/new" class="btn">Create a Reach</a></p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Holders on the mesh</h2>
|
||||
{{if .Reaches}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Reach ID</th><th>Holder ID</th><th>Nomad</th><th>Freeholder</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Reaches}}
|
||||
<tr>
|
||||
<td><a href="/reach/{{.HolderID}}">{{.ReachID}}</a></td>
|
||||
<td>{{.HolderID}}</td>
|
||||
<td>{{if .IsNomad}}yes{{else}}no{{end}}</td>
|
||||
<td>{{if .IsFreeholder}}yes{{else}}no{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>No Reaches yet. <a href="/reach/new">Create a Reach</a> to begin.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,22 +0,0 @@
|
||||
{{define "title"}}Create a Reach — OpenYield{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="panel">
|
||||
<h1>Create a Reach</h1>
|
||||
<p>A Reach is an identity, not a custodial position. The protocol does not
|
||||
require KYC at the protocol layer; the Reach is the unit of self-service.
|
||||
Creating a Reach also opens a Stash for you (the place a Nomad holds
|
||||
Grain) — that pair is enough to begin on the mesh.</p>
|
||||
|
||||
<form method="POST" action="/reach" hx-post="/reach" hx-target="body">
|
||||
<label for="holder_id">Holder ID</label>
|
||||
<input type="text" id="holder_id" name="holder_id" required
|
||||
maxlength="128" placeholder="a by-ID-string of your choosing">
|
||||
<label for="public_key">Public Key</label>
|
||||
<input type="text" id="public_key" name="public_key" required
|
||||
maxlength="128" placeholder="a public key for your Reach">
|
||||
<button type="submit">Create a Reach</button>
|
||||
</form>
|
||||
<p><a href="/reach">Back to Reach list</a></p>
|
||||
</section>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user