You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 1c61f48
Browse filesBrowse the repository at this point in the historyBrowse files
refactor(metadata,objectql,cli): one spelling of the residual-picklist refusal and of two registration refusal envelopes (#28531)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XNLFamdrWU3ohqRH6ptUBYs)_Fixes#28275Fixes#28160
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么**:三处「同一个拒绝写了两份」各收成一份。① 组合栈顶层「没人引用的
picklist」拒绝:判定(哪些列表、按哪些字段算「被引用」)和文案搬进
`@objectstack/metadata/picklist-references` 的
`unreferencedResidualPicklists`,引擎的 `manifest` 服务和 `os validate` / `os
build` 都读它;引擎原来的 picklist 字段遍历也搬过去,objectql 的 `picklist-resolution.ts`
改为转出。② 权限集 `packageId` 归属拒绝的信封、③ `views:` 非容器条目的 422 文案:从 `engine.ts`
导出,顶层残余注册直接调用,不再各抄一份。
- **为什么改**:卡 #28275(分诊 6537443627)与 #28160(范围扩大 6526088897,来自合约复核
6526074825):两处拷贝只靠一个启动对拍测试或文字断言维持一致,改一边就会悄悄漂移。
- **风险与回滚**:纯搬移,不改任何启动结果、错误码、状态码和文案(逐字节);`--json` 发现形状不变。新增公开导出(metadata
新子路径 + objectql 根转出),故 `Clause-②: yes (widening)`,`patch`。回滚即 revert。
-
**先看**:`packages/metadata/src/picklist-references.ts`(新判定与遍历);`packages/objectql/src/plugin.ts`(`unreferencedResidualPicklistRefusal`
与 `registerUnclaimedTopLevelItems`
的两处调用);`packages/cli/src/utils/picklist-references.ts`(`findUnreferencedResidualPicklists`)。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
src/registration-refusal-one-spelling.test.ts` → 7
passed(引擎与两扇门的抛出值逐字段等于唯一导出函数的返回值)。
- **需要你做的**:无,席位落地(合约复核由席位做)。
## What changes
Measured on `origin/main` `23d1303d` before any edit:
`unreferencedResidualPicklistRefusal` was module-private at
`packages/objectql/src/plugin.ts:238`, composing its own text over
`collectManifestPicklistReferences`;
`packages/cli/src/utils/picklist-references.ts` restated the walk
(`fieldPicklistNamesOf`) and the words (`residualPicklistWording`);
`permissionSetOwnerRegistrationRefusal` was module-private at
`packages/objectql/src/engine.ts:4793` and `plugin.ts` rebuilt its
envelope inline; the `views:` container-shape 422 text was spelled in
both `engine.ts` and `plugin.ts`.
### #28275 — one residual-picklist judgement
- **New subpath `@objectstack/metadata/picklist-references`**
(`packages/metadata/src/picklist-references.ts`, own `exports` entry and
tsup entry, after the `./view-container-name` precedent). It holds:
- the manifest picklist walk the engine already used, moved verbatim
from `packages/objectql/src/picklist-resolution.ts`:
`collectPicklistReferences`, `collectManifestPicklistReferences`,
`collectManifestPicklistNames`, `collectManifestPicklistExtensions`, and
the `PicklistReference` / `OrphanPicklistExtension` types;
- `unreferencedResidualPicklists(residual, bodies)`: takes the residual
answer (`unclaimedTopLevel`, read structurally so the entry never loads
the package root) and the package bodies, and returns `{ owner,
picklists: [{ name, field, index, wording }], wording }` or `undefined`.
`wording` is `{ judgement, prescription, text }`; the top-level
`wording` is the engine's one plural sentence, each list's `wording` is
a door's finding. The sentence is byte for byte the text the engine
composed before.
- **Engine.** `unreferencedResidualPicklistRefusal` is now
`picklistMetadataError(unreferencedResidualPicklists(residual,
bodies).wording.text)`. `picklist-resolution.ts` re-exports the moved
walk, so `registry.ts`, the boot audit and the install door import it
from the same place as before, and there is one walk, not two.
`@objectstack/objectql` re-exports `unreferencedResidualPicklists` and
its four types from the root, as it does `viewContainerNameRefusal`.
- **Doors.** `findUnreferencedResidualPicklists` (`os validate` step 2d,
`os build` step 3a-bis) maps the judgement's lists to findings;
`fieldPicklistNamesOf` and `residualPicklistWording` are gone. The
finding keeps `severity`, `rule`, `where`, `path: picklists[i]`,
`message`, `hint`.
- **Boot-parity file** shrinks to a smoke check: one refused stack and
one control, still byte-comparing the boot's message with the door's
`message + ' ' + hint`. The seven stacks it dropped are pinned at least
as close to the code: on the judgement
(`packages/metadata/src/picklist-references.test.ts`), at the door
(`picklist-references.test.ts`, whose residual pins are unchanged) and
at the engine (`manifest-service-residual-objects.test.ts`, unchanged).
### #28160 — one envelope per registration refusal
- `permissionSetOwnerRegistrationRefusal` is exported from `engine.ts`
(module export only; not on any package entry).
`registerUnclaimedTopLevelItems` calls it instead of rebuilding the
`VALIDATION_ERROR` / 400 / `issues[]` envelope.
- New `viewsEntryNotContainerRefusal(itemName, sourceLabel, ownerId)` in
`engine.ts` holds the `views:` container-shape refusal's
`INVALID_METADATA` / 422 envelope and text. The body seam
(`registerMetadataCollections`) and the residual registration (with the
`manifest` label) both throw it.
- `plugin.ts` drops its now-unused `ASSEMBLED_VIEW_ITEMS_KEY` and
`judgePermissionSetOwnerBinding` imports.
Not changed: what the boot refuses or serves, every refusal's code,
status and text, the `--json` finding shape, `packages/spec/**`.
## Tests
- New `packages/objectql/src/registration-refusal-one-spelling.test.ts`
(7 tests): the objectql re-export `toBe` the metadata function; the
boot's residual-picklist refusal equals
`unreferencedResidualPicklists(...).wording.text` and the envelope, for
one and two lists; the residual registration's permission-set refusal
and `views:` refusal each equal, field for field (`message`, `code`,
`status`, `httpStatus`, `issues`), what `engine.ts`'s exported function
returns, and so does the body seam's (`registerApp`) for the same input.
- New `packages/metadata/src/picklist-references.test.ts` (12 tests):
which residual lists are refused and which fields count as naming one
(body object in both spellings, body `objectExtensions`, body nested
`plugins[]`, residual object; a body `picklistExtensions` entry names
nothing), the singular and plural words, the owner word `none` in angle
brackets for a stack with no id.
- `packages/cli/src/utils/picklist-references.test.ts`: one new pin, the
door's findings equal the judgement's lists, locations and per-list
words.
-
`packages/metadata/src/serializers/typescript-serializer-annotation.test.ts`:
its export-entry count control moves 6 → 7 for the new entry (the loop
it guards now visits it too).
- Unchanged and green: the engine's residual pins
(`manifest-service-residual-objects.test.ts`, including the #28153
permission-set and `views:` pins), `picklist-references.test.ts`'s
existing pins.
<details><summary>Ablation — each single-source pin goes red on a
re-inlined copy with one changed word, and only it</summary>
Run with `scripts/ablation-replace.mjs` (mutation landed and restore
proven by blob hash and an empty `git diff HEAD`), on head `3ba35659`
(the source files are byte-identical at the final head; only a test
count moved after it):
| Leg | Mutation | Red | Still green |
| :-- | :-- | :-- | :-- |
| A | `plugin.ts`: the engine composes the residual-picklist text inline
again, "missing" → "absent" | `registration-refusal-one-spelling` × 2
(one list, two lists) | 65 of 67 in the two objectql files, including
every residual-picklist phrase pin |
| B | `cli/.../picklist-references.ts`: the door's `message` re-inlined,
"missing" → "absent" | the new door pin + the boot-parity smoke check |
33 of 35 |
| C1 | `plugin.ts`: the `views:` refusal rebuilt inline, "wrap it" →
"enclose it" | `registration-refusal-one-spelling` residual `views:` pin
| 66 of 67, including the #28153 `views:` pin (it quotes the first
sentence only) |
| C2 | `plugin.ts`: the permission envelope rebuilt inline around the
same message, issue `code` `custom` → `invalid` |
`registration-refusal-one-spelling` residual permission pin | 66 of 67,
including the #28153 permission pin (it matches the message) |
The "still green" column is why the pins are new: the behaviour pins
quote phrases, so a second copy that drifts outside those phrases passed
them.
</details>
## Acceptance notes
- **File surface.** The claim lists `packages/metadata/src/**`,
`plugin.ts`, `engine.ts`, the objectql re-export, the CLI file and its
tests, and changesets. This diff also touches
`packages/metadata/package.json` and `tsup.config.ts` (the new subpath's
entry, the precedent's shape) and
`packages/objectql/src/picklist-resolution.ts` (it re-exports the moved
walk, so the walk is not spelled twice). The `picklist-resolution.ts`
amendment was asked of the seat before the edit.
- **Clause-②** measured `yes (widening)`: a new subpath with new exports
on `@objectstack/metadata`, and a root re-export on
`@objectstack/objectql`. Nothing is newly refused or accepted, so no
`(narrowing)` arm and `Metadata-protocol: none`; `patch` on all three
packages per AGENTS.md Post-Task step 3.
- The two `engine.ts` exports are module exports only
(`@objectstack/objectql`'s entries are `.` and `./core`, neither
re-exports them), so they add no published surface.
- The door's walk is now the engine's. It differs from the old CLI copy
only on shapes the stack parse refuses before the door runs (a
name-keyed `objectExtensions`, an array inside `plugins[]`), so no
parsed stack changes outcome.
## Verification
All on head `b139a24c` (base `origin/main` `23d1303d`), in the card
thread's worktree, after `pnpm --filter '@objectstack/cli^...' --filter
@objectstack/cli build` (lock VERDICT command-exit 0).
<details><summary>Package suites and typecheck</summary>
| Command | Result |
| :-- | :-- |
| `pnpm --filter @objectstack/metadata test` | 65 files, 918 tests
passed |
| `pnpm --filter @objectstack/objectql test` | 510 files, 9868 tests
passed |
| `pnpm --filter @objectstack/cli exec vitest run --project unit` | 360
files, 5144 tests passed |
| `pnpm --filter @objectstack/objectql exec vitest run --project repo` |
1 file, 5 tests passed |
| `pnpm --filter @objectstack/core exec vitest run --project repo` | 6
files, 94 tests passed |
| `pnpm --filter @objectstack/{metadata,objectql,cli} typecheck` | exit
0 each |
| `eslint --no-inline-config` over the 11 changed `.ts` files | exit 0,
no output |
| `node scripts/check-platform-checklist.mjs --changed-from origin/main`
| OK, 13 anchors judged, 0 lost |
The first metadata run had one red,
`typescript-serializer-annotation.test.ts`'s export-entry count (6, now
7 with the new subpath); fixed in `b139a24c` and re-run green above. The
cli `integration` tier is left to CI: no integration-tier file or spawn
entry is in the diff.
</details>
<details><summary>Derived gates — 79 derived, 78 run green, 1 not
measured</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on this diff derives 79 families; every one
ran with its exit code captured before any pipe, and `--ran` reconciles:
`79 derived famil(ies) accounted for — 78 run, 1 NOT-MEASURED (1 DERIVED
from a recorded exit 3)`.
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` — exit 3, PREREQUISITE
NOT MET (needs every package's `dist/`, i.e. a full `pnpm build`). CI
runs it.
- `pnpm check:i18n-coverage` first exited 3 (the showcase example's
`@objectstack/connector-slack` and `@objectstack/service-print` had no
`dist/`); after building the four examples' closures it exited 0.
- `pnpm check:type-check-debt`: `OK — 0 ledger entr(ies) re-measured, 0
raw tsc error(s) total`.
- The families that ran before the last commit (a test-count line) were
re-run on `b139a24c`: all exit 0.
</details>
Serial check (order): no open pull request touches
`packages/objectql/src/{engine,plugin,picklist-resolution,index}.ts`,
`packages/metadata/**` or `packages/cli/src/utils/picklist-references*`
(24 open PRs read, files listed per PR); `git merge-tree --write-tree
HEAD origin/main` (`bff315cd`) is clean, and `origin/main` moved none of
those files since the base.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5oQR8KjnpATg1n2xCN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
refactor(metadata, objectql, cli): one spelling of the residual-picklist refusal, and one envelope for two registration refusals
8
+
9
+
Clause-②: yes (widening)
10
+
Metadata-protocol: none
11
+
12
+
Nothing a stack can declare is refused or accepted differently, and every refusal keeps its code, status and words byte for byte. What moves is where each judgement is written, so the doors that give the same answer read it from one place.
13
+
14
+
-**New subpath `@objectstack/metadata/picklist-references`.** It exports `unreferencedResidualPicklists(residual, bodies)`: given a composed stack's residual (what `unclaimedTopLevel` answers) and its package bodies, it returns every top-level picklist that no package body declares and no object field the engine registers names, each with its location and words, plus the one plural sentence for all of them, or `undefined` when there is none. It also exports the manifest picklist walk the engine already used (`collectManifestPicklistReferences`, `collectPicklistReferences`, `collectManifestPicklistNames`, `collectManifestPicklistExtensions`) and the types `PicklistReference`, `OrphanPicklistExtension`, `ResidualPicklistInput`, `ResidualPicklistWording`, `UnreferencedResidualPicklist` and `UnreferencedResidualPicklists`.
15
+
-**`@objectstack/objectql`** re-exports `unreferencedResidualPicklists` and its four types from the package root. The `manifest` service's `INVALID_METADATA` refusal of such a picklist is now that judgement's sentence, and the engine's picklist walk is the one in `@objectstack/metadata`. The residual registration of a composed stack now throws the body seam's own refusals for a permission set whose `packageId` names another package (`VALIDATION_ERROR` / 400 with `issues[]`) and for a `views:` entry that is not a view container (`INVALID_METADATA` / 422), instead of rebuilding each.
16
+
-**`@objectstack/cli`**: the `picklist-residual-unreferenced` finding of `os validate` and `os build` is read from the same judgement. Its `--json` shape (`severity`, `rule`, `where`, `path`, `message`, `hint`) and its text are unchanged.
0 commit comments