feat(spec): 未知键 lint 下探到嵌套元数据(补上 227 个此前无声的站点) - #4218
Merged
Conversation
…item roots The ledger listed "build the @objectstack/lint unknown-key WARNING layer" as the next step. It had already been built — #3786's lintUnknownAuthoringKeys plus #4167's lintUnknownStackKeys, exported from @objectstack/spec and wired into defineStack(), `os validate` and `os compile` as non-blocking warnings, releases ago. Anyone reading that file for what to do next was being sent to build something that shipped. The ledger now says so. What was actually missing was DEPTH. The walk covered each metadata item's top level plus one hard-coded hop into `object.fields`, leaving 227 strip-mode objects nested below those roots reporting nothing — concentrated exactly where authoring volume is: object 71, view 49, page 24, dashboard 18, agent 16, mapping 14. Those sites were both silently dropping keys AND contributing nothing to the evidence base the v18 close-out is supposed to be scheduled on. The walk now follows the authored value alongside its schema through nested objects, arrays and records. Posture rules are unchanged, so it still never double-reports what the parse handles: strict and passthrough stay silent, only strip reports and is descended through. Unions descend only when the authored value picks a branch unambiguously (a discriminated union whose discriminator the author actually wrote). Otherwise the merged posture applies at that level and the walk stops — guessing a branch would invent findings against a shape nobody wrote. `object.fields` keeps reporting as `field` with its curated guidance, now via an explicit override table instead of a special case, so its own nested sites (`fields.*.options[]`) are covered too. The override keys on the path RELATIVE to the item root, and a record's author-chosen keys deliberately do not join that path — otherwise only the first field would resolve as `field`. Audited the result rather than assuming it, since "our own assets are clean" has been wrong before in this campaign: 43 platform objects + 3 apps + 1 dashboard + 3 pages, plus both example apps, report zero unknown keys. No finding this time. Two silences were checked rather than trusted: dashboard widgets are .strict() (the parse owns that), and a component whose `type` matches no discriminator is deliberately not descended into. Verified: spec suite 7172 green; tsc clean; check:docs, liveness, variant-docs and authorable-surface gates green; content/docs/references unchanged (the hook-body lesson from #4207 — a JSDoc in the wrong place rewrites a page). Refs #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 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
July 30, 2026 16:36
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…#4232) The ledger is the campaign's map — which z.object sites are authorable (the .strict() ratchet target), which are wire, which are deliberately open. Every step reads it to pick the next move, and nothing kept it honest. It went stale twice in one week: a classification verification disproved (hook.zod.ts carried a blanket `authorable (p)` covering a runtime shape, #4207), and a "next step" that had shipped months earlier (the unknown-key warning layer, #4218), which sent the next reader off to rebuild something already in production. A map that drifts is worse than no map, because it gets followed. CHECKS. Site counts (the ledger states its own method — "z.object( occurrences per file" — so every number is verifiable); coverage (every *.zod.ts with sites in a triaged directory needs a row); section totals; and that a row claiming "strict as of" names a file that really contains .strict(). The Class column is deliberately NOT checked. Authorable vs wire vs open is a judgement about who writes the input, and this campaign's rule is verify-before-tightening — encoding it would automate exactly the call that must stay human. The gate protects the arithmetic and coverage so that judgement is always made against current code. Zero-site files are skipped: five of the six the first run flagged hold no z.object at all (enum/token modules). Not a hole — the day one grows its first z.object( it becomes undeclared and the gate says so. FIRST RUN: 11 DRIFTS, in a file being actively edited by the campaign that owns it. Six counts had moved (ui/app.zod.ts 11 → 18, ui/touch.zod.ts 4 → 7, ui/action.zod.ts 8 → 9, ui/view.zod.ts 51 → 50, ui/responsive.zod.ts 6 → 4, automation/flow.zod.ts 12 → 11), two section totals no longer summed, and automation/io-node-config.zod.ts had no row. The app.zod.ts gap was self-inflicted: the app step (#4165) added seven schemas and updated the row's prose without touching its count. Then it worked for real before merging: main landed automation/builtin-node-config.zod.ts (#4045/#4228, eight sites), and merging main turned the gate red on a branch whose own diff touches no schema. Now classified. Every pre-existing count survived that merge unchanged, so the failure was exactly as narrow as it should have been. Verified the gate bites rather than just passes: a moved count fails (plus its section total), a deleted row resurfaces as an undeclared file, and stripping .strict() from approval.zod.ts fails its "strict as of" claim. Refs #4001
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…checked (#4244) Follow-up to the #4177 variant/doc gate, using the signal it produced. That gate passes a variant if the page contains either `<disc>: '<variant>'` or a bare `` `variant` ``, and its header says plainly that the second form is loose: a pass means the page says the word, not that it documents the thing. Six variants were passing on the loose form alone — the gate's own "weak pass" list, and a precise worklist. apps.mdx enumerated nine navigation item types but only documented six. `report`, `action` and `component` shared one sentence — "the spec also defines …" — while the other six each had a section with an example. They now have their own, and the intro no longer implies a second tier of half-supported types. The `action` section earns its place: `actionName` lives inside a nested `actionDef` block, not at the top level, which is exactly the shape a reader guesses wrong. Both it and `component` note which sub-object stays OPEN by design (`actionDef.params`, `component.params`) — the sibling contract owns those, and after #4165 everything around them is strict. knowledge.mdx had a table naming `object` / `file` / `http` and no example of any, so the per-kind keys (`contentFields` vs `prefix` vs `urls`) appeared nowhere. Added one example each. Every example was parsed against the real schema before committing — NavigationItemSchema and KnowledgeSourceKindSchema, six for six. Writing docs from memory is how the region example in #4218 ended up using an `id` key that does not exist. Anchored-form coverage for these two unions: 9/12 → 12/12. Carries an empty changeset. The first push asserted a docs-only PR needs none; Check Changeset disagreed and named the fix — an empty changeset is the repo's way for work that releases nothing, and 72 of them already exist. Refs #4001, #4177
This was referenced Jul 31, 2026
59 tasks
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.
先说一个纠正
账本把「构建
@objectstack/lintunknown-key WARNING 层」列为下一步。它早就建好了——#3786 的lintUnknownAuthoringKeys加 #4167 的lintUnknownStackKeys,已从@objectstack/spec导出,并作为非阻断警告接进defineStack()、os validate、os compile,是好几个版本前的事了。任何照着那份账本找活干的人,都会被指去重建一个已经上线的东西。账本已就此更正。(我在上一个 PR 里刚编辑过这份账本却没发现这点。)
真正缺的是深度,不是存在
遍历此前只覆盖每个元数据项的顶层,外加一处硬编码的
object.fields下探。这留下了 227 个 strip 模式的嵌套对象完全无声——而且集中在授权量最大的地方:objectviewpagedashboardagentmapping这些站点既在静默丢键,又对 v18 strict 收尾所依据的证据基础毫无贡献——而积累证据正是当初建这一层的全部理由。
改动
遍历现在沿着授权值与其 schema 并行下探,穿过嵌套 object、数组、record。姿态规则不变,所以 lint 依然从不重复报告 parse 已经处理的东西:
strict→ 沉默(parse 自己会响)passthrough→ 键合法存活,报了就是撒谎strip→ 报告,并继续向下union 只在授权值明确选定分支时才下探(判别键是作者真写了的)。否则在该层应用合并姿态后停止——瞎猜分支等于对作者从没选过的形状编造 finding。
object.fields仍以field面报告并带它那套精选 guidance,但改为走显式覆写表而非特例分支,因此它自己的嵌套站点(fields.*.options[]等)也一并覆盖了。覆写表按相对项根的路径索引,而 record 的作者自定义键刻意不加入该路径——否则只有第一个 field 能解析为field面。我核查了结果,而不是假设它
这场战役里「我们自己的资产是干净的」已经被证伪过(app 步的
ACCOUNT_APP.defaultOpen就是这么抓到的),所以这次实测:**这次没有 finding。**如实记录,因为并非每次核查都有收获。
两处「沉默」我是查证过而非默认信任的:
dashboard.widgets[]的 element 是.strict()(该 parse 管),而判别值不匹配任何字面量的 component 是刻意不下探的。验证
spec 全套 7172 通过(新增 7 个嵌套下探测试);tsc 干净;
check:docs、check:liveness、check:variant-docs、check:authorable-surface全绿;content/docs/references零变动——这条是 #4207 的教训:一个位置不对的 JSDoc 会改写整张参考页。影响
仍是非阻断警告,与此前完全一致。现有项目会看到更多警告——每一条都是本来就已经在被丢弃的键,现在只是变得可见。
Refs #4001
🤖 Generated with Claude Code
https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
Generated by Claude Code