Repository navigation
feat(cli,lint)!: os validate and os build refuse a field picklist that names no picklist, and lint R8 counts picklist as an options source - #21003
Conversation
os validate and os build refuse a field whose picklist names no picklist the stack declares (an info notice when the declaring package depends on packages outside the stack), os generate picklist writes NAME.picklist.ts, and lint R8 counts picklist as an options source. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…nightly-only Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…side this card's surface Adding a generator re-derives the os init wiring, and the create-objectstack wiring-parity pin then demands the blank starter wire src/picklists too (packages/create-objectstack, outside the claimed surface). The row is kept in the history of this branch for the follow-up. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 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 — 27 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 80ea3a0e97eef2d24e0ad26015be2b49b79499ef && git checkout 80ea3a0e97eef2d24e0ad26015be2b49b79499ef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 315888d660ad3e65a3e32eec47d3c5b6293547ec a044a7f500f213ad8c980eb3a14b2976b20e9b07 && git checkout -B drift-repro 315888d660ad3e65a3e32eec47d3c5b6293547ec && git merge --no-ff a044a7f500f213ad8c980eb3a14b2976b20e9b07
node scripts/docs-audit/affected-docs.mjs --json 315888d660ad3e65a3e32eec47d3c5b6293547ec
|
Contract reviewServed-tier: Inputs read: card #20825 (body; triage ① Derived judgments(a) The shared judge
(b) (c) R8 ( (d) Item 1's ingest half, measured at base. Sound. (e) The changeset Pins (triage's three), read off the head: a stack with a picklist and a field naming it validates, builds (artifact carries the list) and lints clean — door file, three tests; a dangling reference is refused by ② Semver levelRight. ③ Boundary flags
Check-runs on the head ( Implemented-by: VERDICT: PASS Generated by Claude Code |
…y whose extend names no declared picklist (objectstack-ai#21049) Fixes objectstack-ai#20825 Clause-②: no (narrowing) This PR is the item-2 sibling that triage folded into the card (answer `5923151807`): a `picklistExtensions[].extend` that names no picklist is refused by the same judge on the same two doors as a field's dangling `picklist`. It is one more walk, over a second collection, in the file PR objectstack-ai#21003 landed (`b84b240b2a`). Nothing else is on the card: the generator and scaffold half went to objectstack-ai#21018. ## What changes **`os validate` and `os build` refuse an extension whose `extend` names no picklist the stack declares.** `PicklistExtensionSchema` checks the spelling of `extend` and nothing else, so `extend: 'industy'` beside `picklists: [{ name: 'industry', ... }]` parsed. `os validate` exited 0 and `os build` wrote the artifact carrying the extension. Now both exit 1 with `picklist-reference-unknown`; the finding names the extension (`where`), the list it names (`message`), the lists the stack does declare, and the `path` (`picklistExtensions[N].extend`, prefixed with `packages[I].manifest.` for a package body). - **Same walk as the field reference, one collection added.** A one-package stack is judged on its top-level `picklistExtensions`. A `packages[]` stack is judged on each body's own `picklistExtensions`, and a top-level `picklistExtensions` beside `packages[]` is not judged, because the load path does not register from it (pinned). An `extend` resolves against every picklist the stack declares, including one a sibling package in the same artifact owns. - **Same outside-dependency arm.** When the package declaring the extension lists a `manifest.dependencies` entry the stack does not carry, an unresolved `extend` is an `info` notice (`picklist-reference-unverified`) in `warnings` and on the console, naming the extension, the list and the dependencies. It never gates, not even under `--strict`. That package's own `dependencies` decide, 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-unknown` and `picklist-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's `where`, `path` and message already say which key carried it, and the sentences that differ (`extend: '...'` instead of `picklist: '...'`, "the options it adds have no list to join", a hint that offers removing the entry instead of inline `options`) are chosen per site inside the one judge. A second id would split one verdict in two for everything keyed on the rule: the `--json` `errors[].rule` consumers, the door pins, and the already-pending changeset that names `picklist-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) and `compile.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 an `extend` refusal. 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, so `validate-build-gate-parity.test.ts` asked 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, and `collectMetadataStats` are untouched. ## Before / after (real `os` processes on scratch fixtures, run-dev + tsx) Stack: `picklists: [{ name: 'industry', ... }]`, a field `picklist: 'industry'`, and one `picklistExtensions` entry. Before is `origin/main` at `2f2fa11d75`; after is `69f413fb76`. | reading | before | after | |:--|:--|:--| | `os validate`, `extend: 'industy'` | exit 0, "Validation passed" | exit 1, `picklist-reference-unknown` naming `picklist extension "industy"` and `'industy'`, at `picklistExtensions[0].extend` | | `os build`, same stack | exit 0, artifact written carrying `industy` | exit 1, no artifact | | `os validate` / `os build`, `extend: 'industry'` (the control) | exit 0 / 0 | exit 0 / 0, artifact written | After text from `os validate`: ``` ✗ A picklist reference names a picklist this stack does not declare (1 reference) • picklist extension "industy": `extend: 'industy'` names no picklist this stack declares (declared: 'industry'), so the options it adds have no list to join. Correct `extend` to the picklist this entry adds options to, or declare that list — `picklists: [{ name: 'industy', label, options }]` in the package that owns it, or a `*.picklist.ts` file the stack imports. Or remove the entry: it adds options to nothing. rule: picklist-reference-unknown at picklistExtensions[0].extend ``` ## Tests - `packages/cli/src/utils/picklist-references.test.ts` (unit, 19 now, 10 new): the control (a resolving `extend`), 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-level `picklistExtensions` beside `packages[]` 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 resolving `extend` validates and builds with the artifact carrying it as authored (the control); a dangling `extend` is refused by `os validate` (JSON and text face) and by `os build`, which writes no artifact. - Ablations on the committed fix, through `scripts/ablation-replace.mjs` under the verify lock. Each mutation landed (anchor 1 to 0, blob changed) and each restore was proven (blob equal to HEAD, `git diff HEAD` empty). The suites read `src` (vitest imports the module; `bin/run-dev.js` runs `src` through tsx), so there is no build leg. - New walk disabled (`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). - Package branch disabled (`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). - At `69f413fb76`: `os-verify-lock` runs of the door file 11/11 and `@objectstack/cli` typecheck exit 0 (including `check:test-typecheck`); the unit pin 19/19; `pnpm lint` exit 0 (full repo, `eslint . --no-inline-config`). The whole `@objectstack/cli` unit tier, 240 files / 3411 tests, was green at `ec1d965497`, one commit earlier; the delta to the head is one comment sentence in `picklist-references.ts`. - Gates: `dispatch-gates --commands` derived 63 families at `69f413fb76`; all 63 were run, each exit code recorded before any pipe, and `--ran` reconciles 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 - A picklist-bound field still raises the `liveness-planned-property` advisory until the runtime reader (objectstack-ai#19519) lands; unchanged. - Not judged by this change: the runtime metadata write path (Studio or REST `/meta`). - The metadata summary row for picklists (`collectMetadataStats`) and the coarse outside-dependency trigger stay as triage ruled: the row is objectstack-ai#21018's, the trigger a recorded line with no positive case in the repo. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20825
Clause-②: no (narrowing)
This PR lands items 2 and 3 of the direction on the card (triage
5908852656). It measures item 1's ingest half. Theos generate picklisthalf of item 1 is NOT here: it needs files outside this card's claimed surface (see "Held" below). #20825 stays open for that half.What changes
os validateandos buildrefuse a field whosepicklistnames no picklist in the stack (item 2). Ruling5755653853item 3 asks for this loud refusal.FieldSchemachecks the spelling of the name and nothing else, so before this change a typo parsed.os validateexited 0, andos buildwrote the artifact with the typo in it. Now both doors exit 1 withpicklist-reference-unknown. The error names the field (where), the list it names (message), the lists the stack does declare, and thepath.findViewContainerNameRefusalsmakes. A one-package stack is judged on its top-levelobjectsandobjectExtensions. Apackages[]stack is judged on each body's own. A reference resolves against every picklist the stack declares, including one that a sibling package in the same artifact owns.manifest.dependenciesmaps package ids to version ranges. Nothing maps such an id to a file the CLI could open, so these doors cannot read another package's picklists. Take a reference that resolves nowhere in the stack. If the package declaring the field depends on a package the stack does not carry, the reference gets aninfonotice (picklist-reference-unverified). The notice names the field, the list and the dependencies. It rideswarningsand the console, and it never gates, not even under--strict. If the package declaring the field has no outside dependency, the reference is refused. That package's owndependenciesdecides, not a sibling's.packages/cli/src/utils/picklist-references.ts, is called by both doors right after the parse:validate.tsstep 2d andcompile.tsstep 3a-bis.validate-build-gate-parity.test.tsclassifies it as a shared non-registry gate. It is not an@objectstack/lintregistry rule because the verdict depends on WHICH package declares the field, and the union run hands a registry rule the flattened top level, which carries no package provenance.Lint R8
field/select-missing-optionscountspicklistas an options source (item 3). A select, multiselect or radio field that names a picklist is no longer reported. A select with neither source still warns (the control), and itsfixnow namesoptionsandpicklistas alternatives. Before this change the warning pointed atoptions, the key the schema refuses besidepicklist.Item 1, ingest: already true on
origin/main, measured, no change.os buildwritespicklistsandpicklistExtensionsinto the artifact as authored, and the field keepspicklist. Both doors parse throughObjectStackDefinitionSchema, which declares both collections.metadataFileName('picklist', 'industry')already answersindustry.picklist.ts, from the registry's**/*.picklist.ts.Held: the
os generate picklistrowThe generator row was written (
6ef78d3f29) and taken back out (e16359a9fa). A new generator re-derivesos init's wired barrels.create-objectstack-wiring-parity.test.tsthen requires thenpm create objectstackblank starter to wiresrc/pickliststoo. Measured: with the row in place, 4 tests in that file were red, and the other 239 files of the cli unit tier were green. Landing the row needs these files, none of them in this card's claim:packages/create-objectstack/src/templates/blank/objectstack.config.ts, plus a newsrc/picklists/index.tsand a create-objectstack changeset;content/docs/deployment/cli.mdxand the type list inpackages/cli/README.md;skills/objectstack-platform/SKILL.md, whose "seven generator barrels" line names the starter's barrels. That path is Tier H.The PM decides on that wider surface. The report on #20825 sets out the options.
Before / after (real
osprocesses on scratch fixtures, run-dev + tsx)4957ee5ef0)a044a7f500)os validate, fieldpicklist: 'industy', stack declaresindustrypicklist-reference-unknownnamingpick_account.industryand'industy'os build, same stackindustyos validate, same typo,manifest.dependenciesnames an outside packagepicklist-reference-unverifiedinfo lineos validate/os build/os lint, stack with picklistindustryand a field naming itos lintR8 on that fieldos generate picklist industryTests
packages/cli/src/utils/picklist-references.test.ts(unit, 9): the walk, the two verdicts, sibling-package resolution, and the declaring package's own dependencies.packages/cli/test/picklist-reference-doors.test.ts(integration: it spawns the CLI, so the PR tiers run it, not the nightly one; 6): all three doors pass a correct stack, the artifact carries the list,os validaterefuses the typo naming the field and the list (JSON and text), andos buildrefuses it and writes no artifact.packages/lint/src/data-model-rules.picklist-options-source.test.ts(6): R8 for select, multiselect and radio, the neither control, the fix naming both sources, and the empty-string control.scripts/ablation-replace.mjs; each restore was proven (git diff HEADempty, blob equal to HEAD):validate.tsrefusal disabled: 2 red (the twoos validatepins), 4 green.compile.tsrefusal disabled: 1 red (theos buildpin), 5 green.picklistlimb removed: 3 red (select, multiselect, radio), 3 green (the controls).a044a7f500:@objectstack/cliunit tier 240 files / 3401 tests green; the door file 6/6;@objectstack/clitypecheck exit 0;@objectstack/linttypecheck exit 0;@objectstack/linttests 118 files / 5450 tests green. The cli integration tier beyond the new file is declared to CI.dispatch-gates --commandsunion andpnpm lint. The report on picklist kind: theosCLI compile / validate / lint path accepts*.picklist.ts,picklistsandpicklistExtensions, andos validaterefuses apicklistthat names no picklist (Scope 6 of #19518) #20825 gives each reading against its head.Acceptance notes
liveness-planned-propertyadvisory ("setspicklistbut this field property is planned"). That advisory is true until the runtime layer, picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518's sibling 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, reads the reference. It belongs topackages/spec's liveness ledger and is left alone here./meta), and apicklistExtensionsentry whoseextendnames no picklist.os validate/os build/os infoprint has no picklist row (collectMetadataStats,utils/format.ts). It is an observation only and nothing reads it as a verdict.Generated by Claude Code