Skip to content

aggregator(actions): validateNewAction accepts impossible calendar dates — a snooze until 2026-02-30 is stored verbatim and Date.parse reads it as Mar 2, so the card stays snoozed ~2 extra days #3393

Open

Description

@dezren39

Summary

validateNewAction (pm-stack/aggregator/src/actions.ts) is the only validation gate for dashboard actions written through the actions API. For the two date fields it checks nothing but Number.isNaN(Date.parse(x)):

  • payload.until — actions.ts:229-232 (payload.until must be an ISO date/time)
  • payload.proposalUpdatedAt — actions.ts:292-296 (same pattern)

V8's Date.parse does not refuse impossible calendar days — it normalizes them. Verified on the laptop (node v24 / bun, same engine family as the aggregator runtime):

Date.parse("2026-02-30")            = 1772409600000 = 2026-03-02T00:00:00.000Z
Date.parse("2026-02-29")            = finite (2026-03-01; 2026 is not a leap year)
Date.parse("2026-04-31T00:00:00Z")  = finite (2026-05-01)
Date.parse("2026-13-01")            = NaN (only the month range is checked)

So the check catches a bad month and garbage, but an impossible day passes, and the string is stored verbatim (payload.until = until, actions.ts:231).

Verified repro (this run)

Laptop probe ~/git/fleet/probe-2026-11-01-laneA-actionuntil/probe.ts (bun, importing the real validateNewAction from src/actions.ts and cardState from src/card-state.ts; laptop clone ~/git/code at main e6577a96, all five involved files md5-identical to origin/main):

validate until=2026-02-28 -> OK (stored as "2026-02-28")
validate until=2026-02-29 -> OK (stored as "2026-02-29")   ← 2026 has no Feb 29
validate until=2026-02-30 -> OK (stored as "2026-02-30")   ← impossible day
validate until=2026-04-31 -> OK (stored as "2026-04-31")   ← April has 30 days
validate until=2026-13-01 -> ERR payload.until must be an ISO date/time
validate until=not-a-date -> ERR payload.until must be an ISO date/time

cardState at 2026-03-01T12:00Z with until=2026-02-28 -> suppressed: null     (control: snooze over)
cardState at 2026-03-01T12:00Z with until=2026-02-30 -> suppressed: "snoozed" (still hidden on Mar 1)

The consumer side is cardState (src/card-state.ts:75): Date.parse(snooze.payload.until) > now decides suppression, re-parsing the stored string with the same normalizing parser — so the impossible date silently becomes Mar 2 there. A snooze written "until Feb 30" (or a UI/typo producing Feb 29 in a non-leap year) keeps the card hidden ~2 days longer than the chosen horizon, the write returned success, and the stored record names a day that never exists, so no later reader can recover the intent. proposalUpdatedAt has the same shape: an impossible stamp is accepted and stored, and ordering/freshness comparisons against it compare against the normalized instant, not the written one.

Why it matters

Acceptance criteria

  • validateNewAction refuses a payload.until or payload.proposalUpdatedAt whose calendar day does not exist (e.g. 2026-02-29 in 2026, 2026-02-30, 2026-04-31), with the existing "must be an ISO date/time" error; real dates (incl. 2024-02-29) still pass, as do full ISO datetimes for real days.
  • The validation uses a real-calendar check consistent with validate.ts (parseIsoDate semantics for the date part), not Date.parse alone.
  • Tests: actions validation tests gain impossible-day cases for both fields (refused) plus leap-year control (accepted); existing actions/card-state tests still pass.
  • No change to cardState comparison semantics for valid stored dates; already-stored impossible strings are out of scope (note in the fix if a defensive read-side guard is added anyway).

Filed by new-issue-creator-code-a (lane A), run 2026-11-01. Evidence probe: ~/git/fleet/probe-2026-11-01-laneA-actionuntil/ on the laptop.

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    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
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: cron job new-issue-creator-code-a, run 2026-11-01 01:05 CST
    • Found via: filed this run
    • Task: Make validateNewAction refuse impossible calendar dates (2026-02-30, 2026-04-31, non-leap Feb 29) for payload.until and payload.proposalUpdatedAt, using a real-calendar check consistent with validate.ts parseIsoDate instead of Date.parse alone.
    • Expectation: effort/s — done when the acceptance criteria on this issue pass, including validation tests for impossible days on both fields plus a leap-year control.
    • Links: evidence in the issue body; laptop probe at ~/git/fleet/probe-2026-11-01-laneA-actionuntil/probe.ts (real validateNewAction + cardState at main e6577a9: impossible dates accepted verbatim, card still snoozed on Mar 1 with until=2026-02-30; control 2026-02-28 not snoozed).
  4. added
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 90 remaining items

  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 4, 2026
  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    user/escalatedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · blocked (escalated to Drew)

    • Agent: queue worker (repo-worker-code), OpenChamber scheduled lane c8b9355e-319a-4640-8096-7ad19480d151
    • Session / ID: queue task 6024 (small_fix for this issue)
    • Found via: queue claim (attempt 21; the task has cycled park/handoff without progress)
    • Blocked on: PR validateNewAction: refuse impossible calendar dates for until/proposalUpdatedAt #4466 is authored and assigned to Drew (dezren39) — a Drew-owned PR is a hard stop for queue workers, and merging it is his decision.

    Verified 2026-11-04T14:16Z

    Why block instead of another 30-minute park: this task has been claimed 21 times and re-parked each time for the same Drew-owned PR, which is a queue churn loop, not progress. Nothing a worker may do remains.

    Unblock condition: Drew merges or closes PR #4466, or asks for changes. Then re-run the removal/merge checks and complete.

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 convenientqueue/blockedQueue or user escalation statesource/creatorFiled by the new-issue-creator jobstatus/blockedQueue or user escalation statetask/triageQueue packet kind: verify, reproduce, and route this itemuser/escalatedQueue or user escalation statework/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions