diff --git a/Sources/CodexBarCore/Providers/Codex/CodexLineageResetEpochDiagnostics.swift b/Sources/CodexBarCore/Providers/Codex/CodexLineageResetEpochDiagnostics.swift new file mode 100644 index 0000000000..b800ad08c9 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CodexLineageResetEpochDiagnostics.swift @@ -0,0 +1,227 @@ +import Foundation + +/// Counterfactual shadow evidence for snapshots that recur after a strong cumulative-token reset. +/// This analyzer cannot produce accounting rows or selector input and never changes ledger totals. +enum CodexLineageResetEpochDiagnostics { + struct Report: Equatable, Sendable { + let strongResetBoundaryCount: Int + let mixedRegressionCount: Int + let postResetRepeatedFingerprintCount: Int + let sameOwnerRepeatCount: Int + let crossOwnerRepeatCount: Int + let estimatedSuppressed: CodexLineageLedger.Totals + let estimatedSuppressedUTC: [String: CodexLineageLedger.Totals] + let sameOwnerEstimatedSuppressed: CodexLineageLedger.Totals + let sameOwnerEstimatedSuppressedUTC: [String: CodexLineageLedger.Totals] + + static let empty = Self( + strongResetBoundaryCount: 0, + mixedRegressionCount: 0, + postResetRepeatedFingerprintCount: 0, + sameOwnerRepeatCount: 0, + crossOwnerRepeatCount: 0, + estimatedSuppressed: .zero, + estimatedSuppressedUTC: [:], + sameOwnerEstimatedSuppressed: .zero, + sameOwnerEstimatedSuppressedUTC: [:]) + } + + private struct Fingerprint: Hashable { + let last: CodexLineageLedger.Totals + let total: CodexLineageLedger.Totals + } + + private struct CandidateKey: Hashable { + let owner: String + let epoch: Int + let fingerprint: Fingerprint + } + + private struct Occurrence { + let owner: String + let stream: Int + let epoch: Int + let fingerprint: Fingerprint + let date: Date + let originalIndex: Int + } + + static func analyze( + families: [CodexLineageEngine.PreparedFamily], + checkCancellation: CostUsageScanner.CancellationCheck? = nil) throws -> Report + { + var report = Report.empty + for family in families { + try checkCancellation?() + var occurrences: [Occurrence] = [] + for (stream, document) in family.documents.enumerated() { + try checkCancellation?() + let owner = Self.ownerKey(document) + var dated: [(Int, Date, CodexLineageLedger.Observation)] = [] + dated.reserveCapacity(document.observations.count) + for (index, observation) in document.observations.enumerated() { + try checkCancellation?() + guard let date = CostUsageScanner.dateFromTimestamp(observation.timestamp) else { continue } + dated.append((index, date, observation)) + } + dated.sort { + if $0.1 != $1.1 { + return $0.1 < $1.1 + } + return $0.0 < $1.0 + } + var epoch = 0 + var previous: CodexLineageLedger.Totals? + var seenInEpoch: Set = [] + for (index, date, observation) in dated { + try checkCancellation?() + if let previous { + if Self.isStrongReset(from: previous, to: observation.total) { + epoch += 1 + seenInEpoch.removeAll(keepingCapacity: true) + report = report.addingStrongReset() + } else if Self.isMixedRegression(from: previous, to: observation.total) { + report = report.addingMixedRegression() + } + } + previous = observation.total + let fingerprint = Fingerprint(last: observation.last, total: observation.total) + guard seenInEpoch.insert(fingerprint).inserted else { continue } + occurrences.append(.init( + owner: owner, + stream: stream, + epoch: epoch, + fingerprint: fingerprint, + date: date, + originalIndex: index)) + } + } + occurrences.sort { + if $0.date != $1.date { + return $0.date < $1.date + } + if $0.owner != $1.owner { + return $0.owner < $1.owner + } + if $0.epoch != $1.epoch { + return $0.epoch < $1.epoch + } + return $0.originalIndex < $1.originalIndex + } + var familySeen: [Fingerprint: [String: Date]] = [:] + var streamEpochs: [Int: [Fingerprint: Set]] = [:] + var counted: Set = [] + for occurrence in occurrences { + try checkCancellation?() + let priorOwnerEpochs = streamEpochs[occurrence.stream]?[occurrence.fingerprint] ?? [] + let sameOwner = priorOwnerEpochs.contains { $0 < occurrence.epoch } + let crossOwner = familySeen[occurrence.fingerprint]?.contains { owner, date in + owner != occurrence.owner && date < occurrence.date + } ?? false + let key = CandidateKey( + owner: occurrence.owner, + epoch: occurrence.epoch, + fingerprint: occurrence.fingerprint) + if occurrence.epoch > 0, sameOwner || crossOwner, counted.insert(key).inserted { + report = report.addingCandidate( + occurrence.fingerprint.last, + day: Self.utcDayKey(from: occurrence.date), + sameOwner: sameOwner) + } + familySeen[occurrence.fingerprint, default: [:]][occurrence.owner] = min( + familySeen[occurrence.fingerprint]?[occurrence.owner] ?? occurrence.date, + occurrence.date) + streamEpochs[occurrence.stream, default: [:]][occurrence.fingerprint, default: []] + .insert(occurrence.epoch) + } + } + return report + } + + private static func ownerKey(_ document: CodexLineageLedger.Document) -> String { + let owner = UUID(uuidString: document.ownerID)?.uuidString.lowercased() ?? document.ownerID + return document.scopeID + "\u{0}" + owner + } + + private static func utcDayKey(from date: Date) -> String { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt + let components = calendar.dateComponents([.year, .month, .day], from: date) + return String(format: "%04d-%02d-%02d", components.year ?? 0, components.month ?? 0, components.day ?? 0) + } + + private static func isStrongReset( + from previous: CodexLineageLedger.Totals, + to current: CodexLineageLedger.Totals) -> Bool + { + let nonIncreasing = current.input <= previous.input && current.cached <= previous.cached + && current.output <= previous.output + return nonIncreasing && current != previous + } + + private static func isMixedRegression( + from previous: CodexLineageLedger.Totals, + to current: CodexLineageLedger.Totals) -> Bool + { + let regressed = current.input < previous.input || current.cached < previous.cached + || current.output < previous.output + let increased = current.input > previous.input || current.cached > previous.cached + || current.output > previous.output + return regressed && increased + } +} + +extension CodexLineageResetEpochDiagnostics.Report { + fileprivate func addingStrongReset() -> Self { + .init( + strongResetBoundaryCount: self.strongResetBoundaryCount + 1, + mixedRegressionCount: self.mixedRegressionCount, + postResetRepeatedFingerprintCount: self.postResetRepeatedFingerprintCount, + sameOwnerRepeatCount: self.sameOwnerRepeatCount, + crossOwnerRepeatCount: self.crossOwnerRepeatCount, + estimatedSuppressed: self.estimatedSuppressed, + estimatedSuppressedUTC: self.estimatedSuppressedUTC, + sameOwnerEstimatedSuppressed: self.sameOwnerEstimatedSuppressed, + sameOwnerEstimatedSuppressedUTC: self.sameOwnerEstimatedSuppressedUTC) + } + + fileprivate func addingMixedRegression() -> Self { + .init( + strongResetBoundaryCount: self.strongResetBoundaryCount, + mixedRegressionCount: self.mixedRegressionCount + 1, + postResetRepeatedFingerprintCount: self.postResetRepeatedFingerprintCount, + sameOwnerRepeatCount: self.sameOwnerRepeatCount, + crossOwnerRepeatCount: self.crossOwnerRepeatCount, + estimatedSuppressed: self.estimatedSuppressed, + estimatedSuppressedUTC: self.estimatedSuppressedUTC, + sameOwnerEstimatedSuppressed: self.sameOwnerEstimatedSuppressed, + sameOwnerEstimatedSuppressedUTC: self.sameOwnerEstimatedSuppressedUTC) + } + + fileprivate func addingCandidate(_ totals: CodexLineageLedger.Totals, day: String, sameOwner: Bool) -> Self { + var estimated = self.estimatedSuppressed + estimated.add(totals) + var days = self.estimatedSuppressedUTC + var dayTotals = days[day] ?? .zero + dayTotals.add(totals) + days[day] = dayTotals + var sameOwnerEstimated = self.sameOwnerEstimatedSuppressed + var sameOwnerDays = self.sameOwnerEstimatedSuppressedUTC + if sameOwner { + sameOwnerEstimated.add(totals) + var sameOwnerDay = sameOwnerDays[day] ?? .zero + sameOwnerDay.add(totals) + sameOwnerDays[day] = sameOwnerDay + } + return .init( + strongResetBoundaryCount: self.strongResetBoundaryCount, + mixedRegressionCount: self.mixedRegressionCount, + postResetRepeatedFingerprintCount: self.postResetRepeatedFingerprintCount + 1, + sameOwnerRepeatCount: self.sameOwnerRepeatCount + (sameOwner ? 1 : 0), + crossOwnerRepeatCount: self.crossOwnerRepeatCount + (sameOwner ? 0 : 1), + estimatedSuppressed: estimated, + estimatedSuppressedUTC: days, + sameOwnerEstimatedSuppressed: sameOwnerEstimated, + sameOwnerEstimatedSuppressedUTC: sameOwnerDays) + } +} diff --git a/Sources/CodexBarCore/Providers/Codex/CodexLineageShadow.swift b/Sources/CodexBarCore/Providers/Codex/CodexLineageShadow.swift index aae024629e..b3e743e80f 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexLineageShadow.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexLineageShadow.swift @@ -27,6 +27,7 @@ enum CodexLineageShadow { let incompleteProvenanceFamilyCount: Int let containedFamilyCount: Int let containmentReasonCounts: [CodexLineageLedger.ContainmentReason: Int] + let resetEpochDiagnostics: CodexLineageResetEpochDiagnostics.Report } static func run( @@ -66,6 +67,13 @@ enum CodexLineageShadow { unresolvedParents: discovery.unresolvedParents, localTimeZone: localTimeZone, checkCancellation: checkCancellation) + let preparedFamilies = try CodexLineageEngine.prepareFamilies( + documents: documents, + unresolvedParents: discovery.unresolvedParents, + checkCancellation: checkCancellation) + let resetEpochDiagnostics = try CodexLineageResetEpochDiagnostics.analyze( + families: preparedFamilies, + checkCancellation: checkCancellation) let ledger = conservative.primary let legacy = Self.legacyTotalsByDay(legacyDays) let dayKeys = Set(legacy.keys).union(ledger.localDays.keys) @@ -92,7 +100,8 @@ enum CodexLineageShadow { } return false }, - containmentReasonCounts: Self.containmentReasonCounts(conservative.families)) + containmentReasonCounts: Self.containmentReasonCounts(conservative.families), + resetEpochDiagnostics: resetEpochDiagnostics) } private static func containmentReasonCounts( diff --git a/Tests/CodexBarTests/CodexLineageLocalValidationTests.swift b/Tests/CodexBarTests/CodexLineageLocalValidationTests.swift index df373f2cfb..6e8980fdd3 100644 --- a/Tests/CodexBarTests/CodexLineageLocalValidationTests.swift +++ b/Tests/CodexBarTests/CodexLineageLocalValidationTests.swift @@ -20,6 +20,38 @@ struct CodexLineageLocalValidationTests { let finalized: Bool } + @Test + func `local reset epoch diagnostic`() throws { + guard ProcessInfo.processInfo.environment["CODEXBAR_VALIDATE_RESET_EPOCHS_ONLY"] == "1" else { return } + CodexBarLog.setLogLevel(.critical) + let root = try #require(ProcessInfo.processInfo.environment["CODEXBAR_LINEAGE_VALIDATION_ROOT"]) + let snapshotHome = URL(fileURLWithPath: root, isDirectory: true) + .appendingPathComponent("codex-home", isDirectory: true) + let roots = [ + snapshotHome.appendingPathComponent("sessions", isDirectory: true), + snapshotHome.appendingPathComponent("archived_sessions", isDirectory: true), + ] + let included = Self.rollouts(roots: roots, days: Self.discoveryDays) + let report = try Self.resetEpochDiagnostics(includedFiles: included, roots: roots) + let output: [String: Any] = [ + "strongResetBoundaries": report.strongResetBoundaryCount, + "mixedRegressions": report.mixedRegressionCount, + "postResetRepeatedFingerprints": report.postResetRepeatedFingerprintCount, + "sameOwnerRepeats": report.sameOwnerRepeatCount, + "crossOwnerRepeats": report.crossOwnerRepeatCount, + "estimatedSuppressedTokens": report.estimatedSuppressed.input + report.estimatedSuppressed.output, + "estimatedSuppressedUTC": report.estimatedSuppressedUTC.mapValues { $0.input + $0.output }, + "sameOwnerEstimatedSuppressedTokens": report.sameOwnerEstimatedSuppressed.input + + report.sameOwnerEstimatedSuppressed.output, + "sameOwnerEstimatedSuppressedUTC": report.sameOwnerEstimatedSuppressedUTC.mapValues { + $0.input + $0.output + }, + ] + let data = try JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) + let encoded = try #require(String(bytes: data, encoding: .utf8)) + print("CODEX_LINEAGE_RESET_EPOCHS " + encoded) + } + // The opt-in replay is intentionally linear so its immutable snapshot, scan, and comparison // lifecycle stays auditable in one place. // swiftlint:disable function_body_length @@ -76,6 +108,14 @@ struct CodexLineageLocalValidationTests { let references = try Self.referenceTotals() Self.progress("legacy-ready") + let resetEpochDiagnostics: CodexLineageResetEpochDiagnostics.Report? = if ProcessInfo.processInfo + .environment["CODEXBAR_VALIDATE_RESET_EPOCHS"] == "1" + { + try Self.resetEpochDiagnostics(includedFiles: included, roots: [sessions, archived]) + } else { + nil + } + let directModes: [CodexLineageAccountingMode] = switch ProcessInfo.processInfo .environment["CODEXBAR_VALIDATE_SCANNER_MODES"] { @@ -234,6 +274,25 @@ struct CodexLineageLocalValidationTests { "lineageMilliseconds": Self.milliseconds(lineageDuration), ], "ordinaryDayDivergenceCount": ordinaryDivergenceCount, + "resetEpochDiagnostics": [ + "strongResetBoundaries": resetEpochDiagnostics?.strongResetBoundaryCount ?? 0, + "mixedRegressions": resetEpochDiagnostics?.mixedRegressionCount ?? 0, + "postResetRepeatedFingerprints": resetEpochDiagnostics?.postResetRepeatedFingerprintCount ?? 0, + "sameOwnerRepeats": resetEpochDiagnostics?.sameOwnerRepeatCount ?? 0, + "crossOwnerRepeats": resetEpochDiagnostics?.crossOwnerRepeatCount ?? 0, + "estimatedSuppressedTokens": resetEpochDiagnostics.map { + $0.estimatedSuppressed.input + $0.estimatedSuppressed.output + } ?? 0, + "estimatedSuppressedUTC": resetEpochDiagnostics?.estimatedSuppressedUTC.mapValues { + $0.input + $0.output + } ?? [:], + "sameOwnerEstimatedSuppressedTokens": resetEpochDiagnostics.map { + $0.sameOwnerEstimatedSuppressed.input + $0.sameOwnerEstimatedSuppressed.output + } ?? 0, + "sameOwnerEstimatedSuppressedUTC": resetEpochDiagnostics?.sameOwnerEstimatedSuppressedUTC.mapValues { + $0.input + $0.output + } ?? [:], + ], "aggregateImproved": classification.improvesAggregateError, // This replay is one validation artifact, not permission to remove the rollback path. "supportsLegacyRemoval": false, @@ -265,6 +324,29 @@ struct CodexLineageLocalValidationTests { return references } + private static func resetEpochDiagnostics( + includedFiles: [URL], + roots: [URL]) throws -> CodexLineageResetEpochDiagnostics.Report + { + self.progress("reset-epoch-start") + let discovery = try CodexLineageDiscovery.discover(includedFiles: includedFiles, roots: roots) + let documents = discovery.documents.map { document in + CodexLineageLedger.Document( + ownerID: document.ownerID, + metadataSessionID: document.metadataSessionID, + parentSessionID: document.parentSessionID, + observations: document.observations, + scopeID: document.scopeID, + incompleteObservationCount: document.incompleteObservationCount) + } + let families = try CodexLineageEngine.prepareFamilies( + documents: documents, + unresolvedParents: discovery.unresolvedParents) + let report = try CodexLineageResetEpochDiagnostics.analyze(families: families) + Self.progress("reset-epoch-ready") + return report + } + private static func rollouts(roots: [URL], days: Set) -> [URL] { roots.flatMap { root -> [URL] in guard let enumerator = FileManager.default.enumerator( diff --git a/Tests/CodexBarTests/CodexLineageResetEpochDiagnosticsTests.swift b/Tests/CodexBarTests/CodexLineageResetEpochDiagnosticsTests.swift new file mode 100644 index 0000000000..5b0579e9aa --- /dev/null +++ b/Tests/CodexBarTests/CodexLineageResetEpochDiagnosticsTests.swift @@ -0,0 +1,133 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct CodexLineageResetEpochDiagnosticsTests { + @Test + func `monotonic reemission is not reset evidence`() throws { + let repeated = Self.observation("2026-07-09T12:00:00Z", last: 10, total: 100) + let report = try Self.analyze([repeated, repeated]) + + #expect(report == .empty) + } + + @Test + func `same owner fingerprint repeated after strong reset is estimated once`() throws { + let report = try Self.analyze([ + Self.observation("2026-07-09T12:00:00Z", last: 10, total: 100), + Self.observation("2026-07-09T12:01:00Z", last: 1, total: 1), + Self.observation("2026-07-10T00:01:00Z", last: 10, total: 100), + Self.observation("2026-07-10T00:02:00Z", last: 10, total: 100), + ]) + + #expect(report.strongResetBoundaryCount == 1) + #expect(report.postResetRepeatedFingerprintCount == 1) + #expect(report.sameOwnerRepeatCount == 1) + #expect(report.crossOwnerRepeatCount == 0) + #expect(report.estimatedSuppressed == .init(input: 10, cached: 0, output: 0)) + #expect(report.estimatedSuppressedUTC == [ + "2026-07-10": .init(input: 10, cached: 0, output: 0), + ]) + #expect(report.sameOwnerEstimatedSuppressed == .init(input: 10, cached: 0, output: 0)) + #expect(report.sameOwnerEstimatedSuppressedUTC == [ + "2026-07-10": .init(input: 10, cached: 0, output: 0), + ]) + } + + @Test + func `mixed regression does not open a reset epoch`() throws { + let first = CodexLineageLedger.Observation( + timestamp: "2026-07-09T12:00:00Z", + last: .init(input: 10, cached: 0, output: 1), + total: .init(input: 100, cached: 0, output: 10)) + let mixed = CodexLineageLedger.Observation( + timestamp: "2026-07-09T12:01:00Z", + last: .init(input: 10, cached: 0, output: 1), + total: .init(input: 90, cached: 0, output: 11)) + let report = try Self.analyze([first, mixed]) + + #expect(report.strongResetBoundaryCount == 0) + #expect(report.mixedRegressionCount == 1) + #expect(report.postResetRepeatedFingerprintCount == 0) + } + + @Test + func `copied child without local reset is not reset evidence`() throws { + let observation = Self.observation("2026-07-09T12:00:00Z", last: 10, total: 100) + let parent = Self.document(owner: "parent", observations: [observation]) + let child = CodexLineageLedger.Document( + ownerID: "child", + metadataSessionID: "child", + parentSessionID: "parent", + observations: [observation]) + let family = try #require(try CodexLineageEngine.prepareFamilies(documents: [parent, child]).first) + + let report = try CodexLineageResetEpochDiagnostics.analyze(families: [family]) + + #expect(report == .empty) + } + + @Test + func `duplicate documents with the same owner do not manufacture reset history`() throws { + let repeated = Self.observation("2026-07-09T12:00:00Z", last: 10, total: 100) + let reset = Self.observation("2026-07-09T12:01:00Z", last: 1, total: 1) + let replay = Self.observation("2026-07-09T12:02:00Z", last: 10, total: 100) + let firstCopy = Self.document(owner: "owner", observations: [repeated]) + let beforeReset = Self.observation("2026-07-09T11:59:00Z", last: 20, total: 200) + let secondCopy = Self.document(owner: "owner", observations: [beforeReset, reset, replay]) + let family = try #require(try CodexLineageEngine.prepareFamilies( + documents: [firstCopy, secondCopy]).first) + + let report = try CodexLineageResetEpochDiagnostics.analyze(families: [family]) + + #expect(report.strongResetBoundaryCount == 1) + #expect(report.postResetRepeatedFingerprintCount == 0) + } + + @Test + func `cross owner evidence requires a distinct earlier owner`() throws { + let fingerprint = Self.observation("2026-07-09T12:00:00Z", last: 10, total: 100) + let parent = Self.document(owner: "parent", observations: [fingerprint]) + let child = CodexLineageLedger.Document( + ownerID: "child", + metadataSessionID: "child", + parentSessionID: "parent", + observations: [ + Self.observation("2026-07-09T12:01:00Z", last: 20, total: 200), + Self.observation("2026-07-09T12:02:00Z", last: 1, total: 1), + Self.observation("2026-07-09T12:03:00Z", last: 10, total: 100), + ]) + let family = try #require(try CodexLineageEngine.prepareFamilies(documents: [parent, child]).first) + + let report = try CodexLineageResetEpochDiagnostics.analyze(families: [family]) + + #expect(report.sameOwnerRepeatCount == 0) + #expect(report.crossOwnerRepeatCount == 1) + } + + private static func analyze( + _ observations: [CodexLineageLedger.Observation]) throws -> CodexLineageResetEpochDiagnostics.Report + { + let family = try #require(try CodexLineageEngine.prepareFamilies( + documents: [Self.document(owner: "owner", observations: observations)]).first) + return try CodexLineageResetEpochDiagnostics.analyze(families: [family]) + } + + private static func document( + owner: String, + observations: [CodexLineageLedger.Observation]) -> CodexLineageLedger.Document + { + .init(ownerID: owner, metadataSessionID: owner, parentSessionID: nil, observations: observations) + } + + private static func observation( + _ timestamp: String, + last: Int, + total: Int) -> CodexLineageLedger.Observation + { + .init( + timestamp: timestamp, + last: .init(input: last, cached: 0, output: 0), + total: .init(input: total, cached: 0, output: 0)) + } +}