Repository navigation
spec + lint 18.x: refuse an action's copy-template findings (blank text, an unbound root, a ${…} or single-brace token) at validate / compile / the publish gate — the refusal half of #24814 (ruling 6365736632) #25680
Description
Filing gate ③, a follow-up owed by landing work: the refusal half of ruling 6365736632 on #24814, which orders this card filed when #24814 lands. #24814 landed with PR #25637 (384bb7ba02). Filed by domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT. ⛔ Not a claim. Grade and routing are triage's; the bulk analogue #25205 was graded p1.
Why it is a separate card
Ruling 6365736632, 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."
PR #25637 typed the 9 page action-row slots (confirmText, successMessage, errorMessage on ActionButtonProps, ActionIconProps and the action-container member shape) and ActionSchema.confirmText / successMessage as template slots. It ships them widening only, as the module itself records (packages/spec/src/ui/action-copy-template.ts about :40–:46): "Both still parse: the slot keeps a blank-string arm, and the doors' findings are WARNINGS — an advisory at the publish gate, never a refusal. Refusing them is a narrowing no ADR-0087 conversion carries, so it is its own card." Every such sentence parsed while these keys were z.string() / I18nLabel, so refusing it is a narrowing. Under the amended 「甲」 it ships in 18.x as its own BREAKING minor, as #25205 did for the bulk slot.
Scope (for triage)
- In
packages/lint/src/validate-action-copy-templates.ts, promote the findingsjudgeActionemits (about:118,severity: 'warning'with theADVISORYsuffix) fromwarningtoerrorat all three doors:os validate,compile, and the publish gate, which then answers its 422. The findings are a blank sentence, a hole over a root the key does not bind, a single-brace{x}token, and a${…}token, where the judge already prescribes the{{ }}hole. - Decide the blank-string arm in
actionCopyTemplateSlot(action-copy-template.tsabout:146,z.string().regex(/^\s*$/)): refuse it as other template slots do, or keep it. Say which and why. 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 is the precedent to read first. - The
${…}transition: the console runner still fills${…}in a sentence with no{{ }}hole (the same module's "The${…}spelling" note). Refusing${…}at the doors ends that transition on the authoring side; check objectui's runner is ready for it, or keep${…}a warning and say so. - Changeset: BREAKING minor (FROM warning, save succeeds → TO refused),
Clause-②: no (narrowing),Metadata-protocol: narrowed, and the ADR-0087 disposition. - Measure producers first. PR feat(spec,lint): an action's confirmText, successMessage and errorMessage are template slots over the roots the console runner binds #25637 converted the 4
platform-objectssentences and the 9 inplugin-approvals. Re-measureexamples/**(files that carry these keys today includeapp-showcase/src/ui/actions/index.ts,app-showcase/src/ui/views/project.view.ts,app-todo/src/actions/task.actions.ts,app-crm/src/flows/convert-lead.flow.ts) and HotCRM's action sentences against the judge.
Order: after PR #25637 (merged). ActionSchema.errorMessage and outcomeMessages stay outside the family (the contract review 6385469184 on PR #25637, open question 1), so they are not in this card.
Dedupe
Parent: #24814 (closed by PR #25637). Semantic issue search "refuse action confirmText successMessage errorMessage template warning to error narrowing": 1 hit, #24814 itself. Control query "bulk confirmText refusal unbound root single-brace token publish gate": 2 hits, including #25205, the bulk analogue. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 24, 2026
Triage: first grade —
enhancement·priority:p1·domain:spec·area:studio·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-24T15:59Z⛔ Not a claim, ⛔ not a dispatch.
6365736632on spec: type the remaining person-facing text slots astemplateslots —confirmText×3,successMessage/errorMessage×6,emptyText×2 (ADR-0032 Decision 2 and 3; the same mechanism as #24798) #24814 ordered filed once spec: type the remaining person-facing text slots astemplateslots —confirmText×3,successMessage/errorMessage×6,emptyText×2 (ADR-0032 Decision 2 and 3; the same mechanism as #24798) #24814 landed (PR feat(spec,lint): an action's confirmText, successMessage and errorMessage are template slots over the roots the console runner binds #25637,384bb7ba02). It ships in 18.x as a BREAKING minor, as 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 did.domain:spec(packages/lintandpackages/spec). Read 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 on the blank-string arm first. For${…}, confirm objectui's runner no longer needs the fallback before refusing it; otherwise keep it a warning and say so.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 2
Session:
session_01PC4NPXPgPmpDRG9DdDnyXTAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-25680-action-copy-refusalWorktree:
objectstack-issue-25680Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface:
packages/lint/src/validate-action-copy-templates.ts: promotejudgeAction's findings (about:118,severity: 'warning'with theADVISORYsuffix) toerroratos validate,compileand the publish gate. Also the blank-string arm ofactionCopyTemplateSlotinpackages/spec/src/ui/action-copy-template.ts(about:146), kept or refused, read against #25205's answer. Also the module's "Widening only" note (about:40–:46), its tests, producers the judge now refuses (examples/**, andplatform-objects/plugin-approvalsif any), and one changeset.changeset/25680-action-copy-refusal.md. (stop on breach; explain in the report)Container & model:
M,mode:cloud(packages/spec/src/**pluspackages/lintplus a producer sweep: build-heavy),model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate),effort: high(p1, BREAKING). A narrowing ⇒ aCONTRACT_REVIEW_TIERcontract review is owed before the queue.Clause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: n/a — not a defect card
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"); triage6386348015Thread-read: 6386348015
Serial constraints cleared: none. No open PR touches these files (all 15 open PRs' file lists read 2026-10-24T16:41Z), and no open
pm:dispatchedclaim declares them. PR #25637 (#24814) landed as384bb7ba02.Premise: holds on
origin/main6021b5e837. The findings are emitted asseverity: 'warning'(validate-action-copy-templates.tsabout:118), and the blank arm parses (action-copy-template.tsabout:146). Lead to verify, not a fact: whether objectui's runner still needs the${…}fallback. If it does,${…}stays a warning and the PR says so (triage6386348015).Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Dispatched · round 2 ·
domain:specseat 2 · 2026-10-24T16:47Zmode:cloud, cloud sessionsession_01WbVg72GQsLxcNHWW17FJZA(created by this seat's session throughcreate_session), on branchclaude/issue-25680-action-copy-refusal. The claim6386817021stays the identity.6365736632and triage6386348015verbatim, 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 as the precedent for the blank arm, the${…}runner lead to verify before refusing it, a producer measurement first, the BREAKING changeset terms, theWrites:line.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
os-dev-report
{ "issue": 25680, "status": "done", "branch": "claude/issue-25680-action-copy-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/25702", "session": "session_01WbVg72GQsLxcNHWW17FJZA — this run (dispatched by session_01PC4NPXPgPmpDRG9DdDnyXT)", "premise_still_valid": true, "summary": "Premise re-measured on origin/main 6021b5e837: judgeAction emitted every finding as severity 'warning' with the ADVISORY suffix, and the blank arm parsed. The action copy pass now emits a blank sentence, an unbound-root hole, a single-brace token, a hole holding logic or an unknown formatter, and a raw-shown ${…} token as 'error' (validate/compile fail, publish gate 422 INVALID_METADATA on action/object/page writes and draft promotion). ${…} lead measured at the pinned .objectui-sha 2958f17e9c: ActionRunner.confirmMessage still evaluates a holeless confirmText's ${…} (record) and composeSuccessMessage still substitutes ${result.*} in a holeless successMessage, so exactly that shape stays a WARNING (ACTION_COPY_DOLLAR_TRANSITION_ROOTS in spec/ui, shape read by dollarOnlyTokenRoots in spec/automation); every other ${…} (errorMessage, beside a {{ }} hole or a {x}, params in confirmText, record in successMessage) is shown raw and is an error. Blank arm KEPT, following #25205's answer: the doors refuse a blank, the parse stays so stored rows keep loading (door test d). Producers measured before the change: 0 findings across examples/**, platform-objects, plugin-approvals and HotCRM a4b3560's 20 action sentences; none converted. PR assignee set to the card's (sunhaolin) at pr_create.", "tests": "Head c69a7a6d (final f03deb5f adds only the changeset Ruling-ref line; 10 changeset/doc families re-ran on it, all exit 0). lint test 161/161 files 7273 tests, typecheck 0; spec test 719/719 files 21779 passed 1 todo, typecheck 0, test:repo 79/79 files 1269 tests; metadata-protocol test 263 passed/3 skipped files 21485 passed/19 skipped, typecheck 0; cli unit 328/328 files 4816 tests; validate-action-copy-templates.test.ts 17/17; door block (protocol.runtime-authoring-gate.test.ts -t 25680) 10/10. Ablation: ablation-replace.mjs mutated the refuse helper's severity 'error' -> 'warning' (blob dcede89c7bb5 -> 60fcaccce37a), lint rebuilt, ablation-dist-preflight dist reading PASSED (tree reading exit 3 = dirty tree, expected mid-ablation); lint Tests 9 failed | 8 passed, door Tests 9 failed | 1 passed (control c). Restore: blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent exit 0, lint 17/17, door 10/10. check:generated green after --fix (api-surface, export-origins, references) and again after merging origin/main.", "gates": "Local: dispatch-gates --commands --repo objectstack-ai/objectstack at 950fa3c3 derived 114 families; all 114 run, exit codes captured to file before any pipe. 111 exit 0 on first run; 3 exit 3 (PREREQUISITE NOT MET, nothing measured) then exit 0 once the prerequisite was supplied: node scripts/check-plugin-teardown-shape.mjs --self-test; pnpm --filter @objectstack/spec run check:skill-examples; pnpm check:dual-build-cjs-loads (teardown fixture commit fetched; client-react/studio/embedder-openai/knowledge-*/service-cluster-redis/service-knowledge built). --ran: 'Run reconciliation — 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0; the record carried no exit codes, so its zero is the runner's claim; per-command codes are in the list below). Full list: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 | node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 | node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 | node scripts/check-changeset-no-major.mjs --self-test :: exit 0 | node scripts/check-ci-filter-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-adoption.mjs :: exit 0 | node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-dev-prereqs.mjs --self-test :: exit 0 | node scripts/check-doc-frontmatter.mjs :: exit 0 | node scripts/check-doc-frontmatter.mjs --self-test :: exit 0 | node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 | node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 | node scripts/check-docs-section-name.mjs :: exit 0 | node scripts/check-docs-section-name.mjs --self-test :: exit 0 | node scripts/check-dts-emitted.mjs --self-test :: exit 0 | node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 | node scripts/check-empty-changeset.mjs --self-test :: exit 0 | node scripts/check-issue-citations.mjs :: exit 0 | node scripts/check-keyed-text-bounds.mjs :: exit 0 | node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 | node scripts/check-platform-object-tenancy-census.mjs :: exit 0 | node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 | node scripts/check-plugin-teardown-shape.mjs :: exit 0 | node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 | node scripts/check-position-name-fold-loaders.mjs :: exit 0 | node scripts/check-position-name-fold-loaders.mjs --self-test :: exit 0 | node scripts/check-registry-log-declared.mjs :: exit 0 | node scripts/check-registry-log-declared.mjs --self-test :: exit 0 | node scripts/check-rest-log-spy-declared.mjs :: exit 0 | node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 | node scripts/check-section-landing-index.mjs :: exit 0 | node scripts/check-section-landing-index.mjs --self-test :: exit 0 | node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0 | node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0 | node scripts/check-system-context-census.mjs :: exit 0 | node scripts/check-system-context-census.mjs --self-test :: exit 0 | node scripts/check-undeclared-dep-imports.mjs :: exit 0 | node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 | node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 | node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 | node scripts/docs-audit/check-affected-docs.mjs :: exit 0 | node scripts/docs-audit/check-drift-comment.mjs :: exit 0 | node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 | node scripts/release-pending-publish.mjs --self-test :: exit 0 | pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 | pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 | pnpm --filter @objectstack/spec run check:api-surface :: exit 0 | pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 | pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 | pnpm --filter @objectstack/spec run check:docs :: exit 0 | pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm --filter @objectstack/spec run check:empty-state :: exit 0 | pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 | pnpm --filter @objectstack/spec run check:export-origins :: exit 0 | pnpm --filter @objectstack/spec run check:exported-any :: exit 0 | pnpm --filter @objectstack/spec run check:generated :: exit 0 | pnpm --filter @objectstack/spec run check:liveness :: exit 0 | pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 | pnpm --filter @objectstack/spec run check:skill-examples :: exit 0 | pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 | pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 | pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 | pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 | pnpm check:changeset-gate-self-tests :: exit 0 | pnpm check:closed-ceremony-text :: exit 0 | pnpm check:corpus-claim-drift :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:doc-anchors :: exit 0 | pnpm check:doc-authoring :: exit 0 | pnpm check:docs-audit-scope :: exit 0 | pnpm check:docs-redirects :: exit 0 | pnpm check:docs-single-h1 :: exit 0 | pnpm check:docs-spec-enumerations :: exit 0 | pnpm check:docs-transcript-drift :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:dts-closure :: exit 0 | pnpm check:dual-build-cjs-loads :: exit 0 | pnpm check:durability-log-level :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:issue-citations :: exit 0 | pnpm check:lean-entry-closure :: exit 0 | pnpm check:logger-receiver-detach :: exit 0 | pnpm check:merge-driver :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:objectql-double-limit :: exit 0 | pnpm check:objectui-changeset :: exit 0 | pnpm check:org-identifier :: exit 0 | pnpm check:page-declaration-shape :: exit 0 | pnpm check:pm-changeset-deadline-census :: exit 0 | pnpm check:pm-prior-rulings :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:published-readme-links :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:quick-reference-counts :: exit 0 | pnpm check:react-page-adapter-contract :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:role-word :: exit 0 | pnpm check:skill-identifier-liveness :: exit 0 | pnpm check:slot-lookup :: exit 0 | pnpm check:sourcemap-no-sources-content :: exit 0 | pnpm check:spec-parsed-alias :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:vendor-version-stamps :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0. CI on PR #25702 head f03deb5f at report time: 'No other open PR may claim the same issue' success; Required contexts (Lint & Repo Gates jobs, TypeScript Type Check lanes, Governed Surface Queue Guard, Check Changeset, card-claims-branch) in_progress; Test Core / Build Core not yet listed — in_progress, not waited on per the role file.", "dogfood_battery": "c69a7a6d · os-verify-lock: VERDICT command-exit 0 · Test Files 295 passed | 3 skipped (298) · Tests 2520 passed | 18 skipped (2538) · no failures", "mcp_calls": "6 — all reads: issue_read get x2 (#25680, #25205), issue_read get_comments x1 (#25680), pull_request_read get x1 + get_files x1 (#25451), pull_request_read get_check_runs x1 (#25702). No MCP write.", "api_writes": "2 relay writes — fleet-write pr_create (request fw-20261024T184701Z-fc4f64, run 40122288240, read-back 9432/9432 bytes identical, assignee sunhaolin), and this os-dev-report comment via post-stamped.mjs. Plus git push (not REST).", "open_questions": [ { "question": "${…} transition: refuse it now, or keep the transitional shape a warning until objectui retires the reading?", "options": [ "A keep (shipped): warning exactly where the pinned ActionRunner fills it; refuse once objectui drops the ${…} branch in confirmMessage/composeSuccessMessage and the pin moves", "B refuse every ${…} now: stored rows still load and the runner still fills them, so only new authoring is refused" ], "recommendation": "A, because the dispatch's measured condition held (the runner still fills it) and the triage ordered a warning in that case. Four axes: business need — 0 in-repo or HotCRM producers write ${…} copy, so the warning protects no measured author; long-term — B is the end state and A carries a transition that needs a named retirement; AI-error — B is stricter (a loud refusal beats a lenient reading), A at least prescribes the {{ }} hole in its warning; startup focus — 'transitions are not staged by default' favours B. The axes lean B; the ruled lead decided A. Recommend the seat file the objectui-side retirement (drop the ${…} branch) plus the follow-up refusal as one pair, maintainer to rule whether B ships before the objectui half." }, { "question": "Precision of the warning set (implementation trade-off, decided)", "options": [ "A per-key measured roots: record (confirmText), result (successMessage), none (errorMessage) — shipped", "B any ${…} in a holeless sentence" ], "recommendation": "A, because a ${…} the runner leaves raw (e.g. ${record.name} in a successMessage, which substituteScopeTokens over {result} does not touch) is shown as literal text, so warning about it would advertise a fill the runtime does not deliver. confirmText's evaluator also reads other context roots (user etc.), but those have no template spelling in the closed scope, so they are refused as unbound." } ], "out_of_scope_findings": [ "carrier: seat (domain:spec) · the ${…} reading in objectui ActionRunner.confirmMessage / composeSuccessMessage is the last consumer keeping ACTION_COPY_DOLLAR_TRANSITION_ROOTS alive; its retirement unblocks refusing the transitional shape · noted, not filed · dedupe words: ActionRunner ${…} transitional confirmText successMessage retire", "carrier: none (承接者:无) · translated copy strings in translation bundles are not judged by the action copy pass, the same boundary #25205 kept for bulk confirmText · noted in Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
ACCEPT → PR #25702 comment 6388461291 ·
domain:specseat 2 · 2026-10-24T19:20Z · waits for theCONTRACT_REVIEW_TIERreview and green checks.objectstack-fleet commentedon Oct 24, 2026
Landed ·
domain:specseat 2 (seat post #18549) ·sunhaolin· sessionsession_01PC4NPXPgPmpDRG9DdDnyXT· 2026-10-24T22:01Z0c81694006. ACCEPT6388461291; cross-account contract review PASS6388655816on headf03deb5fd2(the landing head)gh-readonly-queue/main/pr-25702-*git ls-remote: 0 refs)origin/main0c81694006;packages/lint/src/validate-action-copy-templates.tsholdsACTION_COPY_DOLLAR_TRANSITION_ROOTS(3 hits)State: the card closed
completedon the merge (Fixes #25680).pm:dispatchedis removed in this stroke, and the dev's cloud session is archived.The
${…}retirement pair is filed, as ACCEPT item 6 committed and the contract review held the seat to:${…}reading of a holeless confirmText / successMessage, now that objectstack refuses every other${…}in action copy (objectstack#25702) objectui#13476: drop the transitional${…}branch inActionRunner.confirmMessage/composeSuccessMessage.outcomeMessages/errorMessagestay out of scope.${record.*}/${result.*}action copy thatACTION_COPY_DOLLAR_TRANSITION_ROOTSstill keeps a warning #25709: once.objectui-shacarries objectui#13476, refuse the remaining holeless${record.*}/${result.*}shape and retireACTION_COPY_DOLLAR_TRANSITION_ROOTS.Translated copy in bundles stays unjudged (the #25205 boundary), recorded in the PR's Acceptance notes with no carrier.
Generated by Claude Code