Repository navigation
Description
Summary
isSnapshotRecord (pm-stack/aggregator/src/snapshots.ts:26-42) — the structural validator every snapshot-store read and append gates on — never checks the record's two optional fields, limit and resetsAt (model: pm-stack/aggregator/src/model.ts:1266-1267, limit?: number | null, resetsAt?: string | null). A stored JSONL record carrying either field in the wrong type passes validation, is returned by SnapshotStore.readAll/byService, and then breaks the service-detail history chart:
resetsAtas a number crashes the chart.resetMarkers(src/derive.ts:975-991) treats any truthyresetsAtas a string:Date.parse(prev.resetsAt)coerces a number to a string and can parse it as a date (e.g.2026→ 2026-01-01), so the marker is emitted with the number as itsts/resetsAt.historyChartHtml(src/render.ts:1869-1875) then callsm.resetsAt.slice(0, 10)on that number →TypeError: m.resetsAt.slice is not a function. The call site is the/quota/service/<id>route (src/server.ts:527,renderServiceDetail→historyChartHtml), which has no try/catch around the render, so one poisoned history record 500s that service's detail page on every load until the record ages out of the store.- A non-numeric
limitsilently NaNs the chart.historyChartHtml(src/render.ts:1866) foldsr.limit ?? 0into theMath.max(...)that computesvmax; an object/array/non-numeric-string limit makesvmaxNaN, so every chart coordinate renders asNaN— the chart is garbage with no error anywhere.
Evidence (verified this run, main 1ddc7720)
Laptop probe ~/git/fleet/probe-2026-10-29-laneA-snapreset/ (verbatim aggregator src/ copied from current main; sha256 of snapshots.ts/render.ts/derive.ts identical on sandbox and probe), Bun, driving the real modules:
isSnapshotRecord({...valid, resetsAt: 2026})→true;limit: "not-a-number"→true;limit: {x:1}→true.resetMarkers([record(resetsAt: 2026, ts 2025-12-01), record(ts 2026-11-01)], "m")→[{ts: 2026, resetsAt: 2026}].historyChartHtml(same pair, "m")→ throwsTypeError: m.resetsAt.slice is not a function.- Control (string
resetsAt: "2026-06-01T00:00:00Z"): chart renders, marker present. historyChartHtmlwithlimit: {x:1}: output containsNaNcoordinates.- Reachability: live source loads history via
SnapshotStore.byService()(src/sources/live.ts:799) intousage.history, keyed by service id — exactly what the service route renders.
Why it matters
The snapshot store is append-only history written by several jobs and a backfill; the validator is the only boundary between an on-disk JSONL line and the renderers, and it already type-checks every other field (including finiteness for scalar value). One malformed line — a writer emitting a numeric epoch-year or a string limit — permanently breaks one service's detail page (crash) or its history chart (NaN), with the failure surfacing far from the bad record.
Acceptance criteria
isSnapshotRecordrejects a record whoselimitis present and not a finite number ornull, and whoseresetsAtis present and not a string thatDate.parseaccepts (ornull).- Regression tests: a numeric-
resetsAtrecord and an object-limitrecord each fail validation / are skipped byreadAll; the string-resetsAt+ numeric-limitcontrol still renders a chart with its reset marker. - No change to the on-disk format, the dedupe key, or behaviour for records whose optional fields are absent or well-typed.
Notes
- Distinct from aggregator: SnapshotStore accepts records it can never read back — non-finite estimate pairs and non-ISO timestamps pass validation, append reports success, and dedupe never sees them #1540 (same validator: non-finite estimate pairs + non-ISO
tspass and can never be read back — different fields, different failure) and aggregator(derive): projectPace throws RangeError on validator-passing pct records — one poisoned snapshot breaks the quota card, service page, and service.json #2346 (validator-passing scalar pct values makeprojectPacethrow — different consumer). - Not self-solved: whether to also harden
resetMarkers/historyChartHtmlagainst already-stored poisoned records, or validator-only, is triage's pin.
Activity
developing-today commentedon Oct 30, 2026
CODE-1268
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 31, 2026
Closing as fixed by PR #3035 (merged 5411129, queue task 4006 slice 1): isSnapshotRecord in snapshots.ts now checks limit/resetsAt — a non-finite/negative limit or an impossible resetsAt date refuses the record, expressed with the Phase 1 validate.ts primitives (isFiniteNumber / isIsoDate). Probe coverage landed in the snapshot-writers suite; laptop verification recorded on PR #3035 (targeted suites 43 pass; full aggregator suite 676 pass / 7 pre-existing page-manifest fails reproduced on clean main).