Skip to content

fix(spec,metadata-protocol)!: 排序节点写 direction 不再静默排反方向,两扇门同一个 change 关 (#4721) - #4922

Merged
xuyushun441-sys merged 5 commits into
mainfrom
claude/issue-4721-sortnode-strict
Aug 3, 2026
Merged

fix(spec,metadata-protocol)!: 排序节点写 direction 不再静默排反方向,两扇门同一个 change 关 (#4721)#4922
xuyushun441-sys merged 5 commits into
mainfrom
claude/issue-4721-sortnode-strict

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4721

按 2026-08-03 11:41Z 维护者裁决(方案 A)执行:把 SortNodeSchemadata/query.zod.ts 的 blanket open 里拎出来单独定级为 authorable,转成 strictObject + aliases,并在同一个 change 里把 wire 侧那扇门一起关掉。Part of #4001

奠基事实(在本分支的 worktree 上对 origin/main = 89d2a4e 实测)

裁决评论里已就地更正过一条前提,派发词照此奠基,这里再实测一次并贴原始输出:

SortNodeSchema.parse({field:"updated_at", direction:"desc"}) => {"field":"updated_at","order":"asc"}
QuerySchema.safeParse({object:"sales", nonsenseKey:1}).success => true
QuerySchema.parse({object:"sales", orderBy:[{field:"updated_at",direction:"desc"}]})
  => {"object":"sales","orderBy":[{"field":"updated_at","order":"asc"}]}

三行的含义,按重要性排:

  1. 降序进去,升序出来,而且 parse 成功。direction 被 strip 掉,order 落回 asc 默认值。带 limit 时这不是「一页顺序不对」,是返回了另一批行,响应里没有任何可区分的信号。这是本单值得做 wire-breaking 的全部理由。
  2. QuerySchema 本身不是 strict——所以本单原始 issue body 里「[P2] A direct engine call silently drops sort/select/skip/populate — declared query contract, zero enforcement #4371 已让顶层拒绝未声明选项键,排序节点是同一不变量下一层」的论证不成立,本 PR 不复活它。[P2] A direct engine call silently drops sort/select/skip/populate — declared query contract, zero enforcement #4371 的检查是 packages/objectql/src/engine.ts 里手写的 rejectUnknownEngineOptions allowlist,只在顶层迭代 Object.entries(bag),不递归进 orderBy[]
  3. 因此:关掉排序节点是开一扇新门,不是补完一条旧不变量。这正是 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 战役 finding 17(「只守一扇门的 bespoke 守卫」)的形状,所以本 PR 的正当性建立在事实 1 上,不建立在一致性论证上。

direction 不是拼写错误,是活着的竞争词表:IReportService.orderBy{field, direction}(packages/spec/src/contracts/report-service.ts:29),而 packages/plugins/plugin-auth/src/objectql-adapter.ts 已经在手工翻译两者:

const orderBy = sortBy ? [{ field: sortBy.field, order: sortBy.direction as 'asc' | 'desc' }] : undefined;

翻译已知必需、却无处强制——ADR-0049 的形状。

改了什么

门一(schema):packages/spec/src/data/query.zod.ts

SortNodeSchema 由裸 z.object 转为 strictObject,带 aliases: { direction: 'order' }

aliases 而不是靠编辑距离,是因为编辑距离永远够不到 directionorder(它不是 typo,是另一个)。这正是 helper 文档里 visibleWhen → visible 那一类,只有手写条目能把处方送到作者手上。一条只说 "Unrecognized key(s): direction" 的拒绝,把调用方留在跟静默 strip 完全相同的位置——他仍然不知道该写什么。

改后实测:

SortNodeSchema.parse({field:'updated_at', direction:'desc'})
  => THROWS: Unrecognized key(s) on this sort node: `direction`. Until #4721 an unknown key
     here was dropped silently and `order` fell back to its `asc` default, so a descending
     request came back ascending — and with `limit`, a different set of rows under an
     ordinary 200. Did you mean `direction` → `order`?

SortNodeSchema.parse({field:'updated_at', order:'desc'})  => {"field":"updated_at","order":"desc"}

QuerySchema.parse({object:'sales', orderBy:[{field:'updated_at',direction:'desc'}], limit:20})
  => THROWS: (同上)

RPC DataEngineFindRequestSchema.parse({method:'find',object:'sales',
       query:{orderBy:[{field:'updated_at',direction:'desc'}]}})
  => THROWS: (同上)

QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success  [本单不扩权 — 应保持 true]
  => true

最后一行是范围证明:QuerySchema 顶层 strict 这个新发现按裁决第 3 条留在 #4001 战役地图里,本 PR 一个字没动。

data-engine.zod.ts 只是 z.array(SortNodeSchema) 引用,无需改动——RPC 那一行就是它带来的,自动跟随。

门二(wire):packages/metadata-protocol/src/protocol.ts

normalizeSortNodes(每一个 REST/RPC ingress 都汇流到这里)现在对 { field, direction } 以点名 order 的消息拒绝,挂在既有的 invalidSortError + INVALID_SORT 上,新增 invalidSortDirectionKeyError。实测:

status = 400 | code = INVALID_SORT | param = orderBy | field = updated_at
message = Query parameter 'orderBy' spells the sort direction for 'updated_at' as `direction`,
  which is not a key on this axis — the QueryAST sort node is `{ field, order }`. It was not
  applied, and an unapplied sort returns the rows in an arbitrary order — which 'top'/'limit'
  then slices into an arbitrary page. Write `{ field: 'updated_at', order: 'desc' }`.
  `direction` is `IReportService.orderBy`'s vocabulary, a genuinely different contract; on this
  axis it was silently dropped and `order` fell back to `asc`, so a descending request came
  back ascending — and with `limit`, a different set of rows.

为什么必须两扇门一起关(裁决硬约束 1):normalizer 跑在任何 QueryAST parse 之前,所以外部调用方永远碰不到 schema;而 SortNodeSchema.parse 有三条 normalizer 永远看不见的路(defineStackdata-engine.zod.tsorderBy: z.array(SortNodeSchema)、直接 parse QueryAST)。只关一扇是 #1535 发出去、#4522 才回来收的那个门不对称。

刻意不动的一处:{field: direction} map 形式里叫 direction 的键是一个普通列名(「按 direction 列排序」),照旧放行。拒绝它会是镜像 bug——一条合法查询,只因为某列名字撞了外来词表就被拒。这条有专门的回归测试。

行为对照

你发的 orderBy 之前 之后
[{ field: 'x', order: 'desc' }] 降序 不变 — 降序
[{ field: 'x', direction: 'desc' }] 200,升序 400 INVALID_SORT,消息点名 order
[{ field: 'x', order: 'desc', direction: 'asc' }] 200,降序 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 改成了 // 行注释,块注释上留了一条说明为什么必须保持短。
  • skill 参考索引新增了 transitive 条目(query.zod.ts 现在 import shared/strict-object),生成物,已 check:generated --fix 重出。
  • ADR-0087 载荷扫描:全仓 orderBy 数组里没有任何 direction: 用法(唯一命中是 IReportService 自己的契约声明和 plugin-auth 那处正确翻译),三个示例 app 的 validate 全部 exit 0。

验证

命令 结果
pnpm --filter @objectstack/spec test Test Files 295 passed (295) / Tests 7367 passed (7367)
pnpm --filter @objectstack/metadata-protocol test Test Files 33 passed (33) / Tests 288 passed (288)
pnpm --filter @objectstack/objectql test Test Files 110 passed (110) / Tests 1742 passed (1742)
pnpm --filter @objectstack/spec --filter @objectstack/objectql typecheck Done / Done(metadata-protocol 无 typecheck 脚本,是 check-type-check-coverage.mjs 里的既有 TYPE_DEBT 条目,28 errors,未变动)
spec 十个 check:* 全跑 全绿(check:generated 8 项 up to date;liveness / empty-state / react-declaration-parity / skill-examples / variant-docs / strictness-ledger / exported-any / dual-source-exports 逐个 PASS)
仓级 gate check:doc-authoring check:nul-bytes check:error-code-casing check:adr-anchors check:type-check-coverage check:release-notes check:merge-driver check:durability-log-level check:startup-registry-verdict 全 PASS
三个示例 app pnpm validate app-crm exit=0 / app-showcase exit=0 / app-todo exit=0

新增/改动的测试:

  • packages/spec/src/data/query.test.ts — 新 describe「SortNode — closed, and direction carries its translation」7 例,包含处方断言(消息含 `direction` → `order`)、orderdirection 同时出现也拒、经 QueryAST.orderBy 的同门断言,以及一条范围 pin:QuerySchema 顶层仍非 strict。
  • packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts — 头部那句 strip 时代的 pin(「the schema is not .strict(), so direction was 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']——表的另一端)。

顺带项(按裁决要求测量后单独立项,均未认领)

不在范围(裁决硬约束 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

claude added 3 commits August 3, 2026 15:33
…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
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 5:09pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…tnode-strict

# Conflicts:
#	docs/audits/2026-07-unknown-key-strictness-ledger.md
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit f61c8cf Aug 3, 2026
24 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4721-sortnode-strict branch August 3, 2026 17:30
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
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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

一个被静默丢弃的排序键对外部调用方仍然是静默的:direction 该 400 还是继续被丢掉(#4674 第 4 项)

2 participants