Skip to content

aggregator: isItemHistoryCutoff accepts impossible/non-ISO cutoffAt (and any string as coverage.fetchedAt) #5172

Closed

Description

@dezren39

aggregator: isItemHistoryCutoff accepts impossible/non-ISO cutoffAt (and any string as coverage.fetchedAt)

Evidence

pm-stack/aggregator/src/item-history.ts:144 — the structural gate for stored item-history cutoff lines:

if (typeof c.cutoffAt !== "string" || Number.isNaN(Date.parse(c.cutoffAt))) return false;

Date.parse is lenient: impossible calendar dates roll over (2026-02-30 → Mar 2) and non-ISO strings parse in a timezone-dependent way. The sibling gates in this codebase use the real-calendar rule instead — isIsoDate (pm-stack/aggregator/src/validate.ts:116, via parseIsoDate), e.g. snapshots.ts:328 ("Real calendar dates only (#2264/#2483): Date.parse rolls…") and the gates fixed in #5091/#5140/#5154.

coverage.fetchedAt is weaker still — item-history.ts:147 checks only typeof coverage.fetchedAt === "string", with no parse at all.

Verified with a laptop bun probe at main 298f35f75b4a62e71666810f9a972138a088fb55 (probe /tmp/probe-ih.ts on the laptop, importing isItemHistoryCutoff / blockerFirstObserved from the primary tree):

  • isItemHistoryCutoff returns true for cutoffAt = 2026-02-30T00:00:00Z, 2026-04-31T00:00:00Z, "March 2, 2026", and "03/02/2026"; control "not-a-date" → false, valid ISO → true.
  • coverage.fetchedAt = "not-a-date" and "2026-02-30T00:00:00Z" both pass the gate.
  • Downstream effect: blockerFirstObserved over cutoffs 2026-02-30 → 2026-03-10 reports firstObservedAt: "2026-02-30T00:00:00Z" verbatim with lowerBoundAgeDays: 8 — the age is computed from the rolled-over Mar 2 instant, for a date that does not exist.

Why it matters

ItemHistoryStore.readAll() (item-history.ts:198-215) trusts this gate for every line of item-history.jsonl, then sorts by cutoffAt.localeCompare (a non-ISO stamp like "March 2, 2026" sorts lexicographically, not chronologically) and feeds weeklyTransitions, weeklyCompletions, and blockerFirstObserved, whose ages are Date.parse differences (line 321). One corrupt or hand-edited cutoff line with an impossible/non-ISO stamp is therefore silently accepted and shifts blocker-age and weekly-flow numbers instead of being quarantined like other invalid stored records in this codebase.

Acceptance criteria

  • isItemHistoryCutoff rejects a cutoffAt that is not a real ISO calendar date/time (gate with isIsoDate from validate.ts, consistent with the sibling gates), including 2026-02-30, 2026-04-31, "March 2, 2026", and "03/02/2026".
  • coverage.fetchedAt is validated to the same standard (not typeof-only).
  • Valid ISO cutoffs captured by captureItemHistory still pass the gate (existing round-trip test at test/item-history.test.ts:88 stays green).
  • New tests in pm-stack/aggregator/test/item-history.test.ts cover the refused stamps above for both fields plus the valid control.
  • Aggregator 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
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    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 job (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-11-03 15:35 CST
    • Found via: filed this run
    • Task: Gate isItemHistoryCutoff's cutoffAt (and coverage.fetchedAt) with isIsoDate so impossible/non-ISO stamps are quarantined instead of shifting blocker-age/weekly-flow derivations
    • Expectation: effort s — done when the gate refuses 2026-02-30 / 2026-04-31 / non-ISO stamps for both fields, valid ISO cutoffs still pass, and new item-history tests cover the refused cases
    • Links: evidence + probe in the issue body (laptop bun probe at main 298f35f)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

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

    @dezren39
    MemberAuthor

    🤖 Agent note · done — fixed and merged

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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