Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

I1409

Derived data stops hashing inputs it cannot control

Where it stands

devtask.issue
Readiness
complete completion is recorded and every required evidence token is present
Status
DONE
Can start
not now
Milestone
Milestone status
VERIFY
Wave
0
Waiting on
none
Holds back
none
Behind it in all
0
Evidence
3 of 3
A move writes the repository: it stamps one field of this issue's record and appends one event to the ledger, and a commit will carry both.
The same moves from a terminal

majordomus plan start I1409

majordomus plan verify I1409

majordomus plan done I1409

What it is

Objective
Make a committed derivation describe something it can be a projection of, and put the staleness refusal where merges actually happen.
Why
Four separate reds on 2026-09-10 shared one root, and the publication was down for hours because of it. A committed artifact that hashes its whole input tree is invalidated by the next merge — not because it is wrong, but because the tree moved; and one that hashes HEAD can never describe the commit it is in. apps/majordomus-cli/src/release/changelog.rs:75 already carries that sentence in its own comment, and the gate reaches past it anyway: a comment is not a constraint. The second half is structural rather than a design flaw: the derived merge driver resolves to ours on purpose and leans on scripts/pages current in .githooks/pre-commit to refuse one command later, and a pre-commit hook cannot run for a merge the server creates. Three of the last five master merges were committed by GitHub. So the guard's coverage is inverse to its usage — present on the route we tell people not to take, absent from the one we recommend — which is why nobody noticed it was missing. A note on why this is filed under a new milestone rather than an existing one: majordomus plan validate refuses an issue that names no milestone, so an area of standing work without one cannot be recorded in the plan at all - which is how a repository lands a great deal of change against a plan that never moves. Attaching this to a least-wrong parent would put a false edge in a model whose value is that status is derived from the graph.
Now
Re-measured 2026-10-14 on master c0e3585dc8. (1) The site data is now a projection of a declared input list (scripts/generate-site-data --inputs, 1495 paths) fingerprinted by content, so a derivation committed at one commit describes that commit after the trunk moves on: case 3048 merges a side branch and a trunk commit that touch no canonical input and the fingerprint and `scripts/pages current` (site and registry halves) are unchanged, while a commit to one canonical input reads as stale (exit 10). (2) Branch protection now requires the `ci` check (required_status_checks.contexts = [ci], strict: false), and a pull request's ci runs on the server-side merge ref, so the staleness refusal does run on the path a GitHub merge takes; strict is false, so a PR judged at an older base can still merge after master moved, and nothing measured here refuses that at merge time. (3) Re-measured 2026-10-15 (I1409-b): a fixture exported with `git archive` into a fresh repository, whose object database holds none of the release commits, used to read as stale (exit 10: changelog.* and artifacts.* differ) because the changelog composed every release section empty. `majordomus generate` now asks git which release windows it cannot read (a release commit, or the previous one, absent, or a shallow repository) and keeps, for those only, the groups and decisions the committed changelog already carries for the same commit, saying so (`scripts/pages current` prints the notice); everything a record carries is still composed and judged. Case 3055 holds it on an export of the commit under test: both halves current, `generate` rewrites nothing, and a moved registry source exits 10. Case 56 still clones and may return to an export. (4) Decided 2026-10-15 (I1409-c, ADR 0101 amendment): the server-merge guard is the ci-time freshness gate, not strict required checks. It acts at three points: the required ci on refs/pull/<n>/merge (site: generate-site-data --check, rust: generate --check); the forge's own text merge, which runs no merge driver and so conflicts on site/data/generated/source.json source_hash whenever both sides changed a canonical input (a driverless git merge-tree of master c0e3585dc8 with #1159 and #1149, both CONFLICTING on GitHub, lists that file); and the merged commit on push, where validate 38635619021 ran derived site data in sync on GitHub's merge c0e3585dc8 and pages 38635619039 ran build (scripts/pages build, exit 10 when stale) at 21:07:39Z before publish at 21:09:30Z. docs/CI.md no longer says nothing server-side compensates; it names these checks and says the pre-commit hook guards only a merge a person commits. Residual, stated in the ADR: derived artifacts outside the site inputs (AGENTS.md regions, .claude/skills) can text-merge cleanly; generate --check on the push run is the first refusal, and the site publishes none of them. No slice remains.
Wanted
A committed derived artifact is a projection of inputs the commit owns, so landing another change does not invalidate it and a fixture without history can still evaluate it. The staleness refusal exists on the path merges actually take, so the first thing that says no is a check rather than the publication.
Done when
unknown: the record declares no `completion`
Risk
Medium. The hashing change touches what every gate compares, so a wrong narrowing makes staleness undetectable rather than merely noisy - the failure would be silent in the direction that matters. The findings behind this record were measured by four sessions on 2026-09-10 and cross-checked between them; the two timing figures those sessions took for scripts/pages current disagreed sixfold on one machine twenty minutes apart, which is why no number here is offered as a benchmark.

Acceptance

  • A derived artifact committed at one commit is still current at that commit after an unrelated change lands on the trunk
  • test/cases/56_derived_current_gate.sh passes in a fixture whose object database does not contain the commit under test
  • The staleness check runs on the path a server-side merge takes, or the record says which decision was taken instead and why
  • No document claims a guard that a merge created by GitHub cannot reach
Validation
scripts/derive-checkscripts/pages currenttest/run.sh 56_derived_current_gate
Evidence required
derivation_survives_an_unrelated_mergefixture_without_history_evaluatesserver_merge_guard_decided
Evidence present
derivation_survives_an_unrelated_mergefixture_without_history_evaluatesserver_merge_guard_decided

Scope

Touches
scriptsapps/majordomus-cli/src/release.ai/repo/citest/cases
Not
Making the Pages build re-derive instead of refusing. Generation here is not deterministic on a given tree - machine-dependent output, %h abbreviating against the local object store, a depth-1 checkout writing a degraded changelog - so a build that re-derived could produce a third answer matching neither side. This exit will be proposed again; it is recorded here so the reason it fails is not rediscovered.Turning on branch protection. Which check is required, and what it costs a merge, is a repository setting and the operator's decision.Weakening the derived merge driver. Resolving to ours is correct; what is missing is the refusal downstream of it on the server-side path.
Depends on
unknown: the record declares no `depends_on`
Priority
p1
Profile
implementation
Parallel safe
false
Owner
unknown: the record declares no `owner`

The work so far

Branches
none
Merged
none
Commits
0
Trunk
master
Sessions
none
Created
2026-09-10
Updated
2026-10-15T07:54:48Z
Started
2026-10-15T07:54:37Z
Verified
2026-10-15T07:54:41Z
Completed
2026-10-15T07:54:48Z

Synchronisation

Adapter
scripts/github-sync
Repository
unknown: the derived plan's header carries no repository
External issue
unknown: a live GitHub state; this executable makes no network call, so only `scripts/github-sync --check` can answer it
State there
unknown: a live GitHub state; this executable makes no network call, so only `scripts/github-sync --check` can answer it

Findings

Nothing is wrong with these records.