capdag 0.183.463 → 0.186.476

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
@@ -5,12 +5,12 @@
5
5
  const {
6
6
  CapUrn, CapKind, CapEffect, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
7
7
  MediaUrn, MediaUrnError, MediaUrnErrorCodes,
8
- Cap, CapGroup, CapManifest, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
8
+ Cap, CapGroup, CapManifest, MediaDef, MediaDefError, MediaDefErrorCodes,
9
9
  resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
10
10
  CartridgeInfo, CartridgeCapSummary, CartridgeSuggestion, CartridgeRepoClient, CartridgeRepoServer,
11
11
  CapFabEdge, CapFabStats, CapFab,
12
12
  StdinSource, StdinSourceKind,
13
- validateNoMediaSpecRedefinitionSync,
13
+ validateNoMediaDefRedefinitionSync,
14
14
  CapArgumentValue, CapArg, ArgSource, validateCapArgs, ValidationError,
15
15
  llmGenerateTextUrn, modelAvailabilityUrn, modelPathUrn,
16
16
  MachineSyntaxError, MachineSyntaxErrorCodes, MachineEdge, Machine, MachineBuilder, parseMachine, parseMachineWithAST,
@@ -318,7 +318,7 @@ function test939_capUrnCanonicalFormDropsWildcardInOut() {
318
318
 
319
319
  const identity = CapUrn.fromString('cap:effect=none');
320
320
  assertEqual(identity.toString(), 'cap:effect=none', 'true identity must preserve explicit effect=none');
321
- assert(identity.toString() !== generic.toString(), 'cap: and cap:effect=none must not collapse');
321
+ assert(identity.toString() !== 'cap:', 'cap:effect=none must not collapse to the illegal bare top form');
322
322
  }
323
323
 
324
324
  // TEST017: Test tag matching: exact match, subset match, wildcard match, value mismatch
@@ -692,9 +692,9 @@ function test047_matchingSemanticsThumbnailVoidInput() {
692
692
 
693
693
  // TEST048: Matching semantics - wildcard direction matches anything
694
694
  function test048_matchingSemanticsWildcardDirection() {
695
- const cap = CapUrn.fromString('cap:in=*;out=*;op');
695
+ const cap = CapUrn.fromString('cap:generate');
696
696
  const request = CapUrn.fromString(testUrn('generate;ext=pdf'));
697
- assert(cap.accepts(request), 'Wildcard cap should accept any request');
697
+ assert(cap.accepts(request), 'Generic declared directions should accept a more specific matching request');
698
698
  }
699
699
 
700
700
  // TEST049: Non-overlapping tags — neither direction accepts
@@ -809,10 +809,10 @@ function test891_directionSemanticSpecificity() {
809
809
 
810
810
  // TEST053: N/A for JS (Rust-only validation infrastructure)
811
811
 
812
- // TEST054: XV5 - Test inline media spec redefinition of existing registry spec is detected and rejected
812
+ // TEST054: XV5 - Test inline media def redefinition of existing registry spec is detected and rejected
813
813
  function test054_xv5InlineSpecRedefinitionDetected() {
814
814
  const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
815
- const mediaSpecs = [
815
+ const mediaDefs = [
816
816
  {
817
817
  urn: MEDIA_STRING,
818
818
  media_type: 'text/plain',
@@ -820,16 +820,16 @@ function test054_xv5InlineSpecRedefinitionDetected() {
820
820
  description: 'Trying to redefine string'
821
821
  }
822
822
  ];
823
- const result = validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup);
823
+ const result = validateNoMediaDefRedefinitionSync(mediaDefs, registryLookup);
824
824
  assert(!result.valid, 'Should fail when redefining registry spec');
825
825
  assert(result.error && result.error.includes('XV5'), 'Error should mention XV5');
826
826
  assert(result.redefines && result.redefines.includes(MEDIA_STRING), 'Should identify MEDIA_STRING as redefined');
827
827
  }
828
828
 
829
- // TEST055: XV5 - Test new inline media spec (not in registry) is allowed
829
+ // TEST055: XV5 - Test new inline media def (not in registry) is allowed
830
830
  function test055_xv5NewInlineSpecAllowed() {
831
831
  const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
832
- const mediaSpecs = [
832
+ const mediaDefs = [
833
833
  {
834
834
  urn: 'media:my-unique-custom-type-xyz123',
835
835
  media_type: 'application/json',
@@ -837,16 +837,16 @@ function test055_xv5NewInlineSpecAllowed() {
837
837
  description: 'A custom output type'
838
838
  }
839
839
  ];
840
- const result = validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup);
840
+ const result = validateNoMediaDefRedefinitionSync(mediaDefs, registryLookup);
841
841
  assert(result.valid, 'New spec not in registry should pass validation');
842
842
  }
843
843
 
844
- // TEST056: XV5 - Test empty media_specs (no inline specs) passes XV5 validation
845
- function test056_xv5EmptyMediaSpecsAllowed() {
844
+ // TEST056: XV5 - Test empty media_defs (no inline specs) passes XV5 validation
845
+ function test056_xv5EmptyMediaDefsAllowed() {
846
846
  const registryLookup = (mediaUrn) => mediaUrn === MEDIA_STRING;
847
- assert(validateNoMediaSpecRedefinitionSync({}, registryLookup).valid, 'Empty object should pass');
848
- assert(validateNoMediaSpecRedefinitionSync(null, registryLookup).valid, 'Null should pass');
849
- assert(validateNoMediaSpecRedefinitionSync(undefined, registryLookup).valid, 'Undefined should pass');
847
+ assert(validateNoMediaDefRedefinitionSync({}, registryLookup).valid, 'Empty object should pass');
848
+ assert(validateNoMediaDefRedefinitionSync(null, registryLookup).valid, 'Null should pass');
849
+ assert(validateNoMediaDefRedefinitionSync(undefined, registryLookup).valid, 'Undefined should pass');
850
850
  }
851
851
 
852
852
  // ============================================================================
@@ -1028,26 +1028,26 @@ function test078_debugMatchingBehavior() {
1028
1028
  }
1029
1029
 
1030
1030
  // ============================================================================
1031
- // media_spec.rs: TEST088-TEST110
1031
+ // media_def.rs: TEST088-TEST110
1032
1032
  // ============================================================================
1033
1033
 
1034
1034
  // TEST088: N/A for JS (async registry, Rust-only)
1035
1035
  // TEST089: N/A for JS
1036
1036
  // TEST090: N/A for JS
1037
1037
 
1038
- // TEST091: Test resolving custom media URN from local media_specs takes precedence over registry
1039
- function test091_resolveCustomMediaSpec() {
1040
- const mediaSpecs = [
1038
+ // TEST091: Test resolving custom media URN from local media_defs takes precedence over registry
1039
+ function test091_resolveCustomMediaDef() {
1040
+ const mediaDefs = [
1041
1041
  { urn: 'media:custom-json', media_type: 'application/json', title: 'Custom JSON', profile_uri: 'https://example.com/schema/custom' }
1042
1042
  ];
1043
- const spec = resolveMediaUrn('media:custom-json', mediaSpecs);
1043
+ const spec = resolveMediaUrn('media:custom-json', mediaDefs);
1044
1044
  assertEqual(spec.contentType, 'application/json', 'Should resolve custom spec');
1045
1045
  assertEqual(spec.profile, 'https://example.com/schema/custom', 'Should have custom profile');
1046
1046
  }
1047
1047
 
1048
- // TEST092: Test resolving custom record media spec with schema from local media_specs
1048
+ // TEST092: Test resolving custom record media def with schema from local media_defs
1049
1049
  function test092_resolveCustomWithSchema() {
1050
- const mediaSpecs = [
1050
+ const mediaDefs = [
1051
1051
  {
1052
1052
  urn: 'media:rich-xml',
1053
1053
  media_type: 'application/xml',
@@ -1056,7 +1056,7 @@ function test092_resolveCustomWithSchema() {
1056
1056
  schema: { type: 'object' }
1057
1057
  }
1058
1058
  ];
1059
- const spec = resolveMediaUrn('media:rich-xml', mediaSpecs);
1059
+ const spec = resolveMediaUrn('media:rich-xml', mediaDefs);
1060
1060
  assertEqual(spec.contentType, 'application/xml', 'Should resolve rich spec');
1061
1061
  assert(spec.schema !== null, 'Should have schema');
1062
1062
  assertEqual(spec.schema.type, 'object', 'Schema should have correct type');
@@ -1068,7 +1068,7 @@ function test093_resolveUnresolvableFailsHard() {
1068
1068
  try {
1069
1069
  resolveMediaUrn('media:nonexistent', []);
1070
1070
  } catch (e) {
1071
- if (e instanceof MediaSpecError && e.code === MediaSpecErrorCodes.UNRESOLVABLE_MEDIA_URN) {
1071
+ if (e instanceof MediaDefError && e.code === MediaDefErrorCodes.UNRESOLVABLE_MEDIA_URN) {
1072
1072
  caught = true;
1073
1073
  }
1074
1074
  }
@@ -1081,45 +1081,45 @@ function test093_resolveUnresolvableFailsHard() {
1081
1081
  // TEST097: N/A for JS (Rust validation function)
1082
1082
  // TEST098: N/A for JS
1083
1083
 
1084
- // TEST099: Test ResolvedMediaSpec is_binary returns true when textable tag is absent
1084
+ // TEST099: Test ResolvedMediaDef is_binary returns true when textable tag is absent
1085
1085
  function test099_resolvedIsBinary() {
1086
- const spec = new MediaSpec('application/octet-stream', null, null, 'Binary', null, MEDIA_IDENTITY);
1086
+ const spec = new MediaDef('application/octet-stream', null, null, 'Binary', null, MEDIA_IDENTITY);
1087
1087
  assert(spec.isBinary(), 'Resolved binary spec should be binary');
1088
1088
  }
1089
1089
 
1090
- // TEST100: Test ResolvedMediaSpec is_record returns true when record marker is present
1090
+ // TEST100: Test ResolvedMediaDef is_record returns true when record marker is present
1091
1091
  function test100_resolvedIsRecord() {
1092
- const spec = new MediaSpec('application/json', null, null, 'Object', null, MEDIA_OBJECT);
1092
+ const spec = new MediaDef('application/json', null, null, 'Object', null, MEDIA_OBJECT);
1093
1093
  assert(spec.isRecord(), 'Resolved object spec should be record');
1094
1094
  }
1095
1095
 
1096
- // TEST101: Test ResolvedMediaSpec is_scalar returns true when list marker is absent
1096
+ // TEST101: Test ResolvedMediaDef is_scalar returns true when list marker is absent
1097
1097
  function test101_resolvedIsScalar() {
1098
- const spec = new MediaSpec('text/plain', null, null, 'String', null, MEDIA_STRING);
1098
+ const spec = new MediaDef('text/plain', null, null, 'String', null, MEDIA_STRING);
1099
1099
  assert(spec.isScalar(), 'Resolved string spec should be scalar');
1100
1100
  }
1101
1101
 
1102
- // TEST102: Test ResolvedMediaSpec is_list returns true when list marker is present
1102
+ // TEST102: Test ResolvedMediaDef is_list returns true when list marker is present
1103
1103
  function test102_resolvedIsList() {
1104
- const spec = new MediaSpec('text/plain', null, null, 'String List', null, MEDIA_STRING_LIST);
1104
+ const spec = new MediaDef('text/plain', null, null, 'String List', null, MEDIA_STRING_LIST);
1105
1105
  assert(spec.isList(), 'Resolved string_list spec should be list');
1106
1106
  }
1107
1107
 
1108
- // TEST103: Test ResolvedMediaSpec is_json returns true when json tag is present
1108
+ // TEST103: Test ResolvedMediaDef is_json returns true when json tag is present
1109
1109
  function test103_resolvedIsJson() {
1110
- const spec = new MediaSpec('application/json', null, null, 'JSON', null, MEDIA_JSON);
1110
+ const spec = new MediaDef('application/json', null, null, 'JSON', null, MEDIA_JSON);
1111
1111
  assert(spec.isJSON(), 'Resolved json spec should be JSON');
1112
1112
  }
1113
1113
 
1114
- // TEST104: Test ResolvedMediaSpec is_text returns true when textable tag is present
1114
+ // TEST104: Test ResolvedMediaDef is_text returns true when textable tag is present
1115
1115
  function test104_resolvedIsText() {
1116
- const spec = new MediaSpec('text/plain', null, null, 'String', null, MEDIA_STRING);
1116
+ const spec = new MediaDef('text/plain', null, null, 'String', null, MEDIA_STRING);
1117
1117
  assert(spec.isText(), 'Resolved string spec should be text');
1118
1118
  }
1119
1119
 
1120
- // TEST105: Test metadata propagates from media spec def to resolved media spec
1120
+ // TEST105: Test metadata propagates from media def def to resolved media def
1121
1121
  function test105_metadataPropagation() {
1122
- const mediaSpecs = [
1122
+ const mediaDefs = [
1123
1123
  {
1124
1124
  urn: 'media:custom-setting',
1125
1125
  media_type: 'text/plain',
@@ -1133,16 +1133,16 @@ function test105_metadataPropagation() {
1133
1133
  }
1134
1134
  }
1135
1135
  ];
1136
- const resolved = resolveMediaUrn('media:custom-setting', mediaSpecs);
1136
+ const resolved = resolveMediaUrn('media:custom-setting', mediaDefs);
1137
1137
  assert(resolved.metadata !== null, 'Should have metadata');
1138
1138
  assertEqual(resolved.metadata.category_key, 'interface', 'Should propagate category_key');
1139
1139
  assertEqual(resolved.metadata.ui_type, 'SETTING_UI_TYPE_CHECKBOX', 'Should propagate ui_type');
1140
1140
  assertEqual(resolved.metadata.display_index, 5, 'Should propagate display_index');
1141
1141
  }
1142
1142
 
1143
- // TEST106: Test metadata and validation can coexist in media spec definition
1143
+ // TEST106: Test metadata and validation can coexist in media definition
1144
1144
  function test106_metadataWithValidation() {
1145
- const mediaSpecs = [
1145
+ const mediaDefs = [
1146
1146
  {
1147
1147
  urn: 'media:bounded-number;numeric',
1148
1148
  media_type: 'text/plain',
@@ -1151,7 +1151,7 @@ function test106_metadataWithValidation() {
1151
1151
  metadata: { category_key: 'inference', ui_type: 'SETTING_UI_TYPE_SLIDER' }
1152
1152
  }
1153
1153
  ];
1154
- const resolved = resolveMediaUrn('media:bounded-number;numeric', mediaSpecs);
1154
+ const resolved = resolveMediaUrn('media:bounded-number;numeric', mediaDefs);
1155
1155
  assert(resolved.validation !== null, 'Should have validation');
1156
1156
  assertEqual(resolved.validation.min, 0, 'Should have min');
1157
1157
  assertEqual(resolved.validation.max, 100, 'Should have max');
@@ -1159,9 +1159,9 @@ function test106_metadataWithValidation() {
1159
1159
  assertEqual(resolved.metadata.category_key, 'inference', 'Should have category_key');
1160
1160
  }
1161
1161
 
1162
- // TEST107: Test extensions field propagates from media spec def to resolved
1162
+ // TEST107: Test extensions field propagates from media def def to resolved
1163
1163
  function test107_extensionsPropagation() {
1164
- const mediaSpecs = [
1164
+ const mediaDefs = [
1165
1165
  {
1166
1166
  urn: 'media:pdf',
1167
1167
  media_type: 'application/pdf',
@@ -1169,7 +1169,7 @@ function test107_extensionsPropagation() {
1169
1169
  extensions: ['pdf']
1170
1170
  }
1171
1171
  ];
1172
- const resolved = resolveMediaUrn('media:pdf', mediaSpecs);
1172
+ const resolved = resolveMediaUrn('media:pdf', mediaDefs);
1173
1173
  assert(Array.isArray(resolved.extensions), 'Extensions should be an array');
1174
1174
  assertEqual(resolved.extensions.length, 1, 'Should have one extension');
1175
1175
  assertEqual(resolved.extensions[0], 'pdf', 'Should have pdf extension');
@@ -1177,15 +1177,15 @@ function test107_extensionsPropagation() {
1177
1177
 
1178
1178
  // TEST108: Test creating new cap with URN, title, and command verifies correct initialization
1179
1179
  function test108_extensionsSerialization() {
1180
- // Test that MediaSpec can hold extensions correctly
1181
- const spec = new MediaSpec('application/pdf', null, null, 'PDF', null, 'media:pdf', null, null, ['pdf']);
1180
+ // Test that MediaDef can hold extensions correctly
1181
+ const spec = new MediaDef('application/pdf', null, null, 'PDF', null, 'media:pdf', null, null, ['pdf']);
1182
1182
  assert(Array.isArray(spec.extensions), 'Extensions should be array');
1183
1183
  assertEqual(spec.extensions[0], 'pdf', 'Should have pdf extension');
1184
1184
  }
1185
1185
 
1186
1186
  // TEST109: Test creating cap with metadata initializes and retrieves metadata correctly
1187
1187
  function test109_extensionsWithMetadataAndValidation() {
1188
- const mediaSpecs = [
1188
+ const mediaDefs = [
1189
1189
  {
1190
1190
  urn: 'media:custom-output',
1191
1191
  media_type: 'application/json',
@@ -1195,7 +1195,7 @@ function test109_extensionsWithMetadataAndValidation() {
1195
1195
  extensions: ['json']
1196
1196
  }
1197
1197
  ];
1198
- const resolved = resolveMediaUrn('media:custom-output', mediaSpecs);
1198
+ const resolved = resolveMediaUrn('media:custom-output', mediaDefs);
1199
1199
  assert(resolved.validation !== null, 'Should have validation');
1200
1200
  assert(resolved.metadata !== null, 'Should have metadata');
1201
1201
  assert(Array.isArray(resolved.extensions), 'Should have extensions');
@@ -1204,7 +1204,7 @@ function test109_extensionsWithMetadataAndValidation() {
1204
1204
 
1205
1205
  // TEST110: Test cap matching with subset semantics for request fulfillment
1206
1206
  function test110_multipleExtensions() {
1207
- const mediaSpecs = [
1207
+ const mediaDefs = [
1208
1208
  {
1209
1209
  urn: 'media:image;jpeg',
1210
1210
  media_type: 'image/jpeg',
@@ -1212,7 +1212,7 @@ function test110_multipleExtensions() {
1212
1212
  extensions: ['jpg', 'jpeg']
1213
1213
  }
1214
1214
  ];
1215
- const resolved = resolveMediaUrn('media:image;jpeg', mediaSpecs);
1215
+ const resolved = resolveMediaUrn('media:image;jpeg', mediaDefs);
1216
1216
  assertEqual(resolved.extensions.length, 2, 'Should have two extensions');
1217
1217
  assertEqual(resolved.extensions[0], 'jpg', 'First extension should be jpg');
1218
1218
  assertEqual(resolved.extensions[1], 'jpeg', 'Second extension should be jpeg');
@@ -1585,7 +1585,7 @@ function test306_availabilityAndPathOutputDistinct() {
1585
1585
  assert(!matchResult, 'availability must not conform to path');
1586
1586
  }
1587
1587
 
1588
- // TEST307: Test model_availability_urn builds valid cap URN with correct op and media specs
1588
+ // TEST307: Test model_availability_urn builds valid cap URN with correct op and media defs
1589
1589
  function test307_modelAvailabilityUrn() {
1590
1590
  const urn = modelAvailabilityUrn();
1591
1591
  assert(urn.hasMarkerTag('model-availability'), 'Must have model-availability marker');
@@ -1597,7 +1597,7 @@ function test307_modelAvailabilityUrn() {
1597
1597
  assert(outSpec.conformsTo(expectedOut), 'output must conform to MEDIA_AVAILABILITY_OUTPUT');
1598
1598
  }
1599
1599
 
1600
- // TEST308: Test model_path_urn builds valid cap URN with correct op and media specs
1600
+ // TEST308: Test model_path_urn builds valid cap URN with correct op and media defs
1601
1601
  function test308_modelPathUrn() {
1602
1602
  const urn = modelPathUrn();
1603
1603
  assert(urn.hasMarkerTag('model-path'), 'Must have model-path marker');
@@ -1661,12 +1661,12 @@ function test312_allUrnBuildersProduceValidUrns() {
1661
1661
  // but are important for capdag-js correctness.
1662
1662
 
1663
1663
  function testJS_buildExtensionIndex() {
1664
- const mediaSpecs = [
1664
+ const mediaDefs = [
1665
1665
  { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1666
1666
  { urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] },
1667
1667
  { urn: 'media:json;textable', media_type: 'application/json', extensions: ['json'] }
1668
1668
  ];
1669
- const index = buildExtensionIndex(mediaSpecs);
1669
+ const index = buildExtensionIndex(mediaDefs);
1670
1670
  assert(index instanceof Map, 'Should return a Map');
1671
1671
  assertEqual(index.size, 4, 'Should have 4 extensions');
1672
1672
  assert(index.has('pdf'), 'Should have pdf');
@@ -1677,51 +1677,51 @@ function testJS_buildExtensionIndex() {
1677
1677
  }
1678
1678
 
1679
1679
  function testJS_mediaUrnsForExtension() {
1680
- const mediaSpecs = [
1680
+ const mediaDefs = [
1681
1681
  { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1682
1682
  { urn: 'media:json;textable;record', media_type: 'application/json', extensions: ['json'] },
1683
1683
  { urn: 'media:json;textable;list', media_type: 'application/json', extensions: ['json'] }
1684
1684
  ];
1685
1685
 
1686
- const pdfUrns = mediaUrnsForExtension('pdf', mediaSpecs);
1686
+ const pdfUrns = mediaUrnsForExtension('pdf', mediaDefs);
1687
1687
  assertEqual(pdfUrns.length, 1, 'Should find 1 URN for pdf');
1688
1688
 
1689
1689
  // Case insensitivity
1690
- const pdfUrnsUpper = mediaUrnsForExtension('PDF', mediaSpecs);
1690
+ const pdfUrnsUpper = mediaUrnsForExtension('PDF', mediaDefs);
1691
1691
  assertEqual(pdfUrnsUpper.length, 1, 'Should find URN with uppercase extension');
1692
1692
 
1693
1693
  // Multiple URNs for same extension
1694
- const jsonUrns = mediaUrnsForExtension('json', mediaSpecs);
1694
+ const jsonUrns = mediaUrnsForExtension('json', mediaDefs);
1695
1695
  assertEqual(jsonUrns.length, 2, 'Should find 2 URNs for json');
1696
1696
 
1697
1697
  // Unknown extension throws
1698
1698
  let thrownError = null;
1699
1699
  try {
1700
- mediaUrnsForExtension('unknown', mediaSpecs);
1700
+ mediaUrnsForExtension('unknown', mediaDefs);
1701
1701
  } catch (e) {
1702
1702
  thrownError = e;
1703
1703
  }
1704
- assert(thrownError instanceof MediaSpecError, 'Should throw MediaSpecError for unknown ext');
1704
+ assert(thrownError instanceof MediaDefError, 'Should throw MediaDefError for unknown ext');
1705
1705
  }
1706
1706
 
1707
1707
  function testJS_getExtensionMappings() {
1708
- const mediaSpecs = [
1708
+ const mediaDefs = [
1709
1709
  { urn: 'media:pdf', media_type: 'application/pdf', extensions: ['pdf'] },
1710
1710
  { urn: 'media:image;jpeg', media_type: 'image/jpeg', extensions: ['jpg', 'jpeg'] }
1711
1711
  ];
1712
- const mappings = getExtensionMappings(mediaSpecs);
1712
+ const mappings = getExtensionMappings(mediaDefs);
1713
1713
  assert(Array.isArray(mappings), 'Should return an array');
1714
1714
  assertEqual(mappings.length, 3, 'Should have 3 mappings');
1715
1715
  }
1716
1716
 
1717
1717
  function testJS_resolveMediaUrnFromSpecs() {
1718
- const mediaSpecs = [
1718
+ const mediaDefs = [
1719
1719
  { urn: MEDIA_STRING, media_type: 'text/plain', title: 'String', profile_uri: 'https://capdag.com/schema/str' },
1720
1720
  { urn: 'media:custom', media_type: 'application/json', title: 'Custom Output', schema: { type: 'object' } }
1721
1721
  ];
1722
- const strSpec = resolveMediaUrn(MEDIA_STRING, mediaSpecs);
1722
+ const strSpec = resolveMediaUrn(MEDIA_STRING, mediaDefs);
1723
1723
  assertEqual(strSpec.contentType, 'text/plain', 'Should resolve string spec');
1724
- const outputSpec = resolveMediaUrn('media:custom', mediaSpecs);
1724
+ const outputSpec = resolveMediaUrn('media:custom', mediaDefs);
1725
1725
  assertEqual(outputSpec.contentType, 'application/json', 'Should resolve custom spec');
1726
1726
  assert(outputSpec.schema !== null, 'Should have schema');
1727
1727
  }
@@ -1737,7 +1737,7 @@ function testJS_capJSONSerialization() {
1737
1737
 
1738
1738
  const json = cap.toJSON();
1739
1739
  assertEqual(typeof json.urn, 'string', 'URN should be string');
1740
- assert(json.media_specs === undefined, 'Cap JSON must not contain media_specs (registry-resolved)');
1740
+ assert(json.media_defs === undefined, 'Cap JSON must not contain media_defs (registry-resolved)');
1741
1741
 
1742
1742
  const restored = Cap.fromJSON(json);
1743
1743
  assertEqual(restored.urn.getInSpec(), MEDIA_VOID, 'Should restore inSpec');
@@ -1790,13 +1790,13 @@ function testJS_capDocumentationOmittedWhenNull() {
1790
1790
  assertEqual(cap.getDocumentation(), null, 'Empty string must collapse to null');
1791
1791
  }
1792
1792
 
1793
- // Documentation propagates from a mediaSpecs definition through
1794
- // resolveMediaUrn into the resolved MediaSpec. Mirrors TEST924 on the Rust
1793
+ // Documentation propagates from a mediaDefs definition through
1794
+ // resolveMediaUrn into the resolved MediaDef. Mirrors TEST924 on the Rust
1795
1795
  // side. This is the path every UI consumer uses, so a break here makes the
1796
1796
  // new field invisible everywhere downstream.
1797
- function testJS_mediaSpecDocumentationPropagatesThroughResolve() {
1797
+ function testJS_mediaDefDocumentationPropagatesThroughResolve() {
1798
1798
  const body = '## Markdown body\n\nWith `code` and a [link](https://example.com).';
1799
- const mediaSpecs = [
1799
+ const mediaDefs = [
1800
1800
  {
1801
1801
  urn: 'media:doc-test;textable',
1802
1802
  media_type: 'text/plain',
@@ -1806,8 +1806,8 @@ function testJS_mediaSpecDocumentationPropagatesThroughResolve() {
1806
1806
  }
1807
1807
  ];
1808
1808
 
1809
- const resolved = resolveMediaUrn('media:doc-test;textable', mediaSpecs);
1810
- assertEqual(resolved.documentation, body, 'documentation must propagate into MediaSpec');
1809
+ const resolved = resolveMediaUrn('media:doc-test;textable', mediaDefs);
1810
+ assertEqual(resolved.documentation, body, 'documentation must propagate into MediaDef');
1811
1811
  // The short description must remain distinct from the long markdown
1812
1812
  // body — they are different fields with different semantics.
1813
1813
  assertEqual(resolved.description, 'short desc', 'description must remain distinct from documentation');
@@ -1837,14 +1837,14 @@ function testJS_stdinSourceNullData() {
1837
1837
  assertEqual(source.data, null, 'Data should be null');
1838
1838
  }
1839
1839
 
1840
- function testJS_mediaSpecConstruction() {
1841
- const spec1 = new MediaSpec('text/plain', 'https://capdag.com/schema/str', null, 'String', null, 'media:string');
1840
+ function testJS_mediaDefConstruction() {
1841
+ const spec1 = new MediaDef('text/plain', 'https://capdag.com/schema/str', null, 'String', null, 'media:string');
1842
1842
  assertEqual(spec1.contentType, 'text/plain', 'Should have content type');
1843
1843
  assertEqual(spec1.profile, 'https://capdag.com/schema/str', 'Should have profile');
1844
1844
  assertEqual(spec1.title, 'String', 'Should have title');
1845
1845
  assertEqual(spec1.mediaUrn, 'media:string', 'Should have mediaUrn');
1846
1846
 
1847
- const spec2 = new MediaSpec('application/octet-stream', null, null, 'Binary', null, 'media:binary');
1847
+ const spec2 = new MediaDef('application/octet-stream', null, null, 'Binary', null, 'media:binary');
1848
1848
  assertEqual(spec2.profile, null, 'Should have null profile');
1849
1849
  }
1850
1850
 
@@ -2562,9 +2562,9 @@ function test1303_withoutTag() {
2562
2562
  const removed2 = cap.withoutTag('EXT');
2563
2563
  assertEqual(removed2.getTag('ext'), undefined, 'withoutTag should be case-insensitive');
2564
2564
 
2565
- assertThrows(() => cap.withoutTag('in'), 'withoutTag must reject in');
2566
- assertThrows(() => cap.withoutTag('out'), 'withoutTag must reject out');
2567
- assertThrows(() => cap.withoutTag('effect'), 'withoutTag must reject effect');
2565
+ assertThrows(() => cap.withoutTag('in'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject in');
2566
+ assertThrows(() => cap.withoutTag('out'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject out');
2567
+ assertThrows(() => cap.withoutTag('effect'), ErrorCodes.INVALID_TAG_FORMAT, 'withoutTag must reject effect');
2568
2568
 
2569
2569
  // Removing non-existent tag is no-op
2570
2570
  const same3 = cap.withoutTag('nonexistent');
@@ -2587,11 +2587,12 @@ function test1304_withInOutSpec() {
2587
2587
  // Chain both
2588
2588
  const changedBoth = cap.withInSpec('media:pdf').withOutSpec(MEDIA_TXT);
2589
2589
  assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
2590
- assertEqual(changedBoth.getOutSpec(), MEDIA_TXT, 'Chain should set outSpec');
2590
+ assertEqual(changedBoth.getOutSpec(), 'media:textable;txt', 'Chain should set outSpec');
2591
2591
 
2592
2592
  const identity = CapUrn.fromString('cap:effect=none');
2593
2593
  assertThrows(
2594
2594
  () => identity.withOutSpec('media:pdf'),
2595
+ ErrorCodes.ILLEGAL_DECLARATION,
2595
2596
  'withOutSpec must revalidate admissibility'
2596
2597
  );
2597
2598
  }
@@ -2646,23 +2647,26 @@ function test1306_areCompatible() {
2646
2647
  // TEST1307: with_tag rejects structural keys
2647
2648
  function test1307_withTagRejectsStructuralKeys() {
2648
2649
  const cap = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
2649
- assertThrows(() => cap.withTag('in', 'media:'), 'withTag must reject in');
2650
- assertThrows(() => cap.withTag('out', 'media:'), 'withTag must reject out');
2651
- assertThrows(() => cap.withTag('effect', 'none'), 'withTag must reject effect');
2650
+ assertThrows(() => cap.withTag('in', 'media:'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject in');
2651
+ assertThrows(() => cap.withTag('out', 'media:'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject out');
2652
+ assertThrows(() => cap.withTag('effect', 'none'), ErrorCodes.INVALID_TAG_FORMAT, 'withTag must reject effect');
2652
2653
  }
2653
2654
 
2654
2655
  // TEST1308: builder rejects structural keys on tag/marker
2655
2656
  function test1308_builderRejectsStructuralKeys() {
2656
2657
  assertThrows(
2657
2658
  () => new CapUrnBuilder().tag('in', 'media:void'),
2659
+ ErrorCodes.INVALID_TAG_FORMAT,
2658
2660
  'builder.tag must reject structural in'
2659
2661
  );
2660
2662
  assertThrows(
2661
2663
  () => new CapUrnBuilder().marker('effect'),
2664
+ ErrorCodes.INVALID_TAG_FORMAT,
2662
2665
  'builder.marker must reject structural effect'
2663
2666
  );
2664
2667
  assertThrows(
2665
2668
  () => new CapUrnBuilder().inSpec('media:void').outSpec('media:record').tag('123', 'value').build(),
2669
+ ErrorCodes.NUMERIC_KEY,
2666
2670
  'builder.build must reject invalid non-structural tags'
2667
2671
  );
2668
2672
  }
@@ -3910,7 +3914,7 @@ function testMachine_capRegistryEntry_construction() {
3910
3914
  cap_description: 'Extracts text from PDF',
3911
3915
  args: [{ media_urn: 'media:pdf', required: true }],
3912
3916
  output: { media_urn: 'media:txt;textable', output_description: 'Extracted text' },
3913
- media_specs: [],
3917
+ media_defs: [],
3914
3918
  urn_tags: { op: 'extract' },
3915
3919
  in_spec: 'media:pdf',
3916
3920
  out_spec: 'media:txt;textable',
@@ -4181,19 +4185,19 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
4181
4185
  };
4182
4186
  }
4183
4187
 
4184
- function makeForEachStep(mediaSpec) {
4188
+ function makeForEachStep(mediaDef) {
4185
4189
  return {
4186
- step_type: { ForEach: { media_spec: mediaSpec } },
4187
- from_spec: mediaSpec,
4188
- to_spec: mediaSpec,
4190
+ step_type: { ForEach: { media_def: mediaDef } },
4191
+ from_spec: mediaDef,
4192
+ to_spec: mediaDef,
4189
4193
  };
4190
4194
  }
4191
4195
 
4192
- function makeCollectStep(mediaSpec) {
4196
+ function makeCollectStep(mediaDef) {
4193
4197
  return {
4194
- step_type: { Collect: { media_spec: mediaSpec } },
4195
- from_spec: mediaSpec,
4196
- to_spec: mediaSpec,
4198
+ step_type: { Collect: { media_def: mediaDef } },
4199
+ from_spec: mediaDef,
4200
+ to_spec: mediaDef,
4197
4201
  };
4198
4202
  }
4199
4203
 
@@ -4293,8 +4297,8 @@ function testRenderer_buildStrandGraphData_singleCapPlain() {
4293
4297
  // (two edges, three nodes). No cardinality marker in the cap label
4294
4298
  // because input_is_sequence == output_is_sequence == false.
4295
4299
  const payload = withMediaDisplayNames({
4296
- source_spec: 'media:a',
4297
- target_spec: 'media:b',
4300
+ source_media_urn: 'media:a',
4301
+ target_media_urn: 'media:b',
4298
4302
  steps: [
4299
4303
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4300
4304
  ],
@@ -4318,8 +4322,8 @@ function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
4318
4322
  // A cap with input_is_sequence=true MUST emit "(n→1)" on its edge
4319
4323
  // label.
4320
4324
  const payload = withMediaDisplayNames({
4321
- source_spec: 'media:a;list',
4322
- target_spec: 'media:b',
4325
+ source_media_urn: 'media:a;list',
4326
+ target_media_urn: 'media:b',
4323
4327
  steps: [
4324
4328
  makeCapStep('cap:in="media:a;list";x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
4325
4329
  ],
@@ -4348,8 +4352,8 @@ function testRenderer_buildStrandGraphData_foreachCollectSpan() {
4348
4352
  // labels on cap edges — they're distinct processing units in the
4349
4353
  // plan. This mirrors capdag's plan_builder.rs exactly.
4350
4354
  const payload = withMediaDisplayNames({
4351
- source_spec: 'media:pdf;list',
4352
- target_spec: 'media:txt;list',
4355
+ source_media_urn: 'media:pdf;list',
4356
+ target_media_urn: 'media:txt;list',
4353
4357
  steps: [
4354
4358
  makeForEachStep('media:pdf;list'),
4355
4359
  makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
@@ -4390,8 +4394,8 @@ function testRenderer_buildStrandGraphData_standaloneCollect() {
4390
4394
  // builder creates a Collect node consuming prev directly — plain
4391
4395
  // direct edge, no iteration/collection semantics.
4392
4396
  const payload = withMediaDisplayNames({
4393
- source_spec: 'media:a',
4394
- target_spec: 'media:b;list',
4397
+ source_media_urn: 'media:a',
4398
+ target_media_urn: 'media:b;list',
4395
4399
  steps: [
4396
4400
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4397
4401
  makeCollectStep('media:b'),
@@ -4418,8 +4422,8 @@ function testRenderer_buildStrandGraphData_unclosedForEachBody() {
4418
4422
  // connecting Cap_a to Cap_b via iteration, with prev becoming the
4419
4423
  // body exit (Cap_b).
4420
4424
  const payload = withMediaDisplayNames({
4421
- source_spec: 'media:a',
4422
- target_spec: 'media:c',
4425
+ source_media_urn: 'media:a',
4426
+ target_media_urn: 'media:c',
4423
4427
  steps: [
4424
4428
  makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4425
4429
  makeForEachStep('media:b'),
@@ -4455,8 +4459,8 @@ function testRenderer_buildStrandGraphData_nestedForEachThrows() {
4455
4459
  // must throw the same error to surface the issue rather than
4456
4460
  // render a malformed graph.
4457
4461
  const payload = withMediaDisplayNames({
4458
- source_spec: 'media:a;list',
4459
- target_spec: 'media:a',
4462
+ source_media_urn: 'media:a;list',
4463
+ target_media_urn: 'media:a',
4460
4464
  steps: [
4461
4465
  makeForEachStep('media:a;list'),
4462
4466
  makeForEachStep('media:a'),
@@ -4492,8 +4496,8 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4492
4496
  // with the entry edge labeled "extract" and an unlabeled
4493
4497
  // connector bridge to the output.
4494
4498
  const payload = withMediaDisplayNames({
4495
- source_spec: 'media:pdf;list',
4496
- target_spec: 'media:txt;list',
4499
+ source_media_urn: 'media:pdf;list',
4500
+ target_media_urn: 'media:txt;list',
4497
4501
  steps: [
4498
4502
  makeForEachStep('media:pdf;list'),
4499
4503
  makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
@@ -4532,7 +4536,7 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4532
4536
  function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
4533
4537
  // [Cap_a(1→1), ForEach, Cap_b(1→1)] with no Collect,
4534
4538
  // source=media:a, target=media:c. Cap_b's to_spec is media:c
4535
- // which is equivalent to target_spec, so the output node is
4539
+ // which is equivalent to target_media_urn, so the output node is
4536
4540
  // merged into step_2.
4537
4541
  //
4538
4542
  // Since both caps are 1→1, neither carries a cardinality
@@ -4541,8 +4545,8 @@ function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
4541
4545
  //
4542
4546
  // Final: 3 nodes (input_slot, step_0, step_2), 2 edges.
4543
4547
  const payload = withMediaDisplayNames({
4544
- source_spec: 'media:a',
4545
- target_spec: 'media:c',
4548
+ source_media_urn: 'media:a',
4549
+ target_media_urn: 'media:c',
4546
4550
  steps: [
4547
4551
  makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4548
4552
  makeForEachStep('media:b'),
@@ -4598,8 +4602,8 @@ function testRenderer_collapseStrand_standaloneCollectCollapses() {
4598
4602
  //
4599
4603
  // Final: 3 nodes (input_slot, step_0, output), 2 edges.
4600
4604
  const payload = withMediaDisplayNames({
4601
- source_spec: 'media:a',
4602
- target_spec: 'media:b;list',
4605
+ source_media_urn: 'media:a',
4606
+ target_media_urn: 'media:b;list',
4603
4607
  steps: [
4604
4608
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4605
4609
  makeCollectStep('media:b'),
@@ -4646,8 +4650,8 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4646
4650
  // No separate output node because step_2's to_spec equals the
4647
4651
  // strand target.
4648
4652
  const payload = withMediaDisplayNames({
4649
- source_spec: 'media:pdf',
4650
- target_spec: 'media:decision',
4653
+ source_media_urn: 'media:pdf',
4654
+ target_media_urn: 'media:decision',
4651
4655
  steps: [
4652
4656
  makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
4653
4657
  makeForEachStep('media:page'),
@@ -4694,15 +4698,15 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4694
4698
 
4695
4699
  function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
4696
4700
  // A strand with a single plain 1→1 cap whose to_spec equals
4697
- // target_spec. The plan-builder topology produces:
4701
+ // target_media_urn. The plan-builder topology produces:
4698
4702
  // input_slot → step_0 (cap) → output
4699
4703
  // The collapse pass merges the trailing output edge because
4700
4704
  // step_0 and output represent the same URN (media:b).
4701
4705
  //
4702
4706
  // Final: 2 nodes (input_slot, step_0), 1 edge.
4703
4707
  const payload = withMediaDisplayNames({
4704
- source_spec: 'media:a',
4705
- target_spec: 'media:b',
4708
+ source_media_urn: 'media:a',
4709
+ target_media_urn: 'media:b',
4706
4710
  steps: [
4707
4711
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4708
4712
  ],
@@ -4730,11 +4734,11 @@ function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
4730
4734
 
4731
4735
  function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
4732
4736
  // A strand with a single plain cap whose to_spec is NOT
4733
- // equivalent to target_spec. The output node must be retained
4737
+ // equivalent to target_media_urn. The output node must be retained
4734
4738
  // and the trailing connector edge preserved.
4735
4739
  const payload = withMediaDisplayNames({
4736
- source_spec: 'media:a',
4737
- target_spec: 'media:b;list',
4740
+ source_media_urn: 'media:a',
4741
+ target_media_urn: 'media:b;list',
4738
4742
  steps: [
4739
4743
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4740
4744
  ],
@@ -4754,15 +4758,15 @@ function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
4754
4758
  'step_0 retained');
4755
4759
  }
4756
4760
 
4757
- function testRenderer_validateStrandPayload_missingSourceSpec() {
4761
+ function testRenderer_validateStrandPayload_missingSourceMediaUrn() {
4758
4762
  let threw = false;
4759
4763
  try {
4760
- rendererValidateStrandPayload({ target_spec: 'media:b', steps: [] });
4764
+ rendererValidateStrandPayload({ target_media_urn: 'media:b', steps: [] });
4761
4765
  } catch (e) {
4762
4766
  threw = true;
4763
- assert(e.message.includes('source_spec'), 'error must name source_spec');
4767
+ assert(e.message.includes('source_media_urn'), 'error must name source_media_urn');
4764
4768
  }
4765
- assert(threw, 'missing source_spec must throw');
4769
+ assert(threw, 'missing source_media_urn must throw');
4766
4770
  }
4767
4771
 
4768
4772
  // ---------------- run builder ----------------
@@ -4789,8 +4793,8 @@ function testRenderer_buildRunGraphData_pagesSuccessesAndFailures() {
4789
4793
  // Show-more nodes: one for 3 hidden successes, one for 2 hidden
4790
4794
  // failures.
4791
4795
  const strand = {
4792
- source_spec: 'media:pdf;list',
4793
- target_spec: 'media:txt',
4796
+ source_media_urn: 'media:pdf;list',
4797
+ target_media_urn: 'media:txt',
4794
4798
  steps: [
4795
4799
  makeForEachStep('media:pdf;list'),
4796
4800
  makeCapStep('cap:in="media:pdf";a;out="media:image;png"', 'a', 'media:pdf', 'media:image;png', false, false),
@@ -4855,8 +4859,8 @@ function testRenderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace(
4855
4859
  // Strand [ForEach, Cap, Collect] → body has 1 cap. Each body
4856
4860
  // replica emits 1 entry node + 1 body cap node = 2 nodes.
4857
4861
  const strand = {
4858
- source_spec: 'media:pdf;list',
4859
- target_spec: 'media:txt',
4862
+ source_media_urn: 'media:pdf;list',
4863
+ target_media_urn: 'media:txt',
4860
4864
  steps: [
4861
4865
  makeForEachStep('media:pdf;list'),
4862
4866
  makeCapStep('cap:in="media:pdf";a;out="media:txt"', 'a', 'media:pdf', 'media:txt', false, false),
@@ -4891,8 +4895,8 @@ function testRenderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap() {
4891
4895
  // failed_cap and the step's cap_urn differ only in tag order — they
4892
4896
  // should still match, proving URNs are not treated as strings.
4893
4897
  const strand = {
4894
- source_spec: 'media:a',
4895
- target_spec: 'media:c',
4898
+ source_media_urn: 'media:a',
4899
+ target_media_urn: 'media:c',
4896
4900
  steps: [
4897
4901
  makeForEachStep('media:a;list'),
4898
4902
  // Canonical form places tags alphabetically: op after in/out.
@@ -4952,13 +4956,13 @@ function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
4952
4956
  // concepts don't leak into the view as boxed nodes.
4953
4957
  //
4954
4958
  // User scenario: [Disbind (1→n), ForEach, make_decision] where
4955
- // target_spec equals the last cap's to_spec, so the backbone
4959
+ // target_media_urn equals the last cap's to_spec, so the backbone
4956
4960
  // collapses to 3 nodes: input_slot, step_0 (Text Page),
4957
4961
  // step_2 (Decision, merged target). No separate `for each` or
4958
4962
  // `collect` boxes.
4959
4963
  const strand = {
4960
- source_spec: 'media:pdf',
4961
- target_spec: 'media:decision',
4964
+ source_media_urn: 'media:pdf',
4965
+ target_media_urn: 'media:decision',
4962
4966
  steps: [
4963
4967
  makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
4964
4968
  makeForEachStep('media:page'),
@@ -4989,7 +4993,7 @@ function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
4989
4993
  // that runs from the pre-foreach node to the body cap. It must
4990
4994
  // survive collapse so the target stays reachable even with zero
4991
4995
  // successful bodies.
4992
- const backboneCapEdges = built.strandBuilt.edges.filter(e => e.edgeClass === 'strand-cap-edge');
4996
+ const backboneCapEdges = built.strandBuilt.edges.filter(e => e.edgeClass.indexOf('strand-cap-edge') >= 0);
4993
4997
  assert(backboneCapEdges.some(e => e.source === 'step_0' && e.target === 'step_2'),
4994
4998
  'foreach-entry backbone edge step_0 → step_2 must be present for fallback connectivity');
4995
4999
 
@@ -5005,8 +5009,8 @@ function testRenderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
5005
5009
  // doesn't see a stale "Decision" placeholder alongside their
5006
5010
  // failed replicas.
5007
5011
  const strand = {
5008
- source_spec: 'media:pdf',
5009
- target_spec: 'media:decision',
5012
+ source_media_urn: 'media:pdf',
5013
+ target_media_urn: 'media:decision',
5010
5014
  steps: [
5011
5015
  makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5012
5016
  makeForEachStep('media:page'),
@@ -5075,8 +5079,8 @@ function testRenderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
5075
5079
  // → body_n_0 (per-body Decision)
5076
5080
  // (no merge edge back into the backbone)
5077
5081
  const strand = {
5078
- source_spec: 'media:pdf',
5079
- target_spec: 'media:decision',
5082
+ source_media_urn: 'media:pdf',
5083
+ target_media_urn: 'media:decision',
5080
5084
  steps: [
5081
5085
  makeCapStep('cap:in="media:pdf";disbind;out="media:page"', 'Disbind', 'media:pdf', 'media:page', false, true),
5082
5086
  makeForEachStep('media:page'),
@@ -5134,8 +5138,8 @@ function testRenderer_buildRunGraphData_closedForeachSuccessMergesAtCollectTarge
5134
5138
  // Actually simpler: [ForEach, Cap_a, Collect] with source=list
5135
5139
  // and target=list.
5136
5140
  const strand = {
5137
- source_spec: 'media:pdf;list',
5138
- target_spec: 'media:txt;list',
5141
+ source_media_urn: 'media:pdf;list',
5142
+ target_media_urn: 'media:txt;list',
5139
5143
  steps: [
5140
5144
  makeForEachStep('media:pdf;list'),
5141
5145
  makeCapStep('cap:in="media:pdf";extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
@@ -5859,8 +5863,8 @@ function test1842_truthTableFullCrossProduct() {
5859
5863
  for (let j = 0; j < forms.length; j++) {
5860
5864
  const instForm = forms[i];
5861
5865
  const pattForm = forms[j];
5862
- const instStr = instForm === '' ? 'cap:' : 'cap:' + instForm;
5863
- const pattStr = pattForm === '' ? 'cap:' : 'cap:' + pattForm;
5866
+ const instStr = instForm === '' ? 'cap:base' : 'cap:base;' + instForm;
5867
+ const pattStr = pattForm === '' ? 'cap:base' : 'cap:base;' + pattForm;
5864
5868
  const inst = CapUrn.fromString(instStr);
5865
5869
  const patt = CapUrn.fromString(pattStr);
5866
5870
  const actual = patt.accepts(inst);
@@ -5981,7 +5985,7 @@ async function runTests() {
5981
5985
  console.log(' SKIP TEST053: N/A for JS (Rust-only validation infrastructure)');
5982
5986
  runTest('TEST054: xv5_inline_spec_redefinition_detected', test054_xv5InlineSpecRedefinitionDetected);
5983
5987
  runTest('TEST055: xv5_new_inline_spec_allowed', test055_xv5NewInlineSpecAllowed);
5984
- runTest('TEST056: xv5_empty_media_specs_allowed', test056_xv5EmptyMediaSpecsAllowed);
5988
+ runTest('TEST056: xv5_empty_media_defs_allowed', test056_xv5EmptyMediaDefsAllowed);
5985
5989
 
5986
5990
  // media_urn.rs: TEST060-TEST078
5987
5991
  console.log('\n--- media_urn.rs ---');
@@ -6004,10 +6008,10 @@ async function runTests() {
6004
6008
  runTest('TEST077: serde_roundtrip (JSON.stringify)', test077_serdeRoundtrip);
6005
6009
  runTest('TEST078: debug_matching_behavior', test078_debugMatchingBehavior);
6006
6010
 
6007
- // media_spec.rs: TEST088-TEST110
6008
- console.log('\n--- media_spec.rs ---');
6011
+ // media_def.rs: TEST088-TEST110
6012
+ console.log('\n--- media_def.rs ---');
6009
6013
  console.log(' SKIP TEST088-090: N/A for JS (async registry, Rust-only)');
6010
- runTest('TEST091: resolve_custom_media_spec', test091_resolveCustomMediaSpec);
6014
+ runTest('TEST091: resolve_custom_media_def', test091_resolveCustomMediaDef);
6011
6015
  runTest('TEST092: resolve_custom_with_schema', test092_resolveCustomWithSchema);
6012
6016
  runTest('TEST093: resolve_unresolvable_fails_hard', test093_resolveUnresolvableFailsHard);
6013
6017
  console.log(' SKIP TEST094: N/A for JS (no registry concept)');
@@ -6076,10 +6080,10 @@ async function runTests() {
6076
6080
  runTest('JS: cap_json_serialization', testJS_capJSONSerialization);
6077
6081
  runTest('JS: cap_documentation_round_trip', testJS_capDocumentationRoundTrip);
6078
6082
  runTest('JS: cap_documentation_omitted_when_null', testJS_capDocumentationOmittedWhenNull);
6079
- runTest('JS: media_spec_documentation_propagates_through_resolve', testJS_mediaSpecDocumentationPropagatesThroughResolve);
6083
+ runTest('JS: media_def_documentation_propagates_through_resolve', testJS_mediaDefDocumentationPropagatesThroughResolve);
6080
6084
  runTest('JS: stdin_source_kind_constants', testJS_stdinSourceKindConstants);
6081
6085
  runTest('JS: stdin_source_null_data', testJS_stdinSourceNullData);
6082
- runTest('JS: media_spec_construction', testJS_mediaSpecConstruction);
6086
+ runTest('JS: media_def_construction', testJS_mediaDefConstruction);
6083
6087
 
6084
6088
  // cartridge_repo: CartridgeRepoServer and CartridgeRepoClient tests
6085
6089
  console.log('\n--- cartridge_repo ---');
@@ -6288,7 +6292,7 @@ async function runTests() {
6288
6292
  runTest('RENDERER: collapseStrand_seqCapBeforeForeach', testRenderer_collapseStrand_sequenceProducingCapBeforeForeach);
6289
6293
  runTest('RENDERER: collapseStrand_plainCapMergesOutput', testRenderer_collapseStrand_plainCapMergesTrailingOutput);
6290
6294
  runTest('RENDERER: collapseStrand_plainCapDistinctTarget', testRenderer_collapseStrand_plainCapDistinctTargetNoMerge);
6291
- runTest('RENDERER: validateStrand_missingSourceSpec', testRenderer_validateStrandPayload_missingSourceSpec);
6295
+ runTest('RENDERER: validateStrand_missingSourceMediaUrn', testRenderer_validateStrandPayload_missingSourceMediaUrn);
6292
6296
 
6293
6297
  console.log('\n--- cap-fab-renderer run builder ---');
6294
6298
  runTest('RENDERER: validateBodyOutcome_negativeIndex', testRenderer_validateBodyOutcome_rejectsNegativeIndex);