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.
- package/cap-fab-renderer.js +100 -1
- package/capdag.js +226 -19
- package/capdag.test.js +167 -29
- package/machine-parser.js +73 -120
- package/machine.pegjs +6 -7
- package/package.json +1 -1
package/cap-fab-renderer.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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]} -> ${
|
|
6713
|
+
output += `[${sources[0]} -> ${alias} -> ${targetName}]`;
|
|
6708
6714
|
} else {
|
|
6709
6715
|
const group = sources.join(', ');
|
|
6710
|
-
output += `[(${group}) -> ${
|
|
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
|
-
|
|
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]} -> ${
|
|
6766
|
+
lines.push(`[${sources[0]} -> ${alias} -> ${targetName}]`);
|
|
6761
6767
|
} else {
|
|
6762
6768
|
const group = sources.join(', ');
|
|
6763
|
-
lines.push(`[(${group}) -> ${
|
|
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
|
-
|
|
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]} -> ${
|
|
6907
|
+
lines.push(`${open}${sources[0]} -> ${token} -> ${targetName}${close}`);
|
|
6819
6908
|
} else {
|
|
6820
6909
|
const group = sources.join(', ');
|
|
6821
|
-
lines.push(`${open}(${group}) -> ${
|
|
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
|
-
* -
|
|
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
|
|
6965
|
-
|
|
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
|
-
|
|
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:
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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:
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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:
|
|
3838
|
-
|
|
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
|
-
|
|
3848
|
-
assertEqual(reparsed.edges()[0].isLoop,
|
|
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:
|
|
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
|
|
4208
|
-
'
|
|
4209
|
-
'
|
|
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'), '
|
|
4213
|
-
assert(mermaid.includes('-.'), 'Should use dotted line for
|
|
4214
|
-
assert(mermaid.includes('.->'), 'Should use dotted arrow for
|
|
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:
|
|
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:
|
|
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:
|
|
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 = "
|
|
174
|
-
const peg$c6 = "
|
|
175
|
-
const peg$c7 = "
|
|
176
|
-
const peg$c8 = "
|
|
177
|
-
const peg$c9 = "\
|
|
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("
|
|
193
|
-
const peg$e6 = peg$literalExpectation("
|
|
194
|
-
const peg$e7 = peg$
|
|
195
|
-
const peg$e8 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false, false);
|
|
196
|
-
const peg$e9 = peg$
|
|
197
|
-
const peg$e10 = peg$
|
|
198
|
-
const peg$e11 = peg$
|
|
199
|
-
const peg$e12 = peg$
|
|
200
|
-
const peg$e13 = peg$
|
|
201
|
-
const peg$e14 = peg$
|
|
202
|
-
const peg$e15 = peg$literalExpectation("
|
|
203
|
-
const peg$e16 = peg$literalExpectation("
|
|
204
|
-
const peg$e17 = peg$
|
|
205
|
-
const peg$e18 = peg$
|
|
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,
|
|
215
|
-
return { type: 'wiring', sources: s.values, capAlias:
|
|
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 {
|
|
223
|
-
function peg$f9(
|
|
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$
|
|
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$
|
|
678
|
+
s2 = peg$c5;
|
|
726
679
|
peg$currPos++;
|
|
727
680
|
} else {
|
|
728
681
|
s2 = peg$FAILED;
|
|
729
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
688
|
+
s2 = peg$c5;
|
|
736
689
|
peg$currPos++;
|
|
737
690
|
} else {
|
|
738
691
|
s2 = peg$FAILED;
|
|
739
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
700
|
+
s2 = peg$c6;
|
|
748
701
|
peg$currPos++;
|
|
749
702
|
} else {
|
|
750
703
|
s2 = peg$FAILED;
|
|
751
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
848
|
-
s2 = peg$
|
|
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$
|
|
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$
|
|
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$
|
|
942
|
-
s0 = peg$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
924
|
+
s1 = peg$c9;
|
|
972
925
|
peg$currPos++;
|
|
973
926
|
} else {
|
|
974
927
|
s1 = peg$FAILED;
|
|
975
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
980
|
-
s3 = peg$
|
|
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$
|
|
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$
|
|
988
|
-
s3 = peg$
|
|
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$
|
|
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$
|
|
952
|
+
s5 = peg$c9;
|
|
1000
953
|
peg$currPos++;
|
|
1001
954
|
} else {
|
|
1002
955
|
s5 = peg$FAILED;
|
|
1003
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
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$
|
|
1036
|
-
s3 = peg$
|
|
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$
|
|
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$
|
|
1044
|
-
s3 = peg$
|
|
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$
|
|
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$
|
|
1008
|
+
s5 = peg$c9;
|
|
1056
1009
|
peg$currPos++;
|
|
1057
1010
|
} else {
|
|
1058
1011
|
s5 = peg$FAILED;
|
|
1059
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
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$
|
|
1044
|
+
s3 = peg$c9;
|
|
1092
1045
|
peg$currPos++;
|
|
1093
1046
|
} else {
|
|
1094
1047
|
s3 = peg$FAILED;
|
|
1095
|
-
if (peg$silentFails === 0) { peg$fail(peg$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 ->
|
|
33
|
-
|
|
34
|
-
|
|
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