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 8a218fc
Browse filesBrowse the repository at this point in the historyBrowse files
fix(spec)!: the view write door refuses an unknown top-level key (#26851)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB5z61o2LpDkGEiu41Hnhi5p)_Fixes#26711
Clause-②: no (narrowing) — the `view` write door refuses a body carrying
an undeclared top-level key on a view item record or an inline list or
form view, which it saved before without the key. Nothing is widened.
Metadata-protocol: narrowed (a saved `view` body may no longer carry a
top-level key its shape does not declare)
Skills-docs: none — no page under skills/** or content/docs teaches that
the view write door kept an unknown top-level key; the authoring
surfaces (defineView / defineViewItem) were already closed
## 维护者速读
- 改了什么:`view` 写入口(`ViewMetadataSchema` 的 view item 记录、内联 list 覆盖、内联 form
覆盖三个成员)从 `.strip()` 改为封闭,未知顶层键返回 422 `unrecognized_keys` 并点名;container
本来就是封闭的。
- 为什么改:自 #20051 起保存的是解析后的 body,未知键(拼错的、已退役的如 `striped`)会得到 200 但被静默丢弃,与
object / page / dashboard 写入口不一致。
- 风险与回滚:收窄型变更,带 `unrecognized_keys` 的旧请求现在会被拒;已测语料(示例 59 个 view、showcase
启动后 36 个已服务 view 的回写)零误伤;唯一受影响的平台写入方是 UI 适配器 `createView` 给 envelope 加顶层
`data`,本 PR 已修。回滚 = revert 本 PR。
- 先看:`packages/spec/src/ui/view.zod.ts`(`closeWireMember` 与三个成员),再看
`view-wire-unknown-key-refusal.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --project local
src/ui/view-wire-unknown-key-refusal.test.ts`,以及 `pnpm --filter
@objectstack/metadata-protocol exec vitest run
src/protocol.save-union-issues.test.ts`。
- 需要你做的:进入队列前需要一次 contract 级评审(收窄型协议变更)。
## What changes
`ViewMetadataSchema` judges every `view` body saved through `PUT
/api/v1/meta/view/:name` (draft or publish), the MCP metadata tools and
Studio. It is a union of four members. The `defineView` container was
already closed; the other three stripped their top level:
- the view item record `{ name, object, viewKind, config }`
(`ViewItemWireSchema`'s arms),
- the inline list view overlay (`ListViewOverlayWireSchema`),
- the inline form view overlay (`FormViewOverlayWireSchema`).
Since the save stores the parsed body (#20051), a key one of them did
not declare answered `200` and was not stored. All three are now closed
with the same `strictObject` / `closedObject` machinery the other doors
use, so the save answers `422 INVALID_METADATA` with an
`unrecognized_keys` issue that names the key and the shape:
``Unrecognized key(s) on this inline list view: `striped`.``
- **Console decorations are kept.** The preprocess still removes the
console's row ids on `filter[]` / `sort[]` before the check; the save
door removes the read decorations (`_diagnostics`, `_draft`) and the
server-stamped provenance (`_packageId`, `_packageVersion`,
`_provenance`) before it validates, as it did before. Every key the
console writes and reads back (`VIEW_CONSOLE_ROUND_TRIP_KEYS`:
`isDefault`, `isPinned`, `sortOrder`, `visibility`, `columnState`,
`_isOverride`) is declared on the member it is written on.
- **Retired keys get what their retirement gives them.** Keys with a
live 18.x tombstone (`filterableFields`, `pageName`, `tabs`, `owner`,
`hidden`) raise their own prescription, lifted to the union message as
before. `striped`, `bordered` and `virtualScroll` retired in 17.x, and
their kits have aged out under the ADR-0087 amendment of 2026-10-26
(#25767 ruling D2 = A), so they are named and pointed at the protocol
upgrade guide, which is the channel that amendment prescribes. I did not
re-add tombstones for them, since that would contradict the ruling.
- **Alias spellings point at the declared key.** `objectName` gets ``Did
you mean `objectName` → `object`?`` and a top-level `id` gets `name`.
- **One platform writer fixed.** `ObjectStackAdapter.createView`
(`@object-ui/data-objectstack`) added a top-level `data` to every spec.
On a ViewItem envelope `data` belongs inside `config`, so that create
would now be refused. It now adds `data` only to a flat list view (patch
changeset for `@object-ui/data-objectstack` under the root `.changeset`,
which is the only changeset directory the release reads). The console's
own create flow goes through `createRuntimeMetadata` / `viewEnvelope`
and was not affected.
- **Reverse pins moved.** `STILL_STRIP` in
`metadata-type-schemas.test.ts` is now empty: `view` was its last entry,
and its "end state" note is kept as history with the premise that
changed. The authoring lint's view-divergence pin now asserts the
registry schema refuses the key, instead of the old strip finding. The
strictness ledger row for `view.zod.ts` is updated and its counts
regenerated.
## Tests
- `packages/spec/src/ui/view-wire-unknown-key-refusal.test.ts` (new, 17
tests) covers each closed member:
- a clean body saves;
- console row ids are still removed;
- a junk key is refused by name with the member's surface;
- the container is still closed;
- `striped` gets the upgrade-guide sentence;
- `filterableFields` gets its live tombstone prescription;
- `objectName` and `id` get their alias guidance.
- `packages/metadata-protocol/src/protocol.save-union-issues.test.ts`
exercises the real `saveMetaItem` path:
- a draft or publish save with a junk key, or with `striped`, on an
inline list view answers 422 `INVALID_METADATA` and writes 0 rows, and
the message names the key;
- the same holds for a junk key on a view item record;
- control: a clean draft stores one row;
- read path: a stored row carrying a junk key is served with
`_diagnostics.valid: false` naming the key.
-
`ui/packages/data-objectstack/src/createView.envelopeData-26711.test.ts`:
`createView` on an envelope sends no top-level `data` and the platform
schema accepts the body. The control shows that body with the old `data`
is refused, and a flat list view still gets its default `data`.
- Re-judged fixtures that froze the strip. Each one now pins the
refusal, or moves the key to where it is declared:
- `view-authoring-wire-split`, `view-filter-rule-wire-id`,
`view-metadata-schema`, `view-overlay-viewkind-arm`,
`view-strictness-batch18`, `view-union-diagnostics`, `view.test`;
- `metadata-type-schemas`, `metadata-authoring-lint`;
- `protocol.graft-folded-form-sections`;
- CI round at `a3aeacbf`: `@objectstack/objectql` `protocol-meta` and
`protocol-publish-rollback` (`objectName` is now refused naming
`object`, and `extra` is gone from the diff fixture); spec
`scripts/strictness-ledger.test` (the `view.zod.ts` strip count 4 → 2,
with a history line); and `view-list-filterable-fields-retirement` (an
EXCLUDED entry for this PR's own refusal pin).
## Acceptance notes
- **The #5599 premise changed.** #5599 kept the members' `.strip()` only
while the save stored the original body. After #20051 the save stores
the parsed body, and #20456 declared the console's round-trip keys, so
stripping no longer kept anything; it only lost it silently.
- **Legacy stored rows.** A row stored before #20051 that still carries
an undeclared top-level key is served badged `_diagnostics.valid:
false`, naming the key. This is pinned on the read side. Its next
console save is refused, naming the key. That part is inferred from the
same parse and was not driven through the console.
- **Corpus measured, nothing refused.** At `ef7067fd`, the example apps'
view barrels (app-crm, app-showcase, app-todo, authored and expanded
items) gave 59 bodies. All 59 save before and after the change. With a
top-level junk key added, 49 saved before and 0 save after.
`platform-objects` ships no view definitions.
## Verification
- Premise re-measured on `main` `ef7067fd` (the QA ran at `3fcafcd3`).
At schema level, the list overlay, the view item record and the form
overlay each accepted a junk key and `striped`, and stripped them. The
container refused.
- Live showcase boot on this branch:
- `PUT /api/v1/meta/view/showcase_task.qa_rk_view?mode=draft` with a
clean body: `200`.
- The same body with `zzz_junk_key` or `striped`: `422
INVALID_METADATA`, and the issue names the key.
- The stored draft keeps only the clean keys.
- Round-trip of 36 served views (GET, then PUT back unchanged as a
draft): 36 accepted, 0 refused.
- CI round at `a3aeacbf` (Test Core 1/8 and 7/8 on `66637aea` were this
PR's): before the main merge, a locked wide run passed 17 of 17 suites
(spec `--project repo`, objectql, rest, runtime, metadata-fs,
metadata-core, cli unit, hono-server, client, audit, mcp, i18n,
automation, http-conformance, security, cloud-connection, client-react).
On the merged tree (`5b87033b`), `@objectstack/metadata-protocol` passed
270 files / 21672 tests, `@objectstack/objectql` passed 475 / 8843, and
spec `check:generated` exits 0.
- Locked test runs on this branch, after the fixture re-judging (logs
below):
- spec, metadata-protocol, lint and metadata;
- the 19 `ui` test files that reference the view schema exports;
- spec and metadata-protocol typecheck;
- `check:test-typecheck`, `check:generated` and
`check:strictness-ledger`.
- Derived gates: all 89 accounted for, 0 not measured. Dogfood on the
final (merged) head: 314 files, 2609 tests green.
<details><summary>Test and gate logs</summary>
Merged head, under `scripts/pm/os-verify-lock.sh` (shared box):
```
spec pnpm --filter @objectstack/spec test Test Files 735 passed (735) · Tests 22045 passed | 1 todo
protocol pnpm --filter @objectstack/metadata-protocol test Test Files 269 passed | 4 skipped · Tests 21628 passed | 26 skipped
ui view 19 ui test files on the view schema exports Test Files 19 passed · Tests 591 passed
adapter pnpm --filter @object-ui/data-objectstack test Test Files 101 passed · Tests 1205 passed
dogfood pnpm --filter '@objectstack/dogfood^...' build; pnpm --filter @objectstack/dogfood test
Test Files 314 passed | 3 skipped (317) · Tests 2609 passed | 20 skipped
generated pnpm --filter @objectstack/spec run check:generated exit 0 (after the merge)
```
Pre-merge head (same diff), also locked: spec and metadata-protocol
typecheck exit 0, `@objectstack/lint` test exit 0,
`@objectstack/metadata` test exit 0, `pnpm lint` exit 0.
Derived gates (`node scripts/pm/dispatch-gates.mjs --commands`, 89
commands, each run with its exit code recorded):
```
Run reconciliation — 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED (a DERIVED zero — all 89 recorded an exit code and none of them is 3).
```
Six of the 89 first answered exit 3 (prerequisite not met): five spec
gates that read `dist` (the test-file edits had moved the source hash)
and `check-plugin-teardown-shape --self-test` (needs an unshallow
clone). They were re-run after `pnpm --filter @objectstack/spec build`
and `git fetch --unshallow`, and all exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [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.
- [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 (the `维护者速读` block is
the maintainer digest the dispatch asks for).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufziYCUHZQE3MXucvWe9)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufziYCUHZQE3MXucvWe9
---------
Co-authored-by: Claude <noreply@anthropic.com>
`ObjectStackAdapter.createView` no longer adds a top-level `data` to a ViewItem envelope (`{ name, object, viewKind, config }`). `data` belongs inside `config` there, and the platform's `view` write door now refuses a top-level key the envelope does not declare (objectstack#26711), so such a create was refused naming `data`. Before, the door dropped the key and the view saved. A flat list view still gets its default `data`, where that shape declares it. The console's own create flow writes through `createRuntimeMetadata` and was not affected.
fix(spec)!: the `view` write door refuses an unknown top-level key on a view item record and on an inline list or form view, as the object, page and dashboard doors do, instead of saving without it
6
+
7
+
`ViewMetadataSchema` judges every `view` body saved through `PUT /api/v1/meta/view/:name` (draft or publish), the MCP metadata tools and Studio. Three of its four members (the view item record `{ name, object, viewKind, config }`, and the inline list and form views that carry their config at the top level) dropped a top-level key they do not declare. The save stores the parsed body, so a misspelt or retired key answered success and was not stored. They now refuse it with `422 INVALID_METADATA` / `unrecognized_keys`, naming the key and the shape: `Unrecognized key(s) on this inline list view: \`striped\`.` The `defineView` container was already closed.
8
+
9
+
Clause-②: no (narrowing) — the `view` write door refuses a body carrying an undeclared top-level key on a view item record or an inline list or form view, which it saved before without the key. Nothing is widened.
10
+
11
+
Metadata-protocol: narrowed (a saved `view` body may no longer carry a top-level key its shape does not declare)
12
+
13
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: the three members declared the same keys before and after, and every key the console writes and reads back is among them. An undeclared key never reached the store, since the save stores the parsed body, so there is nothing for a migration to rewrite; the author decides, and the refusal names the key. -->
14
+
15
+
**BREAKING** accept-set narrowing at the `view` write door (`@objectstack/spec`), served by every door that saves a `view`.
16
+
17
+
-**Before.**`PUT /api/v1/meta/view/:name` with a valid inline list view plus `"zzz_junk_key": true`, or a retired list key such as `"striped": true`, answered `200`, and the key was absent from the stored view. The same held for a view item record and an inline form view.
18
+
-**Now.** The same body answers `422 INVALID_METADATA` with an `unrecognized_keys` issue naming the key. A retired key gets what its retirement gives it: a key whose retirement is still current (`filterableFields`, `pageName`, `tabs`, `owner`, `hidden`) raises its own prescription, as before; one whose retirement has aged out (`striped`, `bordered`, `virtualScroll`) is named and pointed at the protocol upgrade guide. `objectName` is answered with `Did you mean \`objectName\` → \`object\`?`, and a top-level `id` with `name`.
19
+
-**Unchanged.** Every key the console writes and reads back on a stored view (`isDefault`, `isPinned`, `sortOrder`, `visibility`, `columnState`, `_isOverride`) is declared and saves as before. The console's row ids on `filter[]` / `sort[]` are still removed before the check. The read decorations (`_diagnostics`, `_draft`) and the server-stamped provenance (`_packageId`, `_packageVersion`, `_provenance`) are still removed by the save before it validates. `defineView` / `defineViewItem` were already closed.
20
+
-**Who sees it.** An author (a person, an MCP client or an AI agent) who sends a `view` body with a key its shape does not declare. That key was not stored before. A stored view row written before the save began storing the parsed body may still carry such a key; the next save of that row through the console is refused, naming it.
21
+
22
+
**The remedy.** Remove the key, or move it to where it is declared: a list view setting on a view item record goes under `config`, `objectName` is `object`, a top-level `id` is `name`.
0 commit comments