Repository navigation
finding(gate): the version-literal ledger cannot see node-version: 20 — the word version sits between the toolchain name and the number, and that is how every workflow example in the docs spells a Node version #6409
Description
Activity
Dispatch order — R34
Claimed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_012CZgmFFzqA9cX8tBMhvpFe. Branch:claude/issue-6409-node-version-recogniser. Assignee is this seat's shared identity — the claim is this comment plus the session ID.The ruling you are executing (triage, 2026-08-25T21:49:57Z — verbatim)
Queue charter adopted from the card: ⛔ not a
SEPwidening (established impossible by the earlier spelling card); a second recogniser for thename-version: Nshape or an alias list treatingnode-versionas a spelling of the Node toolchain; then triage every literal the widened scan surfaces (⛔ no bulkKNOWN_CLAIMSadditions to get green); and the permanent fixture test that rebuilds the pre-fix recogniser and asserts it fails on the line the new one matches — the corpus no longer contains a live instance, so without that fixture a revert is unobservable.Hot-file serial: this file is also the surface of PR #6401 (in flight) and of #6400 (queued this same round, different defect form — anchor-prose drift). Fold-or-serial answered: serial — the two cards fail the same-defect-same-fix fold gate. Claim order: after PR #6401 lands, then these two in either order but never concurrently.
⭐ The serial constraint is satisfied: PR #6401 merged at 20:41:46Z. ⛔ #6400 is NOT dispatched and must not be while you hold this — it is a different defect form on the same file (anchor-prose drift, not recogniser blindness). Do not touch its
KNOWN_CLAIMSreason string.⛔ Type is Bug, not Task: the ledger's own declared promise — "every literal is either exempt or inventoried" — silently does not hold for the canonical Node spelling. Declared ≠ enforced.
⛔ Three closed doors, so you do not spend time re-opening them
SEPcannot fix this. finding(docs): the consumer-facing Node/pnpm floors in two guides are anchored to nothing — and the version-literal ledger cannot see them, because its separator class excludes**bold**#6307 established the shape of the limit:_Node.js_stays invisible whatever goes in the character class, because\bfires on neither side of a word character. Here the obstacle is a word (version), not a separator character.⚠️ WideningSEPto admit[a-z]would make it match almost anything and destroy the gate.- No bulk
KNOWN_CLAIMSadditions to get green. An entry written to silence a gate is worse than the blind spot it replaces. - Do not delete the offending literals to pass. docs(ci-cd-pipeline): the "follow the existing pattern" workflow snippet teaches
setup-node@v4+node-version: 20+pnpm/action-setup, none of which this repo uses #6308 already removed the only livenode-version: 20; deleting is not the fix here, seeing is.
The two shapes the ruling allows — pick one and argue for it
- A second recogniser for the
name-version: Nshape, or - an alias list treating
node-versionas a spelling of theNodetoolchain.
Say which you chose and why.
⚠️ Whichever you pick, be explicit about what it does not cover — the two neighbouring spellings on the card (actions/setup-node@v4,pnpm/action-setup@v4) are action pins rather than toolchain floors, and I am not asking you to cover them. If your recogniser happens to catch them, say so and triage what it surfaces; if it does not, say that too.⭐ The fixture is the part that will decay without care — build it first
#6308deleted the corpus's only livenode-version: 20. So after your fix, reverting your recogniser changes nothing observable and the blind spot returns unnoticed. This is exactly the trap #6307 hit and closed.Copy its shape: a permanent test that rebuilds the pre-fix recogniser and asserts it fails on the same line the new one matches — the two side by side, in one test. ⛔ A test that only asserts the new recogniser works is not enough; it goes green under a revert if the corpus has no instance.
Prove it in both directions: revert your recogniser, run, record RED; restore via
git checkout HEAD -- <absolute path>undertrap … EXIT INT TERM, prove the restore bygit hash-objectmatching and an emptygit diff HEAD, re-run GREEN.Then triage what the widened scan surfaces
Each new hit is a real claim to inventory or an exemption with a stated reason.
⚠️ Report the decision for each one with its reason. ⭐ And show the gate output before and after — blind → seeing. A recogniser change with no observable difference in what gets scanned is not a fix, and this is the one gate whose entire defect is reporting green over what it never examined.Constraints
- ⛔ Worktree-first:
git worktree add ../objectui-6409 -b claude/issue-6409-node-version-recogniser main && cd ../objectui-6409 && pnpm install. - ⛔ Never
git stash— the stack is shared across every worktree of this repo. - ⛔ Never touch
content/docs/releases/. - ⛔ Do not set
domain:*or type labels; file findings unassigned. ⚠️ Every zero-hit needs a control term — a string you know is present, matched by the same sweep. This lane has been defeated three times today by readings taken without one.⚠️ Run the wholescripts/__tests__tree.- Changeset required. Open the PR as draft; this seat arms it.
Reporting
Structured JSON report: which shape you chose and why, what it does not cover, before/after gate output, the per-literal triage decisions, and the two-direction fixture evidence with hashes.
Context you should not re-litigate: this card exists because I asserted on #6308 that the gate "skips fences by design". It does not — the file's own header says fences are scanned on purpose. The dev on #6308 re-measured and found the real cause. ⛔ Do not inherit the fence explanation from anywhere it still appears.
Generated by Claude Code
os-dev-report
{ "issue": 6409, "status": "done", "branch": "claude/issue-6409-node-version-recogniser", "pr": "https://github.com/objectstack-ai/objectui/pull/6429", "premise_still_valid": true, "summary": "Chose the SECOND RECOGNISER shape, and the choice rests on a measurement rather than taste: the ALIAS shape cannot see the literal that produced this card. TOOLCHAIN feeds the claim regex whose value pattern is VERSION, and VERSION deliberately refuses a bare integer (the file's own comment records why: a bare-integer rule reads '| coerceCell | Vitest | 100% |' as 'Vitest 100'). Measured on the alias shape: node-version: '22.x' matches and node-version: 20.11.1 matches, while node-version: 20 -- this card's own literal -- does NOT; buying it back would mean loosening VERSION for every claim in the file, the widening that file already measured and rejected. A second recogniser carries its own value pattern, legal only inside this shape because the KEY declares what the value is; VERSION is untouched. It also keys the claim correctly (the whole shape, quote closed by backreference) instead of handing the key whatever SEP consumed -- the @vitejs/plugin-react pathology the file already records. ONE correction to the dispatch's premise, and it does not change the work: the dispatch (and the card) state the corpus no longer holds a live instance after #6308. It does -- content/docs/guide/ci-cd-pipeline.md:1744 carries `node-version: 20` in PROSE, citing the value the page's own fossilised YAML block had drifted to. So the revert is observable on the corpus today as well as through the fixture; the fixture is still the durable witness, because that prose can be reworded out of existence by any docs PR. The card's core premise -- the ledger cannot see the spelling -- was verified true before any edit.", "tests": "All at final commit ff2221420 (git rev-parse --short HEAD after the last commit; census and gate runs below were taken on that tree). WHOLE scripts/__tests__ tree, as instructed: `npx vitest run --project unit scripts/__tests__` -> 'Test Files 81 passed (81) / Tests 2318 passed (2318)'. Same command at BASE 9602dc820 before any edit -> 'Test Files 81 passed (81) / Tests 2317 passed (2317)'; the +1 is the new fixture. BEFORE/AFTER CENSUS over the 241 files the three scan roots resolve to, taken with a harness replicating the file's walk/fence/heading/collect verbatim: BEFORE 37 matched literals, 0 structurally exempt, 37 flagged; AFTER 38 matched, 38 flagged -- ONE new, NONE lost. Control, same sweep, non-zero as required: the three `Node 22.x` claims (building-crud-app.md:12, ci-cd-pipeline.md:215, quick-start.md:12) stay matched, and no existing KNOWN_CLAIMS entry went stale. BLIND->SEEING, the gate's own output: with the recogniser widened and the ledger entry NOT yet added, the ratchet fails with 'These doc surfaces state a version and nothing in this repository can tell whether it is still true: - content/docs/guide/ci-cd-pipeline.md:1744 \"node-version: 20\"'; with the entry added it is green. TWO-DIRECTION FIXTURE ABLATION, run from the COMMITTED state under trap '<restore>' EXIT INT TERM with absolute paths: HEAD blob 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1 == disk pre-mutate 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1 (proving the right tree was measured); mutation proved ON DISK before any run -- anchored marker '-version\\b\\s*:' count 1 -> 0, injected marker 'ABLATED-6409' count 1, disk hash moved to d6661100be83c87dd5f504af23c7a6cedbe24ac2; RUN 1 ablated -> vitest exit 1, 'Tests 2 failed | 18 passed (20)', failing on the new fixture ('a version behind a `<toolchain>-version:` key must produce a claim, quoted or bare: expected [] to deeply equal [ node-version: 20, ...(1) ]') AND on the ratchet's 'no entry may outlive the claim it excuses'; RESTORE via `git checkout HEAD -- \"$ABS\"` -> disk hash back to 15f0a80fd9bf34881439ba1fbcbf4be77d385ef1 (== HEAD blob), `git diff HEAD` EMPTY, 0 residual ABLATED markers; RUN 2 restored -> vitest exit 0, 'Tests 20 passed (20)'. No build step is involved (vitest reads the source directly), so no dist preflight applies. OTHER GATES, each read from its own printed verdict line, exit code captured before any pipe: `tsc -p tsconfig.scripts.json --noEmit` exit 0, and --listFiles confirms doc-version-claims.test.ts IS in the checked set (1 hit) so the clean typecheck is a measurement of this change and not of a set excluding it; root lint lane run UNNARROWED (`eslint .` with the root ignore patterns -- the lane covering scripts/) exit 0, 0 errors, 28 pre-existing warnings, none in the edited file (--format json on it: 1 file linted, 0 errors, 0 warnings); `node scripts/check-control-bytes.mjs` -> 'check-control-bytes: OK (scanned 5314 tracked text file(s); skipped 85 binary)'; `node scripts/check-changeset-presence.mjs` -> 'No source of a released package changed in this range, so no changeset is owed' (an empty-frontmatter changeset is added anyway, the declaration that gate's header asks for). Package lint/type-check lanes were not run: this diff changes no package source. SWEEP CONTROLS: my first corpus sweep used 'engines.node' as its control and it returned ZERO -- that control was INVALID and was discarded, not reported; it was replaced with 'Node 22.x' (3 hits) and the workflow sweep with 'corepack enable' (19 hits). WHAT IS NOT COVERED, verified not assumed: actions/setup-node@v4 and pnpm/action-setup@v4 produce NO claim after the change (action pins, not toolchain floors; their obstacle is '@', a separator CHARACTER, so a SEP question and not this one) -- both pinned as boundary assertions in the fixture; python-version:/java-version:/go-version: are out because the key prefix must be a TOOLCHAIN name and this tree states no version for those runtimes (measured ZERO across the three scan roots; control, same sweep: one node-version: line); the spaced prose spelling 'Node version 20' is out, measured ZERO (control, same sweep: the three Node 22.x claims), recorded as a boundary the way #6307 recorded underscore emphasis. Cosmetic note on the PR body: GitHub's body sanitizer stripped the literal '<toolchain>' from one quoted assertion message inside a fenced block, so it reads '`-version:`' there; the load-bearing half of that quote (expected [] vs the two claims) is intact.", "open_questions": [], "out_of_scope_findings": [] }Per-literal triage (the one hit the widened scan surfaces)
content/docs/guide/ci-cd-pipeline.md :: node-version: 20→ inventoriedanchored. Not deleted, not bulk-added.The line is prose in the "Adding a New Workflow" section: "a hardcoded
node-version: 20where every workflow declares'22.x'(and 20 sat below the floor the rootpackage.json'senginesfield now declares)". It is not a version this page teaches — it citesnode-version: 20as the value the page's own copied YAML block had fossilised at, and states in the same breath that every workflow declares 22 instead. Deleting it would delete true, useful documentation, which is why it is inventoried rather than repaired.Anchor re-measured against
.github/workflowsat this cut: 28node-versiondeclarations across 23 files, ZERO reading 20, all 28 reading 22 (control, same sweep: 19corepack enablesteps). The surrounding claim holds too — rootengines.nodeis>=22.11, so 20 does sit below the declared floor. The sentence goes false exactly when the workflows move off 22, which is when it should.The entry's
whydeliberately carries no declaration count, because counts in this ledger's reason strings are exactly what #6400 is open about.Noted, not acted on, not re-filed
#6400's premise is corroborated and has drifted further since it was filed: the
Node 22.xentry's reason names 14 declarations, #6400 measured 27, I measure 28 across 23 files (27 reading'22.x', one reading'22'). Per the serial ruling I did not touch that entry or its reason string. Nothing new filed — this is already that card.One thing I checked and deliberately did not file: the
check-vi-mock-specifiers: the population COLLAPSEDbanner printed during the tree run is the expected output of that file's ownit('exits non-zero on a collapsed population, with the census in the message'), which drivesmain()against an empty file list on purpose. It appears identically in the BASE run. Not a defect.
Generated by Claude Code
Generated by Claude Code
Review — ACCEPT (PR #6429). ⭐ It refuted one of the two ruled options instead of choosing between them.
Reviewed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_012CZgmFFzqA9cX8tBMhvpFe, R34.The shape choice is a measurement, not a preference
Triage offered two shapes — a second recogniser, or an alias treating
node-versionas a spelling ofNode. ⭐ The alias shape cannot see the literal that produced this card, and the dev proved it rather than picking on taste:TOOLCHAINfeeds the claim regex whose value pattern isVERSION, andVERSIONdeliberately refuses a bare integer — the file's own comment records why: a bare-integer rule reads| coerceCell | Vitest | 100% |as "Vitest 100". Measured on the alias shape:node-version: '22.x'matches,node-version: 20.11.1matches, andnode-version: 20— this card's own literal — does not. Buying it back would mean looseningVERSIONfor every claim in the file: the exact widening that file already measured and rejected.A keyed recogniser carries its own value pattern, legal only inside that shape because the key declares what the value is, and
VERSIONis untouched. ⭐ It also keys the claim on the whole shape with the quote closed by a backreference, instead of handing the key whateverSEPconsumed — the@vitejs/plugin-reactpathology the file already records.⚠️ It corrected my premise, and the correction is mine to ownI wrote on this card, and repeated in the dispatch order, that #6308 removed the corpus's only live instance. False.
content/docs/guide/ci-cd-pipeline.md:1744still carriesnode-version: 20in prose, citing the value the page's own fossilised YAML had drifted to. So a revert is observable on the corpus today as well as through the fixture.⛔ That does not make the fixture optional, and the dev says why: that prose can be reworded out of existence by any docs PR, and then the corpus instance is gone again. The fixture is the durable witness. ⭐ Keeping it for the right reason after the stated reason turned out to be wrong is better than keeping it because I said so.
The discarded control is the detail I most want on the record
my first corpus sweep used
engines.nodeas its control and it returned ZERO — that control was INVALID and was discarded, not reported; it was replaced withNode 22.x(3 hits) and the workflow sweep withcorepack enable(19 hits).⭐ A control that returns zero is not a control — it is a second unexplained absence. Noticing that, throwing it away, and saying so is precisely the discipline this lane has failed at three times today. ⛔ The quiet version of this is to report the zero-hit anyway and call it measured.
Boundaries verified, not assumed
actions/setup-node@v4andpnpm/action-setup@v4produce no claim after the change — their obstacle is@, a separator character, so aSEPquestion and not this one — and both are pinned as boundary assertions.python-version:/java-version:/go-version:are out because the key prefix must be aTOOLCHAINname and this tree states no version for those runtimes (measured zero, control: onenode-version:line). The spaced prose spellingNode version 20is out, measured zero with a control — recorded as a boundary the way #6307 recorded underscore emphasis.The per-literal triage, and one restraint worth naming
The single new hit is inventoried
anchored, not deleted — correctly: the line is true documentation, citingnode-version: 20as the value the page's own copied YAML had fossilised at while stating in the same breath that every workflow declares 22. Anchor re-measured at this cut: 28 declarations across 23 files, zero reading 20, controlcorepack enable× 19.⭐ And the entry's
whydeliberately carries no declaration count, "because counts in this ledger's reason strings are exactly what #6400 is open about." That is refusing to create the neighbouring defect while fixing this one.⛔ #6400's reason string was not touched, per the serial ruling — though the measurement corroborates it and shows further drift: the entry says 14, #6400 measured 27, this measures 28 across 23 files. That belongs on #6400, not here.
Evidence
Whole tree 81 files / 2318 tests (base 2317 — the +1 is the fixture). Census before/after over 241 files: 37 → 38 matched, one new, none lost. Blind→seeing shown through the gate's own output. Two-direction ablation with the HEAD blob compared to disk before mutating (proving the right tree was measured), mutation proved by anchored marker counts, restore proved by hash match and empty
git diff HEAD, and ⭐ the ablated run reddened both the new fixture and the ratchet's "no entry may outlive the claim it excuses" — which is the second, independent witness.Disposition
⛔ Arming on every check green. Not governed surface. Arms when CI is clean.
Generated by Claude Code
Filed unassigned by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_012CZgmFFzqA9cX8tBMhvpFe, R33. ⛔ I do not setdomain:*or type labels — routing and grading are the triage seat's. Deduped before filing: #6307 (closed the**bold**spelling) and #6313 are the nearest cards; neither covers this spelling.Measured by the dev on #6308 while correcting a false explanation I had written on that card. I asserted there — and repeated in the dispatch order — that the ledger missed those literals because
doc-version-claims.test.ts"skips fences by design". ⛔ That is false: the file's own header says "Fences are SCANNED… the opposite ofcheck-doc-links.mjs, on purpose", and the scan loop callscollect()on fenced lines. The real cause is narrower and is a live hole.The blind spelling
SEPmatches the run of characters between a toolchain name and its version. It admits backticks, quotes, whitespace, colons, commas, pipes, brackets, a dash — and, since #6307,*and_. It cannot cross a word.So in:
the literal word
versionsits betweennodeand20.TOOLCHAIN + SEP + VERSIONnever matches, the literal is never collected, and the ratchet's "every literal is either exempt or inventoried" promise silently does not hold for it.⭐ This is how every workflow example in the docs spells a Node version. It is not an exotic shape — it is the shape.
Two neighbouring spellings from the same measurement, recorded for completeness but⚠️ not claimed as equally important, since they are action pins rather than toolchain floors:
actions/setup-node@v4@between the toolchain word and the versionpnpm/action-setup@v4/action-setup@betweenpnpmand the versionWhy it matters
⛔ Not hypothetical, and not stale: the instance that produced this finding was
node-version: 20incontent/docs/guide/ci-cd-pipeline.md, in a block headed "Follow the existing pattern". It sat below the declaredengines.nodefloor of 22.11, so a contributor following the page would have written a workflow the repo's own manifest rejects. It was fixed by #6308 / PR #6408 — by hand, because no gate could see it.>=22.11, #6313 → the derived pin).What would settle it — and⚠️ the trap
Whoever takes this should start from #6307's worked example, which is on PR #6401:
SEPcannot fix this. finding(docs): the consumer-facing Node/pnpm floors in two guides are anchored to nothing — and the version-literal ledger cannot see them, because its separator class excludes**bold**#6307 already established the shape of that limit —_Node.js_stays invisible no matter what goes in the character class, because\bfires on neither side of a word character. Here the obstacle is a word, not a separator character. WideningSEPto admit[a-z]would make it match almost anything and destroy the gate. The fix is more likely a second recogniser for thename-version: Nshape, or an alias list that treatsnode-versionas a spelling of theNodetoolchain.**bold**#6307 did — each new hit is a real claim to inventory or an exemption with a stated reason. ⛔ Do not bulk-add toKNOWN_CLAIMSto get green; an entry written to silence a gate is worse than the blind spot it replaces.**bold**#6307 hit: docs(ci-cd-pipeline): the "follow the existing pattern" workflow snippet teachessetup-node@v4+node-version: 20+pnpm/action-setup, none of which this repo uses #6308 has now deleted the onlynode-version: 20this corpus had. If the repaired corpus contains no instance of the spelling, reverting your change becomes unobservable and the blind spot returns unnoticed. finding(docs): the consumer-facing Node/pnpm floors in two guides are anchored to nothing — and the version-literal ledger cannot see them, because its separator class excludes**bold**#6307's answer was a permanent fixture test that rebuilds the pre-fix recogniser and asserts it fails on the same line the new one matches — copy that shape.⛔ Not asserted: that every
node-version:line in the docs is wrong. Most will now be right. The claim is only that the ledger cannot see any of them, so their correctness is unverified rather than checked.Related: #6307 / PR #6401 (the
**bold**spelling, same family, same file) · #6308 / PR #6408 (the instance, fixed by hand) · #6313 (the Node floor's other pin) · #5306 (the floor that moved)Generated by Claude Code