Repository navigation
feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal - #21047
Conversation
…dit, write door Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
… audit, write door and import-template parity Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…g changeset no longer says the reference is unresolved Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…ards Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…— served, written, refused, relabelled Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…n the served read; dogfood serves the list item translated Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…a list nothing declares Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
…ook services up by contract type Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 141 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 c6eda0de7185377662375bf076d335d149133e38 && git checkout c6eda0de7185377662375bf076d335d149133e38
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6073bb96b878eb2980724568ee526157b29ab141 42d750f197f8c38669ef91ec918d672d6db8adbc && git checkout -B drift-repro 6073bb96b878eb2980724568ee526157b29ab141 && git merge --no-ff 42d750f197f8c38669ef91ec918d672d6db8adbc
node scripts/docs-audit/affected-docs.mjs --json 6073bb96b878eb2980724568ee526157b29ab141
|
…od proof; pin object.fields.picklist as a picklist reference site Claude-Session: https://claude.ai/code/session_01MZu5JqVPacMktogpMxq9Xu Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #21047 (card #19519, the runtime sub-issue of #18164), reviewed read-only. Inputs: the card body and all seven comments (pointers 5912707861 and 5916040159, triage 5921928091, claim 5923202473, dispatch record 5923224221, the os-dev-report 5924129203 and the seat answer 5924290772); the parent's rulings 5755653853 and 5904864936 and the design of record 5715762696 they cite; the PR body, its 25-file list and the net diff against the merge base with Checks on the head
① Derived judgmentsEvery accept-set or public-surface change the diff implies, named right or wrong against the card, the rulings and the spec on
② Semver levelChangeset Clause-②: no — holds on this diff. No schema key, error code, accepted shape or spec export is added; the one spec non-test file (
Deliberate correction, owed in this record. The corrected note is
③ Boundary flagsOpen questions of report 5924129203, ruled by the seat answer 5924290772:
Pointer 5912707861 (runtime enumerations and gaps):
Pointer 5916040159 (import-template parity): the Dev flags in the PR body and the report:
Required on the next head: resolve the Implemented-by: VERDICT: FAIL Generated by Claude Code |
Contract reviewServed-tier: PR #21047 (card #19519, the runtime sub-issue of #18164), reviewed read-only as a DELTA from the previous head The delta, Checks on the head
① Derived judgmentsThe delta's two changes, then the carried judgments.
Nothing in the diff is judged WRONG on this head, and no gap remains open. ② Semver levelChangeset Clause-②: no — holds on the net diff against the new base. No schema key, error code, accepted shape or spec export is added; the one spec source file (
Deliberate correction, owed again on this head. The corrected note is The three facts the queue rule needs:
Also read: #20976, on ③ Boundary flagsPatch-round report 5924919167: Dev flags in the patch-round report:
The escalated item from the prior record, the metadata-protocol reference sites: answered on the card by the report's measurement and pinned in the diff; judged adequate in ①. Closed; nothing further to escalate. The prior record's "Required on the next head" is met in full: the Carried from the prior record as acceptance notes, unchanged by the delta: the three out-of-surface edits (the lint pin, the one 19518 sentence, the translation and README ledger rows), each the mechanical consequence of the ledger flip or of ②; the not-changed list with its reasons ( Implemented-by: VERDICT: PASS |
…orWarn opt-in, and never shows the ledger note (objectstack-ai#16094) (objectstack-ai#21092) Fixes objectstack-ai#16094 Clause-②: no Ruling-ref: 5560227939 ## What changes `shouldWarn` in `packages/lint/src/lint-liveness-properties.ts` now admits a ledger row whose status is `dead`, `live-elsewhere` or `experimental`, or that opts in with `authorWarn: true`. Before, only `experimental` warned without an opt-in. `describe()`'s mapping to `liveness-dead-property` / `liveness-live-elsewhere-property` is unchanged. Before the change, both of those rule ids were exported and could never be produced: across the shipped ledgers, not one `dead` or `live-elsewhere` row set `authorWarn`. `checkItem` changes only for rows the ruling newly admits. A row that warns only because of its `dead` / `live-elsewhere` verdict (no `authorWarn`) shows its `authorHint`, else the verdict's existing default hint, and never its ledger `note`. Rows that opt in with `authorWarn`, and `experimental` rows, keep their hint exactly as before. This is the seat's Q3 answer (card comment 5925292339). Other changes: - **Stale comments.** Comments the flip made false are corrected in the same file. The one stale line above the `lintLivenessProperties` registry entry in `authoring-rules.ts` is corrected too. - **Changeset.** One `minor` changeset for `@objectstack/lint`, carrying `Clause-②: no` in its body. ## Recount on `main` Measured at base `6073bb96b8`, all depths: | Rows | Count | |---|---| | `dead` | 109 | | `live-elsewhere` | 1 | | Of those 110, `retiredKey` tombstones (`check:liveness --json`) | 97 | | Authorable | 13 (12 `dead`, 1 `live-elsewhere`) | | Authorable and reachable through the rule's walk | 4 | The 4 reachable rows are `view.name`, `view.label`, `permission.rowLevelSecurity.label` and `permission.rowLevelSecurity.description`. The other 9 authorable rows sit in types the walk never visits: - `connector.metadata` - `manifest.runtime` and `manifest.integrity` - six `realtime_subscription` rows The warn map grows by 105 entries, at depth 1 or less. None of the 105 carries an `authorHint`, and 80 of their notes cite a tracker id. That is why the hint selection changed in this PR. ## The live-elsewhere end-to-end pin: measured impossible, replaced by an equivalent invariant plus a reach sentinel The ruling asks for an end-to-end pin proving `liveness-live-elsewhere-property` is produced against the shipped ledger from its one `live-elsewhere` row. That row is `manifest.runtime`. **Measured: no stack can produce that id through `lintLivenessProperties`.** - `manifest` is not in `TYPE_COLLECTIONS`, and it is not one of the bespoke walks (objects/fields, translation bundles). - `stack.manifest` is a single object, not a collection. - `authorWarnedProperties` has one caller outside its package, `packages/cli/src/utils/i18n-extract.ts`, and that caller asks only about `translation`. No translation row is newly admitted. **The invariant that replaces the literal pin is equivalent for the ruling's purpose, plus a reach sentinel.** Three pins on the shipped ledger hold it: 1. The shipped row is admitted: `authorWarnedProperties('manifest').has('runtime')`. 2. The shipped row maps to `LIVENESS_LIVE_ELSEWHERE_PROPERTY`: `checkItemAgainstWarnMap` over the row as read from the shipped `manifest.json`. 3. **No walk visits `manifest`.** `lintLivenessProperties({ manifest: { runtime, integrity } })` says nothing about either key. This pin goes red the day any walk visits `manifest`, and its comment names `manifest.integrity`. Pin 3 matters because `manifest.integrity` is `dead` in the ledger, yet `os plugin publish` reads its map and refuses on a digest mismatch (`packages/cli/src/commands/plugin/publish.ts:134`). A manifest walk added before that row is re-graded would tell authors a gate-read key is inert. Pin 3 makes that walk a deliberate, visible change. Because `manifest` is not walked, the flip does not surface `manifest.integrity` to any author. ## `--strict`, and why `Clause-②` stays `no` Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` go from exit 0 to exit 1 on a stack that was otherwise warning-clean and authors a view container `label`/`name` or an RLS policy `label`/`description`. Measured with the CLI built from source on fixture stacks, before (base) and at this head: | fixture | `os lint` | `os lint --strict` | `os validate` | `os validate --strict` | |---|---|---|---|---| | clean | 0 → 0 | 0 → 0 | 0 → 0 | 0 → 0 | | view container `label` | 0 → 0 | **0 → 1** | 0 → 0 | **0 → 1** | | RLS policy `label` + `description` | 0 → 0 | **0 → 1** | 0 → 0 | **0 → 1** | | raw config (no `defineStack`) with tombstoned `list.striped` | 0 → 0 | **0 → 1** | 1 → 1 | 1 → 1 | The `Clause-②` criterion is "widens the accept set or enlarges the public surface". A new warning does neither, and both rule ids were already exported. The seat ruled on this as Q1 (card comment 5925292339), citing two precedents: `.changeset/20654-flow-credential-literal-advisory.md` and lint `d753744`. Other doors: - `os build` has no warning-promoting flag. - The runtime publish door runs this rule for `email_template` / `mapping` / `datasource` only, and none of those gains a row. - The `os lint --eval` corpus has no views and no RLS. - `check:i18n-coverage` counts only `i18n/` rules. - No repo gate asserts a zero-warning count on the examples. **Bump: minor.** The at-tier contract review (`5925918475`) set this, and it corrects the earlier `patch`. No export is added or removed, and the default-face accept set is unchanged. But two rule ids become producible, which a consumer sees as two new advisories, and a non-zero exit becomes reachable under `--strict`. The repository grades that shape `minor`: `.changeset/20654` is a new advisory with `Clause-②: no` at `minor`, and the cli 17.5.0 entry grades "the new advisories and the newly reachable non-zero exit" `minor`. `d753744` is a fix to an existing finding, not a precedent for this. The changeset carries `Clause-②: no` in its body. ## Who starts warning These are this repository's example apps, run with `os lint --json` and `os validate --json`, at base and at this head: | example | new findings | exit codes (4 modes) | |---|---|---| | `app-showcase` | 2 × `liveness-dead-property`: permission `showcase_contributor`, `rowLevelSecurity.label` and `.description` | unchanged | | `app-crm` | 0 | unchanged | | `app-todo` | 0 | unchanged | | `app-multi-package` | 0 | unchanged | All four examples already exit 1 under `--strict`. **The two showcase findings' printed hint.** At this head, both read: `Remove it — it is declared in the spec but not consumed at runtime.` That is 69 characters. Before the hint fix, both printed the ledger note instead (939 and 270 characters of maintainer prose). The 270-character one ended "Benign, not authorWarn'd." The showcase's two existing `liveness-planned-property` findings (`externalSharingModel`) are byte-identical, message and fix, between base and this head. ## Retired keys A `retiredKey` tombstone keeps its `dead` row. - **Commands that parse.** `os validate`, `os build` and the runtime gate refuse the key first, so it gets no second report. A `defineStack` config with a retired key fails `os lint` at load as before. - **`os lint` on a raw config.** `os lint` does not Zod-parse. A raw config without `defineStack` that `os lint` accepts, and that carries a retired key, now gets a `liveness-dead-property` warning with the default "Remove it" hint. Before, it got nothing. Ten existing pins asserted silence on such keys. That silence came from the opt-in, not from the tombstone. They are re-judged to assert the `dead` grade, with the parse control alongside: `ViewSchema` refuses `list.striped`. ## Tests All results below are at head `49fb6cfad3`, after merging `origin/main` at `9c8b65aa23` (which carries objectstack-ai#21047). - **Scoped file.** `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts`: 93 passed, against 84 at base. There are 9 new pins, and the 10 silence pins are re-judged. - **Whole package.** `@objectstack/lint`: 118 files, 5486 tests passed. `pnpm --filter @objectstack/lint typecheck`: exit 0. - **CLI.** Unit tier: 240 files, 3419 tests passed. Integration tier: all 69 files, run as 4 lock-held runs, 591 passed and 1 skipped. `lint-per-package-authoring-parity` passes 5/5. - **Gates.** `dispatch-gates --commands`: 61 families, all exit 0. `--ran`: 61 derived, 61 run, 0 NOT-MEASURED. - **eslint.** A narrowed run over the 4 changed `.ts` files: 0 errors, 0 warnings. The new pins, end to end against the shipped ledger: - an authorable `dead` key, `rowLevelSecurity.label` / `.description`, produces `liveness-dead-property`; - the live keys beside it stay silent; - the dotted path fans out past index 0; - the fixture parses through `PermissionSetSchema`; - a tombstoned key is refused at parse; - the three `manifest` pins described above. **Hint pins, ledger-wide:** - All 105 verdict-triggered rows show a hint that is not their note and that carries no tracker id. Anti-vacuity: 80 of those notes do carry one. - **Control:** all 9 rows that warned before the ruling (`authorWarn` or `experimental`) keep `authorHint ?? note` byte for byte. Anti-vacuity: 6 of them show their note today. - Synthetic pins hold the precedence per verdict and opt-in. **Ablations**, through `scripts/ablation-replace.mjs`: the anchor hit and the blob moved each time. Every restore left the blob equal to HEAD and `git diff HEAD` empty, under a trap. | Mutation | Result | What it proves | |---|---|---| | Verdict set reduced to `{experimental}` (pre-ruling behaviour) | 12 red, including both new verdict pins | the verdict pins depend on the flip | | Hint selection reverted to `authorHint ?? note ?? default` | 3 red: the three Q3 pins | the Q3 pins catch the old precedence | | Hint selection widened to never show any note | 3 red, including the byte-for-byte control | the control can fail | The test subject resolves to `src` through the relative import, so no dist build was involved in any ablation. **Narrowed eslint** (the 4 changed `.ts` files, as in Tests above): - Population: `eslint.config.mjs`'s `**/*.{ts,…}` and `packages/**/*.{ts,…}` blocks select all four; the changeset is not linted. - Invariance: the config enables no type-aware linting (no `parserOptions.project` or `projectService`), so this diff cannot move any untouched file's verdict. **Control bytes.** A self-scan of the 5 changed files finds none. `check:nul-bytes` exits 0. ## The CLI parity fixture, re-judged The objectstack-ai#18778 fixture in `packages/cli/test/lint-per-package-authoring-parity.test.ts` declared view containers as `{ name, label, object, list }`. `view.name` and `view.label` are `dead`, so after the flip the union run raised 4 warnings, which broke the fixture's premise that the union raises nothing. CI on `64e0275024` failed two tests (`expected 5 to be 1`, at `:241` and `:279`). The containers now bind by `object` alone (`list.label` is live and stays), with a comment saying why. Each test keeps its intent: the union is clean, there is one per-package survivor, and `--strict` fails with 1. ## Acceptance notes - **Pre-existing note leak on opted-in rows (not touched here, as ruled).** `object.externalSharingModel` is `planned` + `authorWarn` with no `authorHint`, so authoring it prints its ledger note as the fix. That note is 728 characters and cites `objectstack-ai#2696`. Measured on `app-showcase` at this head: 2 findings (`showcase_account`, `showcase_announcement`). Across the shipped ledgers, 6 of the 9 rows that warned before the ruling show their note. AGENTS.md keeps tracker numbers out of anything an author is shown. - **A manifest walk, and the `manifest.integrity` re-grade it would need first.** These have zero measured pull. No example or plugin config in this repository authors `runtime` or `integrity`; `os plugin build` writes `integrity`. The note that deferred the row's status answers 404. No carrier. - **`connector` is not walked.** After the flip, `connector` (a real stack collection, `connectors`) carries a warn-worthy row, `metadata`. By its own note it is an uninterpreted extension bag whose specification is "no consumer", and `connector` is not in `TYPE_COLLECTIONS`. Registering it would warn on every authored `metadata` bag. No carrier. - **objectstack-ai#21047 landed** (`88b484e00c`) and is merged here. The field-set pin is measured, not assumed, at `['conditionalRequired']`: `field.picklist` is `live` with no `authorWarn` after objectstack-ai#21047, and `field.conditionalRequired` is a `dead` tombstone. - **Merge state.** `origin/main` was merged at `9c8b65aa23` (`b0705c1530`). The 6 later `main` commits touch neither `packages/lint`, `packages/spec/liveness` nor the CLI parity test. - **`mapping.connectorSource` crash on `main`, not this PR's.** `main` ships `mapping.connectorSource` as `live` + `authorWarn` (8368f1c), and that crashes `os lint` and `os validate` on any stack authoring it: `describe()` throws its integrity sentinel. This PR's hint control is scoped to rows `describe()` answers, and its COVERAGE pin keeps the sentinel loud. Filed as objectstack-ai#21127. --- _Generated by [Claude Code](https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… starter barrel, and a Picklists count in the metadata summary (objectstack-ai#21167) Part of objectstack-ai#21018 Clause-②: yes (widening) This PR is the code half of objectstack-ai#21018. It covers scope items 1, 2, 3 and 5 of the card body. Item 4, the "seven generator barrels" line in `skills/objectstack-platform/SKILL.md`, is on a Tier H governed path. It follows in its own PR under a second claim, so objectstack-ai#21018 remains open for it. Dispatched by the `domain:cli` seat's PM under claim 5929373213. Dev session `session_01VvcEokUG1tvVxkceYfR5XB`. ## Premise, checked on `origin/main` The card waited on the runtime reader. That reader is on `main` as 88b484e (objectstack-ai#21047), and these are the parts this PR relies on: - `picklists` and `picklistExtensions` are in `METADATA_ARRAY_KEYS` (`packages/objectql/src/engine.ts`). - The registry fold resolves a field's `picklist` into served `options` (`picklist-resolution.ts`, `resolvePicklistFieldsOnto`). - The write door judges a write against the resolved options. - The boot refuses an unresolved name (`packages/objectql/src/plugin.ts`). The repro below measures all of this end to end on a scaffolded project. ## What changed **Item 1: `os generate picklist NAME`** (`packages/cli/src/commands/generate.ts`) - This is the row written and reverted on objectstack-ai#20825's branch (`6ef78d3f29`, reverted in `e16359a9fa`), re-read against the landed reader. - It writes `src/picklists/NAME.picklist.ts` through `definePicklist`. The file name comes from `metadataFileName`, with no override. - The list is collected under `picklists` (`singularToPlural`). `namesObject: false`, and `requires` is empty. - The emitted header states the next rule an author meets: a field that names the list declares no `options` of its own. - The row's comment now names the reader: the authoring doors, the boot refusal, the served options and the write judgement. - Two existing comments counted the roster ("an app or a skill", "14 emission sites across all 7 generators"). Both are reworded so the new row does not make them false. **Item 2: scaffold wiring** - `os init` derives its wiring from the roster (`SCAFFOLD_WIRED_BARRELS`), so the `app` and `plugin` templates wire `src/picklists` with no edit to `init.ts`. - The blank starter in `create-objectstack` gains `src/picklists/index.ts`, byte-identical to the empty barrel `os init` writes. Its `objectstack.config.ts` gains `import * as picklists from './src/picklists';` and `picklists: exportsOf(picklists),`. - `create-objectstack-wiring-parity.test.ts` holds the two scaffolders equal. **Item 3: docs** - `content/docs/deployment/cli.mdx` gets the example line, the table row (`picklist`, `src/picklists/`, `NAME.picklist.ts`, `picklists`) and the "What it does" clause. - `packages/cli/README.md` gets the type list and a short paragraph. **Item 5: the metadata summary** (`packages/cli/src/utils/format.ts`) - `MetadataStats` gains `picklists`, and `collectMetadataStats` counts it through the existing `authoringRuleUnionStack` fold, so both ADR-0130 D4 shapes are covered. - `printMetadataStats` renders it in the `Data:` row, the kind's own domain (`domain: 'data'` in the registry). - A stack with no picklists prints the row it printed before. A `picklistExtensions` entry is not counted as a list. - The two existing test fixtures that spell out every `MetadataStats` member gain `picklists: 0`: `format.metadata-stats-package-fold.test.ts` and `print-metadata-stats-zero-row.test.ts`. **Changesets** - `@objectstack/cli`: `minor`. A new generator kind, and `stats.picklists` is added to the `--json` output of `os validate`, `os build` and `os info`. - `create-objectstack`: `minor`. The starter gains a wired barrel. - Both carry `Clause-②: yes (widening)`. Nothing is narrowed and nothing is renamed, so there is no ADR-0087 marker. ## Repro, before and after Before, on `origin/main` 58a77db: ```text os generate picklist industry exit 1 ✗ Unknown type: picklist (roster lists 7 types) npm create objectstack (blank) src/ = actions apps dashboards flows objects skills views hand-wired picklist + select field os validate exit 0 Data: 1 Objects 3 Fields ``` After, on this branch: ```text npm create objectstack (blank) src/ = actions apps dashboards flows objects picklists skills views os generate picklist industry exit 0 ✓ Reaches the stack: objectstack.config.ts carries it in `picklists` as 'industry' note.object.ts gains industry: Field.select({ picklist: 'industry', label: 'Industry' }) os validate exit 0 Data: 1 Objects 3 Fields 1 Picklists os build exit 0 Data: 1 Objects 3 Fields 1 Picklists (artifact carries `picklists` and the field's `picklist`) os info exit 0 Data: 1 Objects 3 Fields 1 Picklists os info --json stats.picklists = 1 os dev --fresh -p RANDOM GET /api/v1/meta/object/my_app_note 200 fields.industry = { picklist: 'industry', options: [option_a, option_b], … } GET /api/v1/meta/picklist/industry 200 POST /api/v1/data/my_app_note { industry: 'option_a' } 201 POST /api/v1/data/my_app_note { industry: 'option_z' } 400 VALIDATION_FAILED · invalid_option · names picklist "industry" ``` On a project scaffolded before this change (its config wires no `src/picklists`), `os g picklist region` exits 0. It reports `Not wired` and prints the import line and the `defineStack` key to add. ## Tests The pins are: - `packages/cli/src/commands/generate-picklist.pin.test.ts`: - the roster row (`src/picklists`, stack key `picklists`, `namesObject: false`, no `requires`) and the `NAME.picklist.ts` file name; - the scaffold loaded through the loader `os validate` uses (`bundle-require`, `BUNDLE_REQUIRE_EXTERNALS`), parsed by `PicklistSchema`, under the item name `os g` reports; - that scaffold registered through `ObjectQL.registerApp` under `picklists`, serving its options on a `Field.select({ picklist })` field, and passing `PicklistServedFieldSchema`; - a control: the same field with no list serves no options. It constructs `ObjectQL`, so it lands in the `integration` tier. - `packages/cli/src/utils/format.metadata-stats-picklists.test.ts`: - the count, top level and option-B; - that an extension is not a list; - the zero case; - the rendered row `Data: 1 Objects 2 Fields 1 Picklists`; - a control: a stack with no picklists prints `Data: 1 Objects 2 Fields`. The runs, all on HEAD 5a53515 except where noted: | Run | Files | Tests | Result | |:---|---:|---:|:---| | `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` | 241 + 2 | 3427 + 29 | All passed, 29 skipped. The 2 files first failed with `packages/cli is not built` and passed after `pnpm --filter @objectstack/cli build`. | | `--project integration`, the 8 integration files among the related tests | 8 | 66 | Passed: `generate-picklist.pin`, `create-objectstack-stack-reach`, `generate-stack-reach`, `info-detail-package-fold` and 4 more | | `OS_TEST_TIERS=nightly`, the related `*.e2e` files | 6 | 42 | Passed. Includes `generate-scaffolds-reach-stack.e2e`: "`os g picklist` exits 0 and reports no wiring to add" | | `pnpm --filter create-objectstack exec vitest run` | 16 | 249 | Passed | | `pnpm --filter @objectstack/cli typecheck` (`tsc --noEmit && check:test-typecheck`) | | | Exit 0. The test-typecheck ledger is unchanged (3 files / 28 errors) | | `pnpm --filter create-objectstack typecheck` | | | Exit 0 | ## Ablation The ablations ran on the committed fix (e63b1db). Each mutation went through `scripts/ablation-replace.mjs` with the anchor counted on disk. Each restore was proven: the blob equals HEAD and `git diff HEAD` is empty. | Leg | Mutation | Red | Control (green) | |:---|:---|:---|:---| | generator | The `picklist` row deleted from `GENERATORS` (anchor x1 → x0, 53 lines) | `generate-picklist.pin` 3 of 5. `wiring-parity`: imports and stack keys (2) | `generate-scaffold-validates` 19/19; the pin's file-name case and its no-list control | | template | `picklists: exportsOf(picklists),` deleted from the blank config | `wiring-parity`: "hands every wired barrel to its stack key" (1 of 22) | the other 21 | | collect | `picklists: count(stack.picklists),` deleted | `metadata-stats-picklists` 5 and `package-fold` 4 (its `ZEROES` comparisons) | the pin's no-picklists row; `print-metadata-stats-zero-row` | | print | `['Picklists', stats.picklists],` deleted | `metadata-stats-picklists` "Data: row", and `print-metadata-stats-zero-row` "every metric collectMetadataStats counts is rendered" | the other 27 | ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with no paths, derived 96 families. All 96 were run, each with its exit code recorded before any pipe. - Five of them first refused with `PREREQUISITE NOT MET` (exit 3) because their build inputs were missing: `check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity`. All five passed once those inputs were built. - `--ran` reconciliation: `✓ dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED`. **`pnpm lint`, as a proven narrowing** (on HEAD 5a53515): 1. **Population, read from eslint's own config** (`isPathIgnored` / `calculateConfigForFile`): all 8 touched `.ts` files are linted. The 4 touched `.md` / `.mdx` files are outside its population. 2. **Count, from `--format json`**: 8 files, 0 errors, 0 warnings, with `--no-inline-config`. 3. **Invariance**: no resolved config for these files is type-aware (no `parserOptions.project`, no `projectService`, as `eslint.config.mjs` states). This diff touches no lint config or baseline. So it cannot move the verdict on any file it does not touch. ## Not in this PR - **Item 4** (Tier H). On `db48028f1a`, lines 194–195 of `skills/objectstack-platform/SKILL.md` read: ```text generic connector executors in `plugins:`; and the seven generator barrels (`objects`, `views`, `actions`, `flows`, `dashboards`, `apps`, `skills`), ``` The proposed text, net zero lines: ```text generic connector executors in `plugins:`; and the eight generator barrels (`objects`, `views`, `actions`, `flows`, `dashboards`, `apps`, `skills`, `picklists`), ``` - Not carried here, as the card says: the `picklist-reference-unverified` trigger, and a dangling `picklistExtensions[].extend` (folded into objectstack-ai#20825). ## Acceptance notes - **The two starter README listings now name `src/picklists`** (commit `90d55b1e13`, added on contract review `5930680827`): - the `Layout` bullet in `packages/create-objectstack/src/templates/blank/README.md`, which ships into every new project; - the tree in `packages/create-objectstack/README.md`. They ride this PR, not the Tier H item-4 PR, so two non-governed lines do not wait on a human approval. - **`Scaffold with repo dist` is red on this PR, and the PR is held until the next release publishes.** This note was added by the seat. - **Cause:** the job scaffolds with the repo-built `create-objectstack`, then installs the project's framework from the npm registry. The template's `^17.0.0` range resolves to the published `@objectstack/spec` 17.5.0, which predates the `picklists` stack key (`addbbf0`, `.changeset/19518-picklist-kind.md`, still unreleased). So the generated project's `defineStack` refuses the key as unrecognized at the validate step. - **Scope of the measurement:** the repro table's `npm create objectstack` rows and the end-to-end chain were measured on the repo build, not against the registry. - **It is not this PR's code to fix.** Its gate checks the template against the real registry, and the template now runs one release ahead of it. - The check is not required. It runs only on `pull_request` under its path filter (`packages/create-objectstack/**`, `docker/**` and its own workflow file), and it is not red on `main`. - Landing now would leave it red on every later `create-objectstack` PR until that release. - **The hold:** the release that carries the key is the open Version Packages PR objectstack-ai#20639; its `@objectstack/spec` changelog already lists `addbbf0`. Once it merges and publishes, a re-run of this job reads the new spec and the template is coherent with it. - Until then, the PR stays draft and its card is `pm:blocked` on objectstack-ai#20639. - On a project scaffolded by an earlier release, the not-wired hint prints `picklists: Object.values(picklists),`, not the starter's `exportsOf(...)` spelling. Both type-check once the barrel exports a list, which is the only state the hint is printed in. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19519
Clause-②: no
The runtime layer of the shared
picklistkind, phase 1 of #18164. The spec layer (#19518) is already onmain. A select field that authorspicklist: 'industry'is now served with that list's resolved options, a write is judged against them, andpicklistExtensionsfrom other packages merge into the list additively.Dispatched by the
domain:engineseat 2 PM under claim 5923202473. Dev sessionsession_01MZu5JqVPacMktogpMxq9Xu.What changed
Load (
packages/objectql/src/engine.ts,packages/metadata/src/plugin.ts)picklistsandpicklistExtensionsjoinMETADATA_ARRAY_KEYS, first in the list. Both registration seams (manifest and nested plugin) reach them through the sharedregisterMetadataCollectionsbody.picklistExtensionsentry is dispatched toSchemaRegistry.registerPicklistExtension, never registered as an item of its own.pickliststo thepicklistkind, soGET /meta/picklist/NAMEserves the list on an artifact boot.Merge (
packages/objectql/src/registry.ts, newpicklist-resolution.ts)422 INVALID_METADATA, naming both declarations. This holds in either registration order: list then extension, or extension then list. It is never last-wins.Serve
foldExtendersOntoDefinition, shared byresolveObject, the owner layer andfoldObjectExtendersOnto) writes the resolved options onto every field that names a list, and keepspicklist(PicklistServedFieldSchema).sys_metadatabodies, which the protocol folds throughfoldObjectExtendersOnto.translateObject/translatePicklistresolvers relabel the resolved options per request. The dogfood case drives this end to end.Unknown name: a load-time error (
packages/objectql/src/plugin.ts)kernel:readyevery package has registered, so "not declared" is final (AGENTS.md, startup registry reads). A packaged field naming a list nothing declares fails the boot withINVALID_METADATA, naming every such field and its package. ApicklistExtensionsentry extending such a list fails the same way.manifestservice is checked before any of it registers, so a refused install registers nothing.Write validation (
packages/objectql/src/validation/record-validator.ts)optionsjudges the resolved set, and the refusal names the list. The wire code staysinvalid_option.packages/spec/src/system/validation-message.ts):invalid_option_picklist,invalid_option_value_picklistandinvalid_option_picklist_unresolved. They change the message text only and never reach the wire.Error codes (H5). Both refusals reuse
INVALID_METADATA.@objectstack/objectqlalready emits it in the ADR-0112 ledger. No new code, soClause-②: nostands.Enumerations and ledgers
scripts/check-stack-collection-maps.mjs: theMETADATA_ARRAY_KEYSPENDING row is retired. TheARTIFACT_FIELD_TO_TYPErow now carriespicklistExtensionsonly, with a reason: it is merged by the registry and is not a kind.APP_CATEGORY_KEYS/SECURITY_FIELDSkeep their DELIBERATE rows. One is an app-payload heuristic, the other a four-collection security subset, and neither should list picklists.serializers/typescript-serializer.tsannotatespicklistasPicklist(data).field.picklistislive, withoutauthorWarn. Thepicklistkind's rows andtranslation.picklistsarelive. Count shards are regenerated.Tests
packages/objectql/src/engine-picklist.test.ts(25), real engine and registry:fieldsspellings.packages/objectql/src/plugin-picklist-boot-audit.test.ts(6), on a realObjectKernel:packages/objectql/src/protocol-picklist-served-roundtrip.test.ts(2): the protocol read serves the resolved options, and writing the served body back is refused (see H3).packages/rest/src/import-template-route.test.ts: the parity battery gainspicklist_option_default. The template's object read and the engine's import door must both see the list'sdefault: trueoption, or the star and the refusal disagree.packages/qa/dogfood/test/picklist-shared-across-objects.dogfood.test.ts(5), the ADR-0054 runtime proof:Accept-Language: zh-CNrelabels both objects' options and the served list item.packages/metadata/src/serializers/typescript-serializer-annotation.test.tsgains thepicklistrepresentative.packages/lint/src/lint-liveness-properties.test.ts: the pin "the shipped field ledger warns onpicklistalone" is now false, because this change takes the row out ofauthorWarn. It now expects an empty set (a test-only edit outside the claimed surface; see Acceptance notes).Ablation, run once and not kept: replacing
return this.resolvePicklistFields(merged);withreturn merged;inregistry.ts, throughscripts/ablation-replace.mjs, turnedengine-picklist.test.tsred (8 failed, 16 passed of 24 at that commit). The restore was proven: the blob equals HEAD24f6934320bfandgit diff HEADis empty.Open questions for the seat
H3, the served-body round trip: two readings are live. Measured in
protocol-picklist-served-roundtrip.test.ts:GET /meta/object/NAMEservespicklistandoptionstogether, and aPUTof that body is refused422 INVALID_METADATAbyFieldSchema, with its prescription.PicklistServedFieldSchema's doc declares, and stripping at the write door would be consumer-side tolerance.options(a Studio-side change, phase 2).optionsat the write door when they equal the resolved list. It would be declared and tested, but it is still a second shape accepted by an authoring door.resolveObjectFieldLabels(the field-labels endpoint) reads only the bundle and has no field-to-picklist binding. So inherited picklist option labels are still missing there. Fixing it means changingservice-i18nand the runtime i18n dispatcher, which this claim does not cover. The served object (translateObject) does carry them.A pending release note, corrected here: needs confirmation.
.changeset/19518-picklist-kind.md(the spec layer's note, still pending) said: "This release does not resolve the reference. Until the runtime does, a picklist-bound field is served without options, and the liveness ledger grades the keyplannedand warns an author who writes it." With this PR in the same release, that sentence is false. This PR replaces it with "The runtime resolves the reference onto that served field; see the picklist runtime entry of this release."check-empty-changesetrefuses any change to a changeset this PR did not add, and stays red until someone confirms the correction. That is its deliberate-correction path: restoring the old sentence would republish a false one. The alternative is to restore it and let the release author reconcile the two notes.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this branch derived 114 families, and all 114 were run.--ranreconciliation: 113 run, 1 NOT-MEASURED.check:dual-build-cjs-loadsexited 3: its prerequisite isdist/for eight packages this diff does not touch, and CI builds them.check-empty-changesetexits 1: question 3 above.check:platform-checklistexits 1: anABSENT SYMBOLforplugin-auth'stwoFactorinareas/identity-auth.json. It reproduces identically onorigin/mainat2f2fa11d7, so it is not this PR's.check-engine-split-ratioandcheck-plugin-teardown-shape --self-testrefused on the shallow clone, then passed after the deepening each prescribes.check:objectql-double-limitandcheck:slot-lookupcaught two new test doubles in this PR. Both were fixed and re-run green.Local runs:
objectql:pnpm test, 351 files / 6851 tests green, andpnpm typecheckgreen.metadata:pnpm test, 836 green, andpnpm typecheckgreen.restanddogfood.specvalidation-message.test.tsgreen.lintlint-liveness-properties.test.tsgreen.metadata-protocolpicklist-adjacent files green.check:liveness,check:stack-collection-maps,check:startup-registry-verdict,check:durability-log-level,check:doc-authoringandcheck:nul-bytesgreen.Acceptance notes
.changeset/19518-picklist-kind.md, which said this release does not resolve the reference;translation.json/README.mdliveness rows for picklists.metadata-core'sMetadataTypeSchema(it already lacksseed, nothing readspicklistthrough it, and widening a published enum with no consumer is surface without pull);runtimeapp-plugin.ts(see above);examples/app-showcase/src/coverage.ts(itspicklistExtensionswaiver now reads as stale; demonstrating it in the showcase is example-app work);records-forms.picklist-*(the checklist seat re-runs them).origin/mainonce before this PR, cleanly. It brought the CLI's author-time picklist reference check from another card. That door and this runtime audit agree: the CLI refuses within one stack, and the runtime refuses across packages at boot.os devruns reaches the registry only through a full manifest re-registration. Themetadata:reloadedingest re-registers objects, not lists.Generated by Claude Code