Skip to content

Commit aead296

Browse files
fix(spec): the flows translation group goes live — the flow runner reads flows.FLOW.label (#21859)
Fixes #20318 Clause-②: no Stage 3 of the card: the objectstack half that follows objectui's new reader. Three things land together. The `flows` translation group goes `live` in the liveness ledger, in both halves. The tests that pinned the old ledger state move with it. And the CLI's coverage walker demands a flow's own label only where the runner can show it, per `domain:cli`'s ruling A on #6024. Every check measured here is green, `check:i18n-coverage` included, and every example sits at its committed baseline. ## The reader, measured at the pin `.objectui-sha` on `main` is `0abd4f9f8769`. `git merge-base --is-ancestor 39a3e91fad 0abd4f9f8769` exits 0, so the pin carries objectui PR 11636. A positive answer proves itself even on a shallow clone. Read at `0abd4f9f8769`, read-only: - `packages/app-shell/src/views/FlowRunner.tsx:268-275` `displayFlowLabel` returns `translateFlow({ name: flowName, label: flowLabel }, bundle, { locale: language }).label || flowName`. That is the active language's `flows.FLOW.label`, then the served `flowLabel`, then the API name. - `:377` computes `shownFlowLabel`. `:541` draws it in the dialog header, above the step's `DialogTitle`. - `:441` hands `displayFlowLabel(...)` to the `flowRunner.completed` completion toast. The toast shows only when the flow authored no `successMessage`. - `:211-229` `activeFlowsBundle` reads the active language's `flows` group from the i18next `translation` resource tree. The `screens` overlay uses the same read. - `translateFlow` is imported from `@objectstack/spec/system` (`:163`). - The served label comes from `packages/app-shell/src/utils/flowResponse.ts:234-235` (`servedFlowLabel`, on the trigger and resume 200 bodies). - The three hosts open the runner with that label: `hooks/useConsoleActionRuntime.tsx:653`, `views/RecordDetailView.tsx:1118`, and `apps/console/src/pages/developer/FlowRunsPage.tsx:310`. - The reader grep from `domain:cli`'s ruling was re-run at the pin. `git grep -E "translateFlow\b|displayFlowLabel"` over `packages/`, excluding tests and stories, still returns 8 hits: 7 in `FlowRunner.tsx` and 1 comment in `metadata-admin/inspectors/flow-nested-selection.ts`. No reader of a non-screen flow's translated label exists, and the pin has not moved. ## What changed - **`packages/spec/liveness/translation.json`** - `flows.children.label`: `planned` to `live`, with `verifiedAt 2026-10-05` and `evidenceScope: cross-repo`. The evidence uses `file#symbol` anchors, with each objectui pointer prefixed `objectui @0abd4f9f8:` as the ledger README prescribes. The `producer` names the engine's `AutomationResult.flowLabel` copy, the objectui lift and hosts, and the bundle delivery chain the `screens` row already cites. - The `flows` container: both children are now `live`, so it is `live` too, with its own evidence. It drops `authorWarn` and `authorHint`, because a `live` row refuses them. - The `screens` row's note gains one dated line saying that its "NOT flipped with it" clause is superseded. - **`packages/spec/liveness/state-counts/translation.md`**: regenerated with `gen:liveness-counts`. `translation` is now `26 live · 0 planned`, up from `25 · 1`. - **`content/docs/ui/translations.mdx`**: the boundary bullet now says what is true. - Both halves are applied: `screens` through the screen copy, and `label` through the runner header and the toast. - Three strings stay outside the group: the screen `description`, an authored `successMessage`, and the runner chrome. - The tooling asks for the keys, and asks for a flow's own `label` only when the flow has a screen node. - The "the day the runner lands" sentence is gone. - **`packages/spec/src/system/translation.zod.ts`**: the `flows` TSDoc said "The flow's own `label` is read by nothing yet". It now names the reader and the pin. This is the JSDoc only. No `.describe()` string moved, so nothing generated was built from it. - **`examples/`**: the three screen wizards get their `flows` copy in every supported locale. That covers `crm_convert_lead_wizard` (zh-CN, 4 strings), `showcase_reassign_wizard` (zh-CN, 3) and `quick_add_task` (zh-CN and ja-JP, 7 each). - **Changesets**, measured with `npm pack --dry-run`: - `.changeset/20318-flows-translation-live.md` is an `@objectstack/spec` patch. `liveness/translation.json`, `liveness/state-counts/translation.md` and `src/system/translation.zod.ts` all ship, and the control `dist/index.js` ships too. - `.changeset/20318-cli-flow-label-demand.md` is an `@objectstack/cli` patch. `dist/utils/i18n-extract.js` ships (597 files packed; the control `package.json` is present), and the built file carries the predicate. - `content/docs` and `examples/` publish nothing. ### The flow label is demanded only for a flow the runner can open (`domain:cli` ruling A) - **`packages/cli/src/utils/i18n-extract.ts`**: one predicate in `walkScreenFlows`. The `flows.FLOW.label` entry is emitted only when `collectFlowNodesDeep`, the node universe the screens walk already reads at any depth, yields a `screen` node. Nothing else in the walker moves: the node list is computed one statement earlier, so the label entry keeps its place ahead of the screen entries. - The predicate's comment names `FlowRunner` as the reader it mirrors (ruling condition (b)). It also says the predicate widens in the same change as any future reader of a non-screen flow's label. - **Pins** in `packages/cli/test/i18n-flow-screen-coverage.test.ts` (ruling condition (a)): - a scheduled flow with a region and no screen node emits no `flows.FLOW.label` entry; - a flow whose only screen node sits two regions deep (a loop body, then a parallel branch) still emits it; - in one tree holding both flows, the coverage gate asks for the screen flow's label and not the other's. - The existing "reads the region TABLE" test now expects `[]` for its flow, whose only screen-shaped object sits in an `http` payload, so the flow has no screen node and owes no label. It gains a lit control: the same flow with one real screen emits the label and that screen, and still not the payload object. - **Control legs**, each run through `scripts/ablation-replace.mjs` and proved by "ok mutation landed" and "ok restored: blob == HEAD (521163a) and `git diff HEAD` is empty". The test imports `../src`, so no build is in the path. - **The predicate removed** (the label pushed unconditionally) reads `Tests 3 failed | 21 passed (24)`: the no-screen pin, the one-tree pin and the region-table test. - **The predicate over the flat top-level `flow.nodes`** instead of the deep universe reads `Tests 2 failed | 22 passed (24)`: the region-nested pin and the one-tree pin. The region-nested pin cannot go red by removing the predicate, since an unconditional label satisfies it, so this second leg is the one that proves it. ### The tests that pinned the old ledger move with the flip These are test and comment edits, with cross-lane declarations to `domain:cli` and `domain:devx`. No pin was deleted. - **`packages/cli/test/i18n-flow-liveness-gate.test.ts`**: its real-ledger pins now read the post-flip ledger. - The census pin: `authorWarnedTranslationGroups()` equals `[]`, still an equality. - Its "can say no" twin gets its positive from the same reader, `authorWarnedProperties('object')` naming `externalSharingModel`. - The walk with the ledger's own set equals the ungated walk, and `flows` is among its roots. - The untranslated wizard draws exactly five `i18n/missing-flow` keys, and the translated one draws none. - The collision pin: omitting the keys draws demand and no warning, and authoring them draws neither. A witness in the same call raises the one warned object row. - The skeleton scaffolds `flows`. - The injected-set pin "gates whatever the ledger names" still holds the gate for a warned group. - **`packages/lint/src/lint-liveness-properties.test.ts`**: - The locale-keyed walk pins and the runtime-`TranslationItem` anti-fixture now read a planted synthetic `planned` + `authorWarn` group row. They go through `lintLivenessPropertiesFromLedgerDir`, the seam the synthetic `field` ledger already uses. - The real-ledger `translation.flows (planned)` case splits into a silence pin on the shipped row (beside a witness that still warns) and a planted twin that holds the planned-with-`authorHint` wording. - The two silence pins now author `flows` too. - The `authorWarnedProperties` census reads `[]` for translation, with the `object` positive. - The anti-drift pin asks over every top-level row the translation ledger governs, and over the `object` type's non-empty warned set. - **Comments only**: - In `packages/cli/src/utils/i18n-extract.ts`, the extract-surface header lists `inlineHelpText` and says why `flows` is walked, and the `authorWarnedTranslationGroups` docblock says the set is empty today. - The header of `packages/cli/test/i18n-flow-screen-coverage.test.ts`. - `translateFlow`'s docblock in `packages/spec/src/system/i18n-resolver.ts`. No new id: no new or rewritten test title, test string, comment headline or runtime string carries a tracker id as its citation. The new pins, the predicate's comment and the CLI changeset sentence cite behaviour and the reader, not a tracker number. ## What dropping `authorWarn` switched on, and what the predicate takes back With the ledger flipped and no predicate, `os lint ... --json` reported 56 new `i18n/missing-flow` findings across the examples: | example | findings | from screen flows | `label` of a flow with no screen node | |:--|--:|--:|--:| | `app-crm` | 4 | 4 | 0 | | `app-showcase` | 32 | 3 | 29 | | `app-todo` (zh-CN + ja-JP) | 20 | 14 | 6 | The 21 screen-flow strings are translated in this PR. The 35 label demands for flows that never open the runner are what the predicate removes. `check:i18n-coverage` now reads "OK (13 config(s), 621 baselined untranslated string(s), none new)". The ratchet is two-sided, so crm, showcase and todo sit exactly at their committed 101, 414 and 106. `scripts/i18n-coverage-baseline.json` is untouched, and none of the 35 inert strings is authored. `check:i18n` stays green. ## Tests and gates (at `bac6651dd7`, every package built with declarations) - The build was `turbo run build --filter=!@objectstack/docs` (72 tasks, exit 0). - CLI unit tier: `Test Files 259 passed (259)`, `Tests 3786 passed (3786)`. That is 3783 plus the three new predicate pins. The packed-`.d.ts` pins are included. - Full `@objectstack/lint` suite: `Tests 5629 passed (5629)`. - `@objectstack/cli` typecheck exit 0, test layer included ("held in test-typecheck-debt.json", unchanged). - The re-pinned files: the CLI screen-coverage file reads `Tests 24 passed (24)` at HEAD. lint `lint-liveness-properties` is unchanged since `dfa18e7699`, where it read `Tests 97 passed (97)`, and it is inside the 5629 above. The flip's control leg on the re-pinned real-ledger pins (pre-flip ledger restored by a trap, proved by blob hash and an empty `git diff HEAD`) read CLI `6 failed | 23 passed` and lint `5 failed | 92 passed`, while the planted pins stayed green. - `check:liveness` exit 0. `check:i18n-coverage` exit 0, as above. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 115. The `--ran` reconciliation reads "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED", and all 115 exited 0. - `origin/main` (`9f9510f25e`) touches none of these 15 files and does not move `.objectui-sha` (still `0abd4f9f8769`; its last change is `8832655af2`). Not merged, not rebased. The one derived-from file main changed, `scripts/engine-double-contract.pinned.json`, gained an objectql test pin unrelated to this diff. ## Acceptance notes - The flip, the re-pins and the predicate are one commit set on purpose. Each re-pinned file pins the real ledger state, and the predicate is what lets the flip's demand side match its reader, so any part landed alone is red. - Ruling condition (c) is still live until this lands: if `.objectui-sha` moves first, re-run the reader grep on the new pin, and stop if a reader of a non-screen flow's translated label has appeared. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9f9510f commit aead296

15 files changed

Lines changed: 471 additions & 186 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os lint` and `os i18n extract` ask for a flow's `flows.<flow>.label` translation only when the flow has a screen node at any depth, the only kind of flow the console's screen-flow runner opens and names, so a scheduled, record-triggered or API flow with no screen no longer draws an `i18n/missing-flow` demand for a label no surface shows.
6+
7+
Clause-②: no
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: the `flows` translation group is `live`, and so are both its children. The console's screen-flow runner now names the flow by `flows.<flow>.label` in the active language, in the runner's header and in its completion toast. A locale the bundle does not cover shows the label authored on the flow, and then the flow's API name. `screens` was already `live`.
6+
7+
Clause-②: no
8+
9+
- The `flows` row drops `authorWarn` and its `authorHint`. `os lint` and `os validate` no longer warn `liveness-planned-property` on a bundle that authors `flows`. A warning is not a refusal, so the accept set is unchanged.
10+
- Dropping that bit switches on the CLI's i18n coverage demand for `flows.*`. `os lint` now reports a `flows.<flow>.*` key that a supported locale is missing as `i18n/missing-flow`, and `os i18n extract` scaffolds the group into the bundle. The flow's own `label` is demanded only for a flow with a screen node (see the `@objectstack/cli` entry). Under `--i18n-strict` a missing key is an error: translate it, or run `os i18n extract` to scaffold it.
11+
- The `flows` TSDoc in `translation.zod.ts` and the translations guide's boundary note now say that both halves are applied.
12+
- ⛔ No schema, parse, export or accept-set change.

‎content/docs/ui/translations.mdx‎

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -382,27 +382,30 @@ Honest limits worth knowing before you plan around them:
382382
by rule name alone and so could not tell two objects' rules apart; the route
383383
above is object-scoped and shipped with its reader. ADR-0049's 2026-09-04
384384
amendment carries that record.
385-
- **The `flows` group is only partly applied.** A screen flow's copy has
386-
somewhere to live (#7646) and the keys are addressed the way the runner
387-
resolves them — flow name, screen node id, screen field name. The console's
388-
screen-flow runner reads `screens`: each screen's `title` and each field's
389-
`label`, `placeholder` and `inlineHelpText` render in the active locale. The
390-
flow's own `label` is read by nothing yet, so the liveness ledger carries the
391-
group as `planned` and the compile lint warns when you author it. The runner's
392-
own chrome — the Cancel and Submit buttons — is deliberately outside the
393-
group: it belongs to the console's message catalog rather than your app's
394-
bundle.
395-
396-
**So the tooling does not ask you for these keys either.** `os lint` does not
397-
report `flows.*` as missing translations, and `os i18n extract` does not
398-
scaffold them into your bundle — both read the same `planned` row. Without
399-
that, the two halves of a single `os lint` run contradicted each other:
400-
omitting the keys was reported as a coverage gap, and adding them was reported
401-
as authoring a group nothing reads. If you author the copy anyway you get the
402-
liveness warning and nothing else — it is telling you the truth, not asking
403-
you to delete a key you will need later. The day the runner lands and the row
404-
flips to `live`, both the coverage report and the extract skeleton pick the
405-
group up on their own; there is no flag to turn on.
385+
- **The `flows` group is applied by the console's screen-flow runner, and its
386+
face is deliberately small.** The keys are addressed the way the runner
387+
resolves them — flow name, screen node id, screen field name — and both
388+
halves render in the active locale:
389+
- `screens` is applied to the screen copy: each screen's `title`, and each
390+
field's `label`, `placeholder` and `inlineHelpText`;
391+
- the flow's own `label` names the flow in the runner's header, above the
392+
step's heading, and in the completion toast. A locale the bundle does not
393+
cover shows the label authored on the flow, and a backend that serves no
394+
label shows the flow's API name.
395+
396+
Three strings stay outside the group. A screen's `description` renders as
397+
authored. A `successMessage` authored on the flow replaces the completion
398+
sentence and renders as authored too, so the translated label appears in the
399+
toast only when the flow declares none. The runner's own chrome — the Cancel
400+
and Submit buttons — belongs to the console's message catalog rather than your
401+
app's bundle.
402+
403+
**The tooling asks for these keys like any other group's.** `os lint` reports
404+
a missing `flows.*` key against `supportedLocales`, and `os i18n extract`
405+
scaffolds the group into your bundle. A flow's own `label` is asked for only
406+
when the flow has a screen node, at any depth, because the runner opens only
407+
on a screen: a scheduled or record-triggered flow with no screen has no
408+
surface that shows its translated label.
406409
- **No ICU MessageFormat** — plural/gender formatting isn't available;
407410
interpolation is always simple `{variable}` substitution.
408411
- **Runtime authoring is process-wide.** The authored layer is synced across all

‎examples/app-crm/src/translations/crm.translation.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,18 @@ export const CrmTranslationBundle = defineTranslationBundle({
210210
},
211211
},
212212
},
213+
// The lead-conversion wizard: the flow's name in the runner header and
214+
// completion toast, and each screen's heading.
215+
flows: {
216+
crm_convert_lead_wizard: {
217+
label: '将线索转化为客户和商机',
218+
screens: {
219+
screen_already_converted: { title: '已转化' },
220+
screen_account: { title: '第 1 步,共 2 步 · 客户' },
221+
screen_opportunity: { title: '第 2 步,共 2 步 · 商机' },
222+
},
223+
},
224+
},
213225
messages: {
214226
'crm.lead.convert.success': '线索已成功转化为商机。',
215227
'crm.lead.convert.error': '线索转化失败,请重试。',

‎examples/app-showcase/src/system/translations/index.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,5 +1088,18 @@ export const ShowcaseTranslationBundle = {
10881088
},
10891089
},
10901090
},
1091+
// The reassign wizard: the flow's name in the runner header and
1092+
// completion toast, its one screen's heading and its field label.
1093+
flows: {
1094+
showcase_reassign_wizard: {
1095+
label: '重新分配任务',
1096+
screens: {
1097+
collect: {
1098+
title: '新负责人',
1099+
fields: { new_assignee: { label: '新负责人' } },
1100+
},
1101+
},
1102+
},
1103+
},
10911104
},
10921105
};

‎examples/app-todo/src/translations/ja-JP.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ export const jaJP: TranslationData = {
9191
description: '個人タスク管理アプリケーション',
9292
},
9393
},
94+
// The quick-add wizard: the flow's name in the runner header and completion
95+
// toast, each screen's heading and each field label.
96+
flows: {
97+
quick_add_task: {
98+
label: 'タスクをすばやく追加',
99+
screens: {
100+
screen_1: {
101+
title: 'タスクの詳細',
102+
fields: {
103+
subject: { label: 'タスクの件名' },
104+
priority: { label: '優先度' },
105+
dueDate: { label: '期日' },
106+
category: { label: 'カテゴリ' },
107+
},
108+
},
109+
success_screen: { title: 'タスクを作成しました' },
110+
},
111+
},
112+
},
94113
// Single-segment `messages` ids — `t()` walks the dot path, so an id
95114
// containing a dot resolves to nothing; see the `en` bundle (#18566).
96115
messages: {

‎examples/app-todo/src/translations/zh-CN.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ export const zhCN: TranslationData = {
9999
description: '个人任务管理应用',
100100
},
101101
},
102+
// The quick-add wizard: the flow's name in the runner header and completion
103+
// toast, each screen's heading and each field label.
104+
flows: {
105+
quick_add_task: {
106+
label: '快速添加任务',
107+
screens: {
108+
screen_1: {
109+
title: '任务详情',
110+
fields: {
111+
subject: { label: '任务主题' },
112+
priority: { label: '优先级' },
113+
dueDate: { label: '截止日期' },
114+
category: { label: '分类' },
115+
},
116+
},
117+
success_screen: { title: '任务已创建' },
118+
},
119+
},
120+
},
102121
// Single-segment `messages` ids — `t()` walks the dot path, so an id
103122
// containing a dot resolves to nothing; see the `en` bundle (#18566).
104123
messages: {

‎packages/cli/src/utils/i18n-extract.ts‎

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@
107107
* flows.<flow>.screens.<node_id>.title (#7646 / #11287)
108108
* flows.<flow>.screens.<node_id>.fields.<field>.label
109109
* flows.<flow>.screens.<node_id>.fields.<field>.placeholder
110-
* ^ gated: `flows` is `planned` + `authorWarn` in the liveness ledger, so
111-
* these are walked only once the row goes `live` (#11624 — see
112-
* `authorWarnedTranslationGroups`)
110+
* flows.<flow>.screens.<node_id>.fields.<field>.inlineHelpText
111+
* ^ walked because the ledger's `flows` row is `live` and warns no
112+
* author; a group the ledger does warn on is left out of the walk
113+
* (see `authorWarnedTranslationGroups`)
113114
* metadataForms.<type>.label / .description
114115
* metadataForms.<type>.sections.<section>.label / .description
115116
* metadataForms.<type>.fields.<dotPath>.label / .helpText / .placeholder
@@ -1067,8 +1068,9 @@ function walkObjectTabs(config: any, out: ExpectedEntry[]): void {
10671068
}
10681069

10691070
/**
1070-
* Translation groups the shipped liveness ledger warns an author for authoring
1071-
* — today exactly `flows` (`status: planned`, `authorWarn: true`).
1071+
* Translation groups the shipped liveness ledger warns an author for authoring.
1072+
* Today the set is empty: `flows`, the one group ever warned, is `live` now
1073+
* that both its halves are read, so nothing is gated.
10721074
*
10731075
* ## Why the walk surface is gated on this at all (#11624)
10741076
*
@@ -1083,20 +1085,20 @@ function walkObjectTabs(config: any, out: ExpectedEntry[]): void {
10831085
* `i18n/missing-*` family. Under `--i18n-strict` the demand side is an error,
10841086
* so a project could be *forced* to author keys it is then warned for.
10851087
*
1086-
* ⛔ The warn side is not the bug and must not be softened. Only part of the
1087-
* group is read: the console's screen-flow runner reads `screens`, but the
1088-
* flow's own `label` is read by nothing yet (#20318), so a translated flow
1089-
* label really is stored and never shown. The warn is group-level, so it still
1090-
* covers the whole group. The demand is the half that is premature.
1088+
* ⛔ The warn side was not the bug, and it was not softened. While the
1089+
* console's screen-flow runner read only `screens`, a translated flow `label`
1090+
* really was stored and never shown, and the group-level warn said so. The
1091+
* demand was the premature half, and it is the half this gate holds back.
1092+
* The runner now names the flow by `flows.<flow>.label` too, so the row is
1093+
* `live` and the warn and the gate are gone together.
10911094
*
10921095
* ## Shape
10931096
*
10941097
* Group-general, not `flows`-specific, and read from the ledger rather than a
1095-
* switch of our own: the day the row flips to `live` (dropping its
1096-
* `authorWarn`; for `flows` that waits on #20318), the bucket turns itself back
1097-
* on with no edit here — and any FUTURE group that acquires a warn is covered on
1098-
* the day it is marked, rather than re-opening this collision one group at a
1099-
* time.
1098+
* switch of our own: when the `flows` row flipped to `live` and dropped its
1099+
* `authorWarn`, the bucket turned itself back on with no edit here — and any
1100+
* FUTURE group that acquires a warn is covered on the day it is marked, rather
1101+
* than re-opening this collision one group at a time.
11001102
*
11011103
* The join is on the group (`path[0]`) and stops there deliberately: for
11021104
* file-authored bundles the warn side only ever fires at that depth. Its
@@ -1823,12 +1825,25 @@ function walkScreenFlows(config: any, out: ExpectedEntry[]): void {
18231825
if (!flowName) continue;
18241826
const scope: EntryScope = { flowName };
18251827

1828+
const nodes: any[] = collectFlowNodesDeep(flow.nodes);
1829+
18261830
// `flows.<flow>.label` — `lookupFlowLabel`'s key. `Flow.label` is required
18271831
// by the schema, so this is authored text in practice; `pushOptional`
18281832
// keeps a label-less flow from seeding an empty string anyway.
1829-
pushOptional(out, ['flows', flowName, 'label'], flow.label, 'flow', scope);
1830-
1831-
const nodes: any[] = collectFlowNodesDeep(flow.nodes);
1833+
//
1834+
// Demanded only for a flow with a `screen` node, at any depth of the same
1835+
// node universe the screens walk below reads. The predicate mirrors the one
1836+
// reader of the key: the console's `FlowRunner` names the flow by it, in
1837+
// its header and its completion toast, and the runner opens only on a run
1838+
// paused at a screen. A flow that can never pause there (record-triggered,
1839+
// scheduled, an API flow with no screen) has no surface that shows its
1840+
// translated label, so demanding one would ask an author for a string that
1841+
// is stored and never read. When a reader of a non-screen flow's label
1842+
// lands (a run-result toast, say), this predicate widens in the same change
1843+
// as that reader.
1844+
if (nodes.some((node) => node && typeof node === 'object' && node.type === SCREEN_NODE_TYPE)) {
1845+
pushOptional(out, ['flows', flowName, 'label'], flow.label, 'flow', scope);
1846+
}
18321847
for (const node of nodes) {
18331848
if (!node || typeof node !== 'object' || node.type !== SCREEN_NODE_TYPE) continue;
18341849
const nodeId = typeof node.id === 'string' && node.id.length > 0 ? node.id : undefined;

0 commit comments

Comments
 (0)