Uh oh!
There was an error while loading. Please reload this page.
feat(spec): refuse the confirmText + params pair on ActionSchema, scoped by schema boundary - #7972
Conversation
An action declaring both opens TWO sequential dialogs for one decision — the console runner awaits the confirm, then the param prompt, so the first already reads as "the action ran" while nothing has been sent. #7278 and #7309 repaired the 16 shipped sites (PRs #7592, #7827); neither stops the next one being written. This is the structural half: a refusal on ActionSchema whose message names both keys and points the confirm question at the action's top-level `description`. Scoped by schema boundary, not by a param-optionality heuristic. The pair is INTENDED on `BulkActionDefSchema`, where params and confirm render one dialog (a `required` param blocks that dialog's own Confirm button), so the four `examples/app-showcase` bulk defs are correct as written. The refusal lives on ActionSchema's refine chain and is structurally incapable of reaching either `BulkActionDefSchema` or `InlineActionSchema`; both directions are pinned.
…geset The `confirmText` describe() now names the refusal, so an author meets the constraint in the generated reference rather than only in the error. Worded "on a registered action" deliberately: the same field factory renders the InlineAction reference table, where the guard does not apply — an unqualified claim there would be a documented refusal that does not exist. Regenerates content/docs/references/ui/action.mdx (gen:docs).
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 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:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
`content/docs/protocol/objectui/actions.mdx` showed `disable_oauth_application` and `rotate_client_secret` pairing `confirmText` with `params`. Both were already stale — the real actions moved the question to `description` in #7827 — and the guard in this PR makes the documented shape fail to parse. They are labelled "Real-World Examples", so they now mirror what the platform objects actually ship, and the `confirmText` bullet above them states the constraint: param-LESS actions only, question on `description` otherwise, not `ai.description`, and bulk defs unaffected. Found via the PR's docs-drift list; the brace-balanced census could not see these because the examples are YAML.
Uh oh!
There was an error while loading. Please reload this page.
…ged with batch B Second turn of the os-regen relay. `#7985` (batch B — the memory driver's five persistence sub-shapes) and `#7972` landed while this branch was in the queue, so the shared counts artifact met two batches that each decremented it correctly and independently. That is the exact arithmetic #5107 built this artifact to prevent: the rows do not overlap, git merges them without complaint, and the SUBTOTAL — which overlaps nothing — merges clean and wrong. The artifact carries `merge=os-regen` so the merge defers rather than splices, and the only correct resolution is to recompute from the merged tree. Regenerated, never hand-edited: still-open (strip) 149 → 144 (batch B's five) files carrying at least one 26 → 25 authorable — forced scope 9 → 4 data/ strip 107 → 102, data/ strict 57 → 62 total strict 283 → 288 Neither batch's own numbers moved; the combined ones did. `check:strictness-ledger` agrees with the merged tree in both directions (25 open files / 144 strip sites, no closed file still carrying a worklist row — batch B's `driver/memory.zod.ts` row left with their side of the prose). Merged tree verified: spec 382 files / 10109 tests, lint 71 / 1932, check:generated 13/13. Part of #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
Fixes#7428
An action that declares
confirmTextbeside a non-emptyparamsopens two sequential dialogs for one decision — the console runner awaits the confirm, then the param prompt, so the first dialog already reads as "the action ran" while nothing has been sent. #7278 (PR #7592) and #7309 (PR #7827) repaired the sites that shipped it. Repairing instances does not stop the next one being written; this PR is the structural half.The guard
A
.refineappended toActionSchema's chain inpackages/spec/src/ui/action.zod.ts, refusingconfirmText+ a non-emptyparamsarray atpath: ['confirmText']— the key that has to go, since pointing atparamswould tell the author to delete the inputs they need. The message names both keys, states the user-visible consequence, points at the remedy (description), warns off the remedy's trap (ai.description), and names the two shapes that stay legal.Census evidence (the premise, re-measured — not assumed)
Re-ran the card's brace-balanced scan on
origin/main@8d80e12before writing a line of the guard, and again on the final merged tree. Predicate per the #7309 notes recorded on the card: object literals declaring both keys at their own depth, withparams: [non-empty — which is what separates real sites from the schema definition files (action.zod.ts,bulk-action.zod.ts,translation.zod.tsdeclare both keys) and the generated translation bundles (paramsis a record there, not an array).ActionSchemasites, non-testActionSchemasites, including test files and every packageBulkActionDefSchemasitesexamples/app-showcasedefs, correct as-isSo the PM's mechanism premise holds: the
ActionSchemacensus is already 0 after #7592 and #7827, and the refusal breaks nothing that ships today. The only hits anywhere in the repo are the four showcasebulkActionDefs(plus onebulk-action.test.tsfixture), all on the surface the guard is scoped away from.The census had one blind spot, found after opening this PR — see "Docs" below.
Ruling provenance
The card left three design questions open. They were answered by the PM's claim comment of 2026-08-12T08:24Z and its correction minutes later, which in turn defers to the measured correction of 2026-08-11T17:29Z on the same thread. Quoted rather than paraphrased:
From the 17:29Z measured correction — the finding that scoped this guard:
and its reasoning, which this PR's tests pin verbatim in substance:
From the PM's correction comment:
All four are implemented as ruled: refuse severity, schema-boundary scoping with the bulk pinning test, no added
descriptionrequirement, and no migration of the showcase sites.Scoping is structural, not conditional
The refusal is on
ActionSchema's refine chain.BulkActionDefSchemais a separatestrictObjectinbulk-action.zod.tswith its ownsuperRefine, so no refinement here can reach it.InlineActionSchema.pick()s from the shared field factory rather than deriving from this chain, so it is out of reach too — and it deliberately does not pickdescription, so the #7278 remedy has no slot on that surface to move a question into. That is the same argument the 17:29Z correction used to strike the bulk defs, which is why inline is recorded as an open question rather than quietly swept in.action-confirm-params-guard.test.ts(12 tests) pins both directions:confirmText(a truthiness check written against a string would miss it); the path; and the message substance, asserted phrase by phrase rather than assuccess === false.params: [];params+description(the shape approvals:sys_approval_request.approval_reject/approval_recalldeclare bothconfirmTextandparams, so one decision opens two sequential dialogs #7278 migrated to — if this goes red the guard has swallowed its own remedy); andparamswith nodescription, pinning ruling 3.BulkActionDefSchemastill acceptsconfirmText+ non-emptyparams, with the showcase's ownset_labelsliteral; that the two schemas are independent; andInlineActionSchema's current acceptance, with the reason recorded.One test changed shape against a measurement. I had assumed aliases (
confirm→confirmText,inputs→params) were folded before the refinements, and wrote a test asserting the guard fires on the aliased pair. It does not — this repo rejects a near-miss with a rename arrow rather than folding it (Prime Directive #12), so the aliased pair is refused one layer earlier by key recognition and never reaches the refinement. The test now pins that actual mechanism, since the guard's coverage claim depends on it.Reverse verification
Guard committed first, then reverted alone (tests kept), per the discipline that a restore point must actually exist.
defineActionthrow.refuses the pair,says WHY,localized-map form,throws from defineAction. All 8 acceptance/boundary tests stayed green, as they must: they assertsuccess === true, which a schema with no guard satisfies trivially.git checkout <branch> -- <path>from the commit;git diff HEADempty (byte-identical, not a matching line count), 12/12 green again.Docs — a stale example the census could not see
The docs-drift bot's list pointed at
content/docs/protocol/objectui/actions.mdx, and two of its "Real-World Examples" —disable_oauth_applicationandrotate_client_secret— declaredconfirmTextbesideparams. Both were already stale againstmain: the real actions moved the question todescriptionin #7827. This PR makes the documented shape fail to parse, so a doc that was merely out of date became a doc that teaches metadata the spec refuses.Fixed in
b5b4ff8: the two examples now mirror whatsys-oauth-application.object.tsactually ships, and theconfirmTextbullet above them states the constraint (param-LESS only; question ondescription; notai.description; bulk defs unaffected).Why the census missed them: those examples are YAML, and the card's acceptance scan is brace-balanced — it can only see TS/TS-like object literals. A YAML-aware re-scan across all
content/docs/**found exactly these two and nothing else. Worth knowing if the census is ever re-run as a gate: it measures source declarations, not documented ones.Gates
All run locally, foreground, on the merged tree.
@objectstack/specfull suite@objectstack/spectypecheck (incl.check:test-typecheck)check:generated(all 13 artifacts)check:authorable-surfacecheck:adr-anchorscheck:changeset-gate-self-tests@objectstack/lint check:doc-formula-expressionscheck:docs-audit-scopecheck:driver-conformancecheck:i18ncheck:merge-drivercheck:release-bodycheck:spec-parsed-aliascheck:stack-collection-mapscheck:nul-bytesDownstream blast radius, since a refusal is only safe if consumers still parse:
platform-objects342 ✅,plugin-approvals458 ✅,plugin-security1001 ✅,example-showcase193 ✅ — the last being the live proof the four bulk defs still validate.Two gate results needed a prerequisite rather than a fix, both stale-build-state per the AGENTS.md §9 table, both cleared by building rather than asserted away:
check:i18nneeded the built CLI, andexamples/app-showcasetypecheck reported fourCannot find module '@objectstack/connector-*'errors until those packages were built (then clean).The diff regenerates
content/docs/references/ui/action.mdx(viagen:docs), because theconfirmText.describe()now names the constraint so an author meets it in the reference and not only in the error.Worth a reviewer's eye on the wording: that one field factory renders both the ActionSchema table and the InlineAction table, where the guard does not apply. My first draft said "pairing it with a non-empty
paramsis REFUSED" unqualified, which would have published a documented refusal that does not exist on the inline surface — a declared≠enforced inversion. It reads "on a registered action" for that reason.No other generated artifact moved;
authorable-surfaceis unchanged (a refinement adds no authorable key), and the anchor was never hand-edited.Changeset
@objectstack/specminor — an acceptance narrowing is user-visible. It carries the migration diff, theai.descriptiontrap, and each of the four shapes deliberately left accepted.Out of scope
packages/metadata-protocol/src/protocol.tscarries a hand-written JSON-Schema mirror of the action shape that declaresconfirmTextandparamswith no equivalent constraint. Not touched here — a second, independently-maintained contract is a finding of its own rather than a rider on this guard.Generated by Claude Code