Repository navigation
Description
pm-stack(aggregator): freshness() ages impossible and non-ISO measuredAt stamps as fresh — Date.parse rolls 2026-02-30 to Mar 2 and accepts 'March 2, 2026'
Evidence
freshness(measuredAt, cadenceDays, now) in pm-stack/aggregator/src/derive.ts:941 (callers: hub/service card rendering in src/render.ts:2234, :2300, :2513, :2526) parses the stamp with raw Date.parse in both branches:
- Date-only branch (
derive.ts:951):Date.parse(\${measuredAt}T00:00:00Z`)` — an impossible calendar date rolls to the next real day instead of failing. - Datetime branch (
derive.ts:962):Date.parse(measuredAt)— any string V8/Bun can parse, ISO or not, is accepted.
Every sibling aggregator gate that consumes an external stamp uses the real-calendar ISO check isIsoDate / parseIsoDate (pm-stack/aggregator/src/validate.ts:66 / :116), which re-checks the components so impossible dates return null. freshness() never applies it.
Verified this run with a laptop bun probe importing freshness from origin/main dda1def97, now = 2026-03-01T12:00:00Z, cadenceDays = 7 (/tmp/probe-fresh.ts on the laptop):
| measuredAt | result | expected |
|---|---|---|
2026-02-30 (impossible) |
{ stale: false, ageDays: 0 } — rolled to Mar 2, reported as read today |
invalid stamp → { stale: true, ageDays: null } |
March 2, 2026 (non-ISO) |
{ stale: false, ageDays: 0 } |
invalid stamp → { stale: true, ageDays: null } |
2026-04-31 (impossible) |
{ stale: true, ageDays: null } — only via the future-tolerance bound, by accident of the chosen now |
invalid |
2026-02-27 (control) |
{ stale: false, ageDays: 2 } |
correct |
not-a-date (control) |
{ stale: true, ageDays: null } |
correct |
2026-03-01 (control) |
{ stale: false, ageDays: 0 } |
correct |
Why it matters
freshness() decides whether hub and service cards render as stale. An extractor or upstream doc that emits an impossible or non-ISO measuredAt is currently reported as a read taken today (ageDays 0, not stale) — the exact failure the aggregator's ingest contract (#2576 family) exists to prevent: malformed stamps must read as unknown age, never as maximally fresh. The future-tolerance bound (#1568) catches only impossible dates that roll more than a day past now; one that rolls to today or tomorrow passes as fresh.
Acceptance criteria
freshness()returns{ stale: true, ageDays: null }for a present-but-invalid stamp: impossible dates (2026-02-30,2026-04-31) in both the date-only and datetime branches, and non-ISO strings (March 2, 2026,03/02/2026).- Valid stamps are unchanged: date-only stamps still age by Chicago calendar day (the aggregator: freshness() ages date-only stamps by the UTC day, not Chicago — a read dated today reports '1d ago' (and stale) from 19:00 Chicago onward #1629 behavior), datetime stamps by elapsed whole days, and the aggregator: a future-dated read is 'today' forever until its date passes — derive freshness() has no upper bound, so one skewed measuredAt pins a hub card fresh for weeks #1568 future-tolerance bound still applies to real future dates.
- New tests cover impossible date-only, impossible datetime, non-ISO, and the existing valid controls;
bun testinpm-stack/aggregatorpasses.
Dedup: distinct from #1568 (closed; future bound only), #2206 (closed; saas-extract freshestDate), #4987 (closed; parseQuotaCurrent measuredAt). No open issue covers freshness() in derive.ts.
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · state: filed
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
queue/8771-5102-freshness-isodate.dezren39 commentedon Nov 3, 2026
🤖 Agent note · parked
queue/8771-5102-freshness-isodate(worktree ~/git/fleet/8771 on the laptop). Parked, not waiting: after ~5 minutes the Actions "basic" check had not started and Codex had not posted (fleet Actions backlog).fix(aggregator): gate freshness() measuredAt with parseIsoDate (#5102) (
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
awaiting_cicheckpointparseIsoDategate infreshness()plus the pm-stack(aggregator): freshness() ages impossible and non-ISO measuredAt stamps as fresh — Date.parse rolls 2026-02-30 to Mar 2 and accepts 'March 2, 2026' #5102 regression tests; Codex review completed 2026-11-03T20:07Z on the head commit with no findings; targeted tests stood at 50 pass / 0 fail (full-suite failures pre-existing on clean main). Squash-merged as75d3fe5e8edce136b5019a1f0e4f50631f33ec6b; this issue auto-closed on merge. Task 8771 completed with outcomefixed_merged.measuredAtstamps now age as honestly unknown ({ stale: true, ageDays: null }).queue/8771-5102-freshness-isodate