docs(P00): research — v0.6 Nomad Web UI MVP findings

---ci---
phase: 0
milestone: v0.6
status: research
decisions:
  - id: D-074
    decision: HTMX 2.0.10 vendored as web/static/htmx.min.js (single JS file, no build step, no go get — G-006 preserved)
    rationale: htmx.org docs confirm dependency-free single-file install; 2.0.10 is current stable (v4 in beta, Summer 26 target)
    confidence: 0.95
    alternatives: [htmx 1.x (IE support, unnecessary), pin a newer beta (instability risk)]
  - id: D-075
    decision: lexicon_meta_web_test.go scans web/**/*.{html,js,go} as a new sibling firewall (package lexicon_meta_web, subdir lexicon_meta_web/)
    rationale: web/ is a new top-level dir NOT under x/ — the existing lexicon_meta_test.go (x/**/*.go) does not cover it; mirror the lexicon_meta_docs/ subdir pattern with G-013 walk-coverage + G-009 self-test + G-014 shared SyntheticBannedStrings()
    confidence: 0.85
    alternatives: [extend lexicon_meta_test.go to also walk web/ (mixes x/ and web/ concerns), separate .go and .html/.js tests (more files)]
  - id: D-076
    decision: Go 1.22 net/http.ServeMux is the sole router for web/ (method+path patterns, r.PathValue); gorilla/mux NOT used by web/ despite being a transitive cosmos-sdk dep
    rationale: go.mod:3 confirms go 1.22; enhanced ServeMux covers GET/POST + path params for all 5 screens; G-006 zero-dep preserved (no third-party router)
    confidence: 0.95
    alternatives: [gorilla/mux (breaks G-006 for web/, unnecessary), chi/router (new dep)]
  - id: D-077
    decision: frontend-engineer activated for v0.6 with territory web/** (templates, static, handlers, store, main.go, lexicon_meta_web_test.go); backend-engineer co-owns the mock store x/*/types integration
    rationale: first UI milestone — frontend-engineer was deactivated since v0.3 (no UI work); Go html/template + HTMX stack (no node/React) aligns with frameworks; constraints bind G-006 (vendored HTMX), G-003 (app-layer type import), REQ-012 (lexicon), D-073 (bread-scale code constants)
    confidence: 0.90
    alternatives: [keep frontend-engineer deactivated and have backend-engineer own templates (wrong skill fit), activate docs-writer instead (no docs-content work in v0.6)]
---ci---

v0.6 §1: Go html/template + HTMX architecture — server layout (web/main.go,
handlers/, store/, templates/, static/), base template pattern, HTMX 2.0.10
vendoring (single JS file, no build step, G-006 preserved), progressive
enhancement via HX-Request header (fragment vs full-page dispatch), html/template
contextual auto-escaping (XSS prevention).

v0.6 §2: Mock server data model — exact struct shapes verified from source for
all 6 modules (identity Reach, stash Stash+StashActivity+IsMature, window
Window+Scope+RateLimit+Activate/Revoke/Expire, standing Rating/Vouch/Slash/
FreeholderSignals+helpers, bread GrainsPerBread=10000+BreadScaleAll 11 tiers,
bloom BloomRecord+TargetBloomRateBasisPoints=450). Import paths use module
github.com/oy/openyield. Bread-scale code constants are the source of truth
(D-073) — docs/shared/bread-scale.md is outdated (claims 1000x ratios; code
uses 100x).

v0.6 §3: Lexicon firewall extension — pattern to mirror from
lexicon_meta_test.go (x/**/*.go) and lexicon_meta_docs/ (docs/**/*.md); new
lexicon_meta_web/ subdir + package lexicon_meta_web scanning
web/**/*.{html,js,go}; 10 banned terms verified from lexicon.go:30-41 (bank,
deposit, interest, yield, currency, dollar, euro, account, savings, depositor);
G-013 walk-coverage + G-009 self-test + G-014 shared helper.

v0.6 §4: HTTP routing — Go 1.22 net/http.ServeMux method+path patterns
confirmed (go.mod:3); r.PathValue() for path params; gorilla/mux (go.mod:75
transitive) NOT used by web/ per G-006.

v0.6 §5: PERSONAS.md update — frontend-engineer activation (YAML frontmatter
format documented); territory web/**, frameworks Go 1.22+html/template+HTMX+
ServeMux (NO node/React), constraints G-006/G-003/REQ-012/D-073; backend-engineer
co-owns mock store type integration; security/cosmos/mesh/data-engineers
deactivate for v0.6 (UI-only, no runtime).

v0.6 §6: ARCHITECTURE.md update — proposed section outline appended after
line 514 (v0.5 section end); 7 subsections mirroring v0.5 density.
This commit is contained in:
2026-08-18 11:57:23 +00:00
parent 4876c49aa0
commit 6597cbebd8
+530 -1
View File
@@ -2194,4 +2194,533 @@ 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.
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
`&lt;script&gt;...`-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.