Repository navigation
Description
Problem
normalizeResetAt in pm-stack/aggregator/src/quota-gather-map.ts (lines 44–57, at main 67f97516c) normalizes a provider reset stamp to ISO. Its numeric branch is guarded (finite, > 0, valid Date), but its string branch is Date.parse-only:
if (typeof v === "string" && v !== "") {
const t = Date.parse(v);
return Number.isNaN(t) ? null : new Date(t).toISOString();
}Date.parse rolls impossible calendar dates over to a different real date and accepts non-ISO formats, so a drifted/malformed provider stamp is silently converted into a wrong-but-plausible resetsAt instead of null. Garbage strings do return null — the gate is inconsistent within itself: "not-a-date" → null, but "2026-02-30" → a fabricated March 2 reset time.
This contradicts the aggregator's own date contract: isIsoDate/parseIsoDate (pm-stack/aggregator/src/validate.ts:116) rejects impossible dates by round-trip, and the feed gates built on it (isQuotaFreshness feed.ts:502, isQuotaWindow feed.ts:474) already refuse such values.
Evidence (verified this run, laptop bun probe at origin/main 67f97516c)
Probe: /tmp/probe-resetat.ts importing normalizeResetAt from the worktree copy of quota-gather-map.ts; output in /tmp/probe-resetat.log on the laptop:
| input | result |
|---|---|
"2026-02-30" |
"2026-03-02T00:00:00.000Z" — impossible date rolled over |
"2026-04-31" |
"2026-05-01T00:00:00.000Z" — impossible date rolled over |
"2026-02-29" (2026 is not a leap year) |
"2026-03-01T00:00:00.000Z" — rolled over |
"March 2, 2026" |
"2026-03-02T06:00:00.000Z" — non-ISO format accepted (and TZ-shifted) |
"2026-03-02" |
"2026-03-02T00:00:00.000Z" — control OK |
"2026-03-02T00:00:00.000Z" |
unchanged — control OK |
"2024-02-29" (leap year) |
"2024-02-29T00:00:00.000Z" — control OK |
"not-a-date" |
null — garbage already refused |
Call sites that inherit the flaw (all feed the quota-current contract's resetsAt, which drives reset-time display and reset-soon guidance):
pm-stack/aggregator/src/clinepass-adapter.ts:106— ClinePass windowresetsAtpm-stack/aggregator/src/quota-gather-map.ts:279— Meta file-export windowspm-stack/aggregator/src/quota-gather-map.ts:345—entryFromResultprovider windowspm-stack/aggregator/src/quota-gather-map.ts:618— Turso billing period end
Related sites in the same file worth checking in the same pass (not the primary scope): the credentialExpiresAt string branch in entryFromResult (Date.parse + toISOString, ~line 340) and the Turso period start/end Date.parse pair (~lines 621–622), from which windowSeconds is derived.
Why it matters
resetsAt is operator-facing: it tells Drew when a quota pool resets and feeds reset-soon choices. A provider payload that drifts (or a hand-edited file export) carrying an impossible date currently produces a confident, wrong reset time a day or two off, instead of the null the same function already returns for unparseable strings. Every sibling date gate in the aggregator was hardened to the isIsoDate contract for exactly this failure mode; this shared normalizer — the one four call sites funnel through — was missed.
Acceptance criteria
normalizeResetAtreturnsnullfor impossible calendar dates (2026-02-30,2026-04-31,2026-02-29) and for non-ISO date strings ("March 2, 2026").- Valid inputs are unchanged: ISO date/datetime strings normalize as today,
2024-02-29passes, epoch-second and epoch-millisecond numbers behave as today, garbage strings still returnnull. - Regression tests added to
pm-stack/aggregator/test/quota-gather-map.test.tscovering the refused and accepted cases above. - Existing aggregator test suites (at minimum quota-gather-map, clinepass-adapter, feed) still pass.
- The related
credentialExpiresAt/ Turso periodDate.parsesites in the same file are either fixed to the same contract or explicitly called out in the PR as follow-ups.
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
fix(aggregator): gate normalizeResetAt strings on isIsoDate contract (#…
dezren39 commentedon Nov 4, 2026
🤖 Agent note · escalated
This item carries queue/claimed + work/assigned + status/in-progress, but nothing has moved in ~13h against a 2h (effort/s) expectation. Adding user/review per the lifecycle protocol §6 escalation rule. Note: the repo-queue Supabase backend is in an outage window today (code#5370), so the monitor is NOT requeueing from GitHub alone this run — if the holding session is done, the next monitor run after DB recovery will reset the labels and requeue; if you are still holding this, please checkpoint now and the escalation clears.