capdag 1.219.594 → 1.226.626

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.
@@ -590,6 +590,18 @@ function buildStylesheet() {
590
590
  'target-arrow-shape': 'tee',
591
591
  },
592
592
  },
593
+ {
594
+ // A convergence (fan-in) edge: a second producer feeding a cap's
595
+ // non-main argument. Dotted + diamond-tail to read as a merging
596
+ // side-input distinct from the solid main backbone edge.
597
+ selector: 'edge.strand-convergence',
598
+ style: {
599
+ 'line-style': 'dotted',
600
+ 'width': 2,
601
+ 'source-arrow-shape': 'diamond',
602
+ 'source-arrow-color': machineEdgeColor || 'data(color)',
603
+ },
604
+ },
593
605
  {
594
606
  selector: 'edge.faded',
595
607
  style: { 'opacity': fadedEdgeOpacity },
@@ -762,6 +774,28 @@ function validateStrandStep(step, path) {
762
774
  if (typeof body.output_is_sequence !== 'boolean') {
763
775
  throw new Error(`CapFabRenderer: ${path}.step_type.Cap.output_is_sequence must be a boolean`);
764
776
  }
777
+ // `inputs` (capdag CapInput list) carries the data-flow graph: the main
778
+ // input plus any convergence inputs. Optional on legacy payloads, but
779
+ // when present it must be well-formed — the fan-in edges are drawn from
780
+ // it, so a malformed entry is a hard error, not a silent skip.
781
+ if (body.inputs !== undefined) {
782
+ assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
783
+ body.inputs.forEach((input, inputIdx) => {
784
+ const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
785
+ assertObject(input, inputPath);
786
+ assertString(input.arg_urn, `${inputPath}.arg_urn`);
787
+ // serde: unit variant → the string "StrandInput"; struct variant →
788
+ // { Step: { token_id } }. Anything else is malformed.
789
+ if (input.source === 'StrandInput') return;
790
+ if (input.source && typeof input.source === 'object' && input.source.Step) {
791
+ assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
792
+ return;
793
+ }
794
+ throw new Error(
795
+ `CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
796
+ );
797
+ });
798
+ }
765
799
  } else {
766
800
  assertString(body.media_def, `${path}.step_type.${variant}.media_def`);
767
801
  }
@@ -1309,6 +1343,17 @@ function buildStrandGraphData(data) {
1309
1343
  return outerExit;
1310
1344
  }
1311
1345
 
1346
+ // Map every step's stable token_id to its output node id. A cap
1347
+ // step's node (`step_${i}`) IS its `to_spec` output, so a convergence
1348
+ // input naming a producer by token resolves to that producer's output
1349
+ // node — the value flowing into the fan-in arg.
1350
+ const tokenToNodeId = new Map();
1351
+ data.steps.forEach((step, i) => {
1352
+ if (step && typeof step.token_id === 'string' && step.token_id.length > 0) {
1353
+ tokenToNodeId.set(step.token_id, `step_${i}`);
1354
+ }
1355
+ });
1356
+
1312
1357
  data.steps.forEach((step, i) => {
1313
1358
  const variant = Object.keys(step.step_type)[0];
1314
1359
  const nodeId = `step_${i}`;
@@ -1330,8 +1375,43 @@ function buildStrandGraphData(data) {
1330
1375
  if (cardinality !== '1\u21921') {
1331
1376
  label = `${label} (${cardinality})`;
1332
1377
  }
1378
+ // Resolve this cap's actual data-flow producers from `body.inputs`
1379
+ // (capdag CapInput serde shape): each `source` is the string
1380
+ // "StrandInput" (fed by the strand's input anchor → the input slot
1381
+ // node) or `{ Step: { token_id } }` (fed by a producing cap → that
1382
+ // step's output node). This is the authoritative topology; step
1383
+ // ORDER is not — the realizer emits edges greedily in dependency
1384
+ // order, so `prevNodeId` (the previously-emitted step) need not be
1385
+ // one of this cap's inputs at all.
1386
+ const capInputs = Array.isArray(body.inputs) ? body.inputs : [];
1387
+ const producerNodeIds = [];
1388
+ for (const input of capInputs) {
1389
+ const src = input && input.source;
1390
+ if (src === 'StrandInput') {
1391
+ producerNodeIds.push(inputSlotId);
1392
+ continue;
1393
+ }
1394
+ const producerToken = src && typeof src === 'object' && src.Step && src.Step.token_id;
1395
+ if (typeof producerToken !== 'string' || producerToken.length === 0) continue;
1396
+ const producerNodeId = tokenToNodeId.get(producerToken);
1397
+ if (producerNodeId) producerNodeIds.push(producerNodeId);
1398
+ }
1399
+
1400
+ // Pick the source of the single labeled "backbone" edge (the one
1401
+ // carrying the cap title + cardinality). Inside a ForEach body — or
1402
+ // when the cap declares no inputs — keep the linear `prevNodeId`
1403
+ // thread so the ForEach boundary handling and its (1→n) entry marker
1404
+ // are preserved exactly. Otherwise anchor the backbone on a REAL
1405
+ // input: `prevNodeId` if it is genuinely one, else the first actual
1406
+ // producer — so a non-input step ordered just before this cap can
1407
+ // never become a spurious backbone edge.
1408
+ const isForeachContext = isForeachEntry || insideForEachBody !== null;
1409
+ let backboneSource = prevNodeId;
1410
+ if (!isForeachContext && producerNodeIds.length > 0 && !producerNodeIds.includes(prevNodeId)) {
1411
+ backboneSource = producerNodeIds[0];
1412
+ }
1333
1413
  addEdge(
1334
- prevNodeId,
1414
+ backboneSource,
1335
1415
  nodeId,
1336
1416
  label,
1337
1417
  body.title,
@@ -1340,6 +1420,25 @@ function buildStrandGraphData(data) {
1340
1420
  { foreachEntry: isForeachEntry }
1341
1421
  );
1342
1422
 
1423
+ // Convergence (fan-in): draw an edge from every other producer that
1424
+ // isn't the backbone source, so a cap fed by several producers renders
1425
+ // as a DAG, not a chain. Deduplicated against the backbone edge and
1426
+ // against a self-loop.
1427
+ const drawnSources = new Set([backboneSource, nodeId]);
1428
+ for (const producerNodeId of producerNodeIds) {
1429
+ if (drawnSources.has(producerNodeId)) continue;
1430
+ drawnSources.add(producerNodeId);
1431
+ addEdge(
1432
+ producerNodeId,
1433
+ nodeId,
1434
+ '',
1435
+ body.title,
1436
+ body.cap_urn,
1437
+ 'strand-convergence',
1438
+ {}
1439
+ );
1440
+ }
1441
+
1343
1442
  if (insideForEachBody !== null) {
1344
1443
  if (bodyEntry === null) bodyEntry = nodeId;
1345
1444
  bodyExit = nodeId;
package/capdag.js CHANGED
@@ -5876,10 +5876,11 @@ function unixSecondsNow() {
5876
5876
  }
5877
5877
 
5878
5878
  // Default protocol limits / version, mirroring capdag::bifaci::frame.
5879
- const BIFACI_PROTOCOL_VERSION = 2;
5879
+ const BIFACI_PROTOCOL_VERSION = 3;
5880
5880
  const BIFACI_DEFAULT_MAX_FRAME = 3670016;
5881
5881
  const BIFACI_DEFAULT_MAX_CHUNK = 262144;
5882
5882
  const BIFACI_DEFAULT_MAX_REORDER_BUFFER = 64;
5883
+ const BIFACI_DEFAULT_INITIAL_CREDIT = 32;
5883
5884
  const BIFACI_FRAME_TYPE_HELLO = 0;
5884
5885
  // Frame CBOR integer keys (capdag::bifaci::frame::keys).
5885
5886
  const BIFACI_KEY_VERSION = 0;
@@ -6008,6 +6009,7 @@ function encodeHelloFrame() {
6008
6009
  ['max_frame', BIFACI_DEFAULT_MAX_FRAME],
6009
6010
  ['max_chunk', BIFACI_DEFAULT_MAX_CHUNK],
6010
6011
  ['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
6012
+ ['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
6011
6013
  ['version', BIFACI_PROTOCOL_VERSION],
6012
6014
  ],
6013
6015
  };
@@ -6386,7 +6388,9 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6386
6388
  // [alias cap:in="...";op=...;out="..."] — header (defines a cap with alias)
6387
6389
  // [src -> alias -> dst] — wiring (connects nodes via cap)
6388
6390
  // [(a, b) -> alias -> dst] — fan-in wiring
6389
- // [src -> LOOP alias -> dst] — loop wiring (ForEach semantics)
6391
+ //
6392
+ // Per-item map (`is_loop`) is a derived cardinality property, never authored
6393
+ // syntax — there is no LOOP marker in the grammar.
6390
6394
  // ============================================================================
6391
6395
 
6392
6396
  /**
@@ -6502,7 +6506,9 @@ class MachineEdge {
6502
6506
  */
6503
6507
  toString() {
6504
6508
  const sources = this.sources.map(s => s.toString()).join(', ');
6505
- const loopPrefix = this.isLoop ? 'LOOP ' : '';
6509
+ // Debug-only marker for a per-item map edge. Not notation syntax (the
6510
+ // `LOOP` keyword is retired); `is_loop` is a derived cardinality property.
6511
+ const loopPrefix = this.isLoop ? 'map ' : '';
6506
6512
  return `(${sources}) -${loopPrefix}${this.capUrn}-> ${this.target}`;
6507
6513
  }
6508
6514
  }
@@ -6701,13 +6707,13 @@ class Machine {
6701
6707
  const targetKey = edge.target.toString();
6702
6708
  const targetName = nodeNames.get(targetKey);
6703
6709
 
6704
- const loopPrefix = edge.isLoop ? 'LOOP ' : '';
6705
-
6710
+ // `is_loop` is NOT emitted into notation text: it is a derived cardinality
6711
+ // property, not authored syntax (the `LOOP` keyword is retired).
6706
6712
  if (sources.length === 1) {
6707
- output += `[${sources[0]} -> ${loopPrefix}${alias} -> ${targetName}]`;
6713
+ output += `[${sources[0]} -> ${alias} -> ${targetName}]`;
6708
6714
  } else {
6709
6715
  const group = sources.join(', ');
6710
- output += `[(${group}) -> ${loopPrefix}${alias} -> ${targetName}]`;
6716
+ output += `[(${group}) -> ${alias} -> ${targetName}]`;
6711
6717
  }
6712
6718
  }
6713
6719
 
@@ -6754,13 +6760,13 @@ class Machine {
6754
6760
  const targetKey = edge.target.toString();
6755
6761
  const targetName = nodeNames.get(targetKey);
6756
6762
 
6757
- const loopPrefix = edge.isLoop ? 'LOOP ' : '';
6758
-
6763
+ // `is_loop` is NOT emitted into notation text: it is a derived cardinality
6764
+ // property, not authored syntax (the `LOOP` keyword is retired).
6759
6765
  if (sources.length === 1) {
6760
- lines.push(`[${sources[0]} -> ${loopPrefix}${alias} -> ${targetName}]`);
6766
+ lines.push(`[${sources[0]} -> ${alias} -> ${targetName}]`);
6761
6767
  } else {
6762
6768
  const group = sources.join(', ');
6763
- lines.push(`[(${group}) -> ${loopPrefix}${alias} -> ${targetName}]`);
6769
+ lines.push(`[(${group}) -> ${alias} -> ${targetName}]`);
6764
6770
  }
6765
6771
  }
6766
6772
 
@@ -6812,13 +6818,96 @@ class Machine {
6812
6818
  const targetKey = edge.target.toString();
6813
6819
  const targetName = nodeNames.get(targetKey);
6814
6820
 
6815
- const loopPrefix = edge.isLoop ? 'LOOP ' : '';
6821
+ // `is_loop` is NOT emitted into notation text: it is a derived cardinality
6822
+ // property, not authored syntax (the `LOOP` keyword is retired).
6823
+ if (sources.length === 1) {
6824
+ lines.push(`${open}${sources[0]} -> ${alias} -> ${targetName}${close}`);
6825
+ } else {
6826
+ const group = sources.join(', ');
6827
+ lines.push(`${open}(${group}) -> ${alias} -> ${targetName}${close}`);
6828
+ }
6829
+ }
6830
+
6831
+ return bracketed ? lines.join('') : lines.join('\n');
6832
+ }
6833
+
6834
+ /**
6835
+ * Serialize to machine notation rendering each cap by its registered display
6836
+ * alias (shortest name, ties alphabetical) when one exists, falling back to
6837
+ * the canonical-URN `edge_N` header form otherwise. This is the "store
6838
+ * aliased" form: generated and persisted machines use it so the saved
6839
+ * notation reads in terms of aliases.
6840
+ *
6841
+ * Semantics (mirrors Rust Machine::to_machine_notation_aliased):
6842
+ * - A cap WITH a display alias is referenced DIRECTLY in the wiring's
6843
+ * cap-position by its alias name, with NO `[edge_N cap:...]` header (the
6844
+ * grammar forbids an alias in a header's cap position anyway, and a header
6845
+ * would be redundant).
6846
+ * - A cap WITHOUT an alias keeps the synthetic `edge_N` wiring token plus its
6847
+ * `[edge_N cap:...]` header binding it to the canonical cap URN.
6848
+ *
6849
+ * The canonical `edge_N` numbering and node naming are inherited unchanged
6850
+ * from the canonical serializer, so an un-aliased machine produces byte-
6851
+ * identical output to toMachineNotationFormatted.
6852
+ *
6853
+ * @param {FabricRegistryClient} registry - resolves URN → display alias
6854
+ * @param {'bracketed' | 'line-based'} format
6855
+ * @returns {string}
6856
+ */
6857
+ toMachineNotationAliased(registry, format) {
6858
+ if (this._edges.length === 0) {
6859
+ return '';
6860
+ }
6861
+
6862
+ const { aliases, nodeNames, edgeOrder } = this._buildSerializationMaps();
6863
+ const bracketed = format === 'bracketed';
6864
+ const open = bracketed ? '[' : '';
6865
+ const close = bracketed ? ']' : '';
6866
+
6867
+ // Resolve, per edge index, the cap-position token and whether it needs a
6868
+ // header. An aliased cap uses its display alias directly (no header); an
6869
+ // un-aliased cap keeps its synthetic `edge_N` token (with a header).
6870
+ const edgeToken = new Map(); // edgeIdx → token used in the wiring
6871
+ const edgeNeedsHeader = new Map(); // edgeIdx → bool
6872
+ for (const [edgeAlias, info] of aliases) {
6873
+ const edge = this._edges[info.edgeIdx];
6874
+ const displayAlias = registry.displayAliasForUrn(edge.capUrn.toString());
6875
+ if (displayAlias !== null && displayAlias !== undefined) {
6876
+ edgeToken.set(info.edgeIdx, displayAlias);
6877
+ edgeNeedsHeader.set(info.edgeIdx, false);
6878
+ } else {
6879
+ edgeToken.set(info.edgeIdx, edgeAlias);
6880
+ edgeNeedsHeader.set(info.edgeIdx, true);
6881
+ }
6882
+ }
6883
+
6884
+ const lines = [];
6885
+
6886
+ // Headers only for edges that kept a synthetic `edge_N` token (un-aliased
6887
+ // caps). Emit in alias-sorted order to match the canonical serializer.
6888
+ const sortedAliases = Array.from(aliases.entries()).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
6889
+ for (const [, info] of sortedAliases) {
6890
+ if (edgeNeedsHeader.get(info.edgeIdx)) {
6891
+ const edge = this._edges[info.edgeIdx];
6892
+ lines.push(`${open}${edgeToken.get(info.edgeIdx)} ${edge.capUrn}${close}`);
6893
+ }
6894
+ }
6895
+
6896
+ // Wirings in canonical edge order, using the resolved cap-position token.
6897
+ for (const edgeIdx of edgeOrder) {
6898
+ const edge = this._edges[edgeIdx];
6899
+ const token = edgeToken.get(edgeIdx);
6816
6900
 
6901
+ const sources = edge.sources.map(s => nodeNames.get(s.toString()));
6902
+ const targetName = nodeNames.get(edge.target.toString());
6903
+
6904
+ // `is_loop` is NOT emitted into notation text: it is a derived cardinality
6905
+ // property, not authored syntax (the `LOOP` keyword is retired).
6817
6906
  if (sources.length === 1) {
6818
- lines.push(`${open}${sources[0]} -> ${loopPrefix}${alias} -> ${targetName}${close}`);
6907
+ lines.push(`${open}${sources[0]} -> ${token} -> ${targetName}${close}`);
6819
6908
  } else {
6820
6909
  const group = sources.join(', ');
6821
- lines.push(`${open}(${group}) -> ${loopPrefix}${alias} -> ${targetName}${close}`);
6910
+ lines.push(`${open}(${group}) -> ${token} -> ${targetName}${close}`);
6822
6911
  }
6823
6912
  }
6824
6913
 
@@ -6906,7 +6995,8 @@ class Machine {
6906
6995
  * - Leaf targets: stadium-shaped nodes (rounded)
6907
6996
  * - Intermediate nodes: rectangular
6908
6997
  * - Edge labels: op= tag value (or full cap URN if no op)
6909
- * - LOOP edges: dotted line style with "LOOP" prefix on label
6998
+ * - Per-item map edges (`is_loop`): dotted line style `is_loop` is a
6999
+ * derived render property, never the retired `LOOP` keyword
6910
7000
  * - Node labels: derived MediaUrn type
6911
7001
  *
6912
7002
  * @returns {string} Mermaid flowchart definition
@@ -6961,8 +7051,9 @@ class Machine {
6961
7051
  const srcName = nodeNames.get(srcKey);
6962
7052
 
6963
7053
  if (edge.isLoop) {
6964
- // Dotted line for LOOP edges
6965
- lines.push(` ${srcName} -. "LOOP ${label}" .-> ${targetName}`);
7054
+ // Dotted line renders the derived per-item map (`is_loop`) property.
7055
+ // No "LOOP" text — that keyword is retired.
7056
+ lines.push(` ${srcName} -. "${label}" .-> ${targetName}`);
6966
7057
  } else {
6967
7058
  lines.push(` ${srcName} -- "${label}" --> ${targetName}`);
6968
7059
  }
@@ -7080,7 +7171,6 @@ function _parseMachineInternal(input) {
7080
7171
  sources: stmt.sources,
7081
7172
  capAlias: stmt.capAlias,
7082
7173
  target: stmt.target,
7083
- isLoop: stmt.isLoop,
7084
7174
  position: i,
7085
7175
  location: stmt.location,
7086
7176
  sourceLocations: stmt.sourceLocations,
@@ -7202,7 +7292,13 @@ function _parseMachineInternal(input) {
7202
7292
  assignOrCheckNode(wiring.target, capOutMedia, nodeMedia, wiring.position,
7203
7293
  wiring.targetLocation || wiring.location);
7204
7294
 
7205
- edges.push(new MachineEdge(sourceUrns, capUrn, capOutMedia, wiring.isLoop));
7295
+ // `is_loop` is a derived cardinality property, not authored syntax (the
7296
+ // `LOOP` keyword is retired). The pure-JS parse path has no cap definitions
7297
+ // — it derives media only from the cap URN's in=/out= specs — so it cannot
7298
+ // evaluate the ForEach rule (a sequence source feeding a scalar-input cap).
7299
+ // Editors obtain the derived `is_loop` from the engine server's resolved
7300
+ // graph; here it defaults to false.
7301
+ edges.push(new MachineEdge(sourceUrns, capUrn, capOutMedia, false));
7206
7302
  }
7207
7303
 
7208
7304
  return {
@@ -7413,6 +7509,12 @@ class FabricRegistryClient {
7413
7509
  // canonical key only needs to be unique per equivalence class; we
7414
7510
  // store one entry per equivalence class.
7415
7511
  this._mediaCache = new Map();
7512
+ // Normalized alias name → StoredAlias. Mirrors Rust
7513
+ // FabricRegistry::cached_aliases. The display/serialization surfaces
7514
+ // read this synchronously; it is warmed by insertCachedAliasForTest (and,
7515
+ // in the heavier mirrors, a background prefetch this lightweight client
7516
+ // does not implement).
7517
+ this._cachedAliases = new Map();
7416
7518
  }
7417
7519
 
7418
7520
  /**
@@ -7542,12 +7644,88 @@ class FabricRegistryClient {
7542
7644
  return Array.from(ops).sort();
7543
7645
  }
7544
7646
 
7647
+ /**
7648
+ * Insert an alias directly into the in-memory cache, keyed by its
7649
+ * normalized name. Mirrors Rust
7650
+ * FabricRegistry::insert_cached_alias_for_test — it is the warm-cache seam
7651
+ * the display/serialization primitives read from. The lightweight JS client
7652
+ * has no background alias prefetch, so callers seed the cache through here.
7653
+ *
7654
+ * @param {StoredAlias} alias
7655
+ */
7656
+ insertCachedAliasForTest(alias) {
7657
+ const normalized = normalizeAliasName(alias.name);
7658
+ this._cachedAliases.set(normalized, alias);
7659
+ }
7660
+
7661
+ /**
7662
+ * Reverse lookup: the display alias for a `cap:`/`media:` URN, or null if no
7663
+ * cached alias points at it. This is the canonical primitive every UI surface
7664
+ * and notation generator uses to render an aliased name in place of a raw URN.
7665
+ *
7666
+ * The query URN is canonicalised through its own parser (cap vs media by
7667
+ * prefix) before matching, because alias targets are stored canonically — a
7668
+ * non-canonical query (different tag order, redundant whitespace) would
7669
+ * otherwise miss. A URN that is neither a cap nor a media URN, or that fails
7670
+ * to parse, returns null (it cannot have an alias).
7671
+ *
7672
+ * When multiple aliases target the same URN, the winner is the SHORTEST name,
7673
+ * ties broken alphabetically (see selectDisplayAlias). This is deterministic
7674
+ * and stable across processes for a given alias set.
7675
+ *
7676
+ * Mirrors Rust FabricRegistry::display_alias_for_urn.
7677
+ * @param {string} urn
7678
+ * @returns {string|null}
7679
+ */
7680
+ displayAliasForUrn(urn) {
7681
+ // Canonicalise by kind. classifyAliasTarget keys off the parser (cap vs
7682
+ // media), the same classifier the alias publisher uses for targets, so a
7683
+ // query and a stored target canonicalise identically.
7684
+ const kind = classifyAliasTarget(urn);
7685
+ let canonical;
7686
+ if (kind === ALIAS_TARGET_CAP) {
7687
+ canonical = CapUrn.fromString(urn).toString();
7688
+ } else if (kind === ALIAS_TARGET_MEDIA) {
7689
+ canonical = MediaUrn.fromString(urn).toString();
7690
+ } else {
7691
+ return null;
7692
+ }
7693
+
7694
+ const names = [];
7695
+ for (const alias of this._cachedAliases.values()) {
7696
+ if (alias.target === canonical) {
7697
+ names.push(alias.name);
7698
+ }
7699
+ }
7700
+ return selectDisplayAlias(names);
7701
+ }
7702
+
7703
+ /**
7704
+ * All cached aliases whose target is a CAP URN, as [name, capUrn] pairs.
7705
+ * Used by the notation editor to offer registered cap aliases as wiring
7706
+ * completions. Order is unspecified (the caller sorts/filters). Synchronous,
7707
+ * cache-only — relies on the alias cache having been warmed.
7708
+ *
7709
+ * Mirrors Rust FabricRegistry::cached_cap_aliases.
7710
+ * @returns {Array<[string, string]>}
7711
+ */
7712
+ cachedCapAliases() {
7713
+ const pairs = [];
7714
+ for (const alias of this._cachedAliases.values()) {
7715
+ if (classifyAliasTarget(alias.target) === ALIAS_TARGET_CAP) {
7716
+ pairs.push([alias.name, alias.target]);
7717
+ }
7718
+ }
7719
+ return pairs;
7720
+ }
7721
+
7545
7722
  /**
7546
7723
  * Invalidate all caches. Next call to any method fetches fresh data.
7547
7724
  */
7548
7725
  invalidate() {
7549
7726
  this._capCache = null;
7550
7727
  this._mediaCache.clear();
7728
+ this._cachedAliases.clear();
7551
7729
  }
7552
7730
  }
7553
7731
 
@@ -7620,6 +7798,34 @@ function classifyAliasTarget(target) {
7620
7798
  return null;
7621
7799
  }
7622
7800
 
7801
+ /**
7802
+ * Pick the display alias from a set of alias names that all target the same
7803
+ * URN: the SHORTEST name, ties broken alphabetically. Returns null for an
7804
+ * empty set.
7805
+ *
7806
+ * The ordering is total and deterministic: (length, name) lexicographic. So
7807
+ * `png` beats `png-image` (shorter), and between equal-length `a16` / `a09`
7808
+ * the alphabetical-smaller `a09` wins. Stable across processes for a given
7809
+ * alias set, which is what makes aliased UI/notation reproducible.
7810
+ *
7811
+ * Mirrors Rust select_display_alias.
7812
+ * @param {Iterable<string>} names
7813
+ * @returns {string|null}
7814
+ */
7815
+ function selectDisplayAlias(names) {
7816
+ let best = null;
7817
+ for (const name of names) {
7818
+ if (
7819
+ best === null ||
7820
+ name.length < best.length ||
7821
+ (name.length === best.length && name < best)
7822
+ ) {
7823
+ best = name;
7824
+ }
7825
+ }
7826
+ return best;
7827
+ }
7828
+
7623
7829
  /**
7624
7830
  * The published wire/cache shape of a single fabric alias. Mirrors
7625
7831
  * fabric/alias.schema.json: { name, target, version }.
@@ -7682,6 +7888,7 @@ module.exports = {
7682
7888
  isAliasToken,
7683
7889
  normalizeAliasName,
7684
7890
  classifyAliasTarget,
7891
+ selectDisplayAlias,
7685
7892
  StoredAlias,
7686
7893
  Manifest,
7687
7894
  CapUrn,
package/capdag.test.js CHANGED
@@ -36,6 +36,7 @@ const {
36
36
  CAP_IDENTITY,
37
37
  ALIAS_TARGET_CAP, ALIAS_TARGET_MEDIA,
38
38
  tokenIsUrn, isAliasToken, normalizeAliasName, classifyAliasTarget,
39
+ selectDisplayAlias,
39
40
  StoredAlias, Manifest
40
41
  } = require('./capdag.js');
41
42
 
@@ -3307,14 +3308,25 @@ function test6294_Machine_fanInSecondaryUnassignedGetsWildcard() {
3307
3308
  assertEqual(g.edges()[0].sources[1].toString(), 'media:');
3308
3309
  }
3309
3310
 
3310
- // TEST6306: Machine loop edge
3311
- function test6306_Machine_loopEdge() {
3312
- const g = Machine.fromString(
3313
- '[p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"]' +
3314
- '[pages -> LOOP p2t -> texts]'
3311
+ // TEST6306: The retired LOOP keyword is no longer grammar. A wiring that still
3312
+ // writes `LOOP <cap>` before the cap alias no longer parses — `LOOP` is now an
3313
+ // ordinary alias, so `pages -> LOOP p2t -> texts` is two aliases in the cap
3314
+ // position with no arrow between them, which is a syntax error.
3315
+ function test6306_Machine_loopKeywordIsNotGrammar() {
3316
+ assertThrowsWithCode(
3317
+ () => Machine.fromString(
3318
+ '[p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"]' +
3319
+ '[pages -> LOOP p2t -> texts]'
3320
+ ),
3321
+ MachineSyntaxErrorCodes.PARSE_ERROR
3322
+ );
3323
+
3324
+ // `LOOP` on its own in the cap position parses fine — it is just an alias —
3325
+ // but resolves to nothing, proving it carries no special meaning.
3326
+ assertThrowsWithCode(
3327
+ () => Machine.fromString('[pages -> LOOP -> texts]'),
3328
+ MachineSyntaxErrorCodes.UNDEFINED_ALIAS
3315
3329
  );
3316
- assertEqual(g.edgeCount(), 1);
3317
- assertEqual(g.edges()[0].isLoop, true);
3318
3330
  }
3319
3331
 
3320
3332
  // TEST6308: Machine undefined alias fails
@@ -3415,14 +3427,16 @@ function test6331_Machine_lineBasedTwoStepChain() {
3415
3427
  assertEqual(g.edgeCount(), 2);
3416
3428
  }
3417
3429
 
3418
- // TEST6334: Machine line based loop
3419
- function test6334_Machine_lineBasedLoop() {
3420
- const g = Machine.fromString(
3421
- 'p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"\n' +
3422
- 'pages -> LOOP p2t -> texts'
3430
+ // TEST6334: The retired LOOP keyword is not grammar in line-based mode either —
3431
+ // `pages -> LOOP p2t -> texts` is a syntax error, same as the bracketed form.
3432
+ function test6334_Machine_lineBasedLoopKeywordIsNotGrammar() {
3433
+ assertThrowsWithCode(
3434
+ () => Machine.fromString(
3435
+ 'p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"\n' +
3436
+ 'pages -> LOOP p2t -> texts'
3437
+ ),
3438
+ MachineSyntaxErrorCodes.PARSE_ERROR
3423
3439
  );
3424
- assertEqual(g.edgeCount(), 1);
3425
- assertEqual(g.edges()[0].isLoop, true);
3426
3440
  }
3427
3441
 
3428
3442
  // TEST6337: Machine line based fan in
@@ -3834,8 +3848,12 @@ function test6415_Machine_roundtripFanOut() {
3834
3848
  'Fan-out round-trip failed: ' + notation);
3835
3849
  }
3836
3850
 
3837
- // TEST6417: Machine roundtrip loop edge
3838
- function test6417_Machine_roundtripLoopEdge() {
3851
+ // TEST6417: A per-item map (`is_loop`) edge serializes WITHOUT any LOOP marker —
3852
+ // `is_loop` is a derived cardinality property, not authored notation text. The
3853
+ // pure-JS parse path has no cap definitions to re-derive cardinality, so the
3854
+ // reparsed edge has `isLoop === false`; editors get the derived value from the
3855
+ // engine, not from re-parsing.
3856
+ function test6417_Machine_loopEdgeSerializesWithoutLoopText() {
3839
3857
  const original = new Machine([new MachineEdge(
3840
3858
  [MediaUrn.fromString('media:disbound-page;enc=utf-8')],
3841
3859
  CapUrn.fromString('cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"'),
@@ -3843,9 +3861,17 @@ function test6417_Machine_roundtripLoopEdge() {
3843
3861
  true
3844
3862
  )]);
3845
3863
  const notation = original.toMachineNotation();
3864
+ assert(!notation.includes('LOOP'), 'serialized notation must not contain the retired LOOP keyword');
3865
+
3846
3866
  const reparsed = Machine.fromString(notation);
3847
- assert(original.isEquivalent(reparsed), 'Loop round-trip failed');
3848
- assertEqual(reparsed.edges()[0].isLoop, true, 'isLoop must be preserved');
3867
+ assertEqual(reparsed.edgeCount(), 1);
3868
+ assertEqual(reparsed.edges()[0].isLoop, false,
3869
+ 'pure-JS parse path cannot derive cardinality — isLoop defaults to false');
3870
+ // The structural payload (sources, cap, target) round-trips intact.
3871
+ assert(reparsed.edges()[0].capUrn.isEquivalent(original.edges()[0].capUrn),
3872
+ 'cap URN must round-trip');
3873
+ assert(reparsed.edges()[0].target.isEquivalent(original.edges()[0].target),
3874
+ 'target media must round-trip');
3849
3875
  }
3850
3876
 
3851
3877
  // TEST6419: Machine serialization is deterministic
@@ -4202,16 +4228,21 @@ function test6462_Machine_toMermaid_linearChain() {
4202
4228
  assert(mermaid.includes('(['), 'Should have stadium shape nodes');
4203
4229
  }
4204
4230
 
4205
- // TEST6463: Machine to mermaid loop edge
4231
+ // TEST6463: Mermaid renders a per-item map (`is_loop`) edge with a dotted line
4232
+ // — `is_loop` is a kept render property — but emits NO "LOOP" text, since that
4233
+ // keyword is retired. The loop edge is built programmatically because the
4234
+ // grammar no longer has any way to author one.
4206
4235
  function test6463_Machine_toMermaid_loopEdge() {
4207
- const machine = Machine.fromString(
4208
- '[p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"]' +
4209
- '[pages -> LOOP p2t -> texts]'
4210
- );
4236
+ const machine = new Machine([new MachineEdge(
4237
+ [MediaUrn.fromString('media:disbound-page;enc=utf-8')],
4238
+ CapUrn.fromString('cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"'),
4239
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
4240
+ true
4241
+ )]);
4211
4242
  const mermaid = machine.toMermaid();
4212
- assert(mermaid.includes('LOOP'), 'Should include LOOP label');
4213
- assert(mermaid.includes('-.'), 'Should use dotted line for LOOP');
4214
- assert(mermaid.includes('.->'), 'Should use dotted arrow for LOOP');
4243
+ assert(!mermaid.includes('LOOP'), 'Must not emit the retired LOOP label');
4244
+ assert(mermaid.includes('-.'), 'Should use dotted line for the per-item map edge');
4245
+ assert(mermaid.includes('.->'), 'Should use dotted arrow for the per-item map edge');
4215
4246
  }
4216
4247
 
4217
4248
  // TEST6464: Machine to mermaid empty graph
@@ -6383,6 +6414,107 @@ function test1887_manifestSerdeRoundTripsAliases() {
6383
6414
  assertEqual(JSON.stringify(a.toJSON()), '{"name":"pdf2text","target":"cap:effect=none","version":3}', 'alias wire shape');
6384
6415
  }
6385
6416
 
6417
+ // TEST1894: selectDisplayAlias picks the SHORTEST name, ties broken
6418
+ // alphabetically. This is the deterministic ordering every aliased-display
6419
+ // surface relies on; a regression here silently changes which alias the whole
6420
+ // UI renders.
6421
+ function test1894_selectDisplayAliasOrdering() {
6422
+ // Shorter wins over longer regardless of alphabetical order.
6423
+ assertEqual(selectDisplayAlias(['png-image', 'png', 'image-png']), 'png', 'shortest wins');
6424
+ // Equal length → alphabetical (a09 < a16).
6425
+ assertEqual(selectDisplayAlias(['a16', 'a09', 'a12']), 'a09', 'tie → alphabetical');
6426
+ // Single candidate returns itself.
6427
+ assertEqual(selectDisplayAlias(['solo']), 'solo', 'single candidate');
6428
+ // Empty set → null.
6429
+ assertEqual(selectDisplayAlias([]), null, 'empty → null');
6430
+ }
6431
+
6432
+ // TEST1895: displayAliasForUrn reverse-resolves a URN to its display alias.
6433
+ // Proves: (1) the shortest-then-alphabetical winner among multiple aliases on
6434
+ // the same target, (2) a NON-canonical query URN (different tag order) still
6435
+ // resolves because the query is canonicalised before matching, (3) a URN with
6436
+ // no alias returns null, (4) a non-URN string returns null.
6437
+ function test1895_displayAliasForUrn() {
6438
+ const registry = new FabricRegistryClient();
6439
+ const capTarget = CapUrn.fromString('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"').toString();
6440
+ // Two aliases on the same cap target; "i2s" is shorter than "int2str".
6441
+ registry.insertCachedAliasForTest(new StoredAlias('int2str', capTarget, 1));
6442
+ registry.insertCachedAliasForTest(new StoredAlias('i2s', capTarget, 1));
6443
+ // A media alias too. Stored canonically.
6444
+ const jsonTarget = MediaUrn.fromString('media:fmt=json;record').toString();
6445
+ registry.insertCachedAliasForTest(new StoredAlias('json', jsonTarget, 1));
6446
+
6447
+ // Canonical query → shortest alias wins.
6448
+ assertEqual(
6449
+ registry.displayAliasForUrn('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"'),
6450
+ 'i2s', 'shortest cap alias wins');
6451
+ // NON-canonical query (media tags reordered) must still resolve via
6452
+ // canonicalisation.
6453
+ assertEqual(registry.displayAliasForUrn('media:record;fmt=json'), 'json',
6454
+ 'non-canonical media query canonicalises');
6455
+ // A real URN with no alias → null.
6456
+ assertEqual(registry.displayAliasForUrn('media:enc=utf-8;ext=pdf'), null, 'no alias → null');
6457
+ // A non-URN (no cap:/media: prefix) → null, never a throw.
6458
+ assertEqual(registry.displayAliasForUrn('int2str'), null, 'non-URN → null');
6459
+ }
6460
+
6461
+ // TEST1896: cachedCapAliases returns only CAP-targeted aliases as [name,
6462
+ // target] pairs — media aliases are excluded. Drives the notation editor's
6463
+ // registered-alias completions.
6464
+ function test1896_cachedCapAliasesFiltersToCapTargets() {
6465
+ const registry = new FabricRegistryClient();
6466
+ const capTarget = CapUrn.fromString('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"').toString();
6467
+ registry.insertCachedAliasForTest(new StoredAlias('int2str', capTarget, 1));
6468
+ registry.insertCachedAliasForTest(new StoredAlias('json', MediaUrn.fromString('media:fmt=json;record').toString(), 1));
6469
+ const capAliases = registry.cachedCapAliases();
6470
+ assertEqual(capAliases.length, 1, 'only the cap alias is returned');
6471
+ assertEqual(capAliases[0][0], 'int2str', 'pair name');
6472
+ assertEqual(capAliases[0][1], capTarget, 'pair target');
6473
+ }
6474
+
6475
+ // TEST1196: toMachineNotationAliased references an aliased cap DIRECTLY in the
6476
+ // wiring by its display alias (shortest, then alphabetical) with NO header, and
6477
+ // keeps the synthetic `edge_N` token + header for a cap that has no alias.
6478
+ function test1196_aliasedSerializationUsesAliasAndDropsHeader() {
6479
+ const registry = new FabricRegistryClient();
6480
+ const m = Machine.fromString(
6481
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
6482
+ '[embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"]' +
6483
+ '[doc -> extract -> text]' +
6484
+ '[text -> embed -> vectors]'
6485
+ );
6486
+ // Canonical target strings for the two caps, exactly as the serializer keys.
6487
+ const extractTarget = m.edges().find(e => e.capUrn.toString().includes('extract')).capUrn.toString();
6488
+ // Two aliases on the extract cap; "ex" is shorter than "extract-pdf".
6489
+ registry.insertCachedAliasForTest(new StoredAlias('extract-pdf', extractTarget, 1));
6490
+ registry.insertCachedAliasForTest(new StoredAlias('ex', extractTarget, 1));
6491
+ // No alias for the embed cap → it must stay a raw URN with a header.
6492
+
6493
+ const aliased = m.toMachineNotationAliased(registry, 'bracketed');
6494
+
6495
+ // The extract cap is aliased: referenced directly in the wiring by its
6496
+ // SHORTER alias `ex`, with NO header, and its URN must not appear.
6497
+ assert(aliased.includes('-> ex ->'),
6498
+ `extract cap must be referenced by shortest alias 'ex', got: ${aliased}`);
6499
+ assert(!aliased.includes('extract;out'),
6500
+ `the aliased extract cap URN must not appear, got: ${aliased}`);
6501
+ // The longer alias must not be chosen.
6502
+ assert(!aliased.includes('extract-pdf'),
6503
+ `the longer alias must not be used, got: ${aliased}`);
6504
+ // The un-aliased embed cap keeps its synthetic header binding `edge_N` to the
6505
+ // canonical embed URN.
6506
+ assert(/\[edge_\d+ cap:.*embed/.test(aliased),
6507
+ `the un-aliased embed cap must keep its header URN, got: ${aliased}`);
6508
+
6509
+ // An un-aliased machine (empty registry) is byte-identical to the canonical
6510
+ // formatted form — the aliased path adds nothing when no alias exists.
6511
+ const empty = new FabricRegistryClient();
6512
+ assertEqual(
6513
+ m.toMachineNotationAliased(empty, 'bracketed'),
6514
+ m.toMachineNotationFormatted('bracketed'),
6515
+ 'no aliases → identical to canonical bracketed form');
6516
+ }
6517
+
6386
6518
  // ============================================================================
6387
6519
  // Test runner
6388
6520
  // ============================================================================
@@ -6647,7 +6779,7 @@ async function runTests() {
6647
6779
  runTest('MACHINE:fan_out', test6290_Machine_fanOut);
6648
6780
  runTest('MACHINE:fan_in_secondary_assigned_by_prior_wiring', test6292_Machine_fanInSecondaryAssignedByPriorWiring);
6649
6781
  runTest('MACHINE:fan_in_secondary_unassigned_gets_wildcard', test6294_Machine_fanInSecondaryUnassignedGetsWildcard);
6650
- runTest('MACHINE:loop_edge', test6306_Machine_loopEdge);
6782
+ runTest('MACHINE:loop_keyword_is_not_grammar', test6306_Machine_loopKeywordIsNotGrammar);
6651
6783
  runTest('MACHINE:undefined_alias_fails', test6308_Machine_undefinedAliasFails);
6652
6784
  runTest('MACHINE:node_alias_collision', test6310_Machine_nodeAliasCollision);
6653
6785
  runTest('MACHINE:conflicting_media_types_fail', test6312_Machine_conflictingMediaTypesFail);
@@ -6660,7 +6792,7 @@ async function runTests() {
6660
6792
  console.log('\n--- machine/parser.rs (line-based) ---');
6661
6793
  runTest('MACHINE:line_based_simple_chain', test6327_Machine_lineBasedSimpleChain);
6662
6794
  runTest('MACHINE:line_based_two_step_chain', test6331_Machine_lineBasedTwoStepChain);
6663
- runTest('MACHINE:line_based_loop', test6334_Machine_lineBasedLoop);
6795
+ runTest('MACHINE:line_based_loop_keyword_is_not_grammar', test6334_Machine_lineBasedLoopKeywordIsNotGrammar);
6664
6796
  runTest('MACHINE:line_based_fan_in', test6337_Machine_lineBasedFanIn);
6665
6797
  runTest('MACHINE:mixed_bracketed_and_line_based', test6341_Machine_mixedBracketedAndLineBased);
6666
6798
  runTest('MACHINE:line_based_equivalent_to_bracketed', test6345_Machine_lineBasedEquivalentToBracketed);
@@ -6695,7 +6827,7 @@ async function runTests() {
6695
6827
  runTest('MACHINE:roundtrip_single_edge', test6410_Machine_roundtripSingleEdge);
6696
6828
  runTest('MACHINE:roundtrip_two_edge_chain', test6413_Machine_roundtripTwoEdgeChain);
6697
6829
  runTest('MACHINE:roundtrip_fan_out', test6415_Machine_roundtripFanOut);
6698
- runTest('MACHINE:roundtrip_loop_edge', test6417_Machine_roundtripLoopEdge);
6830
+ runTest('MACHINE:loop_edge_serializes_without_loop_text', test6417_Machine_loopEdgeSerializesWithoutLoopText);
6699
6831
  runTest('MACHINE:serialization_is_deterministic', test6419_Machine_serializationIsDeterministic);
6700
6832
  runTest('MACHINE:reordered_edges_produce_same_notation', test6421_Machine_reorderedEdgesProduceSameNotation);
6701
6833
  runTest('MACHINE:multiline_serialize_format', test6429_Machine_multilineSerializeFormat);
@@ -6849,6 +6981,12 @@ async function runTests() {
6849
6981
  runTest('TEST1882: classify_alias_target_by_prefix', test1882_classifyAliasTargetByPrefix);
6850
6982
  runTest('TEST1887: manifest_serde_round_trips_aliases', test1887_manifestSerdeRoundTripsAliases);
6851
6983
 
6984
+ console.log('\n--- URN→alias display + aliased serialization (test1894–test1896, test1196) ---');
6985
+ runTest('TEST1894: select_display_alias_ordering', test1894_selectDisplayAliasOrdering);
6986
+ runTest('TEST1895: display_alias_for_urn', test1895_displayAliasForUrn);
6987
+ runTest('TEST1896: cached_cap_aliases_filters_to_cap', test1896_cachedCapAliasesFiltersToCapTargets);
6988
+ runTest('TEST1196: aliased_serialization_uses_alias', test1196_aliasedSerializationUsesAliasAndDropsHeader);
6989
+
6852
6990
  // Summary
6853
6991
  console.log(`\n${passCount + failCount} tests: ${passCount} passed, ${failCount} failed`);
6854
6992
  if (failCount > 0) {
package/machine-parser.js CHANGED
@@ -170,14 +170,13 @@ function peg$parse(input, options) {
170
170
  const peg$c2 = "(";
171
171
  const peg$c3 = ",";
172
172
  const peg$c4 = ")";
173
- const peg$c5 = "LOOP";
174
- const peg$c6 = "-";
175
- const peg$c7 = ">";
176
- const peg$c8 = "cap:";
177
- const peg$c9 = "\r\n";
178
- const peg$c10 = "\"";
179
- const peg$c11 = "\\\"";
180
- const peg$c12 = "\\\\";
173
+ const peg$c5 = "-";
174
+ const peg$c6 = ">";
175
+ const peg$c7 = "cap:";
176
+ const peg$c8 = "\r\n";
177
+ const peg$c9 = "\"";
178
+ const peg$c10 = "\\\"";
179
+ const peg$c11 = "\\\\";
181
180
 
182
181
  const peg$r0 = /^[a-zA-Z_]/;
183
182
  const peg$r1 = /^[a-zA-Z0-9_\-]/;
@@ -189,21 +188,20 @@ function peg$parse(input, options) {
189
188
  const peg$e2 = peg$literalExpectation("(", false);
190
189
  const peg$e3 = peg$literalExpectation(",", false);
191
190
  const peg$e4 = peg$literalExpectation(")", false);
192
- const peg$e5 = peg$literalExpectation("LOOP", false);
193
- const peg$e6 = peg$literalExpectation("-", false);
194
- const peg$e7 = peg$literalExpectation(">", false);
195
- const peg$e8 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false, false);
196
- const peg$e9 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false, false);
197
- const peg$e10 = peg$literalExpectation("cap:", false);
198
- const peg$e11 = peg$anyExpectation();
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");
191
+ const peg$e5 = peg$literalExpectation("-", false);
192
+ const peg$e6 = peg$literalExpectation(">", false);
193
+ const peg$e7 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false, false);
194
+ const peg$e8 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false, false);
195
+ const peg$e9 = peg$literalExpectation("cap:", false);
196
+ const peg$e10 = peg$anyExpectation();
197
+ const peg$e11 = peg$otherExpectation("newline");
198
+ const peg$e12 = peg$literalExpectation("\r\n", false);
199
+ const peg$e13 = peg$classExpectation(["\n", "\r"], false, false, false);
200
+ const peg$e14 = peg$literalExpectation("\"", false);
201
+ const peg$e15 = peg$literalExpectation("\\\"", false);
202
+ const peg$e16 = peg$literalExpectation("\\\\", false);
203
+ const peg$e17 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false, false);
204
+ const peg$e18 = peg$otherExpectation("required whitespace");
207
205
 
208
206
  function peg$f0(stmts) { return stmts; }
209
207
  function peg$f1(inner) { return inner; }
@@ -211,18 +209,16 @@ function peg$parse(input, options) {
211
209
  function peg$f3(a, c) {
212
210
  return { type: 'header', alias: a.value, capUrn: c.value, location: location(), aliasLocation: a.location, capUrnLocation: c.location };
213
211
  }
214
- function peg$f4(s, lc, t) {
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 };
212
+ function peg$f4(s, c, t) {
213
+ return { type: 'wiring', sources: s.values, capAlias: c.value, target: t.value, location: location(), sourceLocations: s.locations, capAliasLocation: c.location, targetLocation: t.location };
216
214
  }
217
215
  function peg$f5(a) { return { values: [a.value], locations: [a.location] }; }
218
216
  function peg$f6(first, a) { return a; }
219
217
  function peg$f7(first, rest) {
220
218
  return { values: [first.value, ...rest.map(r => r.value)], locations: [first.location, ...rest.map(r => r.location)] };
221
219
  }
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
+ function peg$f8(a) { return { value: a, location: location() }; }
221
+ function peg$f9(c) { return { value: c, location: location() }; }
226
222
  let peg$currPos = options.peg$currPos | 0;
227
223
  let peg$savedPos = peg$currPos;
228
224
  const peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -515,7 +511,7 @@ function peg$parse(input, options) {
515
511
  s3 = peg$parsearrow();
516
512
  if (s3 !== peg$FAILED) {
517
513
  s4 = peg$parse_();
518
- s5 = peg$parseloop_cap_loc();
514
+ s5 = peg$parsealias_loc();
519
515
  if (s5 !== peg$FAILED) {
520
516
  s6 = peg$parse_();
521
517
  s7 = peg$parsearrow();
@@ -673,70 +669,27 @@ function peg$parse(input, options) {
673
669
  return s0;
674
670
  }
675
671
 
676
- function peg$parseloop_cap_loc() {
677
- let s0, s1, s2, s3;
678
-
679
- s0 = peg$currPos;
680
- if (input.substr(peg$currPos, 4) === peg$c5) {
681
- s1 = peg$c5;
682
- peg$currPos += 4;
683
- } else {
684
- s1 = peg$FAILED;
685
- if (peg$silentFails === 0) { peg$fail(peg$e5); }
686
- }
687
- if (s1 !== peg$FAILED) {
688
- s2 = peg$parse__();
689
- if (s2 !== peg$FAILED) {
690
- s3 = peg$parsealias_loc();
691
- if (s3 !== peg$FAILED) {
692
- peg$savedPos = s0;
693
- s0 = peg$f8(s3);
694
- } else {
695
- peg$currPos = s0;
696
- s0 = peg$FAILED;
697
- }
698
- } else {
699
- peg$currPos = s0;
700
- s0 = peg$FAILED;
701
- }
702
- } else {
703
- peg$currPos = s0;
704
- s0 = peg$FAILED;
705
- }
706
- if (s0 === peg$FAILED) {
707
- s0 = peg$currPos;
708
- s1 = peg$parsealias_loc();
709
- if (s1 !== peg$FAILED) {
710
- peg$savedPos = s0;
711
- s1 = peg$f9(s1);
712
- }
713
- s0 = s1;
714
- }
715
-
716
- return s0;
717
- }
718
-
719
672
  function peg$parsearrow() {
720
673
  let s0, s1, s2;
721
674
 
722
675
  s0 = peg$currPos;
723
676
  s1 = [];
724
677
  if (input.charCodeAt(peg$currPos) === 45) {
725
- s2 = peg$c6;
678
+ s2 = peg$c5;
726
679
  peg$currPos++;
727
680
  } else {
728
681
  s2 = peg$FAILED;
729
- if (peg$silentFails === 0) { peg$fail(peg$e6); }
682
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
730
683
  }
731
684
  if (s2 !== peg$FAILED) {
732
685
  while (s2 !== peg$FAILED) {
733
686
  s1.push(s2);
734
687
  if (input.charCodeAt(peg$currPos) === 45) {
735
- s2 = peg$c6;
688
+ s2 = peg$c5;
736
689
  peg$currPos++;
737
690
  } else {
738
691
  s2 = peg$FAILED;
739
- if (peg$silentFails === 0) { peg$fail(peg$e6); }
692
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
740
693
  }
741
694
  }
742
695
  } else {
@@ -744,11 +697,11 @@ function peg$parse(input, options) {
744
697
  }
745
698
  if (s1 !== peg$FAILED) {
746
699
  if (input.charCodeAt(peg$currPos) === 62) {
747
- s2 = peg$c7;
700
+ s2 = peg$c6;
748
701
  peg$currPos++;
749
702
  } else {
750
703
  s2 = peg$FAILED;
751
- if (peg$silentFails === 0) { peg$fail(peg$e7); }
704
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
752
705
  }
753
706
  if (s2 !== peg$FAILED) {
754
707
  s1 = [s1, s2];
@@ -772,7 +725,7 @@ function peg$parse(input, options) {
772
725
  s1 = peg$parsealias();
773
726
  if (s1 !== peg$FAILED) {
774
727
  peg$savedPos = s0;
775
- s1 = peg$f10(s1);
728
+ s1 = peg$f8(s1);
776
729
  }
777
730
  s0 = s1;
778
731
 
@@ -789,7 +742,7 @@ function peg$parse(input, options) {
789
742
  peg$currPos++;
790
743
  } else {
791
744
  s2 = peg$FAILED;
792
- if (peg$silentFails === 0) { peg$fail(peg$e8); }
745
+ if (peg$silentFails === 0) { peg$fail(peg$e7); }
793
746
  }
794
747
  if (s2 !== peg$FAILED) {
795
748
  s3 = [];
@@ -798,7 +751,7 @@ function peg$parse(input, options) {
798
751
  peg$currPos++;
799
752
  } else {
800
753
  s4 = peg$FAILED;
801
- if (peg$silentFails === 0) { peg$fail(peg$e9); }
754
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
802
755
  }
803
756
  while (s4 !== peg$FAILED) {
804
757
  s3.push(s4);
@@ -807,7 +760,7 @@ function peg$parse(input, options) {
807
760
  peg$currPos++;
808
761
  } else {
809
762
  s4 = peg$FAILED;
810
- if (peg$silentFails === 0) { peg$fail(peg$e9); }
763
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
811
764
  }
812
765
  }
813
766
  s2 = [s2, s3];
@@ -832,7 +785,7 @@ function peg$parse(input, options) {
832
785
  s1 = peg$parsecap_urn();
833
786
  if (s1 !== peg$FAILED) {
834
787
  peg$savedPos = s0;
835
- s1 = peg$f11(s1);
788
+ s1 = peg$f9(s1);
836
789
  }
837
790
  s0 = s1;
838
791
 
@@ -844,12 +797,12 @@ function peg$parse(input, options) {
844
797
 
845
798
  s0 = peg$currPos;
846
799
  s1 = peg$currPos;
847
- if (input.substr(peg$currPos, 4) === peg$c8) {
848
- s2 = peg$c8;
800
+ if (input.substr(peg$currPos, 4) === peg$c7) {
801
+ s2 = peg$c7;
849
802
  peg$currPos += 4;
850
803
  } else {
851
804
  s2 = peg$FAILED;
852
- if (peg$silentFails === 0) { peg$fail(peg$e10); }
805
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
853
806
  }
854
807
  if (s2 !== peg$FAILED) {
855
808
  s3 = [];
@@ -912,7 +865,7 @@ function peg$parse(input, options) {
912
865
  peg$currPos++;
913
866
  } else {
914
867
  s3 = peg$FAILED;
915
- if (peg$silentFails === 0) { peg$fail(peg$e11); }
868
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
916
869
  }
917
870
  if (s3 !== peg$FAILED) {
918
871
  s1 = [s1, s2, s3];
@@ -938,12 +891,12 @@ function peg$parse(input, options) {
938
891
  let s0, s1;
939
892
 
940
893
  peg$silentFails++;
941
- if (input.substr(peg$currPos, 2) === peg$c9) {
942
- s0 = peg$c9;
894
+ if (input.substr(peg$currPos, 2) === peg$c8) {
895
+ s0 = peg$c8;
943
896
  peg$currPos += 2;
944
897
  } else {
945
898
  s0 = peg$FAILED;
946
- if (peg$silentFails === 0) { peg$fail(peg$e13); }
899
+ if (peg$silentFails === 0) { peg$fail(peg$e12); }
947
900
  }
948
901
  if (s0 === peg$FAILED) {
949
902
  s0 = input.charAt(peg$currPos);
@@ -951,13 +904,13 @@ function peg$parse(input, options) {
951
904
  peg$currPos++;
952
905
  } else {
953
906
  s0 = peg$FAILED;
954
- if (peg$silentFails === 0) { peg$fail(peg$e14); }
907
+ if (peg$silentFails === 0) { peg$fail(peg$e13); }
955
908
  }
956
909
  }
957
910
  peg$silentFails--;
958
911
  if (s0 === peg$FAILED) {
959
912
  s1 = peg$FAILED;
960
- if (peg$silentFails === 0) { peg$fail(peg$e12); }
913
+ if (peg$silentFails === 0) { peg$fail(peg$e11); }
961
914
  }
962
915
 
963
916
  return s0;
@@ -968,39 +921,39 @@ function peg$parse(input, options) {
968
921
 
969
922
  s0 = peg$currPos;
970
923
  if (input.charCodeAt(peg$currPos) === 34) {
971
- s1 = peg$c10;
924
+ s1 = peg$c9;
972
925
  peg$currPos++;
973
926
  } else {
974
927
  s1 = peg$FAILED;
975
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
928
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
976
929
  }
977
930
  if (s1 !== peg$FAILED) {
978
931
  s2 = [];
979
- if (input.substr(peg$currPos, 2) === peg$c11) {
980
- s3 = peg$c11;
932
+ if (input.substr(peg$currPos, 2) === peg$c10) {
933
+ s3 = peg$c10;
981
934
  peg$currPos += 2;
982
935
  } else {
983
936
  s3 = peg$FAILED;
984
- if (peg$silentFails === 0) { peg$fail(peg$e16); }
937
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
985
938
  }
986
939
  if (s3 === peg$FAILED) {
987
- if (input.substr(peg$currPos, 2) === peg$c12) {
988
- s3 = peg$c12;
940
+ if (input.substr(peg$currPos, 2) === peg$c11) {
941
+ s3 = peg$c11;
989
942
  peg$currPos += 2;
990
943
  } else {
991
944
  s3 = peg$FAILED;
992
- if (peg$silentFails === 0) { peg$fail(peg$e17); }
945
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
993
946
  }
994
947
  if (s3 === peg$FAILED) {
995
948
  s3 = peg$currPos;
996
949
  s4 = peg$currPos;
997
950
  peg$silentFails++;
998
951
  if (input.charCodeAt(peg$currPos) === 34) {
999
- s5 = peg$c10;
952
+ s5 = peg$c9;
1000
953
  peg$currPos++;
1001
954
  } else {
1002
955
  s5 = peg$FAILED;
1003
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
956
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
1004
957
  }
1005
958
  peg$silentFails--;
1006
959
  if (s5 === peg$FAILED) {
@@ -1015,7 +968,7 @@ function peg$parse(input, options) {
1015
968
  peg$currPos++;
1016
969
  } else {
1017
970
  s5 = peg$FAILED;
1018
- if (peg$silentFails === 0) { peg$fail(peg$e11); }
971
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
1019
972
  }
1020
973
  if (s5 !== peg$FAILED) {
1021
974
  s4 = [s4, s5];
@@ -1032,31 +985,31 @@ function peg$parse(input, options) {
1032
985
  }
1033
986
  while (s3 !== peg$FAILED) {
1034
987
  s2.push(s3);
1035
- if (input.substr(peg$currPos, 2) === peg$c11) {
1036
- s3 = peg$c11;
988
+ if (input.substr(peg$currPos, 2) === peg$c10) {
989
+ s3 = peg$c10;
1037
990
  peg$currPos += 2;
1038
991
  } else {
1039
992
  s3 = peg$FAILED;
1040
- if (peg$silentFails === 0) { peg$fail(peg$e16); }
993
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
1041
994
  }
1042
995
  if (s3 === peg$FAILED) {
1043
- if (input.substr(peg$currPos, 2) === peg$c12) {
1044
- s3 = peg$c12;
996
+ if (input.substr(peg$currPos, 2) === peg$c11) {
997
+ s3 = peg$c11;
1045
998
  peg$currPos += 2;
1046
999
  } else {
1047
1000
  s3 = peg$FAILED;
1048
- if (peg$silentFails === 0) { peg$fail(peg$e17); }
1001
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
1049
1002
  }
1050
1003
  if (s3 === peg$FAILED) {
1051
1004
  s3 = peg$currPos;
1052
1005
  s4 = peg$currPos;
1053
1006
  peg$silentFails++;
1054
1007
  if (input.charCodeAt(peg$currPos) === 34) {
1055
- s5 = peg$c10;
1008
+ s5 = peg$c9;
1056
1009
  peg$currPos++;
1057
1010
  } else {
1058
1011
  s5 = peg$FAILED;
1059
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1012
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
1060
1013
  }
1061
1014
  peg$silentFails--;
1062
1015
  if (s5 === peg$FAILED) {
@@ -1071,7 +1024,7 @@ function peg$parse(input, options) {
1071
1024
  peg$currPos++;
1072
1025
  } else {
1073
1026
  s5 = peg$FAILED;
1074
- if (peg$silentFails === 0) { peg$fail(peg$e11); }
1027
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
1075
1028
  }
1076
1029
  if (s5 !== peg$FAILED) {
1077
1030
  s4 = [s4, s5];
@@ -1088,11 +1041,11 @@ function peg$parse(input, options) {
1088
1041
  }
1089
1042
  }
1090
1043
  if (input.charCodeAt(peg$currPos) === 34) {
1091
- s3 = peg$c10;
1044
+ s3 = peg$c9;
1092
1045
  peg$currPos++;
1093
1046
  } else {
1094
1047
  s3 = peg$FAILED;
1095
- if (peg$silentFails === 0) { peg$fail(peg$e15); }
1048
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
1096
1049
  }
1097
1050
  if (s3 !== peg$FAILED) {
1098
1051
  s1 = [s1, s2, s3];
@@ -1119,7 +1072,7 @@ function peg$parse(input, options) {
1119
1072
  peg$currPos++;
1120
1073
  } else {
1121
1074
  s1 = peg$FAILED;
1122
- if (peg$silentFails === 0) { peg$fail(peg$e18); }
1075
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
1123
1076
  }
1124
1077
  while (s1 !== peg$FAILED) {
1125
1078
  s0.push(s1);
@@ -1128,7 +1081,7 @@ function peg$parse(input, options) {
1128
1081
  peg$currPos++;
1129
1082
  } else {
1130
1083
  s1 = peg$FAILED;
1131
- if (peg$silentFails === 0) { peg$fail(peg$e18); }
1084
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
1132
1085
  }
1133
1086
  }
1134
1087
  peg$silentFails--;
@@ -1146,7 +1099,7 @@ function peg$parse(input, options) {
1146
1099
  peg$currPos++;
1147
1100
  } else {
1148
1101
  s1 = peg$FAILED;
1149
- if (peg$silentFails === 0) { peg$fail(peg$e18); }
1102
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
1150
1103
  }
1151
1104
  if (s1 !== peg$FAILED) {
1152
1105
  while (s1 !== peg$FAILED) {
@@ -1156,7 +1109,7 @@ function peg$parse(input, options) {
1156
1109
  peg$currPos++;
1157
1110
  } else {
1158
1111
  s1 = peg$FAILED;
1159
- if (peg$silentFails === 0) { peg$fail(peg$e18); }
1112
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
1160
1113
  }
1161
1114
  }
1162
1115
  } else {
@@ -1165,7 +1118,7 @@ function peg$parse(input, options) {
1165
1118
  peg$silentFails--;
1166
1119
  if (s0 === peg$FAILED) {
1167
1120
  s1 = peg$FAILED;
1168
- if (peg$silentFails === 0) { peg$fail(peg$e19); }
1121
+ if (peg$silentFails === 0) { peg$fail(peg$e18); }
1169
1122
  }
1170
1123
 
1171
1124
  return s0;
package/machine.pegjs CHANGED
@@ -13,7 +13,6 @@
13
13
  // extract cap:in="media:ext=pdf";extract;out="media:txt;textable"
14
14
  // doc -> extract -> text
15
15
  // (thumbnail, model_spec) -> describe -> description
16
- // pages -> LOOP p2t -> texts
17
16
  //
18
17
  // Both forms can be freely mixed in the same program.
19
18
 
@@ -29,9 +28,12 @@ header = a:alias_loc __ c:cap_urn_loc {
29
28
  return { type: 'header', alias: a.value, capUrn: c.value, location: location(), aliasLocation: a.location, capUrnLocation: c.location };
30
29
  }
31
30
 
32
- // Wiring: source -> loop_cap -> target
33
- wiring = s:source_loc _ arrow _ lc:loop_cap_loc _ arrow _ t:alias_loc {
34
- 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 };
31
+ // Wiring: source -> cap -> target
32
+ //
33
+ // The cap position is a plain alias. There is no LOOP marker: per-item map
34
+ // (`is_loop`) is a derived cardinality property, never authored syntax.
35
+ wiring = s:source_loc _ arrow _ c:alias_loc _ arrow _ t:alias_loc {
36
+ return { type: 'wiring', sources: s.values, capAlias: c.value, target: t.value, location: location(), sourceLocations: s.locations, capAliasLocation: c.location, targetLocation: t.location };
35
37
  }
36
38
 
37
39
  source_loc = group_loc / single_alias_loc
@@ -42,9 +44,6 @@ group_loc = "(" _ first:alias_loc rest:("," _ a:alias_loc { return a; })+ _ ")"
42
44
  return { values: [first.value, ...rest.map(r => r.value)], locations: [first.location, ...rest.map(r => r.location)] };
43
45
  }
44
46
 
45
- loop_cap_loc = "LOOP" __ a:alias_loc { return { alias: a.value, isLoop: true, location: a.location }; }
46
- / a:alias_loc { return { alias: a.value, isLoop: false, location: a.location }; }
47
-
48
47
  arrow = "-"+ ">"
49
48
 
50
49
  // Alias with location tracking
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": "1.219.594"
43
+ "version": "1.226.626"
44
44
  }