From faf5d8d71d413f12d456bedd59cd23ccfb22b187 Mon Sep 17 00:00:00 2001 From: gionag Date: Thu, 1 Oct 2026 17:27:59 +0200 Subject: [PATCH 1/3] feat(0.7.1g1P2-1): bind M34 to a canonical authority projection, not raw PIT status Step 1 of the design record (TASK-0.7.1g1P2-AUTHORITY-MODEL-RECONCILIATION.md): the semantic split, with no certificate logic. MutationPopulationEvolutionProof now carries two hashes, both computed by the verifier from the same fresh measurement: - projectionHash (unchanged): the raw-exact canonical comparison projection, raw status included. It remains the measurement proof (M21, exactComparison) and is deliberately NOT relaxed. - authorityProjectionHash (new): identity|outcome|family|module plus family/module topology and analyzer semantics, with raw PIT status deliberately excluded. M34 now consumes the authority projection. C7 (BaselineModel.kt) declares raw PIT status diagnostic evidence and not authority, and four complete unrestricted campaigns at identical effective PIT inputs produced four distinct raw-status digests - three identities of 2544 oscillating only between SURVIVED and TIMED_OUT, all NON_KILLED in every campaign - while this projection was byte-identical every time. M34's previous digest was the measurement proof, so a scheduler race on unrelated neighbours invalidated adjudicated authority; that was an unintended reuse of one hash for two different trust questions, not a deliberate override of C7. Raw status stays authoritative where it belongs: the fresh<->committed exact comparison (M21) and each individual authorization's exact row (M32, unchanged and still status-inclusive - the 67 authorized rows were byte-identical across all four campaigns). Discriminators: T1 (a neighbour's raw status movement leaves the authority projection unchanged, while the raw-exact proof still distinguishes the populations) and T2/T8 (outcome and analyzer semantics changes do move it) at pure-verifier level; T1 again at ceremony level, where an authorization minted while a neighbour was SURVIVED must remain consumable when it is TIMED_OUT. Two test corrections, both evidenced: - the M34 negative test asserted a diagnostic substring ('complete measured population') that the deliberate rewording removes; it now asserts 'authority projection' and still asserts rejection. - MutationRatchetAuthorityTest passed admissions = NONE in the candidate while the real base holds 67 pending authorizations, which asserts a transition M37 forbids. This failure reproduces with an identical signature at the pristine base ec8b4da5 (8 tests, 1 failure, M37 on 050d2b83), so it is pre-existing and not caused by this change; the candidate now retains the base authorizations, so absent targets are M39 warnings as designed. No certificate, no consumption rule changes, no P2. The 67 admissions, baseline, classifications, evolution records, M06/M13/M36/M37 and the canonical outcome mapping are untouched. --- .../MutationPopulationAdmissionCeremony.kt | 45 +++++++----- .../build/quality/MutationRatchetVerifier.kt | 73 +++++++++++++++---- ...MutationPopulationAdmissionCeremonyTest.kt | 42 ++++++++++- .../MutationPopulationEvolutionProofTest.kt | 39 ++++++++++ .../quality/MutationRatchetAuthorityTest.kt | 8 +- .../quality/MutationRatchetTestSupport.kt | 17 ++++- 6 files changed, 187 insertions(+), 37 deletions(-) diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremony.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremony.kt index 40d7652d6..a80fffbce 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremony.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremony.kt @@ -32,13 +32,21 @@ package dev.tramai.build.quality * general M06 bypass is therefore preserved by construction: authorized X plus unauthorized Y in * the same measurement fails for Y, through unchanged M06. * - * The digest binds the *whole* transition, not just the identity's row: the authorization records - * the projection hash of the complete canonical fresh measurement it was minted against, and - * consumption compares it with the verifier's own fresh measurement + * The digest binds the *whole* transition's AUTHORITY content, not just the identity's row: the + * authorization records the **authority projection** — identity, canonical outcome, family, module, + * family/module topology and analyzer semantics — of the canonical fresh measurement it was minted + * against, and consumption compares it with the verifier's own fresh authority projection * ([MutationEvolutionEvidence.proof]). "Binds the neighbours" is what makes this all-or-nothing: if - * any other identity in the population changes between minting and consumption, the digest no - * longer matches and the authorization must be re-minted against the new measurement. A candidate - * can never construct that measurement, because the hash compared is never read from the ledger. + * another identity's canonical outcome, family or module changes between minting and consumption, + * the digest no longer matches and the authorization must be re-minted. A candidate can never + * construct that projection, because the hash compared is never read from the ledger. + * + * Raw PIT status is deliberately outside this digest. C7 ([MutationOutcome]) declares raw status + * diagnostic evidence rather than authority, and four complete unrestricted campaigns at identical + * effective PIT inputs produced four distinct raw-status digests (SURVIVED↔TIMED_OUT on three + * identities of 2544, all NON_KILLED in every campaign) while this projection was byte-identical. + * Raw status remains fully authoritative where it belongs: the fresh↔committed exact comparison + * (M21) and each individual authorization's exact row (M32). * * ## Division of responsibility (one implementation, two call sites) * @@ -65,8 +73,8 @@ object MutationPopulationAdmissionCeremony { fun checks( base: MutationRatchetAuthority, candidate: MutationRatchetCandidate, - freshProjectionHash: String?, - ): List = lifecycleChecks(base, candidate, freshProjectionHash) + freshAuthorityProjectionHash: String?, + ): List = lifecycleChecks(base, candidate, freshAuthorityProjectionHash) /** * The verdict for one identity that is present in the candidate population and absent from the @@ -81,7 +89,7 @@ object MutationPopulationAdmissionCeremony { candidateAdmission: MutationPopulationAdmission?, mutant: MutationOutcome, candidateAnalyzer: MutationAnalyzerSemantics, - freshProjectionHash: String?, + freshAuthorityProjectionHash: String?, ): AdmissionVerdict { val short = short(mutant.identity) return when { @@ -116,7 +124,7 @@ object MutationPopulationAdmissionCeremony { ) } - freshProjectionHash == null -> { + freshAuthorityProjectionHash == null -> { reject( DiagnosticCode.MUTATION_RATCHET_ADMISSION_MISMATCH, "M34: $short cannot be admitted: no proof of the canonical fresh measurement exists, " + @@ -124,13 +132,16 @@ object MutationPopulationAdmissionCeremony { ) } - baseAdmission.populationDigest != freshProjectionHash -> { + baseAdmission.populationDigest != freshAuthorityProjectionHash -> { reject( DiagnosticCode.MUTATION_RATCHET_ADMISSION_MISMATCH, "M34: $short was authorized against population digest " + "${short(baseAdmission.populationDigest)}, but this transition's canonical " + - "fresh measurement hashes to ${short(freshProjectionHash)}. An authorization " + - "binds the complete measured population.", + "authority projection hashes to ${short(freshAuthorityProjectionHash)}. An " + + "authorization binds the measured population's AUTHORITY content — identity, " + + "canonical outcome, family, module, topology and analyzer semantics — and " + + "deliberately not raw PIT status, which C7 declares diagnostic evidence rather " + + "than authority.", ) } @@ -180,10 +191,10 @@ object MutationPopulationAdmissionCeremony { private fun lifecycleChecks( base: MutationRatchetAuthority, candidate: MutationRatchetCandidate, - freshProjectionHash: String?, + freshAuthorityProjectionHash: String?, ): List { val diagnostics = mintChecks(base, candidate) - return diagnostics + consumptionChecks(base, candidate, freshProjectionHash) + return diagnostics + consumptionChecks(base, candidate, freshAuthorityProjectionHash) } /** @@ -222,7 +233,7 @@ object MutationPopulationAdmissionCeremony { private fun consumptionChecks( base: MutationRatchetAuthority, candidate: MutationRatchetCandidate, - freshProjectionHash: String?, + freshAuthorityProjectionHash: String?, ): List { val diagnostics = mutableListOf() val baseAdmissions = base.admissions.byIdentity() @@ -243,7 +254,7 @@ object MutationPopulationAdmissionCeremony { candidateAdmission = candidateAdmission, mutant = mutant, candidateAnalyzer = candidate.population.analyzer, - freshProjectionHash = freshProjectionHash, + freshAuthorityProjectionHash = freshAuthorityProjectionHash, ) is AdmissionVerdict.Authorized if (candidateAdmission == null) { // M37: a retained authorization may only disappear by being consumed. Otherwise a diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt index a25826bae..57d5336e8 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt @@ -89,7 +89,7 @@ class MutationRatchetVerifier { diagnostics += validateClassificationList("base authority", base.classifications) diagnostics += validateClassificationList("candidate", candidate.classifications) diagnostics += baseClassificationIntegrity(base) - val freshProjectionHash = evolutionEvidence.proof?.projectionHash + val freshAuthorityProjectionHash = evolutionEvidence.proof?.authorityProjectionHash diagnostics += outcomeRatchet( base.population, @@ -101,12 +101,12 @@ class MutationRatchetVerifier { AUTHORITY_EXCLUDED_IDENTITIES, base.admissions, candidate.admissions, - freshProjectionHash, + freshAuthorityProjectionHash, ), ) diagnostics += classificationRatchet(base, candidate) diagnostics += MutationEnrollmentCeremony.checks(base, candidate) - diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, freshProjectionHash) + diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, freshAuthorityProjectionHash) diagnostics += familyAndTargetChecks( base.population, @@ -320,7 +320,7 @@ class MutationRatchetVerifier { candidatePopulation = candidatePopulation, baseAdmissions = evolution.baseAdmissions.byIdentity(), candidateAdmissions = evolution.candidateAdmissions.byIdentity(), - freshProjectionHash = evolution.freshProjectionHash, + freshAuthorityProjectionHash = evolution.freshAuthorityProjectionHash, ) // M21: a base identity that simply stopped being measured. Absence is not evidence of @@ -351,7 +351,7 @@ class MutationRatchetVerifier { candidatePopulation: MutationPopulationBaseline, baseAdmissions: Map, candidateAdmissions: Map, - freshProjectionHash: String?, + freshAuthorityProjectionHash: String?, ): List { val diagnostics = mutableListOf() val candidateById = candidatePopulation.mutants.associateBy { it.identity } @@ -368,7 +368,7 @@ class MutationRatchetVerifier { candidateAdmission = candidateAdmissions[id], mutant = candidate, candidateAnalyzer = candidatePopulation.analyzer, - freshProjectionHash = freshProjectionHash, + freshAuthorityProjectionHash = freshAuthorityProjectionHash, ) ) { is MutationPopulationAdmissionCeremony.AdmissionVerdict.Authorized -> { @@ -775,21 +775,27 @@ private data class MutationEvolutionContext( /** Admissions this transition proposes: validated, but never authority for its own admission. */ val candidateAdmissions: MutationPopulationAdmissions = MutationPopulationAdmissions.NONE, /** - * Projection hash of the canonical fresh measurement ([MutationEvolutionEvidence.proof]) - the - * digest a population authorization must match to be consumable. Null means no trusted - * measurement proof exists, and admission then fails closed. + * Authority projection hash of the canonical fresh measurement + * ([MutationPopulationEvolutionProof.authorityProjectionHash]) - the digest a population + * authorization must match to be consumable (M34). Raw PIT status is excluded by design (C7). + * Null means no trusted measurement proof exists, and admission then fails closed. */ - val freshProjectionHash: String? = null, + val freshAuthorityProjectionHash: String? = null, ) // No population hash is stored in mutation-evolution.yml: exact measurement // equality already binds the candidate, while this proof binds the verifier call. -// The proof hashes the FULL canonical comparison projection — identity, raw status, -// canonical outcome, family, module, family/module topology and analyzer semantics — -// not just the identity set, so it cannot be reused for a population that shares the -// identities but differs in outcomes. +// [projectionHash] hashes the FULL canonical comparison projection — identity, raw status, +// canonical outcome, family, module, family/module topology and analyzer semantics — not just the +// identity set, so it cannot be reused for a population that shares the identities but differs in +// outcomes. It stays raw-exact: that is the measurement-truth question. +// [authorityProjectionHash] hashes the authority projection — identity, canonical outcome, family, +// module, topology and analyzer semantics, with raw status deliberately excluded. That is the +// population-context question M34 asks, and it is computed from the same fresh measurement by this +// verifier, so it can never be supplied by a candidate. class MutationPopulationEvolutionProof private constructor( val projectionHash: String, + val authorityProjectionHash: String, ) { fun matches(population: MutationPopulationBaseline): Boolean = projectionHash == population.projectionHash() @@ -828,6 +834,7 @@ class MutationPopulationEvolutionProof private constructor( if (diagnostics.isEmpty()) { MutationPopulationEvolutionProof( projectionHash = fresh.projectionHash(), + authorityProjectionHash = fresh.authorityProjectionHash(), ) } else { null @@ -923,6 +930,44 @@ class MutationPopulationEvolutionProof private constructor( analyzer.timeoutConst + "|" + analyzer.timeoutFactor, ) } + + /** + * The canonical authority projection: the population context a population authorization is + * bound to (M34). Raw PIT status is deliberately EXCLUDED. + * + * C7 ([MutationOutcome]) declares raw `status` diagnostic evidence and not authority, and + * anticipates TIMED_OUT↔SURVIVED scheduler races. Four complete unrestricted campaigns at + * identical effective PIT inputs produced four distinct raw-status digests while this + * projection was byte-identical every time, so binding neighbours' raw status here let a + * scheduler race invalidate adjudicated authority that was itself stable. + * + * What remains is exactly the authority-relevant content: identity, canonical outcome, + * family, module, family/module topology and analyzer semantics. Use this for M34; the + * raw-exact [canonicalProjection] stays the measurement proof (M21, [exactComparison]). + */ + private fun MutationPopulationBaseline.authorityProjection(): String = + buildString { + mutants + .map { "${it.identity}|${it.outcome}|${it.family}|${it.module}" } + .sorted() + .forEach { appendLine(it) } + appendLine( + "topology=" + + byFamily.toSortedMap().entries.joinToString(",") { + "${it.key}:${it.value.modules.sorted().joinToString("+")}" + }, + ) + appendLine( + "analyzer=" + + analyzer.pluginVersion + "|" + analyzer.engineVersion + "|" + + analyzer.mutators + .sorted() + .joinToString("+") + "|" + + analyzer.timeoutConst + "|" + analyzer.timeoutFactor, + ) + } + + private fun MutationPopulationBaseline.authorityProjectionHash(): String = authorityProjection().sha256() } } diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremonyTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremonyTest.kt index 515d5b59f..3c94ad8e0 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremonyTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionCeremonyTest.kt @@ -4,6 +4,7 @@ import dev.tramai.build.quality.MutationPopulationAdmissionCeremony.AdmissionVer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -147,7 +148,7 @@ class MutationPopulationAdmissionCeremonyTest : MutationRatchetTestSupport() { baseAdmissions = admissions(admission("target", populationDigest = digestOf(base))), candidatePopulation = candidate, ) - assertFailsWith(diagnostics, DiagnosticCode.MUTATION_RATCHET_ADMISSION_MISMATCH, "complete measured population") + assertFailsWith(diagnostics, DiagnosticCode.MUTATION_RATCHET_ADMISSION_MISMATCH, "authority projection") } @Test @@ -434,7 +435,7 @@ class MutationPopulationAdmissionCeremonyTest : MutationRatchetTestSupport() { candidateAdmission = null, mutant = target, candidateAnalyzer = semantics, - freshProjectionHash = "0".repeat(64), + freshAuthorityProjectionHash = "0".repeat(64), ) as? AdmissionVerdict.Rejected assertNotNull(rejected) assertEquals(DiagnosticCode.MUTATION_RATCHET_NEW_SURVIVOR, rejected.code) @@ -449,4 +450,41 @@ class MutationPopulationAdmissionCeremonyTest : MutationRatchetTestSupport() { "cannot certify its own survivors." assertEquals(expected, rejected.message) } + + // ── T1 (ceremony level): the case-3 discriminator ── + // + // Four campaigns produced four distinct raw-status digests: three identities of 2544 oscillating + // only SURVIVED<->TIMED_OUT, all NON_KILLED in every campaign, while identity, outcome, family, + // module and analyzer semantics were identical. An authorization minted against one such + // measurement must remain consumable in another. Binding neighbouring raw status (the previous + // behaviour) rejected it, which is the defect this slice removes. + + @Test + fun `T1 a neighbour's raw status movement does not invalidate an authorized consumption`() { + val target = row("target") + val neighbourMinted = row("neighbour", status = "SURVIVED") + val neighbourConsumed = row("neighbour", status = "TIMED_OUT") + val mintedAgainst = population(listOf(target, neighbourMinted)) + val fresh = population(listOf(target, neighbourConsumed)) + + // Raw status moved between the two measurements; authority content did not. + assertEquals(digestOf(mintedAgainst), digestOf(fresh)) + // The raw-exact measurement proof still distinguishes them, so nothing was relaxed where raw + // status genuinely is authority (M21 exact comparison / M32 exact row). + assertNotEquals(rawDigestOf(mintedAgainst), rawDigestOf(fresh)) + + // The authorization was minted against the measurement where the neighbour was SURVIVED and + // is consumed in the measurement where it is TIMED_OUT. It must still be authorized: the + // target is candidate-only NON_KILLED, the row matches exactly, and the population context + // is unchanged in authority terms. + val diagnostics = + verifyAdmission( + basePopulation = population(listOf(neighbourMinted)), + baseAdmissions = admissions(admission("target", populationDigest = digestOf(mintedAgainst))), + candidatePopulation = fresh, + candidateAdmissions = MutationPopulationAdmissions.NONE, + evidence = evidence(fresh), + ) + passes(diagnostics) + } } diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationEvolutionProofTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationEvolutionProofTest.kt index bed0730b4..c45729640 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationEvolutionProofTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationEvolutionProofTest.kt @@ -1,6 +1,8 @@ package dev.tramai.build.quality import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.assertTrue class MutationPopulationEvolutionProofTest : MutationRatchetTestSupport() { @@ -46,4 +48,41 @@ class MutationPopulationEvolutionProofTest : MutationRatchetTestSupport() { assertTrue(comparison.diagnostics.isEmpty()) assertTrue(comparison.proof != null) } + + // ── T1 / T2 / T8: the authority projection is authority content only ── + // + // The empirical defect: four complete unrestricted campaigns at identical effective PIT inputs + // produced four distinct raw-status digests — three identities of 2544 oscillating only between + // SURVIVED and TIMED_OUT, all NON_KILLED in every campaign — while the authority content was + // byte-identical. M34 bound the raw-status digest, so a scheduler race invalidated adjudicated + // authority. Raw status stays authoritative for the measurement proof (M21) and for each + // individual authorization's exact row (M32); it must not be population authority (C7). + + @Test + fun `T1 a neighbour's raw status movement leaves the authority projection unchanged`() { + val before = population(listOf(row("target"), row("neighbour", status = "SURVIVED"))) + val after = population(listOf(row("target"), row("neighbour", status = "TIMED_OUT"))) + assertEquals(digestOf(before), digestOf(after)) + // ...while the raw-exact measurement proof still distinguishes them, so nothing was relaxed + // where raw status genuinely is authority. + assertNotEquals(rawDigestOf(before), rawDigestOf(after)) + } + + @Test + fun `T2 an outcome change changes the authority projection`() { + val survivor = population(listOf(row("target"))) + val killer = population(listOf(row("target", status = "KILLED", outcome = "KILLED"))) + assertNotEquals(digestOf(survivor), digestOf(killer)) + } + + @Test + fun `T8 analyzer semantics change the authority projection`() { + val base = population(listOf(row("target"))) + val drifted = + population( + listOf(row("target")), + analyzer = semantics.copy(timeoutConst = semantics.timeoutConst + 1), + ) + assertNotEquals(digestOf(base), digestOf(drifted)) + } } diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetAuthorityTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetAuthorityTest.kt index f391d0fc6..2952cd337 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetAuthorityTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetAuthorityTest.kt @@ -85,7 +85,13 @@ class MutationRatchetAuthorityTest { population = candidatePopulation, classifications = candidateClassifications, targetFamilies = candidateConfiguration.mutation.targetFamilies, - admissions = MutationPopulationAdmissions.NONE, + // An identity transition must RETAIN the base's pending authorizations: M37 + // forbids an authorization disappearing without a valid consumption, so + // passing NONE here asserted a transition this authority cannot validly make. + // Retained authorizations whose target is absent from the committed + // population are M39 warnings, not failures. This test failed at the + // pristine base ec8b4da5 for exactly that reason. + admissions = authority.admissions, ), executable = MutationPopulationAggregator.canonicalSemantics(), ).filter { it.severity == DiagnosticSeverity.FAILURE } diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetTestSupport.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetTestSupport.kt index eccbff2e0..462797e46 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetTestSupport.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationRatchetTestSupport.kt @@ -220,11 +220,22 @@ abstract class MutationRatchetTestSupport { diagnostics.filter { it.severity == DiagnosticSeverity.FAILURE } /** - * The canonical projection digest the verifier itself computes for a population (the M21 - * exact-comparison proof). Tests must never invent a digest: the admission ceremony compares - * against this value, so using it is what proves the trust direction. + * The canonical projection digest the verifier itself computes for a population. + * + * This is the **authority projection** digest (identity, canonical outcome, family, module, + * topology and analyzer semantics; raw PIT status deliberately excluded), i.e. the M34 + * population context. Tests must never invent a digest: the admission ceremony compares against + * this value, so using it is what proves the trust direction. For the raw-exact measurement + * proof (M21) use `rawDigestOf`. */ protected fun digestOf(population: MutationPopulationBaseline): String = + MutationPopulationEvolutionProof + .exactComparison(population, population) + .proof!! + .authorityProjectionHash + + /** The raw-exact measurement projection digest (M21 exact comparison), raw status included. */ + protected fun rawDigestOf(population: MutationPopulationBaseline): String = MutationPopulationEvolutionProof.exactComparison(population, population).proof!!.projectionHash /** The verifier's own trusted measurement evidence for a population (the M21 exact comparison). */ From 728ee6b73433a55804c363d7b555e20f9327e52b Mon Sep 17 00:00:00 2001 From: gionag Date: Thu, 1 Oct 2026 18:22:25 +0200 Subject: [PATCH 2/3] fix(0.7.1g1P2-1): transport fixture must mint the authority digest; dedup projection suffix Two review corrections on #470, both narrow. 1. The real authority-transport fixture derived the wrong digest semantics. MutationPopulationAdmissionIntegrationTest.projectionDigest() returned exactComparison(...).proof?.projectionHash - the raw-v1 measurement digest - while production M34 now compares authorityProjectionHash. The fixture therefore minted a digest the verifier no longer consumes, and the lane passed while the real transport was broken. It now derives the authority projection, and is renamed authorityProjectionDigest so its semantics are explicit; the doc comment records why minting projectionHash here is the wrong thing. This was the miss in the previous head: the support-helper digest was corrected but the task-level fixture is a separate implementation. Verifier tests prove semantics; real-task tests prove authority transport, and only the former was fixed. 2. The Copilot suggestion: canonicalProjection() and authorityProjection() duplicated the topology= and analyzer= serialization verbatim. Both now end with append(contextLines()), one private helper. Output is byte-identical (same appendLine calls, same order, same buffer), so no digest moves - the assertion that the authority projection is exactly the raw projection minus raw status is now structural rather than incidental. Callers swept, not just the reported file: the discriminator test's uses are proof.matches(...) - the measurement proof, which correctly stays raw - and the wiring test derives no digest. No raw digest derivation remains in any transport fixture. Verification: - :build-logic:test (mutation-authority family, --rerun-tasks) - BUILD SUCCESSFUL, 142 tests, 0 failures - spotlessKotlinCheck -PtramaiFormattingBaseRef=ec8b4da5... - BUILD SUCCESSFUL - :build-logic:canonicalProbeIntegrationTest - fails on CanonicalProbeFunctionalTest.kt:507 with an identical signature at the pristine base ec8b4da5 (BASE_INTEGRATION_RC=1), so it is pre-existing and NOT regenerated here. No Step 2, no certificate, no P2, no authority or config migration. --- .../build/quality/MutationRatchetVerifier.kt | 29 +++++++++---------- ...ationPopulationAdmissionIntegrationTest.kt | 16 ++++++---- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt index 57d5336e8..7b496d6e9 100644 --- a/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt +++ b/build-logic/src/main/kotlin/dev/tramai/build/quality/MutationRatchetVerifier.kt @@ -915,20 +915,7 @@ class MutationPopulationEvolutionProof private constructor( .map { "${it.identity}|${it.status}|${it.outcome}|${it.family}|${it.module}" } .sorted() .forEach { appendLine(it) } - appendLine( - "topology=" + - byFamily.toSortedMap().entries.joinToString(",") { - "${it.key}:${it.value.modules.sorted().joinToString("+")}" - }, - ) - appendLine( - "analyzer=" + - analyzer.pluginVersion + "|" + analyzer.engineVersion + "|" + - analyzer.mutators - .sorted() - .joinToString("+") + "|" + - analyzer.timeoutConst + "|" + analyzer.timeoutFactor, - ) + append(contextLines()) } /** @@ -951,6 +938,18 @@ class MutationPopulationEvolutionProof private constructor( .map { "${it.identity}|${it.outcome}|${it.family}|${it.module}" } .sorted() .forEach { appendLine(it) } + append(contextLines()) + } + + private fun MutationPopulationBaseline.authorityProjectionHash(): String = authorityProjection().sha256() + + /** + * The serialization suffix every projection shares: family/module topology, then analyzer + * semantics. It lives in one place because the authority projection IS the raw projection + * minus raw status - duplicating the suffix would let the two drift apart silently. + */ + private fun MutationPopulationBaseline.contextLines(): String = + buildString { appendLine( "topology=" + byFamily.toSortedMap().entries.joinToString(",") { @@ -966,8 +965,6 @@ class MutationPopulationEvolutionProof private constructor( analyzer.timeoutConst + "|" + analyzer.timeoutFactor, ) } - - private fun MutationPopulationBaseline.authorityProjectionHash(): String = authorityProjection().sha256() } } diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt index ba20cc206..a8c57ee78 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt @@ -100,7 +100,7 @@ class MutationPopulationAdmissionIntegrationTest { // C: the authority base. It carries the base state plus the authorization a FUTURE transition // consumes: an authorization is merged into the base, never created by the transition that uses // it (M31). `fromBaseSha` records the base of the minting transition. - writeAdmissionLedger(candidate, mintingBaseSha, projectionDigest(candidate)) + writeAdmissionLedger(candidate, mintingBaseSha, authorityProjectionDigest(candidate)) commit("authority base with minted population authorizations") baseSha = headSha() git("branch", "authority-base") @@ -427,14 +427,18 @@ class MutationPopulationAdmissionIntegrationTest { } /** - * The verifier's own trusted digest computation, never a value this test invents: the same call - * the task makes to derive the proof it compares an authorization against. + * The verifier's own trusted **authority projection** digest computation, never a value this + * test invents: the same call the task makes to derive the proof M34 compares an authorization + * against. It must be the authority projection (identity, canonical outcome, family, module, + * topology, analyzer) and NOT the raw-exact measurement proof: production M34 compares + * `authorityProjectionHash`, so a fixture minting `projectionHash` would exercise the wrong + * digest semantics and pass while the real transport is broken. */ - private fun projectionDigest(population: MutationPopulationBaseline): String = + private fun authorityProjectionDigest(population: MutationPopulationBaseline): String = MutationPopulationEvolutionProof .exactComparison(population, population) .proof - ?.projectionHash + ?.authorityProjectionHash ?: error("the fixture population is not self-comparable, so no digest can be minted") private fun assertAuthorityTransportPreconditions( @@ -459,7 +463,7 @@ class MutationPopulationAdmissionIntegrationTest { "the candidate source must produce at least one appearing NON_KILLED identity, otherwise " + "the admission ceremony is never reached: base=$baseIds candidate=$candidateIds", ) - assertTrue(projectionDigest(base) != projectionDigest(candidate), "the two populations must differ") + assertTrue(authorityProjectionDigest(base) != authorityProjectionDigest(candidate), "the two populations must differ") } // ── task driving ── From f06c5f446b9ff76cd5e2befe5ebfcbddc8444e04 Mon Sep 17 00:00:00 2001 From: gionag Date: Thu, 1 Oct 2026 20:18:24 +0200 Subject: [PATCH 3/3] fix(0.7.1g1P2-1): wrap the transport assertion for MaxLineLength MutationPopulationAdmissionIntegrationTest.kt:466 exceeded MaxLineLength after the rename to authorityProjectionDigest. Wrapped into the standard multiline form rather than baselined or suppressed: the Detekt baseline is unchanged (base 4792 -> current 4792, 0 added, 0 removed). Gate: verifyStaticAnalysis - no new findings. spotlessKotlinCheck - clean. verifyChangePolicy - PASSED, 7 changed files, change class build-logic, no policy violations. --- .../quality/MutationPopulationAdmissionIntegrationTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt index a8c57ee78..4b9bc4321 100644 --- a/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt +++ b/build-logic/src/test/kotlin/dev/tramai/build/quality/MutationPopulationAdmissionIntegrationTest.kt @@ -463,7 +463,10 @@ class MutationPopulationAdmissionIntegrationTest { "the candidate source must produce at least one appearing NON_KILLED identity, otherwise " + "the admission ceremony is never reached: base=$baseIds candidate=$candidateIds", ) - assertTrue(authorityProjectionDigest(base) != authorityProjectionDigest(candidate), "the two populations must differ") + assertTrue( + authorityProjectionDigest(base) != authorityProjectionDigest(candidate), + "the two populations must differ", + ) } // ── task driving ──