Repository navigation
feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate - #24947
Conversation
… and its params Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
… closed-scope judge Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…rmText's holes, warnings in 18.x Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
… view-write gate Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…s for the bulk confirmText slot Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…nd the unknown-members ledger Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…gate's new rule Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…ression-conformance ledger Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 150 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 8e346007298d5c9d474bbb13c64fd762eb40eb79 && git checkout 8e346007298d5c9d474bbb13c64fd762eb40eb79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2919ddb875cd0daa38ef452d741bcba04cf366af 823f523b15b5f621cf38905c6cbc78612a3b8c14 && git checkout -B drift-repro 2919ddb875cd0daa38ef452d741bcba04cf366af && git merge --no-ff 823f523b15b5f621cf38905c6cbc78612a3b8c14
node scripts/docs-audit/affected-docs.mjs --json 2919ddb875cd0daa38ef452d741bcba04cf366af
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…plate envelope Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
|
Re-timed to 18.x (a triage note for this draft). Triage seat · The maintainer amended 「甲」 (#6015
Generated by Claude Code |
…tocol-19 refusal; the changeset declares Metadata-protocol: added Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…onfirmText wording Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…lk-confirm-template-slot
…lk-confirm-template-slot
Fixes #24798
Clause-②: yes (widening)
Metadata-protocol: semantic
The spec half of ruling B on the decision card, step 1: a list-view bulk action's
confirmTextbecomes an ADR-0032templateslot over a closed scope. The scope's roots arecountand the action's params by name. Its holes are judged atobjectstack validate,objectstack compileand the publish gate by the template judge the flow text slots already use.The ruling this executes (comment 6285948976, items 1–2, verbatim)
What changed
The slot.
bulkActionDefs[].confirmText(packages/spec/src/ui/bulk-action.zod.ts) takes the inputTemplateExpressionInputSchematakes: a bare string is shorthand for{ dialect: 'template', source }, and an envelope must declaredialect: 'template'. Like a notify node'stitle/message, it is built from the package-internaltemplateExpressionInputconstructor, so its refusal prescribes this slot's own hole,{{ count }}. The shared{{record.name}}would name a root this slot does not bind. A stored sentence with no hole parses as before, its text unchanged.The declared roots.
bulkConfirmTemplateRoots(def)andBULK_CONFIRM_COUNT_ROOT(@objectstack/spec/ui) returncount, then each param'snamein declaration order.Root spelling: bare (
{{ tier }}), not{{ params.tier }}. The flow text slots' scope writes its roots bare: a flow variable is{{ v }}, a node output{{ lookup.result }}, and nothing is namespaced. HotCRM's 12 sentences already write{{count}}/{{tier}}, and ruling item 3 has them stand unchanged.countwins over a param namedcount; such a param stays reachable only through its dialog field.The display-value rule step 2 waits on, stated in the slot's
.describe(). A param hole renders the param's DISPLAY value:label, with a multi-value param's labels joined by', ';lookupor user param renders the picked record's display name;{{ amount | currency:EUR }},{{ due | date:long }}).A hole holds no logic.
One judge, not a fork.
packages/spec/src/automation/flow-text-slot-template.tsgains a closed-scope entry,closedScopeTemplateRefusal(text, { slot, roots }). It uses the same single-brace token reader, the same hole grammar and the same root rule astextSlotTemplateRefusal; the internals are factored to shared helpers (pathRoot,pathHoles, a predicate ondoubled). The flow judge's verdicts and sentences are unchanged, with a control pin. The judge's envelope reader is exported astemplateSlotSource, so a door reads a template slot the way the judge does.The doors.
validateStackExpressions(packages/lint/src/validate-expressions.ts) is the ruleobjectstack validate,compileand the publish gate all run. It gains a bulkconfirmTextpass with the same two steps as the flow text slots, in the same order:closedScopeTemplateRefusalnames a single-brace token (with its{{ }}rewrite when the root is bound) and a hole over an unbound root;validateExpression('template', …), which names logic or an unknown formatter in a hole.A blank sentence is named too. The walk covers a view's
listandlistViews, a standalone view's flattened list overlay and its ViewItem record (the shapesPUT /meta/viewstores), and an object's ownlistViews. The publish gate's expression rule now declaresviewinruntimeTypes. A view write runs this pass alone, by construction, and an object write also judges its ownlistViews.The 「甲」 rule: what passes today, and why every finding is a warning
Measured on
origin/mainb42c621c05:confirmTextwasz.string().optional(). Every string passed, including an unbound{{ x }}, a single-brace{x}, a hole holding logic ({{ count * 2 }}) and a blank''. Refusing any of them would be a narrowing that no ADR-0087 conversion carries. So, in 18.x:confirmTextis a warning, which is an advisory at the publish gate and never a refusal;z.string().regex(/^\s*$/), apatternthe JSON Schema carries), while the template arm refuses blank as every template slot does;6303865025), the way lint 18.x:page-predicate-over-budgetbecomes an error, refusing a page component props CEL slot over the platform parse bounds (the refusal half of #24953) #25002 shipped the page-predicate bounds. This PR ships only the advisory; the refusal half is 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.f10bc73a0ftook the earlier "refused from protocol 19" promise out of the code, the describe and the changeset.The widening is the envelope form and the declared holes:
Clause-②: yes (widening),minorfor@objectstack/specand@objectstack/lint.Landing order: steps 1 and 2 have landed; this PR is step 3
confirmTextis now{ dialect: 'template', source }, not a string. objectui used to render it as a React child (plugin-grid/src/components/BulkActionDialog.tsx), so the renderer had to move first. It has:cd14d8eeffe1.BulkActionDef.confirmTextisstring | { dialect?, source }, and the dialog renders throughbulkConfirmTextSource..objectui-shatocd14d8eeffe1(main9057fd41f5).823f523b15.The fill step (
{{count}}, then the param roots) stays on objectstack-ai/objectui#13183, as ruling6285948976describes.Why
Fixes, notRefsThe ruling splits the work by repository (ruling D on #24591). This card carries the spec half, and the renderer half is its own card, objectstack-ai/objectui#13183, whose body reads
Blocked-by: objectstack-ai/objectstack#24798. If this card stayed open until the renderer landed, the renderer card would wait on a card that waits on it. So merging the spec half closes this card. The sibling sweep, #24814, is not addressed here; it stays open and follows serially. The landing-order hazard above is a sequencing condition on merging this PR, not a reason to keep the card open.Contract review
This is a published
@objectstack/specschema, so it needs contract review atCONTRACT_REVIEW_TIER. The diff touches no governed surface (.claude/**,docs/adr/**,AGENTS.md,skills/**and.github/workflows/**are all untouched), so it is neither Tier S nor Tier H. It is under 800 changed lines, under the 3000-line human-merge threshold.Tests (final head
823f523b15, after mergingorigin/maintwice:ef2b46b524, then2919ddb875, which carries #25193)test:repo74 / 1269 passed. typecheck andcheck:generatedexit 0.check:test-typecheckOK.expression-conformance8 passed.check-changeset-no-major --eventagainst the live body readsClause-②: yes (widening)andMetadata-protocol: added, and grades nothingpatch.823f523b15: 115 derived, 114 run at exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, which needs a full build; CI owns it).Earlier rounds (head
9fb508c8a0):pnpm --filter @objectstack/spec test→Test Files 704 passed (704),Tests 21379 passed | 1 todo.pnpm --filter @objectstack/lint test→Test Files 152 passed (152),Tests 7009 passed.pnpm --filter @objectstack/dogfood exec vitest run test/expression-conformance.test.ts→ 8 passed. The new slot is discovered and classified (template-bulk-confirm-text,experimental/unevaluated: nothing in this checkout fills the holes yet).pnpm --filter @objectstack/cli exec vitest run --project uniton the seven i18n suites that readconfirmText(i18n-bulk-action-coverage,i18n-coverage,i18n-extract,i18n-walk-output-parity,i18n-declared-surface-gate,i18n-duplicate-demand,i18n-extract-action-description) → 100 passed.pnpm --filter @objectstack/cli typecheck→ OK. The cliintegrationlayer is declared to CI.node scripts/ablation-replace.mjsonpackages/cli/src/utils/i18n-extract.ts,templateSlotSource(def.confirmText)→def.confirmText): the blob went4c0e88d86ced→dc3e90d6ba5f, and the new envelope pin went red (Tests 1 failed | 17 passed). The restore leg brought the blob back to HEAD andgit diff HEADwas empty.pnpm --filter @objectstack/spec --filter @objectstack/lint typecheck→ bothDone.pnpm --filter @objectstack/spec check:generated→ staleapi-surface/,export-origins/and the reference docs, regenerated with--fix(exactly the five new exports and theconfirmTextrows).node scripts/pm/dispatch-gates.mjs --commands, 115 families at9fb508c8a0), reconciled with--ran: 114 run at exit 0, and 1 NOT MEASURED:check:dual-build-cjs-loadsexited 3 (prerequisite not met: it loads the BUILT entry of every package, about 90), which is declared to CI.check:i18n,check:i18n-coverage,check:i18n-walk-parityandcheck:skill-exampleswent green after building the closures they name, andcheck:type-check-debtwent green on a run without a timeout wrapper.eslint.config.mjsfiles: ['**/*.{ts,…}']minusNEVER_LINTED, with--print-configconfirming each touched file is linted;npx eslint --no-inline-config --format jsonover the 15 touched TS files → 15 files, 0 errors, 0 warnings;parserOptions.project, per its own comment ateslint.config.mjs:325), so this diff cannot move a verdict on an untouched file.node scripts/ablation-replace.mjsonpackages/lint/src/authoring-rules.ts, removing'view'fromruntimeTypes): the anchor went 1 → 0 and the blob37ae7e87aacf→82d615ca2df0. All 3 gate positives invalidate-expressions.bulk-confirm-text.test.tswent red (Tests 3 failed | 11 passed), and the 3 controls stayed green. The restore leg brought the blob back to HEAD andgit diff HEADwas empty. Nodistwas involved, since lint's own tests readsrc.Acceptance notes
os i18n extractand an envelope-formconfirmText(the seat extended the claim topackages/cli/src/utils/i18n-extract.ts).pushBulkActionDefsnow reads the slot throughtemplateSlotSource(def.confirmText), so aconfirmTextwritten as{ dialect: 'template', source }is seeded (sourceValue/inline) exactly as the bare string. Before this,authoredEvidenceread a string or an inline locale map only, and the envelope was recorded like an absent key. The new pin is inpackages/cli/test/i18n-bulk-action-coverage.test.ts.confirmTextin a bundle (objects.OBJ._views.VIEW.bulkActions.DEF.confirmText) is still a plain string, and nothing here judges its holes. Ruling item 1 binds the def's slot, and the bundle side is the sibling sweep's question.countis shadowed by the count root (documented in the slot and inbulkConfirmTemplateRoots). No such param exists in the repository corpus.b42c621c05: 6 bulk defs carry aconfirmText(examples/app-showcaseproject.view.ts), none with a brace, so 0 findings at every door. HotCRM's 12 sentences ({{count}},{{tier}}) are pinned clean in both spec and lint tests, the zh-CN form included.Files outside the claim's listed paths, each forced by the slot change and named here:
packages/spec/src/shared/typed-expression-input.ts: one docblock line naming the constructor's new caller;packages/spec/src/ui/component-props-unknown-members.pin.test.ts: the envelope'sastledgered underEXPRESSION_AST, besidebulkActionDefs[].visible.ast;packages/spec/src/ui/action-confirm-params-guard.test.ts: the parsed value is now the envelope;packages/qa/dogfood/test/expression-conformance.ledger.ts: the slot's conformance row;packages/lint/src/{authoring-rule-wiring,runtime-gate,runtime-gate.page-visibility-predicate-writes,validate-expressions}.test.ts: theviewrule lists, and the receiver table of the declared-keys meta-test;packages/cli/src/utils/i18n-extract.tsandpackages/cli/test/i18n-bulk-action-coverage.test.ts: the envelope-form seed and its pin (claim extended by the seat);.changeset/24798-bulk-confirm-template-slot.mdnow also bumps@objectstack/cli.Generated by Claude Code