capdag 0.178.444 → 0.181.455

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/capdag.js +363 -94
  2. package/capdag.test.js +364 -116
  3. package/package.json +2 -2
package/capdag.test.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // All implementations (Rust, Go, JS, ObjC, Python) must pass these identically.
4
4
 
5
5
  const {
6
- CapUrn, CapKind, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
6
+ CapUrn, CapKind, CapEffect, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
7
7
  MediaUrn, MediaUrnError, MediaUrnErrorCodes,
8
- Cap, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
8
+ Cap, CapGroup, CapManifest, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
9
9
  resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
10
10
  CartridgeInfo, CartridgeCapSummary, CartridgeSuggestion, CartridgeRepoClient, CartridgeRepoServer,
11
11
  CapFabEdge, CapFabStats, CapFab,
@@ -26,7 +26,8 @@ const {
26
26
  MEDIA_FILE_PATH,
27
27
  MEDIA_COLLECTION, MEDIA_COLLECTION_LIST,
28
28
  MEDIA_DECISION,
29
- MEDIA_AUDIO_SPEECH
29
+ MEDIA_AUDIO_SPEECH,
30
+ CAP_IDENTITY
30
31
  } = require('./capdag.js');
31
32
 
32
33
  // ============================================================================
@@ -309,9 +310,15 @@ function test939_capUrnCanonicalFormDropsWildcardInOut() {
309
310
  `input ${JSON.stringify(v)} canonicalized to ${JSON.stringify(parsed.toString())}, expected ${JSON.stringify(canonical)} — wildcard in/out segments must be elided so the registry SHA-256 key is stable across input spellings`
310
311
  );
311
312
  }
312
- // Bare-identity round-trip.
313
- const identity = CapUrn.fromString('cap:in=media:;out=media:');
314
- assertEqual(identity.toString(), 'cap:', 'cap with wildcard in/out and no other tags must canonicalize to bare "cap:"');
313
+ assertThrows(
314
+ () => CapUrn.fromString('cap:in=media:;out=media:'),
315
+ ErrorCodes.ILLEGAL_DECLARATION,
316
+ 'declared top-to-top cap must be rejected as inadmissible'
317
+ );
318
+
319
+ const identity = CapUrn.fromString('cap:effect=none');
320
+ assertEqual(identity.toString(), 'cap:effect=none', 'true identity must preserve explicit effect=none');
321
+ assert(identity.toString() !== generic.toString(), 'cap: and cap:effect=none must not collapse');
315
322
  }
316
323
 
317
324
  // TEST017: Test tag matching: exact match, subset match, wildcard match, value mismatch
@@ -494,17 +501,19 @@ function test027_wildcardTag() {
494
501
  assertEqual(wildcardExt.getTag('ext'), '*', 'Should set ext to wildcard');
495
502
 
496
503
  const wildcardIn = cap.withWildcardTag('in');
497
- assertEqual(wildcardIn.getInSpec(), '*', 'Should set in to wildcard');
504
+ assertEqual(wildcardIn.getInSpec(), 'media:', 'Should set in to canonical top media:');
498
505
 
499
506
  const wildcardOut = cap.withWildcardTag('out');
500
- assertEqual(wildcardOut.getOutSpec(), '*', 'Should set out to wildcard');
507
+ assertEqual(wildcardOut.getOutSpec(), 'media:', 'Should set out to canonical top media:');
501
508
  }
502
509
 
503
- // TEST028: Test empty cap URN defaults to media: wildcard
510
+ // TEST028: Test empty cap URN is illegal
504
511
  function test028_emptyCapUrnNotAllowed() {
505
- const empty = CapUrn.fromString('cap:');
506
- assertEqual(empty.getInSpec(), MEDIA_IDENTITY, 'Empty cap should default in to media:');
507
- assertEqual(empty.getOutSpec(), MEDIA_IDENTITY, 'Empty cap should default out to media:');
512
+ assertThrows(
513
+ () => CapUrn.fromString('cap:'),
514
+ ErrorCodes.ILLEGAL_DECLARATION,
515
+ 'Empty cap must be rejected as inadmissible'
516
+ );
508
517
  }
509
518
 
510
519
  // TEST029: Test minimal valid cap URN has just in and out, empty tags
@@ -683,7 +692,7 @@ function test047_matchingSemanticsThumbnailVoidInput() {
683
692
 
684
693
  // TEST048: Matching semantics - wildcard direction matches anything
685
694
  function test048_matchingSemanticsWildcardDirection() {
686
- const cap = CapUrn.fromString('cap:in=*;out=*');
695
+ const cap = CapUrn.fromString('cap:in=*;out=*;op');
687
696
  const request = CapUrn.fromString(testUrn('generate;ext=pdf'));
688
697
  assert(cap.accepts(request), 'Wildcard cap should accept any request');
689
698
  }
@@ -1209,6 +1218,152 @@ function test110_multipleExtensions() {
1209
1218
  assertEqual(resolved.extensions[1], 'jpeg', 'Second extension should be jpeg');
1210
1219
  }
1211
1220
 
1221
+ // TEST115: Test CapArg serialization and deserialization with multiple sources
1222
+ function test115_capArgSerialization() {
1223
+ const arg = new CapArg(
1224
+ MEDIA_STRING,
1225
+ true,
1226
+ [new ArgSource({ cli_flag: '--name' }), new ArgSource({ position: 0 })],
1227
+ {
1228
+ arg_description: 'The name argument',
1229
+ default_value: 400,
1230
+ metadata: { kind: 'example', flags: [true, false] }
1231
+ }
1232
+ );
1233
+
1234
+ const json = arg.toJSON();
1235
+ assertEqual(json.media_urn, MEDIA_STRING, 'media_urn must serialize');
1236
+ assertEqual(json.required, true, 'required must serialize');
1237
+ assertEqual(json.arg_description, 'The name argument', 'arg_description must serialize');
1238
+ assertEqual(json.default_value, 400, 'numeric default_value must serialize as number');
1239
+ assertEqual(JSON.stringify(json.metadata), JSON.stringify({ kind: 'example', flags: [true, false] }),
1240
+ 'metadata must serialize as arbitrary JSON');
1241
+
1242
+ const roundTripped = CapArg.fromJSON(JSON.parse(JSON.stringify(json)));
1243
+ assertEqual(roundTripped.media_urn, arg.media_urn, 'media_urn must round-trip');
1244
+ assertEqual(roundTripped.required, arg.required, 'required must round-trip');
1245
+ assertEqual(roundTripped.arg_description, arg.arg_description, 'arg_description must round-trip');
1246
+ assertEqual(roundTripped.default_value, 400, 'numeric default_value must round-trip');
1247
+ assertEqual(JSON.stringify(roundTripped.metadata), JSON.stringify({ kind: 'example', flags: [true, false] }),
1248
+ 'metadata must round-trip');
1249
+ assertEqual(roundTripped.sources.length, 2, 'sources length must round-trip');
1250
+ assertEqual(roundTripped.sources[0].cli_flag, '--name', 'cli_flag source must round-trip');
1251
+ assertEqual(roundTripped.sources[1].position, 0, 'position source must round-trip');
1252
+ }
1253
+
1254
+ // TEST116: Test CapArg constructor methods basic and with_description create args correctly
1255
+ function test116_capArgConstructors() {
1256
+ const basicArg = new CapArg(
1257
+ MEDIA_STRING,
1258
+ true,
1259
+ [new ArgSource({ cli_flag: '--name' })]
1260
+ );
1261
+ assertEqual(basicArg.media_urn, MEDIA_STRING, 'basic arg media_urn must match');
1262
+ assertEqual(basicArg.required, true, 'basic arg required must match');
1263
+ assertEqual(basicArg.sources.length, 1, 'basic arg must keep one source');
1264
+ assertEqual(basicArg.arg_description, null, 'basic arg arg_description must be absent');
1265
+ assertEqual(basicArg.default_value, null, 'basic arg default_value must be absent');
1266
+
1267
+ const describedArg = new CapArg(
1268
+ MEDIA_INTEGER,
1269
+ false,
1270
+ [new ArgSource({ position: 0 })],
1271
+ {
1272
+ arg_description: 'The count argument',
1273
+ default_value: 10
1274
+ }
1275
+ );
1276
+ assertEqual(describedArg.media_urn, MEDIA_INTEGER, 'described arg media_urn must match');
1277
+ assertEqual(describedArg.required, false, 'described arg required must match');
1278
+ assertEqual(describedArg.arg_description, 'The count argument', 'described arg description must match');
1279
+ assertEqual(describedArg.default_value, 10, 'described arg default_value must match');
1280
+ }
1281
+
1282
+ // TEST150: JSON roundtrip
1283
+ function test150_capManifestJsonSerialization() {
1284
+ const capUrn = CapUrn.fromString(testUrn('extract;target=metadata'));
1285
+ const cap = new Cap(capUrn, 'Extract Metadata', 'extract-metadata');
1286
+ cap.addArg(new CapArg('media:pdf', true, [new ArgSource({ stdin: 'media:pdf' })]));
1287
+ cap.addArg(new CapArg(
1288
+ 'media:chunk-size;textable;numeric',
1289
+ false,
1290
+ [new ArgSource({ cli_flag: '--chunk-size' })],
1291
+ {
1292
+ arg_description: 'Chunk size',
1293
+ default_value: 400,
1294
+ metadata: { unit: 'words' }
1295
+ }
1296
+ ));
1297
+ cap.addArg(new CapArg(
1298
+ 'media:timestamps;textable;bool',
1299
+ false,
1300
+ [new ArgSource({ cli_flag: '--timestamps' })],
1301
+ {
1302
+ arg_description: 'Include timestamps',
1303
+ default_value: false
1304
+ }
1305
+ ));
1306
+
1307
+ const manifest = new CapManifest(
1308
+ 'TestComponent',
1309
+ '0.1.0',
1310
+ 'release',
1311
+ null,
1312
+ 'A test component',
1313
+ [new CapGroup('default', [cap], [])]
1314
+ );
1315
+
1316
+ manifest.author = 'Test Author';
1317
+
1318
+ const json = manifest.toJSON();
1319
+ assertEqual(json.name, 'TestComponent', 'manifest name must serialize');
1320
+ assertEqual(json.author, 'Test Author', 'author must serialize');
1321
+ assert(Array.isArray(json.cap_groups), 'cap_groups must serialize');
1322
+ assertEqual(json.cap_groups.length, 1, 'cap_groups length must serialize');
1323
+ assertEqual(json.cap_groups[0].caps[0].args[1].default_value, 400, 'numeric default must serialize as number');
1324
+ assertEqual(json.cap_groups[0].caps[0].args[1].metadata.unit, 'words', 'metadata must serialize');
1325
+ assertEqual(json.cap_groups[0].caps[0].args[2].default_value, false, 'boolean default must serialize as boolean');
1326
+
1327
+ const roundTripped = CapManifest.fromJSON(JSON.parse(JSON.stringify(json)));
1328
+ const decodedCap = roundTripped.allCaps()[0];
1329
+ assertEqual(roundTripped.name, manifest.name, 'manifest name must round-trip');
1330
+ assertEqual(roundTripped.author, 'Test Author', 'author must round-trip');
1331
+ assertEqual(roundTripped.cap_groups.length, 1, 'cap_groups length must round-trip');
1332
+ assertEqual(decodedCap.args[1].default_value, 400, 'numeric default must round-trip');
1333
+ assertEqual(JSON.stringify(decodedCap.args[1].metadata), JSON.stringify({ unit: 'words' }),
1334
+ 'metadata must round-trip');
1335
+ assertEqual(decodedCap.args[2].default_value, false, 'boolean default must round-trip');
1336
+ }
1337
+
1338
+ // TEST597: CapArg::with_full_definition stores all fields including optional ones
1339
+ function test597_capArgWithFullDefinition() {
1340
+ const arg = new CapArg(
1341
+ MEDIA_STRING,
1342
+ true,
1343
+ [new ArgSource({ cli_flag: '--name' })],
1344
+ {
1345
+ arg_description: 'User name',
1346
+ default_value: { chunk_size: 400, timestamps: false },
1347
+ metadata: { hint: 'enter name' }
1348
+ }
1349
+ );
1350
+
1351
+ assertEqual(arg.media_urn, MEDIA_STRING, 'media_urn must match');
1352
+ assertEqual(arg.required, true, 'required must match');
1353
+ assertEqual(arg.arg_description, 'User name', 'arg_description must match');
1354
+ assertEqual(JSON.stringify(arg.default_value), JSON.stringify({ chunk_size: 400, timestamps: false }),
1355
+ 'object default_value must be preserved');
1356
+ assertEqual(JSON.stringify(arg.metadata), JSON.stringify({ hint: 'enter name' }),
1357
+ 'metadata must be preserved');
1358
+
1359
+ const roundTripped = CapArg.fromJSON(JSON.parse(JSON.stringify(arg.toJSON())));
1360
+ assertEqual(roundTripped.arg_description, 'User name', 'arg_description must round-trip');
1361
+ assertEqual(JSON.stringify(roundTripped.default_value), JSON.stringify({ chunk_size: 400, timestamps: false }),
1362
+ 'object default_value must round-trip');
1363
+ assertEqual(JSON.stringify(roundTripped.metadata), JSON.stringify({ hint: 'enter name' }),
1364
+ 'metadata must round-trip');
1365
+ }
1366
+
1212
1367
  // ============================================================================
1213
1368
  // cap_fab: browse-mode API used by cap-fab-renderer.js
1214
1369
  //
@@ -2396,7 +2551,7 @@ function test1302_predicateConstantConsistency() {
2396
2551
  // cap_urn.rs: TEST1303-TEST1307 (CapUrn tier tests)
2397
2552
  // ============================================================================
2398
2553
 
2399
- // TEST1303: without_tag removes tag, ignores in/out, case-insensitive for keys
2554
+ // TEST1303: without_tag removes tag, rejects structural keys, case-insensitive for keys
2400
2555
  function test1303_withoutTag() {
2401
2556
  const cap = CapUrn.fromString('cap:in="media:void";test;ext=pdf;out="media:void"');
2402
2557
  const removed = cap.withoutTag('ext');
@@ -2407,11 +2562,9 @@ function test1303_withoutTag() {
2407
2562
  const removed2 = cap.withoutTag('EXT');
2408
2563
  assertEqual(removed2.getTag('ext'), undefined, 'withoutTag should be case-insensitive');
2409
2564
 
2410
- // Removing in/out is silently ignored
2411
- const same = cap.withoutTag('in');
2412
- assertEqual(same.getInSpec(), 'media:void', 'withoutTag must not remove in');
2413
- const same2 = cap.withoutTag('out');
2414
- assertEqual(same2.getOutSpec(), 'media:void', 'withoutTag must not remove out');
2565
+ assertThrows(() => cap.withoutTag('in'), 'withoutTag must reject in');
2566
+ assertThrows(() => cap.withoutTag('out'), 'withoutTag must reject out');
2567
+ assertThrows(() => cap.withoutTag('effect'), 'withoutTag must reject effect');
2415
2568
 
2416
2569
  // Removing non-existent tag is no-op
2417
2570
  const same3 = cap.withoutTag('nonexistent');
@@ -2435,6 +2588,12 @@ function test1304_withInOutSpec() {
2435
2588
  const changedBoth = cap.withInSpec('media:pdf').withOutSpec(MEDIA_TXT);
2436
2589
  assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
2437
2590
  assertEqual(changedBoth.getOutSpec(), MEDIA_TXT, 'Chain should set outSpec');
2591
+
2592
+ const identity = CapUrn.fromString('cap:effect=none');
2593
+ assertThrows(
2594
+ () => identity.withOutSpec('media:pdf'),
2595
+ 'withOutSpec must revalidate admissibility'
2596
+ );
2438
2597
  }
2439
2598
 
2440
2599
  // TEST561: N/A for JS (in_media_urn/out_media_urn not in JS CapUrn)
@@ -2484,15 +2643,28 @@ function test1306_areCompatible() {
2484
2643
 
2485
2644
  // TEST565: N/A for JS (tags_to_string not in JS CapUrn)
2486
2645
 
2487
- // TEST1307: with_tag silently ignores in/out keys
2488
- function test1307_withTagIgnoresInOut() {
2646
+ // TEST1307: with_tag rejects structural keys
2647
+ function test1307_withTagRejectsStructuralKeys() {
2489
2648
  const cap = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
2490
- // Attempting to set in/out via withTag is silently ignored
2491
- const same = cap.withTag('in', 'media:');
2492
- assertEqual(same.getInSpec(), 'media:void', 'withTag must not change in_spec');
2649
+ assertThrows(() => cap.withTag('in', 'media:'), 'withTag must reject in');
2650
+ assertThrows(() => cap.withTag('out', 'media:'), 'withTag must reject out');
2651
+ assertThrows(() => cap.withTag('effect', 'none'), 'withTag must reject effect');
2652
+ }
2493
2653
 
2494
- const same2 = cap.withTag('out', 'media:');
2495
- assertEqual(same2.getOutSpec(), 'media:void', 'withTag must not change out_spec');
2654
+ // TEST1308: builder rejects structural keys on tag/marker
2655
+ function test1308_builderRejectsStructuralKeys() {
2656
+ assertThrows(
2657
+ () => new CapUrnBuilder().tag('in', 'media:void'),
2658
+ 'builder.tag must reject structural in'
2659
+ );
2660
+ assertThrows(
2661
+ () => new CapUrnBuilder().marker('effect'),
2662
+ 'builder.marker must reject structural effect'
2663
+ );
2664
+ assertThrows(
2665
+ () => new CapUrnBuilder().inSpec('media:void').outSpec('media:record').tag('123', 'value').build(),
2666
+ 'builder.build must reject invalid non-structural tags'
2667
+ );
2496
2668
  }
2497
2669
 
2498
2670
  // TEST1294: RULE11 - void-input cap with stdin source rejected
@@ -2551,47 +2723,58 @@ function test1297_rule11NonVoidInputWithStdin() {
2551
2723
  // cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
2552
2724
  // ============================================================================
2553
2725
 
2554
- // TEST639: cap: (empty) defaults to in=media:;out=media:
2555
- function test639_emptyCapDefaultsToMediaWildcard() {
2556
- const cap = CapUrn.fromString('cap:');
2557
- assertEqual(cap.getInSpec(), MEDIA_IDENTITY, 'Empty cap should default in to media:');
2558
- assertEqual(cap.getOutSpec(), MEDIA_IDENTITY, 'Empty cap should default out to media:');
2559
- assertEqual(Object.keys(cap.tags).length, 0, 'Empty cap should have no extra tags');
2726
+ // TEST639: cap: (empty) is the illegal bare top form
2727
+ function test639_emptyCapIsIllegal() {
2728
+ assertThrows(
2729
+ () => CapUrn.fromString('cap:'),
2730
+ ErrorCodes.ILLEGAL_DECLARATION,
2731
+ 'Empty cap must be rejected as inadmissible'
2732
+ );
2560
2733
  }
2561
2734
 
2562
- // TEST640: cap:in defaults out to media:
2563
- function test640_inOnlyDefaultsOutToMedia() {
2564
- const cap = CapUrn.fromString('cap:in');
2565
- assertEqual(cap.getInSpec(), MEDIA_IDENTITY, 'Bare in should normalize to media:');
2566
- assertEqual(cap.getOutSpec(), MEDIA_IDENTITY, 'Missing out should default to media:');
2735
+ // TEST640: cap:in collapses to the same illegal bare top form
2736
+ function test640_inOnlyIsIllegal() {
2737
+ assertThrows(
2738
+ () => CapUrn.fromString('cap:in'),
2739
+ ErrorCodes.ILLEGAL_DECLARATION,
2740
+ 'Bare in must be rejected as inadmissible'
2741
+ );
2567
2742
  }
2568
2743
 
2569
- // TEST641: cap:out defaults in to media:
2570
- function test641_outOnlyDefaultsInToMedia() {
2571
- const cap = CapUrn.fromString('cap:out');
2572
- assertEqual(cap.getInSpec(), MEDIA_IDENTITY, 'Missing in should default to media:');
2573
- assertEqual(cap.getOutSpec(), MEDIA_IDENTITY, 'Bare out should normalize to media:');
2744
+ // TEST641: cap:out collapses to the same illegal bare top form
2745
+ function test641_outOnlyIsIllegal() {
2746
+ assertThrows(
2747
+ () => CapUrn.fromString('cap:out'),
2748
+ ErrorCodes.ILLEGAL_DECLARATION,
2749
+ 'Bare out must be rejected as inadmissible'
2750
+ );
2574
2751
  }
2575
2752
 
2576
- // TEST642: cap:in;out both become media:
2577
- function test642_inOutWithoutValuesBecomeMedia() {
2578
- const cap = CapUrn.fromString('cap:in;out');
2579
- assertEqual(cap.getInSpec(), MEDIA_IDENTITY, 'Bare in should normalize to media:');
2580
- assertEqual(cap.getOutSpec(), MEDIA_IDENTITY, 'Bare out should normalize to media:');
2753
+ // TEST642: cap:in;out collapses to the same illegal bare top form
2754
+ function test642_inOutWithoutValuesAreIllegal() {
2755
+ assertThrows(
2756
+ () => CapUrn.fromString('cap:in;out'),
2757
+ ErrorCodes.ILLEGAL_DECLARATION,
2758
+ 'Bare in/out must be rejected as inadmissible'
2759
+ );
2581
2760
  }
2582
2761
 
2583
- // TEST643: cap:in=*;out=* becomes media:
2584
- function test643_explicitAsteriskIsWildcard() {
2585
- const cap = CapUrn.fromString('cap:in=*;out=*');
2586
- assertEqual(cap.getInSpec(), MEDIA_IDENTITY, 'in=* should normalize to media:');
2587
- assertEqual(cap.getOutSpec(), MEDIA_IDENTITY, 'out=* should normalize to media:');
2762
+ // TEST643: cap:in=*;out=* is the same illegal bare top form
2763
+ function test643_explicitAsteriskIsIllegal() {
2764
+ assertThrows(
2765
+ () => CapUrn.fromString('cap:in=*;out=*'),
2766
+ ErrorCodes.ILLEGAL_DECLARATION,
2767
+ 'Explicit wildcard top-to-top must be rejected as inadmissible'
2768
+ );
2588
2769
  }
2589
2770
 
2590
- // TEST644: cap:in=media:;out=* has specific in, wildcard out
2591
- function test644_specificInWildcardOut() {
2592
- const cap = CapUrn.fromString('cap:in=media:;out=*');
2593
- assertEqual(cap.getInSpec(), 'media:', 'Should have specific in');
2594
- assertEqual(cap.getOutSpec(), 'media:', 'Wildcard out should normalize to media:');
2771
+ // TEST644: cap:in=media:;out=* is the same illegal bare top form
2772
+ function test644_specificInWildcardOutIsIllegal() {
2773
+ assertThrows(
2774
+ () => CapUrn.fromString('cap:in=media:;out=*'),
2775
+ ErrorCodes.ILLEGAL_DECLARATION,
2776
+ 'Top-to-top declared form must be rejected as inadmissible'
2777
+ );
2595
2778
  }
2596
2779
 
2597
2780
  // TEST645: cap:in=*;out=media:text has wildcard in, specific out
@@ -2621,8 +2804,8 @@ function test647_invalidOutSpecFails() {
2621
2804
 
2622
2805
  // TEST648: Wildcard in/out match specific caps
2623
2806
  function test648_wildcardAcceptsSpecific() {
2624
- const wildcard = CapUrn.fromString('cap:in=*;out=*');
2625
- const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
2807
+ const wildcard = CapUrn.fromString('cap:in=*;out=*;raw');
2808
+ const specific = CapUrn.fromString('cap:in="media:";out="media:text";raw');
2626
2809
 
2627
2810
  assert(wildcard.accepts(specific), 'Wildcard should accept specific');
2628
2811
  assert(specific.conformsTo(wildcard), 'Specific should conform to wildcard');
@@ -2630,52 +2813,105 @@ function test648_wildcardAcceptsSpecific() {
2630
2813
 
2631
2814
  // TEST649: Specificity - wildcard has 0, specific has tag count
2632
2815
  function test649_specificityScoring() {
2633
- const wildcard = CapUrn.fromString('cap:in=*;out=*');
2634
- const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
2816
+ const wildcard = CapUrn.fromString('cap:in=*;out=*;raw');
2817
+ const specific = CapUrn.fromString('cap:in="media:";out="media:text";raw');
2635
2818
 
2636
- assertEqual(wildcard.specificity(), 0, 'Wildcard cap should have 0 specificity');
2819
+ assertEqual(wildcard.specificity(), 2, 'Marker-only wildcard cap should have y-axis specificity only');
2637
2820
  assert(specific.specificity() > 0, 'Specific cap should have non-zero specificity');
2638
2821
  }
2639
2822
 
2640
- // TEST650: N/A for JS (JS requires in/out, cap:in=media:;out=media:;test would fail parsing)
2823
+ // TEST650: cap:in=media:;out=media:;test preserves other tags
2824
+ function test650_wildcardPreserveOtherTags() {
2825
+ const cap = CapUrn.fromString('cap:in=media:;out=media:;test');
2826
+ assertEqual(cap.getInSpec(), 'media:', 'in spec should remain media:');
2827
+ assertEqual(cap.getOutSpec(), 'media:', 'out spec should remain media:');
2828
+ assertEqual(cap.getEffect(), CapEffect.DECLARED, 'missing effect should default to declared');
2829
+ assert(cap.hasMarkerTag('test'), 'marker tag should be preserved');
2830
+ }
2641
2831
 
2642
- // TEST651: All identity forms produce the same CapUrn
2643
- function test651_identityFormsEquivalent() {
2832
+ // TEST651: Generic top-to-top spellings are all rejected.
2833
+ function test651_wildcardGenericFormsRejected() {
2644
2834
  const forms = [
2835
+ 'cap:',
2836
+ 'cap:in;out',
2645
2837
  'cap:in=*;out=*',
2646
- 'cap:in="media:";out="media:"',
2838
+ 'cap:in=media:;out=media:',
2839
+ 'cap:in;out=media:',
2840
+ 'cap:in=*;out=media:',
2841
+ 'cap:in=media:;out',
2842
+ 'cap:in=media:;out=*',
2647
2843
  ];
2648
-
2649
- const first = CapUrn.fromString(forms[0]);
2650
- // All forms should produce equivalent caps (wildcard behavior)
2651
- for (let i = 1; i < forms.length; i++) {
2652
- const cap = CapUrn.fromString(forms[i]);
2653
- // Both should accept specific caps
2654
- const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
2655
- assert(first.accepts(specific), `Form 0 should accept specific`);
2656
- assert(cap.accepts(specific), `Form ${i} should accept specific`);
2844
+ for (const form of forms) {
2845
+ assertThrows(
2846
+ () => CapUrn.fromString(form),
2847
+ ErrorCodes.ILLEGAL_DECLARATION,
2848
+ `${form} must be rejected as inadmissible`
2849
+ );
2657
2850
  }
2658
2851
  }
2659
2852
 
2660
- // TEST652: N/A for JS (CAP_IDENTITY constant not in JS)
2853
+ // TEST652: CAP_IDENTITY constant names the true identity cap, not bare cap:
2854
+ function test652_capIdentityConstantWorks() {
2855
+ const identity = CapUrn.fromString(CAP_IDENTITY);
2856
+ assertEqual(identity.toString(), 'cap:effect=none', 'CAP_IDENTITY must be explicit effect=none');
2857
+ assertEqual(identity.kind(), CapKind.IDENTITY, 'CAP_IDENTITY must classify as identity');
2858
+
2859
+ const longForm = CapUrn.fromString('cap:in=media:;out=media:;effect=none');
2860
+ assert(identity.accepts(longForm), 'identity should accept its long form');
2861
+ assert(longForm.accepts(identity), 'long form should accept canonical identity');
2862
+
2863
+ assertThrows(
2864
+ () => CapUrn.fromString('cap:'),
2865
+ ErrorCodes.ILLEGAL_DECLARATION,
2866
+ 'bare cap must be rejected as inadmissible'
2867
+ );
2868
+ }
2869
+
2870
+ // TEST653: invalid effect=none declarations fail at construction.
2871
+ function test653_invalidEffectNoneDeclarationRejected() {
2872
+ assertThrows(
2873
+ () => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
2874
+ ErrorCodes.ILLEGAL_DECLARATION,
2875
+ 'invalid effect=none declaration must fail at construction'
2876
+ );
2877
+ }
2661
2878
 
2662
- // TEST653: Identity (no tags) does not match specific requests via routing
2663
- function test653_identityRoutingIsolation() {
2664
- const identity = CapUrn.fromString('cap:in=*;out=*');
2665
- const specificRequest = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
2879
+ // TEST654: effect=none preserves runtime media identity.
2880
+ function test654_effectNonePreservesRuntimeMedia() {
2881
+ const decimate = CapUrn.fromString('cap:decimate-sequence;effect=none');
2882
+ const png = MediaUrn.fromString('media:image;png');
2883
+ const pdf = MediaUrn.fromString('media:pdf');
2884
+ assertEqual(decimate.inferRuntimeOutputMedia(png).toString(), png.toString(), 'effect=none should preserve png');
2885
+ assertEqual(decimate.inferRuntimeOutputMedia(pdf).toString(), pdf.toString(), 'effect=none should preserve pdf');
2886
+ }
2666
2887
 
2667
- // Identity has specificity 0 (no tags, wildcard directions)
2668
- assertEqual(identity.specificity(), 0, 'Identity specificity should be 0');
2888
+ // TEST655: default effect=declared does not preserve runtime refinements.
2889
+ function test655_effectDeclaredUsesDeclaredOutput() {
2890
+ const resize = CapUrn.fromString('cap:in=media:image;out=media:image;resize');
2891
+ const png = MediaUrn.fromString('media:image;png;width=4000');
2892
+ assertEqual(
2893
+ resize.inferRuntimeOutputMedia(png).toString(),
2894
+ 'media:image',
2895
+ 'default declared effect should collapse to declared output'
2896
+ );
2897
+ }
2669
2898
 
2670
- // Specific request has higher specificity
2671
- assert(specificRequest.specificity() > identity.specificity(),
2672
- 'Specific request should have higher specificity than identity');
2899
+ // TEST656: invalid effect=none declarations fail hard at construction.
2900
+ function test656_invalidEffectNoneFailsHard() {
2901
+ assertThrows(
2902
+ () => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
2903
+ ErrorCodes.ILLEGAL_DECLARATION,
2904
+ 'invalid effect=none declaration must fail at construction'
2905
+ );
2906
+ }
2673
2907
 
2674
- // CapMatcher should prefer specific over identity
2675
- const specificCap = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
2676
- const best = CapMatcher.findBestMatch([identity, specificCap], specificRequest);
2677
- assert(best !== null, 'Should find a match');
2678
- assert(best.hasMarkerTag('test'), 'CapMatcher should prefer specific cap over identity');
2908
+ // TEST657: omitted effect means declared; unconstrained effect must be explicit.
2909
+ function test657_effectDispatchRequiresExplicitWildcard() {
2910
+ const noneProvider = CapUrn.fromString('cap:effect=none');
2911
+ const declaredRequest = CapUrn.fromString('cap:raw');
2912
+ const anyRequest = CapUrn.fromString('cap:?effect');
2913
+ assert(!noneProvider.isDispatchable(declaredRequest), 'effect=none should not silently satisfy declared request');
2914
+ assert(noneProvider.isDispatchable(anyRequest), 'explicit ?effect should accept any provider effect');
2679
2915
  }
2680
2916
 
2681
2917
  // ============================================================================
@@ -5335,25 +5571,28 @@ function testRenderer_validateResolvedMachinePayload_rejectsMissingFields() {
5335
5571
  // surface, not a per-port detail.
5336
5572
  // ============================================================================
5337
5573
 
5338
- // TEST1800: Identity classifier — only the bare cap: form qualifies.
5339
- // Adding any tag (even one that doesn't constrain in/out) demotes
5340
- // the cap to Transform because the operation/metadata axis is no
5341
- // longer fully generic.
5574
+ // TEST1800: Identity classifier — only explicit effect=none qualifies.
5342
5575
  function test1800_kindIdentityOnlyForBareCap() {
5343
- const identity = CapUrn.fromString('cap:');
5344
- assertEqual(identity.kind(), CapKind.IDENTITY, 'cap: should be Identity');
5576
+ const identity = CapUrn.fromString('cap:effect=none');
5577
+ assertEqual(identity.kind(), CapKind.IDENTITY, 'cap:effect=none should be Identity');
5345
5578
 
5346
5579
  for (const spelling of [
5347
- 'cap:in=media:;out=media:',
5348
- 'cap:in=*;out=*',
5349
- 'cap:in=media:',
5350
- 'cap:out=media:',
5580
+ 'cap:in=media:;out=media:;effect=none',
5581
+ 'cap:effect=none;in=*;out=*',
5582
+ 'cap:effect=none;in=media:',
5583
+ 'cap:effect=none;out=media:',
5351
5584
  ]) {
5352
5585
  const cap = CapUrn.fromString(spelling);
5353
5586
  assertEqual(cap.kind(), CapKind.IDENTITY,
5354
- `${spelling} should classify as Identity (canonical form is cap:)`);
5587
+ `${spelling} should classify as Identity`);
5355
5588
  }
5356
5589
 
5590
+ assertThrows(
5591
+ () => CapUrn.fromString('cap:'),
5592
+ ErrorCodes.ILLEGAL_DECLARATION,
5593
+ 'bare cap must be rejected as inadmissible'
5594
+ );
5595
+
5357
5596
  const withOp = CapUrn.fromString('cap:passthrough');
5358
5597
  assertEqual(withOp.kind(), CapKind.TRANSFORM,
5359
5598
  'cap:passthrough specifies the operation axis — not Identity');
@@ -5441,7 +5680,7 @@ function test1810_mediaVoidIsAtomic() {
5441
5680
  // once parsed.
5442
5681
  function test1805_kindInvariantUnderCanonicalSpellings() {
5443
5682
  const cases = [
5444
- { a: 'cap:', b: 'cap:in=media:;out=media:', expected: CapKind.IDENTITY },
5683
+ { a: 'cap:effect=none', b: 'cap:in=media:;out=media:;effect=none', expected: CapKind.IDENTITY },
5445
5684
  {
5446
5685
  a: 'cap:extract;in=media:pdf;out=media:textable',
5447
5686
  b: 'cap:extract;in="media:pdf";out="media:textable"',
@@ -5481,8 +5720,8 @@ function test1805_kindInvariantUnderCanonicalSpellings() {
5481
5720
 
5482
5721
  // TEST1820: A `?`-valued cap-tag scores 0. Same as missing.
5483
5722
  function test1820_specificityQuestionIsZero() {
5484
- const bare = CapUrn.fromString('cap:');
5485
- assertEqual(bare.specificity(), 0, 'cap: must score 0 (top of order)');
5723
+ const bare = CapUrn.fromString('cap:?effect');
5724
+ assertEqual(bare.specificity(), 0, 'cap:?effect must score 0 (fully unconstrained request)');
5486
5725
 
5487
5726
  const withQ = CapUrn.fromString('cap:?target');
5488
5727
  assertEqual(withQ.specificity(), 0,
@@ -5785,6 +6024,10 @@ async function runTests() {
5785
6024
  runTest('TEST108: extensions_serialization', test108_extensionsSerialization);
5786
6025
  runTest('TEST109: extensions_with_metadata_and_validation', test109_extensionsWithMetadataAndValidation);
5787
6026
  runTest('TEST110: multiple_extensions', test110_multipleExtensions);
6027
+ runTest('TEST115: cap_arg_serialization', test115_capArgSerialization);
6028
+ runTest('TEST116: cap_arg_constructors', test116_capArgConstructors);
6029
+ runTest('TEST150: cap_manifest_json_serialization', test150_capManifestJsonSerialization);
6030
+ runTest('TEST597: cap_arg_with_full_definition', test597_capArgWithFullDefinition);
5788
6031
 
5789
6032
  // cap-fab-renderer.js uses CapFab in browse mode (static registry from
5790
6033
  // /api/capabilities). These tests guard the minimal API the renderer relies
@@ -5873,7 +6116,8 @@ async function runTests() {
5873
6116
  runTest('TEST1304: with_in_out_spec', test1304_withInOutSpec);
5874
6117
  runTest('TEST1305: find_all_matches', test1305_findAllMatches);
5875
6118
  runTest('TEST1306: are_compatible', test1306_areCompatible);
5876
- runTest('TEST1307: with_tag_ignores_in_out', test1307_withTagIgnoresInOut);
6119
+ runTest('TEST1307: with_tag_rejects_structural_keys', test1307_withTagRejectsStructuralKeys);
6120
+ runTest('TEST1308: builder_rejects_structural_keys', test1308_builderRejectsStructuralKeys);
5877
6121
  runTest('TEST1294: rule11_void_input_with_stdin_rejected', test1294_rule11VoidInputWithStdinRejected);
5878
6122
  runTest('TEST1295: rule11_non_void_input_without_stdin_rejected', test1295_rule11NonVoidInputWithoutStdinRejected);
5879
6123
  runTest('TEST1296: rule11_void_input_cli_flag_only', test1296_rule11VoidInputCliFlagOnly);
@@ -5881,21 +6125,25 @@ async function runTests() {
5881
6125
 
5882
6126
  // cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
5883
6127
  console.log('\n--- cap_urn.rs (wildcard tests) ---');
5884
- runTest('TEST639: empty_cap_defaults_to_media_wildcard', test639_emptyCapDefaultsToMediaWildcard);
5885
- runTest('TEST640: in_only_defaults_out_to_media', test640_inOnlyDefaultsOutToMedia);
5886
- runTest('TEST641: out_only_defaults_in_to_media', test641_outOnlyDefaultsInToMedia);
5887
- runTest('TEST642: in_out_without_values_become_media', test642_inOutWithoutValuesBecomeMedia);
5888
- runTest('TEST643: explicit_asterisk_is_wildcard', test643_explicitAsteriskIsWildcard);
5889
- runTest('TEST644: specific_in_wildcard_out', test644_specificInWildcardOut);
6128
+ runTest('TEST639: empty_cap_is_illegal', test639_emptyCapIsIllegal);
6129
+ runTest('TEST640: in_only_is_illegal', test640_inOnlyIsIllegal);
6130
+ runTest('TEST641: out_only_is_illegal', test641_outOnlyIsIllegal);
6131
+ runTest('TEST642: in_out_without_values_are_illegal', test642_inOutWithoutValuesAreIllegal);
6132
+ runTest('TEST643: explicit_asterisk_is_illegal', test643_explicitAsteriskIsIllegal);
6133
+ runTest('TEST644: specific_in_wildcard_out_is_illegal', test644_specificInWildcardOutIsIllegal);
5890
6134
  runTest('TEST645: wildcard_in_specific_out', test645_wildcardInSpecificOut);
5891
6135
  runTest('TEST646: invalid_in_spec_fails', test646_invalidInSpecFails);
5892
6136
  runTest('TEST647: invalid_out_spec_fails', test647_invalidOutSpecFails);
5893
6137
  runTest('TEST648: wildcard_accepts_specific', test648_wildcardAcceptsSpecific);
5894
6138
  runTest('TEST649: specificity_scoring', test649_specificityScoring);
5895
- console.log(' SKIP TEST650: N/A for JS (requires in/out)');
5896
- runTest('TEST651: identity_forms_equivalent', test651_identityFormsEquivalent);
5897
- console.log(' SKIP TEST652: N/A for JS (CAP_IDENTITY constant)');
5898
- runTest('TEST653: identity_routing_isolation', test653_identityRoutingIsolation);
6139
+ runTest('TEST650: wildcard_preserve_other_tags', test650_wildcardPreserveOtherTags);
6140
+ runTest('TEST651: wildcard_generic_forms_rejected', test651_wildcardGenericFormsRejected);
6141
+ runTest('TEST652: cap_identity_constant_works', test652_capIdentityConstantWorks);
6142
+ runTest('TEST653: invalid_effect_none_declaration_rejected', test653_invalidEffectNoneDeclarationRejected);
6143
+ runTest('TEST654: effect_none_preserves_runtime_media', test654_effectNonePreservesRuntimeMedia);
6144
+ runTest('TEST655: effect_declared_uses_declared_output', test655_effectDeclaredUsesDeclaredOutput);
6145
+ runTest('TEST656: invalid_effect_none_fails_hard', test656_invalidEffectNoneFailsHard);
6146
+ runTest('TEST657: effect_dispatch_requires_explicit_wildcard', test657_effectDispatchRequiresExplicitWildcard);
5899
6147
 
5900
6148
  // machine module: parser tests (mirrors parser.rs)
5901
6149
  console.log('\n--- machine/parser.rs ---');
@@ -6069,7 +6317,7 @@ async function runTests() {
6069
6317
  runTest('RENDERER: validateResolvedMachine_rejectsMissingFields', testRenderer_validateResolvedMachinePayload_rejectsMissingFields);
6070
6318
 
6071
6319
  console.log('\n--- CapKind classifier (test1800–test1805) ---');
6072
- runTest('TEST1800: kind_identity_only_for_bare_cap', test1800_kindIdentityOnlyForBareCap);
6320
+ runTest('TEST1800: kind_identity_requires_effect_none', test1800_kindIdentityOnlyForBareCap);
6073
6321
  runTest('TEST1801: kind_source_when_input_is_void', test1801_kindSourceWhenInputIsVoid);
6074
6322
  runTest('TEST1802: kind_sink_when_output_is_void', test1802_kindSinkWhenOutputIsVoid);
6075
6323
  runTest('TEST1803: kind_effect_when_both_sides_void', test1803_kindEffectWhenBothSidesVoid);