capdag 1.198.510 → 1.205.540

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.test.js CHANGED
@@ -8,6 +8,12 @@ const {
8
8
  Cap, CapGroup, CapManifest, MediaDef, MediaDefError, MediaDefErrorCodes,
9
9
  resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
10
10
  CartridgeInfo, CartridgeCapSummary, CartridgeSuggestion, CartridgeRepoClient, CartridgeRepoServer,
11
+ hostPlatform, CompatStatus, primaryPackage, CartridgeCompatibilityResolution,
12
+ registryUrlFromBuildEnv,
13
+ slugForSync, CartridgeInstallSource, validateRegistryUrlScheme, RegistryUrlSchemeResultKind,
14
+ CartridgeJson, CartridgeJsonError, CartridgeJsonErrorKind, hashCartridgeDirectory,
15
+ DiscoveryIdentity, DiscoveredCartridge, discoverCartridges,
16
+ CartridgeAttachmentErrorKind, CartridgeChannel,
11
17
  CapFabEdge, CapFabStats, CapFab,
12
18
  StdinSource, StdinSourceKind,
13
19
  validateNoMediaDefRedefinitionSync,
@@ -103,7 +109,7 @@ function runTest(name, fn) {
103
109
  /**
104
110
  * Helper function to build test URNs with required in/out media URNs.
105
111
  * Uses MEDIA_VOID for in and MEDIA_OBJECT for out, matching the
106
- * Rust reference test_urn helper: test_urn(tags) => cap:in="media:void";{tags};out="media:record;textable"
112
+ * Rust reference test_urn helper: test_urn(tags) => cap:in="media:void";{tags};out="media:enc=utf-8;record"
107
113
  */
108
114
  // TEST0051: Urn
109
115
  function test0051_Urn(tags) {
@@ -158,7 +164,7 @@ function test003_directionMatching() {
158
164
  assert(cap.accepts(request), 'Same direction specs should match');
159
165
 
160
166
  // Different direction should not match
161
- const requestDiff = CapUrn.fromString('cap:in="media:textable";generate;out="media:record;textable"');
167
+ const requestDiff = CapUrn.fromString('cap:in="media:enc=utf-8";generate;out="media:enc=utf-8;record"');
162
168
  assert(!cap.accepts(requestDiff), 'Different inSpec should not match');
163
169
 
164
170
  // Wildcard direction matches any
@@ -170,7 +176,7 @@ function test003_directionMatching() {
170
176
  // Key lookup is case-insensitive: uppercase variants of `ext` resolve
171
177
  // to the same keyed tag.
172
178
  function test004_unquotedValuesLowercased() {
173
- const cap = CapUrn.fromString('cap:ext=pdf;generate;in=media:void;out="media:record;textable"');
179
+ const cap = CapUrn.fromString('cap:ext=pdf;generate;in=media:void;out="media:enc=utf-8;record"');
174
180
  assert(cap.hasMarkerTag('generate'), 'Unquoted value should be lowercased');
175
181
  assertEqual(cap.getTag('ext'), 'pdf', 'Unquoted value should be lowercased');
176
182
  assertEqual(cap.getTag('EXT'), 'pdf', 'Key lookup should be case-insensitive');
@@ -458,7 +464,7 @@ function test024_compatibility() {
458
464
  assert(general.accepts(cap1), 'General request should accept more specific instance');
459
465
  assert(!cap1.accepts(general), 'Specific pattern should reject general instance');
460
466
 
461
- const broadIn = CapUrn.fromString('cap:in="media:";generate;out="media:record;textable"');
467
+ const broadIn = CapUrn.fromString('cap:in="media:";generate;out="media:enc=utf-8;record"');
462
468
  assert(!cap1.accepts(broadIn), 'Specific input should not accept broad wildcard input');
463
469
  assert(broadIn.accepts(cap1), 'Wildcard input should accept specific input');
464
470
  }
@@ -479,11 +485,11 @@ function test025_bestMatch() {
479
485
  // TEST026: Test merge combines tags from both caps, subset keeps only specified tags
480
486
  function test026_mergeAndSubset() {
481
487
  const cap1 = CapUrn.fromString(test0051_Urn('generate'));
482
- const cap2 = CapUrn.fromString('cap:in="media:textable";ext=pdf;format=binary;out="media:"');
488
+ const cap2 = CapUrn.fromString('cap:in="media:enc=utf-8";ext=pdf;format=binary;out="media:"');
483
489
 
484
490
  // Merge (other takes precedence)
485
491
  const merged = cap1.merge(cap2);
486
- assertEqual(merged.getInSpec(), 'media:textable', 'Merge should take inSpec from other');
492
+ assertEqual(merged.getInSpec(), 'media:enc=utf-8', 'Merge should take inSpec from other');
487
493
  assertEqual(merged.getOutSpec(), 'media:', 'Merge should take outSpec from other');
488
494
  assert(merged.hasMarkerTag('generate'), 'Merge should keep original tags');
489
495
  assertEqual(merged.getTag('ext'), 'pdf', 'Merge should add other tags');
@@ -492,7 +498,7 @@ function test026_mergeAndSubset() {
492
498
  const sub = merged.subset(['ext']);
493
499
  assertEqual(sub.getTag('ext'), 'pdf', 'Subset should keep ext');
494
500
  assert(!sub.hasMarkerTag('generate'), 'Subset should drop the generate marker');
495
- assertEqual(sub.getInSpec(), 'media:textable', 'Subset should preserve inSpec');
501
+ assertEqual(sub.getInSpec(), 'media:enc=utf-8', 'Subset should preserve inSpec');
496
502
  }
497
503
 
498
504
  // TEST027: Test with_wildcard_tag sets tag to wildcard, including in/out
@@ -724,7 +730,7 @@ function test890_directionSemanticMatching() {
724
730
  'cap:in="media:";generate-thumbnail;out="media:image;png;thumbnail"'
725
731
  );
726
732
  const pdfRequest = CapUrn.fromString(
727
- 'cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"'
733
+ 'cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"'
728
734
  );
729
735
  assert(genericCap.accepts(pdfRequest), 'Generic wildcard cap must accept pdf request');
730
736
 
@@ -736,7 +742,7 @@ function test890_directionSemanticMatching() {
736
742
 
737
743
  // Reverse: specific pdf cap does NOT accept generic bytes request
738
744
  const pdfCap = CapUrn.fromString(
739
- 'cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"'
745
+ 'cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"'
740
746
  );
741
747
  const genericRequest = CapUrn.fromString(
742
748
  'cap:in="media:";generate-thumbnail;out="media:image;png;thumbnail"'
@@ -776,7 +782,7 @@ function test891_directionSemanticSpecificity() {
776
782
  'cap:in="media:";generate-thumbnail;out="media:image;png;thumbnail"'
777
783
  );
778
784
  const specificCap = CapUrn.fromString(
779
- 'cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"'
785
+ 'cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"'
780
786
  );
781
787
 
782
788
  // generic:
@@ -788,20 +794,20 @@ function test891_directionSemanticSpecificity() {
788
794
  'out=image;png;thumbnail(6) + in=media:(0) + generate-thumbnail marker(2) = 60002');
789
795
  // specific:
790
796
  // out=media:image;png;thumbnail -> 6
791
- // in=media:pdf -> 2
797
+ // in=media:ext=pdf -> 4 (ext=pdf is an exact-value tag, not a bare marker)
792
798
  // y: generate-thumbnail marker -> 2
793
- // spec_C = 10000*6 + 100*2 + 2 = 60202
794
- assertEqual(specificCap.specificity(), 10000*6 + 100*2 + 2,
795
- 'out=image;png;thumbnail(6) + in=pdf(2) + generate-thumbnail marker(2) = 60202');
799
+ // spec_C = 10000*6 + 100*4 + 2 = 60402
800
+ assertEqual(specificCap.specificity(), 10000*6 + 100*4 + 2,
801
+ 'out=image;png;thumbnail(6) + in=ext=pdf(4) + generate-thumbnail marker(2) = 60402');
796
802
  assert(specificCap.specificity() > genericCap.specificity(), 'pdf should be more specific');
797
803
 
798
804
  // CapMatcher should prefer more specific
799
805
  const pdfRequest = CapUrn.fromString(
800
- 'cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"'
806
+ 'cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"'
801
807
  );
802
808
  const best = CapMatcher.findBestMatch([genericCap, specificCap], pdfRequest);
803
809
  assert(best !== null, 'Should find a match');
804
- assertEqual(best.getInSpec(), 'media:pdf', 'Should prefer more specific pdf cap');
810
+ assertEqual(best.getInSpec(), 'media:ext=pdf', 'Should prefer more specific pdf cap');
805
811
  }
806
812
 
807
813
  // ============================================================================
@@ -863,21 +869,9 @@ function test060_wrongPrefixFails() {
863
869
  );
864
870
  }
865
871
 
866
- // TEST061: Test is_binary returns true when textable tag is absent (binary = not textable)
867
- function test061_isBinary() {
868
- // Binary types: no textable tag
869
- assert(MediaUrn.fromString(MEDIA_IDENTITY).isBinary(), 'MEDIA_IDENTITY (media:) should be binary');
870
- assert(MediaUrn.fromString(MEDIA_PNG).isBinary(), 'MEDIA_PNG should be binary');
871
- assert(MediaUrn.fromString(MEDIA_PDF).isBinary(), 'MEDIA_PDF should be binary');
872
- assert(MediaUrn.fromString('media:video').isBinary(), 'media:video should be binary');
873
- assert(MediaUrn.fromString('media:epub').isBinary(), 'media:epub should be binary');
874
- // Textable types: is_binary is false
875
- assert(!MediaUrn.fromString('media:textable').isBinary(), 'media:textable should not be binary');
876
- assert(!MediaUrn.fromString('media:textable;record').isBinary(), 'textable map should not be binary');
877
- assert(!MediaUrn.fromString(MEDIA_STRING).isBinary(), 'MEDIA_STRING should not be binary');
878
- assert(!MediaUrn.fromString(MEDIA_JSON).isBinary(), 'MEDIA_JSON should not be binary');
879
- assert(!MediaUrn.fromString(MEDIA_MD).isBinary(), 'MEDIA_MD should not be binary');
880
- }
872
+ // TEST061: REMOVED the binary/text distinction no longer exists in the
873
+ // vocabulary (isBinary() was deleted from MediaUrn; everything is bytes).
874
+ // Encoding is now expressed by the orthogonal `enc=` tag, exercised by TEST067.
881
875
 
882
876
  // TEST062: Test is_record returns true when record marker tag is present indicating key-value structure
883
877
  function test062_isRecord() {
@@ -885,7 +879,7 @@ function test062_isRecord() {
885
879
  assert(MediaUrn.fromString('media:custom;record').isRecord(), 'custom;record should be record');
886
880
  assert(MediaUrn.fromString(MEDIA_JSON).isRecord(), 'MEDIA_JSON should be record');
887
881
  // Without record marker, is_record is false
888
- assert(!MediaUrn.fromString('media:textable').isRecord(), 'plain textable should not be record');
882
+ assert(!MediaUrn.fromString('media:enc=utf-8').isRecord(), 'plain text should not be record');
889
883
  assert(!MediaUrn.fromString(MEDIA_STRING).isRecord(), 'MEDIA_STRING should not be record');
890
884
  assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isRecord(), 'MEDIA_STRING_LIST should not be record');
891
885
  }
@@ -897,7 +891,7 @@ function test063_isScalar() {
897
891
  assert(MediaUrn.fromString(MEDIA_NUMBER).isScalar(), 'MEDIA_NUMBER should be scalar');
898
892
  assert(MediaUrn.fromString(MEDIA_BOOLEAN).isScalar(), 'MEDIA_BOOLEAN should be scalar');
899
893
  assert(MediaUrn.fromString(MEDIA_OBJECT).isScalar(), 'MEDIA_OBJECT (record but scalar) should be scalar');
900
- assert(MediaUrn.fromString('media:textable').isScalar(), 'plain textable should be scalar');
894
+ assert(MediaUrn.fromString('media:enc=utf-8').isScalar(), 'plain text should be scalar');
901
895
  // With list marker, is_scalar is false
902
896
  assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isScalar(), 'MEDIA_STRING_LIST should not be scalar');
903
897
  assert(!MediaUrn.fromString(MEDIA_OBJECT_LIST).isScalar(), 'MEDIA_OBJECT_LIST should not be scalar');
@@ -917,27 +911,34 @@ function test065_isOpaque() {
917
911
  assert(MediaUrn.fromString(MEDIA_STRING).isOpaque(), 'MEDIA_STRING should be opaque');
918
912
  assert(MediaUrn.fromString(MEDIA_STRING_LIST).isOpaque(), 'MEDIA_STRING_LIST (list but no record) should be opaque');
919
913
  assert(MediaUrn.fromString(MEDIA_PDF).isOpaque(), 'MEDIA_PDF should be opaque');
920
- assert(MediaUrn.fromString('media:textable').isOpaque(), 'plain textable should be opaque');
914
+ assert(MediaUrn.fromString('media:enc=utf-8').isOpaque(), 'plain text should be opaque');
921
915
  // With record marker, is_opaque is false
922
916
  assert(!MediaUrn.fromString(MEDIA_OBJECT).isOpaque(), 'MEDIA_OBJECT should not be opaque');
923
917
  assert(!MediaUrn.fromString(MEDIA_JSON).isOpaque(), 'MEDIA_JSON should not be opaque');
924
918
  }
925
919
 
926
- // TEST066: Test is_json returns true only when json marker tag is present for JSON representation
920
+ // TEST066: Test is_json returns true only when fmt=json content-format tag is present
927
921
  function test066_isJson() {
928
922
  assert(MediaUrn.fromString(MEDIA_JSON).isJson(), 'MEDIA_JSON should be json');
923
+ assert(MediaUrn.fromString('media:custom;fmt=json').isJson(), 'fmt=json should be json');
924
+ // record alone does not mean JSON representation
929
925
  assert(!MediaUrn.fromString(MEDIA_OBJECT).isJson(), 'MEDIA_OBJECT should not be json');
926
+ assert(!MediaUrn.fromString('media:enc=utf-8').isJson(), 'plain text should not be json');
930
927
  }
931
928
 
932
- // TEST067: Test is_text returns true only when textable marker tag is present
929
+ // TEST067: Text-representability is now carried by the orthogonal `enc=` tag
930
+ // (the old text marker and isText() are gone). A media is "text" iff it
931
+ // declares an encoding.
933
932
  function test067_isText() {
934
- assert(MediaUrn.fromString(MEDIA_STRING).isText(), 'MEDIA_STRING should be text');
935
- assert(MediaUrn.fromString(MEDIA_INTEGER).isText(), 'MEDIA_INTEGER should be text');
936
- assert(MediaUrn.fromString(MEDIA_JSON).isText(), 'MEDIA_JSON should be text');
937
- // Without textable tag, is_text is false
938
- assert(!MediaUrn.fromString(MEDIA_IDENTITY).isText(), 'MEDIA_IDENTITY should not be text');
939
- assert(!MediaUrn.fromString(MEDIA_PNG).isText(), 'MEDIA_PNG should not be text');
940
- assert(!MediaUrn.fromString(MEDIA_OBJECT).isText(), 'MEDIA_OBJECT (no textable) should not be text');
933
+ // Has enc= text-representable
934
+ assert(MediaUrn.fromString(MEDIA_STRING).getTag('enc') !== undefined, 'MEDIA_STRING should have enc');
935
+ assert(MediaUrn.fromString(MEDIA_BOOLEAN).getTag('enc') !== undefined, 'MEDIA_BOOLEAN should have enc');
936
+ // No enc= not text-representable
937
+ assert(MediaUrn.fromString(MEDIA_INTEGER).getTag('enc') === undefined, 'MEDIA_INTEGER should not have enc');
938
+ assert(MediaUrn.fromString(MEDIA_JSON).getTag('enc') === undefined, 'MEDIA_JSON should not have enc');
939
+ assert(MediaUrn.fromString(MEDIA_IDENTITY).getTag('enc') === undefined, 'MEDIA_IDENTITY should not have enc');
940
+ assert(MediaUrn.fromString(MEDIA_PNG).getTag('enc') === undefined, 'MEDIA_PNG should not have enc');
941
+ assert(MediaUrn.fromString(MEDIA_OBJECT).getTag('enc') === undefined, 'MEDIA_OBJECT should not have enc');
941
942
  }
942
943
 
943
944
  // TEST068: Test is_void returns true when void flag or type=void tag is present
@@ -1006,7 +1007,7 @@ function test075_accepts() {
1006
1007
  // TEST076: Test specificity increases with more tags for ranking conformance
1007
1008
  function test076_specificity() {
1008
1009
  const s1 = MediaUrn.fromString('media:');
1009
- const s2 = MediaUrn.fromString('media:pdf');
1010
+ const s2 = MediaUrn.fromString('media:ext=pdf');
1010
1011
  const s3 = MediaUrn.fromString('media:image;png;thumbnail');
1011
1012
  assert(s2.specificity() > s1.specificity(), 'pdf should be more specific than wildcard');
1012
1013
  assert(s3.specificity() > s2.specificity(), 'image;png;thumbnail should be more specific than pdf');
@@ -1082,7 +1083,7 @@ function test093_resolveUnresolvableFailsHard() {
1082
1083
  // TEST097: N/A for JS (Rust validation function)
1083
1084
  // TEST098: N/A for JS
1084
1085
 
1085
- // TEST099: Test ResolvedMediaDef is_binary returns true when textable tag is absent
1086
+ // TEST099: Test ResolvedMediaDef is_binary returns true when enc tag is absent
1086
1087
  function test099_resolvedIsBinary() {
1087
1088
  const spec = new MediaDef('application/octet-stream', null, null, 'Binary', null, MEDIA_IDENTITY);
1088
1089
  assert(spec.isBinary(), 'Resolved binary spec should be binary');
@@ -1112,7 +1113,7 @@ function test103_resolvedIsJson() {
1112
1113
  assert(spec.isJSON(), 'Resolved json spec should be JSON');
1113
1114
  }
1114
1115
 
1115
- // TEST104: Test ResolvedMediaDef is_text returns true when textable tag is present
1116
+ // TEST104: Test ResolvedMediaDef is_text returns true when enc tag is present
1116
1117
  function test104_resolvedIsText() {
1117
1118
  const spec = new MediaDef('text/plain', null, null, 'String', null, MEDIA_STRING);
1118
1119
  assert(spec.isText(), 'Resolved string spec should be text');
@@ -1164,13 +1165,13 @@ function test106_metadataWithValidation() {
1164
1165
  function test107_extensionsPropagation() {
1165
1166
  const mediaDefs = [
1166
1167
  {
1167
- urn: 'media:pdf',
1168
+ urn: 'media:ext=pdf',
1168
1169
  media_type: 'application/pdf',
1169
1170
  title: 'PDF Document',
1170
1171
  extensions: ['pdf']
1171
1172
  }
1172
1173
  ];
1173
- const resolved = resolveMediaUrn('media:pdf', mediaDefs);
1174
+ const resolved = resolveMediaUrn('media:ext=pdf', mediaDefs);
1174
1175
  assert(Array.isArray(resolved.extensions), 'Extensions should be an array');
1175
1176
  assertEqual(resolved.extensions.length, 1, 'Should have one extension');
1176
1177
  assertEqual(resolved.extensions[0], 'pdf', 'Should have pdf extension');
@@ -1179,7 +1180,7 @@ function test107_extensionsPropagation() {
1179
1180
  // TEST108: Test creating new cap with URN, title, and command verifies correct initialization
1180
1181
  function test108_extensionsSerialization() {
1181
1182
  // Test that MediaDef can hold extensions correctly
1182
- const spec = new MediaDef('application/pdf', null, null, 'PDF', null, 'media:pdf', null, null, ['pdf']);
1183
+ const spec = new MediaDef('application/pdf', null, null, 'PDF', null, 'media:ext=pdf', null, null, ['pdf']);
1183
1184
  assert(Array.isArray(spec.extensions), 'Extensions should be array');
1184
1185
  assertEqual(spec.extensions[0], 'pdf', 'Should have pdf extension');
1185
1186
  }
@@ -1284,9 +1285,9 @@ function test116_capArgConstructors() {
1284
1285
  function test150_capManifestJsonSerialization() {
1285
1286
  const capUrn = CapUrn.fromString(test0051_Urn('extract;target=metadata'));
1286
1287
  const cap = new Cap(capUrn, 'Extract Metadata', 'extract-metadata');
1287
- cap.addArg(new CapArg('media:pdf', true, [new ArgSource({ stdin: 'media:pdf' })]));
1288
+ cap.addArg(new CapArg('media:ext=pdf', true, [new ArgSource({ stdin: 'media:ext=pdf' })]));
1288
1289
  cap.addArg(new CapArg(
1289
- 'media:chunk-size;textable;numeric',
1290
+ 'media:chunk-size;enc=utf-8;numeric',
1290
1291
  false,
1291
1292
  [new ArgSource({ cli_flag: '--chunk-size' })],
1292
1293
  {
@@ -1296,7 +1297,7 @@ function test150_capManifestJsonSerialization() {
1296
1297
  }
1297
1298
  ));
1298
1299
  cap.addArg(new CapArg(
1299
- 'media:timestamps;textable;bool',
1300
+ 'media:bool;enc=utf-8;timestamps',
1300
1301
  false,
1301
1302
  [new ArgSource({ cli_flag: '--timestamps' })],
1302
1303
  {
@@ -1382,34 +1383,34 @@ function test597_capArgWithFullDefinition() {
1382
1383
  // registry name we passed. This is exactly the shape the renderer depends on.
1383
1384
  function test0052_CapFabAddCapPopulatesEdgesAndNodes() {
1384
1385
  const graph = new CapFab();
1385
- const cap = makeGraphCap('media:pdf', 'media:textable', 'PDF to Text');
1386
+ const cap = makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text');
1386
1387
  graph.addCap(cap, 'registry');
1387
1388
 
1388
1389
  const edges = graph.getEdges();
1389
1390
  assertEqual(edges.length, 1, 'Graph must have one edge after a single addCap');
1390
- assertEqual(edges[0].fromUrn, 'media:pdf', 'Edge fromUrn must be cap in_spec');
1391
- assertEqual(edges[0].toUrn, 'media:textable', 'Edge toUrn must be cap out_spec');
1391
+ assertEqual(edges[0].fromUrn, 'media:ext=pdf', 'Edge fromUrn must be cap in_spec');
1392
+ assertEqual(edges[0].toUrn, 'media:enc=utf-8', 'Edge toUrn must be cap out_spec');
1392
1393
  assertEqual(edges[0].registryName, 'registry', 'Edge must carry the registry name passed to addCap');
1393
1394
 
1394
1395
  const nodes = graph.getNodes();
1395
- assert(nodes.has('media:pdf'), 'from_spec must appear as a node');
1396
- assert(nodes.has('media:textable'), 'to_spec must appear as a node');
1396
+ assert(nodes.has('media:ext=pdf'), 'from_spec must appear as a node');
1397
+ assert(nodes.has('media:enc=utf-8'), 'to_spec must appear as a node');
1397
1398
  }
1398
1399
 
1399
1400
  // getOutgoing takes a concrete source URN and returns edges whose from_spec
1400
1401
  // the source conforms to. It must NOT be a plain string lookup.
1401
1402
  function test0053_CapFabGetOutgoingConformsToMatching() {
1402
1403
  const graph = new CapFab();
1403
- graph.addCap(makeGraphCap('media:textable', 'media:embedding-vector', 'Embed text'), 'registry');
1404
+ graph.addCap(makeGraphCap('media:enc=utf-8', 'media:embedding-vector', 'Embed text'), 'registry');
1404
1405
 
1405
- // 'media:txt;textable' conforms to 'media:textable' — renderer relies on
1406
+ // 'media:enc=utf-8;ext=txt' conforms to 'media:enc=utf-8' — renderer relies on
1406
1407
  // this for the browse-mode fan-out from specific source URNs to caps that
1407
1408
  // accept a broader media pattern.
1408
- const outgoingFromSpecific = graph.getOutgoing('media:txt;textable');
1409
+ const outgoingFromSpecific = graph.getOutgoing('media:enc=utf-8;ext=txt');
1409
1410
  assertEqual(outgoingFromSpecific.length, 1, 'Specific URN must match broader cap input');
1410
1411
 
1411
1412
  // The broad URN still matches its own edge.
1412
- const outgoingFromBroad = graph.getOutgoing('media:textable');
1413
+ const outgoingFromBroad = graph.getOutgoing('media:enc=utf-8');
1413
1414
  assertEqual(outgoingFromBroad.length, 1, 'Exact URN must match');
1414
1415
 
1415
1416
  // A totally unrelated URN must not match.
@@ -1421,8 +1422,8 @@ function test0053_CapFabGetOutgoingConformsToMatching() {
1421
1422
  // the renderer colours/groups edges by provenance in browse mode.
1422
1423
  function test0057_CapFabDistinctRegistryNames() {
1423
1424
  const graph = new CapFab();
1424
- graph.addCap(makeGraphCap('media:pdf', 'media:textable', 'PDF to Text'), 'providers');
1425
- graph.addCap(makeGraphCap('media:textable', 'media:embedding-vector', 'Embed'), 'cartridges');
1425
+ graph.addCap(makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text'), 'providers');
1426
+ graph.addCap(makeGraphCap('media:enc=utf-8', 'media:embedding-vector', 'Embed'), 'cartridges');
1426
1427
 
1427
1428
  const edges = graph.getEdges();
1428
1429
  assertEqual(edges.length, 2, 'Two caps must produce two edges');
@@ -1452,7 +1453,7 @@ function test157_stdinSourceFromFileReference() {
1452
1453
  const trackedFileId = 'tracked-file-123';
1453
1454
  const originalPath = '/path/to/original.pdf';
1454
1455
  const securityBookmark = new Uint8Array([0x62, 0x6f, 0x6f, 0x6b]);
1455
- const mediaUrn = 'media:pdf';
1456
+ const mediaUrn = 'media:ext=pdf';
1456
1457
 
1457
1458
  const source = StdinSource.fromFileReference(trackedFileId, originalPath, securityBookmark, mediaUrn);
1458
1459
  assert(source !== null, 'Should create source');
@@ -1488,15 +1489,15 @@ function test159_stdinSourceWithBinaryContent() {
1488
1489
 
1489
1490
  // TEST274: Test CapArgumentValue::new stores media_urn and raw byte value
1490
1491
  function test274_capArgumentValueNew() {
1491
- const arg = new CapArgumentValue('media:model-spec;textable', new Uint8Array([103, 112, 116, 45, 52]));
1492
- assertEqual(arg.mediaUrn, 'media:model-spec;textable', 'mediaUrn must match');
1492
+ const arg = new CapArgumentValue('media:enc=utf-8;model-spec', new Uint8Array([103, 112, 116, 45, 52]));
1493
+ assertEqual(arg.mediaUrn, 'media:enc=utf-8;model-spec', 'mediaUrn must match');
1493
1494
  assertEqual(arg.value.length, 5, 'value must have 5 bytes');
1494
1495
  }
1495
1496
 
1496
1497
  // TEST275: Test CapArgumentValue::from_str converts string to UTF-8 bytes
1497
1498
  function test275_capArgumentValueFromStr() {
1498
- const arg = CapArgumentValue.fromStr('media:string;textable', 'hello world');
1499
- assertEqual(arg.mediaUrn, 'media:string;textable', 'mediaUrn must match');
1499
+ const arg = CapArgumentValue.fromStr('media:enc=utf-8;string', 'hello world');
1500
+ assertEqual(arg.mediaUrn, 'media:enc=utf-8;string', 'mediaUrn must match');
1500
1501
  assertEqual(new TextDecoder().decode(arg.value), 'hello world', 'value must decode correctly');
1501
1502
  }
1502
1503
 
@@ -1508,7 +1509,7 @@ function test276_capArgumentValueAsStrValid() {
1508
1509
 
1509
1510
  // TEST277: Test CapArgumentValue::value_as_str fails for non-UTF-8 binary data
1510
1511
  function test277_capArgumentValueAsStrInvalidUtf8() {
1511
- const arg = new CapArgumentValue('media:pdf', new Uint8Array([0xFF, 0xFE, 0x80]));
1512
+ const arg = new CapArgumentValue('media:ext=pdf', new Uint8Array([0xFF, 0xFE, 0x80]));
1512
1513
  let threw = false;
1513
1514
  try {
1514
1515
  arg.valueAsStr();
@@ -1539,7 +1540,7 @@ function test283_capArgumentValueLargeBinary() {
1539
1540
  for (let i = 0; i < 10000; i++) {
1540
1541
  data[i] = i % 256;
1541
1542
  }
1542
- const arg = new CapArgumentValue('media:pdf', data);
1543
+ const arg = new CapArgumentValue('media:ext=pdf', data);
1543
1544
  assertEqual(arg.value.length, 10000, 'large binary must preserve all bytes');
1544
1545
  assertEqual(arg.value[0], 0, 'first byte check');
1545
1546
  assertEqual(arg.value[255], 255, 'byte 255 check');
@@ -1555,9 +1556,8 @@ const { TaggedUrn } = require('tagged-urn');
1555
1556
  // TEST304: Test MEDIA_AVAILABILITY_OUTPUT constant parses as valid media URN with correct tags
1556
1557
  function test304_mediaAvailabilityOutputConstant() {
1557
1558
  const urn = TaggedUrn.fromString(MEDIA_AVAILABILITY_OUTPUT);
1558
- assert(urn.getTag('textable') !== undefined, 'model-availability must be textable');
1559
+ assertEqual(urn.getTag('enc'), 'utf-8', 'model-availability must carry enc=utf-8');
1559
1560
  assertEqual(urn.getTag('record'), '*', 'model-availability must be record');
1560
- assert(urn.getTag('textable') !== undefined, 'model-availability must not be binary (has textable)');
1561
1561
  const reparsed = TaggedUrn.fromString(urn.toString());
1562
1562
  assert(urn.conformsTo(reparsed), 'roundtrip must match original');
1563
1563
  }
@@ -1565,9 +1565,8 @@ function test304_mediaAvailabilityOutputConstant() {
1565
1565
  // TEST305: Test MEDIA_PATH_OUTPUT constant parses as valid media URN with correct tags
1566
1566
  function test305_mediaPathOutputConstant() {
1567
1567
  const urn = TaggedUrn.fromString(MEDIA_PATH_OUTPUT);
1568
- assert(urn.getTag('textable') !== undefined, 'model-path must be textable');
1568
+ assertEqual(urn.getTag('enc'), 'utf-8', 'model-path must carry enc=utf-8');
1569
1569
  assertEqual(urn.getTag('record'), '*', 'model-path must be record');
1570
- assert(urn.getTag('textable') !== undefined, 'model-path must not be binary (has textable)');
1571
1570
  const reparsed = TaggedUrn.fromString(urn.toString());
1572
1571
  assert(urn.conformsTo(reparsed), 'roundtrip must match original');
1573
1572
  }
@@ -1617,7 +1616,7 @@ function test309_modelAvailabilityAndPathAreDistinct() {
1617
1616
  assert(avail.toString() !== path.toString(), 'availability and path must be distinct');
1618
1617
  }
1619
1618
 
1620
- // TEST310: llm_generate_text_urn() produces a valid cap URN with textable in/out specs
1619
+ // TEST310: llm_generate_text_urn() produces a valid cap URN with enc=utf-8 in/out specs
1621
1620
  function test310_llmGenerateTextUrn() {
1622
1621
  const urn = llmGenerateTextUrn();
1623
1622
  assert(urn.hasMarkerTag('generate_text'), 'Must have generate_text marker');
@@ -1663,9 +1662,9 @@ function test312_allUrnBuildersProduceValidUrns() {
1663
1662
 
1664
1663
  function test0059_JS_buildExtensionIndex() {
1665
1664
  const mediaDefs = [
1666
- { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1665
+ { urn: 'media:ext=pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1667
1666
  { urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] },
1668
- { urn: 'media:json;textable', media_type: 'application/json', extensions: ['json'] }
1667
+ { urn: 'media:fmt=json', media_type: 'application/json', extensions: ['json'] }
1669
1668
  ];
1670
1669
  const index = buildExtensionIndex(mediaDefs);
1671
1670
  assert(index instanceof Map, 'Should return a Map');
@@ -1674,15 +1673,15 @@ function test0059_JS_buildExtensionIndex() {
1674
1673
  assert(index.has('jpg'), 'Should have jpg');
1675
1674
  assert(index.has('jpeg'), 'Should have jpeg');
1676
1675
  assert(index.has('json'), 'Should have json');
1677
- assertEqual(index.get('pdf')[0], 'media:pdf', 'pdf should map correctly');
1676
+ assertEqual(index.get('pdf')[0], 'media:ext=pdf', 'pdf should map correctly');
1678
1677
  }
1679
1678
 
1680
1679
  // TEST0069: J s media urns for extension
1681
1680
  function test0069_JS_mediaUrnsForExtension() {
1682
1681
  const mediaDefs = [
1683
- { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1684
- { urn: 'media:json;textable;record', media_type: 'application/json', extensions: ['json'] },
1685
- { urn: 'media:json;textable;list', media_type: 'application/json', extensions: ['json'] }
1682
+ { urn: 'media:ext=pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1683
+ { urn: 'media:fmt=json;record', media_type: 'application/json', extensions: ['json'] },
1684
+ { urn: 'media:fmt=json;list', media_type: 'application/json', extensions: ['json'] }
1686
1685
  ];
1687
1686
 
1688
1687
  const pdfUrns = mediaUrnsForExtension('pdf', mediaDefs);
@@ -1709,7 +1708,7 @@ function test0069_JS_mediaUrnsForExtension() {
1709
1708
  // TEST0070: J s get extension mappings
1710
1709
  function test0070_JS_getExtensionMappings() {
1711
1710
  const mediaDefs = [
1712
- { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1711
+ { urn: 'media:ext=pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1713
1712
  { urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] }
1714
1713
  ];
1715
1714
  const mappings = getExtensionMappings(mediaDefs);
@@ -1803,7 +1802,7 @@ function test0082_JS_mediaDefDocumentationPropagatesThroughResolve() {
1803
1802
  const body = '## Markdown body\n\nWith `code` and a [link](https://example.com).';
1804
1803
  const mediaDefs = [
1805
1804
  {
1806
- urn: 'media:doc-test;textable',
1805
+ urn: 'media:doc-test;enc=utf-8',
1807
1806
  media_type: 'text/plain',
1808
1807
  title: 'Documented',
1809
1808
  description: 'short desc',
@@ -1811,20 +1810,20 @@ function test0082_JS_mediaDefDocumentationPropagatesThroughResolve() {
1811
1810
  }
1812
1811
  ];
1813
1812
 
1814
- const resolved = resolveMediaUrn('media:doc-test;textable', mediaDefs);
1813
+ const resolved = resolveMediaUrn('media:doc-test;enc=utf-8', mediaDefs);
1815
1814
  assertEqual(resolved.documentation, body, 'documentation must propagate into MediaDef');
1816
1815
  // The short description must remain distinct from the long markdown
1817
1816
  // body — they are different fields with different semantics.
1818
1817
  assertEqual(resolved.description, 'short desc', 'description must remain distinct from documentation');
1819
1818
 
1820
1819
  // Missing documentation must collapse to null, not '' or undefined.
1821
- const noDoc = resolveMediaUrn('media:doc-test;textable', [
1822
- { urn: 'media:doc-test;textable', media_type: 'text/plain', title: 'No Doc' }
1820
+ const noDoc = resolveMediaUrn('media:doc-test;enc=utf-8', [
1821
+ { urn: 'media:doc-test;enc=utf-8', media_type: 'text/plain', title: 'No Doc' }
1823
1822
  ]);
1824
1823
  assertEqual(noDoc.documentation, null, 'Missing documentation must resolve to null');
1825
1824
 
1826
- const emptyDoc = resolveMediaUrn('media:doc-test;textable', [
1827
- { urn: 'media:doc-test;textable', media_type: 'text/plain', title: 'Empty', documentation: '' }
1825
+ const emptyDoc = resolveMediaUrn('media:doc-test;enc=utf-8', [
1826
+ { urn: 'media:doc-test;enc=utf-8', media_type: 'text/plain', title: 'Empty', documentation: '' }
1828
1827
  ]);
1829
1828
  assertEqual(emptyDoc.documentation, null, 'Empty documentation string must collapse to null');
1830
1829
  }
@@ -1884,15 +1883,15 @@ const sampleRegistry = {
1884
1883
  cap_groups: [
1885
1884
  {
1886
1885
  name: 'pdf-processing',
1887
- adapter_urns: ['media:pdf'],
1886
+ adapter_urns: ['media:ext=pdf'],
1888
1887
  caps: [
1889
1888
  {
1890
- urn: 'cap:in="media:pdf";disbind;out="media:disbound-page;textable;list"',
1889
+ urn: 'cap:in="media:ext=pdf";disbind;out="media:disbound-page;enc=utf-8;list"',
1891
1890
  title: 'Disbind PDF',
1892
1891
  description: 'Extract pages'
1893
1892
  },
1894
1893
  {
1895
- urn: 'cap:in="media:pdf";extract-metadata;out="media:file-metadata;textable;record"',
1894
+ urn: 'cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"',
1896
1895
  title: 'Extract Metadata',
1897
1896
  description: 'Get PDF metadata'
1898
1897
  }
@@ -1929,10 +1928,10 @@ const sampleRegistry = {
1929
1928
  cap_groups: [
1930
1929
  {
1931
1930
  name: 'text-processing',
1932
- adapter_urns: ['media:txt;textable'],
1931
+ adapter_urns: ['media:enc=utf-8;ext=txt'],
1933
1932
  caps: [
1934
1933
  {
1935
- urn: 'cap:in="media:txt;textable";disbind;out="media:disbound-page;textable;list"',
1934
+ urn: 'cap:in="media:enc=utf-8;ext=txt";disbind;out="media:disbound-page;enc=utf-8;list"',
1936
1935
  title: 'Disbind Text',
1937
1936
  description: 'Extract text pages'
1938
1937
  }
@@ -1974,7 +1973,7 @@ const sampleRegistry = {
1974
1973
  adapter_urns: ['media:json'],
1975
1974
  caps: [
1976
1975
  {
1977
- urn: 'cap:in="media:json";pretty;out="media:json;textable"',
1976
+ urn: 'cap:in="media:json";pretty;out="media:fmt=json"',
1978
1977
  title: 'Pretty Print JSON',
1979
1978
  description: 'Format JSON',
1980
1979
  command: 'pretty'
@@ -2252,13 +2251,13 @@ function test328_cartridgeRepoServerGetByCategory() {
2252
2251
  function test329_cartridgeRepoServerGetByCap() {
2253
2252
  const server = new CartridgeRepoServer(sampleRegistry);
2254
2253
 
2255
- const disbindCap = 'cap:in="media:pdf";disbind;out="media:disbound-page;textable;list"';
2254
+ const disbindCap = 'cap:in="media:ext=pdf";disbind;out="media:disbound-page;enc=utf-8;list"';
2256
2255
  const cartridges = server.getCartridgesByCap(disbindCap);
2257
2256
 
2258
2257
  assert(cartridges.length === 1, 'Should find 1 cartridge with this cap');
2259
2258
  assert(cartridges[0].id === 'pdfcartridge', 'Should be pdfcartridge');
2260
2259
 
2261
- const metadataCap = 'cap:in="media:pdf";extract-metadata;out="media:file-metadata;textable;record"';
2260
+ const metadataCap = 'cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"';
2262
2261
  const metadataCartridges = server.getCartridgesByCap(metadataCap);
2263
2262
  assert(metadataCartridges.length === 1, 'Should find metadata cap');
2264
2263
  }
@@ -2301,7 +2300,7 @@ function test331_cartridgeRepoClientGetSuggestions() {
2301
2300
 
2302
2301
  client.updateCache('https://example.com/api/cartridges', cartridges);
2303
2302
 
2304
- const disbindCap = 'cap:in="media:pdf";disbind;out="media:disbound-page;textable;list"';
2303
+ const disbindCap = 'cap:in="media:ext=pdf";disbind;out="media:disbound-page;enc=utf-8;list"';
2305
2304
  const suggestions = client.getSuggestionsForCap(disbindCap);
2306
2305
 
2307
2306
  assert(suggestions.length === 1, 'Should find 1 suggestion');
@@ -2316,7 +2315,7 @@ function test331_cartridgeRepoClientGetSuggestions() {
2316
2315
  assert(suggestions[0].capTitle === 'Disbind PDF', 'Should have cap title');
2317
2316
 
2318
2317
  // Nightly cap should also surface a suggestion, with channel set to nightly.
2319
- const prettyCap = 'cap:in="media:json";pretty;out="media:json;textable"';
2318
+ const prettyCap = 'cap:in="media:json";pretty;out="media:fmt=json"';
2320
2319
  const nightlySuggestions = client.getSuggestionsForCap(prettyCap);
2321
2320
  assert(nightlySuggestions.length === 1, 'Should find nightly cap suggestion');
2322
2321
  assert(nightlySuggestions[0].channel === 'nightly', 'Should report nightly channel');
@@ -2425,7 +2424,7 @@ function test335_cartridgeRepoServerClientIntegration() {
2425
2424
  assert(cartridge.buildForPlatform('darwin-arm64') !== null, 'Cartridge should have darwin-arm64 build');
2426
2425
 
2427
2426
  // Client can get suggestions
2428
- const capUrn = 'cap:in="media:pdf";disbind;out="media:disbound-page;textable;list"';
2427
+ const capUrn = 'cap:in="media:ext=pdf";disbind;out="media:disbound-page;enc=utf-8;list"';
2429
2428
  const suggestions = client.getSuggestionsForCap(capUrn);
2430
2429
  assert(suggestions.length === 1, 'Should get suggestions');
2431
2430
  assert(suggestions[0].cartridgeId === 'pdfcartridge', 'Should suggest correct cartridge');
@@ -2437,6 +2436,314 @@ function test335_cartridgeRepoServerClientIntegration() {
2437
2436
  assert(searchResults[0].id === cartridge.id, 'Search and client should agree');
2438
2437
  }
2439
2438
 
2439
+ // ============================================================================
2440
+ // Host-compatibility resolution (cartridge_repo.rs: TEST1849-TEST1853)
2441
+ // ============================================================================
2442
+
2443
+ // Construct a cartridge whose versions/platform-builds are fully specified.
2444
+ // `versions` is given newest-first as [version, [[platform, format, pkgName], ...]];
2445
+ // `version` (the "latest" field) is set to the first entry. Mirrors the Rust
2446
+ // test helper cartridge_with_versions.
2447
+ function cartridgeWithVersions(id, versions) {
2448
+ const versionMap = {};
2449
+ const available = [];
2450
+ for (const [ver, builds] of versions) {
2451
+ available.push(ver);
2452
+ versionMap[ver] = {
2453
+ releaseDate: '2026-02-07',
2454
+ changelog: [],
2455
+ minAppVersion: '',
2456
+ builds: builds.map(([plat, fmt, name]) => ({
2457
+ platform: plat,
2458
+ packages: [{
2459
+ name,
2460
+ sha256: 'deadbeef',
2461
+ size: 4242,
2462
+ url: `https://cartridges.machinefabric.com/${name}`,
2463
+ format: fmt,
2464
+ }],
2465
+ })),
2466
+ };
2467
+ }
2468
+ const latest = versions.length > 0 ? versions[0][0] : '';
2469
+ return new CartridgeInfo({
2470
+ id,
2471
+ name: id,
2472
+ version: latest,
2473
+ description: '',
2474
+ author: '',
2475
+ teamId: 'TEAM123',
2476
+ signedAt: '2026-02-07T00:00:00Z',
2477
+ minAppVersion: '',
2478
+ pageUrl: '',
2479
+ categories: [],
2480
+ tags: [],
2481
+ cap_groups: [],
2482
+ versions: versionMap,
2483
+ availableVersions: available,
2484
+ channel: 'release',
2485
+ registryUrl: 'https://example.com/cartridges',
2486
+ });
2487
+ }
2488
+
2489
+ // TEST1849: latest version has a host build → Compatible, resolving to the
2490
+ // latest version and that platform's native-format package.
2491
+ function test1849_resolveForHostCompatibleLatest() {
2492
+ const cartridge = cartridgeWithVersions('c', [
2493
+ ['1.2.0', [['darwin-arm64', 'pkg', 'c-1.2.0.pkg'], ['linux-x86_64', 'deb', 'c-1.2.0.deb']]],
2494
+ ['1.1.0', [['darwin-arm64', 'pkg', 'c-1.1.0.pkg']]],
2495
+ ]);
2496
+
2497
+ const r = cartridge.resolveForHost('linux-x86_64');
2498
+ assertEqual(r.status, CompatStatus.COMPATIBLE, 'latest with host build is Compatible');
2499
+ assertEqual(r.resolvedVersion, '1.2.0', 'resolves to the latest version');
2500
+ assertEqual(r.resolvedPackage.name, 'c-1.2.0.deb', 'resolves to native-format package');
2501
+ assertEqual(r.resolvedPackage.format, 'deb', 'package format is deb');
2502
+ assert(r.reason === null, 'Compatible carries no reason');
2503
+ assertEqual(r.hostPlatform, 'linux-x86_64', 'host platform echoed back');
2504
+ }
2505
+
2506
+ // TEST1850: the latest version lacks a host build but an older version has one
2507
+ // → CompatibleOutdated, resolving to the NEWEST older version with a host build
2508
+ // (not the oldest), with a reason naming both the latest and the resolved.
2509
+ function test1850_resolveForHostCompatibleOutdated() {
2510
+ const cartridge = cartridgeWithVersions('c', [
2511
+ ['1.3.0', [['darwin-arm64', 'pkg', 'c-1.3.0.pkg']]],
2512
+ ['1.2.0', [['darwin-arm64', 'pkg', 'c-1.2.0.pkg'], ['linux-x86_64', 'deb', 'c-1.2.0.deb']]],
2513
+ ['1.1.0', [['linux-x86_64', 'deb', 'c-1.1.0.deb']]],
2514
+ ]);
2515
+
2516
+ const r = cartridge.resolveForHost('linux-x86_64');
2517
+ assertEqual(r.status, CompatStatus.COMPATIBLE_OUTDATED, 'latest lacks host build → CompatibleOutdated');
2518
+ assertEqual(r.resolvedVersion, '1.2.0', 'newest-with-host-build is 1.2.0, not oldest 1.1.0');
2519
+ assertEqual(r.resolvedPackage.name, 'c-1.2.0.deb', 'resolves to 1.2.0 deb package');
2520
+ assert(r.reason !== null, 'outdated carries a reason');
2521
+ assert(r.reason.includes('1.3.0'), `reason names the latest: ${r.reason}`);
2522
+ assert(r.reason.includes('1.2.0'), `reason names the resolved: ${r.reason}`);
2523
+ }
2524
+
2525
+ // TEST1851: no version ships a host build → Incompatible, no resolved
2526
+ // version/package, reason states the host platform.
2527
+ function test1851_resolveForHostIncompatible() {
2528
+ const cartridge = cartridgeWithVersions('c', [
2529
+ ['1.2.0', [['darwin-arm64', 'pkg', 'c-1.2.0.pkg']]],
2530
+ ['1.1.0', [['darwin-arm64', 'pkg', 'c-1.1.0.pkg']]],
2531
+ ]);
2532
+
2533
+ const r = cartridge.resolveForHost('windows-x86_64');
2534
+ assertEqual(r.status, CompatStatus.INCOMPATIBLE, 'no host build → Incompatible');
2535
+ assert(r.resolvedVersion === null, 'no resolved version when Incompatible');
2536
+ assert(r.resolvedPackage === null, 'no resolved package when Incompatible');
2537
+ assert(r.reason.includes('windows-x86_64'), `reason names the host platform: ${r.reason}`);
2538
+ }
2539
+
2540
+ // TEST1852: a host build whose packages[] is empty AND has no legacy `package`
2541
+ // ships no installer; resolution must SKIP it (not resolve to an un-downloadable
2542
+ // version) and fall through to an older usable version.
2543
+ function test1852_resolveForHostSkipsBuildWithNoInstaller() {
2544
+ const cartridge = cartridgeWithVersions('c', [
2545
+ ['2.0.0', [['linux-x86_64', 'deb', 'c-2.0.0.deb']]],
2546
+ ['1.0.0', [['linux-x86_64', 'deb', 'c-1.0.0.deb']]],
2547
+ ]);
2548
+ // Make 2.0.0's linux build ship nothing installable.
2549
+ const v2 = cartridge.versions['2.0.0'];
2550
+ v2.builds[0].packages = [];
2551
+ delete v2.builds[0].package;
2552
+
2553
+ const r = cartridge.resolveForHost('linux-x86_64');
2554
+ // 2.0.0 is skipped (no installer); newest USABLE host build is 1.0.0.
2555
+ assertEqual(r.status, CompatStatus.COMPATIBLE_OUTDATED, '2.0.0 skipped → CompatibleOutdated to 1.0.0');
2556
+ assertEqual(r.resolvedVersion, '1.0.0', 'resolves to the older usable version');
2557
+ assertEqual(r.resolvedPackage.name, 'c-1.0.0.deb', 'resolves to 1.0.0 deb package');
2558
+ }
2559
+
2560
+ // TEST1853: hostPlatform() returns a normalized {os}-{arch} string with arch
2561
+ // aarch64/arm64 mapped to arm64 — the exact form the registry uses.
2562
+ function test1853_hostPlatformNormalizedForm() {
2563
+ const p = hostPlatform();
2564
+ const dash = p.indexOf('-');
2565
+ assert(dash > 0, `host_platform must be os-arch, got ${p}`);
2566
+ const os = p.slice(0, dash);
2567
+ const arch = p.slice(dash + 1);
2568
+ assert((os === 'darwin' || os === 'linux' || os === 'windows') || os.length > 0, `os segment present: ${os}`);
2569
+ // The registry never uses the raw "aarch64"; it must be normalized.
2570
+ assert(arch !== 'aarch64', 'arch must be normalized to arm64');
2571
+ }
2572
+
2573
+ // ============================================================================
2574
+ // Build-env registry identity (manifest.rs: TEST1872-TEST1874)
2575
+ // ============================================================================
2576
+
2577
+ // TEST1872: a non-empty MFR_CARTRIDGE_REGISTRY_URL passes through verbatim —
2578
+ // a published build reports exactly the URL it was compiled with.
2579
+ function test1872_registryUrlFromBuildEnvPassesThroughNonempty() {
2580
+ const url = 'https://cartridges.machinefabric.com/manifest';
2581
+ assertEqual(registryUrlFromBuildEnv(url), url, 'non-empty URL passes through verbatim');
2582
+ }
2583
+
2584
+ // TEST1873: an unset env (null/undefined) yields null — a dev build has no
2585
+ // baked registry and loads only `dev/` cartridges.
2586
+ function test1873_registryUrlFromBuildEnvNoneForDev() {
2587
+ assert(registryUrlFromBuildEnv(null) === null, 'null env → null (dev build)');
2588
+ assert(registryUrlFromBuildEnv(undefined) === null, 'absent env → null (dev build)');
2589
+ }
2590
+
2591
+ // TEST1874: an exported-but-empty env ('') is neither a dev build nor a valid
2592
+ // identity and MUST fail hard, so the build can never silently hash the empty
2593
+ // string into a fake registry slug.
2594
+ function test1874_registryUrlFromBuildEnvRejectsEmptyString() {
2595
+ let threw = false;
2596
+ try {
2597
+ registryUrlFromBuildEnv('');
2598
+ } catch (e) {
2599
+ threw = true;
2600
+ assert(e.message.includes('MFR_CARTRIDGE_REGISTRY_URL must be unset'), `panic message names the variable: ${e.message}`);
2601
+ }
2602
+ assert(threw, 'empty string must fail hard');
2603
+ }
2604
+
2605
+ // ============================================================================
2606
+ // Cartridge discovery (cartridge_discovery.rs: TEST1875-TEST1878)
2607
+ // ============================================================================
2608
+
2609
+ // Lay down `{root}/{slug}/{channel}/{name}/{version}/`. When `cartridgeJson`
2610
+ // is provided, also write it plus an executable `entry` binary so readFromDir
2611
+ // accepts the directory and discovery reaches its own identity checks. Mirrors
2612
+ // the Rust test helper install_fixture.
2613
+ function discoveryInstallFixture(root, slug, channelFolder, name, version, cartridgeJson, entry) {
2614
+ const fs = require('fs');
2615
+ const path = require('path');
2616
+ const dir = path.join(root, slug, channelFolder, name, version);
2617
+ fs.mkdirSync(dir, { recursive: true });
2618
+ if (cartridgeJson !== null && cartridgeJson !== undefined) {
2619
+ fs.writeFileSync(path.join(dir, 'cartridge.json'), cartridgeJson);
2620
+ const entryPath = path.join(dir, entry);
2621
+ fs.writeFileSync(entryPath, '#!/bin/sh\nexit 0\n');
2622
+ fs.chmodSync(entryPath, 0o755);
2623
+ }
2624
+ }
2625
+
2626
+ function discoveryDevCartridgeJson(channel, fabricManifestVersion) {
2627
+ return JSON.stringify({
2628
+ name: 'cart', version: '1.0.0', channel, registry_url: null, entry: 'cart',
2629
+ installed_at: '2024-01-01T00:00:00Z', fabric_manifest_version: fabricManifestVersion,
2630
+ });
2631
+ }
2632
+
2633
+ function discoveryRegistryCartridgeJson(url, channel, fmv) {
2634
+ return JSON.stringify({
2635
+ name: 'cart', version: '1.0.0', channel, registry_url: url, entry: 'cart',
2636
+ installed_at: '2024-01-01T00:00:00Z', fabric_manifest_version: fmv,
2637
+ });
2638
+ }
2639
+
2640
+ function discoveryMakeTempRoot() {
2641
+ const fs = require('fs');
2642
+ const os = require('os');
2643
+ const path = require('path');
2644
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'capdag-disc-'));
2645
+ }
2646
+
2647
+ function discoveryRemoveRoot(root) {
2648
+ const fs = require('fs');
2649
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch (_e) { /* best effort */ }
2650
+ }
2651
+
2652
+ function discoveryExpectIncompatible(out, kind) {
2653
+ assertEqual(out.length, 1, 'expected exactly one discovered entry');
2654
+ assertEqual(out[0].kind, 'incompatible', 'entry must be incompatible');
2655
+ assertEqual(out[0].error.kind, kind, `wrong attachment-error kind: ${out[0].error.message}`);
2656
+ }
2657
+
2658
+ // TEST1875: scan-all — a registry slug folder AND the dev slot present on disk
2659
+ // are BOTH scanned, regardless of the host's own baked registry. Both fixtures
2660
+ // lack a real bifaci binary, so both end at HandshakeFailed — proving discovery
2661
+ // REACHED them (was not filtered out by a registry pin). A registry-pin
2662
+ // rejection would instead surface BadInstallation and never probe.
2663
+ async function test1875_scanAllReachesBothDevAndRegistrySlugs() {
2664
+ const root = discoveryMakeTempRoot();
2665
+ try {
2666
+ const url = 'https://cartridges.example.com/manifest';
2667
+ const rslug = slugForSync(url);
2668
+ // Host baked for a DIFFERENT registry than the on-disk registry cartridge.
2669
+ const host = new DiscoveryIdentity({ channel: 'nightly', registryUrl: 'https://other.example.com/manifest', fabricManifestVersion: 1 });
2670
+ discoveryInstallFixture(root, 'dev', 'nightly', 'devcart', '1.0.0', discoveryDevCartridgeJson('nightly', 1), 'cart');
2671
+ discoveryInstallFixture(root, rslug, 'nightly', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'nightly', 1), 'cart');
2672
+ const out = await discoverCartridges(root, host);
2673
+ assertEqual(out.length, 2, `both slugs must be scanned, got ${out.length}`);
2674
+ for (const c of out) {
2675
+ assertEqual(c.kind, 'incompatible', 'both reach the probe stage');
2676
+ assertEqual(c.error.kind, CartridgeAttachmentErrorKind.HANDSHAKE_FAILED,
2677
+ `both reached the probe (not registry-pin-rejected): ${c.error.message}`);
2678
+ }
2679
+ } finally {
2680
+ discoveryRemoveRoot(root);
2681
+ }
2682
+ }
2683
+
2684
+ // TEST1876: only the host's channel subtree is scanned. A cartridge under a
2685
+ // slug's `release/` folder is invisible to a nightly host even though the slug
2686
+ // folder is present (its `nightly/` subtree is absent).
2687
+ async function test1876_otherChannelSubtreeIsSkipped() {
2688
+ const root = discoveryMakeTempRoot();
2689
+ try {
2690
+ const url = 'https://cartridges.example.com/manifest';
2691
+ const rslug = slugForSync(url);
2692
+ discoveryInstallFixture(root, rslug, 'release', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'release', 1), 'cart');
2693
+ const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
2694
+ assertEqual(out.length, 0, 'a release-only slug must be invisible to a nightly host');
2695
+ } finally {
2696
+ discoveryRemoveRoot(root);
2697
+ }
2698
+ }
2699
+
2700
+ // TEST1877: a registry cartridge hand-copied under the WRONG registry slug
2701
+ // folder fails the three-place rule (BadInstallation) — scan-all does not mean
2702
+ // "accept anywhere"; placement must still be self-consistent.
2703
+ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
2704
+ const root = discoveryMakeTempRoot();
2705
+ try {
2706
+ const url = 'https://cartridges.example.com/manifest';
2707
+ const wrongSlug = slugForSync('https://somewhere-else.example.com/manifest');
2708
+ const json = discoveryRegistryCartridgeJson(url, 'nightly', 1);
2709
+ discoveryInstallFixture(root, wrongSlug, 'nightly', 'cart', '1.0.0', json, 'cart');
2710
+ const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
2711
+ discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
2712
+ } finally {
2713
+ discoveryRemoveRoot(root);
2714
+ }
2715
+ }
2716
+
2717
+ // TEST1878: a cartridge marked `installed_from: bundle` with no baked hash in
2718
+ // BUNDLED_PROVIDER_HASHES (empty in this mirror) is rejected as BadInstallation
2719
+ // — the bundled-integrity gate fires before the probe. Non-macOS only: on macOS
2720
+ // the baked-hash path is intentionally absent (OS code-signature is the guard),
2721
+ // so a bundled provider is accepted there and would instead end at the probe.
2722
+ async function test1878_bundledProviderWithoutBakedHashIsRejected() {
2723
+ if (process.platform === 'darwin') {
2724
+ // Mirrors the Rust #[cfg(not(target_os = "macos"))] gate: on macOS the
2725
+ // baked-hash path does not exist, so this scenario is not exercised.
2726
+ return;
2727
+ }
2728
+ const root = discoveryMakeTempRoot();
2729
+ try {
2730
+ // Dev slug (null registry) but installed_from=bundle — placement is
2731
+ // self-consistent (null→dev), so it passes readFromDir and reaches the
2732
+ // bundled-hash gate, which has no baked entry → BadInstallation.
2733
+ const json = JSON.stringify({
2734
+ name: 'cart', version: '1.0.0', channel: 'nightly', registry_url: null, entry: 'cart',
2735
+ installed_at: '2024-01-01T00:00:00Z', installed_from: 'bundle', fabric_manifest_version: 1,
2736
+ });
2737
+ discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
2738
+ const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
2739
+ discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
2740
+ assert(out[0].error.message.includes('bundled provider integrity'),
2741
+ `message should name the bundled-integrity failure: ${out[0].error.message}`);
2742
+ } finally {
2743
+ discoveryRemoveRoot(root);
2744
+ }
2745
+ }
2746
+
2440
2747
  // ============================================================================
2441
2748
  // media_urn.rs: TEST1294-TEST1302 (MediaUrn predicates)
2442
2749
  // ============================================================================
@@ -2521,37 +2828,36 @@ function test0086_isCollection() {
2521
2828
 
2522
2829
  // TEST1302: predicates are consistent with constants — every constant triggers exactly the expected predicates
2523
2830
  function test1302_predicateConstantConsistency() {
2524
- // MEDIA_INTEGER must be numeric, text, scalar, NOT binary/bool/image/audio/video
2831
+ // MEDIA_INTEGER must be numeric, scalar, NOT enc-bearing/bool/image/list.
2832
+ // Integers carry no enc= (a number is not a character-encoded string).
2525
2833
  const intUrn = MediaUrn.fromString(MEDIA_INTEGER);
2526
2834
  assert(intUrn.isNumeric(), 'MEDIA_INTEGER must be numeric');
2527
- assert(intUrn.isText(), 'MEDIA_INTEGER must be text');
2835
+ assert(intUrn.getTag('enc') === undefined, 'MEDIA_INTEGER must not carry enc');
2528
2836
  assert(intUrn.isScalar(), 'MEDIA_INTEGER must be scalar');
2529
- assert(!intUrn.isBinary(), 'MEDIA_INTEGER must not be binary');
2530
2837
  assert(!intUrn.isBool(), 'MEDIA_INTEGER must not be bool');
2531
2838
  assert(!intUrn.isImage(), 'MEDIA_INTEGER must not be image');
2532
2839
  assert(!intUrn.isList(), 'MEDIA_INTEGER must not be list');
2533
2840
 
2534
- // MEDIA_BOOLEAN must be bool, text, scalar, NOT numeric
2841
+ // MEDIA_BOOLEAN must be bool, enc-bearing, scalar, NOT numeric
2535
2842
  const boolUrn = MediaUrn.fromString(MEDIA_BOOLEAN);
2536
2843
  assert(boolUrn.isBool(), 'MEDIA_BOOLEAN must be bool');
2537
- assert(boolUrn.isText(), 'MEDIA_BOOLEAN must be text');
2844
+ assert(boolUrn.getTag('enc') === 'utf-8', 'MEDIA_BOOLEAN must carry enc=utf-8');
2538
2845
  assert(boolUrn.isScalar(), 'MEDIA_BOOLEAN must be scalar');
2539
2846
  assert(!boolUrn.isNumeric(), 'MEDIA_BOOLEAN must not be numeric');
2540
2847
 
2541
- // MEDIA_JSON must be json, text, record, scalar, NOT binary
2848
+ // MEDIA_JSON must be json, record, scalar, NOT list — and carries no enc
2849
+ // (fmt= is the content-format axis, orthogonal to enc=).
2542
2850
  const jsonUrn = MediaUrn.fromString(MEDIA_JSON);
2543
2851
  assert(jsonUrn.isJson(), 'MEDIA_JSON must be json');
2544
- assert(jsonUrn.isText(), 'MEDIA_JSON must be text');
2852
+ assert(jsonUrn.getTag('enc') === undefined, 'MEDIA_JSON must not carry enc');
2545
2853
  assert(jsonUrn.isRecord(), 'MEDIA_JSON must be record');
2546
2854
  assert(jsonUrn.isScalar(), 'MEDIA_JSON must be scalar (no list marker)');
2547
- assert(!jsonUrn.isBinary(), 'MEDIA_JSON must not be binary');
2548
2855
  assert(!jsonUrn.isList(), 'MEDIA_JSON must not be list');
2549
2856
 
2550
- // MEDIA_VOID is void, NOT text/numeric but IS binary (no textable tag)
2857
+ // MEDIA_VOID is void, NOT numeric, and carries no enc tag
2551
2858
  const voidUrn = MediaUrn.fromString(MEDIA_VOID);
2552
2859
  assert(voidUrn.isVoid(), 'MEDIA_VOID must be void');
2553
- assert(!voidUrn.isText(), 'MEDIA_VOID must not be text');
2554
- assert(voidUrn.isBinary(), 'MEDIA_VOID must be binary (no textable tag)');
2860
+ assert(voidUrn.getTag('enc') === undefined, 'MEDIA_VOID must not carry enc');
2555
2861
  assert(!voidUrn.isNumeric(), 'MEDIA_VOID must not be numeric');
2556
2862
  }
2557
2863
 
@@ -2593,13 +2899,13 @@ function test1304_withInOutSpec() {
2593
2899
  assertEqual(changedOut.getOutSpec(), 'media:string', 'withOutSpec should change outSpec');
2594
2900
 
2595
2901
  // Chain both
2596
- const changedBoth = cap.withInSpec('media:pdf').withOutSpec(MEDIA_TXT);
2597
- assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
2598
- assertEqual(changedBoth.getOutSpec(), 'media:ext=txt;textable', 'Chain should set outSpec');
2902
+ const changedBoth = cap.withInSpec('media:ext=pdf').withOutSpec(MEDIA_TXT);
2903
+ assertEqual(changedBoth.getInSpec(), 'media:ext=pdf', 'Chain should set inSpec');
2904
+ assertEqual(changedBoth.getOutSpec(), 'media:enc=utf-8;ext=txt', 'Chain should set outSpec');
2599
2905
 
2600
2906
  const identity = CapUrn.fromString('cap:effect=none');
2601
2907
  assertThrows(
2602
- () => identity.withOutSpec('media:pdf'),
2908
+ () => identity.withOutSpec('media:ext=pdf'),
2603
2909
  ErrorCodes.ILLEGAL_DECLARATION,
2604
2910
  'withOutSpec must revalidate admissibility'
2605
2911
  );
@@ -2882,7 +3188,7 @@ function test652_capIdentityConstantWorks() {
2882
3188
  // TEST653: invalid effect=none declarations fail at construction.
2883
3189
  function test653_invalidEffectNoneDeclarationRejected() {
2884
3190
  assertThrows(
2885
- () => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
3191
+ () => CapUrn.fromString('cap:in="media:ext=pdf";effect=none;out="media:enc=utf-8"'),
2886
3192
  ErrorCodes.ILLEGAL_DECLARATION,
2887
3193
  'invalid effect=none declaration must fail at construction'
2888
3194
  );
@@ -2892,7 +3198,7 @@ function test653_invalidEffectNoneDeclarationRejected() {
2892
3198
  function test654_effectNonePreservesRuntimeMedia() {
2893
3199
  const decimate = CapUrn.fromString('cap:decimate-sequence;effect=none');
2894
3200
  const png = MediaUrn.fromString('media:image;png');
2895
- const pdf = MediaUrn.fromString('media:pdf');
3201
+ const pdf = MediaUrn.fromString('media:ext=pdf');
2896
3202
  assertEqual(decimate.inferRuntimeOutputMedia(png).toString(), png.toString(), 'effect=none should preserve png');
2897
3203
  assertEqual(decimate.inferRuntimeOutputMedia(pdf).toString(), pdf.toString(), 'effect=none should preserve pdf');
2898
3204
  }
@@ -2911,7 +3217,7 @@ function test655_effectDeclaredUsesDeclaredOutput() {
2911
3217
  // TEST656: invalid effect=none declarations fail hard at construction.
2912
3218
  function test656_invalidEffectNoneFailsHard() {
2913
3219
  assertThrows(
2914
- () => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
3220
+ () => CapUrn.fromString('cap:in="media:ext=pdf";effect=none;out="media:enc=utf-8"'),
2915
3221
  ErrorCodes.ILLEGAL_DECLARATION,
2916
3222
  'invalid effect=none declaration must fail at construction'
2917
3223
  );
@@ -2944,7 +3250,7 @@ function test0088_Machine_whitespaceOnly() {
2944
3250
  // TEST0089: Machine header only no wirings
2945
3251
  function test0089_Machine_headerOnlyNoWirings() {
2946
3252
  assertThrowsWithCode(
2947
- () => Machine.fromString('[extract cap:in="media:pdf";extract;out="media:txt;textable"]'),
3253
+ () => Machine.fromString('[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]'),
2948
3254
  MachineSyntaxErrorCodes.NO_EDGES
2949
3255
  );
2950
3256
  }
@@ -2953,8 +3259,8 @@ function test0089_Machine_headerOnlyNoWirings() {
2953
3259
  function test0090_Machine_duplicateAlias() {
2954
3260
  assertThrowsWithCode(
2955
3261
  () => Machine.fromString(
2956
- '[ex cap:in="media:pdf";extract;out="media:txt;textable"]' +
2957
- '[ex cap:in="media:pdf";summarize;out="media:txt;textable"]' +
3262
+ '[ex cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3263
+ '[ex cap:in="media:ext=pdf";summarize;out="media:enc=utf-8;ext=txt"]' +
2958
3264
  '[a -> ex -> b]'
2959
3265
  ),
2960
3266
  MachineSyntaxErrorCodes.DUPLICATE_ALIAS
@@ -2964,40 +3270,40 @@ function test0090_Machine_duplicateAlias() {
2964
3270
  // TEST0094: Machine simple linear chain
2965
3271
  function test0094_Machine_simpleLinearChain() {
2966
3272
  const g = Machine.fromString(
2967
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
3273
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
2968
3274
  '[doc -> extract -> text]'
2969
3275
  );
2970
3276
  assertEqual(g.edgeCount(), 1);
2971
3277
  const edge = g.edges()[0];
2972
3278
  assertEqual(edge.sources.length, 1);
2973
- assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:pdf')),
2974
- 'Source should be media:pdf');
2975
- assert(edge.target.isEquivalent(MediaUrn.fromString('media:txt;textable')),
2976
- 'Target should be media:txt;textable');
3279
+ assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:ext=pdf')),
3280
+ 'Source should be media:ext=pdf');
3281
+ assert(edge.target.isEquivalent(MediaUrn.fromString('media:enc=utf-8;ext=txt')),
3282
+ 'Target should be media:enc=utf-8;ext=txt');
2977
3283
  assertEqual(edge.isLoop, false);
2978
3284
  }
2979
3285
 
2980
3286
  // TEST0095: Machine two step chain
2981
3287
  function test0095_Machine_twoStepChain() {
2982
3288
  const g = Machine.fromString(
2983
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
2984
- '[embed cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"]' +
3289
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3290
+ '[embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"]' +
2985
3291
  '[doc -> extract -> text]' +
2986
3292
  '[text -> embed -> vectors]'
2987
3293
  );
2988
3294
  assertEqual(g.edgeCount(), 2);
2989
- assert(g.edges()[0].sources[0].isEquivalent(MediaUrn.fromString('media:pdf')),
2990
- 'First edge source should be media:pdf');
2991
- assert(g.edges()[1].target.isEquivalent(MediaUrn.fromString('media:embedding-vector;record;textable')),
2992
- 'Second edge target should be media:embedding-vector;record;textable');
3295
+ assert(g.edges()[0].sources[0].isEquivalent(MediaUrn.fromString('media:ext=pdf')),
3296
+ 'First edge source should be media:ext=pdf');
3297
+ assert(g.edges()[1].target.isEquivalent(MediaUrn.fromString('media:embedding-vector;enc=utf-8;record')),
3298
+ 'Second edge target should be media:embedding-vector;enc=utf-8;record');
2993
3299
  }
2994
3300
 
2995
3301
  // TEST0096: Machine fan out
2996
3302
  function test0096_Machine_fanOut() {
2997
3303
  const g = Machine.fromString(
2998
- '[meta cap:in="media:pdf";extract-metadata;out="media:file-metadata;record;textable"]' +
2999
- '[outline cap:in="media:pdf";extract-outline;out="media:document-outline;record;textable"]' +
3000
- '[thumb cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"]' +
3304
+ '[meta cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"]' +
3305
+ '[outline cap:in="media:ext=pdf";extract-outline;out="media:document-outline;enc=utf-8;record"]' +
3306
+ '[thumb cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"]' +
3001
3307
  '[doc -> meta -> metadata]' +
3002
3308
  '[doc -> outline -> outline_data]' +
3003
3309
  '[doc -> thumb -> thumbnail]'
@@ -3005,17 +3311,17 @@ function test0096_Machine_fanOut() {
3005
3311
  assertEqual(g.edgeCount(), 3);
3006
3312
  for (const edge of g.edges()) {
3007
3313
  assertEqual(edge.sources.length, 1);
3008
- assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:pdf')),
3009
- 'All fan-out sources should be media:pdf');
3314
+ assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:ext=pdf')),
3315
+ 'All fan-out sources should be media:ext=pdf');
3010
3316
  }
3011
3317
  }
3012
3318
 
3013
3319
  // TEST0097: Machine fan in secondary assigned by prior wiring
3014
3320
  function test0097_Machine_fanInSecondaryAssignedByPriorWiring() {
3015
3321
  const g = Machine.fromString(
3016
- '[thumb cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"]' +
3017
- '[model_dl cap:in="media:model-spec;textable";download;out="media:model-spec;textable"]' +
3018
- '[describe cap:in="media:image;png";describe-image;out="media:image-description;textable"]' +
3322
+ '[thumb cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"]' +
3323
+ '[model_dl cap:in="media:enc=utf-8;model-spec";download;out="media:enc=utf-8;model-spec"]' +
3324
+ '[describe cap:in="media:image;png";describe-image;out="media:enc=utf-8;image-description"]' +
3019
3325
  '[doc -> thumb -> thumbnail]' +
3020
3326
  '[spec_input -> model_dl -> model_spec]' +
3021
3327
  '[(thumbnail, model_spec) -> describe -> description]'
@@ -3027,7 +3333,7 @@ function test0097_Machine_fanInSecondaryAssignedByPriorWiring() {
3027
3333
  // TEST0098: Machine fan in secondary unassigned gets wildcard
3028
3334
  function test0098_Machine_fanInSecondaryUnassignedGetsWildcard() {
3029
3335
  const g = Machine.fromString(
3030
- '[describe cap:in="media:image;png";describe-image;out="media:image-description;textable"]\n' +
3336
+ '[describe cap:in="media:image;png";describe-image;out="media:enc=utf-8;image-description"]\n' +
3031
3337
  '[(thumbnail, model_spec) -> describe -> description]'
3032
3338
  );
3033
3339
  assertEqual(g.edges().length, 1);
@@ -3039,7 +3345,7 @@ function test0098_Machine_fanInSecondaryUnassignedGetsWildcard() {
3039
3345
  // TEST0111: Machine loop edge
3040
3346
  function test0111_Machine_loopEdge() {
3041
3347
  const g = Machine.fromString(
3042
- '[p2t cap:in="media:disbound-page;textable";page-to-text;out="media:txt;textable"]' +
3348
+ '[p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"]' +
3043
3349
  '[pages -> LOOP p2t -> texts]'
3044
3350
  );
3045
3351
  assertEqual(g.edgeCount(), 1);
@@ -3058,7 +3364,7 @@ function test0112_Machine_undefinedAliasFails() {
3058
3364
  function test0113_Machine_nodeAliasCollision() {
3059
3365
  assertThrowsWithCode(
3060
3366
  () => Machine.fromString(
3061
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
3367
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3062
3368
  '[extract -> extract -> text]'
3063
3369
  ),
3064
3370
  MachineSyntaxErrorCodes.NODE_ALIAS_COLLISION
@@ -3069,8 +3375,8 @@ function test0113_Machine_nodeAliasCollision() {
3069
3375
  function test0114_Machine_conflictingMediaTypesFail() {
3070
3376
  assertThrowsWithCode(
3071
3377
  () => Machine.fromString(
3072
- '[cap1 cap:in="media:txt;textable";a;out="media:pdf"]' +
3073
- '[cap2 cap:in="media:audio;wav";b;out="media:txt;textable"]' +
3378
+ '[cap1 cap:in="media:enc=utf-8;ext=txt";a;out="media:ext=pdf"]' +
3379
+ '[cap2 cap:in="media:audio;wav";b;out="media:enc=utf-8;ext=txt"]' +
3074
3380
  '[src -> cap1 -> mid]' +
3075
3381
  '[mid -> cap2 -> dst]'
3076
3382
  ),
@@ -3081,8 +3387,8 @@ function test0114_Machine_conflictingMediaTypesFail() {
3081
3387
  // TEST0117: Machine multiline format
3082
3388
  function test0117_Machine_multilineFormat() {
3083
3389
  const g = Machine.fromString(
3084
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]\n' +
3085
- '[embed cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"]\n' +
3390
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]\n' +
3391
+ '[embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"]\n' +
3086
3392
  '[doc -> extract -> text]\n' +
3087
3393
  '[text -> embed -> vectors]\n'
3088
3394
  );
@@ -3092,11 +3398,11 @@ function test0117_Machine_multilineFormat() {
3092
3398
  // TEST0118: Machine different aliases same graph
3093
3399
  function test0118_Machine_differentAliasesSameGraph() {
3094
3400
  const g1 = Machine.fromString(
3095
- '[ex cap:in="media:pdf";extract;out="media:txt;textable"]' +
3401
+ '[ex cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3096
3402
  '[a -> ex -> b]'
3097
3403
  );
3098
3404
  const g2 = Machine.fromString(
3099
- '[xt cap:in="media:pdf";extract;out="media:txt;textable"]' +
3405
+ '[xt cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3100
3406
  '[x -> xt -> y]'
3101
3407
  );
3102
3408
  assert(g1.isEquivalent(g2), 'Different aliases should produce equivalent graphs');
@@ -3113,7 +3419,7 @@ function test0119_Machine_malformedInputFails() {
3113
3419
  // TEST0120: Machine unterminated bracket fails
3114
3420
  function test0120_Machine_unterminatedBracketFails() {
3115
3421
  assertThrowsWithCode(
3116
- () => parseMachine('[extract cap:in=media:pdf'),
3422
+ () => parseMachine('[extract cap:in=media:ext=pdf'),
3117
3423
  MachineSyntaxErrorCodes.PARSE_ERROR
3118
3424
  );
3119
3425
  }
@@ -3122,22 +3428,22 @@ function test0120_Machine_unterminatedBracketFails() {
3122
3428
 
3123
3429
  function test0121_Machine_lineBasedSimpleChain() {
3124
3430
  const g = Machine.fromString(
3125
- 'extract cap:in="media:pdf";extract;out="media:txt;textable"\n' +
3431
+ 'extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"\n' +
3126
3432
  'doc -> extract -> text'
3127
3433
  );
3128
3434
  assertEqual(g.edgeCount(), 1);
3129
3435
  const edge = g.edges()[0];
3130
- assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:pdf')),
3131
- 'Source should be media:pdf');
3132
- assert(edge.target.isEquivalent(MediaUrn.fromString('media:txt;textable')),
3133
- 'Target should be media:txt;textable');
3436
+ assert(edge.sources[0].isEquivalent(MediaUrn.fromString('media:ext=pdf')),
3437
+ 'Source should be media:ext=pdf');
3438
+ assert(edge.target.isEquivalent(MediaUrn.fromString('media:enc=utf-8;ext=txt')),
3439
+ 'Target should be media:enc=utf-8;ext=txt');
3134
3440
  }
3135
3441
 
3136
3442
  // TEST0122: Machine line based two step chain
3137
3443
  function test0122_Machine_lineBasedTwoStepChain() {
3138
3444
  const g = Machine.fromString(
3139
- 'extract cap:in="media:pdf";extract;out="media:txt;textable"\n' +
3140
- 'embed cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"\n' +
3445
+ 'extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"\n' +
3446
+ 'embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"\n' +
3141
3447
  'doc -> extract -> text\n' +
3142
3448
  'text -> embed -> vectors'
3143
3449
  );
@@ -3147,7 +3453,7 @@ function test0122_Machine_lineBasedTwoStepChain() {
3147
3453
  // TEST0123: Machine line based loop
3148
3454
  function test0123_Machine_lineBasedLoop() {
3149
3455
  const g = Machine.fromString(
3150
- 'p2t cap:in="media:disbound-page;textable";page-to-text;out="media:txt;textable"\n' +
3456
+ 'p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"\n' +
3151
3457
  'pages -> LOOP p2t -> texts'
3152
3458
  );
3153
3459
  assertEqual(g.edgeCount(), 1);
@@ -3157,9 +3463,9 @@ function test0123_Machine_lineBasedLoop() {
3157
3463
  // TEST0124: Machine line based fan in
3158
3464
  function test0124_Machine_lineBasedFanIn() {
3159
3465
  const g = Machine.fromString(
3160
- 'thumb cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"\n' +
3161
- 'model_dl cap:in="media:model-spec;textable";download;out="media:model-spec;textable"\n' +
3162
- 'describe cap:in="media:image;png";describe-image;out="media:image-description;textable"\n' +
3466
+ 'thumb cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"\n' +
3467
+ 'model_dl cap:in="media:enc=utf-8;model-spec";download;out="media:enc=utf-8;model-spec"\n' +
3468
+ 'describe cap:in="media:image;png";describe-image;out="media:enc=utf-8;image-description"\n' +
3163
3469
  'doc -> thumb -> thumbnail\n' +
3164
3470
  'spec_input -> model_dl -> model_spec\n' +
3165
3471
  '(thumbnail, model_spec) -> describe -> description'
@@ -3171,7 +3477,7 @@ function test0124_Machine_lineBasedFanIn() {
3171
3477
  // TEST0125: Machine mixed bracketed and line based
3172
3478
  function test0125_Machine_mixedBracketedAndLineBased() {
3173
3479
  const g = Machine.fromString(
3174
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]\n' +
3480
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]\n' +
3175
3481
  'doc -> extract -> text'
3176
3482
  );
3177
3483
  assertEqual(g.edgeCount(), 1);
@@ -3180,11 +3486,11 @@ function test0125_Machine_mixedBracketedAndLineBased() {
3180
3486
  // TEST0126: Machine line based equivalent to bracketed
3181
3487
  function test0126_Machine_lineBasedEquivalentToBracketed() {
3182
3488
  const g1 = Machine.fromString(
3183
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
3489
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3184
3490
  '[doc -> extract -> text]'
3185
3491
  );
3186
3492
  const g2 = Machine.fromString(
3187
- 'extract cap:in="media:pdf";extract;out="media:txt;textable"\n' +
3493
+ 'extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"\n' +
3188
3494
  'doc -> extract -> text'
3189
3495
  );
3190
3496
  assert(g1.isEquivalent(g2), 'Line-based and bracketed must produce equivalent graphs');
@@ -3194,9 +3500,9 @@ function test0126_Machine_lineBasedEquivalentToBracketed() {
3194
3500
  function test0127_Machine_lineBasedFormatSerialization() {
3195
3501
  const g = new Machine([
3196
3502
  new MachineEdge(
3197
- [MediaUrn.fromString('media:pdf')],
3198
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3199
- MediaUrn.fromString('media:txt;textable'),
3503
+ [MediaUrn.fromString('media:ext=pdf')],
3504
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3505
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3200
3506
  false
3201
3507
  ),
3202
3508
  ]);
@@ -3216,15 +3522,15 @@ function test0127_Machine_lineBasedFormatSerialization() {
3216
3522
  function test0128_Machine_lineBasedAndBracketedParseSameGraph() {
3217
3523
  const g = new Machine([
3218
3524
  new MachineEdge(
3219
- [MediaUrn.fromString('media:pdf')],
3220
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3221
- MediaUrn.fromString('media:txt;textable'),
3525
+ [MediaUrn.fromString('media:ext=pdf')],
3526
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3527
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3222
3528
  false
3223
3529
  ),
3224
3530
  new MachineEdge(
3225
- [MediaUrn.fromString('media:txt;textable')],
3226
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3227
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3531
+ [MediaUrn.fromString('media:enc=utf-8;ext=txt')],
3532
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3533
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3228
3534
  false
3229
3535
  ),
3230
3536
  ]);
@@ -3241,15 +3547,15 @@ function test0128_Machine_lineBasedAndBracketedParseSameGraph() {
3241
3547
 
3242
3548
  function test0129_Machine_edgeEquivalenceSameUrns() {
3243
3549
  const e1 = new MachineEdge(
3244
- [MediaUrn.fromString('media:pdf')],
3245
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3246
- MediaUrn.fromString('media:txt;textable'),
3550
+ [MediaUrn.fromString('media:ext=pdf')],
3551
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3552
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3247
3553
  false
3248
3554
  );
3249
3555
  const e2 = new MachineEdge(
3250
- [MediaUrn.fromString('media:pdf')],
3251
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3252
- MediaUrn.fromString('media:txt;textable'),
3556
+ [MediaUrn.fromString('media:ext=pdf')],
3557
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3558
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3253
3559
  false
3254
3560
  );
3255
3561
  assert(e1.isEquivalent(e2), 'Same URNs should be equivalent');
@@ -3258,15 +3564,15 @@ function test0129_Machine_edgeEquivalenceSameUrns() {
3258
3564
  // TEST0130: Machine edge equivalence different cap urns
3259
3565
  function test0130_Machine_edgeEquivalenceDifferentCapUrns() {
3260
3566
  const e1 = new MachineEdge(
3261
- [MediaUrn.fromString('media:pdf')],
3262
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3263
- MediaUrn.fromString('media:txt;textable'),
3567
+ [MediaUrn.fromString('media:ext=pdf')],
3568
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3569
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3264
3570
  false
3265
3571
  );
3266
3572
  const e2 = new MachineEdge(
3267
- [MediaUrn.fromString('media:pdf')],
3268
- CapUrn.fromString('cap:in="media:pdf";summarize;out="media:txt;textable"'),
3269
- MediaUrn.fromString('media:txt;textable'),
3573
+ [MediaUrn.fromString('media:ext=pdf')],
3574
+ CapUrn.fromString('cap:in="media:ext=pdf";summarize;out="media:enc=utf-8;ext=txt"'),
3575
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3270
3576
  false
3271
3577
  );
3272
3578
  assert(!e1.isEquivalent(e2), 'Different cap URNs should not be equivalent');
@@ -3275,15 +3581,15 @@ function test0130_Machine_edgeEquivalenceDifferentCapUrns() {
3275
3581
  // TEST0131: Machine edge equivalence different targets
3276
3582
  function test0131_Machine_edgeEquivalenceDifferentTargets() {
3277
3583
  const e1 = new MachineEdge(
3278
- [MediaUrn.fromString('media:pdf')],
3279
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3280
- MediaUrn.fromString('media:txt;textable'),
3584
+ [MediaUrn.fromString('media:ext=pdf')],
3585
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3586
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3281
3587
  false
3282
3588
  );
3283
3589
  const e2 = new MachineEdge(
3284
- [MediaUrn.fromString('media:pdf')],
3285
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3286
- MediaUrn.fromString('media:json;record;textable'),
3590
+ [MediaUrn.fromString('media:ext=pdf')],
3591
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3592
+ MediaUrn.fromString('media:fmt=json;record'),
3287
3593
  false
3288
3594
  );
3289
3595
  assert(!e1.isEquivalent(e2), 'Different targets should not be equivalent');
@@ -3292,15 +3598,15 @@ function test0131_Machine_edgeEquivalenceDifferentTargets() {
3292
3598
  // TEST0132: Machine edge equivalence different loop flag
3293
3599
  function test0132_Machine_edgeEquivalenceDifferentLoopFlag() {
3294
3600
  const e1 = new MachineEdge(
3295
- [MediaUrn.fromString('media:pdf')],
3296
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3297
- MediaUrn.fromString('media:txt;textable'),
3601
+ [MediaUrn.fromString('media:ext=pdf')],
3602
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3603
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3298
3604
  false
3299
3605
  );
3300
3606
  const e2 = new MachineEdge(
3301
- [MediaUrn.fromString('media:pdf')],
3302
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3303
- MediaUrn.fromString('media:txt;textable'),
3607
+ [MediaUrn.fromString('media:ext=pdf')],
3608
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3609
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3304
3610
  true
3305
3611
  );
3306
3612
  assert(!e1.isEquivalent(e2), 'Different loop flags should not be equivalent');
@@ -3309,15 +3615,15 @@ function test0132_Machine_edgeEquivalenceDifferentLoopFlag() {
3309
3615
  // TEST0133: Machine edge equivalence source order independent
3310
3616
  function test0133_Machine_edgeEquivalenceSourceOrderIndependent() {
3311
3617
  const e1 = new MachineEdge(
3312
- [MediaUrn.fromString('media:txt;textable'), MediaUrn.fromString('media:model-spec;textable')],
3313
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3314
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3618
+ [MediaUrn.fromString('media:enc=utf-8;ext=txt'), MediaUrn.fromString('media:enc=utf-8;model-spec')],
3619
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3620
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3315
3621
  false
3316
3622
  );
3317
3623
  const e2 = new MachineEdge(
3318
- [MediaUrn.fromString('media:model-spec;textable'), MediaUrn.fromString('media:txt;textable')],
3319
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3320
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3624
+ [MediaUrn.fromString('media:enc=utf-8;model-spec'), MediaUrn.fromString('media:enc=utf-8;ext=txt')],
3625
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3626
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3321
3627
  false
3322
3628
  );
3323
3629
  assert(e1.isEquivalent(e2), 'Source order should not matter for equivalence');
@@ -3326,15 +3632,15 @@ function test0133_Machine_edgeEquivalenceSourceOrderIndependent() {
3326
3632
  // TEST0134: Machine edge equivalence different source count
3327
3633
  function test0134_Machine_edgeEquivalenceDifferentSourceCount() {
3328
3634
  const e1 = new MachineEdge(
3329
- [MediaUrn.fromString('media:txt;textable')],
3330
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3331
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3635
+ [MediaUrn.fromString('media:enc=utf-8;ext=txt')],
3636
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3637
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3332
3638
  false
3333
3639
  );
3334
3640
  const e2 = new MachineEdge(
3335
- [MediaUrn.fromString('media:txt;textable'), MediaUrn.fromString('media:model-spec;textable')],
3336
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3337
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3641
+ [MediaUrn.fromString('media:enc=utf-8;ext=txt'), MediaUrn.fromString('media:enc=utf-8;model-spec')],
3642
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3643
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3338
3644
  false
3339
3645
  );
3340
3646
  assert(!e1.isEquivalent(e2), 'Different source counts should not be equivalent');
@@ -3346,12 +3652,12 @@ function test0135_Machine_graphEquivalenceSameEdges() {
3346
3652
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3347
3653
  );
3348
3654
  const g1 = new Machine([
3349
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3350
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3655
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3656
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3351
3657
  ]);
3352
3658
  const g2 = new Machine([
3353
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3354
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3659
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3660
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3355
3661
  ]);
3356
3662
  assert(g1.isEquivalent(g2), 'Same edges should be equivalent');
3357
3663
  }
@@ -3362,12 +3668,12 @@ function test0136_Machine_graphEquivalenceReorderedEdges() {
3362
3668
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3363
3669
  );
3364
3670
  const g1 = new Machine([
3365
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3366
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3671
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3672
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3367
3673
  ]);
3368
3674
  const g2 = new Machine([
3369
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3370
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3675
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3676
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3371
3677
  ]);
3372
3678
  assert(g1.isEquivalent(g2), 'Reordered edges should still be equivalent');
3373
3679
  }
@@ -3378,11 +3684,11 @@ function test0137_Machine_graphNotEquivalentDifferentEdgeCount() {
3378
3684
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3379
3685
  );
3380
3686
  const g1 = new Machine([
3381
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3687
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3382
3688
  ]);
3383
3689
  const g2 = new Machine([
3384
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3385
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3690
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3691
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3386
3692
  ]);
3387
3693
  assert(!g1.isEquivalent(g2), 'Different edge counts should not be equivalent');
3388
3694
  }
@@ -3393,10 +3699,10 @@ function test0138_Machine_graphNotEquivalentDifferentCap() {
3393
3699
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3394
3700
  );
3395
3701
  const g1 = new Machine([
3396
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3702
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3397
3703
  ]);
3398
3704
  const g2 = new Machine([
3399
- mkEdge('media:pdf', 'cap:in="media:pdf";summarize;out="media:txt;textable"', 'media:txt;textable'),
3705
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";summarize;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3400
3706
  ]);
3401
3707
  assert(!g1.isEquivalent(g2), 'Different caps should not be equivalent');
3402
3708
  }
@@ -3421,13 +3727,13 @@ function test0141_Machine_rootSourcesLinearChain() {
3421
3727
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3422
3728
  );
3423
3729
  const g = new Machine([
3424
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3425
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3730
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3731
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3426
3732
  ]);
3427
3733
  const roots = g.rootSources();
3428
3734
  assertEqual(roots.length, 1);
3429
- assert(roots[0].isEquivalent(MediaUrn.fromString('media:pdf')),
3430
- 'Root source should be media:pdf');
3735
+ assert(roots[0].isEquivalent(MediaUrn.fromString('media:ext=pdf')),
3736
+ 'Root source should be media:ext=pdf');
3431
3737
  }
3432
3738
 
3433
3739
  // TEST0142: Machine leaf targets linear chain
@@ -3436,21 +3742,21 @@ function test0142_Machine_leafTargetsLinearChain() {
3436
3742
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3437
3743
  );
3438
3744
  const g = new Machine([
3439
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3440
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3745
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3746
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3441
3747
  ]);
3442
3748
  const leaves = g.leafTargets();
3443
3749
  assertEqual(leaves.length, 1);
3444
- assert(leaves[0].isEquivalent(MediaUrn.fromString('media:embedding-vector;record;textable')),
3445
- 'Leaf target should be media:embedding-vector;record;textable');
3750
+ assert(leaves[0].isEquivalent(MediaUrn.fromString('media:embedding-vector;enc=utf-8;record')),
3751
+ 'Leaf target should be media:embedding-vector;enc=utf-8;record');
3446
3752
  }
3447
3753
 
3448
3754
  // TEST0143: Machine root sources fan in
3449
3755
  function test0143_Machine_rootSourcesFanIn() {
3450
3756
  const e = new MachineEdge(
3451
- [MediaUrn.fromString('media:txt;textable'), MediaUrn.fromString('media:model-spec;textable')],
3452
- CapUrn.fromString('cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"'),
3453
- MediaUrn.fromString('media:embedding-vector;record;textable'),
3757
+ [MediaUrn.fromString('media:enc=utf-8;ext=txt'), MediaUrn.fromString('media:enc=utf-8;model-spec')],
3758
+ CapUrn.fromString('cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"'),
3759
+ MediaUrn.fromString('media:embedding-vector;enc=utf-8;record'),
3454
3760
  false
3455
3761
  );
3456
3762
  const g = new Machine([e]);
@@ -3461,21 +3767,21 @@ function test0143_Machine_rootSourcesFanIn() {
3461
3767
  // TEST0144: Machine display edge
3462
3768
  function test0144_Machine_displayEdge() {
3463
3769
  const e = new MachineEdge(
3464
- [MediaUrn.fromString('media:pdf')],
3465
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3466
- MediaUrn.fromString('media:txt;textable'),
3770
+ [MediaUrn.fromString('media:ext=pdf')],
3771
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3772
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3467
3773
  false
3468
3774
  );
3469
3775
  const display = e.toString();
3470
- assert(display.includes('media:pdf'), 'Display should contain media:pdf');
3776
+ assert(display.includes('media:ext=pdf'), 'Display should contain media:ext=pdf');
3471
3777
  }
3472
3778
 
3473
3779
  // TEST0145: Machine display graph
3474
3780
  function test0145_Machine_displayGraph() {
3475
3781
  const e = new MachineEdge(
3476
- [MediaUrn.fromString('media:pdf')],
3477
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3478
- MediaUrn.fromString('media:txt;textable'),
3782
+ [MediaUrn.fromString('media:ext=pdf')],
3783
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3784
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3479
3785
  false
3480
3786
  );
3481
3787
  assertEqual(new Machine([e]).toString(), 'Machine(1 edges)');
@@ -3486,9 +3792,9 @@ function test0145_Machine_displayGraph() {
3486
3792
 
3487
3793
  function test0146_Machine_serializeSingleEdge() {
3488
3794
  const g = new Machine([new MachineEdge(
3489
- [MediaUrn.fromString('media:pdf')],
3490
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3491
- MediaUrn.fromString('media:txt;textable'),
3795
+ [MediaUrn.fromString('media:ext=pdf')],
3796
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3797
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3492
3798
  false
3493
3799
  )]);
3494
3800
  const notation = g.toMachineNotation();
@@ -3505,8 +3811,8 @@ function test0147_Machine_serializeTwoEdgeChain() {
3505
3811
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3506
3812
  );
3507
3813
  const g = new Machine([
3508
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3509
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3814
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3815
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3510
3816
  ]);
3511
3817
  const notation = g.toMachineNotation();
3512
3818
  const bracketCount = (notation.match(/\[/g) || []).length;
@@ -3521,9 +3827,9 @@ function test0148_Machine_serializeEmptyGraph() {
3521
3827
  // TEST0149: Machine roundtrip single edge
3522
3828
  function test0149_Machine_roundtripSingleEdge() {
3523
3829
  const original = new Machine([new MachineEdge(
3524
- [MediaUrn.fromString('media:pdf')],
3525
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3526
- MediaUrn.fromString('media:txt;textable'),
3830
+ [MediaUrn.fromString('media:ext=pdf')],
3831
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3832
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3527
3833
  false
3528
3834
  )]);
3529
3835
  const notation = original.toMachineNotation();
@@ -3538,8 +3844,8 @@ function test0151_Machine_roundtripTwoEdgeChain() {
3538
3844
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3539
3845
  );
3540
3846
  const original = new Machine([
3541
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3542
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3847
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3848
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3543
3849
  ]);
3544
3850
  const notation = original.toMachineNotation();
3545
3851
  const reparsed = Machine.fromString(notation);
@@ -3553,9 +3859,9 @@ function test0152_Machine_roundtripFanOut() {
3553
3859
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3554
3860
  );
3555
3861
  const original = new Machine([
3556
- mkEdge('media:pdf', 'cap:in="media:pdf";extract-metadata;out="media:file-metadata;record;textable"', 'media:file-metadata;record;textable'),
3557
- mkEdge('media:pdf', 'cap:in="media:pdf";extract-outline;out="media:document-outline;record;textable"', 'media:document-outline;record;textable'),
3558
- mkEdge('media:pdf', 'cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"', 'media:image;png;thumbnail'),
3862
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"', 'media:enc=utf-8;file-metadata;record'),
3863
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract-outline;out="media:document-outline;enc=utf-8;record"', 'media:document-outline;enc=utf-8;record'),
3864
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"', 'media:image;png;thumbnail'),
3559
3865
  ]);
3560
3866
  const notation = original.toMachineNotation();
3561
3867
  const reparsed = Machine.fromString(notation);
@@ -3566,9 +3872,9 @@ function test0152_Machine_roundtripFanOut() {
3566
3872
  // TEST0153: Machine roundtrip loop edge
3567
3873
  function test0153_Machine_roundtripLoopEdge() {
3568
3874
  const original = new Machine([new MachineEdge(
3569
- [MediaUrn.fromString('media:disbound-page;textable')],
3570
- CapUrn.fromString('cap:in="media:disbound-page;textable";page-to-text;out="media:txt;textable"'),
3571
- MediaUrn.fromString('media:txt;textable'),
3875
+ [MediaUrn.fromString('media:disbound-page;enc=utf-8')],
3876
+ CapUrn.fromString('cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"'),
3877
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3572
3878
  true
3573
3879
  )]);
3574
3880
  const notation = original.toMachineNotation();
@@ -3583,8 +3889,8 @@ function test0154_Machine_serializationIsDeterministic() {
3583
3889
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3584
3890
  );
3585
3891
  const g = new Machine([
3586
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3587
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3892
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3893
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3588
3894
  ]);
3589
3895
  const n1 = g.toMachineNotation();
3590
3896
  const n2 = g.toMachineNotation();
@@ -3597,12 +3903,12 @@ function test0155_Machine_reorderedEdgesProduceSameNotation() {
3597
3903
  [MediaUrn.fromString(src)], CapUrn.fromString(cap), MediaUrn.fromString(tgt), false
3598
3904
  );
3599
3905
  const g1 = new Machine([
3600
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3601
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3906
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3907
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3602
3908
  ]);
3603
3909
  const g2 = new Machine([
3604
- mkEdge('media:txt;textable', 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable'),
3605
- mkEdge('media:pdf', 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable'),
3910
+ mkEdge('media:enc=utf-8;ext=txt', 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record'),
3911
+ mkEdge('media:ext=pdf', 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt'),
3606
3912
  ]);
3607
3913
  assertEqual(g1.toMachineNotation(), g2.toMachineNotation(),
3608
3914
  'Same graph with reordered edges must produce identical notation');
@@ -3611,9 +3917,9 @@ function test0155_Machine_reorderedEdgesProduceSameNotation() {
3611
3917
  // TEST0160: Machine multiline serialize format
3612
3918
  function test0160_Machine_multilineSerializeFormat() {
3613
3919
  const g = new Machine([new MachineEdge(
3614
- [MediaUrn.fromString('media:pdf')],
3615
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3616
- MediaUrn.fromString('media:txt;textable'),
3920
+ [MediaUrn.fromString('media:ext=pdf')],
3921
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3922
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3617
3923
  false
3618
3924
  )]);
3619
3925
  const multi = g.toMachineNotationMultiline();
@@ -3627,9 +3933,9 @@ function test0160_Machine_multilineSerializeFormat() {
3627
3933
  // no privileged `op` tag to derive a friendlier name from.
3628
3934
  function test0161_Machine_aliasFromOpTag() {
3629
3935
  const g = new Machine([new MachineEdge(
3630
- [MediaUrn.fromString('media:pdf')],
3631
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3632
- MediaUrn.fromString('media:txt;textable'),
3936
+ [MediaUrn.fromString('media:ext=pdf')],
3937
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3938
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3633
3939
  false
3634
3940
  )]);
3635
3941
  const notation = g.toMachineNotation();
@@ -3639,9 +3945,9 @@ function test0161_Machine_aliasFromOpTag() {
3639
3945
  // TEST0162: Machine alias fallback without op tag
3640
3946
  function test0162_Machine_aliasFallbackWithoutOpTag() {
3641
3947
  const g = new Machine([new MachineEdge(
3642
- [MediaUrn.fromString('media:pdf')],
3643
- CapUrn.fromString('cap:in="media:pdf";out="media:txt;textable"'),
3644
- MediaUrn.fromString('media:txt;textable'),
3948
+ [MediaUrn.fromString('media:ext=pdf')],
3949
+ CapUrn.fromString('cap:in="media:ext=pdf";out="media:enc=utf-8;ext=txt"'),
3950
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3645
3951
  false
3646
3952
  )]);
3647
3953
  const notation = g.toMachineNotation();
@@ -3652,15 +3958,15 @@ function test0162_Machine_aliasFallbackWithoutOpTag() {
3652
3958
  function test0163_Machine_duplicateOpTagsDisambiguated() {
3653
3959
  const g = new Machine([
3654
3960
  new MachineEdge(
3655
- [MediaUrn.fromString('media:pdf')],
3656
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"'),
3657
- MediaUrn.fromString('media:txt;textable'),
3961
+ [MediaUrn.fromString('media:ext=pdf')],
3962
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"'),
3963
+ MediaUrn.fromString('media:enc=utf-8;ext=txt'),
3658
3964
  false
3659
3965
  ),
3660
3966
  new MachineEdge(
3661
- [MediaUrn.fromString('media:pdf')],
3662
- CapUrn.fromString('cap:in="media:pdf";extract;out="media:json;record;textable"'),
3663
- MediaUrn.fromString('media:json;record;textable'),
3967
+ [MediaUrn.fromString('media:ext=pdf')],
3968
+ CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:fmt=json;record"'),
3969
+ MediaUrn.fromString('media:fmt=json;record'),
3664
3970
  false
3665
3971
  ),
3666
3972
  ]);
@@ -3674,9 +3980,9 @@ function test0163_Machine_duplicateOpTagsDisambiguated() {
3674
3980
  function test0164_Machine_builderSingleEdge() {
3675
3981
  const builder = new MachineBuilder();
3676
3982
  builder.addEdge(
3677
- ['media:pdf'],
3678
- 'cap:in="media:pdf";extract;out="media:txt;textable"',
3679
- 'media:txt;textable'
3983
+ ['media:ext=pdf'],
3984
+ 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"',
3985
+ 'media:enc=utf-8;ext=txt'
3680
3986
  );
3681
3987
  const g = builder.build();
3682
3988
  assertEqual(g.edgeCount(), 1);
@@ -3687,9 +3993,9 @@ function test0164_Machine_builderSingleEdge() {
3687
3993
  function test0165_Machine_builderWithLoop() {
3688
3994
  const builder = new MachineBuilder();
3689
3995
  builder.addEdge(
3690
- ['media:disbound-page;textable'],
3691
- 'cap:in="media:disbound-page;textable";page-to-text;out="media:txt;textable"',
3692
- 'media:txt;textable',
3996
+ ['media:disbound-page;enc=utf-8'],
3997
+ 'cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"',
3998
+ 'media:enc=utf-8;ext=txt',
3693
3999
  true
3694
4000
  );
3695
4001
  const g = builder.build();
@@ -3699,8 +4005,8 @@ function test0165_Machine_builderWithLoop() {
3699
4005
  // TEST0166: Machine builder chaining
3700
4006
  function test0166_Machine_builderChaining() {
3701
4007
  const g = new MachineBuilder()
3702
- .addEdge(['media:pdf'], 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable')
3703
- .addEdge(['media:txt;textable'], 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable')
4008
+ .addEdge(['media:ext=pdf'], 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt')
4009
+ .addEdge(['media:enc=utf-8;ext=txt'], 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record')
3704
4010
  .build();
3705
4011
  assertEqual(g.edgeCount(), 2);
3706
4012
  }
@@ -3708,11 +4014,11 @@ function test0166_Machine_builderChaining() {
3708
4014
  // TEST0167: Machine builder equivalent to parsed
3709
4015
  function test0167_Machine_builderEquivalentToParsed() {
3710
4016
  const parsed = Machine.fromString(
3711
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
4017
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3712
4018
  '[doc -> extract -> text]'
3713
4019
  );
3714
4020
  const built = new MachineBuilder()
3715
- .addEdge(['media:pdf'], 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable')
4021
+ .addEdge(['media:ext=pdf'], 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt')
3716
4022
  .build();
3717
4023
  assert(parsed.isEquivalent(built),
3718
4024
  'Builder-constructed graph should be equivalent to parsed graph');
@@ -3721,8 +4027,8 @@ function test0167_Machine_builderEquivalentToParsed() {
3721
4027
  // TEST0168: Machine builder round trip
3722
4028
  function test0168_Machine_builderRoundTrip() {
3723
4029
  const built = new MachineBuilder()
3724
- .addEdge(['media:pdf'], 'cap:in="media:pdf";extract;out="media:txt;textable"', 'media:txt;textable')
3725
- .addEdge(['media:txt;textable'], 'cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"', 'media:embedding-vector;record;textable')
4030
+ .addEdge(['media:ext=pdf'], 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'media:enc=utf-8;ext=txt')
4031
+ .addEdge(['media:enc=utf-8;ext=txt'], 'cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"', 'media:embedding-vector;enc=utf-8;record')
3726
4032
  .build();
3727
4033
  const notation = built.toMachineNotation();
3728
4034
  const reparsed = Machine.fromString(notation);
@@ -3732,54 +4038,54 @@ function test0168_Machine_builderRoundTrip() {
3732
4038
  // --- CapUrn.isEquivalent/isComparable tests ---
3733
4039
 
3734
4040
  function test0169_Machine_capUrnIsEquivalent() {
3735
- const a = CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"');
3736
- const b = CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"');
4041
+ const a = CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"');
4042
+ const b = CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"');
3737
4043
  assert(a.isEquivalent(b), 'Same cap URNs should be equivalent');
3738
- const c = CapUrn.fromString('cap:in="media:pdf";summarize;out="media:txt;textable"');
4044
+ const c = CapUrn.fromString('cap:in="media:ext=pdf";summarize;out="media:enc=utf-8;ext=txt"');
3739
4045
  assert(!a.isEquivalent(c), 'Different cap URNs should not be equivalent');
3740
4046
  }
3741
4047
 
3742
4048
  // TEST0170: Machine cap urn is comparable
3743
4049
  function test0170_Machine_capUrnIsComparable() {
3744
- const general = CapUrn.fromString('cap:in="media:pdf";out="media:txt;textable"');
3745
- const specific = CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"');
4050
+ const general = CapUrn.fromString('cap:in="media:ext=pdf";out="media:enc=utf-8;ext=txt"');
4051
+ const specific = CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"');
3746
4052
  assert(general.isComparable(specific), 'General should be comparable to specific');
3747
4053
  assert(specific.isComparable(general), 'isComparable should be symmetric');
3748
4054
  }
3749
4055
 
3750
4056
  // TEST0171: Machine cap urn in media urn
3751
4057
  function test0171_Machine_capUrnInMediaUrn() {
3752
- const cap = CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"');
4058
+ const cap = CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"');
3753
4059
  const inUrn = cap.inMediaUrn();
3754
4060
  assert(inUrn instanceof MediaUrn, 'inMediaUrn should return MediaUrn');
3755
- assert(inUrn.isEquivalent(MediaUrn.fromString('media:pdf')), 'inMediaUrn should be media:pdf');
4061
+ assert(inUrn.isEquivalent(MediaUrn.fromString('media:ext=pdf')), 'inMediaUrn should be media:ext=pdf');
3756
4062
  }
3757
4063
 
3758
4064
  // TEST0172: Machine cap urn out media urn
3759
4065
  function test0172_Machine_capUrnOutMediaUrn() {
3760
- const cap = CapUrn.fromString('cap:in="media:pdf";extract;out="media:txt;textable"');
4066
+ const cap = CapUrn.fromString('cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"');
3761
4067
  const outUrn = cap.outMediaUrn();
3762
4068
  assert(outUrn instanceof MediaUrn, 'outMediaUrn should return MediaUrn');
3763
- assert(outUrn.isEquivalent(MediaUrn.fromString('media:txt;textable')), 'outMediaUrn should be media:txt;textable');
4069
+ assert(outUrn.isEquivalent(MediaUrn.fromString('media:enc=utf-8;ext=txt')), 'outMediaUrn should be media:enc=utf-8;ext=txt');
3764
4070
  }
3765
4071
 
3766
4072
  // --- MediaUrn.isEquivalent/isComparable tests ---
3767
4073
 
3768
4074
  function test0173_Machine_mediaUrnIsEquivalent() {
3769
- const a = MediaUrn.fromString('media:pdf');
3770
- const b = MediaUrn.fromString('media:pdf');
4075
+ const a = MediaUrn.fromString('media:ext=pdf');
4076
+ const b = MediaUrn.fromString('media:ext=pdf');
3771
4077
  assert(a.isEquivalent(b), 'Same media URNs should be equivalent');
3772
- const c = MediaUrn.fromString('media:txt;textable');
4078
+ const c = MediaUrn.fromString('media:enc=utf-8;ext=txt');
3773
4079
  assert(!a.isEquivalent(c), 'Different media URNs should not be equivalent');
3774
4080
  }
3775
4081
 
3776
4082
  // TEST0174: Machine media urn is comparable
3777
4083
  function test0174_Machine_mediaUrnIsComparable() {
3778
- const general = MediaUrn.fromString('media:textable');
3779
- const specific = MediaUrn.fromString('media:txt;textable');
4084
+ const general = MediaUrn.fromString('media:enc=utf-8');
4085
+ const specific = MediaUrn.fromString('media:enc=utf-8;ext=txt');
3780
4086
  assert(general.isComparable(specific), 'General should be comparable to specific');
3781
4087
  assert(specific.isComparable(general), 'isComparable should be symmetric');
3782
- const unrelated = MediaUrn.fromString('media:pdf');
4088
+ const unrelated = MediaUrn.fromString('media:ext=pdf');
3783
4089
  assert(!general.isComparable(unrelated), 'Unrelated should not be comparable');
3784
4090
  }
3785
4091
 
@@ -3788,7 +4094,7 @@ function test0174_Machine_mediaUrnIsComparable() {
3788
4094
  // ============================================================================
3789
4095
 
3790
4096
  function test0175_Machine_parseMachineWithAST_headerLocation() {
3791
- const input = '[extract cap:in="media:pdf";extract;out="media:txt;textable"][doc -> extract -> text]';
4097
+ const input = '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"][doc -> extract -> text]';
3792
4098
  const result = parseMachineWithAST(input);
3793
4099
  assert(result.statements.length === 2, 'Should have 2 statements');
3794
4100
  const stmt = result.statements[0];
@@ -3805,7 +4111,7 @@ function test0175_Machine_parseMachineWithAST_headerLocation() {
3805
4111
 
3806
4112
  // TEST0176: Machine parse machine with a s t wiring location
3807
4113
  function test0176_Machine_parseMachineWithAST_wiringLocation() {
3808
- const input = '[extract cap:in="media:pdf";extract;out="media:txt;textable"]\n[doc -> extract -> text]';
4114
+ const input = '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]\n[doc -> extract -> text]';
3809
4115
  const result = parseMachineWithAST(input);
3810
4116
  assert(result.statements.length === 2, 'Should have 2 statements');
3811
4117
  const wiring = result.statements[1];
@@ -3820,7 +4126,7 @@ function test0176_Machine_parseMachineWithAST_wiringLocation() {
3820
4126
 
3821
4127
  // TEST0177: Machine parse machine with a s t multiline positions
3822
4128
  function test0177_Machine_parseMachineWithAST_multilinePositions() {
3823
- const input = '[extract cap:in="media:pdf";extract;out="media:txt;textable"]\n[doc -> extract -> text]';
4129
+ const input = '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]\n[doc -> extract -> text]';
3824
4130
  const result = parseMachineWithAST(input);
3825
4131
  const headerLoc = result.statements[0].location;
3826
4132
  const wiringLoc = result.statements[1].location;
@@ -3831,7 +4137,7 @@ function test0177_Machine_parseMachineWithAST_multilinePositions() {
3831
4137
  // TEST0178: Machine parse machine with a s t fan in source locations
3832
4138
  function test0178_Machine_parseMachineWithAST_fanInSourceLocations() {
3833
4139
  const input = [
3834
- '[describe cap:in="media:image;png";describe-image;out="media:image-description;textable"]',
4140
+ '[describe cap:in="media:image;png";describe-image;out="media:enc=utf-8;image-description"]',
3835
4141
  '[(thumbnail, model_spec) -> describe -> description]'
3836
4142
  ].join('\n');
3837
4143
  const result = parseMachineWithAST(input);
@@ -3843,8 +4149,8 @@ function test0178_Machine_parseMachineWithAST_fanInSourceLocations() {
3843
4149
  // TEST0179: Machine parse machine with a s t alias map
3844
4150
  function test0179_Machine_parseMachineWithAST_aliasMap() {
3845
4151
  const input = [
3846
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]',
3847
- '[embed cap:in="media:txt;textable";embed;out="media:embedding-vector;record;textable"]',
4152
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]',
4153
+ '[embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"]',
3848
4154
  '[doc -> extract -> text]',
3849
4155
  '[text -> embed -> vectors]',
3850
4156
  ].join('\n');
@@ -3862,14 +4168,14 @@ function test0179_Machine_parseMachineWithAST_aliasMap() {
3862
4168
  // TEST0180: Machine parse machine with a s t node media
3863
4169
  function test0180_Machine_parseMachineWithAST_nodeMedia() {
3864
4170
  const input = [
3865
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]',
4171
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]',
3866
4172
  '[doc -> extract -> text]',
3867
4173
  ].join('\n');
3868
4174
  const result = parseMachineWithAST(input);
3869
4175
  assert(result.nodeMedia.has('doc'), 'nodeMedia should have doc');
3870
4176
  assert(result.nodeMedia.has('text'), 'nodeMedia should have text');
3871
- assertEqual(result.nodeMedia.get('doc').toString(), 'media:pdf', 'doc should be media:pdf');
3872
- assertEqual(result.nodeMedia.get('text').toString(), 'media:textable;txt', 'text should be media:textable;txt');
4177
+ assertEqual(result.nodeMedia.get('doc').toString(), 'media:ext=pdf', 'doc should be media:ext=pdf');
4178
+ assertEqual(result.nodeMedia.get('text').toString(), 'media:enc=utf-8;ext=txt', 'text should be media:enc=utf-8;ext=txt');
3873
4179
  }
3874
4180
 
3875
4181
  // TEST0181: Machine error location parse error
@@ -3887,8 +4193,8 @@ function test0181_Machine_errorLocation_parseError() {
3887
4193
  function test0182_Machine_errorLocation_duplicateAlias() {
3888
4194
  try {
3889
4195
  parseMachine(
3890
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
3891
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
4196
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
4197
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3892
4198
  '[doc -> extract -> text]'
3893
4199
  );
3894
4200
  throw new Error('Expected MachineSyntaxError');
@@ -3915,7 +4221,7 @@ function test0183_Machine_errorLocation_undefinedAlias() {
3915
4221
 
3916
4222
  function test0184_Machine_toMermaid_linearChain() {
3917
4223
  const machine = Machine.fromString(
3918
- '[extract cap:in="media:pdf";extract;out="media:txt;textable"]' +
4224
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
3919
4225
  '[doc -> extract -> text]'
3920
4226
  );
3921
4227
  const mermaid = machine.toMermaid();
@@ -3924,8 +4230,8 @@ function test0184_Machine_toMermaid_linearChain() {
3924
4230
  // serializer (the input alias name is not preserved in the rendered
3925
4231
  // diagram — it's a serialization artefact, not part of the machine).
3926
4232
  assert(mermaid.includes('edge_0'), 'Should include edge_0 label');
3927
- assert(mermaid.includes('media:pdf'), 'Should include media:pdf node');
3928
- assert(mermaid.includes('media:textable;txt'), 'Should include media:textable;txt node');
4233
+ assert(mermaid.includes('media:ext=pdf'), 'Should include media:ext=pdf node');
4234
+ assert(mermaid.includes('media:enc=utf-8;ext=txt'), 'Should include media:enc=utf-8;ext=txt node');
3929
4235
  assert(mermaid.includes('-->'), 'Should include arrow');
3930
4236
  // Root source and leaf target should both be stadium shape
3931
4237
  assert(mermaid.includes('(['), 'Should have stadium shape nodes');
@@ -3934,7 +4240,7 @@ function test0184_Machine_toMermaid_linearChain() {
3934
4240
  // TEST0185: Machine to mermaid loop edge
3935
4241
  function test0185_Machine_toMermaid_loopEdge() {
3936
4242
  const machine = Machine.fromString(
3937
- '[p2t cap:in="media:disbound-page;textable";page-to-text;out="media:txt;textable"]' +
4243
+ '[p2t cap:in="media:disbound-page;enc=utf-8";page-to-text;out="media:enc=utf-8;ext=txt"]' +
3938
4244
  '[pages -> LOOP p2t -> texts]'
3939
4245
  );
3940
4246
  const mermaid = machine.toMermaid();
@@ -3953,7 +4259,7 @@ function test0186_Machine_toMermaid_emptyGraph() {
3953
4259
  // TEST0187: Machine to mermaid fan in
3954
4260
  function test0187_Machine_toMermaid_fanIn() {
3955
4261
  const machine = Machine.fromString(
3956
- '[describe cap:in="media:image;png";describe-image;out="media:image-description;textable"]' +
4262
+ '[describe cap:in="media:image;png";describe-image;out="media:enc=utf-8;image-description"]' +
3957
4263
  '[(thumbnail, model_spec) -> describe -> description]'
3958
4264
  );
3959
4265
  const mermaid = machine.toMermaid();
@@ -3965,8 +4271,8 @@ function test0187_Machine_toMermaid_fanIn() {
3965
4271
  // TEST0188: Machine to mermaid fan out
3966
4272
  function test0188_Machine_toMermaid_fanOut() {
3967
4273
  const input = [
3968
- '[meta cap:in="media:pdf";extract-metadata;out="media:file-metadata;record;textable"]',
3969
- '[thumb cap:in="media:pdf";generate-thumbnail;out="media:image;png;thumbnail"]',
4274
+ '[meta cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"]',
4275
+ '[thumb cap:in="media:ext=pdf";generate-thumbnail;out="media:image;png;thumbnail"]',
3970
4276
  '[doc -> meta -> metadata]',
3971
4277
  '[doc -> thumb -> thumbnail]'
3972
4278
  ].join('');
@@ -3975,8 +4281,8 @@ function test0188_Machine_toMermaid_fanOut() {
3975
4281
  // Should have 2 edges
3976
4282
  const arrowCount = (mermaid.match(/-->/g) || []).length;
3977
4283
  assertEqual(arrowCount, 2, 'Fan-out should produce 2 arrows');
3978
- // The root source (media:pdf) should appear once as a node definition
3979
- assert(mermaid.includes('media:pdf'), 'Should include media:pdf');
4284
+ // The root source (media:ext=pdf) should appear once as a node definition
4285
+ assert(mermaid.includes('media:ext=pdf'), 'Should include media:ext=pdf');
3980
4286
  }
3981
4287
 
3982
4288
  // ============================================================================
@@ -3985,36 +4291,36 @@ function test0188_Machine_toMermaid_fanOut() {
3985
4291
 
3986
4292
  function test0189_Machine_capRegistryEntry_construction() {
3987
4293
  const entry = new FabricRegistryEntry({
3988
- urn: 'cap:in="media:pdf";extract;out="media:txt;textable"',
4294
+ urn: 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"',
3989
4295
  title: 'PDF Extractor',
3990
4296
  command: 'extract',
3991
4297
  cap_description: 'Extracts text from PDF',
3992
- args: [{ media_urn: 'media:pdf', required: true }],
3993
- output: { media_urn: 'media:txt;textable', output_description: 'Extracted text' },
4298
+ args: [{ media_urn: 'media:ext=pdf', required: true }],
4299
+ output: { media_urn: 'media:enc=utf-8;ext=txt', output_description: 'Extracted text' },
3994
4300
  media_defs: [],
3995
4301
  urn_tags: { op: 'extract' },
3996
- in_spec: 'media:pdf',
3997
- out_spec: 'media:txt;textable',
4302
+ in_spec: 'media:ext=pdf',
4303
+ out_spec: 'media:enc=utf-8;ext=txt',
3998
4304
  in_media_title: 'PDF Document',
3999
4305
  out_media_title: 'Text'
4000
4306
  });
4001
- assertEqual(entry.urn, 'cap:in="media:pdf";extract;out="media:txt;textable"', 'URN should match');
4307
+ assertEqual(entry.urn, 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"', 'URN should match');
4002
4308
  assertEqual(entry.title, 'PDF Extractor', 'Title should match');
4003
4309
  assertEqual(entry.description, 'Extracts text from PDF', 'Description should match');
4004
- assertEqual(entry.inSpec, 'media:pdf', 'inSpec should match');
4005
- assertEqual(entry.outSpec, 'media:txt;textable', 'outSpec should match');
4310
+ assertEqual(entry.inSpec, 'media:ext=pdf', 'inSpec should match');
4311
+ assertEqual(entry.outSpec, 'media:enc=utf-8;ext=txt', 'outSpec should match');
4006
4312
  assertEqual(entry.urnTags.op, 'extract', 'op tag should match');
4007
4313
  }
4008
4314
 
4009
4315
  // TEST0190: Machine media registry entry construction
4010
4316
  function test0190_Machine_mediaRegistryEntry_construction() {
4011
4317
  const entry = new MediaRegistryEntry({
4012
- urn: 'media:pdf',
4318
+ urn: 'media:ext=pdf',
4013
4319
  title: 'PDF Document',
4014
4320
  media_type: 'application/pdf',
4015
4321
  description: 'Portable Document Format'
4016
4322
  });
4017
- assertEqual(entry.urn, 'media:pdf', 'URN should match');
4323
+ assertEqual(entry.urn, 'media:ext=pdf', 'URN should match');
4018
4324
  assertEqual(entry.title, 'PDF Document', 'Title should match');
4019
4325
  assertEqual(entry.mediaType, 'application/pdf', 'Media type should match');
4020
4326
  assertEqual(entry.description, 'Portable Document Format', 'Description should match');
@@ -4117,7 +4423,7 @@ function test0195_Renderer_cardinalityFromCap_findsStdinArgNotFirstArg() {
4117
4423
  // A naive implementation that reads args[0] would see `cli-only` (not a
4118
4424
  // sequence) and report 1→1 even though the stdin arg is a sequence.
4119
4425
  const cap = {
4120
- urn: 'cap:in="media:textable;list";transcribe;out="media:textable"',
4426
+ urn: 'cap:in="media:enc=utf-8;list";transcribe;out="media:enc=utf-8"',
4121
4427
  args: [
4122
4428
  {
4123
4429
  display_name: 'cli-only',
@@ -4149,7 +4455,7 @@ function test0196_Renderer_cardinalityFromCap_scalarDefaultsWhenFieldsMissing()
4149
4455
  function test0197_Renderer_cardinalityFromCap_outputOnlySequence() {
4150
4456
  // One scalar stdin arg, output is a sequence: expects 1→n.
4151
4457
  const cap = {
4152
- urn: 'cap:in="media:textable";generate;out="media:textable;list"',
4458
+ urn: 'cap:in="media:enc=utf-8";generate;out="media:enc=utf-8;list"',
4153
4459
  args: [{ sources: [{ stdin: {} }], is_sequence: false }],
4154
4460
  output: { is_sequence: true },
4155
4461
  };
@@ -4242,10 +4548,10 @@ function test0204_Renderer_buildBrowseGraphData_rejectsMissingMediaTitles() {
4242
4548
  try {
4243
4549
  rendererBuildBrowseGraphData([
4244
4550
  {
4245
- urn: 'cap:in="media:pdf";extract;out="media:txt;textable"',
4551
+ urn: 'cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"',
4246
4552
  title: 'Extract Text',
4247
- in_spec: 'media:pdf',
4248
- out_spec: 'media:txt;textable',
4553
+ in_spec: 'media:ext=pdf',
4554
+ out_spec: 'media:enc=utf-8;ext=txt',
4249
4555
  in_media_title: 'PDF',
4250
4556
  out_media_title: '',
4251
4557
  },
@@ -4341,10 +4647,10 @@ function test0207_Renderer_classifyStrandCapSteps_capFlags() {
4341
4647
  // have neither.
4342
4648
  const steps = [
4343
4649
  makeForEachStep('media:pdf;list'),
4344
- makeCapStep('cap:in="media:pdf";a;out="media:image;png"', 'a', 'media:pdf', 'media:image;png', false, false),
4650
+ makeCapStep('cap:in="media:ext=pdf";a;out="media:image;png"', 'a', 'media:ext=pdf', 'media:image;png', false, false),
4345
4651
  makeCapStep('cap:in="media:image;png";b;out="media:jpg"', 'b', 'media:image;png', 'media:jpg', false, false),
4346
- makeCapStep('cap:in="media:jpg";c;out="media:txt"', 'c', 'media:jpg', 'media:txt', false, false),
4347
- makeCollectStep('media:txt'),
4652
+ makeCapStep('cap:in="media:jpg";c;out="media:ext=txt"', 'c', 'media:jpg', 'media:ext=txt', false, false),
4653
+ makeCollectStep('media:ext=txt'),
4348
4654
  ];
4349
4655
  const { capStepIndices, capFlags } = rendererClassifyStrandCapSteps(steps);
4350
4656
  assertEqual(capStepIndices.length, 3, 'three cap steps');
@@ -4455,12 +4761,12 @@ function test0211_Renderer_buildStrandGraphData_foreachCollectSpan() {
4455
4761
  target_media_urn: 'media:txt;list',
4456
4762
  steps: [
4457
4763
  makeForEachStep('media:pdf;list'),
4458
- makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
4459
- makeCollectStep('media:txt'),
4764
+ makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
4765
+ makeCollectStep('media:ext=txt'),
4460
4766
  ],
4461
4767
  }, {
4462
4768
  'media:pdf;list': 'PDF List',
4463
- 'media:txt': 'Plain Text',
4769
+ 'media:ext=txt': 'Plain Text',
4464
4770
  'media:txt;list': 'Text List',
4465
4771
  });
4466
4772
  const built = rendererBuildStrandGraphData(payload);
@@ -4603,12 +4909,12 @@ function test0215_Renderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4603
4909
  target_media_urn: 'media:txt;list',
4604
4910
  steps: [
4605
4911
  makeForEachStep('media:pdf;list'),
4606
- makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
4607
- makeCollectStep('media:txt'),
4912
+ makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
4913
+ makeCollectStep('media:ext=txt'),
4608
4914
  ],
4609
4915
  }, {
4610
4916
  'media:pdf;list': 'PDF List',
4611
- 'media:txt': 'Plain Text',
4917
+ 'media:ext=txt': 'Plain Text',
4612
4918
  'media:txt;list': 'Text List',
4613
4919
  });
4614
4920
  const built = rendererBuildStrandGraphData(payload);
@@ -4741,7 +5047,7 @@ function test0217_Renderer_collapseStrand_standaloneCollectCollapses() {
4741
5047
  function test0218_Renderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4742
5048
  // Regression test mirroring the user's real strand:
4743
5049
  // [Cap_disbind (output_is_sequence=true), ForEach, Cap_make_decision],
4744
- // source = media:pdf, target = media:decision (equivalent to
5050
+ // source = media:ext=pdf, target = media:decision (equivalent to
4745
5051
  // the last cap's to_spec).
4746
5052
  //
4747
5053
  // Expected render shape after collapse:
@@ -4756,15 +5062,15 @@ function test0218_Renderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4756
5062
  // No separate output node because step_2's to_spec equals the
4757
5063
  // strand target.
4758
5064
  const payload = withMediaDisplayNames({
4759
- source_media_urn: 'media:pdf',
5065
+ source_media_urn: 'media:ext=pdf',
4760
5066
  target_media_urn: 'media:decision',
4761
5067
  steps: [
4762
- makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5068
+ makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
4763
5069
  makeForEachStep('media:page'),
4764
5070
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
4765
5071
  ],
4766
5072
  }, {
4767
- 'media:pdf': 'PDF',
5073
+ 'media:ext=pdf': 'PDF',
4768
5074
  'media:page': 'Page',
4769
5075
  'media:decision': 'Decision',
4770
5076
  });
@@ -4904,12 +5210,12 @@ function test0223_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
4904
5210
  // failures.
4905
5211
  const strand = {
4906
5212
  source_media_urn: 'media:pdf;list',
4907
- target_media_urn: 'media:txt',
5213
+ target_media_urn: 'media:ext=txt',
4908
5214
  steps: [
4909
5215
  makeForEachStep('media:pdf;list'),
4910
- makeCapStep('cap:in="media:pdf";a;out="media:image;png"', 'a', 'media:pdf', 'media:image;png', false, false),
4911
- makeCapStep('cap:in="media:image;png";b;out="media:txt"', 'b', 'media:image;png', 'media:txt', false, false),
4912
- makeCollectStep('media:txt'),
5216
+ makeCapStep('cap:in="media:ext=pdf";a;out="media:image;png"', 'a', 'media:ext=pdf', 'media:image;png', false, false),
5217
+ makeCapStep('cap:in="media:image;png";b;out="media:ext=txt"', 'b', 'media:image;png', 'media:ext=txt', false, false),
5218
+ makeCollectStep('media:ext=txt'),
4913
5219
  ],
4914
5220
  };
4915
5221
  const outcomes = [];
@@ -4924,7 +5230,7 @@ function test0223_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
4924
5230
  saved_paths: [],
4925
5231
  total_bytes: 0,
4926
5232
  duration_ms: 0,
4927
- failed_cap: 'cap:in="media:image;png";b;out="media:txt"',
5233
+ failed_cap: 'cap:in="media:image;png";b;out="media:ext=txt"',
4928
5234
  error: 'oom',
4929
5235
  });
4930
5236
  }
@@ -4932,9 +5238,9 @@ function test0223_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
4932
5238
  resolved_strand: strand,
4933
5239
  media_display_names: {
4934
5240
  'media:pdf;list': 'PDF List',
4935
- 'media:pdf': 'PDF',
5241
+ 'media:ext=pdf': 'PDF',
4936
5242
  'media:image;png': 'PNG',
4937
- 'media:txt': 'Text',
5243
+ 'media:ext=txt': 'Text',
4938
5244
  },
4939
5245
  body_outcomes: outcomes,
4940
5246
  visible_success_count: 3,
@@ -4971,19 +5277,19 @@ function test0224_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
4971
5277
  // replica emits 1 entry node + 1 body cap node = 2 nodes.
4972
5278
  const strand = {
4973
5279
  source_media_urn: 'media:pdf;list',
4974
- target_media_urn: 'media:txt',
5280
+ target_media_urn: 'media:ext=txt',
4975
5281
  steps: [
4976
5282
  makeForEachStep('media:pdf;list'),
4977
- makeCapStep('cap:in="media:pdf";a;out="media:txt"', 'a', 'media:pdf', 'media:txt', false, false),
4978
- makeCollectStep('media:txt'),
5283
+ makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=txt"', 'a', 'media:ext=pdf', 'media:ext=txt', false, false),
5284
+ makeCollectStep('media:ext=txt'),
4979
5285
  ],
4980
5286
  };
4981
5287
  const payload = {
4982
5288
  resolved_strand: strand,
4983
5289
  media_display_names: {
4984
5290
  'media:pdf;list': 'PDF List',
4985
- 'media:pdf': 'PDF',
4986
- 'media:txt': 'Text',
5291
+ 'media:ext=pdf': 'PDF',
5292
+ 'media:ext=txt': 'Text',
4987
5293
  },
4988
5294
  body_outcomes: [
4989
5295
  { body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, error: 'unknown' },
@@ -5074,10 +5380,10 @@ function test0226_Renderer_buildRunGraphData_backboneHasNoForeachNode() {
5074
5380
  // step_2 (Decision, merged target). No separate `for each` or
5075
5381
  // `collect` boxes.
5076
5382
  const strand = {
5077
- source_media_urn: 'media:pdf',
5383
+ source_media_urn: 'media:ext=pdf',
5078
5384
  target_media_urn: 'media:decision',
5079
5385
  steps: [
5080
- makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5386
+ makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5081
5387
  makeForEachStep('media:page'),
5082
5388
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5083
5389
  ],
@@ -5085,7 +5391,7 @@ function test0226_Renderer_buildRunGraphData_backboneHasNoForeachNode() {
5085
5391
  const payload = {
5086
5392
  resolved_strand: strand,
5087
5393
  media_display_names: {
5088
- 'media:pdf': 'PDF',
5394
+ 'media:ext=pdf': 'PDF',
5089
5395
  'media:page': 'Page',
5090
5396
  'media:decision': 'Decision',
5091
5397
  },
@@ -5123,10 +5429,10 @@ function test0227_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
5123
5429
  // doesn't see a stale "Decision" placeholder alongside their
5124
5430
  // failed replicas.
5125
5431
  const strand = {
5126
- source_media_urn: 'media:pdf',
5432
+ source_media_urn: 'media:ext=pdf',
5127
5433
  target_media_urn: 'media:decision',
5128
5434
  steps: [
5129
- makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5435
+ makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5130
5436
  makeForEachStep('media:page'),
5131
5437
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5132
5438
  ],
@@ -5135,7 +5441,7 @@ function test0227_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
5135
5441
  const payload = {
5136
5442
  resolved_strand: strand,
5137
5443
  media_display_names: {
5138
- 'media:pdf': 'PDF',
5444
+ 'media:ext=pdf': 'PDF',
5139
5445
  'media:page': 'Page',
5140
5446
  'media:decision': 'Decision',
5141
5447
  },
@@ -5194,10 +5500,10 @@ function test0228_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
5194
5500
  // → body_n_0 (per-body Decision)
5195
5501
  // (no merge edge back into the backbone)
5196
5502
  const strand = {
5197
- source_media_urn: 'media:pdf',
5503
+ source_media_urn: 'media:ext=pdf',
5198
5504
  target_media_urn: 'media:decision',
5199
5505
  steps: [
5200
- makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5506
+ makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5201
5507
  makeForEachStep('media:page'),
5202
5508
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5203
5509
  ],
@@ -5205,7 +5511,7 @@ function test0228_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
5205
5511
  const payload = {
5206
5512
  resolved_strand: strand,
5207
5513
  media_display_names: {
5208
- 'media:pdf': 'PDF',
5514
+ 'media:ext=pdf': 'PDF',
5209
5515
  'media:page': 'Page',
5210
5516
  'media:decision': 'Decision',
5211
5517
  },
@@ -5258,16 +5564,16 @@ function test0229_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
5258
5564
  target_media_urn: 'media:txt;list',
5259
5565
  steps: [
5260
5566
  makeForEachStep('media:pdf;list'),
5261
- makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
5262
- makeCollectStep('media:txt'),
5567
+ makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
5568
+ makeCollectStep('media:ext=txt'),
5263
5569
  ],
5264
5570
  };
5265
5571
  const payload = {
5266
5572
  resolved_strand: strand,
5267
5573
  media_display_names: {
5268
5574
  'media:pdf;list': 'PDF List',
5269
- 'media:pdf': 'PDF',
5270
- 'media:txt': 'Text',
5575
+ 'media:ext=pdf': 'PDF',
5576
+ 'media:ext=txt': 'Text',
5271
5577
  'media:txt;list': 'Text List',
5272
5578
  },
5273
5579
  body_outcomes: [
@@ -5427,7 +5733,7 @@ function test0235_Renderer_buildEditorGraphData_rejectsEdgeWithMissingSource() {
5427
5733
  // ---------------- resolved-machine builder ----------------
5428
5734
 
5429
5735
  function test0236_Renderer_buildResolvedMachineGraphData_singleStrandLinearChain() {
5430
- // A single-strand machine: media:pdf → extract → media:txt
5736
+ // A single-strand machine: media:ext=pdf → extract → media:ext=txt
5431
5737
  // → embed → media:embedding. Two edges, three nodes, no
5432
5738
  // loops, no fan-in. Tests the basic shape — nodes and
5433
5739
  // edges flow through verbatim from the resolved machine
@@ -5436,28 +5742,28 @@ function test0236_Renderer_buildResolvedMachineGraphData_singleStrandLinearChain
5436
5742
  strands: [
5437
5743
  {
5438
5744
  nodes: [
5439
- { id: 'n0', urn: 'media:pdf', title: 'PDF' },
5440
- { id: 'n1', urn: 'media:txt;textable', title: 'Plain Text' },
5745
+ { id: 'n0', urn: 'media:ext=pdf', title: 'PDF' },
5746
+ { id: 'n1', urn: 'media:enc=utf-8;ext=txt', title: 'Plain Text' },
5441
5747
  { id: 'n2', urn: 'media:embedding;record', title: 'Embedding Record' },
5442
5748
  ],
5443
5749
  edges: [
5444
5750
  {
5445
5751
  alias: 'edge_0',
5446
- cap_urn: 'cap:in=media:pdf;extract;out=media:txt;textable',
5752
+ cap_urn: 'cap:in="media:ext=pdf";extract;out=media:enc=utf-8;ext=txt',
5447
5753
  title: 'Extract Text',
5448
5754
  is_loop: false,
5449
5755
  assignment: [
5450
- { cap_arg_media_urn: 'media:pdf', source_node: 'n0' },
5756
+ { cap_arg_media_urn: 'media:ext=pdf', source_node: 'n0' },
5451
5757
  ],
5452
5758
  target_node: 'n1',
5453
5759
  },
5454
5760
  {
5455
5761
  alias: 'edge_1',
5456
- cap_urn: 'cap:in=media:textable;embed;out=media:embedding;record',
5762
+ cap_urn: 'cap:in=media:enc=utf-8;embed;out=media:embedding;record',
5457
5763
  title: 'Generate Embedding',
5458
5764
  is_loop: false,
5459
5765
  assignment: [
5460
- { cap_arg_media_urn: 'media:textable', source_node: 'n1' },
5766
+ { cap_arg_media_urn: 'media:enc=utf-8', source_node: 'n1' },
5461
5767
  ],
5462
5768
  target_node: 'n2',
5463
5769
  },
@@ -5493,17 +5799,17 @@ function test0237_Renderer_buildResolvedMachineGraphData_loopEdgeGetsLoopClass()
5493
5799
  strands: [
5494
5800
  {
5495
5801
  nodes: [
5496
- { id: 'n0', urn: 'media:page;textable', title: 'Page' },
5497
- { id: 'n1', urn: 'media:decision;json;record;textable', title: 'Decision Record' },
5802
+ { id: 'n0', urn: 'media:enc=utf-8;page', title: 'Page' },
5803
+ { id: 'n1', urn: 'media:decision;fmt=json;record', title: 'Decision Record' },
5498
5804
  ],
5499
5805
  edges: [
5500
5806
  {
5501
5807
  alias: 'edge_0',
5502
- cap_urn: 'cap:in=media:textable;make-decision;out=media:decision;json;record;textable',
5808
+ cap_urn: 'cap:in=media:enc=utf-8;make-decision;out=media:decision;fmt=json;record',
5503
5809
  title: 'Make Decision',
5504
5810
  is_loop: true,
5505
5811
  assignment: [
5506
- { cap_arg_media_urn: 'media:textable', source_node: 'n0' },
5812
+ { cap_arg_media_urn: 'media:enc=utf-8', source_node: 'n0' },
5507
5813
  ],
5508
5814
  target_node: 'n1',
5509
5815
  },
@@ -5530,18 +5836,18 @@ function test0238_Renderer_buildResolvedMachineGraphData_fanInProducesEdgePerAss
5530
5836
  {
5531
5837
  nodes: [
5532
5838
  { id: 'n0', urn: 'media:image;png', title: 'PNG Image' },
5533
- { id: 'n1', urn: 'media:model-spec;textable', title: 'Model Spec' },
5534
- { id: 'n2', urn: 'media:image-description;textable', title: 'Image Description' },
5839
+ { id: 'n1', urn: 'media:enc=utf-8;model-spec', title: 'Model Spec' },
5840
+ { id: 'n2', urn: 'media:enc=utf-8;image-description', title: 'Image Description' },
5535
5841
  ],
5536
5842
  edges: [
5537
5843
  {
5538
5844
  alias: 'edge_0',
5539
- cap_urn: 'cap:in=media:image;png;describe-image;out=media:image-description;textable',
5845
+ cap_urn: 'cap:in=media:image;png;describe-image;out=media:enc=utf-8;image-description',
5540
5846
  title: 'Describe Image',
5541
5847
  is_loop: false,
5542
5848
  assignment: [
5543
5849
  { cap_arg_media_urn: 'media:image;png', source_node: 'n0' },
5544
- { cap_arg_media_urn: 'media:model-spec;textable', source_node: 'n1' },
5850
+ { cap_arg_media_urn: 'media:enc=utf-8;model-spec', source_node: 'n1' },
5545
5851
  ],
5546
5852
  target_node: 'n2',
5547
5853
  },
@@ -5571,17 +5877,17 @@ function test0239_Renderer_buildResolvedMachineGraphData_multiStrandKeepsStrands
5571
5877
  strands: [
5572
5878
  {
5573
5879
  nodes: [
5574
- { id: 'n0', urn: 'media:pdf', title: 'PDF' },
5575
- { id: 'n1', urn: 'media:txt;textable', title: 'Plain Text' },
5880
+ { id: 'n0', urn: 'media:ext=pdf', title: 'PDF' },
5881
+ { id: 'n1', urn: 'media:enc=utf-8;ext=txt', title: 'Plain Text' },
5576
5882
  ],
5577
5883
  edges: [
5578
5884
  {
5579
5885
  alias: 'edge_0',
5580
- cap_urn: 'cap:in=media:pdf;extract;out=media:txt;textable',
5886
+ cap_urn: 'cap:in="media:ext=pdf";extract;out=media:enc=utf-8;ext=txt',
5581
5887
  title: 'Extract Text',
5582
5888
  is_loop: false,
5583
5889
  assignment: [
5584
- { cap_arg_media_urn: 'media:pdf', source_node: 'n0' },
5890
+ { cap_arg_media_urn: 'media:ext=pdf', source_node: 'n0' },
5585
5891
  ],
5586
5892
  target_node: 'n1',
5587
5893
  },
@@ -5591,17 +5897,17 @@ function test0239_Renderer_buildResolvedMachineGraphData_multiStrandKeepsStrands
5591
5897
  },
5592
5898
  {
5593
5899
  nodes: [
5594
- { id: 'n2', urn: 'media:json;record;textable', title: 'JSON Record' },
5595
- { id: 'n3', urn: 'media:csv;list;record;textable', title: 'CSV Rows' },
5900
+ { id: 'n2', urn: 'media:fmt=json;record', title: 'JSON Record' },
5901
+ { id: 'n3', urn: 'media:fmt=csv;list;record', title: 'CSV Rows' },
5596
5902
  ],
5597
5903
  edges: [
5598
5904
  {
5599
5905
  alias: 'edge_1',
5600
- cap_urn: 'cap:in=media:json;record;textable;convert-format;out=media:csv;list;record;textable',
5906
+ cap_urn: 'cap:in=media:fmt=json;record;convert-format;out=media:fmt=csv;list;record',
5601
5907
  title: 'Convert Format',
5602
5908
  is_loop: false,
5603
5909
  assignment: [
5604
- { cap_arg_media_urn: 'media:json;record;textable', source_node: 'n2' },
5910
+ { cap_arg_media_urn: 'media:fmt=json;record', source_node: 'n2' },
5605
5911
  ],
5606
5912
  target_node: 'n3',
5607
5913
  },
@@ -5633,7 +5939,7 @@ function test0240_Renderer_buildResolvedMachineGraphData_duplicateNodeIdAcrossSt
5633
5939
  const payload = {
5634
5940
  strands: [
5635
5941
  {
5636
- nodes: [{ id: 'n0', urn: 'media:pdf', title: 'PDF' }],
5942
+ nodes: [{ id: 'n0', urn: 'media:ext=pdf', title: 'PDF' }],
5637
5943
  edges: [],
5638
5944
  input_anchor_nodes: ['n0'],
5639
5945
  output_anchor_nodes: ['n0'],
@@ -5733,7 +6039,7 @@ function test1801_kindSourceWhenInputIsVoid() {
5733
6039
  const warm = CapUrn.fromString('cap:in=media:void;out="media:model-artifact";warm');
5734
6040
  assertEqual(warm.kind(), CapKind.SOURCE, 'warm cap is a Source');
5735
6041
 
5736
- const gen = CapUrn.fromString('cap:in=media:void;out=media:textable');
6042
+ const gen = CapUrn.fromString('cap:in=media:void;out="media:enc=utf-8"');
5737
6043
  assertEqual(gen.kind(), CapKind.SOURCE, 'in=void with concrete out is a Source');
5738
6044
  }
5739
6045
 
@@ -5742,7 +6048,7 @@ function test1802_kindSinkWhenOutputIsVoid() {
5742
6048
  const discard = CapUrn.fromString('cap:discard;in=media:;out=media:void');
5743
6049
  assertEqual(discard.kind(), CapKind.SINK, 'discard cap is a Sink');
5744
6050
 
5745
- const log = CapUrn.fromString('cap:in="media:json;textable";log;out=media:void');
6051
+ const log = CapUrn.fromString('cap:in="media:fmt=json";log;out=media:void');
5746
6052
  assertEqual(log.kind(), CapKind.SINK, 'log cap is a Sink');
5747
6053
  }
5748
6054
 
@@ -5759,7 +6065,7 @@ function test1803_kindEffectWhenBothSidesVoid() {
5759
6065
  // TEST1804: Transform classifier — at least one side non-void, and
5760
6066
  // the cap is not the bare identity.
5761
6067
  function test1804_kindTransformForNormalDataProcessors() {
5762
- const extract = CapUrn.fromString('cap:extract;in=media:pdf;out="media:record;textable"');
6068
+ const extract = CapUrn.fromString('cap:extract;in="media:ext=pdf";out="media:enc=utf-8;record"');
5763
6069
  assertEqual(extract.kind(), CapKind.TRANSFORM, 'extract is a Transform');
5764
6070
 
5765
6071
  const labeled = CapUrn.fromString('cap:passthrough;in=media:;out=media:');
@@ -5812,13 +6118,13 @@ function test1805_kindInvariantUnderCanonicalSpellings() {
5812
6118
  const cases = [
5813
6119
  { a: 'cap:effect=none', b: 'cap:in=media:;out=media:;effect=none', expected: CapKind.IDENTITY },
5814
6120
  {
5815
- a: 'cap:extract;in=media:pdf;out=media:textable',
5816
- b: 'cap:extract;in="media:pdf";out="media:textable"',
6121
+ a: 'cap:extract;in="media:ext=pdf";out="media:enc=utf-8"',
6122
+ b: 'cap:extract;in="media:ext=pdf";out="media:enc=utf-8"',
5817
6123
  expected: CapKind.TRANSFORM,
5818
6124
  },
5819
6125
  {
5820
- a: 'cap:in=media:void;out=media:textable;warm',
5821
- b: 'cap:warm;out=media:textable;in=media:void',
6126
+ a: 'cap:in=media:void;out="media:enc=utf-8";warm',
6127
+ b: 'cap:warm;out="media:enc=utf-8";in=media:void',
5822
6128
  expected: CapKind.SOURCE,
5823
6129
  },
5824
6130
  ];
@@ -6017,9 +6323,9 @@ function test1843_rejectInvalidCombinations() {
6017
6323
 
6018
6324
  // TEST1844: out-axis difference dominates combined in+y differences.
6019
6325
  function test1844_axisWeightingOutDominates() {
6020
- const bigOut = CapUrn.fromString('cap:in=media:;out="media:record;textable"');
6326
+ const bigOut = CapUrn.fromString('cap:in=media:;out="media:enc=utf-8;record"');
6021
6327
  const bigInAndY = CapUrn.fromString(
6022
- 'cap:in=media:pdf;out=media:record;!constrained;?target;extract;stage!=alpha;target2=metadata;ver?=draft'
6328
+ 'cap:in="media:ext=pdf";out=media:record;!constrained;?target;extract;stage!=alpha;target2=metadata;ver?=draft'
6023
6329
  );
6024
6330
  assert(bigOut.specificity() > bigInAndY.specificity(),
6025
6331
  'out-axis difference must dominate combined in+y differences');
@@ -6027,7 +6333,7 @@ function test1844_axisWeightingOutDominates() {
6027
6333
 
6028
6334
  // TEST1845: With equal out, in-axis dominates over y-axis.
6029
6335
  function test1845_axisWeightingInDominatesY() {
6030
- const bigIn = CapUrn.fromString('cap:in=media:pdf;out=media:record');
6336
+ const bigIn = CapUrn.fromString('cap:in="media:ext=pdf";out=media:record');
6031
6337
  const bigY = CapUrn.fromString(
6032
6338
  'cap:in=media:;out=media:record;!constrained;?target;extract;stage!=alpha;target2=metadata;ver?=draft'
6033
6339
  );
@@ -6049,7 +6355,7 @@ function test1846_axisWeightingDecodedLayout() {
6049
6355
 
6050
6356
  // TEST1847: Cap with version=0 round-trips with no `version` key on wire
6051
6357
  function test1847_capVersionZeroOmittedOnWire() {
6052
- const urn = CapUrn.fromString('cap:in="media:void";test-op;out="media:record;textable"');
6358
+ const urn = CapUrn.fromString('cap:in="media:void";test-op;out="media:enc=utf-8;record"');
6053
6359
  const cap = new Cap(urn, 'Test Cap', 'test-op');
6054
6360
  // version defaults to 0
6055
6361
  assertEqual(cap.version, 0, 'Default version should be 0');
@@ -6061,7 +6367,7 @@ function test1847_capVersionZeroOmittedOnWire() {
6061
6367
 
6062
6368
  // TEST1848: Cap with version=N round-trips with `version: N` on wire
6063
6369
  function test1848_capVersionNonZeroOnWire() {
6064
- const urn = CapUrn.fromString('cap:in="media:void";versioned-op;out="media:record;textable"');
6370
+ const urn = CapUrn.fromString('cap:in="media:void";versioned-op;out="media:enc=utf-8;record"');
6065
6371
  const cap = new Cap(urn, 'Versioned Cap', 'versioned-op');
6066
6372
  cap.version = 42;
6067
6373
  const json = cap.toJSON();
@@ -6144,7 +6450,7 @@ async function runTests() {
6144
6450
  // media_urn.rs: TEST060-TEST078
6145
6451
  console.log('\n--- media_urn.rs ---');
6146
6452
  runTest('TEST060: wrong_prefix_fails', test060_wrongPrefixFails);
6147
- runTest('TEST061: is_binary', test061_isBinary);
6453
+ console.log(' SKIP TEST061: REMOVED (binary/text distinction gone; see TEST067 for enc=)');
6148
6454
  runTest('TEST062: is_record', test062_isRecord);
6149
6455
  runTest('TEST063: is_scalar', test063_isScalar);
6150
6456
  runTest('TEST064: is_list', test064_isList);
@@ -6258,6 +6564,27 @@ async function runTests() {
6258
6564
  runTest('TEST334: cartridge_repo_client_needs_sync', test334_cartridgeRepoClientNeedsSync);
6259
6565
  runTest('TEST335: cartridge_repo_server_client_integration', test335_cartridgeRepoServerClientIntegration);
6260
6566
 
6567
+ // cartridge_repo.rs: TEST1849-TEST1853 (host-compatibility resolution)
6568
+ console.log('\n--- cartridge_repo.rs (resolve_for_host) ---');
6569
+ runTest('TEST1849: resolve_for_host_compatible_latest', test1849_resolveForHostCompatibleLatest);
6570
+ runTest('TEST1850: resolve_for_host_compatible_outdated', test1850_resolveForHostCompatibleOutdated);
6571
+ runTest('TEST1851: resolve_for_host_incompatible', test1851_resolveForHostIncompatible);
6572
+ runTest('TEST1852: resolve_for_host_skips_build_with_no_installer', test1852_resolveForHostSkipsBuildWithNoInstaller);
6573
+ runTest('TEST1853: host_platform_normalized_form', test1853_hostPlatformNormalizedForm);
6574
+
6575
+ // manifest.rs: TEST1872-TEST1874 (registry_url_from_build_env)
6576
+ console.log('\n--- manifest.rs (registry_url_from_build_env) ---');
6577
+ runTest('TEST1872: registry_url_from_build_env_passes_through_nonempty', test1872_registryUrlFromBuildEnvPassesThroughNonempty);
6578
+ runTest('TEST1873: registry_url_from_build_env_none_for_dev', test1873_registryUrlFromBuildEnvNoneForDev);
6579
+ runTest('TEST1874: registry_url_from_build_env_rejects_empty_string', test1874_registryUrlFromBuildEnvRejectsEmptyString);
6580
+
6581
+ // cartridge_discovery.rs: TEST1875-TEST1878 (scan-all discovery)
6582
+ console.log('\n--- cartridge_discovery.rs (discover_cartridges) ---');
6583
+ await runTest('TEST1875: scan_all_reaches_both_dev_and_registry_slugs', test1875_scanAllReachesBothDevAndRegistrySlugs);
6584
+ await runTest('TEST1876: other_channel_subtree_is_skipped', test1876_otherChannelSubtreeIsSkipped);
6585
+ await runTest('TEST1877: registry_cartridge_under_wrong_slug_is_bad_install', test1877_registryCartridgeUnderWrongSlugIsBadInstall);
6586
+ await runTest('TEST1878: bundled_provider_without_baked_hash_is_rejected', test1878_bundledProviderWithoutBakedHashIsRejected);
6587
+
6261
6588
  // media_urn.rs: TEST1312-TEST1315, TEST1298-TEST1302 (MediaUrn predicates)
6262
6589
  console.log('\n--- media_urn.rs (predicates) ---');
6263
6590
  runTest('TEST1312: is_image', test1312_isImage);