Audit result

The SPA is visually mature but does not reliably distinguish canonical state, derived state, local-only state, and failed state. The audit found 15 reader-facing information defects: 2 critical, 6 high, 5 medium, and 2 low. By defect class, 6 are not shown, 4 are shown but unexplained, and 5 are stale or wrong. The first implementation wave should address ranks 1–8; each is high-impact and has a contained remedy that does not require a visual redesign.

The central pattern is provenance loss. The server distinguishes authored workflow status from effective status, distributes sprint resources separately from the legacy index, emits version tokens, and reports project identity on every crew run. The loader often receives those distinctions and then flattens or discards them before rendering. Two interactive paths go further: they visually acknowledge a change before proving that canonical state changed.

Method and evidence snapshot

The audit read every routed component loaded by docs/index.html, followed each component's field accesses back through docs/ui/state-loader.js, and compared them with live responses from GET /_discover/reckon and GET /crew on 2026-08-21. Findings are limited to information already available to the current surface or to copy that is demonstrably inconsistent with the repository's operating contract. Styling preferences and navigation redesign are excluded.

Evidence Observed Why it matters
Live discovery response 171,172 bytes A cold or invalidated read is large enough that silence during loading reads as failure.
Discovered resources 67 artifacts · 9 sprints The SPA has enough data for typed search, relationship context, and sprint rationale.
Artifact metadata present 66 summaries · 52 linked · 67 created dates Compact surfaces can explain intent and provenance without another request.
Conflicting sprint state S5 active · S9 active · focus S5 The loader silently turns a contradiction into one authoritative-looking sprint.
Derived status divergence 1 plan · 3 open gates The effective blocked state is correct, but compact views hide why it differs from authored active state.
Version provenance 12 resource tokens The payload can identify the loaded snapshot, but the project shell discards every token.
Live crew response 9 runs · 3 projects · gates 189–1,346 chars The global table needs project identity and a compact gate signal, not an unbounded contract cell.

Ranked findings

Rank is ordered by the likelihood and cost of a reader taking the wrong action. “Reader” names the person whose task fails, not a hypothetical persona. Each remedy is deliberately incremental and stays inside the existing shell and visual language.

Rank Impact Class Source file Reader failed Evidence Concrete remedy
1 Critical not-shown docs/ui/state-loader.js:123–150
docs/ui/shell.jsx:826–835
Any reader opening a project, especially over a forwarded port The loader performs the entire discovery fetch before rendering. ReadyGate shows only “Loading plan state…” with no elapsed time or stage, and never catches a rejected STATE_READY promise. The live request took 4.3 s during this audit; the plan records a 16.57 s cold case. Render a timed loading state immediately: “Scanning <project> · 4.3 s elapsed”, then parse/render stages. Catch rejection into an explicit failure panel with endpoint, HTTP status, retry, and service-status guidance. Keep the locked block-on-cold policy; communicate the wait rather than serving distrusted data.
2 Critical stale-or-wrong docs/ui/shell.jsx:602–614
docs/ui/ui.jsx:235–277
A plan owner changing workflow status, archive state, or review state The status menu mutates the inventory and flashes “updated” without awaiting Persist.save. That save reports ok: true even when the server is unreachable and only local storage changed; a second conflict is also echoed locally. Await the canonical write result before showing success. Distinguish “saved to plan HTML vN”, “local only”, and “conflict; not saved”. Revert or visibly mark the optimistic value when canonical persistence fails, and expose a retry path.
3 High stale-or-wrong docs/ui/state-loader.js:177–190,248–257
docs/ui/shell.jsx:1132–1143
A coordinator deciding which sprint is current The live distributed response contains nine sprint resources, with both S5 and S9 marked active, while active_sprint_id is S5. The overview silently selects S5 and labels it as the sprint in focus. The legacy index was last updated 2026-07-29 and lists only S3–S6. Treat multiple active sprints or a legacy focus that disagrees with distributed resources as an integrity state. Show all active sprints, label the selected one “focus from legacy index”, and display a compact warning linking to the conflicting resources. Do not silently pick one.
4 High stale-or-wrong docs/ui/sprint.jsx:65–108,145–223 A planner moving work between workflow columns Cards are draggable and the drop target changes their visible status, but the code calls this a “drag-drop demo” and stores the override only in component state. There is no canonical write, acknowledgement, or local-only label; refresh discards it. Until a validated resource mutation exists, remove draggable and the “drop here” affordance. If editing is retained, use the same version-safe canonical write as plan state, show pending/saved/failed states, and snap back on failure.
5 High not-shown docs/ui/sprint.jsx:42–48,165–215 A sprint reader deciding why an item is present and what closes it Every live sprint item carries why_now and done_when. The board reads a nonexistent justification field and never reads done_when, so all nine sprint resources lose their item rationale and completion contract on screen. Map why_now directly into the existing card rationale line. Add a compact “done when” disclosure per card, preserving the gate summary as the measured-state companion rather than a replacement for the contract.
6 High shown-but-unexplained docs/ui/state-loader.js:159–173
docs/ui/shell.jsx:456–505,717–741
docs/ui/sprint.jsx:201–215
A coordinator diagnosing why apparently active work is blocked The loader deliberately overwrites the compatibility status alias with effective_status. The live plan derived-gate-state is authored active but effectively blocked by three open gates. Lists and title bars show only “blocked”; only the full plan's gate table explains the derivation. On compact plan and sprint surfaces, render “active → blocked” with “3 open gates” and link to the gate section. Reserve plain “blocked” for cases where authored and effective states agree.
7 High not-shown docs/ui/crew.jsx:37–61,100–133 An operator watching concurrent runs The Crew tab fetches the global /crew route. Its nine current rows span reckon, nova, and imas-codex, and every row includes project, but the table has no Project column and the page remains under a project-branded shell. Either fetch /crew/<current-project> for a project-scoped tab or add Project as the first column plus an explicit “all projects” scope label and project filter. Retain the already-correct cross-project links.
8 High stale-or-wrong docs/ui/home.jsx:58–64,237–250 A first-time operator adding a project or recovering the service The fleet page twice directs the reader to ~/docs-server/mounts.json, although the canonical path is ~/.config/reckon/mounts.json and the former is only a legacy fallback. The empty state also says to run uv run reckon serve, bypassing the documented systemd user-service workflow. Show the resolved config path returned by the server, not a hard-coded home path. Point recovery to reckon service status and setup to the managed sync/register command; retain “no restart needed” for mount changes.
9 Medium not-shown docs/ui/plan.jsx:195–262,418–503 A reader relying on a plan's prose, decisions, gates, or comments HTML fetch failure silently falls back to a reduced state-driven body. Full-state fetch failure is swallowed and leaves decisions, gates, and canonical comments absent. Neither case identifies the missing source, so a partial document looks complete. Keep the fallback readable, but place a persistent provenance banner above it: which source failed, which sections are unavailable, HTTP status, and retry. Never let a reduced rendering masquerade as the complete plan.
10 Medium not-shown docs/ui/state-loader.js:248–263
docs/ui/shell.jsx:838–1075
Any reader checking whether the visible project state is current Discovery emitted 12 resource_versions entries, but window.STATE discards the map and records no discovery completion time. Unlike Fleet and Crew, the project shell has no refreshed-at or snapshot identity. Preserve resource_versions, source_format, and a client loaded_at. Show one quiet shell-level line such as “distributed snapshot · 12 resources · loaded 14:03:21”, with manual refresh and a changed-version indicator.
11 Medium shown-but-unexplained docs/ui/graph.jsx:23–93,154–330 A coordinator reading the Graph tab as execution order The first “trajectory” is selected by maximum dependency-hop count among active or blocked plans. It ignores worker-hours, readiness, sprint order, and unresolved gate conditions, while the roadmap surface computes a weighted critical path with those distinctions. The UI shows neither the measure nor its limitations. Fetch the roadmap result for an execution-oriented critical path. If the local graph remains, label it exactly “longest dependency chain by hop count” and show why it differs from executable/weighted order; do not present it as an unlabeled first trajectory.
12 Medium shown-but-unexplained docs/ui/crew.jsx:100–133 An operator scanning for a stalled or failed run The Gate column prints the dispatch completion contract verbatim. Current cells range from 189 to 1,346 characters and are not verdicts; long prose dominates the table while phase and last activity are the actual scan signals. Rename the value “done when” and collapse it to one line with a disclosure. Give Gate its measured verdict when available; keep phase, elapsed, and last activity visible without horizontal prose pressure.
13 Medium stale-or-wrong docs/ui/bits.jsx:134–159 A coordinator copying a handoff prompt The modal says the prompt was built from <slug>.json, but plan HTML is the sole per-plan store. It also instructs every reader to paste into a “Claude conversation”, although runtime routing is provider-neutral. Say “built from live plan HTML and project discovery” and “paste into a fresh agent session”. Include the loaded plan version so the handoff's provenance is checkable.
14 Low shown-but-unexplained docs/ui/shell.jsx:456–505,717–759
docs/ui/sprint.jsx:205–215
A reader who did not author the plan-system vocabulary Compact rows use D 2, ! 3, ms, unlabeled percentage, and “Gates 10/13 passed” without tooltips or links. The same title bar spells out “progress” and “sprint”, showing that space is available for clearer language. Use “2 decisions”, “3 blockers”, “milestone”, and “progress”, or add accessible labels and explanatory tooltips. Make the signals links to the relevant plan section.
15 Low not-shown docs/ui/state-loader.js:258–261
docs/ui/shell.jsx:1128–1330
A project owner looking for blocker ownership and next action The loader keeps the project-level blockers array, including owner, summary, and next action, but no active overview component reads it. Current reckon's only row is resolved; an active row would be equally invisible except for indirect per-plan counts. Add an overview blocker section only when unresolved rows exist, showing summary, owner, gated count, and next action. Keep resolved rows out of the live cockpit and available through history.

Surface coverage

Every routed surface and shared interaction layer was inspected. The table prevents a ranked list from being mistaken for a review of only the files that produced findings.

Surface Primary source Audit outcome
Fleet home and project pickerhome.jsx, _shared.jsxRanks 8 and 10; cards otherwise expose counts, last edit, and activity coherently.
Load and shell readinessstate-loader.js, shell.jsxRanks 1, 3, 6, and 10.
Filters, artifact list, command paletteshell.jsxRanks 6 and 14; typed search includes summary, and type/provenance facets are otherwise visible.
Overview cockpitshell.jsx CockpitBody; loaded legacy cockpit.jsxRanks 3 and 15. The active route uses CockpitBody; the older component is not the rendered authority.
Plan, research, and evidence readerplan.jsx, decision.jsxRanks 2, 6, and 9; typed banners and the explicit gate table communicate rich metadata correctly when both fetches succeed.
Sprint boardsprint.jsxRanks 3–6 and 14.
Dependency graph and plan fangraph.jsxRank 11; authored edges and research→plan→evidence direction are otherwise represented consistently.
Crew watcher and in-flight bandscrew.jsx, plan.jsx, sprint.jsxRanks 7 and 12; three-second polling, elapsed time, phase, and last activity are clear.
Status, decisions, comments, and persistenceui.jsx, bits.jsx, decision.jsx, shell.jsxRanks 2 and 13; decision context and quote-anchored comments are otherwise explicit.
Theme, density, glyphs, base chrome_shared.jsx, glyphs.jsx, shared CSSNo material information defect found; no redesign recommendation.

Recommended implementation sequence

  1. Restore truth at mutation boundaries: ranks 2 and 4. A surface must not claim canonical change without a successful canonical write.
  2. Make waiting and failure legible: ranks 1 and 9, then expose snapshot provenance from rank 10.
  3. Preserve state distinctions already in the payload: ranks 3, 5, 6, and 7.
  4. Correct operational copy: rank 8, then rank 13.
  5. Clarify dense analytical surfaces: ranks 11, 12, 14, and 15.

This sequence is intentionally not a redesign. It adds acknowledgements, source labels, missing fields, and bounded disclosures to existing components. A separate navigation or visual-language proposal is not justified by this evidence.