fix(recurring): reject calendar-impossible startDate, not just wrongly-shaped (C1 residual)
requireStartDateGranularity (store/actions.ts) and its import-path twin (export/json.ts) checked date SHAPE with a regex, not date VALIDITY, so "2026-09-31" (September has 30 days) passed both and threw later out of foldBalances instead of at the write boundary. isValidStartDate (RecurringSection.tsx) had the same hole for a day-shaped value under everyNMonths. Separately, monthOf's regex (domain/months.ts) was unanchored at the end, so "2026-091" was silently read as September 2026. Route day-shaped values through toDayOrdinal and month-shaped values through monthOf (now anchored to accept only "YYYY-MM" or "YYYY-MM-DD" in full, never a prefix) at all three write boundaries, converting whatever they throw into each file's own error style. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
405cf0bf2d
commit
042bc8597d
8 files changed
+183
-8
No files matched your search
@@ -23,8 +23,14 @@ function fromOrdinal(ordinal: number): MonthId {
|
||||
return format(Math.floor(ordinal / 12), (ordinal % 12) + 1);
|
||||
}
|
||||
|
||||
// Anchored at both ends: "YYYY-MM" or "YYYY-MM-DD" only, never a prefix of
|
||||
// something longer. Dual acceptance (with or without a day) is deliberate and
|
||||
// load-bearing — do not narrow it to one form. An unanchored match once let
|
||||
// "2026-091" through as September 2026, silently dropping the trailing "1".
|
||||
const MONTH_OF_DATE = /^(\d{4})-(\d{2})(?:-\d{2})?$/;
|
||||
|
||||
export function monthOf(date: IsoDate): MonthId {
|
||||
const match = /^(\d{4})-(\d{2})/.exec(date);
|
||||
const match = MONTH_OF_DATE.exec(date);
|
||||
if (!match) throw new Error(`Invalid IsoDate: ${date}`);
|
||||
const month = Number(match[2]);
|
||||
if (month < 1 || month > 12) throw new Error(`Invalid IsoDate: ${date}`);
|
||||
|
||||
Reference in new issue
Block a user