6 Commits
Author SHA1 Message Date
mathiasandClaude Sonnet 5 346037c5c8 docs(oath): correct stale Executor framing after swedsl#27 closed
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 4s
CD / var-go/oath (push) Has been skipped
CD / Build & Import (push) Successful in 13s
CD / Deploy via GitOps (push) Has been skipped
swedsl#27 (var-go strategic-fit ADR) closed 2026-07-18 and killed the
Executor/Reviewer path entirely — var-go is gate-only by design, each
consuming repo supplies its own candidate. The real blocker for
enforcement here is swedsl/oath's non-importable module path
(swedsl#35), not a nonexistent Executor. Corrects PROJECT.md,
INCEPTION-OATH.md, and the cd.yml comment to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:16:53 +02:00
mathiasandClaude Sonnet 5 f7a0281ca2 feat(ci): wire var-go/oath gate into CI (#1)
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / var-go/oath (push) Has been skipped
CD / Build & Import (push) Successful in 13s
CD / Deploy via GitOps (push) Has been skipped
Adds an oath job to cd.yml: on pull_request, checks out swedsl (the
vargo-gate source — its oath submodule isn't go-installable, module
path isn't a real import path) and runs cmd/vargo-gate against this
repo's linked issue, posting a var-go/oath commit status.

Deliberately NOT required by branch protection: vargo-gate's candidate
is still a hardcoded toy self-test registry (swedsl's own #9 fixture),
not a real PR-diff checker, so it fails closed against any real oath
until swedsl ships an Executor (swedsl#27). Requiring it now would
permanently block every cad-atlas PR. Disclosed in the CI config
comment, PROJECT.md, and docs/INCEPTION-OATH.md (honest-stub
discipline).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:10:25 +02:00
mathiasandClaude Opus 4.8 60cc8894a2 feat(atlas): UX — overview rail + gate glyphs + pill reconcile (#3,#4,#5)
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 4s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Has been skipped
Closes the UX sprint's authored layer.
#3 Horizontal-scroll blindness → an always-visible "PIPELINE" overview rail:
9 colour-coded chips (00 Notice → 08 Learn) showing the whole shape at a glance,
each click-to-scroll to its stage. First-timers now see the arc + that more exists.
#4 Real gate glyph — 🔒 on the two checkpoint transitions (human @04, CI @06),
desktop spine + mobile rows.
#5 Reconcile legend with Plain: decorative node pills hidden in Plain, so colour
is reserved for live status (green/amber/coral) — exactly what the legend keys.
(#6 attached chips/grammar: the question-vs-statement label mix is meaningful —
decisions vs milestones — kept; SVG chip background skipped as low-value.)

New Stage.short (guarded by JSON validity via existing tests). build/vet/lint(0)/
test green; desktop Plain screenshot-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:46:08 +02:00
mathiasandClaude Opus 4.8 863c4c964b feat(atlas): UX — plain node titles + mobile transition rows (re-review #1,#2)
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 4s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Has been skipped
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) <noreply@anthropic.com>
2026-07-20 10:14:53 +02:00
mathiasandClaude Opus 4.8 ac086b65b9 fix(atlas): UX polish — stage-06 plain nodes + shorter arrow labels (#7)
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 4s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Has been skipped
Stage 06 no longer renders bare in Plain: Build is now tolerant of a jobless/
absent workflow (keeps an authored plain placeholder instead of erroring), and
generated CI job nodes carry a plain hint. Shortened the two overlong transition
labels ("Order written, sealed, agent-ready" → "Sealed & agent-ready",
"Agents produced a change" → "Change proposed") so they stop clipping.

Build tolerance test-first. build/vet/lint(0)/test green; screenshot-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:46:42 +02:00
mathiasandClaude Opus 4.8 d992ca4a01 feat(atlas): UX polish — legend + feedback-bus honesty + Plain declutter (#6, #8)
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 4s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Has been skipped
#6: always-visible legend keying the live-status colours (passed/running/failed),
the governance-gate colour, and the dashed feedback loop.
#8: the 08→TELOS loop is labelled "partly manual today" (honest, per PROJECT.md);
Plain view hides the jargon-dense substrate ribbon + technical footer, so a
non-technical viewer isn't shown koala/iguana/ns/assessor-loop proper nouns —
they remain in Technical view.

Frontend/CSS only; verified by screenshot. build/test/lint green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:40:46 +02:00
8 changed files with 431 additions and 57 deletions
+10 -3
View File
@@ -54,9 +54,16 @@ assessor-loop ledger) → `06 PR → CI` (go test/vet/lint/govulncheck + **var-g
This repo is built *through* the workflow it depicts. It is `dispatch-allow`-enabled, and its This repo is built *through* the workflow it depicts. It is `dispatch-allow`-enabled, and its
own build increments are governed by a **var-go Oath** embedded in their spec issues (see the own build increments are governed by a **var-go Oath** embedded in their spec issues (see the
Stage-03 tracking issue). Bootstrapping honesty (per swedsl honest-stub discipline): the Oath is Stage-03 tracking issue). Bootstrapping honesty (per swedsl honest-stub discipline): `cmd/vargo-gate`
**defined** but `cmd/vargo-gate` is **not yet wired** into this repo's CI — until it is, the Oath is wired into `.gitea/workflows/cd.yml`'s `oath` job (issue #1, verified with a real PR) — it runs
is advisory here. Wiring it is a first tracked task; disclosed in code, this doc, and CI config. on every pull_request, fetches the linked issue's oath, and posts a `var-go/oath` commit status.
But its candidate is still swedsl's hardcoded toy self-test registry, not cad-atlas's own — it
fails closed against any real oath. var-go's Executor/Reviewer path is **not** the blocker (that
was explicitly killed by swedsl's ADR-0002, swedsl#27 — var-go is gate-only by design, each
consumer supplies its own candidate). The real blocker is `swedsl/oath`'s import path
(`swedsl#35`); once fixed, cad-atlas writes its own candidate (`#8`). The status is **not**
required by branch protection, so it can't block merges yet. Disclosed in the CI config comment,
this doc, and `docs/INCEPTION-OATH.md`.
## Brain references (source of truth — `brain_get <path>`) ## Brain references (source of truth — `brain_get <path>`)
+45
View File
@@ -53,6 +53,51 @@ jobs:
- name: Run checks - name: Run checks
run: task check run: task check
oath:
name: var-go/oath
needs: guard
# Only a real pull_request event carries a linked-issue oath to gate (mirrors
# swedsl's own oath job, .gitea/workflows/ci.yml). v1 simplification (swedsl#30):
# the oath issue number is the PR's OWN number.
#
# DISCLOSED LIMITATION (honest-stub discipline, see docs/INCEPTION-OATH.md S3 and
# knowledge/swedsl-vargo-sprint1-enforcement-teeth-verdict.md): cmd/vargo-gate's
# candidate is a hardcoded toy self-test registry (swedsl's own #9 fixture
# vocabulary), not a real PR-diff checker. It will fail closed against any oath
# that isn't that toy vocabulary — which is every real oath, including this repo's
# own #1. A red or green "var-go/oath" status here currently proves the WIRING
# (fetch issue -> gate -> post commit status) runs end-to-end on a real PR, not
# that the PR satisfies its linked issue's oath. Deliberately NOT required by
# branch protection until cad-atlas has its own candidate matching its real oath
# vocabulary (#8, blocked on swedsl/oath's import path, swedsl#35) — making it
# required now would permanently block every cad-atlas PR.
if: needs.guard.outputs.is_template != 'true' && github.event_name == 'pull_request'
runs-on: self-hosted
steps:
- name: Checkout swedsl (var-go source — not go-installable, module path isn't a real import path)
uses: actions/checkout@v4
with:
repository: mathias/swedsl
path: swedsl
token: ${{ secrets.DMABE_GITEA_API_TOKEN }}
- uses: actions/setup-go@v5
with:
go-version-file: swedsl/oath/go.mod
cache: false
- name: Run vargo-gate (fetch -> gate -> post status against this PR)
working-directory: swedsl/oath
env:
VARGO_GITEA_BASEURL: ${{ github.server_url }}
VARGO_GITEA_OWNER: ${{ github.repository_owner }}
VARGO_GITEA_REPO: cad-atlas
VARGO_GITEA_ISSUE: ${{ github.event.pull_request.number }}
VARGO_GITEA_SHA: ${{ github.event.pull_request.head.sha }}
run: |
export DMABE_GITEA_API_TOKEN='${{ secrets.DMABE_GITEA_API_TOKEN }}'
go run ./cmd/vargo-gate
build: build:
name: Build & Import name: Build & Import
needs: [guard, check] needs: [guard, check]
+7 -6
View File
@@ -3,8 +3,9 @@
The acceptance contract for standing up cad-atlas. The sprint is finalized only when this The acceptance contract for standing up cad-atlas. The sprint is finalized only when this
Oath holds. Methodology: brain `wiki/homelab/decisions/inception-sprint-and-oath.md`. Oath holds. Methodology: brain `wiki/homelab/decisions/inception-sprint-and-oath.md`.
> **Status of enforcement:** this Oath is currently **advisory** (human-verified). Machine > **Status of enforcement:** this Oath is currently **advisory** (human-verified). `var-go/oath`
> enforcement via `var-go/oath` is deferred — see the honesty rule below and issue #1. > is wired (issue #1) and runs on every PR, but its candidate is a toy self-test — it fails closed
> against any real oath and is not required by branch protection. See the honesty rule below.
## General clauses (any inception sprint) ## General clauses (any inception sprint)
@@ -24,7 +25,7 @@ Oath holds. Methodology: brain `wiki/homelab/decisions/inception-sprint-and-oath
|---|--------|--------|----------| |---|--------|--------|----------|
| S1 | Atlas served at `/`, renders all 9 stages signal→pod | ✅ | `internal/web/handler.go` + `static/cad-atlas.html` | | S1 | Atlas served at `/`, renders all 9 stages signal→pod | ✅ | `internal/web/handler.go` + `static/cad-atlas.html` |
| S2 | Oath covered in the viz (stages 03 + 06) | ✅ | var-go Oath nodes in the atlas | | S2 | Oath covered in the viz (stages 03 + 06) | ✅ | var-go Oath nodes in the atlas |
| S3 | `var-go/oath` enforces cad-atlas's own PRs | ⏸ **deferred → #1** | var-go v1 candidate is a toy self-test; module not cross-repo consumable. See honesty rule. | | S3 | `var-go/oath` enforces cad-atlas's own PRs | ⏸ **wired, not enforcing → #8** | `oath` job runs + posts status (#1, verified with a real PR). Not branch-protection-required: candidate is still swedsl's toy self-test, fails closed on every real oath. Blocked on `swedsl/oath` import path (swedsl#35) → cad-atlas writing its own candidate (#8). Not blocked on an Executor — that path was killed by swedsl's ADR-0002. See honesty rule. |
## Deployment ## Deployment
@@ -37,9 +38,9 @@ namespace `cad-atlas`, 1 replica, `cad-atlas:80 → :8080` (manifests in `mathia
A clause blocked by an external dependency is **descoped and tracked, never marked satisfied** A clause blocked by an external dependency is **descoped and tracked, never marked satisfied**
a self-lying Oath is a rubber stamp, the exact failure the Oath exists to prevent. S3's real a self-lying Oath is a rubber stamp, the exact failure the Oath exists to prevent. S3's real
enforcement depends on a var-go Executor (swedsl#27) + a published `oath` module; it is tracked as enforcement depends on `swedsl/oath` becoming importable (swedsl#35) and cad-atlas writing its own
a fast-follow on **#1**, not claimed here. The `DMABE_GITEA_API_TOKEN` Actions secret is candidate (#8); it is tracked there, not claimed here. The `DMABE_GITEA_API_TOKEN` Actions secret
pre-provisioned so #1 can land without a secret-write. is pre-provisioned so #1 landed without a secret-write.
## The Oath (advisory form) ## The Oath (advisory form)
+236
View File
@@ -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 0004 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 0003. 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 "→ 0408", 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 0408" 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>
+32 -30
View File
@@ -7,49 +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","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", {"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",
"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":[ "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 #126+"]}, {"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 #126+"]},
{"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":"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":"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":"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/", {"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/",
"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":[ "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", {"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",
"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":[ "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":"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":"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":"🏛️ 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":"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":"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", {"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",
"trans_label":"Order written, 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":[ "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":"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":"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":"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":"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":"⚖️ 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", {"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",
"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":[ "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":"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":"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":"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", {"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",
"trans_label":"Agents produced a change","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":[ "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":"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":"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":"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":"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":"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":"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":"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", {"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",
"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":[]}, "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":[
{"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", {"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","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",
"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":[ "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)", {"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":[ "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"]}
]} ]}
] ]
} }
+17
View File
@@ -60,8 +60,25 @@ func TestDefault_HasPlainLayerAndTransitions(t *testing.T) {
if n.Plain == "" { if n.Plain == "" {
t.Fatalf("stage %s node %q missing plain", s.No, n.Title) 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) {
+5 -3
View File
@@ -15,6 +15,7 @@ type Host struct {
// the technical detail shown on demand. // 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"` 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"`
@@ -31,6 +32,7 @@ type Node struct {
// Nodes are derived from a live source at Build time. // 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"` PlainTitle string `json:"plain_title,omitempty"`
Plain string `json:"plain,omitempty"` Plain string `json:"plain,omitempty"`
@@ -64,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
} }
+69 -5
View File
@@ -65,8 +65,40 @@
.translabel{fill:var(--mono);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace} .translabel{fill:var(--mono);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace}
.translabel-gate{fill:var(--gold);font-weight:700} .translabel-gate{fill:var(--gold);font-weight:700}
.stagehead{min-height:64px} .stagehead{min-height:64px}
@media(min-width:821px){
body.plain .stagehead{min-height:188px} body.plain .stagehead{min-height:188px}
.stage .node:first-of-type{margin-top:24px} .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}
@@ -140,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">
@@ -185,9 +226,9 @@ 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());
const plain = MODE==='plain'; const plain = MODE==='plain';
STAGES.forEach(s=>{ STAGES.forEach((s,idx)=>{
const st=document.createElement('div');st.className='stage '+(s.cls||''); const st=document.createElement('div');st.className='stage '+(s.cls||'');
const head = plain const head = plain
? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+ ? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+
@@ -197,7 +238,9 @@ function renderAtlas(){
(s.nodes||[]).forEach(n=>{ (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>`:'';
const body = plain ? (n.plain||n.d||'') : (n.d||''); 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(!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>`;
@@ -206,6 +249,27 @@ function renderAtlas(){
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);}
}); });
} }
@@ -246,7 +310,7 @@ 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=(STAGES[8]&&STAGES[8].trans_label?STAGES[8].trans_label+' · ':'')+'feedback bus'; 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. // 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. // Always visible (default layer). Gate hops (04→05 human, 06→07 CI) in gold.
const tg=document.getElementById('translabels'); tg.innerHTML=''; const tg=document.getElementById('translabels'); tg.innerHTML='';
@@ -257,7 +321,7 @@ function build(){
t.setAttribute('x',(cs[i]+cs[i+1])/2);t.setAttribute('y',RAILY-9); t.setAttribute('x',(cs[i]+cs[i+1])/2);t.setAttribute('y',RAILY-9);
t.setAttribute('text-anchor','middle'); t.setAttribute('text-anchor','middle');
t.setAttribute('class',gate?'translabel translabel-gate':'translabel'); t.setAttribute('class',gate?'translabel translabel-gate':'translabel');
t.textContent=(gate?' ':'')+s.trans_label; t.textContent=(gate?'🔒 ':'')+s.trans_label;
const ttl=document.createElementNS('http://www.w3.org/2000/svg','title'); const ttl=document.createElementNS('http://www.w3.org/2000/svg','title');
ttl.textContent=s.trans||''; t.appendChild(ttl); ttl.textContent=s.trans||''; t.appendChild(ttl);
tg.appendChild(t); tg.appendChild(t);