Skip to content

fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed - #2995

Merged
gsxdsm merged 2 commits into
mainfrom
fix/fnxc-validate-stamp-hour
Jul 31, 2026
Merged

fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed#2995
gsxdsm merged 2 commits into
mainfrom
fix/fnxc-validate-stamp-hour

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

check-fnxc-future-dates.mjs validates the date portion of a stamp and never looks at the clock time:

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.

gsxdsm and others added 2 commits July 30, 2026 23:46
STAMP captures only the date and the future check compares that capture alone,
so a stamp could carry any hh:mm. Four stamps on main already read -24:40 or
-24:00 and a fifth -25:30 is in flight.

AGENTS.md specifies yyyy-MM-dd-hh:mm. The stamp exists to make the FNXC record a
readable chronology, and a time that cannot exist quietly costs it that.

Counted alongside the future-dated population rather than as a second gate: same
defect class, and one ratchet is cheaper to keep honest than two.

The four existing stamps are normalized by clamping the impossible hour to 23
with minutes preserved, so relative ordering within each file survives. That is
a normalization with a stated rule, not a claim about the true minute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#2992 merged while this branch was open, putting 2026-07-30-25:30 on main. The
extended gate flagged them in CI, which is the gate working -- it is the first
thing it has caught that was not already there when it was written.

Clamped to 23:30 by the same rule as the other four: hour to 23, minutes kept.

Baseline deliberately NOT re-recorded. It is timezone-dependent -- a UTC run
tightens 176 files relative to a UTC-7 run because stamps dated tomorrow are not
future there -- so committing either machine's copy would hand the other a red
gate. Main's committed baseline passes under both, verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gsxdsm
gsxdsm force-pushed the fix/fnxc-validate-stamp-hour branch from 97c4f4a to b7e25d0 Compare July 31, 2026 06:48
@gsxdsm

gsxdsm commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

CI went red, and the gate was right — it caught something that wasn't there when I opened this

scripts/recover-stale-blocked-by.mjs: 1 future-dated FNXC stamp(s), baseline allows 0
scripts/__tests__/recover-stale-blocked-by.test.mjs: 1 future-dated FNXC stamp(s), baseline allows 0

#2992 merged while this branch was open, putting 2026-07-30-25:30 on main. I had predicted on that PR that its stamps would fail once this landed; it happened in the opposite order, which is the better one — the gate's first catch is a stamp it was not written against.

Rebased onto current main and normalized both by the same rule as the other four (hour → 23, minutes kept). Nine stamps across five PRs are now real clock times.

The baseline is timezone-dependent, and I am deliberately not committing one

Worth recording, because it cost me the diagnosis and will cost the next person the same:

run result
local (UTC-7, today = 2026-07-30) 475 known, none added
UTC (today = 2026-07-31) "baseline TIGHTENED for 176 file(s)", 183 known

Same tree, same code — the date the run compares against decides whether a stamp dated tomorrow counts as future. So a baseline generated on my machine and committed would hand CI a different population, and vice versa. Committing either machine's copy is a way to make the gate wrong for everyone else.

Main's committed baseline passes under both — verified by running each with the file restored in between, so neither run's self-write contaminated the other — so this PR does not touch it. That is also why the earlier -24:40 stamps could not simply be re-dated to 2026-07-31-00:40: correct by the clock, future-dated in half the world's timezones, red gate for whoever runs it from the Americas.

Re-verified after the rebase: impossible-hour mutation still flagged, gate exit 0 under both TZ=UTC and local, lint clean.

That self-write behaviour is the same trait that let a stale baseline reach main earlier today. It is the single thing about this gate I would change next — writing on a drop while merely reporting on a rise means the file's contents depend on who ran it last and where they were sitting.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@gsxdsm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 324488b7-50ec-498c-a9ee-97e9d46ab227

📥 Commits

Reviewing files that changed from the base of the PR and between 65f4e85 and b7e25d0.

📒 Files selected for processing (6)
  • packages/cli/src/__tests__/task-list-board-columns.test.ts
  • packages/cli/src/commands/task.ts
  • scripts/__tests__/recover-stale-blocked-by.test.mjs
  • scripts/check-fnxc-future-dates.mjs
  • scripts/check-lane-wiring.mjs
  • scripts/recover-stale-blocked-by.mjs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gsxdsm
gsxdsm merged commit 72f5f8e into main Jul 31, 2026
6 checks passed
@gsxdsm
gsxdsm deleted the fix/fnxc-validate-stamp-hour branch July 31, 2026 07:03
gsxdsm added a commit that referenced this pull request Jul 31, 2026
#3006)

**`main` is currently red on the FNXC gate.**

```
$ node scripts/check-fnxc-future-dates.mjs   # on origin/main
  scripts/reconcile-task-state-consistency.mjs: 2 future-dated FNXC stamp(s), baseline allows 0
  scripts/lib/backend-db.mjs: 1
  scripts/__tests__/reconcile-task-state-consistency.test.mjs: 1
exit 1
```

#2994 carried four `2026-07-30-26:10` stamps. I flagged them on that PR
before it merged; #2995 (the hour check) landed first, so the merge
order turned the warning into a red gate rather than a red PR.

Clamped to `23:10` — same rule as the nine before it: hour to `23`,
minutes preserved, so ordering within each file survives. This is a
normalization with a stated rule, not a claim about the true minute.

**Verified:** FNXC gate exit 0, `reconcile-task-state-consistency` 8
pass / 0 fail. Comment-text only.

### Worth fixing at the source

Thirteen impossible-hour stamps across six PRs in two days, and the
hours climb — `24:40` → `25:30` → `26:10`. They are being written as a
continuing sequence past midnight rather than read off a clock, which is
a reasonable instinct and produces an invalid stamp every time.

The trap is that the honest spelling does not work either: a genuine
post-midnight stamp needs *tomorrow's* date, and the gate compares
against the **local** calendar — so `2026-07-31-00:40` written from
UTC-7 is future-dated and fails for a different reason. Clamping to
`23:xx` is currently the only spelling that satisfies both, which is not
obvious and is why this keeps recurring.

If it recurs again, the fix is probably in the error message rather than
more normalization PRs: the gate could name the valid range and the
timezone it compares against, so the next author sees the constraint at
the moment they hit it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 31, 2026
… on them

`main` is red on `check-fnxc-future-dates`. The cause is four stamps reading
`2026-07-30-26:10` — an hour that cannot exist.

They are exactly what #2995 taught this gate to catch. That PR landed the hour validation
(`00-23`) after #2999 had already merged these four, so the gate started reporting a defect
that was sitting there rather than one anybody introduced afterwards. The guard is working;
nothing had been checking before it.

Corrected by literal normalisation — 26:10 on the 30th IS 02:10 on the 31st — which keeps
the chronology the author was recording rather than flattening the stamps to an arbitrary
in-range hour. AGENTS.md specifies `yyyy-MM-dd-hh:mm`, and the stamp's whole purpose is a
readable why-does-this-exist trail, so the ordering is the part worth preserving.

THE BASELINE TIGHTENING RIDES ALONG, and it is a date rollover rather than anything anyone
did. Stamps written yesterday as `2026-07-31` were future then and were baselined as such;
today they are past, so 176 files ratchet to zero. The gate rewrites the file as a side
effect and exits 0, so leaving it uncommitted dirties the tree on every subsequent run for
everyone — which is why it belongs in this commit rather than a later one. Re-recording on
a decrease is the rule this gate and its siblings already state.

Worth knowing about the design: this churn recurs whenever a day boundary passes with
future-dated stamps in the baseline, and it shrinks only as people stop writing them —
which is the behaviour the gate exists to produce. 93 files still carry a non-zero
allowance, so the drain is not finished.

MEASURED
- gate red before, exit 0 after, and stable across two consecutive runs
- baseline -176/+25 entries, all date-rollover
- inert-seam, sql-literal, lane-wiring gates and the census: all green
- the reconciler's own suite: green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 31, 2026
… on them (#3010)

## `main` is currently red on `check-fnxc-future-dates`

Four stamps read `2026-07-30-26:10` — an hour that cannot exist.

They're exactly what #2995 taught this gate to catch. That PR landed the
hour validation (`00-23`) *after* #2999 had already merged these four,
so the gate started reporting a defect that was already sitting there
rather than one introduced afterwards. **The guard is working**; nothing
was checking before it.

```
scripts/lib/backend-db.mjs:41
scripts/reconcile-task-state-consistency.mjs:8, :51
scripts/__tests__/reconcile-task-state-consistency.test.mjs:109
```

Corrected by **literal normalisation** — 26:10 on the 30th *is* 02:10 on
the 31st — rather than flattening them to an arbitrary in-range hour.
AGENTS.md specifies `yyyy-MM-dd-hh:mm`, and the stamp exists to give a
readable why-does-this-exist trail, so the ordering is the part worth
preserving.

## The baseline tightening rides along, and it's a date rollover

Stamps written yesterday as `2026-07-31` were future *then* and were
baselined as such. Today they're past, so **176 files ratchet to zero**.
Nobody did anything.

The gate rewrites the baseline as a side effect and exits 0, so leaving
it uncommitted dirties the tree on every subsequent run **for everyone**
— which is why it belongs in this commit rather than a later one.
Re-recording on a decrease is the rule this gate and its siblings
already state.

Worth knowing about the design, since I wrote it: this churn recurs
whenever a day boundary passes with future-dated stamps in the baseline,
and it shrinks only as people stop writing them — which is the behaviour
the gate exists to produce. **93 files still carry a non-zero
allowance**, so the drain isn't finished. If it stays noisy once those
clear, the gate's fail-on-tighten contract is the thing to revisit, not
the stamps.

## Measured

| check | result |
|---|---|
| gate | red before, **exit 0 after**, stable across two consecutive
runs |
| baseline | −176/+25 entries, all date-rollover |
| inert-seam · sql-literal · lane-wiring · census | all green |
| reconciler's own suite | green |

## One correction to a claim I made earlier this session

While investigating I reported the gate as hanging for 600s. It wasn't —
the harness killed the process (exit 144) and the empty output made it
look like a stall. The gate completes in seconds. Noting it because I
nearly filed a performance bug against a healthy script.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 31, 2026
…letion date (#3007)

The **fifth** instance of the async-memo shape #2998 documents, and the
one that survived #3001's sweep.

`lifecycleDates` gates its `completed` value on `isCompleteColumn ||
isArchivedColumn` — both derived from the async `taskColumnFlags` prop —
while listing neither:

```js
}, [task.createdAt, task.executionCompletedAt, task.archivedAt, task.column, locale, lifecycleNowMs]);
```

First paint runs with the flags undefined, the role helpers fall back to
the legacy ids, and on a board whose complete lane is named anything but
`done` that answers false. The flags arrive, `task.column` has not
changed, nothing recomputes, and the card renders **no "Completed
<date>" line at all**.

## Why #3001's sweep called this covered

That PR recorded `mergeSignature` as *"the last live site … nine
persistent candidates, seven covered transitively or by a dependency
that already carries the flags."* This memo was presumably in the
covered pile, and the reasoning is nearly right: it **does** list a
dependency that changes — `lifecycleNowMs`.

But that value is driven by a timer scheduled with
`millisecondsUntilNextLocalMidnight` (FN-8561, so compact date labels
turn over at the viewer's midnight). **A dependency that changes once a
day is not coverage for a value that must be correct on first paint.**
The card shows no completion date for the rest of the session.

That distinction is worth adding to the doc's property 2: *does a listed
dependency change* is the wrong question — *does it change when the
resolved value arrives* is the right one.

## Verification

| state | result |
|---|---|
| clean | 2/2 pass |
| revert the dep fix | **1 failed / 1 passed** |

The control case (a `done` board) passes either way by design, so a
failure in the renamed case means "renamed board", not "nothing
renders".

**One trap worth recording**, since it nearly cost me the finding: my
first `completedLine()` used `time[datetime]:last-of-type`. When only
the *Created* line renders, that selector returns **that** element — so
the pre-resolution absence assertion silently passed against the wrong
node. The test now matches on the element's own `Completed` label. A
positional selector cannot express "this specific line is missing".

`tsc -p tsconfig.app.json` 0 errors, lint clean, 8/8 across all three
renamed-lane TaskCard suites.

## Note

`main` is currently red on the FNXC gate for an unrelated reason
(#2994's impossible-hour stamps landing after #2995); fixed in #3006.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant