Filed unassigned by the domain:devx seat while implementing #11608 (the packages/lintflows leg). packages/spec is outside that card's declared file surface, so this is its own card, not a rider.
Measured
packages/spec/src/system/i18n-resolver.ts, translateFlow:
constnodes=Array.isArray(flow.nodes)
? flow.nodes.map((node)=>{…translateScreenNode(node,name,bundle,opts)…})
: undefined;It reads the flat flow.nodes array only. But FlowNode.config carries ADR-0031 regions — loop.config.body, parallel.config.branches[].nodes, try_catch.config.try / .catch, each holding a full nodes: z.array(FlowNodeSchema), nesting arbitrarily. A type: 'screen' node in any of them is a real screen: the executor pauses on it and the client receives its ScreenSpec.nodeId.
So for a nested screen, flows.<name>.screens.<node_id>.{title,fields.*} is authored, parses (the schema is keyed by node id and knows nothing about depth), and translateFlow walks straight past it. The wizard step renders its source-locale heading and field labels while the sibling screens one level up are translated — the same invisible hole the flows group was added to close (#7646).
Why this is worth a card rather than a note
This is the fourth pass in this repo to be written against the flat one-liner, and the first three were each filed and fixed separately:
| card | pass | outcome |
|---|
| #4347 | applyConversionsToFlow | conditions inside a loop never converted to CEL |
| #4380 | flow lint rules | two gating errors silently inert, a third downgraded |
| #5383 | flow-inert-node-condition | 8 real inert conditions shipped |
packages/spec/src/automation/region-slots.ts exists precisely so this fact has one declaration (FLOW_REGION_SLOTS), and its header names the four passes that need it. translateFlow is a fifth consumer that needs it and does not read it.
Note the asymmetry that makes this easy to miss: the client-side resolver path is already correct. resolveFlowScreenTitle / lookupFlowScreenCopy take a bare flowName + nodeId, so a paused nested screen resolves fine. Only the document-overlay path (translateFlow) is depth-blind — so whether this ships as a user-visible bug depends on which of the two the runner wiring picks, which #11287's docblock explicitly defers to a downstream card.
Shape of the fix
Walk regions via FLOW_REGION_SLOTS_BY_TYPE (or reuse the region-aware walk) inside translateFlow, preserving its reference-identity discipline: a node that resolved nothing must come back as the same reference, or nodesChanged starts reporting untouched documents as changed.
Not blocking #11608
The lint leg landing there collects its universe with walkFlowNodes, so it already resolves nested screens and does not report them as orphans — deliberately the safe direction for a warning. That means the lint rule and translateFlow currently disagree about which keys are live, and this card is what closes the gap.
Refs: #11608 · #11287 · #7646 · #4380 · #5383 · #4347
Filed unassigned by the
domain:devxseat while implementing #11608 (thepackages/lintflowsleg).packages/specis outside that card's declared file surface, so this is its own card, not a rider.Measured
packages/spec/src/system/i18n-resolver.ts,translateFlow:It reads the flat
flow.nodesarray only. ButFlowNode.configcarries ADR-0031 regions —loop.config.body,parallel.config.branches[].nodes,try_catch.config.try/.catch, each holding a fullnodes: z.array(FlowNodeSchema), nesting arbitrarily. Atype: 'screen'node in any of them is a real screen: the executor pauses on it and the client receives itsScreenSpec.nodeId.So for a nested screen,
flows.<name>.screens.<node_id>.{title,fields.*}is authored, parses (the schema is keyed by node id and knows nothing about depth), andtranslateFlowwalks straight past it. The wizard step renders its source-locale heading and field labels while the sibling screens one level up are translated — the same invisible hole theflowsgroup was added to close (#7646).Why this is worth a card rather than a note
This is the fourth pass in this repo to be written against the flat one-liner, and the first three were each filed and fixed separately:
applyConversionsToFlowloopnever converted to CELflow-inert-node-conditionpackages/spec/src/automation/region-slots.tsexists precisely so this fact has one declaration (FLOW_REGION_SLOTS), and its header names the four passes that need it.translateFlowis a fifth consumer that needs it and does not read it.Note the asymmetry that makes this easy to miss: the client-side resolver path is already correct.
resolveFlowScreenTitle/lookupFlowScreenCopytake a bareflowName+nodeId, so a paused nested screen resolves fine. Only the document-overlay path (translateFlow) is depth-blind — so whether this ships as a user-visible bug depends on which of the two the runner wiring picks, which #11287's docblock explicitly defers to a downstream card.Shape of the fix
Walk regions via
FLOW_REGION_SLOTS_BY_TYPE(or reuse the region-aware walk) insidetranslateFlow, preserving its reference-identity discipline: a node that resolved nothing must come back as the same reference, ornodesChangedstarts reporting untouched documents as changed.Not blocking #11608
The lint leg landing there collects its universe with
walkFlowNodes, so it already resolves nested screens and does not report them as orphans — deliberately the safe direction for awarning. That means the lint rule andtranslateFlowcurrently disagree about which keys are live, and this card is what closes the gap.Refs: #11608 · #11287 · #7646 · #4380 · #5383 · #4347