diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 085fda188b..04976b2d31 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "cdef6eb9658a43e2" + static let value = "d820fd60567bfc57" } diff --git a/Sources/CodexBarCore/Providers/Codex/CodexLineageLedger.swift b/Sources/CodexBarCore/Providers/Codex/CodexLineageLedger.swift new file mode 100644 index 0000000000..5f638bac49 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Codex/CodexLineageLedger.swift @@ -0,0 +1,192 @@ +import Foundation + +/// Experimental accounting model for Codex rollout families. +/// +/// Rollout files are overlapping physical views of a logical lineage. The ledger builds the +/// transitive lineage first, then admits each complete token observation once per lineage. +/// It intentionally does not participate in production cost totals yet. +enum CodexLineageLedger { + struct Totals: Equatable, Hashable, Sendable { + var input: Int + var cached: Int + var output: Int + + static let zero = Self(input: 0, cached: 0, output: 0) + + mutating func add(_ other: Self) { + self.input += other.input + self.cached += other.cached + self.output += other.output + } + } + + struct Observation: Equatable, Sendable { + let eventID: String? + let timestamp: String + let last: Totals + let total: Totals + + init(eventID: String? = nil, timestamp: String, last: Totals, total: Totals) { + self.eventID = eventID + self.timestamp = timestamp + self.last = last + self.total = total + } + } + + struct Document: Equatable, Sendable { + /// Canonical owner from the rollout filename when available. + let ownerID: String + /// Session identity persisted in metadata. Fork copies may retain an ancestor identity. + let metadataSessionID: String? + let parentSessionID: String? + let observations: [Observation] + } + + struct Report: Equatable, Sendable { + let utcDays: [String: Totals] + let localDays: [String: Totals] + let componentCount: Int + let acceptedObservationCount: Int + let duplicateObservationCount: Int + } + + enum LedgerError: Error, Equatable { + case emptyOwnerID + case invalidTimestamp(String) + } + + static func reconcile(documents: [Document], localTimeZone: TimeZone) throws -> Report { + var graph = DisjointSet() + for document in documents { + guard !document.ownerID.isEmpty else { throw LedgerError.emptyOwnerID } + graph.insert(document.ownerID) + if let metadataSessionID = Self.nonEmpty(document.metadataSessionID) { + graph.union(document.ownerID, metadataSessionID) + } + if let parentSessionID = Self.nonEmpty(document.parentSessionID) { + graph.union(document.ownerID, parentSessionID) + } + } + + var acceptedByComponent: [String: [ObservationIdentity: AcceptedObservation]] = [:] + var acceptedFingerprintByComponentOwner: [String: [String: [Fingerprint: ObservationIdentity]]] = [:] + var physicalObservationCount = 0 + for document in documents { + let componentID = graph.find(document.ownerID) + var accepted = acceptedByComponent[componentID] ?? [:] + var acceptedFingerprintByOwner = acceptedFingerprintByComponentOwner[componentID] ?? [:] + for observation in document.observations { + physicalObservationCount += 1 + let date = try Self.date(from: observation.timestamp) + let fingerprint = Fingerprint(last: observation.last, total: observation.total) + let proposedIdentity = ObservationIdentity( + eventID: Self.nonEmpty(observation.eventID), + fingerprint: fingerprint) + let identity = accepted[proposedIdentity] == nil + ? acceptedFingerprintByOwner[document.ownerID]?[fingerprint] ?? proposedIdentity + : proposedIdentity + if let existing = accepted[identity], existing.date <= date { + continue + } + accepted[identity] = AcceptedObservation(date: date, last: observation.last) + if identity == proposedIdentity { + acceptedFingerprintByOwner[document.ownerID, default: [:]][fingerprint] = identity + } + } + acceptedByComponent[componentID] = accepted + acceptedFingerprintByComponentOwner[componentID] = acceptedFingerprintByOwner + } + + var utcDays: [String: Totals] = [:] + var localDays: [String: Totals] = [:] + var acceptedObservationCount = 0 + for accepted in acceptedByComponent.values { + acceptedObservationCount += accepted.count + for observation in accepted.values { + Self.add(observation.last, on: observation.date, timeZone: .gmt, to: &utcDays) + Self.add(observation.last, on: observation.date, timeZone: localTimeZone, to: &localDays) + } + } + + return Report( + utcDays: utcDays, + localDays: localDays, + componentCount: Set(documents.map { graph.find($0.ownerID) }).count, + acceptedObservationCount: acceptedObservationCount, + duplicateObservationCount: physicalObservationCount - acceptedObservationCount) + } + + private struct Fingerprint: Equatable, Hashable { + let last: Totals + let total: Totals + } + + private enum ObservationIdentity: Equatable, Hashable { + case event(String, Fingerprint) + case fingerprint(Fingerprint) + + init(eventID: String?, fingerprint: Fingerprint) { + self = eventID.map { .event($0, fingerprint) } ?? .fingerprint(fingerprint) + } + } + + private struct AcceptedObservation { + let date: Date + let last: Totals + } + + private static func nonEmpty(_ value: String?) -> String? { + guard let value, !value.isEmpty else { return nil } + return value + } + + private static func date(from timestamp: String) throws -> Date { + guard let date = CostUsageScanner.dateFromTimestamp(timestamp) else { + throw LedgerError.invalidTimestamp(timestamp) + } + return date + } + + private static func add( + _ totals: Totals, + on date: Date, + timeZone: TimeZone, + to days: inout [String: Totals]) + { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let components = calendar.dateComponents([.year, .month, .day], from: date) + guard let year = components.year, let month = components.month, let day = components.day else { return } + let key = String(format: "%04d-%02d-%02d", year, month, day) + var dayTotals = days[key] ?? .zero + dayTotals.add(totals) + days[key] = dayTotals + } + + private struct DisjointSet { + private var parents: [String: String] = [:] + + mutating func insert(_ item: String) { + if self.parents[item] == nil { + self.parents[item] = item + } + } + + mutating func find(_ item: String) -> String { + self.insert(item) + guard let parent = self.parents[item], parent != item else { return item } + let root = self.find(parent) + self.parents[item] = root + return root + } + + mutating func union(_ first: String, _ second: String) { + let firstRoot = self.find(first) + let secondRoot = self.find(second) + if firstRoot != secondRoot { + self.parents[secondRoot] = firstRoot + } + } + } +} diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index fef88de04f..c38448426e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -98,6 +98,13 @@ enum CostUsageScanner { let totals: CostUsageCodexTotals } + private struct CodexParsedTokenEvidence { + let sessionId: String? + let forkedFromId: String? + let snapshots: [CodexTimestampedTotals] + let observations: [CodexLineageLedger.Observation] + } + enum CodexForkBaseline { case resolved(CostUsageCodexTotals?) case unresolved @@ -1680,14 +1687,17 @@ enum CostUsageScanner { private static func parseCodexTokenSnapshots( fileURL: URL, - checkCancellation: CancellationCheck? = nil) throws -> ( - sessionId: String?, - snapshots: [CodexTimestampedTotals]) + collectLineageObservations: Bool = false, + checkCancellation: CancellationCheck? = nil) throws -> CodexParsedTokenEvidence { var sessionId: String? + var forkedFromId: String? var accumulator = CodexSnapshotAccumulator() var snapshots: [CodexTimestampedTotals] = [] + var observations: [CodexLineageLedger.Observation] = [] var warnedAboutUnparsedTimestamp = false + var currentTurnID: String? + var tokenEventCountByTurn: [String: Int] = [:] func parsedSnapshotDate(timestamp: String) -> Date? { let date = Self.dateFromTimestamp(timestamp) @@ -1701,13 +1711,31 @@ enum CostUsageScanner { return date } - func appendSnapshot(timestamp: String, last: CostUsageCodexTotals?, total: CostUsageCodexTotals?) { + func appendSnapshot( + timestamp: String, + turnID: String?, + last: CostUsageCodexTotals?, + total: CostUsageCodexTotals?) + { guard last != nil || total != nil else { return } let counted = accumulator.apply(last: last, total: total) snapshots.append(CodexTimestampedTotals( timestamp: timestamp, date: parsedSnapshotDate(timestamp: timestamp), totals: counted)) + guard collectLineageObservations else { return } + let eventID = turnID.map { turnID in + let ordinal = tokenEventCountByTurn[turnID, default: 0] + tokenEventCountByTurn[turnID] = ordinal + 1 + return "\(turnID):\(ordinal)" + } + if let last, let total { + observations.append(CodexLineageLedger.Observation( + eventID: eventID, + timestamp: timestamp, + last: Self.lineageTotals(last), + total: Self.lineageTotals(total))) + } } do { @@ -1724,9 +1752,18 @@ enum CostUsageScanner { if sessionId == nil { sessionId = metadata.sessionId } + if forkedFromId == nil { + forkedFromId = metadata.forkedFromId + } case let .tokenCount(record): - appendSnapshot(timestamp: record.timestamp, last: record.last, total: record.total) - case .turnContext, .taskStarted: + appendSnapshot( + timestamp: record.timestamp, + turnID: record.turnID ?? currentTurnID, + last: record.last, + total: record.total) + case let .taskStarted(turnID): + currentTurnID = turnID + case .turnContext: break } return @@ -1746,11 +1783,18 @@ enum CostUsageScanner { ?? obj["sessionId"] as? String ?? obj["id"] as? String } + if forkedFromId == nil { + forkedFromId = Self.codexForkParentId(from: payload) + } return } guard obj["type"] as? String == "event_msg" else { return } guard let payload = obj["payload"] as? [String: Any] else { return } + if payload["type"] as? String == "task_started" { + currentTurnID = Self.codexTurnID(from: payload) + return + } guard payload["type"] as? String == "token_count" else { return } guard let info = payload["info"] as? [String: Any] else { return } guard let timestamp = obj["timestamp"] as? String else { return } @@ -1774,7 +1818,11 @@ enum CostUsageScanner { cached: max(0, toInt($0["cached_input_tokens"] ?? $0["cache_read_input_tokens"])), output: max(0, toInt($0["output_tokens"]))) } - appendSnapshot(timestamp: timestamp, last: last, total: total) + appendSnapshot( + timestamp: timestamp, + turnID: Self.codexTurnID(from: payload) ?? currentTurnID, + last: last, + total: total) } }) } catch is CancellationError { @@ -1785,7 +1833,47 @@ enum CostUsageScanner { metadata: ["path": fileURL.path, "error": error.localizedDescription]) } - return (sessionId, snapshots) + return CodexParsedTokenEvidence( + sessionId: sessionId, + forkedFromId: forkedFromId, + snapshots: snapshots, + observations: observations) + } + + static func parseCodexLineageDocument( + fileURL: URL, + checkCancellation: CancellationCheck? = nil) throws -> CodexLineageLedger.Document + { + let parsed = try Self.parseCodexTokenSnapshots( + fileURL: fileURL, + collectLineageObservations: true, + checkCancellation: checkCancellation) + return CodexLineageLedger.Document( + ownerID: Self.codexRolloutOwnerID(fileURL: fileURL) ?? parsed.sessionId ?? fileURL.standardizedFileURL.path, + metadataSessionID: parsed.sessionId, + parentSessionID: parsed.forkedFromId, + observations: parsed.observations) + } + + static func parseCodexTokenEvidenceCountsForTesting( + fileURL: URL, + collectLineageObservations: Bool) throws -> (snapshots: Int, observations: Int) + { + let parsed = try Self.parseCodexTokenSnapshots( + fileURL: fileURL, + collectLineageObservations: collectLineageObservations) + return (parsed.snapshots.count, parsed.observations.count) + } + + private static func lineageTotals(_ totals: CostUsageCodexTotals) -> CodexLineageLedger.Totals { + CodexLineageLedger.Totals(input: totals.input, cached: totals.cached, output: totals.output) + } + + private static func codexRolloutOwnerID(fileURL: URL) -> String? { + let stem = fileURL.deletingPathExtension().lastPathComponent + guard stem.count >= 36 else { return nil } + let candidate = String(stem.suffix(36)) + return UUID(uuidString: candidate) == nil ? nil : candidate.lowercased() } static func parseCodexFile( diff --git a/Tests/CodexBarTests/CodexLineageLedgerTests.swift b/Tests/CodexBarTests/CodexLineageLedgerTests.swift new file mode 100644 index 0000000000..1918904582 --- /dev/null +++ b/Tests/CodexBarTests/CodexLineageLedgerTests.swift @@ -0,0 +1,348 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct CodexLineageLedgerTests { + @Test + func `fast rollout parser adapts complete token states into a ledger document`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let ownerID = "019f55a1-7f6e-70c0-8e4f-f5bbefa9b7ac" + let fileURL = environment.root.appendingPathComponent( + "rollout-2026-07-09T12-00-00-\(ownerID).jsonl") + let contents = [ + #"{"type":"session_meta","payload":{"id":"metadata-id","forked_from_id":"parent-id"}}"#, + #"{"type":"event_msg","payload":{"type":"task_started","turn_id":"turn-a"}}"#, + Self.tokenCountLine( + timestamp: "2026-07-09T12:00:00Z", + last: (input: 100, cached: 40, output: 10), + total: (input: 100, cached: 40, output: 10)), + Self.tokenCountLine( + timestamp: "2026-07-09T12:01:00Z", + last: (input: 50, cached: 20, output: 5), + total: (input: 150, cached: 60, output: 15)), + #"{"type":"event_msg","timestamp":"2026-07-09T12:02:00Z","payload":{"type":"token_count","info":{"# + + #""total_token_usage":{"input_tokens":200,"cached_input_tokens":80,"output_tokens":20}}}}"#, + ].joined(separator: "\n") + try contents.write(to: fileURL, atomically: true, encoding: .utf8) + + let document = try CostUsageScanner.parseCodexLineageDocument(fileURL: fileURL) + + #expect(document.ownerID == ownerID) + #expect(document.metadataSessionID == "metadata-id") + #expect(document.parentSessionID == "parent-id") + #expect(document.observations.count == 2) + #expect(document.observations.map(\.eventID) == ["turn-a:0", "turn-a:1"]) + #expect(document.observations[0].last == .init(input: 100, cached: 40, output: 10)) + #expect(document.observations[1].total == .init(input: 150, cached: 60, output: 15)) + } + + @Test + func `snapshot only parsing skips lineage observation collection`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let fileURL = environment.root.appendingPathComponent("rollout-with-token-states.jsonl") + let contents = [ + Self.tokenCountLine( + timestamp: "2026-07-09T12:00:00Z", + last: (input: 100, cached: 40, output: 10), + total: (input: 100, cached: 40, output: 10)), + Self.tokenCountLine( + timestamp: "2026-07-09T12:01:00Z", + last: (input: 50, cached: 20, output: 5), + total: (input: 150, cached: 60, output: 15)), + ].joined(separator: "\n") + try contents.write(to: fileURL, atomically: true, encoding: .utf8) + + let snapshotsOnly = try CostUsageScanner.parseCodexTokenEvidenceCountsForTesting( + fileURL: fileURL, + collectLineageObservations: false) + let lineage = try CostUsageScanner.parseCodexTokenEvidenceCountsForTesting( + fileURL: fileURL, + collectLineageObservations: true) + + #expect(snapshotsOnly.snapshots == 2) + #expect(snapshotsOnly.observations == 0) + #expect(lineage.snapshots == 2) + #expect(lineage.observations == 2) + } + + @Test + func `ledger document parsing propagates cancellation`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let fileURL = environment.root.appendingPathComponent("rollout-without-owner.jsonl") + try Self.tokenCountLine( + timestamp: "2026-07-09T12:00:00Z", + last: (input: 100, cached: 0, output: 10), + total: (input: 100, cached: 0, output: 10)) + .write(to: fileURL, atomically: true, encoding: .utf8) + + #expect(throws: CancellationError.self) { + _ = try CostUsageScanner.parseCodexLineageDocument( + fileURL: fileURL, + checkCancellation: { throw CancellationError() }) + } + } + + @Test + func `transitive lineage counts copied observations once`() throws { + let first = Self.observation(timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let second = Self.observation(timestamp: "2026-07-09T12:01:00Z", input: 50, totalInput: 150) + let third = Self.observation(timestamp: "2026-07-09T12:02:00Z", input: 25, totalInput: 175) + let documents = [ + Self.document(owner: "root", observations: [first]), + Self.document(owner: "child", metadata: "root", parent: "root", observations: [first, second]), + Self.document(owner: "grandchild", metadata: "child", parent: "child", observations: [ + first, + second, + third, + ]), + ] + + let report = try CodexLineageLedger.reconcile( + documents: documents, + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + + #expect(report.utcDays["2026-07-09"]?.input == 175) + #expect(report.localDays["2026-07-09"]?.input == 175) + #expect(report.componentCount == 1) + #expect(report.acceptedObservationCount == 3) + #expect(report.duplicateObservationCount == 3) + } + + @Test + func `equal observations in disconnected lineages remain additive`() throws { + let observation = Self.observation(timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let report = try CodexLineageLedger.reconcile( + documents: [ + Self.document(owner: "first", observations: [observation]), + Self.document(owner: "second", observations: [observation]), + ], + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + + #expect(report.utcDays["2026-07-09"]?.input == 200) + #expect(report.componentCount == 2) + #expect(report.acceptedObservationCount == 2) + #expect(report.duplicateObservationCount == 0) + } + + @Test + func `copy stable identities preserve independent equal observations`() throws { + let copied = Self.observation( + eventID: "turn-a:0", timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let independent = Self.observation( + eventID: "turn-b:0", timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let report = try CodexLineageLedger.reconcile( + documents: [ + Self.document(owner: "root", observations: [copied]), + Self.document(owner: "child", parent: "root", observations: [copied, independent]), + ], + localTimeZone: .gmt) + + #expect(report.utcDays["2026-07-09"]?.input == 200) + #expect(report.acceptedObservationCount == 2) + #expect(report.duplicateObservationCount == 1) + } + + @Test + func `unchanged state reemissions within a lineage count once`() throws { + let observation = Self.observation(timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let report = try CodexLineageLedger.reconcile( + documents: [Self.document(owner: "root", observations: [observation, observation, observation])], + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + + #expect(report.utcDays["2026-07-09"]?.input == 100) + #expect(report.acceptedObservationCount == 1) + #expect(report.duplicateObservationCount == 2) + } + + @Test + func `parser ordinals do not revive an unchanged same turn state`() throws { + let environment = try CostUsageTestEnvironment() + defer { environment.cleanup() } + let fileURL = environment.root.appendingPathComponent("rollout-2026-07-09T12-00-00-root.jsonl") + let metadata = """ + {"timestamp":"2026-07-09T12:00:00Z","type":"session_meta","payload":{"id":"root"}} + {"timestamp":"2026-07-09T12:00:00Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-a"}} + """ + let tokenState = Self.tokenCountLine( + timestamp: "2026-07-09T12:01:00Z", + last: (input: 100, cached: 40, output: 10), + total: (input: 100, cached: 40, output: 10)) + try "\(metadata)\n\(tokenState)\n\(tokenState)" + .write(to: fileURL, atomically: true, encoding: .utf8) + + let document = try CostUsageScanner.parseCodexLineageDocument(fileURL: fileURL) + #expect(document.observations.map(\.eventID) == ["turn-a:0", "turn-a:1"]) + let report = try CodexLineageLedger.reconcile(documents: [document], localTimeZone: .gmt) + + #expect(report.utcDays["2026-07-09"]?.input == 100) + #expect(report.acceptedObservationCount == 1) + #expect(report.duplicateObservationCount == 1) + } + + @Test + func `matching event hints with different token states remain distinct`() throws { + let first = Self.observation( + eventID: "turn-a:0", timestamp: "2026-07-09T12:00:00Z", input: 100, totalInput: 100) + let forked = Self.observation( + eventID: "turn-a:0", timestamp: "2026-07-09T12:01:00Z", input: 25, totalInput: 125) + let report = try CodexLineageLedger.reconcile( + documents: [Self.document(owner: "root", observations: [first, forked])], + localTimeZone: .gmt) + + #expect(report.utcDays["2026-07-09"]?.input == 125) + #expect(report.acceptedObservationCount == 2) + #expect(report.duplicateObservationCount == 0) + } + + @Test + func `complete token state distinguishes observations within a lineage`() throws { + let first = Self.observation( + timestamp: "2026-07-09T12:00:00Z", + input: 100, + cached: 40, + output: 10, + totalInput: 100) + let changedTotal = Self.observation( + timestamp: "2026-07-09T12:01:00Z", + input: 100, + cached: 40, + output: 10, + totalInput: 200) + let changedLast = Self.observation( + timestamp: "2026-07-09T12:02:00Z", + input: 125, + cached: 50, + output: 15, + totalInput: 200) + let report = try CodexLineageLedger.reconcile( + documents: [ + Self.document(owner: "root", observations: [first, changedTotal, changedLast]), + ], + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + + #expect(report.utcDays["2026-07-09"] == .init(input: 325, cached: 130, output: 35)) + #expect(report.acceptedObservationCount == 3) + #expect(report.duplicateObservationCount == 0) + } + + @Test + func `UTC and local projections preserve their distinct day boundaries`() throws { + let observation = Self.observation(timestamp: "2026-07-10T02:00:00Z", input: 100, totalInput: 100) + let report = try CodexLineageLedger.reconcile( + documents: [Self.document(owner: "root", observations: [observation])], + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + + #expect(report.utcDays["2026-07-10"]?.input == 100) + #expect(report.localDays["2026-07-09"]?.input == 100) + } + + @Test(arguments: [ + ("archived-fork-33ce-3869", 15_309_178), + ("live-fork-4d90-52bf", 26_801_911), + ]) + func `sanitized fork fixtures collapse copied prefixes and unchanged reemissions`( + fixtureName: String, + expectedTokens: Int) throws + { + let fixture = try SanitizedForkFamilyFixture.load(named: fixtureName) + let parentMetadata = try fixture.sessionMetadata(named: "parent") + let childMetadata = try fixture.sessionMetadata(named: "child") + let parent = try fixture.events(named: "parent") + let child = try fixture.events(named: "child") + let documents = [ + CodexLineageLedger.Document( + ownerID: "parent-owner", + metadataSessionID: parentMetadata.id, + parentSessionID: parentMetadata.forkedFromID, + observations: parent.map(Self.observation)), + CodexLineageLedger.Document( + ownerID: "child-owner", + metadataSessionID: childMetadata.id, + parentSessionID: childMetadata.forkedFromID, + observations: child.map(Self.observation)), + ] + + let report = try CodexLineageLedger.reconcile( + documents: documents, + localTimeZone: #require(TimeZone(identifier: "America/New_York"))) + let total = report.utcDays.values.reduce(0) { partial, totals in + partial + totals.input + totals.output + } + + #expect(total == expectedTokens) + #expect(total < fixture.manifest.oracle.dedupedLastTokens) + } + + @Test + func `document order does not change lineage totals or attribution`() throws { + let copiedLater = Self.observation(timestamp: "2026-07-10T00:05:00Z", input: 100, totalInput: 100) + let original = Self.observation(timestamp: "2026-07-09T23:55:00Z", input: 100, totalInput: 100) + let root = Self.document(owner: "root", observations: [original]) + let child = Self.document(owner: "child", parent: "root", observations: [copiedLater]) + let timeZone = try #require(TimeZone(identifier: "America/New_York")) + + let forward = try CodexLineageLedger.reconcile(documents: [root, child], localTimeZone: timeZone) + let reversed = try CodexLineageLedger.reconcile(documents: [child, root], localTimeZone: timeZone) + + #expect(forward == reversed) + #expect(forward.utcDays["2026-07-09"]?.input == 100) + #expect(forward.utcDays["2026-07-10"] == nil) + } + + private static func document( + owner: String, + metadata: String? = nil, + parent: String? = nil, + observations: [CodexLineageLedger.Observation]) -> CodexLineageLedger.Document + { + .init( + ownerID: owner, + metadataSessionID: metadata, + parentSessionID: parent, + observations: observations) + } + + private static func observation( + eventID: String? = nil, + timestamp: String, + input: Int, + cached: Int = 0, + output: Int = 0, + totalInput: Int) -> CodexLineageLedger.Observation + { + .init( + eventID: eventID, + timestamp: timestamp, + last: .init(input: input, cached: cached, output: output), + total: .init(input: totalInput, cached: cached, output: output)) + } + + private static func observation( + _ event: SanitizedForkFamilyFixture.TokenEvent) -> CodexLineageLedger.Observation + { + .init( + timestamp: event.timestamp, + last: .init( + input: event.last.inputTokens, + cached: event.last.cachedInputTokens, + output: event.last.outputTokens), + total: .init( + input: event.total.inputTokens, + cached: event.total.cachedInputTokens, + output: event.total.outputTokens)) + } + + private static func tokenCountLine( + timestamp: String, + last: (input: Int, cached: Int, output: Int), + total: (input: Int, cached: Int, output: Int)) -> String + { + #"{"type":"event_msg","timestamp":"\#(timestamp)","payload":{"type":"token_count","info":{"# + + #""last_token_usage":{"input_tokens":\#(last.input),"cached_input_tokens":\#(last.cached),"# + + #""output_tokens":\#(last.output)},"total_token_usage":{"input_tokens":\#(total.input),"# + + #""cached_input_tokens":\#(total.cached),"output_tokens":\#(total.output)}}}}"# + } +}