Skip to content

pm-stack(aggregator): sanitizeSaasServices accepts inverted/negative PAYG estimates and negative metric values — records derived from them are refused by isSnapshotRecord #5188

Closed

Description

@dezren39

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, and percent need only isFiniteNumber — negative values pass.
  • isPaygEstimate (feed.ts:622, :625-626): low and high need only isFiniteNumber — an inverted pair (low > high) and negative endpoints pass.
  • isSaasService (feed.ts:632, :652, :659): billedUsd and cadenceDays likewise need only isFiniteNumber — 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 service billedUsd=-99 and cadenceDays=-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}, and isSnapshotRecord returns 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

  • isPaygEstimate rejects inverted pairs (low > high) and negative endpoints, mirroring the isSnapshotRecord pair rule in snapshots.ts.
  • isUsageMetric rejects negative used, limit, and percent values (null/undefined allowances unchanged).
  • isSaasService rejects negative billedUsd (when present and non-null) and negative cadenceDays.
  • A service failing any of these is dropped by sanitizeSaasServices (counted in dropped), while valid services — including zero values and a low === high estimate — still pass.
  • New tests cover each refused shape plus the valid controls; the aggregator bun test suite and typecheck pass.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Nov 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-11-03 16:05 CST
    • Found via: filed this run
    • Task: Filed verified bug — sanitizeSaasServices (feed.ts) accepts inverted/negative PAYG estimates and negative metric/billed/cadence values that isSnapshotRecord refuses; fix enqueued as a small_fix packet.
    • Expectation: effort s — done when the feed gate rejects those shapes, valid controls pass, and new tests + aggregator suite/typecheck are green.
    • Links: probe ~/git/fleet/probe-2026-11-03-laneA-saas (laptop)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedQueue worker lifecycle state
    on Nov 3, 2026
  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    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.

  5. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked-up

  6. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 3, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/pm-stackpm-stack (Pad/Grist) in developing-today/codekind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions