# Federation C4 Model (Structurizr) — Design Spec
Goal: Render the swarph federation's cross-repo meta-model as an interactive C4 model, auto-generated from the CodeGraph fleet index — replacing today's flat Mermaid federation view — with private-topology-safe two-view scoping.
Architecture: Reuse swarph_fed.py's existing deterministic cross-repo DAG derivation (declared manifests ∪ grounded CodeGraph import-sites, with declared-vs-observed reconciliation). Add a Structurizr emitter (sibling of viz.py) that writes a Structurizr workspace .dsl, and host a self-hosted Structurizr Lite service (Docker, lab-ovh tailnet) that renders it with native C4 drill-down. The swarph-desktop Federation tab links to the Lite view. Regeneration rides the existing nightly fed_refresh.sh.
Tech Stack: Python 3.11 (stdlib + tomllib, matching swarph-codegraph-fed), Structurizr DSL, structurizr/lite Docker image, systemd (service + timer), existing swarph-desktop web UI.
Global Constraints
- Generated, never hand-authored. The model is 100% derived from the fleet index + repo pyprojects on every refresh. No manual arranging/annotating; the emitter is deterministic (same inputs → byte-identical
.dsl). - Two views, privacy-scoped. The emitter produces
full.dsl(all indexed repos incl. private) ANDpublic.dsl(public repos only), driven by fleet-index visibility. The full model is served ONLY on the tailnet-bound Lite instance (lab/owner/commander); the public model is the only thing safe to share/embed off-tailnet. Honorsfeedback_private_topology_in_derived_artifactsand the A8 visibility gate — a derived artifact must not leak private-repo topology. - Reuse
swarph_fed.py. Do NOT reimplement repo discovery, DAG building, grounding, or reconciliation. The emitter consumesswarph_fed'sRepoobjects +build_effective_dag+reconcileoutput. - stdlib-only for the emitter (like the rest of swarph-codegraph-fed). No new Python deps.
- Additive.
viz.py(Mermaid) stays; the Structurizr emitter is a sibling.swarph_fed.py's public functions are unchanged (extended only if a needed field is missing).
Locked Decisions (from brainstorming 2026-07-17)
1. Tool: C4 model / Structurizr. 2. Sourcing: generated from the fleet index (not hand-curated). 3. Renderer: self-hosted Structurizr Lite (interactive, native drill-down) — not inline export, not static SVG. 4. Scope: two views — gated full + public-only.
---
Components
1. Structurizr emitter — structurizr_emit.py (swarph-codegraph-fed)
A new module, sibling of viz.py. Pure function, deterministic, stdlib-only.
Interface:
`python
def to_structurizr(
repos: list[fed.Repo],
effective_dag: dict[str, list[str]],
reconcile: dict, # swarph_fed.reconcile() output: missing/dead per repo
*,
visibility: dict[str, str], # repo-name -> "public" | "private" (from fleet index)
groups: dict[str, str], # repo-name -> hosting cell ("lab-ovh" | "droplet" | ...)
public_only: bool = False, # True -> emit only public repos + edges between them
) -> str: # a Structurizr DSL workspace document
...
`
Called twice by the CLI/refresh: public_only=False → full.dsl, public_only=True → public.dsl.
Model mapping (fleet DAG → C4):
| Data source | C4 encoding |
|---|---|
| SoftwareSystem | one softwareSystem "Swarph Federation" |
| each Repo | a container with description = sorted provides packages |
| Repo.cg_nodes (symbol count) | size tag size-S/M/L/XL (bucketed) → element style with growing width/height — "bigger repo, bigger box" |
| visibility[name] | tag public/private → element style (private = muted fill/dashed border) |
| groups[name] (hosting cell) | Structurizr group " boundary |
| effective_dag edge | a -> b "imports" relationship |
| edge in reconcile.missing_declared (imported, not declared) | relationship tag missing-decl → red/dashed style |
| edge in reconcile.dead_dep (declared, never imported) | relationship tag dead-dep → faded/dotted style |
| Repo.import_sites counts | relationship description suffix (N sites) |
Views: systemContext swarph + container swarph (auto-layout) + a component view per container generated from that repo's top-level modules (CodeGraph file-path prefixes), each component carrying its key symbols — the C4 drill-down. matrix.py's seam/definition matrix is surfaced as the per-container detail (the "definition matrix").
Size bucketing (deterministic): size-S < 300 symbols, size-M 300–1500, size-L 1500–5000, size-XL ≥ 5000. (Grounded in current partition counts: swarph-shared 211=S, mesh-gateway 1239=M, swarph-cli 2885=L, hedge-fund-mcp 8810=XL.)
2. Visibility + groups resolver
A small helper (in structurizr_emit.py or fed_refresh.sh) reads the fleet index ~/.swarph/codegraph/index.db repos table for visibility, and a static groups map (repo → hosting cell: lab-ovh for on-box repos, droplet for hedge-fund-mcp, etc. — a hand-maintained dict, small and stable, ~11 entries). If a repo is absent from the fleet index, default visibility private (fail-closed) and group unknown.
3. Structurizr Lite service — Docker + systemd (lab-ovh)
- Image
structurizr/lite(Java, serves HTTP on container :8080), watching a mounted workspace dir containingworkspace.dsl(symlinked/copied fromfull.dsl). - Bound to the tailnet interface only (publish on the tailscale IP, not 0.0.0.0) so the FULL model is never reachable off-tailnet.
- Managed by a systemd unit
swarph-federation-c4.service(like lab's other self-hosted services). Restart-on-failure. - The public view is NOT served by this instance;
public.dslis rendered/exported for embedding (see §4).
4. swarph-desktop integration
- Federation tab (
swarph-desktop/web/app.js, ~line 530) currently renders auto-generated Mermaid from/federation. Change: the tab links to (or iframes) the tailnet Lite URL for the full interactive model. - The daemon's
/federationendpoint additionally serves the public model (Lite-rendered PNG/SVG export ofpublic.dsl, or a public Lite instance) for any shareable/embedded surface. The public view is the only one exposed beyond the tailnet.
5. Refresh — extend fed_refresh.sh
After the existing swarph_fed.py run, the script invokes the emitter to write full.dsl + public.dsl into the Lite workspace dir. Runs nightly after the index full-sweep (04:30) → federation refresh (04:45); Lite auto-reloads the changed workspace. Add an on-demand target (fed_refresh.sh --now) already implicit in the script.
---
Data Flow
`
~/.swarph/codegraph/index.db + repo pyprojects
│ discover_repos / ground / build_effective_dag / reconcile (swarph_fed.py, REUSED)
▼
repos[], effective_dag, reconcile + visibility (fleet index), groups (static map)
│ to_structurizr(public_only=False|True) (structurizr_emit.py, NEW)
▼
full.dsl ──► Structurizr Lite (Docker, tailnet-only) ──► swarph-desktop Federation tab (link)
public.dsl ──► render/export (PNG/SVG or public Lite) ──► shareable/embeddable surfaces
`
Error Handling
- Emitter is fail-safe on a per-repo basis: a repo missing from the fleet index →
private/unknown/size-S defaults, still emitted (never crashes the whole.dsl). Mirrorsswarph_fed.ground's graceful no-op. - A malformed/absent fleet index → emitter falls back to visibility=
privatefor ALL repos (fail-closed: never accidentally leak a private repo intopublic.dsl). fed_refresh.shwrites.dslatomically (tmp + mv, matching its existing report write) so Lite never reads a half-written workspace.- Lite service down → the desktop link shows a clear "federation model offline" state, not a broken iframe.
Testing
test_structurizr_emit.py(stdlibunittest/pytest, offline — no Lite, no real repos):- Determinism: same inputs → byte-identical
.dsl. - Size bucketing: cg_nodes at each boundary → correct
size-*tag. - Reconciliation styling: a
missing_declarededge gets tagmissing-decl; adead_depedge getsdead-dep; a clean edge gets neither. - Two-view privacy (the load-bearing test): given a private repo,
public_only=Trueoutput contains NO reference to it (name, container, or any edge touching it) — assert the private repo's name is absent frompublic.dsl. And a private repo with a missing fleet-index entry defaults to private (fail-closed) → excluded from public. - Groups: repos placed under the correct
groupboundary. - Valid DSL shape: output parses as a well-formed workspace (balanced braces;
softwareSystem/container/relationship/views/stylesblocks present). - Reuse swarph_fed's existing tests (
test_fed.py,test_matrix.py,test_seam.py) unchanged — the derivation is not modified. - Manual acceptance:
full.dslloads in Structurizr Lite, drill-down works, sizes/colors read correctly;public.dslshows only public repos.
Build Order / Task Decomposition
1. Emitter core (structurizr_emit.py + tests): to_structurizr producing valid DSL from in-memory repos/dag/reconcile fixtures. Deterministic, two-view, reconciliation tags, size buckets, groups. (Testable in isolation — no Lite, no real repos.)
2. Visibility + groups wiring: read fleet-index visibility, static groups map, fail-closed defaults. Wire the emitter into a swarph_fed.py CLI subcommand (--emit structurizr --out DIR).
3. fed_refresh.sh extension: generate full.dsl + public.dsl atomically into the Lite workspace dir on each refresh.
4. Structurizr Lite service: Docker + swarph-federation-c4.service systemd unit, tailnet-bound.
5. swarph-desktop integration: Federation tab → link/iframe the Lite URL; /federation serves the public export.
Tasks 1–3 are the deterministic core (swarph-codegraph-fed, git repo, testable). Tasks 4–5 are deployment/integration (commander-gated where they touch service hosting / a new exposed surface).
Open Items / Risks
- Coverage of non-Python repos:
swarph_fed's cross-repo edges come from pyproject deps + Python import grounding. JS/TS repos (gridiron, openclaw) will appear as sized nodes with sparse/no edges — informative (size, visibility, group) but not richly connected. Acceptable for v1; a JS/TS seam extractor is a future extension. - openclaw indexing: currently omitted (CodeGraph walked
node_modulesand timed out). Addnode_modules/*tofleet.DEFAULT_EXCLUDESand index with that skip before openclaw joins the model as a real node. Tracked separately. - Public-view rendering: Structurizr Lite is a viewer, not a headless exporter; producing a shareable PNG/SVG of
public.dslmay needstructurizr-cli export(a second image) or a second Lite instance. Resolve in Task 4. - Deployment/hosting is commander-gated (new self-hosted service + a new exposed surface for the public view), per the lab charter.
---
Brainstormed with the commander 2026-07-17. Decisions: C4/Structurizr · generated · Structurizr Lite · two-view (gated full + public-only).