Skip to content

fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names - #28231

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-kwainq
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-kwainq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 问题:带 packages[] 的组合栈,若顶层声明了一个 picklist,既没有任何包体声明、也没有任何对象字段引用,启动照常成功,该列表在引擎注册表里静默缺失。
  • 修复:manifest 服务在注册任何内容之前拒绝这种工件,返回 INVALID_METADATA(422),消息点名每个 picklist 和所属栈,并给出处方。
  • 处方:在使用它的包的 packages[] 条目里声明;或由对象字段引用它;或从顶层删除。
  • 不变:被对象字段引用的残余 picklist 保持现有行为(顶层对象"列出但不服务"、包体字段由 picklist 审计拒绝);包体声明的 picklist、无残余 picklist 的栈、不带 packages[] 的栈均不变。
  • 破坏性:是,收窄(以前能启动的组合现在被拒绝),patch 级 changeset,无 schema 变更,ADR-0087 not-required。
  • 文件面:仅 packages/objectql/src/plugin.ts 的残余注册段、其残余测试文件和一个 changeset。

Clause-②: yes (narrowing) — a stack with packages[] whose top level declares a picklist that no package body declares and no object field names booted, and now refuses the boot with INVALID_METADATA (422)
Metadata-protocol: none

What changed

The manifest service registers a composed stack's residual (top-level items no package body declares) under the stack's own id, except picklists, which stay metadata only (RESIDUAL_TYPES_WITH_OWN_PATH). A residual picklist that an object field names was loud already. One that no object field names was neither registered nor refused.

unreferencedResidualPicklistRefusal (in packages/objectql/src/plugin.ts) reads the residual rule's own answer (unclaimedTopLevel), takes its picklists, and keeps those that no object field names. "Names" is judged off the artifact alone: its top level and every package body, objects and objectExtensions both, through the existing collectManifestPicklistReferences. It is never judged off the registry, which is still filling during a boot. If any are left, register throws before anything of the artifact registers. The refusal uses the existing picklist envelope picklistMetadataError (INVALID_METADATA, 422), so no new error code is added. The residual objects rule's line does not move.

Measurement (on main at 35f20551, then on this branch)

Composition main This branch
Residual picklist that no object field names boots, state=running; resolvePicklistOptions('acme_unused') is undefined; no warning names it refused at boot, INVALID_METADATA, the message names acme_unused and stack com.example.acme.release
packages: [] with only such a picklist on the top level boots; the list is absent; nothing names it refused, as above
CONTROL: residual picklist that a residual object's field names boots; the object is listed and not served (one warning); the list is not registered unchanged
CONTROL: picklist a body declares boots; the list resolves to ['open'] unchanged

Pins (manifest-service-residual-objects.test.ts, new describe)

On main's plugin.ts: 4 red, 5 green (the 5 controls). On this branch: 9 of 9 green. Both residual test files: 72 of 72 green. Full @objectstack/objectql suite: green (counts under Gates).

  • Red on main, green here: the boot refusal (code, status, picklist, stack, all three remedies); several picklists named in one refusal, and not one a field names; packages: [] with only such a picklist; after start(), the artifact is refused before anything of it registers.
  • Controls (green on both): a residual picklist a residual object's field names keeps the listed-and-not-served line, and the list stays unregistered; a residual picklist a body object's field names still fails the boot's picklist audit, not this refusal; a body-declared picklist registers whether or not the top level repeats it; a stack with no residual picklist boots unchanged; a stack without packages[] registers its unnamed picklist as before.

Ablations, each restored from the committed head:

Ablation Red
A1: drop the throw the 4 refusal pins
A2: treat no picklist as named both "a field names it" controls, the "several in one refusal" pin, and the existing listed-and-not-served pin
A3: count references on the top level only, not in bodies the body-field audit control
A4: take every top-level picklist, not only the residual's the body-declared control and the no-packages[] control
A5: refuse whenever there is a residual 52 tests, the no-residual-picklist control among them

Gates

Run on the head b4c76532, which merges main at 7811c3f6, after a full rebuild:

  • dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 61 families. All 61 were run and all exited 0. --ran reports 61 run, 0 NOT-MEASURED and 0 UNRUN, and exits 0.
  • The derivation names some work outside those 61 that cannot run locally: 4 families that take a value from the workflow, 5 CI jobs scheduled by these paths, and 5 type-check lanes. These are NOT MEASURED here, and CI runs them.
  • pnpm lint: exit 0. pnpm --filter @objectstack/objectql run typecheck: exit 0.
  • Test suites: @objectstack/objectql 9732 of 9732 green, @objectstack/runtime 8855 green with 19 skipped, @objectstack/metadata 906 of 906 green.

Fixes #28159

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1Np3hU7RQnA13YZcL

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 3 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 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 — 20 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 7c3cc889f7c64d465486bb94ded8c8ea558d6945 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 66fb75a77b5ca348e83f7cae783d54e5607bad6a — the merge of head a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1 into base 7c3cc889f7c64d465486bb94ded8c8ea558d6945, 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 66fb75a77b5ca348e83f7cae783d54e5607bad6a && git checkout 66fb75a77b5ca348e83f7cae783d54e5607bad6a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7c3cc889f7c64d465486bb94ded8c8ea558d6945 a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1 && git checkout -B drift-repro 7c3cc889f7c64d465486bb94ded8c8ea558d6945 && git merge --no-ff a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1

node scripts/docs-audit/affected-docs.mjs --json 7c3cc889f7c64d465486bb94ded8c8ea558d6945

⚠️ 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 7c3cc889f7c64d465486bb94ded8c8ea558d6945 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head b4c7653 · 2026-10-30T18:40Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b4c76532970377a3e113509196fb60e13fd4d6bc
Local-runs: none
Scope: packages/objectql/src/plugin.ts, packages/objectql/src/manifest-service-residual-objects.test.ts, .changeset/28159-residual-picklist-unreferenced.md + packages/objectql/src/picklist-resolution.ts (collectManifestPicklistReferences, collectPicklistReferences, manifestSources, collectManifestPicklistNames, collectManifestPicklistExtensions, describeUnresolvedPicklistReferences, picklistMetadataError), packages/metadata/src/plugin.ts (unclaimedTopLevel, artifactItemSlot, ARTIFACT_FIELD_TO_TYPE, registerUnclaimedTopLevel, the residual-rule header), packages/objectql/src/registry.ts (findOrphanPicklistExtensions), packages/objectql/src/engine.ts (the picklistExtensions branch of the body seam), packages/runtime/src/app-plugin.ts (the config door's manifest.register and registerUnclaimedTopLevel call sites), packages/spec/src/stack.zod.ts (picklists, the residual-rule prose at the permission-set owner gate, the objectExtensions target prose, the compose strategies), packages/spec/src/data/field.zod.ts (the picklist key and its type refinement), packages/spec/src/ui/action.zod.ts (param options), packages/cli/src/utils/picklist-references.ts (the author-time door's reading of a composed stack), packages/objectql/CHANGELOG.md (declaration precedent), scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, docs/adr/0130-release-artifact-as-co-ownership-boundary.md D4.

Inputs: card #28159 (body; triage 6525964223; claim 6526699957; os-dev report 6530710763), PR #28231 (body, file list, net diff against main at the head: merge-base 7811c3f6, three files, +178/-1), the head's check-runs, and context card #28093 / PR #28153 with records 6525505738 and 6526074825 (should-fix #3 there is this card). Read-only: the tree was judged, not run.

Check-runs at 2026-10-30T18:54Z: 48 check-runs on the head, every one completed. 35 success, among them Lint & Repo Gates, TypeScript Type Check, Build Core, Test Core and all 8 shards, Check Changeset, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate, Live E2E UI, Test UI, every Lint · and Type Check · leg. 13 skipped are path-gated UI, docs and opt-in lanes. Nothing is still running. Since the merge-base, no commit on origin/main (now 7c3cc889) touches plugin.ts, picklist-resolution.ts, packages/metadata/src/plugin.ts or the residual test file; the PR reads mergeable_state: clean.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged in the tree:

  1. A composed stack whose residual holds a picklist that no object field names is refused at register, before anything of it registers — right. unreferencedResidualPicklistRefusal reads the residual rule's own answer (unclaimedTopLevel, the one copy), takes its picklist entries by name (artifactItemSlot guarantees one), builds the "named" set from collectManifestPicklistReferences over the artifact and every body, and throws picklistMetadataError when any is left. The throw sits after the three residual reads and before the packages: [] branch, both sealed audits and the registerApp loop, so a refused artifact registers nothing (pinned after start(): owners and package ids unchanged). The error is the existing envelope: INVALID_METADATA, 422, httpStatus 422; no new code.
  2. The refusal is judged off the artifact alone, never the registry — right, and the right choice: the registry is still filling during a boot (the same reason registerUnclaimedTopLevelObjects judges off the bodies). "Named" means a picklist key on a field (collectPicklistReferences), in either field spelling, on objects and objectExtensions, in the manifest and its nested plugins[]. The field schema admits the picklist key on option types only (field.zod.ts refinement) and no other published surface names a picklist item by name (actions' params carry inline options; dashboards and kanban lanes read a field's picklist through the field), so the field key is the whole reference surface. But see ④ should-fix Add metamodel interfaces for ObjectQL/ObjectUI contract #1: the artifact's TOP LEVEL contributes references from items the engine never registers.
  3. A residual picklist that a field names keeps the objectql: a composed stack's residual top-level permissions (and other registry-read types) are never registered, like flows were before #28048 #28093 line — right. A residual object's field naming it: registerUnclaimedTopLevelObjects still lists that object and does not serve it (one warning), the list stays unregistered (pinned, the CONTROL that was already red under ablation A2). A body object's field naming it: named holds it, so this refusal is silent; the kernel:ready audit (findUnresolvedPicklistReferences) refuses it as "no loaded package declares" (pinned, the wording asserted). The residual objects rule's line does not move.
  4. A picklist a body declares is never residual — right: unclaimedTopLevel claims every slot a body occupies, so a top-level copy (the ADR-0130 D4 read-both shape, or composeStacks' picklists: 'concat') is claimed by name and never reaches this refusal (pinned both ways).
  5. A stack without packages[] is untouched — right: unclaimedTopLevel answers undefined without an array packages, and its top-level picklists register through registerApp as before (pinned: the unnamed list resolves to ['a']).
  6. packages: [] with only such a picklist is refused — right: with no body nothing is claimed, so every top-level item is residual under manifest.id; the refusal fires before the "registered nothing" warning (pinned).
  7. Both boot doors meet the refusal — right. The config door (AppPlugin.init) hands the whole stack to manifest.register before its own registerUnclaimedTopLevel; the refusal propagates out of init and the boot fails. The late paths (install, rehydrate) enter the same register and are refused before the sealed picklist audit, which on main would already have refused a body's picklistExtensions entry extending the residual list as an orphan (③).
  8. No public export, route, schema or spec surface changes. packages/spec is untouched; the only published package the diff moves is @objectstack/objectql.

② Semver level

  • Changeset .changeset/28159-residual-picklist-unreferenced.md: @objectstack/objectql patch; body carries Clause-②: yes (narrowing) and Metadata-protocol: none, both line-initial and identical to the PR body's two lines.
  • Clause-②: yes (narrowing) is right for the arm: a composition that booted on main (①.1, ①.6) is refused on the head; nothing is widened. The title carries ! and the body a BREAKING banner scoped to composed stacks, as a declared narrowing owes.
  • Metadata-protocol: none is right: no key, accepted value, type or default an author writes changes; PicklistSchema and ObjectStackDefinitionSchema accept what they accepted, and the refusal is a composition-time judgement of the same class as the boot's existing picklist audit. check-changeset-no-major.mjs states the rule the other way too (a (narrowing) arm never becomes narrowed), and this package has shipped the same pair for boot-time refusals before (504d8b3, the controlled_by_parent boot refusal; be78977). Level per ruling A: none takes patch. Right; Check Changeset green.
  • ADR-0087 marker not-required (no-migration-prescription): right. No metadata surface is retired, so objectstack migrate meta has nothing to rewrite; the changeset's "remedy" is an authoring choice among three placements of a declaration, not a FROM-to-TO rewrite of consumer code, and the gate that re-checks that claim statement-against-statement is green on the head.

③ Boundary flags

④ Design judgment

The narrowing itself, against the published spec on main. The spec declares picklists as "shared option lists that select fields reference by name", admits the picklist key on option-typed fields only, and reads a composed stack's picklist vocabulary as its bodies' at every door that judges a reference: os compile resolves a body's field against the bodies' lists alone and refuses otherwise; the boot's kernel:ready audit refuses a body's field naming a list only the top level declares; the residual objects rule lists and does not serve a residual object whose field names one. So on main a top-level picklist of a composed stack has no field it can serve, by every judge the platform already runs; the one case where nothing said anything was the list no field names, which the metadata door listed under manifest.id and the engine never held. Does the head take a declared capability away at the consumer? The capability the spec declares for a picklist (be offered by a field) was never deliverable from a composed stack's top level, and that is already the ruled line of #28093, pinned and unchanged here. What the head removes is a listing with nothing behind it, and it replaces silence with the refusal the sibling judges already give the referenced case, with the three remedies. That is making the platform's own refusal real for the one gap, not narrowing a delivered capability. The route is triage's, and the PR declares it as a narrowing, which is the honest arm. Holds, with the spec-prose and author-time gap named in should-fix #3 so the published spec says the line the runtime now enforces.

packages/objectql/src/plugin.ts — holds, with one should-fix. Break attempts:

  • A reference the check could miss. Lookup, formula, master-detail and text fields carry no picklist key by schema; a picklist key on a non-option type is refused at parse, and at runtime collectPicklistReferences reads the key off any field regardless of type, so the runtime set is a superset of what strict parsing admits. Views, actions, reports and dashboards never name a picklist item. objectExtensions in a body and nested plugins[] are walked. No miss found.
  • A reference the check counts that never registers — should-fix Add metamodel interfaces for ObjectQL/ObjectUI contract #1. collectManifestPicklistReferences(artifact, undefined) walks the artifact's whole top level: every top-level objects entry, including the copies a body claims (which the residual rule never registers from the top level) and the top-level objectExtensions, which in a composed stack no door registers (RESIDUAL_TYPES_WITH_OWN_PATH's own words: the residual rule never lists one; stack.zod.ts: "the top-level objectExtensions are not read there"), and the top-level plugins[], which only a body's registerApp registers. A residual picklist named ONLY by one of those is held as "named", not refused, and is then dropped exactly as the card describes, while nothing serves the field either. Reachable through a top-level objectExtensions entry in a composed stack (itself silently dropped on main, a separate defect) or a top-level object copy whose fields diverge from the body's. Fix: build named from the bodies plus the residual's own object entries (residual.items.filter(type === 'object')), which is the set registerUnclaimedTopLevelObjects judges, and pin the top-level-objectExtensions case red. Not a must-fix: the hole is narrower than the one the head closes, and nothing that booted on main is wrongly refused by it.
  • The referenced residual case moving by accident. The CONTROL pins hold both sides (residual object's field: listed and not served, list unregistered; body object's field: the audit's words, not this refusal's). Holds.
  • Late artifacts. The refusal precedes the sealed audits; a late artifact's residual picklist that a body field names is refused by the sealed audit's unresolved leg (declared is the bodies' names, the registry does not hold it), the same answer the boot gives. Holds.
  • Order and partial state. Thrown before registerApp, before the residual registrations and before the bridge; the config door's metadata-side registerUnclaimedTopLevel runs after manifest.register in init, so a refused boot writes nothing there either. Holds.
  • Owner fallback. With no manifest.id and no id the message names the stack by the literal none fallback, the spelling the objects rule's warning uses. Holds.
  • The envelope. picklistMetadataError is the audit's; PicklistMetadataError is imported as a type for the return. Holds.
  • Docblocks. The RESIDUAL_TYPES_WITH_OWN_PATH entry now states the refusal; the new function's docblock states the artifact-only judgement. Right, apart from should-fix Add metamodel interfaces for ObjectQL/ObjectUI contract #1's precision.

packages/objectql/src/manifest-service-residual-objects.test.ts — holds. Nine pins in one describe: the refusal's code, status, picklist, stack and all three remedies; several lists in one refusal and the named one excluded; packages: []; post-start() refusal with nothing registered; and five CONTROLs that hold the four unchanged lines (residual-field, body-field, body-declared with and without a top-level copy, no residual picklist, no packages[]). Fixtures reuse the file's residualStack / payload shape, the one AppPlugin hands over. Nothing pins the picklistExtensions case (should-fix #2) or the top-level-objectExtensions case (should-fix #1).

.changeset/28159-residual-picklist-unreferenced.md — holds (②): level, both declaration lines, the BREAKING banner scoped to composed stacks, the ADR-0087 marker and the remedy match the diff.

Should-fixes:

  1. unreferencedResidualPicklistRefusal counts references from top-level items the engine never registers (objectExtensions, body-claimed object copies, top-level plugins[]); judge "named" off the bodies plus the residual's own objects, and pin the top-level-objectExtensions case.
  2. Pin the case the dev flagged: a body's picklistExtensions entry extending a residual picklist that no field names is refused by this refusal, at boot and after start().
  3. File the follow-up card (outside this claim's surface): the author-time door (packages/cli/src/utils/picklist-references.ts) should report a composed stack's top-level picklist that no body declares and no field names, so os validate foretells this boot refusal; and the published spec's residual-rule prose (packages/spec/src/stack.zod.ts, the permission-set owner gate's docblock; the residual-rule header in packages/metadata/src/plugin.ts) should carry the picklist line the runtime now enforces (referenced: listed and not served; unreferenced: refused).

No must-fix. No fail-open finding at rule level beyond the one the card names and the head closes.

Implemented-by: claude/project-thread-kwainq
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head a2fb76a · 2026-10-30T20:04Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1
Local-runs: none
Scope: packages/objectql/src/plugin.ts, packages/objectql/src/manifest-service-residual-objects.test.ts, .changeset/28159-residual-picklist-unreferenced.md + packages/objectql/src/picklist-resolution.ts (collectManifestPicklistReferences, manifestSources, collectPicklistReferences), packages/metadata/src/plugin.ts (unclaimedTopLevel, ARTIFACT_FIELD_TO_TYPE), packages/objectql/src/registry.ts (findOrphanPicklistExtensions), packages/objectql/src/engine.ts (the picklistExtensions branch of the body seam), packages/spec/src/stack.zod.ts (the objectExtensions target prose). The rest of record 6531047218's scope was re-read only where this delta touches it.

This record supersedes 6531047218 (head b4c76532, a PASS with three should-fixes). Delta reviewed: b4c76532970377a3e113509196fb60e13fd4d6bc..a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1 — the fix da7e7ee3 (plugin.ts, the residual test file), the changeset sentence 942aae2b, and the merge a2fb76a0 — with the merge's effect judged through the net diff against main (merge-base 7c3cc889: the same three files, +223/-1), not the merge commit's own diff. Inputs as before (card #28159 with every comment; the PR body, file list and net diff; the head's check-runs; the delta claim 6532384405; card #28248). Read-only: the tree was judged, not run.

Check-runs at 2026-10-30T20:07Z: 48 check-runs on the head, every one completed: 35 success (Lint & Repo Gates, TypeScript Type Check, Build Core, Test Core and all 8 shards, Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate, Live E2E UI, Test UI, every Lint · and Type Check · leg), 13 skipped (path-gated UI, docs and opt-in lanes). Nothing has yet to report. origin/main has moved on to 86cf7cef since the merge-base; no commit on it between 7811c3f6 and 86cf7cef touches packages/objectql/src, packages/metadata/src/plugin.ts, packages/runtime/src/app-plugin.ts, packages/spec/src/stack.zod.ts, packages/spec/src/data/field.zod.ts or packages/cli/src/utils/picklist-references.ts. The PR reads mergeable_state: clean, still a draft.

① Derived judgments

The merge, judged by the tree:

  1. a2fb76a0 is a plain merge. Its tree is byte-identical to the automatic merge of its two parents (942aae2b and 7c3cc889, the origin/main it took in); nothing was resolved by hand. The two main commits it brings (3739d8d6, 7c3cc889) touch spec and lint surfaces only, none of the cited files. The net plugin.ts and test-file deltas against main differ from the first record's by exactly the fix commit's lines (interdiff read); the net file list is still the same three files. The first record's ①.1, ①.3–①.8 stand unchanged.

The fix, judged by the tree (unreferencedResidualPicklistRefusal):
2. "Named" is now judged off the fields the engine registers from this artifact, and nothing else — right, and it is the first record's should-fix #1 taken in full. The artifact parameter is gone; the sources are [...bodies, { objects: residualObjects }], where residualObjects are the residual rule's own object entries — the set registerUnclaimedTopLevelObjects registers. collectManifestPicklistReferences over a body still walks its objects, its objectExtensions and its nested plugins[] (manifestSources), each of which registerApp registers; over the synthetic { objects } it walks the residual's objects alone, and that object has no objectExtensions or plugins key to widen it. A top-level objectExtensions entry (never listed by the residual rule: no slot), a top-level copy of a body-declared object (claimed by name, never registered from the top level) and a top-level plugins[] entry (registered only through a body) no longer count. The first record's ①.2 is amended accordingly; the call site passes (residualAnswer, ordered), nothing else changes in register.
3. A residual picklist named only by a non-registered top-level item is now refused — right, and the right direction: on main such a list was dropped silently (the card's own defect through a narrower door) and on b4c76532 it was accepted and still dropped. The two new refusal pins hold the objectExtensions case and the diverging-copy case; the message names all three non-registered sources so an author who wrote one learns why.
4. A residual picklist that only a body's picklistExtensions entry extends is refused here, at boot and after start() — right; it was refused on main too (as an orphan at kernel:ready, or by the sealed audit's orphan leg on a late artifact), so only the words move. The first record's should-fix #2 is taken, with both paths pinned and the post-start() half asserting nothing registered.
5. The message is reworded ("no object field the engine registers names it", plus the parenthesis naming the three non-registered sources); the first refusal pin's assertion follows it. The three remedies are unchanged. Right.
6. Accept-set direction unchanged. Still yes (narrowing); the fix refuses strictly more than b4c76532 did and nothing that main served is newly refused (a list only a non-registered item named served no field on main either). No public export, route, schema or spec surface moves.

② Semver level

  • Changeset: still @objectstack/objectql patch; Clause-②: yes (narrowing) and Metadata-protocol: none line-initial in both the changeset and the PR body. 942aae2b adds one sentence to the Now bullet stating which fields count as naming the list and that a list only a non-registered top-level item names, or only a picklistExtensions entry extends, is refused too — right, that is what the diff now does. Title !, BREAKING banner, ADR-0087 marker not-required (no-migration-prescription) unchanged and still right (first record ②). Check Changeset green on this head.

③ Boundary flags

  • First record should-fix Add metamodel interfaces for ObjectQL/ObjectUI contract #1 (references counted from items the engine never registers): taken in da7e7ee3, both halves (the source set and the pins). Closed. The dev reports the top-level plugins[] case is not pinned — right, it is not; it is named in the docblock, the message and the changeset. Should-fix, carried: one pin (a residual picklist named only by a field of a top-level plugins[] entry's object is refused), so a later change that hands the artifact back into the source list is caught by the file rather than by a reader.
  • First record should-fix ✨ Set up Copilot instructions #2 (picklistExtensions pin): taken, both paths. Closed.
  • First record should-fix Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 (author-time door and spec prose): filed as cli/spec: os validate does not report a composed stack's top-level picklist that the boot now refuses, and the residual-rule prose has no picklist line #28248 (open, finding, p2, pm:blocked, blocked by this PR; its Wanted names both halves and the judgement the boot now gives). Carried; nothing further owed on this PR.
  • The PR body is stale against the head. Its "What changed" paragraph still says "Names is judged off the artifact alone: its top level and every package body", which is b4c76532's rule and the one the fix removed; its Pins section still counts 9 (4 red on main) and lists none of the three new pins. The tree is judged, not the body (this record's ① holds the head), but the body is what a maintainer reads before the tree. Should-fix: bring the "What changed" and Pins sections to the head (the changeset already carries the right sentence).
  • No dev flag arrived on the card after the os-dev report; the delta's account reached this record through the dispatch, not a card comment. Nothing to answer beyond the three changes it names, each verified above.
  • Governed surfaces: none in the file list (Governed Surface Queue Guard green on this head).

④ Design judgment

The narrowing itself — the first record's judgment stands and is tighter: every judge the platform runs on a composed stack's picklist vocabulary (os compile, the kernel:ready audit, the residual objects rule, and now this refusal) reads the same set — the bodies' fields and the residual's objects' fields — so a top-level picklist a composed stack declares either serves a registered field's listed-not-served line or is refused with the three remedies, and no reading is left where it is dropped in silence. The spec-prose and author-time gap is carried by #28248.

packages/objectql/src/plugin.ts — holds. Break attempts on the fix: a body's nested plugins[] objects still count (they register under the body, registerPlugin); a body's objectExtensions still counts (registered by the body seam); a residual object whose field names the list still counts and keeps the listed-not-served line (CONTROL pin unchanged); a residual object the registry would refuse for ownership still counts as naming the list — right, since that object is the residual rule's and the objects rule, not this refusal, says why it is not served; the residual's objectExtensions cannot leak in because the residual rule never lists one (artifactItemSlot gives it no slot). The synthetic { objects: residualObjects } is a fresh literal passed to a reader that never mutates its input. The docblock's new ⛔ paragraph states the rule exactly as the code applies it. Order, envelope and owner fallback are unchanged from the first record.

packages/objectql/src/manifest-service-residual-objects.test.ts — holds. Three new pins under a shared isTopLevelOnlyRefusal assertion (code, status, the list and the stack named): the top-level objectExtensions entry; the top-level copy of acme_case with a diverging status field against the body's acme_case; and the body picklistExtensions entry, at boot and after start() with owners and package ids asserted empty. Each is green only when the artifact's top level is excluded from the source set, so together they are the regression pin for should-fix #1's mechanism. The first refusal pin tracks the reworded message. Nothing pins the top-level plugins[] case (③).

.changeset/28159-residual-picklist-unreferenced.md — holds (②); the added sentence is true of the tree.

No must-fix. Two should-fixes, both carried on this PR: pin the top-level plugins[] case; bring the PR body's "What changed" and Pins sections to the head. No fail-open finding at rule level.

Implemented-by: claude/project-thread-kwainq
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:合成栈(带 packages[])顶层声明、没有包体声明、也没有任何“引擎会注册的对象字段”引用的残留 picklist,以前开机时被悄悄丢掉;现在开机即拒绝(INVALID_METADATA / 422,既有错误码),消息写出 picklist 名、栈名与三条处方(放进包体 / 让对象字段引用 / 删掉)。被残留对象字段引用的 picklist 行为不变。
  • 为什么改:objectql: a composed stack's residual top-level permissions (and other registry-read types) are never registered, like flows were before #28048 #28093 的契约复审 should-fix Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3:这是残留顶层项里唯一既不注册也不拒绝的类型;分诊选“响亮拒绝 + 处方”。
  • 风险与回滚:Clause-②: yes (narrowing),标题带 !:main 上能开机的这种组合现在会被拒绝;不改 schema、不加错误码。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/plugin.ts 的 unreferencedResidualPicklistRefusal。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/manifest-service-residual-objects.test.ts src/manifest-service-residual-security-seal.test.ts,75 passed。
  • 需要你做的:无(非受管;契约复审 PASS;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: a2fb76a079fd8e6b022d4f9b86f263b2f3c1cbf1

  • Shape: 3 files, +223 / −1. They are packages/objectql/src/plugin.ts (the residual registration section), manifest-service-residual-objects.test.ts and one patch changeset for @objectstack/objectql. check-governed-merges --pr 28231: NOT governed (0 of 3 paths).
  • Premise measured: on main (35f20551), a residual picklist that no field names booted (state=running), with the list absent and no warning. The four CONTROLs kept main's behaviour:
    • a picklist named by a residual object's field ("listed and not served");
    • a body-declared picklist;
    • a stack with no residual picklist;
    • a stack without packages[].
  • Pins and runs: both residual files pass 75/75, and 7 pins fail against main's plugin.ts.
    • Ablations A1 to A8 each turned their pins red and were restored. A6 puts the artifact's whole top level back into the named set, and it fails the two new top-level pins.
    • objectql: 9735 passed; typecheck 0.
    • dispatch-gates --ran on this head: 61 derived, 61 run, 0 NOT-MEASURED.
    • pnpm lint: 0.
  • Gate check-runs on the head: 48 runs, 35 success, 13 skipped, 0 failure/pending; mergeable_state clean.
  • Contract review: owed on the narrowing face at CONTRACT_REVIEW_TIER. It is a PASS on this head, in record 6532447134, which supersedes 6531047218 (PASS on b4c76532). Neither has a must-fix.
  • Correction to the PR body (written on b4c76532; the relay cannot re-edit it):
    • "What changed" says the named set is judged "off the artifact alone: its top level and every package body". On this head the named set is the fields the engine registers: every package body's objects, objectExtensions and nested plugins[], plus the residual's own objects (unclaimedTopLevel).
    • A residual picklist named only by a top-level objectExtensions entry, a top-level copy of a body-declared object with diverging fields, or a top-level plugins[] entry is therefore refused too. None of those is registered in a composed stack.
    • The Pins section's "9 (4 red on main)" is now 12 in the new describe (7 red on main), with three new pins:
      • top-level objectExtensions;
      • the diverging body-claimed copy;
      • a body picklistExtensions entry on an unnamed residual picklist, which was refused on main too (only the message differs).
  • Breaking: the PR title carries ! and the changeset a BREAKING banner, with ADR-0087 marked not-required (no-migration-prescription): the refusal's message is the prescription.
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 20:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 20:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 35ffb77 Oct 30, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kwainq branch October 30, 2026 20:44
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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql: a composed stack's residual top-level picklist that no object names is neither registered nor refused

2 participants