feat(spec): strictObject、首批注册类型转换,以及那个立刻抓到两个活 422 的检查(#4001) - #4514
Merged
Conversation
…the check that found two live 422s (#4001) Three things, in the order they forced each other. 1. `strictObject` — closing a shape is one call. The #4001 wiring was four parts per schema plus a drift test: a hand-transcribed `const X_KEYS = [...]`, a `strictUnknownKeyError({ knownKeys: X_KEYS, … })` call, the `{ error }` argument, `.strict()`, and an "accepts every declared key" probe to catch the array drifting from the shape it describes. 34 key arrays and 16 probe files, with most of the authorable surface still ahead. The array was never necessary. `knownKeys` feeds one thing — the edit-distance fallback — and the shape object is at the call site. `strictObject` reads the keys from `shape`, which also retires the per-schema probe: a list read from the shape cannot disagree with it. `aliases` and `guidance` stay hand-written and stay OPTIONAL; they carry judgement rather than transcription, and treating curation as a precondition is part of why this ratchet moved slowly. 2. A sharper target than "the five authorable directories". That triage answers "is this authorable?" but not "is this PARSED?" — and after #4410 the second question decides whether a flip enforces anything at all. `BUILTIN_METADATA_TYPE_SCHEMAS` answers both: every entry is author-written and parsed on three paths (`defineStack()`, `/api/v1/meta/types/:type`, the Studio form). Ten had no strictness; `seed` and `doc` are the first two converted. Five of the ten live in `system/`, which the directory triage never covered — the two lenses miss different things, so the ledger now carries both. 3. The check, and the two live bugs it found immediately. `MetadataPlugin`'s loader stamps `_packageId` / `_provenance` on every registered type, so a strict schema that does not declare `MetadataProtectionFields` rejects its own loader's output — a hard 422 on the ADR-0094 overlay path. That defect had been found three times by hand (`permission`, `position`, then `seed`/`doc` here). Finding one thing three times is evidence the check is missing, so this adds it as an invariant over the registry. It found the fourth and fifth on its first run: `hook` and `datasource` had both gone strict in the #4001 data step WITHOUT the envelope, and were sitting on `main` in the hard-422 class. Three prior hand-searches for exactly this defect had walked past them. Both now declare it; no registered type rejects the envelope any more. The test asserts that case unconditionally — no exemption list — and tracks the quieter strip case (`field` only) separately, since each entry there becomes a rejection the day its schema closes. Also: the ledger gate caught its own blind spot again. `strictObject(` did not match its `z.object(` counting method, so the first conversion read as a site disappearing. Counting only `z.object(` would make "this directory got solved" and "this directory got deleted" produce the same number, so the method now counts both. Authoring impact on `seed` / `doc`: a key the schema never declared is rejected instead of silently discarded — it was already ignored, so no working behavior changes. Rejections name the surface, echo the key and suggest the closest declared one (`rows` → `records`, `body` → `content`), with tombstones for `path` / `slug` on `doc`. Published JSON Schema unchanged: output-mode conversion already emitted `additionalProperties: false` for these shapes. `validation` is the remaining registered type with the envelope gap — a `z.lazy()` discriminated union whose variants `.extend()` a shared base, so it needs per-variant conversion rather than one call. Tracked in the test's debt list and the ledger. Verified: spec 282 files / 7115 tests, `tsc --noEmit` clean, all 8 generated artifacts current, all 15 `check:*` gates green. Example-app seed definitions and doc frontmatter checked directly against the new shapes — only declared keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 1, 2026 15:05
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
三件事,按它们互相逼出来的顺序。
1.
strictObject—— 收紧一个 schema 变成一次调用#4001 此前的标准接法是每个 schema 四个零件外加一个防漂移测试:手抄的
const X_KEYS = [...]、strictUnknownKeyError({ knownKeys: X_KEYS, … })调用、{ error }参数、.strict(),再加一个「accepts every declared key」探针来抓那个数组和它描述的 shape 之间的漂移。34 个手抄键数组、16 个探针测试文件,而大部分可授权面还没开始。
那个数组从来就不必要。
knownKeys只喂一件事——编辑距离兜底——而 shape 对象就在调用点上。strictObject直接从shape读,于是探针测试也一并退休:从 shape 读出来的键列表不可能和 shape 不一致。aliases和guidance保持手写,且保持可选——它们承载的是判断而不是誊抄。把「策展」当成前提条件,正是这个棘轮走得慢的原因之一。2. 一个比「5 个可授权目录」更准的靶子
那份三分账回答的是「这是作者写的吗」,但没回答「这会被 parse 吗」——而 #4410 之后,第二个问题才决定一次收紧到底执行了什么。收紧一个没人解析的 schema,等于制造新的「声明但惰性」。
BUILTIN_METADATA_TYPE_SCHEMAS同时回答两个:每一项都是作者写的,且在三条路径上被解析(defineStack()、/api/v1/meta/types/:type、Studio 表单)。10 个完全没有 strict。本 PR 转换头两个:
seed+doc。值得注意其中 5 个住在
system/——目录三分账压根没覆盖那里。两种视角漏的东西不一样,所以账本现在两个都记。3. 那个检查,和它立刻找到的两个活 bug
MetadataPlugin的加载器给每个注册类型盖_packageId/_provenance戳。所以一个 strict 但没声明MetadataProtectionFields的 schema,会拒收自己 loader 的输出——ADR-0094 overlay 路径上的硬 422。这个缺陷此前被人工找到过三次(
permission、position、然后是本 PR 里的seed/doc)。同一件事被人工找到三次,说明缺的是检查,不是搜索。 所以本 PR 把它写成对整个注册表的不变量断言。
它第一次运行就找到了第四和第五个:
三轮专门找这个缺陷的人工搜索,从它们旁边走过去了。两个都已修复——现在没有任何注册类型拒收信封。
测试把两种严重性分开:
field),且每一项在它的 schema 被收紧那天都会变成拒收顺带:账本闸门又抓到了自己
strictObject(不匹配它的z.object(计数法,于是第一个转换就让闸门变红。这不是小事:如果只数
z.object(,那么「这个目录被解决了」和「这个目录被删了」会产生完全相同的数字。计数法现在两个都数。作者可见的影响
seed/doc上,一个 schema 从未声明的键现在被拒绝而不是静默丢弃——它本来就在被忽略,所以没有任何正在工作的行为改变。拒绝信息点名 surface、回显键、并给出最近的已声明键(
rows→records、body→content),doc上还给path/slug配了墓碑。已发布的 JSON Schema 不变:输出模式转换本来就为这些 shape 输出
additionalProperties: false。遗留
validation是剩下唯一带信封缺口的注册类型——它是z.lazy()判别联合,变体各自.extend()共享基类,需要逐变体转换而不是一次调用。已记在测试的债务清单和账本里。验证
@objectstack/spec:282 文件 / 7115 用例通过,tsc --noEmit干净strictObject自身 13 项(含.extend()继承、.strip()回退、toJSONSchema在 lazy proxy 上的 ADR-0089 D3a 隐患)check:*闸门全绿objectstack validate全量跑在 CLI 构建完成后补充)参考
🤖 Generated with Claude Code
https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
Generated by Claude Code