Repository navigation
Commit 5bd3d2f
fix(metadata-protocol): diagnostics stop flagging
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XQiacasSoJzxf6CYy6p4JTT)_
Fixes #26886
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:字段只写了 `picklist`(共享选项列表)时,元数据诊断(Setup › 所有元数据类型的问题数、条目
`_diagnostics`、`GET /meta/diagnostics`)不再误报「`picklist` 和 `options`
不能同时声明」。真正同时写了两个键的字段照报,文案和级别不变。
- **为什么改**:运行时把共享列表的选项解析到字段上再下发,诊断却校验了这份解析后的副本,于是每个用共享选项列表的应用(HotCRM 有
11 个字段)都给管理员亮红色错误,说应用坏了,实际选择框工作正常。
- **风险与回滚**:低。`/meta/object/:name` 下发内容不变(仍是 `picklist` + 解析后的
`options`),规则本身不动。新增的只是 picklist 解析在它生成的 options
数组上打一个不可枚举的标记,诊断见标记才略过;标记丢失时退回旧行为(误报),不会漏报。回滚即 revert。
-
**先看**:`packages/metadata-protocol/src/metadata-diagnostics.ts`(`withoutResolvedPicklistOptions`);`packages/objectql/src/picklist-resolution.ts`(`resolveField`
打标记的条件);`packages/metadata-core/src/resolved-picklist-options.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
src/protocol-picklist-served-roundtrip.test.ts`,预期 4/4 通过,其中三入口用例只报
`fields.both.options` 一条。
- **需要你做的**:无,席位落地。
## What changes
The global-picklist resolution in `@objectstack/objectql`
(`resolveField` in `picklist-resolution.ts`) now marks the `options`
array it derives for a field that names a picklist, and the load-time
diagnostics in `@objectstack/metadata-protocol`
(`computeMetadataDiagnostics`) leave a marked array out of the document
before the schema parse. A field is therefore judged as its author wrote
it:
| Field as authored | Served by `GET /meta/object/:name` | Diagnostic
before | Diagnostic after |
|---|---|---|---|
| `picklist: 'industry'` | `picklist` + resolved `options` | "`picklist`
and `options` cannot both be declared" | clean |
| `picklist` and its own `options` | `picklist` + resolved `options` |
flagged | flagged, same message and severity, at `fields.NAME.options` |
| inline `options` only | unchanged | clean | clean |
- **When the resolution marks.** Only when the body it resolves carried
no `options`, or carried options that are themselves marked (a served
copy re-folded in memory). A body that brought unmarked `options` beside
`picklist` is an authored pair and its resolved options stay unmarked,
so the rule still fires.
- **The mark.** `markResolvedPicklistOptions` /
`isResolvedPicklistOptions`, a new module in
`@objectstack/metadata-core` (`resolved-picklist-options.ts`): a
non-enumerable property keyed with `Symbol.for`, on the array itself. It
is not in any response, stored row, JSON or memo key; every loaded copy
of the package reads the one key. A copy of the array (`.map`, spread,
JSON round trip) does not carry it, so a reader of a copy reports the
diagnostic: the failure direction is the old false error, never a missed
one.
- **Why `metadata-core`.** Both sides already declare it. The resolution
sits in the lean `@objectstack/objectql/core` closure, which must not
import `@objectstack/metadata-protocol` (ADR-0076 D2;
`core-boundary.ratchet.test.ts` and `check:lean-entry-closure` both
refused the first version of this PR, which put the mark in
`metadata-protocol`).
- **Unchanged.** The rule itself (`packages/spec/src/data/field.zod.ts`,
untouched); what `/meta` serves; the save door (a PUT of the served body
is still refused 422 `INVALID_METADATA`, pinned by the existing test in
`protocol-picklist-served-roundtrip.test.ts`). No second copy of the
rule.
### Doors that show the diagnostic, all through the one seam
`computeMetadataDiagnostics` is the only place the read side parses an
item, so every door below is fixed by the one change:
1. An item read's `_diagnostics` (`getMetaItem` →
`decorateMetadataItem`).
2. A list read's per-item `_diagnostics` (`getMetaItems` →
`decorateMetadataItems`).
3. `GET /api/v1/meta/diagnostics` (`protocol-meta-diagnostics.ts` reads
the list's `_diagnostics`, or computes it), which is what Setup › All
Metadata Types counts.
4. The layered read's `_diagnostics` on the effective item
(`protocol.ts`, `getMetaItemLayers`).
The boot-time scan of stored `sys_metadata` rows (`protocol.ts`,
`convertStoredItem` → `computeMetadataDiagnostics`) parses the stored
body, which is authored and never resolved, so it is unmarked and keeps
flagging a stored row that carries both keys.
## Why
Seat ruling on the card: judge what the author wrote, never the resolved
copy; "If the resolved shape is the only thing that pass can see, the
resolution may mark what it derived." Measured which applies: the
authored item is not visible at the decoration seam.
- Every object read folds through the registry's `finishServedObject`
(`foldObjectExtendersOnto` / `resolveObject`) before decoration, and
that fold resolves the picklist.
- The MetadataService body the item read prefers can itself be a served
copy: the boot bridge copies `registry.getAllObjects()` (resolved
bodies) into the service.
- `resolveField` overwrites a field's `options` unconditionally, so even
an authored pair reaches the decoration seam with the picklist's
options: the served copy alone cannot tell the two apart.
So the resolution marks what it derived. Stripping every `options`
beside `picklist` in the diagnostics instead would silence an authored
pair on every door: a consumer-side tolerance the ruling excludes.
## Tests
- `metadata-core` `resolved-picklist-options.test.ts` (new, 4): the mark
is in place and only on the marked array, invisible to JSON and key
enumeration, gone from any copy of the array, kept through a copy of the
field holding it.
- `metadata-protocol`
`metadata-diagnostics.resolved-picklist-options.test.ts` (new, 6):
picklist-only field served with marked options is clean; an authored
pair is flagged at `fields.industry.options` with the rule's message;
inline-only clean; a marked array on a field with no `picklist` is
judged as written; a marked and an unmarked document with the same JSON
are two verdicts in either order (the verdict memo); the decorated item
keeps both keys and no trace of the mark.
- `objectql` `protocol-picklist-served-roundtrip.test.ts` (+2): a real
`ObjectQL` registry with a code-registered object (picklist-only
`industry`, inline `stage`, authored pair `both`) through
`ObjectStackProtocolImplementation`: the item read, the list read and
`getMetaDiagnostics` each report exactly `fields.both.options`; the item
read still serves `industry` with `picklist` and the resolved options.
<details><summary>Red first, ablation</summary>
- Red on `main` (`57820085`), same harness as the new objectql test,
before any edit: the item read, the list read and `getMetaDiagnostics`
each reported `fields.industry.options` "`picklist` and `options` cannot
both be declared" for a field declaring only `picklist`.
- Ablation (committed fix): in `picklist-resolution.ts`,
`markResolvedPicklistOptions(resolved);` replaced by `void 0;` (on disk:
0 of the first, 1 of the second) turned both new objectql tests red (2
failed, 2 passed: the authored-pair control and the existing round-trip
tests stay green). Restored from HEAD; `git diff HEAD` empty afterwards.
</details>
<details><summary>Live check (os dev)</summary>
`examples/app-todo` with its `category` field switched to `picklist:
'todo_category'` and a matching `picklists` entry (a scratch edit, not
committed; restored), booted with `os dev --fresh`, signed in as the
seeded admin:
| | `main` metadata-protocol + objectql (base blobs rebuilt) | this
branch |
|---|---|---|
| `GET /meta/diagnostics?type=object` | 1 entry: `todo_task`
`fields.category.options` "`picklist` and `options` cannot both be
declared" | 0 entries |
| `GET /meta/diagnostics` (all types, 177 items) | total 1 | total 0 |
| `GET /meta/object/todo_task` `fields.category` | `picklist:
"todo_category"` + 5 resolved `options` | identical |
| `todo_task._diagnostics` | `valid: false` (that one error) | `valid:
true` |
The count drops by exactly the false row. Base blobs were restored and
both packages rebuilt from HEAD afterwards.
</details>
## Acceptance notes
- **HotCRM path not booted.** The card's own reproduction is HotCRM on
published 18.2.0. The live check above uses the in-repo `app-todo` with
the same field shape (a code package's object naming a picklist);
booting HotCRM against this branch's packages was not done.
- **A copy loses the mark.** Any future step between the fold and the
decoration that copies a field's `options` array (rather than the field)
would bring the false error back for that door, loudly and in the safe
direction. Today no step does: the item, list and sweep doors and the
live boot all read clean.
## Verification
All at head `a6f469a3`, after the final commit:
- Package suites (under the verify lock): `@objectstack/objectql` test:
Test Files 475 passed (475), Tests 8851 passed;
`@objectstack/metadata-protocol` test: Test Files 274 passed, 4 skipped
(278), Tests 21699 passed, 26 skipped; `@objectstack/metadata-core`
test: Test Files 21 passed, Tests 840 passed. `typecheck` of all three:
exit 0.
- `eslint --no-inline-config` on the seven touched `.ts` files: exit 0.
- Derived gate families (`node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`): 62 derived, 62 run, every one exit
0; `--ran` reconciliation: "62 derived famil(ies) accounted for — 62
run, 0 NOT-MEASURED". `check:dual-build-cjs-loads` was read after a full
`pnpm build` (it reads every package's `dist/`): 112 require entry
points across 69 packages load. `check:lean-entry-closure` and
`core-boundary.ratchet.test.ts` are green on this head (both were red on
the first version, which imported `metadata-protocol` into the lean core
closure).
- Not run locally, owned by CI: `test:repo`, the repo-wide `pnpm lint`,
and the families outside the derived list.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (`eslint
--no-inline-config` on the touched files, exit 0.)
- [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.
- [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_01FEkQ5nFWpcGdLrSDAf6vS9
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nFWpcGdLrSDAf6vS9)_
---------
Co-authored-by: Claude <noreply@anthropic.com>picklist and options on a field that declares only picklist (#26953)1 parent 49b5059 commit 5bd3d2f
8 files changed
Lines changed: 302 additions & 2 deletions
File tree
- .changeset
- packages
- metadata-core/src
- metadata-protocol/src
- objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
| 45 | + | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
| |||
175 | 177 | | |
176 | 178 | | |
177 | 179 | | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
178 | 226 | | |
179 | 227 | | |
180 | 228 | | |
| |||
224 | 272 | | |
225 | 273 | | |
226 | 274 | | |
227 | | - | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
228 | 279 | | |
229 | 280 | | |
230 | 281 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
34 | 39 | | |
35 | 40 | | |
36 | 41 | | |
| |||
133 | 138 | | |
134 | 139 | | |
135 | 140 | | |
136 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
137 | 150 | | |
138 | 151 | | |
139 | 152 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
0 commit comments