Repository navigation
Commit c045c34
domain:server#3 · os-bill · session_01Utp6CYvKbGaAgjBLQdkwPB
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgRSN1GiP9UgXGt4JWsZMg3B)_
Fixes #30093
Clause-②: no (narrowing) — an unknown sort field on a page slot moves
from silent or warning to error: `os validate` / `os build` / `os lint`
and the `page` publish door refuse a page sort they accepted before.
Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/deployment/validating-metadata.mdx
## 维护者速读
- **改了什么:** 页面上所有存储的排序(元素 `dataSource.sort`、关联列表的 `sort` 与
`parentDataSource.sort`、各列表类区块自带的 `sort`、列表页 `interfaceConfig.sort`、应用页
`record.sort`)写了对象没有的字段时,`os validate` 和页面发布门一律报 `page-field-unknown`
错误,指到具体槽位路径,并给出最接近的字段名。
- **为什么改:** 列表门对未知排序字段每次读取都答 `400
INVALID_SORT`。之前只有两个槽位在命令行给警告,其余槽位没人检查,页面照样发布成功,第一次打开就坏。
- **风险与回滚:** 这是收窄:以前能保存的这类页面,下次保存会被 422
拒绝;已存的页面照常加载,不受影响。点状路径、`created_at` 这类系统列、本栈没有的对象、没有字段表的对象都不判。回滚 =
revert 本 PR。
- **先看:** `packages/lint/src/validate-sortable-fields.ts` 的
`checkPageSortDeclaration`(三问的顺序与跳过条件);`packages/lint/src/validate-page-field-bindings.ts`
的
`isPageSortRef`(这里不再读页面排序,避免一处报两次);`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`
末尾的 #30093 用例。
- **两分钟验证:** 运行 `pnpm --filter @objectstack/metadata-protocol exec
vitest run src/protocol.runtime-authoring-gate.test.ts -t 30093`,预期 6
个全绿:5 个槽位各自被 422 `INVALID_METADATA` 拒绝、`page-field-unknown`
指到槽位路径、没有存下任何行;对照组用真实字段全部保存成功。
- **需要你做的:** 无,席位落地。
## What changes
- **`validateSortableFields` asks every stored page sort slot three
questions, in the list door's order** (`checkPageSortDeclaration`):
existence (`page-field-unknown`, `error`), provenance
(`page-field-unprovisioned`, `warning`, a registry-injected anchor on an
ADR-0015 `external` object) and sortability (`sort-field-unsortable`,
`error`, unchanged from #30084). The slots and their bindings are the
ones #30084 already walks, each bound to the object its renderer reads:
`dataSource.sort` → `dataSource.object`; a block's own `sort` →
`dataSource.object ?? BLOCK_OBJECT_PROP`
(`PAGE_BLOCK_SORT_OBJECT_PROP`); `parentDataSource.sort` →
`parentDataSource.object`; `interfaceConfig.sort` →
`interfaceConfig.source ?? page.object`; `record.sort` →
`record.object`. Keys are read by `readSortKeys`, so the legacy string
form is judged by its head, and the `-field` shorthand and
comma-separated keys are each judged.
- **The grade is `error` on every slot.** The list door
(`assertSortFieldsExist`) answers an unknown sort name `400
INVALID_SORT` on the first read and every read after it, which is the
list view's `sort-field-unknown` grade. The id stays the page family's
(`page-field-unknown`), so a page author reads the page rule.
- **Why this member.** `validateSortableFields` is the one
reference-integrity member that declares `page` in `runtimeTypes`
(#30084), so the page publish door (`PUT /api/v1/meta/page/NAME`;
Studio, REST, MCP) now refuses the save `422 INVALID_METADATA` instead
of storing a page that breaks on its first read.
`validatePageFieldBindings` runs on the CLI only.
- **`validatePageFieldBindings` no longer reads a page sort**
(`isPageSortRef` sets aside a related list's `sort` refs;
`interfaceConfig.sort` is no longer collected), so each slot has exactly
one reader and one finding. `relatedListFieldRefs` and `sortFieldRefs`
keep their exported behaviour for out-of-repo callers and the react
surface.
- **Skips, the `page-field-unknown` family's own:** a dotted
relationship path (the door refuses it as its own verdict), a
registry-injected system column (an author-declared field of a system
name falls through to the projection, keeping #30084's verdict on it),
an object the stack or the publish snapshot does not carry, and an
object with no field map.
## Tests
- `validate-sortable-fields.test.ts`, new `describe` (#30093): every
page slot refused as `page-field-unknown` at `error` at its slot path
(rule + path + severity asserted for all nine), message and fix line
pinned once; `validatePageFieldBindings` reports none of those slots
while still reading a related list's columns (non-vacuity); each slot
bound to its renderer's object; legacy string head, `-field` and comma
keys; the skips; provenance on the external object at `warning` and
silence on the local twin; an author-declared system-named field keeps
`sort-field-unsortable`. The #30023 control no longer lists an unknown
name as "not this rule's", and the one-line coverage pin now expects the
two page ids beside the three sort ids.
- `validate-page-field-bindings.test.ts`: the related-list case drops
its sort expectation; the #4340 legacy-string case moves to the sortable
suite and is replaced by a pin that this walk reads no
`interfaceConfig.sort` in either form while still reading the columns.
- `protocol.runtime-authoring-gate.test.ts`, new top-level `describe`
(#30093): five slots (element `dataSource.sort`,
`parentDataSource.sort`, a related list's `sort`,
`interfaceConfig.sort`, an app page's `record.sort`) each refused `422`
/ `INVALID_METADATA` with one `page-field-unknown` issue at `error` at
the slot path, nothing stored; control: the same pages on a declared
field save.
## Acceptance notes
- The two code notes on the page crossing
(`packages/lint/src/reference-integrity-suite.ts`, the
`validateSortableFields` member note;
`packages/lint/src/authoring-rules.ts`, the `page` crossing note) said
it judges a page sort "by the sortability projection alone". Corrected
in review round 1 (comments only).
- A `kind: 'react'` page's sort props stay with
`validate-react-page-props` (`sortFieldRefs`, the `skipped` grade). They
are JSX props, not stored page sort slots, and are outside this card.
## Review round 1: what changed
Head `df135352` → `647a1c1f`, one commit, after the contract review
(FAIL, comment 6649790600): one must-fix and one should-fix, both taken.
- **M1.** `content/docs/deployment/validating-metadata.mdx` §5 said a
`record:related_list`'s `columns`/`sort`/`filter` resolve through the
page-binding rule and are advisory. It now names `columns`/`filter`
only, and a new paragraph says a page sort is the exception: every
stored page sort slot naming a field the bound object lacks is a
`page-field-unknown` **error**, and the page publish door refuses the
save `422 INVALID_METADATA`, because the list door answers it `400
INVALID_SORT` on every load. The `Skills-docs:` line in this body and
the changeset now reads `updated here —
content/docs/deployment/validating-metadata.mdx`.
- **S1.** The two stale code notes (`reference-integrity-suite.ts` near
line 289, `authoring-rules.ts` near lines 1253–1256) now say the page
crossing also answers existence (`page-field-unknown`, `error`) and
provenance (`page-field-unprovisioned`, `warning`). Comments only.
**Round 1 verification, head `647a1c1f`.** `node
scripts/pm/dispatch-gates.mjs --commands` now derives 87 families (the
docs edit adds 29, the docs-audit and spec doc families among them); all
87 exit 0, `--ran`: 87 derived, 87 run, 0 NOT-MEASURED. `pnpm lint`:
exit 0. `@objectstack/lint` rebuilt after the comment edits. The source
change since round 0 is comments and one docs page, so the round-0 test,
dogfood and reverse-verification readings below still describe the code.
<details><summary>Round 1 gate record (87 derived + pnpm lint)</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm lint :: exit 0
```
</details>
## Verification (round 0, head `df135352`)
All readings on head `df135352` (this branch with `origin/main` at
`6f095b9c` merged), after a full `turbo run build
--filter='!@objectstack/docs'` under the verify lock (exit 0).
- `pnpm --filter @objectstack/lint test`: Test Files 193 passed (193),
Tests 8001 passed (8001). `pnpm --filter @objectstack/lint run
typecheck`: exit 0.
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 312
passed, 4 skipped (316); Tests 22360 passed, 26 skipped. `pnpm --filter
@objectstack/metadata-protocol typecheck`: exit 0.
- `pnpm lint`: exit 0.
- `node scripts/pm/dispatch-gates.mjs --ran`: 58 derived families, 58
run, 0 NOT-MEASURED, every exit code 0.
- Dogfood (`pnpm --filter @objectstack/dogfood test`, the title carries
`!`): Test Files 1 failed, 418 passed, 3 skipped (422); Tests 1 failed,
3487 passed, 36 skipped. The one failure is
`test/permission-set-clone-door.dogfood.test.ts` › "a platform set …
copies into its shipping package's pair" (`422`: permission
`my_member_baseline` grants on `sys_comment`, "not defined in objects").
It is not this diff: `main` at `6f095b9c` is red on the same test
(Dogfood Regression Gate (2/3), job 127024968149), and this diff touches
no permission, object or clone path.
- **Reverse verification.** With the two lint sources put back to
`origin/main` (`git restore --source=origin/main`), the two lint suites
go 8 failed / 122 passed: every new #30093 case and the two moved
page-field-bindings cases, plus the one-line coverage pin. With lint
rebuilt from those sources, the door suite goes 5 failed / 1 passed: the
five refusals red (the page saved), the control green. Both restored
with `git checkout HEAD --`, `git status` clean.
<details><summary>Gate record (58 derived + pnpm lint)</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm lint :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYvKbGaAgjBLQdkwPB)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CYvKbGaAgjBLQdkwPB
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1fd0c32 commit c045c34
9 files changed
Lines changed: 417 additions & 48 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- lint/src
- metadata-protocol/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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
166 | | - | |
| 166 | + | |
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
172 | 180 | | |
173 | 181 | | |
174 | 182 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1252 | 1252 | | |
1253 | 1253 | | |
1254 | 1254 | | |
1255 | | - | |
1256 | | - | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
1257 | 1258 | | |
1258 | 1259 | | |
1259 | 1260 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
286 | 286 | | |
287 | 287 | | |
288 | 288 | | |
289 | | - | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
290 | 294 | | |
291 | 295 | | |
292 | 296 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
426 | | - | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
427 | 430 | | |
428 | 431 | | |
429 | 432 | | |
| |||
432 | 435 | | |
433 | 436 | | |
434 | 437 | | |
435 | | - | |
436 | 438 | | |
437 | 439 | | |
438 | 440 | | |
| |||
731 | 733 | | |
732 | 734 | | |
733 | 735 | | |
734 | | - | |
735 | | - | |
736 | | - | |
737 | | - | |
738 | | - | |
739 | | - | |
740 | | - | |
741 | | - | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
742 | 741 | | |
743 | 742 | | |
744 | 743 | | |
745 | 744 | | |
746 | 745 | | |
747 | 746 | | |
748 | 747 | | |
749 | | - | |
| 748 | + | |
750 | 749 | | |
751 | 750 | | |
752 | 751 | | |
753 | | - | |
754 | | - | |
755 | | - | |
756 | | - | |
757 | | - | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
758 | 757 | | |
759 | 758 | | |
760 | 759 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
55 | 70 | | |
56 | 71 | | |
57 | 72 | | |
| |||
600 | 615 | | |
601 | 616 | | |
602 | 617 | | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
603 | 632 | | |
604 | 633 | | |
605 | 634 | | |
| |||
646 | 675 | | |
647 | 676 | | |
648 | 677 | | |
649 | | - | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
650 | 686 | | |
651 | 687 | | |
652 | 688 | | |
| |||
693 | 729 | | |
694 | 730 | | |
695 | 731 | | |
| 732 | + | |
| 733 | + | |
696 | 734 | | |
697 | 735 | | |
698 | | - | |
699 | 736 | | |
700 | 737 | | |
701 | 738 | | |
| |||
0 commit comments