generated from mathias/template-go-web
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60cc8894a2 | ||
|
|
863c4c964b | ||
|
|
ac086b65b9 | ||
|
|
d992ca4a01 | ||
|
|
5048450b79 | ||
|
|
1f7c9ab1eb | ||
|
|
3e28dcf69f |
@@ -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 `<title>` 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>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# CAD Atlas — Fresh-Eyes UX Review
|
||||||
|
|
||||||
|
Reviewer role: fresh-eyes UX (not implementer). This document critiques the copy and
|
||||||
|
information architecture of the "From Signal to Pod" atlas and specifies the
|
||||||
|
progressive-disclosure layer for the coming sprint. It does not change code.
|
||||||
|
|
||||||
|
Sources reviewed:
|
||||||
|
- `internal/atlas/atlas.json` (authored stages + nodes)
|
||||||
|
- `internal/web/handler.go` (which stages get live data overlaid)
|
||||||
|
- `.context/PROJECT.md` (ground-truth meaning of each stage/gate)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Diagnosis — why the current atlas is hard for a naive viewer
|
||||||
|
|
||||||
|
The atlas is written by the person who built the pipeline, for the person who built the
|
||||||
|
pipeline. Almost every node names a **mechanism** (`Ed25519 admission controller`,
|
||||||
|
`var-go Oath`, `dma-cli`, `assessor-loop ledger`, `agentsquad`, `ISC`, `TELOS`) rather
|
||||||
|
than the **thing that happens to a piece of work**. A compliance officer or a new
|
||||||
|
engineer cannot answer the two questions they actually have: *"what is happening to the
|
||||||
|
work at this step?"* and *"why does it move to the next step?"*
|
||||||
|
|
||||||
|
The second question is completely unanswered. The atlas renders nine stages connected by
|
||||||
|
arrows, but the arrows carry **zero copy**. There is no statement of what has to be true
|
||||||
|
for work to advance — which is exactly where the interesting governance lives (a human
|
||||||
|
sign-off at 04, a green CI gate at 06, an integrity check at 03). The pipeline's whole
|
||||||
|
selling point is "auditable, gated flow," yet the gates between stages are invisible.
|
||||||
|
A viewer sees a row of jargon boxes and an implied left-to-right drift, with no sense of
|
||||||
|
what earns each hop.
|
||||||
|
|
||||||
|
Two smaller aggravators: (a) the reel is dense — 20+ nodes, colour-coded pills whose
|
||||||
|
meaning is never keyed, and Swedish-homelab proper nouns (koala, iguana, flamingo) that a
|
||||||
|
stakeholder can't decode; (b) Stage 06 is authored-empty (it's generated live from CI),
|
||||||
|
so on a cold/offline load that column can read as "nothing happens here," which is the
|
||||||
|
opposite of the truth — CI is a governance gate.
|
||||||
|
|
||||||
|
The fix is not to dumb it down. It is to make **plain-language the default layer** — a
|
||||||
|
one-line "what happens + why" per stage, and a labelled "what must be true to advance"
|
||||||
|
per arrow — and demote today's precise, correct technical copy to an **on-demand layer**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Per-stage plain-language map (00–08)
|
||||||
|
|
||||||
|
| stage | plain_title | plain_what (one jargon-free sentence) | keep_technical (on-demand) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **00 Signals** | Notice what's happening | New ideas and developments worth reacting to are collected — mostly an automated daily/weekly scan of AI news, plus things saved by hand. | "Signals → mathias/signals"; Applied AI Radar (Tier-1 daily / Tier-2 weekly), verified-primary bar; brain capture; aspirational inbox surfaces (not built). |
|
||||||
|
| **01 TELOS** | Why we're here | 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. | "TELOS — intention substrate", `wiki/telos/`, `brain_query wing=telos`. |
|
||||||
|
| **02 Strategic session** | Think it through | 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. | "Strategic session" — claude.ai frontier + brain MCP; ADRs/specs; ISC acceptance criteria; LLM Council (fan-out → anonymous cross-review → chairman synth); Autoresearch Council. |
|
||||||
|
| **03 Spec → Gitea issue** | Write the work order | The decision is turned into a precise, self-contained work order that an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal. | "Spec → Gitea issue" — binary ISC, risk tier LOW/MED/HIGH, reg-risk assessment, no open human deps; Ed25519 admission controller (#36); var-go Oath (single fenced block, fail-closed). |
|
||||||
|
| **04 Human dispatch gate** | Human says go | A person reviews the work order and its risk and decides whether to release it — this is the one and only checkpoint where work does not move on its own. | "Human dispatch gate — the only checkpoint"; ratify plan + risk tier; Session-Dispatch bridge (claude.ai MCP → `workflow_run_trigger` → `cad-dispatch.yml` → agentsquad); dispatch-allow eligibility. |
|
||||||
|
| **05 Execute · agentsquad** | Agents do the work | 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. | "Execute · agentsquad" on koala; Task API (`POST /tasks`); executor+reviewer loop (ADK Go + LiteLLM, reviewer on distinct tier); dma-cli routing + 3-layer scope guardrail; assessor-loop attestation ledger + brain session_log. |
|
||||||
|
| **06 PR → CI** | Automatic quality checks | 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. | "PR → CI" — Gitea Actions `cd.yml`; go test/vet/lint/govulncheck; **var-go/oath gate** (correctness floor over the reviewer, anti-rubber-stamp #55). *Nodes generated live from the latest CI run.* |
|
||||||
|
| **07 CD → pod** | Ship it | 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. | "CD → pod" — Flux GitOps → k3s on koala; "push ≠ deploy: bump tag in mathias/infra"; ntfy on deploy. *Deploy + Flux state overlaid live.* |
|
||||||
|
| **08 Loop back** | Did it work? | 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. | "Loop back → TELOS (feedback bus)"; session_log + attestation → brain; outcome scored vs originating goal; arc partly manual (improvement target). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Per-node plain restatements
|
||||||
|
|
||||||
|
The existing `d` text stays as the **technical detail layer**. Each `plain` line below is
|
||||||
|
the jargon-free default. Kept accurate to PROJECT.md.
|
||||||
|
|
||||||
|
**Stage 00 — Signals**
|
||||||
|
- *Applied AI Radar* → **"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."**
|
||||||
|
- *Manual capture* → **"Anything interesting spotted by hand gets saved into the same inbox."**
|
||||||
|
- *Aspirational surfaces* → **"Planned-but-not-built: sending ideas in by Telegram, voice, or a URL."** (mark clearly as a gap / not yet real.)
|
||||||
|
|
||||||
|
**Stage 01 — TELOS**
|
||||||
|
- *Intention substrate* → **"The master list of mission, goals, problems, and current status — the yardstick everything downstream is measured against."**
|
||||||
|
|
||||||
|
**Stage 02 — Strategic session**
|
||||||
|
- *Design · ADRs · specs* → **"A human and a top-tier AI model figure out the approach and write down the decision plus what a finished result must prove."**
|
||||||
|
- *LLM Council* → **"For hard calls, several AI models answer independently, anonymously critique each other, and a 'chair' model synthesises one verdict — reduces any single model's bias."**
|
||||||
|
- *Autoresearch Council* → **"A parallel version of the same review that vets research findings before they're allowed through."**
|
||||||
|
|
||||||
|
**Stage 03 — Spec → Gitea issue**
|
||||||
|
- *Contract enforced* → **"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."**
|
||||||
|
- *Admission controller* → **"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it."**
|
||||||
|
- *var-go Oath* → **"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."**
|
||||||
|
|
||||||
|
**Stage 04 — Human dispatch gate**
|
||||||
|
- *Human triggers execution* → **"A person confirms the plan and its risk level, then releases the work — nothing runs until they do."**
|
||||||
|
- *Session-Dispatch bridge* → **"The approval flips a switch that hands the signed work order over to the agents to start execution."**
|
||||||
|
|
||||||
|
**Stage 05 — Execute · agentsquad**
|
||||||
|
- *Task API* → **"A request kicks off a job and hands back an id you can poll for progress."**
|
||||||
|
- *Executor + reviewer loop* → **"One agent does the work; a second, independent agent on a different model reviews it — so nothing marks its own homework."**
|
||||||
|
- *dma-cli · routing + scope* → **"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."**
|
||||||
|
- *assessor-loop ledger* → **"Every step is recorded in a tamper-evident log so the whole run can be audited afterwards."**
|
||||||
|
|
||||||
|
**Stage 06 — PR → CI** *(nodes generated live from the latest CI run — no authored nodes)*
|
||||||
|
- Live jobs render here; the plain framing for the column is: **"Automated tests and safety checks run on the proposed change, including a check that it truly satisfies the work order — only a clean pass moves on."**
|
||||||
|
|
||||||
|
**Stage 07 — CD → pod**
|
||||||
|
- *Deploy on green* → **"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)."** *(live deploy + Flux status also shown.)*
|
||||||
|
|
||||||
|
**Stage 08 — Loop back**
|
||||||
|
- *Close the loop* → **"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."**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Transitions — the key deliverable
|
||||||
|
|
||||||
|
For each arrow: *what moves the work forward, and what must be true for it to advance.*
|
||||||
|
These should be rendered **on the arrows themselves** (see §5). Today they are blank.
|
||||||
|
|
||||||
|
- **00 → 01 — "Does it matter to us?"**
|
||||||
|
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.
|
||||||
|
|
||||||
|
- **01 → 02 — "Worth a session?"**
|
||||||
|
A goal or problem on the board becomes the seed for a design session when it's decided
|
||||||
|
it's worth working on now. The goal is the input the session must trace back to.
|
||||||
|
|
||||||
|
- **02 → 03 — "Decision reached."**
|
||||||
|
Once the debate converges on a decision (and what "done" will mean), it advances only
|
||||||
|
when that thinking is written down as a concrete, testable specification — not while
|
||||||
|
the answer is still open.
|
||||||
|
|
||||||
|
- **03 → 04 — "Order written, sealed, agent-ready."**
|
||||||
|
Work 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 does not reach
|
||||||
|
the gate.
|
||||||
|
|
||||||
|
- **04 → 05 — "A human said go."**
|
||||||
|
This is 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.
|
||||||
|
|
||||||
|
- **05 → 06 — "Agents produced a change."**
|
||||||
|
Work 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.
|
||||||
|
|
||||||
|
- **06 → 07 — "All checks green."**
|
||||||
|
The change 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.
|
||||||
|
|
||||||
|
- **07 → 08 — "It's live."**
|
||||||
|
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."
|
||||||
|
|
||||||
|
- **08 → TELOS (feedback bus, dashed) — "What did we learn?"**
|
||||||
|
The scored outcome flows back into the mission board so goals, problems, and priorities
|
||||||
|
update. This is the loop that makes the pipeline a cycle rather than a line. Note per
|
||||||
|
PROJECT.md this arc is **partly manual today** and is an explicit improvement target —
|
||||||
|
the dashed styling should read as "aspirational / not fully automated," not just decorative.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Progressive-disclosure recommendations
|
||||||
|
|
||||||
|
**Default (Plain) layer — what everyone sees on load:**
|
||||||
|
- Each stage column shows: the **plain_title** as the headline, the technical title as a
|
||||||
|
smaller subtitle, and the one-line **plain_what** directly under it.
|
||||||
|
- Each node shows its **plain** one-liner as the primary text. The current `d` string is
|
||||||
|
hidden by default.
|
||||||
|
- Each arrow shows a short **transition label** (the bolded phrase from §4, e.g. "A human
|
||||||
|
said go", "All checks green") — this is the single biggest comprehension win and must
|
||||||
|
ship in the default layer, not behind a toggle.
|
||||||
|
|
||||||
|
**On hover / expand (per node):**
|
||||||
|
- Reveal the technical `d` text, the `tags`, and the pill's meaning.
|
||||||
|
- On the arrow, hovering expands the short label into the full "what must be true to
|
||||||
|
advance" sentence from §4.
|
||||||
|
|
||||||
|
**Plain ⇄ Technical toggle (global):**
|
||||||
|
- A single top-level switch, defaulting to **Plain**. Persist the choice (localStorage).
|
||||||
|
- Plain: plain_title headline, plain_what, plain node lines, short arrow labels. Proper
|
||||||
|
nouns (koala/iguana/agentsquad/TELOS) suppressed or shown only as a footnote.
|
||||||
|
- Technical: today's exact copy — titles, `d` strings, tags, substrate host specs — i.e.
|
||||||
|
the atlas as it exists now. Nothing is lost; the current view becomes "Technical."
|
||||||
|
- The toggle should crossfade in place, not reflow the whole layout, so a viewer can flip
|
||||||
|
back and forth and map plain↔technical on the same node.
|
||||||
|
|
||||||
|
**Visual cues for the currently-bare transitions:**
|
||||||
|
- Give every arrow a **label chip** sitting on the spine. Gate arrows (04→05 human, 06→07
|
||||||
|
CI) get a distinct treatment — a lock/shield glyph and a stronger colour — because those
|
||||||
|
are the governance moments the whole atlas exists to show.
|
||||||
|
- Make the **08 → TELOS feedback bus** visibly different (dashed + "partly manual" tag) so
|
||||||
|
its aspirational status is honest, matching PROJECT.md's dogfooding-honesty discipline.
|
||||||
|
- Add a small, always-visible **legend** keying the pill colours and the three gate types
|
||||||
|
(integrity / eligibility / correctness), since colour is currently unexplained.
|
||||||
|
- For **Stage 06** (authored-empty, generated live): when no live CI data is present, show
|
||||||
|
the plain_what placeholder ("Automated tests and safety checks run…") rather than an
|
||||||
|
empty column, so it never reads as "nothing happens here."
|
||||||
|
|
||||||
|
**Three-gate overlay (stretch, high value for the compliance audience):**
|
||||||
|
- A "show governance gates" toggle that highlights the three orthogonal gates on top of
|
||||||
|
the pipeline: integrity (03, Ed25519), eligibility (04/05, dispatch-allow), correctness
|
||||||
|
(06, Oath). This directly serves the "audit chain is the viz data" thesis for a
|
||||||
|
compliance/exec viewer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Prioritized punch list (top 8 by comprehension impact)
|
||||||
|
|
||||||
|
1. **Label every arrow with a plain "what must be true to advance" phrase** (§4). Biggest
|
||||||
|
miss, biggest win — turns a row of boxes into a story of gated flow. Default layer.
|
||||||
|
2. **Add a plain_what one-liner per stage** as the default column copy (§2), with the
|
||||||
|
technical title demoted to subtitle.
|
||||||
|
3. **Ship the Plain ⇄ Technical global toggle, defaulting to Plain**, persisting choice;
|
||||||
|
current copy becomes the Technical view (nothing thrown away).
|
||||||
|
4. **Rewrite node primary text to the plain lines** (§3); move existing `d` to hover/expand.
|
||||||
|
5. **Visually distinguish the two real gates (04 human, 06 CI)** with lock/shield glyphs and
|
||||||
|
stronger colour so the checkpoints read as checkpoints.
|
||||||
|
6. **Add a legend** keying pill colours and the three gate types — colour currently carries
|
||||||
|
meaning nobody can decode.
|
||||||
|
7. **Fix the Stage-06 empty-column problem**: show a plain placeholder when live CI data is
|
||||||
|
absent, so the CI gate never looks like a no-op.
|
||||||
|
8. **Make the 08→TELOS feedback bus honestly aspirational** (dashed + "partly manual" tag),
|
||||||
|
and suppress homelab proper nouns (koala/iguana/flamingo/agentsquad/TELOS) in Plain mode,
|
||||||
|
surfacing them only in Technical or a footnote.
|
||||||
+38
-27
@@ -7,40 +7,51 @@
|
|||||||
],
|
],
|
||||||
"ns": "Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council",
|
"ns": "Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council",
|
||||||
"stages": [
|
"stages": [
|
||||||
{"no":"STAGE 00","title":"Signals","path":"→ mathias/signals","nodes":[
|
{"no":"STAGE 00","short":"Notice","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",
|
||||||
{"t":"Applied AI Radar","d":"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).","tags":["cron · daily/weekly","→ signals #1–26+"]},
|
"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":"Manual capture","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]},
|
{"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":"Aspirational surfaces","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]}
|
{"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","path":"wiki/telos/","nodes":[
|
{"no":"STAGE 01","short":"Why","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/",
|
||||||
{"t":"Intention substrate","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=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_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","path":"claude.ai frontier + brain MCP","nodes":[
|
{"no":"STAGE 02","short":"Think","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",
|
||||||
{"t":"Design · ADRs · specs","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]},
|
"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":"🏛️ LLM Council","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":"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":"Autoresearch Council","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]}
|
{"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","path":"agent-ready contract","nodes":[
|
{"no":"STAGE 03","short":"Write order","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",
|
||||||
{"t":"Contract enforced","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]},
|
"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":"Admission controller","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]},
|
{"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":"⚖️ var-go Oath","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":"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","path":"the only checkpoint","nodes":[
|
{"no":"STAGE 04","short":"Human go","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",
|
||||||
{"t":"Human triggers execution","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true},
|
"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":"Session-Dispatch bridge","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","path":"koala · cmd/agentsquad-serve","nodes":[
|
{"no":"STAGE 05","short":"Build","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",
|
||||||
{"t":"Task API","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]},
|
"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":"Executor + reviewer loop","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true},
|
{"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":"dma-cli · routing + scope","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":"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":"assessor-loop ledger","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]}
|
{"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","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs","nodes":[]},
|
{"no":"STAGE 06","short":"Check","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",
|
||||||
{"no":"STAGE 07","cls":"cd","title":"CD → pod","path":"Flux GitOps → k3s","generate":"deploy-state","nodes":[
|
"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":"Deploy on green","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]}
|
{"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 08","cls":"telos","title":"Loop back","path":"→ TELOS (feedback bus)","nodes":[
|
{"no":"STAGE 07","short":"Ship","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",
|
||||||
{"t":"Close the loop","pill":"var(--violet)","d":"session_log + attestation → brain. Score deploy outcome vs originating goal. (arc partly manual — improvement target.)","tags":["continuous"]}
|
"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_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","short":"Learn","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_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"]}
|
||||||
]}
|
]}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,46 @@ func TestBuild_OverlaysCIStageNodesFromWorkflow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefault_HasPlainLayerAndTransitions(t *testing.T) {
|
||||||
|
a, err := atlas.Build(atlas.DataJSON, []byte("jobs:\n guard:\n a: 1\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build embedded atlas: %v", err)
|
||||||
|
}
|
||||||
|
for _, s := range a.Stages {
|
||||||
|
if s.PlainTitle == "" {
|
||||||
|
t.Fatalf("stage %s missing plain_title", s.No)
|
||||||
|
}
|
||||||
|
if s.TransLabel == "" {
|
||||||
|
t.Fatalf("stage %s missing trans_label", s.No)
|
||||||
|
}
|
||||||
|
if s.Generate == "ci-jobs" {
|
||||||
|
continue // nodes are generated live, no authored plain
|
||||||
|
}
|
||||||
|
for _, n := range s.Nodes {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuild_KeepsCIPlaceholderWhenNoJobs(t *testing.T) {
|
||||||
|
atlasJSON := []byte(`{"substrate":[],"stages":[
|
||||||
|
{"no":"STAGE 06","title":"PR → CI","generate":"ci-jobs","nodes":[{"t":"Automated checks","plain":"checks run here"}]}
|
||||||
|
]}`)
|
||||||
|
// A workflow with no jobs block must NOT error and must keep the placeholder.
|
||||||
|
a, err := atlas.Build(atlasJSON, []byte("name: cd\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build should tolerate a no-jobs workflow, got: %v", err)
|
||||||
|
}
|
||||||
|
if len(a.Stages[0].Nodes) != 1 || a.Stages[0].Nodes[0].Title != "Automated checks" {
|
||||||
|
t.Fatalf("placeholder not kept: %+v", a.Stages[0].Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuild_ErrorsOnBadAtlasJSON(t *testing.T) {
|
func TestBuild_ErrorsOnBadAtlasJSON(t *testing.T) {
|
||||||
if _, err := atlas.Build([]byte("{not json"), []byte("jobs:\n x:\n a: 1\n")); err == nil {
|
if _, err := atlas.Build([]byte("{not json"), []byte("jobs:\n x:\n a: 1\n")); err == nil {
|
||||||
t.Fatal("expected error on bad atlas JSON, got nil")
|
t.Fatal("expected error on bad atlas JSON, got nil")
|
||||||
|
|||||||
@@ -53,6 +53,70 @@ func HostsFromNodes(nodesJSON []byte) ([]Host, error) {
|
|||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flux is the reconciliation state of a Flux Kustomization.
|
||||||
|
type Flux struct {
|
||||||
|
Ready bool
|
||||||
|
Reason string
|
||||||
|
Revision string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FluxStatus parses a Flux Kustomization object into its reconcile state.
|
||||||
|
func FluxStatus(kustJSON []byte) (Flux, error) {
|
||||||
|
var k struct {
|
||||||
|
Status struct {
|
||||||
|
Conditions []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
} `json:"conditions"`
|
||||||
|
LastAppliedRevision string `json:"lastAppliedRevision"`
|
||||||
|
} `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(kustJSON, &k); err != nil {
|
||||||
|
return Flux{}, fmt.Errorf("parse kustomization: %w", err)
|
||||||
|
}
|
||||||
|
f := Flux{Revision: shortRev(k.Status.LastAppliedRevision)}
|
||||||
|
for _, c := range k.Status.Conditions {
|
||||||
|
if c.Type == "Ready" {
|
||||||
|
f.Ready = c.Status == "True"
|
||||||
|
f.Reason = c.Reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FluxNode renders the Flux reconcile state as a stage node.
|
||||||
|
func FluxNode(f Flux) Node {
|
||||||
|
if f.Ready {
|
||||||
|
return Node{
|
||||||
|
Title: "⟳ Flux · reconciled · " + f.Revision,
|
||||||
|
Pill: "var(--green)",
|
||||||
|
Tags: []string{"live · k8s"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Node{
|
||||||
|
Title: "⟳ Flux · " + f.Reason,
|
||||||
|
Pill: "var(--coral)",
|
||||||
|
Tags: []string{"live · k8s"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortRev turns a Flux revision "main@sha1:<full>" into "main@<short>".
|
||||||
|
func shortRev(rev string) string {
|
||||||
|
at := strings.Index(rev, "@")
|
||||||
|
if at < 0 {
|
||||||
|
return rev
|
||||||
|
}
|
||||||
|
branch, sha := rev[:at], rev[at+1:]
|
||||||
|
if c := strings.LastIndex(sha, ":"); c >= 0 {
|
||||||
|
sha = sha[c+1:]
|
||||||
|
}
|
||||||
|
if len(sha) > 7 {
|
||||||
|
sha = sha[:7]
|
||||||
|
}
|
||||||
|
return branch + "@" + sha
|
||||||
|
}
|
||||||
|
|
||||||
// Deploy is the live state of a Kubernetes Deployment.
|
// Deploy is the live state of a Kubernetes Deployment.
|
||||||
type Deploy struct {
|
type Deploy struct {
|
||||||
Image string
|
Image string
|
||||||
|
|||||||
@@ -98,6 +98,34 @@ func TestNamespaceSummary_CapsWithMore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFluxStatus_ParsesReadyReasonAndShortRevision(t *testing.T) {
|
||||||
|
k := []byte(`{"status":{
|
||||||
|
"conditions":[{"type":"Ready","status":"True","reason":"ReconciliationSucceeded","message":"Applied revision: main@sha1:7a51d1d13944"}],
|
||||||
|
"lastAppliedRevision":"main@sha1:7a51d1d13944c481b04fc434d9a2aca2a25632e4"}}`)
|
||||||
|
|
||||||
|
f, err := atlas.FluxStatus(k)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FluxStatus: %v", err)
|
||||||
|
}
|
||||||
|
if !f.Ready || f.Reason != "ReconciliationSucceeded" || f.Revision != "main@7a51d1d" {
|
||||||
|
t.Fatalf("flux = %+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFluxNode_GreenWhenReadyCoralWhenNot(t *testing.T) {
|
||||||
|
ready := atlas.FluxNode(atlas.Flux{Ready: true, Reason: "ReconciliationSucceeded", Revision: "main@7a51d1d"})
|
||||||
|
if ready.Pill != "var(--green)" {
|
||||||
|
t.Fatalf("ready pill = %q", ready.Pill)
|
||||||
|
}
|
||||||
|
if ready.Title != "⟳ Flux · reconciled · main@7a51d1d" {
|
||||||
|
t.Fatalf("title = %q", ready.Title)
|
||||||
|
}
|
||||||
|
notReady := atlas.FluxNode(atlas.Flux{Ready: false, Reason: "BuildFailed"})
|
||||||
|
if notReady.Pill != "var(--coral)" {
|
||||||
|
t.Fatalf("not-ready pill = %q", notReady.Pill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeployState_ParsesImageAndReplicas(t *testing.T) {
|
func TestDeployState_ParsesImageAndReplicas(t *testing.T) {
|
||||||
dep := []byte(`{"spec":{"replicas":2,"template":{"spec":{"containers":[
|
dep := []byte(`{"spec":{"replicas":2,"template":{"spec":{"containers":[
|
||||||
{"name":"cad-atlas","image":"localhost:5000/cad-atlas:3ff922a"}]}}},
|
{"name":"cad-atlas","image":"localhost:5000/cad-atlas:3ff922a"}]}}},
|
||||||
|
|||||||
+17
-6
@@ -11,9 +11,12 @@ type Host struct {
|
|||||||
Spec string `json:"k"`
|
Spec string `json:"k"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node is a card within a stage.
|
// 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 {
|
type Node struct {
|
||||||
Title string `json:"t"`
|
Title string `json:"t"`
|
||||||
|
PlainTitle string `json:"plain_t,omitempty"`
|
||||||
|
Plain string `json:"plain,omitempty"`
|
||||||
Desc string `json:"d,omitempty"`
|
Desc string `json:"d,omitempty"`
|
||||||
Pill string `json:"pill,omitempty"`
|
Pill string `json:"pill,omitempty"`
|
||||||
Cls string `json:"cls,omitempty"`
|
Cls string `json:"cls,omitempty"`
|
||||||
@@ -22,14 +25,22 @@ type Node struct {
|
|||||||
Gate bool `json:"gate,omitempty"`
|
Gate bool `json:"gate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage is one column of the pipeline. When Generate is set, its Nodes are
|
// Stage is one column of the pipeline. PlainTitle/Plain are the plain-language
|
||||||
// derived from a source at Build time rather than taken from the authored data.
|
// default layer; Title/Path/Nodes[].Desc are the technical layer. TransLabel/
|
||||||
|
// Trans annotate the outgoing transition (the arrow to the next stage): what
|
||||||
|
// moves work forward and what must be true to advance. When Generate is set,
|
||||||
|
// Nodes are derived from a live source at Build time.
|
||||||
type Stage struct {
|
type Stage struct {
|
||||||
No string `json:"no"`
|
No string `json:"no"`
|
||||||
|
Short string `json:"short,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
PlainTitle string `json:"plain_title,omitempty"`
|
||||||
|
Plain string `json:"plain,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Cls string `json:"cls,omitempty"`
|
Cls string `json:"cls,omitempty"`
|
||||||
Generate string `json:"generate,omitempty"`
|
Generate string `json:"generate,omitempty"`
|
||||||
|
TransLabel string `json:"trans_label,omitempty"`
|
||||||
|
Trans string `json:"trans,omitempty"`
|
||||||
Nodes []Node `json:"nodes"`
|
Nodes []Node `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +66,12 @@ func Build(atlasJSON, workflow []byte) (Atlas, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
jobs, err := JobsFromWorkflow(workflow)
|
jobs, err := JobsFromWorkflow(workflow)
|
||||||
if err != nil {
|
if err != nil || len(jobs) == 0 {
|
||||||
return Atlas{}, fmt.Errorf("stage %s: %w", a.Stages[i].No, err)
|
continue // no CI job list available — keep the authored plain placeholder
|
||||||
}
|
}
|
||||||
nodes := make([]Node, 0, len(jobs))
|
nodes := make([]Node, 0, len(jobs))
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
nodes = append(nodes, Node{Title: j})
|
nodes = append(nodes, Node{Title: j, Plain: "An automated check that must pass."})
|
||||||
}
|
}
|
||||||
a.Stages[i].Nodes = nodes
|
a.Stages[i].Nodes = nodes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,20 +73,20 @@ func RecentRuns(tasksJSON []byte, n int) ([]RunDot, error) {
|
|||||||
|
|
||||||
// aggregateState folds per-job outcomes into a run outcome.
|
// aggregateState folds per-job outcomes into a run outcome.
|
||||||
func aggregateState(jobs []Job) string {
|
func aggregateState(jobs []Job) string {
|
||||||
allSucceeded := true
|
pending := false
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
switch j.State() {
|
switch j.State() {
|
||||||
case "failure", "cancelled", "error":
|
case "failure", "cancelled", "error":
|
||||||
return "failure"
|
return "failure"
|
||||||
case "success":
|
case "success", "skipped": // completed OK — skipped (e.g. deploy on a tag push) doesn't block
|
||||||
default:
|
default:
|
||||||
allSucceeded = false
|
pending = true // running / in_progress / waiting / queued / unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allSucceeded {
|
if pending {
|
||||||
return "success"
|
|
||||||
}
|
|
||||||
return "running"
|
return "running"
|
||||||
|
}
|
||||||
|
return "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestRunJobs parses a Gitea `/actions/tasks` response (per-job entries,
|
// LatestRunJobs parses a Gitea `/actions/tasks` response (per-job entries,
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ func TestRecentRuns_GroupsRunsNewestFirstWithAggregateState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunState_SkippedJobsCountAsOK(t *testing.T) {
|
||||||
|
// A tag-push run skips the deploy job; the run still succeeded.
|
||||||
|
s := atlas.RunSummary{Jobs: []atlas.Job{
|
||||||
|
{Status: "skipped"}, {Status: "success"}, {Status: "success"},
|
||||||
|
}}
|
||||||
|
if s.State() != "success" {
|
||||||
|
t.Fatalf("skipped+success run state = %q, want success", s.State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
||||||
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
||||||
t.Fatal("expected error on empty, got nil")
|
t.Fatal("expected error on empty, got nil")
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ func Deployment() ([]byte, error) {
|
|||||||
return get("/apis/apps/v1/namespaces/cad-atlas/deployments/cad-atlas")
|
return get("/apis/apps/v1/namespaces/cad-atlas/deployments/cad-atlas")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FluxKustomization returns the raw JSON for the Flux `apps` Kustomization that
|
||||||
|
// reconciles this repo's manifests.
|
||||||
|
func FluxKustomization() ([]byte, error) {
|
||||||
|
return get("/apis/kustomize.toolkit.fluxcd.io/v1/namespaces/flux-system/kustomizations/apps")
|
||||||
|
}
|
||||||
|
|
||||||
func get(path string) ([]byte, error) {
|
func get(path string) ([]byte, error) {
|
||||||
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
|
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||||
if host == "" || port == "" {
|
if host == "" || port == "" {
|
||||||
|
|||||||
+14
-2
@@ -56,11 +56,17 @@ func NewHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ld.deploy != nil || ld.flux != nil {
|
||||||
|
var live []atlas.Node
|
||||||
if ld.deploy != nil {
|
if ld.deploy != nil {
|
||||||
node := atlas.DeployNode(*ld.deploy)
|
live = append(live, atlas.DeployNode(*ld.deploy))
|
||||||
|
}
|
||||||
|
if ld.flux != nil {
|
||||||
|
live = append(live, atlas.FluxNode(*ld.flux))
|
||||||
|
}
|
||||||
for i := range a.Stages {
|
for i := range a.Stages {
|
||||||
if a.Stages[i].Generate == "deploy-state" {
|
if a.Stages[i].Generate == "deploy-state" {
|
||||||
a.Stages[i].Nodes = append([]atlas.Node{node}, a.Stages[i].Nodes...)
|
a.Stages[i].Nodes = append(live, a.Stages[i].Nodes...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +82,7 @@ type liveOverlayData struct {
|
|||||||
ns string
|
ns string
|
||||||
run *atlas.RunSummary
|
run *atlas.RunSummary
|
||||||
deploy *atlas.Deploy
|
deploy *atlas.Deploy
|
||||||
|
flux *atlas.Flux
|
||||||
timeline []atlas.RunDot
|
timeline []atlas.RunDot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +128,11 @@ func liveOverlay() liveOverlayData {
|
|||||||
d.deploy = &dep
|
d.deploy = &dep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if raw, err := cluster.FluxKustomization(); err == nil {
|
||||||
|
if f, err := atlas.FluxStatus(raw); err == nil {
|
||||||
|
d.flux = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
liveCache = d
|
liveCache = d
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,47 @@
|
|||||||
display:inline-flex;align-items:center;justify-content:center;
|
display:inline-flex;align-items:center;justify-content:center;
|
||||||
font-size:9px;color:#08121f;font-weight:600}
|
font-size:9px;color:#08121f;font-weight:600}
|
||||||
|
|
||||||
|
/* progressive disclosure */
|
||||||
|
.tech-sub{color:var(--dim);font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;margin:1px 0 5px}
|
||||||
|
.plain-what{color:var(--ink);font-size:12.5px;line-height:1.45;margin-bottom:2px;opacity:.92}
|
||||||
|
.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}
|
||||||
|
@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}
|
||||||
|
.legend{display:flex;gap:16px;flex-wrap:wrap;align-items:center;
|
||||||
|
padding:7px 22px;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
|
||||||
|
.legend .lbl{letter-spacing:1.5px;margin-right:2px}
|
||||||
|
.legend .lg{display:inline-flex;align-items:center;gap:6px}
|
||||||
|
.legend .lg i{width:11px;height:11px;border-radius:3px;display:inline-block;border:1px solid rgba(0,0,0,.35)}
|
||||||
|
.legend .dash{width:18px;border-top:2px dashed var(--violet);display:inline-block}
|
||||||
|
/* overview rail — see the whole 9-stage shape + jump to any stage */
|
||||||
|
.rail{display:flex;gap:4px;flex-wrap:wrap;align-items:center;
|
||||||
|
padding:8px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
|
||||||
|
.rail .lbl{color:var(--dim);letter-spacing:1.5px;margin-right:4px;font-size:11px}
|
||||||
|
.railchip{font:inherit;font-size:11px;cursor:pointer;color:var(--ink);
|
||||||
|
background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:4px 9px}
|
||||||
|
.railchip:hover{border-color:var(--blue)}
|
||||||
|
.railchip b{color:var(--dim);font-weight:600;margin-right:3px}
|
||||||
|
.railchip.gate{border-color:rgba(245,185,66,.5)} .railchip.telos{border-color:rgba(155,140,255,.5)}
|
||||||
|
.railchip.exec{border-color:rgba(255,122,92,.5)} .railchip.cd{border-color:rgba(74,208,122,.5)}
|
||||||
|
.railarr{color:var(--dim);font-size:10px}
|
||||||
|
/* Plain view: drop decorative node pills so colour is reserved for live status (keyed in the legend) */
|
||||||
|
body.plain .node .pill{display:none}
|
||||||
|
|
||||||
.scroll{overflow-x:auto;padding:24px 22px 20px}
|
.scroll{overflow-x:auto;padding:24px 22px 20px}
|
||||||
.track{position:relative;display:flex;align-items:flex-start;min-width:max-content}
|
.track{position:relative;display:flex;align-items:flex-start;min-width:max-content}
|
||||||
svg.spine{position:absolute;left:0;top:0;z-index:0;pointer-events:none;overflow:visible}
|
svg.spine{position:absolute;left:0;top:0;z-index:0;pointer-events:none;overflow:visible}
|
||||||
@@ -123,6 +164,7 @@
|
|||||||
<h1><b>CAD</b> Atlas · From Signal to Pod</h1>
|
<h1><b>CAD</b> Atlas · From Signal to Pod</h1>
|
||||||
<span class="sub mono">one human gate · everything up- and downstream is agents · <em id="ver">dev</em></span>
|
<span class="sub mono">one human gate · everything up- and downstream is agents · <em id="ver">dev</em></span>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<button id="mode" class="on">View · <span id="modeState">Plain</span></button>
|
||||||
<button id="replay"><span class="dot"></span> Replay</button>
|
<button id="replay"><span class="dot"></span> Replay</button>
|
||||||
<button id="slowmo">Slow-mo · <span id="slowState">off</span></button>
|
<button id="slowmo">Slow-mo · <span id="slowState">off</span></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,6 +172,15 @@
|
|||||||
|
|
||||||
<div class="substrate" id="substrate"><span class="lbl mono">SUBSTRATE</span></div>
|
<div class="substrate" id="substrate"><span class="lbl mono">SUBSTRATE</span></div>
|
||||||
<div class="tl mono" id="timeline"></div>
|
<div class="tl mono" id="timeline"></div>
|
||||||
|
<div class="legend mono">
|
||||||
|
<span class="lbl">KEY</span>
|
||||||
|
<span class="lg"><i style="background:var(--green)"></i>passed / ready</span>
|
||||||
|
<span class="lg"><i style="background:var(--amber)"></i>running / waiting</span>
|
||||||
|
<span class="lg"><i style="background:var(--coral)"></i>failed / blocked</span>
|
||||||
|
<span class="lg"><i style="background:var(--gold)"></i>governance gate — must pass to advance</span>
|
||||||
|
<span class="lg"><span class="dash"></span> feedback loop · partly manual</span>
|
||||||
|
</div>
|
||||||
|
<div class="rail mono" id="rail"></div>
|
||||||
|
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="track" id="track">
|
<div class="track" id="track">
|
||||||
@@ -145,6 +196,7 @@
|
|||||||
stroke-dasharray="5 5" opacity=".7"></path>
|
stroke-dasharray="5 5" opacity=".7"></path>
|
||||||
<text id="loopLbl" fill="#9b8cff" font-size="11"
|
<text id="loopLbl" fill="#9b8cff" font-size="11"
|
||||||
font-family="ui-monospace,monospace" opacity=".85"></text>
|
font-family="ui-monospace,monospace" opacity=".85"></text>
|
||||||
|
<g id="translabels"></g>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="pulse" id="pulse"></div>
|
<div class="pulse" id="pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +215,7 @@
|
|||||||
// empty and are filled by init()'s fetch; on failure the page shows an error
|
// empty and are filled by init()'s fetch; on failure the page shows an error
|
||||||
// banner rather than stale inline data.
|
// banner rather than stale inline data.
|
||||||
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[];
|
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[];
|
||||||
|
let MODE = localStorage.getItem('atlas-mode') || 'plain'; // 'plain' | 'technical'
|
||||||
|
|
||||||
const track=document.getElementById('track');
|
const track=document.getElementById('track');
|
||||||
let stageEls=[];
|
let stageEls=[];
|
||||||
@@ -173,19 +226,50 @@ function renderAtlas(){
|
|||||||
el.innerHTML=`<b>${h.n}</b><span class="k mono">${h.k}</span>`;sub.appendChild(el);});
|
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);
|
const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh);
|
||||||
stageEls=[];
|
stageEls=[];
|
||||||
track.querySelectorAll('.stage').forEach(el=>el.remove());
|
track.querySelectorAll('.stage,.trans-row').forEach(el=>el.remove());
|
||||||
STAGES.forEach(s=>{
|
const plain = MODE==='plain';
|
||||||
const st=document.createElement('div');st.className='stage '+s.cls;
|
STAGES.forEach((s,idx)=>{
|
||||||
let h=`<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
|
const st=document.createElement('div');st.className='stage '+(s.cls||'');
|
||||||
s.nodes.forEach(n=>{
|
const head = plain
|
||||||
|
? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+
|
||||||
|
`<div class="tech-sub">${s.title}</div>`+(s.plain?`<div class="plain-what">${s.plain}</div>`:'')
|
||||||
|
: `<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
|
||||||
|
let h = `<div class="stagehead">${head}</div>`;
|
||||||
|
(s.nodes||[]).forEach(n=>{
|
||||||
const pill=n.pill?`<span class="pill" style="background:${n.pill}"></span>`:'';
|
const pill=n.pill?`<span class="pill" style="background:${n.pill}"></span>`:'';
|
||||||
let inner=`<div class="t">${pill}${n.t}</div><div class="d">${n.d}</div>`;
|
const body = plain ? (n.plain||n.d||'') : (n.d||'');
|
||||||
|
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.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>`;
|
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>`;
|
||||||
|
}
|
||||||
if(n.gate)inner+=`<div class="gatebtns mono"><div class="g ok">✓ approve</div><div class="g no">✕ reject</div></div>`;
|
if(n.gate)inner+=`<div class="gatebtns mono"><div class="g ok">✓ approve</div><div class="g no">✕ reject</div></div>`;
|
||||||
h+=`<div class="node ${n.cls||''}">${inner}</div>`;
|
h+=`<div class="node ${n.cls||''}">${inner}</div>`;
|
||||||
});
|
});
|
||||||
st.innerHTML=h;track.appendChild(st);stageEls.push(st);
|
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">${(idx===4||idx===6)?'🔒 ':''}${s.trans_label}</span>${s.trans?' '+s.trans:''}`;
|
||||||
|
track.appendChild(tr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderRail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRail(){
|
||||||
|
const rail=document.getElementById('rail');
|
||||||
|
rail.innerHTML='<span class="lbl">PIPELINE</span>';
|
||||||
|
STAGES.forEach((s,i)=>{
|
||||||
|
const c=document.createElement('button');
|
||||||
|
c.className='railchip'+(s.cls?' '+s.cls:'');
|
||||||
|
c.innerHTML=`<b>${(s.no||'').replace('STAGE ','')}</b>${s.short||s.plain_title||s.title}`;
|
||||||
|
c.onclick=()=>{ if(stageEls[i]) stageEls[i].scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'}); };
|
||||||
|
rail.appendChild(c);
|
||||||
|
if(i<STAGES.length-1){const a=document.createElement('span');a.className='railarr';a.textContent='→';rail.appendChild(a);}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,13 +291,16 @@ function renderTimeline(){
|
|||||||
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
|
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
|
||||||
loopPath=document.getElementById('loopPath'), loopLbl=document.getElementById('loopLbl'),
|
loopPath=document.getElementById('loopPath'), loopLbl=document.getElementById('loopLbl'),
|
||||||
pulse=document.getElementById('pulse');
|
pulse=document.getElementById('pulse');
|
||||||
const RAILY=70;
|
let RAILY=70;
|
||||||
let cs=[],loopY=0,spineLen=0,loopLen=0,slow=false,raf=null,t0=null,mobile=false;
|
let cs=[],loopY=0,spineLen=0,loopLen=0,slow=false,raf=null,t0=null,mobile=false;
|
||||||
|
|
||||||
function build(){
|
function build(){
|
||||||
mobile=window.matchMedia('(max-width:820px)').matches;
|
mobile=window.matchMedia('(max-width:820px)').matches;
|
||||||
if(mobile)return;
|
if(mobile)return;
|
||||||
cs=stageEls.map(s=>s.offsetLeft+s.offsetWidth/2);
|
cs=stageEls.map(s=>s.offsetLeft+s.offsetWidth/2);
|
||||||
|
// spine sits in the band between the (uniform) stage headers and the first node
|
||||||
|
const firstTops=stageEls.map(s=>{const n=s.querySelector('.node');return n?s.offsetTop+n.offsetTop:120;});
|
||||||
|
RAILY=Math.max(66, Math.min(...firstTops)-16);
|
||||||
const maxBottom=Math.max(...stageEls.map(s=>s.offsetTop+s.offsetHeight));
|
const maxBottom=Math.max(...stageEls.map(s=>s.offsetTop+s.offsetHeight));
|
||||||
loopY=maxBottom+40;
|
loopY=maxBottom+40;
|
||||||
spine.setAttribute('width',track.scrollWidth);
|
spine.setAttribute('width',track.scrollWidth);
|
||||||
@@ -223,7 +310,22 @@ function build(){
|
|||||||
const lastX=cs[cs.length-1], telosX=cs[1];
|
const lastX=cs[cs.length-1], telosX=cs[1];
|
||||||
loopPath.setAttribute('d',`M ${lastX} ${RAILY} L ${lastX} ${loopY} L ${telosX} ${loopY} L ${telosX} ${RAILY}`);
|
loopPath.setAttribute('d',`M ${lastX} ${RAILY} L ${lastX} ${loopY} L ${telosX} ${loopY} L ${telosX} ${RAILY}`);
|
||||||
loopLbl.setAttribute('x',(telosX+lastX)/2-90);loopLbl.setAttribute('y',loopY-8);
|
loopLbl.setAttribute('x',(telosX+lastX)/2-90);loopLbl.setAttribute('y',loopY-8);
|
||||||
loopLbl.textContent='feedback bus · outcome → goal';
|
loopLbl.textContent=(STAGES[8]&&STAGES[8].trans_label?STAGES[8].trans_label+' · ':'')+'feedback bus · partly manual today';
|
||||||
|
// transition labels on the spine — the "what must be true to advance" story.
|
||||||
|
// Always visible (default layer). Gate hops (04→05 human, 06→07 CI) in gold.
|
||||||
|
const tg=document.getElementById('translabels'); tg.innerHTML='';
|
||||||
|
for(let i=0;i<cs.length-1;i++){
|
||||||
|
const s=STAGES[i]; if(!s||!s.trans_label) continue;
|
||||||
|
const gate = (i===4||i===6);
|
||||||
|
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
|
||||||
|
t.setAttribute('x',(cs[i]+cs[i+1])/2);t.setAttribute('y',RAILY-9);
|
||||||
|
t.setAttribute('text-anchor','middle');
|
||||||
|
t.setAttribute('class',gate?'translabel translabel-gate':'translabel');
|
||||||
|
t.textContent=(gate?'🔒 ':'')+s.trans_label;
|
||||||
|
const ttl=document.createElementNS('http://www.w3.org/2000/svg','title');
|
||||||
|
ttl.textContent=s.trans||''; t.appendChild(ttl);
|
||||||
|
tg.appendChild(t);
|
||||||
|
}
|
||||||
spineLen=spinePath.getTotalLength();loopLen=loopPath.getTotalLength();
|
spineLen=spinePath.getTotalLength();loopLen=loopPath.getTotalLength();
|
||||||
}
|
}
|
||||||
function run(ts){
|
function run(ts){
|
||||||
@@ -246,6 +348,16 @@ function replay(){cancelAnimationFrame(raf);t0=null;build();if(!mobile)raf=reque
|
|||||||
document.getElementById('replay').onclick=replay;
|
document.getElementById('replay').onclick=replay;
|
||||||
document.getElementById('slowmo').onclick=e=>{slow=!slow;e.currentTarget.classList.toggle('on',slow);
|
document.getElementById('slowmo').onclick=e=>{slow=!slow;e.currentTarget.classList.toggle('on',slow);
|
||||||
document.getElementById('slowState').textContent=slow?'on':'off';replay();};
|
document.getElementById('slowState').textContent=slow?'on':'off';replay();};
|
||||||
|
function applyMode(){
|
||||||
|
document.getElementById('modeState').textContent = MODE==='plain'?'Plain':'Technical';
|
||||||
|
document.getElementById('mode').classList.toggle('on', MODE==='plain');
|
||||||
|
document.body.classList.toggle('plain', MODE==='plain');
|
||||||
|
}
|
||||||
|
document.getElementById('mode').onclick=()=>{
|
||||||
|
MODE = MODE==='plain' ? 'technical' : 'plain';
|
||||||
|
localStorage.setItem('atlas-mode',MODE);
|
||||||
|
applyMode(); renderAtlas(); replay();
|
||||||
|
};
|
||||||
window.addEventListener('resize',()=>{clearTimeout(window._r);window._r=setTimeout(replay,150);});
|
window.addEventListener('resize',()=>{clearTimeout(window._r);window._r=setTimeout(replay,150);});
|
||||||
async function init(){
|
async function init(){
|
||||||
try{
|
try{
|
||||||
@@ -265,6 +377,7 @@ async function init(){
|
|||||||
'<div class="path mono">/api/atlas.json failed to load</div></div>');
|
'<div class="path mono">/api/atlas.json failed to load</div></div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
applyMode();
|
||||||
renderAtlas();
|
renderAtlas();
|
||||||
renderTimeline();
|
renderTimeline();
|
||||||
replay();
|
replay();
|
||||||
|
|||||||
Reference in New Issue
Block a user