Skip to content

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

Closed

Description

@objectstack-fleet

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 findings judgeAction emits (about :118, severity: 'warning' with the ADVISORY suffix) from warning to error at 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.ts about :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-objects sentences and the 9 in plugin-approvals. Re-measure examples/** (files that carry these keys today include app-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

  1. added
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    enhancementNew feature or request
    priority:p1High: required for production / M2
    on Oct 24, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25680-action-copy-refusal
    Worktree: objectstack-issue-25680
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/lint/src/validate-action-copy-templates.ts: promote judgeAction's findings (about :118, severity: 'warning' with the ADVISORY suffix) to error at os validate, compile and the publish gate. Also the blank-string arm of actionCopyTemplateSlot in packages/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/**, and platform-objects / plugin-approvals if 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/** plus packages/lint plus 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 ⇒ a CONTRACT_REVIEW_TIER contract 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"); triage 6386348015
    Thread-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:dispatched claim declares them. PR #25637 (#24814) landed as 384bb7ba02.
    Premise: holds on origin/main 6021b5e837. The findings are emitted as severity: 'warning' (validate-action-copy-templates.ts about :118), and the blank arm parses (action-copy-template.ts about :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 (triage 6386348015).


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 2 · domain:spec seat 2 · 2026-10-24T16:47Z


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25702 comment 6388461291 · domain:spec seat 2 · 2026-10-24T19:20Z · waits for the CONTRACT_REVIEW_TIER review and green checks.

  7. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T22:01Z

    reading value
    PR #25702 MERGED through the queue at 2026-10-24T21:54:38Z; merge commit 0c81694006. ACCEPT 6388461291; cross-account contract review PASS 6388655816 on head f03deb5fd2 (the landing head)
    queue branch gh-readonly-queue/main/pr-25702-* gone (git ls-remote: 0 refs)
    origin/main carries 0c81694006; packages/lint/src/validate-action-copy-templates.ts holds ACTION_COPY_DOLLAR_TRANSITION_ROOTS (3 hits)

    State: the card closed completed on the merge (Fixes #25680). pm:dispatched is 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:

    Translated copy in bundles stays unjudged (the #25205 boundary), recorded in the PR's Acceptance notes with no carrier.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:specenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions