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:
@@ -73,6 +73,10 @@ Public `@fusion/core` exports consumed by runtime tools should include a literal
|
||||
<!-- FNXC:EngineProcessRules 2026-06-26-03:58: FN-7056 adds a focused static guard for user-configured command paths. Keep the protected-path registry in the test file, not as a whole-file execSync ban, because engine git plumbing still has legitimate deterministic execSync uses. -->
|
||||
`packages/engine/src/__tests__/user-configured-command-no-execsync.test.ts` guards user-configured command execution helpers against accidental `execSync` usage or dropped async bounds. Its registry covers verification helpers, `fn_run_verification`, executor configured-command execution, merger post-merge script execution, routine command execution, and the native/bubblewrap/sandbox-exec sandbox backends. Each protected slice must keep the appropriate bounded async safeguard (`timeout`/`timeoutMs`, `maxBuffer`, or `maxLifetimeMs`). The test intentionally slices named function bodies instead of scanning whole files; deterministic git-plumbing `execSync` in merger/self-healing/already-merged/integration/worktree-prune paths and the executor git ancestry check are explicitly out of scope.
|
||||
|
||||
## FNXC future-date advisory
|
||||
|
||||
<!-- FNXC:FnxcStampHygiene 2026-08-01-01:30: The baseline ratchet prevents new future stamps but cannot say whether a tolerated stamp is physically plausible. Keep the advisory non-blocking so existing authors' records are visible without forcing a mass rewrite. -->
|
||||
`pnpm check:fnxc-future-dates` continues to block new future-dated stamps while reporting a non-blocking advisory for the existing population. It scans only `packages/`, `scripts/`, and `docs/`, classifying each future stamp as **timezone-plausible** (≤26h ahead), **suspect** (>26h through 48h), or **implausible** (>48h). Run `pnpm check:fnxc-future-dates -- --report-anomalies` for a read-only full census grouped by file and area; report mode never writes the baseline or fails the build.
|
||||
|
||||
## Lifecycle-column census (report-only)
|
||||
|
||||
|
||||
185
scripts/__tests__/check-fnxc-future-dates.test.mjs
Normal file
185
scripts/__tests__/check-fnxc-future-dates.test.mjs
Normal 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"]));
|
||||
});
|
||||
@@ -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.`);
|
||||
|
||||
Reference in New Issue
Block a user