fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed (#2995)
`check-fnxc-future-dates.mjs` validates the **date** portion of a stamp
and never looks at the clock time:
```js
const STAMP = /FNXC:[A-Za-z0-9_-]+\s+(\d{4}-\d{2}-\d{2})/g;
…
for (const match of source.matchAll(STAMP)) if (match[1] > today) hits += 1;
```
The capture stops before the hour, so a stamp may carry **any** `hh:mm`
and pass. Found while pre-flighting #2992, whose new comments read
`2026-07-30-25:30`.
## It is not one typo
Four stamps **already on `main`** carry a clock time that cannot exist:
```
packages/cli/src/__tests__/task-list-board-columns.test.ts:2 -24:40
packages/cli/src/commands/task.ts:29 -24:40
packages/cli/src/commands/task.ts:636 -24:40
scripts/check-lane-wiring.mjs:18 -24:00
```
Three separate authors, so this is the gate's blind spot rather than one
person's slip — and #2992 adds two more, which is how I noticed.
AGENTS.md specifies `yyyy-MM-dd-hh:mm`. The stamp's whole purpose is to
make the FNXC record a readable chronology of *why* code exists; a
timestamp that cannot exist quietly costs it that, and nothing was going
to catch it.
## The fix
Hours `00-23`, minutes `00-59`, counted per file **alongside** the
future-dated population rather than as a separate gate — same defect
class (a stamp that does not describe a real moment), and one ratchet is
cheaper to keep honest than two.
**Mutations, both directions:**
| stamp | result |
|---|---|
| `2026-07-30-25:00` | **flagged** |
| `2026-07-30-23:75` | **flagged** |
| clean tree | `475 known future-dated stamp(s), none added`, exit 0 |
## On the four existing stamps
Normalized by clamping the impossible hour to `23`, minutes preserved,
so relative ordering within each file survives. **That is a
normalization with a stated rule, not a claim about the true minute** —
`-24:40` most plausibly meant "just past midnight", but writing
`2026-07-31-00:40` would be future-dated against today's local calendar
and fail the very gate this PR extends. Clamping keeps every stamp real,
ordered, and non-future; the exact minute was already unrecoverable.
**Verified:** FNXC gate exit 0, lane-wiring gate exit 0,
`task-list-board-columns` 5/5, lint clean.
Comment-only changes to the CLI files (stamp text inside FNXC blocks),
so no behaviour change and no changeset.
Noted separately on #2992 so its two new stamps get corrected there
rather than landing and immediately failing this gate.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
FNXC:CliBoardVocabulary 2026-07-30-24:40:
|
||||
FNXC:CliBoardVocabulary 2026-07-30-23:40:
|
||||
THE INVARIANT: `fn task list` prints every card, whatever its board calls the lane.
|
||||
|
||||
`runTaskList` iterated the six-id `COLUMNS` constant and filtered `t.column === col`, so a card in a
|
||||
|
||||
@@ -26,7 +26,7 @@ function columnLabel(column: ColumnId): string {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:CliBoardVocabulary 2026-07-30-24:40:
|
||||
FNXC:CliBoardVocabulary 2026-07-30-23:40:
|
||||
The lanes `fn task list` prints, derived from the CARDS rather than from the legacy enum.
|
||||
|
||||
`runTaskList` iterated the six-id `COLUMNS` constant and filtered `t.column === col`, so a task in a
|
||||
@@ -633,7 +633,7 @@ export async function runTaskList(projectName?: string) {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:CliBoardVocabulary 2026-07-30-24:40:
|
||||
FNXC:CliBoardVocabulary 2026-07-30-23:40:
|
||||
Iterate the columns the BOARD has, not the legacy six — a renamed card was not printed AT ALL.
|
||||
|
||||
This loop ran `for (const col of COLUMNS)` and filtered `t.column === col`, so any task in a
|
||||
|
||||
@@ -119,7 +119,7 @@ test("treats soft-deleted blockers as missing and never plans for deleted depend
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:OperatorScriptLaneAssumptions 2026-07-30-25:30:
|
||||
FNXC:OperatorScriptLaneAssumptions 2026-07-30-23:30:
|
||||
THE INVARIANT: a board this script cannot reason about is REPORTED, never silently skipped.
|
||||
|
||||
Every lane test in the planner is a legacy id, and the candidate gate is `row.column !== "todo"`, so a
|
||||
|
||||
@@ -41,6 +41,32 @@ shape the rule actually prescribes, which is the worst possible subset to miss.
|
||||
*/
|
||||
const STAMP = /FNXC:[A-Za-z0-9_-]+\s+(\d{4}-\d{2}-\d{2})/g;
|
||||
|
||||
/*
|
||||
FNXC:FnxcStampHygiene 2026-07-30-21:40:
|
||||
THE HOUR WAS NEVER VALIDATED, so `2026-07-30-25:30` passed this gate.
|
||||
|
||||
`STAMP` captures only the date, and the future check compares that capture alone — a stamp could
|
||||
carry any `hh:mm` at all. Four stamps on `main` already read `-24:40` or `-24:00`, and a fifth
|
||||
`-25:30` arrived with the next PR. AGENTS.md specifies `yyyy-MM-dd-hh:mm`, where `hh` is a clock
|
||||
hour, and the whole point of the stamp is to make the FNXC record a readable chronology; a time that
|
||||
cannot exist quietly costs it that.
|
||||
|
||||
Counted per file alongside the future-dated population rather than as a separate gate, because it is
|
||||
the same defect class — a stamp that does not describe a real moment — and one ratchet is cheaper to
|
||||
keep honest than two.
|
||||
*/
|
||||
const STAMP_TIME = /FNXC:[A-Za-z0-9_-]+\s+\d{4}-\d{2}-\d{2}-(\d{2}):(\d{2})/g;
|
||||
|
||||
/** Hours 00-23, minutes 00-59. Returns the count of stamps whose clock time cannot exist. */
|
||||
function impossibleClockTimes(source) {
|
||||
let bad = 0;
|
||||
STAMP_TIME.lastIndex = 0;
|
||||
for (const match of source.matchAll(STAMP_TIME)) {
|
||||
if (Number(match[1]) > 23 || Number(match[2]) > 59) bad += 1;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
function* walk(dir) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (SKIP_DIRS.has(entry)) continue;
|
||||
@@ -87,6 +113,7 @@ function scan() {
|
||||
STAMP.lastIndex = 0;
|
||||
let hits = 0;
|
||||
for (const match of source.matchAll(STAMP)) if (match[1] > today) hits += 1;
|
||||
hits += impossibleClockTimes(source);
|
||||
if (hits > 0) counts[relative(REPO, file).split("\\").join("/")] = hits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { findLaneAcceptingFunctions, findUnwiredCallSites } from "./lib/lane-wir
|
||||
const ROOT = process.cwd();
|
||||
const BASELINE = join(ROOT, "scripts/lib/lane-wiring-baseline.json");
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-24:00:
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-23:00:
|
||||
`packages/dashboard/app` and `plugins` are scanned, and `.tsx` counts — their absence was the
|
||||
blind spot the OLDER guard already learned about and this one re-opened.
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function isTerminalColumn(column) {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:OperatorScriptLaneAssumptions 2026-07-30-25:30:
|
||||
FNXC:OperatorScriptLaneAssumptions 2026-07-30-23:30:
|
||||
Refuse to look healthy on a board this script cannot reason about.
|
||||
|
||||
Every lane test here is a legacy id: `isTerminalColumn` is done/archived, "active" is
|
||||
|
||||
Reference in New Issue
Block a user