Skip to content

feat(spec): 未声明键 lint 从 2 个面扩到全部可 lint 元数据集合 (#3786) - #4178

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

feat(spec): 未声明键 lint 从 2 个面扩到全部可 lint 元数据集合 (#3786)#4178
os-zhuang merged 3 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

#4148(已合并)。当时 lint 只盖 object / field —— 因为 #4120 是在那两个面上抓到真漂移的。合并后量了一下:那两个是样本,不是总体

测量结果

25 个可授权元数据类型里,.strict() 的只有一小撮(#4001 Tier-A 的 flow/permission/position/tool,以及本 PR 评审期间刚毕业的 app,#4165)。其余全部对未声明键的行为与 field 一模一样 —— 解析通过、值被丢、零信号。作者在 pageagentdashboard 上写错键,和 #4120 之前毫无区别,而 lint 看不见。

另外,#4148 里我说「先跑一段收集实证、再定 .strict() 收紧的排期」—— 但只盖 2 个面时,那句话缺乏支撑。

做法:覆盖面是派生的,不是列出来的

遍历器现在走全部可 lint 集合(当前 16 个:object, page, dashboard, report, dataset, action, job, agent, skill, hook, mapping, datasource, view, email_template, doc, book),且三个输入全部派生:

问题 来源
有哪些集合 PLURAL_TO_SINGULAR —— normalizer 用的同一张边界映射,新集合注册进去的那一刻就被覆盖
每个集合由哪个 schema 裁决 getMetadataTypeSchema —— 类型→Zod 的唯一注册表
lint 是否有意义 读 schema 自己的未知键姿态(见下)

在终结手抄清单的工具里再手写第三份「lint 覆盖哪些类型」的清单,就是 #3786 本身。

姿态规则保证 lint 永远不会与 parse 唱反调:

评审期间发生的事,恰好证明了设计

CI 第一轮红了:#4165 在本 PR in-flight 时把 app 收成 .strict()(并行 agent 在推进 tier 程序)。派生自动适应 —— app 掉出可 lint 集合,因为它的 parse 已经响亮 —— 而逐名钉死的覆盖断言把这次收缩变成了红测试而非无声事件。这正是钉名单存在的意义:覆盖缩水时,由人确认это是「毕业」而不是回归,然后把名字从覆盖名单挪进 strict 名单。已随 merge main 一并处理,changeset 和 docs 的措辞也改为描述规则而非快照。

结构:遍历器搬进 kernel/

盖全部类型意味着 import 全部 schema,而 lint 原来住的 /data 子路径被前端 bundle 消费(objectui 从那里拿 REFERENCE_VALUE_TYPES)—— 把 schema 宇宙拖进前端 chunk 是真实回归。所以:遍历器 → kernel/metadata-authoring-lint.ts(紧挨它读的 schema 注册表);比对器、guidance 表、finding 类型留在 data/(前端安全)。包根导出不变,defineStack / os validate / os build 零改动直接获得更宽覆盖。

唯一的 import 位置变化:lintUnknownAuthoringKeys 不再从 /data 子路径导出(root / /kernel 可用;changeset 里写了 FROM→TO)。

验证

  • 三个示例 app 全 clean,零误报(showcase:28 pages / 29 flows / 11 actions ……)。第一次跑 showcase 炸了 —— 是 stale dist 缺 feat(spec): FormSection.pane — explicit split-pane placement (objectui#2153 follow-up) #4160 刚加的 FormSection.pane(AGENTS.md 里写明的那个 check:api-surface 陷阱),重建即消失,与本改动无关
  • 变异测试三方向全红:砍掉 union 处理(view 掉出覆盖,被点名断言抓住)、反转 strict 过滤(flow 开始被重复报告)、悄悄跳过一个集合(pages)—— 恢复后全绿
  • 覆盖断言:派生结果 ≥15 且逐名点名(含 union 代表 view),并且每个可 lint 集合都真的对 bogus 键产出 finding —— 「覆盖」是被证明的,不是被声称的
  • merge main 后 spec 7133 tests / 274 files 全绿;六个生成物闸门全 PASS;端到端确认 pages.home.visibleWhennagents.helper.temprature 都被报出

这一步之后

listLintableAuthoringCollections 作为数据导出 —— #4001 的 strict tier 排期现在有了真实证据基座,而且每次毕业都会以「覆盖断言变红 → 挪名单」的形式被显式记录。

…ection (#3786)

#4148's lint covered object and field — the two surfaces #4120 caught real
drift on. A measurement showed those two were a sample, not the population: of
the authorable metadata types, only four are .strict() (flow / permission /
position / tool, the #4001 Tier-A set); the other eighteen strip an undeclared
key exactly the way field did. A misspelled key on a page, an agent or a
dashboard got the same parse-clean-value-gone silence, with no lint watching.

The walker now covers every metadata collection — 17 today — with its coverage
DERIVED, not listed: collections from PLURAL_TO_SINGULAR (the same boundary map
the normalizer uses), the judging schema from the canonical type→Zod registry,
and lintability read off each schema's own unknown-key posture (strip → lint;
strict → skip, the parse is already loud; passthrough → skip, nothing is
dropped; unions → member-key union, lintable only with a stripping member and
no passthrough member). A third hand-written coverage list would have been the
#3786 shape all over again, inside the tool built to end it.

Structure: the walker moves to kernel/metadata-authoring-lint.ts, beside the
schema registry it reads — covering every type means importing every schema,
and the /data subpath it used to live in is consumed by frontend bundles
(objectui reads REFERENCE_VALUE_TYPES from it); the comparator, guidance tables
and finding shape stay in data/authoring-key-lint.ts, frontend-safe. The root
export is unchanged, so defineStack, os validate and os build pick the wider
coverage up with no code change of their own.

listLintableAuthoringCollections is exported so the coverage test can assert
the derivation has not quietly shrunk — `view` is pinned by name there, because
it is the union representative: a regression dropping unions would shrink
coverage without failing the count.

Verified clean against app-todo, app-crm and app-showcase (zero false
positives; the first showcase run failed on a stale dist missing #4160's
FormSection.pane — the documented check:api-surface caveat, gone on rebuild).
Verified by mutation: dropping union handling, inverting the strict filter, and
skipping a collection each turn the tests red. spec 7108 tests green, all six
generated-artifact gates PASS.

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 2:14pm

Request Review

@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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 30, 2026
claude added 2 commits July 30, 2026 14:10
…ge gate did its job

Merge origin/main and record the first live graduation: #4165 made the app
shell strict while this PR was in CI. The derivation adapted on its own — app
fell out of the lintable set because its parse is now loud — and the pinned
expectation list is what turned that shrink into a red test instead of a silent
one. That is the exact scenario the pin exists for: a human confirms the shrink
is a graduation, not a regression, and moves the name from the covered list to
the strict list.

Changeset and docs updated to stop hardcoding the count and the strict roster —
both now describe the rule (the strict bucket grows as tiers graduate) rather
than a snapshot of it.

7133 tests green on the merged base; all six gates PASS; the three example apps
stay clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
@os-zhuang os-zhuang changed the title feat(spec): 未声明键 lint 从 2 个面扩到全部 17 个元数据集合 (#3786) feat(spec): 未声明键 lint 从 2 个面扩到全部可 lint 元数据集合 (#3786) Jul 30, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 14:39
@os-zhuang
os-zhuang merged commit f6472d7 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-checklist-drift-investigation-ewgd6l branch July 30, 2026 14:39
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
#4178 landed while this was in review and refactored the unknown-authoring-key
rule from a two-surface check (object/field) into a walker that derives its
coverage from `PLURAL_TO_SINGULAR` and reads each schema's own unknown-key
posture. This rebases the #4167 contribution onto that architecture instead of
carrying the version it replaced.

What changes vs. the pre-merge shape:

- `lintUnknownAuthoringKeys` keeps its single-argument signature. The top-level
  pass is a separate export, `lintUnknownStackKeys(rawStack, stackSchema)`.
- It reuses the walker's own `keyPosture`, so it lints only while the stack
  schema STRIPS unknown keys and goes quiet if that schema is ever made strict —
  the lint can never become a second, disagreeing voice next to the parse.
- `STACK_KEY_GUIDANCE` stays in `data/authoring-key-lint.ts` beside the other
  two curated tables, held to the same non-rotting discipline.

The two passes are complementary, not redundant: the walker iterates metadata
COLLECTIONS, so a stack whose only mistake is at the envelope level — no
objects, no pages, nothing to iterate — walks clean and reports nothing. A test
pins exactly that.

Verified end to end after the rebuild: `defineStack` warns at config-load time,
and `os validate` / `os build` report `stack.storage` with its prescription plus
`stack.datasource` with its edit-distance suggestion, alongside the field-level
findings the walker already produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
The top-level coverage moved to `lintUnknownStackKeys` when this was rebuilt on
#4178's walker; the comment still pointed at `lintUnknownAuthoringKeys`, which
no longer does that job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…, and report undeclared top-level keys (#4167) (#4189)

* fix(spec,cli): config.storage was never a stack key — stop reading it, and report undeclared top-level keys (#4167)

`os serve` read `config.storage` and forwarded it to StorageServicePlugin. It
could almost never arrive: `ObjectStackDefinitionSchema` does not declare
`storage` and is not `.strict()`, so `defineStack` — which every documented
authoring path and every compiled artifact goes through — strips the key before
serve runs. The one combination that reached the branch (bare-object config on
the config-boot path) then carried the `driver`/`root` spelling the plugin does
not read either.

So one authoring key worked on a single unreachable-in-practice path and
vanished silently everywhere else. A host writing `storage: { driver: 's3', … }`
believed it had configured S3 and got local disk.

- serve no longer reads it. `resolveStorageCapabilityArg` takes only the env
  root, and the production warning names the two channels that work
  (OS_STORAGE_*, Setup → Settings) instead of advertising the one that does not.
- `lintUnknownAuthoringKeys` now covers TOP-LEVEL stack keys, not just object
  and field keys. `storage` gets a prescriptive entry naming both channels and
  why a stack definition is the wrong home for a credential — it would commit it
  to git and to any published artifact. An ordinary misspelling still gets the
  edit-distance suggestion (`datasource` → `datasources`), and the rule now runs
  on a stack with no `objects` at all, which previously exited early.
- `os migrate files-to-references` shares the resolver. It built the same dead
  `{ driver: 'local', root }`, so its adapter used `./storage` while the server
  writes under `.objectstack/data/uploads` since #4096 — and that command
  reconciles what records claim against what storage holds, so a disagreeing
  root reconciled against the wrong tree.

`lintUnknownAuthoringKeys(rawStack)` becomes `(rawStack, stackSchema)`. Required
rather than optional so a caller that forgets it fails to compile instead of
silently losing the check — the exact failure this rule reports. Injected rather
than imported because stack.zod.ts imports this module and importing back would
close a cycle.

Verified end to end: authoring `storage:` through defineStack warns at load
("defineStack: stack.storage: 'storage' is not a declared stack key…"), and
`os compile` reports it for configs that skip defineStack. spec 273 files /
7150 tests, cli 89 files / 916 tests, root `pnpm lint` clean.

Nothing that worked is being removed: `storage` was never in the schema, is
undocumented, and has no consumer in objectstack-ai/cloud (checked directly).

Closes #4167

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

* docs(validating-metadata): the undeclared-key rule covers top-level stack keys too (#4167)

The one page that documents the #3786 rule described it as object/field only,
which #4167 made inaccurate: `lintUnknownAuthoringKeys` now also reports keys
`ObjectStackDefinitionSchema` does not declare.

- Section title and intro name all three schemas.
- The gate table row becomes "Undeclared stack/object/field keys".
- Adds the top-level case with both guidance kinds, using `storage` as the
  worked example — it is the one where the silence is easiest to miss, because
  the key reads as configuration that took effect, and points at the two
  channels that do configure storage (OS_STORAGE_* and the Settings UI, which is
  also where credentials belong — a stack definition is committed to git and
  compiled into any published artifact).

Docs-only; found by acting on the docs-drift advisory rather than waving it
through. `pnpm check:doc-authoring` clean.

Refs #4167

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

* chore(spec): record STACK_KEY_GUIDANCE in the API-surface snapshot (#4167)

`check:api-surface` failed on the #4167 commit: exporting `STACK_KEY_GUIDANCE`
adds to `@objectstack/spec`'s public surface, and the generated snapshot is the
gate's baseline.

    ./data
      + STACK_KEY_GUIDANCE (const)
    @objectstack/spec public API changed: 0 breaking (removed/narrowed), 1 added.

Regenerated with the command the gate names. The diff is one line — the export
is additive, alongside the FIELD_KEY_GUIDANCE / OBJECT_KEY_GUIDANCE entries it
sits with, and nothing was removed or narrowed.

Also ran the rest of the gates CI runs, so this does not take another round:
doc-authoring, role-word, org-identifier, authz-resolver, route-envelope,
error-code-casing, wildcard-fallthrough, release-notes, node-version, plus the
spec package's authorable-surface, spec-changes, upgrade-guide, liveness and
docs checks — all green.

Refs #4167

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

* refactor(spec): rebuild the top-level stack-key lint on the #4178 walker

#4178 landed while this was in review and refactored the unknown-authoring-key
rule from a two-surface check (object/field) into a walker that derives its
coverage from `PLURAL_TO_SINGULAR` and reads each schema's own unknown-key
posture. This rebases the #4167 contribution onto that architecture instead of
carrying the version it replaced.

What changes vs. the pre-merge shape:

- `lintUnknownAuthoringKeys` keeps its single-argument signature. The top-level
  pass is a separate export, `lintUnknownStackKeys(rawStack, stackSchema)`.
- It reuses the walker's own `keyPosture`, so it lints only while the stack
  schema STRIPS unknown keys and goes quiet if that schema is ever made strict —
  the lint can never become a second, disagreeing voice next to the parse.
- `STACK_KEY_GUIDANCE` stays in `data/authoring-key-lint.ts` beside the other
  two curated tables, held to the same non-rotting discipline.

The two passes are complementary, not redundant: the walker iterates metadata
COLLECTIONS, so a stack whose only mistake is at the envelope level — no
objects, no pages, nothing to iterate — walks clean and reports nothing. A test
pins exactly that.

Verified end to end after the rebuild: `defineStack` warns at config-load time,
and `os validate` / `os build` report `stack.storage` with its prescription plus
`stack.datasource` with its edit-distance suggestion, alongside the field-level
findings the walker already produced.

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

* docs(cli): name the right lint export in the serve storage doc-comment

The top-level coverage moved to `lintUnknownStackKeys` when this was rebuilt on
#4178's walker; the comment still pointed at `lintUnknownAuthoringKeys`, which
no longer does that job.

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

* fix(spec): the top-level lint must not call a working `onEnable` dropped

Caught by running the new lint against our own shipped examples before
merging: `examples/app-todo` and `examples/app-showcase` both got

  ⚠ stack.onEnable: 'onEnable' is not a declared stack key, so its value is
    dropped at load.

which is false. `onEnable` is a function, so `ObjectStackDefinitionSchema`
cannot declare it and `dist/objectstack.json` cannot carry it — but `AppPlugin`
reads it straight off the authored bundle and calls it at `start()`
(`app-plugin.ts`), and the artifact-boot path grafts it back (#4095). It is the
documented place to register action handlers. "Not declared" and "dropped at
load" are different claims, and this is the one surface where they come apart.

New `STACK_RUNTIME_MEMBERS` names the authored top-level members the runtime
honours off the bundle; `lintUnknownStackKeys` treats them as declared. The
CLI's `GRAFTABLE_RUNTIME_MEMBERS` is now DERIVED from it instead of restating
it — two hand-written copies could disagree, and the disagreement would be
silent in exactly the direction this lint family exists to catch.

`onDisable` is deliberately excluded from the exemption: it is declared in the
protocol but no kernel, runtime or service calls it, so a value written there
really does go nowhere. A test pins both halves of that distinction.

Re-harvested after the fix: all three shipped examples validate with zero
undeclared-key warnings.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants