capdag 0.166.402 → 0.169.414

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/capdag.js CHANGED
@@ -5381,13 +5381,17 @@ class Machine {
5381
5381
  // uses `edge_<idx>` unconditionally — there is no privileged tag (such
5382
5382
  // as the legacy `op=…` tag) we can derive a friendlier name from, so
5383
5383
  // we mirror the same pure-index scheme here.
5384
+ // Number aliases by position in the canonical (sorted) edge order so
5385
+ // that two graphs with the same edges in different insertion orders
5386
+ // produce identical notation. Without this the alias number tracks
5387
+ // insertion order and serialization is non-canonical.
5384
5388
  const aliases = new Map();
5385
- for (const idx of edgeOrder) {
5389
+ edgeOrder.forEach((idx, position) => {
5386
5390
  const edge = this._edges[idx];
5387
- const alias = `edge_${idx}`;
5391
+ const alias = `edge_${position}`;
5388
5392
  const capStr = edge.capUrn.toString();
5389
5393
  aliases.set(alias, { edgeIdx: idx, capStr });
5390
- }
5394
+ });
5391
5395
 
5392
5396
  // Step 3: Generate node names
5393
5397
  // Collect all unique media URNs, assign names in order of first appearance
package/capdag.test.js CHANGED
@@ -164,13 +164,14 @@ function test003_directionMatching() {
164
164
  assert(wildcardCap.accepts(request), 'Wildcard direction should match any');
165
165
  }
166
166
 
167
- // TEST004: Test that unquoted keys and values are normalized to lowercase
167
+ // TEST004: Test that unquoted keys and values are normalized to lowercase.
168
+ // Key lookup is case-insensitive: uppercase variants of `ext` resolve
169
+ // to the same keyed tag.
168
170
  function test004_unquotedValuesLowercased() {
169
171
  const cap = CapUrn.fromString('cap:ext=pdf;generate;in=media:void;out="media:record;textable"');
170
172
  assert(cap.hasMarkerTag('generate'), 'Unquoted value should be lowercased');
171
173
  assertEqual(cap.getTag('ext'), 'pdf', 'Unquoted value should be lowercased');
172
- // Key lookup is case-insensitive
173
- assertEqual(cap.getTag('OP'), 'generate', 'Key lookup should be case-insensitive');
174
+ assertEqual(cap.getTag('EXT'), 'pdf', 'Key lookup should be case-insensitive');
174
175
  }
175
176
 
176
177
  // TEST005: Test that quoted values preserve case while unquoted are lowercased
@@ -363,15 +364,16 @@ function test020_specificity() {
363
364
  'out=record=2, in=*->0, y=test marker=2 -> 20002');
364
365
  }
365
366
 
366
- // TEST021: Test builder creates cap URN with correct tags and direction specs
367
+ // TEST021: Test builder creates cap URN with marker + keyed tags and direction specs.
368
+ // `op` is no longer a special key — operation names are markers (value-less tags).
367
369
  function test021_builder() {
368
370
  const cap = new CapUrnBuilder()
369
371
  .inSpec('media:void')
370
372
  .outSpec('media:object')
371
- .tag('op', 'generate')
373
+ .marker('generate')
372
374
  .tag('ext', 'pdf')
373
375
  .build();
374
- assert(cap.hasMarkerTag('generate'), 'Builder should set op');
376
+ assert(cap.hasMarkerTag('generate'), 'Builder should set the generate marker');
375
377
  assertEqual(cap.getTag('ext'), 'pdf', 'Builder should set ext');
376
378
  assertEqual(cap.getInSpec(), 'media:void', 'Builder should set inSpec');
377
379
  assertEqual(cap.getOutSpec(), 'media:object', 'Builder should set outSpec');
@@ -2906,8 +2908,9 @@ function testMachine_lineBasedFormatSerialization() {
2906
2908
  const lineBased = g.toMachineNotationFormatted('line-based');
2907
2909
  assert(!lineBased.includes('['), 'Line-based format must not contain brackets');
2908
2910
  assert(!lineBased.includes(']'), 'Line-based format must not contain brackets');
2909
- assert(lineBased.includes('extract cap:'), 'Should contain header');
2910
- assert(lineBased.includes('-> extract ->'), 'Should contain wiring');
2911
+ // Aliases are pure-index `edge_<N>` (no privileged tag to derive a friendlier name).
2912
+ assert(lineBased.includes('edge_0 cap:'), 'Should contain header');
2913
+ assert(lineBased.includes('-> edge_0 ->'), 'Should contain wiring');
2911
2914
 
2912
2915
  // Round-trip
2913
2916
  const reparsed = Machine.fromString(lineBased);
@@ -3177,8 +3180,9 @@ function testMachine_serializeSingleEdge() {
3177
3180
  false
3178
3181
  )]);
3179
3182
  const notation = g.toMachineNotation();
3180
- assert(notation.includes('[extract '), 'Should use extract alias: ' + notation);
3181
- assert(notation.includes('-> extract ->'), 'Should have extract in wiring: ' + notation);
3183
+ // Aliases are pure-index `edge_<N>` (no privileged tag to derive a friendlier name).
3184
+ assert(notation.includes('[edge_0 '), 'Should use edge_0 alias: ' + notation);
3185
+ assert(notation.includes('-> edge_0 ->'), 'Should have edge_0 in wiring: ' + notation);
3182
3186
  assert(notation.includes('[n0 ->'), 'Should use n0 for source: ' + notation);
3183
3187
  assert(notation.includes('-> n1]'), 'Should use n1 for target: ' + notation);
3184
3188
  }
@@ -3298,6 +3302,8 @@ function testMachine_multilineSerializeFormat() {
3298
3302
  assert(g.isEquivalent(reparsed), 'Multi-line round-trip failed');
3299
3303
  }
3300
3304
 
3305
+ // Aliases are pure-index `edge_<N>` regardless of the cap's tags; there is
3306
+ // no privileged `op` tag to derive a friendlier name from.
3301
3307
  function testMachine_aliasFromOpTag() {
3302
3308
  const g = new Machine([new MachineEdge(
3303
3309
  [MediaUrn.fromString('media:pdf')],
@@ -3306,7 +3312,7 @@ function testMachine_aliasFromOpTag() {
3306
3312
  false
3307
3313
  )]);
3308
3314
  const notation = g.toMachineNotation();
3309
- assert(notation.includes('[extract '), 'Expected extract alias, got: ' + notation);
3315
+ assert(notation.includes('[edge_0 '), 'Expected edge_0 alias, got: ' + notation);
3310
3316
  }
3311
3317
 
3312
3318
  function testMachine_aliasFallbackWithoutOpTag() {
@@ -3320,6 +3326,7 @@ function testMachine_aliasFallbackWithoutOpTag() {
3320
3326
  assert(notation.includes('edge_'), 'Expected fallback alias, got: ' + notation);
3321
3327
  }
3322
3328
 
3329
+ // Pure-index aliases inherently disambiguate edges that share a marker tag.
3323
3330
  function testMachine_duplicateOpTagsDisambiguated() {
3324
3331
  const g = new Machine([
3325
3332
  new MachineEdge(
@@ -3336,8 +3343,8 @@ function testMachine_duplicateOpTagsDisambiguated() {
3336
3343
  ),
3337
3344
  ]);
3338
3345
  const notation = g.toMachineNotation();
3339
- assert(notation.includes('extract_1') || notation.includes('extract_2'),
3340
- 'Duplicate ops must be disambiguated: ' + notation);
3346
+ assert(notation.includes('edge_0') && notation.includes('edge_1'),
3347
+ 'Two edges must serialize with two distinct aliases: ' + notation);
3341
3348
  }
3342
3349
 
3343
3350
  // --- Machine builder tests ---
@@ -3575,7 +3582,10 @@ function testMachine_toMermaid_linearChain() {
3575
3582
  );
3576
3583
  const mermaid = machine.toMermaid();
3577
3584
  assert(mermaid.startsWith('flowchart LR'), 'Should start with flowchart LR');
3578
- assert(mermaid.includes('extract'), 'Should include extract label');
3585
+ // Edge labels are pure-index `edge_<N>` aliases from the canonical
3586
+ // serializer (the input alias name is not preserved in the rendered
3587
+ // diagram — it's a serialization artefact, not part of the machine).
3588
+ assert(mermaid.includes('edge_0'), 'Should include edge_0 label');
3579
3589
  assert(mermaid.includes('media:pdf'), 'Should include media:pdf node');
3580
3590
  assert(mermaid.includes('media:textable;txt'), 'Should include media:textable;txt node');
3581
3591
  assert(mermaid.includes('-->'), 'Should include arrow');
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.166.402"
43
+ "version": "0.169.414"
44
44
  }