@@ -252,25 +252,72 @@ function outputTypeForNode(
252252 : null ;
253253}
254254
255- export function deriveWorkflowInterfaceV1 ( args : {
256- workflowId : string ;
257- etag ?: string | null ;
258- graph : WorkflowInterfaceGraph ;
259- registry : WorkflowInterfaceRegistry ;
260- } ) : WorkflowInterfaceV1 {
261- const diagnostics : WorkflowInterfaceDiagnostic [ ] = [ ] ;
262- const inputs : WorkflowInterfaceInput [ ] = [ ] ;
263- const outputs : WorkflowInterfaceOutput [ ] = [ ] ;
264- const nodes = Array . isArray ( args . graph . nodes ) ? args . graph . nodes : [ ] ;
265- const edges = Array . isArray ( args . graph . edges ) ? args . graph . edges : [ ] ;
266- const nodesById = new Map < string , WorkflowInterfaceGraphNode > ( ) ;
267- const inputNames = new Set < string > ( ) ;
268- const outputNames = new Set < string > ( ) ;
255+ /**
256+ * The type behind a generic `Output` node: what its single incoming `value`
257+ * edge carries. Every failure path pushes a diagnostic before returning null.
258+ */
259+ function resolveOutputSourceType (
260+ edge : WorkflowInterfaceGraphEdge ,
261+ nodesById : ReadonlyMap < string , WorkflowInterfaceGraphNode > ,
262+ registry : WorkflowInterfaceRegistry ,
263+ outputNodeId : string ,
264+ outputName : string ,
265+ diagnostics : WorkflowInterfaceDiagnostic [ ]
266+ ) : { type : TypeMetadata ; stream : boolean } | null {
267+ const source = isString ( edge . source )
268+ ? nodesById . get ( edge . source )
269+ : undefined ;
270+ if ( ! source ) {
271+ diagnostics . push ( {
272+ severity : "error" ,
273+ code : "missing_source_node" ,
274+ message : `Workflow output '${ outputName } ' references a source node that is not present in the graph.` ,
275+ node_id : outputNodeId ,
276+ pin_name : outputName
277+ } ) ;
278+ return null ;
279+ }
269280
270- for ( const node of nodes ) {
271- if ( isString ( node . id ) ) nodesById . set ( node . id , node ) ;
281+ const handle = String ( edge . sourceHandle ?? edge . source_handle ?? "" ) ;
282+ if ( ! handle ) {
283+ diagnostics . push ( {
284+ severity : "error" ,
285+ code : "missing_source_handle" ,
286+ message : `Workflow output '${ outputName } ' has an incoming edge without a source handle.` ,
287+ node_id : outputNodeId ,
288+ pin_name : outputName
289+ } ) ;
290+ return null ;
272291 }
273292
293+ const resolved = outputTypeForNode ( source , handle , registry ) ;
294+ if ( resolved ) return resolved ;
295+
296+ const sourceType = isString ( source . type ) ? source . type : "" ;
297+ const hasDynamicHandle =
298+ record ( record ( source . dynamic_outputs ) ?. [ handle ] ) !== null ;
299+ const sourceMetadata = registry . resolveMetadata ( sourceType ) ;
300+ const packUnavailable = ! hasDynamicHandle && ! sourceMetadata ;
301+ diagnostics . push ( {
302+ severity : "error" ,
303+ code : packUnavailable ? "missing_node_metadata" : "missing_output_handle" ,
304+ message : packUnavailable
305+ ? `Node metadata for workflow output source '${ sourceType } ' is unavailable. The node pack may be disabled or not loaded.`
306+ : `Source handle '${ handle } ' is not declared by node '${ sourceType } '.` ,
307+ node_id : isString ( source . id ) ? source . id : outputNodeId ,
308+ pin_name : handle
309+ } ) ;
310+ return null ;
311+ }
312+
313+ function collectInputs (
314+ nodes : readonly WorkflowInterfaceGraphNode [ ] ,
315+ registry : WorkflowInterfaceRegistry ,
316+ diagnostics : WorkflowInterfaceDiagnostic [ ]
317+ ) : WorkflowInterfaceInput [ ] {
318+ const inputs : WorkflowInterfaceInput [ ] = [ ] ;
319+ const seenNames = new Set < string > ( ) ;
320+
274321 for ( const node of nodes ) {
275322 const nodeId = isString ( node . id ) ? node . id : "" ;
276323 const nodeType = isString ( node . type ) ? node . type : "" ;
@@ -286,7 +333,7 @@ export function deriveWorkflowInterfaceV1(args: {
286333 } ) ;
287334 continue ;
288335 }
289- if ( inputNames . has ( name ) ) {
336+ if ( seenNames . has ( name ) ) {
290337 diagnostics . push ( {
291338 severity : "error" ,
292339 code : "duplicate_input_name" ,
@@ -296,9 +343,9 @@ export function deriveWorkflowInterfaceV1(args: {
296343 } ) ;
297344 continue ;
298345 }
299- inputNames . add ( name ) ;
346+ seenNames . add ( name ) ;
300347
301- const resolved = outputTypeForNode ( node , "output" , args . registry ) ;
348+ const resolved = outputTypeForNode ( node , "output" , registry ) ;
302349 if ( ! resolved ) {
303350 diagnostics . push ( {
304351 severity : "error" ,
@@ -334,6 +381,19 @@ export function deriveWorkflowInterfaceV1(args: {
334381 inputs . push ( input ) ;
335382 }
336383
384+ return inputs ;
385+ }
386+
387+ function collectOutputs (
388+ nodes : readonly WorkflowInterfaceGraphNode [ ] ,
389+ edges : readonly WorkflowInterfaceGraphEdge [ ] ,
390+ nodesById : ReadonlyMap < string , WorkflowInterfaceGraphNode > ,
391+ registry : WorkflowInterfaceRegistry ,
392+ diagnostics : WorkflowInterfaceDiagnostic [ ]
393+ ) : WorkflowInterfaceOutput [ ] {
394+ const outputs : WorkflowInterfaceOutput [ ] = [ ] ;
395+ const seenNames = new Set < string > ( ) ;
396+
337397 for ( const node of nodes ) {
338398 const nodeId = isString ( node . id ) ? node . id : "" ;
339399 const nodeType = isString ( node . type ) ? node . type : "" ;
@@ -350,7 +410,7 @@ export function deriveWorkflowInterfaceV1(args: {
350410 } ) ;
351411 continue ;
352412 }
353- if ( outputNames . has ( name ) ) {
413+ if ( seenNames . has ( name ) ) {
354414 diagnostics . push ( {
355415 severity : "error" ,
356416 code : "duplicate_output_name" ,
@@ -360,66 +420,30 @@ export function deriveWorkflowInterfaceV1(args: {
360420 } ) ;
361421 continue ;
362422 }
363- outputNames . add ( name ) ;
364-
365- let resolved : { type : TypeMetadata ; stream : boolean } | null = dedicatedType
423+ seenNames . add ( name ) ;
424+
425+ // A dedicated media output carries its own type, so its edges are never
426+ // inspected — and a bad one is never reported.
427+ const incoming = dedicatedType
428+ ? [ ]
429+ : edges . filter (
430+ ( edge ) =>
431+ edge . target === nodeId &&
432+ String ( edge . targetHandle ?? edge . target_handle ?? "" ) === "value"
433+ ) ;
434+ const resolved = dedicatedType
366435 ? { type : dedicatedType , stream : false }
367- : null ;
368- let incoming : WorkflowInterfaceGraphEdge [ ] = [ ] ;
369- if ( ! dedicatedType ) {
370- incoming = edges . filter (
371- ( edge ) =>
372- edge . target === nodeId &&
373- String ( edge . targetHandle ?? edge . target_handle ?? "" ) === "value"
374- ) ;
375- if ( incoming . length === 1 ) {
376- const edge = incoming [ 0 ] ! ;
377- const source =
378- isString ( edge . source )
379- ? nodesById . get ( edge . source )
380- : undefined ;
381- const handle = String ( edge . sourceHandle ?? edge . source_handle ?? "" ) ;
382- if ( ! source ) {
383- diagnostics . push ( {
384- severity : "error" ,
385- code : "missing_source_node" ,
386- message : `Workflow output '${ name } ' references a source node that is not present in the graph.` ,
387- node_id : nodeId ,
388- pin_name : name
389- } ) ;
390- } else if ( ! handle ) {
391- diagnostics . push ( {
392- severity : "error" ,
393- code : "missing_source_handle" ,
394- message : `Workflow output '${ name } ' has an incoming edge without a source handle.` ,
395- node_id : nodeId ,
396- pin_name : name
397- } ) ;
398- } else {
399- resolved = outputTypeForNode ( source , handle , args . registry ) ;
400- if ( ! resolved ) {
401- const dynamic = record ( source . dynamic_outputs ) ;
402- const hasDynamicHandle = record ( dynamic ?. [ handle ] ) !== null ;
403- const sourceType =
404- isString ( source . type ) ? source . type : "" ;
405- const sourceMetadata = args . registry . resolveMetadata ( sourceType ) ;
406- diagnostics . push ( {
407- severity : "error" ,
408- code :
409- ! hasDynamicHandle && ! sourceMetadata
410- ? "missing_node_metadata"
411- : "missing_output_handle" ,
412- message :
413- ! hasDynamicHandle && ! sourceMetadata
414- ? `Node metadata for workflow output source '${ sourceType } ' is unavailable. The node pack may be disabled or not loaded.`
415- : `Source handle '${ handle } ' is not declared by node '${ sourceType } '.` ,
416- node_id : isString ( source . id ) ? source . id : nodeId ,
417- pin_name : handle
418- } ) ;
419- }
420- }
421- }
422- }
436+ : incoming . length === 1
437+ ? resolveOutputSourceType (
438+ incoming [ 0 ] ! ,
439+ nodesById ,
440+ registry ,
441+ nodeId ,
442+ name ,
443+ diagnostics
444+ )
445+ : null ;
446+
423447 if ( ! resolved ) {
424448 diagnostics . push ( {
425449 severity : "error" ,
@@ -441,6 +465,32 @@ export function deriveWorkflowInterfaceV1(args: {
441465 } ) ;
442466 }
443467
468+ return outputs ;
469+ }
470+
471+ export function deriveWorkflowInterfaceV1 ( args : {
472+ workflowId : string ;
473+ etag ?: string | null ;
474+ graph : WorkflowInterfaceGraph ;
475+ registry : WorkflowInterfaceRegistry ;
476+ } ) : WorkflowInterfaceV1 {
477+ const diagnostics : WorkflowInterfaceDiagnostic [ ] = [ ] ;
478+ const nodes = Array . isArray ( args . graph . nodes ) ? args . graph . nodes : [ ] ;
479+ const edges = Array . isArray ( args . graph . edges ) ? args . graph . edges : [ ] ;
480+ const nodesById = new Map < string , WorkflowInterfaceGraphNode > ( ) ;
481+ for ( const node of nodes ) {
482+ if ( isString ( node . id ) ) nodesById . set ( node . id , node ) ;
483+ }
484+
485+ const inputs = collectInputs ( nodes , args . registry , diagnostics ) ;
486+ const outputs = collectOutputs (
487+ nodes ,
488+ edges ,
489+ nodesById ,
490+ args . registry ,
491+ diagnostics
492+ ) ;
493+
444494 return {
445495 version : 1 ,
446496 workflow_id : args . workflowId ,
0 commit comments