From ea9e382c606f4562392f74d04953b695377ae8a4 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Mon, 13 Jul 2026 15:25:37 -0400 Subject: [PATCH] Define Codex lineage promotion gates --- .../CodexLineagePromotionEvaluator.swift | 227 ++++++++++++++++++ .../CodexLineagePromotionEvaluatorTests.swift | 204 ++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 Sources/CodexBarCore/Providers/Codex/CodexLineagePromotionEvaluator.swift create mode 100644 Tests/CodexBarTests/CodexLineagePromotionEvaluatorTests.swift diff --git a/Sources/CodexBarCore/Providers/Codex/CodexLineagePromotionEvaluator.swift b/Sources/CodexBarCore/Providers/Codex/CodexLineagePromotionEvaluator.swift new file mode 100644 index 0000000000..300197d5dd --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CodexLineagePromotionEvaluator.swift @@ -0,0 +1,227 @@ +import Foundation + +/// Evidence-based gate for promoting lineage accounting behind a reversible authority switch. +/// +/// This evaluator does not choose token totals or tune an error percentage. It verifies that the +/// independently collected correctness and operational evidence is complete enough to promote. +enum CodexLineagePromotionEvaluator { + enum CancellationStage: String, CaseIterable, Equatable, Hashable, Sendable { + case rootIndexing + case parsing + case graphConstruction + case reconciliation + case prePublication + } + + struct FamilyRoutingEvidence: Equatable, Sendable { + let primaryFamilyCount: Int + let containedFamilyCount: Int + let doubleContributionFamilyCount: Int + let permanentContainmentSupported: Bool + } + + struct PerformanceEvidence: Equatable, Sendable { + let coldMeasured: Bool + let warmMeasured: Bool + let memoryBoundMeasured: Bool + /// Set only after review finds a correctness-neutral regression material enough to block promotion. + let hasMaterialRegression: Bool + } + + struct RollbackEvidence: Equatable, Sendable { + let legacyWholeScanAvailable: Bool + let rollbackPathVerified: Bool + } + + struct Input: Equatable, Sendable { + let residualReport: CodexLineageResidualClassifier.Report + /// Finalized fork-heavy UTC days whose ledger error must be lower than legacy error. + let targetDays: Set + /// Large residuals require an explicit human-reviewed classification before promotion. + let reviewedResidualDays: Set + let adversarialGoldensPassed: Bool + let boundedDiscoveryPassed: Bool + let familyRouting: FamilyRoutingEvidence + let performance: PerformanceEvidence + let cancellationStages: Set + let atomicPublicationPassed: Bool + let rollback: RollbackEvidence + } + + enum Blocker: String, CaseIterable, Equatable, Hashable, Sendable { + case invalidResidualEvidence + case missingTargetDay + case provisionalTargetDay + case targetDayNotImproved + case aggregateErrorNotImproved + case ordinaryDayRegression + case unreviewedLargeResidual + case ledgerDefect + case adversarialGoldenFailure + case boundedDiscoveryFailure + case containmentUnavailable + case familyDoubleContribution + case performanceEvidenceMissing + case materialPerformanceRegression + case cancellationEvidenceMissing + case atomicPublicationFailure + case legacyRollbackUnavailable + case rollbackUnverified + } + + struct Decision: Equatable, Sendable { + /// Canonical case order keeps logs, snapshots, and review artifacts deterministic. + let blockers: [Blocker] + + var canPromote: Bool { + self.blockers.isEmpty + } + + /// Promotion never removes the permanent family-level containment route. + let keepsFamilyContainment: Bool + /// Legacy remains a whole-scan emergency authority until its dedicated removal work. + let keepsLegacyEmergencyRollback: Bool + } + + static func evaluate(_ input: Input) -> Decision { + var blockers: Set = [] + let report = input.residualReport + Self.addReportBlockers(report, to: &blockers) + var daysByID: [String: CodexLineageResidualClassifier.DayResult] = [:] + for day in report.days { + if daysByID.updateValue(day, forKey: day.day) != nil { + blockers.insert(.invalidResidualEvidence) + } + if day.classification == .ledgerDefect { + blockers.insert(.ledgerDefect) + } + if Self.requiresReview(day.classification), !input.reviewedResidualDays.contains(day.day) { + blockers.insert(.unreviewedLargeResidual) + } + } + Self.addTargetBlockers(input.targetDays, daysByID: daysByID, to: &blockers) + Self.addOperationalBlockers(input, to: &blockers) + + return Decision( + blockers: Blocker.allCases.filter(blockers.contains), + keepsFamilyContainment: input.familyRouting.permanentContainmentSupported, + keepsLegacyEmergencyRollback: input.rollback.legacyWholeScanAvailable) + } + + private static func addReportBlockers( + _ report: CodexLineageResidualClassifier.Report, + to blockers: inout Set) + { + if !self.hasValidShape(report) || report.invalidSampleCount > 0 { + blockers.insert(.invalidResidualEvidence) + } + if !report.improvesAggregateError { + blockers.insert(.aggregateErrorNotImproved) + } + if report.ordinaryDayRegressionCount > 0 { + blockers.insert(.ordinaryDayRegression) + } + } + + private static func addTargetBlockers( + _ targetDays: Set, + daysByID: [String: CodexLineageResidualClassifier.DayResult], + to blockers: inout Set) + { + if targetDays.isEmpty { + blockers.insert(.missingTargetDay) + } + for targetDay in targetDays { + guard let day = daysByID[targetDay] else { + blockers.insert(.missingTargetDay) + continue + } + if day.classification == .provisional { + blockers.insert(.provisionalTargetDay) + } else if day.classification == .invalidInput { + blockers.insert(.invalidResidualEvidence) + } else if let legacyError = day.legacyAbsoluteError, let ledgerError = day.ledgerAbsoluteError, + ledgerError >= legacyError + { + blockers.insert(.targetDayNotImproved) + } + } + } + + private static func addOperationalBlockers(_ input: Input, to blockers: inout Set) { + if !input.adversarialGoldensPassed { + blockers.insert(.adversarialGoldenFailure) + } + if !input.boundedDiscoveryPassed { + blockers.insert(.boundedDiscoveryFailure) + } + if !input.familyRouting.permanentContainmentSupported { + blockers.insert(.containmentUnavailable) + } + if input.familyRouting.doubleContributionFamilyCount != 0 { + blockers.insert(.familyDoubleContribution) + } + let familyCountIsInvalid = input.familyRouting.primaryFamilyCount < 0 + || input.familyRouting.containedFamilyCount < 0 + || input.familyRouting.doubleContributionFamilyCount < 0 + || (input.familyRouting.primaryFamilyCount == 0 && input.familyRouting.containedFamilyCount == 0) + if familyCountIsInvalid { + blockers.insert(.invalidResidualEvidence) + } + if !input.performance.coldMeasured || !input.performance.warmMeasured || !input.performance + .memoryBoundMeasured + { + blockers.insert(.performanceEvidenceMissing) + } + if input.performance.hasMaterialRegression { + blockers.insert(.materialPerformanceRegression) + } + if input.cancellationStages != Set(CancellationStage.allCases) { + blockers.insert(.cancellationEvidenceMissing) + } + if !input.atomicPublicationPassed { + blockers.insert(.atomicPublicationFailure) + } + if !input.rollback.legacyWholeScanAvailable { + blockers.insert(.legacyRollbackUnavailable) + } + if !input.rollback.rollbackPathVerified { + blockers.insert(.rollbackUnverified) + } + } + + private static func hasValidShape(_ report: CodexLineageResidualClassifier.Report) -> Bool { + guard report.invalidSampleCount >= 0, + report.ordinaryDayRegressionCount >= 0, + report.finalizedReferenceTokens >= 0, + report.finalizedLegacyTokens >= 0, + report.finalizedLedgerTokens >= 0, + report.legacyAbsoluteError >= 0, + report.ledgerAbsoluteError >= 0, + report.invalidSampleCount == report.days.count(where: { $0.classification == .invalidInput }), + report.legacyAbsoluteError == abs(report.finalizedLegacyTokens - report.finalizedReferenceTokens), + report.ledgerAbsoluteError == abs(report.finalizedLedgerTokens - report.finalizedReferenceTokens) + else { return false } + return report.days.allSatisfy { day in + switch day.classification { + case .invalidInput, .provisional: + day.legacyAbsoluteError == nil && day.ledgerAbsoluteError == nil + case .withinTolerance, .unavailableHistory, .unsupportedEventShape, .utcLocalAttribution, + .accountingSemantics, .containment, .ledgerDefect: + day.legacyAbsoluteError != nil && day.ledgerAbsoluteError != nil + } + } + } + + private static func requiresReview(_ classification: CodexLineageResidualClassifier.Classification) -> Bool { + switch classification { + case .withinTolerance: + false + case .invalidInput, .provisional: + false + case .unavailableHistory, .unsupportedEventShape, .utcLocalAttribution, .accountingSemantics, + .containment, .ledgerDefect: + true + } + } +} diff --git a/Tests/CodexBarTests/CodexLineagePromotionEvaluatorTests.swift b/Tests/CodexBarTests/CodexLineagePromotionEvaluatorTests.swift new file mode 100644 index 0000000000..ba29ae74af --- /dev/null +++ b/Tests/CodexBarTests/CodexLineagePromotionEvaluatorTests.swift @@ -0,0 +1,204 @@ +import Testing +@testable import CodexBarCore + +struct CodexLineagePromotionEvaluatorTests { + @Test + func `complete evidence promotes while retaining containment and legacy rollback`() { + let decision = CodexLineagePromotionEvaluator.evaluate(Self.readyInput()) + + #expect(decision.canPromote) + #expect(decision.blockers.isEmpty) + #expect(decision.keepsFamilyContainment) + #expect(decision.keepsLegacyEmergencyRollback) + } + + @Test + func `promotion is evidence based rather than a fixed aggregate percentage`() { + let report = CodexLineageResidualClassifier.classify(samples: [ + Self.sample( + day: "2026-07-09", + reference: 1_000_000, + legacy: 100_000, + ledger: 100_001, + ordinary: false, + exhaustive: true), + ]) + var input = Self.readyInput(report: report, targetDays: ["2026-07-09"]) + input = .init( + residualReport: input.residualReport, + targetDays: input.targetDays, + reviewedResidualDays: ["2026-07-09"], + adversarialGoldensPassed: input.adversarialGoldensPassed, + boundedDiscoveryPassed: input.boundedDiscoveryPassed, + familyRouting: input.familyRouting, + performance: input.performance, + cancellationStages: input.cancellationStages, + atomicPublicationPassed: input.atomicPublicationPassed, + rollback: input.rollback) + + let decision = CodexLineagePromotionEvaluator.evaluate(input) + #expect(decision.canPromote) + } + + @Test + func `ordinary regression and unreviewed residual block promotion`() { + let report = CodexLineageResidualClassifier.classify(samples: [ + Self.sample(day: "2026-07-09", reference: 1000, legacy: 500, ledger: 900, ordinary: false), + Self.sample(day: "2026-07-10", reference: 1000, legacy: 1000, ledger: 1100, ordinary: true), + ]) + let ready = Self.readyInput(report: report, targetDays: ["2026-07-09"]) + let input = CodexLineagePromotionEvaluator.Input( + residualReport: ready.residualReport, + targetDays: ready.targetDays, + reviewedResidualDays: [], + adversarialGoldensPassed: ready.adversarialGoldensPassed, + boundedDiscoveryPassed: ready.boundedDiscoveryPassed, + familyRouting: ready.familyRouting, + performance: ready.performance, + cancellationStages: ready.cancellationStages, + atomicPublicationPassed: ready.atomicPublicationPassed, + rollback: ready.rollback) + + let decision = CodexLineagePromotionEvaluator.evaluate(input) + #expect(decision.blockers.contains(.ordinaryDayRegression)) + #expect(decision.blockers.contains(.unreviewedLargeResidual)) + #expect(!decision.canPromote) + } + + @Test + func `non target large residual still requires review`() { + let report = CodexLineageResidualClassifier.classify(samples: [ + Self.sample(day: "2026-07-09", reference: 1000, legacy: 500, ledger: 950, ordinary: false), + Self.sample(day: "2026-07-10", reference: 1000, legacy: 100, ledger: 900, ordinary: false), + ]) + let ready = Self.readyInput(report: report, targetDays: ["2026-07-09"]) + let input = CodexLineagePromotionEvaluator.Input( + residualReport: ready.residualReport, + targetDays: ready.targetDays, + reviewedResidualDays: ["2026-07-09"], + adversarialGoldensPassed: ready.adversarialGoldensPassed, + boundedDiscoveryPassed: ready.boundedDiscoveryPassed, + familyRouting: ready.familyRouting, + performance: ready.performance, + cancellationStages: ready.cancellationStages, + atomicPublicationPassed: ready.atomicPublicationPassed, + rollback: ready.rollback) + + #expect(CodexLineagePromotionEvaluator.evaluate(input).blockers == [.unreviewedLargeResidual]) + } + + @Test + func `duplicate residual days fail closed without trapping`() { + let sample = Self.sample(day: "2026-07-09", reference: 1000, legacy: 500, ledger: 950, ordinary: false) + let report = CodexLineageResidualClassifier.classify(samples: [sample, sample]) + let decision = CodexLineagePromotionEvaluator.evaluate(Self.readyInput( + report: report, + targetDays: ["2026-07-09"])) + + #expect(decision.blockers.contains(.invalidResidualEvidence)) + #expect(!decision.canPromote) + } + + @Test + func `family overlap blocks promotion even when residual totals improve`() { + let ready = Self.readyInput() + let input = CodexLineagePromotionEvaluator.Input( + residualReport: ready.residualReport, + targetDays: ready.targetDays, + reviewedResidualDays: ready.reviewedResidualDays, + adversarialGoldensPassed: true, + boundedDiscoveryPassed: true, + familyRouting: .init( + primaryFamilyCount: 10, + containedFamilyCount: 2, + doubleContributionFamilyCount: 1, + permanentContainmentSupported: true), + performance: ready.performance, + cancellationStages: ready.cancellationStages, + atomicPublicationPassed: true, + rollback: ready.rollback) + + let decision = CodexLineagePromotionEvaluator.evaluate(input) + #expect(decision.blockers == [.familyDoubleContribution]) + } + + @Test + func `operational and rollback evidence are mandatory`() { + let ready = Self.readyInput() + let input = CodexLineagePromotionEvaluator.Input( + residualReport: ready.residualReport, + targetDays: ready.targetDays, + reviewedResidualDays: ready.reviewedResidualDays, + adversarialGoldensPassed: true, + boundedDiscoveryPassed: true, + familyRouting: ready.familyRouting, + performance: .init( + coldMeasured: true, + warmMeasured: false, + memoryBoundMeasured: false, + hasMaterialRegression: true), + cancellationStages: [.parsing, .reconciliation], + atomicPublicationPassed: false, + rollback: .init(legacyWholeScanAvailable: false, rollbackPathVerified: false)) + + let decision = CodexLineagePromotionEvaluator.evaluate(input) + #expect(decision.blockers.contains(.performanceEvidenceMissing)) + #expect(decision.blockers.contains(.materialPerformanceRegression)) + #expect(decision.blockers.contains(.cancellationEvidenceMissing)) + #expect(decision.blockers.contains(.atomicPublicationFailure)) + #expect(decision.blockers.contains(.legacyRollbackUnavailable)) + #expect(decision.blockers.contains(.rollbackUnverified)) + #expect(decision.blockers == CodexLineagePromotionEvaluator.Blocker.allCases.filter { + decision.blockers.contains($0) + }) + } + + private static func readyInput( + report: CodexLineageResidualClassifier.Report? = nil, + targetDays: Set = ["2026-07-09", "2026-07-10"]) -> CodexLineagePromotionEvaluator.Input + { + let report = report ?? CodexLineageResidualClassifier.classify(samples: [ + Self.sample(day: "2026-07-09", reference: 1000, legacy: 500, ledger: 950, ordinary: false), + Self.sample(day: "2026-07-10", reference: 2000, legacy: 1000, ledger: 1980, ordinary: false), + Self.sample(day: "2026-07-08", reference: 500, legacy: 500, ledger: 500, ordinary: true), + ]) + return .init( + residualReport: report, + targetDays: targetDays, + reviewedResidualDays: targetDays, + adversarialGoldensPassed: true, + boundedDiscoveryPassed: true, + familyRouting: .init( + primaryFamilyCount: 10, + containedFamilyCount: 2, + doubleContributionFamilyCount: 0, + permanentContainmentSupported: true), + performance: .init( + coldMeasured: true, + warmMeasured: true, + memoryBoundMeasured: true, + hasMaterialRegression: false), + cancellationStages: Set(CodexLineagePromotionEvaluator.CancellationStage.allCases), + atomicPublicationPassed: true, + rollback: .init(legacyWholeScanAvailable: true, rollbackPathVerified: true)) + } + + private static func sample( + day: String, + reference: Int, + legacy: Int, + ledger: Int, + ordinary: Bool, + exhaustive: Bool = false) -> CodexLineageResidualClassifier.Sample + { + .init( + day: day, + referenceTokens: reference, + isReferenceFinalized: true, + isOrdinaryDay: ordinary, + legacyTokens: legacy, + ledgerUTCTokens: ledger, + ledgerLocalTokens: ledger, + evidence: .init(localCorpusWasExhaustive: exhaustive, duplicateObservationCount: ordinary ? 0 : 1)) + } +}