FN-8640: add FNXC stamp anomaly advisory

Add a non-blocking census for implausible future-dated FNXC stamps.

- Classify tolerated future stamps by timezone plausibility and report notable anomalies.
- Add injectable gate seams and coverage for advisory, report, baseline, and discovery behavior.
- Document the advisory and preserve read-only check-mode baseline handling.

Files changed:
 docs/testing.md                                    |   4 +
 scripts/__tests__/check-fnxc-future-dates.test.mjs | 185 ++++++++
 scripts/check-fnxc-future-dates.mjs                | 495 +++++++++++----------
 3 files changed, 439 insertions(+), 245 deletions(-)

Fusion-Task-Id: FN-8640

Fusion-Task-Lineage: 7e57feb0-95e2-46b9-a1cf-9bd93c40d8e0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-31 19:03:08 -07:00
parent 69bc9fc7ab
commit dfe050e8d4
3 changed files with 459 additions and 265 deletions

View File

@@ -0,0 +1,185 @@
import test from "node:test";
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative } from "node:path";
import {
ROOTS,
classifyFutureStamp,
hoursAhead,
runGate,
walk,
} from "../check-fnxc-future-dates.mjs";
const NOW = new Date("2026-08-01T00:00:00.000Z");
function fixture(t, { files = {}, baseline = {}, baselineText } = {}) {
const root = mkdtempSync(join(tmpdir(), "fnxc-future-dates-"));
const baselinePath = join(root, "baseline.json");
for (const scanRoot of ROOTS) mkdirSync(join(root, scanRoot), { recursive: true });
for (const [file, source] of Object.entries(files)) {
const path = join(root, file);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, source);
}
if (baselineText !== undefined) writeFileSync(baselinePath, baselineText);
else writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`);
t.after(() => rmSync(root, { recursive: true, force: true }));
return { root, baselinePath };
}
function stamp(area, date, time = "12:00") {
return `/* FNXC:${area} ${date}-${time}: fixture */`;
}
function run(root, baselinePath, mode = "check") {
return runGate({ root, baselinePath, now: NOW, mode });
}
test("classifies future stamps at exact plausibility boundaries", () => {
const reference = new Date("2026-08-01T04:00:00.000Z");
assert.equal(classifyFutureStamp("2026-08-02", reference), "timezone-plausible"); // 20h
assert.equal(classifyFutureStamp("2026-08-02", new Date("2026-07-31T22:00:00.000Z")), "timezone-plausible"); // 26h
assert.equal(classifyFutureStamp("2026-08-02", new Date("2026-07-31T21:00:00.000Z")), "suspect"); // 27h
assert.equal(classifyFutureStamp("2026-08-03", NOW), "suspect"); // 48h
assert.equal(classifyFutureStamp("2026-08-04", new Date("2026-08-01T23:00:00.000Z")), "implausible"); // 49h
assert.equal(classifyFutureStamp("2026-10-19", NOW), "implausible");
assert.equal(hoursAhead("2026-10-19", NOW) / 24, 79);
assert.equal(classifyFutureStamp("2026-08-01", NOW), "past-or-today");
assert.equal(hoursAhead("not-a-date", NOW), null);
assert.equal(classifyFutureStamp("not-a-date", NOW), "past-or-today");
});
test("tolerated population gets a non-blocking advisory and preserves baseline bytes", (t) => {
const { root, baselinePath } = fixture(t, {
files: { "packages/source.ts": `${stamp("Plausible", "2026-08-02")}\n${stamp("Extreme", "2026-10-19")}` },
baseline: { "packages/source.ts": 2 },
});
const before = readFileSync(baselinePath, "utf8");
const result = run(root, baselinePath);
assert.equal(result.exitCode, 0);
assert.deepEqual(result.byBand, { timezonePlausible: 1, suspect: 0, implausible: 1 });
assert.equal(result.baselineWritten, false);
assert.equal(readFileSync(baselinePath, "utf8"), before);
assert.match(result.lines.join("\n"), /informational only; does not fail the build/);
const extremeOffender = ["packages/source.ts — ", ["FNXC", "Extreme 2026-10-19"].join(":"), " (79.0 days ahead)"].join("");
assert(result.lines.includes(` ${extremeOffender}`));
});
test("timezone-plausible-only and duplicate stamps retain their historical counts without an advisory", (t) => {
const duplicate = stamp("Duplicate", "2026-08-02");
const { root, baselinePath } = fixture(t, {
files: { "packages/source.ts": `${duplicate}\n${duplicate}` },
baseline: { "packages/source.ts": 2 },
});
const result = run(root, baselinePath);
assert.equal(result.exitCode, 0);
assert.equal(result.records.length, 2);
assert.equal(result.byBand.timezonePlausible, 2);
assert.equal(result.byBand.suspect, 0);
assert.equal(result.byBand.implausible, 0);
assert.doesNotMatch(result.lines.join("\n"), /advisory/);
});
test("check mode preserves new-stamp, impossible-time, tighten, update, and unavailable-baseline behavior", (t) => {
const newStamp = fixture(t, { files: { "packages/new.ts": stamp("New", "2026-08-02") }, baseline: {} });
const newBefore = readFileSync(newStamp.baselinePath, "utf8");
const newResult = run(newStamp.root, newStamp.baselinePath);
assert.equal(newResult.exitCode, 1);
assert.equal(newResult.baselineWritten, false);
assert.equal(readFileSync(newStamp.baselinePath, "utf8"), newBefore);
const impossible = fixture(t, { files: { "packages/bad.ts": stamp("BadTime", "2026-08-01", "25:61") }, baseline: {} });
const impossibleBefore = readFileSync(impossible.baselinePath, "utf8");
const impossibleResult = run(impossible.root, impossible.baselinePath);
assert.equal(impossibleResult.exitCode, 1);
assert(impossibleResult.lines.some((line) => line.includes([["FNXC", "BadTime 2026-08-01-25:61"].join(":")])));
assert.equal(readFileSync(impossible.baselinePath, "utf8"), impossibleBefore);
const tighten = fixture(t, { files: { "packages/low.ts": stamp("Low", "2026-08-02") }, baseline: { "packages/low.ts": 2 } });
const tightenBefore = readFileSync(tighten.baselinePath, "utf8");
const tightenResult = run(tighten.root, tighten.baselinePath);
assert.equal(tightenResult.exitCode, 0);
assert.equal(tightenResult.baselineWritten, false);
assert.equal(readFileSync(tighten.baselinePath, "utf8"), tightenBefore);
assert.match(tightenResult.lines.join("\n"), /CAN BE TIGHTENED/);
const update = fixture(t, { files: { "packages/update.ts": stamp("Update", "2026-08-02") }, baseline: {} });
const updateResult = run(update.root, update.baselinePath, "update");
assert.equal(updateResult.exitCode, 0);
assert.equal(updateResult.baselineWritten, true);
assert.deepEqual(JSON.parse(readFileSync(update.baselinePath, "utf8")), { "packages/update.ts": 1 });
const missing = fixture(t, { files: { "packages/missing.ts": stamp("Missing", "2026-08-02") } });
rmSync(missing.baselinePath);
const missingResult = run(missing.root, missing.baselinePath);
assert.equal(missingResult.exitCode, 1);
assert.equal(missingResult.baselineWritten, false);
assert.equal(existsSync(missing.baselinePath), false);
const malformed = fixture(t, { files: { "packages/malformed.ts": stamp("Malformed", "2026-08-02") }, baselineText: "not json" });
const malformedBefore = readFileSync(malformed.baselinePath, "utf8");
const malformedResult = run(malformed.root, malformed.baselinePath);
assert.equal(malformedResult.exitCode, 1);
assert.equal(malformedResult.baselineWritten, false);
assert.equal(readFileSync(malformed.baselinePath, "utf8"), malformedBefore);
});
test("report mode is a read-only census that bypasses all gate failures", (t) => {
const files = {
"packages/suspect.ts": stamp("SuspectArea", "2026-08-03"),
"docs/extreme.md": stamp("ExtremeArea", "2026-10-19"),
};
const { root, baselinePath } = fixture(t, { files, baseline: {} });
const before = readFileSync(baselinePath, "utf8");
assert.equal(run(root, baselinePath).exitCode, 1);
const report = run(root, baselinePath, "report");
const output = report.lines.join("\n");
assert.equal(report.exitCode, 0);
assert.equal(report.baselineWritten, false);
assert.equal(readFileSync(baselinePath, "utf8"), before);
assert.match(output, /packages\/suspect\.ts[\s\S]*FNXC:SuspectArea/);
assert.match(output, /docs\/extreme\.md[\s\S]*FNXC:ExtremeArea/);
assert.match(output, /FNXC:SuspectArea: 1/);
assert.match(output, /FNXC:ExtremeArea: 1/);
assert.match(output, /band totals: timezone-plausible=0, suspect=1, implausible=1/);
const missing = fixture(t, { files, baseline: {} });
rmSync(missing.baselinePath);
const missingReport = run(missing.root, missing.baselinePath, "report");
assert.equal(missingReport.exitCode, 0);
assert.equal(missingReport.baselineWritten, false);
assert.match(missingReport.lines.join("\n"), /baseline unavailable — census only/);
assert.equal(existsSync(missing.baselinePath), false);
const malformed = fixture(t, { files: { "packages/time.ts": stamp("Clock", "2026-08-01", "24:00") }, baselineText: "broken" });
const malformedBefore = readFileSync(malformed.baselinePath, "utf8");
const malformedReport = run(malformed.root, malformed.baselinePath, "report");
assert.equal(malformedReport.exitCode, 0);
assert.equal(malformedReport.baselineWritten, false);
assert.match(malformedReport.lines.join("\n"), /baseline unavailable — census only/);
assert.match(malformedReport.lines.join("\n"), /impossible clock times/);
assert.equal(readFileSync(malformed.baselinePath, "utf8"), malformedBefore);
});
test("discovery remains the ROOTS-scoped walk and excludes files outside it", (t) => {
const { root, baselinePath } = fixture(t, {
files: {
"packages/schema.sql": stamp("Sql", "2026-10-19"),
"packages/code.ts": stamp("Typescript", "2026-10-19"),
"docs/guide.md": stamp("Docs", "2026-10-19"),
"packages/ignored.txt": stamp("IgnoredExtension", "2026-10-19"),
"outside.ts": stamp("Outside", "2026-10-19"),
},
baseline: { "packages/schema.sql": 1, "packages/code.ts": 1, "docs/guide.md": 1 },
});
const expected = ROOTS.flatMap((scanRoot) => [...walk(join(root, scanRoot))]
.map((path) => relative(root, path).split("\\").join("/"))).sort();
const result = run(root, baselinePath);
assert.equal(result.exitCode, 0);
assert.deepEqual(result.scannedFiles, expected);
assert(!result.scannedFiles.includes("outside.ts"));
assert(!result.records.some((record) => record.file === "outside.ts"));
assert.deepEqual(new Set(result.records.map((record) => record.file)), new Set(["packages/schema.sql", "packages/code.ts", "docs/guide.md"]));
});

View File

@@ -1,71 +1,74 @@
/*
FNXC:FnxcStampHygiene 2026-07-30-23:55:
FNXC STAMPS DATED IN THE FUTURE, FROZEN AT TODAY'S POPULATION.
AGENTS.md requires every FNXC comment to carry a `yyyy-MM-dd-hh:mm` stamp, and nothing checks it. The
only feedback loop is a reviewer noticing, and on 2026-07-30 alone reviewers caught FOUR future-dated
stamps across separate PRs (#2843, #2852, #2856, #2892). Every one was hand-written with nothing to
verify against.
A stamp dated after the change was written is not cosmetic. These comments are the project's record of
WHY code exists, and the census, the solutions docs and several review conventions read them
chronologically — "recorded 2026-07-31" next to a 2026-07-30 commit makes the ordering wrong for
exactly the reader the comment is for.
WHY A BASELINE RATCHET AND NOT A HARD FAIL. 84 source files already carry a future stamp, the furthest
nearly three months out. A gate that fails on all of them is unmergeable and would be turned off, and
mass-editing 84 files to satisfy a new check is churn nobody asked for. So the population is frozen:
a NEW future-dated stamp fails, an existing one does not, and a count that DROPS also fails so a fixed
file cannot leave a slot the surface silently regrows into. Same shape as the SQL column-literal gate.
WHY "FUTURE" AND NOT "MATCHES THE COMMIT DATE". A stamp legitimately predates its commit — work
written Monday and landed Wednesday is normal and correct. Only a date that has not happened yet is
unambiguously wrong, so that is the whole rule; it catches every case a reviewer has caught so far
without inventing a stricter one nobody follows.
FNXC:FnxcStampHygiene 2026-08-01-01:17:
The baseline ratchet answers whether the future-dated population grew, not whether an individual
stamp is plausible. That lets a 79-day-out stamp remain tolerated indefinitely. This advisory makes
that distinction visible without failing: 111 existing stamps belong to other authors, and a bulk
rewrite would create the churn this gate is meant to avoid.
*/
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join, relative, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const ROOTS = ["packages", "scripts", "docs"];
export const ROOTS = ["packages", "scripts", "docs"];
const BASELINE = join(REPO, "scripts", "lib", "fnxc-future-dates-baseline.json");
/* Build output and vendored bundles are generated; their stamps are copies of the source ones. */
const SKIP_DIRS = new Set(["node_modules", "dist", ".gate-bundle", "coverage", "build", ".next"]);
/*
FNXC:FnxcStampHygiene 2026-07-31-03:40 (#2941 review): HYPHENS ARE PART OF THE REQUIRED FORM.
AGENTS.md specifies `FNXC:Area-of-product`, and the first matcher accepted only `[A-Za-z0-9_]+` — so
every hyphenated area, i.e. the documented spelling, was skipped entirely. The gate was blind to the
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.
FNXC:FnxcStampHygiene 2026-08-01-01:17:
Hyphens are part of the required `FNXC:Area-of-product` form, so this one shared matcher remains the
source of both ratchet and advisory records. A second pattern would let the two reports disagree.
*/
const STAMP_TIME = /FNXC:[A-Za-z0-9_-]+\s+\d{4}-\d{2}-\d{2}-(\d{2}):(\d{2})/g;
export const STAMP = /FNXC:([A-Za-z0-9_-]+)\s+(\d{4}-\d{2}-\d{2})/g;
export 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. */
/** The stamp an author should paste, in the project's `yyyy-MM-dd-HH:mm` form, in UTC. */
function nowStamp() {
const d = new Date();
const p = (n) => String(n).padStart(2, "0");
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}-${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
/*
FNXC:FnxcStampHygiene 2026-08-01-01:17:
26 hours is wider than the UTC+14 maximum offset, so a stamp inside a day-and-a-bit can be an
author-local date and needs no action.
*/
export const TIMEZONE_PLAUSIBLE_HOURS = 26;
/*
FNXC:FnxcStampHygiene 2026-08-01-01:17:
48 hours is the deliberately conservative headline threshold: nothing inside two days is called out
loudly. The 26–48 hour suspect band still counts the source measurement's >26h population rather
than silently dropping it.
*/
export const IMPLAUSIBLE_HOURS = 48;
/** Returns a valid UTC-midnight date for a YYYY-MM-DD value, or null without throwing. */
function parseStampDate(stampDate) {
if (typeof stampDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(stampDate)) return null;
const parsed = new Date(`${stampDate}T00:00:00.000Z`);
return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== stampDate ? null : parsed;
}
function impossibleClockTimes(source) {
/** Returns the UTC hours a date-only stamp is ahead of the reference, or null when it is invalid. */
export function hoursAhead(stampDate, referenceDate) {
const stamp = parseStampDate(stampDate);
const reference = referenceDate instanceof Date ? referenceDate : new Date(referenceDate);
if (!stamp || Number.isNaN(reference.getTime())) return null;
return (stamp.getTime() - reference.getTime()) / (60 * 60 * 1000);
}
/** Classifies every parseable stamp; invalid dates are non-anomalies rather than scanner failures. */
export function classifyFutureStamp(stampDate, referenceDate) {
const hours = hoursAhead(stampDate, referenceDate);
if (hours === null || hours <= 0) return "past-or-today";
if (hours <= TIMEZONE_PLAUSIBLE_HOURS) return "timezone-plausible";
if (hours <= IMPLAUSIBLE_HOURS) return "suspect";
return "implausible";
}
/** The stamp an author should paste, in the project's `yyyy-MM-dd-HH:mm` form, in UTC. */
export function nowStamp(now = new Date()) {
const p = (n) => String(n).padStart(2, "0");
return `${now.getUTCFullYear()}-${p(now.getUTCMonth() + 1)}-${p(now.getUTCDate())}-${p(now.getUTCHours())}:${p(now.getUTCMinutes())}`;
}
export function impossibleClockTimes(source) {
let bad = 0;
STAMP_TIME.lastIndex = 0;
for (const match of source.matchAll(STAMP_TIME)) {
@@ -74,237 +77,239 @@ function impossibleClockTimes(source) {
return bad;
}
function* walk(dir) {
export function* walk(dir) {
for (const entry of readdirSync(dir)) {
if (SKIP_DIRS.has(entry)) continue;
const full = join(dir, entry);
if (statSync(full).isDirectory()) yield* walk(full);
/* `.js`/`.cjs` too: FNXC comments live in plain-JS scripts as well, and omitting them let a
future-dated stamp land unseen in exactly the files this repo writes tooling in. */
/*
FNXC:FnxcStampHygiene 2026-07-30-00:00 (#2953 follow-up): EVERY FILE TYPE THAT CARRIES A STAMP.
The filter listed the types stamps were EXPECTED in, not the ones they OCCUR in, so the gate was
blind wherever the convention had spread on its own. `.sql` was the costly omission: migrations
carry a stamp recording when a schema change landed, they are the files where a wrong date
misleads most, and one of them held a stamp dated nearly three months out. `.css` had drifted
furthest by volume (1023 stamps across 123 files, from the dashboard CSS split). A gate whose
coverage is a guess about where authors write comments will always trail the authors.
*/
else if (/\.(tsx?|m?js|cjs|md|sql|css|html|ya?ml|json|sh)$/.test(full)) yield full;
}
}
/*
Today in the repo's LOCAL calendar; a stamp for today is fine, tomorrow is not.
/** Preserves the existing later-of-local-and-UTC calendar bound for an injectable clock. */
export function todayFor(now = new Date()) {
const localToday = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
].join("-");
const utcToday = now.toISOString().slice(0, 10);
return { localToday, utcToday, today: localToday > utcToday ? localToday : utcToday };
}
FNXC:FnxcStampHygiene 2026-07-31-03:40 (#2941 review): `toISOString()` is UTC, so for anyone west of
Greenwich it rolls the date forward for part of each day — a stamp written correctly at 5pm in
California read as "tomorrow" and failed the gate. Authors write the local date, so the comparison
has to use the local one.
FNXC:FnxcStampHygiene 2026-08-01-00:10 (five reds in two hours — LOCAL alone is not enough either):
The fleet writes stamps from MANY machines and this gate evaluates them on ONE. #2941 fixed the
author-west-of-the-runner case; the mirror case is an author EAST of it, and that is what broke main
five times in two hours. Measured: three direct-to-main commits landed at 16:12/16:32/16:40 PDT —
23:12/23:32/23:40 UTC on the 31st — carrying stamps of 2026-08-01-00:20/00:50/01:05. Those are
neither the runner's local date nor UTC; they are the AUTHOR's local date in a UTC+1 container. The
gate, running in PDT, called every one of them "tomorrow" and reddened main for every other lane.
So "future" cannot mean "after the runner's calendar". It means after EVERY calendar a correct
author could plausibly be writing from, which is bounded below by the runner's local date and above
by UTC (or vice versa west of Greenwich). Comparing against the LATER of the two accepts both
honest cases and still catches a genuinely invented date — the 2026-08-06 stamp in scheduler.ts,
six days out, fails under this rule exactly as it did before.
This preserves #2941's fix rather than reverting it: west of Greenwich the local date is the earlier
of the pair, so a 5pm-in-California stamp still passes.
*/
const now = new Date();
const localToday = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
].join("-");
const utcToday = now.toISOString().slice(0, 10);
/** The later of the two — a stamp is future only if it is ahead of both. */
const today = localToday > utcToday ? localToday : utcToday;
function scan() {
/**
* Scans exactly the historical roots. `root` is a repository root, not a directory to broaden.
*/
export function scan({ root = REPO, referenceDate = new Date(`${todayFor().today}T00:00:00.000Z`) } = {}) {
const counts = {};
for (const root of ROOTS) {
let base;
try { base = statSync(join(REPO, root)); } catch { continue; }
if (!base.isDirectory()) continue;
for (const file of walk(join(REPO, root))) {
const records = [];
const impossibleRecords = [];
const scannedFiles = [];
for (const scanRoot of ROOTS) {
const directory = join(root, scanRoot);
try { if (!statSync(directory).isDirectory()) continue; } catch { continue; }
for (const file of walk(directory)) {
const fileKey = relative(root, file).split("\\").join("/");
scannedFiles.push(fileKey);
const source = readFileSync(file, "utf8");
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;
STAMP.lastIndex = 0;
for (const match of source.matchAll(STAMP)) {
const [area, stamp] = [match[1], match[2]];
const hours = hoursAhead(stamp, referenceDate);
const band = classifyFutureStamp(stamp, referenceDate);
if (hours !== null && hours > 0) {
hits += 1;
records.push({ file: fileKey, area, stamp, hoursAhead: hours, daysAhead: hours / 24, band });
}
}
STAMP_TIME.lastIndex = 0;
for (const match of source.matchAll(STAMP_TIME)) {
if (Number(match[1]) > 23 || Number(match[2]) > 59) {
hits += 1;
impossibleRecords.push({ file: fileKey, stamp: match[0] });
}
}
if (hits > 0) counts[fileKey] = hits;
}
}
return counts;
scannedFiles.sort();
return { counts, records, impossibleRecords, scannedFiles };
}
const found = scan();
function byBand(records) {
return {
timezonePlausible: records.filter((record) => record.band === "timezone-plausible").length,
suspect: records.filter((record) => record.band === "suspect").length,
implausible: records.filter((record) => record.band === "implausible").length,
};
}
const updateBaseline = process.argv.includes("--update-baseline");
if (updateBaseline) {
writeFileSync(BASELINE, `${JSON.stringify(found, null, 2)}\n`);
function advisoryLines(records, bands) {
if (bands.suspect + bands.implausible === 0) return [];
const lines = [
"[check-fnxc-future-dates] advisory (informational only; does not fail the build):",
` future-dated total: ${records.length}`,
` timezone-plausible (<=26h; explainable by author timezone — no action needed): ${bands.timezonePlausible}`,
` suspect (>26h, <=48h): ${bands.suspect}`,
` implausible (>48h): ${bands.implausible}`,
];
const offenders = records.filter((record) => record.band === "implausible")
.sort((a, b) => b.daysAhead - a.daysAhead || a.file.localeCompare(b.file)).slice(0, 10);
if (offenders.length > 0) {
lines.push(" worst implausible offenders:");
for (const record of offenders) {
lines.push(` ${record.file} — FNXC:${record.area} ${record.stamp} (${record.daysAhead.toFixed(1)} days ahead)`);
}
}
return lines;
}
function reportLines(records, impossibleRecords, bands) {
const lines = ["[check-fnxc-future-dates] anomaly census (read-only; informational only):"];
const anomalies = records.filter((record) => record.band === "suspect" || record.band === "implausible")
.sort((a, b) => a.file.localeCompare(b.file) || b.daysAhead - a.daysAhead);
let currentFile;
for (const record of anomalies) {
if (record.file !== currentFile) {
currentFile = record.file;
lines.push(` ${currentFile}`);
}
lines.push(` FNXC:${record.area} ${record.stamp} — ${record.band} (${record.daysAhead.toFixed(1)} days ahead)`);
}
if (anomalies.length === 0) lines.push(" no suspect or implausible stamps");
if (impossibleRecords.length > 0) {
lines.push(" impossible clock times (informational):");
for (const record of impossibleRecords) lines.push(` ${record.file} — ${record.stamp}`);
}
const areas = new Map();
for (const record of anomalies) areas.set(record.area, (areas.get(record.area) ?? 0) + 1);
lines.push(" anomaly counts by FNXC area:");
for (const [area, count] of [...areas].sort(([a], [b]) => a.localeCompare(b))) lines.push(` FNXC:${area}: ${count}`);
lines.push(` band totals: timezone-plausible=${bands.timezonePlausible}, suspect=${bands.suspect}, implausible=${bands.implausible}`);
return lines;
}
function readBaseline(baselinePath) {
let baseline;
try { baseline = JSON.parse(readFileSync(baselinePath, "utf8")); } catch {
return { error: "[check-fnxc-future-dates] missing or malformed baseline; run with --update-baseline" };
}
if (baseline === null || typeof baseline !== "object" || Array.isArray(baseline)) {
return { error: "[check-fnxc-future-dates] baseline must be a JSON object of file -> count" };
}
for (const [file, count] of Object.entries(baseline)) {
if (!Number.isSafeInteger(count) || count < 0) {
return { error: `[check-fnxc-future-dates] baseline entry "${file}" must be a non-negative safe integer, got ${JSON.stringify(count)}` };
}
}
return { baseline };
}
function writeBaseline(baselinePath, baseline) {
writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`);
}
/**
* Runs the historical ratchet plus its non-blocking plausibility advisory.
*
* FNXC:FnxcStampHygiene 2026-08-01-01:17:
* The injected root, baseline, and clock make the real gate testable without changing discovery or
* the later-of-local-and-UTC bound. Report mode is intentionally read-only so operators can census a
* tolerated population without banking, tightening, or failing it.
*/
export function runGate({ root = REPO, baselinePath = join(root, "scripts", "lib", "fnxc-future-dates-baseline.json"), now = new Date(), mode = "check" } = {}) {
const { today } = todayFor(now);
const referenceDate = new Date(`${today}T00:00:00.000Z`);
const { counts: found, records, impossibleRecords, scannedFiles } = scan({ root, referenceDate });
const bands = byBand(records);
const result = {
exitCode: 0,
futureTotal: records.length,
byBand: bands,
records,
scannedFiles,
baselineWritten: false,
lines: [],
};
if (mode === "report") {
if (readBaseline(baselinePath).error) result.lines.push("[check-fnxc-future-dates] baseline unavailable — census only");
result.lines.push(...reportLines(records, impossibleRecords, bands));
return result;
}
if (mode === "update") {
writeBaseline(baselinePath, found);
result.baselineWritten = true;
const total = Object.values(found).reduce((a, b) => a + b, 0);
result.lines.push(`[check-fnxc-future-dates] baseline written: ${total} stamp(s) in ${Object.keys(found).length} file(s)`);
return result;
}
const loaded = readBaseline(baselinePath);
if (loaded.error) {
result.exitCode = 1;
result.lines.push(loaded.error);
return result;
}
const baseline = loaded.baseline;
const problems = [];
const offendingFiles = [];
for (const [file, count] of Object.entries(found)) {
const allowed = baseline[file] ?? 0;
if (count > allowed) {
offendingFiles.push(file);
problems.push(` ${file}: ${count} future-dated FNXC stamp(s), baseline allows ${allowed}`);
}
}
const tightened = [];
for (const [file, allowed] of Object.entries(baseline)) {
const count = found[file] ?? 0;
if (count < allowed) tightened.push(` ${file}: ${allowed} -> ${count}`);
}
/*
FNXC:FnxcStampHygiene 2026-08-01-01:17:
Check mode must remain read-only: an aging stamp can lower the census without an author action.
Report the available tightening, but require the explicit update mode to record it.
*/
if (tightened.length > 0) {
result.lines.push(
`[check-fnxc-future-dates] baseline CAN BE TIGHTENED for ${tightened.length} file(s):`,
...tightened.sort(),
" run `pnpm check:fnxc-future-dates --update-baseline` to record it (one commit, one author).",
);
}
if (problems.length > 0) {
result.exitCode = 1;
result.lines.push("", "[check-fnxc-future-dates] FNXC stamp population changed:", "", ...problems.sort());
for (const file of offendingFiles) {
const source = readFileSync(join(root, file), "utf8");
const bad = [];
STAMP.lastIndex = 0;
for (const match of source.matchAll(STAMP)) if (classifyFutureStamp(match[2], referenceDate) !== "past-or-today") bad.push(`${match[0]} (dated after today)`);
STAMP_TIME.lastIndex = 0;
for (const match of source.matchAll(STAMP_TIME)) if (Number(match[1]) > 23 || Number(match[2]) > 59) bad.push(`${match[0]} (impossible clock time)`);
if (bad.length > 0) result.lines.push("", ` ${file}`, ...[...new Set(bad)].map((line) => ` ${line}`));
}
result.lines.push(
`\nA stamp dated after today (${today}) records the change as happening in the future, which makes`,
"the FNXC record — the project's why-does-this-exist trail — read out of order. An hour above 23",
"or a minute above 59 is not a real time at all.",
"Use the current date and a real clock time. If a count went DOWN, re-record the baseline in the",
`same commit.\nCurrent UTC stamp to use: ${nowStamp(now)}\n`,
);
return result;
}
const total = Object.values(found).reduce((a, b) => a + b, 0);
console.log(`[check-fnxc-future-dates] baseline written: ${total} stamp(s) in ${Object.keys(found).length} file(s)`);
process.exit(0);
result.lines.push(`[check-fnxc-future-dates] ${total} known future-dated stamp(s), none added.`);
result.lines.push(...advisoryLines(records, bands));
return result;
}
/*
FNXC:FnxcStampHygiene 2026-07-31-03:45 (#2941 review): VALIDATE THE SHAPE, not just the JSON.
The first version caught only a parse error, so `null`, an array, or a negative/NaN count reached the
comparison and either crashed with a stack trace or — worse — compared as `undefined` and silently
allowed everything. A ratchet whose baseline can be quietly neutered by a bad edit is not a ratchet.
*/
let baseline;
try {
baseline = JSON.parse(readFileSync(BASELINE, "utf8"));
} catch {
console.error("[check-fnxc-future-dates] missing or malformed baseline; run with --update-baseline");
process.exit(1);
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const mode = process.argv.includes("--report-anomalies") ? "report" : process.argv.includes("--update-baseline") ? "update" : "check";
const result = runGate({ mode });
for (const line of result.lines) console.log(line);
process.exit(result.exitCode);
}
if (baseline === null || typeof baseline !== "object" || Array.isArray(baseline)) {
console.error("[check-fnxc-future-dates] baseline must be a JSON object of file -> count");
process.exit(1);
}
for (const [file, count] of Object.entries(baseline)) {
/*
FNXC:FnxcStampHygiene 2026-07-30-23:55 (#2941 review): SAFE integer, not just integer.
`Number.isInteger(9007199254740992)` is true, but that value is past 2^53-1 where JavaScript stops
distinguishing adjacent integers — so it compares greater than any count this scanner can produce and
silently disables the ratchet for that file. A validator whose purpose is "this baseline cannot be
neutered by a bad edit" has to reject the value that neuters it most completely.
*/
if (!Number.isSafeInteger(count) || count < 0) {
console.error(`[check-fnxc-future-dates] baseline entry "${file}" must be a non-negative safe integer, got ${JSON.stringify(count)}`);
process.exit(1);
}
}
const problems = [];
const offendingFiles = [];
for (const [file, count] of Object.entries(found)) {
const allowed = baseline[file] ?? 0;
if (count > allowed) offendingFiles.push(file);
if (count > allowed) problems.push(` ${file}: ${count} future-dated FNXC stamp(s), baseline allows ${allowed}`);
}
/*
FNXC:FnxcStampHygiene 2026-07-30-23:20 (#2941 CI red — a ratchet whose own measurement moves with the
clock): A DROP TIGHTENS, IT DOES NOT FAIL.
I copied the drop-fails rule from the SQL ratchet without noticing that this population is not stable
the way that one is. "Is this stamp in the future" is answered against TODAY, so every date boundary
the runner crosses converts some future stamps into past ones and the count falls ON ITS OWN — no code
change involved. With drop-fails that guarantees a red gate on some later day, and it fired within
hours: the baseline was recorded at 2026-07-30 local while CI runs in UTC, already 2026-07-31.
Both sibling ratchets reached the same conclusion for the ordinary reason (the drop is rarely the
failing author's to fix). Here it is stronger still: nobody CAUSED the drop, so there is no author to
fix it. The ceiling follows the count down, says what it lowered, and exits 0; the RISE check — the
actual purpose, "no NEW future-dated stamp" — is untouched and still fails hard.
The rewritten baseline must be committed to take effect; in CI the write is discarded with the runner,
which is why the gate goes green rather than silently banking a stale allowance.
*/
const tightened = [];
for (const [file, allowed] of Object.entries(baseline)) {
const count = found[file] ?? 0;
if (count < allowed) tightened.push(` ${file}: ${allowed} -> ${count}`);
}
/*
FNXC:FnxcStampHygiene 2026-08-01-00:55 (a CHECK must not modify the tree it is checking):
This block used to rewrite the baseline on every plain run. The tightening itself is right — the
comment above explains why banking a stale allowance is worse — but performing it as a SIDE EFFECT of
checking handed every worker an identical uncommitted diff they had not written.
Measured cost: on 2026-07-31/08-01 nine PRs chased three defects in this gate's area, and two of them
(#3283, #3285, five minutes apart, `+0/-1` each) deleted the SAME baseline line. Neither author wrote
it; the gate wrote it, in both of their checkouts, and each reasonably committed what they found. I
also mis-attributed my own dirty tree to leftover work and retracted a measurement partly on that
basis.
So: still computed, still reported loudly, but only WRITTEN under --update-baseline. A plain run is
read-only and stays green — failing on a tightening would redden main every time a stamp simply ages
into the past, which is exactly why the auto-write existed.
*/
if (tightened.length > 0) {
console.log(`[check-fnxc-future-dates] baseline CAN BE TIGHTENED for ${tightened.length} file(s):`);
for (const line of tightened.sort()) console.log(line);
if (updateBaseline) {
for (const [file, allowed] of Object.entries(baseline)) {
const count = found[file] ?? 0;
if (count < allowed) { if (count === 0) delete baseline[file]; else baseline[file] = count; }
}
writeFileSync(BASELINE, `${JSON.stringify(baseline, null, 2)}\n`);
console.log("[check-fnxc-future-dates] baseline re-recorded.");
} else {
console.log(" run `pnpm check:fnxc-future-dates --update-baseline` to record it (one commit, one author).");
}
}
if (problems.length > 0) {
console.error("\n[check-fnxc-future-dates] FNXC stamp population changed:\n");
for (const line of problems.sort()) console.error(line);
/*
FNXC:FnxcStampHygiene 2026-07-31-07:45 (#3006 fixed the stamps; this fixes why they were hard to
find): NAME THE OFFENDING STAMP, AND WHICH RULE IT BROKE.
This gate counts TWO defects — a date after today, and an impossible clock time — but the failure
text only ever explained the first. Main went red on four `2026-07-30-26:10` stamps (hour 26) and
the message sent every reader to inspect `2026-07-30`, a perfectly valid past date. The gate had
detected the right thing and described a different one, so the natural conclusion was "the gate is
broken", not "the stamp is". Confirming otherwise took reproducing the regex by hand, getting zero,
and then instrumenting `scan()` to discover `hits += impossibleClockTimes(source)`.
A gate that misdescribes what it caught spends the reader's trust, which is worth more than the
one re-read of already-failing files that printing the real offenders costs.
*/
for (const file of offendingFiles) {
let source;
try { source = readFileSync(join(REPO, file), "utf8"); } catch { continue; }
const bad = [];
STAMP.lastIndex = 0;
for (const match of source.matchAll(STAMP)) if (match[1] > today) bad.push(`${match[0]} (dated after today)`);
STAMP_TIME.lastIndex = 0;
for (const match of source.matchAll(STAMP_TIME)) {
if (Number(match[1]) > 23 || Number(match[2]) > 59) bad.push(`${match[0]} (impossible clock time)`);
}
if (bad.length > 0) {
console.error(`\n ${file}`);
for (const line of [...new Set(bad)]) console.error(` ${line}`);
}
}
console.error(
`\nA stamp dated after today (${today}) records the change as happening in the future, which makes\n`
+ "the FNXC record — the project's why-does-this-exist trail — read out of order. An hour above 23\n"
+ "or a minute above 59 is not a real time at all.\n"
+ "Use the current date and a real clock time. If a count went DOWN, re-record the baseline in the\n"
+ "same commit.\n"
/*
FNXC:FnxcDateGate 2026-07-31-23:39:
PRINT THE STAMP TO USE, do not just say "use the current date".
Three separate commits landed a future-dated stamp in one evening, each turning this blocking gate
red on main. The offsets were 1-2 hours, not wrong dates — the shape of a clock or timezone
difference rather than carelessness, and telling that author to "use the current date" is telling
them to use the value they thought they already had.
Printing the exact UTC stamp makes the correction copy-paste instead of a second judgement call.
Cheap: one `date -u`-equivalent read on a path that has already failed.
*/
+ `Current UTC stamp to use: ${nowStamp()}\n`,
);
process.exit(1);
}
const total = Object.values(found).reduce((a, b) => a + b, 0);
console.log(`[check-fnxc-future-dates] ${total} known future-dated stamp(s), none added.`);