The website at https://majordomus.dev/ is a projection of this
repository. It is built the way Majordomus asks projects to build their own instruction
files: one canonical source, generated outputs, and a check that fails when the two drift.
Purpose
Let a visitor understand in about two minutes what Majordomus is, what it guarantees, what is only advisory, what is not built, and how to start, without any sentence on the site being maintained separately from the repository that backs it.
Canonical sources
| layer | owns | lives in | edited by hand |
|---|---|---|---|
| product truth | policy schema, profiles, the worker instructions, the shell CLI | share/skeleton/**, bin/majordomus, lib/**, share/commands.yaml | yes |
| the Rust executable | every capability with its schemas, exposures, stability, provenance, benchmark and cache policy; the modules; the command line with the examples of every command | capability! and module! declarations under apps/majordomus-cli/src/capability/builtin/, apps/majordomus-cli/src/cli.rs (clap for the structure, cli::EXAMPLES for the examples) | yes |
| the executable's narrative | what the crate is, owns and refuses; discovery, transports, side effects, architecture | apps/majordomus-cli/README.md, docs/CAPABILITIES.md, docs/MCP.md | yes |
| benchmark evidence | the regression policy and the accepted baselines | .ai/repo/benchmarks/rust/policy.yaml, .ai/repo/benchmarks/rust/baseline.<platform>.json | policy yes; a baseline by majordomus bench baseline update |
| registry projections | the OpenAPI document, the registry manifest, the capability, module and benchmark references, the command line, and the manifest of the whole generated set — each document in every encoding it is committed in | docs/generated/** (artifacts.md is the generated index of it) | never — majordomus generate |
| registry dataset | everything the site renders about the executable: descriptors, modules, command line, MCP and HTTP surfaces, benchmarks, the index | site/data/registry/registry.json | never — majordomus generate site |
| public narrative | what it is, why, how, what it refuses | README.md, docs/*.md | yes |
| claims | every capability with status, source, implementation, test | docs/CLAIMS.yaml | yes |
| marketing copy | the hero's positioning sentences, section leads, button labels; no capability claims, no numbers, 80-line budget | site/data/marketing.toml | yes |
| navigation | the intents, their dropdown items and hrefs, and the routes that moved ([[redirects]], rendered as Zola aliases) | site/data/nav.toml | yes |
| claim detail | what each claim means, how it works, how to see it, what it does not cover, why it exists | docs/claims/<id>.md | yes |
| product features | one file per feature: the references naming what it is made of, and the editorial decisions nothing can infer; every surface, count and route beside it is derived (docs/PRODUCT.md, ADR 23) | .ai/repo/features/<id>.md | yes |
| case studies | the recognition moments, each with its homepage hook in front matter | site/content-src/why/*.md | yes |
| skills | the repository's skills, one directory each; the site reads the catalogue lib/skills.sh derives from the source class skill | .ai/repo/skills/<id>/SKILL.md | yes |
| rendering reference | representative Markdown for visual validation | site/content-src/render-test.md | yes |
| derived data | stable JSON the templates read | site/data/generated/*.json | never |
| the layer's directory contracts | every directory of .ai/, the contract it carries or the one that exempts it, and the counts | site/data/generated/context.json (from majordomus context list --json) | never |
| derived release artifact | the claims matrix as Markdown | docs/SITE_CLAIMS.md | never |
| derived content | canonical Markdown with generated front matter; one page per profile, claim, status, responsibility, command, doctrine, use case, application, milestone, issue, case study, skill, module and capability | site/content/{docs,profiles,guarantees,supervises,commands,doctrines,use-cases,applications,plan,why,registry,skills}/, render-test.md, architecture.md | never |
| derived routes and links for the executable | one route per module and per capability, the executable's pages, the API anchor, the source on GitHub, the claims attached to each surface | site/data/generated/executable.json | never |
| the native command line, as the site renders it | the command tree flattened, each command with its route, usage, arguments, children and executed examples | site/data/generated/cli.json (from docs/generated/cli.json, via scripts/lib/cli-site.jq) | never |
| build provenance | the commit, its time and its cleanliness, the site's input hash, the registry and index fingerprints, and each composed surface with the commit its producer declared | site/data/build.json, served as /build.json | never — scripts/site-build, not committed |
| the crate's reference | the rustdoc pages of apps/majordomus-cli, composed at /rustdoc (RUSTDOC.md) | target/web/rustdoc/, with the surface.json that declares what it was built from | never — scripts/rust-check --doc, not committed |
| presentation | Zola templates and Tera 2 components | site/templates/** | yes |
| styling entry | Tailwind v4 + Flowbite v4 directives | site/tailwind.css | yes |
| behaviour | theme toggle, the diagram viewer, the chart component | site/theme.js, site/diagrams.js, site/charts.js | yes |
| output | the static site | site/public/** | never |
What the publishing clone must be able to answer
The site links to the repository on its forge, and scripts/ci/link-check decides those
links against git rather than over the network: commit/<sha> and compare/a...b are
resolved in the history, releases/tag/<t> and tree/<tag> in the tags. A clone that was
not given those cannot decide such a link. It must then refuse rather than guess — and
scripts/site-check counts a refusal as a failure, so the publication stops.
That is not a hypothetical cost. Between 2026-09-14 18:34 and 2026-09-15 00:0x the
deploy checked out at depth 1, which truncates the history and fetches no tags. Six links
to v0.3.1, v0.5.0 and v0.6.0 — tags that exist and carry published releases — were
reported as non-existent, 2302 commit links were undecidable, publish was skipped on every
run, and the live site stayed at 8cf457000 for seven hours while master moved three merges
ahead. Every gate was green throughout, because nothing asked whether the site was current.
So the requirement is declared, not written into the workflow:
# .ai/repo/ci/pages.yaml
deploy:
checkout:
history: full # renders as fetch-depth: 0
tags: required # actions/checkout fetches none at a shallow depth| what | where |
|---|---|
| the requirement | deploy.checkout in .ai/repo/ci/pages.yaml |
| the depth it implies | scripts/pages checkout — prints 0 |
| the clone a run was given | scripts/pages checkout --verify — exit 10 when it cannot decide a ref |
| the workflow held to the model | test/cases/97_pages_fast_path.sh |
The workflow is an adapter over that, the same way its paths: block is an adapter over
scripts/pages paths. Changing what the publisher needs from its clone is an edit to the
model and nowhere else: the depth, the verification and the test all read it. A test that
asserted fetch-depth: 0 literally would hold one patch in place; the case compares the two
and fails when they disagree, whichever of them moved.
--verify runs in the deploy job immediately after checkout, so a runner that does not
honour the declaration fails there, naming the depth — rather than three steps later inside
link-check, where a checkout problem presents as a link defect.
When a publication fails: the scheduled retry
A push-triggered run that fails leaves the site where it was, and nothing asks again. On
2026-09-14 the site stood seven hours while every run failed. So .github/workflows/pages.yml
also runs on a clock, trigger.schedule in .ai/repo/ci/pages.yaml, every thirty minutes —
the window scripts/ci/pages-check judges by (OWED_AFTER, 1800 s); a schedule slower than the
window cannot notice what the window calls owed. test/cases/97_pages_fast_path.sh holds the
workflow's cron to the model's.
A scheduled run asks before it works, in a small job of its own (owed):
| state | remedy | run ends |
|---|---|---|
| the site is current | none; nothing else runs | green |
| gh-pages does not carry the trunk's site | the rustdoc and deploy jobs run as on a push | red |
| gh-pages is right, GitHub's own build errored | scripts/pages rebuild | red |
Red is deliberate: the run repaired a fault the push-triggered run should not have left, and
healing without reporting would hide a fault that recurs every thirty minutes. The
intervention job says what was owed and what was done.
A rebuild is a different action from a redeploy. Deploying an identical tree changes nothing
when GitHub's builder is what failed, and a request for a build preempts a build in flight:
asking while GitHub is building kills that build. scripts/pages rebuild therefore asks only
when the build of the commit errored and nothing is building, reads that state through
scripts/pages built --porcelain (the one reader of it), exits 12 without asking when the state
cannot be read or a build is in flight, and reads the state again after asking.
"gh-pages is right" is measured, not assumed: the owed job chooses the rebuild only when the
top-level commit of gh-pages' build.json is the trunk commit the run checked out. An errored
build of an older tree — a push deploy that failed before publishing, over a build that had
errored too — is deployed, because rebuilding it would republish the stale site on every run.
test/cases/625_a_rebuild_is_asked_only_when_it_helps.sh holds each of those, the last by
running the workflow's own remedy step against both trees.
Projection pipeline
Two generators, one graph. The Rust executable projects its own registry; the site
generator projects the canonical files and consumes the executable's projections through
one machine-readable boundary, docs/generated/registry.json (and openapi.json); Zola
renders templates over what both wrote. scripts/derive runs the stages in order,
scripts/derive-check proves the committed result, and no stage reads its own output.
flowchart TB
subgraph canonical
R[capability! and module! declarations, src/cli.rs with cli::EXAMPLES]
K[share/kinds.yaml, schemas, providers, policy]
D[README, docs/*.md, docs/claims, CLAIMS.yaml, RESPONSIBILITIES.yaml]
C[share/commands.yaml, use-cases.yaml, applications.yaml]
P[.ai/repo/project, rules, benchmarks]
end
subgraph stageA[stage A · majordomus generate, code only]
GA[docs/generated: openapi, registry, capabilities, cli, modules/*.md — json+yaml+md as each document is committed · share/allow/*.txt · AGENTS.md, CLAUDE.md]
end
subgraph stageB[stage B · scripts/generate-site-data]
GB[site/data/generated/*.json · site/content/** · docs/SITE_CLAIMS.md, PLAN_STATUS.md]
end
subgraph stageC[stage C · majordomus generate, over the index]
GC[site/data/registry/registry.json · docs/generated/benchmarks.* · docs/generated/artifacts.*]
end
subgraph build[scripts/site-build]
Z[zola + Tailwind → site/public · build.json]
end
R --> GA
K --> GA
D --> GB
C --> GB
P --> GB
GA -->|registry.json, openapi.json, cli.json| GB
R --> GC
P --> GC
GB -->|the derived documents are objects of the index| GC
GB --> Z
GC --> Z
| stage | generator | reads | writes | checked by |
|---|---|---|---|---|
| A | majordomus generate | the Rust declarations, share/, the policy and the provider templates | docs/generated/{openapi,registry,cli}.{json,yaml}, docs/generated/{capabilities,cli}.md, docs/generated/modules/*.md, share/allow/*.txt, the provider bootstraps | majordomus generate --check |
| B | scripts/generate-site-data | every canonical file (--inputs lists them) and stage A's registry.json and openapi.json | site/data/generated/*.json, site/content/**, docs/SITE_CLAIMS.md, docs/PLAN_STATUS.md | scripts/generate-site-data --check (by input hash) |
| C | majordomus generate | the Rust declarations and the index of the layer, which now holds the documents stage B wrote | site/data/registry/registry.json, docs/generated/benchmarks.{md,json,yaml}, docs/generated/artifacts.{json,yaml,md} | majordomus generate --check |
| build | scripts/site-build | stages B and C, the templates, the assets, and the artifact of every published static surface docs/generated/web.json names | site/public/** (with each such surface composed at its mount), site/data/build.json, site/static/build.json | scripts/site-check, scripts/site-probe |
The boundary between the executable and the site is data, in both directions of reading:
the site generator reads the registry manifest for the ids it turns into routes and never
parses Rust; the templates read the registry dataset for every fact about a capability and
never restate one; the executable knows nothing of Zola. The manifest is a projection used
as an interface between stages, not a second truth: a capability exists in one place, and
generate --check refuses a manifest that no longer says what the declarations say.
Why stage C exists: the dataset lists every object of the layer with its size, and the
derived documents stage B writes are objects of the layer, so the dataset is derived after
them. Stage C's outputs are read by nothing but the site build, so the graph has no cycle
and a second scripts/derive on a clean tree changes nothing — git status stays empty,
which test/cases/51_derived_artifacts_committed.sh and the release criterion require.
scripts/generate-site-data reads every canonical input, normalises it once, and writes:
| file | from | what |
|---|---|---|
project.json | bin/majordomus, README.md, LICENSE | name, version, tagline, licence, commands, exit codes |
profiles.json | share/skeleton/profiles/*.yaml | every profile, every field |
policy.json | share/skeleton/policy.yaml | the policy as structure, plus the raw text |
capabilities.json | docs/CLAIMS.yaml | every claim; the generator fails on a missing path or an untested guaranteed claim |
commands.json | share/commands.yaml, docs/CLI.md, the test cases | every shell command with its semantics, narrative and evidence |
catalogue.json | .ai/repo/use-cases/, .ai/repo/applications/ | the use cases and applications, cross-referenced |
doctrines.json | the rule packages | every doctrine with its enforcement chain |
plan.json | .ai/repo/project/ | milestones, issues, the dependency graph, derived status |
openapi.json | docs/generated/openapi.json | the HTTP API in the shape api.html and api-tag.html render, with the one page every operation and schema is published on (scripts/lib/openapi-site.jq) |
artifacts.json | docs/generated/artifacts.json | the generator's own manifest — declarations only, carrying schema: 1 like every other file of that directory with the manifest's own schema id under manifest_schema, moved under site/data/ so registry-artifacts.html can load it; nothing is added and no file is named in the template. Sizes and hashes are dropped: two of the manifest's subjects are written by stage C, so a copy carrying them could never converge with stage B |
executable.json | docs/generated/registry.json, capabilities.json | one route per module and per capability, the executable's pages, API anchors, sources on GitHub, the claims attached to each surface by the path of their implementation (scripts/lib/executable-site.jq) |
lifecycle.json | lib/finish.sh, share/skeleton/ai/repo/workflows/task-lifecycle.md, share/standard/majordomus/ | outcome vocabulary, divergence labels, lifecycle steps, the ten principles (the rules tagged principle) |
diagrams.json | the files above | Mermaid source projected from data |
readme.json | README.md | the sections the homepage renders, by heading; a renamed heading fails the build |
docs.json | docs/README.md | the documentation index |
source.json | the inputs | version, input hash, generator version, input list; no commit (see build provenance) |
site/content/docs/*.md is written from docs/*.md with a generated front matter, links
rewritten to site routes, and the whole body wrapped in a Tera raw block so that Tera never
templates canonical Markdown. The tag itself cannot be written out here: a literal closing
raw tag in a canonical document would end the wrapper the projection puts around it.
site/content/registry/executable.md is apps/majordomus-cli/README.md through the same
projection.
Derived files, classified
| class | what | examples | rule |
|---|---|---|---|
| canonical | authored, the only place a fact lives | the Rust declarations, share/*.yaml, docs/*.md, docs/CLAIMS.yaml, .ai/repo/**, templates, nav.toml, marketing.toml | edited by hand |
| derived, committed | a projection reviewers see on GitHub and CI compares with its sources | docs/generated/**, share/allow/**, AGENTS.md, CLAUDE.md, site/data/generated/**, site/data/registry/**, docs/SITE_CLAIMS.md, docs/PLAN_STATUS.md | regenerated with scripts/derive, committed with the change that moved it; scripts/derive-check refuses drift |
| derived, build only | rebuilt on every build, never committed | site/content/{docs,registry,commands,...}/, site/static/{app.css,js/,openapi.json,build.json}, site/data/build.json, site/public/**, target/web/rustdoc/ | gitignored; scripts/site-check proves the build's own guards (13e, 13a) and that every composed surface is this build's (13m) |
| local, ephemeral | this checkout's state | .ai/local/**, apps/majordomus-cli/target/ | never a source of anything the site shows |
The generator is an input, and it is not in the table
Every row above classifies a file. The classification is incomplete in one way that has already cost this repository a near-miss: the derived files are a projection of the canonical sources as read by one executable, and that executable is not a file in the tree. Two builds of the same declared version can project different registries.
So an executable borrowed from another worktree — through MAJORDOMUS_BIN, which is how a
session avoids a ten-minute rebuild — is a third input, and an unsuitable one fails in the
worst available direction: it reports the tree stale, printing a remedy that regenerates,
and regenerating rewrites each document from a model the tree no longer holds. On 2026-09-10
that removed about 9,700 lines from docs/generated/changelog.json and exited zero.
Before deriving with an executable you did not just build:
env -u MAJORDOMUS_SHARE "$MAJORDOMUS_BIN" generate --check # must say: in sync
and after any derivation, before committing:
git diff --stat -- docs/generated site/data # mass deletion is corruption
project.a-tool-that-cannot-match-the-tree-refuses is the rule; the guard that enforces it
lives in the executable, so it holds for every caller rather than for one script.
Where do I edit this?
| I want to change | edit | never edit |
|---|---|---|
| a Rust capability's title, description, schema, stability, tags, benchmark or cache policy | its capability! declaration under apps/majordomus-cli/src/capability/builtin/ | docs/generated/**, site/data/registry/registry.json, any /registry/ page |
| how a capability is exposed (MCP tool, resource, HTTP route, CLI path) | the exposure of the same declaration | docs/generated/openapi.json, /docs/api/, /registry/mcp/ |
| a module's title, description or stability | its module! declaration | docs/generated/modules/*.md, /registry/modules/ |
| the native command line: a command, an argument, a default, a value set | the clap declaration in apps/majordomus-cli/src/cli.rs | docs/generated/cli.md, docs/generated/cli.json, site/data/generated/cli.json, /registry/cli/, /docs/cli/** |
| an example of a native command, or adding a command that has none | cli::EXAMPLES in the same file, beside the declaration | an example written into a template, a page or cli.md: the pages render the argument vectors the crate's tests execute |
| the benchmark regression thresholds | .ai/repo/benchmarks/rust/policy.yaml | the policy table on /registry/benchmarks/ |
| an accepted baseline | majordomus bench baseline update on the platform it measures | the baseline JSON by hand |
| what the executable is, owns, refuses; discovery; transports; side effects | apps/majordomus-cli/README.md | /registry/executable/ |
| a shell command's category, stage, reads, writes, syntax, exit codes | share/commands.yaml (semantics) and docs/CLI.md (narrative) | commands.json, /commands/<name>/ |
| a use case or an application | .ai/repo/use-cases/, .ai/repo/applications/ | catalogue.json, its route |
| a claim | docs/CLAIMS.yaml and docs/claims/<id>.md | capabilities.json, docs/SITE_CLAIMS.md, /guarantees/ |
| a doctrine | the rule file under .ai/repo/rules/ or the vendored package | doctrines.json, /doctrines/ |
| a milestone or an issue | .ai/repo/project/** | plan.json, docs/PLAN_STATUS.md, /plan/ |
| a profile, the policy's schema | share/skeleton/** | profiles.json, policy.json |
| a kind or a schema | share/kinds.yaml, share/schemas/, or .ai/repo/knowledge/ for this repository | share/allow/*.txt, the kinds table |
| the provider bootstraps | .ai/repo/policy.yaml, the provider templates | AGENTS.md, CLAUDE.md, GEMINI.md |
| how a page looks | site/templates/**, site/tailwind.css | site/data/generated/** to make a template's life easier |
| which pages the executable's section has, or how claims attach to its surfaces | scripts/generate-site-data, scripts/lib/executable-site.jq | a route list in a template or in nav.toml |
| the editorial navigation | site/data/nav.toml — intents and their fixed pages | an entry per module or capability: those routes are enumerated by the generator, and site-check refuses one typed here |
| the homepage's words | site/data/marketing.toml | a number, a claim word |
After any edit in the left column: just derive, review the diff of the derived files,
just derive-check, commit both together.
Markdown rendering
Canonical Markdown must stay readable on GitHub, so the site uses only syntax GitHub renders
natively. scripts/lib/project-markdown.awk projects three GitHub-native constructs into site
components, on the derived copy only:
| in the canonical file | on the site |
|---|---|
```mermaid fence | <pre class="mermaid">, named, linked and given its controls by the build, drawn client-side in the viewer (DIAGRAMS.md); the source stays visible without JavaScript |
> [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION] | Flowbite alert |
| a pipe table | wrapped in overflow-x-auto so wide tables scroll inside their container |
Everything else is Zola's Markdown renderer inside a Flowbite Typography container
(format dark:format-invert): headings with anchors, lists, task lists, footnotes,
blockquotes, and class-based syntax highlighting ([markdown.highlighting] style = "class",
themes ayu-light and ayu-dark). Zola writes the two theme stylesheets into site/public/;
scripts/site-build scopes the dark one under .dark so class-based dark mode selects it.
site/content-src/render-test.md exercises every construct and is linked from nowhere.
Mermaid
Client-side, from the pinned npm package, vendored to static/js/mermaid.min.js at build.
Chosen over build-time SVG because the Mermaid CLI needs a headless browser in CI and the
site must stay reproducible from bash, Node and Zola alone. No page loads the build with a
script tag: site/diagrams.js is deferred and fetches it once the page has been parsed.
It initialises with securityLevel: 'strict', takes its colours from the page's design
tokens, and draws again when the theme changes. Four diagrams are projected from data, never
drawn by hand: the task lifecycle from the outcome vocabulary, the policy-to-instruction-file
graph from the policy's projections, the site pipeline from the generator's input list, and
the core model from the README. A failed diagram leaves its source visible and never breaks
the page.
After Zola, scripts/site-build runs scripts/lib/diagrams.mjs over the built pages: every
diagram gets an id (diagram-<name>), the list of its nodes, the viewer's toolbar and a
list of the links inferred from the identities in site/data/generated. The viewer, the
address scheme and the chart component, which draws a table the page already renders with
the vendored Apache ECharts build, are described in DIAGRAMS.md
(ADR 0152).
Zola structure
site/
config.toml base_url, class-based highlighting, heading anchors, footnotes
content/ _index and one stub per page; docs/ and render-test.md are generated
content-src/ hand-written pages that get the same projection as docs
templates/
base.html head, theme-before-paint, navbar, footer, Flowbite init, Alpine
components.html Tera 2 components: badge, code, card, section_head, diagram
index.html homepage: the business hero, then the sections, all from generated data
page.html standalone page with breadcrumb and Typography body
docs-section.html /docs/ index from the section's pages
docs-page.html document with sidebar navigation
profiles.html policy.html guarantees.html getting-started.html architecture.html
404.html
partials/ navbar, footer, theme-toggle, breadcrumb, profile-cards, mermaid
data/marketing.toml the only hand-written prose
data/generated/ written by generate-site-data, committed, checked
tailwind.css theme.js diagrams.js
static/ images copied from assets/; app.css and js/ written at build
public/ output
Zola 0.23 runs Tera 2: components replace macros, import no longer exists, array indexing
is x[1], undefined variables are errors, filter and slice are gone in favour of list
comprehensions and Python-style slicing.
Tailwind, Flowbite, Alpine
site/tailwind.css follows the Flowbite quickstart for Tailwind v4: @import "tailwindcss",
@import "flowbite/src/themes/default", @plugin "flowbite/plugin",
@plugin "flowbite-typography", @source for node_modules/flowbite and the templates, and
@custom-variant dark (&:where(.dark, .dark *)) from the dark-mode guide. Flowbite v4's
semantic tokens (bg-neutral-primary-soft, text-heading, border-default, rounded-base)
are used throughout. The theme names Inter first; it is not loaded, so the stack falls to the
system font on purpose.
Flowbite's JavaScript is not loaded. The only two things the site used it for, the navbar's
mobile collapse (data-collapse-toggle) and its dropdowns (data-dropdown-toggle), are
site/nav.js, about 2 KB, which base.html loads deferred and site-check requires exactly
once per page. Removing the 134 KB bundle took the homepage from 338555 to 207140 bytes of
scripts and stylesheets. Flowbite's styles stay; they are CSS, compiled into app.css.
The homepage is a projection of site/data/registry/product.json, which the executable
writes from the features of the layer, the capability registry and the web topology. Every
chapter, mark, count, provider, kind and rule on it is read from that dataset; the template
names none of them, and scripts/site-check fails a template, a generator or a navigation
file that names a feature route or a provider's own file. The hero's second column is the
answer majordomus product matrix prints, rendered as a table rather than padded text so
the columns align at every width and a screen reader reads a table rather than a picture of
one. The words around it — the title, the mantra, the section leads and the button labels —
are marketing.toml's, which carries no number and no capability claim and is held to a
sixty-line budget.
The one thing a person decides is which features are chapters and in what order, and that
decision lives in the feature files themselves (featured and weight), not here: a
feature becomes a chapter by declaring itself one. docs/PRODUCT.md is the contract, ADR 23
is the decision, and project.product-surface-derived is the rule.
Routes that moved are declared once, in nav.toml under [[redirects]], beside the routes
that did not. The generator turns each into a Zola alias on the page it points at, so the
old address answers with a redirect; a redirect whose target the generator did not produce
fails the build rather than publishing a link into nothing. An alias page is a redirect and
not a page, so scripts/lib/site-pages.awk exempts it from the contract a page is held to.
Alpine.js is vendored and used for two things: the copy button on code snippets (x-data,
x-on:click, x-text) and the raw-policy disclosure on /policy/. Both degrade: the code
and the policy are in the HTML; only the control disappears without JavaScript.
Custom CSS: one rule, [x-cloak]{display:none!important}, inline in base.html, required by
Alpine's documentation. Inline style="" attributes are forbidden and scripts/site-check
fails on one.
Flowbite guidance consulted
The Flowbite LLM entry point (flowbite.com/docs/getting-started/llm/) points at llms.txt,
which is an index. The pages read and followed, on 2026-09-04: getting-started/quickstart
(Tailwind v4 directives), getting-started/javascript (initFlowbite), customize/dark-mode
(@custom-variant, toggle markup and script, reproduced in theme.js), components/navbar,
components/card, components/badge, components/footer, components/typography. The repository's
installed flowbite is 4.0.2, matching the documented token vocabulary.
Languages
The site is written once, in English, and offered in every language
site/i18n/languages.json declares (ADR 0218). Nobody writes a second language: each copy is
derived from the English build.
| piece | what it is |
|---|---|
site/i18n/languages.json | the source language, the languages offered, each named in itself |
site/i18n/<lang>/memory/*.jsonl | the translation memory: one line per English segment, keyed by its hash |
site/i18n/<lang>/guide.json | the style and glossary every translator of that language is given |
site/i18n/<lang>/edition.toml | the language the directory declares, which english-only-check holds to its declaration |
scripts/lib/site-i18n.mjs | the == languages pass of scripts/site-build: segments every page, writes /<lang>/ |
scripts/site-translate | carries the missing segments to a translator and its answers into the memory |
scripts/lib/site-i18n-probe.mjs | follows every link of every page, in every language, in Chromium |
partials/language-switch.html, partials/language-detect.html | the switch in the bar, and the choice of language on arrival |
The pass. After Zola, the accessibility pass and the diagrams, every built page is cut into
segments — each run of inline content between block boundaries, and each attribute a reader
meets (title, alt, aria-label, the description) — with inline markup replaced by numbered
placeholders (<t1>…</t1>, and <t2/> for code and images). Code, pre, SVG and anything
marked translate="no" is never a segment. The copy under /<lang>/ is the English page with
every segment the memory knows replaced, every internal link pointed at that language's copy,
lang, the canonical URL and the hreflang alternates set, the switch pointed at each copy and
the machine-translation notice revealed. What the memory does not know stays English, is
counted in /<lang>/i18n.json, and is listed with the pages it is on in
target/site-i18n/missing.json. A build calls no model: it is a pure function of the English
pages and the memory.
Translating. A changed English segment has a new key, so its old translation is never served.
scripts/site-build # lists what the memory lacks
scripts/site-translate status # how much is open
scripts/site-translate run --advisor <id> # translate through an advisor of the catalogue
scripts/site-translate export --out DIR --min-pages 2 --batch-chars 16000 # batches for subagents
scripts/site-translate import DIR/*.json # admit filled batches into the memory
A translation is admitted only when it carries every placeholder of its English exactly once, nested the same way; anything else is refused and the English is served.
A value inside a sentence. The memory keys a segment by its whole text, digits included,
so a sentence that carries a count, a date or a version gets a new key each time the value
changes, and its copy falls back to English until the memory is filled again. Until a number
in a segment is a placeholder of its own (I3758), a template wraps each derived value in an
inline element the pass does not enter: <data value="…">, or a <span translate="no">. That
element stands in the segment as a void placeholder, so the words around it keep one key
whatever the value is. The plan's figures are written this way
(site/templates/plan-section.html); a block element marked translate="no" is a boundary
instead, and splits the sentence in two.
The reader's language. The switch is Flowbite's navbar language dropdown: every language
named in itself, lang and hreflang on each link, aria-current on the one being read. A
reader who arrives from outside the site on an English page, has chosen nothing and whose browser
prefers a declared language is sent to that page's copy in it; a choice made with the switch is
stored and kept. A link followed inside the site, a reload and a crawler stating no such
preference are never redirected, so every URL means the page it names.
Authored pages. A page written in another language ([extra] lang, declared in
english-only-allow.txt) is carried into each copy unchanged. Its copies name it canonical, and
a pair of authored counterparts ([extra] alternate) keeps its own alternates.
Every link, every language. scripts/lib/site-i18n-probe.mjs loads every page of every
copy in Chromium, answered from the build itself, and holds each to its language: every link
lands on a built file, a link inside a copy stays in that copy's language, the switch leads to
each copy and marks the current one, and the alternates name each other both ways.
Adding a language. One entry in languages.json, a guide.json, an edition.toml, its
declaration in scripts/ci/english-only-allow.txt, and a translation run. No template, generator
or page changes.
The bar of the other surfaces. /ui wears the site's bar (scripts/ui navbar), and its
language switch leads to the site's front page in each language until the UI itself is offered
in them (intent one-bar-and-one-language-on-every-surface).
Routes
Every tile on the homepage is a link to a page with its own title, description, canonical URL and Open Graph metadata. The route classes and their sources:
| route | source | template |
|---|---|---|
/ | site/data/registry/product.json and product-graph.json (chapters, interfaces, matrix, providers, kinds, doctrine), marketing.toml (the words), readme.json (the finish contract), distribution.json (the install command), the why section | index.html |
/why/, /why/<slug>/ | site/content-src/why/*.md | why-section.html, why.html |
/features/, /features/<id>/, /features/matrix/ | site/data/registry/product.json and product-graph.json, with each page's prose read from the feature's own file | features-section.html, feature.html, features-matrix.html |
/getting-started/ | project.json, policy.json, lifecycle.json | getting-started.html |
/supervises/, /supervises/<slug>/ | readme.json (What it does rows, cross-linked to commands and claims by keyword) | supervises-section.html, responsibility.html |
/commands/, /commands/<name>/ | commands.json (each command's section of docs/CLI.md) | commands-section.html, command.html |
/profiles/, /profiles/<slug>/ | profiles.json | profiles.html, profile.html |
/policy/ | policy.json including the raw file | policy.html |
/guarantees/, /guarantees/<status>/, /guarantees/<id>/ | capabilities.json, docs/claims/<id>.md | guarantees.html, status.html, claim.html |
/limitations/, /roadmap/ | readme.json sections by heading | readme-section.html |
/architecture/ | site/content-src/architecture.md, source.json, diagrams.json | architecture.html |
/docs/, /docs/<doc>/ | docs/*.md listed in docs/README.md | docs-section.html, docs-page.html |
/docs/api/, /docs/api/<tag>/, /docs/api/shared/, /openapi.json | openapi.json (from docs/generated/openapi.json) | api.html (the index), api-tag.html (a page per tag, and shared types) |
/docs/cli/, /docs/cli/<command path>/ | cli.json (from docs/generated/cli.json, itself the clap declaration and cli::EXAMPLES) | docs-cli.html, docs-cli-group.html, docs-cli-command.html |
/doctrines/, /doctrines/<slug>/ | doctrines.json | doctrines-section.html, doctrine.html |
/use-cases/, /use-cases/<id>/, /applications/, /applications/<id>/ | catalogue.json | use-cases-section.html, use-case.html, applications-section.html, application.html |
/plan/, /plan/dag/, /plan/<id>/ | plan.json | plan-section.html, dag.html, milestone.html, issue.html |
/evidence/, /map/ | claims-graph.json, architecture.json | evidence.html, map.html |
/registry/ | site/data/registry/registry.json, executable.json | registry.html |
/registry/executable/ | apps/majordomus-cli/README.md, executable.json (claims) | registry-executable.html |
/registry/modules/, /registry/modules/<id>/ | the dataset's modules, executable.json | registry-modules.html, registry-module.html |
/registry/capabilities/, /registry/capabilities/<slug>/ | the dataset's descriptors, benchmarks and coverage, executable.json (module, API anchor, source, claims, tests) | registry-capabilities.html, registry-capability.html |
/registry/cli/ | the dataset's cli (the clap declaration) | registry-cli.html |
/registry/mcp/ | the dataset's mcp (tools, resources, protocol) | registry-mcp.html |
/registry/benchmarks/ | the dataset's benchmarks (targets, coverage, policy, baselines) | registry-benchmarks.html |
/registry/artifacts/ | artifacts.json (the generator's own manifest) | registry-artifacts.html |
/rustdoc/, and every page rustdoc writes beneath it | target/web/rustdoc/, written by scripts/rust-check --doc and composed by scripts/site-build from docs/generated/web.json | — (rustdoc's own pages; index.html is the producer's landing page) |
/build.json | scripts/site-build | — (served raw) |
/render-test/ | site/content-src/render-test.md, noindex | docs-page.html |
A capability's route slug is its id with . and _ replaced by - (objects.search →
/registry/capabilities/objects-search/, repository.scope_classify →
/registry/capabilities/repository-scope-classify/): Zola slugifies every path that way,
so the dataset names the route Zola actually serves. The API reference's operation anchors
are HTML ids Zola never touches, so they replace only the . (#op-objects-search,
#op-repository-scope_classify); executable.json carries both spellings (slug,
api_anchor), so the two link each other without a table.
A native command's route is its path under /docs/cli/, one segment per word, with a
trailing slash: ["majordomus", "bench", "baseline", "update"] is
/docs/cli/bench/baseline/update/. The rule is one function in the executable
(cli::route), the route it produced is carried in docs/generated/cli.json, and every
consumer downstream — the site generator, the templates, site-check, site-probe — reads
that field rather than deriving one of its own. A command that has commands under it is a
Zola section so that its children have routes beneath it, and a command that has none is a
page; which it is comes from the data, so a command that grows a subcommand changes shape by
itself. site-check compares the declared routes and the built ones as sets, both ways: a
command with no page fails, and a page whose command the executable no longer has fails as an
orphan.
/docs/cli/ belongs to the native command line, the Rust executable's. docs/CLI.md
specifies the shell tool bin/majordomus — a different program, whose structured
reference is /commands/ — and renders at /docs/cli-specification/. The rule that turns a
document into a route is one function in the site generator (doc_slug), and the route it
produced is recorded in docs.json, so the link rewriting, the pages and site-check read
it instead of each computing a slug. Do not describe share/commands.yaml or docs/CLI.md
as sources for the Rust executable: they are the shell tool's, and the two command surfaces
are documented apart on purpose.
Navigation is site/data/nav.toml: a handful of intents (Why, Get started, Concepts, Plan,
Trust, Executable, Reference), rendered as Flowbite dropdowns on desktop and as labelled flat
lists inside the collapsed menu on phones. It holds editorial intent — which fixed pages an
intent leads to — and never an enumeration: the modules and capabilities are listed by the
generator, and scripts/site-check refuses a module or capability route typed into it.
scripts/site-check verifies every navigation and homepage link resolves, every tile class
has its page, for the executable's section that every module and capability of the dataset
has its page and no page exists without its entry, and for the command line that the routes
under /docs/cli/ are exactly the commands the executable declares, that each page renders
its own usage, arguments and example anchors and links up and down, and that no command
route is typed by hand into a template, the generator or the navigation. scripts/site-probe
then fetches every declared command route from a running server and reads the deepest command
page back.
Responsive strategy
Mobile-first: base classes describe the phone layout, sm:/md:/lg:/xl: add columns.
Every <pre> and <table> sits in a scroll container, either an explicit overflow-x-auto
wrapper or a Typography container carrying [&_pre]:overflow-x-auto and
[&_table]:overflow-x-auto. Grid items that hold code carry min-w-0. Validation is
measured, not eyeballed: scripts/site-probe builds a copy under the deployment prefix, serves
it locally, loads every route in headless Chrome inside iframes of 320, 390 and 1280 px and
asserts document.scrollWidth == clientWidth, then exercises the mobile menu, a navigation
dropdown and the theme toggle and counts rendered Mermaid diagrams. It runs in the Pages
workflow and skips with a notice where no Chrome exists. test/cases/09_site_mobile_first.sh
lints the built HTML for the structural causes of overflow without a browser.
Accessibility
One h1 per page, <main>, <nav aria-label>, a skip link, aria-current on active
navigation, aria-expanded/aria-controls on the menu toggle, aria-label on icon-only
controls, role="note" on callouts, visible focus rings (focus:ring-*, never removed),
Flowbite's contrast tokens in both themes. Everything except the copy button and the
disclosure works without JavaScript.
GitHub Pages deployment
The site is published by .github/workflows/pages.yml, which triggers directly on a push to
master whose paths can change the site and runs beside validate.yml rather than after it.
Its deploy job proves the committed site/data/generated is current for the tree by comparing
the tree's canonical input hash with the one source.json carries
(scripts/generate-site-data --fingerprint), renders it (scripts/pages build), runs every
static check over the output (scripts/pages check), and pushes gh-pages. A tree whose
committed derived data is stale is refused rather than regenerated.
The crate's rustdoc reference is part of those bytes and is not committed, so a rustdoc job
of its own, which the deploy job needs, produces it from the same commit through the composite
action .github/actions/rustdoc — the one the validation workflow uses too — and hands it over
as an artifact of the run. The deploy job fetches that tree before it renders,
scripts/site-build composes it at /rustdoc from docs/generated/web.json, and
scripts/site-check refuses a composed surface that is missing or was built from another
commit (section 13m). Once the site serves the deployed commit, scripts/pages verify-rustdoc
checks the public reference against it, and that step is a hard one: a deployment whose public
reference is absent, broken or stale fails the run. RUSTDOC.md has the whole
path and ADR 86 the decision.
Everything that decides whether a change may merge — the behavioural suite, the crate's
gates, coverage, the macOS suite, the benchmark check and the browser probe — runs on the same
commit in validate.yml and gates the ci status. The publication path carries what can make
the published bytes wrong and nothing else; the reasoning, the budgets and the measurements
are GITHUB_PAGES_PERFORMANCE.md, and CI.md has the
validation shape. The rust job runs majordomus generate --check and the site job
scripts/generate-site-data --check; locally scripts/derive-check (just derive-check)
runs both, and scripts/derive (just derive) regenerates every derived artifact in
dependency order.
GitHub Pages serves the gh-pages branch (source: branch, path /), and whoever pushes
that branch deploys. One script does it, scripts/site-deploy: it refuses a dirty tree and a
commit origin/master does not contain, runs the gate (generate-site-data --check,
majordomus generate --check, site-build, site-check, --probe on request), copies
site/public into a worktree of gh-pages with .nojekyll, commits it with the source
commit in the message, and pushes. GitHub's own "pages build and deployment" then serves it
in under a minute; the footer of every page names the commit it was built from, so the
deploy is verifiable with curl. Unchanged output is not pushed twice, and a --dry-run
shows the commit it would push. test/cases/96_site_deploy.sh runs the whole path against a
local bare remote: every refusal, the first deploy, the no-op redeploy, and the check that
site/public was built from HEAD.
The job of .github/workflows/pages.yml is the gated form of the same command: it builds and
checks the commit and then runs scripts/site-deploy --skip-build. A person runs
scripts/site-deploy (or just site-deploy) when the operator wants the site live without
waiting for the Actions queue, and that path — no queue in front of it at all — is why the
branch source is kept instead of the native Pages artifact flow, which would make a workflow
run the only way to publish. The procedure is .ai/repo/skills/deploy-site/SKILL.md. Pointing
Pages at the branch is a one-time scripts/site-deploy --configure-pages.
The documentation a deploy publishes
The build writes /docs/index.json last, with scripts/lib/docs-index.mjs. It reads the
pages the build just produced, the composed surfaces' among them (the crate's reference at
/rustdoc), so it lists no route of its own. Each page has its route, its
title and its description; an entity page is also joined to its object, and the index
carries the /build.json identity of the build. The build fails when a route that
site/data/publication.toml publishes, or an entity page, was not built. After the site
serves the pushed commit, the workflow runs scripts/pages verify-docs --commit <sha>. The
public index must name that commit, and each of the following must answer 200: the
documentation root, every route the catalogue publishes a kind at, one page of every entity
kind, /openapi.json and /build.json. Run the same command by hand against any deployed
commit. test/cases/500_docs_index.sh holds the generator and the probe to their refusals
against a served fixture.
The UI a deploy publishes
The site carries majordomus ui at /ui/ as a snapshot read at the published commit (ADR
0141, docs/MAJORDOMUS_UI.md). The workflow's ui-snapshot job builds the executable from
that commit and runs majordomus web report ui-snapshot; it runs beside the rustdoc job,
and the deploy job waits for both. After scripts/pages check and before the push, the deploy
job places the snapshot at the mount its own surface.json declares. It refuses a snapshot
that is absent, one read at another commit, and a mount the site itself already built.
The Cockpit is published the same way at /cockpit/ (ADR 0147). The same job runs
majordomus web report cockpit-snapshot after the UI's, because the Cockpit's snapshot reads
first what the UI's links to and then leads those links to its own pages; both directories
are uploaded once that step is over. The deploy job places every snapshot the model declares
at the mount the topology gives it, and after the push
scripts/pages verify-ui --surface cockpit asks the public site for it.
The snapshot is a generated surface, known only where its producer ran, so it is not in the
committed topology and scripts/site-build does not compose it: a local build of the site has
no /ui/ unless that step is repeated by hand. The site's checks do not judge its pages.
That a publication holds it is declared once, in .ai/repo/ci/pages.yaml under
deploy.snapshots: the served surface it is a snapshot of, where the producer writes it, the
producer, and the attribute its pages state their commit with. The mount is the topology's
mount of that surface, and scripts/pages snapshots prints the resolved rows. Three readers
hold a publication to that entry (project.the-published-ui-is-the-trunks):
scripts/site-deployrefuses a site whose tree has no snapshot at the mount, one read at another commit thanHEAD, or one that declares another mount, and says how to place it. A deploy from a terminal therefore cannot publish the site without/ui/.--release-metadatachanges two files of what is already served and is not asked.scripts/pages verify-uiasks the public site, once it serves the deployed commit:/ui/surface.jsonnames that commit, the landing page's banner states it,/ui/snapshot.jsonrecords it and says what ended the reading, and every page the record lists answers 200. It prints how many pages it asked for, how many--max-pagesleft unasked, how many the snapshot itself left out and what stopped it. Exit 10 is a UI that is missing, another commit's or not whole; 12 is a site that never answered. The deploy'sthe UI it publishesstep runs it and is notcontinue-on-error.- The
pages-livegate asks the same probe of the published commit (section 2c ofscripts/ci/pages-check), so the live/ui/is held to the trunk within the window the site is.
test/cases/2983_the_published_ui_is_the_trunks.sh holds all three against a served fixture.
The release the site offers
Which release is the latest is one fact with three public surfaces: the homepage (the navbar
badge, the hero and the trust strip's data-release), /releases/latest.json, which the
installer resolves, and the newest GitHub release. A release reaches them in that order — GitHub,
then latest.json from the release's publish job (scripts/site-deploy --release-metadata,
ADR 0131), then the pages from the build of master that job asks for — and the record that
archives it lands later still, through its own pull request.
So the pages do not read the record. scripts/site-build derives the release the site offers as
the newer of the record's projection (site/data/registry/distribution.json) and the
releases/latest.json that origin/gh-pages serves, read from git and named in build.json
(release.source, release.published_from); base.html sets offered from it and every page
reads offered. When the published release is the newer, the build serves its two documents, so
the metadata beside the pages names what they offer. scripts/site-build --release prints the
derivation alone.
It is checked three times: scripts/site-check (section 17b) refuses a build whose homepage,
/build.json and /releases/latest.json disagree, or whose two committed projections of the latest release do;
the deploy's the release it offers step runs scripts/pages release-live once the site serves
the new commit; and scripts/ci/pages-check asks the same probe as section 5 of pages-live, on
the schedule and at finish. A disagreement younger than deploy.release_window
(.ai/repo/ci/pages.yaml) after GitHub's release is a publication in flight; older, or a site
offering a version GitHub has not published, is a failure. The rule is
project.the-site-offers-the-release-it-published.
When a deploy does not happen
Publication is two steps owned by two parties, and either can be missing while everything
visible stays green. This repository pushes gh-pages; GitHub's own "pages build and
deployment" turns that branch into the served site, and it has its own status, which can be
errored for a branch that was pushed perfectly — it was, on 2026-09-10 at 17:35, and the
site served the previous commit for 27 minutes with nothing red anywhere.
The workflow's own conclusion does not answer the question either. pages.yml serialises its
runs and never cancels the one in flight (cancel-in-progress: false; cancelling it starved
publication on 2026-09-12, when every superseding run was itself superseded), but GitHub drops
an older pending run for a newer one, and a cancelled run is not a failure: on 2026-09-09/10,
while a push still cancelled the run in flight, fourteen of seventeen cancelled runs had already
pushed gh-pages before they died, and no reader could tell them from the three that had not.
So the run says which it was — an annotation and a line in its summary — and cancellation
itself cannot strand the site, because a pending run is only ever dropped for a newer run of
the same workflow, and the run in flight always finishes: the chain ends in a run that
publishes or in one that goes red.
What is left stale is a run that never published and whose successor then failed to
build. Nothing in the deploy can report that, because by then there is no deploy.
The reader is the pages-live gate (scripts/ci/pages-check), which runs on every full
validation and asks four things: the published commit is on master, master has not moved past
it beyond the deploy window (30 minutes, sized from a 25-minute Actions queue observed on
2026-09-10), the live site serves what gh-pages published, and GitHub's own build of that
branch did not error. A section of its own asks a fifth: the reference at /rustdoc/ is
whole and was built from the commit the site serves, by the same scripts/pages verify-rustdoc
the deploy runs. Another asks the same of the UI's snapshot at /ui/, by
scripts/pages verify-ui. The trigger paths it uses to decide whether a publication is owed are
scripts/pages paths — the same list that is the workflow's own paths: — so a commit that
cannot change the site does not owe a deploy.
The site answers at its own domain, and one file keeps it there. site/static/CNAME names the
host; the build copies it into site/public like any other static file and scripts/site-deploy
publishes it with the rest. GitHub Pages reads that file on every deploy and unsets the custom
domain the moment a published tree stops carrying it — which would take the advertised install
command down with the site, so scripts/site-check refuses a build whose CNAME and base_url
disagree. Those are the two places the host is written, and there is no third: base_url is what
every generated link and every check resolves against, CNAME is what GitHub reads. The project
address the site was published at before, under korczis.github.io, redirects here.
Sync guarantee
scripts/derive-check is the one read-only gate; it composes the generators' own checks
and names every stale artifact before it exits:
majordomus generate --checkderives every projection of the registry, the schemas, the policy and the index again and compares byte for byte:docs/generated/**,share/allow/**, the provider bootstraps,site/data/registry/registry.json.scripts/generate-site-data --checkregenerates into a temporary directory and diffs every generated file,docs/SITE_CLAIMS.mdanddocs/PLAN_STATUS.mdagainst the committed ones;source.jsonis compared by its input hash, which covers every canonical file the generator reads,docs/generated/registry.jsonanddocs/generated/openapi.jsonamong them, so a change to the executable's code moves the site's hash.
The chain is tested edge by edge. apps/majordomus-cli/tests/projections.rs proves that a
descriptor changed in code moves the OpenAPI document, the reference, the manifest and the
site dataset, and only the sections that project it (an exposure removed moves the routes
and the coverage, not the tools; a description moves the descriptor everywhere), and that
the dataset's tools are the MCP surface's and its routes the document's.
test/cases/95_executable_reference.sh proves that a capability added to the manifest gains
its route, its index entries and its links from the site generator alone, that one removed
loses them with no orphan left, and that a manifest naming an unknown module, a missing
source file or another schema is refused. scripts/site-check proves the rendered side:
every module and capability of the dataset has its page, no page exists without its entry,
the CLI, MCP and benchmark pages render every command, tool, target and baseline, every
capability page links its module and its operation, and no template, generator or
navigation file names a capability by hand. test/cases/11_site_derivation.sh changes the
version, a profile, a principle, a policy value, a claim and a document heading in a
scratch copy and asserts each change appears in the derived data; test/cases/10_site_data.sh
proves a missing path or an untested guaranteed claim fails the generator;
test/cases/51_derived_artifacts_committed.sh proves the committed derived files match
their sources and that none embeds the commit it lands in.
The commit a page was built from is not a derived file's business: scripts/site-build
writes site/data/build.json (for the footer, the architecture page and the roadmap's "observed
at") and site/static/build.json, served at /build.json, with the commit, that commit's time
(commit_time, never the build's clock), whether the tree was dirty, the site's input hash, the registry and index fingerprints, and under surfaces each
composed surface with the commit its producer declared; scripts/site-check proves the served
file names HEAD and the data the site was built from, and that every composed surface was built
from that same commit, so a deployment can be verified from outside against the commit that was
meant to deploy.
Local development
npm ci # Tailwind, Flowbite, Alpine, Mermaid — pinned
brew install zola # or the release binary; CI pins 0.23.4
just derive # every committed derived artifact, in order (scripts/derive)
just derive-check # is every committed derived artifact current? writes nothing
just test # the gates CI would run for this working tree (scripts/ci/run-plan)
scripts/site-serve # generate, build, serve the built pages at http://127.0.0.1:1111/
scripts/rust-check --doc # the crate's reference, target/web/rustdoc — site-build composes it and refuses without it
scripts/site-build # production build into site/public/
scripts/site-check # the static checks CI runs
scripts/site-probe # the browser-measured checks (needs Chrome; --quick for one page per section)
SITE_PROBE_JOBS=4 scripts/site-probe # as CI runs it: four routes at a time
The one workflow after a change to anything canonical — a Rust declaration, a document, a
claim, a command's semantics — is: just derive, review the diff of the derived files (it
is the change, seen from every projection), just derive-check, commit both. Forgetting
the first step is caught by the third, and by CI.
Adding new canonical data
Add the field to the file that owns it, read it in scripts/generate-site-data, render it in
a template, and extend test/cases/11_site_derivation.sh with the change-and-assert pair.
Commit the regenerated site/data/generated/. Do not add a field to the JSON by hand.
Adding new site components
Define a Tera 2 component in site/templates/components.html, using Flowbite markup from the
component's documentation page, with mobile classes first. Call it as {{ <name attr="…" /> }}.
If it needs a class Tailwind cannot see in the templates, it does not belong here.
What must never be edited manually
docs/generated/**, share/allow/**, site/data/registry/**, site/data/generated/**,
site/content/{docs,profiles,guarantees,supervises,commands,doctrines,use-cases,applications,plan,why,registry}/**,
site/content/render-test.md, docs/SITE_CLAIMS.md, docs/PLAN_STATUS.md,
site/static/app.css, site/static/js/**, site/static/images/**, site/static/openapi.json,
site/static/build.json, site/data/build.json, site/public/**, CLAUDE.md, AGENTS.md.
Change the canonical file; just derive; rebuild.