Repository navigation
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
Conversation
…ompile and the publish gate (WIP) Claude-Session: https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA Co-authored-by: Claude <noreply@anthropic.com>
…the action copy refusal Claude-Session: https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA Co-authored-by: Claude <noreply@anthropic.com>
…tion-copy-refusal
…l changeset Claude-Session: https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
席位核对ACCEPT · #25680 ·
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
…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>
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"); triage6386348015The refusal half of ruling
6365736632on #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-0032templateslots and shipped every door finding as a warning. This PR makes them errors, shaped like #25205 / PR #25451 for the bulk slot.What changes
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 nowseverity: 'error', and theADVISORYsuffix is gone from them. The judge, the walk, the roots and the sentences are unchanged.objectstack validateandcompilefail on anerrorfromvalidateStackExpressions. The publish gate splitserrorsfromadvisoriesby severity (runtime-gate.ts), so an activeaction,objectorpagewrite, and a draft promotion, answer422 INVALID_METADATAwith anexpression-invalidissue at the key. No door code moves.${…}: kept a warning only where objectui's runner still fills it. Measured at the pinned.objectui-sha2958f17e9c(objectuipackages/core/src/actions/ActionRunner.ts):confirmMessagestill evaluates aconfirmTextwith no{{ }}hole through its${…}expression evaluator (whoserecordis the record the action runs against), andcomposeSuccessMessagestill substitutes${result.*}(and nothing else) in asuccessMessagewith no{{ }}hole; anerrorMessageis 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 anerrorMessage, beside a{{ }}hole or a{x}token, overparamsin aconfirmText, or overrecordin asuccessMessage.ACTION_COPY_DOLLAR_TRANSITION_ROOTS(@objectstack/spec/ui). The shape is read by the one template reader:dollarOnlyTokenRoots(@objectstack/spec/automation, besideclosedScopeTemplateRefusal), so lint holds no second reading of the dialect.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.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), theauthoring-rules.tsregistration comment, threevalidate-expressions.tscomments, and the dogfoodexpression-conformance.ledger.tsrows'enforcement/notetext.affected-docs.mjs --json origin/mainnamed 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/25680-action-copy-refusal.md:@objectstack/lintand@objectstack/specminor, both declaration lines, a FROM/TO block, and the ADR-0087 dispositionnot-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/lintat6021b5e837, before the change)examples/app-crm,app-showcase(23 registeredsuccessMessages, 29 pages),app-todo(2confirmText, 16successMessage),app-multi-package: 0 findings.@objectstack/platform-objects(12confirmText, 48successMessage, as one stack of its exported objects): 0.@objectstack/plugin-approvals(9successMessage): 0. Control: a planted{{ nope.x }}drew 1 (a warning before, an error after).a4b3560(read-only clone): its 20 action sentences (1confirmText, 19successMessage,src/{sales,service,marketing}/actions) judged through the same built pass: 0 findings, against an error for a planted{count}. Its bulkconfirmTexts are 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 pass, already measured there.Tests (head
c69a7a6d; final headf03deb5fadds only the changeset'sRuling-ref:line, and the ten changeset / doc families re-ran on it, all exit 0)validate-action-copy-templates.test.ts: every pin flips fromwarningtoerror, plus new cases: a single-brace token and an empty blank as errors; the four raw-shown${…}shapes as errors;${…}in a row'serrorMessageas an error; the transitional${record.*}/${result.*}as warnings; and the gate block now assertserrorswith emptyadvisoriesfor 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:dollarOnlyTokenRootsanswers the roots for an all-${…}holeless sentence andundefinedfor no token, a{{ }}hole, a bare{x}, logic, and an empty string.action-copy-template.test.ts: the transition roots per key.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the realsaveMetaItem: (a) each of four texts on a page row is refused withINVALID_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 equalsrunAuthoringRules('build', …)'s onrule,whereandmessage; (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.scripts/ablation-replace.mjs, the refuse helper'sseverity: 'error'back to'warning'; blobdcede89c7bb5→60fcaccce37a;ablation-dist-preflight.mjsdist reading PASSED on the mutated build): lintTests 9 failed | 8 passed, doorTests 9 failed | 1 passed(the passing one is control (c)). Restore leg: blob back to HEAD,git diff HEADempty, lint rebuilt, preflight--absentexit 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:repo79/79 files, 1269 tests.check:generatedgreen after--fixregenerated api-surface, export-origins and the references, and green again after mergingorigin/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.pnpm --filter @objectstack/dogfood test, owed by the(narrowing)arm) atc69a7a6d: 295 passed / 3 skipped files, 2520 passed / 18 skipped.dispatch-gates --commandsgives 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).--ranreconciles 114/114. Results are in the os-dev-report comment.Acceptance notes
${…}stays a warning where the runner fills it, as the triage ordered. Refusing it is owed once objectui'sActionRunnerretires the${…}reading inconfirmMessageandcomposeSuccessMessage. Who carries that is a seat call; this PR names it in the report and files nothing.ActionSchema.errorMessageandoutcomeMessagesstay outside the family (contract review6385469184on PR feat(spec,lint): an action's confirmText, successMessage and errorMessage are template slots over the roots the console runner binds #25637); a pin asserts a${…}inActionSchema.errorMessagestill draws nothing.CONTRACT_REVIEW_TIERis the seat's. No governed surface is touched (.claude/**,docs/adr/**,AGENTS.md,skills/**,.github/workflows/**).🤖 Generated with Claude Code
https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA
Generated by Claude Code