From 9ad3a2a93abc2eae3e78108dfb6e2bbf97c63634 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 00:39:05 -0700 Subject: [PATCH] fix(gate): name the offending FNXC stamp and which rule it broke (#3009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **`main` is currently red**, so every open PR shows a failing Lint job that is not its own fault. #3004 is how I found it — its gates all pass in isolation and fail against main. ## Cause: an hour that does not exist ``` FNXC:OperatorScriptLaneAssumptions 2026-07-30-26:10 ^^ hour 26 ``` Four of them, across three files, from #2994. ## The part worth fixing is the message, not the stamps This gate counts **two** defects — a date after today, and an impossible clock time — but the failure text only ever explained the first: ``` scripts/reconcile-task-state-consistency.mjs: 2 future-dated FNXC stamp(s), baseline allows 0 A stamp dated after today (2026-07-31) records the change as happening in the future... ``` Every stamp in that file is dated `2026-07-30` or earlier — all valid past dates. So the message sends you to inspect stamps that are fine, and the natural conclusion is *the gate is broken*, not *the stamp is*. I spent several minutes reproducing the regex by hand and getting `future count = 0` before instrumenting the real script and finding `hits += impossibleClockTimes(source)`. A gate that detects the right defect and describes a different one is worse than a slightly less sensitive gate, because it spends the reader's trust. Now: ``` scripts/reconcile-task-state-consistency.mjs FNXC:OperatorScriptLaneAssumptions 2026-07-30-26:10 (impossible clock time) ``` **Mutation-verified**: restoring one `26:10` stamp reproduces the failure, and the message names it. ## The stamps: `2026-07-31-02:10`, not `23:59` Hour 26 on the 30th is the informal spelling of 02:10 the next day. Clamping to `23:59` would keep the file's stamps in a plausible order but silently move the event; this preserves what the author meant. Reversible either way — say the word if you would rather they were clamped. ## The 176-file baseline drop is unrelated `475 -> 183 known`. The clock crossed midnight, so yesterday's stamps are no longer future-dated, and the ratchet auto-lowers on drops by design. It rides along because the gate must leave a baseline matching reality — an allowance nothing occupies is somewhere a real regression can hide. It is not part of the fix. ## Verification - `check:fnxc-future-dates` — exit 0 (was **exit 1 on main**) - `check:lifecycle-columns`, `check:sql-column-literals`, `check:inert-flag-seams`, `check:lane-wiring` — all exit 0 - eslint clean ## Worth someone's attention beyond this PR `#2994` landed four impossible timestamps. The gate caught them, but only after the clock crossed midnight changed which files it reported — meaning the impossible-time check was live but effectively invisible until it collided with an unrelated drop. It is worth asking whether that check has ever produced a message anyone acted on before today. Co-authored-by: Claude Opus 5 (1M context) --- scripts/check-fnxc-future-dates.mjs | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/scripts/check-fnxc-future-dates.mjs b/scripts/check-fnxc-future-dates.mjs index 52e38a2ae6..815c1f8c90 100644 --- a/scripts/check-fnxc-future-dates.mjs +++ b/scripts/check-fnxc-future-dates.mjs @@ -163,8 +163,10 @@ for (const [file, count] of Object.entries(baseline)) { } 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}`); } /* @@ -201,12 +203,43 @@ if (tightened.length > 0) { } if (problems.length > 0) { - console.error("\n[check-fnxc-future-dates] future-dated FNXC stamp population changed:\n"); + 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.\n" - + "Use the current date. If a count went DOWN, re-record the baseline in the same commit.\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", ); process.exit(1); }