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:
gsxdsm
2026-07-31 00:03:02 -07:00
committed by GitHub
parent cf6062c524
commit 72f5f8e51a
6 changed files with 33 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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