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
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…emas (#4001 Tier-A) (#4071)

Zod's default .strip silently discarded undeclared keys on authorable schemas —
the instance kept parsing, so a mis-spelled or wrong-layer key shipped as
metadata that quietly ignored the author's config (#3405, #1535). This lands
the #4001 Tier-A slice inside the v17 breaking window.

- docs/audits/2026-07-unknown-key-strictness-ledger.md: the authorable / wire /
  open triage (#4001 step 1), classified by "who writes this schema's input",
  with the ratchet's verified next targets.
- shared/suggestions.zod.ts: new strictUnknownKeyError factory generalizing the
  #3746 hand-rolled map (alias table + length-relative edit distance +
  tombstone/wrong-layer guidance); ui/action.zod.ts re-homes onto it with
  byte-identical messages.
- security/permission.zod.ts: PermissionSetSchema, ObjectPermissionSchema,
  FieldPermissionSchema, AdminScopeSchema are .strict().
  EffectiveObjectPermissionSchema explicitly .strip()s back — response shapes
  stay wire-tolerant.
- automation/flow.zod.ts: FlowSchema, FlowNodeSchema, FlowEdgeSchema,
  FlowVariableSchema are .strict(); a node's config record stays open (the
  executor's configSchema, #4027/#4040, and the ADR-0087 conversion layer own it).

Three real defects the gate caught in its first two runs, all previously
invisible — each key written by real code, silently dropped at parse:
PermissionSetSchema could represent neither `description` (authored by the
built-in default sets, read by the Setup projection) nor the ADR-0010 runtime
protection envelope (stamped by applyProtection on every metadata type and
round-tripped through getMetaItemLayered -> saveMetaItem; every sibling
registered type already spread MetadataProtectionFields, permission was the
outlier); and a dogfood fixture declared the object-level `sharingModel` on a
flow. Two of the three are inverse drift — the direction the per-property
liveness ledger cannot see.

Migration is behavior-preserving by construction: any key now rejected was
previously stripped and therefore never had a runtime effect.
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…4001 step 3) (#4119)

Third click of the unknown-key strictness ratchet (flow + permission in
#4071, RLS / sharing / position in #4099). Approval is a v17-new authoring
surface — tightened while young, before stored volume exists:

- ApprovalNodeConfigSchema, ApprovalNodeApproverSchema,
  ApprovalEscalationSchema and DecisionOutputDefSchema are .strict() with
  fixable errors via the shared strictUnknownKeyError factory.
- ApprovalNodeConfig's guidance is the ADR-0019 re-home map: steps ->
  successive approval nodes on the canvas, entryCriteria -> the entering
  edge's condition, onApprove/onReject -> the approve/reject out-edges,
  rejectionBehavior -> a declared back-edge (ADR-0044) with maxRevisions.
- The published JSON schema (getApprovalNodeConfigJsonSchema) carries
  additionalProperties:false into the Studio property form AND registerFlow's
  per-node config validation (#4027/#4040). Verified the #3746 hazard:
  z.toJSONSchema on the strict lazySchema does not throw.

Verified: spec 6995 tests + tsc clean; all 12 check gates; plugin-approvals
326 / service-automation 457; dogfood 72 files / 418 tests; showcase / crm /
todo validate clean.
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 2, 2026
…te name it references (objectstack-ai#4457) (objectstack-ai#4508)

A selection-bar bulk action was `z.array(z.record(z.string(), z.any()))` — no
shape at all. The real contract lived in objectui's `BulkActionDef` interface
and in the executor that reads it, so every authoring mistake landed as a silent
runtime downgrade: `opeartion` parsed and the executor hit `Unknown operation:
undefined` per row; `excution: 'aggregate'` parsed and left the def per-record,
so the endpoint written for ONE `_selectedIds` call got N calls instead.

`ui/bulk-action.zod.ts` types it with the treatment `ActionParamSchema` got in
objectstack-ai#3746/objectstack-ai#4001: a strict def whose unknown-key error names the offending key and
the canonical spelling. It also refuses the combinations the executor never
reads (`patch` outside an update, `execution` outside a custom, `params` on a
delete, `batchSize` on an aggregate) and a hand-written `actionDef`, which the
renderer attaches and which authored by hand would smuggle an action definition
past the action registry.

One shape that parsed before is now rejected: `operation: 'custom'` without
`execution: 'aggregate'`. `resolveBulkActions` attaches a dispatcher for exactly
one authored shape; every other custom def falls to `Promise.resolve()` per row
— a button that reports success for every selected record and does nothing. The
error names both legal forms.

`params[]` stays `.passthrough()` (the renderer declares a widget-config
catch-all), and the bulk-param/action-param spelling divergence is documented
rather than converged — that needs a cross-repo change, and typing them as they
are is what makes the divergence visible.

Lint: `validate-action-name-refs` now covers `bulkActionDefs`, for the entries
that are references rather than button ids (`execution: 'aggregate'`). The walk
also reaches an object's own `listViews` for the first time, and the hint no
longer tells a bulk-surface author to add a `locations` entry the selection bar
does not read.

Verified zero new findings against app-showcase / app-crm / app-todo.


Claude-Session: https://claude.ai/code/session_01S9aiswZBzoVYsyLKRuGByE

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 2, 2026
…s` stops pretending to be a gate (objectstack-ai#4001) (objectstack-ai#4528)

Mechanical work on the registered-type line with `strictObject`: `report`,
`dataset`, `email_template`, `skill`, `job`, `book`. One call each, plus the
aliases that fit the surface's own vocabulary — `sections`/`chapters`/`toc` →
`groups` on a book, `cron`/`interval` → `schedule` on a job, `title`/`content`/
`html` → `subject`/`body` on an email template.

One of the six is not mechanical, and it is the class this campaign exists for.

`skill` accepted a `permissions` key and dropped it. Skill invocation was never
permission-gated, so an author who wrote `permissions: ['order.manage']`
believed they had restricted who could invoke the skill, and had not. A silent
permission hole — the same shape as `visibleWhen` → `visible` in objectstack-ai#3746, where
the most valuable alias was not a typo but a key that READS as a security
control and silently is not one.

A test pinned that strip as correct behaviour. Its comment even carried the
right answer — gate at the AGENT via `access`/`permissions`, enforced since
objectstack-ai#1884 — but a comment in a test file reaches everyone except the author who got
it wrong. The rejection now carries the prescription; the test asserts the
rejection.

That is the fourth test in this campaign found codifying a strip-era fiction as
expected behaviour (`position.parent`, `object.namespace`, the retired
`compactLayout` alias, and now this one). The pattern is consistent enough to
state: when a schema is silently lenient, its tests eventually assert the
leniency, and the assertion then reads as intent.

Registered types closed at the top level: 16 of 25, up from 9 when this line
started. Still open: action, agent, dashboard, field, mapping, page,
translation, view.

The warning layer's covered population drops 12 roots → 6, which is the parse
taking over rather than coverage rotting; nested strip sites under a closed root
still report, unchanged.

Verified: spec 284 files / 7187 tests, `tsc --noEmit` clean, all 8 generated
artifacts current, all 15 `check:*` gates green.


Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 2, 2026
…ectstack-ai#4533)

* feat(spec)!: action closes, and the ADR-0010 envelope debt list reaches zero (objectstack-ai#4001)

`ActionParamSchema` has been strict since objectstack-ai#3746 — the template this campaign was
generalized from, and the source of its sharpest lesson: `visibleWhen` →
`visible` showed the most valuable alias entry is rarely a typo but a key that
reads as a control and silently is not one. The action AROUND the param stayed
open for three more releases.

The AI exposure block is why this one mattered. `ActionAiSchema` is the
governance gate — its own doc says a half-finished or unreviewed action must
never be silently armed — yet `requireConfirmation`, one letter off, was dropped
in silence, so an author who asked for a human-in-the-loop gate on an
AI-invoked action did not get one and was not told.

The root tombstones the key this campaign cares most about. `permissions` on an
action is NOT a permission gate: authorization comes from the object's
permission sets and, on the AI surface, from the agent's access/permissions
(objectstack-ai#1884). The rejection says so and names the trap beside it — `visible` and
`disabled` are UI predicates, they hide or grey a button, they do not stop a
request. Third time in this campaign after skill.permissions and
agent.visibility, always a different surface, always reading like a control.

The undeclared-envelope debt list is now EMPTY. The structural walk opened it
with eight names after replacing a probe that had been hiding seven; `action`
was the last. The empty set is kept, not deleted: with no exemptions the
DECLARES case runs over every registered type, so a new type shipping without
the spread fails immediately instead of being quietly added to a list.

Registered types closed: 24 of 25. Only `view` remains.

Two lint-layer facts recorded rather than papered over: the array-index test has
run out of subject (no strip-mode array-of-objects remains anywhere), and `view`
is the last open root — when it closes, change the floor to 0 and assert the
empty set deliberately rather than deleting the test, because an empty result
nobody chose is indistinguishable from a derivation that broke.

Verified: 284 files / 7240 tests, tsc clean, 8 generated artifacts current, all
spec gates green, and CRM/Todo/showcase/platform-objects build.

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

* fix(spec): an action's capability gate is real — point the near-misses AT it (objectstack-ai#4001)

The first draft of this batch's guidance claimed `permissions` on an action is
not a permission gate and sent authors to the object's permission sets. That is
wrong: `requiredPermissions` (ADR-0066 D4) is a declared key enforced with a 403
on the platform action route, and `ui/actions.mdx` teaches it by name. Anyone
following the prescription would have been invited to delete a working gate.

  - `permissions` / `capabilities` / `requiresPermissions` / `acl` now RENAME
    onto `requiredPermissions` instead of pointing away from it.
  - The dead `requiredPermissions` guidance entry is removed — the key is
    declared, so strict never reaches it; the entry was wrong AND unreachable.
  - `location` → `locations` was backwards: `locations` is the declared key, so
    the alias would have suggested renaming a correct key to a nonexistent one.
  - The `visible`/`disabled` warning survives on `hidden`, where it belongs:
    hiding is not gating, and a headless action is `locations: []`.

Ledger finding 7 for the fourth time — this campaign's own prescriptions are a
surface that can be confidently wrong. Caught by checking the docs the drift
report flagged against the schema rather than against memory of it.

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

* test(metadata-protocol): a stored-row fixture wrote `object` where the key is `objectName` (objectstack-ai#4001)

`ActionSchema` has never declared `object` — the key is `objectName`. Two
stored-row fixtures wrote it anyway, `.strip` ate it, and the tests passed. With
the shape closed the row now fails spec validation after conversion and the
protocol reports `invalid: 1`.

Worth naming because of where it was: these fixtures stand in for pre-17 rows in
a database, so a typo in one reads as a claim about real legacy data — "stored
actions carry `object`" — when no such key ever existed. There is no missing
conversion here; `git log -S` finds no commit that ever declared it.

Sixth strip-era fiction this campaign has found in a test, and the first dressed
as a stored ROW rather than an authored literal.

Also the reason this was caught by CI rather than locally: the previous pass ran
only `packages/spec`. The blast radius of closing a registered type is every
package that parses that type.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Aug 3, 2026
`gen:docs` only — the property tables for `automation/Webhook` and the
`integration/WebhookConfig` extension gain the `protection` block and the seven
`_lock*`/`_provenance`/`_package*` rows. Checked for the #3746 trap: no page
description or frontmatter moved, so no internal comment leaked onto a public
reference page.

Part of #4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
xuyushun441-sys pushed a commit that referenced this pull request Aug 4, 2026
getFileDescription() 用裸正则在原始源码上找第一个 doc block,所以在 // 行里
引用那个双星开头的字面量也会被匹配成文件的首个 doc block —— 于是这条警告
自己把 theme/chart 两个参考页的描述清空了。check:docs 抓到;改写措辞后
两页与 main 逐字节相同。

同时把 clickAction 的 finding-7 说明从 JSDoc 移到 // 注释:它是给下一个
维护者的话,不是协议文档,不该发布到公开参考页。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…objectstack-ai#4974)

* feat(spec)!: close nine authorable automation shapes against unknown keys (objectstack-ai#4001 batch 11)

zod's default `.strip` discards an undeclared key and parses on. Batch 11 closes
the nine remaining shapes in `automation/`'s main body, each rejection naming the
surface, the key, and — where the word is recognisable — the canonical spelling.

`flow.zod.ts`: the four OUTER shapes were closed earlier; their six nested
authoring blocks were not, so the gate rejected `nodee:` at node level while
`connectorConfig: { connectorID }` parsed clean and dispatched against an
undefined connector id. Now strict: `FlowNode.connectorConfig` / `.position` /
`.inputSchema` / `.waitEventConfig` / `.boundaryConfig`, `Flow.errorHandling`.
Deliberately still open, now pinned in code AND tests so a later sweep stops
rather than "finishing" the file: the node `config` slot (ADR-0018) and
`FlowVersionHistorySchema` (emitted, not authored — its `definition` is
`FlowSchema`, so the authored half is gated anyway).

Every alias is a real in-repo spelling of the same knob on a neighbouring
surface, taken from an AST census over the repo's own payloads, not guessed:
`backoffMs` from `shared/retry-policy.zod.ts` (objectstack-ai#4661), `initialDelayMs` /
`maxDelayMs` from connector `RetryConfig`, `attachedToRef` / `cancelActivity`
from BPMN 2.0. Two cases get a prescription instead of a rename because a rename
would be wrong: `inputSchema.optional` is the opposite polarity of `required`,
and `errorHandling.maxAttempts` counts the first attempt while `maxRetries` does
not — renaming it alone would quietly run one attempt fewer.

`time-relative-trigger.zod.ts`: the schema objectstack-ai#4852 made visible (its only site was
line-wrapped, the old counter read zero, and a zero-site file is skipped by the
coverage walk). It sits under the open node `config` slot, so it is the only key
gate there is — and it is `safeParse`d at BIND time, so `offsetDay` beside a
valid mode used to bind a sweep with the author's narrowing discarded and report
itself configured.

`flow-function.zod.ts`: binds at authoring only (boot reads
`normalizeFlowFunctionEntry`), stated in the code rather than implied. That is
why it matters: the reader takes two keys and ignores the rest by construction,
so a misspelled `effect` was dropped and then not looked for — the function ran
and its writes were counted as none, keeping objectstack-ai#4354's broken-sweep query silent.

`webhook.zod.ts`: the ledger's `(p)` resolved to the opposite of "spec-only" —
three parse doors, one of them `bootstrapDeclaredWebhooks` at BOOT, which warns
and SKIPS on failure. Hence the ADR-0010 envelope in the same commit:
`applyProtection` stamps `_packageId`/`_provenance` on every type, so closing the
shape without declaring them would have turned every package-shipped webhook
into a skipped subscription after a redeploy. Strictness also rides `.extend()`
onto the connector `WebhookConfigSchema` — verified against real zod and pinned
in `connector.test.ts`.

Ledger: `automation/` 67 → 58 strip; three rows reached zero and were deleted by
the objectstack-ai#4852 reverse pin. Gate re-proved red in both directions before trusting it,
and every new assertion was proved to depend on the tightening by neutering
`strictObject` (19 red, restored byte-identical).

Part of objectstack-ai#4001

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

* docs(spec): regenerate reference pages for the webhook ADR-0010 envelope

`gen:docs` only — the property tables for `automation/Webhook` and the
`integration/WebhookConfig` extension gain the `protection` block and the seven
`_lock*`/`_provenance`/`_package*` rows. Checked for the objectstack-ai#3746 trap: no page
description or frontmatter moved, so no internal comment leaked onto a public
reference page.

Part of objectstack-ai#4001

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

* fix(spec): correct three history strings that named the wrong failure

Self-review against the actual behaviour, before the claim shipped. The
`connectorConfig` / `boundaryConfig` / `position` / `inputSchema` prose named a
slip on a REQUIRED key (`connectorID`, `attachedToRef`) as the silent case. It
never was: a required key spelled wrong then reads as MISSING, which zod has
always rejected loudly. Nothing was silent about it.

What `.strip` actually swallowed is the OPTIONAL half — the mapped `input` map
(dispatched as `input ?? {}`: a successful connector call carrying nothing),
BPMN's `cancelActivity: false` (so `interrupting` fell to its `true` default and
a NON-interrupting event cancelled the host anyway), `optional: false` (so
`required` fell to false and `validateNodeInputSchemas` had nothing to require),
a canvas hint beside x/y. I.e. exactly the keys an author adds to CONSTRAIN
behaviour, replaced by a permissive default.

The corrected version is the stronger argument as well as the true one, and
getting it wrong is the failure mode this campaign has already paid for four
times (the withdrawn guidance prescriptions): rejection prose is behaviour, not
documentation. Fixed in the schemas, the module comment, the tests and the
ledger row; a test now pins the required-key case as loud so the distinction
cannot quietly re-blur.

Part of objectstack-ai#4001

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…#4001 批 15) (objectstack-ai#5043)

* feat(spec)!: 收紧 theme/chart 的未知键,并按门测量把 chart 的两个站点留开(objectstack-ai#4001 批 15)

theme.zod.ts 14 个站点全部 strictObject;chart.zod.ts 5 个站点收紧,
ChartAggregate/ChartGroupBy 两个站点按实测(有承载键但无 parse)留开并记录。

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

* docs(audits): 台账记录 批 15 的门测量与两张表重算(objectstack-ai#4001)

ui/ triage: theme 行改判 authorable+strict;chart 行拆出并记 5 收紧 / 2 no gate。
remaining-strip map: theme 行删除(已关闭),chart 7→2,表头 123→104,
小计由存活行重算 102 of 104。

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

* fix(spec): 把 objectstack-ai#3746 陷阱的警告本身移出 doc block,并补 批 15 changeset(objectstack-ai#4001)

getFileDescription() 用裸正则在原始源码上找第一个 doc block,所以在 // 行里
引用那个双星开头的字面量也会被匹配成文件的首个 doc block —— 于是这条警告
自己把 theme/chart 两个参考页的描述清空了。check:docs 抓到;改写措辞后
两页与 main 逐字节相同。

同时把 clickAction 的 finding-7 说明从 JSDoc 移到 // 注释:它是给下一个
维护者的话,不是协议文档,不该发布到公开参考页。

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

---------

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:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants