feat(cli)!: os validate and os build refuse a picklistExtensions entry whose extend names no declared picklist - #21049
Conversation
…y whose extend names no declared picklist
The picklist-reference judge walked a field's `picklist` only. A
`picklistExtensions[].extend` is the same reference one key over, and
`PicklistExtensionSchema` checks only its spelling, so `extend: 'industy'`
beside `picklists: [{ name: 'industry', ... }]` parsed, validated and built.
The judge now walks the second collection on the load path's reading (the
top-level entries of a one-package stack, each `packages[]` body's own
otherwise) and gives an unresolved `extend` the same two verdicts as a
field: refused with `picklist-reference-unknown`, or an info notice
`picklist-reference-unverified` when the declaring package depends on a
package outside the stack. Both doors' refusal heading no longer says
"A field".
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…ut claiming a runtime merge The judge's header said the options of an extension with a dangling `extend` were added to a list nobody declared. No runtime reader merges extensions yet, so nothing was added; the artifact shipped an extension of an undeclared list. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 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 13e745e72a4aaeec5c8d459d391c6ca6b6badd7a && git checkout 13e745e72a4aaeec5c8d459d391c6ca6b6badd7a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d67b94280a2f2b2f1d4a3497142511ede21b8d42 69f413fb76e2dca9f395ba0116aef4d85d6182c8 && git checkout -B drift-repro d67b94280a2f2b2f1d4a3497142511ede21b8d42 && git merge --no-ff 69f413fb76e2dca9f395ba0116aef4d85d6182c8
node scripts/docs-audit/affected-docs.mjs --json d67b94280a2f2b2f1d4a3497142511ede21b8d42
|
Contract reviewServed-tier: Inputs read: card #20825 (body; triage ① Derived judgments(a) The new walk over
(b) The rule id is reused ( (c) (d) Pins and ablations.
(e) The changeset ② Semver levelRight. ③ Boundary flags
Check-runs on the head ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20825
Clause-②: no (narrowing)
This PR is the item-2 sibling that triage folded into the card (answer
5923151807): apicklistExtensions[].extendthat names no picklist is refused by the same judge on the same two doors as a field's danglingpicklist. It is one more walk, over a second collection, in the file PR #21003 landed (b84b240b2a). Nothing else is on the card: the generator and scaffold half went to #21018.What changes
os validateandos buildrefuse an extension whoseextendnames no picklist the stack declares.PicklistExtensionSchemachecks the spelling ofextendand nothing else, soextend: 'industy'besidepicklists: [{ name: 'industry', ... }]parsed.os validateexited 0 andos buildwrote the artifact carrying the extension. Now both exit 1 withpicklist-reference-unknown; the finding names the extension (where), the list it names (message), the lists the stack does declare, and thepath(picklistExtensions[N].extend, prefixed withpackages[I].manifest.for a package body).picklistExtensions. Apackages[]stack is judged on each body's ownpicklistExtensions, and a top-levelpicklistExtensionsbesidepackages[]is not judged, because the load path does not register from it (pinned). Anextendresolves against every picklist the stack declares, including one a sibling package in the same artifact owns.manifest.dependenciesentry the stack does not carry, an unresolvedextendis aninfonotice (picklist-reference-unverified) inwarningsand on the console, naming the extension, the list and the dependencies. It never gates, not even under--strict. That package's owndependenciesdecide, not a sibling's. The coarse trigger itself (any outside id downgrades every unresolved reference in that package) is unchanged: it stays the recorded line triage kept.Rule id: reuse
picklist-reference-unknownandpicklist-reference-unverified, no new sibling id. What is judged is one fact, a name that resolves to no picklist the stack declares, with one cure class (declare the list or correct the name) and the same two verdicts. The finding'swhere,pathand message already say which key carried it, and the sentences that differ (extend: '...'instead ofpicklist: '...', "the options it adds have no list to join", a hint that offers removing the entry instead of inlineoptions) are chosen per site inside the one judge. A second id would split one verdict in two for everything keyed on the rule: the--jsonerrors[].ruleconsumers, the door pins, and the already-pending changeset that namespicklist-reference-unknown. Nothing in the repo registers these ids in a ledger.The two doors' refusal heading no longer says "A field".
validate.ts(step 2d) andcompile.ts(step 3a-bis) printed "A field names a picklist this stack does not declare (N references)" above the refusals, which would be false for anextendrefusal. It now reads "A picklist reference names a picklist this stack does not declare (N references)". That literal and the two step comments are the only edits in those files; the call and the notice printing are unchanged, sovalidate-build-gate-parity.test.tsasked for no registration (no new identifier is called from either command; the whole cli unit tier is green).packages/spec/**,packages/lint/**, the generator and scaffold, andcollectMetadataStatsare untouched.Before / after (real
osprocesses on scratch fixtures, run-dev + tsx)Stack:
picklists: [{ name: 'industry', ... }], a fieldpicklist: 'industry', and onepicklistExtensionsentry. Before isorigin/mainat2f2fa11d75; after is69f413fb76.os validate,extend: 'industy'picklist-reference-unknownnamingpicklist extension "industy"and'industy', atpicklistExtensions[0].extendos build, same stackindustyos validate/os build,extend: 'industry'(the control)After text from
os validate:Tests
packages/cli/src/utils/picklist-references.test.ts(unit, 19 now, 10 new): the control (a resolvingextend), the refusal naming extension, list, declared lists and path, a stack that declares no picklist, a field and an extension judged in one pass without hiding each other, the outside-dependency notice, sibling-package resolution, the declaring package's own dependencies, and a top-levelpicklistExtensionsbesidepackages[]not judged.packages/cli/test/picklist-reference-doors.test.ts(integration: it spawns the CLI, so the PR tiers run it; 11 now, 5 new): a resolvingextendvalidates and builds with the artifact carrying it as authored (the control); a danglingextendis refused byos validate(JSON and text face) and byos build, which writes no artifact.scripts/ablation-replace.mjsunder the verify lock. Each mutation landed (anchor 1 to 0, blob changed) and each restore was proven (blob equal to HEAD,git diff HEADempty). The suites readsrc(vitest imports the module;bin/run-dev.jsrunssrcthrough tsx), so there is no build leg.if (false && Array.isArray(extensions))): unit 7 red and 12 green; doors 3 red (JSON, text,os build) and 8 green (both control pins and the six field pins).if (false && parsed.packages !== undefined)): the new top-level-not-judged pin goes red, along with the two per-package pins that depend on the branch (5 red, 14 green).69f413fb76:os-verify-lockruns of the door file 11/11 and@objectstack/clitypecheck exit 0 (includingcheck:test-typecheck); the unit pin 19/19;pnpm lintexit 0 (full repo,eslint . --no-inline-config). The whole@objectstack/cliunit tier, 240 files / 3411 tests, was green atec1d965497, one commit earlier; the delta to the head is one comment sentence inpicklist-references.ts.dispatch-gates --commandsderived 63 families at69f413fb76; all 63 were run, each exit code recorded before any pipe, and--ranreconciles 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. Four of them (check:i18n,check:i18n-coverage,check:i18n-walk-parity,check:dual-build-cjs-loads) first exited 3 (prerequisite not met, nothing measured: no built CLI or workspace dist); after building their prerequisites they were re-run and exited 0 on a final sweep.Acceptance notes
liveness-planned-propertyadvisory until the runtime reader (picklist metadata kind — runtime: resolvepicklist→ options when serving field metadata, validate writes against the resolved set, apply package-level extensions (phase 1 of objectstack#18164) #19519) lands; unchanged./meta).collectMetadataStats) and the coarse outside-dependency trigger stay as triage ruled: the row is cli + create-objectstack:os generate picklistand thesrc/picklistsscaffold wiring (os init / npm create), after the picklist runtime reader #19519 lands (split from #20825 item 1) #21018's, the trigger a recorded line with no positive case in the repo.Generated by Claude Code