From 7d3f4407a75945b4bcd55815183c4a70f7d07547 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 23:05:51 +0200 Subject: [PATCH] ADR 0267 (proposed): an artifact is rebuilt only when its build source changes A merge to a shared repository moved every module that read it, in a gated tier each (issue 338). The operator's rule maps a change onto the artifacts whose build source it touches and adds only modules with a build dependency on a changed artifact; sharing a repository is no dependency. Supersedes in part ADR 0162 decision 1 (the packages kind) and ADR 0238 decision 3 (whole repository and context reads); adds build source to the glossary. --- 00-META/glossary.md | 9 + ...e-produces-a-tiered-plan-the-mesh-keeps.md | 7 + ...n-itself-and-so-it-rolls-out-unattended.md | 5 + ...-off-the-trunk-and-published-only-on-it.md | 6 + ...y-along-build-dependencies-on-artifacts.md | 255 ++++++++++++++++++ 5 files changed, 282 insertions(+) create mode 100644 02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md diff --git a/00-META/glossary.md b/00-META/glossary.md index eab4f295..44e05f94 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -300,6 +300,15 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data. ([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)). An image is one kind of artifact, and a module is not an image. A package and an artifact are two protocols, not one store being weak ([ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md)). +- **build source** — the files one artifact is built from, in one or more repositories: for a Go program + its import closure (the directories of its packages, its Go module files) and its recipe; for any other + artifact the paths its manifest names, by default its module's own directory. Each artifact of a build + has its own, and a change rebuilds the artifacts whose build source it touches + ([ADR 0267](../02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md), + proposed). Older records (ADR 0015, ADR 0111) and designs say *build source* for the repository a module + is built from; this entry narrows it to the files within, and that use reads as *the repository holding + the build source*. Sharing a repository is not a dependency, and no build source is a whole repository + holding another module's files. - **catalogue** — what the mesh holds of every module, at which commit; and the repository the modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling. *Not:* ~~catalog~~ (hq) diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md index b5343e63..f8e3738a 100644 --- a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -11,6 +11,13 @@ extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md > **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged. +> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md) (proposed; it takes effect when it is accepted).** +> In decision 1, the `packages` kind ("the module's build reads the other's repository") and the *code* +> dependency ("B packages A's source … rebuilt whenever A changes, in the same tier") no longer hold: a file +> two modules build from moves both through their build sources, and no edge is drawn for sharing a +> repository. `stands-on` and `declared` name an artifact at each end. The build and runtime dependencies, +> `built-by`, and decisions 2 to 4 stand as decided here. + ## Context A merge on the forge reaches the controller as an event, and the controller asks the build diff --git a/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md b/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md index 045f60c2..b73ecea1 100644 --- a/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md +++ b/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md @@ -16,6 +16,11 @@ extends: 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-t > no core module starts on that module's word, and *held* is a delivery's state, released by a person through > `mesh-delivery`'s `release`. These are the gate rules ADR 0239 applies when a member of a delivery group fails. +> **The mechanism changed — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md) (proposed; it takes effect when it is accepted).** +> What stands: a rebuild that changes nothing is no move. What moved: the source fingerprint of the issue 280 +> insight below is taken per artifact over its build source, not per build over the module's directory and +> each context's whole repository, and an artifact with an equal fingerprint is not built again. + ## Context **The operator said "start phase 4"** of [to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) diff --git a/02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md b/02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md index 9ab33b5d..282014f6 100644 --- a/02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md +++ b/02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md @@ -18,6 +18,12 @@ extends: 02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before- > leads to the delivery's view on the pull request. The *change plan* is called the **delivery plan**. Decisions > 1 to 5 stand as decided here. +> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md) (proposed; it takes effect when it is accepted).** In +> decision 3, "the whole repository for a module built from its root, and a repository a recipe names (the +> build record's `read`)" no longer holds: a changed file touches the artifacts whose build source contains +> it, and through them their modules. The one mapping function, a file read by no build touching nothing, +> and a new `module.json` being a new module, stand. + ## Context [ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md) put a diff --git a/02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md b/02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md new file mode 100644 index 00000000..14124194 --- /dev/null +++ b/02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md @@ -0,0 +1,255 @@ +--- +topic: building it +status: proposed +date: 2026-10-08 +deciders: jochen +reconstructed: false +supersedes-in-part: + - 0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md + - 0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md +extends: 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md +--- + +# 267. An artifact is rebuilt only when its build source changes, and a module moves only along build dependencies on artifacts + +## Context + +The operator stated the rule on 2026-10-08, in their words: + +> If only files for module A changed, only module A is taken into the deploy plan. Then the deploy plan +> checks our module graph to check which modules have a dependency on the changed module. However, 1 +> commit might change files from more than 1 module in a repository. + +> a change to A … moves A and all modules with a build dependency on A + +> you should only check the build dependencies in the module graph in our module catalog + +And: sharing a repository is not a dependency — "there is no repository dependency". What the operator +calls the *deploy plan* is, in the glossary's words, the delivery plan's **build plan**: the modules a +delivery moves and their dependents, in tiers. The glossary's *deploy plan* is only its per-node part. + +### How a merge reaches the nodes today + +This description was read from the records and from mesh-controller at `8170fc5`, and confirmed by the +operator before this record was written. + +- **A pull request is a delivery** ([ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)): + checked by the merge gate and the repository check ([ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md) §4, + [ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)), + published once merged on the trunk, and walked: one node first, judged at the first-node gate, put back + if it fails ([ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md), + [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)). +- **What moves** is computed by one function, ADR 0238 §3: "A changed file touches exactly the modules + whose build reads it: a module's own directory, the whole repository for a module built from its root, + and a repository a recipe names." Dependents are added along the catalogue's one relation + ([ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) §1), with four kinds: `stands-on`, + `declared`, `packages` ("the module's build reads the other's repository") and `built-by`, which + "orders and gates but never widens". +- **A build is one module at one commit, and makes every artifact its manifest declares**, every time, in + name order (the builder's `build`). The recipe and the compiler see the module's own directory ([issue + 280](../04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md)); a module + built from its repository's root sees the whole repository; an artifact's `context` clones another + repository whole ([ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md): "A build + context may live on the git seat"). +- **A build produces artifacts, and most are bundles.** [ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md): + "An artifact is anything a build produces … *Image* is one kind. A module is not an image; a module may + build several artifacts and install none." The kinds in the code are `bundle`, `archive`, `image` and + `upstream`, served by the artifact store by digest, and `package`, published to the package registry by + version ([ADR 0075](0075-two-stores-and-which-provides-what.md), [ADR 0076](0076-the-sdk-is-a-published-package.md)). + On the catalogue's trunk on 2026-10-08, 99 of 119 modules build something; they declare 97 bundles, 6 + archives, 5 images and 3 upstream copies, no package; 9 modules build more than one artifact. +- **The facts already name artifacts.** A resource names the artifact it runs by its local name, resolved + to a digest when the build is registered. `build.on` names a base as a module *and* an artifact, and a + build records what it stood on as `artifact-store:///@`. A send recreates per + resource: the walk of 2026-10-08 said "recreates 1 of route-proxy's 3 container(s) (with a new image: + server)". **Three things reduce that to modules**: the inventory's edges (mesh-controller + `internal/inventory/dependencies.go` keeps `on.Module` and cuts a recorded base at its first `/`), the + build request (one per module), and the source fingerprint (one per build). +- **A new build is a change unless its fingerprint repeats** (ADR 0236, its note on issue 280): + "Such a rebuild is registered with the artifacts of the build it repeats, so no machine is sent a new + digest." The fingerprint hashes the module directory's tree, **each context's whole repository tree**, + every base by digest and the toolchain. A build that resolves packages from the registry has none, and + is compared by its artifacts' digests. + +### What went wrong + +Issue 338 +(hq pull request #231, not yet merged when this was written). Two catalogue modules, `route-proxy` and `build-agent`, +build an image from a recipe whose context is the controller's repository. Every merge to that +repository on 2026-10-08 planned the same three modules in three gated tiers, whatever the files: + +- `mergeCandidates` and `readsFrom` (mesh-controller `cmd/mesh-controller/upgrades.go`) add a module + whose build read the merged repository with no test on the changed paths; `touchedBy`, the function + ADR 0238 §3 names, is never applied to it. +- `dependencies.go` draws a `packages` edge from it to every module built from that repository. So the + `packages` edge is a **context read**, nothing to do with the `package` artifact kind, and it treats + sharing a repository as a dependency. +- **The live tiers come from the code, not from ADR 0162.** That record puts a code dependency "in the + same tier"; the planner put each in a tier of its own (mesh-controller → build-agent → route-proxy, + the last ordered by `built-by`). Each tier waited for its own first-node gate, about three and a half + minutes. Seven completed controller walks took 10 to 11 minutes in gates alone, about 15 minutes in + all, against 4 to 5 for the controller's own tier. +- **The fingerprint is wider than what the build compiles.** It hashes the context's whole tree, so no + two builds repeat. The route proxy's program, built at each of nine merges with `-trimpath + -buildvcs=false`, changed at two of them; it was recreated on its node at five of seven walks. +- **A file in a module's directory is not the right test either.** The merge that did change the route + proxy's program changed nothing under its directory: it changed `internal/broker`, which the proxy + imports. A program's source is its import closure, not its directory. +- The controller is built from its repository's root, so a change to the route proxy alone, or to + `README.md`, moves the controller too. + +The controller's planner rules are held by tests since mesh-controller #167 +(`cmd/mesh-controller/planner_rules_test.go`, `planner_records_test.go`); the rows tagged *issue 338* +assert today's repository-grain behaviour and say they change with this decision. + +[ADR 0009](0009-modules-and-the-graph.md) already said what the build edge should be: "derived rather +than declared … a declared list of dependencies drifts from the imports it describes", and "Reading build +edges needs a language-aware tool per language, which is the real cost". The code derives the edge from +the bases a build was handed, and draws none from imports. + +## Considered Options + +**What a change is mapped onto.** + +1. *Keep repository grain* (today). Rejected: it is issue 338. A module reading a repository moves on every + merge to it, and each such module costs a gated tier. +2. *Module grain, narrowed*: one build source per module, edges module to module, one fingerprint per + build. The smaller change, and honest about its cost: 9 of the 99 modules that build something build + more than one artifact, and with the fingerprint's repeat rule an unchanged artifact rebuilt with its + module is still sent nowhere. Rejected because the operator's rule is about what a build *produces*, + and the mesh already records the artifact on every edge and every resource; module grain is the + planner discarding what the records hold. A module built against one artifact of another would move + whenever any other artifact of it changed — the same widening as issue 338, one level down. +3. **Artifact grain** — chosen. Each artifact has its own build source and fingerprint; a dependency points + at an artifact. + +**How an artifact's build source is known.** + +1. *Hand-listed paths, checked against the import closure.* Rejected for Go: a list drifts, and a path + missing from it is a real change missed, which is worse than a needless rebuild. The check that would + catch the drift has to compute the closure anyway, so the list adds nothing but a way to be wrong. +2. **Derived from the import closure for Go** (`go list -deps`) — chosen; ADR 0009's "derived rather + than declared", for the language the mesh's own code and 97 bundles are written in. +3. **Declared paths for anything without a reader** — chosen, and made loud: the builder hands such a + build only its declared paths, so a missing path is a failed build, not a missed change. + +**Other remedies.** + +1. *Split the route proxy and the build seat's program into repositories of their own.* Rejected: they + import the controller's packages, which would move to the SDK, and a change there then moves all three + through a real build edge; the root-built controller would still claim every file. The rule has to + hold whatever a repository's layout is. +2. *Reproducible builds alone.* Rejected as the remedy: the planner would still plan and gate three tiers, + and the gates are the cost (7 of about 11 minutes). Kept as one rule of this decision, for the builds + whose fingerprint cannot be narrow. + +## Decision + +1. **Every artifact has a build source**: the files it is built from, in one or more repositories. One + build may produce many artifacts, of any kind, and each has its own. + - **An artifact that compiles a Go program** (a Go bundle, or an image or package whose recipe compiles + one) has its build source **derived**: the directories of every package in the program's import + closure inside the repositories it reads (`go list -deps`, the standard library left out), its Go + module files (`go.mod`, `go.sum`, `vendor/modules.txt`), and its recipe. The manifest names the + program's package where it does not already: a Go bundle's `from` does; an image whose recipe + compiles Go gains a field naming the package it compiles. + - **Any other artifact** has the paths its manifest names, within its module's repository or its + context. Absent, it is the module's own directory — today's rule — or, for an `archive`, its `from`. + - **A build source is never a whole repository that holds another module's files.** A module built + from its repository's root (the controller) and an artifact built from a context (the route proxy, + the build seat's program) are narrowed by rule 1's derivation or by named paths. A manifest whose + build source would be such a repository is refused by the catalogue's check. + - The builder records each artifact's build source with the build. A Go closure can only change by an + edit to a file already in it (an import line in one of its packages, or a module file), so the build + source recorded for the trunk's last build is enough to map the next change. An artifact with no + recorded build source (its first build) is built. +2. **A change moves exactly the artifacts whose build source it touches.** The planner's one mapping + function (ADR 0238 §3) maps each changed file onto artifacts, then onto the modules that build them. + One commit may touch artifacts of several modules, in one repository or across the repositories a + build source spans. A change to a module's manifest moves that module; it rebuilds only the artifacts + whose own declaration or build source changed. +3. **A dependency points at an artifact.** A build dependency is an edge from a module's artifact to the + artifact it is built against: a base named in `build.on`, a base the build recorded standing on, a + bundle's toolchain, a package resolved by version. The plan adds, transitively, every module with a + build dependency on a changed artifact, as the catalogue's module graph states it, in the next tier + after that artifact is built. A dependent is planned when its base's fingerprint changed and **left + out when the base is built to the same digest**. +4. **Nothing else moves a module.** Not sharing a repository, not a whole-repository context, not a file + outside every build source. The `packages` edge kind is removed. `built-by` stays as ADR 0162 left it: + it orders the modules a plan already holds after the build seat's holder and never adds one. +5. **An artifact whose build source did not change is not rebuilt as a new digest and not sent.** Its + fingerprint is computed per artifact, over its build source (the git object of each of its paths), the + bases it stands on by digest, its recipe and its toolchain. An equal fingerprint keeps the digest the + mesh holds, and the artifact is not built again. A module is sent only when an artifact it runs has a + new digest or its definition changed; one whose artifacts are all unchanged is not recreated. +6. **A build is reproducible where its fingerprint cannot be narrow.** An unchanged build source gives an + identical artifact, for every kind: nothing in an artifact varies with the commit, the clock or the + path it was built at. For Go: `-trimpath` (the toolchain has it) and `-buildvcs=false`, and no `.git` + in what a recipe sees. This matters for a build with no fingerprint (one resolving packages from the + registry) and for any build source wider than what is compiled. It is not the remedy for issue 338; + rules 1 to 5 are. + +### What this supersedes, and what stands + +- **ADR 0162 §1, in part.** The `packages` kind ("the module's build reads the other's repository") and + the paragraph's *code dependency* ("B packages A's source — means B is rebuilt whenever A changes, in the + same tier") no longer hold: a shared file is in both artifacts' build sources and moves both by rule 2, + and no edge is drawn for it. `stands-on` and `declared` hold, but name an artifact at each end, not a + module. The *build* and *runtime* dependency tier rules, `built-by`, §2 (the plan is a record), §3 and §4 + stand. +- **ADR 0238 §3, in part.** "the whole repository for a module built from its root, and a repository a + recipe names (the build record's `read`)" no longer holds: a changed file touches the artifacts whose + build source contains it, and through them their modules. That one function maps every path, that a + file read by no build touches nothing, and that a new `module.json` is a new module, stand. +- **ADR 0236's insight of issue 280, as a mechanism.** "two builds of a module with one fingerprint as one + build" becomes one artifact with one fingerprint. The decision it supports — a rebuild that changes + nothing is no move — stands. This is a mechanism change, not a supersession. + +## Consequences + +- **The manifest schema grows two fields on an artifact**: the paths of its build source within its + repository or context, and, for an image or package whose recipe compiles Go, the package it compiles. + Their names are settled at the build handoff. `route-proxy` and `build-agent` take the second; the + controller's own manifest builds `cmd/mesh-controller` as a Go bundle and is narrowed by derivation, + so `README.md`, `examples/route-proxy` and `cmd/mesh-builder` stop moving it. +- **The builder builds and fingerprints per artifact.** It derives a Go artifact's build source, records + it and the fingerprint with each artifact, hands a declared-path build only those paths, and skips an + artifact whose fingerprint the mesh holds. One build request may still name a module; what it builds + inside it is per artifact. +- **The planner** drops the packaging kind in `mergeCandidates` and `readsFrom`, and the `packages` + addition in `reachOfMerge` and `SourceMoved`; it maps files onto artifacts through the recorded build + sources, and confirms a dependent after its base is built. +- **The inventory's relation** (`internal/inventory/dependencies.go`) draws edges between artifacts, keeps + `on.Artifact` and the recorded base's artifact segment, and loses `EdgePackages`. `moveFacts.identical`, + which today compares catalogue commits, compares digests. +- **A controller merge that changes only the controller's program** plans one module in one tier. A change + to a package all three programs import plans all three: the build seat's holder first (`built-by`), + then the controller and the route proxy together, two tiers where there were three. +- **What got harder**: a Go program's build source needs the Go toolchain to read, on the builder; a + second language gets a reader of its own or declared paths. A declared-path build that reads a file it + did not declare now fails where it used to succeed. +- **Design** [to-be 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md) and + [to-be 10](../03-DESIGN/01-to-be/10-delivery.md) name this record when it is accepted, and the build + handoff opens on mesh-controller and the catalogue. + +## How this is checked + +| Rule | Checked by | +|---|---| +| A change moves exactly the artifacts whose build source it touches; a file in none moves nothing | mesh-controller `planner_rules_test.go`, `TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt`: rows for a file in one artifact's build source, a file shared by two modules' build sources, a `README.md` at a root-built module's root | +| Sharing a repository moves nothing (issue 338) | `TestASharedRepositoryMovesWhatPackagesItAsItDoesToday` in `planner_rules_test.go` and `TestASharedRepositoryIsPlannedFromTheRecordsAsItIsToday` in `planner_records_test.go`: the rows tagged *issue 338* flip — a `README.md` change plans no module, a change to the proxy's package plans the proxy alone, a shared package plans three modules in two tiers | +| A plan is the touched artifacts' modules and what is reachable along build dependencies, and nothing else | the property test `TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges`, with `packages` removed from the widening kinds and edges at artifact grain | +| A dependency points at an artifact; a dependent of an unchanged artifact of a changed module does not move | a new inventory test over a fixture: a module with two artifacts, a dependent built against one; a change to the other plans no dependent | +| A dependent whose base is rebuilt to the same digest leaves the plan | a new planner test: a base rebuilt with an equal digest; the dependent's tier asks nothing | +| A Go artifact's build source is its import closure | a builder test over a fixture repository: the derived build source equals `go list -deps` for the program, and grows when an import is added | +| No build source is a whole repository holding another module's files | a catalogue check in `merge-check.sh`: every manifest's artifacts derived or declared, and a root or context build source refused | +| A declared-path build sees only its paths | a builder test: a recipe reading an undeclared file fails, naming it | +| An unchanged build source is not rebuilt and not sent | a builder test: two builds at commits differing outside an artifact's build source give one fingerprint and one build; a walk test: no send for it | +| A build is reproducible | a builder test per kind: building one source twice gives one digest | +| Live | the `plans` what-if for the controller's repository with only `README.md` answers no module; with the proxy's package alone, `route-proxy` alone | + +## References + +- Issue 338 and its diagnosis (hq pull request #231); issue [280](../04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md) +- [ADR 0009](0009-modules-and-the-graph.md), [ADR 0072](0072-two-graphs-and-the-build-chain.md), [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md), [ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md), [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md), [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md), [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) +- mesh-controller #167 (the planner's rules held by a table and a property test)