From 2262a82f51b4cb68567d579029750977839817f4 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Mon, 10 Aug 2026 14:13:57 -0700 Subject: [PATCH] fix(pr-attribution): time-bound working-directory correlation The cwd evidence rule attributed ANY session sharing a checkout with a PR-linked session, with no time bound - so a repo whose only captured PR link was pasted once became a black hole: 129 of 131 sessions and a month of unrelated work (~$7.4K direct, $11.2K displayed) attributed to one PR, observed live on the desktop Pull requests tab. Cwd anchors now carry the evidence sessions' own activity window (union across evidence for the same PR set), and only sessions overlapping that window plus a 6h pad inherit the PR. The rule's charter is 'a tool session launched around PR work in this checkout', which is inherently a same-working-stretch claim; the design's own philosophy (timestamps narrow, never create) now applies to this rule too. On the real corpus the row corrected to $450.87 / 21 sessions across the PR's actual two-day working stretch. Regression pins both directions: nearby same-cwd session inherits, weeks-later one never does; multiple evidence sessions widen the window. --- src/parser.ts | 32 ++++++++++++++++++--- tests/cross-provider-pr-correlation.test.ts | 25 ++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 13449e5e..712295b0 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -3595,13 +3595,31 @@ export function correlateCrossProviderPrSessions(projects: ProjectSummary[]): vo // Prompt-linked sessions become valid cwd anchors too. Attribute only when an // exact cwd maps to one PR set; a main checkout used for multiple PRs remains // intentionally ambiguous. - const refsByCwd = new Map>() + // + // Time-bounded (the eywa#160 lesson): cwd evidence also carries the evidence + // sessions' own activity window, and only sessions OVERLAPPING that window + // (plus a pad) inherit the PR. Without the bound, a repo whose only captured + // PR link was pasted once became a black hole — every session ever run in + // that checkout, a month of unrelated work included, was attributed to it + // (129 of 131 sessions, ~$7.4K direct, observed on real data). The rule's + // charter is "a tool session launched around PR work in this checkout", + // which is inherently a same-working-stretch claim. + const CWD_WINDOW_PAD_MS = 6 * 60 * 60 * 1000 + type CwdAnchor = { refs: string[]; startMs: number; endMs: number } + const refsByCwd = new Map>() for (const [session, evidenceRefs] of evidence) { const cwd = normalizedWorkingDirectory(session.workingDirectory) if (!cwd || evidenceRefs.length !== 1) continue + const startMs = Date.parse(session.firstTimestamp) + const endMs = Date.parse(session.lastTimestamp) + if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) continue const refs = evidenceRefs.slice().sort() - const sets = refsByCwd.get(cwd) ?? new Map() - sets.set(refs.join('\0'), refs) + const key = refs.join('\0') + const sets = refsByCwd.get(cwd) ?? new Map() + const existing = sets.get(key) + sets.set(key, existing + ? { refs, startMs: Math.min(existing.startMs, startMs), endMs: Math.max(existing.endMs, endMs) } + : { refs, startMs, endMs }) refsByCwd.set(cwd, sets) } for (const session of sessions) { @@ -3609,7 +3627,13 @@ export function correlateCrossProviderPrSessions(projects: ProjectSummary[]): vo const cwd = normalizedWorkingDirectory(session.workingDirectory) if (!cwd) continue const sets = refsByCwd.get(cwd) - if (sets?.size === 1) assignCorrelatedPrs(session, [...sets.values()][0]!, 'working-directory') + if (sets?.size !== 1) continue + const anchor = [...sets.values()][0]! + const sessionStart = Date.parse(session.firstTimestamp) + const sessionEnd = Date.parse(session.lastTimestamp) + if (!Number.isFinite(sessionStart) || !Number.isFinite(sessionEnd)) continue + if (sessionEnd < anchor.startMs - CWD_WINDOW_PAD_MS || sessionStart > anchor.endMs + CWD_WINDOW_PAD_MS) continue + assignCorrelatedPrs(session, anchor.refs, 'working-directory') } } diff --git a/tests/cross-provider-pr-correlation.test.ts b/tests/cross-provider-pr-correlation.test.ts index 8353957b..9968700a 100644 --- a/tests/cross-provider-pr-correlation.test.ts +++ b/tests/cross-provider-pr-correlation.test.ts @@ -106,3 +106,28 @@ describe('cross-provider PR correlation', () => { expect(codex.prAttributionSource).toBe('launcher-prompt') }) }) + +describe('working-directory correlation is time-bounded (eywa#160 regression)', () => { + it('attributes a same-cwd session inside the evidence window, never one weeks later', () => { + // One session in the repo genuinely produced the PR link. A tool session + // an hour later in the same checkout is PR work; a session three weeks + // later in the same checkout is just... work. Before the bound, the + // later session (and 128 real siblings) inherited the PR, attributing a + // month of unrelated spend ($7.4K observed) to a single PR row. + const linked = session({ id: 'anchor', provider: 'claude', timestamp: '2026-07-11T10:00:00Z', message: 'open the PR', refs: [A], cwd: '/repo/eywa' }) + const nearby = session({ id: 'nearby', provider: 'codex', timestamp: '2026-07-11T11:30:00Z', message: 'run the review suite for the change', cwd: '/repo/eywa' }) + const weeksLater = session({ id: 'later', provider: 'codex', timestamp: '2026-08-02T09:00:00Z', message: 'completely unrelated feature work', cwd: '/repo/eywa' }) + correlateCrossProviderPrSessions([project([linked, nearby, weeksLater])]) + expect(nearby.prLinks).toEqual([A]) + expect(nearby.prAttributionSource).toBe('working-directory') + expect(weeksLater.prLinks).toBeUndefined() + }) + + it('widens the window across multiple evidence sessions for the same PR', () => { + const early = session({ id: 'e1', provider: 'claude', timestamp: '2026-07-11T10:00:00Z', message: 'open', refs: [A], cwd: '/repo/eywa' }) + const late = session({ id: 'e2', provider: 'claude', timestamp: '2026-07-14T10:00:00Z', message: 'follow-up', refs: [A], cwd: '/repo/eywa' }) + const between = session({ id: 'mid', provider: 'codex', timestamp: '2026-07-12T12:00:00Z', message: 'work in between the two linked sessions', cwd: '/repo/eywa' }) + correlateCrossProviderPrSessions([project([early, late, between])]) + expect(between.prLinks).toEqual([A]) + }) +})