Repository navigation
fix(objectql)!: a field's picklist resolves from the package that declares it; the api index keeps one item per package and name - #31731
Conversation
…s package; key the api index by (package, name) A field's picklist, a picklistExtensions target and the boot/install audits resolve the list name through the package-scoped resolver from the package that wrote the reference (ADR-0150 D3/D4). A packaged reference whose only holder is an environment-authored list is served with no options and named at boot, never a refused boot. The api endpoint index keeps one slot per (package, name). Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
… resolve by referrer Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
…t-only picklist Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 25e4e81da1c6337fed13b984f00a322b5ddd2813 && git checkout 25e4e81da1c6337fed13b984f00a322b5ddd2813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 87d945873da8e13ae2b6ee5ac43c075203abc337 bf7e3295e9ed0c6ca46217e172e25ed5f4aeaee7 && git checkout -B drift-repro 87d945873da8e13ae2b6ee5ac43c075203abc337 && git merge --no-ff bf7e3295e9ed0c6ca46217e172e25ed5f4aeaee7
node scripts/docs-audit/affected-docs.mjs --json 87d945873da8e13ae2b6ee5ac43c075203abc337
|
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review (preliminary): E6 #31678, head 62e2f40 · 2026-11-09T00:36Z This is a preliminary verdict. The final check runs on the head after R2 #31716 lands and N1 shows Item 1: picklist referrer with more than one packaged contributor. Item 2: the install judge's artifact-wide Reads as passing:
Not a governed surface. Seat 2 lands it after my final PASS. |
…rrer reaches the same list; judge a sibling's list through the resolver at install Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
…t the install door Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-steve · session_01ADHkmRb3wUaRcujR7QEm2o Both items from the preliminary epic review (6729460160) are fixed at head Item 1: picklist referrer with more than one packaged contributor.
Item 2: the install judge's artifact-wide
Runs on
Still owed for the final head: merge Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmRb3wUaRcujR7QEm2o CI on
Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmRb3wUaRcujR7QEm2o Test Core (4/8) on If another shard goes red on No other test in this shard failed. Nothing new to fix here. Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmRb3wUaRcujR7QEm2o Test Core (1/8) on
Generated by Claude Code |
…de the declared closure Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
…boot warning for it Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o Co-authored-by: Claude <noreply@anthropic.com>
…ndencies in the picklist judge The referrer check now treats the platform holder kind the lean resolver returns as one holder, and the install judge's pending-manifest view hands the closure walk the registered residual owners' declared dependencies, so a package that reaches a list through a residual owner is not refused at install. The residual fixture declares its sibling dependency in its manifest, where the residual owner's dependencies are read. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmRb3wUaRcujR7QEm2o
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review, final: E6 #31678, head bf7e329 · 2026-11-09T05:50Z Verdict: PASS.
Seat 2 marks it ready, accepts and lands it. It is not a governed surface. Child A records the ADR-0130 dated note and the |
|
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM Contract reviewServed-tier: Inputs: card #31678 (body and all 10 comments, rulings 6728478528, 6728613177 and 6729820123 included), PR #31731 body and file list, the net diff of ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged against ADR-0150 D3/D4 on
② Semver levelChangeset
③ Boundary flagsEvery dev flag and
④ Design judgmentThe narrowing itself, against the published spec on Attempts to break it, read in the live tree:
Findings (must-fix: none; should-fix: none; nits: six):
Nits alone: the round ends on this head. F1 to F3 are follow-up carriers, F4 to F6 accepted deviations; nothing is asked of this PR's code or prose. Red lines (card acceptance). Resolution never loosens: confirmed by reading, no fallback path remains. Ambiguity is reported, never guessed: every refusal carries R's sentence naming every candidate (boot warning, install refusal, Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM 维护者速读
席位核对 — ACCEPT · domain:server#2 · Head-sha
|
…0129 and ADR-0130 point at it (ADR-0150 child A) (objectstack-ai#31841) <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbRSEASPFQojyg3PhVYDG2Jp)_ Fixes objectstack-ai#31685 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改文档(四份 ADR 和一处代码注释,不改代码)。ADR-0150 补记子卡上已裁定、或由专题席位记录的七条:闭包外的包被拒绝(含同一发布包里没声明依赖的兄弟包)、阶段一之前就重名的部署被拒绝、同对象同名动作被拒绝(落在动作收尾卡)、`base`/`system`/`sys` 不受同命名空间唯一规则约束、扩展目标和 everyone/guest 锚点按无包调用方解析、无归属的引擎函数算平台持有者。ADR-0048 §5 的 lint 条目改成现状;ADR-0129、ADR-0130 加带日期的交叉引用。 - 为什么改:这些裁决都已在子卡上定下并有记录,但 ADR-0150 正文还写着旧说法(比如「阶段一覆盖的类型不会有两个持有者」「动作已被阶段一兜底」);P 合入后 ADR-0048 §5 还说 lint 对所有裸名告警、阶段三会改成拒绝。不改的话,后续子卡会照着过时的正文去做。 - 风险与回滚:只改文字,无运行时风险,不发版(`skip-changeset`)。回滚即撤销这一个提交。 - 先看:ADR-0150 D3 新增的 (c) 段和「Covered and uncovered types」段;ADR-0048 §5 第一个条目;ADR-0130 D1 下的注记。 - 两分钟验证:运行 `pnpm check:adr-index && node scripts/check-adr-links.mjs`,两条都应退出 0。 - 需要你做的:契约复审通过后,审阅并合并(Tier H,`docs/adr/**`)。 ## What changes Docs only: four ADRs and one code comment. No code, schema, lint or skills change. - **ADR-0150** (status line gains an **Amended** entry). Each sentence cites the record it writes in: - **D3 (c)**: a package caller never resolves to a package outside its declared closure by bare name. This is step 3 of the ruled rule (6723462584), recorded as such by the epic seat (6728487836). A sibling package of the same release artifact is no exception unless declared (N1, ruled A, 6729820123), with the severity that record gives. - **D3, caller-less references**: a picklist extension's target (epic-seat ruling X, 6728613177) and the audience anchors `everyone` and `guest` (an epic-seat reading recorded in its E3 review, 6732998040 on draft PR objectstack-ai#31815) resolve as a caller with no package. - **D2**: the shareable namespaces `base`, `system` and `sys` are not judged by child N's co-owner rule (epic red-line review of N, 6728571147 on PR objectstack-ai#31717); a qualified address there answers `409` with the candidates, and G1 pins it. - **D4**: an owner-less engine function registration is the platform's holder, reachable from every closure like `sys` (R2 review 6729484996 item 2, refined by re-review 6730444942 item 1). - **Execution plan, covered types**: the object-less action is what phase 1 covers. Two cases keep two package holders even so: a clash sealed before phase 1 (objectstack-ai#31677 Q1 ruled A, 6729831159) and a same-object same-name action pair (objectstack-ai#31697 ruled A, 6729825528). The sentence "no two package holders of a name exist while its refusal stands" is narrowed accordingly. - **Execution plan, releasability**: the "except D3 (a)" sentence now also names D3 (c). - **TL;DR 7**: the three ruled refusals above join the list; "Measured first-party, none of them refuses anything" becomes "Where measured first-party, ...". - **ADR-0048**: - §5's `[done]` authoring-lint bullet is rewritten in place, as d5106ae did for the same bullet. It now describes what `naming/namespace-prefix` reports since child P (objectstack-ai#31765): the duplicate finding (`DUPLICATE_KEYED_TYPES`), and the bare-name finding for capabilities and positions only (`PREFIX_JUDGED_COLLECTIONS`). Phase 3 of ruling C is withdrawn (record 6721941146 on objectstack-ai#30806). - A dated note under §3.4 says its lint sentence still holds (see Acceptance notes). - The status line gains an **Amended** entry. - **ADR-0129**: a cross-reference to ADR-0150 on the status line, and a dated note under D4. ADR-0028 is re-opened for the behavioural tier only, objects stay under this record, and ruling C's planned amendment of D2 is withdrawn (6721941146). - **ADR-0130**: one dated note under D1's first paragraph (N1, 6729820123). Co-ownership does not decide how a reference between two packages of one artifact resolves; the sibling allowance narrows to declared dependencies (ADR-0150 D3). The ADR-0130 text itself is not rewritten. - **`packages/cli/src/utils/picklist-references.ts`**: the module comment at line 42 gains a dated note pointing at ADR-0150 D3 and the ADR-0130 note. Comment only. ## Why Child A of epic objectstack-ai#31527. The epic seat carried seven items into A's ADR-0150 text (comment 6729937614 on objectstack-ai#31685), on top of the card's ADR-0048 and ADR-0129 items. Each item was checked against the record it cites before its sentence was written. ## Tests None: a docs-only change (`skip-changeset`). The one code edit is a comment, so there is no tested behaviour to ablate. ## Acceptance notes - **Deviation: ADR-0048 §3.4 is noted, not amended.** The card called its lint sentence stale. Measured on `origin/main` 732def5, it is not: the sentence cites the rule for "an author shipping two `page/home` in one package". That duplicate finding still ships (`DUPLICATE_KEYED_TYPES` in `packages/cli/src/commands/lint.ts`), and ADR-0150 D9 keeps it. So §3.4 gets a dated note saying the sentence holds and pointing at §5, and only §5's bullet is rewritten. The seat agreed to this reading. - **Deviation: the ADR-0130 note sits under D1.** ADR-0130 has no sentence about resolving picklists or references. The CLI comment at `picklist-references.ts:42` leans on D1's co-ownership claim, so that is the sentence N1 means. The seat agreed to this placement. - **Status-line pointers:** ADR-0028's and ADR-0048's pointers to ADR-0150 landed with objectstack-ai#31586, and they are not duplicated here. ADR-0150's own status line still reads "Proposed (2026-11-08) ... becomes Accepted by the maintainer's merge", and ADR-0048's pointer still says "takes effect when ADR-0150 is accepted". Both are left as written; they are noted here for the seat. - **`tool`, corrected per contract review F1** (record 6734267031): `tool` joined phase 1's list (objectstack-ai#31414; ADR-0048 P1.10), and `BARE_NAME_ONE_HOLDER_TYPES` (`packages/core/src/package-scoped-resolution.ts`) lists 13 entries with `tool` last. ADR-0150 now counts 14 covered types (the constant's 13 plus permission sets), 11 uncovered types without `tool`, and 14 B children. The same fix commit (0a18e76) drops the letter from the N1 citation (F2), dates the ADR-0130 note's install-judge reading to E6 objectstack-ai#31731 (F3), and scopes the sealed-clash count to E5's measurement (F5). - **The E3 reading** is cited as an epic-seat reading recorded on draft PR objectstack-ai#31815 (6732998040), not as a ruling. ## Verification <details><summary>Readings</summary> Measured on commit bfc1d99 (base `origin/main` 732def5); after merging `origin/main` at ca23ce3, none of the five files changed, and `check:adr-index`, `check-adr-links` and `check-adr-anchors` were re-run there: exit 0 each. - `dispatch-gates`: 56 gates derived for this diff, 56 run, all exit 0. `--ran` reconcile: 0 NOT-MEASURED, 0 UNRUN. - Prerequisite builds under the verify lock: spec + formula + lint prebuild, VERDICT 0; full `turbo run build --filter=./packages/**`, VERDICT 0 (81/81 tasks). - First pass: 6 gates exited 3 (prerequisites missing, not a measurement): the teardown self-test (pinned fixture commit 621a487 not fetched in a shallow clone), and dual-build-cjs-loads, i18n, i18n-coverage, i18n-walk-parity and lean-entry-closure (needed built packages). After fetching the commit and running the full build, all six exit 0. - Body gates with this body as the event payload: check-closing-keyword-parity, check-changeset-no-major, check:adr-0087-registration and check-partof-closing-keyword, exit 0 each. - Diff: 5 files, +106 / -26. The review-fix commit 0a18e76 touches only ADR-0150 and ADR-0130; `check:adr-index`, `check-adr-links` and `check-adr-anchors` were re-run on it: exit 0 each. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. (No name added.) - [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran. The repo-wide `pnpm lint` scan is CI's run; the only source edit is a comment.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jUkMujHycV1pMLHL8h --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jUkMujHycV1pMLHL8h)_ <!-- signed: domain:spec#2 · zhuangjianguo · session_01QsR2jUkMujHycV1pMLHL8h --> --------- Co-authored-by: Claude <noreply@anthropic.com>
…klist without a declared dependency (objectstack-ai#31864) <!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi97hkDTw4QcCCcbBHrZv1dPd)_ Fixes objectstack-ai#31832 Clause-②: no (narrowing) Metadata-protocol: none Narrowing: yes. Measured: on base `cc16008a` the door accepts a field naming a sibling package's list with no declared dependency (the new refusal pin is RED there, see Verification); on head it refuses it as `picklist-reference-unknown`. The runtime already refuses the same reference since E6 (objectstack-ai#31731): the boot warns and serves no options, the post-seal install judge refuses. Narrowing face: `os validate` / `os build` exit 1 for a `packages[]` artifact whose field `picklist` is held only by packages outside the declaring package's declared dependency closure (plus `sys`), when that package declares no dependency outside the artifact. Two further refusals, both already refused by the boot (`INVALID_METADATA`, quoting the same sentence) and refused here whatever the outside dependencies: (a) a field whose package declares dependencies on two packages of the artifact that both declare the list (`ambiguous-in-closure`); (b) a `picklistExtensions` target two packages of the artifact declare (`ambiguous-in-environment`). Both were accepted on base. First-party count: 0 (no `examples/**` app and no platform package declares a picklist; E6 measured the same). Skills-docs: none. No published skill teaches a field naming another package's picklist; `skills/objectstack-data/SKILL.md` mentions picklist only for `fieldPatches`. ## 维护者速读 - 改了什么:`os validate` / `os build` 现在按运行时的规则解析字段的 `picklist`:先找声明该字段的包自己的列表,再找它声明的依赖闭包(加 `sys`)。同一发布包里兄弟包的列表,如果没有声明依赖,会被拒绝,提示句与运行时一字不差,并给出要加的那一行 `dependencies`。 - 为什么改:E6 之后,运行时对这种引用会告警、字段没有选项、安装被拒;可作者期的门仍然放行,作者直到安装才发现。现在 `os validate` 当场说清楚。 - 风险与回滚:收窄。多包制品里依赖兄弟包列表却没声明依赖的字段,从“校验通过”变成“校验失败”;运行时早已拒绝它,所以只是提前报错。第一方计数 0。回滚即 revert 本 PR,无数据迁移。 - 先看:`packages/cli/src/utils/picklist-references.ts` 的 `referenceResolver`(直接调用 `resolvePackageScopedHolders` + `packageResolutionClosure`,没有第二套闭包遍历)和 `refusedReferenceHint`;`picklist-references.boot-parity.test.ts` 末尾新 describe(门的句子等于启动告警的句子)。 - 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run src/utils/picklist-references.test.ts src/utils/picklist-references.boot-parity.test.ts`,预期 53 passed;其中 “a field naming a sibling package's list (ADR-0150 D3)” 一组在 main 上是红的。 - 需要你做的:无,席位落地(落地前席位做 CONTRACT_REVIEW_TIER 合同审查并告知 epic 席位)。 ## What changes 1. **The door resolves a reference with the runtime's rule, not a copy of it.** `judgePicklistReferences` builds the holders of each list from the bodies that declare it (`packageScopedHolderSource`), and asks `resolvePackageScopedHolders` (`@objectstack/core`) with the closure from `packageResolutionClosure` (`@objectstack/metadata-core`), over a registry view of the artifact (each body answers `getPackage` for its own id). Those are the calls the registry's `picklistReferenceVerdict` makes, so no second closure walk exists. Both packages were already direct dependencies of `@objectstack/cli`. 2. **A field's `picklist` resolves from the package that declares the field**: own list, then the unique holder in its declared dependency closure plus `sys`, else R's refusal. A refusal is `picklist-reference-unknown` with R's message verbatim (it names every candidate package and says to declare the dependency), and a hint carrying the exact entry, for example `'com.example.core': '^1.0.0'` added to the `dependencies` of the declaring package, plus the consequence (no options, values refused). No new rule id, error code or export. 3. **The outside-dependency arm softens only what an outside package could fix.** When the declaring package also depends on a package the artifact does not carry, a name nothing here holds, or one held only outside the closure (`outside-closure`), is reported as `picklist-reference-unverified` (info, never gating), because that dependency could supply the list. Two holders one reference reaches (`ambiguous-in-closure`, `ambiguous-in-environment`) are refused even then: an outside package can add a holder, never remove one (contract review F1). 4. **A `picklistExtensions` entry's `extend` resolves with no caller** (epic-seat ruling X on the E6 card; the registry does the same): any single holder in the stack, so an extension of a sibling's list still needs no dependency (pinned as a CONTROL), and a target two packages hold is refused, naming both. 5. **A stack with no `packages[]` is unchanged**: it is one package, every list it declares is its own. 6. **`os explain picklist-reference-unverified` follows the rule** (`packages/lint/src/rule-explanations.ts`, added to the surface by the seat's claim amendment 6735317750): its first paragraph now says how a field's `picklist` resolves (declaring package, declared dependencies transitively, plus `sys`; a sibling counts only when declared; an extension against the one package that holds the list; two holders refused), and its fix paragraph adds declaring the dependency. 7. **The closure notice is one short verdict line.** When the declaring package also has an outside dependency, the notice names the holders (`FIELD: 'LIST' is only in 'PKG', undeclared — not judged: 'DEP' may hold it`) instead of quoting R's whole sentence; pinned at most 200 characters with realistic package and field names, and its hint names the dependency to declare. The refusal keeps R's sentence verbatim. 8. The module header's ADR-0130 paragraph is rewritten to the ruled rule, citing ADR-0150 D3 and ruling 6729820123, worded after child A's ADR-0130 note in PR objectstack-ai#31841. The text-face heading over the reference refusals now names the closure case too. ## Tests - `picklist-references.test.ts`, new describe "a field naming a sibling package's list (ADR-0150 D3)", 12 cases: refused with R's sentence (`packageScopedRefusalMessage`) and the dependency hint; two undeclared sibling holders both named; reported, not refused, when the declaring package has an outside dependency (hint names the dependency); that notice at most 200 characters with realistic names; two declared sibling holders refused `ambiguous-in-closure`, with and without an outside dependency; an `objectExtensions` field judged from the extending package; CONTROL declared dependency; CONTROL transitive dependency; CONTROL own list; CONTROL `sys` list with no dependency; CONTROL sibling extension with no dependency; an extension target two packages hold refused `ambiguous-in-environment`, with and without an outside dependency. - `picklist-references.boot-parity.test.ts`, new describe, 4 cases on a real kernel: the door's refusal message equals the boot's `findOutsideClosurePicklistReferences()` refusal for the same field; CONTROL with the dependency declared, the door accepts and the boot reports nothing; a two-holder field and a two-holder extension target, each refused by the door and by the boot (`INVALID_METADATA`, the boot's message quoting the door's sentence). - **Forced test changes:** `picklistRefusalHeading` CONTROL pin (heading text now names the closure case; renamed from "keep the heading they always had"); the boot-parity file's objectstack-ai#29898 describe docblock (it described the sibling allowance under ADR-0130). the first boot-parity describe's CONTROL stack now declares the core body's dependency on the app body, and asserts the door accepts the whole stack (before, it was a stack this door now refuses, passing only because it filtered to `picklist-residual-unreferenced`). No assertion of an existing case changed otherwise. ## Acceptance notes - PR objectstack-ai#31841 (child A, Tier H) adds a dated note to this file's module header saying the code still resolves against every body's picklists. objectstack-ai#31841 landed first (8079fa9), so this branch merged main (no conflicts) and deleted that note in `661fbd90`; the file is again the blob it was at `e32fe7b2`. - File surface: `packages/cli/src/utils/picklist-references.ts`, its two test files, root `.changeset/31832-*.md`, and `packages/lint/src/rule-explanations.ts` (the `picklist-reference-unverified` entry's text only, claim amendment 6735317750). No test pin on that text moved: `rule-explanations.test.ts` and `packages/cli/test/explain-rule-id.test.ts` list the id only. ## Verification <details><summary>RED on main, then green on head (e32fe7b)</summary> Head implementation swapped for base `cc16008a`'s blob (`931bc8f2`), tests from head, under the verify lock: 11 failed / 42 passed. Failing: every new refusal and notice pin (sibling refusal, two undeclared holders, outside-dependency notice, realistic-length notice, two declared holders, `objectExtensions` field, two-holder extension), the three refusal boot-parity pins, and the forced heading pin. Every new CONTROL passed on main. Restored with `git checkout HEAD`, `git diff HEAD` empty. Head: Test Files 2 passed, Tests 53 passed. </details> <details><summary>Ablations (each through scripts/ablation-replace.mjs; each restored: blob equals HEAD 0e54d2b, git diff HEAD empty)</summary> - **Closure back to whole-stack resolution** (`closureOf` returns every artifact package): 5 failed / 48 passed — sibling refusal, outside-dependency notice, realistic-length notice, `objectExtensions` field, boot-parity sentence. The two-holder pins stay green in the expected direction: with every package reachable they are still refused as ambiguous. - **F1: an outside dependency softens every refusal again** (the refuse branch tests only `judged.outside.length === 0`): 2 failed / 51 passed — two declared holders with an outside dependency, two-holder extension with an outside dependency. - **F2: a two-holder verdict passes, as on base** (a non-`outside-closure` refusal skipped like a resolution): 4 failed / 49 passed — both two-holder unit pins and both two-holder boot-parity pins. </details> <details><summary>Gates on head e32fe7b, rerun on 661fbd9</summary> - After merging main (`ac8fede8`) and deleting the note, on `661fbd90`: closure build, the two picklist suites (53 passed), cli typecheck and cli unit (406 files, 5807 tests) exit 0; the same 69 derived families all exit 0, `--ran` 69/69, 0 NOT-MEASURED, 0 UNRUN. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 69 families derived for the 5 changed paths. All 69 ran on `e32fe7b2`, every one exit 0; `--ran` reconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN. (Earlier: `check-adr-0087-registration` was red on `fcf3a4b9` until `8f820082` added the changeset's `not-required (no-migration-prescription)` marker; 63/63 green on `8f820082`, 69/69 on `688a9602`.) - `pnpm --filter @objectstack/lint build`, `test` (197 files, 8365 tests) and `typecheck`: exit 0. - `pnpm --filter @objectstack/cli build` and `typecheck` (tsc plus `check:test-typecheck`): exit 0. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 406 files, 5807 tests passed. The integration tier is declared to CI (no spawn entry or integration file touched). - eslint `--no-inline-config` over the changed source files: exit 0 (neither package has a `lint` script). - Full turbo build of `packages/**` (on `fcf3a4b9`): exit 0. - Full dogfood battery on `e32fe7b2`: exit 0; 441 files passed, 3 skipped; 3662 tests passed, 42 skipped; no failures. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmQYiGGwc3V7yGZ4iTo --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmQYiGGwc3V7yGZ4iTo)_ <!-- signed: domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX --> --------- Co-authored-by: Claude <noreply@anthropic.com>
Requested by Steve · project thread
Part of #31678
Clause-②: no (narrowing)
Metadata-protocol: none
Narrowing: yes. (1) D3 (a): a package field whose
picklistonly an environment-authored item holds is served with no options and its values are refused. (2) The general outside-closure case, ruled inside ADR-0150 D3 step 3: a package field whosepicklistanother package holds, with no declared dependency on that package, is refused the same way. N1 is ruled on #31678 (the declared dependency closure only), so this includes a sibling in the same artifact. First-party count: 0 for both in shipped code. Noexamples/**app and no platform package declares a picklist (only notes inexamples/app-showcase/src/coverage.tsmention them). In first-party tests, two fixtures relied on a sibling's list with no declared dependency and get a forceddependenciesedit (listed under Tests):manifest-service-residual-objects.test.tsandcli/src/utils/picklist-references.boot-parity.test.ts. HotCRM is not in this session's repository scope, so it is not measured. D3 (a) is not measurable first-party, because no environment exists there.Skills-docs: none. The published skills never teach a field that names another package's picklist;
skills/objectstack-data/SKILL.mdmentions picklist only forfieldPatches.This PR is a draft until the epic seat's final check. N1 is ruled and R2 #31733 has landed, so nothing is held red on this head.
维护者速读
picklist名称现在按“声明该字段的包”去解析(自己的包 → 已声明依赖的包 + sys → 拒绝),统一走 ADR-0150 的包作用域解析器;同一发布包内的兄弟包也必须声明依赖(N1 已裁决);api 端点索引改为按(包, 名称)各留一条。packages/objectql/src/registry.ts中的picklistReferrerOf与picklistReferenceVerdict,以及plugin.ts中启动审计的告警分支和安装判定。pnpm --filter @objectstack/objectql exec vitest run src/engine-picklist.test.ts src/plugin-picklist-boot-audit.test.ts src/manifest-service-residual-objects.test.ts,看 “ADR-0150 D3” 相关 describe 全绿。What changes
Picklist, by the referrer's package (
@objectstack/objectql). Every picklist lookup now calls R'sresolvePackageScopedName(typepicklist), which since R2 #31733 is an adapter over the lean core rule andpackageResolutionClosure. There is no second closure walk, no id-prefix reading and no first-registered fallback on any touched path.picklistresolves with the package that declares that field as the caller. The referrers are every packagedownorextendcontributor that declares thatpickliston that field (picklistReferrerOf). The field is served only when every referrer reaches the same holder; otherwise it is served no options. There is no fold-order pick. A field written only by an environment-authored body (a tenant overlay, an object created in Studio) has no package, so it resolves environment-wide, as before.picklistExtensionsentry'sextendresolves caller-less (the epic seat's ruling X on this card). An extension still adds its options to the one list itsextendnames in the environment, whether or not its package depends on that list's package. Extensions are folded onto a field's list only when the caller-less holder is the same holder the field resolved.kernel:ready) still throws for a reference no holder can satisfy (not found, ambiguous). For every outside-closure refusal (an environment-only list, or a package outside the declared closure, a sibling included) it warns and the boot goes on (ruling N2). The read serves that field no options and the validator refuses its values.residualOwnerDependenciesOf). A manifest's own lists short-circuit. It refuses loudly, carrying R's refusal message, which names every holder. Extension targets are judged caller-less.describeUnresolvedPicklistReferencesappends R's refusal to each line. Its headline is unchanged.The api endpoint index, by (package, name) (
@objectstack/metadata).MetadataManager.listForIndex('api')(the matcher'slistApiItemsand the anonymous-flow-door audit) now keeps one slot per (package, name) instead of one per bare name. A package-less row under a name exactly one package holds is that package's customization, so it keeps sharing that slot (first entry wins, registry before loaders, as before). An environment-only row has its own slot. There is no resolver call there:@objectstack/metadatacannot import R (objectql depends on it), and the request side is caller-less. The route grammar and precedence are untouched (they belong to G1 and the router).Every by-name reader and the referrer it now passes
SchemaRegistry.resolvePicklistFieldsthroughfinishServedObject(every served object, fromfoldExtendersOntoDefinitionand the no-extenders branch offoldObjectExtendersOnto)packageId(picklistReferrerOf), served only when all reach one holder;nullwhen only an environment-authored body declares the fieldSchemaRegistry.resolvePicklistOptions(name, callerPackageId?)SchemaRegistry.findUnresolvedPicklistReferences/findOutsideClosurePicklistReferences(boot audit,plugin.tskernel:ready)packageIdSchemaRegistry.findOrphanPicklistExtensions(boot audit)registerPicklistExtension/assertPicklistValuesDistinctplugin.ts, manifest service)protocol-save-door-refusals.ts(@objectstack/metadata-protocol, save door)cli/src/utils/seed-record-values.tsMetadataManager.listForIndex('api')→EndpointMatcherNot in this PR.
import-mapping.ts,analytics-routes.ts) and service-analytics' cube-name reads resolve as package-less callers per R. There is no code for them here.That is why the first line says
Part of, notFixes.Tests
New pins, each ablation-verified.
packages/objectql/src/engine-picklist.test.ts, describepicklist — resolved from the package that writes the reference (ADR-0150 D3):findOutsideClosurePicklistReferencesnames it.findUnresolvedPicklistReferencesis empty andfindOutsideClosurePicklistReferencesnames the field, with R's refusal namingpackage "com.test.lists".packages/objectql/src/plugin-picklist-boot-audit.test.ts:the picklist boot audit — a list only the environment holds (ADR-0150 D3 (a)): the boot succeeds and serves no options; a late artifact is refused, naming the environment; CONTROL: with a dependency, it registers; a list held by a package outside the closure boots with a warning and no options.the picklist install door — a sibling in the same artifact (ADR-0150 D3): a sibling's list with no declared dependency is refused, naming that sibling. CONTROL: with the dependency, the artifact registers.packages/objectql/src/manifest-service-residual-objects.test.ts: after start(), a package depending on the residual owner reaches the list its dependency holds.packages/metadata/src/metadata-manager-match-endpoint.test.ts, describethe endpoint index keeps one slot per (package, name) (ADR-0150 D4):Forced test edits (fixtures that relied on outside-closure resolution, which D3 step 3 and the N1 ruling now refuse):
engine-picklist.test.ts: theHEALTHapp gainsdependencies: { 'com.test.picklist.core': '^1.0.0' }.engine-picklist.test.ts: the load-order test registers its object undercom.test.core(the list's own package) instead ofcom.test.early.plugin-picklist-boot-audit.test.ts"boots when the list arrives…": the app gainsdependencies: { 'com.test.boot.lists': '^1.0.0' }.plugin-picklist-boot-audit.test.ts"after the boot, an artifact that brings its own list…":com.test.lategains the same dependency.manifest-service-residual-objects.test.ts"a residual object whose field names a picklist a BODY declares…": the stack'smanifestgainsdependencies: { 'com.example.acme.service': '^2.4.0' }(N1: a sibling needs a declared dependency).packages/cli/src/utils/picklist-references.boot-parity.test.ts: the core body gainsdependencies: { 'com.example.parity.app': '^1.0.0' }(N1, same reason).Red on
main. The first round's pins were run againstmain's source:expected [{Gold}] to be undefined); its three controls pass.expected ['technology'] to deeply equal [],expected [Function] to throw); its control passes.expected undefined to be 'b'); its two controls pass.The two-referrer, sibling-door and boot-warning pins guard this PR's own rules (on
maina sibling's list resolves first-registered), so their proof is the ablations below. The residual-owner pin is green onmaintoo (no closure check there) and guards the install view; its proof is ablation 7.Ablations (
scripts/ablation-replace.mjs; restored each time, blob equal to HEAD):byPackagedelegation: one red.residualOwnerDependenciesOffrom the install view: the residual-owner pin red (refused at install, "held outside the declared dependency closure of package "com.example.late"").Acceptance notes
Red line 3.
registry-bare-name-one-holder.test.ts,-ai-family,-flow-family,-request-family, andpackage-scoped-resolver.test.ts.bare-name-one-holder.tsandpackage-scoped-resolver.tsare not edited.main. Api routes answer as before while each api name has one package holder.not-required (no-migration-prescription), in the changeset.Clause-②: no (narrowing), above and in the changeset, with a BREAKING banner.Holds cleared. N1 is ruled on #31678 (declared closure only): the residual test is green with its forced edit. R2 #31733 landed: the core-boundary ratchet and
check:lean-entry-closureare green with no change here beyond reading R2's newplatformholder kind insameHolder.Open questions (seat's call). New names:
findOutsideClosurePicklistReferences,picklistReferenceVerdict, and thebyPackageoption of the privatelistForIndex. Types:PicklistFieldResolver,UnresolvedPicklistReference,UnresolvedPicklistExtension.Verification
Head:
bf7e3295e9ed0c6ca46217e172e25ed5f4aeaee7.Everything below ran on this head, after merging
mainat2b752aa4f(R2 #31733 included).pnpm --filter @objectstack/objectql test: 577 files and 11821 tests pass, 0 red (the core-boundary ratchet and the residual test included).pnpm --filter @objectstack/metadata test: 71 files and 987 tests pass.packages/cliseed-record-values.test.ts,picklist-references.test.ts,picklist-references.boot-parity.test.ts: 3 files and 48 tests pass.seed-record-valueswas red only against a stale objectql build; it is green once objectql is rebuilt.typecheckfor@objectstack/objectql,@objectstack/metadataand@objectstack/cli: exit 0. The clicheck:test-typecheckstep ran out of the default 2 GB Node heap in this container on the first run and passes with--max-old-space-size=6144.--no-inline-config) on every changed.tsfile: exit 0.pnpm turbo run build: 120 of 120 tasks succeed.dispatch-gates --ran: 75 derived, 75 run, 0 not measured, 0 unrun, every one exit 0.check:lean-entry-closureholds its admitted set (15 packages, no denied name) for both@objectstack/objectql/coreconditions.pnpm --filter @objectstack/dogfood test): 440 files and 3654 tests pass, 3 files skipped.mainand the seven ablations are listed under Tests.