Repository navigation
Description
pm-stack(aggregator): sanitizeSaasServices accepts inverted/negative PAYG estimates and negative metric values — records derived from them are refused by isSnapshotRecord
Evidence
The feed-boundary gate for SaaS services in pm-stack/aggregator/src/feed.ts checks only finiteness, never range or ordering:
isUsageMetric(feed.ts:606, :611-615):used,limit, andpercentneed onlyisFiniteNumber— negative values pass.isPaygEstimate(feed.ts:622, :625-626):lowandhighneed onlyisFiniteNumber— an inverted pair (low > high) and negative endpoints pass.isSaasService(feed.ts:632, :652, :659):billedUsdandcadenceDayslikewise need onlyisFiniteNumber— negatives pass.
Sibling gates in the same codebase refuse exactly these shapes: isSnapshotRecord (src/snapshots.ts:344) requires a non-negative limit (isNonNegative), refuses negative pct values, and its estimate-pair branch (:358) requires p.low <= p.high. The producer-side extractors were fixed for the same class in #4942 / #2508, but this downstream sanitizer — the boundary that is supposed to drop malformed rows at feed read (#2451) — was not.
Verified with a laptop bun probe at main d99399c1fde06102c6a8b8f53931e6ef99c59085 (probe ~/git/fleet/probe-2026-11-03-laneA-saas/probe.ts on the laptop, importing sanitizeSaasServices / recordsFromSaasServices / isSnapshotRecord from the primary tree):
- Valid control service: kept (dropped=0). Garbage control (
high: "x"): dropped. paygEstimate {low:100, high:50}: kept;countedEstimate(model.ts:1188, 1.2 × high) = 60 — a counted figure below the estimate's own low.paygEstimate {low:-10, high:-50}: kept;countedEstimate= -60, rendered verbatim by render.ts:2307/:2536 as a negative PAYG estimate.- Metric
used=-500,limit=-100,percent=-20, plus servicebilledUsd=-99andcadenceDays=-30: all kept. - Round-trip loss:
recordsFromSaasServices(snapshot-records.ts:105-115) turns the kept inverted estimate into a snapshot record{low:100, high:50}, andisSnapshotRecordreturns false for it — so a service the feed accepts produces snapshot history that is silently dropped on readback.
Why it matters
sanitizeSaasServices is the trust boundary for feed-supplied SaaS data: anything it keeps is rendered as measured usage/PAYG money (render.ts metric rows and PAYG cards, derive.ts:895-901 attention flags via countedEstimate) and converted into snapshot records. Inverted/negative estimates and negative usage/limits therefore display nonsense money, suppress or distort the payg-unbilled attention flag (counted is compared against the $5 bubble threshold), and create snapshot records the snapshot store itself refuses — silent history loss instead of a quarantined row at the boundary.
Acceptance criteria
isPaygEstimaterejects inverted pairs (low > high) and negative endpoints, mirroring theisSnapshotRecordpair rule in snapshots.ts.isUsageMetricrejects negativeused,limit, andpercentvalues (null/undefined allowances unchanged).isSaasServicerejects negativebilledUsd(when present and non-null) and negativecadenceDays.- A service failing any of these is dropped by
sanitizeSaasServices(counted indropped), while valid services — including zero values and alow === highestimate — still pass. - New tests cover each refused shape plus the valid controls; the aggregator bun test suite and typecheck pass.
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 3, 2026
agent-note:v1 — task 8873 (lane code-m): implementation complete on PR #5192 (branch queue/8873-5188-feed-range-gates, head d6231ea). feed.ts sanitizers now mirror snapshots.ts via isNonNegative: isUsageMetric rejects negative used/limit/percent, isPaygEstimate rejects negative endpoints and low>high (low===high and zero pass), isSaasService rejects negative billedUsd/cadenceDays. Tests: new gate block in test/saas-feed-gate.test.ts, targeted suite 10/10 on the laptop; full aggregator suite + tsc failures are identical on clean main (stash-verified pre-existing: snapshot-lock timing flake, web-push unhandled error, item-history GhKind + overview HealthView tsc errors from PR #5176) — zero errors in the touched files. Task parked 20m awaiting the Codex review on PR #5192; resume merges under the standing criteria and closes this issue.
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked-up
pm-stack(aggregator): range/order gates for SaaS feed sanitizers (#5188…
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
percent-key assertion) is a test-coverage suggestion only — the validator readsv.percentidentically for absent and undefined keys, so no behavior gap; left as-is.