Skip to content

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

@os-warren

Filed unassigned by the domain:devx @ objectui execution seat (#5748), PM session session_012CZgmFFzqA9cX8tBMhvpFe, R33. ⛔ I do not set domain:* 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 of check-doc-links.mjs, on purpose", and the scan loop calls collect() on fenced lines. The real cause is narrower and is a live hole.

The blind spelling

SEP matches 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:

node-version: 20

the literal word version sits between node and 20. TOOLCHAIN + SEP + VERSION never 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:

line what breaks the match
actions/setup-node@v4 @ between the toolchain word and the version
pnpm/action-setup@v4 /action-setup@ between pnpm and the version

Why it matters

⛔ Not hypothetical, and not stale: the instance that produced this finding was node-version: 20 in content/docs/guide/ci-cd-pipeline.md, in a block headed "Follow the existing pattern". It sat below the declared engines.node floor 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.

⚠️ The failure direction is the bad one, and it is the same one #6307 was filed for: the gate reports green over claims it never examined. #6307 closed one spelling; this is another, and this one is the canonical spelling for the toolchain whose floor moved twice today (#5306 → >=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:

  1. ⭐ SEP cannot 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 \b fires on neither side of a word character. Here the obstacle is a word, not a separator character. Widening SEP to admit [a-z] would make it match almost anything and destroy the gate. The fix is more likely a second recogniser for the name-version: N shape, or an alias list that treats node-version as a spelling of the Node toolchain.
  2. Then triage what the widened scan surfaces, exactly as 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 did — each new hit is a real claim to inventory or an exemption with a stated reason. ⛔ Do not bulk-add to KNOWN_CLAIMS to get green; an entry written to silence a gate is worse than the blind spot it replaces.
  3. ⚠️ Guard against the trap 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 hit: 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 has now deleted the only node-version: 20 this 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

Activity

  1. self-assigned this
    on Aug 25, 2026
  2. os-warren commented on Aug 25, 2026

    @os-warren
    CollaboratorAuthor

    Dispatch order — R34

    Claimed by the domain:devx @ objectui execution seat (#5748), PM session session_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 SEP widening (established impossible by the earlier spelling card); a second recogniser for the name-version: N shape or an alias list treating node-version as a spelling of the Node toolchain; then triage every literal the widened scan surfaces (⛔ no bulk KNOWN_CLAIMS additions 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_CLAIMS reason 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

    1. SEP cannot 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 \b fires on neither side of a word character. Here the obstacle is a word (version), not a separator character. ⚠️ Widening SEP to admit [a-z] would make it match almost anything and destroy the gate.
    2. No bulk KNOWN_CLAIMS additions to get green. An entry written to silence a gate is worse than the blind spot it replaces.
    3. 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 live node-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: N shape, or
    • an alias list treating node-version as a spelling of the Node toolchain.

    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

    #6308 deleted the corpus's only live node-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> under trap … EXIT INT TERM, prove the restore by git hash-object matching and an empty git 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 whole scripts/__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

  3. os-warren commented on Aug 25, 2026

    @os-warren
    CollaboratorAuthor

    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 → inventoried anchored. Not deleted, not bulk-added.

    The line is prose in the "Adding a New Workflow" section: "a hardcoded node-version: 20 where every workflow declares '22.x' (and 20 sat below the floor the root package.json's engines field now declares)". It is not a version this page teaches — it cites node-version: 20 as 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/workflows at this cut: 28 node-version declarations across 23 files, ZERO reading 20, all 28 reading 22 (control, same sweep: 19 corepack enable steps). The surrounding claim holds too — root engines.node is >=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 why deliberately 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.x entry'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 COLLAPSED banner printed during the tree run is the expected output of that file's own it('exits non-zero on a collapsed population, with the census in the message'), which drives main() 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

  4. os-warren commented on Aug 25, 2026

    @os-warren
    CollaboratorAuthor

    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 session session_012CZgmFFzqA9cX8tBMhvpFe, R34.

    The shape choice is a measurement, not a preference

    Triage offered two shapes — a second recogniser, or an alias treating node-version as a spelling of Node. ⭐ The alias shape cannot see the literal that produced this card, and the dev proved it rather than picking on taste:

    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, node-version: 20.11.1 matches, and node-version: 20 — this card's own literal — does not. Buying it back would mean loosening VERSION for 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 VERSION is untouched. ⭐ It also keys the claim on the whole shape with the quote closed by a backreference, instead of handing the key whatever SEP consumed — the @vitejs/plugin-react pathology the file already records.

    ⚠️ It corrected my premise, and the correction is mine to own

    I 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:1744 still carries node-version: 20 in 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.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).

    ⭐ 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@v4 and pnpm/action-setup@v4 produce no claim after the change — their obstacle is @, a separator character, so a SEP question 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 a TOOLCHAIN name and this tree states no version for those runtimes (measured zero, control: one node-version: line). The spaced prose spelling Node version 20 is 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, citing node-version: 20 as 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, control corepack enable × 19.

    ⭐ And the entry's why deliberately 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions