`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>
144 lines
6.2 KiB
JavaScript
144 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-30-19:20:
|
|
Ratchet the population of call sites that do not pass a resolved-lane argument. See
|
|
scripts/lib/lane-wiring-census.mjs for why this is a census with a baseline and not a hard guard.
|
|
|
|
Usage:
|
|
node scripts/check-lane-wiring.mjs # fail if any file's count ROSE
|
|
node scripts/check-lane-wiring.mjs --update-baseline # re-record (downward moves only)
|
|
*/
|
|
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join, relative } from "node:path";
|
|
import { findLaneAcceptingFunctions, findUnwiredCallSites } from "./lib/lane-wiring-census.mjs";
|
|
|
|
const ROOT = process.cwd();
|
|
const BASELINE = join(ROOT, "scripts/lib/lane-wiring-baseline.json");
|
|
/*
|
|
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.
|
|
|
|
`unwired-lane-parameter-guard.test.ts` scans exactly six roots including these two, and its note
|
|
records why: an unwired `completeColumnsByTaskId` sat on `main` unreported because the glasses plugin
|
|
was not in the list. Plugins hold real lane logic — they resolve workflow IRs, filter by column, and
|
|
decide what "finished" means — and `dashboard/app` is where the board is actually rendered.
|
|
|
|
The extension is TWO changes, and either alone still misses most of it: those trees are overwhelmingly
|
|
`.tsx`, which the file filter below excluded, so adding the roots without the extension would have
|
|
scanned a handful of files and reported a reassuring near-zero.
|
|
|
|
Measured on the widened scan, re-measured after rebasing onto main: 15 further call sites, none of
|
|
which any gate could see before. They are recorded in the baseline rather than fixed here — they span
|
|
three other batches — and audited in the PR that widened this, because baselining a site nobody looked
|
|
at is how a ratchet becomes decoration.
|
|
|
|
FNXC:WorkflowLifecycleColumns 2026-07-30-23:55 (#2978 review — coderabbitai, "correct the audited
|
|
call-site count"): the note said 9 and the review said 10, from the pre-rebase baseline. BOTH are now
|
|
wrong: main gained sites in these very trees while the PR sat, so the widening currently uncovers 15.
|
|
A hand-written count beside a generated baseline dates the moment it is written — this one is stamped
|
|
so the next reader knows to re-measure rather than trust it.
|
|
*/
|
|
const ROOTS = [
|
|
"packages/core/src",
|
|
"packages/engine/src",
|
|
"packages/dashboard/src",
|
|
"packages/dashboard/app",
|
|
"packages/cli/src",
|
|
"plugins",
|
|
];
|
|
|
|
function sources(dir, out = []) {
|
|
for (const entry of readdirSync(dir)) {
|
|
const p = join(dir, entry);
|
|
if (statSync(p).isDirectory()) {
|
|
if (entry === "__tests__" || entry === "node_modules" || entry === "dist") continue;
|
|
sources(p, out);
|
|
} else if (
|
|
(entry.endsWith(".ts") || entry.endsWith(".tsx"))
|
|
&& !entry.endsWith(".d.ts")
|
|
&& !entry.includes(".test.")
|
|
&& !entry.includes(".spec.")
|
|
) {
|
|
out.push(p);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-30-23:58 (#2978 review — coderabbitai, "fail closed when a
|
|
required root cannot be scanned"): AN UNREADABLE ROOT IS A GATE THAT LIES, NOT A GATE THAT PASSES.
|
|
|
|
`catch { return [] }` turned a missing or unreadable root into an empty file list, so a typo'd path or
|
|
a permissions failure produced a smaller scan and a confident `none added`. Every root here is part of
|
|
the coverage contract — `plugins` and `dashboard/app` were added BECAUSE they were unscanned — so
|
|
silently dropping one restores exactly the blindness this change exists to remove.
|
|
|
|
Fails closed with the root path in the message. A gate whose errors land on "nothing to report" is the
|
|
one failure mode a ratchet must not have.
|
|
*/
|
|
const files = ROOTS.flatMap((r) => {
|
|
try {
|
|
return sources(join(ROOT, r));
|
|
} catch (error) {
|
|
throw new Error(`[check-lane-wiring] required root "${r}" could not be scanned: ${error.message}`);
|
|
}
|
|
});
|
|
const accepting = findLaneAcceptingFunctions(files);
|
|
const unwired = findUnwiredCallSites(files, accepting);
|
|
|
|
const counts = {};
|
|
for (const hit of unwired) {
|
|
const key = relative(ROOT, hit.file);
|
|
counts[key] = (counts[key] ?? 0) + 1;
|
|
}
|
|
|
|
if (process.argv.includes("--update-baseline")) {
|
|
writeFileSync(BASELINE, `${JSON.stringify({ counts }, null, 2)}\n`);
|
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
console.log(`[check-lane-wiring] baseline written: ${total} unwired call site(s) in ${Object.keys(counts).length} file(s)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
let baseline = { counts: {} };
|
|
try { baseline = JSON.parse(readFileSync(BASELINE, "utf8")); } catch { /* first run */ }
|
|
|
|
const rose = [];
|
|
const fell = [];
|
|
for (const [file, count] of Object.entries(counts)) {
|
|
const allowed = baseline.counts[file] ?? 0;
|
|
if (count > allowed) rose.push(` ${file}: ${count} unwired now, baseline allows ${allowed}`);
|
|
else if (count < allowed) fell.push(` ${file}: ${allowed} -> ${count}`);
|
|
}
|
|
for (const [file, allowed] of Object.entries(baseline.counts)) {
|
|
if (!(file in counts) && allowed > 0) fell.push(` ${file}: ${allowed} -> 0`);
|
|
}
|
|
|
|
if (rose.length > 0) {
|
|
console.error("[check-lane-wiring] call sites not passing a resolved lane argument INCREASED:\n");
|
|
console.error(rose.join("\n"));
|
|
console.error(`
|
|
A function that accepts a lane answer was called without one. That is the shape behind #2956, #2963
|
|
and #2964: a fix adds an optional parameter, and a caller added later never passes it — so the callee
|
|
silently falls back to the legacy literal and the board's own lanes are ignored.
|
|
|
|
If the call site genuinely should not pass one (identity already proven by a stronger means, a
|
|
sentinel column, or a dead export), record it:
|
|
|
|
node scripts/check-lane-wiring.mjs --update-baseline
|
|
`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (fell.length > 0) {
|
|
console.log("[check-lane-wiring] unwired call sites decreased:\n");
|
|
console.log(fell.join("\n"));
|
|
console.log("\nRe-record the baseline in the same commit so the allowance cannot be regrown into:\n");
|
|
console.log(" node scripts/check-lane-wiring.mjs --update-baseline\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
console.log(`[check-lane-wiring] ${total} known unwired call site(s), none added.`);
|