fix(spec,metadata-protocol)!: 排序节点写 direction 不再静默排反方向,两扇门同一个 change 关 (#4721) - #4922
Merged
Merged
Conversation
…ion `direction` (#4721) `SortNodeSchema` was a plain `z.object`, so zod's `.strip` default applied: SortNodeSchema.parse({ field: 'updated_at', direction: 'desc' }) → { field: 'updated_at', order: 'asc' } The foreign key was discarded, `order` fell back to `asc`, and the sort ran the OPPOSITE way under an ordinary success — with `limit`, a different set of rows, with no signal anywhere in the response. `direction` is not a typo: it is `IReportService.orderBy`'s live vocabulary, which plugin-auth's objectql adapter already translates by hand. A translation known to be necessary and enforced nowhere is the ADR-0049 shape. Both doors onto that shape are closed here, in one change: - `SortNodeSchema` (spec/src/data/query.zod.ts) → `strictObject` with `aliases: { direction: 'order' }`, so the rejection carries the translation. Edit distance can never bridge `direction` → `order`, so a bare "unrecognized key" would leave the caller where the silent strip did. - `normalizeSortNodes` (metadata-protocol/src/protocol.ts) — the ingress every external `orderBy` funnels through — refuses `{ field, direction }` with 400 INVALID_SORT naming `order` and quoting the corrected node. Closing only the schema would repeat the #1535/#4522 door asymmetry: `SortNodeSchema.parse` is reachable by three paths the REST normalizer never sees, and the normalizer runs ahead of any QueryAST parse. Deliberately NOT in scope: `QuerySchema`'s top level stays non-strict (`QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success === true`) — tracked in the #4001 campaign map for its own batch. The `{field: direction}` map form is untouched: there `direction` is an ordinary column name, and refusing it would be the mirror-image bug. Strictness ledger: `query.zod.ts` keeps its `open` class for the four dialect sites; `SortNodeSchema` is carved out as authorable (4 strip of 5), which also resolves the recorded classification conflict — the FILE was the wrong unit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
…oves to line comments `build-docs.ts` takes the FIRST `/** */` block in a `.zod.ts` file as the reference page's description, and joins every line of it with a blank line. The long #4721 rationale therefore rendered as a 56-line wall at the top of `content/docs/references/data/query.mdx`, where a customer reads what a sort node IS — not why one schema in the file is strict. The prose is unchanged, it is just `//` instead of `/** */` so the generator cannot pick it up, with a note at the top saying why it must stay that way. Regenerated: query.mdx (two lines), and the two skill reference indexes, which grew transitive entries because query.zod.ts now imports shared/strict-object. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 3, 2026
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 16:52
xuyushun441-sys
enabled auto-merge
August 3, 2026 16:52
…tnode-strict # Conflicts: # docs/audits/2026-07-unknown-key-strictness-ledger.md
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 3, 2026
Ledger conflict was expected and is the three-way `automation/` header the dispatch note warned about: 批 9 (#4925) landed on main while this branch was open, and both sides edit the same section. Resolved by keeping BOTH sides' row edits and recomputing the header from the merged rows rather than taking a side: 批 9 removed builtin-node-config (8) / schemaless-node-config (4) / io-node-config (2), and rewrote their triage notes 批 11 removed flow-function (1) / time-relative-trigger (1) / webhook (1), and dropped flow.zod.ts 7 → 1 header 67 → 44 strip of 75 (13+10+1+6+5+5+4, summed from the rows) authorable 41 → 18 (etl 7 + state-machine 6 + control-flow 5) `check:strictness-ledger` arbitrates that arithmetic and passes; the header was NOT copied from either side. os-regen four-step ran in full: merge → `git checkout origin/main --` every `merge=os-regen` path → `pnpm install --frozen-lockfile` + rebuild spec + `check:generated --fix` (regenerated `content/docs/references/**` only) → assert BOTH sides survive. Sibling entries verified present after the wholesale regeneration: 批 9's NotifyConfig / HttpConfig / Script node-config pages and #4922's `data/SortNode` are all in `authorable-surface.json` beside this branch's `automation/Webhook:_packageId` / `:protection`. Merged-tree verification (§10: main touched packages/spec, the same package as this diff, so the full suite was re-run rather than scoped): check:generated 8/8 · all 8 pure audits PASS (incl. strictness-ledger) @objectstack/spec 297 files / 7450 tests · tsc --noEmit clean Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4721
按 2026-08-03 11:41Z 维护者裁决(方案 A)执行:把
SortNodeSchema从data/query.zod.ts的 blanketopen里拎出来单独定级为 authorable,转成strictObject+aliases,并在同一个 change 里把 wire 侧那扇门一起关掉。Part of #4001。奠基事实(在本分支的 worktree 上对
origin/main=89d2a4e实测)裁决评论里已就地更正过一条前提,派发词照此奠基,这里再实测一次并贴原始输出:
三行的含义,按重要性排:
direction被 strip 掉,order落回asc默认值。带limit时这不是「一页顺序不对」,是返回了另一批行,响应里没有任何可区分的信号。这是本单值得做 wire-breaking 的全部理由。QuerySchema本身不是 strict——所以本单原始 issue body 里「[P2] A direct engine call silently dropssort/select/skip/populate— declared query contract, zero enforcement #4371 已让顶层拒绝未声明选项键,排序节点是同一不变量下一层」的论证不成立,本 PR 不复活它。[P2] A direct engine call silently dropssort/select/skip/populate— declared query contract, zero enforcement #4371 的检查是packages/objectql/src/engine.ts里手写的rejectUnknownEngineOptionsallowlist,只在顶层迭代Object.entries(bag),不递归进orderBy[]。direction不是拼写错误,是活着的竞争词表:IReportService.orderBy用{field, direction}(packages/spec/src/contracts/report-service.ts:29),而packages/plugins/plugin-auth/src/objectql-adapter.ts已经在手工翻译两者:翻译已知必需、却无处强制——ADR-0049 的形状。
改了什么
门一(schema):
packages/spec/src/data/query.zod.tsSortNodeSchema由裸z.object转为strictObject,带aliases: { direction: 'order' }。用
aliases而不是靠编辑距离,是因为编辑距离永远够不到direction→order(它不是 typo,是另一个词)。这正是 helper 文档里visibleWhen → visible那一类,只有手写条目能把处方送到作者手上。一条只说 "Unrecognized key(s):direction" 的拒绝,把调用方留在跟静默 strip 完全相同的位置——他仍然不知道该写什么。改后实测:
最后一行是范围证明:
QuerySchema顶层 strict 这个新发现按裁决第 3 条留在 #4001 战役地图里,本 PR 一个字没动。data-engine.zod.ts只是z.array(SortNodeSchema)引用,无需改动——RPC 那一行就是它带来的,自动跟随。门二(wire):
packages/metadata-protocol/src/protocol.tsnormalizeSortNodes(每一个 REST/RPC ingress 都汇流到这里)现在对{ field, direction }以点名order的消息拒绝,挂在既有的invalidSortError+INVALID_SORT上,新增invalidSortDirectionKeyError。实测:为什么必须两扇门一起关(裁决硬约束 1):normalizer 跑在任何 QueryAST parse 之前,所以外部调用方永远碰不到 schema;而
SortNodeSchema.parse有三条 normalizer 永远看不见的路(defineStack、data-engine.zod.ts的orderBy: z.array(SortNodeSchema)、直接 parse QueryAST)。只关一扇是 #1535 发出去、#4522 才回来收的那个门不对称。刻意不动的一处:
{field: direction}map 形式里叫direction的键是一个普通列名(「按direction列排序」),照旧放行。拒绝它会是镜像 bug——一条合法查询,只因为某列名字撞了外来词表就被拒。这条有专门的回归测试。行为对照
orderBy[{ field: 'x', order: 'desc' }][{ field: 'x', direction: 'desc' }]400 INVALID_SORT,消息点名order[{ field: 'x', order: 'desc', direction: 'asc' }]400 INVALID_SORT'-x'/['-x']/{ x: 'desc' }{ direction: 'desc' }(map 形式)direction列排台账 / 生成物
docs/audits/2026-07-unknown-key-strictness-ledger.md:query.zod.ts的「动代码前先裁定」记录按裁决 A 结清。分类结论写成可复用的一句——当 blanket 的 per-file 定级和 per-schema 发现冲突时,先怀疑 blanket:open是判给过滤表达式(谓词值里流用户数据)的,而排序节点是封闭两键元组、没有任何用户数据面。triage 行拆成filter.zod.ts/query.zod.ts两行;remaining-strip 行 5 → 4,data/表头 121 → 120,「66 wire/open」→ 65。pnpm --filter @objectstack/spec check:strictness-ledger通过。content/docs/references/data/query.mdx只动了两行。第一次改动时它变成了一堵 56 行的墙——build-docs.ts取文件里第一个/** */块作为整页描述,并且逐行插空行。所以长篇 rationale 改成了//行注释,块注释上留了一条说明为什么必须保持短。query.zod.ts现在 importshared/strict-object),生成物,已check:generated --fix重出。orderBy数组里没有任何direction:用法(唯一命中是IReportService自己的契约声明和 plugin-auth 那处正确翻译),三个示例 app 的validate全部 exit 0。验证
pnpm --filter @objectstack/spec testTest Files 295 passed (295)/Tests 7367 passed (7367)pnpm --filter @objectstack/metadata-protocol testTest Files 33 passed (33)/Tests 288 passed (288)pnpm --filter @objectstack/objectql testTest Files 110 passed (110)/Tests 1742 passed (1742)pnpm --filter @objectstack/spec --filter @objectstack/objectql typecheckDone/Done(metadata-protocol无 typecheck 脚本,是check-type-check-coverage.mjs里的既有 TYPE_DEBT 条目,28 errors,未变动)check:*全跑check:generated8 项 up to date;liveness/empty-state/react-declaration-parity/skill-examples/variant-docs/strictness-ledger/exported-any/dual-source-exports逐个 PASS)check:doc-authoringcheck:nul-bytescheck:error-code-casingcheck:adr-anchorscheck:type-check-coveragecheck:release-notescheck:merge-drivercheck:durability-log-levelcheck:startup-registry-verdict全 PASSpnpm validateapp-crm exit=0/app-showcase exit=0/app-todo exit=0新增/改动的测试:
packages/spec/src/data/query.test.ts— 新 describe「SortNode — closed, anddirectioncarries its translation」7 例,包含处方断言(消息含`direction` → `order`)、order与direction同时出现也拒、经QueryAST.orderBy的同门断言,以及一条范围 pin:QuerySchema顶层仍非 strict。packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts— 头部那句 strip 时代的 pin(「the schema is not.strict(), sodirectionwas dropped rather than flagged」)明确标注已退休并说明原因,新增 6 例外部调用方拒绝测试:三种 wire 拼写、engine 未被调用的断言、处方断言、direction列名放行、以及规范拼写仍然到达 engine 并正确排序的 control。packages/objectql/src/query-expression-conformance.test.ts— 在真引擎上补一例:先证order: 'desc'+top: 2得到['E','D'],再证同一请求换成direction是 400(改前它解析成['A','B']——表的另一端)。顺带项(按裁决要求测量后单独立项,均未认领)
as any/: any擦除(#4721 的顺带项,已实测残余量) #4918 — lint 禁止对引擎查询选项as any/: any。实测残余量:非测试代码里 25 处调用点擦除 + 11 处const opts/options/query: any(测试里另有 126 处)。36 处不是「顺手清掉」的量级,故按 Prime Directive chore: version packages #10 立项而非在本 PR 实现;issue 里把三个需要拍板的规则边界问题列了出来。os-view-grid片段生成的 view 现在 parse 不过:list.defaultSort/list.pageSize已被 #4001 关掉 #4917 — VS Code 扩展os-view-grid片段展开后 parse 不过(list.defaultSort/list.pageSize被 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 关掉了)。与本单无关,发现于 ADR-0087 载荷扫描。不在范围(裁决硬约束 3)
QuerySchema顶层 strict、objectql/src/engine.ts递归进orderBy[]的引擎侧加固——都没做。前者进 #4001 战役地图按批次走;后者按派发词,#4721 的范围是外部调用方,内部调用方由 #4720 的类型恢复覆盖。Draft,未 ready、未合并。
.changeset/sort-node-direction-rejected.md为 major(.changeset/pre.json仍是mode: pre/tag: rc,v17 窗口内),changeset 正文带 FROM → TO 与完整行为对照表。Generated by Claude Code