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 @@ -868,6 +868,12 @@ abstract class MaintainabilityBaselinePlugin : Plugin<Project> {
// 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),
// The promotion declaration is loaded from the transition's OWN tree by design:
// it is candidate-side, because it must name the base SHA it promotes onto and
// the base branch cannot name the commit that will contain it. Absent means "no
// promotion", which is the fail-closed state - M35/M45 then bind every
// appearing row to the transition base exactly as before.
promotion = MutationPopulationPromotionLoader.load(project.rootDir),
)
val diagnostics =
MutationRatchetVerifier().verify(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ object MutationAuthorityDigestCertificateCeremony {
candidate: MutationAuthorityDigestCertificates,
baseSha: String,
validConsumptions: Set<CertificateConsumption.Valid> = emptySet(),
promotion: MutationPopulationPromotion? = null,
): List<VerificationDiagnostic> =
mintChecks(base, candidate, baseSha) +
mintChecks(base, candidate, baseSha, promotion) +
retentionChecks(base, candidate) +
singleUseChecks(base, candidate, validConsumptions) +
removalChecks(base, candidate, validConsumptions)
Expand All @@ -48,17 +49,23 @@ object MutationAuthorityDigestCertificateCeremony {
* Anti-replay at mint time: a certificate payload cannot be replayed onto a different authority
* base. Deliberately not re-checked later - a certificate legitimately survives intermediate
* merges, so requiring it to track the immediate base would make delayed consumption impossible.
*
* One narrow exception (0.7.1i): a certificate carried across a release promotion boundary by an
* exact [MutationPopulationPromotion] was not minted here, so it is not bound to this base. The
* exception applies only to the source digest the declaration carries.
*/
private fun mintChecks(
base: MutationAuthorityDigestCertificates,
candidate: MutationAuthorityDigestCertificates,
baseSha: String,
promotion: MutationPopulationPromotion?,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val baseByFromDigest = base.byFromDigest()
for ((fromDigest, certificate) in candidate.byFromDigest()) {
if (fromDigest in baseByFromDigest) continue
if (certificate.fromBaseSha != baseSha) {
val carriedForward = promotion != null && certificate.fromDigest == promotion.populationDigest
if (certificate.fromBaseSha != baseSha && !carriedForward) {
diagnostics +=
certificateFailure(
fromDigest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ object MutationPopulationAdmissionCeremony {
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
authority: AdmissionAuthority,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, authority)
promotion: MutationPopulationPromotion? = null,
): List<VerificationDiagnostic> = lifecycleChecks(base, candidate, authority, promotion)

/**
* The verdict for one identity that is present in the candidate population and absent from the
Expand Down Expand Up @@ -201,8 +202,9 @@ object MutationPopulationAdmissionCeremony {
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
authority: AdmissionAuthority,
promotion: MutationPopulationPromotion?,
): List<VerificationDiagnostic> {
val diagnostics = mintChecks(base, candidate)
val diagnostics = mintChecks(base, candidate, promotion)
return diagnostics + consumptionChecks(base, candidate, authority)
}

Expand All @@ -212,15 +214,22 @@ object MutationPopulationAdmissionCeremony {
* onto a different authority base. It is deliberately NOT re-checked at consumption: a pending
* authorization legitimately survives intermediate merges, and requiring it to track the
* immediate base would make delayed consumption impossible.
*
* One narrow exception (0.7.1i): a row carried across a release promotion boundary by an exact
* [MutationPopulationPromotion] was not minted here, so it is not bound to this base. The
* exception applies only to the digest the declaration carries - a newly minted row has a
* different digest and is rejected exactly as before.
*/
private fun mintChecks(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
promotion: MutationPopulationPromotion?,
): List<VerificationDiagnostic> {
val diagnostics = mutableListOf<VerificationDiagnostic>()
val baseAdmissions = base.admissions.byIdentity()
for ((id, admission) in candidate.admissions.byIdentity().filterKeys { it !in baseAdmissions }) {
if (admission.fromBaseSha != base.baseSha) {
val carriedForward = promotion != null && admission.populationDigest == promotion.populationDigest
if (admission.fromBaseSha != base.baseSha && !carriedForward) {
diagnostics +=
VerificationDiagnostic.failure(
DiagnosticCode.MUTATION_RATCHET_ADMISSION_INVALID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package dev.tramai.build.quality

import org.gradle.api.GradleException
import org.yaml.snakeyaml.LoaderOptions
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.error.YAMLException
import java.io.File

/**
* A promotion declaration: the statement that one exact, already-certified evidence population is
* CARRIED ACROSS a release promotion boundary rather than minted by the promotion transition.
*
* It grants no authority. M35 and M45 bind a newly introduced admission/certificate to the authority
* base it was minted against, and a promotion cannot satisfy that binding by any legitimate means:
* re-minting admissions, rewriting `fromBaseSha`, manufacturing replacement certificates and a
* per-entry migration chain are all forbidden. This declaration states the narrow fact the promotion
* actually needs - the appearing evidence is the SAME certified population, unchanged - so the
* mint-binding rules do not apply to it.
*
* SHA-exact and candidate-side: it names the exact base SHA the promotion is proposed against, so it
* can never authorize a different transition, and it travels in the promotion's own candidate because
* the base branch cannot name a commit that does not exist until the declaration is committed.
*
* Fail-closed: an absent or malformed declaration, or a declaration that does not describe the
* candidate exactly, means "no promotion" - every appearing row then fails M35/M45 as before.
*/
data class MutationPopulationPromotion(
/** The exact authority base SHA this promotion is proposed against. */
val promotionBase: String,
/** The digest of the carried population; the only digest this declaration carries forward. */
val populationDigest: String,
/** The exact number of admissions in the carried population. */
val admissions: Int,
/** The exact number of digest-migration certificates in the carried population. */
val certificates: Int,
) {
companion object {
/**
* The declaration that legitimately carries the candidate's appearing evidence across
* [base], or null when there is none or it does not describe that evidence exactly.
*
* Every field is enforced against the ledgers themselves, so a declaration cannot authorize a
* population other than the one it names: the base SHA must be this transition's, both counts
* must equal the number of rows appearing relative to the base, and every appearing row must
* carry the declared digest. A single mismatch anywhere yields no promotion at all.
*/
fun carried(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
): MutationPopulationPromotion? =
candidate.promotion?.takeIf { promotion ->
promotion.promotionBase == base.baseSha &&
carriesAdmissions(base, candidate, promotion) &&
carriesCertificates(base, candidate, promotion)
}

/** The exact admission count, and every appearing row's digest. */
private fun carriesAdmissions(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
promotion: MutationPopulationPromotion,
): Boolean {
val baseAdmissions = base.admissions.byIdentity().keys
val appearing =
candidate.admissions
.byIdentity()
.filterKeys { it !in baseAdmissions }
.values
return appearing.size == promotion.admissions &&
appearing.all { it.populationDigest == promotion.populationDigest }
}

/** The exact certificate count, and every appearing certificate's source digest. */
private fun carriesCertificates(
base: MutationRatchetAuthority,
candidate: MutationRatchetCandidate,
promotion: MutationPopulationPromotion,
): Boolean {
val baseCertificates = base.certificates.byFromDigest().keys
val appearing =
candidate.certificates
.byFromDigest()
.filterKeys { it !in baseCertificates }
.values
return appearing.size == promotion.certificates &&
appearing.all { it.fromDigest == promotion.populationDigest }
}
}
}

/**
* Parses config/quality/mutation-population-promotions.yml.
*
* An absent file means "no promotion", which is fail-closed: every appearing admission and
* certificate is then bound to the transition base by M35/M45 exactly as before. A present but
* malformed file is a hard failure - a broken declaration must never silently degrade into "no
* promotion happened to be needed".
*
* 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.
Comment on lines +99 to +100
*/
object MutationPopulationPromotionLoader {
const val FILE_NAME = "config/quality/mutation-population-promotions.yml"

private const val SCHEMA_VERSION = "1"
private const val MAX_ALIASES_FOR_COLLECTIONS = 20
private val COMMIT_SHA = Regex("[0-9a-f]{40}")
private val DIGEST = Regex("[0-9a-f]{64}")

fun load(repositoryRoot: File): MutationPopulationPromotion? {
val file = File(repositoryRoot, FILE_NAME)
if (!file.isFile) {
return null
}
val declarations = parseDeclarations(readLedger(file))
// One transition has exactly one base, so at most one declaration can apply. More than one
// would make the promotion depend on file order, which is not an authority.
require(declarations.size <= 1) {
"$FILE_NAME: at most one promotion declaration may be present; found ${declarations.size}"
}
return declarations.singleOrNull()
}

private fun readLedger(file: File): Map<String, Any?> {
val loaderOptions =
LoaderOptions().apply {
isAllowDuplicateKeys = false
maxAliasesForCollections = MAX_ALIASES_FOR_COLLECTIONS
}
return try {
@Suppress("UNCHECKED_CAST")
Yaml(loaderOptions).load<Map<String, Any?>>(file.readText(Charsets.UTF_8)) ?: emptyMap()
} catch (e: YAMLException) {
throw GradleException("Invalid $FILE_NAME: ${e.message}", e)
}
}

private fun parseDeclarations(raw: Map<String, Any?>): List<MutationPopulationPromotion> {
val schemaVersion = raw["schemaVersion"]?.toString()
require(schemaVersion == SCHEMA_VERSION) {
"$FILE_NAME: schemaVersion must be '$SCHEMA_VERSION', was '$schemaVersion'"
}
val promotions = raw["promotions"]
require(promotions is List<*>) { "$FILE_NAME: promotions must be a list" }
return promotions.mapIndexed { index, item ->
require(item is Map<*, *>) { "$FILE_NAME: promotions[$index] must be a mapping" }
val promotionBase =
requireCanonical(item, "promotionBase", index, COMMIT_SHA, "a 40-hex commit SHA")
val populationDigest =
requireCanonical(item, "populationDigest", index, DIGEST, "a 64-hex digest")
MutationPopulationPromotion(
promotionBase = promotionBase,
populationDigest = populationDigest,
admissions = requiredCount(item, "admissions", index),
certificates = requiredCount(item, "certificates", index),
)
}
}

/**
* Counts are part of the declaration's meaning, so an absent one is invalid rather than zero: a
* declaration that forgot `certificates` must not read as "carries no certificates".
*/
private fun requiredCount(
entry: Map<*, *>,
field: String,
index: Int,
): Int {
val value =
(entry[field] as? Number)?.toInt()
?: throw GradleException("$FILE_NAME: promotions[$index].$field is required and must be an integer")
require(value >= 0) {
"$FILE_NAME: promotions[$index].$field must not be negative"
}
return value
}

private fun requireCanonical(
entry: Map<*, *>,
field: String,
index: Int,
pattern: Regex,
expected: String,
): String {
val value =
entry[field]?.toString()?.takeIf { it.isNotBlank() }
?: throw GradleException("$FILE_NAME: promotions[$index].$field is required and must be non-blank")
require(pattern.matches(value)) {
"$FILE_NAME: promotions[$index].$field '$value' is not $expected"
}
return value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ data class MutationRatchetCandidate(
* unconsumed and M47 fails loudly, which is the conservative direction.
*/
val certificates: MutationAuthorityDigestCertificates = MutationAuthorityDigestCertificates.NONE,
/**
* Promotion declaration proposed by this transition (0.7.1i). Not authority: it grants nothing
* and authorizes no admission, it states that an already-certified evidence population is
* carried across a release promotion boundary and therefore was not minted here, so M35/M45 do
* not bind it to the promotion base.
*
* Defaults to absent, which is the conservative state: with no promotion every appearing
* admission and certificate must still bind the transition base, exactly as before.
*/
val promotion: MutationPopulationPromotion? = null,
)

object MutationRatchetAuthorityLoader {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ class MutationRatchetVerifier {
)
diagnostics += classificationRatchet(base, candidate)
diagnostics += MutationEnrollmentCeremony.checks(base, candidate)
diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, admissionAuthority)
// A promotion declaration carries already-certified evidence across the release promotion
// boundary. Computed once from the same ledgers both ceremonies judge, so the admission and
// certificate rules cannot disagree about what was carried. Null means "no promotion".
val promotion = MutationPopulationPromotion.carried(base, candidate)
diagnostics += MutationPopulationAdmissionCeremony.checks(base, candidate, admissionAuthority, promotion)
// 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 +=
Expand All @@ -120,6 +124,7 @@ class MutationRatchetVerifier {
candidate = candidate.certificates,
baseSha = base.baseSha,
validConsumptions = admissionAuthority.certifiedConsumptions,
promotion = promotion,
)
diagnostics +=
familyAndTargetChecks(
Expand Down
Loading
Loading