Skip to content

aggregator: roadmap Horizon dates are never validated — month 13 invents a Q5 lane and an 'undefined 2026' month, month 00 a Q0 lane, and ymdLabel throws on them #1628

Closed

Description

@dezren39

Summary

The aggregator's roadmap date parsing accepts impossible calendar dates. laneFor's date regex (pm-stack/aggregator/src/derive.ts:65, DATE = /^(\d{4})-(\d{2})-\d{2}/) and rowDate's identical regex (derive.ts:459, YMD) check only the shape of a Horizon cell — two digits for the month — never that the month/day exist. A Grist "Program roadmap" Horizon typo such as 2026-13-15 or 2026-00-15 therefore does not fall into the "other / unscheduled" lane the code reserves for unrecognized horizons (laneFor, derive.ts:71-82); it is treated as a real date and produces nonsense lanes and labels, and one label helper throws on it:

  • laneFor("2026-13-15") → { key: "2026-Q5", label: "Q5 2026", order: 8109 } — a fifth quarter, sorting exactly where Q1 2027 sorts.
  • laneFor("2026-00-15") → { key: "2026-Q0", label: "Q0 2026", order: 8104 }.
  • monthTimeline on a 2026-13-15 row → lane { key: "2026-13", label: "undefined 2026" } — the literal text "undefined 2026" is what the month-grain roadmap page renders (render.ts:1514 calls timelineTable(monthTimeline(rows), …); monthLabel, derive.ts:504-507, indexes MONTH_NAMES[m - 1] unchecked).
  • ymdLabel("2026-13-15") throws TypeError: undefined is not an object (evaluating 'MONTH_NAMES[d.getUTCMonth()].slice') (derive.ts:499-502) — an invalid Date yields NaN month/day indexes. Any present or future caller that labels a row-derived date (rather than a generated lane day) crashes on this input.
  • ymdLabel("2026-02-30") silently relabels the impossible date as "Mon Mar 2" (JS Date roll-over), and roadmapDayGrain counts a 2026-02-30 / 2026-13-15 row as "dated, outside this window" (outside = 1, coarse = 0) instead of as an unparseable/coarse row, so the roadmap's own accounting misstates it.

Verified (this run, on main be7cf62)

Probe ON THE LAPTOP (Bun 1.4.2, driving the real derive.ts — sha256 312ac0fb… identical in both clones; probe + log in the goal's hidden_files/probe-2026-10-28-laneB-horizondate/, run twice with identical results):

CTRL laneFor 2026-10-24: {"key":"2026-Q4","label":"Q4 2026","order":8108}
CTRL laneFor Q1 2027:    {"key":"2027-Q1","label":"Q1 2027","order":8109}
CTRL monthLabel 2026-10: October 2026
CTRL ymdLabel 2026-10-24: Sat Oct 24
BUG laneFor 2026-13-15:  {"key":"2026-Q5","label":"Q5 2026","order":8109}
BUG laneFor 2026-00-15:  {"key":"2026-Q0","label":"Q0 2026","order":8104}
BUG monthLabel 2026-13:  undefined 2026
BUG monthLabel 2026-00:  undefined 2026
BUG rowDate 2026-13-15:  2026-13-15        (accepted as a date)
BUG rowDate 2026-02-30:  2026-02-30        (accepted as a date)
BUG ymdLabel 2026-02-30: Mon Mar 2         (silently a different date)
BUG ymdLabel 2026-13-15: TypeError … MONTH_NAMES[d.getUTCMonth()].slice (throws)
BUG monthTimeline lanes for 2026-13-15: [{"key":"2026-13","label":"undefined 2026","order":24325}]
BUG dayGrain 2026-02-30: outside=1 coarse=0 laned=0

Existing tests cover only valid horizons (test/render.test.ts:127-129: Q1 2027, 2026-10-24, Ongoing).

Reachability: Horizon is a free-text Grist cell, edited by humans and fleet jobs; the roadmap views are rendered from whatever it contains. One mistyped month (2026-13-… for a December/January slip, or a 00 from a spreadsheet export) is enough to put a phantom Q5/Q0 lane and an "undefined 2026" heading on the live roadmap, mis-sort the row into a neighbouring quarter, and count it as a real dated row outside the day-grain window.

Why it matters

The roadmap is the Program view's backbone: quarter/month/day grains all derive from these same helpers, so one bad cell corrupts every grain differently (phantom quarter, undefined month heading, miscounted day window) instead of degrading once, visibly, into the existing "other/coarse" bucket. The helpers' own contract already anticipates unrecognized horizons — laneFor sorts "anything else" after every quarter — the date branch simply never validates that its digits form a date, so the fallback never fires for the most likely typo class.

Acceptance criteria

  • A Horizon whose digits do not form a real calendar date (month 00/13, day 00, Feb 30, Apr 31, …) is treated as unrecognized: laneFor returns the "other" lane for it and rowDate returns null, so the row lands in the coarse/other bucket in every grain.
  • No roadmap surface can render the literal undefined as a lane/month label, and ymdLabel/monthLabel cannot throw or silently relabel an impossible date (defensive behaviour for direct callers is pinned by a test).
  • Regression tests cover at least: 2026-13-15, 2026-00-15, 2026-02-30 through laneFor, rowDate, monthTimeline, and roadmapDayGrain (counted coarse, not "outside"), plus the existing valid controls (2026-10-24 → Q4 2026, a real Feb 29 on a leap year).
  • Existing aggregator test suites still pass.

Activity

  1. developing-today commented on Oct 28, 2026

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

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-28 03:50 CDT
    • Found via: filed this run
    • Task: Triage the roadmap Horizon date-validation defect: laneFor/rowDate accept impossible dates (Q5/Q0 lanes, 'undefined 2026' month label, ymdLabel throws, day-grain miscounts them as outside-window).
    • Expectation: effort/s — done looks like impossible dates falling back to the other/coarse bucket in every grain, with regression tests for 2026-13-15 / 2026-00-15 / 2026-02-30.
    • Links: probe + log in the goal's hidden_files/probe-2026-10-28-laneB-horizondate/ (laptop, real derive.ts, sha256 312ac0fb identical both clones)
  4. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 11 remaining items

  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added 2 commits that reference this issue on Oct 28, 2026
  8. dezren39 commented on Oct 28, 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/sSmall: expected under 2 hours 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