# mgf-fed-web — implementation spec v0.1

**Companion to:** the proposal brief. **Audience:** the MGF agent (implementer) + Bassam/CTO/QA. **Status:** provisional — every open fork is a `CONFIRM` line in §7; nothing here is silently assumed. **Schema version used throughout:** `0.1`.

This spec turns the brief into something buildable: the sync truth model, the protocol surface each project exposes, the SourceProvider seam, the eight data schemas, the web layout and interaction, and a milestoned path with acceptance criteria. A working visual prototype ships alongside it (`mgf-fed-web_dashboard_wireframe.html`) so the visualization quality is concrete, not described.

---

## 1. The sync truth model (the heart of this)

The whole point of mgf-fed-web is that you can trust what you're looking at. So **sync state is a first-class value**, shown everywhere, and the dashboard never guesses.

### 1.1 fed-resources, recap
Each project carries `fed-resources/`, committed to git, with two strictly-separated subtrees governed by different rules:
- `generated/` — tool-owned, schema-versioned, **freshness-gated**, project → web, never hand-edited.
- `feedback/` — authored (web/founder → project), rounds-keyed, **the generator never writes here**.

The generator's write-scope is exactly `generated/`. That single rule is what keeps authored content out of a tool-overwritten tree.

### 1.2 The manifest is the sync anchor
`generated/manifest.json` records, at generation time: the `schema_version`, the project's `source_commit` (HEAD when generated), the generator name+version, the `generated_at` UTC, and a `sha256` for every generated file. Sync is decided against this anchor.

### 1.3 What "in sync" means — precisely
A project is **in sync** iff regenerating `generated/` from the project's current source would produce **byte-identical** output to what's committed. That is the authoritative test (`regenerate → assert no diff`), the same discipline as a reproducible-build check. The `source_commit` pointer is fast-path metadata for display; the content test is truth.

### 1.4 Two tiers of check
- **Shallow check** — instant, from the manifest alone, no generator run: do the files on disk match their recorded `sha256` (untampered)? does `source_commit == current HEAD`? Good enough for a fast dashboard load and a first verdict.
- **Deep check** — authoritative, runs the generator into a temp dir and diffs against the committed `generated/`, **without writing**. This is what "Check sync" triggers.

### 1.5 Sync states (the language, defined once)
Used identically per-project and (aggregated) globally:

| State | Meaning | How it's reached | Action offered |
|---|---|---|---|
| `IN_SYNC` | committed `generated/` == regenerate output | deep check passes (or shallow: hashes match + `source_commit==HEAD`) | none |
| `STALE` | regenerate output differs — the project moved ahead of its dashboard | deep check diff ≠ ∅ (or shallow: `source_commit != HEAD`) | **Force-sync** |
| `TAMPERED` | files don't match their own manifest `sha256` — `generated/` was hand-edited | shallow hash mismatch | **Force-sync** (regenerate) |
| `UNKNOWN` | project not found / unreachable / no `fed-resources/` | provider can't locate it | wire the project |
| `UNREADABLE` | `fed-resources/` present but manifest missing/corrupt or `schema_version` unrecognized | manifest parse/version fail | fix/regenerate; upgrade reader |
| `CHECKING` | a check is in flight | transient | — |
| `SYNCING` | a force-sync (regenerate) is in flight | transient | — |
| `ERROR` | check or generate failed (generator crashed, nonzero exit) | command failure | read the error, fix |

**Global state** = aggregate: `IN_SYNC` only if every project is `IN_SYNC`; otherwise the worst actionable state with counts (e.g. `DRIFT — 3 in sync · 1 stale · 1 unknown`).

### 1.6 Honest-absence (non-negotiable)
Missing, stale, unreadable, unknown-schema → the web renders the **explicit state with the reason and the one action that fixes it**. Never a blank, never an inferred value, never a stale view shown as if current. Absence is a state, not a gap.

---

## 2. The project contract — the `fedres` CLI

Every project that participates exposes one CLI surface (name it `fedres`, or a `make` target — `CONFIRM D-SYNC-1`). All commands are **deterministic**, emit **structured JSON** on `--format json`, and **exit nonzero on failure**. This is the protocol; mgf-fed-web is a pure consumer of it.

### 2.1 `fedres status --format json` — shallow, non-mutating, instant
Reads the manifest + the project's current HEAD. No generator run.
```json
{
  "schema_version": "0.1",
  "project": "soothfast",
  "source_commit": "f588d2a",
  "head_commit": "7160a43",
  "generated_at": "2026-06-28T06:41:02Z",
  "files_ok": true,
  "shallow_state": "STALE",
  "reason": "source_commit (f588d2a) != head_commit (7160a43): generated/ built from an older commit"
}
```
`shallow_state ∈ {IN_SYNC, STALE, TAMPERED, UNREADABLE}`. `files_ok` = every file matches its manifest `sha256`.

### 2.2 `fedres check --format json` — deep, authoritative, non-mutating
Regenerates into a temp dir, diffs against committed `generated/`, writes nothing.
```json
{
  "schema_version": "0.1",
  "project": "soothfast",
  "state": "STALE",
  "changed_files": ["status.json", "roadmap.json"],
  "detail": "2 of 6 generated files would change on regenerate",
  "checked_at": "2026-06-28T07:10:00Z"
}
```
`state ∈ {IN_SYNC, STALE, TAMPERED, ERROR}`. On `ERROR`, include `error` with the generator's stderr tail. This is the call behind the web's **Check sync**.

### 2.3 `fedres generate [--commit] --format json` — force-sync, **mutating** the working tree
Regenerates `generated/` in place from current source and rewrites the manifest (`source_commit←HEAD`, new hashes, new `generated_at`).
```json
{
  "schema_version": "0.1",
  "project": "soothfast",
  "regenerated": true,
  "changed_files": ["status.json", "roadmap.json"],
  "new_source_commit": "7160a43",
  "committed": false,
  "note": "generated/ rebuilt in the working tree; uncommitted — review and commit on the project side"
}
```
`--commit` is optional and **off by default for governed repos** (`CONFIRM D-SYNC-2`): the web never auto-commits into a repo with its own enactment rules; it regenerates and surfaces the diff, and you commit project-side. For a non-governed project, `--commit` may be allowed.

### 2.4 `fedres run <task|op> --trace [--format json]` — traced execution (emits live activity)
Runs a real operation (a build, a test suite, `verify.sh`, a reproduction) and **emits a live event stream** to `live/sessions/<id>.ndjson` while it runs (§4A), updating `live/CURRENT`. On completion it writes a **gated run-record** into `generated/runs/` (§4A.3) and refreshes any affected `generated/` state. The *result* is deterministic (same inputs → same output digests); only the *timing/ordering* of events is not. The trace is a side-channel — it never feeds the sync gate.
```json
{ "schema_version":"0.1","project":"soothfast","session":"sess_20260628T1830_a91f7c2",
  "op":"zig build test","task":"T-161","result":"ok","duration_s":42,
  "run_record":"generated/runs/2026-06-28-t161.json","live_log":"live/sessions/sess_20260628T1830_a91f7c2.ndjson" }
```

### 2.5 `fedres tail [--session <id>] [--follow] [--format json]` — read the live stream
Streams the NDJSON for the active (or named) session. The web uses this in package-less local mode, or tails the file directly through the provider seam. Non-mutating; if no session is active it returns an empty stream with `active:false` (honest-absence, not an error).

### 2.6 The freshness gate (project-side CI)
Each project's gate runs `fedres check` and **fails if state ≠ IN_SYNC**. This is what makes "committed `generated/`" mean "current `generated/`" instead of "stale snapshot." Wire it into the same place the project runs its other gates. The gate covers `generated/**` only — never `live/` or `feedback/`.

---

## 3. mgf-fed-web ↔ projects — the SourceProvider seam

One interface, two implementations, so the disk→package switch is additive (the write-side mirror of the same idea handles feedback delivery).

| Capability | `DiskProvider` (now) | `PackageProvider` (later, pre-release) |
|---|---|---|
| Read `generated/` + manifest | read the project's working tree on disk | read published package data |
| Shallow status | run `fedres status` (or read manifest + `git rev-parse HEAD`) | read manifest from package; `head` unavailable → shallow degrades to "as published" |
| Deep check / force-sync | shell into `fedres check` / `fedres generate` | needs the **live** repo → routed to project-side; web shows "deep actions run in the project" |
| Deliver feedback | write into the sibling `fed-resources/feedback/` (uncommitted) | produce the artifact for explicit delivery into the live repo |
| Which `generated/` the web shows | the working tree (so force-sync reflects instantly) | the published snapshot |

The web reads through the provider; it never assumes a project is local. In package mode, deep check and force-sync are honestly shown as "run in the project" rather than faked.

---

## 4. Data schemas (canonical JSON, `schema_version: "0.1"`)

Cross-cutting conventions:
- Every file carries `schema_version` and `kind`. Unknown `schema_version` → the web renders `UNREADABLE` for that project, never a guess.
- **ids** are short, stable, project-local strings (`T-157`, `stage-2a`, `S-1`, `FB-1`).
- **`applies_to`** `{ "view": "...", "ref": "<id>" }` lets a suggestion or feedback item be placed inline against the exact thing it concerns.
- **`evidence`** is a short provenance string; **`links`** are `{label, ref}` pointing at the underlying artifact (e.g. `ops/rounds/v15/findings.json`).
- **state enums** are closed sets (below). The web color-codes only known values.
- Canonical JSON (sorted keys, stable formatting) so hashes are stable and diffs are meaningful.

### 4.1 `manifest.json`
```json
{
  "schema_version": "0.1",
  "kind": "manifest",
  "project": { "id": "soothfast", "name": "Soothfast", "kind": "zig-kernel" },
  "generated_at": "2026-06-28T06:41:02Z",
  "source_commit": "7160a43",
  "generator": { "name": "fedres", "version": "0.1.0" },
  "files": [
    { "path": "status.json",        "sha256": "…", "schema": "status@0.1" },
    { "path": "architecture.json",  "sha256": "…", "schema": "architecture@0.1" },
    { "path": "focus.json",         "sha256": "…", "schema": "focus@0.1" },
    { "path": "suggestions.json",   "sha256": "…", "schema": "suggestions@0.1" },
    { "path": "roadmap.json",       "sha256": "…", "schema": "roadmap@0.1" }
  ]
}
```

### 4.2 `status.json` — the work units (feeds every lens)
The task is the atom the lenses slice. Each carries enough for "in progress / stalled / next-best / decisions" to be answered from reported fact, with the web deriving only orderings.
```json
{
  "schema_version": "0.1", "kind": "status", "project": "soothfast",
  "summary": { "total": 6, "by_state": { "done": 3, "in_progress": 1, "next": 1, "postponed": 1 } },
  "tasks": [
    { "id": "T-157", "title": "verify() Increment 1 — artifact invariant", "state": "done",
      "stage": "stage-2a", "depends_on": ["T-143"], "owner": "implementer",
      "risk": "low", "impact": "high", "effort": "M", "eta": "2026-06",
      "started_at": "2026-06-10T00:00:00Z", "updated_at": "2026-06-28T06:41:02Z",
      "evidence": "zig build test 91/91; verify.sh 710f9ea4…620b unmoved",
      "links": [{ "label": "R15 findings", "ref": "ops/rounds/v15/findings.json" }] },

    { "id": "T-160", "title": "buildRunAt end-to-end test", "state": "next",
      "stage": "stage-2a", "depends_on": ["T-157"], "owner": "implementer",
      "risk": "low", "impact": "medium", "effort": "S", "eta": "2026-07",
      "evidence": "C15-4: wrapper has no in-repo caller", "links": [] },

    { "id": "T-170", "title": "Second independent firmware reproduction", "state": "blocked",
      "stage": "stage-2a", "depends_on": [], "owner": "founder",
      "risk": "medium", "impact": "medium", "effort": "L", "eta": null,
      "started_at": "2026-05-02T00:00:00Z",
      "reason": "blocked on physical RAK4631/nrf52840 board + Zephyr SDK (GATE-FORGE-REAL-FLASH, founder-only)",
      "evidence": "Group F / D-16 staged", "links": [] }
  ]
}
```
Enums and fields:
- `state ∈ {next, todo, in_progress, done, skipped, postponed, blocked}`.
- `risk` / `impact ∈ {low, medium, high}` — drives the risk register and the next-best ranking.
- **`effort`** (one half of "cost") `∈ {XS, S, M, L, XL}` *or* an integer points value — pick one convention per project (`CONFIRM D-COST-1`) and keep it consistent.
- **`eta`** (the other half of "cost") — a calendar estimate: `"2026-07"`, `"2026-W30"`, or an ISO date. `null` = not estimated (honest-absence, not 0).
- `started_at` — when work began; the **age** a stalled item shows is `now − started_at`.
- `reason` — **required when `state ∈ {blocked, postponed}`** (a stalled item with no reason is itself a finding the web surfaces). Free text, the "why it stopped."
- `priority` (optional) `∈ {1..5}` or `{low, medium, high}` — a founder/agent override that floats an item in the next-best ranking above the derived score.
- `stage` — id reference into `roadmap.json` stages, so the roadmap can join tasks to stages and the critical-path lens can trace work to the chain.
- `owner` free-ish (`founder | implementer | court | qa | cto | …`); `owner == "founder"` feeds the **Decisions** lens.

### 4.3 `architecture.json` — feeds the Architecture lens (projection of the existing visual model, **not a redraw**)
Two modes; a project picks the one that reuses its real artifact.
```json
{ "schema_version": "0.1", "kind": "architecture", "project": "soothfast",
  "mode": "embed_svg",
  "embed": { "ref": "docs/visual/atlas_v0_7.svg", "title": "Atlas v0.7", "source_of_truth": true } }
```
or, when no pre-rendered diagram exists:
```json
{ "schema_version": "0.1", "kind": "architecture", "project": "soothfast",
  "mode": "graph",
  "graph": {
    "layers": ["core", "commons", "governance", "tools"],
    "nodes": [
      { "id": "engine",         "label": "compliance/engine", "layer": "core" },
      { "id": "canonical_json", "label": "commons/canonical_json", "layer": "commons" },
      { "id": "providers",      "label": "commons/providers", "layer": "commons" }
    ],
    "edges": [
      { "from": "engine", "to": "canonical_json", "kind": "depends_on" },
      { "from": "engine", "to": "providers", "kind": "depends_on" }
    ]
  } }
```
`mode ∈ {embed_svg, graph}`. `edge.kind ∈ {depends_on, calls, contains, data_flow}`. The web embeds the SVG (preferred — single source of truth) or renders the graph; it does not maintain a parallel diagram.

### 4.4 `focus.json` — current-focus sequence (optional; informs In-progress / Next-best ordering)
```json
{ "schema_version": "0.1", "kind": "focus", "project": "soothfast",
  "headline": "Closing R15 follow-ups before R16 intake",
  "items": [
    { "seq": 1, "title": "Add buildRunAt coverage test (C15-4)", "depends_on": [],
      "risk": "low", "impact": "medium", "status": "in_progress",
      "note": "kernel test: report==buildRun on '.', governed_fs restored on success+error" },
    { "seq": 2, "title": "Decide emit-set versioning shape (C15-3)", "depends_on": [1],
      "risk": "medium", "impact": "high", "status": "next",
      "note": "founder decision; recommend dedicated manifest + no-drift test" }
  ] }
```
`status ∈ {in_progress, next, blocked, done}`. `depends_on` references `seq` numbers within this list.

### 4.5 `suggestions.json` — the project agent's own recommendations + evidence (surface inline against the items they `apply_to`)
```json
{ "schema_version": "0.1", "kind": "suggestions", "project": "soothfast",
  "items": [
    { "id": "S-1", "title": "Land PRV-10 drift-guard before R16", "severity": "low",
      "applies_to": { "view": "roadmap", "ref": "stage-2a" },
      "rationale": "v0.1.4 SHOULD; forge already binds toolchain.lock sha",
      "evidence": "C15-5; sha256(toolchain.lock)=3145d185…2bc659",
      "proposed_action": "add const toolchain_lock_digest + RED drift test",
      "raised_by": "court", "raised_at": "2026-06-28T06:41:02Z" }
  ] }
```
`severity ∈ {info, low, medium, high}`. `applies_to.view ∈ {status, architecture, focus, roadmap, general}`. The web shows these standalone AND inline against the referenced item.

### 4.6 `roadmap.json` — the end-to-end chain (answers Q1 at stage level)
Stages now carry `risk`, `effort`, `eta`, `owner` directly, so the roadmap shows start → done → now → left *with* the dimensions you asked for — a chain can't render risk/cost legibly, so the web pairs the chain with a **roadmap table** keyed on these fields.
```json
{ "schema_version": "0.1", "kind": "roadmap", "project": "soothfast",
  "final_stage": { "id": "v1-0", "label": "Announced v1.0" },
  "stages": [
    { "id": "stage-0",  "label": "Stage-0 core",      "state": "done",        "progress": 1.0,
      "depends_on": [], "owner": "implementer", "risk": "low",    "effort": "L",  "eta": "2026-05", "summary": "L3-clean vs v0.1.1" },
    { "id": "stage-2a", "label": "Stage-2a (verify)", "state": "in_progress", "progress": 0.6,
      "depends_on": ["stage-0"], "owner": "implementer", "risk": "medium", "effort": "L", "eta": "2026-07", "summary": "gate open; 2 partials forward-gated" },
    { "id": "stage-2b", "label": "Address-emit gate", "state": "next",        "progress": 0.0,
      "depends_on": ["stage-2a"], "owner": "founder",  "risk": "high",   "effort": "M",  "eta": "2026-08", "summary": "route Address fields; emit test" },
    { "id": "v1-0",     "label": "Announced v1.0",    "state": "planned",     "progress": 0.0,
      "depends_on": ["stage-2b"], "owner": "founder",  "risk": "low",    "effort": "S",  "eta": "2026-Q4" }
  ] }
```
`state ∈ {done, in_progress, next, planned, blocked}`. `progress ∈ [0,1]`. `risk ∈ {low,medium,high}`. `effort` / `eta` as in §4.2 (`null` eta = not estimated). `depends_on` → renders the chain/DAG, the **critical path** (longest dependency chain to `final_stage`), and the "you are here" marker. Tasks join via their `stage` field.

### 4.7 `feedback.json` — inbound (web → project), under `feedback/rounds/v<N>/`
```json
{ "schema_version": "0.1", "kind": "feedback", "round": 4, "project": "soothfast",
  "authored_by": "bassam", "authored_at": "2026-06-28T07:20:00Z",
  "items": [
    { "id": "FB-1", "kind": "direction", "priority": "high",
      "title": "Prioritize the address-emit gate after R16",
      "body": "Detailed, actionable text the project can act on…",
      "applies_to": { "view": "roadmap", "ref": "stage-2b" } }
  ] }
```
`item.kind ∈ {direction, question, concern, approval, priority_change}`. `priority ∈ {low, medium, high}`. Non-operative: this is actionable text, not a change to the project.

### 4.8 `response.json` — return leg (project → web), same round folder
```json
{ "schema_version": "0.1", "kind": "response", "round": 4, "project": "soothfast",
  "responded_by": "implementer", "responded_at": "2026-06-28T09:00:00Z",
  "answers": [
    { "feedback_id": "FB-1", "status": "accepted",
      "body": "Sequenced stage-2b right after R16 close; roadmap updated.",
      "evidence": "roadmap.json stage-2b depends_on stage-2a", "links": [] }
  ],
  "agent_raised": [
    { "id": "AR-1", "kind": "concern",
      "body": "stage-2b needs the address set frozen first (PROP-017 → U-003).",
      "evidence": "U-003 deferred with reserved field" }
  ] }
```
`answer.status ∈ {accepted, done, deferred, rejected, needs_clarification}`. `agent_raised[].kind ∈ {concern, suggestion, question}` — this is the agent's own channel back to you. A round is **answered** (web-derived) when every `feedback.item` has a matching `answer`.

### 4.9 `federation.json` — cross-project coupling (optional)
Declares what a project consumes from another and how its roadmap depends on a sibling's. This is what makes the cross-project roadmap edges and the coupling lens **real** instead of inferred — absent file = no declared coupling.
```json
{ "schema_version": "0.1", "kind": "federation", "project": "soothsmith",
  "consumes": [
    { "from": "soothfast", "what": "commons/canonical_json",
      "why": "single canonicalizer — no second implementation (drift)",
      "pinned_at": "surface_version 1.0.0",
      "on_upstream_change": "trigger_check" }
  ],
  "roadmap_external_deps": [
    { "stage": "v1-0", "depends_on": "soothfast:v1-0" }
  ] }
```
`on_upstream_change ∈ {trigger_check, notify, none}` — when the upstream project advances past `pinned_at`, the web flags the downstream for a check (or just notes it). `roadmap_external_deps[].depends_on` uses `"<project>:<stage>"` so the federation roadmap can draw the inter-project edge as fact, not a `derived` guess.

### 4.10 The "changed since" delta (web-derived, no new file)
The **Changed** lens needs no schema — the web diffs a project's last two committed `generated/` snapshots and reports what moved (stage progress, task state transitions, new/closed suggestions, new findings). A project *may* add an optional `changelog` array to `status.json` for narrative ("R15 closed; C15-* opened"), but the structural delta is computed. Everything the Changed lens shows beyond a `changelog` entry is tagged `derived`.

---

## 4A. Live activity channel (real-time, ungated)

Real-time turns a read-mostly provenance tool into a live monitor — a different product with different uptime expectations. The design keeps the two natures **physically separate** so the monitor's flakiness can never erode trust in the provenance surface, and so a live log line is never mistaken for a committed fact.

### 4A.1 Two channels, by nature
| | `generated/` (state) | `live/` (activity) — **new** |
|---|---|---|
| Question it answers | "Where is the project?" | "What is it doing right now?" |
| Lifecycle | durable, **committed**, freshness-**gated** | ephemeral, **gitignored**, **never gated** |
| Shape | deterministic (regenerate → byte-identical) | append-only event log; timing non-deterministic |
| If it breaks | sync goes STALE/TAMPERED (loud, by design) | live pane shows "stream idle"; **sync surface unaffected** |

`live/` is a sibling of `generated/` so the same `SourceProvider` finds it, but the manifest's covered set is **`generated/**` only**. The generator never writes `live/`; the gate never reads it.
```
<project>/fed-resources/
  manifest.json          # covers generated/** only
  generated/             # gated state  (+ runs/  ← durable run-records, §4A.3)
  feedback/              # authored, never gated by the generator
  live/                  # EPHEMERAL · GITIGNORED · NEVER GATED   ← new
    CURRENT              # text pointer: active session id, or absent
    sessions/<id>.ndjson # append-only event stream, one JSON object per line
```

### 4A.2 The event stream (NDJSON, one object per line)
Append-only; `seq` is monotonic so the reader can detect gaps; the file *is* the session record, so a dropped poll loses nothing.
```json
{"v":"0.1","session":"sess_20260628T1830_a91f7c2","seq":1,"ts":"2026-06-28T18:30:01.220Z","kind":"session_start","op":"zig build test","actor":"implementer","task":"T-161","stage":"stage-2a"}
{"v":"0.1","session":"…","seq":2,"ts":"…","kind":"step","task":"T-161","label":"compile commons/canonical_json","progress":0.15}
{"v":"0.1","session":"…","seq":3,"ts":"…","kind":"log","task":"T-161","level":"info","msg":"surface_version 1.0.0 pinned"}
{"v":"0.1","session":"…","seq":4,"ts":"…","kind":"gate","task":"T-161","gate":"emit-set no-drift","result":"pass"}
{"v":"0.1","session":"…","seq":5,"ts":"…","kind":"artifact","task":"T-161","name":"verify.sh digest","digest":"sha256:710f9ea4…620b"}
{"v":"0.1","session":"…","seq":6,"ts":"…","kind":"session_end","result":"ok","duration_s":42,"run_record":"generated/runs/2026-06-28-t161.json"}
```
`kind ∈ {session_start, step, progress, log, artifact, gate, error, session_end}`. `progress ∈ [0,1]`. `session_end.result ∈ {ok, fail, aborted}` and points at the gated run-record it produced. These event kinds are also the **notification triggers** the future mobile app subscribes to (§8) — keep them stable.

### 4A.3 The durable shadow — `run_record` (gated, in `generated/runs/`)
When a traced op finishes, it writes a committed, freshness-gated record. This is the **verifiable** "what happened" (re-running yields identical input/output digests); the live stream is only the ephemeral "what's happening." The run-record feeds the **Changed** lens.
```json
{ "schema_version":"0.1","kind":"run_record","session":"sess_20260628T1830_a91f7c2",
  "op":"zig build test","task":"T-161","started_at":"2026-06-28T18:30:01Z","duration_s":42,"result":"ok",
  "inputs":[{"name":"toolchain.lock","digest":"sha256:3145d18…"}],
  "outputs":[{"name":"verify.sh digest","digest":"sha256:710f9ea4…620b"}],
  "gates":[{"gate":"emit-set no-drift","result":"pass"}] }
```
So: **live stream** = is it moving / what's it doing (ephemeral, telemetry); **run-record** = what happened, re-derivable (committed, provenance). Never conflated.

**Gate interaction (decide explicitly — `CONFIRM D-LIVE-2`):** run-records are *history* — `generate` projects current state and cannot reproduce a past execution, so run-records must be **exempt from the regenerate→identical assertion** or every traced run would force STALE permanently. The clean split: the reproduce-gate covers the **projected state** files (`status/roadmap/architecture/focus/suggestions`); `generated/runs/**` is **committed and integrity-hashed in the manifest** (so tampering is caught) but **outside the reproduce scope** (append-only history, not regenerated). Confirm this is the intended behaviour.

### 4A.4 The web follows; it does not command
The web is read-only on project state and, in package mode, cannot reach into a project at all — so it **cannot make a project go live**. It *discovers* an active session (the `live/CURRENT` pointer exists and its log is growing) and lights a telemetry-styled `● live` marker on that project. The button is **"Follow live"** (attach to a session the project is emitting), not "Activate."

The project emits when a traced op runs — `fedres run <task> --trace`, or the project's real build/test wrapped to `--emit-activity`. Two ways a session starts:
- **Project-initiated (primary, works in every mode):** a dev or CI runs `fedres run … --trace`; the web sees the session appear and offers Follow.
- **Web-triggered (local-dev convenience only — `CONFIRM D-LIVE-1`):** a "Start traced run" button drops a request sentinel `live/requests/<id>.json`; a project-side `fedres watch` daemon picks it up and runs it. Requires a running daemon and a writable sibling tree — **impossible once projects are PyPI packages**, so it's a nicety, not the foundation.

### 4A.5 Live UI — a mode on the In-progress lens (not a separate firehose)
Live data lands on the work units you already have. The **In-progress lens gains a Follow-live toggle**; when following, each in-progress task shows its current step, a live progress bar, last-event age, and the latest log line, with an expandable **session timeline** for the raw event browse. One mental model, not a bolt-on console.
- **Play/Pause** — pause stops the web tailing and freezes the view; the project keeps writing the log regardless.
- **History = active session only.** You can scroll the current (or just-ended) session's events. Older sessions are gitignored ephemera and may be pruned; the *durable* history lives in committed run-records, shown in **Changed**.
- **Telemetry marker, not sync chrome.** The live pane wears a distinct `telemetry` tag and the note "live activity — ephemeral, not provenance." If the tail breaks, the sync-truth surface stays visibly unaffected.

### 4A.6 Honest-absence (mandatory here)
- not emitting → "No live session — run `fedres run <task> --trace`" (never a spinner);
- stream stalls → "stream idle — last event 45s ago" (never a forever-spinner);
- session ends → "ended ok in 42s — see run-record" with a link into Changed;
- package mode → "live tracking is local-dev only; this is a frozen snapshot";
- unreachable log → UNREADABLE, like any other resource.

---

## 5. The web — layout & interaction

The prototype (`mgf-fed-web_dashboard_wireframe.html`) is the reference implementation of this section; open it to see the state language, the sync matrix, the lenses (Roadmap, In progress, Stalled, Next best, Decisions, Changed, Coupling), and the feedback thread in motion, at both project and federation scope. What follows is the spec it satisfies.

### 5.1 Information architecture
- **Instrument chrome (dark), readout (light).** A top status bar + left project rail in dark "instrument" chrome; the content area is a light paper readout (so every view prints clean). Boldness is spent on the sync-truth language and the roadmap chain; tables stay quiet.
- **Top bar:** product mark · the **global sync pill** (the single most characteristic element — `All in sync` / `Drift — 3 in sync · 1 stale · 1 unknown` / `Checking…`) · last-checked · `Check all` · `Force-sync all` · `Print / Export`.
- **Left rail:** `Federation overview` at top, then the project list, each row a name + `kind` + a **sync badge**. A collapsible **legend** pins the state language so colors/icons are never ambiguous.

### 5.2 The state language (defined once, used everywhere)
A fixed mapping of state → color + icon + label, shown in the legend and reused in the pill, the matrix, and every per-panel freshness chip. Provenance identity (commits, hashes, timestamps, state codes) is set in **monospace** — it's first-class in this world, not decoration. `derived` (web-inferred) content carries a distinct violet tag so it's never confused with project-reported facts.

### 5.3 Federation overview (landing) — combined lenses
The overview is the **same lenses, aggregated across projects** (see §5.4a). It opens on the **sync matrix** — every project × state, `source@<commit>` vs `HEAD@<commit>`, last-checked, per-row `Check` / `Force-sync` — the literal "is everything in sync, per project." A lens switcher then runs any lens federation-wide: cross-project Roadmap (with real inter-project edges from `federation.json`, `derived` only where inferred), In progress, Stalled, Next best (ranked across all projects), Decisions (your combined worklist), Risk register, Coupling. One mechanism, both scopes.

### 5.4 Per-project dashboard — header + lenses
- **Header strip:** name · `kind` · sync badge · `source@<commit>` vs `HEAD@<commit>` · `generated_at` · `Check sync` · `Force-sync` · `Compose feedback`. If not `IN_SYNC`, a one-line banner states exactly what's behind and the single action that fixes it.
- The body is a row of **lenses** (below). A lens is a question; the per-project view answers it for one project, the overview answers it across all. The same render code serves both scopes — the only difference is the project set passed in.

### 5.4a The lenses (this is what answers the use-case questions)
Each maps to a question you asked. "Reported" = straight from `generated/`; "derived" = web-computed and tagged.

1. **Roadmap** *(Q1 — the whole arc)*. The chain (start → done → in-progress → next → planned, "you are here") **paired with a roadmap table**: stage · state · progress · **risk** · **effort** · **eta** · deps · owner. Reported. The chain shows shape; the table shows the dimensions a chain can't.
2. **In progress** *(Q2 — what's happening now)*. Every `state == in_progress` task: title, owner, age (`now − started_at`), effort, eta, the stage it advances. Reported + age derived. **Gains a Follow-live mode** (§4A.5): when a project is emitting a traced session, each in-progress task shows current step · live progress · last-event age · latest log, with an expandable session timeline — telemetry-tagged, never confused with committed state.
3. **Stalled** *(Q3 — started, not finished, and why)*. `state ∈ {blocked, postponed}` plus any `in_progress` past a staleness threshold — each with its **`reason`** and **age**. A stalled item missing a `reason` is itself flagged. Reported + age/flag derived. *(This is the lens the old design couldn't answer.)*
4. **Next best** *(Q4 — most valuable next, with why)*. Candidates are `next`/`todo` with deps met. The web ranks them and **shows the inputs as the "why"** (§5.4b): impact, unblock-leverage, readiness, effort. Your `priority` override floats an item above the score. Derived ordering over reported facts.
5. **Decisions** *(Q5 — your worklist)*. `owner == "founder"` items + anything an answer marked `needs_clarification` + open feedback rounds. Because only you enact, this is literally what's waiting on you; combined across projects = one queue. Reported.
6. **Changed** *(Q5 — catch me up)*. The delta since the last snapshot (§4.10): stages that advanced, tasks that moved state, new/closed suggestions, new findings — plus any `changelog` narrative. Mostly derived, tagged.
7. **Coupling** *(Q5 — cross-project)*. From `federation.json`: what this project consumes from siblings and the `on_upstream_change` policy; the web flags when an upstream advance should trigger a downstream check. Reported.
8. **Architecture**. Embeds the project's own diagram (`embed_svg`) or renders the `graph`; a note names the source ("projected from Atlas v0.7"). Suggestions surface inline against the items they `apply_to`.

Overview-only derived lenses built from the same data: **Risk register** (all `risk == high` tasks + stages, sorted) and **Critical path** (the longest dependency chain to `final_stage` — "these stages gate v1.0").

### 5.4b The next-best ranking (the only non-trivial derivation — defined so it's auditable)
For each candidate task `t` (state `next`/`todo`):
- `ready(t)` = every id in `t.depends_on` is `done`. Not-ready candidates are listed separately as "blocked by deps," never silently dropped.
- `leverage(t)` = count of tasks that transitively depend on `t` (DFS over the reverse-dependency graph) — how much finishing `t` unblocks.
- `impact_w` = {low:1, medium:2, high:3} from reported `impact`.
- `effort_w` = {XS:1, S:1, M:2, L:3, XL:4} (or points bucketed) from reported `effort`.
- **score(t) = (impact_w × 2) + leverage(t) − (effort_w − 1)**, computed only for ready tasks; ties break by lower effort then higher impact.
- A reported `priority` floats an item **within its readiness class**: a `priority:high`/`1` *ready* task sorts to the top of the actionable list. A high-priority task that is **blocked by dependencies is not "to do next"** — it surfaces at the top of a separate "highest-value, but blocked" list with what it's waiting on, so the answer to "what do I do next" stays honestly actionable.
The card shows score **with its inputs** (`impact high · unblocks 3 · ready · effort S`) so the ranking is never a black box. The weights live in one place and are stated here; change them in one constant. Everything here is graph math over reported facts — honest, re-derivable, tagged `derived`.

### 5.5 Sync UI — global + per-project
- `Check sync` is **safe** (non-mutating): it runs the deep check; the target shows `CHECKING` with a brief scan, then resolves to a state. `Force-sync` is **mutating**: it warns, runs `fedres generate`, and on success the state settles to `IN_SYNC` with a result line — "regenerated, N files changed — uncommitted (act on project side)" for governed repos. Both report progress and outcome; neither is faked. `Check all` / `Force-sync all` fan out across projects and aggregate into the global pill.

### 5.6 Feedback — compose + the closed loop
- **Compose:** pick the view + `ref` (sets `applies_to`), write item(s), set `kind` + `priority` → `Deliver feedback`. This **produces** `feedback.json` and **delivers** it into the target's `feedback/rounds/v<N>/` inbox (local mode: written into the sibling tree, uncommitted). The UI confirms "delivered — act on it in the project."
- **Round thread:** each round shows your feedback items and the project's `response` (answers with status chips + the agent's own `agent_raised` concerns/suggestions), with the round marked `open` / `answered` (derived). This is the loop, visualized — both directions, no item lost.

### 5.7 Honest-absence rendering (per view)
Missing project → `UNKNOWN` card with "wire this project." Stale → the view renders with a `STALE` ribbon and "Force-sync to rebuild." Unreadable/unknown-schema → `UNREADABLE` with "regenerate / upgrade reader." Empty data → "Nothing reported yet," never a blank table. No view ever shows stale data as if current.

### 5.8 Print / export
Every view has a print stylesheet (chrome stripped, paginated, light). `Print / Export` prints the current scope; a project **review pack** export bundles all lenses + open feedback for a meeting. Built for the use case "review with the CTO and QA."

### 5.9 Quality bar (build to it without announcing it)
Responsive to mobile; visible keyboard focus; `prefers-reduced-motion` respected (the scan/settle animations degrade to instant); `derived` always tagged; the **no-blank rule** (every absence is an explicit state with an action). Copy is instrument-voiced: active, specific, sentence case; an action keeps its name through the flow (`Force-sync` → "Force-syncing…" → "Synced").

---

## 6. Implementation path (milestones with acceptance)

Each milestone is independently demoable; later ones are additive, never a re-architecture.

- **M0 — the contract.** Publish the `0.1` schemas (§4) + a validator (`fedres-validate`) that checks any `generated/` file and `feedback`/`response`. *Done when:* the validator passes on the example payloads and fails closed on a malformed one.
- **M1 — one project emits + the CLI + the gate.** Implement the `fedres` CLI (§2) for one project (dogfood on mgf-fed-web itself, or soothfast): `status` / `check` / `generate`, the manifest, and the freshness gate. *Done when:* `fedres check` returns `IN_SYNC` right after `generate`, and `STALE` after a source change; the gate fails on drift.
- **M2 — web, read-only.** The shell + legend + federation overview + one project's lenses, read through `DiskProvider` from `generated/`. *Done when:* the lenses render from real `generated/` data and honest-absence shows for a missing project.
- **M3 — sync.** Shallow status on load, deep `Check sync`, `Force-sync`, per-project + global, with the full state language and progress/result. *Done when:* the matrix reflects true states, `Check` is provably non-mutating, `Force-sync` regenerates and settles to `IN_SYNC`.
- **M4 — the feedback loop.** Compose → produce + deliver `feedback.json`; ingest `response.json`; the round thread with `open`/`answered` and `agent_raised`. *Done when:* a feedback item delivered from the web appears in the project's inbox and its response renders in the thread.
- **M5 — polish.** Print/export + review pack; the architecture `embed_svg` path; `derived` cross-project inference on the overview; multi-project. *Done when:* a project review pack prints cleanly and the cross-project roadmap renders with `derived` tags.
- **M6 — live activity (real-time).** `fedres run --trace` emitting NDJSON to `live/` + the run-record into `generated/runs/`; the web's Follow-live mode on the In-progress lens (discovery marker, current step / progress / last-event age / latest log, session timeline, Play/Pause, the idle/ended states), telemetry-tagged and gate-excluded. *Done when:* a real traced op streams into the lens live, the sync surface stays untouched when the tail stalls, and the op's run-record appears in Changed after `session_end`.
- **M7 — package mode (pre-release).** Fill in `PackageProvider` behind the seam; deep actions route to project-side honestly; live tracking degrades to "local-dev only." *Done when:* the web reads a published package and shows package-mode limits explicitly, with no change to M2–M6 code shape.

---

## 7. Non-negotiables & open decisions

**Non-negotiables (carry from the brief):** generated-not-asserted + freshness-gated; one versioned symmetric schema with a version-aware reader; honest-absence (no blanks, no stale-as-current); no second source of truth (projection of real artifacts, including the existing visual model); read-only for project state, feedback is non-operative and produced+delivered (no auto-commit into governed repos); everything printable.

**Resolved:** **D1** — feedback is non-operative; web produces + delivers into the target `feedback/` inbox; you act/commit project-side.

**Confirm before/while building (so nothing is assumed):**
- **D-SYNC-1** — the project CLI surface: a `fedres` binary, or `make fedres-{status,check,generate}` targets? (affects how the web shells out)
- **D-SYNC-2** — `Force-sync` on a governed repo: regenerate-only (you commit project-side, my default) vs allow `--commit` for non-governed projects. Confirm the default and the per-project allow-list.
- **D-SYNC-3** — which `generated/` the web reads in local mode: the **working tree** (force-sync reflects instantly — my default) vs the **committed** tree (durable, but force-sync needs a commit to show).
- **D2** — PyPI distribution unit: per-project packages vs one aggregate (shapes `PackageProvider`).
- **D3** — schema home + governance: in mgf-standard vs a dedicated `fed-schema` package; confirm starting `schema_version` `0.1`.
- **D4** — directory + CLI naming (`fed-resources/{generated,feedback}/`, `fedres`).
- **D5** — round identity: per-project `v<N>` counters vs a global federation round number.
- **D6** — architecture view source: confirm `embed_svg` (reuse the existing Atlas/UML) is preferred over the `graph` redraw.
- **D-VIZ-1** — the local-mode feedback delivery sub-choice from the brief: web writes straight into the sibling `feedback/` tree (my default) vs produces-for-you-to-drop.
- **D-COST-1** — the `effort` convention: T-shirt sizes `{XS,S,M,L,XL}` (my default) vs integer points. `eta` stays a separate calendar field either way. Pick one and keep it consistent across projects so the next-best `effort_w` weighting compares apples to apples.
- **D-LIVE-1** — live-session activation: **follow-only** (the web attaches to sessions the project emits — my default, works in every mode) vs **also** the local-dev "Start traced run" sentinel + `fedres watch` daemon (a convenience that can't survive package mode). Confirm whether to build the daemon path at all for v1.
- **D-LIVE-2** — run-records vs the freshness gate (§4A.3): confirm run-records are integrity-hashed in the manifest but **exempt from the regenerate→identical assertion** (history can't be regenerated), so a traced run doesn't force STALE.

Raise any concern or counter-proposal through the same loop (`agent_raised`) and commit to it.

---

## 8. Future directions (post-v1, recorded so they shape v1)

These are **not** in v1 scope. They're noted now because they constrain choices made today (mainly: keep the live event kinds and the per-project resource shape stable).

### 8.1 Mobile app
A native mobile companion — deliberately **simpler than the web**, not a port:
- **One project per request.** No federation grid; you pick a project and see its lenses one at a time (Roadmap, In progress, Stalled, Next best, Decisions). The federation overview is a desktop concern.
- **Notifications on device.** Push for the events that matter: sync drift detected, a stalled item that's missing a reason, a round answered (`response.json` arrived), a live session ending `ok`/`fail`, an upstream-coupling trigger. These map **directly onto the §4A.2 event kinds + the sync states** — no new project-side surface; a small push relay watches `generated/` + `live/` and fans out. This is *why* the event kinds are called out as stable in §4A.2.
- **Read-mostly.** Same read-only-on-state rule; at most it composes feedback into the same `feedback/` inbox.
- Scope deferred; flagged here only so the schema and event model stay notification-friendly from day one.

### 8.2 Live push (latency)
Replace the v1 poll/tail with a `watch`/SSE push from `fedres` so live events arrive sub-second. The file remains the session record, so push is purely a latency optimization layered on §4A — never the foundation.
