Repository navigation
一个被静默丢弃的排序键对外部调用方仍然是静默的:direction 该 400 还是继续被丢掉(#4674 第 4 项) #4721
Description
Activity
裁决(随 #4001 维护者「必要的全收」裁决,搭 v17 breaking 窗口):方案 (a)+(b) 结合 ——
- (a)
SortNodeSchema.strict()化:未知键静默丢弃正是 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 全仓战役在杀的形状,[P2] A direct engine call silently dropssort/select/skip/populate— declared query contract, zero enforcement #4371 已让顶层拒绝未声明选项键,排序节点不该是缺口;线协议 breaking 搭 v17 major 窗口合法; - (b) 在 strict 拒绝之上,对
direction键给点名order的专门错误消息(外来词汇提示,送达处方)——direction是唯一有已知正确译法的外来键(IReportService词汇),泛化的 "unrecognized key" 不给处方,AI 和外部调用方拿到 400 后仍不知道改成什么。
单独只做 (b) 会留下「顶层 strict、排序节点宽容」的不一致;单独只做 (a) 错失唯一能给处方的键。与 #4001 同批派发。
顺带项(lint 禁对引擎查询选项
as any)由执行 agent 评估树里残余擦除量后决定是否单独立项。
Generated by Claude Code
- (a)
维护者裁决(2026-08-03):方案 A —— 把
SortNodeSchema拎出 blanketopen单独定级,strictObject+aliases,两扇门同一个 change 关⚠️ 先更正本单原裁决(06:25Z)引用的一条前提 —— 它已被 #4001 重测实测推翻:#4371 已让顶层拒绝未声明选项键,排序节点不该是缺口
不成立。实测:#4371 的检查是
packages/objectql/src/engine.ts里手写的rejectUnknownEngineOptionsallowlist,只在顶层迭代Object.entries(bag);而QuerySchema本身不是 strict —— 对照探针QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success === true。但结论反而更该成立,理由换成实测事实:
SortNodeSchema.parse({ field, direction: 'desc' }) → { field, order: 'asc' }静默排反方向。带
limit时返回的是另一批行,且无任何信号。三轴
① 实际业务需求 ——
direction不是拼写错误,是活着的竞争词表:IReportService.orderBy用{field, direction},plugin-auth/objectql-adapter.ts:536已经在手工翻译两者。外部调用方拿到的是「看起来成功的升序数据」—— 静默错数据是对业务最贵的一类错。② 平台长远 ——
query.zod.ts的 blanketopen判给的是过滤表达式(谓词值里流用户数据),而SortNodeSchema是封闭的两键元组{field, order},没有任何用户数据面。按文件整体定级才是那个不精确的东西,不是 strict 化。方案 B(只修 normalizer)是 #4001 战役文档点名的反模式 —— 第六个「只守一扇门的 bespoke 守卫」:
SortNodeSchema.parse经defineStack、data-engine.zod.ts的orderBy: z.array(SortNodeSchema)、以及直接 parse QueryAST 三条路都可达,normalizer 只拦 REST 一条。③ 防 AI 犯错 —— 编辑距离永远够不到
direction(它不是 typo),只有strictObject的aliases: { direction: 'order' }能把处方送到手上。这正是本单原裁决 (b) 想要的「点名order的专门错误」,用战役已有机制实现,不另造守卫。执行约束(硬性)
- 两扇门必须同一个 change 关:
SortNodeSchemastrict 化 且normalizeSortNodes(protocol.ts:1102)点名拒绝direction。只关 schema 会重演 Object-levelworkflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535 object 守卫、feat(spec)!: ObjectSchema 在 parse 路径上收紧,而不只是 create() —— #1535 的奠基例子一直是活的(#4001 批 3a) #4522 才发现的那个门不对称。 - wire 面 breaking → v17 窗口内。
QuerySchema顶层不 strict 这个新发现不在本单扩权 —— 记进 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 战役地图,按批次走。
并入 #4001 首批执行,不单独派 dev。
Generated by Claude Code
- 两扇门必须同一个 change 关:
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorMore actions🔒 认领:PM 循环第 1 轮,按 11:41Z 维护者裁决(方案 A)并入 #4001 首批执行
会话:session_01Ehu85kCXvMcr…→ 完整 IDsession_01Ehu85kbvMcrNTUJjwxvLJ9
分支:claude/issue-4721-sortnode-strict
Worktree:objectstack-issue-4721
域:domain:engine×domain:spec(跨域单认领,文件面全量声明如下)
文件面:packages/spec/src/data/query.zod.ts、packages/metadata-protocol/src/protocol.ts(normalizeSortNodes)、packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts、spec 台账行与生成物继承的实测前提(已在本单 11:41Z 裁决评论就地更正,派发词照此奠基):
QuerySchema不是 strict;#4371 是engine.ts里手写的顶层 allowlist,不递归进orderBy[]。结论不变,依据为实测:SortNodeSchema.parse({field, direction:'desc'})→{field, order:'asc'},静默排反方向。硬约束照裁决:两扇门同一个 change 关;
QuerySchema顶层不 strict 不在本单扩权;v17 窗口内落地(changeset major)。派发前快检:
origin/main=89d2a4e,query.zod.ts的SortNodeSchema仍为裸 strip(实查);protocol.ts今日被 #4841 改过,已写进派发词。与在飞 #4636(metadata-protocol/src/plugin.ts)文件不相交。
Generated by Claude Code
- added a commit that references this issue
on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 CollaboratorMore actions✅ 验收通过 —— PR #4922(draft,等 CI)
对照实际 diff 复核(10 文件),裁决三条硬约束逐条落实:
- 两扇门同一 change:
SortNodeSchema→strictObject+aliases: {direction: 'order'}(query.zod.ts);normalizeSortNodes对良构节点上的direction以400 INVALID_SORT点名拒绝(新invalidSortDirectionKeyError挂既有错误族),测试断言engine 未被调用——拒绝真的发生在门口。 - v17 窗口:spec + metadata-protocol 双 major changeset,带 FROM→TO 与五行行为对照表。
- 不扩权:
QuerySchema顶层原样,且加了范围 pin 测试(nonsenseKey探针保持 true)——后来者想 blanket strict 这个文件必须有自己的裁决,不能顺手带走。
奠基更正落实:PR body 以三条 origin/main 实测探针开篇,明文退休「#4371 同一不变量」论证,正当性只建立在「静默排反方向」实测上——与本单 11:41Z 裁决评论的更正完全一致。
质量亮点:
{field: direction}map 形式(列名恰叫direction)刻意放行 + 回归测试——避掉了镜像 bug;strip 时代的 pin 注释标注退休而非删除;台账把「per-file blanket 与 per-schema 发现冲突时先怀疑 blanket」写成可复用结论。验证:spec 7367 / metadata-protocol 288 / objectql 1742 全过,十道 check 全绿,三示例 validate exit 0,ADR-0087 载荷扫描零
direction:命中。⛔ releases/ 零触碰。衍生:#4918(engine 查询选项
as any擦除,实测 36 处非测试代码,按 PD#10 立项)、#4917(VS Code 片段被 #4001 关掉的键弄失效)——均已实核存在,进下轮分诊。落地排程:等 CI 绿后入队,串行在 #4909(已在队)之后;两 PR 生成物零交集,台账不同 section,预期干净。
Generated by Claude Code
- 两扇门同一 change:
- added 4 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Aug 10, 2026
从 #4674 的第 4 项拆出来——那一项写的是「值得单独决定」,这就是那个单独决定。#4720 修了两个内部站点,没有动这一类。
类修复保护的不是同一批人
#4674 提了两条路:
SortNodeSchema加.strict()),direction变成 400;direction,以一条点名order的消息拒绝它。两条都住在入站
findDatanormalizer 上。而 #4674 自己的「为什么没被抓住」第 2 条已经确认:那条路径对这次的两个站点根本不运行——它们是协议直接调this.engine.find,在自己的守卫内侧而非后面。所以这个类修复不会抓住催生它的那个 bug。这不构成放弃它的理由,但它把要解决的问题换了一个:
tsc—EngineQueryOptions.orderBy是SortNodeSchema[]as any删除)orderBy)direction对内部调用方,真正的教训是「别擦除类型」,而不是「加个运行时闸」。本单只关心第二行。
决定点
SortNodeSchema不是.strict(),所以任何未知键都被丢掉,而不是被标记。给外部调用方发一个{ field: 'updated_at', direction: 'desc' },他们得到的是一个看起来成功的升序响应——带limit时还意味着返回了另一批行。没有任何信号。两个选项的代价不同:
(a)
SortNodeSchema加.strict()—— 覆盖面完整,但这是对已发布线协议的破坏性变更:今天任何在排序节点里多带一个键的客户端会开始收到 400。而且它把所有未知键一视同仁,而direction是唯一一个有已知正确译法的。(b) 专门识别
direction,以点名order的消息拒绝 —— 窄,不破坏无关的多余键,并且送达一条处方而不只是一个拒绝。这正是retiredKey()的做法(通过一次 parse 把修复建议交到调用方手里),区别在于direction从来不是我们这条轴上的键,所以「墓碑」不是准确的词——它是一条外来词汇提示。倾向 (b),但 (a) 的「未知键一律拒绝」在别处是这个仓的既有姿态(#4371 让引擎拒绝未声明的选项键——只是在顶层,没有递归进排序节点),所以这里存在一致性论据,值得一并权衡。
为什么值得做
direction不是一个凭空的拼写错误,它是同一个概念的两套活词汇:SortNodeSchema—{ field, order }— QueryAST /EngineQueryOptions(query.zod.ts:12)IReportService.orderBy—{ field, direction? }— 一份真正不同的契约(report-service.ts:29)plugin-auth/objectql-adapter.ts:536已经显式地在两者间翻译。也就是说这个翻译是已知必需的,只是没有在任何地方被强制;调用方忘了翻译时,结果是一个静默的错误答案而不是一个错误。这就是 ADR-0049 的形状。顺带一提:一个更便宜的内部护栏
如果要收紧内部这一侧,能想到的最小手段是一条 lint 规则,禁止对引擎查询选项做
as any/: any—— 是类型擦除而不是缺少运行时闸让 #4674 溜过去的。这是否值得单开一单,取决于树里这种擦除还有多少。关联:#4674、#4720、#4363、#4371、ADR-0049