What it means
A maintainer changes a canonical file — a Rust declaration, a document, a claim, a command's semantics — and runs just derive. Every committed projection of that file is regenerated, in the order the projections depend on each other, and the diff is the change seen from every interface. just derive-check proves the committed tree is what its sources produce, writes nothing, and names every stale artifact in one report; CI and the Pages workflow run the same script. Nobody remembers which generator to run after which edit; the graph does.
How it works
scripts/derive is the graph spelled out once, in three stages. Stage A, majordomus generate: the projections that depend on code alone — docs/generated/{openapi.json,registry.json,capabilities.md,cli.md,modules/*.md}, share/allow/*.txt, the provider bootstraps. Stage B, scripts/generate-site-data: the site's data and content and the derived documents (docs/SITE_CLAIMS.md, docs/PLAN_STATUS.md), from the canonical files and from stage A's registry manifest and OpenAPI document. Stage C, majordomus generate again: the site's registry dataset and the benchmark matrix, which read the index of the layer, and the documents stage B wrote are objects of that index. No stage reads its own output, and stage C's outputs are read by nothing but the site build, so the graph has no cycle and a second run on a clean tree changes nothing.
scripts/derive-check runs majordomus generate --check and scripts/generate-site-data --check, both before exiting, and exits 10 when either found a stale file. The first covers every projection of the registry, the schemas, the policy and the index; the second covers the site's data by an input hash that includes the registry manifest and the OpenAPI document, so a capability edited in Rust is caught by the first and a manifest regenerated without the site by the second.
The commit a page was built from is a fact of the build, not of a derived file: scripts/site-build writes site/data/build.json and site/static/build.json (served at /build.json) with the commit, whether the tree was dirty, the site's input hash and the registry and index fingerprints; neither is committed. source.json carried the commit before, which made the site generator non-idempotent on a clean tree and exempted one file from the rule every other derived file is held to.
How to see it
git status --short # clean
just derive # three stages, ~30 s
git status --short # still clean: nothing to regenerate
just derive-check # every derived artifact is current
sed -i '' 's/Search objects/Find objects/' apps/majordomus-cli/src/capability/builtin/objects.rs
just derive-check # exit 10: docs/generated/... (differs), site/data/registry/registry.json (differs)
just derive && just derive-check # in sync again, with the change in every projection
git checkout apps/majordomus-cli docs siteWhat it does not cover
The generators own their outputs and their checks; the scripts orchestrate and add nothing of their own. A derived file that a generator does not compare (there is none today) would not be compared here either. The build-only files (site/content/**, site/public/**, build.json) are rebuilt by scripts/site-build and proven by scripts/site-check, not by this gate.
Why it exists
Two generators had grown side by side, each with its own entry point and its own check, and the order between them was a comment in the justfile that was wrong once the site consumed the executable's projections. test/cases/51_derived_artifacts_committed.sh proves the committed derived files match their sources and that none of them embeds the commit it lands in — source.json included, since the exemption is gone — and, where cargo is present, that scripts/derive-check passes on this checkout.