generated from mathias/template-go-web
feat(atlas): UX sprint — progressive disclosure (Plain default + transitions)
Makes the atlas self-explanatory. Every stage/node gains a plain-language layer (plain_title + plain "what happens" + jargon-free node text); the previous technical copy demotes to a subtitle + on-demand detail. A Plain⇄Technical toggle (default Plain, persisted) flips the whole atlas. Biggest win: every transition arrow is now LABELLED with "what must be true to advance" (the gated- flow story that was invisible), gate hops (human @04, CI @06) styled distinctly. Spine repositioned into a uniform header band so labels never collide with copy. Copy grounded in a fresh-eyes UX review (docs/UX-REVIEW.md, reviewer≠implementer). Data model: plain_title/plain/trans_label/trans on Stage, plain on Node — guarded by a test (every stage has plain_title + a transition). Live overlays unchanged. Verified: build/vet/lint(0)/test green; Plain render screenshot-checked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-27
@@ -7,40 +7,49 @@
|
||||
],
|
||||
"ns": "Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council",
|
||||
"stages": [
|
||||
{"no":"STAGE 00","title":"Signals","path":"→ mathias/signals","nodes":[
|
||||
{"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+"]},
|
||||
{"t":"Manual capture","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]},
|
||||
{"t":"Aspirational surfaces","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]}
|
||||
{"no":"STAGE 00","title":"Signals","plain_title":"Notice what's happening","plain":"New ideas and developments worth reacting to are collected — mostly an automated daily/weekly scan of AI news, plus things saved by hand.","path":"→ mathias/signals",
|
||||
"trans_label":"Does it matter to us?","trans":"A raw signal only advances if it connects to something we actually care about. Most captured signals stop here; the few that touch the mission get pulled up against a goal.","nodes":[
|
||||
{"t":"Applied AI Radar","plain":"An automated scan reads AI news every day (and deeper every week) and keeps only claims backed by a real paper, benchmark, code, or named lab.","d":"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).","tags":["cron · daily/weekly","→ signals #1–26+"]},
|
||||
{"t":"Manual capture","plain":"Anything interesting spotted by hand gets saved into the same inbox.","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]},
|
||||
{"t":"Aspirational surfaces","plain":"Planned but not built yet: sending ideas in by Telegram, voice, or a URL.","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]}
|
||||
]},
|
||||
{"no":"STAGE 01","cls":"telos","title":"TELOS","path":"wiki/telos/","nodes":[
|
||||
{"t":"Intention substrate","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=telos"]}
|
||||
{"no":"STAGE 01","cls":"telos","title":"TELOS","plain_title":"Why we're here","plain":"The mission, goals, and problems we're actually trying to solve live here — every piece of work downstream has to trace back to one of these goals.","path":"wiki/telos/",
|
||||
"trans_label":"Worth a session?","trans":"A goal or problem on the board becomes the seed for a design session when it's decided worth working on now. The goal is the input the session must trace back to.","nodes":[
|
||||
{"t":"Intention substrate","plain":"The master list of mission, goals, problems, and current status — the yardstick everything downstream is measured against.","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=telos"]}
|
||||
]},
|
||||
{"no":"STAGE 02","title":"Strategic session","path":"claude.ai frontier + brain MCP","nodes":[
|
||||
{"t":"Design · ADRs · specs","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]},
|
||||
{"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":"Autoresearch Council","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]}
|
||||
{"no":"STAGE 02","title":"Strategic session","plain_title":"Think it through","plain":"A human and AI models work out what to do and why, debating hard calls and writing down the decision and what \"done\" will mean.","path":"claude.ai frontier + brain MCP",
|
||||
"trans_label":"Decision reached","trans":"It advances only when the thinking converges on a decision and is written down as a concrete, testable specification — not while the answer is still open.","nodes":[
|
||||
{"t":"Design · ADRs · specs","plain":"A human and a top-tier AI model figure out the approach and write down the decision plus what a finished result must prove.","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]},
|
||||
{"t":"🏛️ LLM Council","plain":"For hard calls, several AI models answer independently, anonymously critique each other, and a \"chair\" model synthesises one verdict — reducing any single model's bias.","cls":"council","pill":"var(--violet)","d":"fan-out → anonymous cross-review → chairman synth. glm-4.7-flash · qwen36-35b · gemma4-31b (chair).","tags":["hard strategic Q","chat.d-ma.be"]},
|
||||
{"t":"Autoresearch Council","plain":"A parallel version of the same review that vets research findings before they're allowed through.","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]}
|
||||
]},
|
||||
{"no":"STAGE 03","title":"Spec → Gitea issue","path":"agent-ready contract","nodes":[
|
||||
{"t":"Contract enforced","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]},
|
||||
{"t":"Admission controller","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]},
|
||||
{"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"]}
|
||||
{"no":"STAGE 03","title":"Spec → Gitea issue","plain_title":"Write the work order","plain":"The decision is turned into a precise, self-contained work order an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal.","path":"agent-ready contract",
|
||||
"trans_label":"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":[
|
||||
{"t":"Contract enforced","plain":"The work order must have a clear pass/fail test, a risk rating, a regulatory-risk note, and no unfinished human dependencies before it counts as agent-ready.","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]},
|
||||
{"t":"Admission controller","plain":"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it.","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]},
|
||||
{"t":"⚖️ var-go Oath","plain":"A machine-checkable \"definition of done\" is embedded in the work order — exactly one, or the order is rejected — later used to prove the result actually meets the spec.","cls":"oath","pill":"var(--gold)","d":"Acceptance contract embedded in the issue as a var fenced block. Exactly one — zero/multiple fail closed. Prose → typed steps; failures anchored to byte spans.","tags":["swedsl · var-go","defined here → enforced @06"]}
|
||||
]},
|
||||
{"no":"STAGE 04","cls":"gate","title":"Human dispatch gate","path":"the only checkpoint","nodes":[
|
||||
{"t":"Human triggers execution","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true},
|
||||
{"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"]}
|
||||
{"no":"STAGE 04","cls":"gate","title":"Human dispatch gate","plain_title":"Human says go","plain":"A person reviews the work order and its risk and decides whether to release it — the one and only checkpoint where work does not move on its own.","path":"the only checkpoint",
|
||||
"trans_label":"A human said go","trans":"The hard stop. Nothing crosses automatically — a person must review the plan and risk and explicitly release it, and the repo must be on the allow-list, before any agent starts. This is the single human checkpoint in the whole pipeline.","nodes":[
|
||||
{"t":"Human triggers execution","plain":"A person confirms the plan and its risk level, then releases the work — nothing runs until they do.","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true},
|
||||
{"t":"Session-Dispatch bridge","plain":"The approval flips a switch that hands the signed work order over to the agents to start execution.","cls":"bridge","pill":"var(--blue)","d":"claude.ai MCP → gitea:workflow_run_trigger → cad-dispatch.yml → agentsquad. The final design→execution bridge.","tags":["workflow_dispatch"]}
|
||||
]},
|
||||
{"no":"STAGE 05","cls":"exec","title":"Execute · agentsquad","path":"koala · cmd/agentsquad-serve","nodes":[
|
||||
{"t":"Task API","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]},
|
||||
{"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":"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":"assessor-loop ledger","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]}
|
||||
{"no":"STAGE 05","cls":"exec","title":"Execute · agentsquad","plain_title":"Agents do the work","plain":"AI agents actually build the thing — one writes, a second independent one reviews it to avoid marking its own homework — and every step is logged for the audit trail.","path":"koala · cmd/agentsquad-serve",
|
||||
"trans_label":"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":[
|
||||
{"t":"Task API","plain":"A request kicks off a job and hands back an id you can poll for progress.","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]},
|
||||
{"t":"Executor + reviewer loop","plain":"One agent does the work; a second, independent agent on a different model reviews it — so nothing marks its own homework.","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true},
|
||||
{"t":"dma-cli · routing + scope","plain":"A router sends each agent to the right AI backend and enforces what it is and isn't allowed to touch, with a confirmation gate as a guardrail.","cls":"bridge","pill":"var(--blue)","d":"Harness-config arm: routes agents to the right LLM backend. Three-layer scope policy + confirmation gate = CAD guardrail.","tags":["backend routing","scope guardrail"]},
|
||||
{"t":"assessor-loop ledger","plain":"Every step is recorded in a tamper-evident log so the whole run can be audited afterwards.","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]}
|
||||
]},
|
||||
{"no":"STAGE 06","title":"PR → CI","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs","nodes":[]},
|
||||
{"no":"STAGE 07","cls":"cd","title":"CD → pod","path":"Flux GitOps → k3s","generate":"deploy-state","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"]}
|
||||
{"no":"STAGE 06","title":"PR → CI","plain_title":"Automatic quality checks","plain":"The proposed change is run through automated tests and safety checks — including a check that it actually satisfies the work order's definition of done — and only a clean pass lets it continue.","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs",
|
||||
"trans_label":"All checks green","trans":"Advances only if every automated check passes — tests, linters, security scan, and the Oath check proving it meets the original work order. Any red gate stops it here; a passing reviewer is not enough to override a failed Oath.","nodes":[]},
|
||||
{"no":"STAGE 07","cls":"cd","title":"CD → pod","plain_title":"Ship it","plain":"Once everything is green, the change is deployed automatically to the live server — with the rule that merging code alone doesn't ship it; the release has to be pointed at the new version.","path":"Flux GitOps → k3s","generate":"deploy-state",
|
||||
"trans_label":"It's live","trans":"Once the new version is actually running on the server, the deployed outcome becomes the input to scoring. Advancing means shipped and observable, not just merged.","nodes":[
|
||||
{"t":"Deploy on green","plain":"When all checks pass, the release system rolls the new version onto the live server automatically — but only once the release is pointed at that version (merging code alone doesn't ship it).","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]}
|
||||
]},
|
||||
{"no":"STAGE 08","cls":"telos","title":"Loop back","path":"→ TELOS (feedback bus)","nodes":[
|
||||
{"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"]}
|
||||
{"no":"STAGE 08","cls":"telos","title":"Loop back","plain_title":"Did it work?","plain":"The result is scored against the goal that started it and fed back into the mission board, so the next round of planning learns from what shipped.","path":"→ TELOS (feedback bus)",
|
||||
"trans_label":"What did we learn?","trans":"The scored outcome flows back into the mission board so goals, problems, and priorities update — the loop that makes the pipeline a cycle rather than a line. Partly manual today; an explicit improvement target.","nodes":[
|
||||
{"t":"Close the loop","plain":"The outcome is scored against the goal that started it and written back to the mission board, so future planning learns from what actually shipped.","pill":"var(--violet)","d":"session_log + attestation → brain. Score deploy outcome vs originating goal. (arc partly manual — improvement target.)","tags":["continuous"]}
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -41,6 +41,29 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_ErrorsOnBadAtlasJSON(t *testing.T) {
|
||||
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")
|
||||
|
||||
+18
-9
@@ -11,9 +11,11 @@ type Host struct {
|
||||
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 {
|
||||
Title string `json:"t"`
|
||||
Plain string `json:"plain,omitempty"`
|
||||
Desc string `json:"d,omitempty"`
|
||||
Pill string `json:"pill,omitempty"`
|
||||
Cls string `json:"cls,omitempty"`
|
||||
@@ -22,15 +24,22 @@ type Node struct {
|
||||
Gate bool `json:"gate,omitempty"`
|
||||
}
|
||||
|
||||
// Stage is one column of the pipeline. When Generate is set, its Nodes are
|
||||
// derived from a source at Build time rather than taken from the authored data.
|
||||
// Stage is one column of the pipeline. PlainTitle/Plain are the plain-language
|
||||
// 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 {
|
||||
No string `json:"no"`
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Cls string `json:"cls,omitempty"`
|
||||
Generate string `json:"generate,omitempty"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
No string `json:"no"`
|
||||
Title string `json:"title"`
|
||||
PlainTitle string `json:"plain_title,omitempty"`
|
||||
Plain string `json:"plain,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Cls string `json:"cls,omitempty"`
|
||||
Generate string `json:"generate,omitempty"`
|
||||
TransLabel string `json:"trans_label,omitempty"`
|
||||
Trans string `json:"trans,omitempty"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
}
|
||||
|
||||
// Atlas is the full data model the frontend renders.
|
||||
|
||||
@@ -59,6 +59,15 @@
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
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}
|
||||
body.plain .stagehead{min-height:188px}
|
||||
.stage .node:first-of-type{margin-top:24px}
|
||||
|
||||
.scroll{overflow-x:auto;padding:24px 22px 20px}
|
||||
.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}
|
||||
@@ -123,6 +132,7 @@
|
||||
<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>
|
||||
<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="slowmo">Slow-mo · <span id="slowState">off</span></button>
|
||||
</div>
|
||||
@@ -145,6 +155,7 @@
|
||||
stroke-dasharray="5 5" opacity=".7"></path>
|
||||
<text id="loopLbl" fill="#9b8cff" font-size="11"
|
||||
font-family="ui-monospace,monospace" opacity=".85"></text>
|
||||
<g id="translabels"></g>
|
||||
</svg>
|
||||
<div class="pulse" id="pulse"></div>
|
||||
</div>
|
||||
@@ -163,6 +174,7 @@
|
||||
// empty and are filled by init()'s fetch; on failure the page shows an error
|
||||
// banner rather than stale inline data.
|
||||
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[];
|
||||
let MODE = localStorage.getItem('atlas-mode') || 'plain'; // 'plain' | 'technical'
|
||||
|
||||
const track=document.getElementById('track');
|
||||
let stageEls=[];
|
||||
@@ -174,14 +186,22 @@ function renderAtlas(){
|
||||
const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh);
|
||||
stageEls=[];
|
||||
track.querySelectorAll('.stage').forEach(el=>el.remove());
|
||||
const plain = MODE==='plain';
|
||||
STAGES.forEach(s=>{
|
||||
const st=document.createElement('div');st.className='stage '+s.cls;
|
||||
let h=`<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
|
||||
s.nodes.forEach(n=>{
|
||||
const st=document.createElement('div');st.className='stage '+(s.cls||'');
|
||||
const head = plain
|
||||
? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+
|
||||
`<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>`:'';
|
||||
let inner=`<div class="t">${pill}${n.t}</div><div class="d">${n.d}</div>`;
|
||||
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>`;
|
||||
const body = plain ? (n.plain||n.d||'') : (n.d||'');
|
||||
let inner=`<div class="t">${pill}${n.t}</div>`+(body?`<div class="d">${body}</div>`:'');
|
||||
if(!plain){
|
||||
if(n.tags&&n.tags.length)inner+=n.tags.map(t=>`<span class="tag mono">${t}</span>`).join('');
|
||||
if(n.risk)inner+=`<div class="risk mono"><span class="lo">LOW · auto</span><span class="md">MED · ntfy gate</span><span class="hi">HIGH · blocked</span></div>`;
|
||||
}
|
||||
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>`;
|
||||
});
|
||||
@@ -207,13 +227,16 @@ function renderTimeline(){
|
||||
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
|
||||
loopPath=document.getElementById('loopPath'), loopLbl=document.getElementById('loopLbl'),
|
||||
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;
|
||||
|
||||
function build(){
|
||||
mobile=window.matchMedia('(max-width:820px)').matches;
|
||||
if(mobile)return;
|
||||
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));
|
||||
loopY=maxBottom+40;
|
||||
spine.setAttribute('width',track.scrollWidth);
|
||||
@@ -223,7 +246,22 @@ function build(){
|
||||
const lastX=cs[cs.length-1], telosX=cs[1];
|
||||
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.textContent='feedback bus · outcome → goal';
|
||||
loopLbl.textContent=(STAGES[8]&&STAGES[8].trans_label?STAGES[8].trans_label+' · ':'')+'feedback bus';
|
||||
// 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();
|
||||
}
|
||||
function run(ts){
|
||||
@@ -246,6 +284,16 @@ function replay(){cancelAnimationFrame(raf);t0=null;build();if(!mobile)raf=reque
|
||||
document.getElementById('replay').onclick=replay;
|
||||
document.getElementById('slowmo').onclick=e=>{slow=!slow;e.currentTarget.classList.toggle('on',slow);
|
||||
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);});
|
||||
async function init(){
|
||||
try{
|
||||
@@ -265,6 +313,7 @@ async function init(){
|
||||
'<div class="path mono">/api/atlas.json failed to load</div></div>');
|
||||
return;
|
||||
}
|
||||
applyMode();
|
||||
renderAtlas();
|
||||
renderTimeline();
|
||||
replay();
|
||||
|
||||
Reference in New Issue
Block a user