Skip to content

aggregator(derive): projectPace throws RangeError on validator-passing pct records — one poisoned snapshot breaks the quota card, service page, and service.json #2346

Closed

Description

@dezren39

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 → null and slope <= 0.05 → null (derive.ts:935-937). Nothing bounds lastVal from 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 numeric value is accepted iff Number.isFinite(r.value). There is no pct range check, so a record with unit: "pct" and value: -1_000_000_000 is a valid store record: SnapshotStore.readAll() returns it and append() happily persists it.

  • Call sites do not catch: render.ts paceLineHtml (called per service card) calls projectPace directly; the service detail path maps projectPace over every metric (render.ts, paces construction); src/server.ts /api/quota/service.json maps 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

  • projectPace never throws for any records isSnapshotRecord accepts: out-of-range projections return null (or a clamped/null projectedExhaustionAt) 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.json succeed for a service whose stored history contains such a record.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. added
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run 2026-10-29 09:05 CDT
    • Found via: filed this run
    • Task: Triage the projectPace RangeError on validator-passing pct records; pin the no-projection rule for out-of-domain values, then route a small_fix packet with regression tests
    • Expectation: effort/xs — done when triage pins the fallback rule and a fix packet with the extreme/mild negative cases exists
    • Links: probe /tmp/probe-laneA-pace/probe.ts (bun, real derive.ts + snapshots.ts, main 5a34fa7)
  5. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  6. 16 remaining items

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume)

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 1, 2026
  10. added a commit that references this issue on Nov 1, 2026
  11. dezren39 commented on Nov 1, 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

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions