capdag 0.106.243 → 0.109.248

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.
@@ -663,22 +663,38 @@ function classifyStrandCapSteps(steps) {
663
663
  return { capStepIndices, capFlags };
664
664
  }
665
665
 
666
- // Build the strand graph. Every Cap step becomes an edge
667
- // (`from_spec` → `to_spec`). ForEach / Collect are NOT rendered as nodes
668
- // they are transitions that annotate the adjacent cap edges:
666
+ // Build the strand graph by mirroring capdag's plan builder
667
+ // (`capdag/src/planner/plan_builder.rs::build_plan_from_path`). The plan
668
+ // builder is the authoritative source of truth for how strand steps
669
+ // translate into a DAG of nodes and edges:
669
670
  //
670
- // * A Cap whose previous step is ForEach gets the prefix "for each"
671
- // on its edge label (the first-body entry edge).
672
- // * A Cap whose next step is Collect gets the suffix "collect" on its
673
- // edge label (the last-body exit edge).
671
+ // * Node IDs are positional: `input_slot`, `step_0`, `step_1`, …,
672
+ // `output`. They are NOT media URN strings — URN comparisons for
673
+ // graph topology are wrong because the planner connects steps by
674
+ // the order-theoretic `conformsTo` relation, not by string equality.
675
+ // * `prev_node_id` is a single running pointer, only advanced by Cap
676
+ // steps. ForEach steps mark the start of a body span without
677
+ // advancing prev; the body's first Cap still connects to whatever
678
+ // was before the ForEach.
679
+ // * Cap inside a ForEach body connects from `prev_node_id` like any
680
+ // other cap, AND tracks `body_entry` (first cap in body) and
681
+ // `body_exit` (most recent cap in body).
682
+ // * Collect after a ForEach body creates a ForEach node with
683
+ // boundaries, an iteration edge to body_entry, a Collect node, and
684
+ // a collection edge from body_exit to Collect. prev_node_id becomes
685
+ // the Collect node.
686
+ // * Standalone Collect (no enclosing ForEach) creates a Collect node
687
+ // consuming prev_node_id directly.
688
+ // * Unclosed ForEach with no body caps is a terminal unwrap — the
689
+ // ForEach node is skipped; prev_node_id stays as-is.
690
+ // * Unclosed ForEach WITH body caps gets a ForEach node, iteration
691
+ // edge to body_entry, and prev_node_id becomes body_exit.
674
692
  //
675
- // Source-to-body and body-to-target continuity is guaranteed by the
676
- // strand structure itself: consecutive cap steps satisfy
677
- // `stepN.to_spec == stepN+1.from_spec`, and fix-up edges are added
678
- // explicitly when the strand's `source_spec` differs from the first cap
679
- // step's `from_spec` (the ForEach shape transition) or when the strand's
680
- // `target_spec` differs from the last cap step's `to_spec` (the Collect
681
- // shape transition).
693
+ // Node labels come from the `media_display_names` map keyed by the
694
+ // step's canonical URN (or source_spec/target_spec for the boundary
695
+ // nodes). ForEach and Collect nodes display "for each" / "collect".
696
+ // Cap edges carry the cap title plus cardinality marker when either
697
+ // input or output is a sequence.
682
698
  function buildStrandGraphData(data) {
683
699
  validateStrandPayload(data);
684
700
 
@@ -686,143 +702,197 @@ function buildStrandGraphData(data) {
686
702
  const sourceSpec = canonicalMediaUrn(data.source_spec);
687
703
  const targetSpec = canonicalMediaUrn(data.target_spec);
688
704
 
689
- const canonicalDisplayLookup = new Map();
705
+ // Look up a display name for a media URN via the host-supplied map.
706
+ // Uses `MediaUrn.isEquivalent` so tag-order variation doesn't defeat
707
+ // the lookup — URNs are compared semantically, never as raw strings.
708
+ const MediaUrn = requireHostDependency('MediaUrn');
709
+ const displayEntries = [];
690
710
  for (const [urn, display] of Object.entries(mediaDisplayNames)) {
691
- if (typeof display === 'string' && display.length > 0) {
692
- canonicalDisplayLookup.set(canonicalMediaUrn(urn), display);
711
+ if (typeof display !== 'string' || display.length === 0) continue;
712
+ try {
713
+ displayEntries.push({ media: MediaUrn.fromString(urn), display });
714
+ } catch (_) {
715
+ // Skip entries with unparseable URN keys — the host payload is
716
+ // trusted, but malformed keys are not fatal.
693
717
  }
694
718
  }
695
-
696
- const nodesMap = new Map();
697
- function ensureNode(canonicalUrn) {
698
- if (!nodesMap.has(canonicalUrn)) {
699
- const displayName = canonicalDisplayLookup.get(canonicalUrn);
700
- nodesMap.set(canonicalUrn, {
701
- id: canonicalUrn,
702
- label: displayName !== undefined ? displayName : mediaNodeLabel(canonicalUrn),
703
- fullUrn: canonicalUrn,
704
- });
719
+ function displayNameFor(canonicalUrn) {
720
+ const candidate = MediaUrn.fromString(canonicalUrn);
721
+ for (const entry of displayEntries) {
722
+ if (candidate.isEquivalent(entry.media)) return entry.display;
705
723
  }
706
- return canonicalUrn;
724
+ return mediaNodeLabel(canonicalUrn);
707
725
  }
708
726
 
709
- ensureNode(sourceSpec);
710
- ensureNode(targetSpec);
711
-
712
- const { capStepIndices } = classifyStrandCapSteps(data.steps);
713
- const firstCapIdx = capStepIndices.length > 0 ? capStepIndices[0] : -1;
714
- const lastCapIdx = capStepIndices.length > 0 ? capStepIndices[capStepIndices.length - 1] : -1;
715
- const hasLeadingForEach = data.steps.some((s, i) =>
716
- Object.keys(s.step_type)[0] === 'ForEach' && i < (firstCapIdx === -1 ? Infinity : firstCapIdx));
717
- const hasTrailingCollect = data.steps.some((s, i) =>
718
- Object.keys(s.step_type)[0] === 'Collect' && i > (lastCapIdx === -1 ? -Infinity : lastCapIdx));
719
-
727
+ const nodes = [];
720
728
  const edges = [];
721
- let capEdgeIdx = 0;
729
+ const nodeIds = new Set();
722
730
 
723
- // Prepend a fix-up edge from source_spec to the first cap step's
724
- // from_spec when they differ. This is the ForEach shape transition
725
- // rendered as an explicit edge.
726
- if (firstCapIdx >= 0) {
727
- const firstCap = data.steps[firstCapIdx];
728
- const firstCapFrom = canonicalMediaUrn(firstCap.from_spec);
729
- if (firstCapFrom !== sourceSpec) {
730
- ensureNode(firstCapFrom);
731
- const label = hasLeadingForEach ? 'for each' : '';
732
- edges.push({
733
- id: `strand-edge-${capEdgeIdx}`,
734
- source: sourceSpec,
735
- target: firstCapFrom,
736
- title: label || 'fan-out',
737
- label,
738
- cardinality: '',
739
- capUrn: '',
740
- color: edgeHueColor(capEdgeIdx),
741
- });
742
- capEdgeIdx++;
743
- }
731
+ function addNode(id, label, fullUrn, nodeClass) {
732
+ if (nodeIds.has(id)) return;
733
+ nodeIds.add(id);
734
+ nodes.push({
735
+ id,
736
+ label,
737
+ fullUrn: fullUrn || '',
738
+ nodeClass: nodeClass || '',
739
+ });
744
740
  }
745
-
746
- // Cap step edges. Each Cap's edge connects its from_spec to its
747
- // to_spec. Adjacent cap edges are continuous because the planner
748
- // guarantees stepN.to_spec == stepN+1.from_spec. "for each" and
749
- // "collect" labels belong on the fix-up edges around the cap chain,
750
- // not on the cap edges themselves — those labels describe shape
751
- // transitions between the source list and the first body scalar (and
752
- // between the last body scalar and the target list), which are the
753
- // fix-up edges.
754
- data.steps.forEach((step) => {
741
+ let edgeCounter = 0;
742
+ function addEdge(source, target, label, title, fullUrn, edgeClass) {
743
+ edges.push({
744
+ id: `strand-edge-${edgeCounter}`,
745
+ source,
746
+ target,
747
+ label: label || '',
748
+ title: title || '',
749
+ fullUrn: fullUrn || '',
750
+ edgeClass: edgeClass || '',
751
+ color: edgeHueColor(edgeCounter),
752
+ });
753
+ edgeCounter++;
754
+ }
755
+
756
+ // Entry node — the strand's source media spec.
757
+ const inputSlotId = 'input_slot';
758
+ addNode(inputSlotId, displayNameFor(sourceSpec), sourceSpec, 'strand-source');
759
+
760
+ let prevNodeId = inputSlotId;
761
+
762
+ // Track ForEach body membership. `insideForEachBody = { index, nodeId }`
763
+ // records which ForEach step we're inside and the id we'll give its
764
+ // eventual node. `bodyEntry`/`bodyExit` track the first and most
765
+ // recent Cap step inside that body.
766
+ let insideForEachBody = null;
767
+ let bodyEntry = null;
768
+ let bodyExit = null;
769
+
770
+ // Finalize an outer ForEach body when a nested ForEach starts before
771
+ // the outer's Collect. Mirrors plan_builder.rs:238-289.
772
+ function finalizeOuterForEach(outerForEach, outerEntry, outerExit) {
773
+ const outerForEachInput = outerForEach.index === 0
774
+ ? inputSlotId
775
+ : `step_${outerForEach.index - 1}`;
776
+ // Create the ForEach node + direct edge from its input + iteration
777
+ // edge into the body's first cap.
778
+ addNode(outerForEach.nodeId, 'for each', '', 'strand-foreach');
779
+ addEdge(outerForEachInput, outerForEach.nodeId, 'for each', 'for each', '', 'strand-iteration');
780
+ addEdge(outerForEach.nodeId, outerEntry, '', '', '', 'strand-iteration');
781
+ return outerExit;
782
+ }
783
+
784
+ data.steps.forEach((step, i) => {
755
785
  const variant = Object.keys(step.step_type)[0];
756
- if (variant !== 'Cap') return;
757
- const body = step.step_type.Cap;
758
- const fromCanonical = canonicalMediaUrn(step.from_spec);
759
- const toCanonical = canonicalMediaUrn(step.to_spec);
760
- ensureNode(fromCanonical);
761
- ensureNode(toCanonical);
786
+ const nodeId = `step_${i}`;
787
+
788
+ if (variant === 'Cap') {
789
+ const body = step.step_type.Cap;
790
+ const toCanonical = canonicalMediaUrn(step.to_spec);
791
+ addNode(nodeId, displayNameFor(toCanonical), toCanonical, 'strand-cap');
792
+
793
+ let label = body.title;
794
+ const cardinality = cardinalityLabel(body.input_is_sequence, body.output_is_sequence);
795
+ if (cardinality !== '1\u21921') {
796
+ label = `${label} (${cardinality})`;
797
+ }
798
+ addEdge(prevNodeId, nodeId, label, body.title, body.cap_urn, 'strand-cap-edge');
799
+
800
+ if (insideForEachBody !== null) {
801
+ if (bodyEntry === null) bodyEntry = nodeId;
802
+ bodyExit = nodeId;
803
+ }
762
804
 
763
- let label = body.title;
764
- const cardinality = cardinalityLabel(body.input_is_sequence, body.output_is_sequence);
765
- if (cardinality !== '1\u21921') {
766
- label = `${label} (${cardinality})`;
805
+ prevNodeId = nodeId;
806
+ return;
767
807
  }
768
808
 
769
- edges.push({
770
- id: `strand-edge-${capEdgeIdx}`,
771
- source: fromCanonical,
772
- target: toCanonical,
773
- title: body.title,
774
- label,
775
- cardinality,
776
- capUrn: body.cap_urn,
777
- color: edgeHueColor(capEdgeIdx),
778
- });
779
- capEdgeIdx++;
780
- });
809
+ if (variant === 'ForEach') {
810
+ // If we're already inside a ForEach body when another ForEach
811
+ // starts, finalize the outer one first.
812
+ if (insideForEachBody !== null) {
813
+ const outer = insideForEachBody;
814
+ const entry = bodyEntry !== null ? bodyEntry : prevNodeId;
815
+ const exit = bodyExit !== null ? bodyExit : prevNodeId;
816
+ if (bodyEntry === null) {
817
+ // Outer ForEach with no body caps is an illegal nesting; the
818
+ // plan builder throws. Mirror that.
819
+ throw new Error(
820
+ `CapGraphRenderer strand: nested ForEach at step[${i}] but outer ForEach at step[${outer.index}] has no body caps`
821
+ );
822
+ }
823
+ prevNodeId = finalizeOuterForEach(outer, entry, exit);
824
+ bodyEntry = null;
825
+ bodyExit = null;
826
+ }
827
+ insideForEachBody = { index: i, nodeId };
828
+ bodyEntry = null;
829
+ bodyExit = null;
830
+ // Do NOT advance prevNodeId — the body's first cap will connect
831
+ // to whatever was before the ForEach.
832
+ return;
833
+ }
781
834
 
782
- // Append a fix-up edge from the last cap's to_spec to target_spec
783
- // when they differ. This is the Collect shape transition as an
784
- // explicit edge.
785
- if (lastCapIdx >= 0) {
786
- const lastCap = data.steps[lastCapIdx];
787
- const lastCapTo = canonicalMediaUrn(lastCap.to_spec);
788
- if (lastCapTo !== targetSpec) {
789
- const label = hasTrailingCollect ? 'collect' : '';
790
- edges.push({
791
- id: `strand-edge-${capEdgeIdx}`,
792
- source: lastCapTo,
793
- target: targetSpec,
794
- title: label || 'fan-in',
795
- label,
796
- cardinality: '',
797
- capUrn: '',
798
- color: edgeHueColor(capEdgeIdx),
799
- });
800
- capEdgeIdx++;
835
+ if (variant === 'Collect') {
836
+ if (insideForEachBody !== null) {
837
+ const outer = insideForEachBody;
838
+ const entry = bodyEntry !== null ? bodyEntry : prevNodeId;
839
+ const exit = bodyExit !== null ? bodyExit : prevNodeId;
840
+ const outerForEachInput = outer.index === 0
841
+ ? inputSlotId
842
+ : `step_${outer.index - 1}`;
843
+
844
+ addNode(outer.nodeId, 'for each', '', 'strand-foreach');
845
+ addEdge(outerForEachInput, outer.nodeId, 'for each', 'for each', '', 'strand-iteration');
846
+ addEdge(outer.nodeId, entry, '', '', '', 'strand-iteration');
847
+
848
+ addNode(nodeId, 'collect', '', 'strand-collect');
849
+ addEdge(exit, nodeId, 'collect', 'collect', '', 'strand-collection');
850
+
851
+ insideForEachBody = null;
852
+ bodyEntry = null;
853
+ bodyExit = null;
854
+ prevNodeId = nodeId;
855
+ } else {
856
+ // Standalone Collect — scalar → list-of-one. Mirrors
857
+ // plan_builder.rs:333-355.
858
+ addNode(nodeId, 'collect', '', 'strand-collect');
859
+ addEdge(prevNodeId, nodeId, 'collect', 'collect', '', 'strand-collection');
860
+ prevNodeId = nodeId;
861
+ }
862
+ return;
801
863
  }
802
- }
803
864
 
804
- // Edge case: if there are no Cap steps at all, still connect source
805
- // to target directly so the graph has at least one edge. This handles
806
- // degenerate strands (e.g. identity).
807
- if (firstCapIdx === -1 && sourceSpec !== targetSpec) {
808
- edges.push({
809
- id: `strand-edge-${capEdgeIdx}`,
810
- source: sourceSpec,
811
- target: targetSpec,
812
- title: '',
813
- label: '',
814
- cardinality: '',
815
- capUrn: '',
816
- color: edgeHueColor(capEdgeIdx),
817
- });
818
- }
865
+ throw new Error(`CapGraphRenderer strand: unknown step_type variant '${variant}' at step[${i}]`);
866
+ });
819
867
 
820
- return {
821
- nodes: Array.from(nodesMap.values()),
822
- edges,
823
- sourceSpec,
824
- targetSpec,
825
- };
868
+ // Handle unclosed ForEach after the walk. Mirrors plan_builder.rs:362-428.
869
+ if (insideForEachBody !== null) {
870
+ const outer = insideForEachBody;
871
+ const hasBodyEntry = bodyEntry !== null;
872
+ if (hasBodyEntry) {
873
+ const entry = bodyEntry;
874
+ const exit = bodyExit;
875
+ const outerForEachInput = outer.index === 0
876
+ ? inputSlotId
877
+ : `step_${outer.index - 1}`;
878
+ addNode(outer.nodeId, 'for each', '', 'strand-foreach');
879
+ addEdge(outerForEachInput, outer.nodeId, 'for each', 'for each', '', 'strand-iteration');
880
+ addEdge(outer.nodeId, entry, '', '', '', 'strand-iteration');
881
+ prevNodeId = exit;
882
+ }
883
+ // hasBodyEntry === false is a terminal unwrap — skip the ForEach
884
+ // node entirely, prev_node_id stays as-is.
885
+ insideForEachBody = null;
886
+ bodyEntry = null;
887
+ bodyExit = null;
888
+ }
889
+
890
+ // Final output node. Mirrors plan_builder.rs:430-432.
891
+ const outputId = 'output';
892
+ addNode(outputId, displayNameFor(targetSpec), targetSpec, 'strand-target');
893
+ addEdge(prevNodeId, outputId, '', '', '', 'strand-cap-edge');
894
+
895
+ return { nodes, edges, sourceSpec, targetSpec };
826
896
  }
827
897
 
828
898
  function strandCytoscapeElements(built) {
@@ -833,9 +903,7 @@ function strandCytoscapeElements(built) {
833
903
  label: node.label,
834
904
  fullUrn: node.fullUrn,
835
905
  },
836
- classes: node.id === built.sourceSpec ? 'strand-source'
837
- : node.id === built.targetSpec ? 'strand-target'
838
- : '',
906
+ classes: node.nodeClass || '',
839
907
  }));
840
908
  const edgeElements = built.edges.map(edge => ({
841
909
  group: 'edges',
@@ -845,10 +913,10 @@ function strandCytoscapeElements(built) {
845
913
  target: edge.target,
846
914
  label: edge.label,
847
915
  title: edge.title,
848
- cardinality: edge.cardinality,
849
- fullUrn: edge.capUrn,
916
+ fullUrn: edge.fullUrn,
850
917
  color: edge.color,
851
918
  },
919
+ classes: edge.edgeClass || '',
852
920
  }));
853
921
  return nodeElements.concat(edgeElements);
854
922
  }
@@ -870,6 +938,39 @@ function findCapStepIndexByUrn(steps, targetUrnString) {
870
938
  return -1;
871
939
  }
872
940
 
941
+ // Remove nodes and edges belonging to the ForEach body's interior cap
942
+ // steps from a strand backbone. In run mode, these are replaced by
943
+ // per-body replicas; keeping the prototype chain alongside the
944
+ // replicas produces a confusing double-render. The ForEach and Collect
945
+ // nodes themselves, plus their iteration/collection edges, stay.
946
+ function stripBodyInteriorFromStrandBackbone(built, steps, foreachStepIdx, collectStepIdx) {
947
+ const bodyEnd = collectStepIdx >= 0 ? collectStepIdx : steps.length;
948
+ // Collect the positional IDs of body-interior cap steps (the caps
949
+ // strictly between ForEach and Collect).
950
+ const interiorIds = new Set();
951
+ for (let i = foreachStepIdx + 1; i < bodyEnd; i++) {
952
+ if (Object.keys(steps[i].step_type)[0] === 'Cap') {
953
+ interiorIds.add(`step_${i}`);
954
+ }
955
+ }
956
+ if (interiorIds.size === 0) return built;
957
+
958
+ const keptNodes = built.nodes.filter(n => !interiorIds.has(n.id));
959
+ const keptEdges = built.edges.filter(e =>
960
+ !interiorIds.has(e.source) && !interiorIds.has(e.target));
961
+
962
+ // After stripping, the ForEach node and the Collect node (or the
963
+ // output node if no Collect) may become disconnected — the body
964
+ // replicas will bridge them. That's fine; cytoscape's ELK layout
965
+ // handles disconnected subgraphs.
966
+ return {
967
+ nodes: keptNodes,
968
+ edges: keptEdges,
969
+ sourceSpec: built.sourceSpec,
970
+ targetSpec: built.targetSpec,
971
+ };
972
+ }
973
+
873
974
  function buildRunGraphData(data) {
874
975
  validateRunPayload(data);
875
976
 
@@ -879,11 +980,14 @@ function buildRunGraphData(data) {
879
980
  const strandInput = Object.assign({}, data.resolved_strand, {
880
981
  media_display_names: data.media_display_names,
881
982
  });
882
- const strandBuilt = buildStrandGraphData(strandInput);
983
+ const strandBuiltRaw = buildStrandGraphData(strandInput);
883
984
 
884
985
  // Locate the ForEach/Collect span in the backbone for body-replica
885
- // placement. We anchor body replicas to the first Cap step after a
886
- // ForEach (the fan-out point) and merge back at the first Collect.
986
+ // placement. The strand builder uses positional node IDs mirroring
987
+ // the plan builder (`step_0`, `step_1`, ). The ForEach node at
988
+ // step index i has id `step_i`; body replicas fan out from that
989
+ // ForEach node and (when a Collect closes the body) merge into the
990
+ // Collect node at `step_j`.
887
991
  const steps = data.resolved_strand.steps;
888
992
  let foreachStepIdx = -1;
889
993
  let collectStepIdx = -1;
@@ -894,6 +998,17 @@ function buildRunGraphData(data) {
894
998
  }
895
999
  const hasForeach = foreachStepIdx >= 0;
896
1000
 
1001
+ // In run mode we want per-body replicas to REPLACE the prototype cap
1002
+ // chain inside the ForEach body, not sit alongside it. Strip the
1003
+ // backbone nodes and edges that correspond to body-interior Cap
1004
+ // steps and their direct edges, keeping only the input slot, the
1005
+ // ForEach node, the Collect node (if any), the output node, and any
1006
+ // caps OUTSIDE the body span. Body replicas will connect from the
1007
+ // ForEach node and merge at the Collect node.
1008
+ const strandBuilt = hasForeach
1009
+ ? stripBodyInteriorFromStrandBackbone(strandBuiltRaw, steps, foreachStepIdx, collectStepIdx)
1010
+ : strandBuiltRaw;
1011
+
897
1012
  // Filter and bound the outcomes.
898
1013
  const allOutcomes = data.body_outcomes.slice().sort((a, b) => a.body_index - b.body_index);
899
1014
  const successes = allOutcomes.filter(o => o.success);
@@ -903,10 +1018,8 @@ function buildRunGraphData(data) {
903
1018
  const hiddenSuccessCount = successes.length - visibleSuccess.length;
904
1019
  const hiddenFailureCount = failures.length - visibleFailure.length;
905
1020
 
906
- // Map each displayed body to its per-body chain. The chain starts at
907
- // the first Cap step's from_spec (the node immediately after the
908
- // ForEach) and extends through either all body caps (success) or up to
909
- // the failed_cap (failure).
1021
+ // Collect the Cap steps inside the ForEach body. Each body replica
1022
+ // chains through these caps.
910
1023
  const bodyCapSteps = [];
911
1024
  const bodyStart = hasForeach ? foreachStepIdx + 1 : 0;
912
1025
  const bodyEnd = collectStepIdx >= 0 ? collectStepIdx : steps.length;
@@ -916,19 +1029,24 @@ function buildRunGraphData(data) {
916
1029
  }
917
1030
  }
918
1031
 
919
- // Replica node/edge ids are prefixed to avoid collision with backbone
920
- // ids.
1032
+ // Body replicas fan out from the ForEach node. If no ForEach, fall
1033
+ // back to the input_slot. When the strand closes the body with a
1034
+ // Collect, replicas merge into that Collect node; otherwise
1035
+ // (unclosed ForEach) they merge into the `output` node.
1036
+ const anchorNodeId = hasForeach ? `step_${foreachStepIdx}` : 'input_slot';
1037
+ const mergeNodeId = collectStepIdx >= 0 ? `step_${collectStepIdx}` : 'output';
1038
+
921
1039
  const replicaNodes = [];
922
1040
  const replicaEdges = [];
923
1041
 
924
- function buildBodyReplica(outcome, groupIndex) {
1042
+ function buildBodyReplica(outcome) {
925
1043
  const success = outcome.success;
926
1044
  const successClass = success ? 'body-success' : 'body-failure';
927
1045
  const edgeClass = success ? 'body-success' : 'body-failure';
928
1046
  const colorVar = success ? '--graph-body-edge-success' : '--graph-body-edge-failure';
929
1047
 
930
- // Find the trace end (failed_cap stops the trace for failed
931
- // bodies). Comparison uses CapUrn.isEquivalent.
1048
+ // Trace end: failures stop at failed_cap. `CapUrn.isEquivalent`
1049
+ // is used for the match — never string equality.
932
1050
  let traceEnd = bodyCapSteps.length;
933
1051
  if (!success && typeof outcome.failed_cap === 'string' && outcome.failed_cap.length > 0) {
934
1052
  const CapUrn = requireHostDependency('CapUrn');
@@ -941,12 +1059,9 @@ function buildRunGraphData(data) {
941
1059
  }
942
1060
  }
943
1061
  }
944
- if (traceEnd === 0) return; // Nothing to render for this body.
1062
+ if (traceEnd === 0) return;
945
1063
 
946
- // Anchor to the first body cap's from_spec, which equals the
947
- // ForEach's from_spec (fan-out from the same source node).
948
- const anchorCanonical = canonicalMediaUrn(bodyCapSteps[0].step.from_spec);
949
- let prevBodyNodeId = anchorCanonical;
1064
+ let prevBodyNodeId = anchorNodeId;
950
1065
  const bodyKey = `body-${outcome.body_index}`;
951
1066
  const titleLabel = typeof outcome.title === 'string' && outcome.title.length > 0
952
1067
  ? outcome.title
@@ -973,7 +1088,7 @@ function buildRunGraphData(data) {
973
1088
  id: `${bodyKey}-e-${i}`,
974
1089
  source: prevBodyNodeId,
975
1090
  target: replicaNodeId,
976
- label: '',
1091
+ label: i === 0 ? body.title : '',
977
1092
  title: body.title,
978
1093
  fullUrn: body.cap_urn,
979
1094
  color: `var(${colorVar})`,
@@ -984,16 +1099,17 @@ function buildRunGraphData(data) {
984
1099
  prevBodyNodeId = replicaNodeId;
985
1100
  }
986
1101
 
987
- // If the collect exists and the body succeeded, attach the replica
988
- // tail to the collect's to_spec node so the graph visibly fans in.
989
- if (success && collectStepIdx >= 0) {
990
- const collectTo = canonicalMediaUrn(steps[collectStepIdx].to_spec);
1102
+ // Successful bodies merge their replica tail back into the Collect
1103
+ // node (or `output` if the ForEach is unclosed) so the graph
1104
+ // visibly fans in. Failed bodies do NOT merge — the trace
1105
+ // terminates at the failed cap.
1106
+ if (success) {
991
1107
  replicaEdges.push({
992
1108
  group: 'edges',
993
1109
  data: {
994
1110
  id: `${bodyKey}-merge`,
995
1111
  source: prevBodyNodeId,
996
- target: collectTo,
1112
+ target: mergeNodeId,
997
1113
  label: '',
998
1114
  title: 'collect',
999
1115
  fullUrn: '',
@@ -1005,14 +1121,13 @@ function buildRunGraphData(data) {
1005
1121
  }
1006
1122
  }
1007
1123
 
1008
- visibleSuccess.forEach((o, i) => buildBodyReplica(o, i));
1009
- visibleFailure.forEach((o, i) => buildBodyReplica(o, i));
1124
+ visibleSuccess.forEach((o) => buildBodyReplica(o));
1125
+ visibleFailure.forEach((o) => buildBodyReplica(o));
1010
1126
 
1011
1127
  // Build success and failure "show more" nodes when there are hidden
1012
- // outcomes. Anchored at the same ForEach fan-out source.
1128
+ // outcomes. Anchored at the ForEach node (or input_slot if none).
1013
1129
  const showMoreNodes = [];
1014
1130
  if (hasForeach && bodyCapSteps.length > 0) {
1015
- const anchorCanonical = canonicalMediaUrn(bodyCapSteps[0].step.from_spec);
1016
1131
  if (hiddenSuccessCount > 0) {
1017
1132
  const nodeId = 'show-more-success';
1018
1133
  showMoreNodes.push({
@@ -1030,7 +1145,7 @@ function buildRunGraphData(data) {
1030
1145
  group: 'edges',
1031
1146
  data: {
1032
1147
  id: 'show-more-success-edge',
1033
- source: anchorCanonical,
1148
+ source: anchorNodeId,
1034
1149
  target: nodeId,
1035
1150
  label: '',
1036
1151
  title: '',
@@ -1057,7 +1172,7 @@ function buildRunGraphData(data) {
1057
1172
  group: 'edges',
1058
1173
  data: {
1059
1174
  id: 'show-more-failure-edge',
1060
- source: anchorCanonical,
1175
+ source: anchorNodeId,
1061
1176
  target: nodeId,
1062
1177
  label: '',
1063
1178
  title: '',
package/capdag.test.js CHANGED
@@ -4030,86 +4030,178 @@ function testRenderer_classifyStrandCapSteps_nestedForks() {
4030
4030
  assert(!capFlags.get(5).prevForEach && capFlags.get(5).nextCollect, 'cap3 outer exit');
4031
4031
  }
4032
4032
 
4033
- function testRenderer_buildStrandGraphData_labelsForkBoundaries() {
4034
- // End-to-end: a foreach strand in which source_spec (media:pdf;list)
4035
- // differs from the first cap's from_spec (media:pdf) produces an
4036
- // explicit fix-up edge source→firstCap.from labeled "for each". The
4037
- // cap edge carries only the cap title. The topology is
4038
- // media:pdf;list --[for each]--> media:pdf --[extract text]--> media:txt
4039
- // Only two edges ForEach is NOT a separate node, Collect is absent
4040
- // because the strand's target_spec already matches the last cap's
4041
- // to_spec (no fan-in fix-up needed).
4033
+ // Helper: find an edge with the given source/target ids.
4034
+ function findEdge(edges, source, target) {
4035
+ return edges.find(e => e.source === source && e.target === target);
4036
+ }
4037
+
4038
+ function testRenderer_buildStrandGraphData_singleCapPlain() {
4039
+ // Minimal strand with one plain 1→1 cap. Plan builder produces:
4040
+ // input_slot step_0 (cap) output
4041
+ // (two edges, three nodes). No cardinality marker in the cap label
4042
+ // because input_is_sequence == output_is_sequence == false.
4043
+ const payload = {
4044
+ source_spec: 'media:a',
4045
+ target_spec: 'media:b',
4046
+ steps: [
4047
+ makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4048
+ ],
4049
+ };
4050
+ const built = rendererBuildStrandGraphData(payload);
4051
+ const nodeIds = built.nodes.map(n => n.id).sort();
4052
+ assertEqual(JSON.stringify(nodeIds), JSON.stringify(['input_slot', 'output', 'step_0']),
4053
+ 'nodes are input_slot + step_0 + output (positional ids)');
4054
+ assertEqual(built.edges.length, 2, 'two edges: input_slot→step_0 and step_0→output');
4055
+ const capEdge = findEdge(built.edges, 'input_slot', 'step_0');
4056
+ assert(capEdge !== undefined, 'cap edge from input_slot to step_0 exists');
4057
+ assertEqual(capEdge.label, 'x', 'plain cap edge label is the cap title with no cardinality marker');
4058
+ const outEdge = findEdge(built.edges, 'step_0', 'output');
4059
+ assert(outEdge !== undefined, 'output edge from step_0 to output exists');
4060
+ }
4061
+
4062
+ function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
4063
+ // A cap with input_is_sequence=true MUST emit "(n→1)" on its edge
4064
+ // label.
4065
+ const payload = {
4066
+ source_spec: 'media:a;list',
4067
+ target_spec: 'media:b',
4068
+ steps: [
4069
+ makeCapStep('cap:in="media:a;list";op=x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
4070
+ ],
4071
+ };
4072
+ const built = rendererBuildStrandGraphData(payload);
4073
+ const capEdge = findEdge(built.edges, 'input_slot', 'step_0');
4074
+ assert(capEdge !== undefined, 'cap edge exists');
4075
+ assert(capEdge.label.includes('(n\u21921)'),
4076
+ `cap edge label must include (n\u21921) marker; got: ${capEdge.label}`);
4077
+ }
4078
+
4079
+ function testRenderer_buildStrandGraphData_foreachCollectSpan() {
4080
+ // Strand: [ForEach, Cap, Collect]. Plan builder produces:
4081
+ // input_slot (source) →direct→ step_1 (cap) — cap emits its own
4082
+ // direct edge from prev
4083
+ // input_slot →direct→ step_0 (foreach) — created when Collect
4084
+ // step_0 →iteration→ step_1 — iteration edge
4085
+ // step_1 →collection→ step_2 (collect) — collection edge
4086
+ // step_2 →direct→ output — output connector
4087
+ //
4088
+ // (six nodes: input_slot, step_0, step_1, step_2, output; five
4089
+ // edges.) ForEach and Collect are REAL nodes in the graph, not
4090
+ // labels on cap edges — they're distinct processing units in the
4091
+ // plan. This mirrors capdag's plan_builder.rs exactly.
4042
4092
  const payload = {
4043
4093
  source_spec: 'media:pdf;list',
4044
- target_spec: 'media:txt',
4094
+ target_spec: 'media:txt;list',
4045
4095
  steps: [
4046
4096
  makeForEachStep('media:pdf;list'),
4047
- makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract text', 'media:pdf', 'media:txt', false, false),
4097
+ makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
4098
+ makeCollectStep('media:txt'),
4048
4099
  ],
4049
4100
  };
4050
4101
  const built = rendererBuildStrandGraphData(payload);
4051
- assertEqual(built.edges.length, 2, 'one fix-up edge + one cap edge');
4052
- const forEachEdge = built.edges[0];
4053
- assertEqual(forEachEdge.label, 'for each', 'first edge is the for-each fan-out');
4054
- assertEqual(forEachEdge.source, 'media:pdf;list', 'for-each sources from source_spec');
4055
- assertEqual(forEachEdge.target, 'media:pdf', 'for-each targets first cap.from_spec');
4056
- const capEdge = built.edges[1];
4057
- assertEqual(capEdge.label, 'extract text', 'cap edge carries only the cap title');
4058
- assertEqual(capEdge.source, 'media:pdf', 'cap sources from its from_spec');
4059
- assertEqual(capEdge.target, 'media:txt', 'cap targets its to_spec');
4060
- }
4061
-
4062
- function testRenderer_buildStrandGraphData_collectFixupEdge() {
4063
- // When the strand's target_spec (media:pdf;list) differs from the
4064
- // last cap's to_spec (media:pdf), the builder emits an explicit
4065
- // fix-up edge lastCap.to target labeled "collect".
4102
+ const nodeIds = built.nodes.map(n => n.id).sort();
4103
+ assertEqual(JSON.stringify(nodeIds),
4104
+ JSON.stringify(['input_slot', 'output', 'step_0', 'step_1', 'step_2']),
4105
+ 'positional nodes for source, foreach, cap, collect, output');
4106
+
4107
+ // The five edges the plan builder would produce:
4108
+ assert(findEdge(built.edges, 'input_slot', 'step_1') !== undefined,
4109
+ 'cap direct edge input_slot→step_1 (prev wasn\'t advanced by ForEach)');
4110
+ assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
4111
+ 'foreach input edge input_slot→step_0');
4112
+ assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
4113
+ 'iteration edge step_0→step_1 (body entry)');
4114
+ assert(findEdge(built.edges, 'step_1', 'step_2') !== undefined,
4115
+ 'collection edge step_1→step_2 (body exit collect)');
4116
+ assert(findEdge(built.edges, 'step_2', 'output') !== undefined,
4117
+ 'output edge step_2→output');
4118
+
4119
+ // ForEach and Collect nodes carry their canonical labels.
4120
+ const foreachNode = built.nodes.find(n => n.id === 'step_0');
4121
+ assertEqual(foreachNode.label, 'for each', 'ForEach node labeled "for each"');
4122
+ const collectNode = built.nodes.find(n => n.id === 'step_2');
4123
+ assertEqual(collectNode.label, 'collect', 'Collect node labeled "collect"');
4124
+ }
4125
+
4126
+ function testRenderer_buildStrandGraphData_standaloneCollect() {
4127
+ // Strand with a standalone Collect (no enclosing ForEach). Plan
4128
+ // builder creates a Collect node consuming prev directly — plain
4129
+ // direct edge, no iteration/collection semantics.
4066
4130
  const payload = {
4067
4131
  source_spec: 'media:a',
4068
- target_spec: 'media:pdf;list',
4132
+ target_spec: 'media:b;list',
4069
4133
  steps: [
4070
- makeCapStep('cap:in="media:a";op=x;out="media:pdf"', 'x', 'media:a', 'media:pdf', false, false),
4071
- makeCollectStep('media:pdf'),
4134
+ makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4135
+ makeCollectStep('media:b'),
4072
4136
  ],
4073
4137
  };
4074
4138
  const built = rendererBuildStrandGraphData(payload);
4075
- assertEqual(built.edges.length, 2, 'one cap edge + one collect fix-up');
4076
- const capEdge = built.edges[0];
4077
- assertEqual(capEdge.label, 'x', 'cap edge plain title');
4078
- const collectEdge = built.edges[1];
4079
- assertEqual(collectEdge.label, 'collect', 'collect fix-up labeled "collect"');
4080
- assertEqual(collectEdge.source, 'media:pdf', 'collect sources from last cap.to_spec');
4081
- assertEqual(collectEdge.target, 'media:pdf;list', 'collect targets target_spec');
4082
- }
4083
-
4084
- function testRenderer_buildStrandGraphData_plainCapNoMarker() {
4085
- // A plain 1→1 cap step (no ForEach/Collect adjacency, no sequence)
4086
- // must not emit any cardinality marker in the label. This catches a
4087
- // regression where "1→1" was accidentally appended to every edge.
4139
+ assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
4140
+ 'cap edge input_slot → step_0');
4141
+ assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
4142
+ 'standalone collect edge step_0 → step_1 (Collect node)');
4143
+ assert(findEdge(built.edges, 'step_1', 'output') !== undefined,
4144
+ 'output edge step_1 output');
4145
+ const collectNode = built.nodes.find(n => n.id === 'step_1');
4146
+ assertEqual(collectNode.label, 'collect', 'Collect node labeled "collect"');
4147
+ }
4148
+
4149
+ function testRenderer_buildStrandGraphData_unclosedForEachBody() {
4150
+ // Strand: [Cap_a, ForEach, Cap_b] with no closing Collect. The plan
4151
+ // builder's "unclosed ForEach" branch creates a ForEach node
4152
+ // connecting Cap_a to Cap_b via iteration, with prev becoming the
4153
+ // body exit (Cap_b).
4088
4154
  const payload = {
4089
4155
  source_spec: 'media:a',
4090
- target_spec: 'media:b',
4156
+ target_spec: 'media:c',
4091
4157
  steps: [
4092
- makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4158
+ makeCapStep('cap:in="media:a";op=a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4159
+ makeForEachStep('media:b'),
4160
+ makeCapStep('cap:in="media:b";op=b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
4093
4161
  ],
4094
4162
  };
4095
4163
  const built = rendererBuildStrandGraphData(payload);
4096
- assertEqual(built.edges.length, 1, 'one edge');
4097
- assertEqual(built.edges[0].label, 'x', 'plain cap has no cardinality suffix');
4098
- }
4099
-
4100
- function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
4101
- // A cap with input_is_sequence=true MUST emit "(n→1)" on the label.
4164
+ // Cap_a connects from input_slot.
4165
+ assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
4166
+ 'cap_a edge input_slot → step_0');
4167
+ // Cap_b still connects directly from step_0 (the ForEach didn't
4168
+ // advance prev). This mirrors plan_builder.
4169
+ assert(findEdge(built.edges, 'step_0', 'step_2') !== undefined,
4170
+ 'cap_b direct edge step_0 → step_2');
4171
+ // ForEach node at step_1 with direct edge from step_0 and iteration
4172
+ // edge to step_2.
4173
+ assert(findEdge(built.edges, 'step_0', 'step_1') !== undefined,
4174
+ 'foreach input edge step_0 → step_1');
4175
+ assert(findEdge(built.edges, 'step_1', 'step_2') !== undefined,
4176
+ 'iteration edge step_1 → step_2 (body entry)');
4177
+ // Output connects from step_2 (body exit).
4178
+ assert(findEdge(built.edges, 'step_2', 'output') !== undefined,
4179
+ 'output edge step_2 → output');
4180
+ }
4181
+
4182
+ function testRenderer_buildStrandGraphData_nestedForEachThrows() {
4183
+ // Nested ForEach without an intervening body cap in the outer
4184
+ // ForEach is an illegal nesting per plan_builder. The renderer
4185
+ // must throw the same error to surface the issue rather than
4186
+ // render a malformed graph.
4102
4187
  const payload = {
4103
- source_spec: 'media:a;list',
4104
- target_spec: 'media:b',
4188
+ source_spec: 'media:a;list;list',
4189
+ target_spec: 'media:a',
4105
4190
  steps: [
4106
- makeCapStep('cap:in="media:a;list";op=x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
4191
+ makeForEachStep('media:a;list;list'),
4192
+ makeForEachStep('media:a;list'),
4193
+ makeCapStep('cap:in="media:a";op=x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
4107
4194
  ],
4108
4195
  };
4109
- const built = rendererBuildStrandGraphData(payload);
4110
- assertEqual(built.edges.length, 1, 'one edge');
4111
- assert(built.edges[0].label.includes('(n\u21921)'),
4112
- `edge label must include (n\u21921) marker; got: ${built.edges[0].label}`);
4196
+ let threw = false;
4197
+ try {
4198
+ rendererBuildStrandGraphData(payload);
4199
+ } catch (e) {
4200
+ threw = true;
4201
+ assert(e.message.includes('nested ForEach'),
4202
+ 'error must name the nested-ForEach violation');
4203
+ }
4204
+ assert(threw, 'nested ForEach without outer body cap must throw');
4113
4205
  }
4114
4206
 
4115
4207
  function testRenderer_validateStrandPayload_missingSourceSpec() {
@@ -4702,10 +4794,12 @@ async function runTests() {
4702
4794
  runTest('RENDERER: validateStrandStep_booleanIsSequence', testRenderer_validateStrandStep_requiresBooleanIsSequence);
4703
4795
  runTest('RENDERER: classifyStrandCapSteps_simple', testRenderer_classifyStrandCapSteps_capFlags);
4704
4796
  runTest('RENDERER: classifyStrandCapSteps_nested', testRenderer_classifyStrandCapSteps_nestedForks);
4705
- runTest('RENDERER: buildStrand_labelsForkBoundaries', testRenderer_buildStrandGraphData_labelsForkBoundaries);
4706
- runTest('RENDERER: buildStrand_collectFixupEdge', testRenderer_buildStrandGraphData_collectFixupEdge);
4707
- runTest('RENDERER: buildStrand_plainCapNoMarker', testRenderer_buildStrandGraphData_plainCapNoMarker);
4797
+ runTest('RENDERER: buildStrand_singleCapPlain', testRenderer_buildStrandGraphData_singleCapPlain);
4708
4798
  runTest('RENDERER: buildStrand_sequenceShowsCardinality', testRenderer_buildStrandGraphData_sequenceShowsCardinality);
4799
+ runTest('RENDERER: buildStrand_foreachCollectSpan', testRenderer_buildStrandGraphData_foreachCollectSpan);
4800
+ runTest('RENDERER: buildStrand_standaloneCollect', testRenderer_buildStrandGraphData_standaloneCollect);
4801
+ runTest('RENDERER: buildStrand_unclosedForEachBody', testRenderer_buildStrandGraphData_unclosedForEachBody);
4802
+ runTest('RENDERER: buildStrand_nestedForEachThrows', testRenderer_buildStrandGraphData_nestedForEachThrows);
4709
4803
  runTest('RENDERER: validateStrand_missingSourceSpec', testRenderer_validateStrandPayload_missingSourceSpec);
4710
4804
 
4711
4805
  console.log('\n--- cap-graph-renderer run builder ---');
package/machine-parser.js CHANGED
@@ -174,13 +174,15 @@ function peg$parse(input, options) {
174
174
  const peg$c6 = "-";
175
175
  const peg$c7 = ">";
176
176
  const peg$c8 = "cap:";
177
- const peg$c9 = "\"";
178
- const peg$c10 = "\\\"";
179
- const peg$c11 = "\\\\";
177
+ const peg$c9 = "\r\n";
178
+ const peg$c10 = "\"";
179
+ const peg$c11 = "\\\"";
180
+ const peg$c12 = "\\\\";
180
181
 
181
182
  const peg$r0 = /^[a-zA-Z_]/;
182
183
  const peg$r1 = /^[a-zA-Z0-9_\-]/;
183
- const peg$r2 = /^[ \t\r\n]/;
184
+ const peg$r2 = /^[\n\r]/;
185
+ const peg$r3 = /^[ \t\r\n]/;
184
186
 
185
187
  const peg$e0 = peg$literalExpectation("[", false);
186
188
  const peg$e1 = peg$literalExpectation("]", false);
@@ -194,29 +196,33 @@ function peg$parse(input, options) {
194
196
  const peg$e9 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false, false);
195
197
  const peg$e10 = peg$literalExpectation("cap:", false);
196
198
  const peg$e11 = peg$anyExpectation();
197
- const peg$e12 = peg$literalExpectation("\"", false);
198
- const peg$e13 = peg$literalExpectation("\\\"", false);
199
- const peg$e14 = peg$literalExpectation("\\\\", false);
200
- const peg$e15 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false, false);
201
- const peg$e16 = peg$otherExpectation("required whitespace");
199
+ const peg$e12 = peg$otherExpectation("newline");
200
+ const peg$e13 = peg$literalExpectation("\r\n", false);
201
+ const peg$e14 = peg$classExpectation(["\n", "\r"], false, false, false);
202
+ const peg$e15 = peg$literalExpectation("\"", false);
203
+ const peg$e16 = peg$literalExpectation("\\\"", false);
204
+ const peg$e17 = peg$literalExpectation("\\\\", false);
205
+ const peg$e18 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false, false);
206
+ const peg$e19 = peg$otherExpectation("required whitespace");
202
207
 
203
208
  function peg$f0(stmts) { return stmts; }
204
209
  function peg$f1(inner) { return inner; }
205
- function peg$f2(a, c) {
210
+ function peg$f2(inner) { return inner; }
211
+ function peg$f3(a, c) {
206
212
  return { type: 'header', alias: a.value, capUrn: c.value, location: location(), aliasLocation: a.location, capUrnLocation: c.location };
207
213
  }
208
- function peg$f3(s, lc, t) {
214
+ function peg$f4(s, lc, t) {
209
215
  return { type: 'wiring', sources: s.values, capAlias: lc.alias, isLoop: lc.isLoop, target: t.value, location: location(), sourceLocations: s.locations, capAliasLocation: lc.location, targetLocation: t.location };
210
216
  }
211
- function peg$f4(a) { return { values: [a.value], locations: [a.location] }; }
212
- function peg$f5(first, a) { return a; }
213
- function peg$f6(first, rest) {
217
+ function peg$f5(a) { return { values: [a.value], locations: [a.location] }; }
218
+ function peg$f6(first, a) { return a; }
219
+ function peg$f7(first, rest) {
214
220
  return { values: [first.value, ...rest.map(r => r.value)], locations: [first.location, ...rest.map(r => r.location)] };
215
221
  }
216
- function peg$f7(a) { return { alias: a.value, isLoop: true, location: a.location }; }
217
- function peg$f8(a) { return { alias: a.value, isLoop: false, location: a.location }; }
218
- function peg$f9(a) { return { value: a, location: location() }; }
219
- function peg$f10(c) { return { value: c, location: location() }; }
222
+ function peg$f8(a) { return { alias: a.value, isLoop: true, location: a.location }; }
223
+ function peg$f9(a) { return { alias: a.value, isLoop: false, location: a.location }; }
224
+ function peg$f10(a) { return { value: a, location: location() }; }
225
+ function peg$f11(c) { return { value: c, location: location() }; }
220
226
  let peg$currPos = options.peg$currPos | 0;
221
227
  let peg$savedPos = peg$currPos;
222
228
  const peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -444,6 +450,18 @@ function peg$parse(input, options) {
444
450
  peg$currPos = s0;
445
451
  s0 = peg$FAILED;
446
452
  }
453
+ if (s0 === peg$FAILED) {
454
+ s0 = peg$currPos;
455
+ s1 = peg$parseinner();
456
+ if (s1 !== peg$FAILED) {
457
+ s2 = peg$parse_();
458
+ peg$savedPos = s0;
459
+ s0 = peg$f2(s1);
460
+ } else {
461
+ peg$currPos = s0;
462
+ s0 = peg$FAILED;
463
+ }
464
+ }
447
465
 
448
466
  return s0;
449
467
  }
@@ -470,7 +488,7 @@ function peg$parse(input, options) {
470
488
  s3 = peg$parsecap_urn_loc();
471
489
  if (s3 !== peg$FAILED) {
472
490
  peg$savedPos = s0;
473
- s0 = peg$f2(s1, s3);
491
+ s0 = peg$f3(s1, s3);
474
492
  } else {
475
493
  peg$currPos = s0;
476
494
  s0 = peg$FAILED;
@@ -506,7 +524,7 @@ function peg$parse(input, options) {
506
524
  s9 = peg$parsealias_loc();
507
525
  if (s9 !== peg$FAILED) {
508
526
  peg$savedPos = s0;
509
- s0 = peg$f3(s1, s5, s9);
527
+ s0 = peg$f4(s1, s5, s9);
510
528
  } else {
511
529
  peg$currPos = s0;
512
530
  s0 = peg$FAILED;
@@ -549,7 +567,7 @@ function peg$parse(input, options) {
549
567
  s1 = peg$parsealias_loc();
550
568
  if (s1 !== peg$FAILED) {
551
569
  peg$savedPos = s0;
552
- s1 = peg$f4(s1);
570
+ s1 = peg$f5(s1);
553
571
  }
554
572
  s0 = s1;
555
573
 
@@ -585,7 +603,7 @@ function peg$parse(input, options) {
585
603
  s8 = peg$parsealias_loc();
586
604
  if (s8 !== peg$FAILED) {
587
605
  peg$savedPos = s5;
588
- s5 = peg$f5(s3, s8);
606
+ s5 = peg$f6(s3, s8);
589
607
  } else {
590
608
  peg$currPos = s5;
591
609
  s5 = peg$FAILED;
@@ -610,7 +628,7 @@ function peg$parse(input, options) {
610
628
  s8 = peg$parsealias_loc();
611
629
  if (s8 !== peg$FAILED) {
612
630
  peg$savedPos = s5;
613
- s5 = peg$f5(s3, s8);
631
+ s5 = peg$f6(s3, s8);
614
632
  } else {
615
633
  peg$currPos = s5;
616
634
  s5 = peg$FAILED;
@@ -634,7 +652,7 @@ function peg$parse(input, options) {
634
652
  }
635
653
  if (s6 !== peg$FAILED) {
636
654
  peg$savedPos = s0;
637
- s0 = peg$f6(s3, s4);
655
+ s0 = peg$f7(s3, s4);
638
656
  } else {
639
657
  peg$currPos = s0;
640
658
  s0 = peg$FAILED;
@@ -672,7 +690,7 @@ function peg$parse(input, options) {
672
690
  s3 = peg$parsealias_loc();
673
691
  if (s3 !== peg$FAILED) {
674
692
  peg$savedPos = s0;
675
- s0 = peg$f7(s3);
693
+ s0 = peg$f8(s3);
676
694
  } else {
677
695
  peg$currPos = s0;
678
696
  s0 = peg$FAILED;
@@ -690,7 +708,7 @@ function peg$parse(input, options) {
690
708
  s1 = peg$parsealias_loc();
691
709
  if (s1 !== peg$FAILED) {
692
710
  peg$savedPos = s0;
693
- s1 = peg$f8(s1);
711
+ s1 = peg$f9(s1);
694
712
  }
695
713
  s0 = s1;
696
714
  }
@@ -754,7 +772,7 @@ function peg$parse(input, options) {
754
772
  s1 = peg$parsealias();
755
773
  if (s1 !== peg$FAILED) {
756
774
  peg$savedPos = s0;
757
- s1 = peg$f9(s1);
775
+ s1 = peg$f10(s1);
758
776
  }
759
777
  s0 = s1;
760
778
 
@@ -814,7 +832,7 @@ function peg$parse(input, options) {
814
832
  s1 = peg$parsecap_urn();
815
833
  if (s1 !== peg$FAILED) {
816
834
  peg$savedPos = s0;
817
- s1 = peg$f10(s1);
835
+ s1 = peg$f11(s1);
818
836
  }
819
837
  s0 = s1;
820
838
 
@@ -856,7 +874,7 @@ function peg$parse(input, options) {
856
874
  }
857
875
 
858
876
  function peg$parsecap_urn_body() {
859
- let s0, s1, s2;
877
+ let s0, s1, s2, s3;
860
878
 
861
879
  s0 = peg$parsequoted_value();
862
880
  if (s0 === peg$FAILED) {
@@ -878,16 +896,31 @@ function peg$parse(input, options) {
878
896
  s1 = peg$FAILED;
879
897
  }
880
898
  if (s1 !== peg$FAILED) {
881
- if (input.length > peg$currPos) {
882
- s2 = input.charAt(peg$currPos);
883
- peg$currPos++;
899
+ s2 = peg$currPos;
900
+ peg$silentFails++;
901
+ s3 = peg$parseNL();
902
+ peg$silentFails--;
903
+ if (s3 === peg$FAILED) {
904
+ s2 = undefined;
884
905
  } else {
906
+ peg$currPos = s2;
885
907
  s2 = peg$FAILED;
886
- if (peg$silentFails === 0) { peg$fail(peg$e11); }
887
908
  }
888
909
  if (s2 !== peg$FAILED) {
889
- s1 = [s1, s2];
890
- s0 = s1;
910
+ if (input.length > peg$currPos) {
911
+ s3 = input.charAt(peg$currPos);
912
+ peg$currPos++;
913
+ } else {
914
+ s3 = peg$FAILED;
915
+ if (peg$silentFails === 0) { peg$fail(peg$e11); }
916
+ }
917
+ if (s3 !== peg$FAILED) {
918
+ s1 = [s1, s2, s3];
919
+ s0 = s1;
920
+ } else {
921
+ peg$currPos = s0;
922
+ s0 = peg$FAILED;
923
+ }
891
924
  } else {
892
925
  peg$currPos = s0;
893
926
  s0 = peg$FAILED;
@@ -901,44 +934,73 @@ function peg$parse(input, options) {
901
934
  return s0;
902
935
  }
903
936
 
937
+ function peg$parseNL() {
938
+ let s0, s1;
939
+
940
+ peg$silentFails++;
941
+ if (input.substr(peg$currPos, 2) === peg$c9) {
942
+ s0 = peg$c9;
943
+ peg$currPos += 2;
944
+ } else {
945
+ s0 = peg$FAILED;
946
+ if (peg$silentFails === 0) { peg$fail(peg$e13); }
947
+ }
948
+ if (s0 === peg$FAILED) {
949
+ s0 = input.charAt(peg$currPos);
950
+ if (peg$r2.test(s0)) {
951
+ peg$currPos++;
952
+ } else {
953
+ s0 = peg$FAILED;
954
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
955
+ }
956
+ }
957
+ peg$silentFails--;
958
+ if (s0 === peg$FAILED) {
959
+ s1 = peg$FAILED;
960
+ if (peg$silentFails === 0) { peg$fail(peg$e12); }
961
+ }
962
+
963
+ return s0;
964
+ }
965
+
904
966
  function peg$parsequoted_value() {
905
967
  let s0, s1, s2, s3, s4, s5;
906
968
 
907
969
  s0 = peg$currPos;
908
970
  if (input.charCodeAt(peg$currPos) === 34) {
909
- s1 = peg$c9;
971
+ s1 = peg$c10;
910
972
  peg$currPos++;
911
973
  } else {
912
974
  s1 = peg$FAILED;
913
- if (peg$silentFails === 0) { peg$fail(peg$e12); }
975
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
914
976
  }
915
977
  if (s1 !== peg$FAILED) {
916
978
  s2 = [];
917
- if (input.substr(peg$currPos, 2) === peg$c10) {
918
- s3 = peg$c10;
979
+ if (input.substr(peg$currPos, 2) === peg$c11) {
980
+ s3 = peg$c11;
919
981
  peg$currPos += 2;
920
982
  } else {
921
983
  s3 = peg$FAILED;
922
- if (peg$silentFails === 0) { peg$fail(peg$e13); }
984
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
923
985
  }
924
986
  if (s3 === peg$FAILED) {
925
- if (input.substr(peg$currPos, 2) === peg$c11) {
926
- s3 = peg$c11;
987
+ if (input.substr(peg$currPos, 2) === peg$c12) {
988
+ s3 = peg$c12;
927
989
  peg$currPos += 2;
928
990
  } else {
929
991
  s3 = peg$FAILED;
930
- if (peg$silentFails === 0) { peg$fail(peg$e14); }
992
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
931
993
  }
932
994
  if (s3 === peg$FAILED) {
933
995
  s3 = peg$currPos;
934
996
  s4 = peg$currPos;
935
997
  peg$silentFails++;
936
998
  if (input.charCodeAt(peg$currPos) === 34) {
937
- s5 = peg$c9;
999
+ s5 = peg$c10;
938
1000
  peg$currPos++;
939
1001
  } else {
940
1002
  s5 = peg$FAILED;
941
- if (peg$silentFails === 0) { peg$fail(peg$e12); }
1003
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
942
1004
  }
943
1005
  peg$silentFails--;
944
1006
  if (s5 === peg$FAILED) {
@@ -970,31 +1032,31 @@ function peg$parse(input, options) {
970
1032
  }
971
1033
  while (s3 !== peg$FAILED) {
972
1034
  s2.push(s3);
973
- if (input.substr(peg$currPos, 2) === peg$c10) {
974
- s3 = peg$c10;
1035
+ if (input.substr(peg$currPos, 2) === peg$c11) {
1036
+ s3 = peg$c11;
975
1037
  peg$currPos += 2;
976
1038
  } else {
977
1039
  s3 = peg$FAILED;
978
- if (peg$silentFails === 0) { peg$fail(peg$e13); }
1040
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
979
1041
  }
980
1042
  if (s3 === peg$FAILED) {
981
- if (input.substr(peg$currPos, 2) === peg$c11) {
982
- s3 = peg$c11;
1043
+ if (input.substr(peg$currPos, 2) === peg$c12) {
1044
+ s3 = peg$c12;
983
1045
  peg$currPos += 2;
984
1046
  } else {
985
1047
  s3 = peg$FAILED;
986
- if (peg$silentFails === 0) { peg$fail(peg$e14); }
1048
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
987
1049
  }
988
1050
  if (s3 === peg$FAILED) {
989
1051
  s3 = peg$currPos;
990
1052
  s4 = peg$currPos;
991
1053
  peg$silentFails++;
992
1054
  if (input.charCodeAt(peg$currPos) === 34) {
993
- s5 = peg$c9;
1055
+ s5 = peg$c10;
994
1056
  peg$currPos++;
995
1057
  } else {
996
1058
  s5 = peg$FAILED;
997
- if (peg$silentFails === 0) { peg$fail(peg$e12); }
1059
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
998
1060
  }
999
1061
  peg$silentFails--;
1000
1062
  if (s5 === peg$FAILED) {
@@ -1026,11 +1088,11 @@ function peg$parse(input, options) {
1026
1088
  }
1027
1089
  }
1028
1090
  if (input.charCodeAt(peg$currPos) === 34) {
1029
- s3 = peg$c9;
1091
+ s3 = peg$c10;
1030
1092
  peg$currPos++;
1031
1093
  } else {
1032
1094
  s3 = peg$FAILED;
1033
- if (peg$silentFails === 0) { peg$fail(peg$e12); }
1095
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
1034
1096
  }
1035
1097
  if (s3 !== peg$FAILED) {
1036
1098
  s1 = [s1, s2, s3];
@@ -1053,20 +1115,20 @@ function peg$parse(input, options) {
1053
1115
  peg$silentFails++;
1054
1116
  s0 = [];
1055
1117
  s1 = input.charAt(peg$currPos);
1056
- if (peg$r2.test(s1)) {
1118
+ if (peg$r3.test(s1)) {
1057
1119
  peg$currPos++;
1058
1120
  } else {
1059
1121
  s1 = peg$FAILED;
1060
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1122
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
1061
1123
  }
1062
1124
  while (s1 !== peg$FAILED) {
1063
1125
  s0.push(s1);
1064
1126
  s1 = input.charAt(peg$currPos);
1065
- if (peg$r2.test(s1)) {
1127
+ if (peg$r3.test(s1)) {
1066
1128
  peg$currPos++;
1067
1129
  } else {
1068
1130
  s1 = peg$FAILED;
1069
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1131
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
1070
1132
  }
1071
1133
  }
1072
1134
  peg$silentFails--;
@@ -1080,21 +1142,21 @@ function peg$parse(input, options) {
1080
1142
  peg$silentFails++;
1081
1143
  s0 = [];
1082
1144
  s1 = input.charAt(peg$currPos);
1083
- if (peg$r2.test(s1)) {
1145
+ if (peg$r3.test(s1)) {
1084
1146
  peg$currPos++;
1085
1147
  } else {
1086
1148
  s1 = peg$FAILED;
1087
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1149
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
1088
1150
  }
1089
1151
  if (s1 !== peg$FAILED) {
1090
1152
  while (s1 !== peg$FAILED) {
1091
1153
  s0.push(s1);
1092
1154
  s1 = input.charAt(peg$currPos);
1093
- if (peg$r2.test(s1)) {
1155
+ if (peg$r3.test(s1)) {
1094
1156
  peg$currPos++;
1095
1157
  } else {
1096
1158
  s1 = peg$FAILED;
1097
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1159
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
1098
1160
  }
1099
1161
  }
1100
1162
  } else {
@@ -1103,7 +1165,7 @@ function peg$parse(input, options) {
1103
1165
  peg$silentFails--;
1104
1166
  if (s0 === peg$FAILED) {
1105
1167
  s1 = peg$FAILED;
1106
- if (peg$silentFails === 0) { peg$fail(peg$e16); }
1168
+ if (peg$silentFails === 0) { peg$fail(peg$e19); }
1107
1169
  }
1108
1170
 
1109
1171
  return s0;
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.106.243"
43
+ "version": "0.109.248"
44
44
  }