( states: AttributionState[], stagedFiles: string[], )
| 547 | * Compares session baseline to committed state. |
| 548 | */ |
| 549 | export async function calculateCommitAttribution( |
| 550 | states: AttributionState[], |
| 551 | stagedFiles: string[], |
| 552 | ): Promise<AttributionData> { |
| 553 | const cwd = getAttributionRepoRoot() |
| 554 | const sessionId = getSessionId() |
| 555 | |
| 556 | const files: Record<string, FileAttribution> = {} |
| 557 | const excludedGenerated: string[] = [] |
| 558 | const surfaces = new Set<string>() |
| 559 | const surfaceCounts: Record<string, number> = {} |
| 560 | |
| 561 | let totalClaudeChars = 0 |
| 562 | let totalHumanChars = 0 |
| 563 | |
| 564 | // Merge file states from all sessions |
| 565 | const mergedFileStates = new Map<string, FileAttributionState>() |
| 566 | const mergedBaselines = new Map< |
| 567 | string, |
| 568 | { contentHash: string; mtime: number } |
| 569 | >() |
| 570 | |
| 571 | for (const state of states) { |
| 572 | surfaces.add(state.surface) |
| 573 | |
| 574 | // Merge baselines (earliest baseline wins) |
| 575 | // Handle both Map and plain object (in case of serialization) |
| 576 | const baselines = |
| 577 | state.sessionBaselines instanceof Map |
| 578 | ? state.sessionBaselines |
| 579 | : new Map( |
| 580 | Object.entries( |
| 581 | (state.sessionBaselines ?? {}) as Record< |
| 582 | string, |
| 583 | { contentHash: string; mtime: number } |
| 584 | >, |
| 585 | ), |
| 586 | ) |
| 587 | for (const [path, baseline] of baselines) { |
| 588 | if (!mergedBaselines.has(path)) { |
| 589 | mergedBaselines.set(path, baseline) |
| 590 | } |
| 591 | } |
| 592 | |
| 593 | // Merge file states (accumulate contributions) |
| 594 | // Handle both Map and plain object (in case of serialization) |
| 595 | const fileStates = |
| 596 | state.fileStates instanceof Map |
| 597 | ? state.fileStates |
| 598 | : new Map( |
| 599 | Object.entries( |
| 600 | (state.fileStates ?? {}) as Record<string, FileAttributionState>, |
| 601 | ), |
| 602 | ) |
| 603 | for (const [path, fileState] of fileStates) { |
| 604 | const existing = mergedFileStates.get(path) |
| 605 | if (existing) { |
| 606 | mergedFileStates.set(path, { |
no test coverage detected