@bian-womp/spark-graph 0.2.16 → 0.2.18

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.
package/lib/cjs/index.cjs CHANGED
@@ -477,23 +477,20 @@ class GraphRuntime {
477
477
  }
478
478
  // Instantiate edges
479
479
  gr.edges = GraphRuntime.buildEdges(def, registry, gr.resolvedByNode);
480
- // After nodes and edges exist, seed registry- and graph-level defaults
480
+ // After nodes and edges exist, seed registry-, dynamic- and graph-level defaults
481
481
  for (const n of def.nodes) {
482
482
  const node = gr.nodes.get(n.nodeId);
483
483
  const desc = registry.nodes.get(n.typeId);
484
484
  if (!node || !desc)
485
485
  continue;
486
- // Resolve registry-level defaults (object or function)
487
- const regDefaults = typeof desc.inputDefaults === "function"
488
- ? desc.inputDefaults({
489
- params: n.params,
490
- environment: gr.environment,
491
- })
492
- : desc.inputDefaults ?? {};
486
+ // Resolve registry-level defaults and dynamic (resolved) defaults
487
+ const regDefaults = desc.inputDefaults ?? {};
488
+ const dynDefaults = gr.resolvedByNode.get(n.nodeId)?.inputDefaults ?? {};
493
489
  const graphDefaults = n.initialInputs ?? {};
494
- // Apply precedence: graph-level overrides registry-level
490
+ // Apply precedence: graph-level overrides dynamic, which overrides registry-level
495
491
  const merged = {
496
492
  ...regDefaults,
493
+ ...dynDefaults,
497
494
  ...graphDefaults,
498
495
  };
499
496
  for (const [handle, value] of Object.entries(merged)) {
@@ -915,10 +912,12 @@ class GraphRuntime {
915
912
  continue;
916
913
  const overrideInputs = n.resolvedHandles?.inputs;
917
914
  const overrideOutputs = n.resolvedHandles?.outputs;
915
+ const overrideDefaults = n.resolvedHandles?.inputDefaults;
918
916
  // Merge base with overrides (allow partial resolvedHandles)
919
917
  const inputs = { ...desc.inputs, ...overrideInputs };
920
918
  const outputs = { ...desc.outputs, ...overrideOutputs };
921
- out.set(n.nodeId, { inputs, outputs });
919
+ const inputDefaults = { ...desc.inputDefaults, ...overrideDefaults };
920
+ out.set(n.nodeId, { inputs, outputs, inputDefaults });
922
921
  }
923
922
  return out;
924
923
  }
@@ -1326,15 +1325,12 @@ class GraphRuntime {
1326
1325
  if (defNode) {
1327
1326
  const desc = registry.nodes.get(defNode.typeId);
1328
1327
  if (desc) {
1329
- const regDefaults = typeof desc.inputDefaults === "function"
1330
- ? desc.inputDefaults({
1331
- params: defNode.params,
1332
- environment: this.environment,
1333
- })
1334
- : desc.inputDefaults ?? {};
1328
+ const regDefaults = desc.inputDefaults ?? {};
1329
+ const dynDefaults = this.resolvedByNode.get(defNode.nodeId)?.inputDefaults ?? {};
1335
1330
  const graphDefaults = defNode.initialInputs ?? {};
1336
1331
  const merged = {
1337
1332
  ...regDefaults,
1333
+ ...dynDefaults,
1338
1334
  ...graphDefaults,
1339
1335
  };
1340
1336
  for (const h of Array.from(prevSet)) {
@@ -1406,6 +1402,34 @@ class GraphRuntime {
1406
1402
  }
1407
1403
  }
1408
1404
  }
1405
+ // Seed defaults for nodes (new or existing) where inputs are still undefined and not inbound
1406
+ for (const n of def.nodes) {
1407
+ const node = this.nodes.get(n.nodeId);
1408
+ const desc = registry.nodes.get(n.typeId);
1409
+ if (!node || !desc)
1410
+ continue;
1411
+ const regDefaults = desc.inputDefaults ?? {};
1412
+ const dynDefaults = this.resolvedByNode.get(n.nodeId)?.inputDefaults ?? {};
1413
+ const graphDefaults = n.initialInputs ?? {};
1414
+ const merged = {
1415
+ ...regDefaults,
1416
+ ...dynDefaults,
1417
+ ...graphDefaults,
1418
+ };
1419
+ const inboundSet = nextInbound.get(n.nodeId) ?? new Set();
1420
+ for (const [handle, value] of Object.entries(merged)) {
1421
+ if (value === undefined)
1422
+ continue;
1423
+ if (inboundSet.has(handle))
1424
+ continue;
1425
+ if (node.inputs[handle] !== undefined)
1426
+ continue;
1427
+ node.inputs[handle] =
1428
+ typeof structuredClone === "function"
1429
+ ? structuredClone(value)
1430
+ : JSON.parse(JSON.stringify(value));
1431
+ }
1432
+ }
1409
1433
  // Prune array bucket contributions for edges that no longer exist
1410
1434
  const validPerTarget = new Map();
1411
1435
  for (const ed of this.edges) {
@@ -1440,35 +1464,56 @@ class GraphBuilder {
1440
1464
  const issues = [];
1441
1465
  const nodeIds = new Set();
1442
1466
  const edgeIds = new Set();
1467
+ // Precompute effective handle maps (registry statics merged with per-node resolvedHandles)
1468
+ const nodeById = new Map();
1469
+ const effByNodeId = new Map();
1470
+ const getEffectiveHandles = (n) => {
1471
+ if (!n)
1472
+ return { inputs: {}, outputs: {} };
1473
+ const desc = this.registry.nodes.get(n.typeId);
1474
+ const resolved = n.resolvedHandles || {};
1475
+ const inputs = { ...desc?.inputs, ...resolved.inputs };
1476
+ const outputs = { ...desc?.outputs, ...resolved.outputs };
1477
+ return { inputs, outputs };
1478
+ };
1479
+ const normOut = (decl) => Array.isArray(decl) ? decl : decl ? [decl] : [];
1480
+ const inferEdgeType = (srcDeclared, dstDeclared, explicit) => {
1481
+ if (explicit)
1482
+ return explicit;
1483
+ if (Array.isArray(srcDeclared) && dstDeclared)
1484
+ return dstDeclared;
1485
+ if (srcDeclared)
1486
+ return Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
1487
+ return undefined;
1488
+ };
1489
+ const canFlow = (from, to) => {
1490
+ if (!to || !from)
1491
+ return true;
1492
+ const arr = Array.isArray(from) ? from : [from];
1493
+ return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
1494
+ };
1495
+ const pushIssue = (level, code, message, data) => {
1496
+ issues.push({ level, code, message, data });
1497
+ };
1443
1498
  // nodes exist, ids unique, and categories registered
1444
1499
  for (const n of def.nodes) {
1445
1500
  if (nodeIds.has(n.nodeId)) {
1446
- issues.push({
1447
- level: "error",
1448
- code: "NODE_ID_DUP",
1449
- message: `Duplicate nodeId ${n.nodeId}`,
1450
- data: { nodeId: n.nodeId },
1501
+ pushIssue("error", "NODE_ID_DUP", `Duplicate nodeId ${n.nodeId}`, {
1502
+ nodeId: n.nodeId,
1451
1503
  });
1452
1504
  }
1453
1505
  else {
1454
1506
  nodeIds.add(n.nodeId);
1455
1507
  }
1508
+ nodeById.set(n.nodeId, n);
1509
+ effByNodeId.set(n.nodeId, getEffectiveHandles(n));
1456
1510
  const nodeType = this.registry.nodes.get(n.typeId);
1457
1511
  if (!nodeType) {
1458
- issues.push({
1459
- level: "error",
1460
- code: "NODE_TYPE_MISSING",
1461
- message: `Unknown node type ${n.typeId}`,
1462
- data: { typeId: n.typeId, nodeId: n.nodeId },
1463
- });
1512
+ pushIssue("error", "NODE_TYPE_MISSING", `Unknown node type ${n.typeId}`, { typeId: n.typeId, nodeId: n.nodeId });
1464
1513
  continue;
1465
1514
  }
1466
1515
  if (!this.registry.categories.has(nodeType.categoryId)) {
1467
- issues.push({
1468
- level: "error",
1469
- code: "CATEGORY_MISSING",
1470
- message: `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`,
1471
- });
1516
+ pushIssue("error", "CATEGORY_MISSING", `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`);
1472
1517
  }
1473
1518
  }
1474
1519
  // edges validation: nodes exist, handles exist, type exists
@@ -1477,186 +1522,96 @@ class GraphBuilder {
1477
1522
  const inboundArrayOk = new Set();
1478
1523
  for (const e of def.edges) {
1479
1524
  if (edgeIds.has(e.id)) {
1480
- issues.push({
1481
- level: "error",
1482
- code: "EDGE_ID_DUP",
1483
- message: `Duplicate edge id ${e.id}`,
1484
- data: { edgeId: e.id },
1525
+ pushIssue("error", "EDGE_ID_DUP", `Duplicate edge id ${e.id}`, {
1526
+ edgeId: e.id,
1485
1527
  });
1486
1528
  }
1487
1529
  else {
1488
1530
  edgeIds.add(e.id);
1489
1531
  }
1490
- const srcNode = def.nodes.find((nn) => nn.nodeId === e.source.nodeId);
1491
- const dstNode = def.nodes.find((nn) => nn.nodeId === e.target.nodeId);
1532
+ const srcNode = nodeById.get(e.source.nodeId);
1533
+ const dstNode = nodeById.get(e.target.nodeId);
1492
1534
  if (!srcNode)
1493
- issues.push({
1494
- level: "error",
1495
- code: "EDGE_SOURCE_MISSING",
1496
- message: `Edge ${e.id} source node missing`,
1497
- data: { edgeId: e.id },
1535
+ pushIssue("error", "EDGE_SOURCE_MISSING", `Edge ${e.id} source node missing`, {
1536
+ edgeId: e.id,
1498
1537
  });
1499
1538
  if (!dstNode)
1500
- issues.push({
1501
- level: "error",
1502
- code: "EDGE_TARGET_MISSING",
1503
- message: `Edge ${e.id} target node missing`,
1504
- data: { edgeId: e.id },
1539
+ pushIssue("error", "EDGE_TARGET_MISSING", `Edge ${e.id} target node missing`, {
1540
+ edgeId: e.id,
1505
1541
  });
1506
- // Infer edge type when missing. For union sources, prefer target input type if available.
1507
- let effectiveTypeId = e.typeId;
1508
- let _srcDeclared;
1509
- let _dstDeclared;
1510
- if (srcNode) {
1511
- const srcType = this.registry.nodes.get(srcNode.typeId);
1512
- if (srcType)
1513
- _srcDeclared = srcType.outputs[e.source.handle];
1514
- }
1515
- if (dstNode) {
1516
- const dstType = this.registry.nodes.get(dstNode.typeId);
1517
- if (dstType)
1518
- _dstDeclared = getInputTypeId(dstType.inputs, e.target.handle);
1519
- }
1520
- if (!effectiveTypeId) {
1521
- if (Array.isArray(_srcDeclared) && _dstDeclared) {
1522
- // When source is a union and target input type is known, adopt the input type
1523
- // so validation checks are performed against the target, not an arbitrary variant.
1524
- effectiveTypeId = _dstDeclared;
1525
- }
1526
- else if (_srcDeclared) {
1527
- effectiveTypeId = Array.isArray(_srcDeclared)
1528
- ? _srcDeclared[0]
1529
- : _srcDeclared;
1530
- }
1531
- }
1542
+ // Effective handle declarations
1543
+ const srcEff = effByNodeId.get(e.source.nodeId) || {
1544
+ outputs: {},
1545
+ };
1546
+ const dstEff = effByNodeId.get(e.target.nodeId) || {
1547
+ inputs: {}};
1548
+ const _srcDeclared = srcNode
1549
+ ? srcEff.outputs[e.source.handle]
1550
+ : undefined;
1551
+ const _dstDeclared = dstNode
1552
+ ? getInputTypeId(dstEff.inputs, e.target.handle)
1553
+ : undefined;
1554
+ // Effective edge type
1555
+ const effectiveTypeId = inferEdgeType(_srcDeclared, _dstDeclared, e.typeId);
1532
1556
  const type = effectiveTypeId
1533
1557
  ? this.registry.types.get(effectiveTypeId)
1534
1558
  : undefined;
1535
1559
  if (!type) {
1536
- issues.push({
1537
- level: "error",
1538
- code: "TYPE_MISSING",
1539
- message: `Edge ${e.id} type missing or unknown`,
1540
- data: { edgeId: e.id },
1560
+ pushIssue("error", "TYPE_MISSING", `Edge ${e.id} type missing or unknown`, {
1561
+ edgeId: e.id,
1541
1562
  });
1542
1563
  }
1543
1564
  if (srcNode) {
1544
- const srcType = this.registry.nodes.get(srcNode.typeId);
1545
- if (srcType && !(e.source.handle in srcType.outputs)) {
1546
- issues.push({
1547
- level: "error",
1548
- code: "OUTPUT_MISSING",
1549
- message: `Edge ${e.id} source output ${e.source.handle} missing on ${srcNode.typeId}`,
1550
- data: {
1551
- edgeId: e.id,
1552
- nodeId: srcNode.nodeId,
1553
- output: e.source.handle,
1554
- },
1555
- });
1565
+ if (!(e.source.handle in srcEff.outputs)) {
1566
+ 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 });
1556
1567
  }
1557
- if (srcType) {
1558
- const declared = srcType.outputs[e.source.handle];
1559
- const declaredArr = Array.isArray(declared)
1560
- ? declared
1561
- : declared
1562
- ? [declared]
1563
- : [];
1564
- if (declaredArr.length > 0 && effectiveTypeId) {
1565
- for (const s of declaredArr) {
1566
- if (s !== effectiveTypeId &&
1567
- !this.registry.canCoerce(s, effectiveTypeId)) {
1568
- issues.push({
1569
- level: "error",
1570
- code: "TYPE_MISMATCH_OUTPUT",
1571
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${s}) and no coercion exists`,
1572
- data: {
1573
- edgeId: e.id,
1574
- nodeId: srcNode.nodeId,
1575
- output: e.source.handle,
1576
- declared: s,
1577
- effectiveTypeId,
1578
- },
1579
- });
1580
- }
1581
- }
1582
- }
1568
+ const declaredArr = normOut(srcEff.outputs[e.source.handle]);
1569
+ if (declaredArr.length > 0 &&
1570
+ effectiveTypeId &&
1571
+ !canFlow(declaredArr, effectiveTypeId)) {
1572
+ pushIssue("error", "TYPE_MISMATCH_OUTPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declaredArr.join("|")}) and no coercion exists`, {
1573
+ edgeId: e.id,
1574
+ nodeId: srcNode.nodeId,
1575
+ output: e.source.handle,
1576
+ declared: declaredArr.join("|"),
1577
+ effectiveTypeId,
1578
+ });
1583
1579
  }
1584
1580
  }
1585
1581
  if (dstNode) {
1586
- const dstType = this.registry.nodes.get(dstNode.typeId);
1587
- if (dstType && !(e.target.handle in dstType.inputs)) {
1588
- issues.push({
1589
- level: "error",
1590
- code: "INPUT_MISSING",
1591
- message: `Edge ${e.id} target input ${e.target.handle} missing on ${dstNode.typeId}`,
1592
- data: {
1593
- edgeId: e.id,
1594
- nodeId: dstNode.nodeId,
1595
- input: e.target.handle,
1596
- },
1597
- });
1582
+ if (!(e.target.handle in dstEff.inputs)) {
1583
+ pushIssue("error", "INPUT_MISSING", `Edge ${e.id} target input ${e.target.handle} missing on ${dstNode.typeId}`, { edgeId: e.id, nodeId: dstNode.nodeId, input: e.target.handle });
1584
+ }
1585
+ // Private inputs should not accept edges
1586
+ if (isInputPrivate(dstEff.inputs, e.target.handle)) {
1587
+ 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 });
1598
1588
  }
1599
- if (dstType) {
1600
- // Private inputs should not accept edges
1601
- if (isInputPrivate(dstType.inputs, e.target.handle)) {
1602
- issues.push({
1603
- level: "error",
1604
- code: "INPUT_PRIVATE",
1605
- message: `Edge ${e.id} targets private input ${dstNode.typeId}.${e.target.handle}`,
1606
- data: {
1589
+ const declaredIn = getInputTypeId(dstEff.inputs, e.target.handle);
1590
+ if (declaredIn && effectiveTypeId) {
1591
+ if (srcNode) {
1592
+ const srcDeclared = srcEff.outputs[e.source.handle];
1593
+ const srcArr = normOut(srcDeclared).length
1594
+ ? normOut(srcDeclared)
1595
+ : [effectiveTypeId];
1596
+ if (!canFlow(srcArr, declaredIn)) {
1597
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} output type ${srcArr.join("|")} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`, {
1607
1598
  edgeId: e.id,
1608
1599
  nodeId: dstNode.nodeId,
1609
1600
  input: e.target.handle,
1610
- },
1611
- });
1612
- }
1613
- const declaredIn = getInputTypeId(dstType.inputs, e.target.handle);
1614
- if (declaredIn && effectiveTypeId) {
1615
- // If source is a union, ensure each variant can reach declaredIn
1616
- if (srcNode) {
1617
- const srcType = this.registry.nodes.get(srcNode.typeId);
1618
- const srcDeclared = srcType?.outputs[e.source.handle];
1619
- const srcArr = Array.isArray(srcDeclared)
1620
- ? srcDeclared
1621
- : srcDeclared
1622
- ? [srcDeclared]
1623
- : effectiveTypeId
1624
- ? [effectiveTypeId]
1625
- : [];
1626
- for (const s of srcArr) {
1627
- if (s !== declaredIn &&
1628
- !this.registry.canCoerce(s, declaredIn)) {
1629
- issues.push({
1630
- level: "error",
1631
- code: "TYPE_MISMATCH_INPUT",
1632
- message: `Edge ${e.id} output type ${s} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`,
1633
- data: {
1634
- edgeId: e.id,
1635
- nodeId: dstNode.nodeId,
1636
- input: e.target.handle,
1637
- declared: declaredIn,
1638
- effectiveTypeId: s,
1639
- },
1640
- });
1641
- }
1642
- }
1643
- }
1644
- else if (declaredIn !== effectiveTypeId &&
1645
- !this.registry.canCoerce(effectiveTypeId, declaredIn)) {
1646
- issues.push({
1647
- level: "error",
1648
- code: "TYPE_MISMATCH_INPUT",
1649
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`,
1650
- data: {
1651
- edgeId: e.id,
1652
- nodeId: dstNode.nodeId,
1653
- input: e.target.handle,
1654
- declared: declaredIn,
1655
- effectiveTypeId,
1656
- },
1601
+ declared: declaredIn,
1602
+ effectiveTypeId: srcArr.join("|"),
1657
1603
  });
1658
1604
  }
1659
1605
  }
1606
+ else if (!canFlow(effectiveTypeId, declaredIn)) {
1607
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`, {
1608
+ edgeId: e.id,
1609
+ nodeId: dstNode.nodeId,
1610
+ input: e.target.handle,
1611
+ declared: declaredIn,
1612
+ effectiveTypeId,
1613
+ });
1614
+ }
1660
1615
  }
1661
1616
  }
1662
1617
  // Track multiple inbound edges targeting the same input handle
@@ -1664,10 +1619,7 @@ class GraphBuilder {
1664
1619
  inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
1665
1620
  // If the target input is declared as an array type, allow multi-inbound (runtime will append)
1666
1621
  if (dstNode) {
1667
- const dstType = this.registry.nodes.get(dstNode.typeId);
1668
- const declaredIn = dstType
1669
- ? getInputTypeId(dstType.inputs, e.target.handle)
1670
- : undefined;
1622
+ const declaredIn = getInputTypeId((effByNodeId.get(dstNode.nodeId) || { inputs: {} }).inputs, e.target.handle);
1671
1623
  if (typeof declaredIn === "string" && declaredIn.endsWith("[]")) {
1672
1624
  inboundArrayOk.add(inboundKey);
1673
1625
  }
@@ -2689,6 +2641,30 @@ function setupBasicGraphRegistry() {
2689
2641
  return { Items: Array.from({ length }, (_, i) => ins[`Item${i}`]) };
2690
2642
  },
2691
2643
  });
2644
+ // Decompose array into dynamic item outputs
2645
+ registry.registerNode({
2646
+ id: "base.array.decompose",
2647
+ categoryId: "compute",
2648
+ inputs: { Items: "base.object" },
2649
+ outputs: {},
2650
+ resolveHandles: ({ inputs }) => {
2651
+ const maxLen = 64;
2652
+ const arr = Array.isArray(inputs?.Items) ? inputs?.Items : [];
2653
+ const n = Math.max(0, Math.min(maxLen, arr.length));
2654
+ const dyn = {};
2655
+ for (let i = 0; i < n; i++)
2656
+ dyn[`Item${i}`] = "base.object";
2657
+ return { outputs: dyn };
2658
+ },
2659
+ impl: (ins) => {
2660
+ const arr = Array.isArray(ins.Items) ? ins.Items : [];
2661
+ const out = {};
2662
+ const n = Math.max(0, Math.min(64, arr.length));
2663
+ for (let i = 0; i < n; i++)
2664
+ out[`Item${i}`] = arr[i];
2665
+ return out;
2666
+ },
2667
+ });
2692
2668
  // Select
2693
2669
  registry.registerNode({
2694
2670
  id: "base.select",