fix(spec,rest): 手抄清单排查 — 元数据表单 ↔ Zod 对账闸门 + 五处静默漂移 (#3786) - #4120
Merged
os-zhuang merged 4 commits intoJul 30, 2026
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
os-zhuang
marked this pull request as ready for review
July 30, 2026 11:09
This was referenced Jul 30, 2026
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.
This was referenced Jul 30, 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.
按 #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 失败类。objectcapabilities不是 ObjectSchema 的键(应为enable)objectFieldSchema从未声明的键reportaria+performance已被 #3496 从 ReportSchema 删除,表单照旧渲染hook/actionbody.memoryMb缺失hook.form.ts自己的文档注释就写着这个键)pageinterfaceConfig.sort缺失object那 16 个键分两类,每个判定都有据可依:schema 做过的改名(referenceFilter→lookupFilters、cascadeDelete→deleteBehavior、formula→expression、displayFormat→autonumberFormat、summaryType/summaryField→summaryOperations),以及已被 prune 为两层皆死的键(indexed#2377,以及auditTrail/dataQuality/encryptionConfig家族)。顺带暴露一个后果:这些漂移已经渗进生成物并被认真翻译了 —— 四个语种的 metadataForms bundle 里躺着一批"存不进去的开关"的译文。本 PR 一并重新生成。
机制
metadata-form-zod-reconciliation.test.ts遍历每份注册表单,与getMetadataTypeSchema()对账。两个方向刻意不对称:configSchemaand the keys its executor actually reads are still unreconciled —notifyhonourscfg.source, which no schema declares #4045 / fix(automation,lint,spec): validate the expression slots a node'sconfigSchemadeclares (#4027) #4040 的规矩校验非空、且两侧仍能解析。变异测试验证闸门确实会红:重新加回一个被丢弃的键 / 删掉一个已覆盖的键 / 让表单提供一个已登记豁免的键 —— 三种方向分别触发 1、4、4 个失败。
另外两处
ActionAiCategorySchema的注释说它镜像ai/tool.zod的ToolCategorySchema、要求"两侧同时更新" —— 但POST /data/sharing/rules绕过SharingRuleSchema:criteria缺失或拼错静默变成"共享该对象全部记录",与 ADR-0049 "never seeded as a permissive match-all" 直接冲突 #3896 已经把ToolCategorySchema连同它所类型化的惰性tool.category一起删了。该指令指向一个不存在的源,读者会去找一个并不存在的第二侧。这是该模式的终末形态:注释腐烂得比清单还安静。改为声明其唯一权威地位(无需闸门 —— 已无对账对象)。rest-server.ts的TRANSLATABLE_META_TYPES手抄集合改为派生:translateMetadataDocument的if链改成 dispatch table,导出TRANSLATABLE_METADATA_TYPES。第二份清单是被消灭而不是被检查 —— derive-or-gate 里更好的那一半。验证
packages/spec7008 tests、rest505、platform-objects239 全绿;spec 八个生成物闸门 +check:i18n(9 包)全 PASS。尚未处理(建议另开)
AUDIT_TIMELINE_FIELDS(objectql/validation/rule-validator.ts)—— 注释"Kept in sync with registry.ts",目前内容一致但无闸门。派生需要把registry.ts的四段 if 链重构成数据表,动的是热路径,值得单独一个 PR 带自己的测试。顺带一提:这份 4 元审计字段清单跨两仓至少有四份拷贝(见下)。objectui(排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移 #3786 点名的头号嫌疑)已扫描完毕 → 排查「手抄 spec 清单 + "keep in sync" 注释」模式:objectui 侧六处,目前全部一致但零闸门(framework#3786) objectui#3017。结论:六处候选,目前全部内容一致,没有查到活的漂移,但同样零闸门。其中最扎眼的是两处抄的正是「spec 已经为此导出的常量」,可以直接删掉第二份清单:
ActionDefaultInspector.tsx的LOCATIONS← spec 已导出ACTION_LOCATIONS;FlowReferenceField.tsx的KIND_TO_RECORD_LOOKUP← spec 的APPROVER_VALUE_SOURCES就是为了让 objectui 别再自己推导而专门发布的(其文档注释记载了 审批节点「处理人 Value」应改为记录 lookup(现查 metadata 端点→只能手填);附 approver value 语义核实 + queue 未实现 #3508 那次翻车:设计器自带的第一份数据契约抄错了,把目录类 kind 全接到元数据注册表上,候选返回空、控件退化成自由文本框,于是sales_manager被打进只接受三个值的字段)。spec 侧用satisfies保证了新增ApproverType不声明 binding 即编译错误 —— 但这层保证一步都没传导到 objectui。Closes #3786 的 framework 部分。