@@ -4,6 +4,11 @@ import { createLogger } from '@sim/logger'
44import { and , eq , inArray , isNull , like , or , sql } from 'drizzle-orm'
55import type { ReferenceNode } from '@/lib/api/contracts/workflow-references'
66import { getCustomBlockRowsForWorkspace } from '@/lib/workflows/custom-blocks/operations'
7+ import {
8+ type CanonicalGroup ,
9+ type CanonicalModeOverrides ,
10+ resolveActiveCanonicalValue ,
11+ } from '@/lib/workflows/subblocks/visibility'
712import { CUSTOM_BLOCK_TYPE_PREFIX } from '@/blocks/custom/build-config'
813import { BlockType , isWorkflowBlockType } from '@/executor/constants'
914
@@ -16,6 +21,18 @@ const logger = createLogger('WorkflowReferences')
1621 */
1722const MAX_REFERENCE_DEPTH = 25
1823
24+ /**
25+ * The `workflowId` canonical pair on a workflow / workflow_input block: the basic
26+ * `workflowId` selector and the advanced `manualWorkflowId` input. Used with
27+ * {@link resolveActiveCanonicalValue} so the reference resolves to the value of the
28+ * block's ACTIVE mode — never a dormant mode's retained (stale) value.
29+ */
30+ const WORKFLOW_ID_CANONICAL_GROUP : CanonicalGroup = {
31+ canonicalId : 'workflowId' ,
32+ basicId : 'workflowId' ,
33+ advancedIds : [ 'manualWorkflowId' ] ,
34+ }
35+
1936/** A workspace-local, non-archived workflow node. */
2037export interface WorkflowNode {
2138 id : string
@@ -30,6 +47,11 @@ export interface ReferenceBlockRow {
3047 childFromSelector : string | null
3148 /** `manualWorkflowId` sub-block value (advanced mode), if a workflow block. */
3249 childFromManual : string | null
50+ /**
51+ * The block's `data.canonicalModes` override (basic/advanced per canonical id),
52+ * used to pick the active `workflowId` value. Absent for most blocks.
53+ */
54+ canonicalModes : CanonicalModeOverrides | null
3355}
3456
3557/** A custom-block type slug bound to its source workflow. */
@@ -56,8 +78,11 @@ interface ReferenceGraph {
5678 * - **custom blocks** (`custom_block_<id>`), whose type slug maps to a bound
5779 * source workflow via `customBlocks`.
5880 *
59- * Edges are scoped to workspace-local, non-archived workflows: self-references,
60- * empty values, and ids outside `workflows` are dropped.
81+ * The workflow-block child is the value of the block's ACTIVE mode
82+ * ({@link resolveActiveCanonicalValue}), so a dormant basic/advanced value can't
83+ * mask the live one. Edges are scoped to workspace-local, non-archived workflows;
84+ * empty values and ids outside `workflows` are dropped. A workflow that calls
85+ * itself is kept — the tree builder renders it as a `cycle` leaf.
6186 */
6287export function buildReferenceGraph (
6388 workflows : WorkflowNode [ ] ,
@@ -74,7 +99,7 @@ export function buildReferenceGraph(
7499 const reverse = new Map < string , Set < string > > ( )
75100
76101 const addEdge = ( parentId : string , childId : string ) => {
77- if ( ! childId || childId === parentId ) return
102+ if ( ! childId ) return
78103 if ( ! nameById . has ( parentId ) || ! nameById . has ( childId ) ) return
79104 if ( ! forward . has ( parentId ) ) forward . set ( parentId , new Set ( ) )
80105 forward . get ( parentId ) ?. add ( childId )
@@ -84,8 +109,12 @@ export function buildReferenceGraph(
84109
85110 for ( const block of blocks ) {
86111 if ( isWorkflowBlockType ( block . type ) ) {
87- const childId = block . childFromSelector || block . childFromManual
88- if ( childId ) addEdge ( block . parentId , childId )
112+ const active = resolveActiveCanonicalValue (
113+ WORKFLOW_ID_CANONICAL_GROUP ,
114+ { workflowId : block . childFromSelector , manualWorkflowId : block . childFromManual } ,
115+ block . canonicalModes ?? undefined
116+ )
117+ if ( typeof active === 'string' && active ) addEdge ( block . parentId , active )
89118 continue
90119 }
91120 const sourceId = sourceByCustomType . get ( block . type )
@@ -97,16 +126,22 @@ export function buildReferenceGraph(
97126
98127/**
99128 * Expand a direction of the graph into a tree rooted at `rootId`. `adjacency` is
100- * either the forward (callees) or reverse (callers) map. A node already on the
101- * current DFS path is emitted as a `cycle: true` leaf and not re-expanded, so
102- * `A → B → A` terminates. Children are sorted by name for stable rendering.
129+ * either the forward (callees) or reverse (callers) map.
130+ *
131+ * A node already on the current DFS path is emitted as a `cycle: true` leaf and
132+ * not re-expanded, so `A → B → A` (and a self-call `A → A`) terminates. A node
133+ * already fully expanded elsewhere in this tree (reachable via another acyclic
134+ * path — a diamond) is emitted once more as a plain leaf without re-expanding its
135+ * subtree: the edge stays visible, but a densely reconverging graph can't blow up
136+ * exponentially. Children are sorted by name for stable rendering.
103137 */
104138function buildTree (
105139 rootId : string ,
106140 adjacency : Map < string , Set < string > > ,
107141 nameById : Map < string , string >
108142) : ReferenceNode [ ] {
109143 const path = new Set < string > ( [ rootId ] )
144+ const expanded = new Set < string > ( )
110145
111146 const expand = ( id : string , depth : number ) : ReferenceNode [ ] => {
112147 if ( depth >= MAX_REFERENCE_DEPTH ) return [ ]
@@ -126,6 +161,11 @@ function buildTree(
126161 nodes . push ( { id : childId , name, cycle : true , children : [ ] } )
127162 continue
128163 }
164+ if ( expanded . has ( childId ) ) {
165+ nodes . push ( { id : childId , name, cycle : false , children : [ ] } )
166+ continue
167+ }
168+ expanded . add ( childId )
129169 path . add ( childId )
130170 nodes . push ( { id : childId , name, cycle : false , children : expand ( childId , depth + 1 ) } )
131171 path . delete ( childId )
@@ -185,6 +225,7 @@ export async function getWorkflowReferences(
185225 childFromManual : sql <
186226 string | null
187227 > `${ workflowBlocks . subBlocks } -> 'manualWorkflowId' ->> 'value'` ,
228+ canonicalModes : sql < CanonicalModeOverrides | null > `${ workflowBlocks . data } -> 'canonicalModes'` ,
188229 } )
189230 . from ( workflowBlocks )
190231 . innerJoin ( workflow , eq ( workflow . id , workflowBlocks . workflowId ) )
0 commit comments