Skip to content

fix(objectql,lint)!: json_schema 规则的 format 关键字真正生效 —— 注册 ajv-formats (#5029) - #5182

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5029-ajv-formats-enforced
Aug 4, 2026
Merged

fix(objectql,lint)!: json_schema 规则的 format 关键字真正生效 —— 注册 ajv-formats (#5029)#5182
os-zhuang merged 2 commits into
mainfrom
claude/issue-5029-ajv-formats-enforced

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5029

按 PM 裁定取正文方案 1:运行时注册 ajv-formats,让 format 真正被强制。

缺陷

ajv 8 不内置 format —— 它在独立的 ajv-formats 包里。运行时的共享实例只有:

const ajv = new Ajv({ allErrors: true, strict: false });

strict: false 下未注册的 format 不是错误:ajv 只打一行日志,然后丢弃该关键字

于是一条 json_schema 规则可以编译成功、在每次写入时运行、强制 typerequired,却对 format 什么都不做 —— 对每一条记录,永远如此。唯一的信号是编译期一行不指名任何规则、也不指名任何对象的 stderr。

这是 #4649 / #4762 的同一族,再往里一层,而且更阴:失败是部分的。规则在开发时会老老实实拒绝 type 错误、required 缺失的负载,读起来像在正常工作,而 format 那一半从未触发过。format 又恰好是 JSON Schema 里最常被伸手去拿的关键字之一(emailuriuuiddatedate-timeipv4),所以这不是什么冷僻角落 —— 它正是 AI 写元数据时第一个会写下的形状。

改动

packages/objectql/src/validation/rule-validator.ts —— 依赖并注册 ajv-formats:

const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);

默认(full) format 集,是个刻意的选择:fast 模式恰好在作者最常用的那几个 format 上拿正确性换速度,而一个「大致匹配」的 format 只是同一个「声明 ≠ 强制」缺陷换了个小一点的洞。

packages/lint/src/validate-rule-compilability.ts(声明过的跨域 devx 最小触碰) —— #4762 的发布门禁与运行时同一个 ajv 环境编译,这是它的设计前提,所以它同步注册同一个插件(同样惰性加载:ajv-formatsrequire('ajv/dist/compile/codegen'),急切 import 等于把 ajv 从后门拖上内核启动路径)。

这一步不是装饰性对齐。ajv-formats 还会注册 formatMinimum / formatMaximum 两个关键字:

环境 { format: 'date', formatMinimum: 42 }
无插件(strict: false) 未知关键字 → 静默忽略 → 编译通过
有插件 走元 schema 校验 → 抛错 formatMinimum value must be ["string"]

也就是说:门禁若不装插件,就会放行一条运行时随后拒绝编译的 schema —— 规则过了审、进了元数据、然后对每一条记录什么都不强制,正是该门禁存在的全部理由。parity 测试现在从运行时源码里读出 addFormats(ajv) 这行注册,两边不可能再无声漂移。

作者面不变。 format 仍是合法、可发布的标准 JSON Schema 关键字;正文的方案 2(发布期拒收 format)经权衡后未采纳 —— 拒收标准 JSON Schema 会把作者推向私有写法。变的只是「声明」现在为真。

⚠️ 行为变更:影响存量数据

changeset 里已响亮写明。要点:

  • formatjson_schema 规则,今天能过的写入,升级后可能被拒 —— 包括 flow、seed、导入和集成写入,不只是 UI。
  • 不做追溯校验:已落库的行不会被重新验证,也没有迁移;但下一次触碰该字段的写入会被检查,包括只是把同一个 JSON blob 原样重发的 PATCH。
  • 升级前的动作:把所有 type: 'json_schema' 且 schema 内任意层级(含 $defs / $refconditionalthen / otherwise)带 format 的规则找出来,逐个核对存量列;若某个 format 只是「愿景」而非事实,先删掉再升级 —— 删除这个关键字从此是个有意义、看得见的动作,而不再是空操作。

仓内扫描结果:没有任何 in-tree 的 json_schema 规则携带 format 关键字(showcase 的 support_config_shape 用的是 enum / minimum;packages/lint 测试里那条 format-bearing fixture 正是「门禁照常放行」的用例)。因此本次改动不需要任何语义抉择,examples 与存量测试全绿。

刻意保留的残留边界

拼错的 format 名仍然被忽略。 format: 'emial'strict: false 下编译通过 —— ajv 打一行 unknown format "emial" ignored 然后丢掉它 —— 运行时与门禁行为一致,所以一个拼写错误依然什么都不强制。

本 PR 不改这个行为,而是在两个包里都用测试钉住它:让它成为一条有记录的、刻意的边界,而不是一个疏漏;将来要翻它,就必须是一次会让这些测试变红的自觉动作。门禁这边尤其不能自作主张 —— 门禁若给出一个运行时并不认同的判决,那就是 strict: true 那个错误换了顶帽子。单独记为 #5178(附 Blocked-by: #5029)。

验证

pnpm --filter @objectstack/objectql --filter @objectstack/lint typecheck   → Done / Done
pnpm --filter @objectstack/objectql test    → Test Files 115 passed | Tests 1826 passed
pnpm --filter @objectstack/lint      test   → Test Files  57 passed | Tests 1184 passed
pnpm --filter @objectstack/example-showcase test → 11 passed | 83 passed
pnpm --filter @objectstack/dogfood   test   → 83 passed (1 skipped) | 483 passed (3 skipped)
pnpm --filter @objectstack/spec check:generated → ✓ All 8 generated artifacts are up to date

新增测试(运行时侧,rule-validator.test.tsjson_schema — format is actually enforced (#5029)):format: 'email'not-an-email / 收 ops@objectstack.ai;uuid / date-time / uri 各一正一反;$ref 与数组元素内的嵌套 format;JSON 字符串值这条路径同样强制;以及拼错 format 名的现状钉子。

门禁侧(validate-rule-compilability.test.ts › parity):从运行时源码读出 addFormats(ajv)(并断言是无参调用 = 默认 full 集);带 format 的 schema 照常放行;拼错的 format 名照常放行;formatMinimum 的合法/非法两例证明插件那一半 parity 确实承重。

启动路径契约(lazy-deps.test.ts / runtime-lazy-deps.test.ts)把 ajv-formats 加进 LAZY_DEPS:判 format 规则时两者都不加载,第一条 json_schema 规则才付出代价。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7


Generated by Claude Code

claude added 2 commits August 4, 2026 07:51
ajv 8 不内置 `format` —— 它在独立的 `ajv-formats` 包里。运行时的共享实例只有
`new Ajv({ allErrors: true, strict: false })`,而 `strict: false` 下未注册的
format 不是错误:ajv 只打一行日志,然后**丢弃该关键字**。

于是一条 `json_schema` 规则可以编译成功、在每次写入时运行、强制 `type` 与
`required`,却对 `format` 什么都不做 —— 对每一条记录,永远如此。唯一的信号是
编译期一行不指名任何规则、任何对象的 stderr。这是 #4649 / #4762 的同一族,再
往里一层,而且更阴:失败是**部分的**,规则在开发时会拒绝错误的 `type` 负载,
读起来像在工作,而 `format` 那一半从未触发。

改动:

- `@objectstack/objectql` 依赖并注册 `ajv-formats`(`addFormats(ajv)`),取
  **默认(full)** format 集 —— `fast` 模式恰好在作者最常用的那几个 format 上
  拿正确性换速度,一个"大致匹配"的 format 只是同一个「声明 ≠ 强制」缺陷换了个
  小一点的洞。
- `@objectstack/lint` 的 #4762 发布门禁与运行时**同一个 ajv 环境**编译,因此同步
  注册同一个插件。这不是装饰性对齐:`ajv-formats` 还会注册
  `formatMinimum` / `formatMaximum`,没有插件时它们是未知关键字(`strict: false`
  ⇒ 静默忽略),门禁会放行一条运行时随后拒绝编译的 schema —— 规则过审却什么也
  不强制,正是该门禁存在的理由。parity 测试现在从运行时源码里读出插件注册,两
  边不可能再无声漂移。

作者面不变:`format` 仍是合法、可发布的标准 JSON Schema 关键字(#5029 正文的
方案 2「发布期拒收 format」经权衡后未采纳)。变的只是声明now为真。

已知残留边界(本 PR 刻意不改、并以测试钉住):拼错的 format 名仍被忽略 ——
`format: 'emial'` 在 `strict: false` 下编译通过、什么都不强制,运行时与门禁一致。
单独记在 #5178Fixes #5029

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@vercel

vercel Bot commented Aug 4, 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 4, 2026 8:04am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/objectql.

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

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A json_schema validation rule's format keyword is silently IGNORED — ajv runs without ajv-formats, so format: 'email' enforces nothing

2 participants