ADR 0247: Declared generated artifacts regenerate; they are never merged textually
Review-metadata amendment:
[generated.<name>]now also accepts an optional, default-offlinguist_generatedpresentation flag. The regeneration and merge contract below is unchanged.Clone-local driver amendment (2026-09-03): setup and every normal refresh reconciliation install one
merge.discern-generated.driver=truevalue in the common clone config. Reconciliation removes redundant worktree-local copies and disablesextensions.worktreeConfigonly when no other checkout-specific setting needs it. A fresh clone gains the driver after setup ordiscern refresh; tracked attributes alone do not transport local Git configuration.
Status: accepted
Context
§Committing a generated file is correct under two conditions: consumers must be able to read the artifact without running its generator, and a gate must make drift between the artifact and its sources a failure rather than a discouragement. discern already lives on both sides of that rule. Every install commits the compiled agent files (AGENTS.md, CLAUDE.md, GEMINI.md), held current by the gate's guidance currency check; this repository additionally commits a family of codegen outputs (the config reference, JSON Schemas, the registry atlas, the maintained ADR index) held by drift tests and the strand check.
The cost shows up in parallel work. A generated file is a pure function of its sources, so two branches that each change a source conflict textually in the derived artifact even though neither touched the same fact — two landed ADRs collide in the maintained index, two registry changes collide in the atlas, a guidance change on the trunk collides with every in-flight worktree's compiled agent files. During busy fleet sessions these conflicts arrive at discern update, which aborts the merge and refuses (ADR 0055 deliberately rejected resolving conflicts for the agent: "conflicts need judgment"). The agent then hand-merges a file whose merged content is fully determined by the merged sources — judgment applied to a question that has none, with a real chance of committing a hand-edit the next gate run flags as drift.
The forces: the resolution of a conflict in a derived artifact is never a textual merge (it is "merge the sources, regenerate"); the engine cannot know which paths are derived without being told; the project's own gate already runs regeneration in the build stage and already detects post-build drift; and the update verb already re-runs a regeneration (compileGuidelines) and the convergence buckets after every merge.
Decision
§A project declares its derived, committed artifacts in a new [generated.<name>] record family in discern.toml: paths (scope-paths globs naming the committed artifacts the group owns), run (the command that rewrites them from their sources), and an optional timeout. The declaration asserts one fact: these paths are a pure function of the rest of the tree, and this command recomputes them deterministically. Every consumer derives from that single declaration:
- The gate plans each group's
runas a build-stage job (labelledgenerated:<name>) on every full gate run and fails — a distinctgenerated_driftfailed stage — when regeneration changes a declared path (the artifact was stale: the remedy names the group, its command, and says to commit the regeneration) or when a generator's changes escape its declared globs (the declaration under-covers: the remedy says to widen it). Attribution rides glob ownership. discern updateno longer refuses when every conflicted path lies inside declaredpaths(or belongs to the built-in group below): it takes either side, completes the merge, re-runs the affected groups, and reports what it resolved and regenerated. This is a scoped amendment to ADR 0055: conflicts in declared derived artifacts need regeneration, not judgment. Conflicts touching any undeclared path still abort and refuse exactly as before.- The compiled agent surfaces discern itself generates (agent files, the maintained ADR index) form a built-in group requiring no declaration, regenerated by the refresh compile that update's convergence step already runs.
discern doctorverifies the contract without writing: each group's command resolves, and its globs match tracked files (an all-ignored group is inert and says so — untracked artifacts never conflict and never drift, and leaving them untracked remains the right choice when nothing consumes them pre-generation).discern couplingremoves declared paths from historical baskets and the current change set before it computes edges, basket fences, or counts. An explicit query names the owning group; automatic gate advice omits routine exclusions.- A managed
.gitattributesblock (the delimited-ownership model of ADR 0093) marks declared paths with adiscern-generatedmerge driver. Setup and normal refresh reconciliation install its one clone-local shared definition, so a rawgit mergeoutside discern's verbs keeps the current side without conflict markers and the gate converges the content afterwards. discern patternswatches the recorded per-job gate timings and flags a generator that comes to dominate gate time — advisory only.
The explicit nos:
- No skip, cache, or on-demand knob. Generators run on every full gate. A generator too slow for that is a finding for
patternsand a prompt to restructure, not a correctness exemption. - The gate still never merges (ADR 0050 holds) and never auto-heals (ADR 0034 holds): it regenerates and fails on drift; only
updateresolves conflicts, and only inside declared paths. - No orphan pruning. A file the merged generator no longer emits is the generator's own job to delete; the engine never removes a declared path itself.
- Not a scope.
[scopes]answers what review a region gets;[generated]answers who owns the bytes. A path may be in both; the two tables stay orthogonal.
Consequences
§- Merge conflicts in derived artifacts become non-events for fleets:
updateresolves them mechanically, and correctness never depends on that resolution being clever — whatever update produces, the next gate run re-derives the artifacts and fails on any mismatch. - Every full gate run now pays every generator's runtime, by design. The pressure this creates is visible (per-job timings, the patterns advisory) rather than traded away for a staleness window.
runcommands must be deterministic: a generator that embeds timestamps or randomness makes an already-regenerated tree dirty again, which reads asgenerated_drifton every run. The remedy text names this signature.- The declaration is held honest structurally: a generator writing outside its globs fails the gate, so
pathscannot silently under-cover as the generator grows. - ADR 0055's conflict contract is narrowed, not replaced; ADR 0034's no-auto-heal rule and ADR 0050's no-merge-in-done rule are unchanged.
Alternatives considered
§- Don't commit generated artifacts. Correct where nothing consumes them pre-generation, and doctor says so — but agent files must exist before any agent can act, and map-published references are read from the tree. For those, "don't commit" was never available.
- An
inputs-style replay knob (skip regeneration when no declared input changed, as standards do). Rejected for now: it reintroduces a staleness window and a second glob list to keep honest, for a cost the patterns advisory makes visible instead. - A union or ours-only merge strategy without regeneration. Hides the conflict but ships wrong derived content until someone regenerates; only acceptable beneath a gate that always regenerates — which is what the merge driver plus the build-stage jobs provide together.
- Overloading
[scopes]with ageneratedflag. A scope's paths order is already contractual for export (ADR 0246) and its flags tune review; grafting ownership semantics onto it would make one table answer three unrelated questions.