Repository navigation
feat(automation)!: a flow's successMessage / errorMessage are {{ }} text slots over the run's variables - #28720
Conversation
…slots Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…templates Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ement Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…veness rows Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ow-message-template Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
📓 Docs Drift CheckThis PR changes 3 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 33bf73b9a77c5c0c353a21e5252a45d2e19d9921 && git checkout 33bf73b9a77c5c0c353a21e5252a45d2e19d9921
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7e6e8a464799af6271410ec6b4b3e119716e2f3a 5db38c6984c22d07ce726fd1c0641b0bb1128fb2 && git checkout -B drift-repro 7e6e8a464799af6271410ec6b4b3e119716e2f3a && git merge --no-ff 5db38c6984c22d07ce726fd1c0641b0bb1128fb2
node scripts/docs-audit/affected-docs.mjs --json 7e6e8a464799af6271410ec6b4b3e119716e2f3a
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ow-message-template Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
successMessage / errorMessage are {{ }} text slots over the run's variablessuccessMessage / errorMessage are {{ }} text slots over the run's variables
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 维护者速读(席位终稿 · head
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix. Re-write Should-fix. The doubled Implemented-by: VERDICT: FAIL Generated by Claude Code |
…wire; name the flow-level slot once The trigger-path pin asserted the old verbatim contract with a single-brace token, which registerFlow now refuses. It now pins three cases on the wire: a brace-free hostile string byte for byte, a filled double-brace hole, and the single-brace spelling refused at registration. The two doors' diagnostics no longer repeat the word flow for the flow-level slots. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 席位核对 · delta for the FAIL 6561130776 · card #28054 · 2026-11-01T17:27ZThe new head is
Dev runs at
Trap for the record: Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 5db38c6 · delta from cccbc85 (the FAIL 6561130776's must-fix and should-fix, plus one comment-only JSDoc line) |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: The head moved past this seat's own record on this PR, so the delta re-review is this seat's ( Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Delta record. This is the same reviewer's delta review of the FAIL in comment 6561130776 on head Interdiff
① Derived judgments
② Semver levelUntouched by the delta. No changeset, baseline, ledger or schema shape moved; the interdiff is two test files, two diagnostics strings, one test re-pin and two JSDoc lines. ③ Boundary flags
④ Design judgmentCarried forward. The delta touches one Must-fix. None. Should-fix. None. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Landing stroke · 2026-11-01T18:03Z · head
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #28054
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no page under
skills/**teaches a flow's ownsuccessMessage/errorMessage. The two docs pages that do (content/docs/automation/flows.mdx, the flow property table, andcontent/docs/ui/translations.mdx, the server-rendered group) are updated in this diff.维护者速读
successMessage/errorMessage现在是{{ }}文本槽,填的是本次运行的变量。引擎在每个结束出口渲染它们:首次运行、重试、恢复,失败时取失败那一刻的变量。和end节点的message一样,按运行的语言先选翻译模板再填洞。注册、os validate和发布这几道门也按节点文本槽的规则判这两个键。控制台不再自己叠加翻译,服务端发什么就显示什么。'{{enrolledCount}} contacts enrolled',用户看到的就是带花括号的原文。{{的消息逐字节不变。第一方的流程消息里含{或{{的有 0 条(实测,唯一带这两个键的examples/app-crm的 convert-lead 也不含)。只有写了单花括号{var}或编译不过的{{ }}的流程,会在注册时被拒,报错会给出{{ var }}的写法。spec、service-automation、lint 都升 minor。回滚就是 revert 这个 PR。packages/services/service-automation/src/node-text-rendering.ts的renderFlowTerminalMessage,dag-traversal.ts/run-resume.ts/engine.ts的结束出口,packages/spec/src/automation/flow-text-slot-template.ts的FLOW_TEXT_SLOTS。pnpm --filter @objectstack/service-automation exec vitest run src/flow-terminal-message-template.test.ts,预期 17 passed。其中首次运行和恢复两条路径都应答出 "3 contacts enrolled"。What changes
packages/spec/src/automation/flow-text-slot-template.tsaddsFLOW_TEXT_SLOTS(successMessage,errorMessage) andflowTextSlotSources(flow). The doors read these the way they readflowNodeTextSlotSources. The flow-level slots are left to the doors, not the parse, which is how a node slot other thanendis judged.flow.zod.ts: the docblock line saying{var}is NOT interpolated is flipped, and both describes now say these are{{ }}templates over the run's variables. The describes ofAutomationResult(contracts/automation-service.ts) and of the automation API response andFlowFailureDetails(api/automation-api.zod.ts) now say rendered, not copied.system/i18n-resolver.tsaddsflowTerminalMessageKey(flow, key), which spellsflows.FLOW.successMessage/.errorMessage, asflowRefusalMessageKeydoes for a refusal.system/translation.zod.ts: the two translation leaves are nowflowTextSlotTranslation(), the same leaf a screen'stitle/descriptionand a refusal'smessageuse. So a single-brace{token}in a translated toast is refused, and an empty string is accepted.node-text-rendering.tsaddsrenderFlowTerminalMessage, andengine.tsadds a wrapper of the same name, in the [maintainer] refactor(service-automation): splitsrc/engine.ts(13,735 lines, +2,199 in 30 days) along itsAutomationEngineseams, one move-only slice per PR #25222 split pattern. It hands the slot to the existingrenderFlowTextSlot, the function the refusingendnode'smessagegoes through. So the translation is picked first inAutomationContext.locale(translatedFlowTemplate, through the i18n service), then the holes are filled byrenderTextSlot. Every terminal exit is covered:execute(): success and failure.dag-traversal.tsexecuteWithoutRetry: success, and failure over the variables at the failure point,$errorincluded.run-resume.tsresumeInternal: success, and the stranded-run failure.retryExecution: each attempt renders with the template dispatched for the run. The exhausted exit reports the last attempt's rendering. An attempt that never reached the DAG renders over an empty map.{{ x | currency }}hole with no tenant default currency) is left off the result with one warning, and the run's verdict stands.grepforflow.successMessage/flow.errorMessage:engine.ts,dag-traversal.ts,run-resume.ts: rendered.map-node.ts,subflow-node.ts: comment mentions only. Neither forwards a child flow's message to its parent's result, so there is nothing to render.flow-registration-checks.ts(registerFlow) andpackages/lint/src/validate-expressions.ts(objectstack validate, compile, the publish gate) judge both keys as they judge a node text slot:textSlotTemplateRefusalfor a single-brace token, with the same words as anendnode.validateExpression('template', …)for a hole that does not compile.flows[i].successMessage.ui/packages/app-shellflowResponse.ts/flowLaunch.ts/FlowRunner.tsx/RecordDetailView.tsx/useConsoleActionRuntime.tsx: the client overlay offlows.FLOW.successMessage/.errorMessageis removed. The engine now serves them translated and filled, and an overlay would put the bundle's template, holes raw, over the filled sentence. This is the A screen flow's select options and screendescriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 ruling A, as already applied to the screen heading.launchConsoleFlowtakeslanguagein place oftranslation: { i18n, language }.interpretFlowResponsetakes a locale string.FlowCopyLocaleandtranslatedFlowMessageare gone.packages/spec/liveness/translation.json: the twoflows.*rows are repointed from the console to the engine and staylive.packages/spec/dropped-refinements.baseline.json: 22 new sites, the two leaves under each of 11 published schemas. Hand-edited as that ledger requires, with the exact list the build printed. It is the same dropped.refine()the sibling leavesscreens.title/descriptionandrefusals.messagealready carry.gen:schema,gen:docs,check:generated --fix):content/docs/references/{api/automation-api,automation/flow,system/translation}.mdx,packages/spec/api-surface/{automation,system}.json,packages/spec/export-origins/{automation,system}.json.content/docs/automation/flows.mdxandcontent/docs/ui/translations.mdxare updated..changeset/28054-flow-message-template.mdcovers spec, service-automation and lint (minor each), plus app-shell, and carries both declaration lines.Tests
packages/services/service-automation/src/flow-terminal-message-template.test.ts(new, 17 tests):'{{enrolledCount}} contacts enrolled'withenrolledCount = 3answers "3 contacts enrolled" on the first-run path and on the resume path, and over a later retry attempt's variables.errorMessagefilled at the failure point, a{{ $error.message }}hole, and the retry-exhausted exit.renderTextSlot(template, new Map())for any text slot.registerFlowrefuses a single-brace token with the sametextSlotTemplateRefusalwords it gives anendnode.registerFlowrefuses a malformed{{ enrolledCount + 1 }}with the samevalidateExpressionmessage at flow and at node level.flows.enrol.successMessage.packages/spec/src/automation/flow-text-slot-template.test.ts:FLOW_TEXT_SLOTSkeys equalFLOW_TERMINAL_MESSAGE_KEYS. Also coversflowTextSlotSourcesand the judge on{enrolledCount}.packages/spec/src/system/translation.test.ts: the toasts are keyed as templates that keep their holes. A single brace in a translated toast is refused at['flows','enrol',key], and an empty string is accepted.packages/lint/src/validate-expressions.test.ts: both keys checked forwhereandpath. A single brace and a malformed hole get the end node's message, and well-formed messages are clean.flowResponse.test.ts,flowLaunch.translation-12075.test.tsxandFlowRunner.flowsTranslation-12075.test.tsxflipped to pin the toasts as served. Each flipped pin asserts the served string, and that the bundle template is not drawn over it.scripts/ablation-replace.mjsmaderenderFlowTerminalMessagereturn the authored text, which is the old copy. It reported "ok mutation landed: anchor 1 -> 0". The new file went 11 failed / 6 passed: every render pin went red, and the door, control and key pins stayed green as expected. The file was restored withgit checkout HEAD -- pathunder a trap, andgit diff HEADwas empty afterwards.Acceptance notes
{token}and a{{ }}that does not compile; both were accepted before. Rendering{{ }}changes meaning and widens no accept set. So the PR declaresClause-②: no (narrowing)/Metadata-protocol: narrowed(AGENTS.md step 3: a value refused isnarrowed; several ⇒ narrowed over semantic). The changeset carries the BREAKING banner, the remedy andadr-0087: not-required (no-migration-prescription). Measured reach: 0 first-party flow messages contain{. The(narrowing)arm's dogfood battery ran atcccbc85418: 371 files passed / 3 skipped, 2994 tests passed / 28 skipped, 0 failed.FlowTextTemplateErrorthere (the only known case is acurrencyhole with no tenant default) leaves the message off the result with one warning, so the caller shows its own sentence and the verdict and history row stand. Anendnode's refusal throws instead, because there the refusal is the verdict.lint-flow-patterns.tsbare-$advisory still covers node slots only.flows.FLOW.successMessage/.errorMessagebefore it renders, exactly asrenderRefusalMessageand the screen executor do. The console overlay is removed, per A screen flow's select options and screendescriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 ruling A.Verification
Suites below at head
e99d244155; the declaration commit and the merge oforigin/mainthat followed (headcccbc85418) re-ran the changeset gates (check:adr-0087-registration,check-changeset-no-major,check-empty-changeset: exit 0), regeneration (all 14 artefacts up to date) and the full dogfood battery (exit 0). Every exit code below was written to a file before any pipe.@objectstack/service-automation...,@objectstack/lint...) under the verify lockgen:schema,gen:docs, thencheck:generated --fix(regenerated api-surface and export-origins, re-checked green)ui/packages/app-shelltype-check(after building its workspace deps)git diff HEADempty--ran--ranexit 0check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET): it needs every workspace package'sdist, and 34 are not built in this container. Nothing was measured; CI's build job runs it.check:skill-examplesandcheck:i18n-walk-parityfirst exited 3 for the same reason. After building@objectstack/client-reactand@objectstack/clieach re-ran with exit 0, and the table records the re-run.dispatch-gates --ran reconciliation
Every gate command with its recorded exit code (112)
Ablation evidence
Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.
🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V