Skip to content

fix(cli)!: os validate and os build refuse a composed stack's top-level picklist that the boot refuses - #28274

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

维护者速读

  • 问题:PR fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names #28231 之后,组合栈(带 packages[])顶层声明、无包体声明且无引擎注册字段引用的 picklist 会在启动时被拒绝,但 os validate / os build 照常通过,作者到启动才发现。
  • 修复:两道作者期入口复用残余规则(unclaimedTopLevel)并按启动的同一判定拒绝它,规则 id picklist-residual-unreferenced,消息与处方逐字等同启动的拒绝文字。
  • "被引用"的集合与启动相同:包体的 objects / objectExtensions / 嵌套 plugins[] 字段,加上残余对象的字段;顶层 objectExtensions、包体对象的顶层副本、顶层 plugins[] 不算。
  • 共享方式:引擎未导出该判定,且本卡文件面不含 packages/objectql,所以在 cli 中重述,并以启动对拍测试(真实内核 + ObjectQLPlugin)逐字节锁住两边一致。
  • 文案:packages/metadata 残余规则头注释与 packages/spec/src/stack.zod.ts 的一个 docblock 补上 picklist 例外,无 schema 变更,check:generated 通过。
  • 破坏性:作者期收窄(Clause-②: no (narrowing)),只拒绝启动本已拒绝的栈;patch 级 changeset,ADR-0087 not-required。

Clause-②: no (narrowing) — os validate and os build refuse a composed stack whose top level declares a picklist that no package body declares and no object field the engine registers names, which they accepted before; the boot already refuses every such stack, so nothing that boots is newly refused, and nothing is widened
Metadata-protocol: none

What changed

  • The judgement (packages/cli/src/utils/picklist-references.ts). judgePicklistReferences gains a third refusal, picklist-residual-unreferenced. It reads the residual rule's own answer (unclaimedTopLevel from @objectstack/metadata, the copy both boot doors and view-container-names.ts read) and takes its picklists. Each one that no object field the engine registers names is refused, located at its top-level index (picklists[i]). "Named" is the boot's set (unreferencedResidualPicklistRefusal in @objectstack/objectql): a field of a body's objects / objectExtensions / nested plugins[], or of a residual object. A top-level objectExtensions entry, a top-level copy of a body-declared object, and a top-level plugins[] entry name nothing. Both doors call this function (os validate step 2d, os build step 3a-bis), so both refuse.
  • The doors' text face (validate.ts, compile.ts). The heading over the refusals now comes from one shared picklistRefusalHeading. It names a residual refusal as the boot's refusal, not as "a picklist reference names a picklist this stack does not declare". With only reference refusals, the heading is byte-identical to before. --json is unchanged in shape: the new refusal rides errors like the others.
  • The prose. The residual-rule header in packages/metadata/src/plugin.ts and the permission-set owner gate's docblock in packages/spec/src/stack.zod.ts now state the picklist exception. A residual picklist is metadata only: when a residual object's field names it, the object is listed and not served; when no registered field names it, the stack is refused at boot and by os validate / os build first. This is the one docblock in stack.zod.ts. It is not a .describe(), so nothing generated moves, and pnpm --filter @objectstack/spec check:generated reports all 14 artifacts up to date.
  • The gate-parity ledger (packages/cli/test/validate-build-gate-parity.test.ts) classifies picklistRefusalHeading as presentation, beside printPicklistReferenceNotices.

Judgement-sharing route: restated, held by a boot-parity pin

The judgement is restated in the CLI rather than imported. The engine's unreferencedResidualPicklistRefusal and collectManifestPicklistReferences are module-private in @objectstack/objectql. Exporting them is an edit to packages/objectql, which this card's claim excludes. What is shared is the residual rule itself (unclaimedTopLevel), so which items are residual is never re-derived. What is restated is the "named" walk and the wording.

picklist-references.boot-parity.test.ts keeps the restatement honest. It judges each stack with the door, then boots it on a real ObjectKernel with the real ObjectQLPlugin, handed over the way AppPlugin hands a config stack. Where the door refuses one list, the boot's INVALID_METADATA message must equal the door's message + ' ' + hint byte for byte. On every control, neither side raises this refusal.

A follow-up could export the engine's judgement from @objectstack/objectql and drop the restatement, the way viewContainerNameRefusal is shared.

Measurement

The doors were measured with the real CLI (os validate --json, os build) on main's picklist-references.ts (f40b777d) and on this branch. The boot was measured by the parity test's kernel boot on the same engine (merged PR #28231).

Stack Boot main: validate / build This branch: validate / build
Composed, top-level picklist no body declares and no field names refused, INVALID_METADATA exit 0, no finding / exit 0, artifact written exit 1, picklist-residual-unreferenced at picklists[0] / exit 1, no artifact
CONTROL: same list, named by a residual object's field boots (object listed, not served) exit 0 / exit 0 unchanged
CONTROL: body-declared list, repeated on the top level boots exit 0 / exit 0 unchanged
CONTROL: no packages[], unnamed top-level list boots exit 0 / exit 0 unchanged

Pins

  • New unit pins (picklist-references.test.ts), 9 refusal or heading pins:
    • the refusal (rule, where, path, the list, the stack, all three remedies);
    • each list as its own finding;
    • a list named only by a top-level objectExtensions entry;
    • a list named only by a diverging top-level copy of a body object;
    • a list named only by a top-level plugins[] entry;
    • a list only a body picklistExtensions entry extends;
    • the three heading pins: references alone keep the old heading, residual alone, and both together.
  • New unit CONTROLs, 4: a residual object's field names the list; a body object's field names it (still picklist-reference-unknown alone); a body-declared list, repeated on the top level or not; a stack without packages[].
  • Boot-parity pins: 4 refused stacks and 4 controls.
  • Against main's picklist-references.ts: 13 fail and 27 pass. The 13 are all 6 unit refusal pins, all 3 heading pins (the function is new), and all 4 parity refusal pins. Every control passes.
  • On this branch: 40/40 pass.

Ablations, each restored from HEAD afterwards:

Ablation Fails
B1: never raise the refusal 10: every refusal pin, unit and parity
B2: drop residual objects from the named set 2: the residual-object control, unit and parity
B3: drop bodies from the named set 1: the body-field control
B4: also count the whole top level as named 5: the objectExtensions, diverging-copy and plugins[] pins, unit and parity
B5: take every top-level picklist, not only the residual's 1: the parity body-declared control
B6: one word of the wording drifts from the boot's 4: every parity refusal pin

Gates

Final head e1913ec6, which merges main at 361ea242 (it brings PR #28244's other region of stack.zod.ts, merged cleanly). Run after a full rebuild:

  • Derived gates. dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 85 families. All 85 were run with exit codes recorded, and all exited 0. --ran reports 85 run, 0 NOT-MEASURED and 0 UNRUN, and exits 0.
  • Not measured locally. The derivation names work outside the 85 that cannot run locally: 4 families that take a value from the workflow, 5 CI jobs these paths schedule, and 5 type-check lanes. CI runs them.
  • Generated output. pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.
  • Lint and typecheck. pnpm lint: exit 0. pnpm --filter @objectstack/cli run typecheck and pnpm --filter @objectstack/metadata run typecheck: exit 0 (on 1fa87152; the later commits change only the changeset and merge main).
  • Suites on e1913ec6. The four picklist/door files: 80/80 (picklist-references.test.ts, the boot-parity file, validate-build-gate-parity.test.ts, picklist-reference-doors.test.ts).
  • Suites on 36aea717, the previous merge head.
    • @objectstack/cli: 537 files pass. One file, migrate/data-commands.absent-database.integration.test.ts, hit a beforeAll timeout under box load; re-run alone, it passes 51/51.
    • @objectstack/metadata: 906/906.
    • Dogfood battery under the verify lock: VERDICT command-exit 0, 360 files pass and 3 skipped, 2896 tests pass and 20 skipped.

Fixes #28248

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1Np3hU7RQnA13YZcL

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/metadata, @objectstack/spec, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/metadata/src/plugin.ts, packages/spec/src/stack.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 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 b358e7995e6f5a04a7e231abb8d4628d61e9a017.

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

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/metadata/src/plugin.ts, packages/spec/src/stack.zod.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 81 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 59 of 211 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 — 179 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 b358e7995e6f5a04a7e231abb8d4628d61e9a017 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json b358e7995e6f5a04a7e231abb8d4628d61e9a017

⚠️ 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 b358e7995e6f5a04a7e231abb8d4628d61e9a017 → 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 e1913ec · 2026-10-31T00:55Z

@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: e1913ec6b97cb36cdb19fd08685ede4f9f737886
Local-runs: none
Scope: packages/spec/src/stack.zod.ts + packages/objectql/src/plugin.ts on main (RESIDUAL_TYPES_WITH_OWN_PATH, unreferencedResidualPicklistRefusal and its wording, registerUnclaimedTopLevelObjects, the register order), packages/objectql/src/picklist-resolution.ts (collectManifestPicklistReferences, manifestSources, collectPicklistReferences), packages/metadata/src/plugin.ts (unclaimedTopLevel, artifactItemSlot, the residual-rule header this diff extends), packages/metadata/src/view-container-name.ts and packages/objectql/src/view-container-name-refusal.ts (how viewContainerNameRefusal is shared), packages/cli/src/utils/picklist-references.ts, packages/cli/src/utils/picklist-references.boot-parity.test.ts, packages/cli/src/utils/picklist-references.test.ts, packages/cli/src/utils/artifact-packages.ts (artifactPackages), packages/cli/src/utils/view-container-names.ts (the precedent import), packages/cli/src/commands/validate.ts and compile.ts (the two call sites), packages/cli/test/validate-build-gate-parity.test.ts (the GATES and NOT_A_GATE rosters), packages/cli/package.json (dependencies), packages/cli/vitest.config.ts (UNIT_FILES), .changeset/28248-validate-residual-picklist.md, scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs. hits is the one stack.zod.ts docblock; every other path of the diff is the lane seat's ACCEPT at TIER_DEFAULT, read here as cited code only.

Inputs: card #28248 (body; triage 6532308421; unblock 6533130283; claim 6533173970; os-dev report 6536799895), PR #28274 (body, file list, net diff against main at the head: merge-base 361ea242 is main's tip, nine files, +399/-6), the head's check-runs, and the claim 6536814763. Read-only: the tree was judged, not run.

Check-runs at 2026-10-31T01:01Z: 41 check-runs on the head. 13 success (Check Changeset, Check PR Size, PR-body guards, Governed Surface Queue Guard, Auto Label, Flag docs affected, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Plan Test Core shards, Type Check · source gates, Dogfood Regression Gate, TypeScript Type Check planning leg, filter); 8 skipped (path-gated UI, docs and opt-in lanes). 20 still in_progress and not yet reported: Build Core, Build UI Deps, Live E2E UI, Test Core (1/8) through (8/8), Lint · CI, release & engine gates, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace — so the aggregate Lint & Repo Gates, TypeScript Type Check, Build Core and Test Core contexts have not reported, and the ADR-0087 and changeset-level gates (Lint · CI, release & engine gates) are among them. The verdict below is on the diff; the landing waits on those. PR mergeable_state: blocked (draft, checks pending).

① Derived judgments

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

  1. os validate and os build now refuse a composed stack whose residual holds a picklist that no object field the engine registers names — right, and the card's first Wanted. findUnreferencedResidualPicklists returns [] without a packages key, reads the residual through unclaimedTopLevel from @objectstack/metadata (the one copy; not re-derived, as view-container-names.ts already reads it), takes its picklist entries, and builds "named" from each body (artifactPackages(parsed)[i].body, which is packages[i].manifest — the same bodies resolveArtifactPackageOrder hands the boot) plus the residual's own object entries. That is the engine's set on main ([...bodies, { objects: residualObjects }]). One finding per list, severity: 'error', rule: picklist-residual-unreferenced, path: picklists[i] from the residual entry's own field/index. Both doors call judgePicklistReferences ahead of every artifact write (compile.ts step 3a-bis; the artifact write is far below), so os build writes nothing for a refused stack and --json carries the finding in errors unchanged in shape.
  2. The named-walk matches the engine's reader — right. fieldPicklistNamesOf walks objects and objectExtensions of the source and of each plain-object entry of its nested plugins[], in both field spellings, reading the picklist key as a non-empty string — collectManifestPicklistReferences over manifestSources does the same. The one difference is that the CLI also accepts a record-shaped objectExtensions where the engine reads an array only; the stack schema declares objectExtensions as an array, so on a parsed stack the shape is unreachable and the sets are equal.
  3. The wording is the engine's, byte for byte, for one list — right. residualPicklistWording(name, owner).judgement + ' ' + .prescription equals the engine's singular message on main character for character (the parenthesis, the "nothing said" sentence, the three remedies and the listed-and-not-served aside included), with the same literal fallback for a stack with no id. For several residual lists the engine says one plural sentence and the door emits one singular finding per list; that is the door's per-item shape, the parity pin is scoped to single-list stacks by its own docblock, and the remedy is identical either way.
  4. The three cases the boot does not refuse are not refused here — right. A residual object's field naming the list keeps it out of "named"'s complement (CONTROL pinned, unit and parity); a body's field naming it is picklist-reference-unknown as before and not this rule (CONTROL pinned); a body-declared list, repeated on the top level or not, is claimed and never residual (CONTROL pinned, unit and parity); a stack without packages[] returns before unclaimedTopLevel (CONTROL pinned, unit and parity).
  5. The text face's heading — picklistRefusalHeading prints the previous sentence byte-identically when only reference refusals are present (pinned), names a residual refusal as the boot's, and joins both; --json does not read it. Right, and presentation only (⑥).
  6. The gate-parity ledger — picklistRefusalHeading is added to NOT_A_GATE beside printPicklistReferenceNotices; judgePicklistReferences was already a GATE row, so the new refusal rides an existing gate both doors run. Right: without this row the ledger would fail on the new import both doors share, and the row is a true classification.
  7. The boot-parity test — boots a real ObjectKernel with the real ObjectQLPlugin and a registering plugin that hands { ...manifest, ...stack } to the manifest service in init(), the way AppPlugin does; asserts INVALID_METADATA and message === door.message + ' ' + door.hint on four refused stacks and no such refusal on four controls. @objectstack/objectql and @objectstack/core are dependencies of packages/cli already (os serve boots a kernel), so the import is not a hoisting accident; the file sits under src/utils/*.test.ts, which the walk behind UNIT_FILES collects. Right.
  8. Prose — the residual-rule header in packages/metadata/src/plugin.ts and the stack.zod.ts docblock gain the picklist line (④). Not a .describe(), so no generated artifact moves.
  9. No published schema, export, route or boot behaviour changes. The engine is untouched; packages/spec changes one comment; @objectstack/cli gains one exported rule id and one exported heading function.

② Semver level

  • Changeset .changeset/28248-validate-residual-picklist.md: @objectstack/cli patch, @objectstack/metadata patch, @objectstack/spec patch; body carries Clause-②: no (narrowing) and Metadata-protocol: none, both line-initial and identical to the PR body's two lines. The metadata and spec bumps carry comment-only hunks — over-declared rather than wrong; skip-changeset was never available because @objectstack/cli publishes behaviour.
  • Clause-②: no (narrowing) is right, and it is the reader's own spelling for this case. clause2-line.mjs defines the four combinations: no (narrowing) is "NOT a widening, but breaking. This is the whole point of the arm"; yes (narrowing) is "a diff that widens one surface and narrows another". This diff widens nothing (the value no is truthful) and os build now stops writing an artifact it wrote before (the arm is owed), and AGENTS.md step 3 admits no with the (narrowing) arm (only no (widening) is malformed). The claim expected a bare no; the dev's arm is the stricter and more honest reading, and the title's !, the scoped BREAKING banner and the ADR-0087 marker follow from it. For the record, this is the more exact spelling than the yes (narrowing) the merged PR fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names #28231 carried for a pure narrowing; both pairs grade patch under ruling A with Metadata-protocol: none, so no level was affected there.
  • Metadata-protocol: none — right: no key, value, type or default an author writes changes; the refusal is a tooling judgement of a composition the schema still accepts, and the prose is comments. check-changeset-no-major.mjs admits none beside any arm. Level: none takes patch. Right; Check Changeset green.
  • ADR-0087 not-required (no-migration-prescription): right — nothing an author writes is retired, objectstack migrate meta has nothing to rewrite, and the "remedy" is an authoring placement, not a FROM-to-TO rewrite. The gate that re-checks it (Lint · CI, release & engine gates) has not reported on this head.

③ Boundary flags

  • open_questions: [] — none to answer.
  • Dev flag: arm no (narrowing) rather than the claim's bare no. Answered in ②: right.
  • Dev flag: a contract review is owed off the stack.zod.ts hunk although it is prose. Right — reviewFace reads the schema face off the path; this record is it, and ④ judges the docblock as the one hits file.
  • Dev flag, and the dispatch's first question: restating the judgement in the CLI, held by a boot-parity pin, versus exporting it the way viewContainerNameRefusal is shared. Read: viewContainerNameRefusal is defined once in @objectstack/metadata (view-container-name.ts), re-exported by @objectstack/objectql (view-container-name-refusal.ts), and imported by the CLI from @objectstack/objectql; the engine's picklist judge and its reader are module-private in packages/objectql/src/plugin.ts and picklist-resolution.ts, and the claim excluded packages/objectql and allowed packages/metadata prose only, so neither export was in reach. What the head shares is the residual rule itself (unclaimedTopLevel), which decides WHICH items are residual; what it restates is the named-walk and the words, and the parity pin holds the words byte-equal on a real boot and the walk equal on eight stacks, red the moment either side moves (ablation B6 is the dev's; the pin's assertion is read here). Acceptable for this card, as a should-fix carried on a card, not a must-fix: the repo's standing is one spelling per judgement (the residual-rule header's own "never a second copy at a door"; record 6525505738 treated a copied refusal envelope the same way and filed objectql: the permission-set owner refusal envelope is built twice (body seam and residual registration) #28160). The restatement is the smaller evil than a packages/objectql edit outside the claim, and the pin makes the copy loud; but the cost is real and asymmetric — a future wording change in packages/objectql reds packages/cli's suite, and the engine's function docblock (excluded file) does not point at the parity file. Should-fix ✨ Set up Copilot instructions #2: file the follow-up card the dev names: one exported judge (named-walk plus wording), defined once — @objectstack/metadata beside unclaimedTopLevel is the precedent's home, with @objectstack/objectql re-exporting as it does for the container judge — imported by both doors; the parity pin then shrinks to a smoke check or goes.
  • The dispatch's second question: is packages/cli/test/validate-build-gate-parity.test.ts a fair reading of "their tests" in the claim? Yes. The claim's surface names "the os validate / os compile caller that reports it if needed" and "their tests"; the ledger is the test of exactly those two callers (it holds that both doors run the same gates and classifies every shared import), and the diff's one row there is forced by the new import both callers take. A ledger row is the minimal edit that keeps the callers' test true; leaving it out would red the ledger, not pass it.
  • Dev flag: the existing picklist-reference-unknown walk does not judge a residual object's field naming a list nobody declares, nor a body's nested plugins[]. Read: right, and outside this card (that walk predates it; the residual object's field naming an undeclared list is the boot's listed-and-not-served case, reported by the boot, not refused). No action here; a card if the seat wants os validate to foretell that warning too.
  • Dev flag: the two should-fixes of record 6532447134 (the objectql plugins[] pin, the PR fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names #28231 body) fall outside this claim. Right; PR fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names #28231 is merged. The CLI side of the plugins[] case is pinned in the unit file here, but not in the parity file — it is the one refused case without a boot twin. Should-fix Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3: add it to STACKS in the parity test (one line), so the door's and the boot's exclusion of top-level plugins[] is held equal like the other three.
  • Dev deviations (suites outside os-verify-lock, restarted gate runs): the head's check-runs are the gate verdicts this record relies on; twenty of them have not reported.
  • Serial constraint: PR feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) #28244 edits another region of stack.zod.ts; the head merges main at 361ea242 and the merge-base IS main's tip, so there is nothing left to merge. Governed surfaces: none (Governed Surface Queue Guard green).

④ Design judgment

packages/spec/src/stack.zod.ts (the one hits file: five docblock lines on applyPermissionSetOwnerBindingGate) — holds, with one should-fix. The scenario it is accepted against is the card's second Wanted: the published spec's residual-rule prose carries the picklist line the runtime enforces. Against the code it cites, each sentence: "a residual PICKLIST is kept as metadata only, never registered with the engine" — true on main (RESIDUAL_TYPES_WITH_OWN_PATH excludes picklist from unclaimedTopLevelItems; the metadata door registers it; the engine never does). "When a residual object's field names it, the boot lists that object and does not serve it" — true (registerUnclaimedTopLevelObjects judges the field against the bodies' lists and warns). "When no object field the engine registers names it, the boot refuses the stack, and os validate / os build refuse it first" — true on this head (unreferencedResidualPicklistRefusal before anything registers; judgePicklistReferences at both doors). Placement: the one docblock where the published spec states the residual rule in general, the one the card named. Not a .describe(), so check:generated has nothing to move. Break attempt: the docblock states two of the three lines. The case of a package BODY's field naming a residual picklist — refused by the boot's kernel:ready picklist audit as "no loaded package declares", and picklist-reference-unknown at the doors — is stated in the metadata header this same diff writes ("when a body's field names it, the boot's picklist audit refuses that field") and omitted here. A reader of the spec's sentence "when a residual object's field names it, the boot lists that object and does not serve it" may infer that a body's field naming it serves. Should-fix #1: add the body-field clause to the stack.zod.ts docblock so the two prose surfaces say the same three lines.

The cited CLI code was read to judge the docblock's third sentence, not reviewed; what the read found is recorded in ① and ③ for the lane seat.

Should-fixes:

  1. packages/spec/src/stack.zod.ts: add the third line (a body's field naming a residual picklist fails the boot's picklist audit, picklist-reference-unknown at the doors), as the metadata header states it.
  2. File the follow-up card for one exported judgement (named-walk plus wording) defined once, @objectstack/metadata beside unclaimedTopLevel on the viewContainerNameRefusal precedent, imported by the engine and both doors; the restatement and most of the parity pin then go.
  3. picklist-references.boot-parity.test.ts: add the top-level plugins[] stack to STACKS, the one refused case without a boot twin.

No must-fix. No fail-open finding: every stack this diff refuses is one the boot refuses on main, and every stack the boot accepts passes both doors as before.

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 3298154 · 2026-10-31T01:56Z

@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: 329815488ac87fac89f1dc1341c30f2b6a32c3fb
Local-runs: none
Scope: packages/spec/src/stack.zod.ts + packages/metadata/src/plugin.ts (the residual-rule header), packages/cli/src/utils/picklist-references.boot-parity.test.ts, packages/cli/src/utils/picklist-references.ts (findUnreferencedResidualPicklists, fieldPicklistNamesOf), packages/objectql/src/plugin.ts on main (unreferencedResidualPicklistRefusal, the register order), packages/metadata/src/plugin.ts (unclaimedTopLevel, ARTIFACT_FIELD_TO_TYPE). The rest of record 6536889061's scope was re-read only where this delta touches it; hits is still the one stack.zod.ts docblock, and every other path of the diff is the lane seat's ACCEPT at TIER_DEFAULT.

This record supersedes 6536889061 (head e1913ec6, a PASS with three should-fixes). Delta reviewed: e1913ec6b97cb36cdb19fd08685ede4f9f737886..329815488ac87fac89f1dc1341c30f2b6a32c3fb — 54a4166f (the docblock's body-field line and the matching parenthetical in the metadata header) and 32981548 (the top-level plugins[] stack in the parity file). Both are single-parent commits; no merge is in the delta, the merge-base against main is still 361ea242, and the net diff against it is still the same nine files (+406/-6). Inputs as before (card #28248 with every comment; the PR body, file list and net diff; the head's check-runs; the delta claim 6537481253; card #28275). Read-only: the tree was judged, not run.

Check-runs at 2026-10-31T01:59Z: 49 check-runs on the head, every one completed: 39 success — Lint & Repo Gates and every Lint · leg (CI, release & engine gates among them, so the ADR-0087 and changeset-level gates have now reported green), TypeScript Type Check and every Type Check · leg, Build Core, Test Core and all 8 shards, Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate, Spec property liveness, Live E2E UI, Test UI, Type Check UI, Build & E2E UI, Build UI Deps — and 10 skipped (opt-in, docs and matrix-template lanes). Nothing has yet to report. origin/main has moved on to c5bf56b8 since the merge-base; no commit on it touches packages/objectql/src, packages/metadata/src/plugin.ts, packages/spec/src/stack.zod.ts, packages/cli/src/utils, packages/cli/src/commands or the gate-parity ledger. The PR reads mergeable_state: clean, still a draft.

① Derived judgments

  1. Nothing in the accept set or the public surface moves in the delta. 54a4166f changes comments only (a docblock and a // header); 32981548 adds one entry to a test fixture. The first record's ①.1–①.9 stand unchanged; no source line of picklist-references.ts, the two doors, the ledger or the changeset is touched.
  2. The parity file gains the top-level plugins[] stack — right, and the first record's should-fix Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 taken exactly. The fixture puts plugins: [{ name, objects: [noteObj with a field naming parity_unused] }] on the composed stack's top level beside the residual list. At the door: findUnreferencedResidualPicklists builds "named" from the bodies and the residual's objects, and a top-level plugins[] entry is neither (ARTIFACT_FIELD_TO_TYPE lists no plugins, so the residual rule never lists it), so the list is refused. At the boot: register hands unreferencedResidualPicklistRefusal the bodies alone, so the same list is refused with the same words; the top-level plugins key is read by nothing on a packages[] artifact. The it.each case therefore asserts INVALID_METADATA and the byte-equal message like the other four refused stacks, and Test Core is green on this head with the file in UNIT_FILES. Every refused case the unit file pins now has a boot twin.

② Semver level

  • Unchanged: @objectstack/cli, @objectstack/metadata, @objectstack/spec patch; Clause-②: no (narrowing) and Metadata-protocol: none line-initial in the changeset and the PR body; title !, scoped BREAKING banner, ADR-0087 not-required (no-migration-prescription). The delta adds no published text to the changeset and moves no .describe(), so nothing generated changes (Spec property liveness and the Lint · legs green). The first record's ② stands, and the gates it said had not reported have now reported success.

③ Boundary flags

④ Design judgment

packages/spec/src/stack.zod.ts (the one hits file; the delta's eight docblock lines) — holds. Against the code it cites, the three sentences in order: a residual object's field naming the list makes that object listed and not served (registerUnclaimedTopLevelObjects on main); a package body's field naming it is refused by the boot's kernel:ready picklist audit (findUnresolvedPicklistReferences — the residual list is never in the registry) and reported by the doors as picklist-reference-unknown (the pre-existing walk, pinned as such in the unit CONTROL); no registered field naming it refuses the stack at boot and at both doors first. Each true in the tree; the audit's hook is named correctly (kernel:ready), and the doors' rule id is spelled as exported. The published spec's prose and the runtime now agree on all three lines, which is the card's second Wanted in full. Nothing else in the docblock moved.

No must-fix, no should-fix left on this PR. No fail-open finding.

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

维护者速读

  • 改了什么:os validate / os build 现在提前拒绝开机会拒绝的那种残留顶层 picklist(合成栈顶层声明、没有包体声明、也没有引擎会注册的对象字段引用),判定与措辞和开机一致(picklist-residual-unreferenced,定位 picklists[i]);packages/metadata 残留规则头注与 stack.zod.ts 一处文档块补上 picklist 的三条线。
  • 为什么改:PR fix(objectql)!: refuse a composed stack's top-level picklist that no package body declares and no object field names #28231 之后开机会拒绝它,但作者在 os validate 时看不到,只能等开机才知道。
  • 风险与回滚:Clause-②: no (narrowing),标题带 !:validate/build 现在拒绝以前能构建、但开机本来就会拒绝的栈;不改 schema、不加错误码、生成物不动。回滚即 revert 本 PR。
  • 先看:packages/cli/src/utils/picklist-references.ts 与 picklist-references.boot-parity.test.ts(真开机与 CLI 措辞逐字节一致)。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run src/utils/picklist-references.test.ts src/utils/picklist-references.boot-parity.test.ts,全绿。
  • 需要你做的:无(非受管;契约复审 PASS;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: 329815488ac87fac89f1dc1341c30f2b6a32c3fb

  • Shape: 9 files, +406 / −6.

    • The judgement and heading in packages/cli, with validate.ts / compile.ts printing through one shared heading.
    • The unit and boot-parity tests, and one presentation row in validate-build-gate-parity.test.ts.
    • Prose only in packages/metadata/src/plugin.ts and one docblock in packages/spec/src/stack.zod.ts, under triage's one-PR cross-domain exception.
    • One changeset: patch for cli, metadata and spec.

    check-governed-merges --pr 28274: NOT governed (0 of 9 paths). check:generated: all 14 artifacts fresh.

  • Premise measured: with the real CLI on main, a residual picklist that no registered field names gave validate/build exit 0 and wrote an artifact, while the boot refused it INVALID_METADATA. On the branch both exit 1 and no artifact is written. The four CONTROLs are unchanged.

    • The four picklist and door files pass 81/81. Against main's file, 13 fail and every control passes.
    • Six ablations (B1–B6) each turned their pins red and were restored. B6, one word of the wording drifting, reds every parity pin.
    • Lint 0. dispatch-gates --ran: 85 derived, 85 run, 0 NOT-MEASURED.
    • cli suite 537 files, metadata 906/906, typechecks 0, and dogfood VERDICT 0, all on the earlier head 36aea717. Later commits are comments, one fixture entry and main merges.
  • Gate check-runs on the head: 49 runs, 39 success, 10 skipped, 0 failure/pending; mergeable_state clean.

  • Contract review: owed on the schema face at CONTRACT_REVIEW_TIER, with hits on stack.zod.ts (docblock only). It is a PASS on this head in record 6537507568, which supersedes 6536889061 (PASS on e1913ec6). Neither has a must-fix.

  • Accepted deviations:

    • The claim expected a bare Clause-②: no. The arm is no (narrowing), the stricter honest reading, because os build stops writing an artifact it wrote before. The title carries !, the changeset a scoped BREAKING banner, and the ADR-0087 marker reads not-required (no-migration-prescription).
    • packages/cli/test/validate-build-gate-parity.test.ts is read as the callers' test under the claim's "their tests". The record agrees: the ledger is exactly the two doors' shared-gate test.
  • Correction to the PR body (written on e1913ec6; the relay cannot re-edit it): the prose bullet now also carries the body-field line, and the focused test count is 81/81 on this head (the boot-parity file has 9 stacks: 5 refused, 4 controls).

  • 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 31, 2026 02:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 02:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 3fd4d6b Oct 31, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kwainq branch October 31, 2026 02:24
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.

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

2 participants