Skip to content

feat(lint): refuse a MISSPELLED format in a json_schema validation rule at publish time (#5178) - #5490

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5178-unknown-format-gate
Aug 5, 2026
Merged

feat(lint): refuse a MISSPELLED format in a json_schema validation rule at publish time (#5178)#5490
os-zhuang merged 2 commits into
mainfrom
claude/issue-5178-unknown-format-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5178

前提复核(先证实,再动手)

按 os-dev 规程,先在合并后的 origin/main 上用仓库自己的依赖重跑了 issue 的 repro,确认前提仍然成立:

  • ajv@8.20.0 + ajv-formats@3.0.1,运行时的选项原样:
unknown format "emial" ignored in schema at path "#/properties/e"
validates {e: zzz} = true            ← 拼错的 format:记录被接受
correct-spelling validates {e: zzz} = false   ← 拼对的 format:确实拒收

前提成立,按裁定的方案 1 实施。

做了什么

新增 gating 规则 validateRuleSchemaFormats(packages/lint/src/validate-rule-schema-formats.ts),规则 id validation-rule-json-schema-unknown-format,在 AUTHORING_RULES 里一行接入,os validate / os build / os lint 三个命令自动拾取(接入方式与 validate-rule-compilability 完全一致:tier: 'gating' / input: 'parsed' / commands: ALL / surfaces: CLI_ONLY + RUNTIME_OBJECT_WRITES_P2)。

每条 finding 点名:规则名、对象名、RFC 6901 JSON Pointer、以及最近似的合法名:

objects.account.validations.support_shape.schema#/properties/email/format
  ... names `format: 'emial'` at `#/properties/email/format`, which is not a
  registered format. ... the schema compiles, the rule ships and runs on every
  write, ... and this constraint is enforced on no record, ever. The record is
  ACCEPTED, so nothing downstream reports the gap either.
  hint: Did you mean `format: 'email'`? The registered names are: binary, byte,
  date, date-time, ... — the default `ajv-formats` set, the one
  `rule-validator.ts` registers (#5029).

format 注册集:从同一注册路径枚举,不硬编码

registeredFormatNames() 读的是 createRuntimeAjv() 这个活实例ajv.formats 键集 —— 也就是本仓库门禁为镜像运行时而构造的那台 ajv,其选项与插件注册由既有 parity 测试对着 rule-validator.ts 的源码钉住。硬编码清单会是第三份意见,而且是唯一没人会去更新的那份:插件哪天加了一个名字,清单就开始拒收写入路径真正会执行的 format —— 正是"门禁把好元数据判红,于是被关掉,于是什么都不保护"这条失败模式,只不过枪口对着门禁自己。枚举则让门禁在插件升级时自动跟随,一行都不用改。

顺带回答了派发单里"含 fast 模式差异如有"这一问:实测两种模式注册的名字集合完全相同(26 个),只有实现不同 —— 而即便哪天不再相同,实例仍然知道,清单仍然不会知道。

遍历是 JSON-Schema 感知的,因为 format 不是魔法词

朴素的"收集任意深度的每一个 format 键"会在数据上产生假阳性,而假阳性是本条验收的红线。所以只下潜到 JSON Schema 定义为子 schema 的位置:propertiesitems(2020-12 的 schema 形态与 draft-07 的 tuple 数组两种)、anyOf/allOf/oneOf/prefixItems$defs/definitionsadditionalPropertiespatternPropertiesif/then/elsenotcontainspropertyNamesdependentSchemas、draft-07 dependencies,任意深度;外层还覆盖 conditionalthen/otherwise 子规则(evaluateRule 会递归进去,撞的是同一个 checkJsonSchema)。

反过来,持有任意数据的位置一律不读:default / const / enum / examples 里的 format ajv 从不当 schema 解释,本来就不生效、也从来不是要它生效 —— 报它等于凭一份合法文档凭空发明一个缺陷。非字符串的 format(如 format: 42)交给 validation-rule-json-schema-uncompilable,那条已经用 ajv 自己的话拒收了(data/properties/e/format must be string),这里再报一遍只是噪音。

运行时没动;#4762/#5029 的编译 parity 也没动

方案 2 出局的理由在实现里落成了结构:这条判断叠在编译旁边,不是折进编译里。validateRuleCompilability 依旧在运行时的精确环境里编译、依旧放行拼错的 format(因为在那里它确实编译得过),它的 #5029 钉子

it('a MISSPELLED format name is published by THIS gate — the compile parity, unchanged (#5029/#5178)')

断言一字未改,只加了注释说明拒收现在由谁给出。两条规则回答两个不同的问题 —— "ajv 接受这份 schema 吗?"与"这个 format 关键字会起作用吗?" —— 并共享同一次遍历(walkObjectValidationRules)和同一台 ajv,所以它们不可能对"有哪些规则"或"什么叫已注册"产生分歧。

ajv/ajv-formats 保持惰性,而且本条比邻居更惰:只有当 schema 真的写下了某个 format 名字时才去取注册集,所以一条不含 formatjson_schema 规则两个包都不加载 —— lazy-deps.test.ts 的三层(结构扫描、两个 dist 子进程探针、进程内行为)都钉了这一点。

反向验证:方向是先判定、再运行的

这条不是"把删掉的肢体接回去看诊断变红"那一类。金丝雀方向在写之前就定了:合并进编译门禁会让 publish gate 与写入路径对"什么能编译"产生分歧 —— 而那正是 validate-rule-compilability.ts 存在的唯一理由。所以正确的预期是 旧门禁在同一份 fixture 上保持绿,新规则在它旁边判红:

const stack = objectWith(schemaRule({ type: 'object', properties: { email: { format: 'emial' } } }));
expect(validateRuleCompilability(stack)).toEqual([]);                          // 保持绿 —— 编译 parity 完好
expect(ids(stack)).toEqual([VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT]);           // 新规则判红

这条断言若哪天变红,说明两个判断被合并了、parity 契约已破 —— 测试里写明了这层含义。另有一例钉住"schema 既编译不过、又拼错 format"时两个判决各出一条、各归其主

假阳性红线 + 消费半径清扫

验证

命令 结果
pnpm --filter @objectstack/lint test(合并 main 后) 59 files / 1285 tests passed
pnpm --filter @objectstack/lint typecheck 干净(tsc --noEmit 无输出)
pnpm --filter @objectstack/cli test 80 files / 780 tests passed
pnpm --filter @objectstack/metadata-protocol test 42 files / 388 tests passed
新测试单跑 56 tests passed(其中 26 条为合法 format 逐一绿)
node scripts/check-nul-bytes.mjs OK(5464 files;含 #5460 刚加严的 DEL 扫描),另对本 PR 每个文件做了 grep -naP 自扫
check:doc-authoring / check:published-files / check:type-check-coverage 全绿

packages/lint 是发布包,已附非空 changeset(.changeset/json-schema-rule-unknown-format-gate.md),含 FROM/TO 式的升级说明:门禁若新拒了某条规则,那个 format 名从来就没被执行过,改对拼写或换 pattern 即可,没有任何原本在工作的元数据会改变行为。

🤖 Generated with Claude Code

https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE


Generated by Claude Code

claude added 2 commits August 5, 2026 14:04
…n rule at publish time (#5178)

#5029 registered `ajv-formats` so a `json_schema` rule's `format` is really
enforced. Under `strict: false` an UNRECOGNISED format name stays a non-event:
ajv logs one line at compile time and DROPS the keyword, so `format: 'emial'`
leaves the rule declared, listed, running on every write, enforcing `type` and
`required` — and enforcing nothing for the keyword its author wrote. The record
is ACCEPTED, which is the silent direction.

New gating rule `validateRuleSchemaFormats`
(`validation-rule-json-schema-unknown-format`), one entry in AUTHORING_RULES so
`os validate` / `os build` / `os lint` all run it. Each finding names the rule,
the object, the RFC 6901 JSON Pointer to the keyword and the nearest registered
name.

The vocabulary is enumerated off a live instance of the same ajv the publish
gate builds to mirror the runtime (`registeredFormatNames()`), never a
hardcoded list that would start refusing formats the write path enforces the
day the plugin adds one.

The walk visits only real subschema positions (`properties`, both `items`
forms, `anyOf`/`allOf`/`oneOf`/`prefixItems`, `$defs`/`definitions`,
`additionalProperties`, `patternProperties`, `if`/`then`/`else`, `not`,
`contains`, `propertyNames`, `dependentSchemas`, draft-07 `dependencies`) at
any depth, plus a `conditional`'s `then`/`otherwise` rules. A `format` inside
`default`/`const`/`enum`/`examples` is data ajv never reads as schema, so it is
deliberately not reported; a non-string `format` is left to
`validation-rule-json-schema-uncompilable`.

The runtime is untouched and the #4762/#5029 compile parity is untouched: this
judgement is laid BESIDE the compile, never folded into it.
`validateRuleCompilability` still compiles in the runtime's exact environment
and still publishes a typo'd format, and its #5029 pin passes verbatim. Both
rules share one traversal (`walkObjectValidationRules`) and one ajv
environment, so they cannot disagree about which rules exist or what
"registered" means. `ajv`/`ajv-formats` stay lazy — and this rule is lazier:
a `json_schema` rule naming no `format` loads neither.

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

vercel Bot commented Aug 5, 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 5, 2026 2:14pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 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/permissions/authorization.mdx (via @objectstack/lint)
  • 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

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A MISSPELLED format name in a json_schema validation rule still enforces nothing — format: 'emial' is logged-and-dropped, at runtime AND at publish

2 participants