Skip to content

fix(spec,rest): 手抄清单排查 — 元数据表单 ↔ Zod 对账闸门 + 五处静默漂移 (#3786) - #4120

Merged
os-zhuang merged 4 commits into
mainfrom
claude/spec-checklist-drift-investigation-ewgd6l
Jul 30, 2026
Merged

fix(spec,rest): 手抄清单排查 — 元数据表单 ↔ Zod 对账闸门 + 五处静默漂移 (#3786)#4120
os-zhuang merged 4 commits into
mainfrom
claude/spec-checklist-drift-investigation-ewgd6l

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

#3786 排查「手抄 spec 清单 + "keep in sync" 注释」模式。framework 侧最大的一窝在 packages/spec 自己的元数据表单里:METADATA_FORM_REGISTRY 的 17 份 defineForm 全部手写 Zod 的键名却从不 import 它 —— 两份清单、一句"保持同步"、零机制。十七份里已有四份漂移,且全部静默

已确认的漂移(全部无报错)

失败原因一致:ObjectSchema / FieldSchema 刻意不是 .strict(),所以表单提供的、schema 未声明的键解析通过后被静默丢弃 —— 正是 field.zod.ts 的 prune 墓碑注释里已用散文写明的 ADR-0104 失败类。

表单 漂移 作者实际看到的
object capabilities 不是 ObjectSchema 的键(应为 enable) 整个 Capabilities 分区 7 个开关存不进去
object 内联字段网格提供了 16 个 FieldSchema 从未声明的键 PII / Encrypted / Indexed / Immutable 等开关存不进去
report aria + performance 已被 #3496 从 ReportSchema 删除,表单照旧渲染 Advanced 两个控件存不进去
hook / action body.memoryMb 缺失 L2 内存上限无法在 Studio 设置(讽刺的是 hook.form.ts 自己的文档注释就写着这个键)
page interfaceConfig.sort 缺失 页面默认排序根本无法设置

object 那 16 个键分两类,每个判定都有据可依:schema 做过的改名(referenceFilterlookupFilterscascadeDeletedeleteBehaviorformulaexpressiondisplayFormatautonumberFormatsummaryType/summaryFieldsummaryOperations),以及已被 prune 为两层皆死的键(indexed #2377,以及 auditTrail/dataQuality/encryptionConfig 家族)。

顺带暴露一个后果:这些漂移已经渗进生成物并被认真翻译了 —— 四个语种的 metadataForms bundle 里躺着一批"存不进去的开关"的译文。本 PR 一并重新生成。

机制

metadata-form-zod-reconciliation.test.ts 遍历每份注册表单,与 getMetadataTypeSchema() 对账。两个方向刻意不对称:

变异测试验证闸门确实会红:重新加回一个被丢弃的键 / 删掉一个已覆盖的键 / 让表单提供一个已登记豁免的键 —— 三种方向分别触发 1、4、4 个失败。

另外两处

验证

packages/spec 7008 tests、rest 505、platform-objects 239 全绿;spec 八个生成物闸门 + check:i18n(9 包)全 PASS。

尚未处理(建议另开)

Closes #3786 的 framework 部分。

claude added 3 commits July 30, 2026 10:06
…nt drifts (#3786)

Every entry in METADATA_FORM_REGISTRY is a hand-written `defineForm` layout
naming keys of a Zod schema it never imports: two descriptions of one key set,
a "keep in sync" comment, and no mechanism. #3786 asked for a sweep of that
shape. Four of the seventeen forms had already drifted, each silently — the
schemas are deliberately not `.strict()`, so a key they do not declare parses
clean and is stripped on the way to storage (the ADR-0104 failure class the
FieldSchema prune tombstone already names in prose).

What an author saw before this change:

  object   `capabilities` is not an ObjectSchema key (it is `enable`), so the
           whole Capabilities section — 7 toggles — saved nothing.
  object   the inline column grid offered 16 keys FieldSchema never declared:
           renames the schema had made (referenceFilter→lookupFilters,
           cascadeDelete→deleteBehavior, formula→expression, displayFormat→
           autonumberFormat, summaryType/summaryField→summaryOperations) and
           keys pruned as dead in both layers (indexed #2377, and the
           auditTrail/dataQuality/encryptionConfig family). PII, Encrypted,
           Indexed and Immutable were switches that saved nothing.
  report   `aria` + `performance` were pruned from ReportSchema by #3496; the
           form kept rendering both.
  hook,    `body.memoryMb` was unauthorable — named in hook.form.ts's own doc
  action   comment, absent from the list below it.
  page     `interfaceConfig.sort` was unauthorable, so a page's default sort
           order could not be set in Studio at all.

The mechanism is metadata-form-zod-reconciliation.test.ts, which walks every
registered form and reconciles it against getMetadataTypeSchema(). The two
directions are deliberately asymmetric: form-only (a control whose value is
discarded) is always a defect and is not ledgerable; zod-only is ledgerable
with a reason, for a deprecated key held back from new authoring or a curated
quick-add subset. Ledger entries are checked for non-vacuity and for still
resolving on both sides, per the #4045 / #4040 discipline.

Verified by mutation: re-adding a stripped key, dropping a covered key, and
offering a ledgered omission each turn the gate red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
…ment whose source was deleted (#3786)

Two follow-ons from the form reconciliation.

The metadata-form translation bundles are DERIVED from METADATA_FORM_REGISTRY,
so correcting the forms moved their key set. Regenerated all four locales:
the dead controls take their translations with them (fields.pii,
fields.encrypted, fields.indexed, fields.filterable, fields.placeholder,
fields.cascadeDelete, fields.summaryType, fields.displayFormat) and the
schema-backed keys arrive (deleteBehavior, lookupFilters, expression,
autonumberFormat, summaryOperations). Worth noting what those bundles were:
four locales of translated labels for switches that saved nothing — the drift
had propagated into a generated artifact and been dutifully translated there.

`ActionAiCategorySchema` carried the same pattern in its terminal state. Its
comment said it mirrored `ToolCategorySchema` in ai/tool.zod and told the next
author to "update both sides" — but #3896 deleted `ToolCategorySchema` along
with the inert `tool.category` key it typed. The instruction had been pointing
at a source that no longer exists, sending any reader looking for a second
side there is none of. The enum is canonical now and says so; no gate, because
there is nothing left to reconcile against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
…he translator dispatch (#3786)

`rest-server.ts` carried `TRANSLATABLE_META_TYPES` — a literal
`Set(['view','action','object','app','dashboard','page'])` under a comment
asking the next author to keep it in step with `translateMetadataDocument`'s
type dispatch in spec. The two agreed today, but nothing made them: adding a
translator in spec would have left the REST boundary serving that type
untranslated, silently, which is the #3786 shape exactly.

`translateMetadataDocument`'s `if` chain becomes a dispatch table, and the
type list is exported as `TRANSLATABLE_METADATA_TYPES` derived from its keys.
rest reads that instead of restating it. One declaration, no gate needed —
the second list is gone rather than checked, which is the better half of the
derive-or-gate prescription.

The lookup stays lazily imported and is now memoised, so `spec/system` remains
off rest's module-init path exactly as before.

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

vercel Bot commented Jul 30, 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 Jul 30, 2026 10:27am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/rest, @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/connect-mcp.mdx (via @objectstack/rest)
  • 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/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/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/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, @objectstack/rest, @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 packages/rest, @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/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @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/platform-objects, @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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Jul 30, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 11:09
@os-zhuang
os-zhuang merged commit 20bc1ec into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-checklist-drift-investigation-ewgd6l branch July 30, 2026 11:09
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…ly dropped (#3786) (#4148)

ObjectSchema and FieldSchema are deliberately not `.strict()`, so a key they do
not declare parses clean and is stripped on the way to storage — no error, no
warning, the setting simply is not there. That is the ADR-0104 failure class the
FieldSchema prune tombstone already describes in prose, and #4120 found five live
instances of it inside this package.

`lintUnknownAuthoringKeys` reports every such key with what to do about it. Two
guidance tables separate a rename (`formula` → `expression`, `capabilities` →
`enable`) from a retirement with no successor (`pii`, `indexed`, `encrypted`); a
retirement suppresses the edit-distance fallback on purpose, since the nearest
key by spelling would read as advice while being noise. Plain typos still get it.

It never rejects. Strict is the destination — the enforce side of ADR-0049, the
tier programme #4001 began on flow and permission — but object and field are the
two most-authored surfaces in the protocol, so the tightening gets scheduled on
what this finds rather than guessed at.

Wired pre-parse into all three layers that perform the discard, since after the
parse there is nothing left to report: `defineStack`, `os validate` (including
`--json`), and `os build`/`os compile` for configs that skip `defineStack`.

Verified clean against app-todo, app-crm and app-showcase — no false positives.
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.

排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移

2 participants