Skip to content

pm-stack(aggregator): validateStreamEvent accepts impossible calendar dates — Date.parse rolls 2026-02-30 over to March 2 instead of refusing it #5154

Closed

Description

@dezren39

Problem

The aggregator's ingest contract (pm-stack/aggregator/src/validate.ts:1-9) says every ingested document is validated at its boundary for "shape, element types, value ranges, and real calendar dates", and validate.ts provides parseIsoDate/isIsoDate for exactly this: "impossible calendar dates FAIL instead of rolling over: 2026-02-30 and 2026-13-01 are null, not March dates."

The stream-event boundary never got that check. validateStreamEvent (pm-stack/aggregator/src/events.ts:54) validates the event timestamp with Date.parse only:

if (typeof v.at !== "string" || !Number.isFinite(Date.parse(v.at))) {
  return { ok: false, error: "event.at must be an ISO timestamp string" };
}

Date.parse rolls day-overflow dates over instead of rejecting them, so an event stamped 2026-02-30T10:00:00Z is accepted and stored verbatim, while every consumer (pruneEvents retention at events.ts:96-105, filterEvents newest-first ordering) parses it as 2026-03-02 — a different day than the record claims. The same validator gates both ingestion paths: EventsStore.append (events.ts:243) and the on-load filter isStreamEvent (events.ts:88-89, 187-191), so an impossible-date record also survives reload instead of being quarantined as malformed.

Sibling boundaries were hardened for this exact failure mode: the DNS register (#2199, PR #4835, plus #4858 for the JSON path) and the charges rows (#3458) all refuse impossible dates via isIsoDate. The event stream — the fleet's own audit trail, persisted to the events file with 30-day / 5000-record retention — was left on the lenient check.

Verified this run (laptop probe, origin/main 1e3763f)

Probe /tmp/probe-events.ts on the laptop (log /tmp/probe-scan21.log), importing validateStreamEvent and pruneEvents from current main:

2026-02-30            ok= true  stored_at=2026-02-30            parsed=2026-03-02T00:00:00.000Z
2026-02-30T10:00:00Z  ok= true  stored_at=2026-02-30T10:00:00Z  parsed=2026-03-02T10:00:00.000Z
2026-04-31T10:00:00Z  ok= true  stored_at=2026-04-31T10:00:00Z  parsed=2026-05-01T10:00:00.000Z
2026-13-01T00:00:00Z  ok= false event.at must be an ISO timestamp string
2024-02-29T10:00:00Z  ok= true  (valid leap date, control)
2026-01-15T10:00:00Z  ok= true  (control)
2026-01-01T25:00:00Z  ok= false (hour overflow already refused by Date.parse)
PRUNE_KEPT: 1  (the 2026-02-30 event is retained, treated as March 2)

So month-13 and hour-25 are refused only because Date.parse happens to return NaN for them; day-overflow within a real month — the case parseIsoDate was written for — sails through.

Why it matters

Stream events are the fleet's shared audit/activity record: a producer (or a hand-edited / corrupt events file) that stamps an impossible date silently shifts the event to a different real day for retention, ordering, and filtering, and the stored string disagrees with every parsed view of it. The boundary's own contract and the shared primitive both say this input must be refused at ingest; every comparable aggregator boundary already does.

Acceptance criteria

  • validateStreamEvent refuses an at that is shape-valid ISO but not a real calendar date/time (e.g. 2026-02-30, 2026-02-30T10:00:00Z, 2026-04-31T10:00:00Z), using the shared parseIsoDate/isIsoDate primitive from validate.ts rather than a new local parser.
  • Real timestamps still validate, including leap dates (2024-02-29T10:00:00Z), offsets, and fractional seconds — the shapes the existing tests in pm-stack/aggregator/test/events.test.ts and orchestration.test.ts exercise.
  • On load, an impossible-date record in the events file is treated as malformed by the existing isStreamEvent path (quarantine/drop behavior unchanged otherwise).
  • New tests in pm-stack/aggregator/test/events.test.ts cover the impossible-date refusals and the leap-date control; existing events/orchestration tests stay green.
  • Related site to check while in the area (separate change is fine): isSavedView in pm-stack/aggregator/src/views.ts:100-102 uses the same Date.parse-only pattern for createdAt/updatedAt.

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 B, slug new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b run 2026-11-03 07:28 CST; filing executed by the action-outbox-processor (outbox entry OB-2026-11-03-096) after the creator run's filing attempt hit a GitHub secondary rate limit (403, 2026-11-03T13:28:26Z)
    • Found via: filed this run
    • Task: Fix validateStreamEvent in pm-stack/aggregator so impossible calendar dates are refused at ingest via the shared parseIsoDate/isIsoDate primitive instead of rolling over via Date.parse.
    • Expectation: effort/s; done = impossible dates (e.g. 2026-02-30, 2026-04-31) refused on ingest and treated as malformed on load, real timestamps including leap dates still validate, new tests in pm-stack/aggregator/test/events.test.ts cover the refusals and the leap control.
    • Links: queue packet packet-5154-fix.json on repo-health-code-pilot (enqueued by the outbox processor)
  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 · 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