Skip to content

fix(lint)!: refuse a stored page sort naming an unknown field, on every page sort slot (#30093) - #30270

Draft
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-ru7gx5
Draft

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-ru7gx5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CYvKbGaAgjBLQdkwPB

Requested by Bill · project thread
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 末尾的 lint: page sorts naming an unknown field get no reader on element dataSource.sort / app page record.sort, and stay warning elsewhere while the door answers 400 #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 fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #30084). The slots and their bindings are the ones fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #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 (fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #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 fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #30084's verdict on it), an object the stack or the publish snapshot does not carry, and an object with no field map.

Tests

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.

Round 1 gate record (87 derived + pnpm lint)
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

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 lint: page sorts naming an unknown field get no reader on element dataSource.sort / app page record.sort, and stay warning elsewhere while the door answers 400 #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.
Gate record (58 derived + pnpm lint)
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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CYvKbGaAgjBLQdkwPB

…ry page sort slot

Every stored page sort slot (element dataSource.sort, a related list's sort
and parentDataSource.sort, each sort-carrying block's own sort,
interfaceConfig.sort, an app page's record.sort) is now asked existence and
provenance by validateSortableFields, the member the page publish door runs:
an unknown name is page-field-unknown at error, as the list door answers it
400 INVALID_SORT on every read. validatePageFieldBindings no longer reads a
page sort, so each slot has one reader.

Claude-Session: https://claude.ai/code/session_01Utp6CYvKbGaAgjBLQdkwPB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 7 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04f36b63f8d2f32b00428929b07268130881e30f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a820e9b5c395f0f57ce5d4fcdef29576b7912f6c — the merge of head 647a1c1fda4e3b6426696b10e82370e612a558a4 into base 04f36b63f8d2f32b00428929b07268130881e30f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a820e9b5c395f0f57ce5d4fcdef29576b7912f6c && git checkout a820e9b5c395f0f57ce5d4fcdef29576b7912f6c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04f36b63f8d2f32b00428929b07268130881e30f 647a1c1fda4e3b6426696b10e82370e612a558a4 && git checkout -B drift-repro 04f36b63f8d2f32b00428929b07268130881e30f && git merge --no-ff 647a1c1fda4e3b6426696b10e82370e612a558a4

node scripts/docs-audit/affected-docs.mjs --json 04f36b63f8d2f32b00428929b07268130881e30f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 04f36b63f8d2f32b00428929b07268130881e30f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: df13535218c10a428edd8c6c4d62a6f123b9ddc4
Local-runs: none
Scope: packages/lint/src/validate-sortable-fields.ts, packages/lint/src/validate-sortable-fields.test.ts, packages/lint/src/validate-page-field-bindings.ts, packages/lint/src/validate-page-field-bindings.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts + packages/metadata-protocol/src/protocol-query-validation.ts (assertSortFieldsExist, resolveQueryFields), packages/lint/src/system-fields.ts (SYSTEM_FIELDS), packages/lint/src/validate-searchable-fields.ts (indexObjectSearchTargets), packages/spec/src/system/constants/system-names.ts (SystemFieldName), packages/lint/src/reference-integrity-suite.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/index.ts, content/docs/deployment/validating-metadata.mdx

Face: narrowing (Clause-②: no (narrowing) on the PR body and the changeset, line-initial on both). No published schema and no governed rules path is in the committed tree, so the narrowing face alone sets the tier. Review by an isolated reader of PR #30270 (card #30093); reads: card body and comments 6632471866 / 6648657894 / 6649613982, PR body, the full diff, the files named in Scope: at this head in a separate worktree, the head's check-runs and the base's (6f095b9c) dogfood job via the REST API. Nothing was built or run; the container carries no node_modules (check-adr-0087-registration.mjs stops for want of tsx), so every gate reading below is CI's or NOT MEASURED.

① Derived judgments

Declarations (PR body and changeset, both line-initial):

  • Clause-②: no (narrowing) — right. Nothing is widened: no new export (packages/lint/src/index.ts untouched; checkPageSortDeclaration, validatePageSorts, isPageSortRef are module-private), no new rule id (page-field-unknown / page-field-unprovisioned already exist in validate-page-field-bindings.ts#PAGE_FIELD_UNKNOWN / #PAGE_FIELD_UNPROVISIONED and are already exported), no new error code (422 INVALID_METADATA is the door's existing envelope), SortableFieldFinding.rule is string. The accept set only shrinks.
  • Metadata-protocol: narrowed — right. A value a page author could write (a sort naming a field the object lacks) is now refused at the page publish door and by os validate/os build/os lint.
  • Skills-docs: none — wrong. content/docs/deployment/validating-metadata.mdx §5 "Page components bound to fields that don't exist" (lines 164–170 on main and at this head) says: "A record:related_list's columns/sort/filter resolve against its related object (objectName) … Advisory, like form-layout field references — every consumer degrades rather than failing." After this PR that sentence is false in the word sort twice over: §5's rule (validatePageFieldBindings) no longer resolves a related list's sort at all (isPageSortRef sets it aside), and an unknown name there is an error at the CLI and a 422 at the page door, not advisory. The dev's justification ("nothing there describes an unknown page sort as a warning") does not survive reading §5. skills/** carries nothing on it (checked skills/objectstack-ui/{SKILL.md,rules/pages.md,rules/list-views.md}); os explain texts for page-field-unknown, sort-field-unknown, sort-field-unsortable (rule-explanations.ts) stay true. The doc table "The one gate, four doors" has no row for validateSortableFields at all — pre-existing since A view's declared sort naming a formula field clears lint, then 400s on every load — the SORT axis has no authoring gate, unlike SEARCH (#6674) #9257/fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #30084, not made false here. → must-fix M1 below.
  • adr-0087 marker not-required (no-migration-prescription) — reads right: no key added/removed/renamed; a refused name never sorted anything, so there is no FROM→TO. Check Changeset is green on this head; the local gate is NOT MEASURED (no tsx).

Accept-set changes, each judged:

  1. Page publish door (PUT /api/v1/meta/page/NAME, Studio/REST/MCP) now refuses 422 INVALID_METADATA a page whose element dataSource.sort, record:related_list sort / parentDataSource.sort, any PAGE_BLOCK_SORT_OBJECT_PROP block's own sort, interfaceConfig.sort or app-page record.sort names an undotted field the bound object does not declare and that is not a system name. Reached through validateSortableFields (reference-integrity-suite.ts:294, runtimeTypes: ['flow','view','page']) — the same member the door already ran for fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #30084, so no new door wiring. Right. Binding per slot (validate-sortable-fields.ts#validatePageSorts): dataSource.object; dataSource.object ?? props[PAGE_BLOCK_SORT_OBJECT_PROP[type]]; parentDataSource.object; interfaceConfig.source ?? page.object; record.object — the card's bindings, unchanged from fix(lint)!: refuse a stored sort on a sortable: false field, on list views and every stored page sort #30084.
  2. Nothing is refused that the runtime accepts. Door accept set (protocol-query-validation.ts#resolveQueryFields → #assertSortFieldsExist): Object.keys(registry.getObject(o).fields) ∪ {id, created_at, updated_at}, judged on the head segment. Lint accept set (#checkPageSortDeclaration): target.names ∪ SYSTEM_FIELDS, dotted names skipped, objects with no projection or no field map skipped. SYSTEM_FIELDS = FIELD_GROUP_SYSTEM_FIELDS ∪ values(SystemFieldName) and SystemFieldName.ID = 'id' (system-names.ts:163), so id, created_at, updated_at, owner_id, created_by, updated_by, organization_id, tenant_id, owning_business_unit_id, is_deleted, deleted_at all pass lint. The registry's injected columns are a subset of that set, so lint ⊇ door on undotted names: over-inclusion is the stated posture (system-fields.ts header). A formula field is declared, so it passes existence and reaches the sortability verdict (sort-field-unsortable, unchanged). A lookup field name (undotted) is declared, passes. Dotted paths skipped — the door refuses them as its own verdict. Where the door returns no gate (legacy array field map, empty map) lint still judges a declared-map object by recordsOf(fields) — stricter than the door's existence check, but the field genuinely does not exist and the list-view sibling (#checkSortDeclaration) has judged that case the same way since A view's declared sort naming a formula field clears lint, then 400s on every load — the SORT axis has no authoring gate, unlike SEARCH (#6674) #9257. No false-refusal scenario found.
  3. CLI (os validate / os build / os lint): page-field-unknown on a related list's sort and on interfaceConfig.sort moves warning → error; the other four slot families move silent → error. Right, and one finding per slot: validatePageFieldBindings drops sortFieldRefs(cfg.sort …) and filters split.related through isPageSortRef; the new test "is the one reader of a page sort" asserts no .sort path from the bindings walk on all nine slots with a non-vacuity control on columns.
  4. Severity split vs siblings — consistent. checkFieldRefs (validate-page-field-bindings.ts) grades page-field-unknown error when the consequence is queried and warning when skipped; a sort is a query position that the door refuses, so error is the queried grade. List-view twin: sort-field-unknown error, sort-field-unprovisioned warning. Provenance stays warning everywhere (page-field-unprovisioned; no door refuses it). Message/hint reuse suggestName and unprovisionedAnchorVerdict/Fix, the family's shared clauses.
  5. Public export change of @objectstack/lint: none. index.ts untouched; exported relatedListFieldRefs / sortFieldRefs keep yielding sort refs (react surface and out-of-repo callers unchanged); checkSortDeclaration's exported signature unchanged. Behavioural change on two exported functions is exactly what the changeset names.
  6. Import direction: validate-sortable-fields.ts now imports two string constants from validate-page-field-bindings.ts; that file imports nothing from the sort module — no cycle.
  7. Overlap with lint: the object publish door does not run validateSortableFields, so an object whose listViews sort a sortable:false field saves #30095 (held serial): none in code — this PR does not touch runtimeTypes. Note for lint: the object publish door does not run validateSortableFields, so an object whose listViews sort a sortable:false field saves #30095: once object joins that member's runtimeTypes, the object-write snapshot will carry pages too, and this PR's page-slot existence check will then also run on an object save; lint: the object publish door does not run validateSortableFields, so an object whose listViews sort a sortable:false field saves #30095's rollout decision should say whether that is intended.

② Semver level

@objectstack/lint: minor with fix(lint)!: title, BREAKING banner, both declaration lines and one adr-0087 marker in the body — consistent with AGENTS.md Post-Task Checklist step 3: Metadata-protocol: narrowed ⇒ at least minor on the package the diff moves. @objectstack/metadata-protocol is touched only in a test file (files: ["dist", …] publishes nothing of it), so no changeset is owed there; the door's new refusal is the lint member's behaviour and is named in the lint changeset. check-changeset-no-major (local) exit 0; Check Changeset green on head.

③ Boundary flags

  • Dev flag: stale comments in reference-integrity-suite.ts:289 and authoring-rules.ts:1253–1256 ("judges it by the sortability projection alone"; "a stored page sort naming a field the list door refuses (sortable: false, a formula)"). Confirmed stale at this head. They are code comments, not accept set and not governed rules text (reviewFace on the PR's paths returns no hits), so not a must-fix on their own; the dev stopped at the claimed file surface as the claim instructs. Recommended: the seat widens the surface by those two comment hunks on the delta commit M1 already requires (cheaper than a card); otherwise a follow-up card. Should-fix S1.
  • Dev flag: kind: 'react' page sort props stay with validate-react-page-props at skipped (warning). Accepted as out of scope: they are JSX props, not stored page sort slots; the card names stored slots only. A react <ListView sort> naming an unknown field still saves and still answers 400 at read — worth a card, not this one.
  • Dogfood (! title): PR-level Dogfood Regression Gate check is success on df135352; the shard matrix is skipped on PR. The dev's full local battery on head reports 1 failure, test/permission-set-clone-door.dogfood.test.ts › "a platform set … copies into its shipping package's pair". Read from the API: base 6f095b9c job Dogfood Regression Gate (2/3) (id 127024968149) failed on step "Boot example apps and exercise real user flows" with the same FAIL line (permission-set-clone-door.dogfood.test.ts > … copies into its shipping package's pair, Test Files 1 failed | 139 passed). Same signature on the merge-base ⇒ not counted against this head. The diff touches no permission/clone path.
  • Check-runs at review time: Lint ESLint, PM dispatch-gates, tooling self-tests, Governed Surface Queue Guard, Check Changeset, PR-body guards, Build UI Deps, Dogfood Regression Gate all success; Type Check ×5, Test Core ×8, Test UI ×16, Build Core, Live E2E UI and four Lint jobs still in_progress. All-green is the seat's landing precondition, not this verdict.
  • open_questions: []; author is objectstack-fleet[bot] via relay (no human login to name).

④ Design judgment

Narrowing judged against the published spec and runtime on main: it takes no declared capability away — every page sort slot keeps its schema shape (ElementDataSourceSchema.sort, interfaceConfig.sort, record.sort, block sort), and a sort the door now refuses at save was already refused at every read by assertSortFieldsExist (400 INVALID_SORT, documented in content/docs/api/data-api.mdx and references/api/sortability.mdx). It makes the spec's own refusal real at the earlier door. Door order (existence → provenance → sortability) mirrors assertSortFieldsExist's unknown > dotted > unmaterializable precedence. The id choice (page-field-unknown rather than sort-field-unknown) is defensible — the author reads the page rule — and the one-line verdict pin now holds the two page ids to the same ≤200-char shape.

Against the card's acceptance: readers added for element dataSource.sort, parentDataSource.sort, record.sort (and the four block families) bound to the renderer's object — yes; severity decided (error) with the ADR-0087 disposition written — yes.

Tests: validate-sortable-fields.test.ts new describe asserts [rule, severity, path] tuples for all nine slot paths, the legacy string head / -field / comma forms, the three skips, provenance at warning on the external object with silence on the local twin, and the author-declared system-name fallthrough to sort-field-unsortable. The door suite asserts status 422, code 'INVALID_METADATA', exactly one page-field-unknown issue with severity: 'error' at the slot path and activePages(rows) empty, for five slots, plus a CONTROL that saves the same five pages on subject and leaves one active row. Reverse verification is reported (8 lint / 5 door reds with sources restored). Readings are the dev's and CI's; NOT MEASURED here.

Must-fix

  • M1 — Skills-docs: none is false (content/docs/deployment/validating-metadata.mdx §5, lines ~164–170). Scenario: an author reads §5, writes record:related_list.properties.sort: [{ field: 'ghost' }], expects "advisory … degrades rather than failing", and gets 422 INVALID_METADATA from Studio. Fix either (a) amend the sentence in this PR — drop sort from the columns/sort/filter list and add one line that a page sort naming an unknown field is an error (page-field-unknown) judged with the sort rules and refused at the page publish door — and keep Skills-docs: none only if the dev also states the amended sentence; or (b) leave the doc and change both declaration lines to Skills-docs: follow-up owed #N with the card filed. Prose face: the lane seat checks the amended sentence against the diff at ACCEPT.

Should-fix (same delta commit, seat's call on surface)

  • S1 — refresh the two stale notes: reference-integrity-suite.ts:289 ("by the sortability projection alone") and authoring-rules.ts:1253–1256 (add existence page-field-unknown to the list of what a page write refuses).

Implemented-by: claude/project-thread-ru7gx5
Reviewed-by: session_01Utp6CbPxYq1wQmUNWvMZkC

VERDICT: FAIL

…ed at the page door

validating-metadata.mdx section 5 no longer lists a related list's sort as
an advisory page-binding check, and the two code notes on the page crossing
now say it also answers existence and provenance.

Claude-Session: https://claude.ai/code/session_01Utp6CYvKbGaAgjBLQdkwPB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 647a1c1fda4e3b6426696b10e82370e612a558a4
Local-runs: none
Scope: packages/lint/src/reference-integrity-suite.ts, packages/lint/src/authoring-rules.ts, content/docs/deployment/validating-metadata.mdx, .changeset/30093-page-sort-unknown-field.md + packages/lint/src/validate-sortable-fields.ts (validatePageSorts, checkPageSortDeclaration), packages/lint/src/validate-page-field-bindings.ts (isPageSortRef), packages/metadata-protocol/src/protocol-query-validation.ts (assertSortFieldsExist)

Delta review by the same isolated reader, PR #30270 (card #30093), narrowing face. Previous record on df13535218c10a428edd8c6c4d62a6f123b9ddc4 (PR comment 6649790600): FAIL on M1 (Skills-docs: none false against validating-metadata.mdx §5) with should-fix S1 (two stale code notes). This record judges the one commit df135352 → 647a1c1f (docs(lint): say a page sort naming an unknown field is an error refused at the page door); every ①–④ item of the earlier record that the delta does not touch stands as written there. Reads: git diff df135352..647a1c1f (4 files, +18/−5), the rewritten PR body, the head's check-runs, PR #27374's body (the dev's body-shape precedent). Nothing built or run.

① Derived judgments

  • No source change. The two packages/lint/src hunks (reference-integrity-suite.ts:286–293, authoring-rules.ts:1252–1257) are // comment lines only; no code token moves. packages/metadata-protocol is untouched. The accept set, the public surface and every reading in the earlier record's ① (accept-set items 1–7, severity split, lint ⊇ door on id/system names, no import cycle, no export change) stand unchanged.
  • M1 resolved. validating-metadata.mdx §5 now reads "A record:related_list's columns/filter resolve against its related object" — sort dropped, matching isPageSortRef filtering the bindings walk. The new paragraph is true sentence by sentence against the code at this head: the list door answers an unknown sort field 400 INVALID_SORT on every load (assertSortFieldsExist); the slot list — element dataSource.sort, a related list's sort and parentDataSource.sort, a list block's own sort (the PAGE_BLOCK_SORT_OBJECT_PROP rows; "list block" is a fair docs-level name for list-view / related_list / line_items / object-grid / calendar / gantt / map / timeline), interfaceConfig.sort, an app page's record.sort — is validatePageSorts' walk exactly; "a field the bound object lacks" is target.names ∪ SYSTEM_FIELDS membership; page-field-unknown error is the finding checkPageSortDeclaration pushes; 422 INVALID_METADATA is the door's envelope the gate test asserts. The following "Skipped" paragraph (relationship paths, registry-injected system fields, other packages' objects) applies to the sort slots too, and is true of them (name.includes('.'), SYSTEM_FIELDS.has, !projection).
  • Declaration lines (PR body and changeset, both line-initial): Clause-②: no (narrowing) — right, unchanged. Metadata-protocol: narrowed — right, unchanged. Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx — right: updated here is one of SKILL.md's four spellings (updated here | follow-up owed — FILES | follow-up #N | none — REASON); the trailing path is an annotation no gate reads (no script greps Skills-docs), and it names the file the diff edits. skills/** carries nothing on page sort validation, so nothing else is owed there. The adr-0087 marker is unchanged and still right.
  • S1 resolved. reference-integrity-suite.ts note now says the member also asks existence (page-field-unknown, error) and provenance (page-field-unprovisioned, warning) which validatePageFieldBindings no longer reads — true. authoring-rules.ts note now lists "a field the object does not have, as page-field-unknown" among what a page write refuses — true.

② Semver level

Unchanged: @objectstack/lint: minor, fix(lint)!:, BREAKING banner, one adr-0087 marker; only the Skills-docs: line of the changeset moved. Consistent with AGENTS.md Post-Task Checklist step 3. Check Changeset succeeded on this head (127052665625); a second run triggered by the body edit was in_progress at read time.

③ Boundary flags

  • PR-body shape. Line 1 is the dev's signature (domain:server#3 · os-bill · session_…), lines 2–3 the attribution comment and "Requested by", line 4 Fixes #30093, then the three declaration lines. PR feat(cli, runtime, core): os serve loads the plugins os plugin install installed, re-verified and dispatched by runtime tier; os plugin disable | enable | uninstall (#27123, slice 2 of 3) #27374 (merged 2026-10-29) carries the identical first four lines, and PR-body guards (claim · duplicate · Part-of · single-writer path) is success on this head after the rewrite (127063697444, started 05:12Z). Acceptable: the guards read the body as the dev shaped it and the precedent landed. The review-checklist's "正文首行引用卡片" read strictly puts the card on line 1; that is a TIER_DEFAULT ACCEPT item for the lane seat, not a contract defect, and the closing keyword sits alone beside the one card number (check-closing-keyword-parity green).
  • "Review round 1" section names the round-0 record (comment 6649790600), M1 and S1 as taken, and 87 gate families run at 647a1c1f; the body says the round-0 test / dogfood / reverse-verification readings still describe the code — true, since no source token moved.
  • Check-runs on 647a1c1f: every non-skipped run success — Lint & Repo Gates, TypeScript Type Check (the two required floors), Type Check ×5, Test Core ×8 + aggregate, Test UI ×16 + aggregate, Build Core / UI Deps / Docs, Build & E2E UI, Live E2E UI, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected, Dogfood Regression Gate, Spec property liveness, PR-body guards ×2 — except one Check Changeset re-run in_progress. Dogfood shard matrix skipped on PR as before; the merge-base red (permission-set-clone-door, job 127024968149) is unchanged by a comments-and-docs commit.
  • Earlier flags (react page sort props out of scope; lint: the object publish door does not run validateSortableFields, so an object whose listViews sort a sortable:false field saves #30095 overlap note) stand.

④ Design judgment

The narrowing itself is unchanged from the earlier record and stands: no declared capability removed, the spec's existing 400 INVALID_SORT refusal made real at the page door. The delta only makes the prose faces say so. One docs-side observation, not a defect: the §5 paragraph does not mention that an author-declared field of a system name falls through to the sortability verdict, nor the one-reader guarantee; both are os explain / code-note detail, below the doc's altitude. The "one gate, four doors" table still has no row for validateSortableFields (pre-existing since #9257/#30084, noted in the earlier record, not owed here).

No must-fix. No should-fix.

Implemented-by: claude/project-thread-ru7gx5
Reviewed-by: session_01Utp6CbPxYq1wQmUNWvMZkC

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

席位核对

ACCEPT · head 647a1c1fda4e3b6426696b10e82370e612a558a4 · card #30093 (claim 6648657894, amended 6649800491) · 2026-11-05T05:17Z

  • Shape: draft PR to main, author objectstack-fleet[bot] (relay), Fixes #30093; body's first line is the dev's signature line with the attribution block on lines 2–3 (same shape as merged feat(cli, runtime, core): os serve loads the plugins os plugin install installed, re-verified and dispatched by runtime tier; os plugin disable | enable | uninstall (#27123, slice 2 of 3) #27374; PR-body guards success on this head). 9 files, +417 −48. check-governed-merges.mjs --pr 30270: NOT governed ⇒ not Tier H.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: narrowed, Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx; changeset @objectstack/lint minor with BREAKING and adr-0087 not-required (no-migration-prescription).
  • Contract review: at CONTRACT_REVIEW_TIER on the narrowing face: record 6649790600 on df135352 (FAIL: M1 the §5 sentence, S1 two stale comments), delta record 6650369794 on this head (PASS; delta is the changeset line, §5 and two comment-only hunks, no non-comment packages/*/src change).
  • Accept set (from the record): every stored page sort slot is judged by validateSortableFields in door order; an unknown field is page-field-unknown (error, CLI) and 422 INVALID_METADATA at the page publish door; lint's accept set (names ∪ SYSTEM_FIELDS) ⊇ the runtime's, so nothing the renderer sorts on is refused.
  • Gates (check-runs on this head): 69 runs, 59 success, 10 skipped, 0 failed; Lint & Repo Gates, TypeScript Type Check, Check Changeset, Dogfood Regression Gate success. The earlier Test UI failure was on df135352: shards cancelled by the push, not a test failure. Skips read by name (path-gated, opt-in harnesses, the schedule gate): check-expected-skips.mjs cannot run in this container (yaml not installed).
  • Refusal assertions: lint pins assert rule, path, severity on nine slots; door pins assert status 422, code INVALID_METADATA, nothing stored, with a saving CONTROL; ablation recorded (lint 8 red, door 5 red, restored).
  • Prose read against the diff: the §5 related-list sentence and the new page-sort paragraph; the changeset's BREAKING banner.
  • Accepted deviation: file surface widened for the patch round (amendment 6649800491).
  • Carried: lint: the object publish door does not run validateSortableFields, so an object whose listViews sort a sortable:false field saves #30095 (the object publish door does not run validateSortableFields) is next on this seat.
  • Writes: dev api_writes 2 + issue_patch 1 + 3 pushes, mcp_calls 0.
  • Landing: held. The seat's session does not ready and arm auto-merge on a BREAKING narrowing without the maintainer's word; the seat asked the maintainer in its project thread, together with fix(lint)!: under a package closure, judge a dashboard or report filter key on another package's dataset against that dataset's object (#30005) #30231.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant