feat(spec,cli): 把「静默丢弃的 object/field 键」变成作者看得见的告警 (#3786 根因第一步) - #4148
Merged
os-zhuang merged 2 commits intoJul 30, 2026
Conversation
…ly dropped (#3786) `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 author configured something and it 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: a `pii` toggle, an `indexed` toggle and a `cascadeDelete` select that had been rendering in Studio for releases while saving nothing. `lintUnknownAuthoringKeys` reports every such key, naming the path and what to do. Two guidance tables carry the difference between a rename (`formula` → `expression`, `capabilities` → `enable`) and a retirement with no successor (`pii`, `indexed`, `encrypted`). A retirement suppresses the edit-distance fallback on purpose: `pii` is three edits from `min`, and "did you mean min?" reads as advice while being nonsense. Plain typos still get it (`requred` → `required`). 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 flipping them is a migration event for every consumer. This produces the evidence to schedule that on, and costs nobody a migration. Wired pre-parse into both layers that perform the discard, since after the parse there is nothing left to report: `defineStack` (console, once per path, strict and non-strict alike) and `os validate` (non-blocking, and in `--json` rather than computed then discarded). Two failures the tests caught on the way in, both kept as regression cover: the edit-distance fallback firing on retired keys, and a guidance entry for `conditionalRequired` — which is still DECLARED as a retiredKey() tombstone, so the schema already rejects it with its own prescription and the lint never reaches it. Verified against app-todo, app-crm and app-showcase: all clean, no false positives. spec 7110 tests green; the 5 remaining `os serve` e2e failures reproduce on a clean tree (they need a fully built workspace). 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 2 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ate table says so (#3786) Follow-up to the lint's first commit, closing a gap the docs-drift check made visible: `validating-metadata.mdx` is a catalog of what `os validate`/`os build` report, and the new check was not in it. Writing the row surfaced the real omission underneath — the check ran in `validate.ts` only. `defineStack` already covers any config authored through it, whichever command loads it. What it does not cover is a config that SKIPS it — a plain object default-export, or `strict: false` — and on that path `os build` would emit an artifact with the key silently gone. `compile.ts` now runs the same pre-parse report, at the same seam as the ADR-0089 visibility rule beside it. Verified: a plain-object config with `pii` + `indexed` now warns on the build path. Not a parity-test violation either way — `validate-build-gate-parity.test.ts` enforces compile ⊆ validate, and validate already had it. This makes the two agree in the direction the test cannot see. Docs: a row in the two-entry-point table, and a §9 section showing what the finding looks like and why a retired key deliberately gets no "did you mean". 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 12:44
This was referenced Jul 30, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…ection (#3786) (#4178) #4148's lint covered object and field — a sample, not the population: only a handful of authorable types are .strict(); every other type strips an undeclared key with no signal. The walker now covers every lintable collection (16 today), with its coverage DERIVED, not listed: collections from PLURAL_TO_SINGULAR, the judging schema from the type→Zod registry, and lintability read off each schema's own unknown-key posture (strip → lint; strict → skip, the parse is loud; passthrough → skip, nothing is dropped; unions → member-key union). The walker lives in kernel/ beside the schema registry; the comparator and guidance tables stay in data/, frontend-safe. Root export unchanged, so defineStack, os validate and os build pick the coverage up with no change. Mid-review, #4165 graduated `app` to strict — the derivation adapted on its own, and the pinned coverage expectation turned the shrink into a red test until a human confirmed it was a graduation, not a regression. That is the gate working as designed; the strict roster in the test now records it. Verified clean against the three example apps (zero false positives), by mutation in three directions, and 7133 spec tests green on the merged base.
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 排查的根因第一步。前置工作:#4120(已合并)、objectui#3019(已合并)、cloud#925。``
问题
ObjectSchema/FieldSchema刻意不是.strict(),所以它们未声明的键解析通过、然后在写入路径上被丢掉。没有报错、没有告警 —— 作者配了个东西,它就是不在。这正是FieldSchema的 prune 墓碑注释里已经用散文写明的 ADR-0104 失败类,而 #4120 在packages/spec自己内部就查出五处活的实例:一个pii开关、一个indexed开关、一个cascadeDelete下拉 —— 在 Studio 里渲染了好几个版本,一直什么都没存。做法
新增
lintUnknownAuthoringKeys,把每一个「即将被丢弃」的键报出来,带路径和可操作的下一步:两张 guidance 表区分改名(
formula→expression、cascadeDelete→deleteBehavior、capabilities→enable…)和无后继的退役(pii、indexed、encrypted、startingNumber…)。退役项刻意关掉编辑距离兜底 ——pii距min恰好 3 编辑,"did you mean min?" 读起来像真建议,其实是噪音。普通拼写错误照常兜底(requred→required)。表里每一条都是野外实测到的,并有测试断言每个改名目标确实是 schema 声明的键 —— 建议本身不会腐烂成指向不存在的键。它永远不拒绝
把这两个 schema 改成
.strict()是终点(ADR-0049 的 enforce 侧,也是 #4001 已在 flow/permission 上开跑的 tier 程序)。但object和field是协议里被作者写得最多的两个面,直接翻转会开始拒绝今天能解析的元数据 —— 那是一次影响所有消费方的迁移事件,应该基于实证来排期,而不是靠猜。这条规则产出那份实证,同时不让任何人付迁移成本。接入两个执行丢弃的层,都在 pre-parse(解析就是吃掉键的那一步,之后就没东西可报了):
defineStack—— console 告警,按路径去重,strict 与非 strict 模式都跑(键两种模式下都会丢)os validate—— 非阻塞 warning,并且计入--json输出而不是算完就扔(这份文件自己就修过一次同形状的 bug)验证
app-todo/app-crm/app-showcase)全部 clean,零误报packages/spec7110 tests 全绿;spec 九个闸门(含check:liveness)全 PASSos servee2e 失败在干净树上同样复现(需要完整构建的 workspace),我 stash 后逐一核对过,与本 PR 无关写测试时抓到自己两个 bug,都留作回归覆盖:退役键上错误触发了编辑距离兜底;以及给
conditionalRequired写了 guidance —— 而它其实仍被声明为retiredKey()墓碑,schema 自带拒绝与处方,lint 根本够不到它。下一步
这只是根因的第一步。等 lint 在真实数据上跑一段、未声明键收敛到接近零,再按 #4001 的 tier 节奏把
object/field收紧成.strict()—— 那时候就有实证依据了。Generated by Claude Code