@bian-womp/spark-graph 0.2.16 → 0.2.17

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
@@ -1440,35 +1440,56 @@ class GraphBuilder {
1440
1440
  const issues = [];
1441
1441
  const nodeIds = new Set();
1442
1442
  const edgeIds = new Set();
1443
+ // Precompute effective handle maps (registry statics merged with per-node resolvedHandles)
1444
+ const nodeById = new Map();
1445
+ const effByNodeId = new Map();
1446
+ const getEffectiveHandles = (n) => {
1447
+ if (!n)
1448
+ return { inputs: {}, outputs: {} };
1449
+ const desc = this.registry.nodes.get(n.typeId);
1450
+ const resolved = n.resolvedHandles || {};
1451
+ const inputs = { ...desc?.inputs, ...resolved.inputs };
1452
+ const outputs = { ...desc?.outputs, ...resolved.outputs };
1453
+ return { inputs, outputs };
1454
+ };
1455
+ const normOut = (decl) => Array.isArray(decl) ? decl : decl ? [decl] : [];
1456
+ const inferEdgeType = (srcDeclared, dstDeclared, explicit) => {
1457
+ if (explicit)
1458
+ return explicit;
1459
+ if (Array.isArray(srcDeclared) && dstDeclared)
1460
+ return dstDeclared;
1461
+ if (srcDeclared)
1462
+ return Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
1463
+ return undefined;
1464
+ };
1465
+ const canFlow = (from, to) => {
1466
+ if (!to || !from)
1467
+ return true;
1468
+ const arr = Array.isArray(from) ? from : [from];
1469
+ return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
1470
+ };
1471
+ const pushIssue = (level, code, message, data) => {
1472
+ issues.push({ level, code, message, data });
1473
+ };
1443
1474
  // nodes exist, ids unique, and categories registered
1444
1475
  for (const n of def.nodes) {
1445
1476
  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 },
1477
+ pushIssue("error", "NODE_ID_DUP", `Duplicate nodeId ${n.nodeId}`, {
1478
+ nodeId: n.nodeId,
1451
1479
  });
1452
1480
  }
1453
1481
  else {
1454
1482
  nodeIds.add(n.nodeId);
1455
1483
  }
1484
+ nodeById.set(n.nodeId, n);
1485
+ effByNodeId.set(n.nodeId, getEffectiveHandles(n));
1456
1486
  const nodeType = this.registry.nodes.get(n.typeId);
1457
1487
  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
- });
1488
+ pushIssue("error", "NODE_TYPE_MISSING", `Unknown node type ${n.typeId}`, { typeId: n.typeId, nodeId: n.nodeId });
1464
1489
  continue;
1465
1490
  }
1466
1491
  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
- });
1492
+ pushIssue("error", "CATEGORY_MISSING", `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`);
1472
1493
  }
1473
1494
  }
1474
1495
  // edges validation: nodes exist, handles exist, type exists
@@ -1477,186 +1498,96 @@ class GraphBuilder {
1477
1498
  const inboundArrayOk = new Set();
1478
1499
  for (const e of def.edges) {
1479
1500
  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 },
1501
+ pushIssue("error", "EDGE_ID_DUP", `Duplicate edge id ${e.id}`, {
1502
+ edgeId: e.id,
1485
1503
  });
1486
1504
  }
1487
1505
  else {
1488
1506
  edgeIds.add(e.id);
1489
1507
  }
1490
- const srcNode = def.nodes.find((nn) => nn.nodeId === e.source.nodeId);
1491
- const dstNode = def.nodes.find((nn) => nn.nodeId === e.target.nodeId);
1508
+ const srcNode = nodeById.get(e.source.nodeId);
1509
+ const dstNode = nodeById.get(e.target.nodeId);
1492
1510
  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 },
1511
+ pushIssue("error", "EDGE_SOURCE_MISSING", `Edge ${e.id} source node missing`, {
1512
+ edgeId: e.id,
1498
1513
  });
1499
1514
  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 },
1515
+ pushIssue("error", "EDGE_TARGET_MISSING", `Edge ${e.id} target node missing`, {
1516
+ edgeId: e.id,
1505
1517
  });
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
- }
1518
+ // Effective handle declarations
1519
+ const srcEff = effByNodeId.get(e.source.nodeId) || {
1520
+ outputs: {},
1521
+ };
1522
+ const dstEff = effByNodeId.get(e.target.nodeId) || {
1523
+ inputs: {}};
1524
+ const _srcDeclared = srcNode
1525
+ ? srcEff.outputs[e.source.handle]
1526
+ : undefined;
1527
+ const _dstDeclared = dstNode
1528
+ ? getInputTypeId(dstEff.inputs, e.target.handle)
1529
+ : undefined;
1530
+ // Effective edge type
1531
+ const effectiveTypeId = inferEdgeType(_srcDeclared, _dstDeclared, e.typeId);
1532
1532
  const type = effectiveTypeId
1533
1533
  ? this.registry.types.get(effectiveTypeId)
1534
1534
  : undefined;
1535
1535
  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 },
1536
+ pushIssue("error", "TYPE_MISSING", `Edge ${e.id} type missing or unknown`, {
1537
+ edgeId: e.id,
1541
1538
  });
1542
1539
  }
1543
1540
  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
- });
1541
+ if (!(e.source.handle in srcEff.outputs)) {
1542
+ 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
1543
  }
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
- }
1544
+ const declaredArr = normOut(srcEff.outputs[e.source.handle]);
1545
+ if (declaredArr.length > 0 &&
1546
+ effectiveTypeId &&
1547
+ !canFlow(declaredArr, effectiveTypeId)) {
1548
+ pushIssue("error", "TYPE_MISMATCH_OUTPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declaredArr.join("|")}) and no coercion exists`, {
1549
+ edgeId: e.id,
1550
+ nodeId: srcNode.nodeId,
1551
+ output: e.source.handle,
1552
+ declared: declaredArr.join("|"),
1553
+ effectiveTypeId,
1554
+ });
1583
1555
  }
1584
1556
  }
1585
1557
  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
- });
1558
+ if (!(e.target.handle in dstEff.inputs)) {
1559
+ 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 });
1560
+ }
1561
+ // Private inputs should not accept edges
1562
+ if (isInputPrivate(dstEff.inputs, e.target.handle)) {
1563
+ 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
1564
  }
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: {
1565
+ const declaredIn = getInputTypeId(dstEff.inputs, e.target.handle);
1566
+ if (declaredIn && effectiveTypeId) {
1567
+ if (srcNode) {
1568
+ const srcDeclared = srcEff.outputs[e.source.handle];
1569
+ const srcArr = normOut(srcDeclared).length
1570
+ ? normOut(srcDeclared)
1571
+ : [effectiveTypeId];
1572
+ if (!canFlow(srcArr, declaredIn)) {
1573
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} output type ${srcArr.join("|")} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`, {
1607
1574
  edgeId: e.id,
1608
1575
  nodeId: dstNode.nodeId,
1609
1576
  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
- },
1577
+ declared: declaredIn,
1578
+ effectiveTypeId: srcArr.join("|"),
1657
1579
  });
1658
1580
  }
1659
1581
  }
1582
+ else if (!canFlow(effectiveTypeId, declaredIn)) {
1583
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`, {
1584
+ edgeId: e.id,
1585
+ nodeId: dstNode.nodeId,
1586
+ input: e.target.handle,
1587
+ declared: declaredIn,
1588
+ effectiveTypeId,
1589
+ });
1590
+ }
1660
1591
  }
1661
1592
  }
1662
1593
  // Track multiple inbound edges targeting the same input handle
@@ -1664,10 +1595,7 @@ class GraphBuilder {
1664
1595
  inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
1665
1596
  // If the target input is declared as an array type, allow multi-inbound (runtime will append)
1666
1597
  if (dstNode) {
1667
- const dstType = this.registry.nodes.get(dstNode.typeId);
1668
- const declaredIn = dstType
1669
- ? getInputTypeId(dstType.inputs, e.target.handle)
1670
- : undefined;
1598
+ const declaredIn = getInputTypeId((effByNodeId.get(dstNode.nodeId) || { inputs: {} }).inputs, e.target.handle);
1671
1599
  if (typeof declaredIn === "string" && declaredIn.endsWith("[]")) {
1672
1600
  inboundArrayOk.add(inboundKey);
1673
1601
  }