Skip to content

feat(spec): reject unknown keys on an action param instead of stripping them (#3405) - #3746

Merged
os-zhuang merged 3 commits into
mainfrom
claude/action-param-inline-lookup-5e7as6
Jul 28, 2026
Merged

feat(spec): reject unknown keys on an action param instead of stripping them (#3405)#3746
os-zhuang merged 3 commits into
mainfrom
claude/action-param-inline-lookup-5e7as6

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes part 3 of #3405 — the item #3406 deferred as「单独评估、必要时拆出去做」。

问题

#3406 / objectui#2786 修的是症状:给内联 record-picker 参数加了 reference 键,并让缺目标的参数在解析期报错。导致它的机制原样留着 —— ActionParamSchema 是 zod 默认 .strip,任何它没声明的键都被静默丢弃,参数照常 parse 通过。

作者写了一个语义完全正确、且与 FieldSchema.reference 同名的键,得到的唯一反馈是一个要求粘 UUID 的文本框。下一个写错键名的作者会以同样的方式失败,同样毫无声音。

这一条不修,#3405 修的就只是一个键,不是那个坑。

改动

ActionParamSchema 改为 .strict(),并配一个让拒绝可修而不只是大声的 error map:

  • 大小写 / 下划线走形(help_texthelpTextdefault_valuedefaultValue)交给共享的 findClosestMatches,距离上界沿用 data/object.zod.tssuggestKey长度相对公式 —— 固定距离 3 会给 wibble 推荐 visible
  • 编辑距离够不到的语义近义键FIELD_TYPE_ALIASES 的风格显式列出:

最后这条是这个 PR 超出「防错别字」的价值所在:ADR-0089 把 visibleWhen 定为 view/page schema 的正统拼法,在这里借用它,过去会把参数的能力开关整个剥掉,让它无条件渲染 —— 一个静默失效的权限门。

遵循 ADR-0078(no-silently-inert-metadata)与 ADR-0049(enforce-or-remove),做法与 ADR-0089 D3a 对 view/page schema 的处理一致。

影响面(issue 里担心的那一点)

issue 原文说这条「影响面比前两条大得多,可能踩到其他既有元数据」。实测本仓零破坏:

  • @objectstack/spec 全量 258 文件 / 6716 用例通过
  • tsc --noEmit 干净
  • app-showcase / app-crm / app-todo 三个示例应用全部 validate 通过 —— 仓库里没有任何既有元数据带着未声明的参数键
  • 下游消费包回归:lint 467 例、metadata 276 例、metadata-core 100 例、platform-objects 223 例、metadata-protocol 70 例、sdui-parser 6 例,全过

验证

在 showcase 的内联 picker 参数上实种一个坏键(保留合法的 reference,只加一个 visibleWhen,以隔离本 PR 的新行为):

✗ "code": "unrecognized_keys",
  "message": "Unrecognized key(s) on this action param: `visibleWhen`. Until #3405
  these were dropped silently — the param still parsed, so a mis-spelled config
  shipped as a control that quietly ignored it. Did you mean `visibleWhen` → `visible`?"

去掉后 ✓ Validation passedreference_to 同样命中 → reference

破坏性说明

带多余键的既有参数现在会 parse 失败。这正是本 PR 的意图 —— 那些键本来就没有生效,只是坏得没声音。错误信息直接点名该键并给出正确拼法,changeset 里附了 FROM → TO 对照表。

遗留(不在本 PR)

#3405 验收里的 PLAT-DEF-005 真机回归仍卡在发版:npm 上 @objectstack/spec 最新是 16.1.0(2026-07-22T01:06Z),早于 #3406 合并(同日 20:46Z)。我拉 16.1.0 的 tarball 确认过,dist/ 里没有 reference 的校验。天顺 EHR 那行 reference: 'sys_user' 至今仍被服务端 strip。#3406 的 changeset 也还挂在 .changeset/ 里未消费 —— 同一个佐证。需要发一次 spec 版本才能收尾。

🤖 Generated with Claude Code

https://claude.ai/code/session_01LkvDs4y4gveyB5NJZKxaZt


Generated by Claude Code

…ng them (#3405)

Closes part 3 of #3405 — the item deferred out of #3406 as "evaluate
separately".

Parts 1 and 2 gave an inline record-picker param a `reference` key and made a
targetless one a parse error. That fixed the symptom. The mechanism that caused
it stayed: `ActionParamSchema` was zod-default `.strip`, so any key it does not
declare was discarded silently and the param went on parsing. An author wrote a
correct, clearly intended `reference: 'sys_user'`, the key was eaten, and the
dialog rendered a text box asking a human to paste a UUID — no error anywhere.
The next mis-spelled key would have failed the same way, just as quietly.

An action param is now `.strict()`, with an error map that makes the rejection
fixable rather than merely loud:

- Case/underscore slips (`help_text` → `helpText`, `default_value` →
  `defaultValue`) resolve through the shared `findClosestMatches`, bounded by
  the same length-relative distance `suggestKey` uses in `data/object.zod.ts` —
  a flat distance of 3 suggests `visible` for `wibble`.
- Semantic near-misses edit distance cannot reach are named explicitly, in the
  `FIELD_TYPE_ALIASES` style: `reference_to` / `referenceTo` / `targetObject` →
  `reference` (the runtime field shape spells it the first way, objectui's
  resolved param the second), and `visibleWhen` / `visibleOn` / `visibility` →
  `visible`. That last one is why this matters beyond typos: ADR-0089 made
  `visibleWhen` canonical on view/page schemas, so borrowing it here used to
  strip a param's capability gate and render it unconditionally.

Follows ADR-0078 (no-silently-inert-metadata) and ADR-0049 (enforce-or-remove),
and matches the precedent set by ADR-0089 D3a for the view/page schemas.

Verification: spec 258 files / 6716 tests pass; `tsc --noEmit` clean;
app-showcase, app-crm and app-todo all `validate` clean — no existing metadata
in the repo carried an undeclared param key. Planting `visibleWhen` on
showcase's inline picker param reproduces the new error with the
`visibleWhen` → `visible` prescription, and removing it validates again.

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

vercel Bot commented Jul 28, 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 28, 2026 1:49am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

104 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/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/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.

claude added 2 commits July 28, 2026 01:41
`scripts/build-docs.ts` `getFileDescription()` takes the FIRST `/** */` block
in a module, verbatim, as the description of its generated reference page.
Placing the new `ACTION_PARAM_KEYS` / error-map helpers above the "Action
Parameter Schema" JSDoc therefore replaced the public authoring guide on
`content/docs/references/ui/action.mdx` with an internal note about why a key
list is kept beside the schema — which is what `check:docs` caught.

Moved the helpers back below that JSDoc (after the `lazySchema` import, where
they were originally). The generated doc is byte-identical to main again:
`check:docs` reports 250 generated files in sync.

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

Two more generated-artifact gates behind `check:docs`, both tripped by the same
commit:

- `check:skill-refs` — importing `shared/suggestions.zod.ts` from
  `ui/action.zod.ts` pulls it into the transitive reference set of the
  objectstack-data / -ui / -platform skills. Regenerated via `gen:skill-refs`;
  the diff is the one expected line per skill, and `action.zod.ts` still
  resolves to "Action Parameter Schema", confirming the JSDoc-order fix held.

- `check:api-surface` — `actionParamUnknownKeyError` was exported, which added
  it to the package's public API. It has no caller outside its own module, so
  the export was unnecessary: unlike `strictVisibilityError`, which is shared
  across the view/page schemas, this map is wired into exactly one schema.
  Made it module-private; the public API surface is now unchanged by this PR.

All ten `check:*` gates in packages/spec pass locally, alongside 258 files /
6716 tests and a clean `tsc --noEmit`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkvDs4y4gveyB5NJZKxaZt
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 02:01
@os-zhuang
os-zhuang merged commit 4727eb8 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/action-param-inline-lookup-5e7as6 branch July 28, 2026 02:02
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:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants