You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2821e9f
Browse filesBrowse the repository at this point in the historyBrowse files
feat(cli)!: os validate and os build refuse a picklistExtensions entry whose extend names no declared picklist (#21049)
Fixes#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 #21003 landed (`b84b240b2a`). Nothing else is on the card: the
generator and scaffold half went to #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 (#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
#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>
feat(cli)!: `objectstack validate` and `objectstack build` refuse a `picklistExtensions` entry whose `extend` names no picklist the stack declares (#20825)
6
+
7
+
Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a `picklistExtensions[].extend` that names no picklist the stack declares; nothing is accepted that was refused before)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable changes spelling or type: `packages/spec` is untouched, and `picklistExtensions[].extend` stays the snake_case name it was. What changes is that two authoring commands now refuse one authored shape, an extension whose `extend` names no picklist in the stack. `objectstack migrate meta` could not rewrite that shape even in principle, because which list the author meant is not in the metadata. Nothing here judges a stored row. -->
10
+
11
+
**BREAKING** — an accept-set narrowing on two authoring commands, shipped as
12
+
`minor` under the launch-window convention. A stack with a `picklistExtensions`
13
+
entry whose `extend` names no picklist the stack declares — `extend: 'industy'`
14
+
beside a `picklists: [{ name: 'industry', … }]` — used to pass `objectstack
15
+
validate` and `objectstack build` (which wrote the artifact). Both now exit 1 and
16
+
name the extension and the list it names (`picklist-reference-unknown`, the rule a
17
+
field's dangling `picklist` already gets).
18
+
**One-line fix:** correct `extend` to the picklist the entry adds options to, declare
19
+
the list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file
20
+
the stack imports), or remove the entry.
21
+
22
+
**Which extensions are judged.** The ones the load path registers: the top-level
23
+
`picklistExtensions` of a one-package stack, or each `packages[]` entry's own. An
24
+
`extend` resolves against every picklist the stack declares, including one a sibling
25
+
package in the same artifact owns.
26
+
27
+
**A list from a package outside the stack is reported, not refused.** When the
28
+
package declaring the extension lists a `manifest.dependencies` entry the stack does
29
+
not carry, the list may live there, and these commands cannot read it. That
30
+
extension is an `info` notice (`picklist-reference-unverified`) in `warnings` and on
31
+
the console, naming the extension, the list and the dependencies — never a failure,
0 commit comments