a-derive-does-only-the-work-its-inputs-changed — A derive does only the work its inputs changed, and can say why
The derivation is a typed graph the executable owns: every producer is a node with declared inputs and outputs, every node has a key computed from the bytes it reads, a read-only plan says for each node whether it would be reused or regenerated and why, and an incremental run regenerates only the nodes whose keys moved. The full derivation stays as the oracle, and a test holds the incremental result byte-identical to it.
PLANNED 0 of 9 issues done
- ready
- 2
- blocked
- 7
- active
- 0
- verify
- 0
- done
- 0
- cancelled
- 0
Problem
Every derive regenerates everything. scripts/derive builds the executable, runs majordomus generate over every target, runs scripts/generate-site-data over the whole site (use-case scenarios included), runs majordomus generate over every target a second time and rewrites the merge policy, whatever changed; a one-line edit of a rule costs what a change to the generator costs, and docs/OFFLOAD.md records that cost as about ten minutes on an idle machine and up to an hour on the shared one. The order of the stages is a comment at the top of a shell script, so nothing can be asked which producer owns an output or which inputs it read. The owner's brief of 2026-10-12, campaigns/majordomus-incremental-derive-prompt-pack/README.md, asks for the full regeneration to be replaced by a typed, content-addressed, incremental derivation graph without weakening the full derivation or the freshness checks; its pages 01 to 04, 06, 07, 10 and 11 are what this milestone plans.
Outcome
The derivation is a typed graph the executable owns: every producer is a node with declared inputs and outputs, every node has a key computed from the bytes it reads, a read-only plan says for each node whether it would be reused or regenerated and why, and an incremental run regenerates only the nodes whose keys moved. The full derivation stays as the oracle, and a test holds the incremental result byte-identical to it.
Current state
Read on 2026-10-13, not re-read when revived on 2026-10-18. scripts/derive runs, under the machine's derive lock (lib/machine_lock.sh), a cargo build unless MAJORDOMUS_BIN is set, then stage A (majordomus generate --strict, all 17 targets of Target::ALL in apps/majordomus-cli/src/generate.rs), stage B (scripts/generate-site-data, 2622 lines of shell, which runs majordomus usecase run for every scenario), stage C (the same generate over all targets again) and stage D (scripts/gitattributes). No stage is skipped when its inputs are unchanged; the only saving is that generate::write leaves a file alone when its bytes already match. scripts/generate-site-data carries one fingerprint over its whole input list (--fingerprint, --inputs), and its --check stages a full generation and diffs it. The executable has no derive command; majordomus offload derive moves the same full derive to another machine. test/cases/400_derivation_is_one_graph.sh proves the stage order with stand-in generators. .github/workflows/validate.yml allows generate-site-data --check 2700 seconds.
Desired state
One module of the crate declares the derivation graph and answers it as a capability. A derive on a tree whose inputs did not change runs no generator. A changed input regenerates the nodes that read it and the nodes downstream of them, and no others. majordomus derive plan and majordomus derive explain answer what would run and why without writing. scripts/derive --full and scripts/derive-check keep their behaviour and exit codes, and a case compares the two paths byte for byte.
Scope
- .ai/repo/adrs
- .ai/repo/project
- .ai/repo/rules/project
- apps/majordomus-cli/src/derivation
- apps/majordomus-cli/src/generate.rs
- apps/majordomus-cli/src/generation.rs
- apps/majordomus-cli/src/commands
- apps/majordomus-cli/src/capability/builtin
- apps/majordomus-cli/src/cli.rs
- apps/majordomus-cli/src/ui
- apps/majordomus-cli/tests
- scripts/derive
- scripts/derive-check
- scripts/generate-site-data
- scripts/gitattributes
- test/cases
- docs
Out of scope
- A store of outputs shared between worktrees or with CI, and making the incremental path the default: milestone a-derivation-computed-once-is-reused.
- Narrowing what a committed fingerprint covers so that an unrelated merge does not invalidate it: milestone derived-data-integrity and I1409.
- Rewriting scripts/generate-site-data in Rust. Its sections become nodes of the graph; its migration off shell is the automation inventory's (ADR 0069).
- A daemon, a database or a second scheduler outside the crate.
- Weakening scripts/derive-check, the derived merge driver or the generation guard that refuses a borrowed executable.
Acceptance criteria
- One capability answers the derivation graph, and every path in docs/generated/artifacts.json and every tree scripts/generate-site-data publishes belongs to exactly one node of it
- A graph with a cycle, an output claimed by two nodes or a generator no node declares is refused, and the refusal names the node
- A node's key changes when a declared input is edited, renamed, deleted or added and when the executable's generation changes, and does not change when an undeclared path changes
- A second incremental derive on an unchanged tree runs no generator and writes no file
- A missing or altered output is regenerated even when the node's key did not move
- For every fixture of the differential case, the tracked files an incremental derive leaves are byte-identical to those of a full derive, deletions included
- derive plan and derive explain write nothing, and explain names the input that moved a node's key
- scripts/derive-check exits 0, 10 and 15 for the same trees as before
Validation
- bin/majordomus plan validate
- cargo test --manifest-path apps/majordomus-cli/Cargo.toml --lib derivation
- MJ_TEST_JOBS=2 bash test/run.sh 400_derivation_is_one_graph 51_derived_artifacts_committed 104_strict_derivation
- scripts/derive-check
- majordomus generate --check
Evidence required
- derive_baseline_measurements
- derivation_graph_capability
- node_key_property_tests
- warm_no_op_runs_no_generator
- incremental_equals_full_case
- omitted_edge_turns_the_gate_red
Risks
- An input a generator reads and the graph does not declare is silent in the dangerous direction: the node is reused and the tree is stale while every check that trusts the key reads green. scripts/derive-check must go on comparing bytes, and the differential case must include a deliberately omitted edge.
- ADR 0041 holds that inputs unchanged is not proof. A reused node is a saving in a derive and never a verdict in a gate.
- Stage B is shell and runs scenarios with effects in temporary repositories; declaring its reads completely may cost more than the split saves, in which case the node stays coarse.
- The subsystem is touched by every branch, so each change lands into contention with whatever else is deriving.
Issues
Each one is an execution contract with its own acceptance criteria, validation command and required evidence. The wave column is the layer of the dependency graph it sits in.
| issue | status | wave | depends on | title |
|---|---|---|---|---|
| I2521 | READY | 0 | — | What a derive costs is measured per stage and per kind of change, and every file a stage reads is listed |
| I2522 | BLOCKED | 1 | I2521 | The derivation is a typed graph in the crate: every producer is a node with declared inputs and outputs, answered by one capability |
| I2523 | BLOCKED | 2 | I2522 | A node's key is computed from the bytes its producer reads, the index membership it enumerates and the generation of the executable |
| I2524 | BLOCKED | 3 | I2523 | majordomus derive plan and derive explain say, without writing, which nodes would run and which input moved each one |
| I2525 | BLOCKED | 4 | I2524 | An incremental derive runs only the nodes its plan marks, a no-op derive runs no generator, and the full derive stays as it is |
| I2526 | BLOCKED | 5 | I2525 | The site generator's sections are nodes with inputs of their own, and the use-case scenarios run only when what they exercise changed |
| I2527 | BLOCKED | 5 | I2525 | An incremental derive equals a full derive byte for byte, and an omitted dependency turns a gate red |
| I2528 | BLOCKED | 4 | I2524 | The UI shows the derivation graph and its last plan, and one documentation page explains the model |
| I2847 | READY | 0 | — | every file the site generator reads by a literal path is one of its declared inputs |
The graph
flowchart LR
I2521["I2521<br/>What a derive costs is measured per stage and per kind of change, and every file a stage reads is listed"]:::ready
I2522["I2522<br/>The derivation is a typed graph in the crate: every producer is a node with declared inputs and outputs, answered by one capability"]:::blocked
I2523["I2523<br/>A node's key is computed from the bytes its producer reads, the index membership it enumerates and the generation of the executable"]:::blocked
I2524["I2524<br/>majordomus derive plan and derive explain say, without writing, which nodes would run and which input moved each one"]:::blocked
I2525["I2525<br/>An incremental derive runs only the nodes its plan marks, a no-op derive runs no generator, and the full derive stays as it is"]:::blocked
I2526["I2526<br/>The site generator's sections are nodes with inputs of their own, and the use-case scenarios run only when what they exercise changed"]:::blocked
I2527["I2527<br/>An incremental derive equals a full derive byte for byte, and an omitted dependency turns a gate red"]:::blocked
I2528["I2528<br/>The UI shows the derivation graph and its last plan, and one documentation page explains the model"]:::blocked
I2847["I2847<br/>every file the site generator reads by a literal path is one of its declared inputs"]:::ready
I2521 --> I2522
I2522 --> I2523
I2523 --> I2524
I2524 --> I2525
I2524 --> I2528
I2525 --> I2526
I2525 --> I2527
classDef done stroke:#16a34a,fill:#dcfce7,color:#14532d,stroke-width:2px
classDef active stroke:#2563eb,fill:#eff6ff,color:#1e3a8a,stroke-width:2px
classDef verify stroke:#7c3aed,fill:#f5f3ff,color:#4c1d95,stroke-width:2px
classDef ready stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef planned stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef blocked stroke:#b45309,fill:#fffbeb,color:#78350f,stroke-width:2px
classDef cancelled stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2px
classDef superseded stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2pxLinked from this diagram (9)
- I2521 What a derive costs is measured per stage and per kind of change, and every file a stage reads is listed
- I2522 The derivation is a typed graph in the crate: every producer is a node with declared inputs and outputs, answered by one capability
- I2523 A node's key is computed from the bytes its producer reads, the index membership it enumerates and the generation of the executable
- I2524 majordomus derive plan and derive explain say, without writing, which nodes would run and which input moved each one
- I2525 An incremental derive runs only the nodes its plan marks, a no-op derive runs no generator, and the full derive stays as it is
- I2526 The site generator's sections are nodes with inputs of their own, and the use-case scenarios run only when what they exercise changed
- I2527 An incremental derive equals a full derive byte for byte, and an omitted dependency turns a gate red
- I2528 The UI shows the derivation graph and its last plan, and one documentation page explains the model
- I2847 every file the site generator reads by a literal path is one of its declared inputs
Canonical record: .ai/repo/project/milestones/a-derive-does-only-the-work-its-inputs-changed.yaml. Read it back with majordomus plan show a-derive-does-only-the-work-its-inputs-changed.