From 863c4c964b2b7492d1a0ee899f180f3ed02ea39b Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 20 Jul 2026 10:14:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(atlas):=20UX=20=E2=80=94=20plain=20node=20?= =?UTF-8?q?titles=20+=20mobile=20transition=20rows=20(re-review=20#1,#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lap-2 review's top two findings: #1 Node titles were still jargon (the lap-1 disease one level down). Every node now has a plain_t ("var-go Oath" → "Definition of done", "Admission controller" → "Tamper-proof seal", …); Plain view leads with it and demotes the technical name to a dim in-card subtitle — mirrors the stage-head pattern. plain_t on every authored node is now guard-tested. #2 Transition labels lived only in the SVG spine (display:none <820px) with a hover-only rationale — invisible on phones. Added stacked-layout transition rows (HTML, always-visible, full sentence, no hover) shown on mobile; desktop header band scoped to ≥821px so mobile isn't stretched. build/vet/lint(0)/test green; desktop Plain screenshot-verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/UX-REVIEW-2.md | 236 +++++++++++++++++++++++++++++ internal/atlas/atlas.json | 38 ++--- internal/atlas/build_test.go | 3 + internal/atlas/model.go | 17 ++- internal/web/static/cad-atlas.html | 29 +++- 5 files changed, 291 insertions(+), 32 deletions(-) create mode 100644 docs/UX-REVIEW-2.md diff --git a/docs/UX-REVIEW-2.md b/docs/UX-REVIEW-2.md new file mode 100644 index 0000000..9ac810f --- /dev/null +++ b/docs/UX-REVIEW-2.md @@ -0,0 +1,236 @@ +# CAD Atlas — Fresh-Eyes UX Review, Lap 2 + +Reviewer role: fresh-eyes UX (not implementer). Second pass, judging the shipped +progressive-disclosure sprint against the goal set in `UX-REVIEW.md`. This document +critiques; it does not change code. + +Sources reviewed: +- `docs/UX-REVIEW.md` (the spec that was implemented — lap 1) +- `internal/atlas/atlas.json` (authored data: `plain_title`, `plain`, `trans_label`, + `trans` per stage; `plain` per node) +- `internal/web/static/cad-atlas.html` (the renderer: Plain⇄Technical toggle, spine + transition labels, legend, substrate/footer hidden in Plain) +- `/tmp/ux5/home.png` (deployed Plain view, stages 00–04 in frame) + +--- + +## 1. Verdict + +**The sprint largely achieved the *phase* half of the success criterion and made a real +dent in the *transition* half — but it does not fully clear the bar. Grade for a naive +viewer: B / B+.** + +Success criterion was: a viewer with no briefing can explain **every phase** AND **what +advances work across every transition.** + +- **Phases: pass.** This is the big win. The headline hierarchy works — big plain_title + ("Notice what's happening", "Why we're here", "Agents do the work"), a dim technical + subtitle, and a jargon-free "what happens" sentence. A cold stakeholder can now narrate + every column. This is a genuine, measurable improvement over lap 1, where every column + was a mechanism name. Full marks here. + +- **Transitions: partial.** The short spine labels ("Does it matter to us?", "Worth a + session?", "Decision reached", "Sealed & agent-ready") convey the *gist* of each hop, + which is a step-change from the blank arrows of lap 1. But three things hold it back + from "can explain what advances work": + 1. The actual "what must be true to advance" sentence (`trans`) is **hover-only** (an + SVG `` tooltip). That is undiscoverable — nothing signals it's hoverable — and + dead on touch devices. So the naive viewer gets the one-line theme, not the gate + logic. + 2. The labels are **terse and grammatically mixed**: some are questions ("Does it matter + to us?"), some are achieved-states ("Decision reached", "It's live"). A first-timer + builds two different mental models — "is this the question asked here, or the answer + reached?" — from one row. + 3. **They vanish on narrow screens.** All transition labels live inside `svg.spine`, + which is `display:none` under 820px, and the stacked mobile layout renders no HTML + fallback. The single biggest comprehension win of the sprint is absent on a phone. + +- **The viewport undermines both.** The atlas is a 9-column horizontal scroll and a laptop + shows ~stages 00–03. That means **stage 04, the one human gate — the thing the header + literally advertises ("one human gate") — is off-screen on load**, along with execution, + CI, deploy, and the loop. A first-timer cannot see the shape of the gated flow, cannot + see that a human checkpoint exists, and gets no affordance that five more stages are to + the right. The atlas's whole payoff — "see the auditable flow from signal to pod at a + glance" — is not deliverable in the default viewport. + +Net: the copy layer is excellent and the toggle is the right architecture. The remaining +gap is structural (what's visible, and where the transition rationale lives), plus one +stubborn copy residue (node titles). A naive viewer *can* explain the phases unprompted; +they can explain the transitions only at a slogan level, and only for the half of the +pipeline they happen to scroll to. + +--- + +## 2. What works (the genuine wins) + +- **Plain default + persisted toggle.** Right call, right default. Nothing was thrown + away; Technical is the old atlas verbatim. Architecture matches the spec. +- **Stage headline hierarchy.** plain_title dominant, technical title demoted to a dim mono + subtitle, plain sentence underneath. Clean, scannable, correct visual weight. +- **Transitions exist at all.** Even terse, the labelled arrows turn a row of boxes into a + narrated flow. This was the #1 lap-1 miss and it shipped in the default layer. +- **Density dropped in Plain.** Tags, risk chips, host ribbon and footer are all suppressed + in Plain (`body.plain`), so the reading surface is airy and calm — a real contrast to the + lap-1 wall of pills. +- **Stage 06 no longer reads as empty.** An authored "Automated checks" node with plain copy + now backstops the live CI generation, so the CI gate never looks like a no-op on a cold + load. Directly fixes lap-1 punch-item 7. +- **Feedback bus is honestly hedged.** Dashed violet + "partly manual today" in both the loop + label and the legend. Matches the dogfooding-honesty discipline. +- **The pulse gives direction.** The travelling dot is a low-cost "this flows left-to-right, + then loops back" cue — helpful for orientation. + +--- + +## 3. What's still weak (shipped Plain view) + +**a. Node titles are still pure mechanism-jargon — the exact lap-1 disease, one level down.** +The node *body* is now plain, but the node *title* — the first, boldest thing the eye lands +on in a card — is untouched: "Applied AI Radar", "Intention substrate", "Admission +controller", "var-go Oath", "Session-Dispatch bridge", "Executor + reviewer loop", +"dma-cli · routing + scope", "assessor-loop ledger". The plain body can't fully rescue a +card whose title already framed it as a mechanism. The stage head got the plain-title/tech- +subtitle treatment; the node did **not** — an inconsistency that leaves each column half- +translated. See §4. + +**b. The transition rationale is hidden and fragile.** As in §1: `trans` is hover-only +(undiscoverable, touch-dead) and the whole label layer disappears under 820px. The "what +must be true to advance" — the governance content the atlas exists to show — is the least +robustly delivered part of the whole thing. + +**c. Horizontal scroll with no affordance = half the pipeline is invisible.** Nothing at the +right edge signals "more stages this way" — no fade, no scrollbar cue, no "→ 04–08", no +overview. A first-timer may reasonably believe the pipeline is five stages that end at "Write +the work order". The marquee human gate and the entire execute→ship→loop arc are off-frame by +default. This is the single largest comprehension barrier remaining. + +**d. Transition-label attachment is weaker than specced.** Lap-1 §5 asked for **label chips +sitting on the spine**; what shipped is floating 10px mono text ~9px above the arrow with no +background. On the dark grid it reads, but it floats — the viewer has to mentally bind the +text to the arrow beneath it. A chip (or a short leader) would make the label read as *of* +the arrow, not near it. + +**e. Gate treatment is cryptic.** Lap-1 §5 asked for a lock/shield glyph + stronger colour on +the two real gates (04 human, 06 CI). What shipped is a "▸ " prefix + gold + bold on those two +labels. A naive viewer will not read "▸" as "governance checkpoint"; the only real cue is +"one of these labels is gold", which leans entirely on the legend. The two most important hops +in the whole atlas deserve a glyph that says *stop / check*, not an arrowhead character. + +**f. The legend doesn't match the colours actually on screen.** The KEY decodes green/amber/ +coral as CI run-states plus gold=gate + dashed=feedback. But in Plain the cards show **violet, +blue, and coral pills** and **coloured/dashed borders** (council=violet, bridge=dashed-blue, +oath=dashed-gold, executor=coral) that the KEY never explains — while the green/amber/coral CI +states the KEY *does* explain are barely present in Plain. So the viewer sees a violet dot on +"Intention substrate" and a dashed-gold box on "var-go Oath" with no way to decode them, and a +legend describing states they can't see. Colour is carrying two unrelated meanings (authored +semantics vs. live CI status) under one key. Either key every colour/border shown in Plain, or +strip the decorative pills/borders in Plain so colour means only what the legend says. + +**g. The technical subtitle is a wash — mild noise, mild help.** Under the plain_title sits +`s.title`: sometimes near-plain ("Signals", "Strategic session"), sometimes pure jargon +("TELOS", "Spec → Gitea issue", "PR → CI", "Execute · agentsquad"). For a naive viewer roughly +half of these subtitles are undecodable filler directly under the headline; for a new engineer +they're a useful canonical-name bridge without a mode-switch. Because it's dim and small the +noise cost is low, so **keep it** — but note the *inconsistency*: the stage trusts the reader +with a dim technical name under a plain headline, yet the node doesn't extend that same courtesy +(§4). Apply the pattern uniformly. + +**h. Unexplained accent colours on headlines.** plain_title is violet for TELOS/Loop and amber +for the gate. Meaningful to the author, unkeyed for the viewer — a minor echo of problem (f). + +**i. The feedback bus reads as a mystery line in-viewport.** The dashed violet return leg drops +straight down out of the TELOS column, but its label ("What did we learn? · feedback bus") +sits at the very bottom of a 9-column-wide canvas — off-screen for anyone who hasn't scrolled +down and right. In the default view you see an unexplained dashed vertical line and no origin +(stage 08 is off-frame right). The honesty hedge is good; the *legibility* of the loop in the +first screen is poor. + +--- + +## 4. Node-title question — recommendation: **add plain node titles; keep the technical name as a dim subtitle inside the card.** + +Do **not** keep titles as-is, and do **not** simply swap in plain titles and delete the +technical ones. Mirror the pattern the stage head already uses, one level down: + +``` +[pill] Marks its own homework? No. ← plain_t (bold, primary) + Executor + reviewer loop ← t (dim mono subtitle) + One agent does the work; a second, ← plain (body, already shipped) + independent agent reviews it … +``` + +Reasoning for the mixed audience: + +- **The title is the frame.** The eye reads title → body. A jargon title ("Admission + controller", "var-go Oath") sets a mechanism frame that a plain body then fights against. + This is precisely the lap-1 diagnosis ("every node names a *mechanism* rather than the + *thing that happens to the work*") — it was fixed for stages and bodies but left standing in + node titles. The job is half-done until titles get the same treatment. +- **A naive viewer needs the plain title.** "Marks its own homework? No.", "Signed so tampering + shows", "The one human yes/no", "A machine-checkable definition of done" — these are + explainable at a glance; "dma-cli · routing + scope" is not. +- **A new engineer still needs the canonical name.** "var-go Oath", "Ed25519 admission + controller", "assessor-loop ledger" are the searchable terms that connect the picture to the + code and the brain. Deleting them would help the stakeholder and hurt the engineer — the + wrong trade for a "everyone" audience. +- **Consistency is its own win.** Right now stage heads say "plain big / technical small" and + nodes say "technical only". Two rules for the same card type is friction. One rule, applied + at both levels, makes the whole atlas feel like one designed system and makes the toggle's + mental model ("plain names up front, mechanisms one layer in") coherent. + +Concretely: add an optional `plain_t` per node in `atlas.json`; in Plain render `plain_t` as +the title and `t` as a `tech-sub`-style dim line (reuse the existing class); in Technical keep +today's behaviour (`t` as title). Nodes without a `plain_t` fall back to `t`, so it's an +incremental authoring task, not a big-bang rewrite. + +--- + +## 5. Prioritized next punch list (top 6 by comprehension impact) + +1. **Add plain node titles (`plain_t`), technical name demoted to a dim in-card subtitle.** + Highest impact: the title is the first thing read and it's still the lap-1 jargon disease. + Finishes the progressive-disclosure job the stages already got. (§3a, §4) + +2. **Solve the horizontal-scroll blindness.** A first-timer must be able to tell the pipeline + is nine stages and reach the human gate and the loop. Ship at least a right-edge fade + + "→ stages 04–08" hint; ideally a "fit to width / overview" zoom toggle so the whole gated + shape (and the one human gate the header promises) is visible at a glance. (§3c) + +3. **Render transition labels in the stacked/narrow layout, and surface the `trans` sentence + without a hover.** The labels currently die under 820px (they live only in the SVG) and the + gate rationale is hover-only/touch-dead. Emit `trans_label` as an HTML element between + stacked stages, and make the full `trans` reachable by click/tap (expandable), not just + desktop hover. This is the "explain every transition" half of the success criterion. (§1, §3b) + +4. **Give the two governance gates (04 human, 06 CI) a real glyph and make the feedback loop + legible in-viewport.** Replace the "▸" prefix with a lock/shield on the gold gate labels so + the checkpoints read as checkpoints; and attach a visible "What did we learn?" chip to the + top of the feedback return leg near TELOS so the dashed line isn't a mystery in the first + screen. (§3e, §3i) + +5. **Reconcile the legend with the colours on screen in Plain.** Either key every pill/border + meaning the Plain view shows (violet/blue/coral pills; council/bridge/oath borders) or drop + the decorative pills/borders in Plain so colour means only the CI states the KEY describes. + Today the legend and the canvas disagree. (§3f, §3h) + +6. **Turn floating transition text into attached chips and normalise the grammar.** Give each + label a small chip background so it reads as *on* the arrow, and pick one voice — all + "what-must-be-true" states ("Mattered to us", "Decision reached", "Human said go", "All + checks green", "It's live") reads more consistently than mixing questions and states. (§3d) + +--- + +### Scorecard vs. lap-1 punch list + +| Lap-1 item | Status | +|---|---| +| 1. Label every arrow | **Shipped** (desktop only; hover-only rationale; dies on mobile) | +| 2. plain_what per stage + demoted title | **Shipped** — clean | +| 3. Plain⇄Technical toggle, default Plain, persisted | **Shipped** — correct | +| 4. Plain node primary text, `d` on demand | **Half** — bodies plain, **titles still jargon** (§4) | +| 5. Distinguish the two gates (lock/shield) | **Weak** — "▸"+gold, no glyph | +| 6. Legend keying colours + gate types | **Partial** — legend exists but doesn't match Plain colours | +| 7. Fix stage-06 empty column | **Shipped** — authored fallback node | +| 8. Honest feedback bus + suppress proper nouns in Plain | **Half** — bus honest; proper nouns still leak via node titles + tech-subs | +</content> +</invoke> diff --git a/internal/atlas/atlas.json b/internal/atlas/atlas.json index 26445f5..81ed3ef 100644 --- a/internal/atlas/atlas.json +++ b/internal/atlas/atlas.json @@ -9,49 +9,49 @@ "stages": [ {"no":"STAGE 00","title":"Signals","plain_title":"Notice what's happening","plain":"New ideas and developments worth reacting to are collected — mostly an automated daily/weekly scan of AI news, plus things saved by hand.","path":"→ mathias/signals", "trans_label":"Does it matter to us?","trans":"A raw signal only advances if it connects to something we actually care about. Most captured signals stop here; the few that touch the mission get pulled up against a goal.","nodes":[ - {"t":"Applied AI Radar","plain":"An automated scan reads AI news every day (and deeper every week) and keeps only claims backed by a real paper, benchmark, code, or named lab.","d":"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).","tags":["cron · daily/weekly","→ signals #1–26+"]}, - {"t":"Manual capture","plain":"Anything interesting spotted by hand gets saved into the same inbox.","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]}, - {"t":"Aspirational surfaces","plain":"Planned but not built yet: sending ideas in by Telegram, voice, or a URL.","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]} + {"t":"Applied AI Radar","plain_t":"Automated news scan","plain":"An automated scan reads AI news every day (and deeper every week) and keeps only claims backed by a real paper, benchmark, code, or named lab.","d":"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).","tags":["cron · daily/weekly","→ signals #1–26+"]}, + {"t":"Manual capture","plain_t":"Saved by hand","plain":"Anything interesting spotted by hand gets saved into the same inbox.","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]}, + {"t":"Aspirational surfaces","plain_t":"Not built yet","plain":"Planned but not built yet: sending ideas in by Telegram, voice, or a URL.","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]} ]}, {"no":"STAGE 01","cls":"telos","title":"TELOS","plain_title":"Why we're here","plain":"The mission, goals, and problems we're actually trying to solve live here — every piece of work downstream has to trace back to one of these goals.","path":"wiki/telos/", "trans_label":"Worth a session?","trans":"A goal or problem on the board becomes the seed for a design session when it's decided worth working on now. The goal is the input the session must trace back to.","nodes":[ - {"t":"Intention substrate","plain":"The master list of mission, goals, problems, and current status — the yardstick everything downstream is measured against.","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=telos"]} + {"t":"Intention substrate","plain_t":"The goal board","plain":"The master list of mission, goals, problems, and current status — the yardstick everything downstream is measured against.","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=telos"]} ]}, {"no":"STAGE 02","title":"Strategic session","plain_title":"Think it through","plain":"A human and AI models work out what to do and why, debating hard calls and writing down the decision and what \"done\" will mean.","path":"claude.ai frontier + brain MCP", "trans_label":"Decision reached","trans":"It advances only when the thinking converges on a decision and is written down as a concrete, testable specification — not while the answer is still open.","nodes":[ - {"t":"Design · ADRs · specs","plain":"A human and a top-tier AI model figure out the approach and write down the decision plus what a finished result must prove.","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]}, - {"t":"🏛️ LLM Council","plain":"For hard calls, several AI models answer independently, anonymously critique each other, and a \"chair\" model synthesises one verdict — reducing any single model's bias.","cls":"council","pill":"var(--violet)","d":"fan-out → anonymous cross-review → chairman synth. glm-4.7-flash · qwen36-35b · gemma4-31b (chair).","tags":["hard strategic Q","chat.d-ma.be"]}, - {"t":"Autoresearch Council","plain":"A parallel version of the same review that vets research findings before they're allowed through.","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]} + {"t":"Design · ADRs · specs","plain_t":"Decide the approach","plain":"A human and a top-tier AI model figure out the approach and write down the decision plus what a finished result must prove.","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]}, + {"t":"🏛️ LLM Council","plain_t":"AI review panel","plain":"For hard calls, several AI models answer independently, anonymously critique each other, and a \"chair\" model synthesises one verdict — reducing any single model's bias.","cls":"council","pill":"var(--violet)","d":"fan-out → anonymous cross-review → chairman synth. glm-4.7-flash · qwen36-35b · gemma4-31b (chair).","tags":["hard strategic Q","chat.d-ma.be"]}, + {"t":"Autoresearch Council","plain_t":"Research review panel","plain":"A parallel version of the same review that vets research findings before they're allowed through.","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]} ]}, {"no":"STAGE 03","title":"Spec → Gitea issue","plain_title":"Write the work order","plain":"The decision is turned into a precise, self-contained work order an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal.","path":"agent-ready contract", "trans_label":"Sealed & agent-ready","trans":"Advances to the gate only when the spec is a complete contract: a pass/fail test, a risk tier, a regulatory note, no open human dependencies, one embedded Oath, and a valid cryptographic signature. A malformed or unsigned order fails closed and never reaches the gate.","nodes":[ - {"t":"Contract enforced","plain":"The work order must have a clear pass/fail test, a risk rating, a regulatory-risk note, and no unfinished human dependencies before it counts as agent-ready.","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]}, - {"t":"Admission controller","plain":"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it.","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]}, - {"t":"⚖️ var-go Oath","plain":"A machine-checkable \"definition of done\" is embedded in the work order — exactly one, or the order is rejected — later used to prove the result actually meets the spec.","cls":"oath","pill":"var(--gold)","d":"Acceptance contract embedded in the issue as a var fenced block. Exactly one — zero/multiple fail closed. Prose → typed steps; failures anchored to byte spans.","tags":["swedsl · var-go","defined here → enforced @06"]} + {"t":"Contract enforced","plain_t":"The work-order rules","plain":"The work order must have a clear pass/fail test, a risk rating, a regulatory-risk note, and no unfinished human dependencies before it counts as agent-ready.","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]}, + {"t":"Admission controller","plain_t":"Tamper-proof seal","plain":"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it.","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]}, + {"t":"⚖️ var-go Oath","plain_t":"Definition of done","plain":"A machine-checkable \"definition of done\" is embedded in the work order — exactly one, or the order is rejected — later used to prove the result actually meets the spec.","cls":"oath","pill":"var(--gold)","d":"Acceptance contract embedded in the issue as a var fenced block. Exactly one — zero/multiple fail closed. Prose → typed steps; failures anchored to byte spans.","tags":["swedsl · var-go","defined here → enforced @06"]} ]}, {"no":"STAGE 04","cls":"gate","title":"Human dispatch gate","plain_title":"Human says go","plain":"A person reviews the work order and its risk and decides whether to release it — the one and only checkpoint where work does not move on its own.","path":"the only checkpoint", "trans_label":"A human said go","trans":"The hard stop. Nothing crosses automatically — a person must review the plan and risk and explicitly release it, and the repo must be on the allow-list, before any agent starts. This is the single human checkpoint in the whole pipeline.","nodes":[ - {"t":"Human triggers execution","plain":"A person confirms the plan and its risk level, then releases the work — nothing runs until they do.","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true}, - {"t":"Session-Dispatch bridge","plain":"The approval flips a switch that hands the signed work order over to the agents to start execution.","cls":"bridge","pill":"var(--blue)","d":"claude.ai MCP → gitea:workflow_run_trigger → cad-dispatch.yml → agentsquad. The final design→execution bridge.","tags":["workflow_dispatch"]} + {"t":"Human triggers execution","plain_t":"The go button","plain":"A person confirms the plan and its risk level, then releases the work — nothing runs until they do.","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true}, + {"t":"Session-Dispatch bridge","plain_t":"Hand-off to agents","plain":"The approval flips a switch that hands the signed work order over to the agents to start execution.","cls":"bridge","pill":"var(--blue)","d":"claude.ai MCP → gitea:workflow_run_trigger → cad-dispatch.yml → agentsquad. The final design→execution bridge.","tags":["workflow_dispatch"]} ]}, {"no":"STAGE 05","cls":"exec","title":"Execute · agentsquad","plain_title":"Agents do the work","plain":"AI agents actually build the thing — one writes, a second independent one reviews it to avoid marking its own homework — and every step is logged for the audit trail.","path":"koala · cmd/agentsquad-serve", "trans_label":"Change proposed","trans":"Advances when the agents finish and open a proposed change (a PR) with its audit log attached. Until there's a concrete change to test, nothing moves.","nodes":[ - {"t":"Task API","plain":"A request kicks off a job and hands back an id you can poll for progress.","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]}, - {"t":"Executor + reviewer loop","plain":"One agent does the work; a second, independent agent on a different model reviews it — so nothing marks its own homework.","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true}, - {"t":"dma-cli · routing + scope","plain":"A router sends each agent to the right AI backend and enforces what it is and isn't allowed to touch, with a confirmation gate as a guardrail.","cls":"bridge","pill":"var(--blue)","d":"Harness-config arm: routes agents to the right LLM backend. Three-layer scope policy + confirmation gate = CAD guardrail.","tags":["backend routing","scope guardrail"]}, - {"t":"assessor-loop ledger","plain":"Every step is recorded in a tamper-evident log so the whole run can be audited afterwards.","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]} + {"t":"Task API","plain_t":"Start a job","plain":"A request kicks off a job and hands back an id you can poll for progress.","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]}, + {"t":"Executor + reviewer loop","plain_t":"Build + independent review","plain":"One agent does the work; a second, independent agent on a different model reviews it — so nothing marks its own homework.","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true}, + {"t":"dma-cli · routing + scope","plain_t":"Router & guardrails","plain":"A router sends each agent to the right AI backend and enforces what it is and isn't allowed to touch, with a confirmation gate as a guardrail.","cls":"bridge","pill":"var(--blue)","d":"Harness-config arm: routes agents to the right LLM backend. Three-layer scope policy + confirmation gate = CAD guardrail.","tags":["backend routing","scope guardrail"]}, + {"t":"assessor-loop ledger","plain_t":"Audit log","plain":"Every step is recorded in a tamper-evident log so the whole run can be audited afterwards.","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]} ]}, {"no":"STAGE 06","title":"PR → CI","plain_title":"Automatic quality checks","plain":"The proposed change is run through automated tests and safety checks — including a check that it actually satisfies the work order's definition of done — and only a clean pass lets it continue.","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs", "trans_label":"All checks green","trans":"Advances only if every automated check passes — tests, linters, security scan, and the Oath check proving it meets the original work order. Any red gate stops it here; a passing reviewer is not enough to override a failed Oath.","nodes":[ - {"t":"Automated checks","plain":"Tests, linters, a security scan, plus a check that the change actually meets the work order — all must pass to continue.","d":"go test · vet · lint · govulncheck + var-go/oath gate.","tags":["green = proceed"]} + {"t":"Automated checks","plain_t":"Quality checks","plain":"Tests, linters, a security scan, plus a check that the change actually meets the work order — all must pass to continue.","d":"go test · vet · lint · govulncheck + var-go/oath gate.","tags":["green = proceed"]} ]}, {"no":"STAGE 07","cls":"cd","title":"CD → pod","plain_title":"Ship it","plain":"Once everything is green, the change is deployed automatically to the live server — with the rule that merging code alone doesn't ship it; the release has to be pointed at the new version.","path":"Flux GitOps → k3s","generate":"deploy-state", "trans_label":"It's live","trans":"Once the new version is actually running on the server, the deployed outcome becomes the input to scoring. Advancing means shipped and observable, not just merged.","nodes":[ - {"t":"Deploy on green","plain":"When all checks pass, the release system rolls the new version onto the live server automatically — but only once the release is pointed at that version (merging code alone doesn't ship it).","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]} + {"t":"Deploy on green","plain_t":"Auto-deploy when green","plain":"When all checks pass, the release system rolls the new version onto the live server automatically — but only once the release is pointed at that version (merging code alone doesn't ship it).","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]} ]}, {"no":"STAGE 08","cls":"telos","title":"Loop back","plain_title":"Did it work?","plain":"The result is scored against the goal that started it and fed back into the mission board, so the next round of planning learns from what shipped.","path":"→ TELOS (feedback bus)", "trans_label":"What did we learn?","trans":"The scored outcome flows back into the mission board so goals, problems, and priorities update — the loop that makes the pipeline a cycle rather than a line. Partly manual today; an explicit improvement target.","nodes":[ - {"t":"Close the loop","plain":"The outcome is scored against the goal that started it and written back to the mission board, so future planning learns from what actually shipped.","pill":"var(--violet)","d":"session_log + attestation → brain. Score deploy outcome vs originating goal. (arc partly manual — improvement target.)","tags":["continuous"]} + {"t":"Close the loop","plain_t":"Score & feed back","plain":"The outcome is scored against the goal that started it and written back to the mission board, so future planning learns from what actually shipped.","pill":"var(--violet)","d":"session_log + attestation → brain. Score deploy outcome vs originating goal. (arc partly manual — improvement target.)","tags":["continuous"]} ]} ] } diff --git a/internal/atlas/build_test.go b/internal/atlas/build_test.go index d417fe5..5b478a1 100644 --- a/internal/atlas/build_test.go +++ b/internal/atlas/build_test.go @@ -60,6 +60,9 @@ func TestDefault_HasPlainLayerAndTransitions(t *testing.T) { if n.Plain == "" { t.Fatalf("stage %s node %q missing plain", s.No, n.Title) } + if n.PlainTitle == "" { + t.Fatalf("stage %s node %q missing plain_t", s.No, n.Title) + } } } } diff --git a/internal/atlas/model.go b/internal/atlas/model.go index c86a406..5a569c4 100644 --- a/internal/atlas/model.go +++ b/internal/atlas/model.go @@ -14,14 +14,15 @@ type Host struct { // Node is a card within a stage. Plain is the jargon-free default text; Desc is // the technical detail shown on demand. type Node struct { - Title string `json:"t"` - Plain string `json:"plain,omitempty"` - Desc string `json:"d,omitempty"` - Pill string `json:"pill,omitempty"` - Cls string `json:"cls,omitempty"` - Tags []string `json:"tags,omitempty"` - Risk bool `json:"risk,omitempty"` - Gate bool `json:"gate,omitempty"` + Title string `json:"t"` + PlainTitle string `json:"plain_t,omitempty"` + Plain string `json:"plain,omitempty"` + Desc string `json:"d,omitempty"` + Pill string `json:"pill,omitempty"` + Cls string `json:"cls,omitempty"` + Tags []string `json:"tags,omitempty"` + Risk bool `json:"risk,omitempty"` + Gate bool `json:"gate,omitempty"` } // Stage is one column of the pipeline. PlainTitle/Plain are the plain-language diff --git a/internal/web/static/cad-atlas.html b/internal/web/static/cad-atlas.html index e535bf4..e22c09a 100644 --- a/internal/web/static/cad-atlas.html +++ b/internal/web/static/cad-atlas.html @@ -65,8 +65,18 @@ .translabel{fill:var(--mono);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace} .translabel-gate{fill:var(--gold);font-weight:700} .stagehead{min-height:64px} - body.plain .stagehead{min-height:188px} - .stage .node:first-of-type{margin-top:24px} + @media(min-width:821px){ + body.plain .stagehead{min-height:188px} + .stage .node:first-of-type{margin-top:24px} + } + .trans-row{display:none} + @media(max-width:820px){ + .trans-row{display:block;margin:0 0 10px 6px;padding:7px 12px;border-left:3px solid var(--mono); + background:var(--panel);border-radius:0 8px 8px 0;font-size:12px;color:var(--dim);line-height:1.45} + .trans-row .tr-lbl{display:block;color:var(--mono);font-weight:600;margin-bottom:2px} + .trans-row-gate{border-left-color:var(--gold)} + .trans-row-gate .tr-lbl{color:var(--gold)} + } /* Plain view hides the jargon-dense infra ribbon + technical footer */ body.plain .substrate{display:none} body.plain footer{display:none} @@ -202,9 +212,9 @@ function renderAtlas(){ el.innerHTML=`<b>${h.n}</b><span class="k mono">${h.k}</span>`;sub.appendChild(el);}); const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh); stageEls=[]; - track.querySelectorAll('.stage').forEach(el=>el.remove()); + track.querySelectorAll('.stage,.trans-row').forEach(el=>el.remove()); const plain = MODE==='plain'; - STAGES.forEach(s=>{ + STAGES.forEach((s,idx)=>{ const st=document.createElement('div');st.className='stage '+(s.cls||''); const head = plain ? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+ @@ -214,7 +224,9 @@ function renderAtlas(){ (s.nodes||[]).forEach(n=>{ const pill=n.pill?`<span class="pill" style="background:${n.pill}"></span>`:''; const body = plain ? (n.plain||n.d||'') : (n.d||''); - let inner=`<div class="t">${pill}${n.t}</div>`+(body?`<div class="d">${body}</div>`:''); + const ntitle = (plain && n.plain_t) ? n.plain_t : n.t; + const nsub = (plain && n.plain_t && n.plain_t!==n.t) ? `<div class="tech-sub">${n.t}</div>` : ''; + let inner=`<div class="t">${pill}${ntitle}</div>${nsub}`+(body?`<div class="d">${body}</div>`:''); if(!plain){ if(n.tags&&n.tags.length)inner+=n.tags.map(t=>`<span class="tag mono">${t}</span>`).join(''); if(n.risk)inner+=`<div class="risk mono"><span class="lo">LOW · auto</span><span class="md">MED · ntfy gate</span><span class="hi">HIGH · blocked</span></div>`; @@ -223,6 +235,13 @@ function renderAtlas(){ h+=`<div class="node ${n.cls||''}">${inner}</div>`; }); st.innerHTML=h;track.appendChild(st);stageEls.push(st); + // stacked-layout transition row (shown on mobile where the SVG spine is hidden) + if(s.trans_label){ + const tr=document.createElement('div'); + tr.className='trans-row'+((idx===4||idx===6)?' trans-row-gate':''); + tr.innerHTML=`<span class="tr-lbl">${s.trans_label}</span>${s.trans?' '+s.trans:''}`; + track.appendChild(tr); + } }); }