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 |
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