Skip to content

feat(spec): strictObject、首批注册类型转换,以及那个立刻抓到两个活 422 的检查(#4001) - #4514

Merged
os-zhuang merged 1 commit into
mainfrom
claude/strict-schema-authz-surface-s8vnok
Aug 1, 2026
Merged

feat(spec): strictObject、首批注册类型转换,以及那个立刻抓到两个活 422 的检查(#4001)#4514
os-zhuang merged 1 commit into
mainfrom
claude/strict-schema-authz-surface-s8vnok

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

三件事,按它们互相逼出来的顺序。

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 不一致。

aliasesguidance 保持手写,且保持可选——它们承载的是判断而不是誊抄。把「策展」当成前提条件,正是这个棘轮走得慢的原因之一。

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。

这个缺陷此前被人工找到过三次permissionposition、然后是本 PR 里的 seed/doc)。

同一件事被人工找到三次,说明缺的是检查,不是搜索。 所以本 PR 把它写成对整个注册表的不变量断言。

它第一次运行就找到了第四和第五个:

hookdatasource#4001 data step 收紧时都没声明信封,两个都在硬 422 那一类,就躺在 main 上。

三轮专门找这个缺陷的人工搜索,从它们旁边走过去了。两个都已修复——现在没有任何注册类型拒收信封

测试把两种严重性分开:

  • 拒收(strict + 未声明)= 活的破坏 → 无条件断言,没有豁免清单
  • 静默剥离(strip 模式)= 保护元数据在往返中丢失 → 单独的债务清单(目前只有 field),且每一项在它的 schema 被收紧那天都会变成拒收

顺带:账本闸门又抓到了自己

strictObject( 不匹配它的 z.object( 计数法,于是第一个转换就让闸门变红。

这不是小事:如果只数 z.object(,那么「这个目录被解决了」和「这个目录被删了」会产生完全相同的数字。计数法现在两个都数。

作者可见的影响

seed / doc 上,一个 schema 从未声明的键现在被拒绝而不是静默丢弃——它本来就在被忽略,所以没有任何正在工作的行为改变

拒绝信息点名 surface、回显键、并给出最近的已声明键(rowsrecordsbodycontent),doc 上还给 path / slug 配了墓碑。

已发布的 JSON Schema 不变:输出模式转换本来就为这些 shape 输出 additionalProperties: false

遗留

validation 是剩下唯一带信封缺口的注册类型——它是 z.lazy() 判别联合,变体各自 .extend() 共享基类,需要逐变体转换而不是一次调用。已记在测试的债务清单和账本里。

验证

  • @objectstack/spec282 文件 / 7115 用例通过tsc --noEmit 干净
  • 新增不变量测试 51 项全绿strictObject 自身 13 项(含 .extend() 继承、.strip() 回退、toJSONSchema 在 lazy proxy 上的 ADR-0089 D3a 隐患)
  • 8 个生成物闸门全部 up-to-date
  • 15 个 check:* 闸门全绿
  • 示例应用的 seed 定义与 doc frontmatter 直接对照新 shape 检查过:只有已声明的键(objectstack validate 全量跑在 CLI 构建完成后补充)

参考

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY


Generated by Claude Code

…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
@vercel

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

Request Review

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 15:05
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit e5e7ee0 Aug 1, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/strict-schema-authz-surface-s8vnok branch August 1, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants