aedee4b8231bf050c3240a00ab6645ede5d87ee9
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
72f5f8e51a |
fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed (#2995)
`check-fnxc-future-dates.mjs` validates the **date** portion of a stamp
and never looks at the clock time:
```js
const STAMP = /FNXC:[A-Za-z0-9_-]+\s+(\d{4}-\d{2}-\d{2})/g;
…
for (const match of source.matchAll(STAMP)) if (match[1] > today) hits += 1;
```
The capture stops before the hour, so a stamp may carry **any** `hh:mm`
and pass. Found while pre-flighting #2992, whose new comments read
`2026-07-30-25:30`.
## It is not one typo
Four stamps **already on `main`** carry a clock time that cannot exist:
```
packages/cli/src/__tests__/task-list-board-columns.test.ts:2 -24:40
packages/cli/src/commands/task.ts:29 -24:40
packages/cli/src/commands/task.ts:636 -24:40
scripts/check-lane-wiring.mjs:18 -24:00
```
Three separate authors, so this is the gate's blind spot rather than one
person's slip — and #2992 adds two more, which is how I noticed.
AGENTS.md specifies `yyyy-MM-dd-hh:mm`. The stamp's whole purpose is to
make the FNXC record a readable chronology of *why* code exists; a
timestamp that cannot exist quietly costs it that, and nothing was going
to catch it.
## The fix
Hours `00-23`, minutes `00-59`, counted per file **alongside** the
future-dated population rather than as a separate gate — same defect
class (a stamp that does not describe a real moment), and one ratchet is
cheaper to keep honest than two.
**Mutations, both directions:**
| stamp | result |
|---|---|
| `2026-07-30-25:00` | **flagged** |
| `2026-07-30-23:75` | **flagged** |
| clean tree | `475 known future-dated stamp(s), none added`, exit 0 |
## On the four existing stamps
Normalized by clamping the impossible hour to `23`, minutes preserved,
so relative ordering within each file survives. **That is a
normalization with a stated rule, not a claim about the true minute** —
`-24:40` most plausibly meant "just past midnight", but writing
`2026-07-31-00:40` would be future-dated against today's local calendar
and fail the very gate this PR extends. Clamping keeps every stamp real,
ordered, and non-future; the exact minute was already unrecoverable.
**Verified:** FNXC gate exit 0, lane-wiring gate exit 0,
`task-list-board-columns` 5/5, lint clean.
Comment-only changes to the CLI files (stamp text inside FNXC blocks),
so no behaviour change and no changeset.
Noted separately on #2992 so its two new stamps get corrected there
rather than landing and immediately failing this gate.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
968af0822c |
gate: the lane-wiring ratchet did not scan plugins, dashboard/app, or any .tsx (#2978)
## The new gate re-opened a blind spot the old one had already learned about `check-lane-wiring.mjs` (#2966) scanned four roots and only `.ts`: ```js const ROOTS = ["packages/core/src", "packages/engine/src", "packages/dashboard/src", "packages/cli/src"]; ``` `unwired-lane-parameter-guard.test.ts` scans **six**, including `packages/dashboard/app` and `plugins`, and its FNXC note records exactly why: > `plugins` is scanned, and its absence was half of a real escape. […] an unwired `completeColumnsByTaskId` sat on `main` unreported: the guard found 0 across 1753 files, and 0 again across 2114 once plugins were added, because the shape was invisible too. **Fixing either alone would still have missed it.** That is the same trap here, and it needed **two** changes. Those trees are overwhelmingly `.tsx`, which the file filter excluded — so adding the roots without the extension would have scanned a handful of files and reported a reassuring near-zero. ## What the widened scan found: 10 sites, in 8 files, audited not blind-baselined | site | verdict | | --- | --- | | `dependency-graph/GraphTaskNode.tsx` (`isTaskStuck`) | **real** — `isTaskStuck` takes an optional 4th `columnFlags`; omitted, `isWipColumnRole` falls back to the literal, so **no card on a renamed board is ever shown stuck** in the graph | | `dashboard/app/Lane.tsx`, `ListView.tsx` (`sortTasksForDisplayColumn`) | **real**, dashboard batch | | `dashboard/app/ModelSelectorTab.tsx` ×2 (`resolveEffectiveExecutor`/`Validator`) | **real**, dashboard batch | | `dashboard/app/TaskDetailModal.tsx` (`isNearDuplicateCanonicalInactive`) | **real**, dashboard batch | | `even-cards/routes/board-routes.ts` ×3 (`boardToDeck`) | **cannot be fixed in place** — deprecated plugin depending on `@fusion/plugin-sdk` alone, with no resolution source | | `even-realities-glasses/routes/board-routes.ts:141` (`boardToDeck`) | **harmless by construction** — the `{ maxCards: 1 }` summary call slices `active` to empty, so `terminalColumns` cannot change its output; documented in `cards.ts` | They are baselined rather than fixed because they span three other batches. I did **not** fix the graph one despite it being my area: wiring it needs the plugin prop contract to carry column flags, and the plugin's own `dashboard-interop.d.ts` declares `isTaskStuck` with only three parameters — so it crosses the dashboard↔plugin API boundary rather than being a local change. ## Merge-order hazard, stated precisely A **decrease** also exits 1 (`process.exit(1)` on the `decreased` branch), and #2976 wires `packages/cli/src/commands/task-lifecycle.ts`, which is present in this baseline. **If #2976 lands after this PR, main's gate goes red** until the baseline is re-recorded. It fails loudly rather than silently, so it is a chore not a risk. Merging #2976 first and letting me re-record here is the cleanest order — say the word and I will push the re-record. ## Verification (measured) - `check-lane-wiring` — green, **19 known / none added** (was 9 across 4 roots) - `unwired-lane-parameter-guard.test.ts` — **9 passed**, the older guard is unaffected - `lifecycle-column-census --strict`, `check-sql-column-literals`, `check-fnxc-future-dates` — green Gate/tooling only; no product file is touched. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Expanded lane-wiring checks to cover dashboard and plugin code. * Added support for scanning `.tsx` files while excluding declarations, tests, specs, and ignored directories. * Updated baseline coverage counts for the additional files. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19deb42170 |
gate: ratchet call sites that never receive the lane answer (#2966)
**This is the gap that let three defects reach `main` in one day.** `unwired-lane-parameter.mjs` catches a parameter that reaches **no** caller. It is deliberately satisfied by a mention *anywhere*, so **partial** wiring is invisible to it: | | | | --- | --- | | #2956 | `getInReviewStallReason` wired at **0 of 4** call sites while both siblings were wired | | #2963 | both merge entry points unwired — merging was **impossible** on a renamed board | | #2964 | merge-confirmed finalization unwired — **already-landed work parked `failed`** | Every one was a fix that added an optional parameter without the call-site sweep that has to follow it. The existing guard was green throughout, correctly by its own contract. ## A census, not a guard — and that distinction is the whole design Auditing the sites this finds showed **four of seven were legitimately unwired**: `skipColumnIdentityCheck` callers have already proven lane identity by a stronger means, a sentinel-column caller wants the identity check satisfied by construction, and a dead export has no caller to wire at all. A check that failed on those is ~57% false positives. The sibling guard's own header says why that is worse than a miss — *"it teaches people to disable the check"* — and I agree, so this does not do it. Instead it ratchets like the lifecycle census: **36 known unwired sites across 20 files**, allowed to shrink and not to grow. A new unwired caller raises the count and fails; wiring one lowers it and re-records. The recurrence — adding a caller that forgets the lane answer — is precisely what gets caught, and the legitimate sites cost one baseline line each instead of a permanently red gate. ## Detection is AST-based, deliberately It finds exported functions accepting a lane-named argument — directly *or* as an options-bag member — then finds call sites passing none of them. Not regex: the ad-hoc scan I used during the audit produced false negatives on multi-line calls, which is exactly how a caller gets missed in the first place. Using a heuristic to police a defect caused by a heuristic seemed like a poor trade. ## Verified to fail on the recurrence A ratchet that cannot fail is worse than none, so this was measured rather than assumed. Injecting one new unwired caller into `self-healing.ts`: ``` [check-lane-wiring] call sites not passing a resolved lane argument INCREASED: packages/engine/src/self-healing.ts: 9 unwired now, baseline allows 8 ``` exit 1, naming the file and the delta. ## Placement Runs as a named `check:lane-wiring` step in `pr-checks.yml` beside the lifecycle, SQL, inert-seam and FNXC ratchets — same convention, same failure ergonomics, ~1s. Note the baseline records today's state, which still includes the #2963/#2964 sites because those fixes have not merged yet. When they land the count drops and the baseline is re-recorded downward — the ratchet working as intended rather than a conflict. ## Verification `pnpm test:gate` 161 + 487 + 13 + 71; `tsc` engine clean; lint, lifecycle census `--strict`, FNXC gate, and the new check all clean. |