docir — design documents Graph

Documents / Decisions / adr-307ba1f1a820

The site publishes the relation graph as an interactive constellation page

Why docir build emits graph.html — a deterministic per-type constellation map of the corpus — plus a landing-page index, and why the graph is a domain projection (graph_payload) rather than a second data path.

adr-307ba1f1a820decisionaccepted#architecture#cli#docs
View as Markdown◉ View in graph

Context

docir build (adr-a343140d72e2) publishes one page per document plus an index. The typed relation graph — the thing that distinguishes docir from a folder of markdown — was only visible one document at a time, as "links to / linked from" lists. Three interactive prototypes (constellation, orbit, lanes) were built against docir's own corpus (105 documents, 166 edges, 69% resolved issues, 90% relates_to); the constellation was selected: overview first, a deterministic per-type ring layout, filter chips, and a pinned detail card.

Decision

The build emits graph.html on every run, linked from the index (a call-to-action) and from every document page. The index doubles as a landing page: corpus stats and the graph CTA above the fold, an autofocused filter, and a "recently updated" strip (skipped for small corpora, hidden while filtering).

  • The graph is a domain projection. graph_payload(site) derives nodes and typed edges from the resolved site, so degree counts both directions and dangling edges are excluded. The page embeds that JSON; it is exported from api.py for anything else that wants the same shape.
  • Deterministic layout, no physics. One ring per type, radius from member count, the highest-edge-weight type at the centre, golden-angle spirals inside each ring. The same corpus always draws the same map.
  • Closed work is hidden by default. The site receives no schema, so inactive statuses are recognised by name (the union of the bundled profiles' inactive statuses); unknown statuses count as open. Unknown types and relation kinds get deterministic spare hues rather than a shared grey or the relates_to fallback style.
  • One chrome, own palettes. The page shares the site's CSS tokens (extracted to infra/theme.py); only the type/kind colour vocabularies are its own, because the map is where colour carries category.
  • Inline data is script-safe. </ is escaped in the embedded JSON so a document title cannot terminate the <script> element.

Consequences

  • Page count per build grows by one; the graph inlines the corpus skeleton (no bodies), so size scales with metadata only.
  • The concepts/ prototype directory is deleted entirely; the module copy is canonical, and the three-way comparison (constellation / orbit / lanes) survives in this ADR's context section.
  • The card's "open document" link hands off from exploration (graph) to reading (page); in-card links navigate the graph and relax filters when they target a hidden document, so the card and the map cannot disagree.

Amendments (2026-08-04)

Two follow-on decisions landed the same day, both index/graph UX:

graph.html#<id> pins that document on load (card open, filters relaxed if they hid it); every document page links to it, and pinning keeps the fragment in sync via replaceState. A hashchange listener covers same-document fragment navigation, which never re-runs load-time init.

Faceted index filters.

Type and status are multi-select checkbox facets (OR within, AND across, live result counts); the status facet is narrowed by the type selection and drops selections that become unavailable. Dates offer rolling presets (7/30/90 days, this year) plus an absolute custom range — rolling for "what changed lately" links, absolute for citations. The whole state mirrors into the URL query. Chosen over single-selects after the standard faceted-search findings (counts per option, dependent facets, instant application).

[hidden] reset.

Author display rules on rows and facet labels overrode the UA's [hidden] mapping, so filtered-out rows never left the screen when their section stayed visible. Both stylesheets now carry [hidden]{display:none!important}.

To amend: Re-verify: