Repository navigation
0.7.1i: carry an already-certified evidence population across the promotion boundary - #483
Conversation
…motion boundary The 0.7.1 -> release/0.7.0 promotion (#440) fails the mutation ratchet on M35 x 67 admissions and M45 x 1 certificate: both rules bind a NEWLY INTRODUCED row to the authority base it was minted against, and a promotion cannot satisfy that by any legitimate means - re-minting admissions, rewriting fromBaseSha, manufacturing replacement certificates and a per-entry migration chain are all forbidden. Minting and promotion are different operations. This adds the one concept that distinguishes them: MutationPopulationPromotion, a declaration that an already-certified evidence population is carried across a promotion boundary and therefore was not minted by the transition that carries it. It grants no authority. Four fields - promotionBase, populationDigest, admissions, certificates - are each enforced against the ledgers themselves, so a declaration cannot describe a population other than the one it names. SHA-exact and candidate-side, because the base branch cannot name the commit that will contain the declaration. M35 and M45 each gain one narrow guard: a row is not bound to this base when the exact declaration carried it. Everything else fails exactly as before - a newly minted row carries a different digest and is still refused. The declaration is computed once in MutationRatchetVerifier, so the admission and certificate rules cannot drift apart. Verified: the real verifyMutationRatchet against release/0.7.0 PASSES (the exact check #440 fails); 12 discriminators green, covering every field of the declaration plus its absence; spotlessCheck and verifyStaticAnalysis clean, with the two findings this code raised (ReturnCount, ThrowsCount) fixed in source - no suppression, no baseline entry. No fromBaseSha rewritten, no admission re-minted, no gate weakened.
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It deliberately relaxes an authority/mint-binding invariant (M35/M45) via a self-asserted candidate-side declaration, which is exactly the kind of governance-boundary change the repository guidance reserves for human review.
Review effort: Balanced
Findings: 1
What changed in this PR
This PR adds a new concept — MutationPopulationPromotion — to the mutation-ratchet governance machinery in build-logic. The mutation ratchet's mint-binding rules (M35 for admissions, M45 for digest-migration certificates) require every newly introduced admission/certificate to record a fromBaseSha equal to the transition's authority base. During a release promotion (0.7.1 → release/0.7.0), the entire promoted evidence line looks newly introduced relative to the new base, so M35/M45 reject the whole population. This change introduces a narrow, SHA-exact, candidate-side declaration that states an already-certified population is carried across the promotion boundary (not minted there), relaxing M35/M45 only for the exact declared digest and counts while leaving every other protection in force.
Changes:
- New
MutationPopulationPromotiondata class + fail-closed YAML loader + a singlecarried(...)rule that validates the declaration against the actual ledgers (base SHA match, exact admission/certificate counts, and every appearing row's digest). - Threaded the computed-once
promotionthroughMutationRatchetVerifierinto both the admission ceremony (M35) and certificate ceremony (M45), adding one guard each (fromBaseSha != baseSha && !carriedForward). - New declaration file and 12 negative-proof tests asserting that any single field deviation (or absence) carries nothing and the original rules fire.
| File | Description |
|---|---|
build-logic/.../MutationPopulationPromotion.kt |
New declaration type, carried(...) rule, and fail-closed loader. |
config/quality/mutation-population-promotions.yml |
The concrete 0.7.1 → release/0.7.0 promotion declaration (67 admissions, 1 certificate). |
build-logic/.../MutationRatchetVerifier.kt |
Computes the promotion once and passes it to both ceremonies. |
build-logic/.../MutationRatchetAuthority.kt |
Adds optional candidate-side promotion field (defaults to absent/fail-closed). |
build-logic/.../MutationPopulationAdmissionCeremony.kt |
M35 gains a carriedForward exception bound to the declared digest. |
build-logic/.../MutationAuthorityDigestCertificateCeremony.kt |
M45 gains the same carriedForward exception for certificates. |
build-logic/.../MaintainabilityBaselinePlugin.kt |
Loads the promotion from the candidate tree and wires it into the candidate. |
build-logic/.../MutationPopulationPromotionTest.kt |
12 negative-proof tests for the carry rule and loader. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| * Notes carry declaration history, so a later promotion appends rather than replaces: an older | ||
| * declaration simply never matches the transition being verified, since the base SHA is exact. |
0f3e9d2
into
epic/0.7.1-control-plane-authority

Problem
The 0.7.1 →
release/0.7.0promotion (#440) fails on exactly one check,tests, in the step "Verify mutation ratchet against PR base":M35 and M45 bind a newly introduced admission / certificate to the authority base it was minted against. A promotion has no legitimate way to satisfy that: re-minting admissions, rewriting
fromBaseSha, manufacturing replacement certificates and a per-entry migration chain are all forbidden. Every row the promoted line added looks newly introduced relative torelease/0.7.0, so the whole population is refused.Minting and promotion are different operations. This adds the one concept that distinguishes them.
Change:
MutationPopulationPromotionIt grants no authority. It declares that an already-certified evidence population is carried across a promotion boundary, and therefore was not minted by the transition that carries it.
Four fields, all enforced against the ledgers themselves:
MutationPopulationPromotion.kt— the declaration, its fail-closed loader, and onecarried(...)rule.config/quality/mutation-population-promotions.yml— the declaration above.promotionBasewas verified equal to the liverelease/0.7.0tip.MutationRatchetCandidategains the declaration (absent by default = fail-closed).MutationRatchetVerifiercomputes it once from the same ledgers both ceremonies judge, so the admission and certificate rules cannot drift apart.if (fromBaseSha != baseSha && !carriedForward). Everything else fails exactly as before.Why candidate-side
The declaration must be SHA-exact ("this promotion, against this base"), and the base branch cannot name the commit that will contain the declaration — a file cannot contain its own commit SHA. So it rides in the promotion's own candidate. This is a deliberate, bounded departure from "authority must pre-exist in the base": the declaration admits nothing, it only certifies an unchanged, digest-bound set, and a genuinely newly minted row carries a different digest and is still refused by M35/M45.
Negative proofs (12 tests, all green)
Each changes exactly one field of an otherwise exact declaration, or removes it:
promotionBase→ nothing carried, M35 firespopulationDigest→ nothing carried, M35 firesVerification
./gradlew verifyMutationRatchet -PtramaiMutationBaseSha=origin/release/0.7.0→Mutation ratchet (base 9ccbd0d3…) baseline verification PASSED.Task executed, not UP-TO-DATE. This is the exact check Epic/0.7.1 control plane authority #440 fails.:build-logic:test --tests MutationPopulationPromotionTest→ 12 tests, 0 failures.spotlessApply/spotlessCheck -PtramaiFormattingBaseRef=80cecf9f…→ clean; the formatter touched only this slice's files.verifyStaticAnalysis -PtramaiStaticAnalysisBaseRef=origin/master→ clean. The two findings raised by this code (ReturnCounton the first form ofcarried,ThrowsCountinparseDeclarations) were fixed in source — no@Suppress, no baseline entry.verifyChangePolicy -PchangePolicyBase=80cecf9f…→ PASSED, 8 files, no violations.Not touched
No
fromBaseSharewritten, no admission re-minted, no replacement certificate, no per-entry migration chain, no baseline or deviation change, no gate weakened. M35/M45 continue to bind everything that was actually minted by the transition.Disclosed
The
policy-maintainabilitylane pinsexpected: 364and its filter set includes--tests 'dev.tramai.build.quality.Mutation*Test', which now matches this PR's new test class. The lane measures 448 on this tree (0 failures; the epic's own drift is +72, this class is +12). That pin lives in.github/workflows/maintainability-baseline.yml, which does not run for epic-targeted PRs, so it is not changed here — workflow changes ship alone. It is queued as its own workflow-only change carrying the measured 448.