Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand All @@ -65,8 +73,8 @@ object MutationPopulationAdmissionCeremony {
fun checks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshProjectionHash: String?,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, freshProjectionHash)
freshAuthorityProjectionHash: String?,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, freshAuthorityProjectionHash)

/**
* The verdict for one identity that is present in the candidate population and absent from the
Expand All @@ -81,7 +89,7 @@ object MutationPopulationAdmissionCeremony {
candidateAdmission: MutationPopulationAdmission?,
mutant: MutationOutcome,
candidateAnalyzer: MutationAnalyzerSemantics,
freshProjectionHash: String?,
freshAuthorityProjectionHash: String?,
): AdmissionVerdict {
val short = short(mutant.identity)
return when {
Expand Down Expand Up @@ -116,21 +124,24 @@ 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, " +
"so the authorized population digest cannot be checked. Admission fails closed.",
)
}

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.",
)
}

Expand Down Expand Up @@ -180,10 +191,10 @@ object MutationPopulationAdmissionCeremony {
private fun lifecycleChecks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshProjectionHash: String?,
freshAuthorityProjectionHash: String?,
): List<VerificationDiagnostic> {
val diagnostics = mintChecks(base, candidate)
return diagnostics + consumptionChecks(base, candidate, freshProjectionHash)
return diagnostics + consumptionChecks(base, candidate, freshAuthorityProjectionHash)
}

/**
Expand Down Expand Up @@ -222,7 +233,7 @@ object MutationPopulationAdmissionCeremony {
private fun consumptionChecks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshProjectionHash: String?,
freshAuthorityProjectionHash: String?,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val baseAdmissions = base.admissions.byIdentity()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -351,7 +351,7 @@ class MutationRatchetVerifier {
candidatePopulation: MutationPopulationBaseline,
baseAdmissions: Map<String, MutationPopulationAdmission>,
candidateAdmissions: Map<String, MutationPopulationAdmission>,
freshProjectionHash: String?,
freshAuthorityProjectionHash: String?,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val candidateById = candidatePopulation.mutants.associateBy { it.identity }
Expand All @@ -368,7 +368,7 @@ class MutationRatchetVerifier {
candidateAdmission = candidateAdmissions[id],
mutant = candidate,
candidateAnalyzer = candidatePopulation.analyzer,
freshProjectionHash = freshProjectionHash,
freshAuthorityProjectionHash = freshAuthorityProjectionHash,
)
) {
is MutationPopulationAdmissionCeremony.AdmissionVerdict.Authorized -> {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -828,6 +834,7 @@ class MutationPopulationEvolutionProof private constructor(
if (diagnostics.isEmpty()) {
MutationPopulationEvolutionProof(
projectionHash = fresh.projectionHash(),
authorityProjectionHash = fresh.authorityProjectionHash(),
)
} else {
null
Expand Down Expand Up @@ -908,6 +915,41 @@ class MutationPopulationEvolutionProof private constructor(
.map { "${it.identity}|${it.status}|${it.outcome}|${it.family}|${it.module}" }
.sorted()
.forEach { appendLine(it) }
append(contextLines())
}

/**
* 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) }
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(",") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
Loading
Loading