fix: --project/--exclude are ignored by the durable headline totals - #864
Open
GodCC6 wants to merge 1 commit into
Open
fix: --project/--exclude are ignored by the durable headline totals#864GodCC6 wants to merge 1 commit into
GodCC6 wants to merge 1 commit into
Conversation
GodCC6
force-pushed
the
fix/project-filter-durable-headline
branch
from
July 31, 2026 11:29
2bc7f21 to
6d071d6
Compare
The durable headline unions the carry-forward daily cache with today's live parse. Cached days were sliced to the requested provider (sliceDayToProvider) but never to the requested project, so a --project/--exclude query counted every historical day WHOLE while the detail panels — By Project / By Activity / By Model, all built from the name-filtered live parse — left the filtered projects out. The Overview total and the panels below it could not be reconciled; on a real one-week corpus the headline ran $2,428.85 over the By Project sum, which is exactly the excluded projects' spend. Add sliceDayToProject, the project-level counterpart of sliceDayToProvider, and apply it to the cache-sourced days. Cost, calls, sessions and savings come out of the per-project day stats the cache has carried since v15, so they stay exact and still include days whose session files have expired. Provider slices carry their own project split, so --provider on top of a project filter stays consistent. Fields the cache has no per-project split for are handled explicitly rather than reported as the filtered projects' own: - tokens/models/categories come from the (project-filtered) live parse when a project filter is active, which is exact for every surviving session and matches the panels that read the same parse; - days, or provider slices, carried from before v15 have no project split at all and cannot be attributed, so they contribute nothing and their cost is returned as unattributedCostUSD and footnoted by the terminal overview instead of being folded into a filtered total.
GodCC6
force-pushed
the
fix/project-filter-durable-headline
branch
from
July 31, 2026 11:32
6d071d6 to
4bfcc02
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--project/--excludewere ignored by the durable headline:buildDurablePeriodslices the cache-sourced days to the requested provider but never to the requested project, so the Overview total counted excluded projects while By Project / By Activity / By Model (built from the name-filtered live parse) left them out.sliceDayToProject, the project-level counterpart ofsliceDayToProvider, so cost / calls / sessions / savings are sliced out of the per-project day stats the cache has carried since v15 — exact, and still including days whose session files have expired.unattributedCostUSDand footnoted, never silently folded in.Testing
npm testpassesnpm run buildsucceedsnpm test: 2,492 passed, 5 failed — all 5 fail identically on a clean checkout of this PR's base commit, so none are mine:cache-refresh-lock(fence/heartbeat),parser(copilot file-purge, orphan fingerprint),sharing/connect-timeout, andcli-durable-totals"resolves provider filters identically". That last one fails onmainbecauseparseAllSessionsreturns nothing for a today-only range once the on-disk session cache is warm — unrelated to this change, but you may want a separate look.npx tsc --noEmitis clean.Repro
Overview reported $7,962.65 while every By Project row summed to ~$5,503.70 — a ~$2,458.95 gap, exactly the excluded projects' spend. By Activity and By Model both summed into the same $5,500–5,600 band, so the detail panels agreed with each other and only the headline was off.
Measured on the same corpus with
--format json(the-p weekwindow had moved on by then, so absolute figures differ slightly):Calls were off by the same mechanism: 84,458 → 56,492. The small residual is not from this change — the unfiltered control shows a larger pre-existing divergence, because the durable path counts whole local days out of the cache while the live parse filters turns by exact timestamp, so the two differ at a rolling range's boundary.
Footnote behaviour on the same real cache (15 of 93 days predate the per-project split):
Root cause
buildDurablePeriod()unions two sources: today, viafp(await parseAllSessions(...))wherefp = filterProjectsByName(...)— filtered — and history, viagetDaysInRange(cache, ...)merged byunionDaysForPeriod()— never project-filtered. Provider filtering did slice the historical days (pf === 'all' ? allDays : allDays.map(d => sliceDayToProvider(d, pf))); there was simply no project-level counterpart.buildJsonReport()readstotalCostUSDstraight fromdurable.data, which is why Overview overstated, while By Project reads the correctly-filtereddurable.liveProjects.Filter matching mirrors
filterProjectsByNameexactly — case-insensitive substring against the project name or its path, include then exclude — so a filter selects the same projects whether it resolves against a fresh parse or the day cache. Provider slices carry their ownprojectssplit, so they are sliced too and--provider X --exclude Ystays consistent with the day-level slice.Known limitation — your call
ProjectDayStatsis{ cost, calls, savingsUSD, sessions, path? }: there is no per-project split for tokens, models or categories. I picked the option that never reports another project's numbers as the filtered projects', but the alternative is defensible and I'm happy to switch:unattributedCostUSDand the overview footnotes it. Folding them in would keep--project Xclaiming spend that provably isn't X's, so I chose a stated shortfall over a silent overstatement — but that trades against the never-lose-history invariant, so say the word if you'd rather keep them.A refinement I deliberately left out: on the 13 days in my cache where the day has a split but one provider slice doesn't, the missing slice is derivable as
day.projectsminus the other slices' projects. That assumes the merged day is internally consistent and a clamped subtraction would fail silently when it isn't, so it seemed better as a follow-up than as part of a correctness fix.Adjacent, not fixed here
The menubar /
statusprovider list has the same root cause:usage-aggregator.ts:632-635buildsunfilteredProviderDaysfromgetDaysInRange(cache, ...)(unfiltered) plusgetTodayAllDays()(filtered), so per-provider costs still ignore--project/--excludefor historical days. I left it out because narrowing it also changes which providers appear in the list at all, which felt like your call rather than mine. Happy to do it here or in a follow-up.Tests
tests/project-filter-durable-totals.test.ts, 11 cases, the failing-first ones written before the fix: exclude and include filters against a two-project carried day, path matching, a fully-excluded day, project-underneath-provider, and the two pre-v15 fallbacks plus the overview footnote. Two are invariant guards that hold before and after the change rather than red-first tests — the unfiltered headline being byte-for-byte what it was, and the menubar payload staying in step with the report under a project filter.Filed as #865, which also carries the two policy questions above in case you'd rather settle them there.
🤖 Generated with Claude Code