feat(spec): 未声明键 lint 从 2 个面扩到全部可 lint 元数据集合 (#3786) - #4178
Merged
os-zhuang merged 3 commits intoJul 30, 2026
Conversation
…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
|
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:
|
…-drift-investigation-ewgd6l
…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
marked this pull request as ready for review
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>
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.
承 #4148(已合并)。当时 lint 只盖
object/field—— 因为 #4120 是在那两个面上抓到真漂移的。合并后量了一下:那两个是样本,不是总体。测量结果
25 个可授权元数据类型里,
.strict()的只有一小撮(#4001 Tier-A 的flow/permission/position/tool,以及本 PR 评审期间刚毕业的app,#4165)。其余全部对未声明键的行为与field一模一样 —— 解析通过、值被丢、零信号。作者在page、agent、dashboard上写错键,和 #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 用的同一张边界映射,新集合注册进去的那一刻就被覆盖getMetadataTypeSchema—— 类型→Zod 的唯一注册表在终结手抄清单的工具里再手写第三份「lint 覆盖哪些类型」的清单,就是 #3786 本身。
姿态规则保证 lint 永远不会与 parse 唱反调:
view的三形态)→ 成员键并集;仅当有成员 strip 且无成员 passthrough 时可 lint评审期间发生的事,恰好证明了设计
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)。验证
FormSection.pane— explicit split-pane placement (objectui#2153 follow-up) #4160 刚加的FormSection.pane(AGENTS.md 里写明的那个 check:api-surface 陷阱),重建即消失,与本改动无关view掉出覆盖,被点名断言抓住)、反转 strict 过滤(flow 开始被重复报告)、悄悄跳过一个集合(pages)—— 恢复后全绿view),并且每个可 lint 集合都真的对 bogus 键产出 finding —— 「覆盖」是被证明的,不是被声称的pages.home.visibleWhenn、agents.helper.temprature都被报出这一步之后
listLintableAuthoringCollections作为数据导出 —— #4001 的 strict tier 排期现在有了真实证据基座,而且每次毕业都会以「覆盖断言变红 → 挪名单」的形式被显式记录。