Skip to content

feat(lint)!: refuse an action's copy-template findings (blank, unbound root, single-brace, logic, raw-shown ${…}) at validate, compile and the publish gate - #25702

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25680-action-copy-refusal
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25680-action-copy-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25680

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Ruling-ref: 6365736632 (on #24814, the refusal half: "its own BREAKING minor card, filed when this card lands, as #25205 is for the bulk slot"); triage 6386348015

The refusal half of ruling 6365736632 on #24814, verbatim: "The refusal half (blank and door findings as errors) is its own BREAKING minor card, filed when this card lands, as #25205 is for the bulk slot." #24814 (PR #25637, 384bb7ba02) typed an action's copy keys as ADR-0032 template slots and shipped every door finding as a warning. This PR makes them errors, shaped like #25205 / PR #25451 for the bulk slot.

What changes

  • The action copy pass refuses (packages/lint/src/validate-action-copy-templates.ts, judgeAction). A blank sentence, a hole over a root the key does not bind, a single-brace {x} token, a hole holding logic or an unknown formatter, and a ${…} token the runner shows raw are now severity: 'error', and the ADVISORY suffix is gone from them. The judge, the walk, the roots and the sentences are unchanged.
  • How it reaches each door. objectstack validate and compile fail on an error from validateStackExpressions. The publish gate splits errors from advisories by severity (runtime-gate.ts), so an active action, object or page write, and a draft promotion, answer 422 INVALID_METADATA with an expression-invalid issue at the key. No door code moves.
  • ${…}: kept a warning only where objectui's runner still fills it. Measured at the pinned .objectui-sha 2958f17e9c (objectui packages/core/src/actions/ActionRunner.ts): confirmMessage still evaluates a confirmText with no {{ }} hole through its ${…} expression evaluator (whose record is the record the action runs against), and composeSuccessMessage still substitutes ${result.*} (and nothing else) in a successMessage with no {{ }} hole; an errorMessage is shown as written (actionCopySource, no substitution). So, per the triage's lead, a ${…} token stays a warning exactly when the runner fills it: a sentence with no {{ }} hole whose every token is ${record.*} (confirmText) or ${result.*} (successMessage). Its finding now ends "A warning, not a refusal, for now: the console's runner still fills …". Every other ${…} is shown raw by the runner, so it is an error: in an errorMessage, beside a {{ }} hole or a {x} token, over params in a confirmText, or over record in a successMessage.
    • The roots are a spec fact beside the slot's other runner-measured roots: ACTION_COPY_DOLLAR_TRANSITION_ROOTS (@objectstack/spec/ui). The shape is read by the one template reader: dollarOnlyTokenRoots (@objectstack/spec/automation, beside closedScopeTemplateRefusal), so lint holds no second reading of the dialect.
  • The blank arm is kept (actionCopyTemplateSlot, comment only), following spec + lint 18.x: refuse a bulk confirmText hole over an unbound root or a single-brace token at validate / compile / the publish gate (the refusal half of #24798, under the amended 「甲」) #25205's answer for the bulk slot. The parse is what the boot load and the read path judge a stored row with; dropping the arm would make a stored blank sentence an invalid row on load, a narrowing wider than the three doors. The doors refuse a blank sentence in this same change, so every finding is one kind of verdict: a door refusal over text that parses. Door test (d) pins this for a blank and for an unbound hole.
  • Prose made false by the flip, corrected: the module notes (action-copy-template.ts "Widening only" section, validate-action-copy-templates.ts), the three copy .describe()s and their regenerated references (content/docs/references/{ui/action,ui/component,data/object,kernel/metadata-plugin}.mdx), the authoring-rules.ts registration comment, three validate-expressions.ts comments, and the dogfood expression-conformance.ledger.ts rows' enforcement / note text. affected-docs.mjs --json origin/main named 17 pages (4 release-owned, untouched); none of the 13 hand-written ones states the warning-only rule or a ${…} copy sentence, so none changes.
  • Changeset .changeset/25680-action-copy-refusal.md: @objectstack/lint and @objectstack/spec minor, both declaration lines, a FROM/TO block, and the ADR-0087 disposition not-required (no-migration-prescription), worded like spec + lint 18.x: refuse a bulk confirmText hole over an unbound root or a single-brace token at validate / compile / the publish gate (the refusal half of #24798, under the amended 「甲」) #25205's.

Producers, measured first (through the built @objectstack/lint at 6021b5e837, before the change)

Tests (head c69a7a6d; final head f03deb5f adds only the changeset's Ruling-ref: line, and the ten changeset / doc families re-ran on it, all exit 0)

  • validate-action-copy-templates.test.ts: every pin flips from warning to error, plus new cases: a single-brace token and an empty blank as errors; the four raw-shown ${…} shapes as errors; ${…} in a row's errorMessage as an error; the transitional ${record.*} / ${result.*} as warnings; and the gate block now asserts errors with empty advisories for a page write (single-brace, unbound, blank), errors for an action and an object write, and one advisory for each transitional sentence. 17/17.
  • flow-text-slot-template.test.ts: dollarOnlyTokenRoots answers the roots for an all-${…} holeless sentence and undefined for no token, a {{ }} hole, a bare {x}, logic, and an empty string. action-copy-template.test.ts: the transition roots per key.
  • New door block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the real saveMetaItem: (a) each of four texts on a page row is refused with INVALID_METADATA / 422 and nothing is stored, an action write is refused the same way, and the draft promotion is refused too; (b) the door's finding equals runAuthoringRules('build', …)'s on rule, where and message; (c) control: the transitional ${record.name} sentence saves with one advisory; (d) a stored page with a blank or an unbound sentence loads (invalid: 0) and its next save is refused, the row untouched. 10/10.
  • Ablation (scripts/ablation-replace.mjs, the refuse helper's severity: 'error' back to 'warning'; blob dcede89c7bb5 → 60fcaccce37a; ablation-dist-preflight.mjs dist reading PASSED on the mutated build): lint Tests 9 failed | 8 passed, door Tests 9 failed | 1 passed (the passing one is control (c)). Restore leg: blob back to HEAD, git diff HEAD empty, lint rebuilt, preflight --absent exit 0, lint 17/17 and door 10/10 green.
  • pnpm --filter @objectstack/lint test: 161/161 files, 7273 tests; typecheck exit 0.
  • pnpm --filter @objectstack/spec test: 719/719 files, 21779 passed, 1 todo; typecheck exit 0; test:repo 79/79 files, 1269 tests. check:generated green after --fix regenerated api-surface, export-origins and the references, and green again after merging origin/main.
  • pnpm --filter @objectstack/metadata-protocol test (the door's consumer): 263 passed / 3 skipped files, 21485 passed / 19 skipped; typecheck exit 0.
  • pnpm --filter @objectstack/cli exec vitest run --project unit (os validate / compile): 328/328 files, 4816 tests.
  • Full dogfood (pnpm --filter @objectstack/dogfood test, owed by the (narrowing) arm) at c69a7a6d: 295 passed / 3 skipped files, 2520 passed / 18 skipped.
  • Derived gates: dispatch-gates --commands gives 114 families; all 114 exit 0 (three first answered exit 3, prerequisite not met, and were re-run once their prerequisite was built or fetched: check-plugin-teardown-shape --self-test, check:skill-examples, check:dual-build-cjs-loads). --ran reconciles 114/114. Results are in the os-dev-report comment.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/ui.json, packages/spec/export-origins/automation.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/ai/connect-mcp.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/api/client-sdk.mdx (via errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/automation/flows.mdx (via errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/getting-started/build-with-claude-code.mdx (via successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/kernel/cluster.mdx (via ADVISORY (symbol, a top-level const object))
  • content/docs/kernel/contracts/data-engine.mdx (via ADVISORY (symbol, a top-level const object))
  • content/docs/protocol/kernel/i18n-standard.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/protocol/objectui/actions.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/protocol/objectui/record-alert.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/ui/actions.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/ui/translations.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/releases/v17/17-5.mdx (via errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/releases/v17/17-7.mdx (via successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))
  • content/docs/releases/v18/18-1.mdx (via confirmText (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), errorMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS), successMessage (symbol, a field of const object ACTION_COPY_DOLLAR_TRANSITION_ROOTS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/ui.json, packages/spec/export-origins/automation.json, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b77bb37eb2c738c40093acb04f19fa8fc2dd605a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f8b0e522b7a814d3659ff8f17e2514c20d55d364 — the merge of head f03deb5fd2e31334b86c2d27c7578fcc262df33d into base b77bb37eb2c738c40093acb04f19fa8fc2dd605a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f8b0e522b7a814d3659ff8f17e2514c20d55d364 && git checkout f8b0e522b7a814d3659ff8f17e2514c20d55d364
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b77bb37eb2c738c40093acb04f19fa8fc2dd605a f03deb5fd2e31334b86c2d27c7578fcc262df33d && git checkout -B drift-repro b77bb37eb2c738c40093acb04f19fa8fc2dd605a && git merge --no-ff f03deb5fd2e31334b86c2d27c7578fcc262df33d

node scripts/docs-audit/affected-docs.mjs --json b77bb37eb2c738c40093acb04f19fa8fc2dd605a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b77bb37eb2c738c40093acb04f19fa8fc2dd605a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25680 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T19:19Z · head f03deb5fd2 · report 6388146584 · seat review at TIER_DEFAULT. The path limb hits (packages/spec/src/ui/action-copy-template.ts), and the change is a narrowing, so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25680. The Clause-②: no (narrowing), Metadata-protocol: narrowed and Ruling-ref: lines are line-initial in the body and in the changeset (@objectstack/lint and @objectstack/spec minor; the summary line is feat(lint)!:; ADR-0087 not-required (no-migration-prescription) with its reasons). 20 files, +408/−78. Not governed. No content/docs/releases/**.
  2. The ruling, measured on the diff. The refusal half of 6365736632, shaped like spec + lint 18.x: refuse a bulk confirmText hole over an unbound root or a single-brace token at validate / compile / the publish gate (the refusal half of #24798, under the amended 「甲」) #25205 / PR feat(lint)!: refuse a bulk action's confirmText hole over an unbound root, a single-brace token, a hole holding logic and a blank sentence at validate, compile and the publish gate #25451.
  3. Changeset, checked line by line. FROM warning plus advisory → TO error / 422, located at the key; the refused texts enumerated; the one-line fix; the remaining warning, named and bounded; "Unchanged": the parse and blank arm, errorMessage / outcomeMessages outside the family, OS_ALLOW_UNLINTED_METADATA_WRITES, and the two new exports. The producer measurement through built @objectstack/lint at 6021b5e837: 0 findings in examples/** (app-showcase, app-todo, app-crm, app-multi-package), platform-objects (12 confirmText, 48 successMessage) and plugin-approvals (9), and 0 across HotCRM a4b3560's 20 sentences, against 1 for a planted defect each.
  4. Gates. Check-runs on f03deb5fd2 at this record: 41; 39 completed with none failed, 2 still running. Local, per the report:
    • 114/114 derived; three were re-run green after prerequisites were met.
    • lint 161 files / 7273 tests; spec 719 / 21779 and test:repo 79 / 1269; consumers metadata-protocol 263 / 21485 and cli 328 / 4816. The new door block is 10/10. The dogfood battery is 295 files, green.
    • Ablation through lint dist: severity error → warning turns 9 lint and 9 door tests red, then restored blob-identical.
    • Landing waits for every check green and the contract-review PASS.
  5. Deviations accepted. The final head adds only the changeset's Ruling-ref line; the 10 changeset and doc families re-ran on it. mcp_calls 6, all reads (the Writes line forbids MCP writes, not reads). Relay writes 2.
  6. Open questions and out-of-scope outcomes.
  7. Landing. Not governed, 486 changed lines. Once a CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f03deb5fd2e31334b86c2d27c7578fcc262df33d
Local-runs: none
Scope: packages/spec/src/ui/action-copy-template.ts, packages/spec/src/ui/index.ts, packages/spec/src/automation/flow-text-slot-template.ts + packages/lint/src/validate-action-copy-templates.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/validate-expressions.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/lint/src/validate-action-copy-templates.test.ts, packages/spec/src/ui/action-copy-template.test.ts, packages/spec/src/automation/flow-text-slot-template.test.ts, packages/spec/api-surface/ui.json, packages/spec/api-surface/automation.json, packages/spec/export-origins/ui.json, packages/spec/export-origins/automation.json, packages/qa/dogfood/test/expression-conformance.ledger.ts (prose faces .changeset/25680-action-copy-refusal.md, content/docs/references/** read, not scope)

① Derived judgments

② Semver level

'@objectstack/lint': minor + '@objectstack/spec': minor with a feat(lint)!: title, FROM/TO block, one-line fix, and ADR-0087 not-required (no-migration-prescription) is correct: Metadata-protocol: narrowed takes at least minor, and the repair is the author's rewrite of a sentence (which root a hole should name is intent), which no ledger entry can derive. All 47 check runs on head f03deb5fd2 are success or skipped (Test Core 1–6/6, Build Core, Build Docs, Dogfood Regression Gate, Type Check ×5, Lint ×9, Check Changeset ×2, Governed Surface Queue Guard, Spec property liveness, claim gates); none red, none pending.

③ Boundary flags

  • ${…} retirement pair. The transitional warning now has a named end: the seat carries one objectui card (drop the ${…} branch in ActionRunner.confirmMessage / composeSuccessMessage) paired with a spec/lint follow-up that refuses the remaining shape once the pin moves, both filed at this PR's merge (seat 6388461291 item 6). Hold the seat to that filing; the dev's four-axis note that B (refuse now) is the end state is recorded on the card, not re-opened here.
  • Translated copy in translation bundles is not judged, the same boundary spec + lint 18.x: refuse a bulk confirmText hole over an unbound root or a single-brace token at validate / compile / the publish gate (the refusal half of #24798, under the amended 「甲」) #25205 kept. Acceptance notes; no carrier.
  • Changeset Clause-② line carries its arm as (narrowing: …gloss…) rather than the bare (narrowing) the PR body uses. Check Changeset and check-adr-0087-registration passed on it, so the recognisers accept the glossed arm; noted for the record-shape ledger, not a defect.

Implemented-by: session_01WbVg72GQsLxcNHWW17FJZA
Reviewed-by: session_01Bf4vhia9fHNXpqC1PCfxie

VERDICT: PASS

needs:contract-review removed in this stroke. Seat (domain:spec 2): pr_ready + automerge_enable when ready.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 0c81694 Oct 24, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25680-action-copy-refusal branch October 24, 2026 21:54
os-tesla pushed a commit that referenced this pull request Oct 25, 2026
…objectui#13476) (#13482)

<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DYL5YEa1zRjoXpJH1JbeMbZ)_

Fixes #13476

Clause-②: no
Metadata-protocol: narrowed

## Summary

objectui#13367 kept one transitional reading in `ActionRunner`: a
`confirmText` or `successMessage` sentence with no `{{ }}` hole still
had its `${…}` tokens filled. This PR removes both branches, so a
holeless sentence is shown exactly as written, the same as any other
copy with no hole. It is the objectui half of
#25702; #25709
(warning to error, delete `ACTION_COPY_DOLLAR_TRANSITION_ROOTS`) waits
for objectstack's `.objectui-sha` to carry this merge.

## Changes

- `confirmMessage`: the `!hasTemplateHoles(text)` branch returns `text`
instead of `templates.evaluate(text)`. The method no longer takes the
evaluator; the param dialog's `interpolate` (still the `${…}` evaluator,
out of scope) now builds it right where it is used.
- `composeSuccessMessage`: a holeless `successMessage` returns
`successCopy` instead of running `substituteScopeTokens`.
- Docblocks updated: `ActionDef.confirmText`,
`ActionDef.successMessage`, the `RUNNER_TEXT` note,
`composeSuccessMessage`, `confirmMessage`, `templateEvaluatorFor`,
`interpolateTarget`'s note and `substituteScopeTokens` (the grammar's
one toast caller is now `outcomeMessages`), plus `hasTemplateHoles` in
`actionCopyTemplate.ts`.
- Docs: `content/docs/core/enhanced-actions.mdx` "Post-success
behaviour" now says `outcomeMessages` reads `${result.*}` and
`successMessage` reads `{{ result.* }}` / `{{ record.* }}`; the
complex-action example's `confirmText` uses a `{{ record.customer_name
}}` hole.
- Changeset `.changeset/13476-holeless-action-copy.md`:
`@object-ui/core` minor (a narrowing; objectui marks breaking changes
`minor`).

**Untouched, per the card:** `outcomeMessages` keeps its `${result.*}`
reading through `substituteScopeTokens`; `errorMessage` is still shown
as written.

The claim's file surface was amended by the seat (card comment
6391456644) for `actionCopyTemplate.ts`, the docs page, and the
app-shell test. Two more test files outside it also drove the old
reading end to end and are converted here:
`packages/plugin-grid/src/__tests__/rowActionRecordHole-12967.test.tsx`
and
`packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx`,
plus
`packages/react/src/hooks/__tests__/useActionTextLocalizer.outcomeMessages-11344.test.tsx`.

## Tests

Every fixture that fed the runner a holeless `${…}` `confirmText` /
`successMessage` now authors the `{{ }}` hole the runner reads, and
three rows pin the new behaviour:

- `ActionRunner.copyTemplate-13367`: "a `${…}` token in a sentence with
no hole is text, not an expression" (header door and row door), and "a
`${result.*}` token in a sentence with no hole is text, not filled".
- `ActionRunner.outcomeMessages-11344`: "rung 2 — a `${result.*}` token
in a successMessage with no hole is text". The "reads the `result` scope
only" row moves to an `outcomeMessages` entry, the one toast rung that
still uses the `${…}` grammar.
- `ActionRunner.rowRecordTemplates-12967`: the row-less control now
reads `Pause ?` (an unbound `{{ record.* }}` root prints empty) where
the evaluator left `${record.display_name}` raw.

Runs, at head `55dfcfd`, from the repo root:

- `pnpm exec vitest run packages/core/src/actions/`: 41 files, 625 tests
passed.
- Converted and neighbouring suites (app-shell invite feedback, toolbar
script selection, `onSuccess.navigate` hosts, param-dialog record holes,
`consoleServerAction`, components `renderers/action/`, element-button
i18n copy, plugin-grid row-action record hole, console registry parity,
react `hooks/`, app-shell `hooks/__tests__/`, record-alert, ListView).
The first sweep (44 files, 968 tests) failed 3 rows in
`rowActionRecordHole-12967` and `toolbarScriptSelection-12331`, which
still authored `${…}`; after converting them, those 2 files pass (11
tests). The second sweep: 120 files, 1233 tests passed.
- `pnpm --filter @object-ui/core type-check`: exit 0, after building the
core dependency closure.

**Ablation** (commit first, then mutate, trap-restored): with
`ActionRunner.ts` reverted to `origin/main`, the three new rows fail
(`Tests 3 failed | 45 passed (48)`); restored, `git diff HEAD` is empty
and the blob hash matches HEAD.

## Gates (local subset)

- `check:new-line-citations`, `check:control-bytes`, `check:doc-fences`,
`check:doc-types`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check:action-forward-parity`: exit
0.
- `node scripts/check-changeset-presence.mjs`: passes, 1 changeset
declared.
- NOT MEASURED locally: `check:doc-snippets` and `check:doc-examples`
(exit 2, precondition: the doc-snippet build closure is not built here).
CI runs both; the docs edit changes one string literal inside a typed
snippet.
- Full `pnpm test` and `pnpm lint` are CI's.

## Acceptance notes

- No in-tree producer authors the holeless `${…}` shape: no non-test
`confirmText` / `successMessage` in this repo carries `${`, and
objectstack's measurement (card) found none in examples,
`platform-objects`, `plugin-approvals` or HotCRM.
- Fixtures left as `${…}` on purpose: `element-button-i18nCopy-12757`
and `action-outcomeMessages-forward-11344` (components) only pin that
copy reaches the runner unchanged, or never reach rung 2; the react
localizer's `outcomeMessages` fixtures are rung 1, which keeps
`${result.*}`.
- `ActionParamDialog.tsx`'s `interpolate` docblock still says it fills
`${...}` "in the template scope the runner reads `confirmText` in". The
record binding is still shared; the dialect differs now. Carrier: none,
noted only.

- [x] Adds no product-specific widget, dialog, hook or string (see
AGENTS.md › Product-neutral renderer)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01P5j5dvWsBeNZNp2qLwwRqH)_

---------

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

3 participants