Skip to content

feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal - #21047

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-19519-picklist-runtime
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-19519-picklist-runtime

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #19519
Clause-②: no

The runtime layer of the shared picklist kind, phase 1 of #18164. The spec layer (#19518) is already on main. A select field that authors picklist: 'industry' is now served with that list's resolved options, a write is judged against them, and picklistExtensions from other packages merge into the list additively.

Dispatched by the domain:engine seat 2 PM under claim 5923202473. Dev session session_01MZu5JqVPacMktogpMxq9Xu.

What changed

Load (packages/objectql/src/engine.ts, packages/metadata/src/plugin.ts)

  • picklists and picklistExtensions join METADATA_ARRAY_KEYS, first in the list. Both registration seams (manifest and nested plugin) reach them through the shared registerMetadataCollections body.
  • A picklistExtensions entry is dispatched to SchemaRegistry.registerPicklistExtension, never registered as an item of its own.
  • The artifact door maps picklists to the picklist kind, so GET /meta/picklist/NAME serves the list on an artifact boot.

Merge (packages/objectql/src/registry.ts, new picklist-resolution.ts)

  • A list's options are its own, followed by each extension's.
  • A value the list already carries is refused with 422 INVALID_METADATA, naming both declarations. This holds in either registration order: list then extension, or extension then list. It is never last-wins.
  • A package that registers again replaces its own contribution. Uninstalling a package removes its values.

Serve

  • The registry's object fold (foldExtendersOntoDefinition, shared by resolveObject, the owner layer and foldObjectExtendersOnto) writes the resolved options onto every field that names a list, and keeps picklist (PicklistServedFieldSchema).
  • Every object read and the write door therefore read one resolved set. That covers registry-sourced bodies and sys_metadata bodies, which the protocol folds through foldObjectExtendersOnto.
  • Resolution is re-derived on every fold, never trusted. A stale served copy (the boot bridge writes resolved bodies into the metadata service) gets the current list. When the list is gone, its options are removed, so the write door refuses rather than accepting a stale value.
  • An object with no picklist-bound field is returned by reference, so its read is byte-identical to before.
  • Locale: the existing translateObject / translatePicklist resolvers 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)

  • At kernel:ready every package has registered, so "not declared" is final (AGENTS.md, startup registry reads). A packaged field naming a list nothing declares fails the boot with INVALID_METADATA, naming every such field and its package. A picklistExtensions entry extending such a list fails the same way.
  • After that, the vocabulary is sealed. An artifact registered through the manifest service is checked before any of it registers, so a refused install registers nothing.
  • Tenant overlays are never judged at boot: a stored row cannot fail a boot. The write door still refuses every value of such a field.

Write validation (packages/objectql/src/validation/record-validator.ts)

  • The door that judges inline options judges the resolved set, and the refusal names the list. The wire code stays invalid_option.
  • Three message keys carry the sentence, in en, zh-CN, ja-JP and es-ES (packages/spec/src/system/validation-message.ts): invalid_option_picklist, invalid_option_value_picklist and invalid_option_picklist_unresolved. They change the message text only and never reach the wire.
  • A picklist-bound field with no resolved options accepts no value. It is never read as free-form.

Error codes (H5). Both refusals reuse INVALID_METADATA. @objectstack/objectql already emits it in the ADR-0112 ledger. No new code, so Clause-②: no stands.

Enumerations and ledgers

  • scripts/check-stack-collection-maps.mjs: the METADATA_ARRAY_KEYS PENDING row is retired. The ARTIFACT_FIELD_TO_TYPE row now carries picklistExtensions only, with a reason: it is merged by the registry and is not a kind. APP_CATEGORY_KEYS / SECURITY_FIELDS keep their DELIBERATE rows. One is an app-payload heuristic, the other a four-collection security subset, and neither should list picklists.
  • serializers/typescript-serializer.ts annotates picklist as Picklist (data).
  • Liveness: field.picklist is live, without authorWarn. The picklist kind's rows and translation.picklists are live. Count shards are regenerated.

Tests

  • packages/objectql/src/engine-picklist.test.ts (25), real engine and registry:
    • served shape on two objects;
    • load-order independence, and a nested plugin's lists;
    • duplicate refusal in four shapes, plus the replay;
    • uninstall;
    • unknown-name and orphan-extension audit, attributed per package;
    • write accept, write refuse naming the list, and the unresolved refusal;
    • option default from the list;
    • stale-copy handling, including both fields spellings.
  • packages/objectql/src/plugin-picklist-boot-audit.test.ts (6), on a real ObjectKernel:
    • boot refused for an unknown field reference and for an orphan extension;
    • boot accepted when the list arrives from a package registered later;
    • post-boot artifacts refused before registering, or accepted.
  • 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 gains picklist_option_default. The template's object read and the engine's import door must both see the list's default: true option, or the star and the refusal disagree.
  • packages/qa/dogfood/test/picklist-shared-across-objects.dogfood.test.ts (5), the ADR-0054 runtime proof:
    • one list, two objects, two packages composed into one artifact;
    • the extension's value is accepted on object A;
    • a value outside the set is refused on object B, naming the list;
    • Accept-Language: zh-CN relabels both objects' options and the served list item.
  • packages/metadata/src/serializers/typescript-serializer-annotation.test.ts gains the picklist representative.
  • packages/lint/src/lint-liveness-properties.test.ts: the pin "the shipped field ledger warns on picklist alone" is now false, because this change takes the row out of authorWarn. 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); with return merged; in registry.ts, through scripts/ablation-replace.mjs, turned engine-picklist.test.ts red (8 failed, 16 passed of 24 at that commit). The restore was proven: the blob equals HEAD 24f6934320bf and git diff HEAD is empty.

Open questions for the seat

  1. H3, the served-body round trip: two readings are live. Measured in protocol-picklist-served-roundtrip.test.ts: GET /meta/object/NAME serves picklist and options together, and a PUT of that body is refused 422 INVALID_METADATA by FieldSchema, with its prescription.

    • This PR keeps refusing. That is what PicklistServedFieldSchema's doc declares, and stripping at the write door would be consumer-side tolerance.
    • The cost: a Studio editor that writes back what it read is refused on any object with a picklist-bound field until it drops options (a Studio-side change, phase 2).
    • The alternative is to strip options at 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.
  2. 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 changing service-i18n and the runtime i18n dispatcher, which this claim does not cover. The served object (translateObject) does carry them.

  3. 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 key planned and 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-changeset refuses 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/objectstack on this branch derived 114 families, and all 114 were run.

  • --ran reconciliation: 113 run, 1 NOT-MEASURED. check:dual-build-cjs-loads exited 3: its prerequisite is dist/ for eight packages this diff does not touch, and CI builds them.
  • check-empty-changeset exits 1: question 3 above.
  • check:platform-checklist exits 1: an ABSENT SYMBOL for plugin-auth's twoFactor in areas/identity-auth.json. It reproduces identically on origin/main at 2f2fa11d7, so it is not this PR's.
  • check-engine-split-ratio and check-plugin-teardown-shape --self-test refused on the shallow clone, then passed after the deepening each prescribes.
  • check:objectql-double-limit and check:slot-lookup caught two new test doubles in this PR. Both were fixed and re-run green.

Local runs:

  • objectql: pnpm test, 351 files / 6851 tests green, and pnpm typecheck green.
  • metadata: pnpm test, 836 green, and pnpm typecheck green.
  • Typecheck green for rest and dogfood.
  • spec validation-message.test.ts green.
  • lint lint-liveness-properties.test.ts green.
  • The metadata-protocol picklist-adjacent files green.
  • check:liveness, check:stack-collection-maps, check:startup-registry-verdict, check:durability-log-level, check:doc-authoring and check:nul-bytes green.
  • eslint on the 16 touched TS/MJS files: 0 errors, 0 warnings.

Acceptance notes

  • Outside the claimed surface, changed because this PR makes them false:
    • the lint pin above;
    • one sentence of the pending .changeset/19518-picklist-kind.md, which said this release does not resolve the reference;
    • the translation.json / README.md liveness rows for picklists.
  • Not changed:
    • metadata-core's MetadataTypeSchema (it already lacks seed, nothing reads picklist through it, and widening a published enum with no consumer is surface without pull);
    • runtime app-plugin.ts (see above);
    • examples/app-showcase/src/coverage.ts (its picklistExtensions waiver now reads as stale; demonstrating it in the showcase is example-app work);
    • the platform-checklist items records-forms.picklist-* (the checklist seat re-runs them).
  • Merged origin/main once 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.
  • HMR: a picklist edited while os dev runs reaches the registry only through a full manifest re-registration. The metadata:reloaded ingest re-registers objects, not lists.

Generated by Claude Code

claude added 11 commits October 1, 2026 02:11
… 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>
…— 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>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata, @objectstack/objectql, @objectstack/spec, touching 63 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/field.json, packages/spec/liveness/picklist.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6073bb96b878eb2980724568ee526157b29ab141.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/field.json, packages/spec/liveness/picklist.json, …) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6073bb96b878eb2980724568ee526157b29ab141 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c6eda0de7185377662375bf076d335d149133e38 — the merge of head 42d750f197f8c38669ef91ec918d672d6db8adbc into base 6073bb96b878eb2980724568ee526157b29ab141, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6073bb96b878eb2980724568ee526157b29ab141 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 1, 2026 04:08
…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4bb0dadcbf74720e148c2183c0f689e83e9af737
Local-runs: none

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 main (b253fadfb7d1); the 34 check-runs on the head. Not read: the dispatch order or the dispatching seat's conclusions.

Checks on the head

  • Green, required: TypeScript Type Check (with the four Type Check · gates), Build Core (which hosts check:dual-build-cjs-loads, the one family the dev could not measure locally), Dogfood Regression Gate and its three shards, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, and Test Core shards 1, 2, 3, 4 and 6. Green, advisory: Spec property liveness, Check PR Size, Check Documentation Links, the claim and single-writer guards, Auto Label, Dogfood Verify CLI, Vercel.
  • Test Core (5/6): in_progress at review time. Not a verdict either way.
  • Red, by design: Check Changeset (pr-automation.yml), step "Reject an empty-frontmatter changeset added by this PR". Its annotation names the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 foreign-changeset rule on .changeset/19518-picklist-kind.md and prints both classes; this diff is the DELIBERATE CORRECTION class. Judged in ② below. That workflow triggers on pull_request only and has no merge_group leg.
  • Red, this PR's: Lint & Repo Gates (required) failed at step 10, "ADR anchors + number uniqueness (governed code names its decision)", which is pnpm check:adr-anchors; steps 11 to 192 were skipped (the job's own tail annotation: never_ran=183 failed=1 ran=8). Not a merge-base signature: the ten preceding pull_request runs of lint.yml (36787041198 down to 36779752495) all pass step 10. The job log is not readable from this container, so the cause is derived from the tree rather than quoted: the diff adds the repository's only tracked-file citation of ADR-0136 (packages/qa/dogfood/test/picklist-shared-across-objects.dogfood.test.ts, line 4, "ADR-0136 D2.4"); docs/adr/ at this head holds 0135-identity-and-access-architecture.md and 0137-predicate-fault-semantics-are-contract.md and no 0136 record; the merge base has zero tracked files citing that number; the gate's third rule fails any bare ADR-NNNN in a tracked file that names no record, and its UNRESOLVED_ADR_CITATIONS allowlist is empty. The number was copied in good faith from the card body and the parent's rulings, which are issue text the gate never scans. Consequence beyond the one red: ESLint, the stack-collection-maps gate this diff edits, the error-code provenance and casing guards and every other Lint family are unmeasured by CI on this head; the dev's local greens for them are self-report.

① Derived judgments

Every accept-set or public-surface change the diff implies, named right or wrong against the card, the rulings and the spec on main.

  1. METADATA_ARRAY_KEYS gains picklists and picklistExtensions (engine.ts), first in the list; an extension entry is routed to SchemaRegistry.registerPicklistExtension and never registered as an item. Both keys are already declared on the stack shape by picklist metadata kind — spec: picklist collection, Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518; the runtime now reads them. No accept-set change. RIGHT.
  2. ARTIFACT_FIELD_TO_TYPE.picklists = 'picklist' (metadata/plugin.ts): the artifact door registers a kind already in DEFAULT_METADATA_TYPE_REGISTRY, so GET /meta/picklist/NAME answers through the generic meta route with the PicklistSchema item; picklistExtensions stays unmapped because it is not an item. No new kind, no new route. RIGHT. The matching check-stack-collection-maps.mjs edits (the METADATA_ARRAY_KEYS PENDING row retired, the artifact-door row narrowed to picklistExtensions with its reason) read true of the diff; that gate did not run on this head (step 27 skipped).
  3. Served field shape: picklist kept and the resolved options written on, in the one fold every object read and the write door share (foldExtendersOntoDefinition, foldObjectExtendersOnto, including a body the registry never saw); re-derived on every fold, stale options dropped when the list is gone, by reference when no field names a list. That is PicklistServedFieldSchema from picklist metadata kind — spec: picklist collection, Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518; no new served key. RIGHT.
  4. Writing the served body back is still refused 422 INVALID_METADATA by FieldSchema (both keys together), nothing stripped on the write side. Ruling 5904864936 declares the reference "mutually exclusive with options"; the seat answer takes Q1 A; the contract-first rule forbids a write-side strip. RIGHT.
  5. Additive merge: a repeated value is refused INVALID_METADATA naming both declarations, in either registration order, never last-wins; same-package re-registration is a replay; uninstall removes that package's values. Design of record item 4 ("additive only") and the card's scope item 1. RIGHT.
  6. Unknown name: a load-time refusal at kernel:ready naming each field and its package, and each orphan extension; the vocabulary is then sealed and a late artifact through the manifest service is judged before anything of it registers; tenant overlays and tenant-authored bodies are never judged at boot. Card scope item 2, and the startup-registry rule (seal, then judge). Only a field that authors picklist, a key no released app can carry yet, can trigger it, so no existing boot changes. RIGHT.
  7. Error codes: both refusals reuse INVALID_METADATA; the ADR-0112 ledger (packages/spec/src/api/error-code-ledger.zod.ts) already lists it in the @objectstack/objectql block (and in metadata-protocol's). No new code is minted; the report's H5 claim is verified. RIGHT.
  8. Write door: the record validator judges a picklist-bound field against the resolved options, the refusal names the list, an unresolved list accepts no provided value (single and multi-value branches), and an omitted value still passes because isMissing returns before the option check. Wire code stays invalid_option; messageParams.picklist feeds only the rendered sentence (the params of the message render), not the returned error object (field, code, message, constraint, options). No new served key on the wire. RIGHT.
  9. BUILTIN_VALIDATION_MESSAGES (spec non-test source, exported from @objectstack/spec/system) gains invalid_option_picklist, invalid_option_value_picklist and invalid_option_picklist_unresolved in four locales. The export's type (a record of records of strings) does not change; the translation override face messages['validation.field.*'] already accepts any key; no schema accepts or refuses anything new. Message text, not a contract key: not a Clause-② widening, but the path arm that owes this review. RIGHT.
  10. Liveness ledger: field.picklist from planned with authorWarn to live (evidence registry.ts#resolvePicklistOptions); the nine picklist rows and translation.picklists to live; count shards regenerated; Spec property liveness is green on the head. The ledger ships in the spec tarball (liveness is in files), so os lint and os validate stop warning on picklist, which the changeset's Ledger bullet says. The lint-liveness-properties.test.ts pin flip and the README row follow mechanically. RIGHT.
  11. An option marked default: true on the list reaches the engine's insert default and the import template through the resolved options; the rest parity battery gains picklist_option_default (pointer 5916040159). RIGHT.
  12. The TypeScript serializer annotates picklist as Picklist (data); Data.Picklist is exported by picklist.zod.ts. RIGHT.
  13. New public API of @objectstack/objectql: SchemaRegistry (exported from the barrel) gains registerPicklistExtension, resolvePicklistOptions, findUnresolvedPicklistReferences and findOrphanPicklistExtensions; picklist-resolution.ts is not exported. Package API widening, graded minor in ②; not a spec-contract widening. RIGHT.
  14. Locale: the existing translateObject and translatePicklist resolvers relabel the resolved options and the served list item per request, driven end to end by the dogfood case. RIGHT.
  15. WRONG: the dogfood file's header cites ADR-0136 D2.4, a number with no record under docs/adr/. That is the check:adr-anchors red above. On the next head: cite the record that actually holds the decision, qualify a sibling registry's number the way the gate recognises, or drop the bare number. The card body and the parent's rulings carry the same bare number; that is a note for the seat, not this PR's to fix.
  16. GAP: pointer 5912707861 lists the metadata-protocol reference sites as an enumeration that does not list picklist. No metadata-protocol file is in the diff and the report does not say whether the derived index (reference-sites.ts, which walks DEFAULT_METADATA_TYPE_REGISTRY and each type's schema by property name) already yields object.fields{}.picklist as a site for target picklist; its derivation pin does not name picklist. Escalated in ③.

② Semver level

Changeset .changeset/19519-picklist-runtime.md: @objectstack/objectql: minor, @objectstack/metadata: patch, @objectstack/spec: patch; body carries Clause-②: no.

Clause-②: no — holds on this diff. No schema key, error code, accepted shape or spec export is added; the one spec non-test file (validation-message.ts) changes message text; the claim 5923202473 and the PR body declare no consistently.

  • objectql minor: RIGHT (new public registry methods and new runtime behaviour, nothing removed).
  • spec patch: RIGHT (message text and a ledger regrade; no schema, accept-set, error-code or export change).
  • metadata patch: ACCEPTED, lenient. The artifact door newly registers a kind and the serializer annotates it; minor is the conventional grade for new door behaviour. Both notes compile into one release and the declaration is no, so this is not a refusal ground; it is named here.
  • No other released package publishes: the rest, lint and dogfood changes are test files.

Deliberate correction, owed in this record. The corrected note is .changeset/19518-picklist-kind.md, pending and unreleased on the merge base (the spec layer's note). Old sentence: "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 key planned and warns an author who writes it." New sentence: "The runtime resolves the reference onto that served field; see the picklist runtime entry of this release." Judged clause by clause against this head:

  • "The runtime resolves the reference onto that served field": TRUE. SchemaRegistry.resolvePicklistFields in the object fold writes the list's resolved options onto every field that names it (registry.ts), pinned by engine-picklist.test.ts and the dogfood case, and the Dogfood Regression Gate is green on the head.
  • "see the picklist runtime entry of this release": TRUE. .changeset/19519-picklist-runtime.md is added in the same diff, so changeset version compiles both notes into one release; the entry exists at this head.
  • Each of the old sentence's three facts is reversed by this diff: the resolver exists, the served field carries options, and field.json grades the key live without authorWarn. Restoring the old sentence would publish three false statements.
  • The red is by design: the gate's own annotation names the DELIBERATE CORRECTION class and its remedy ("do NOT restore it"), and the gate has no merge_group leg. Correction CONFIRMED on the text. Because this record's verdict is FAIL on an unrelated ground, the record that lets the PR ride this red into the queue is the PASS on the corrected head; if neither note changes, this judgment carries over to it unchanged.

③ Boundary flags

Open questions of report 5924129203, ruled by the seat answer 5924290772:

  • Q1 (H3, the served-body write-back): ruled A. The diff keeps refusing (protocol-picklist-served-roundtrip.test.ts), matching ruling 5904864936. Answered. The named cost (Studio's object designer round trip) sits on objectui#10202, phase 2.
  • Q2 (the pending note): ruled A through this record. Judged in ②: confirmed on the text.
  • Q3 (resolveObjectFieldLabels): ruled B; the diff touches neither service-i18n nor the dispatcher. Answered.

Pointer 5912707861 (runtime enumerations and gaps):

  • METADATA_ARRAY_KEYS, ARTIFACT_FIELD_TO_TYPE, typescript-serializer.ts, the stack-collection-maps PENDING rows, the served-shape round trip and the field.picklist ledger row: done, judged in ①.
  • runtime app-plugin.ts APP_CATEGORY_KEYS / SECURITY_FIELDS: unchanged; the report explains both carry DELIBERATE rows (H2 partly falsified). Answered.
  • metadata-core types.ts enum: unchanged; the report explains it is not lockstep (it lacks seed) and has no picklist reader. Answered; carried as a finding with no carrier.
  • metadata-protocol reference sites: NOT answered. ESCALATED to the seat: confirm on the card that the derived index already yields object.fields{}.picklist as a site for target picklist (the admin "Used by" panel and its delete safety read it), or file the follow-up. Not a blocker for this card's scope; it must not stay silent.
  • resolveObjectFieldLabels: Q3 above.

Pointer 5916040159 (import-template parity): the picklist_option_default row is added and the mirror reads the served field. Answered.

Dev flags in the PR body and the report:

  • Outside the claimed surface, changed because the diff makes them false: the lint pin, one sentence of the 19518 note, the translation and README ledger rows. Each is the mechanical consequence of the ledger flip or of ②. Accepted.
  • Not changed, with reasons: the metadata-core enum, app-plugin.ts, examples/app-showcase/src/coverage.ts (its picklistExtensions waiver now reads stale; example-app work), the records-forms.picklist-* checklist items (the checklist seat re-runs them). Accepted as acceptance notes.
  • HMR: metadata:reloaded re-ingests objects, not picklists or extensions (not measured). Accepted as noted; a carrier is owed once os dev users bind picklists.
  • check:platform-checklist red on main: [finding] check:platform-checklist is red on main: identity-auth.json anchors auth-plugin.ts#twoFactor, which #20429 turned into an inline nested key #20464, not this PR's and not in per-PR CI. Accepted.
  • check:dual-build-cjs-loads NOT-MEASURED locally: hosted by Build Core in CI, green on the head. Measured.
  • The report's claim that all 114 derived gate families ran green locally except the two named is contradicted on the head for check:adr-anchors, and unverified for the 183 Lint families CI skipped after it. The next head re-measures them.

Required on the next head: resolve the ADR-0136 citation in the dogfood test; leave both changeset notes as they are; push; a fresh record on that head.

Implemented-by: claude/issue-19519-picklist-runtime
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 42d750f197f8c38669ef91ec918d672d6db8adbc
Local-runs: none

PR #21047 (card #19519, the runtime sub-issue of #18164), reviewed read-only as a DELTA from the previous head 4bb0dadcbf74720e148c2183c0f689e83e9af737, whose at-tier record is PR comment 5924563914 (FAIL on one ground: the bare ADR-0136 citation that turned check:adr-anchors red). Inputs: the card body and all eight comments (the two pointers 5912707861 and 5916040159, triage 5921928091, claim 5923202473, dispatch record 5923224221, the round-0 report 5924129203, the seat answer 5924290772 and the patch-round report 5924919167); the PR body as it now reads, its 26-file list and the net diff against the new merge base with main (8f784959cf7e597e4582a70b93633b10eddb2dcf); the 42 check-runs on the head. Not read: the dispatch order or the dispatching seat's conclusions.

The delta, 4bb0dadcb..42d750f19, is two commits: the fix ed5853cd5 (two files: the dogfood header's citation, and an eleven-line pin in packages/metadata-protocol/src/reference-sites.derivation.test.ts) and a merge of origin/main that moved the merge base from b253fadfb to 8f784959c. The merge is clean: git diff-tree --cc on the merge commit prints no conflict hunk, and the one file that differs from both parents, packages/rest/src/import-template-route.test.ts, is git's auto-merge of main's #20977 battery rewrite with this PR's parity row, whose text is unchanged and sits 67 lines lower. Of the 26 files in the net diff, 24 have a net hunk byte-identical to the previous head's against its base (index lines aside); the rest test's hunk is identical in content and differs only in position; the 26th is the new pin. Main's incoming commits touch none of the PR's source files (engine.ts, registry.ts, plugin.ts, record-validator.ts, picklist-resolution.ts, metadata/plugin.ts, the serializer); its AGENTS.md edit is the merge-tree probe recipe, and its check-empty-changeset.mjs edit is the self-test battery for #20976, with the two-class refusal text unchanged.

Checks on the head

  • Required, all seven green (conclusion: success): Lint & Repo Gates (run 36814102433), TypeScript Type Check with the four Type Check · gates, Test Core with all six shards (shard 5/6, in_progress on the prior record, is now success), Dogfood Regression Gate with its three shards, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. The Lint job's green means step 10 (check:adr-anchors) and the 183 families CI skipped behind it on the prior head are now measured by CI, not self-reported.
  • Advisory green: Spec property liveness, Check Documentation Links, Check PR Size, Auto Label, Dogfood Verify CLI, Flag docs affected by code changes, filter, and the three claim, single-writer and part-of guards (each ran twice, both green). Skipped by paths filter or opt-in: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in); and the second Auto Label and Check PR Size, which the edited re-fire excludes by design.
  • Red by design: Check Changeset (run 36814102459, job 110215299915). Its annotation on .changeset/19518-picklist-kind.md is the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 foreign-changeset refusal, printing both classes and the DELIBERATE CORRECTION remedy, which reads: do NOT restore it, say so on the PR and get it confirmed. Judged in ②.
  • in_progress, not a verdict: a second Check Changeset (run 36818752192), fired by the pull_request: edited event when the PR body was edited after the patch-round report. Not waited for and not polled; this record draws nothing from it.

① Derived judgments

The delta's two changes, then the carried judgments.

  1. Dogfood header citation (packages/qa/dogfood/test/picklist-shared-across-objects.dogfood.test.ts, line 4): ADR-0136 D2.4 is replaced by the ADR-0054 runtime proof. docs/adr/0054-runtime-proof-for-authorable-surface.md exists at this head (Accepted) and its decision, the prove-it-runs leg, is that a live authorable property is proven by a dogfood test that authors it against the real in-process stack and asserts the runtime result; that is exactly what this file does for field.picklist's flip to live. The head's tracked files cite ADR-0136 zero times; UNRESOLVED_ADR_CITATIONS is untouched; check:adr-anchors is green inside the Lint job. The field.json row carries no proof binding, and none is owed: ADR-0054's ratchet binds only the classes in proof-registry.mts, field.picklist is not one, and Spec property liveness is green; the row's note names the dogfood test as its end-to-end evidence. Prior item 15 (WRONG) is resolved. RIGHT.
  2. The escalated reference-sites item (prior item 16, pointer 5912707861). The pointer listed metadata-protocol's reference sites as an enumeration that did not list picklist. It is not an enumeration: deriveReferenceSites (reference-sites.ts) walks every type in DEFAULT_METADATA_TYPE_REGISTRY through its JSON schema, descending properties, items and additionalProperties, and rule 1 binds a property to target T when its name spells T and its value is name-shaped. picklist has been a declared type since picklist metadata kind — spec: picklist collection, Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518; FieldSchema.picklist is a SnakeCaseIdentifierSchema string with no enum, const or format, so carriesNames holds; the walk reaches it under object.fields and records fromType object, property picklist, target picklist. There is no row to add, and the report's measured byTarget.get('picklist') (field, object, translation.picklists) agrees with the rule. The pin: expect(sitesFor('picklist')).toContainEqual({ fromType: 'object', property: 'picklist' }). sitesFor projects each site to { fromType, property }, the same helper the file's earlier pins use, so the deep-equality match is well-formed. It is falsifiable: it reds if picklist leaves the registry, if the walk stops descending into fields, or if the property is respelled. It pins a property rather than a path, which is the index's own declared design, and ObjectSchema declares no top-level picklist, so the only source of that site is fields{}.picklist: the pin is as precise as the index allows. The report says it was not ablated; its red direction follows from the rule (an undeclared type has no spelling in the singular map and yields no site), so no probe is spent on it here. Adequate. The pointer's item is answered and closed.
  3. Items 1 to 14 of the prior record carry over, each still RIGHT, because the net hunk of every file they judge is byte-identical between the two bases and main's incoming commits touch none of those files. Restated for a reader of this record alone:
    • METADATA_ARRAY_KEYS gains picklists and picklistExtensions, an extension entry routed to SchemaRegistry.registerPicklistExtension and never registered as an item; keys picklist metadata kind — spec: picklist collection, Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 already declared on the stack shape; no accept-set change. RIGHT.
    • ARTIFACT_FIELD_TO_TYPE.picklists = 'picklist': the artifact door registers a kind already in the registry; picklistExtensions stays unmapped because it is not an item; no new kind, no new route. RIGHT. The check-stack-collection-maps.mjs edits (PENDING row retired, artifact-door row narrowed to picklistExtensions with its reason) read true of the diff, and that gate is now measured green by the Lint job.
    • Served shape: picklist kept and resolved options written on, in the one fold every object read and the write door share, re-derived per fold, stale options dropped when the list is gone, by reference when no field names a list. PicklistServedFieldSchema; no new served key. RIGHT.
    • Writing the served body back stays refused 422 INVALID_METADATA by FieldSchema; nothing stripped write-side. Ruling 5904864936 (mutually exclusive with options); seat Q1 A. RIGHT.
    • Additive merge: a repeated value refused INVALID_METADATA naming both declarations, in either registration order, never last-wins; same-package re-registration replays; uninstall removes that package's values. RIGHT.
    • Unknown name: a load-time refusal at kernel:ready naming each field, each orphan extension and its package; vocabulary sealed after; a late artifact judged before anything of it registers; tenant overlays never judged at boot. Only a field authoring picklist can trigger it, so no existing boot changes. RIGHT.
    • Error codes: both refusals reuse INVALID_METADATA, already in the ADR-0112 ledger's @objectstack/objectql block; nothing minted. RIGHT.
    • Write door: the record validator judges a picklist-bound field against the resolved options, names the list, accepts nothing for an unresolved list, and lets an omitted value pass through isMissing; wire code stays invalid_option; messageParams.picklist feeds the rendered sentence only. RIGHT.
    • BUILTIN_VALIDATION_MESSAGES gains three keys in four locales; the export's type is unchanged and no schema accepts or refuses anything new. Message text, not a contract key. RIGHT.
    • Liveness ledger: field.picklist to live without authorWarn; the picklist rows and translation.picklists to live; count shards regenerated; Spec property liveness green; os lint and os validate stop warning on the key, as the changeset says. The lint pin flip and README row follow mechanically. RIGHT.
    • A list option marked default: true reaches the engine's insert default and the import template through the resolved options; the rest parity battery's picklist_option_default row survives main's fix(rest): the import template answers to the import door's gates, not the export's (#20896) #20977 rewrite with its text unchanged and Test Core green on the merged head (pointer 5916040159). RIGHT.
    • The TypeScript serializer annotates picklist as Picklist (data). RIGHT.
    • New public API of @objectstack/objectql: SchemaRegistry gains registerPicklistExtension, resolvePicklistOptions, findUnresolvedPicklistReferences, findOrphanPicklistExtensions; picklist-resolution.ts is not exported. Package API widening graded minor in ②; not a spec-contract widening. RIGHT.
    • Locale: translateObject and translatePicklist relabel the resolved options and the served list item per request; the dogfood case drives it end to end and the Dogfood gate is green on this head. RIGHT.

Nothing in the diff is judged WRONG on this head, and no gap remains open.

② Semver level

Changeset .changeset/19519-picklist-runtime.md: blob 2cb5af66447e on both heads, byte-identical. @objectstack/objectql: minor, @objectstack/metadata: patch, @objectstack/spec: patch; body carries Clause-②: no, and so does line 2 of the PR body.

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 (validation-message.ts) changes message text; the claim, the PR body and the changeset declare no consistently.

  • objectql minor: RIGHT (new public registry methods and new runtime behaviour, nothing removed).
  • spec patch: RIGHT (message text and a ledger regrade; no schema, accept-set, error-code or export change).
  • metadata patch: ACCEPTED, lenient, as on the prior head (the artifact door newly registers a kind; minor would be the conventional grade; both notes compile into one release and the declaration is no, so not a refusal ground).
  • No other released package publishes: the rest, lint, metadata-protocol and dogfood changes are test files.

Deliberate correction, owed again on this head. The corrected note is .changeset/19518-picklist-kind.md, pending and unreleased. Byte identity, measured: the note's blob is d59900409296 on both the previous head and this one, and its base blob is 01dae8d8f138 on both merge bases and on origin/main as fetched for this review, so the one-line hunk this PR carries is byte-identical to the one the prior record judged. Old sentence: "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 key planned and warns an author who writes it." New sentence: "The runtime resolves the reference onto that served field; see the picklist runtime entry of this release." The prior record's judgment therefore carries over, and it is re-read true on this head: SchemaRegistry.resolvePicklistFields writes the resolved options onto the served field (registry.ts, pinned by engine-picklist.test.ts and the dogfood case, Dogfood gate green); the 19519-picklist-runtime entry is added in the same diff so changeset version compiles both into one release; field.json grades the key live without authorWarn. Restoring the old sentence would publish three false statements. Correction CONFIRMED.

The three facts the queue rule needs:

  1. The gate is red by design: the Check Changeset annotation on this head names the DELIBERATE CORRECTION class and its remedy (do NOT restore it; get it confirmed on the PR), and this diff is that class, not a COLLISION (the PR adds its own note under a name that cannot collide).
  2. It does not run on merge_group: .github/workflows/pr-automation.yml at this head triggers on pull_request only (types opened, synchronize, reopened, labeled, unlabeled, edited); it is absent from the set of workflows that carry a merge_group leg; and Check Changeset is not one of the seven required contexts.
  3. This record, posted on PR feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal #21047, is the PR comment that records it.

Also read: #20976, on main between the two bases, now runs the ADR-0087 disposition and major-bump steps past the designed red. The job's annotations name one failing step only, the foreign-changeset refusal. By the text, the 19518 note is Clause-②: yes (widening) with @objectstack/spec: minor (a widening is not breaking, so no ADR-0087 disposition is owed), and the 19519 note bumps nothing to major. No contradiction is read from those steps.

③ Boundary flags

Patch-round report 5924919167: open_questions is empty. Q1 A, Q2 A and Q3 B from seat answer 5924290772 stand; the diff on those three points is unchanged from the prior head, where each was judged answered.

Dev flags in the patch-round report:

  • "The PR body still reads ADR-0136 D2.4 once; suggested seat edit": the body now reads "the ADR-0054 runtime proof" in the dogfood bullet and carries zero occurrences of ADR-0136. That edit is what fired the second Check Changeset run noted above. Closed.
  • "The new pin was not ablated": judged in ① item 2; accepted without a probe.
  • NOT-MEASURED locally, PREREQUISITE NOT MET: check:dual-build-cjs-loads is hosted by Build Core, green; check:type-check-debt is hosted by Type Check · debt ledger, green. Both measured by CI on this head.
  • Non-zero, kept: check-empty-changeset, judged in ②; check:platform-checklist, red on a clean origin/main worktree in both rounds (the twoFactor ABSENT SYMBOL in areas/identity-auth.json), which is [finding] check:platform-checklist is red on main: identity-auth.json anchors auth-plugin.ts#twoFactor, which #20429 turned into an inline nested key #20464, already open, and kept out of per-PR CI by checklist-status.yml. Not this PR's. Accepted.
  • Lint-family local greens (check:adr-anchors, check:error-code-casing, check:error-code-provenance, check:stack-collection-maps, eslint on the 17 touched files): no longer self-report; the Lint job is green on this head.

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 ADR-0136 citation is resolved; both changeset notes are untouched (blobs equal across heads); the branch was pushed; this is the fresh record on that head.

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 (metadata-core's enum, runtime app-plugin.ts, the coverage.ts waiver in examples/app-showcase, the records-forms.picklist-* checklist items for the checklist seat to re-run); and the HMR note (metadata:reloaded re-ingests objects, not picklists or extensions; not measured; a carrier is owed once os dev users bind picklists). The named cost of Q1 A, Studio's object-designer round trip, sits on objectui#10202 (phase 2), as the seat answer records.

Implemented-by: claude/issue-19519-picklist-runtime
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

Merged via the queue into main with commit 88b484e Oct 1, 2026
42 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19519-picklist-runtime branch October 1, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

2 participants