capdag 0.186.476 → 0.187.479

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 (2) hide show
  1. package/cap-fab-renderer.js +340 -31
  2. package/package.json +1 -1
@@ -789,6 +789,18 @@ function validateBodyOutcome(outcome, path) {
789
789
  }
790
790
  }
791
791
 
792
+ function validateRunIOItem(item, path) {
793
+ assertObject(item, path);
794
+ assertString(item.label, `${path}.label`);
795
+ assertString(item.path, `${path}.path`);
796
+ if (typeof item.is_directory !== 'boolean') {
797
+ throw new Error(`CapFabRenderer run mode: ${path}.is_directory must be a boolean`);
798
+ }
799
+ if (typeof item.file_count !== 'number' || item.file_count < 0) {
800
+ throw new Error(`CapFabRenderer run mode: ${path}.file_count must be a non-negative number`);
801
+ }
802
+ }
803
+
792
804
  function validateRunPayload(data) {
793
805
  if (!data || typeof data !== 'object') {
794
806
  throw new Error('CapFabRenderer run mode: data must be an object');
@@ -812,6 +824,23 @@ function validateRunPayload(data) {
812
824
  if (typeof data.total_body_count !== 'number' || data.total_body_count < 0) {
813
825
  throw new Error('CapFabRenderer run mode: data.total_body_count must be a non-negative number');
814
826
  }
827
+ if (data.input_items !== undefined) {
828
+ assertArray(data.input_items, 'run mode data.input_items');
829
+ data.input_items.forEach((item, idx) => {
830
+ validateRunIOItem(item, `run mode data.input_items[${idx}]`);
831
+ });
832
+ }
833
+ if (data.input_runs !== undefined) {
834
+ assertArray(data.input_runs, 'run mode data.input_runs');
835
+ data.input_runs.forEach((run, idx) => {
836
+ assertObject(run, `run mode data.input_runs[${idx}]`);
837
+ validateRunIOItem(run.input, `run mode data.input_runs[${idx}].input`);
838
+ assertArray(run.outputs, `run mode data.input_runs[${idx}].outputs`);
839
+ run.outputs.forEach((output, outIdx) => {
840
+ validateRunIOItem(output, `run mode data.input_runs[${idx}].outputs[${outIdx}]`);
841
+ });
842
+ });
843
+ }
815
844
  }
816
845
 
817
846
  function validateEditorGraphPayload(data) {
@@ -1553,6 +1582,253 @@ function stripRunBackboneReplicaNodes(built, dropStepIds) {
1553
1582
  };
1554
1583
  }
1555
1584
 
1585
+ function emptyRunBackbone() {
1586
+ return {
1587
+ nodes: [],
1588
+ edges: [],
1589
+ sourceMediaUrn: '',
1590
+ targetMediaUrn: '',
1591
+ };
1592
+ }
1593
+
1594
+ function buildExternalInputRunGraphData(
1595
+ data,
1596
+ inputRuns,
1597
+ allOutcomes,
1598
+ visibleSuccess,
1599
+ visibleFailure,
1600
+ hiddenSuccessCount,
1601
+ hiddenFailureCount,
1602
+ displayNameFor
1603
+ ) {
1604
+ if (inputRuns.length <= 1 || allOutcomes.length === 0) return null;
1605
+
1606
+ const capSteps = data.resolved_strand.steps
1607
+ .filter(step => Object.keys(step.step_type)[0] === 'Cap')
1608
+ .map(step => step.step_type.Cap);
1609
+ if (capSteps.length === 0) {
1610
+ throw new Error('CapFabRenderer run mode: external multi-input runs require at least one Cap step in resolved_strand.');
1611
+ }
1612
+
1613
+ const visibleOutcomes = visibleSuccess.concat(visibleFailure);
1614
+ const CapUrn = requireHostDependency('CapUrn');
1615
+ const sourceCanonical = canonicalMediaUrn(data.resolved_strand.source_media_urn);
1616
+ const targetCanonical = canonicalMediaUrn(data.resolved_strand.target_media_urn);
1617
+ const anchorNodeId = 'external-input-anchor';
1618
+ const replicaNodes = [{
1619
+ group: 'nodes',
1620
+ data: {
1621
+ id: anchorNodeId,
1622
+ label: displayNameFor(sourceCanonical),
1623
+ fullUrn: sourceCanonical,
1624
+ },
1625
+ classes: 'strand-source',
1626
+ }];
1627
+ const replicaEdges = [];
1628
+ const showMoreNodes = [];
1629
+
1630
+ for (const outcome of allOutcomes) {
1631
+ if (outcome.body_index >= inputRuns.length) {
1632
+ throw new Error(
1633
+ `CapFabRenderer run mode: body_outcomes[body_index=${outcome.body_index}] exceeds input_runs length ${inputRuns.length}`
1634
+ );
1635
+ }
1636
+ }
1637
+
1638
+ function buildReplica(outcome) {
1639
+ const runDef = inputRuns[outcome.body_index];
1640
+ const success = outcome.success;
1641
+ const nodeClass = success ? 'body-success' : 'body-failure';
1642
+ const edgeClass = success ? 'body-success' : 'body-failure';
1643
+ const edgeColor = success ? 'var(--graph-body-edge-success)' : 'var(--graph-body-edge-failure)';
1644
+ const bodyKey = `external-body-${outcome.body_index}`;
1645
+ const sourceNodeId = `${bodyKey}-input`;
1646
+ const sourceLabel = typeof outcome.title === 'string' && outcome.title.length > 0
1647
+ ? outcome.title
1648
+ : runDef.input.label;
1649
+
1650
+ replicaNodes.push({
1651
+ group: 'nodes',
1652
+ data: {
1653
+ id: sourceNodeId,
1654
+ label: sourceLabel,
1655
+ fullUrn: runDef.input.path,
1656
+ bodyIndex: outcome.body_index,
1657
+ bodyTitle: sourceLabel,
1658
+ },
1659
+ classes: `${nodeClass} run-input-item`,
1660
+ });
1661
+ replicaEdges.push({
1662
+ group: 'edges',
1663
+ data: {
1664
+ id: `${bodyKey}-entry`,
1665
+ source: anchorNodeId,
1666
+ target: sourceNodeId,
1667
+ label: '',
1668
+ title: runDef.input.path,
1669
+ fullUrn: '',
1670
+ color: getCssVar('--graph-edge-color'),
1671
+ bodyIndex: outcome.body_index,
1672
+ },
1673
+ classes: edgeClass,
1674
+ });
1675
+
1676
+ let traceEnd = capSteps.length;
1677
+ if (!success) {
1678
+ if (typeof outcome.failed_cap === 'string' && outcome.failed_cap.length > 0) {
1679
+ const failedCap = CapUrn.fromString(outcome.failed_cap);
1680
+ traceEnd = 0;
1681
+ for (let i = 0; i < capSteps.length; i++) {
1682
+ const candidate = CapUrn.fromString(capSteps[i].cap_urn);
1683
+ if (candidate.isEquivalent(failedCap)) {
1684
+ traceEnd = i + 1;
1685
+ break;
1686
+ }
1687
+ }
1688
+ } else {
1689
+ traceEnd = 0;
1690
+ }
1691
+ }
1692
+
1693
+ let prevNodeId = sourceNodeId;
1694
+ for (let i = 0; i < traceEnd; i++) {
1695
+ const cap = capSteps[i];
1696
+ const targetMedia = canonicalMediaUrn(i === capSteps.length - 1 ? data.resolved_strand.target_media_urn : data.resolved_strand.steps.filter(step => Object.keys(step.step_type)[0] === 'Cap')[i].to_spec);
1697
+ const isLastExecutedStep = i === traceEnd - 1;
1698
+ const outputs = success ? runDef.outputs : [];
1699
+
1700
+ if (success && isLastExecutedStep && outputs.length > 0) {
1701
+ outputs.forEach((output, outputIdx) => {
1702
+ const outputNodeId = `${bodyKey}-output-${outputIdx}`;
1703
+ replicaNodes.push({
1704
+ group: 'nodes',
1705
+ data: {
1706
+ id: outputNodeId,
1707
+ label: output.label,
1708
+ fullUrn: output.path,
1709
+ bodyIndex: outcome.body_index,
1710
+ bodyTitle: sourceLabel,
1711
+ },
1712
+ classes: nodeClass,
1713
+ });
1714
+ replicaEdges.push({
1715
+ group: 'edges',
1716
+ data: {
1717
+ id: `${bodyKey}-output-edge-${i}-${outputIdx}`,
1718
+ source: prevNodeId,
1719
+ target: outputNodeId,
1720
+ label: outputIdx === 0 ? cap.title : '',
1721
+ title: cap.title,
1722
+ fullUrn: cap.cap_urn,
1723
+ color: edgeColor,
1724
+ bodyIndex: outcome.body_index,
1725
+ },
1726
+ classes: edgeClass,
1727
+ });
1728
+ });
1729
+ return;
1730
+ }
1731
+
1732
+ const stepNodeId = `${bodyKey}-step-${i}`;
1733
+ replicaNodes.push({
1734
+ group: 'nodes',
1735
+ data: {
1736
+ id: stepNodeId,
1737
+ label: displayNameFor(isLastExecutedStep ? targetCanonical : targetMedia),
1738
+ fullUrn: isLastExecutedStep ? targetCanonical : targetMedia,
1739
+ bodyIndex: outcome.body_index,
1740
+ bodyTitle: sourceLabel,
1741
+ },
1742
+ classes: nodeClass,
1743
+ });
1744
+ replicaEdges.push({
1745
+ group: 'edges',
1746
+ data: {
1747
+ id: `${bodyKey}-step-edge-${i}`,
1748
+ source: prevNodeId,
1749
+ target: stepNodeId,
1750
+ label: cap.title,
1751
+ title: cap.title,
1752
+ fullUrn: cap.cap_urn,
1753
+ color: edgeColor,
1754
+ bodyIndex: outcome.body_index,
1755
+ },
1756
+ classes: edgeClass,
1757
+ });
1758
+ prevNodeId = stepNodeId;
1759
+ }
1760
+ }
1761
+
1762
+ visibleOutcomes.forEach(buildReplica);
1763
+
1764
+ if (hiddenSuccessCount > 0) {
1765
+ showMoreNodes.push({
1766
+ group: 'nodes',
1767
+ data: {
1768
+ id: 'show-more-success',
1769
+ label: `+${hiddenSuccessCount} more succeeded`,
1770
+ fullUrn: '',
1771
+ showMoreGroup: 'success',
1772
+ hiddenCount: hiddenSuccessCount,
1773
+ },
1774
+ classes: 'show-more body-success',
1775
+ });
1776
+ replicaEdges.push({
1777
+ group: 'edges',
1778
+ data: {
1779
+ id: 'show-more-success-edge',
1780
+ source: anchorNodeId,
1781
+ target: 'show-more-success',
1782
+ label: '',
1783
+ title: '',
1784
+ fullUrn: '',
1785
+ color: 'var(--graph-body-edge-success)',
1786
+ },
1787
+ classes: 'body-success',
1788
+ });
1789
+ }
1790
+ if (hiddenFailureCount > 0) {
1791
+ showMoreNodes.push({
1792
+ group: 'nodes',
1793
+ data: {
1794
+ id: 'show-more-failure',
1795
+ label: `+${hiddenFailureCount} failed`,
1796
+ fullUrn: '',
1797
+ showMoreGroup: 'failure',
1798
+ hiddenCount: hiddenFailureCount,
1799
+ },
1800
+ classes: 'show-more body-failure',
1801
+ });
1802
+ replicaEdges.push({
1803
+ group: 'edges',
1804
+ data: {
1805
+ id: 'show-more-failure-edge',
1806
+ source: anchorNodeId,
1807
+ target: 'show-more-failure',
1808
+ label: '',
1809
+ title: '',
1810
+ fullUrn: '',
1811
+ color: 'var(--graph-body-edge-failure)',
1812
+ },
1813
+ classes: 'body-failure',
1814
+ });
1815
+ }
1816
+
1817
+ return {
1818
+ strandBuilt: emptyRunBackbone(),
1819
+ replicaNodes,
1820
+ replicaEdges,
1821
+ showMoreNodes,
1822
+ totals: {
1823
+ hiddenSuccessCount,
1824
+ hiddenFailureCount,
1825
+ totalBodyCount: data.total_body_count,
1826
+ visibleSuccessCount: visibleSuccess.length,
1827
+ visibleFailureCount: visibleFailure.length,
1828
+ },
1829
+ };
1830
+ }
1831
+
1556
1832
  function buildRunGraphData(data) {
1557
1833
  validateRunPayload(data);
1558
1834
 
@@ -1566,19 +1842,36 @@ function buildRunGraphData(data) {
1566
1842
  const strandBuiltRaw = buildStrandGraphData(strandInput);
1567
1843
  let strandBuiltCollapsed = collapseStrandShapeTransitions(strandBuiltRaw);
1568
1844
 
1569
- // Run mode overrides the input_slot node's label with the
1570
- // host-supplied `source_display` (runtime input filename).
1571
- // Strand mode ignores this field so the abstract graph shows
1572
- // the media def's title from `media_display_names`.
1573
- if (typeof data.source_display === 'string' && data.source_display.length > 0) {
1574
- strandBuiltCollapsed = {
1575
- nodes: strandBuiltCollapsed.nodes.map(n =>
1576
- n.id === 'input_slot' ? Object.assign({}, n, { label: data.source_display }) : n),
1577
- edges: strandBuiltCollapsed.edges,
1578
- sourceMediaUrn: strandBuiltCollapsed.sourceMediaUrn,
1579
- targetMediaUrn: strandBuiltCollapsed.targetMediaUrn,
1580
- };
1581
- }
1845
+ const inputItems = Array.isArray(data.input_items) ? data.input_items : [];
1846
+ const inputRuns = Array.isArray(data.input_runs) ? data.input_runs : [];
1847
+ const inputReplicaNodes = [];
1848
+ const inputReplicaEdges = [];
1849
+ inputItems.forEach((item, idx) => {
1850
+ const nodeId = `input-item-${idx}`;
1851
+ inputReplicaNodes.push({
1852
+ group: 'nodes',
1853
+ data: {
1854
+ id: nodeId,
1855
+ label: item.label,
1856
+ fullUrn: item.path,
1857
+ inputIndex: idx,
1858
+ inputPath: item.path,
1859
+ },
1860
+ classes: 'run-input-item',
1861
+ });
1862
+ inputReplicaEdges.push({
1863
+ group: 'edges',
1864
+ data: {
1865
+ id: `input-item-edge-${idx}`,
1866
+ source: nodeId,
1867
+ target: 'input_slot',
1868
+ label: '',
1869
+ title: item.path,
1870
+ fullUrn: '',
1871
+ color: getCssVar('--graph-edge-color'),
1872
+ },
1873
+ });
1874
+ });
1582
1875
 
1583
1876
  // Locate the ForEach/Collect span in the raw steps. Positional
1584
1877
  // IDs survive the collapse (node IDs are `step_${i}` from the
@@ -1605,6 +1898,36 @@ function buildRunGraphData(data) {
1605
1898
  const hiddenFailureCount = failures.length - visibleFailure.length;
1606
1899
  const visibleOutcomes = visibleSuccess.concat(visibleFailure);
1607
1900
 
1901
+ // Look up a display name for a media URN via the host-supplied
1902
+ // `media_display_names` map. Uses `MediaUrn.isEquivalent` for
1903
+ // semantic URN equality.
1904
+ const MediaUrn = requireHostDependency('MediaUrn');
1905
+ const mediaDisplayNames = data.media_display_names || {};
1906
+ const displayEntries = [];
1907
+ for (const [urn, display] of Object.entries(mediaDisplayNames)) {
1908
+ if (typeof display !== 'string' || display.length === 0) continue;
1909
+ try {
1910
+ displayEntries.push({ media: MediaUrn.fromString(urn), display });
1911
+ } catch (_) { /* ignore malformed keys */ }
1912
+ }
1913
+ function displayNameFor(canonicalUrn) {
1914
+ return requireExplicitDisplayName(canonicalUrn, displayEntries, 'run node');
1915
+ }
1916
+
1917
+ const externalInputRunBuilt = buildExternalInputRunGraphData(
1918
+ data,
1919
+ inputRuns,
1920
+ allOutcomes,
1921
+ visibleSuccess,
1922
+ visibleFailure,
1923
+ hiddenSuccessCount,
1924
+ hiddenFailureCount,
1925
+ displayNameFor
1926
+ );
1927
+ if (externalInputRunBuilt !== null) {
1928
+ return externalInputRunBuilt;
1929
+ }
1930
+
1608
1931
  // Per-body replicas only fire when there's a ForEach AND at
1609
1932
  // least one visible outcome. Without outcomes, the strand
1610
1933
  // backbone renders the "plan preview" unchanged.
@@ -1628,8 +1951,8 @@ function buildRunGraphData(data) {
1628
1951
  if (!shouldExpand) {
1629
1952
  return {
1630
1953
  strandBuilt: strandBuiltCollapsed,
1631
- replicaNodes: [],
1632
- replicaEdges: [],
1954
+ replicaNodes: inputReplicaNodes,
1955
+ replicaEdges: inputReplicaEdges,
1633
1956
  showMoreNodes: [],
1634
1957
  totals: {
1635
1958
  hiddenSuccessCount,
@@ -1729,24 +2052,10 @@ function buildRunGraphData(data) {
1729
2052
 
1730
2053
  const replicaNodes = [];
1731
2054
  const replicaEdges = [];
2055
+ replicaNodes.push(...inputReplicaNodes);
2056
+ replicaEdges.push(...inputReplicaEdges);
1732
2057
  let replicasBuiltCount = 0;
1733
2058
 
1734
- // Look up a display name for a media URN via the host-supplied
1735
- // `media_display_names` map. Uses `MediaUrn.isEquivalent` for
1736
- // semantic URN equality.
1737
- const MediaUrn = requireHostDependency('MediaUrn');
1738
- const mediaDisplayNames = data.media_display_names || {};
1739
- const displayEntries = [];
1740
- for (const [urn, display] of Object.entries(mediaDisplayNames)) {
1741
- if (typeof display !== 'string' || display.length === 0) continue;
1742
- try {
1743
- displayEntries.push({ media: MediaUrn.fromString(urn), display });
1744
- } catch (_) { /* ignore malformed keys */ }
1745
- }
1746
- function displayNameFor(canonicalUrn) {
1747
- return requireExplicitDisplayName(canonicalUrn, displayEntries, 'run node');
1748
- }
1749
-
1750
2059
  // The per-body "entry" node represents one item of the
1751
2060
  // sequence being iterated. Its URN is:
1752
2061
  // * the sequence producer cap's `to_spec` (if such a cap
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.186.476"
43
+ "version": "0.187.479"
44
44
  }