发现于 #5500(flow.test.ts fixture 形状普查)第 4 类的契约测量。按 Prime Directive #10 单独记录,未在那个 PR 里顺手改。
事实
packages/spec/src/data/filter.zod.ts:92 起:
export const ComparisonOperatorSchema = lazySchema(() => z.object({
$gt: z.union([z.number(), z.date(), FieldReferenceSchema]).optional(),
$gte: z.union([z.number(), z.date(), FieldReferenceSchema]).optional(),
$lt: z.union([z.number(), z.date(), FieldReferenceSchema]).optional(),
$lte: z.union([z.number(), z.date(), FieldReferenceSchema]).optional(),
}));
即:声明里没有 string。但平台自己在这四个槽位里传的恰恰只有字符串:
日期宏解析器只能产出字符串。packages/core/src/utils/filter-tokens.ts:18-19 的规范示例就是这个形状:
{ close_date: { $gte: '{current_year_start}' } } → { close_date: { $gte: '2026-01-01' } }。
实现侧 :303case 'now': return now.toISOString();、:319addUnits(...).toISOString()、asYmd(...) 全部返回 string,没有任何分支返回 number 或 Date。
packages/spec/src/data/date-macros.zod.ts:34-35 也明说「the DRIVER only ever sees ISO date / timestamp strings」。
一方调用方一律传 .toISOString():
packages/objectql/src/lifecycle/lifecycle-service.ts:991-992 — const keepCutoff = new Date(...).toISOString(); → { created_at: { $lt: keepCutoff } }- 同文件
:1054-1055 — tCutoff 同样是 .toISOString() packages/plugins/plugin-email/src/outbox-sweep.ts:155,160 — const cutoff = new Date(now - minAgeMs).toISOString(); → { created_at: { $lt: cutoff } }
为什么今天没人撞到
ComparisonOperatorSchema没有任何运行期消费者。仓库内 grep 只命中它自己的单测(filter.zod 的定义、data/filter.test.ts)和导出桶,objectql 的读写路径、各 driver、flow 的 CRUD executor 都不拿它校验 where。flow 节点侧的 filter 另有声明(z.record(z.string(), z.unknown()),builtin-node-config.zod.ts:219),值一律 unknown,所以也到不了这里。
也就是说:这是一处 declared 与 practiced 不一致,但 declared 那侧不设防,所以不产生运行期失败 —— 按 finding 记录,严重度留给分诊轮。
危害面
它仍然是一份发布出去的契约:作者(尤其 AI 作者)读 $lt: number | date | FieldReference 会得出「日期窗口要传 Date 对象或时间戳数字」的结论,而平台全部通路实际产出并接受的是 ISO 字符串。#5500 的分诊注记正是卡在这个问题上(「插值后的 ISO 日期串走 record-form where 是否是我们要教的形状」)。
可选方向(未拍板)
- A. 补
string 进联合(并在 .describe() 里点明 ISO date / date-time 字符串是规范形态)。与 filter-tokens.ts 的自带示例、与全部一方调用方一致,改动最小。代价:string 很宽,拿不到格式校验。 - B. 补一个受约束的字符串(如 ISO date / date-time 的 refine),表达力更准,但要确认 driver 侧对 date-only 与 full-timestamp 的处理一致。
- C. 维持声明、改调用方传
Date。与日期宏解析器的产物冲突(它只能给字符串),等于要求每个消费点再把宏解析结果转回 Date,和 ADR 里「driver 只看 ISO 串」的既定方向相反。
倾向 A 或 B —— C 与既有 ADR 方向相反,不宜。
相关
#5500(测量来源)、#3810(filter 槽位的两套 {…} 方言归属)
发现于 #5500(flow.test.ts fixture 形状普查)第 4 类的契约测量。按 Prime Directive #10 单独记录,未在那个 PR 里顺手改。
事实
packages/spec/src/data/filter.zod.ts:92起:即:声明里没有
string。但平台自己在这四个槽位里传的恰恰只有字符串:日期宏解析器只能产出字符串。
packages/core/src/utils/filter-tokens.ts:18-19的规范示例就是这个形状:{ close_date: { $gte: '{current_year_start}' } }→{ close_date: { $gte: '2026-01-01' } }。实现侧
:303case 'now': return now.toISOString();、:319addUnits(...).toISOString()、asYmd(...)全部返回 string,没有任何分支返回number或Date。packages/spec/src/data/date-macros.zod.ts:34-35也明说「the DRIVER only ever sees ISO date / timestamp strings」。一方调用方一律传
.toISOString():packages/objectql/src/lifecycle/lifecycle-service.ts:991-992—const keepCutoff = new Date(...).toISOString();→{ created_at: { $lt: keepCutoff } }:1054-1055—tCutoff同样是.toISOString()packages/plugins/plugin-email/src/outbox-sweep.ts:155,160—const cutoff = new Date(now - minAgeMs).toISOString();→{ created_at: { $lt: cutoff } }为什么今天没人撞到
ComparisonOperatorSchema没有任何运行期消费者。仓库内grep只命中它自己的单测(filter.zod的定义、data/filter.test.ts)和导出桶,objectql 的读写路径、各 driver、flow 的 CRUD executor 都不拿它校验where。flow 节点侧的filter另有声明(z.record(z.string(), z.unknown()),builtin-node-config.zod.ts:219),值一律unknown,所以也到不了这里。也就是说:这是一处 declared 与 practiced 不一致,但 declared 那侧不设防,所以不产生运行期失败 —— 按 finding 记录,严重度留给分诊轮。
危害面
它仍然是一份发布出去的契约:作者(尤其 AI 作者)读
$lt: number | date | FieldReference会得出「日期窗口要传 Date 对象或时间戳数字」的结论,而平台全部通路实际产出并接受的是 ISO 字符串。#5500 的分诊注记正是卡在这个问题上(「插值后的 ISO 日期串走 record-form where 是否是我们要教的形状」)。可选方向(未拍板)
string进联合(并在.describe()里点明 ISO date / date-time 字符串是规范形态)。与filter-tokens.ts的自带示例、与全部一方调用方一致,改动最小。代价:string很宽,拿不到格式校验。Date。与日期宏解析器的产物冲突(它只能给字符串),等于要求每个消费点再把宏解析结果转回Date,和 ADR 里「driver 只看 ISO 串」的既定方向相反。倾向 A 或 B —— C 与既有 ADR 方向相反,不宜。
相关
#5500(测量来源)、#3810(filter 槽位的两套
{…}方言归属)