* test(#3696): failing-first coverage for the last_activity invariant and --strict exit status * fix(#3696): report the last_activity invariant and make the verdict gateable with --strict * fix(#3696): agree with the real reader on last_activity, and stop reporting structure as truncation * chore(#3696): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -33,6 +33,10 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { collectSection } from './markdown-sectionizer.cjs';
|
||||
// #3696: the calendar-validity predicate moved to the STATE.md document module
|
||||
// so `state validate` can assert the same `last_activity` invariant this reader
|
||||
// already enforces (ADR-227). Two copies would let the two surfaces disagree
|
||||
// about whether a STATE.md is usable — which is the defect #3696 reports.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import ioMod = require('./io.cjs');
|
||||
const { output } = ioMod;
|
||||
@@ -229,27 +233,6 @@ const ISO_LEADING_RE =
|
||||
*/
|
||||
const ZONE_DESIGNATOR_RE = /^\s*[A-Z]{2,5}(?![A-Za-z])/;
|
||||
|
||||
/**
|
||||
* True only when y/m/d name a date that actually exists on the calendar.
|
||||
*
|
||||
* `Date.parse` validates shape but not value: it rolls an out-of-range day
|
||||
* FORWARD rather than rejecting it (`2026-02-30` -> `2026-03-02`,
|
||||
* `2026-04-31` -> `2026-05-01`). Shape-only validation would therefore
|
||||
* propagate a different, wrong instant instead of failing safe — precisely
|
||||
* what ADR-227 ("validate shape AND value; on failure of either layer coerce
|
||||
* to the contract's safe default, never propagate") exists to prevent. A
|
||||
* round-trip through Date.UTC detects the rollover: any component the
|
||||
* constructor normalised comes back changed.
|
||||
*/
|
||||
function isRealCalendarDate(year: number, month: number, day: number): boolean {
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
||||
const probe = new Date(Date.UTC(year, month - 1, day));
|
||||
return (
|
||||
probe.getUTCFullYear() === year &&
|
||||
probe.getUTCMonth() === month - 1 &&
|
||||
probe.getUTCDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
function parseActivityTimestamp(raw: string | null): number | null {
|
||||
if (!raw) return null;
|
||||
@@ -260,7 +243,7 @@ function parseActivityTimestamp(raw: string | null): number | null {
|
||||
// Reject an impossible calendar date outright rather than letting
|
||||
// Date.parse substitute a rolled-forward one. null = "no activity signal",
|
||||
// the safe default staleActivity already fails open on.
|
||||
if (!isRealCalendarDate(Number(year), Number(month), Number(day))) return null;
|
||||
if (!stateDocument.isRealCalendarDate(Number(year), Number(month), Number(day))) return null;
|
||||
// The date is real, so stay as liberal as before (Postel): a whole-string
|
||||
// parse still wins when the engine can make sense of the value. Reading the
|
||||
// token first would silently DROP a trailing zone name -- "2026-06-08
|
||||
|
||||
Reference in New Issue
Block a user