Skip to content

aggregator(snapshots): isSnapshotRollup count accepts fractional and infinite counts — 1.5 and a JSON 1e999 pass the gate and poison every later compaction report for the month #3443

Closed

Description

@dezren39

Bug

isSnapshotRollup (pm-stack/aggregator/src/snapshots.ts:91) checks the rollup's record count with only:

if (typeof r.count !== "number" || !(r.count >= 1)) return false;

That refuses 0 and negatives, but a count is a count of raw records — it must be a positive integer. Today the gate also passes:

  • a fractional count (1.5), and
  • an infinite count (Infinity — reachable from a real JSONL line, because JSON.parse("1e999") is Infinity, so a rollup line carrying "count": 1e999 parses to an infinite count).

count is not decorative. Compaction trusts it arithmetically: compactSnapshotMonth sums existing rollup counts into the report (snapshots.ts:725 per-service before += r.count; :732 recordsBefore: records.length + existing.reduce((n, r) => n + r.count, 0)), and re-compaction seeds from existing rollups and increments the carried count (aggAddRecord, :154 o.count += 1), so a fractional count stays fractional forever (1.5 → 2.5 → 3.5 …) and an infinite count makes every later compaction report for that month report recordsBefore: Infinity. Rollup files are kept indefinitely, so the corrupted count is re-seeded on every compaction rather than aging out.

This is the one numeric field #3365's hardening of this same gate did not cover: its acceptance pins the timestamp, limit/resetsAt, pair-value, and month rules, but not count.

Evidence (verified this run, main ba2e97d)

Laptop probe ~/git/fleet/probe-2026-11-01-laneA-gendates/probe.ts (Bun 1.4.2), importing the real snapshots.ts from the laptop clone — snapshots.ts md5 298f32a5, identical on sandbox main ba2e97d and the probe clone; output in probe.txt in the same directory:

  • Control rollup (count: 3, otherwise well-formed) → isSnapshotRollup = true.
  • count: 1.5 → true.
  • count: Infinity → true; the same value reached via JSON.parse of a line with "count":1e999 → Infinity → gate true.
  • Controls on the existing rule: count: 0 → false, count: -2 → false (already refused).

Why it matters

The snapshot store is the usage hub's history, and its compaction reports ("records before/after", per-service counts) are the operator's evidence that retention is lossless. A count the gate certifies but no writer can produce silently falsifies those reports for the affected month — permanently, because rollups never expire and each compaction re-seeds from the last. Every other count-like field in this codebase's gates is checked as an integer (e.g. isRepoCensus uses Number.isInteger); this one was left as a bare range check.

Acceptance criteria

  • isSnapshotRollup refuses a count that is not a positive integer (Number.isInteger(count) && count >= 1): fractional, infinite, and NaN counts fail the gate, alongside the already-refused 0/negatives.
  • A rollup line with "count": 1e999 or "count": 1.5 in a rollup file is dropped by readAll/rollupKeys/compaction seeding like any other malformed rollup line; a well-formed rollup still passes and expands unchanged.
  • Regression tests beside the existing snapshot gate tests pin each refused shape plus the valid control.
  • No change to the rollup file format or to compaction output for valid rollups.

Distinct from #3365 (same function's timestamp/limit/resetsAt/pair/month rules — its acceptance does not mention count), #3364 (coverage census counts), #3285 (compact script --months argument).

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — picked up — OpenChamber scheduled lane

  5. 4 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — handed off and done — OpenChamber scheduled lane

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  10. dezren39 commented on Nov 2, 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)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/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/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