Skip to content

feat(spec): 未知键 lint 下探到嵌套元数据(补上 227 个此前无声的站点) - #4218

Merged
os-zhuang merged 1 commit into
mainfrom
claude/unknown-key-stripping-strictness-h3a3zt
Jul 30, 2026
Merged

feat(spec): 未知键 lint 下探到嵌套元数据(补上 227 个此前无声的站点)#4218
os-zhuang merged 1 commit into
mainfrom
claude/unknown-key-stripping-strictness-h3a3zt

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

先说一个纠正

账本把「构建 @objectstack/lint unknown-key WARNING 层」列为下一步。它早就建好了——#3786lintUnknownAuthoringKeys#4167lintUnknownStackKeys,已从 @objectstack/spec 导出,并作为非阻断警告接进 defineStack()os validateos compile,是好几个版本前的事了。

任何照着那份账本找活干的人,都会被指去重建一个已经上线的东西。账本已就此更正。(我在上一个 PR 里刚编辑过这份账本却没发现这点。)

真正缺的是深度,不是存在

遍历此前只覆盖每个元数据项的顶层,外加一处硬编码的 object.fields 下探。这留下了 227 个 strip 模式的嵌套对象完全无声——而且集中在授权量最大的地方:

类型 未覆盖嵌套站点
object 71
view 49
page 24
dashboard 18
agent 16
mapping 14

这些站点既在静默丢键,又对 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 就是这么抓到的),所以这次实测:

资产 结果
43 个平台 object + 3 app + 1 dashboard + 3 page 零未知键
app-showcase(23 object / 20 page / 4 dataset / 3 dashboard) 零未知键
app-crm(6 object / 1 page / 1 dataset / 1 dashboard / 1 action) 零未知键

**这次没有 finding。**如实记录,因为并非每次核查都有收获。

两处「沉默」我是查证过而非默认信任的:dashboard.widgets[] 的 element 是 .strict()(该 parse 管),而判别值不匹配任何字面量的 component 是刻意不下探的。

验证

spec 全套 7172 通过(新增 7 个嵌套下探测试);tsc 干净;check:docscheck:livenesscheck:variant-docscheck:authorable-surface 全绿;content/docs/references 零变动——这条是 #4207 的教训:一个位置不对的 JSDoc 会改写整张参考页。

影响

仍是非阻断警告,与此前完全一致。现有项目会看到更多警告——每一条都是本来就已经在被丢弃的键,现在只是变得可见。

Refs #4001

🤖 Generated with Claude Code

https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4


Generated by Claude Code

…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
@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 4:19pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

106 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 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/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 July 30, 2026 16:36
@os-zhuang
os-zhuang merged commit a329cca into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/unknown-key-stripping-strictness-h3a3zt branch July 30, 2026 16:37
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants