v3/docs/adr/ADR-322A-evaluation-promotion-transaction.md
RUFLO_FLYWHEEL_TRANSACTION_V1This specification defines the boundary between evaluating a candidate and making it active. It owns purity, eligibility, compare-and-swap promotion, serving epochs, idempotency, concurrency, crash recovery, and temporary legacy behavior. It does not define how candidates are proposed (ADR-322B) or how receipts are encoded and verified (ADR-322C).
The promotion authority maintains one serializable state machine:
ActivePolicyState {
lineageId
activeChampionRef
activeGateVersion
activePolicySchemaVersion
activeSafetyEnvelopeRef
ledgerHead
servingEpoch
transactionVersion
}
ReceiptState {
receiptId
promotedAt: timestamp | null
promotionTransactionId: UUIDv7 | null
status: evaluated | consumed | expired | revoked
}
Signed evaluation receipts are immutable. Consumption and promotion metadata live in ReceiptState.
evaluateFlywheelCandidate(input) -> EvaluationReceipt
promoteFlywheelCandidate(receiptId, confirmation) -> PromotionResult
materializeServingEpoch(servingEpoch) -> MaterializationResult
recoverPromotionState() -> RecoveryResult
evaluateFlywheelCandidate may persist evidence and receipts but cannot modify ActivePolicyState, runtime policy files, or served state.
promoteFlywheelCandidate is the only ADR-322 interface authorized to advance the active champion. It requires explicit confirmation for interactive CLI/MCP calls.
Promotion is eligible only when:
activeChampionRef == receipt.baselineRef
AND receipt.decision == "accepted"
AND receiptState.promotedAt == null
AND receiptState.status == "evaluated"
AND ledgerHead == receipt.expectedLedgerHead
AND receipt.gateVersion == activeGateVersion
AND receipt.policySchemaVersion == activePolicySchemaVersion
AND receipt.safetyEnvelopeRef == activeSafetyEnvelopeRef
AND receipt.expiresAt > transactionTime
Within one serializable transaction:
ReceiptState consumed and bind its transaction ID.activeChampionRef.servingEpoch.promotedAt, proposer/substitution identity, and recovery metadata.All changes commit or none do. A transaction-capable store is preferred; a write-ahead journal is acceptable only when deterministic recovery is proven by the same fault suite.
The committed active champion is authoritative. Runtime materialization is derived, idempotent state:
ServedPolicyState {
championRef
servingEpoch
materializedAt
materializationHash
}
Materialization writes a complete epoch to a temporary location, validates its hash, then atomically switches the runtime pointer. A crash can leave the previous epoch served temporarily, but cannot create two authoritative champions. Recovery converges to the committed ActivePolicyState.
ReceiptState.receiptId consumption.servingEpoch.PromotionResult.For at most one release, legacy implicit application is available only with:
RUFLO_FLYWHEEL_LEGACY_APPLY=1
Every use emits a structured deprecation event. New CLI/MCP surfaces never set or honor this flag. Without it, the compatibility wrapper evaluates only. The implicit-application path is removed in the following release.