@bian-womp/spark-graph 0.3.31 → 0.3.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/lib/cjs/index.cjs +338 -133
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/builder/GraphBuilder.d.ts +0 -1
  4. package/lib/cjs/src/builder/GraphBuilder.d.ts.map +1 -1
  5. package/lib/cjs/src/builder/Registry.d.ts +6 -0
  6. package/lib/cjs/src/builder/Registry.d.ts.map +1 -1
  7. package/lib/cjs/src/core/type-utils.d.ts +11 -0
  8. package/lib/cjs/src/core/type-utils.d.ts.map +1 -1
  9. package/lib/cjs/src/index.d.ts +4 -1
  10. package/lib/cjs/src/index.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/utils/merge.d.ts.map +1 -1
  12. package/lib/cjs/src/runtime/GraphRuntime.d.ts +16 -1
  13. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
  14. package/lib/cjs/src/runtime/components/Graph.d.ts +1 -2
  15. package/lib/cjs/src/runtime/components/Graph.d.ts.map +1 -1
  16. package/lib/cjs/src/runtime/components/HandleResolver.d.ts.map +1 -1
  17. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  18. package/lib/cjs/src/runtime/components/RuntimeValidatorManager.d.ts +31 -0
  19. package/lib/cjs/src/runtime/components/RuntimeValidatorManager.d.ts.map +1 -0
  20. package/lib/cjs/src/runtime/components/graph-utils.d.ts +15 -4
  21. package/lib/cjs/src/runtime/components/graph-utils.d.ts.map +1 -1
  22. package/lib/cjs/src/runtime/components/interfaces.d.ts +24 -1
  23. package/lib/cjs/src/runtime/components/interfaces.d.ts.map +1 -1
  24. package/lib/cjs/src/runtime/components/types.d.ts +1 -2
  25. package/lib/cjs/src/runtime/components/types.d.ts.map +1 -1
  26. package/lib/esm/index.js +336 -134
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/src/builder/GraphBuilder.d.ts +0 -1
  29. package/lib/esm/src/builder/GraphBuilder.d.ts.map +1 -1
  30. package/lib/esm/src/builder/Registry.d.ts +6 -0
  31. package/lib/esm/src/builder/Registry.d.ts.map +1 -1
  32. package/lib/esm/src/core/type-utils.d.ts +11 -0
  33. package/lib/esm/src/core/type-utils.d.ts.map +1 -1
  34. package/lib/esm/src/index.d.ts +4 -1
  35. package/lib/esm/src/index.d.ts.map +1 -1
  36. package/lib/esm/src/misc/utils/merge.d.ts.map +1 -1
  37. package/lib/esm/src/runtime/GraphRuntime.d.ts +16 -1
  38. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/components/Graph.d.ts +1 -2
  40. package/lib/esm/src/runtime/components/Graph.d.ts.map +1 -1
  41. package/lib/esm/src/runtime/components/HandleResolver.d.ts.map +1 -1
  42. package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  43. package/lib/esm/src/runtime/components/RuntimeValidatorManager.d.ts +31 -0
  44. package/lib/esm/src/runtime/components/RuntimeValidatorManager.d.ts.map +1 -0
  45. package/lib/esm/src/runtime/components/graph-utils.d.ts +15 -4
  46. package/lib/esm/src/runtime/components/graph-utils.d.ts.map +1 -1
  47. package/lib/esm/src/runtime/components/interfaces.d.ts +24 -1
  48. package/lib/esm/src/runtime/components/interfaces.d.ts.map +1 -1
  49. package/lib/esm/src/runtime/components/types.d.ts +1 -2
  50. package/lib/esm/src/runtime/components/types.d.ts.map +1 -1
  51. package/package.json +2 -2
package/lib/esm/index.js CHANGED
@@ -16,19 +16,42 @@ function getTypedOutputValue(v) {
16
16
  return v.__spark_value;
17
17
  return v;
18
18
  }
19
- function getInputTypeId(inputs, handle) {
19
+ /**
20
+ * Get the full declared type(s) for an input handle (supports union types)
21
+ * Returns the typeId as-is: string for single type, string[] for union types
22
+ */
23
+ function getInputDeclaredTypes(inputs, handle) {
20
24
  const v = inputs ? inputs[handle] : undefined;
21
25
  if (!v)
22
26
  return undefined;
23
- return typeof v === "string" ? v : v.typeId;
27
+ if (typeof v === "string")
28
+ return v;
29
+ if (Array.isArray(v))
30
+ return v;
31
+ return v.typeId;
32
+ }
33
+ /**
34
+ * Get the primary (first) type ID for an input handle.
35
+ * For union types, returns the first type in the array.
36
+ * This maintains backward compatibility for code that expects a single type.
37
+ */
38
+ function getInputTypeId(inputs, handle) {
39
+ const decl = getInputDeclaredTypes(inputs, handle);
40
+ if (!decl)
41
+ return undefined;
42
+ return Array.isArray(decl) ? decl[0] : decl;
24
43
  }
25
44
  function isInputPrivate(inputs, handle) {
26
45
  const v = inputs ? inputs[handle] : undefined;
27
- return !!(v && typeof v === "object" && v.private);
46
+ if (!v || typeof v === "string" || Array.isArray(v))
47
+ return false;
48
+ // At this point, v must be an object with optional private/metadata fields
49
+ return !!(typeof v === "object" && v !== null && "private" in v && v.private);
28
50
  }
29
51
  /**
30
52
  * Merge two InputHandleDescriptor values, with dynamic taking precedence.
31
53
  * If both have metadata, merge the metadata objects (dynamic overrides static).
54
+ * Supports union types (arrays) in both static and dynamic descriptors.
32
55
  */
33
56
  function mergeInputHandleDescriptors(staticDesc, dynamicDesc) {
34
57
  // If only one exists, return it
@@ -36,12 +59,17 @@ function mergeInputHandleDescriptors(staticDesc, dynamicDesc) {
36
59
  return dynamicDesc;
37
60
  if (!dynamicDesc)
38
61
  return staticDesc;
39
- // If both are strings, dynamic wins
40
- if (typeof staticDesc === "string" && typeof dynamicDesc === "string") {
62
+ // If both are primitive (string or array), dynamic wins
63
+ if ((typeof staticDesc === "string" || Array.isArray(staticDesc)) &&
64
+ (typeof dynamicDesc === "string" || Array.isArray(dynamicDesc))) {
41
65
  return dynamicDesc;
42
66
  }
43
- const staticObj = typeof staticDesc === "string" ? { typeId: staticDesc } : staticDesc;
44
- const dynamicObj = typeof dynamicDesc === "string" ? { typeId: dynamicDesc } : dynamicDesc;
67
+ const staticObj = typeof staticDesc === "string" || Array.isArray(staticDesc)
68
+ ? { typeId: staticDesc }
69
+ : staticDesc;
70
+ const dynamicObj = typeof dynamicDesc === "string" || Array.isArray(dynamicDesc)
71
+ ? { typeId: dynamicDesc }
72
+ : dynamicDesc;
45
73
  // Merge: dynamic takes precedence, but merge metadata objects
46
74
  const merged = {
47
75
  typeId: dynamicObj.typeId ?? staticObj.typeId,
@@ -62,7 +90,7 @@ function mergeInputHandleDescriptors(staticDesc, dynamicDesc) {
62
90
  */
63
91
  function getInputHandleMetadata(inputs, handle) {
64
92
  const v = inputs ? inputs[handle] : undefined;
65
- if (!v || typeof v === "string")
93
+ if (!v || typeof v === "string" || Array.isArray(v))
66
94
  return undefined;
67
95
  return v.metadata;
68
96
  }
@@ -257,7 +285,11 @@ class Registry {
257
285
  if (cached)
258
286
  return cached;
259
287
  if (fromTypeId === toTypeId) {
260
- const res = { kind: "sync", convert: (v) => v };
288
+ const res = {
289
+ kind: "sync",
290
+ convert: (v) => v,
291
+ cost: { edges: 0, async: 0 },
292
+ };
261
293
  this.resolvedCache.set(cacheKey, res);
262
294
  return res;
263
295
  }
@@ -267,6 +299,7 @@ class Registry {
267
299
  const res = {
268
300
  kind: "sync",
269
301
  convert: directSync.convert,
302
+ cost: { edges: 1, async: 0 },
270
303
  };
271
304
  this.resolvedCache.set(cacheKey, res);
272
305
  return res;
@@ -276,6 +309,7 @@ class Registry {
276
309
  const res = {
277
310
  kind: "async",
278
311
  convertAsync: directAsync.convertAsync,
312
+ cost: { edges: 1, async: 1 },
279
313
  };
280
314
  this.resolvedCache.set(cacheKey, res);
281
315
  return res;
@@ -332,6 +366,10 @@ class Registry {
332
366
  const cur = queue.shift();
333
367
  if (cur.node === toTypeId) {
334
368
  // Compose
369
+ const cost = {
370
+ edges: cur.cost.edges,
371
+ async: cur.cost.async,
372
+ };
335
373
  const hasAsync = cur.path.some((s) => s.kind === "async");
336
374
  if (!hasAsync) {
337
375
  const convert = (value) => {
@@ -342,7 +380,11 @@ class Registry {
342
380
  }
343
381
  return acc;
344
382
  };
345
- const res = { kind: "sync", convert };
383
+ const res = {
384
+ kind: "sync",
385
+ convert,
386
+ cost,
387
+ };
346
388
  this.resolvedCache.set(cacheKey, res);
347
389
  return res;
348
390
  }
@@ -359,7 +401,11 @@ class Registry {
359
401
  }
360
402
  return acc;
361
403
  };
362
- const res = { kind: "async", convertAsync };
404
+ const res = {
405
+ kind: "async",
406
+ convertAsync,
407
+ cost,
408
+ };
363
409
  this.resolvedCache.set(cacheKey, res);
364
410
  return res;
365
411
  }
@@ -816,9 +862,6 @@ class Graph {
816
862
  const edge = this.edges.find((e) => e.id === edgeId);
817
863
  if (!edge)
818
864
  return;
819
- if (updates.effectiveTypeId !== undefined) {
820
- edge.effectiveTypeId = updates.effectiveTypeId;
821
- }
822
865
  if (updates.dstDeclared !== undefined) {
823
866
  edge.dstDeclared = updates.dstDeclared;
824
867
  }
@@ -1412,14 +1455,13 @@ function buildEdges(def, registry, resolvedByNode) {
1412
1455
  return def.edges.map((e) => {
1413
1456
  const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
1414
1457
  const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
1415
- const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
1458
+ const { srcDeclared, dstDeclared } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode);
1416
1459
  const { convert, convertAsync } = buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
1417
1460
  return {
1418
1461
  id: e.id,
1419
1462
  source: { ...e.source },
1420
1463
  target: { ...e.target },
1421
1464
  typeId: e.typeId, // Preserve original (may be undefined)
1422
- effectiveTypeId, // Always present
1423
1465
  convert,
1424
1466
  convertAsync,
1425
1467
  srcUnionTypes: Array.isArray(srcDeclared) ? [...srcDeclared] : undefined,
@@ -1431,37 +1473,79 @@ function buildEdges(def, registry, resolvedByNode) {
1431
1473
  /**
1432
1474
  * Extract edge type information from resolved handles
1433
1475
  * Used by both buildEdges and updateNodeHandles to avoid duplication
1476
+ * Now supports union types on both source (output) and destination (input) handles
1434
1477
  */
1435
- function extractEdgeTypes(sourceNodeId, sourceHandle, targetNodeId, targetHandle, resolvedByNode, explicitTypeId) {
1478
+ function extractEdgeTypes(sourceNodeId, sourceHandle, targetNodeId, targetHandle, resolvedByNode) {
1436
1479
  const srcResolved = resolvedByNode.get(sourceNodeId);
1437
1480
  const dstResolved = resolvedByNode.get(targetNodeId);
1438
1481
  const srcDeclared = srcResolved
1439
1482
  ? srcResolved.outputs[sourceHandle]
1440
1483
  : undefined;
1441
1484
  const dstDeclared = dstResolved
1442
- ? getInputTypeId(dstResolved.inputs, targetHandle)
1485
+ ? getInputDeclaredTypes(dstResolved.inputs, targetHandle)
1443
1486
  : undefined;
1444
- let effectiveTypeId = explicitTypeId;
1445
- if (!effectiveTypeId) {
1446
- // Infer if not explicitly set
1447
- effectiveTypeId = Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
1448
- }
1449
1487
  return {
1450
1488
  srcDeclared,
1451
1489
  dstDeclared,
1452
- effectiveTypeId: effectiveTypeId ?? "untyped",
1453
1490
  };
1454
1491
  }
1455
1492
  // Static helper: build edge converters for type coercion
1493
+ // Now supports union types on both source (output) and destination (input) handles
1456
1494
  function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
1457
1495
  if (!dstDeclared || !srcDeclared) {
1458
1496
  return {};
1459
1497
  }
1460
- const isUnion = Array.isArray(srcDeclared);
1461
- const srcTypes = isUnion ? srcDeclared : [srcDeclared];
1462
- // Helper to get the coercion for a specific type
1463
- const getCoercion = (typeId) => {
1464
- return registry.resolveCoercion(typeId, dstDeclared);
1498
+ const isSrcUnion = Array.isArray(srcDeclared);
1499
+ const srcTypes = isSrcUnion ? srcDeclared : [srcDeclared];
1500
+ const isDstUnion = Array.isArray(dstDeclared);
1501
+ const dstTypes = isDstUnion ? dstDeclared : [dstDeclared];
1502
+ // Helper to compare coercion costs (sync preferred, then fewer steps)
1503
+ const compareCost = (a, b) => {
1504
+ // Prefer sync over async
1505
+ if (a.kind === "sync" && b.kind === "async")
1506
+ return -1;
1507
+ if (a.kind === "async" && b.kind === "sync")
1508
+ return 1;
1509
+ // If same kind, prefer fewer edges
1510
+ if (a.cost.edges !== b.cost.edges)
1511
+ return a.cost.edges - b.cost.edges;
1512
+ // If same edges, prefer fewer async steps
1513
+ return a.cost.async - b.cost.async;
1514
+ };
1515
+ // Helper to find the best coercion from a source type to any destination type
1516
+ const getCoercion = (srcTypeId) => {
1517
+ const candidates = [];
1518
+ // Try all destination types and collect valid coercions
1519
+ for (const dstTypeId of dstTypes) {
1520
+ const coercion = registry.resolveCoercion(srcTypeId, dstTypeId);
1521
+ if (coercion) {
1522
+ candidates.push({
1523
+ dstType: dstTypeId,
1524
+ coercion,
1525
+ });
1526
+ }
1527
+ }
1528
+ if (candidates.length === 0)
1529
+ return null;
1530
+ // Select best by cost: sync preferred, then fewer edges, then fewer async steps
1531
+ const best = candidates.reduce((best, cur) => {
1532
+ return compareCost(cur.coercion, best.coercion) < 0 ? cur : best;
1533
+ });
1534
+ if (best.coercion.kind === "sync") {
1535
+ return {
1536
+ kind: "sync",
1537
+ convert: best.coercion.convert,
1538
+ dstType: best.dstType,
1539
+ };
1540
+ }
1541
+ else {
1542
+ return {
1543
+ kind: "async",
1544
+ convert: (v) => v, // placeholder, not used for async
1545
+ convertAsync: best.coercion.convertAsync,
1546
+ dstType: best.dstType,
1547
+ };
1548
+ }
1465
1549
  };
1466
1550
  // Resolve coercions for all source types
1467
1551
  const coercions = srcTypes.map(getCoercion);
@@ -1470,7 +1554,7 @@ function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
1470
1554
  const extractPayload = (v) => {
1471
1555
  const typeId = getTypedOutputTypeId(v);
1472
1556
  const payload = getTypedOutputValue(v);
1473
- if (isUnion) {
1557
+ if (isSrcUnion) {
1474
1558
  if (!typeId) {
1475
1559
  throw new Error(`Typed output required for union source (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
1476
1560
  }
@@ -1484,17 +1568,27 @@ function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
1484
1568
  }
1485
1569
  return { typeId: typeId || srcTypes[0], payload };
1486
1570
  };
1571
+ const wrapIfDstUnion = (dstType, val) => {
1572
+ if (!isDstUnion || !dstType)
1573
+ return val;
1574
+ return typed(dstType, val);
1575
+ };
1487
1576
  if (hasAsync) {
1488
1577
  return {
1489
1578
  convertAsync: async (v, signal) => {
1490
1579
  const { typeId, payload } = extractPayload(v);
1491
1580
  const res = getCoercion(typeId);
1492
- if (!res)
1493
- return payload;
1494
- if (res.kind === "async") {
1495
- return await res.convertAsync(payload, signal);
1581
+ if (!res) {
1582
+ const fallbackType = isDstUnion && typeId && dstTypes.includes(typeId)
1583
+ ? typeId
1584
+ : undefined;
1585
+ return wrapIfDstUnion(fallbackType, payload);
1496
1586
  }
1497
- return res.convert(payload);
1587
+ if (res.kind === "async" && res.convertAsync) {
1588
+ const converted = await res.convertAsync(payload, signal);
1589
+ return wrapIfDstUnion(res.dstType, converted);
1590
+ }
1591
+ return wrapIfDstUnion(res.dstType, res.convert(payload));
1498
1592
  },
1499
1593
  };
1500
1594
  }
@@ -1507,15 +1601,69 @@ function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
1507
1601
  convert: (v) => {
1508
1602
  const { typeId, payload } = extractPayload(v);
1509
1603
  const res = getCoercion(typeId);
1510
- if (!res)
1511
- return payload;
1604
+ if (!res) {
1605
+ const fallbackType = isDstUnion && typeId && dstTypes.includes(typeId)
1606
+ ? typeId
1607
+ : undefined;
1608
+ return wrapIfDstUnion(fallbackType, payload);
1609
+ }
1512
1610
  if (res.kind === "async") {
1513
1611
  throw new Error(`Async coercion required but convert used (${edgeLabel})`);
1514
1612
  }
1515
- return res.convert(payload);
1613
+ const converted = res.convert(payload);
1614
+ return wrapIfDstUnion(res.dstType, converted);
1516
1615
  },
1517
1616
  };
1518
1617
  }
1618
+ /**
1619
+ * Compute effective inputs for a node by merging real inputs with defaults.
1620
+ * This is a shared utility used by both NodeExecutor and runtime validators.
1621
+ *
1622
+ * @param nodeId - The node ID to compute effective inputs for
1623
+ * @param graph - Graph component to access node and handle information
1624
+ * @param registry - Registry to access node type descriptors and defaults
1625
+ * @returns Record of effective input values (real inputs merged with defaults)
1626
+ */
1627
+ function getEffectiveInputs(nodeId, graph, registry) {
1628
+ const node = graph.getNode(nodeId);
1629
+ if (!node)
1630
+ return {};
1631
+ const desc = registry.nodes.get(node.typeId);
1632
+ if (!desc)
1633
+ return {};
1634
+ const resolved = graph.getResolvedHandles(nodeId);
1635
+ const regDefaults = desc.inputDefaults ?? {};
1636
+ const dynDefaults = resolved?.inputDefaults ?? {};
1637
+ // Identify which handles are dynamically resolved (not in registry statics)
1638
+ const staticHandles = new Set(Object.keys(desc.inputs ?? {}));
1639
+ const dynamicHandles = new Set(Object.keys(resolved?.inputs ?? {}).filter((h) => !staticHandles.has(h)));
1640
+ // Precedence: dynamic > registry
1641
+ const mergedDefaults = {
1642
+ ...regDefaults,
1643
+ ...dynDefaults,
1644
+ };
1645
+ // Start with real inputs only (no defaults)
1646
+ const effective = { ...node.inputs };
1647
+ // Build set of inbound handles (wired inputs)
1648
+ const inboundEdges = graph.getInboundEdges(nodeId);
1649
+ const inbound = new Set(inboundEdges.map((e) => e.target.handle));
1650
+ // Apply defaults only for:
1651
+ // 1. Unbound handles that have no explicit value
1652
+ // 2. Static handles (not dynamically resolved)
1653
+ for (const [handle, defaultValue] of Object.entries(mergedDefaults)) {
1654
+ if (defaultValue === undefined)
1655
+ continue;
1656
+ if (inbound.has(handle))
1657
+ continue; // Don't override wired inputs
1658
+ if (effective[handle] !== undefined)
1659
+ continue; // Already has value
1660
+ if (dynamicHandles.has(handle))
1661
+ continue; // Skip defaults for dynamic handles
1662
+ // Clone to avoid shared references
1663
+ effective[handle] = structuredClone(defaultValue);
1664
+ }
1665
+ return effective;
1666
+ }
1519
1667
 
1520
1668
  /**
1521
1669
  * HandleResolver component - manages dynamic handle resolution
@@ -1615,12 +1763,11 @@ class HandleResolver {
1615
1763
  const dstNode = this.graph.getNode(e.target.nodeId);
1616
1764
  const oldDstDeclared = e.dstDeclared;
1617
1765
  // Extract edge types using shared helper (handles both source and target updates)
1618
- const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
1766
+ const { srcDeclared, dstDeclared } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode);
1619
1767
  // Update converters
1620
1768
  const conv = buildEdgeConverters(srcDeclared, dstDeclared, registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
1621
1769
  // Update edge properties via Graph
1622
1770
  this.graph.updateEdgeProperties(e.id, {
1623
- effectiveTypeId: !e.typeId ? effectiveTypeId : undefined,
1624
1771
  dstDeclared,
1625
1772
  srcUnionTypes: Array.isArray(srcDeclared)
1626
1773
  ? [...srcDeclared]
@@ -2153,47 +2300,10 @@ class NodeExecutor {
2153
2300
  * Compute effective inputs for a node by merging real inputs with defaults
2154
2301
  */
2155
2302
  getEffectiveInputs(nodeId) {
2156
- const node = this.graph.getNode(nodeId);
2157
- if (!node)
2158
- return {};
2159
2303
  const registry = this.graph.getRegistry();
2160
2304
  if (!registry)
2161
2305
  return {};
2162
- const desc = registry.nodes.get(node.typeId);
2163
- if (!desc)
2164
- return {};
2165
- const resolved = this.graph.getResolvedHandles(nodeId);
2166
- const regDefaults = desc.inputDefaults ?? {};
2167
- const dynDefaults = resolved?.inputDefaults ?? {};
2168
- // Identify which handles are dynamically resolved (not in registry statics)
2169
- const staticHandles = new Set(Object.keys(desc.inputs ?? {}));
2170
- const dynamicHandles = new Set(Object.keys(resolved?.inputs ?? {}).filter((h) => !staticHandles.has(h)));
2171
- // Precedence: dynamic > registry
2172
- const mergedDefaults = {
2173
- ...regDefaults,
2174
- ...dynDefaults,
2175
- };
2176
- // Start with real inputs only (no defaults)
2177
- const effective = { ...node.inputs };
2178
- // Build set of inbound handles (wired inputs)
2179
- const inboundEdges = this.graph.getInboundEdges(nodeId);
2180
- const inbound = new Set(inboundEdges.map((e) => e.target.handle));
2181
- // Apply defaults only for:
2182
- // 1. Unbound handles that have no explicit value
2183
- // 2. Static handles (not dynamically resolved)
2184
- for (const [handle, defaultValue] of Object.entries(mergedDefaults)) {
2185
- if (defaultValue === undefined)
2186
- continue;
2187
- if (inbound.has(handle))
2188
- continue; // Don't override wired inputs
2189
- if (effective[handle] !== undefined)
2190
- continue; // Already has value
2191
- if (dynamicHandles.has(handle))
2192
- continue; // Skip defaults for dynamic handles
2193
- // Clone to avoid shared references
2194
- effective[handle] = structuredClone(defaultValue);
2195
- }
2196
- return effective;
2306
+ return getEffectiveInputs(nodeId, this.graph, registry);
2197
2307
  }
2198
2308
  /**
2199
2309
  * Create an execution context for a node
@@ -2299,6 +2409,20 @@ class NodeExecutor {
2299
2409
  // Early validation for auto-mode paused state
2300
2410
  if (this.runtime.isPaused())
2301
2411
  return;
2412
+ // Check runtime validators (check current state, not just graph definition)
2413
+ const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
2414
+ if (runtimeValidationError) {
2415
+ this.eventEmitter.emit("error", {
2416
+ kind: "system",
2417
+ message: runtimeValidationError.message,
2418
+ code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
2419
+ details: {
2420
+ nodeId,
2421
+ ...runtimeValidationError.details,
2422
+ },
2423
+ });
2424
+ return;
2425
+ }
2302
2426
  // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
2303
2427
  // so that handle resolution can track these run contexts
2304
2428
  if (runContextIds) {
@@ -2757,6 +2881,61 @@ class NodeExecutor {
2757
2881
  }
2758
2882
  }
2759
2883
 
2884
+ /**
2885
+ * RuntimeValidatorManager component - manages runtime validators
2886
+ */
2887
+ class RuntimeValidatorManager {
2888
+ constructor(graph, registry) {
2889
+ this.graph = graph;
2890
+ this.registry = registry;
2891
+ this.validators = [];
2892
+ }
2893
+ /**
2894
+ * Set the registry (called when registry changes)
2895
+ */
2896
+ setRegistry(registry) {
2897
+ this.registry = registry;
2898
+ }
2899
+ /**
2900
+ * Register a runtime validator that will be called before node execution.
2901
+ * Validators are called in registration order - if any returns true, execution is blocked.
2902
+ */
2903
+ registerValidator(validator) {
2904
+ this.validators.push(validator);
2905
+ }
2906
+ /**
2907
+ * Unregister a runtime validator.
2908
+ */
2909
+ unregisterValidator(validator) {
2910
+ const index = this.validators.indexOf(validator);
2911
+ if (index >= 0) {
2912
+ this.validators.splice(index, 1);
2913
+ }
2914
+ }
2915
+ /**
2916
+ * Check if any runtime validator blocks execution for this node.
2917
+ * Returns RuntimeValidationError if execution should be blocked, null otherwise.
2918
+ */
2919
+ hasBlock(nodeId) {
2920
+ if (!this.registry)
2921
+ return null;
2922
+ for (const validator of this.validators) {
2923
+ try {
2924
+ const result = validator(nodeId, this.graph, this.registry);
2925
+ if (result !== false) {
2926
+ // Validator returned an error object
2927
+ return result;
2928
+ }
2929
+ }
2930
+ catch (err) {
2931
+ // Don't let validator errors break execution - log and continue
2932
+ console.error(`Runtime validator error for node ${nodeId}:`, err);
2933
+ }
2934
+ }
2935
+ return null;
2936
+ }
2937
+ }
2938
+
2760
2939
  // Types are now imported from components/types.ts (re-exported above)
2761
2940
  class GraphRuntime {
2762
2941
  constructor() {
@@ -2773,6 +2952,8 @@ class GraphRuntime {
2773
2952
  this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this);
2774
2953
  // Create NodeExecutor with EdgePropagator and HandleResolver
2775
2954
  this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this, this);
2955
+ // Create RuntimeValidatorManager
2956
+ this.runtimeValidatorManager = new RuntimeValidatorManager(this.graph);
2776
2957
  }
2777
2958
  static create(def, registry, opts) {
2778
2959
  const gr = new GraphRuntime();
@@ -2784,6 +2965,7 @@ class GraphRuntime {
2784
2965
  gr.handleResolver.setRegistry(registry);
2785
2966
  gr.handleResolver.setEnvironment(gr.environment);
2786
2967
  gr.nodeExecutor.setEnvironment(gr.environment);
2968
+ gr.runtimeValidatorManager.setRegistry(registry);
2787
2969
  // Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
2788
2970
  const initial = gr.isPaused()
2789
2971
  ? {
@@ -3001,6 +3183,26 @@ class GraphRuntime {
3001
3183
  this.handleResolver.scheduleRecomputeHandles(nodeId);
3002
3184
  }
3003
3185
  }
3186
+ /**
3187
+ * Register a runtime validator that will be called before node execution.
3188
+ * Validators are called in registration order - if any returns true, execution is blocked.
3189
+ */
3190
+ registerRuntimeValidator(validator) {
3191
+ this.runtimeValidatorManager.registerValidator(validator);
3192
+ }
3193
+ /**
3194
+ * Unregister a runtime validator.
3195
+ */
3196
+ unregisterRuntimeValidator(validator) {
3197
+ this.runtimeValidatorManager.unregisterValidator(validator);
3198
+ }
3199
+ /**
3200
+ * Check if any runtime validator blocks execution for this node.
3201
+ * Returns RuntimeValidationError if execution should be blocked, null otherwise.
3202
+ */
3203
+ hasRuntimeValidationBlock(nodeId) {
3204
+ return this.runtimeValidatorManager.hasBlock(nodeId);
3205
+ }
3004
3206
  getGraphDef() {
3005
3207
  const nodes = [];
3006
3208
  this.graph.forEachNode((n) => {
@@ -3466,20 +3668,12 @@ class GraphBuilder {
3466
3668
  return { inputs, outputs };
3467
3669
  };
3468
3670
  const normOut = (decl) => Array.isArray(decl) ? decl : decl ? [decl] : [];
3469
- const inferEdgeType = (srcDeclared, dstDeclared, explicit) => {
3470
- if (explicit)
3471
- return explicit;
3472
- if (Array.isArray(srcDeclared) && dstDeclared)
3473
- return dstDeclared;
3474
- if (srcDeclared)
3475
- return Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
3476
- return undefined;
3477
- };
3478
3671
  const canFlow = (from, to) => {
3479
3672
  if (!to || !from)
3480
3673
  return true;
3481
- const arr = Array.isArray(from) ? from : [from];
3482
- return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
3674
+ const srcTypes = Array.isArray(from) ? from : [from];
3675
+ const dstTypes = Array.isArray(to) ? to : [to];
3676
+ return srcTypes.some((s) => dstTypes.some((t) => s === t || !!this.registry.canCoerce(s, t)));
3483
3677
  };
3484
3678
  // Helper to validate enum value
3485
3679
  const validateEnumValue = (typeId, value, nodeId, handle) => {
@@ -3590,37 +3784,29 @@ class GraphBuilder {
3590
3784
  };
3591
3785
  const dstEff = effByNodeId.get(e.target.nodeId) || {
3592
3786
  inputs: {}};
3593
- const _srcDeclared = srcNode
3594
- ? srcEff.outputs[e.source.handle]
3595
- : undefined;
3596
- const _dstDeclared = dstNode
3597
- ? getInputTypeId(dstEff.inputs, e.target.handle)
3598
- : undefined;
3599
- // Effective edge type
3600
- const effectiveTypeId = inferEdgeType(_srcDeclared, _dstDeclared, e.typeId);
3601
- const type = effectiveTypeId
3602
- ? this.registry.types.get(effectiveTypeId)
3603
- : undefined;
3604
- if (!type) {
3605
- pushIssue("error", "TYPE_MISSING", `Edge ${e.id} type missing or unknown`, {
3606
- edgeId: e.id,
3607
- });
3787
+ // Validate explicit type if provided
3788
+ if (e.typeId) {
3789
+ const type = this.registry.types.get(e.typeId);
3790
+ if (!type) {
3791
+ pushIssue("error", "TYPE_MISSING", `Edge ${e.id} explicit type ${e.typeId} is missing or unknown`, { edgeId: e.id });
3792
+ }
3608
3793
  }
3609
3794
  if (srcNode) {
3610
3795
  if (!(e.source.handle in srcEff.outputs)) {
3611
3796
  pushIssue("error", "OUTPUT_MISSING", `Edge ${e.id} source output ${e.source.handle} missing on ${srcNode.typeId}`, { edgeId: e.id, nodeId: srcNode.nodeId, output: e.source.handle });
3612
3797
  }
3613
3798
  const declaredArr = normOut(srcEff.outputs[e.source.handle]);
3614
- if (declaredArr.length > 0 &&
3615
- effectiveTypeId &&
3616
- !canFlow(declaredArr, effectiveTypeId)) {
3617
- pushIssue("error", "TYPE_MISMATCH_OUTPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declaredArr.join("|")}) and no coercion exists`, {
3618
- edgeId: e.id,
3619
- nodeId: srcNode.nodeId,
3620
- output: e.source.handle,
3621
- declared: declaredArr.join("|"),
3622
- effectiveTypeId,
3623
- });
3799
+ if (declaredArr.length > 0) {
3800
+ // Check if explicit type matches source output
3801
+ if (e.typeId && !canFlow(declaredArr, e.typeId)) {
3802
+ pushIssue("error", "TYPE_MISMATCH_OUTPUT", `Edge ${e.id} explicit type ${e.typeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declaredArr.join("|")}) and no coercion exists`, {
3803
+ edgeId: e.id,
3804
+ nodeId: srcNode.nodeId,
3805
+ output: e.source.handle,
3806
+ declared: declaredArr.join("|"),
3807
+ typeId: e.typeId,
3808
+ });
3809
+ }
3624
3810
  }
3625
3811
  }
3626
3812
  if (dstNode) {
@@ -3631,30 +3817,30 @@ class GraphBuilder {
3631
3817
  if (isInputPrivate(dstEff.inputs, e.target.handle)) {
3632
3818
  pushIssue("error", "INPUT_PRIVATE", `Edge ${e.id} targets private input ${dstNode.typeId}.${e.target.handle}`, { edgeId: e.id, nodeId: dstNode.nodeId, input: e.target.handle });
3633
3819
  }
3634
- const declaredIn = getInputTypeId(dstEff.inputs, e.target.handle);
3635
- if (declaredIn && effectiveTypeId) {
3820
+ const declaredIn = getInputDeclaredTypes(dstEff.inputs, e.target.handle);
3821
+ const declaredInArr = normOut(declaredIn);
3822
+ if (declaredInArr.length > 0) {
3636
3823
  if (srcNode) {
3637
3824
  const srcDeclared = srcEff.outputs[e.source.handle];
3638
- const srcArr = normOut(srcDeclared).length
3639
- ? normOut(srcDeclared)
3640
- : [effectiveTypeId];
3641
- if (!canFlow(srcArr, declaredIn)) {
3642
- pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} output type ${srcArr.join("|")} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`, {
3825
+ const srcArr = normOut(srcDeclared);
3826
+ if (srcArr.length > 0 && !canFlow(srcArr, declaredInArr)) {
3827
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} output type ${srcArr.join("|")} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredInArr.join("|")})`, {
3643
3828
  edgeId: e.id,
3644
3829
  nodeId: dstNode.nodeId,
3645
3830
  input: e.target.handle,
3646
- declared: declaredIn,
3647
- effectiveTypeId: srcArr.join("|"),
3831
+ declared: declaredInArr.join("|"),
3832
+ typeId: srcArr.join("|"),
3648
3833
  });
3649
3834
  }
3650
3835
  }
3651
- else if (!canFlow(effectiveTypeId, declaredIn)) {
3652
- pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`, {
3836
+ else if (e.typeId && !canFlow([e.typeId], declaredInArr)) {
3837
+ // External source with explicit type
3838
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} explicit type ${e.typeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredInArr.join("|")}) and no coercion exists`, {
3653
3839
  edgeId: e.id,
3654
3840
  nodeId: dstNode.nodeId,
3655
3841
  input: e.target.handle,
3656
- declared: declaredIn,
3657
- effectiveTypeId,
3842
+ declared: declaredInArr.join("|"),
3843
+ typeId: e.typeId,
3658
3844
  });
3659
3845
  }
3660
3846
  }
@@ -5590,9 +5776,17 @@ function buildTypeMaps(def) {
5590
5776
  const nodeOutputTypes = new Map();
5591
5777
  if (node.resolvedHandles?.inputs) {
5592
5778
  for (const [handleId, handleDesc] of Object.entries(node.resolvedHandles.inputs)) {
5593
- const typeId = typeof handleDesc === "string"
5594
- ? handleDesc
5595
- : handleDesc?.typeId;
5779
+ let typeId;
5780
+ if (typeof handleDesc === "string") {
5781
+ typeId = handleDesc;
5782
+ }
5783
+ else if (Array.isArray(handleDesc)) {
5784
+ typeId = handleDesc[0]; // Use first type for type map (backward compat)
5785
+ }
5786
+ else {
5787
+ const descTypeId = handleDesc.typeId;
5788
+ typeId = Array.isArray(descTypeId) ? descTypeId[0] : descTypeId;
5789
+ }
5596
5790
  if (typeId)
5597
5791
  nodeInputTypes.set(handleId, typeId);
5598
5792
  }
@@ -5615,9 +5809,17 @@ function buildTypeMaps(def) {
5615
5809
  if (!nodeInputTypes.has(handleId) && node.resolvedHandles?.inputs) {
5616
5810
  const inputDesc = node.resolvedHandles.inputs[handleId];
5617
5811
  if (inputDesc) {
5618
- const typeId = typeof inputDesc === "string"
5619
- ? inputDesc
5620
- : inputDesc?.typeId;
5812
+ let typeId;
5813
+ if (typeof inputDesc === "string") {
5814
+ typeId = inputDesc;
5815
+ }
5816
+ else if (Array.isArray(inputDesc)) {
5817
+ typeId = inputDesc[0]; // Use first type for type map (backward compat)
5818
+ }
5819
+ else {
5820
+ const descTypeId = inputDesc.typeId;
5821
+ typeId = Array.isArray(descTypeId) ? descTypeId[0] : descTypeId;
5822
+ }
5621
5823
  if (typeId)
5622
5824
  nodeInputTypes.set(handleId, typeId);
5623
5825
  }
@@ -6053,5 +6255,5 @@ function buildValueConverter(config) {
6053
6255
  };
6054
6256
  }
6055
6257
 
6056
- export { BaseCompareOperation, BaseLogicOperation, BaseMathOperation, CompositeCategory, ComputeCategory, GraphBuilder, GraphRuntime, LevelLogger, LocalEngine, Registry, buildValueConverter, computeGraphCenter, convertSnapshot, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createSimpleGraphDef, createSimpleGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, findMatchingPaths, generateId, getInputHandleMetadata, getInputTypeId, getTypedOutputTypeId, getTypedOutputValue, getValueAtPath, installLogging, isInputPrivate, isTypedOutput, mergeInputHandleDescriptors, mergeRuntimeState, mergeSnapshots, offsetImportedPositions, parseJsonPath, registerDelayNode, registerProgressNodes, setValueAtPath, setValueAtPathWithCreation, typed };
6258
+ export { BaseCompareOperation, BaseLogicOperation, BaseMathOperation, CompositeCategory, ComputeCategory, Graph, GraphBuilder, GraphRuntime, LevelLogger, LocalEngine, Registry, buildValueConverter, computeGraphCenter, convertSnapshot, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createSimpleGraphDef, createSimpleGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, findMatchingPaths, generateId, getEffectiveInputs, getInputDeclaredTypes, getInputHandleMetadata, getInputTypeId, getTypedOutputTypeId, getTypedOutputValue, getValueAtPath, installLogging, isInputPrivate, isTypedOutput, mergeInputHandleDescriptors, mergeRuntimeState, mergeSnapshots, offsetImportedPositions, parseJsonPath, registerDelayNode, registerProgressNodes, setValueAtPath, setValueAtPathWithCreation, typed };
6057
6259
  //# sourceMappingURL=index.js.map