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