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 @@ -99,6 +99,72 @@ sealed interface CertificateConsumption {
) : CertificateConsumption
}

/**
* The transition's population-authority context for one admission verdict (M34).
*
* Carries the two things a verdict needs about authority - the canonical fresh projection and the
* consumptions that projection certifies - as one value, so the two judgment paths (the lifecycle
* scan and the appearing-identity path) are handed the same object and cannot disagree about
* consumption. Bundling them also keeps [MutationPopulationAdmissionCeremony.appearanceVerdict]
* inside the repository's parameter budget without suppressing anything.
*
* Produced exactly once per transition and passed unchanged to every consumer - M34, the admission
* lifecycle and the certificate lifecycle (M44/M47). Consumers must never re-derive it: a
* value-equal recomputation is not the same property as shared evidence, and only one production
* site makes it structurally impossible for the rules to disagree about which consumptions were
* proven. `CertificateCustodyTransportTest` pins that with a source-level assertion.
*/
data class AdmissionAuthority(
val freshAuthorityProjectionHash: String?,
val certifiedConsumptions: Set<CertificateConsumption.Valid> = emptySet(),
)

/**
* The certified consumptions a base can prove for this fresh projection (Step 3b).
*
* The single production site for the facts M34, M44 and M47 act on: they all consume the same
* `Set<Valid>`, so no consumer re-derives its own and the call sites cannot drift apart. Both inputs
* come from the coherent transition context - the base snapshot supplies the certificate ledger, the
* historical admission digests and the exact authorized identity set, while the authority-v2
* projection comes only from the fresh measurement.
*
* One fact is produced per distinct historical digest the base's admissions were minted under, and
* each citation is verified against that admission's own digest (M41) and the exact base
* authorization set (M42). A null projection yields no facts: without a fresh measurement there is
* nothing to certify, which is the fail-closed state.
*/
fun certifiedConsumptions(
certificates: MutationAuthorityDigestCertificates,
baseAdmissions: MutationPopulationAdmissions,
freshAuthorityProjectionHash: String?,
): Set<CertificateConsumption.Valid> {
val projection = freshAuthorityProjectionHash ?: return emptySet()
val identities = baseAdmissions.admissions.map { it.identity }
val historicalDigests = baseAdmissions.admissions.map { it.populationDigest }.distinct()
return certificates.certificates
.flatMap { certificate ->
historicalDigests.mapNotNull { digest ->
verifyCertificateConsumption(
certificate = certificate,
baseCertificates = certificates,
citedAdmissionPopulationDigest = digest,
baseAdmissionIdentities = identities,
freshAuthorityProjectionHash = projection,
) as? CertificateConsumption.Valid
}
}.toSet()
}

/**
* The authority context for a verdict over one base snapshot and this transition's fresh projection.
*/
fun MutationRatchetAuthority.admissionAuthority(freshAuthorityProjectionHash: String?): AdmissionAuthority =
AdmissionAuthority(
freshAuthorityProjectionHash = freshAuthorityProjectionHash,
certifiedConsumptions =
certifiedConsumptions(certificates, admissions, freshAuthorityProjectionHash),
)

/**
* The consumption entry point for call sites: delegates to [CertificateConsumption.Valid.verify], so
* there is exactly one implementation of the proof and no second route to a fact.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,10 @@ abstract class MaintainabilityBaselinePlugin : Plugin<Project> {
// defaulted away: without it M35 cannot bind a minted authorization to its
// base, and M36-M38 cannot see retention or rewriting of a pending row.
admissions = MutationPopulationAdmissionLoader.load(project.rootDir),
// The transition's own certificate ledger, loaded the same way: M45 binds a
// newly minted certificate to this base, M46 rejects a rewritten retained one,
// and M47 must be able to see a base certificate disappear.
certificates = MutationAuthorityDigestCertificateLoader.load(project.rootDir),
)
val diagnostics =
MutationRatchetVerifier().verify(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ object MutationPopulationAdmissionCeremony {
fun checks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshAuthorityProjectionHash: String?,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, freshAuthorityProjectionHash)
authority: AdmissionAuthority,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, authority)

/**
* The verdict for one identity that is present in the candidate population and absent from the
Expand All @@ -89,8 +89,10 @@ object MutationPopulationAdmissionCeremony {
candidateAdmission: MutationPopulationAdmission?,
mutant: MutationOutcome,
candidateAnalyzer: MutationAnalyzerSemantics,
freshAuthorityProjectionHash: String?,
authority: AdmissionAuthority,
): AdmissionVerdict {
val freshAuthorityProjectionHash = authority.freshAuthorityProjectionHash
val certifiedConsumptions = authority.certifiedConsumptions
val short = short(mutant.identity)
return when {
baseAdmission == null -> {
Expand Down Expand Up @@ -132,7 +134,14 @@ object MutationPopulationAdmissionCeremony {
)
}

baseAdmission.populationDigest != freshAuthorityProjectionHash -> {
// M34 with certified migration (Step 3b): a raw-v1 authorization remains consumable when a
// base certificate translates exactly its historical digest into this transition's fresh
// authority projection, bounded to the exact base authorization set. The facts come from
// one production site ([certifiedConsumptions]) and a Valid exists only if M43-M42 passed
// against the base ledger, so this branch cannot be reached by asserting anything. Without
// a certificate the condition is unchanged and M34 fails exactly as it did before.
baseAdmission.populationDigest != freshAuthorityProjectionHash &&
certifiedConsumptions.none { it.fromDigest == baseAdmission.populationDigest } -> {
reject(
DiagnosticCode.MUTATION_RATCHET_ADMISSION_MISMATCH,
"M34: $short was authorized against population digest " +
Expand Down Expand Up @@ -191,10 +200,10 @@ object MutationPopulationAdmissionCeremony {
private fun lifecycleChecks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshAuthorityProjectionHash: String?,
authority: AdmissionAuthority,
): List<VerificationDiagnostic> {
val diagnostics = mintChecks(base, candidate)
return diagnostics + consumptionChecks(base, candidate, freshAuthorityProjectionHash)
return diagnostics + consumptionChecks(base, candidate, authority)
}

/**
Expand Down Expand Up @@ -233,7 +242,7 @@ object MutationPopulationAdmissionCeremony {
private fun consumptionChecks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
freshAuthorityProjectionHash: String?,
authority: AdmissionAuthority,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val baseAdmissions = base.admissions.byIdentity()
Expand All @@ -254,7 +263,7 @@ object MutationPopulationAdmissionCeremony {
candidateAdmission = candidateAdmission,
mutant = mutant,
candidateAnalyzer = candidate.population.analyzer,
freshAuthorityProjectionHash = freshAuthorityProjectionHash,
authority = authority,
) 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 @@ -40,6 +40,17 @@ data class MutationRatchetAuthority(
* must fail to compile rather than silently degrade the transition to "no authorizations".
*/
val admissions: MutationPopulationAdmissions,
/**
* Base-side digest-migration certificates (0.7.1g1P2, M40-M47). OPTIONAL authority, like
* [enrollments]: a base that predates the certificate ledger simply has none, which is the most
* restrictive state - no certified migration exists, so every raw-v1 admission still fails M34.
* A present ledger is validated by its own loader, so a malformed one fails hard rather than
* degrading into "no certificates".
*
* The default is convenient for fixtures and is always the conservative answer; the loader passes
* it explicitly, read from the base revision.
*/
val certificates: MutationAuthorityDigestCertificates = MutationAuthorityDigestCertificates.NONE,
)

/**
Expand Down Expand Up @@ -68,6 +79,18 @@ data class MutationRatchetCandidate(
* not compile. Missing it silently turned every candidate proposal into "none".
*/
val admissions: MutationPopulationAdmissions,
/**
* Certificates this transition proposes. They are validated here (M45 binds a newly introduced
* certificate to the base it is proposed against, M46 rejects a rewritten retained one) but they
* can never authorize a consumption in the same transition: only
* [MutationRatchetAuthority.certificates] is consulted, so a candidate that both mints and
* consumes fails M43.
*
* Defaults to [MutationAuthorityDigestCertificates.NONE] for fixtures. The loader always passes it
* explicitly. A call site that forgot would weaken nothing: every removal then looks
* unconsumed and M47 fails loudly, which is the conservative direction.
*/
val certificates: MutationAuthorityDigestCertificates = MutationAuthorityDigestCertificates.NONE,
)

object MutationRatchetAuthorityLoader {
Expand Down Expand Up @@ -164,13 +187,29 @@ object MutationRatchetAuthorityLoader {
admissionsFile.writeText(admissionsAtBase.output, Charsets.UTF_8)
}
val admissions = MutationPopulationAdmissionLoader.load(tempDir)
// Digest-migration certificates (0.7.1g1P2) are OPTIONAL authority for the same reason: a
// base that predates the ledger has none, which is the most restrictive state, since no
// certified migration exists and every raw-v1 admission still fails M34. A present ledger
// is validated by its own loader, so a malformed one fails hard instead of degrading into
// "no certificates".
val certificatesFile = File(qualityDir, "mutation-authority-digest-certificates.yml")
val certificatesAtBase =
runGit(
rootDir,
listOf("show", "$baseSha:${MutationAuthorityDigestCertificateLoader.FILE_NAME}"),
)
if (certificatesAtBase.exitCode == 0) {
certificatesFile.writeText(certificatesAtBase.output, Charsets.UTF_8)
}
val certificates = MutationAuthorityDigestCertificateLoader.load(tempDir)
return MutationRatchetAuthority(
baseSha = baseSha,
population = population,
classifications = classifications,
targetFamilies = configuration.mutation.targetFamilies,
enrollments = enrollments,
admissions = admissions,
certificates = certificates,
)
} finally {
tempDir.deleteRecursively()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class MutationRatchetVerifier {
diagnostics += validateClassificationList("candidate", candidate.classifications)
diagnostics += baseClassificationIntegrity(base)
val freshAuthorityProjectionHash = evolutionEvidence.proof?.authorityProjectionHash
// Produced ONCE for the whole transition. M34, the admission lifecycle and the certificate
// lifecycle (M44/M47) all receive this exact instance, so they cannot quietly come to
// different conclusions about which consumptions were proven.
val admissionAuthority = base.admissionAuthority(freshAuthorityProjectionHash)
diagnostics +=
outcomeRatchet(
base.population,
Expand All @@ -102,11 +106,21 @@ class MutationRatchetVerifier {
base.admissions,
candidate.admissions,
freshAuthorityProjectionHash,
admissionAuthority,
),
)
diagnostics += classificationRatchet(base, candidate)
diagnostics += MutationEnrollmentCeremony.checks(base, candidate)
diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, freshAuthorityProjectionHash)
diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, admissionAuthority)
// Certificate custody (M44-M47), decided on the same facts M34 uses: one production site, so the
// lifecycle rules cannot disagree with the admission verdicts about what was consumed.
diagnostics +=
MutationAuthorityDigestCertificateCeremony.checks(
base = base.certificates,
candidate = candidate.certificates,
baseSha = base.baseSha,
validConsumptions = admissionAuthority.certifiedConsumptions,
)
diagnostics +=
familyAndTargetChecks(
base.population,
Expand Down Expand Up @@ -320,7 +334,7 @@ class MutationRatchetVerifier {
candidatePopulation = candidatePopulation,
baseAdmissions = evolution.baseAdmissions.byIdentity(),
candidateAdmissions = evolution.candidateAdmissions.byIdentity(),
freshAuthorityProjectionHash = evolution.freshAuthorityProjectionHash,
authority = evolution.authority,
)

// M21: a base identity that simply stopped being measured. Absence is not evidence of
Expand Down Expand Up @@ -351,7 +365,7 @@ class MutationRatchetVerifier {
candidatePopulation: MutationPopulationBaseline,
baseAdmissions: Map<String, MutationPopulationAdmission>,
candidateAdmissions: Map<String, MutationPopulationAdmission>,
freshAuthorityProjectionHash: String?,
authority: AdmissionAuthority,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val candidateById = candidatePopulation.mutants.associateBy { it.identity }
Expand All @@ -368,7 +382,7 @@ class MutationRatchetVerifier {
candidateAdmission = candidateAdmissions[id],
mutant = candidate,
candidateAnalyzer = candidatePopulation.analyzer,
freshAuthorityProjectionHash = freshAuthorityProjectionHash,
authority = authority,
)
) {
is MutationPopulationAdmissionCeremony.AdmissionVerdict.Authorized -> {
Expand Down Expand Up @@ -781,6 +795,12 @@ private data class MutationEvolutionContext(
* Null means no trusted measurement proof exists, and admission then fails closed.
*/
val freshAuthorityProjectionHash: String? = null,
/**
* Authority context for appearing-identity verdicts (M34, Step 3b): the fresh projection plus the
* consumptions it certifies. Defaults to no projection and no consumptions, which is the
* fail-closed state: an appearing identity with no trusted measurement proof still fails M34.
*/
val authority: AdmissionAuthority = AdmissionAuthority(null),
)

// No population hash is stored in mutation-evolution.yml: exact measurement
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dev.tramai.build.quality

import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

/**
* Base-revision transport for the digest-migration certificate ledger (Step 3b).
*
* M43/M47 are judged against the certificate ledger **as it exists in the base**, so the base side
* must be readable at a revision - not only from the working tree. This proves both directions
* against real repository history, using the same `git show`-into-a-temp-tree path the admissions,
* enrollments and baseline already use, so one authority snapshot carries the whole base context:
* population, classifications, enrollments, admissions and certificates.
*
* Both SHAs are real and immutable: `9ebe7b44` is the P1M merge that minted the certificate, and
* `64d05450` is the base P1M was proposed against, which predates the ledger entirely.
*/
class CertificateBaseRevisionTransportTest {
private val repositoryRoot = repositoryRoot()

@Test
fun `the revision that minted the certificate exposes it through the authority snapshot`() {
val authority = MutationRatchetAuthorityLoader.load(repositoryRoot, P1M_MERGE)

val certificate = authority.certificates.certificates.single()
assertEquals(
authority.admissions.admissions
.map { it.populationDigest }
.toSet(),
setOf(certificate.fromDigest),
)
}

@Test
fun `a base that predates the certificate ledger exposes no certificates`() {
val authority = MutationRatchetAuthorityLoader.load(repositoryRoot, PRE_P1M_BASE)

assertTrue(
authority.certificates.certificates.isEmpty(),
"a base predating the ledger must expose no certificates, got " +
"${authority.certificates.certificates.map { it.fromDigest }}",
)
// The same base still carries its authorizations: this isolates the certificate ledger
// rather than proving that an unrelated empty snapshot is empty.
assertTrue(authority.admissions.admissions.isNotEmpty())
}

/**
* The repository root, found by walking up to the `gradlew` marker (the idiom the other
* real-task tests use) rather than assuming a fixed depth below it.
*/
private fun repositoryRoot(): File {
var candidate = File(System.getProperty("user.dir"))
while (candidate.parentFile != null && !File(candidate, "gradlew").isFile) {
candidate = candidate.parentFile!!
}
check(File(candidate, "gradlew").isFile) {
"no repository root (gradlew marker) found above ${System.getProperty("user.dir")}"
}
return candidate
}

private companion object {
/** The P1M merge commit: the first revision whose tree contains the certificate ledger. */
const val P1M_MERGE = "9ebe7b4430760ac313874750e7c5ac9bbe56ef1e"

/** The base the P1M transition was proposed against: predates the certificate ledger. */
const val PRE_P1M_BASE = "64d05450c285ecd9cf3635ca2935da816f649856"
}
}
Loading
Loading