feat(atlas): Phase B — data-driven /api/atlas.json, CI stage generated from cd.yml
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s

The atlas no longer hand-maintains its content. Authored data lives in one place
(internal/atlas/atlas.json); the Go layer overlays sourced facts and serves the
result at /api/atlas.json; the frontend fetches + renders (inline arrays kept only
as an offline fallback). First generated source: the CI/CD stage's nodes are parsed
from the repo's own .gitea/workflows/cd.yml — so the viz shows the pipeline that
actually runs (guard/check/build/deploy), dropping the aspirational var-go/oath-gate
node that isn't wired yet. That's the point: it can't drift from the real pipeline.

New internal/atlas package (JobsFromWorkflow, Build) built test-first. Adds
gopkg.in/yaml.v3 (justified: parsing the workflow YAML; stdlib has no YAML).

Verified: go build/vet/lint(0)/test green; /api/atlas.json → 9 stages, CI = real
jobs; frontend renders from the fetch (screenshot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 00:34:06 +02:00
co-authored by Claude Opus 4.8
parent c3fda9b26b
commit a9c72d6ca8
12 changed files with 375 additions and 24 deletions
+46
View File
@@ -0,0 +1,46 @@
{
"substrate": [
{"n":"koala","k":"RTX 5070 · k3s control-plane · Gitea · LiteLLM :30401 · llama-swap :31234 · searxng"},
{"n":"iguana","k":"M2 Ultra · Ollama / mlx"},
{"n":"flamingo","k":"daily driver · ~/dev"},
{"n":"piguard","k":"NGINX reverse-proxy · ntfy"}
],
"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 #126+"]},
{"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 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 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 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 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 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 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","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 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"]}
]}
]
}
+48
View File
@@ -0,0 +1,48 @@
package atlas_test
import (
"reflect"
"testing"
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
)
func TestBuild_OverlaysCIStageNodesFromWorkflow(t *testing.T) {
atlasJSON := []byte(`{
"substrate": [{"n":"koala","k":"gpu"}],
"stages": [
{"no":"STAGE 06","title":"PR → CI","generate":"ci-jobs","nodes":[]},
{"no":"STAGE 01","title":"TELOS","nodes":[{"t":"Intention substrate"}]}
]}`)
workflow := []byte("jobs:\n check:\n a: 1\n build:\n b: 2\n deploy:\n c: 3\n")
a, err := atlas.Build(atlasJSON, workflow)
if err != nil {
t.Fatalf("Build: %v", err)
}
// CI stage nodes generated from the workflow's jobs, in order.
var titles []string
for _, n := range a.Stages[0].Nodes {
titles = append(titles, n.Title)
}
if want := []string{"check", "build", "deploy"}; !reflect.DeepEqual(titles, want) {
t.Fatalf("CI stage nodes = %v, want %v", titles, want)
}
// Non-generated stage is untouched.
if a.Stages[1].Nodes[0].Title != "Intention substrate" {
t.Fatalf("authored stage was altered: %+v", a.Stages[1])
}
// Substrate preserved.
if len(a.Substrate) != 1 || a.Substrate[0].Name != "koala" {
t.Fatalf("substrate not preserved: %+v", a.Substrate)
}
}
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")
}
}
+15
View File
@@ -0,0 +1,15 @@
package atlas
import _ "embed"
// DataJSON is the authored atlas content (substrate + stage narrative).
// Sourced/generated facts are overlaid onto it at Build time.
//
//go:embed atlas.json
var DataJSON []byte
// Default builds the atlas from the embedded authored data, overlaying the
// given CI workflow (for the stage marked generate:ci-jobs).
func Default(workflow []byte) (Atlas, error) {
return Build(DataJSON, workflow)
}
+66
View File
@@ -0,0 +1,66 @@
package atlas
import (
"encoding/json"
"fmt"
)
// Host is a substrate machine/entry (koala, iguana, …).
type Host struct {
Name string `json:"n"`
Spec string `json:"k"`
}
// Node is a card within a stage.
type Node struct {
Title string `json:"t"`
Desc string `json:"d,omitempty"`
Pill string `json:"pill,omitempty"`
Cls string `json:"cls,omitempty"`
Tags []string `json:"tags,omitempty"`
Risk bool `json:"risk,omitempty"`
Gate bool `json:"gate,omitempty"`
}
// Stage is one column of the pipeline. When Generate is set, its Nodes are
// derived from a source at Build time rather than taken from the authored data.
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"`
}
// Atlas is the full data model the frontend renders.
type Atlas struct {
Substrate []Host `json:"substrate"`
NS string `json:"ns,omitempty"`
Stages []Stage `json:"stages"`
}
// Build unmarshals the authored atlas JSON and overlays generated facts from
// real sources, so sourced parts can't drift. Currently: any stage marked
// `"generate":"ci-jobs"` gets its Nodes replaced by the workflow's job list.
func Build(atlasJSON, workflow []byte) (Atlas, error) {
var a Atlas
if err := json.Unmarshal(atlasJSON, &a); err != nil {
return Atlas{}, fmt.Errorf("parse atlas data: %w", err)
}
for i := range a.Stages {
if a.Stages[i].Generate != "ci-jobs" {
continue
}
jobs, err := JobsFromWorkflow(workflow)
if err != nil {
return Atlas{}, fmt.Errorf("stage %s: %w", a.Stages[i].No, err)
}
nodes := make([]Node, 0, len(jobs))
for _, j := range jobs {
nodes = append(nodes, Node{Title: j})
}
a.Stages[i].Nodes = nodes
}
return a, nil
}
+48
View File
@@ -0,0 +1,48 @@
// Package atlas builds the CAD Atlas data model, deriving parts from real
// sources (the repo's own CI workflow, infra manifests, the live cluster)
// so the visualization can't drift from reality.
package atlas
import (
"fmt"
"gopkg.in/yaml.v3"
)
// JobsFromWorkflow extracts the job names, in document order, from a Gitea
// Actions / GitHub Actions workflow YAML. Used to generate the CI/CD stage of
// the atlas from the pipeline that actually runs, rather than hand-authoring it.
func JobsFromWorkflow(workflow []byte) ([]string, error) {
var doc yaml.Node
if err := yaml.Unmarshal(workflow, &doc); err != nil {
return nil, fmt.Errorf("parse workflow: %w", err)
}
if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode {
return nil, fmt.Errorf("workflow: expected a top-level mapping")
}
jobs := mappingValue(doc.Content[0], "jobs")
if jobs == nil {
return nil, fmt.Errorf("workflow: no jobs block")
}
if jobs.Kind != yaml.MappingNode {
return nil, fmt.Errorf("workflow: jobs is not a mapping")
}
// A mapping node stores keys and values as alternating Content entries;
// keys are the even indices, in document order.
names := make([]string, 0, len(jobs.Content)/2)
for i := 0; i+1 < len(jobs.Content); i += 2 {
names = append(names, jobs.Content[i].Value)
}
return names, nil
}
// mappingValue returns the value node for key in a YAML mapping node, or nil.
func mappingValue(m *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(m.Content); i += 2 {
if m.Content[i].Value == key {
return m.Content[i+1]
}
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package atlas_test
import (
"reflect"
"testing"
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
)
func TestJobsFromWorkflow_ReturnsJobNamesInDocumentOrder(t *testing.T) {
workflow := []byte(`
name: CD
"on":
push:
branches: [main]
jobs:
guard:
runs-on: self-hosted
check:
needs: guard
runs-on: self-hosted
build:
needs: [guard, check]
runs-on: self-hosted
deploy:
needs: [guard, build]
runs-on: self-hosted
`)
jobs, err := atlas.JobsFromWorkflow(workflow)
if err != nil {
t.Fatalf("JobsFromWorkflow: %v", err)
}
want := []string{"guard", "check", "build", "deploy"}
if !reflect.DeepEqual(jobs, want) {
t.Fatalf("jobs = %v, want %v", jobs, want)
}
}
func TestJobsFromWorkflow_ErrorsOnMalformedYAML(t *testing.T) {
if _, err := atlas.JobsFromWorkflow([]byte("jobs: [this is: not valid")); err == nil {
t.Fatal("expected error on malformed YAML, got nil")
}
}