Repository navigation
Description
Problem
aggregateBillingUsage in pm-stack/aggregator/src/sources/github-billing.ts:78-113 (the #1051 collector core, merged in PR #2236) validates almost nothing about the unified billing endpoint's items:
- Date: the only check is
typeof item.date === "string" && item.date.length < 10skip (:81); the first 10 chars are taken verbatim (:82)."2026-13-99"and"2026-02-30"both pass. - Amounts:
quantity,grossAmount, andnetAmountare summed whenevertypeof === "number"(:84,:99-100) — negative values pass; no finiteness or sign check anywhere in the file.
The two failure modes land in different places, and both are bad:
- One impossible date discards the whole month's collection.
recordsFromBillingUsage(:149-168) stamps each day withdayTs(date)(snapshot-records.ts:32-34— plain string interpolation,"2026-13-99T12:00:00Z"). Those records failisSnapshotRecord(snapshots.ts:304,Date.parseis NaN), and the collector treats any invalid derived record as fatal:scripts/collect-github-billing.ts:84-86throws and appends nothing — every good day in the month is lost because one item's date was malformed. A Feb-30 date is worse in the opposite direction:Date.parserolls it over, so the record passes validation while carrying a literal nonexistent day as itsts(and dedupe key). - Negative usage is stored as fact. A negative
quantity/grossAmount/netAmountproduces negative day aggregates, and the derived records (e.g.actions-minutes: -120,billing-gross: -0.96) passisSnapshotRecord(finite is all it asks) and are appended to the snapshot store, corrupting the GitHub service's usage/billing series.
Sibling collectors in this repo all guard this: the Supabase exporter rejects negative/non-finite reads (parseDbSize, parseRequestCount in pm-stack/supabase-export/supabase-quota-export.mjs), and quota-gather-map.ts's num() is finite-gated. This file is the unguarded outlier.
Evidence (verified this run, main 7d1b081c)
Laptop probe (~/git/fleet/probe-2026-10-29-laneB-billing/probe2.ts, real github-billing.ts + snapshots.ts from a fresh clone at main, run with bun):
control (120 min, $0.96) => day aggregates correct; 3 records, all isSnapshotRecord = true
quantity/gross/net all negative => aggregates -120 / -0.96 / -0.96; records emitted with
value -120 and -0.96, isSnapshotRecord = true for all (stored)
date "2026-13-99T00:00:00Z" => day "2026-13-99" aggregates fine; records ts
"2026-13-99T12:00:00Z", isSnapshotRecord = false for all
=> collector throws, whole month appended: nothing
date "2026-02-30" => records ts "2026-02-30T12:00:00Z", isSnapshotRecord = true
(Date.parse rollover) — nonexistent day stored as-is
mixed good day + one month-13 item => 6 records, only 3 valid — the good day is discarded with the bad one
Why it matters
This collector is the GitHub billing series' only writer. Either failure silently degrades it: a single malformed item from the endpoint (shape drift, a partial response, a future SKU/date format) zeros out a month of Actions/Codespaces/billing history with only a thrown error in a daily job log, and a negative row — which no downstream check rejects — permanently writes negative minutes/dollars into the snapshot store that quota views and reconciliation read.
Acceptance criteria
- Items with a non-calendar date (month 13, Feb 30, non-date prefix) are rejected or skipped at aggregation time with the day/item identifiable — they never produce a record, and never take the month's good days down with them.
- Negative (and non-finite, for direct API callers of the pure functions)
quantity/grossAmount/netAmountvalues cannot produce negative aggregates or stored records; the chosen treatment (skip item vs skip field vs fail the run) is pinned by triage and documented in the file header. - The collector's all-or-nothing validation throw (
collect-github-billing.ts:84-86) is reconsidered for whatever validation remains downstream: one bad item must not discard a month of valid records. - Regression tests in
test/github-billing.test.tscover: month-13 date, Feb-30 date, negative quantity, negative gross/net, and a mixed good+bad month that still yields the good days' records. - Well-formed items are unchanged: existing
github-billing.test.tsexpectations still pass.
Dedup: #1051 (closed) specified this collector; PR #2236 built it. #1540/#2483 concern the snapshot store validators (snapshots.ts pairs/limit/resetsAt) — this is the producer-side item validation in sources/github-billing.ts, which neither covers. Title/body searches for github-billing, aggregateBillingUsage, and "billing usage" returned no covering issue, open or closed.
Activity
developing-today commentedon Oct 30, 2026
CODE-1272
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
2 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done (triage)
Verdict: CONFIRMED at current main
a11cfea3c(2026-11-01), with one behavior update vs the original filing.Evidence (fresh, this run):
src/sources/github-billing.tsaggregateBillingUsage: the date check is onlytypeof string && length >= 10, first 10 chars taken verbatim;quantitysums whenevertypeof number;grossAmount/netAmountlikewise. No calendar, sign, or finiteness check anywhere.recordsFromBillingUsage(github-billing.ts:149-168) stamps each day viadayTs(day.date)and pushes every non-zero field as a snapshot record;scripts/collect-github-billing.ts:84-86filters byisSnapshotRecordand throws if any derived record is invalid — appending nothing for the whole month.2026-13-99→ 3 / 0 valid (month lost); negative quantity → value −5, valid, stored; negative gross/net → −2/−3, valid, stored; NaN quantity → invalid record (month poisoned); mixed good+bad month → 6 / 3 valid, and the collector would throw away the good 3 too.2026-02-30→ 3 / 0 valid. Since the original probe,isSnapshotRecordgained the strictisIsoDatets gate (aggregator(snapshot-records): shape-only dates win freshestDate — a 2026-13-01 'Last checked' silently drops a pool's snapshot history, 2026-02-30 is stored as a phantom March date #2264/aggregator(snapshots): isSnapshotRecord never checks limit/resetsAt — a numeric resetsAt crashes the service history chart, a non-numeric limit NaNs it #2483), so Feb-30 no longer rolls over into a stored record — it has moved into the whole-month-loss class. Same fix covers it.isSnapshotRecord(snapshots.ts:296+) refuses negatives only forunit === "pct"(aggregator(derive): projectPace throws RangeError on validator-passing pct records — one poisoned snapshot breaks the quota card, service page, and service.json #2346), so negative minutes/hours/USD pass.Layer pinned — aggregation owns validation (
aggregateBillingUsage, github-billing.ts); the collector is NOT given per-item tolerance (its all-or-nothing throw stays as a backstop for genuinely new bug classes):isIsoDate(date.slice(0, 10))is false (validate.ts strict primitive — the same one the store gates on).quantity/grossAmount/netAmountcontribute only when finite and ≥ 0, else 0 — extending the existing non-number fallback. Negative consumption is never a legitimate endpoint semantic, a negative/non-finite field must neither be stored nor poison the month, and zeroing one field does not discard the item's other valid fields.Regression tests named in the fix packet (test/github-billing.test.ts): month-13 skipped with the rest of the month intact, Feb-30 skipped, negative quantity / negative gross-net zeroed per-field, NaN/Infinity zeroed, mixed month derives a 100%-valid record set, well-formed months unchanged.
dezren39 commentedon Nov 1, 2026
agent-note:v1
state: claimed
task: 5350 (repo-health-code-pilot, lane D)
stage: pickup
did: claimed small_fix; revalidated #2493 OPEN with no held/owned labels and no competing PR; aggregateBillingUsage at src/sources/github-billing.ts:78 matches the triage pin on main a11cfea.
resume: implement the aggregation-boundary validation on a laptop worktree, test, open PR, merge when verified.
Merge pull request #3445 from developing-today/queue/5350-2493-billin…
dezren39 commentedon Nov 1, 2026
agent-note:v1
state: done
task: 5350 (repo-health-code-pilot, lane D)
stage: fixed_merged
did: PR #3445 merged (merge commit b09c9ce); this issue auto-closed. aggregateBillingUsage now skips items whose date is not a real calendar date (isIsoDate) and counts quantity/grossAmount/netAmount only when finite and >= 0 (isNonNegative). Targeted tests 10/10 on the laptop worktree; Codex review completed with no findings.
resume: none — terminal.