fix(tasks): refuse a cycle start that is not a real date

A corrupt import is stored as it came, and "2026-13-01" passed the
pattern, then threw in periodOf on every render. "2026-02-30" rolled into
March instead and moved the cycle. The schema now round-trips the date,
so either is kept on disk and left off screen like any unreadable task.
Found by the schema-guardian review.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-16 05:37:36 +02:00
co-authored by Claude Opus 5.5
parent 2477b0a8a7
commit 8d0b45f83a
2 changed files with 19 additions and 1 deletions
+12 -1
View File
@@ -17,7 +17,18 @@ import { DAY } from "./time.ts";
/** ISO weekday: Monday is 1, Sunday is 7. */
export const WeekdaySchema = z.number().int().min(1).max(7);
const DayKey = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
/**
* A real calendar date. The round trip refuses "2026-13-01", which would throw
* in `periodOf`, and "2026-02-30", which would roll into March and move the
* cycle — a corrupt import is stored verbatim, so the schema is the guard.
*/
const DayKey = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.refine((s) => {
const ms = Date.parse(`${s}T00:00:00Z`);
return Number.isFinite(ms) && new Date(ms).toISOString().slice(0, 10) === s;
});
export const PeriodSchema = z.union([
z.object({ unit: z.literal("week"), startsOn: WeekdaySchema }),
+7
View File
@@ -124,4 +124,11 @@ describe("periodOf: cycles", () => {
expect(PeriodSchema.safeParse(cycle(367, "2026-01-01")).success).toBe(false);
expect(PeriodSchema.safeParse(cycle(14, "16 Oct")).success).toBe(false);
});
test("a start that is not a real date is refused, not rolled over or thrown on later", () => {
// A corrupt import is stored verbatim; the schema is what keeps it off screen.
expect(PeriodSchema.safeParse(cycle(14, "2026-13-01")).success).toBe(false);
expect(PeriodSchema.safeParse(cycle(14, "2026-02-30")).success).toBe(false);
expect(PeriodSchema.safeParse(cycle(14, "2028-02-29")).success).toBe(true);
});
});