Repository navigation
fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door - #30021
Conversation
…d the data door (#29896) Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
…bility projection and the data door (#29896) Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
…es, changeset (#29896) Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 42 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9ed311cb85d83c624ec815e936a829b9426fbc65 && git checkout 9ed311cb85d83c624ec815e936a829b9426fbc65
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 63b86b385dd2cbfeba131438112c49be2c74df91 8738359bde443b9bca880039ffb1b5d017d61813 && git checkout -B drift-repro 63b86b385dd2cbfeba131438112c49be2c74df91 && git merge --no-ff 8738359bde443b9bca880039ffb1b5d017d61813
node scripts/docs-audit/affected-docs.mjs --json 63b86b385dd2cbfeba131438112c49be2c74df91
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: accept-set narrowing at the Data API list door ( ① Derived judgments
② Semver levelDeclared: ③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: REWORK REWORK items:
|
…narrowed declaration and upgrade entry (#29896) Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
…29896) Claude-Session: https://claude.ai/code/session_01WGufzhnsFtaPy7QwK43MYt Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: At
The stored surfaces:
Face: delta review of the fix-up after REWORK 6624674350. The round-2 change is confined to the declaration, one new semantic entry, the stdio MCP door with its three-test pin, the category-6 docblock with its regenerated page, and the liveness note. ① Derived judgments
② Semver level
③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29896
Clause-②: yes (narrowing) —
GET /api/v1/data/:object(and every request that reaches the same list door:POST /api/v1/data/:object/query, the export route, the RPC dispatcher, the record-pagerelated[].sort) and the stdio MCPquery_recordsrefuse a sort on a field whose author declaredsortable: false, which they used to answer sorted by the column; a stored list viewsortorrecord:related_listsortnaming such a field is now refused when it is read. The widening half:@objectstack/spec/apiexportsFIELD_UNSORTABLE_AUTHOR_DECLARED, andFieldSortabilitySchema.reasonaccepts'author-declared'beside'virtual-type'.Metadata-protocol: narrowed
Skills-docs: none —
skills/**names nosortablefield key, andcontent/docs/**already teachessortable: falseas "do not allow sorting by this field" andINVALID_SORTas "the field must exist and havesortable: true", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry.维护者速读
sortable: false以后,对象元数据里随附的「可排序投影」把它报成不可排序(reason: 'author-declared'),列表表头和排序选择器因此不再提供它;数据接口和 stdio MCP 的query_records对这个字段的排序直接 400INVALID_SORT,和对公式字段排序的拒收是同一个答复。GET /api/v1/data/对象?sort=该字段照样 200 并按它排序,Studio 还把它当成一个有效开关展示。sortable: false字段的排序从 200 变成 400;已存的列表视图sort或record:related_list的sort若指向这种字段,读取时也会 400,升级指南条目field-sortable-false-sort-refused说明怎么改。仓内没有任何对象写sortable: false;没写这个键或写true的字段完全不变;引擎内部调用(钩子、流程)不受影响。回滚就是 revert 本 PR。packages/metadata-protocol/src/protocol-query-validation.ts的assertSortFieldsAreOffered(新判定,按投影拒收)、packages/spec/src/api/sortability.zod.ts的resolveObjectSortability(投影读取该键)、packages/rest/src/data-sort-field-sortable-flag-door.test.ts(真实路由钉子)。pnpm --filter @objectstack/rest exec vitest run src/data-sort-field-sortable-flag-door.test.ts,预期 6 个用例全过(投影报不可排序、sort=nosort400 且未读库、普通字段与sortable: true字段双向排序照常、公式字段拒收不变)。What changes
packages/spec/src/api/sortability.zod.ts).resolveObjectSortabilityanswers{ sortable: false, reason: 'author-declared' }for a field whosesortableis the literalfalse, the same entry shape a virtual type gets ({ sortable: false, reason: 'virtual-type' }). A field with nosortablekey, orsortable: true(the schema default once parsed), keeps its storage verdict. Aformulafield also markedsortable: falsekeepsreason: 'virtual-type'.FieldSortabilitySchema.reasonbecomesz.enum(['virtual-type', 'author-declared']), andFIELD_UNSORTABLE_AUTHOR_DECLAREDis exported besideFIELD_UNSORTABLE_VIRTUAL_TYPE. The module docblock gains category 6.packages/metadata-protocol/src/protocol-query-validation.ts).assertSortFieldsExistgains a fourth verdict afterunknown>dotted> unmaterializable:assertSortFieldsAreOfferedcomputes the sameresolveObjectSortabilityprojection over the registered object and refuses any undotted sort name whose entry sayssortable: false, through the sameinvalidSortError(400 INVALID_SORT,param,field,fields,object) the formula refusal uses. It does not read the key a second time, so the door and the affordance cannot disagree. Every caller offindDatapasses this door: the REST list route,POST /data/:object/query, the export route and the RPC dispatcher.query_recordsrefuses it too (packages/mcp/src/stdio-data-bridge.ts). That bridge handsorderBystraight toengine.find, so it never passed the list door.query()now callsunofferedSortRefusalafter the exposure and stored-metadata refusals and before the engine: the sameresolveObjectSortabilityprojection over the objectdescribe_objectserves, and the same envelope (INVALID_SORT,status/httpStatus400,field,fields,object,param: 'orderBy'). The function is exported within the package only, like its siblings;index.tsdoes not publish it.assertOrderByIsMaterializable(objectql/engine.ts) answers whether a driver can order by a column, and a stored column can. In-process callers (hooks, flows) keep their sort.sortable: falsegoverns what the platform offers callers, not what its storage can do. This is written down in the new function's docblock and in the projection's category 6.packages/spec/src/migrations/entries/semantic/18.field-sortable-false-sort-refused.ts: the stored surfaces are a list view'ssort, a pagerecord:related_listblock'ssort(it reaches the record-pagerelated[].sort:ui/packages/app-shell/src/views/recordPagePayload.ts#recordPageRelatedListscopies it there) and the related list'sdataSource.sort; the replacement is dropping the sort or removingsortable: false. No mechanical rewrite exists, so it is a D3 semantic entry, on the precedent of17.engine-find-formula-order-by-refused.liveness/field.jsonsortablestayslive(the grade does not move) and gainsevidence(sortability.zod.ts#resolveObjectSortability,protocol-query-validation.ts#assertSortFieldsAreOffered) and a note naming the pins. Before, the row said "grid." with no evidence while nothing read the key.api-surface/api.jsonandexport-origins/api.json(+1 export),content/docs/references/api/{sortability,protocol}.mdx(the docblock and thereasonenum).@objectstack/spec,@objectstack/metadata-protocoland@objectstack/mcp, allminor(Metadata-protocolnarrowed), BREAKING banner, Before/Now/Stored sorts, ADR-0087registered field-sortable-false-sort-refused.Why
The card's reproduction at
589049b0: an object withnosort: { type: 'text', sortable: false }servedsortability.fields.nosort = { sortable: true }, andGET /api/v1/data/OBJ?sort=nosortanswered 200 sorted. Re-measured on this branch's baseeee41490through the real routes (the new pin file, run before the fix): the projection pins were red withexpected { sortable: true } to deeply equal { sortable: false, …(1) }, andsort=nosortanswered 200 with the rows inxray, yankee, zuluorder (sorted by the column). The formula control in the same file was already400 INVALID_SORT. So the door's existing answer for a field the projection calls unsortable is a refusal, and this PR gives that answer, which narrows the door's accept set.Tests
packages/rest/src/data-sort-field-sortable-flag-door.test.ts(new, 6), on thedata-search-field-searchable-flag-door.test.tsharness: realSqlDriver(SQLite), realObjectQL, realObjectStackProtocolImplementation, realRestServerroutes.nosort{ sortable: false, reason: 'author-declared' },nameandrank{ sortable: true },calcvirtual-type.GET /api/v1/meta/:type/:nameenvelope carries the same verdict and deep-equalsresolveObjectSortability(item). The envelope'sgetMetaItemanswer is the producer's shape over the registered object, because this harness boots no metadata service.sort=nosort,sort=-nosort,$orderby=nosort descandsort=name,nosortare each400 INVALID_SORT, naming'nosort', with zero driver reads.name(nosortablekey) andrank(sortable: true) sort both ways;sort=calc(formula) is still400 INVALID_SORT"a formula field", with zero driver reads.packages/mcp/src/stdio-data-bridge.sortable-false.test.ts(new, 3), on the stand-in engine ofstdio-data-bridge.internal-field-evaluation.test.ts:orderByonnosort, alone and behind a sortable field, isINVALID_SORT400 withfield,objectandparam: 'orderBy', and the engine is never called; controls (nosortablekey,sortable: true, a filter on the field) reach the engine unchanged;query_recordsrenders the refusal as its envelope.packages/spec/src/api/sortability.test.ts(+6): the author-declared verdict with a no-key neighbour, the PARSED field (FieldSchema.parse: unset andtruestay sortable,falsedoes not), only the literalfalsecounts (0,nullstay sortable), a formula withsortable: falsekeepsvirtual-type, the array field-map shape, and wire validity underObjectSortabilitySchema.Acceptance notes
ObjectGrid.tsxoverrides each generated column'ssortablethroughisPlatformSortableField, and the ListView Sort picker reads the projection. Two gaps remain, and the seat files adomain:uicard for them:ListView.tsxseedscurrentSortfrom the view's storedsortand sends it to the server unfiltered (~L3186;filterPlatformSortableSortcovers only the header and the emit/persist path), and under client-side sorting the header is still offered.packages/lint/src/validate-sortable-fields.tsjudges a list view's declaredsortforunknown,dottedand virtual names, and not for a field declaringsortable: false. A view whose declared sort names such a field publishes clean, and its sort is then refused at the door, or dropped by the grid's projection filter. No in-repo object authorssortable: false(git grep -nE "sortable\s*:\s*false"outsideui/and tests: zero producers), so nothing in the shipped corpus moves.(narrowing); the new export and the widenedreasonenum make ityes. The first round declaredMetadata-protocol: noneandnot-required (no-migration-prescription). That was wrong: a stored list viewsort(and arecord:related_listsort) naming asortable: falsefield loaded sorted before and is refused when read now, so the kind isnarrowed, every moved package goesminor, and the disposition isregisteredwith the semantic entry above. The first changeset's sentence "A sort the door now refuses is a request, never a stored value" is struck.protocol.tsuntouched. The door's body lives inprotocol-query-validation.ts; the method docblock inprotocol.tsis left alone so this PR does not overlap the large-file split of that file.Verification
Round 2 (head
8738359b), the contract-review reworkEvery reading below is at head
8738359b, baseeee41490. Main has moved 2 commits since, with no overlapping path, and the localmerge-treeis clean. Round 2 touches only the changeset, the upgrade entry, the stdio bridge with its pin, one.describe()sentence and its regenerated page, and the liveness note (git diff --stat bc7630ee..HEAD: 7 files, +225 / −18).protocol-query-validation.tsand the rest pin are unchanged from round 1.packages/mcp/src/stdio-data-bridge.sortable-false.test.ts. Without the guard it readsTests 2 failed | 1 passed (3): the bridge resolved with the engine called, andquery_recordsanswered a result instead ofisError. With the guard it readsTests 3 passed (3).if (sortRefusal && 'ABLATED_29896_MCP'.length < 0) throw sortRefusal). The two refusal tests went red and the control stayed green. The file was then restored, the blob matches HEAD, and the pin is green again.pnpm --filter @objectstack/mcp test: Test Files 56 passed, Tests 663 passed.typecheck(tsc + test-typecheck) exits 0 after8738359btyped the stand-infind. The first run reported one TS2493 in the new file, and no debt ledger entry was added.pnpm --filter @objectstack/spec test: Test Files 811 passed, Tests 24376 passed. This includes the semantic-migration registry suites, with the new entryfield-sortable-false-sort-refused.data-sort-field-sortable-flag-door.test.ts: 6 passed (unchanged). eslint on the changed files: exit 0.check:generated --fixregenerated onlycontent/docs/references/api/sortability.mdx, which is committed inf320e8de.check-changeset-no-major --base origin/main, with this body as the event: exit 0. It readsmetadata-protocol narrowed, and no package whose published source it moves is graded patch.check-adr-0087-registration --base origin/main: exit 0,registered field-sortable-false-sort-refused (new here).pnpm --filter @objectstack/dogfood testat8738359b, under the verify lock, afterturbo build --filter=@objectstack/mcp... --filter=@objectstack/dogfood^...(69/69): Test Files 416 passed | 3 skipped (419), Tests 3456 passed | 28 skipped (3484), with no failures.node scripts/pm/dispatch-gates.mjs --commandsderives 107 commands at8738359b: round 1's 105, pluscheck:spec-changesandcheck:upgrade-guide. All 107 exit 0. The--ranreconciliation reads "107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED".Round 1 (head
bc7630ee)Every reading below is at head
bc7630ee, baseeee41490(main has moved one unrelated commit since,9d1a465f0; no overlapping path, localmerge-treeclean).The card's reproduction, before and after (the new pin file)
eee41490, same harness):Tests 3 failed | 3 passed (6). The projection pins failed withexpected { sortable: true } to deeply equal { sortable: false, …(1) }, and{"sort":"nosort"}answered 200 withrecordsinxray, yankee, zuluorder (sorted bynosort):expected 200 to be 400. The three controls passed, includingsort=calc(formula) already400 INVALID_SORT.Tests 6 passed (6).Ablation (one, of the projection change)
Mutation:
| undefined)?.sortable === false→| undefined)?.sortable === 'ABLATED_29896'insortability.zod.ts, viascripts/ablation-replace.mjs(anchor 1 → 0, bloba15d6b6a4b9a→2e0f03fd3415). Thenpnpm --filter @objectstack/spec build, andablation-dist-preflight.mjs @objectstack/spec ABLATED_29896exited 0 (marker indist/api/index.jsandindex.mjs).sort=nosortrefusal went red, which shows the door judges by the projection. The three controls stayed green. The specsortability.test.tsgave 3 failed / 15 passed (the author-declared pins).a15d6b6a4b9a(== HEAD),git diff HEADempty,git status --porcelainclean, spec rebuilt,--absentpreflight exit 0 over 592 built files.Package tests and typecheck
pnpm --filter @objectstack/spec test: Test Files 811 passed, Tests 24376 passed | 1 todo;typecheckexit 0.pnpm --filter @objectstack/metadata-protocol test: Test Files 312 passed | 4 skipped, Tests 22346 passed | 26 skipped;typecheckexit 0.pnpm --filter @objectstack/rest test: Test Files 385 passed, Tests 7184 passed | 332 skipped;typecheck(tsc + test-typecheck) exit 0 afterbc7630eetyped the request list (the first run reported one TS2345 in the new file; no debt ledger entry was added).eslint --no-inline-configover the four changed source/test files: exit 0.pnpm lint(repo-wide): exit 0 atbc7630ee.Full build and dogfood (narrowing arm)
turbo run build --concurrency=2 --filter=!@objectstack/docs: 119/119 tasks successful.pnpm --filter @objectstack/dogfood testatbc7630ee, under the verify lock:VERDICT command-exit 0, Test Files 416 passed | 3 skipped (419), Tests 3456 passed | 28 skipped (3484), no failures.Derived gates (dispatch-gates --commands)
node scripts/pm/dispatch-gates.mjs --commandsderives 105 commands (the same list at61e984efand atbc7630ee). All 105 ran after the full build.--ranreconciliation: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED".check-plugin-teardown-shape --self-testcould not read its pinned fixture commit on the shallow clone; aftergit fetch origin 621a4876…it passed (48 cases).check-changeset-no-majorwith this body as the event: exit 0, "declares clause-②yes (narrowing)and metadata-protocolnone… introduces no changeset entry gradedminoror above and moves no metadata-protocol baseline".check-adr-0087-registrationwith this body as the event: exit 0,[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).pnpm --filter @objectstack/spec check:generated --fixregenerated exactly the three it proved stale (api-surface/,export-origins/,content/docs/references/**).check:authorable-surfaceandcheck:livenessare green.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code