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
@@ -32,6 +32,12 @@ export function isDayGranular(date: IsoDate): boolean {
|
||||
* `addDays` directly. Reuses the domain's own parsers rather than a second,
|
||||
* possibly-diverging regex.
|
||||
*
|
||||
* A day-SHAPED value is routed through `toDayOrdinal` even under
|
||||
* `everyNMonths`: `monthOf` only extracts the month and never checks the day,
|
||||
* so without this a calendar-impossible date like "2026-09-31" (September has
|
||||
* 30 days) would pass here and could still be stored, only to throw three
|
||||
* months later out of the fold.
|
||||
*
|
||||
* Exported for `RecurringSection.test.ts` — this is the one thing standing
|
||||
* between the "Starts" field and C2 (an empty or malformed value bricking
|
||||
* every route the fold touches), so it is tested directly rather than only
|
||||
@@ -39,7 +45,7 @@ export function isDayGranular(date: IsoDate): boolean {
|
||||
*/
|
||||
export function isValidStartDate(value: string, kind: Recurrence["kind"]): boolean {
|
||||
try {
|
||||
if (kind === "everyNMonths") monthOf(value);
|
||||
if (kind === "everyNMonths" && !isDayGranular(value)) monthOf(value);
|
||||
else toDayOrdinal(value);
|
||||
return true;
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user