Repository navigation
Description
Summary
projectPace in the pm-stack aggregator throws RangeError: Invalid Date on snapshot records that pass the store's own validator, and the throw propagates uncaught through the quota card render, the service detail page, and /api/quota/service.json. One poisoned pct record in the append-only snapshot store breaks those views for its service on every render until the record is removed by hand.
Evidence (verified on main 5a34fa7)
-
pm-stack/aggregator/src/derive.ts:939-940— the projection is computed with no range guard:const projectedExhaustionAt = lastVal >= 100 ? last.ts : new Date(Date.parse(last.ts) + ((100 - lastVal) / slope) * 86_400_000).toISOString();
The only guards above it are
days < 0.5 → nullandslope <= 0.05 → null(derive.ts:935-937). Nothing boundslastValfrom below, so a small positive slope plus a very negative last value pushes the projected instant past the maximum representable Date (±8.64e15 ms), producing an Invalid Date whose.toISOString()throws. -
pm-stack/aggregator/src/snapshots.ts:36-52(isSnapshotRecord) — a numericvalueis accepted iffNumber.isFinite(r.value). There is no pct range check, so a record withunit: "pct"andvalue: -1_000_000_000is a valid store record:SnapshotStore.readAll()returns it andappend()happily persists it. -
Call sites do not catch:
render.tspaceLineHtml(called per service card) callsprojectPacedirectly; the service detail path mapsprojectPaceover every metric (render.ts,pacesconstruction);src/server.ts/api/quota/service.jsonmaps it the same way. A throw in any of them aborts the whole render/response, not just the pace line.
Reproduction (probe run this run, bun, against unmodified main)
Records (both pass isSnapshotRecord):
{ ts: "2026-10-20T00:00:00Z", value: -1_000_000_000, unit: "pct", metricId: "weekly", ... }
{ ts: "2026-10-22T00:00:00Z", value: -999_999_999, unit: "pct", metricId: "weekly", ... }projectPace(recs, "weekly") → throws RangeError: Invalid Date (slope = 0.5 pts/day over 2 days; projected offset ≈ 2×10⁹ days ≈ 1.7×10¹⁷ ms, past the Date maximum).
Control: ordinary records (30 → 40 over the same window) return a sane projection (2026-11-03T00:00:00.000Z).
A milder variant does not throw but is still wrong: values −5000 → −4999.88 (slope 0.06) project exhaustion in the year 2259 — a negative-percent metric should support no exhaustion projection at all.
Why it matters
The snapshot store is append-only and written by several jobs (fleet-feed collector, stewardship-cycle, the Jules keeper, a Python mirror). A single bad pct observation — a misparse, a sign flip, a units mixup upstream — permanently poisons the quota views for that service: the store has no delete path, readAll keeps returning the record, and every affected page/API call throws instead of degrading to "no pace line", which is the function's own documented contract ("Null when history cannot support a projection — never invented", derive.ts:923-925).
Related but distinct: #1540 covers the validator accepting records it cannot read back (non-finite estimate pairs, non-ISO timestamps). This issue is downstream of the validator: even records the validator legitimately accepts can crash the projection, so the fix belongs in projectPace as well as (optionally) the validator.
Acceptance criteria
projectPacenever throws for any recordsisSnapshotRecordaccepts: out-of-range projections returnnull(or a clamped/nullprojectedExhaustionAt) instead of an Invalid Date exception.- A last value below 0 (or otherwise outside the plausible pct domain) yields no exhaustion projection.
- Regression tests in the aggregator test suite cover: the extreme-negative pair above, the mildly-negative pair, and the existing sane cases (single point, flat slope) still behaving as before.
- Quota card render, service detail render, and
/api/quota/service.jsonsucceed for a service whose stored history contains such a record.
Activity
developing-today commentedon Oct 29, 2026
CODE-1191
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
16 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up (resume)
Merge pull request #3411 from developing-today/queue/5244-2346-projec…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done