lint: widen the vocabulary gate's face refusal to every published package and retire boot-refusal - #17056
Conversation
…ed package and retire boot-refusal `check-dispatcher-error-vocabulary` refused a site classified away from registration only under `packages/spec/src/` — the one package #16449 had measured. The #16404 ruling it enforces is about every code that ships in `dist`, so the refusal now covers every published workspace member's `src/`, enumerated from the manifests through the shared workspace parse and held against a floor so a broken enumeration refuses instead of going quiet. `packages/spec/src/` keeps the stricter verdict set. Outside it, `pending-registration` keeps a named, dated allowance owed to #8846, so the two rows whose disposition belongs to #8846 / #9460 do not red on a card that is not theirs; every run prints how many rows still stand on it. With the whole scan population inside the face, `boot-refusal` is a verdict no row can legally carry, so it retires from `CodeVerdict`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 00dbfa40d760868d6d5e07696163ac86876ced57 && git checkout 00dbfa40d760868d6d5e07696163ac86876ced57
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce7bae8b4424cba3d7a4dea3ef24dcf11ba0a24c daf46e6e83dc197ba6bd0281da64e9b7104c01b0 && git checkout -B drift-repro ce7bae8b4424cba3d7a4dea3ef24dcf11ba0a24c && git merge --no-ff daf46e6e83dc197ba6bd0281da64e9b7104c01b0
node scripts/docs-audit/affected-docs.mjs --json ce7bae8b4424cba3d7a4dea3ef24dcf11ba0a24c |
…e verdict `packages/spec` ships `src/**/*.zod.ts` in its `files[]`, so the ledger's TSDoc is published prose. One sentence said the dispatcher vocabulary's `boot-refusal` verdict records reachability for codes not yet registered and pointed at the file this branch deletes it from — a live-mechanism claim, not a case that can no longer arise, so a reader would follow the pointer and find nothing. It now records the retirement and names what replaced it. The paragraph below it claimed only `packages/spec/src/**` is held mechanically. That understated the rule after the widening, so it now states the published face, the stricter spec sub-face and the dated #8846 allowance, with both finding kinds named. The mirrored reference page is regenerated with `check:generated --fix` (`gen:docs`), never by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
…den-vocabulary-gate-retire-boot-refusal
… main `os-regen-merge.sh` step 4. Both sides had changed the generated page, so step 2 restored main's side and this regenerates it from the merged source: the reference now carries #16783's three new rows and this branch's corrected TSDoc together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
…workspace glob non-empty The changeset half is the binding correction: `packages/spec` ships `src/**/*.zod.ts`, so the ledger's TSDoc goes out in the tarball verbatim and a docs-only edit to it publishes. `skip-changeset` was wrong the moment that file entered the diff; this adds the `@objectstack/spec: patch` the lane's five precedents on this head all carry for the same shape. Also from the same review, non-binding: - The published face's floor could not catch a lost workspace glob and the docblock implied it could. Measured per glob on this tree: the largest is `packages/*` at 31 of 88 members, so losing it entirely still leaves 57 — over any floor low enough not to red on ordinary churn — and `expandWorkspaceGlob` returns `[]` for a vanished parent silently. `emptyWorkspaceGlobs` now refuses that case by name, the floor's docblock states what it can and cannot see, and a self-test case asserts the arithmetic in the direction that keeps the pin necessary. - The TSDoc said the gate refuses every verdict that parks a site away from registration, which over-generalises the allowance stated eight lines below it. It now names `pending-registration` and its scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
Fixes #16649
Second half of the card. The first half (registering the fourteen
door: 'none'codes) landed as #16879 on 2026-09-08; this is the mechanical closure the card's Scope and triage acceptance items 3/4/5 ask for.Implemented per the director seat's ruling of 2026-09-08 (comment 5586454647, decision batch #95), quoted verbatim in the card. The later release comment 5586642296 re-lists options A/B/C as open; it was written thirteen minutes after the ruling and does not cite it. This PR follows the ruling; A/B/C were not re-litigated.
What changed
scripts/check-dispatcher-error-vocabulary.mjs— the face refusal, which askedfile.startsWith('packages/spec/src/')and nothing else, now asks which published package'ssrc/a site sits in. Members are enumerated from their manifests throughscripts/workspace-enumerator.mjs, the repo's one parse ofpnpm-workspace.yaml; "published" isname && private !== true, the same testcheck-published-files.mjsapplies, because the ruling's premise is about what a consumer installs. Measured on this tree: 80 workspace members, 70 published, 69 of those with asrc/on disk. The derivation emits one prefix per published member and does not probe the disk forsrc/, so the printed count is members, not members-with-sources; that is stated where it is derived.The two faces stay separate because they differ:
packages/spec/src/keepsforeign-vocabulary/runtime-pinnedonly, and the widened region additionally admitspending-registrationunder a named, dated allowance owed to #8846. The spec region keeps reportingspec-face-unregisteredand the widened region reports a newpublished-face-unregistered, so the ledger's existing prose about that kind stays true and the two regions stay distinguishable in output.The enumeration is guarded two ways, because one of them is not enough and saying otherwise would be the false comfort this gate exists to refuse. The floor (
PUBLISHED_SOURCE_FACE_FLOOR = 40, against 70 measured) catches a collapse. It cannot catch a lost workspace glob, and that is measured rather than assumed: per glob on this tree,packages/*31 ·packages/services/*16 ·packages/plugins/*15 ·packages/drivers/*5 ·examples/*5 ·packages/qa/*4 ·packages/connectors/*4 ·packages/apps/*3 ·packages/triggers/*3 ·packages/adapters/*1 ·apps/*1 = 88 members before the published filter. Losing the largest entirely still leaves 57, over any floor low enough not to red on ordinary churn — andexpandWorkspaceGlobreturns[]for a vanished parent silently, sopackages/*could disappear, takingpackages/specwith it, and a floor of 40 would not notice.So
emptyWorkspaceGlobsrefuses any non-exclusion glob that expands to nothing, by name. Zero do today. A self-test case asserts the arithmetic in the direction that keeps the pin necessary — if losing the largest glob ever would breach the floor, the assertion reds and tells the next author the docblock is stale, rather than quietly flattering the floor.The
unclassified-siteremedy text told every author to reach forpending-registration. Insidepackages/spec/src/that is the one verdict the face refuses, so the text is now face-aware and names the ledger row first.packages/runtime/src/dispatcher-error-vocabulary.ts—boot-refusalretired fromCodeVerdict. With the whole scan population inside the face (52 sites: 47 published-face, 5 spec-face), no row can legally carry it. Its own doc comment had already named this as the condition for its retirement. A paragraph in its place records what it meant and why it must not come back; the running log records the fourth cycle closing mechanically rather than by having been emptied once.The ledger TSDoc, corrected (patch round)
packages/specshipssrc/**/*.zod.tsin itsfiles[], soerror-code-ledger.zod.ts's TSDoc is published prose, and retiring the verdict made part of it false rather than merely stale. Two blocks changed, both inside the:100-132docblock; the two regions contended by #17015 and #16783 were not touched.:109-113— false, and fixed. It said the dispatcher vocabulary'sboot-refusalverdict records reachability for codes not yet registered, and pointed at the file this branch deletes it from. That describes where a live mechanism lives, not a case that can no longer arise, so a reader would have followed the pointer and found nothing. It now records the retirement and names what replaced it: the gate refuses every verdict that parks a site away from registration under any published package'ssrc/, so adoor: 'none'code has no resting place short of a ledger row.:119-124— also changed, and here is why. The vacuous-disjunct argument does hold for it: with the verdict gone, "aboot-refusalorpending-registrationrow is a finding" keeps a true half. But that is not the only thing the paragraph claims. It openedpackages/spec/src/** is held to this mechanically, which after this PR understates what is enforced — a reader would conclude only the spec tree is guarded, which is exactly the "守了一部分 reads the same as 守住了" failure this card exists to close. So it now states the published face, the stricter spec sub-face and the dated #8846 allowance, with both finding kinds named.A later review round tightened one more sentence in the same block: "the gate now refuses every verdict that parks a site away from registration" over-generalised the allowance stated eight lines below it, so it now names
pending-registrationand its scope explicitly.The mirrored reference page
content/docs/references/api/error-code-ledger.mdxwas regenerated withcheck:generated --fix(gen:docs), never hand-edited.The eight other
boot-refusalmentions in that file were read and left: every one is past tense about what a now-deleted row recorded and ratcheted out, with the reading itself stated to live on the ledger row now. None asserts a live mechanism. On the final head they are:172,:502,:834,:860,:1096,:1141,:1162,:1239— re-read after #16783 landed, since the numbers an earlier revision of this body carried were taken before that merge.Acceptance item 4: the red-before-green demonstration
Item 4 was written when the fourteen codes were unregistered, so "run the widened gate and watch it red on all fourteen" was the available proof.
git grep -c "verdict: 'boot-refusal'"now returns 0 — the first half consumed that proof. The equivalent was constructed instead, and it measures the delta rather than merely showing the gate can fail: one mutation, two gates, the same tree.packages/types/src/node.ts'sMODULE_NOT_FOUNDrow (a published package outsidepackages/spec/src/) was flipped fromforeign-vocabularytosandbox-authored— still a legalCodeVerdict, so nothing else catches it.origin/main's script, byte-for-byte (308374 bytes), run from this worktree'sscripts/so itsROOTresolves here[published-face-unregistered] packages/types/src/node.ts stamps unregistered code 'MODULE_NOT_FOUND' (objlit) ... classifies it 'sandbox-authored' — a verdict refused under a published package's src/Mutation proven on disk before either leg: HEAD blob
0a1e68f2..., mutated blob26b59e1e..., anchor count 1 to 0 and injected count 0 to 1. Restored undertrap ... EXIT INT TERMwithgit checkout HEAD -- ABSOLUTE_PATH; restored blob equals the HEAD blob,git diff HEAD --statandgit status --porcelainboth empty. The temporary base-gate copy was removed by the same trap.Two further ablations, same discipline:
'pending-registration'from the widened verdict set (the rule applied verbatim, with no carve-out) reds exactly two rows, and they are exactly the two the ruling named:AMBIGUOUS_METADATA_STEMinpackages/metadataandowd_widening_forbiddeninpackages/plugins/plugin-security. Gate exit 1, restored blob equals HEAD.Acceptance item 5: the negative controls
The two surviving verdicts must not be refused by the widening, or the implementation would be "everything unregistered reds", which passes item 4 while deleting two legitimate exemptions. On the live tree the widened gate is green over all 52 rows — 49
foreign-vocabulary, 1runtime-pinned, 2pending-registration— and the--self-testbattery pins each one outsidepackages/spec/src/as an explicit case, alongside the two boundary controls (a private member'ssrc/and a published package file outsidesrc/, both outside the face).The battery was rewritten and its floor raised from 7 to 21. Every case passes its own
publishedFaces, becausereconciledefaults to none; the last three cases hold the fixture population against the live enumeration, so the battery cannot be green over a workspace this repo does not have.Clause 2 and the changeset, both re-measured
Clause-②: no, re-derived rather than inherited.git grep -n "dispatcher-error-vocabulary" HEAD -- packages/runtime/src/index.tsreturns zero: the module is not exported from the published entry point. Confirmed from the dist side after building@objectstack/runtime:boot-refusal,CodeVerdict,UNREGISTERED_CODE_SITES,SANDBOX_AUTHORED_LIMBanddispatcher-error-vocabularyeach hit 0 of the 6 files inpackages/runtime/dist/, against positive controlsMissingDriverPackageErrorandresolveProjectDatabaseUrlat 4 files each. Deleting a member of a type that does not ship, plus a strictly stricter gate, moves no published face.Re-derived again after the TSDoc edit, because that edit lands in⚠️ The clause-2 enqueue gate's PATH leg will nonetheless fire, because the diff now touches
packages/spec/src/**: the ledger holds the same 282 unique codes before and after it (279 before #16783 landed and added three), every changed line in that file is a TSDoc*line with no code line moved, and the generatedapi-surfaceandspec-changes.jsonartifacts did not move —check:generatedfound only the docs page stale. Prose in a shipped file widens no accept set and adds no key, soClause-②stays no.packages/spec/src/**; that is expected and is the PM's to handle with a contract-review-tier review before enqueue. It is not a reason to declareyes, and the prose was not left false to dodge it.The changeset is⚠️ That error came from the dispatch instruction that ordered the TSDoc fix, which stated the only consequence was the clause-2 path leg firing and did not re-derive the changeset judgment after putting a shipped file into the diff — recorded here at the PM's request rather than carried as this branch's own reasoning. The correction is measured, not argued:
"@objectstack/spec": patch, and the earlierskip-changesetwas wrong.packages/spec'sfiles[]is['dist','json-schema','liveness','prompts','llms.txt','README.md','src/**/*.zod.ts','CHANGELOG.md','api-surface','spec-changes.json'], sosrc/api/error-code-ledger.zod.tsships verbatim in the tarball and a docs-only edit to it publishes. Five precedents on this head carry"@objectstack/spec": patchfor exactly this shape. The level ispatchand stays there — no schema, accept set, default or refusal moves, andERROR_CODE_LEDGERholds the same members before and after.The rest of the diff still publishes nothing, and both halves were measured for it: 70 published manifests examined, none carries a
files[]entry that could ship a rootscripts/path, the root manifest isprivate: true, and@objectstack/runtime'sfiles[]is['dist','README.md','CHANGELOG.md']so itssrc/is not shipped — corroborated by the dist symbol readings above.packages/specis the one released package this diff touches, and it is the one the changeset names.Verification
Final head
daf46e6e.origin/mainmoved twice under this branch and was merged both times throughbash scripts/pm/os-regen-merge.sh; the second merge (b3c14d2a) brought #16783, one of the two PRs that contended this ledger file. Its regions (:677-728) and this branch's (:106-136) are disjoint and the source merged with no conflict; the generated page is where the narrative needs care, and an earlier revision of this section got it wrong. What the commit graph actually shows: git's own merge kept the branch's bytes in the merge commit (b3c14d2a's page blobef68ab6eis identical to branch parentd1fb2897's, not to main'sb079fc5b). The script's step 2 then staged main's side, the regen pre-commit hook refused that commit, and step 4's regeneration replaced it — so the branch side was never actually restored-over, and the net effect was the regenerated page (07cb189d, blobc677c8ca), carrying #16783's three new rows and this branch's corrected TSDoc together. Verified after the merge rather than assumed: all fourteen of PR #16879's registrations still present (14/14), ledger at 282 unique codes,boot-refusalstill absent fromCodeVerdict, and this branch'sderivePublishedFacesand corrected TSDoc both intact.origin/mainmoved twice more after that and was merged again the same way; all readings below are fromdaf46e6e, re-taken there rather than carried forward.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, letting it take the change set from the merge base itself: 116 families over the five changed paths (64 → 111 when the TSDoc and docs paths arrived, → 116 when the changeset did). Re-derived and re-run in full on the final head after the last merge. 115 exit 0. Reconciled with--ran: "116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED".check:doc-formula-expressions,check:doc-security-postureandcheck:docs-transcript-driftnamed@objectstack/formula/@objectstack/lintas unbuilt,check:skill-examplesasked for@objectstack/client-react(its own text warns the alternative is a false green), andcheck:lean-entry-closureasked for@objectstack/objectql— building those closures took all five to exit 0.check-empty-changeset,check-changeset-no-major --base origin/mainandcheck-changeset-fixed, each exit 0.pnpm check:dispatcher-error-vocabularyexit 0 — self-test "10 shapes + 348 assertions OK", production run "52 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846)".pnpm check:dual-build-cjs-loadsexit 3,PREREQUISITE NOT MET— it reads built output and some packages have nodist/in this worktree; its own text says "This is NOT a pass: nothing was measured". Recorded as NOT MEASURED, not as a failure, and not as a pass either. Its self-test passed (93 cases). Whole-tree family, CI owns it.scripts/pm/os-verify-lock.sh:pnpm --workspace-concurrency=2 --filter '@objectstack/runtime...' buildexit 0,pnpm --filter @objectstack/runtime typecheckexit 0,pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/actions-validation-envelope.test.tsexit 0. WrapperVERDICT command-exit 0.*.test.ts; its tests are its--self-test, wired intocheck:dispatcher-error-vocabularyand run above.git grepof the script's filename across test files finds one carrier,packages/spec/src/api/error-code-ledger.test.ts— run, 20/20 passed.eslint . --no-inline-configexit 0 over 6398 files, 0 errors, 0 warnings, with the changed files confirmed present in the linted set.22efeead, before the last two merges; the 116-family sweep was re-run on the final head, this was not. CI has nopaths:filter on lint, so it covers the final head.Acceptance notes
Not filed, recorded here.
The
error-code-ledger.zod.tsprose is fixed in this PR, not deferred — see the patch-round section above. An earlier revision of this PR argued it could be left because it "describes a case that can no longer arise"; that argument was wrong for:109-113, which describes where a live mechanism lives, and it is withdrawn.Still out of scope and genuinely deferred: the three source comments in
packages/core(×2) andpackages/drivers/driver-sql(×1) that state the pre-ruling position. #16884 is already open on exactly those and explicitly names the staleboot-refusalpointer, so nothing was re-filed. Successor: #16884.packages/spec/src/stack.zod.tsmentions theboot-refusalclass but qualifies itself with "on the tree this landed against", so it stays true as a historical reading and needs no change. Successor: none.The
sandbox-authoredverdict is now refused everywhere in the published face, which follows from "refusing exactly the verdicts the spec face refuses today exceptpending-registration". No row on this tree carries it — the sandbox limb is outside the scan by construction — so this is a widening with no live subject. It was not retired: that is not ruled here.🤖 Generated with Claude Code
https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Generated by Claude Code