capns 0.50.11396 → 0.58.11575

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/RULES.md +33 -0
  2. package/capns.js +201 -31
  3. package/capns.test.js +154 -17
  4. package/package.json +2 -2
package/RULES.md CHANGED
@@ -70,6 +70,39 @@ Examples:
70
70
  | 11 | MISSING_OUT_SPEC | Cap URN missing required `out` tag |
71
71
  | 12 | INVALID_MEDIA_URN | Direction spec value is not a valid Media URN |
72
72
 
73
+ ## Validation Rules
74
+
75
+ ### XV5: No Redefinition of Registry Media Specs
76
+
77
+ Inline media specs in a capability's `media_specs` table must not redefine media specs that already exist in the global registry or built-in specs.
78
+
79
+ ```javascript
80
+ const { validateNoMediaSpecRedefinitionSync, MEDIA_STRING } = require('capns');
81
+
82
+ // This will fail - MEDIA_STRING is a built-in spec
83
+ const mediaSpecs = {
84
+ [MEDIA_STRING]: { media_type: 'text/plain', title: 'My String' }
85
+ };
86
+ const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
87
+ // result: { valid: false, error: 'XV5: ...', redefines: ['media:textable;form=scalar'] }
88
+
89
+ // This is allowed - custom spec that doesn't exist
90
+ const customSpecs = {
91
+ 'media:my-custom-type': { media_type: 'application/json', title: 'My Type' }
92
+ };
93
+ const customResult = validateNoMediaSpecRedefinitionSync(customSpecs);
94
+ // result: { valid: true }
95
+ ```
96
+
97
+ For server-side validation with registry access, use the async version:
98
+ ```javascript
99
+ const { validateNoMediaSpecRedefinition } = require('capns');
100
+
101
+ const result = await validateNoMediaSpecRedefinition(mediaSpecs, {
102
+ registryLookup: async (urn) => await mediaStore.get(urn) !== null
103
+ });
104
+ ```
105
+
73
106
  ## Cross-Language Compatibility
74
107
 
75
108
  This JavaScript implementation follows the same rules as:
package/capns.js CHANGED
@@ -755,26 +755,28 @@ const MediaSpecErrorCodes = {
755
755
  * Well-known built-in media URN constants
756
756
  * These media URNs are implicitly available and do not need to be declared in mediaSpecs
757
757
  */
758
- const MEDIA_STRING = 'media:string;textable;scalar';
759
- const MEDIA_INTEGER = 'media:integer;textable;numeric;scalar';
760
- const MEDIA_NUMBER = 'media:number;textable;numeric;scalar';
761
- const MEDIA_BOOLEAN = 'media:boolean;textable;scalar';
762
- const MEDIA_OBJECT = 'media:object;textable;keyed';
763
- const MEDIA_STRING_ARRAY = 'media:string-array;textable;sequence';
764
- const MEDIA_INTEGER_ARRAY = 'media:integer-array;textable;numeric;sequence';
765
- const MEDIA_NUMBER_ARRAY = 'media:number-array;textable;numeric;sequence';
766
- const MEDIA_BOOLEAN_ARRAY = 'media:boolean-array;textable;sequence';
767
- const MEDIA_OBJECT_ARRAY = 'media:object-array;textable;keyed;sequence';
768
- const MEDIA_BINARY = 'media:raw;binary';
758
+ const MEDIA_STRING = 'media:textable;form=scalar';
759
+ const MEDIA_INTEGER = 'media:integer;textable;numeric;form=scalar';
760
+ const MEDIA_NUMBER = 'media:textable;numeric;form=scalar';
761
+ const MEDIA_BOOLEAN = 'media:bool;textable;form=scalar';
762
+ const MEDIA_OBJECT = 'media:form=map;textable';
763
+ const MEDIA_STRING_ARRAY = 'media:textable;form=list';
764
+ const MEDIA_INTEGER_ARRAY = 'media:integer;textable;numeric;form=list';
765
+ const MEDIA_NUMBER_ARRAY = 'media:textable;numeric;form=list';
766
+ const MEDIA_BOOLEAN_ARRAY = 'media:bool;textable;form=list';
767
+ const MEDIA_OBJECT_ARRAY = 'media:form=list;textable';
768
+ const MEDIA_BINARY = 'media:bytes';
769
769
  const MEDIA_VOID = 'media:void';
770
770
  // Semantic content types
771
- const MEDIA_PNG = 'media:png;binary';
772
- const MEDIA_AUDIO = 'media:wav;audio;binary;';
773
- const MEDIA_VIDEO = 'media:video;binary';
774
- const MEDIA_TEXT = 'media:text;textable';
771
+ const MEDIA_PNG = 'media:image;png;bytes';
772
+ const MEDIA_AUDIO = 'media:wav;audio;bytes;';
773
+ const MEDIA_VIDEO = 'media:video;bytes';
774
+ // Semantic AI input types
775
+ const MEDIA_AUDIO_SPEECH = 'media:audio;wav;bytes;speech';
776
+ const MEDIA_IMAGE_THUMBNAIL = 'media:image;png;bytes;thumbnail';
775
777
  // Document types (PRIMARY naming - type IS the format)
776
- const MEDIA_PDF = 'media:pdf;binary';
777
- const MEDIA_EPUB = 'media:epub;binary';
778
+ const MEDIA_PDF = 'media:pdf;bytes';
779
+ const MEDIA_EPUB = 'media:epub;bytes';
778
780
  // Text format types (PRIMARY naming - type IS the format)
779
781
  const MEDIA_MD = 'media:md;textable';
780
782
  const MEDIA_TXT = 'media:txt;textable';
@@ -782,8 +784,16 @@ const MEDIA_RST = 'media:rst;textable';
782
784
  const MEDIA_LOG = 'media:log;textable';
783
785
  const MEDIA_HTML = 'media:html;textable';
784
786
  const MEDIA_XML = 'media:xml;textable';
785
- const MEDIA_JSON = 'media:json;textable;keyed';
786
- const MEDIA_YAML = 'media:yaml;textable;keyed';
787
+ const MEDIA_JSON = 'media:json;textable;form=map';
788
+ const MEDIA_JSON_SCHEMA = 'media:json;json-schema;textable;form=map';
789
+ const MEDIA_YAML = 'media:yaml;textable;form=map';
790
+ // Semantic input types
791
+ const MEDIA_MODEL_SPEC = 'media:model-spec;textable;form=scalar';
792
+ const MEDIA_MODEL_REPO = 'media:model-repo;textable;form=map';
793
+ // Semantic output types
794
+ const MEDIA_MODEL_DIM = 'media:model-dim;integer;textable;numeric;form=scalar';
795
+ const MEDIA_DECISION = 'media:decision;bool;textable;form=scalar';
796
+ const MEDIA_DECISION_ARRAY = 'media:decision;bool;textable;form=list';
787
797
 
788
798
  // =============================================================================
789
799
  // SCHEMA URL CONFIGURATION
@@ -851,7 +861,6 @@ const BUILTIN_SPECS = {
851
861
  [MEDIA_PNG]: 'image/png; profile=https://capns.org/schema/image',
852
862
  [MEDIA_AUDIO]: 'audio/wav; profile=https://capns.org/schema/audio',
853
863
  [MEDIA_VIDEO]: 'video/mp4; profile=https://capns.org/schema/video',
854
- [MEDIA_TEXT]: 'text/plain; profile=https://capns.org/schema/text',
855
864
  // Document types (PRIMARY naming)
856
865
  [MEDIA_PDF]: 'application/pdf',
857
866
  [MEDIA_EPUB]: 'application/epub+zip',
@@ -867,7 +876,7 @@ const BUILTIN_SPECS = {
867
876
  };
868
877
 
869
878
  /**
870
- * Check if a media URN has a marker tag (e.g., binary, keyed, textable).
879
+ * Check if a media URN has a marker tag (e.g., bytes, json, textable).
871
880
  * Uses TaggedUrn parsing for proper tag detection.
872
881
  * @param {string} mediaUrn - The media URN
873
882
  * @param {string} tagName - The marker tag name to check
@@ -879,6 +888,20 @@ function hasMediaUrnTag(mediaUrn, tagName) {
879
888
  return parsed.getTag(tagName) !== undefined;
880
889
  }
881
890
 
891
+ /**
892
+ * Check if a media URN has a tag with a specific value (e.g., form=map).
893
+ * Uses TaggedUrn parsing for proper tag detection.
894
+ * @param {string} mediaUrn - The media URN
895
+ * @param {string} tagName - The tag key to check
896
+ * @param {string} tagValue - The expected tag value
897
+ * @returns {boolean} True if the tag has the expected value
898
+ */
899
+ function hasMediaUrnTagValue(mediaUrn, tagName, tagValue) {
900
+ if (!mediaUrn) return false;
901
+ const parsed = TaggedUrn.fromString(mediaUrn);
902
+ return parsed.getTag(tagName) === tagValue;
903
+ }
904
+
882
905
  /**
883
906
  * Parsed MediaSpec structure
884
907
  *
@@ -996,23 +1019,63 @@ class MediaSpec {
996
1019
  }
997
1020
 
998
1021
  /**
999
- * Check if this media spec represents binary output.
1000
- * Returns true if the "binary" marker tag is present in the source media URN.
1022
+ * Check if this media spec represents binary data.
1023
+ * Returns true if the "bytes" marker tag is present in the source media URN.
1001
1024
  * @returns {boolean} True if binary
1002
1025
  */
1003
1026
  isBinary() {
1004
1027
  if (!this.mediaUrn) return false;
1005
- return hasMediaUrnTag(this.mediaUrn, 'binary');
1028
+ return hasMediaUrnTag(this.mediaUrn, 'bytes');
1029
+ }
1030
+
1031
+ /**
1032
+ * Check if this media spec represents a map/object structure (form=map).
1033
+ * This indicates a key-value structure, regardless of representation format.
1034
+ * @returns {boolean} True if map
1035
+ */
1036
+ isMap() {
1037
+ if (!this.mediaUrn) return false;
1038
+ return hasMediaUrnTagValue(this.mediaUrn, 'form', 'map');
1039
+ }
1040
+
1041
+ /**
1042
+ * Check if this media spec represents a scalar value (form=scalar).
1043
+ * @returns {boolean} True if scalar
1044
+ */
1045
+ isScalar() {
1046
+ if (!this.mediaUrn) return false;
1047
+ return hasMediaUrnTagValue(this.mediaUrn, 'form', 'scalar');
1006
1048
  }
1007
1049
 
1008
1050
  /**
1009
- * Check if this media spec represents JSON/keyed output.
1010
- * Returns true if the "keyed" marker tag is present in the source media URN.
1011
- * @returns {boolean} True if JSON/keyed
1051
+ * Check if this media spec represents a list/array structure (form=list).
1052
+ * @returns {boolean} True if list
1053
+ */
1054
+ isList() {
1055
+ if (!this.mediaUrn) return false;
1056
+ return hasMediaUrnTagValue(this.mediaUrn, 'form', 'list');
1057
+ }
1058
+
1059
+ /**
1060
+ * Check if this media spec represents structured data (map or list).
1061
+ * Structured data can be serialized as JSON when transmitted as text.
1062
+ * Note: This does NOT check for the explicit `json` tag - use isJSON() for that.
1063
+ * @returns {boolean} True if structured (map or list)
1064
+ */
1065
+ isStructured() {
1066
+ return this.isMap() || this.isList();
1067
+ }
1068
+
1069
+ /**
1070
+ * Check if this media spec represents JSON representation specifically.
1071
+ * Returns true if the "json" marker tag is present in the source media URN.
1072
+ * Note: This only checks for explicit JSON format marker.
1073
+ * For checking if data is structured (map/list), use isStructured().
1074
+ * @returns {boolean} True if JSON representation
1012
1075
  */
1013
1076
  isJSON() {
1014
1077
  if (!this.mediaUrn) return false;
1015
- return hasMediaUrnTag(this.mediaUrn, 'keyed');
1078
+ return hasMediaUrnTag(this.mediaUrn, 'json');
1016
1079
  }
1017
1080
 
1018
1081
  /**
@@ -1138,6 +1201,88 @@ function isBuiltinMediaUrn(mediaUrn) {
1138
1201
  return BUILTIN_SPECS.hasOwnProperty(mediaUrn);
1139
1202
  }
1140
1203
 
1204
+ /**
1205
+ * XV5: Validate that inline media_specs don't redefine built-in/registry specs.
1206
+ *
1207
+ * For capns-js (client-side), we check against BUILTIN_SPECS.
1208
+ * Server-side validation (capns_dot_org) should check against the full registry.
1209
+ *
1210
+ * @param {Object} mediaSpecs - The inline media_specs object from a capability
1211
+ * @param {Object} [options] - Validation options
1212
+ * @param {Function} [options.registryLookup] - Optional async function to check registry (for server-side)
1213
+ * @returns {Promise<{valid: boolean, error?: string, redefines?: string[]}>}
1214
+ */
1215
+ async function validateNoMediaSpecRedefinition(mediaSpecs, options = {}) {
1216
+ if (!mediaSpecs || typeof mediaSpecs !== 'object' || Object.keys(mediaSpecs).length === 0) {
1217
+ return { valid: true };
1218
+ }
1219
+
1220
+ const { registryLookup } = options;
1221
+ const redefines = [];
1222
+
1223
+ for (const mediaUrn of Object.keys(mediaSpecs)) {
1224
+ // Check against built-in specs first (always available)
1225
+ if (isBuiltinMediaUrn(mediaUrn)) {
1226
+ redefines.push(mediaUrn);
1227
+ continue;
1228
+ }
1229
+
1230
+ // If a registry lookup function is provided (server-side), check against it
1231
+ if (registryLookup && typeof registryLookup === 'function') {
1232
+ try {
1233
+ const existsInRegistry = await registryLookup(mediaUrn);
1234
+ if (existsInRegistry) {
1235
+ redefines.push(mediaUrn);
1236
+ }
1237
+ } catch (err) {
1238
+ // Network/registry unavailable - log warning and allow (graceful degradation)
1239
+ console.warn(`[WARN] XV5: Could not verify inline spec '${mediaUrn}' against registry: ${err.message}. Allowing operation in offline mode.`);
1240
+ }
1241
+ }
1242
+ }
1243
+
1244
+ if (redefines.length > 0) {
1245
+ return {
1246
+ valid: false,
1247
+ error: `XV5: Inline media specs redefine existing registry specs: ${redefines.join(', ')}`,
1248
+ redefines
1249
+ };
1250
+ }
1251
+
1252
+ return { valid: true };
1253
+ }
1254
+
1255
+ /**
1256
+ * XV5: Synchronous version for checking against built-in specs only.
1257
+ * Use this for client-side validation where registry lookup isn't available.
1258
+ *
1259
+ * @param {Object} mediaSpecs - The inline media_specs object from a capability
1260
+ * @returns {{valid: boolean, error?: string, redefines?: string[]}}
1261
+ */
1262
+ function validateNoMediaSpecRedefinitionSync(mediaSpecs) {
1263
+ if (!mediaSpecs || typeof mediaSpecs !== 'object' || Object.keys(mediaSpecs).length === 0) {
1264
+ return { valid: true };
1265
+ }
1266
+
1267
+ const redefines = [];
1268
+
1269
+ for (const mediaUrn of Object.keys(mediaSpecs)) {
1270
+ if (isBuiltinMediaUrn(mediaUrn)) {
1271
+ redefines.push(mediaUrn);
1272
+ }
1273
+ }
1274
+
1275
+ if (redefines.length > 0) {
1276
+ return {
1277
+ valid: false,
1278
+ error: `XV5: Inline media specs redefine existing built-in specs: ${redefines.join(', ')}`,
1279
+ redefines
1280
+ };
1281
+ }
1282
+
1283
+ return { valid: true };
1284
+ }
1285
+
1141
1286
  /**
1142
1287
  * Check if a CapUrn represents binary output.
1143
1288
  * Throws error if the output spec cannot be resolved - no fallbacks.
@@ -1153,10 +1298,11 @@ function isBinaryCapUrn(capUrn, mediaSpecs = {}) {
1153
1298
 
1154
1299
  /**
1155
1300
  * Check if a CapUrn represents JSON output.
1301
+ * Note: This checks for explicit JSON format marker only.
1156
1302
  * Throws error if the output spec cannot be resolved - no fallbacks.
1157
1303
  * @param {CapUrn} capUrn - The cap URN
1158
1304
  * @param {Object} mediaSpecs - Optional mediaSpecs lookup table
1159
- * @returns {boolean} True if JSON
1305
+ * @returns {boolean} True if explicit JSON tag present
1160
1306
  * @throws {MediaSpecError} If 'out' tag is missing or spec ID cannot be resolved
1161
1307
  */
1162
1308
  function isJSONCapUrn(capUrn, mediaSpecs = {}) {
@@ -1164,6 +1310,20 @@ function isJSONCapUrn(capUrn, mediaSpecs = {}) {
1164
1310
  return mediaSpec.isJSON();
1165
1311
  }
1166
1312
 
1313
+ /**
1314
+ * Check if a CapUrn represents structured output (map or list).
1315
+ * Structured data can be serialized as JSON when transmitted as text.
1316
+ * Throws error if the output spec cannot be resolved - no fallbacks.
1317
+ * @param {CapUrn} capUrn - The cap URN
1318
+ * @param {Object} mediaSpecs - Optional mediaSpecs lookup table
1319
+ * @returns {boolean} True if structured (map or list)
1320
+ * @throws {MediaSpecError} If 'out' tag is missing or spec ID cannot be resolved
1321
+ */
1322
+ function isStructuredCapUrn(capUrn, mediaSpecs = {}) {
1323
+ const mediaSpec = MediaSpec.fromCapUrn(capUrn, mediaSpecs);
1324
+ return mediaSpec.isStructured();
1325
+ }
1326
+
1167
1327
  /**
1168
1328
  * Registration attribution - who registered a capability and when
1169
1329
  */
@@ -1758,7 +1918,7 @@ class ValidationError extends Error {
1758
1918
  return `Cap '${capUrn}' argument '${details.argumentName}' expects media_spec '${details.expectedMediaSpec}' but ${errors} for value: ${JSON.stringify(details.actualValue)}`;
1759
1919
  }
1760
1920
  return `Cap '${capUrn}' argument '${details.argumentName}' expects type '${details.expectedType}' but received '${details.actualType}' with value: ${JSON.stringify(details.actualValue)}`;
1761
- case 'ArgumentValidationFailed':
1921
+ case 'MediaValidationFailed':
1762
1922
  return `Cap '${capUrn}' argument '${details.argumentName}' failed validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
1763
1923
  case 'MediaSpecValidationFailed':
1764
1924
  return `Cap '${capUrn}' argument '${details.argumentName}' failed media spec '${details.mediaUrn}' validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
@@ -3346,8 +3506,11 @@ module.exports = {
3346
3506
  MediaSpecErrorCodes,
3347
3507
  isBinaryCapUrn,
3348
3508
  isJSONCapUrn,
3509
+ isStructuredCapUrn,
3349
3510
  resolveMediaUrn,
3350
3511
  isBuiltinMediaUrn,
3512
+ validateNoMediaSpecRedefinition,
3513
+ validateNoMediaSpecRedefinitionSync,
3351
3514
  BUILTIN_SPECS,
3352
3515
  getSchemaBaseURL,
3353
3516
  getProfileURL,
@@ -3366,7 +3529,8 @@ module.exports = {
3366
3529
  MEDIA_PNG,
3367
3530
  MEDIA_AUDIO,
3368
3531
  MEDIA_VIDEO,
3369
- MEDIA_TEXT,
3532
+ MEDIA_AUDIO_SPEECH,
3533
+ MEDIA_IMAGE_THUMBNAIL,
3370
3534
  // Document types (PRIMARY naming)
3371
3535
  MEDIA_PDF,
3372
3536
  MEDIA_EPUB,
@@ -3378,7 +3542,13 @@ module.exports = {
3378
3542
  MEDIA_HTML,
3379
3543
  MEDIA_XML,
3380
3544
  MEDIA_JSON,
3545
+ MEDIA_JSON_SCHEMA,
3381
3546
  MEDIA_YAML,
3547
+ MEDIA_MODEL_SPEC,
3548
+ MEDIA_MODEL_REPO,
3549
+ MEDIA_MODEL_DIM,
3550
+ MEDIA_DECISION,
3551
+ MEDIA_DECISION_ARRAY,
3382
3552
  CapMatrixError,
3383
3553
  CapMatrix,
3384
3554
  BestCapSetMatch,
package/capns.test.js CHANGED
@@ -33,7 +33,9 @@ const {
33
33
  CapGraph,
34
34
  // StdinSource
35
35
  StdinSource,
36
- StdinSourceKind
36
+ StdinSourceKind,
37
+ // XV5 validation
38
+ validateNoMediaSpecRedefinitionSync
37
39
  } = require('./capns.js');
38
40
 
39
41
  // Test assertion utility
@@ -68,13 +70,14 @@ function assertThrows(fn, expectedErrorCode, message) {
68
70
  */
69
71
  function testUrn(tags) {
70
72
  if (!tags || tags === '') {
71
- return 'cap:in="media:void";out="media:object"';
73
+ return 'cap:in="media:void";out="media:form=map"';
72
74
  }
73
- return 'cap:in="media:void";out="media:object";' + tags;
75
+ return 'cap:in="media:void";out="media:form=map";' + tags;
74
76
  }
75
77
 
76
78
  // Test suite - defined at the end of file
77
79
 
80
+ // TEST001: Test that cap URN is created with tags parsed correctly and direction specs accessible
78
81
  function testCapUrnCreation() {
79
82
  console.log('Testing Cap URN creation...');
80
83
 
@@ -83,16 +86,17 @@ function testCapUrnCreation() {
83
86
  assertEqual(cap.getTag('target'), 'thumbnail', 'Should get target tag');
84
87
  assertEqual(cap.getTag('ext'), 'pdf', 'Should get ext tag');
85
88
  assertEqual(cap.getInSpec(), 'media:void', 'Should get inSpec');
86
- assertEqual(cap.getOutSpec(), 'media:object', 'Should get outSpec');
89
+ assertEqual(cap.getOutSpec(), 'media:form=map', 'Should get outSpec');
87
90
 
88
91
  console.log(' ✓ Cap URN creation');
89
92
  }
90
93
 
94
+ // TEST002: Test that tag keys and values are normalized to lowercase for case-insensitive comparison
91
95
  function testCaseInsensitive() {
92
96
  console.log('Testing case insensitive behavior...');
93
97
 
94
98
  // Test that different casing produces the same URN
95
- const cap1 = CapUrn.fromString('cap:IN="media:void";OUT="media:object";OP=Generate;EXT=PDF;Target=Thumbnail');
99
+ const cap1 = CapUrn.fromString('cap:IN="media:void";OUT="media:form=map";OP=Generate;EXT=PDF;Target=Thumbnail');
96
100
  const cap2 = CapUrn.fromString(testUrn('op=generate;ext=pdf;target=thumbnail'));
97
101
 
98
102
  // Both should be normalized to lowercase
@@ -114,7 +118,7 @@ function testCaseInsensitive() {
114
118
 
115
119
  // Case-insensitive in/out lookup
116
120
  assertEqual(cap1.getTag('IN'), 'media:void', 'Should lookup in with case-insensitive key');
117
- assertEqual(cap1.getTag('OUT'), 'media:object', 'Should lookup out with case-insensitive key');
121
+ assertEqual(cap1.getTag('OUT'), 'media:form=map', 'Should lookup out with case-insensitive key');
118
122
 
119
123
  // Matching should work case-insensitively
120
124
  assert(cap1.matches(cap2), 'Should match case-insensitively');
@@ -123,6 +127,7 @@ function testCaseInsensitive() {
123
127
  console.log(' ✓ Case insensitive behavior');
124
128
  }
125
129
 
130
+ // TEST003: Test that URN without cap prefix is rejected with MISSING_CAP_PREFIX error
126
131
  function testCapPrefixRequired() {
127
132
  console.log('Testing cap: prefix requirement...');
128
133
 
@@ -140,6 +145,7 @@ function testCapPrefixRequired() {
140
145
  console.log(' ✓ Cap prefix requirement');
141
146
  }
142
147
 
148
+ // TEST004: Test that URNs with and without trailing semicolon are equivalent
143
149
  function testTrailingSemicolonEquivalence() {
144
150
  console.log('Testing trailing semicolon equivalence...');
145
151
 
@@ -160,18 +166,20 @@ function testTrailingSemicolonEquivalence() {
160
166
  console.log(' ✓ Trailing semicolon equivalence');
161
167
  }
162
168
 
169
+ // TEST005: Test that toString produces canonical form with alphabetically sorted tags
163
170
  function testCanonicalStringFormat() {
164
171
  console.log('Testing canonical string format...');
165
172
 
166
173
  const cap = CapUrn.fromString(testUrn('op=generate;target=thumbnail;ext=pdf'));
167
174
  // Should be sorted alphabetically and have no trailing semicolon in canonical form
168
175
  // in/out are included in alphabetical order: 'ext' < 'in' < 'op' < 'out' < 'target'
169
- // Colons don't need quoting - media:void and media:object are valid unquoted values
170
- assertEqual(cap.toString(), 'cap:ext=pdf;in=media:void;op=generate;out=media:object;target=thumbnail', 'Should be alphabetically sorted');
176
+ // Values with '=' need quoting - media:form=map requires quotes
177
+ assertEqual(cap.toString(), 'cap:ext=pdf;in=media:void;op=generate;out="media:form=map";target=thumbnail', 'Should be alphabetically sorted');
171
178
 
172
179
  console.log(' ✓ Canonical string format');
173
180
  }
174
181
 
182
+ // TEST006: Test that cap matches request with exact tags, subset tags, and wildcards
175
183
  function testTagMatching() {
176
184
  console.log('Testing tag matching...');
177
185
 
@@ -200,6 +208,7 @@ function testTagMatching() {
200
208
  console.log(' ✓ Tag matching');
201
209
  }
202
210
 
211
+ // TEST007: Test that missing tags are treated as wildcards for matching
203
212
  function testMissingTagHandling() {
204
213
  console.log('Testing missing tag handling...');
205
214
 
@@ -217,6 +226,7 @@ function testMissingTagHandling() {
217
226
  console.log(' ✓ Missing tag handling');
218
227
  }
219
228
 
229
+ // TEST008: Test that specificity counts non-wildcard tags including in and out
220
230
  function testSpecificity() {
221
231
  console.log('Testing specificity...');
222
232
 
@@ -239,6 +249,7 @@ function testSpecificity() {
239
249
  console.log(' ✓ Specificity');
240
250
  }
241
251
 
252
+ // TEST009: Test that compatibility checks if caps can handle same requests
242
253
  function testCompatibility() {
243
254
  console.log('Testing compatibility...');
244
255
 
@@ -262,6 +273,7 @@ function testCompatibility() {
262
273
  console.log(' ✓ Compatibility');
263
274
  }
264
275
 
276
+ // TEST010: Test that CapUrnBuilder creates valid URN with tags and direction specs
265
277
  function testBuilder() {
266
278
  console.log('Testing builder...');
267
279
 
@@ -295,6 +307,7 @@ function testBuilder() {
295
307
  console.log(' ✓ Builder');
296
308
  }
297
309
 
310
+ // TEST011: Test convenience methods withTag, withoutTag, withInSpec, withOutSpec, merge, subset, withWildcardTag
298
311
  function testConvenienceMethods() {
299
312
  console.log('Testing convenience methods...');
300
313
 
@@ -305,7 +318,7 @@ function testConvenienceMethods() {
305
318
  assertEqual(modified.getTag('op'), 'generate', 'Should preserve original tag');
306
319
  assertEqual(modified.getTag('ext'), 'pdf', 'Should add new tag');
307
320
  assertEqual(modified.getInSpec(), 'media:void', 'Should preserve inSpec');
308
- assertEqual(modified.getOutSpec(), 'media:object', 'Should preserve outSpec');
321
+ assertEqual(modified.getOutSpec(), 'media:form=map', 'Should preserve outSpec');
309
322
 
310
323
  // Test withTag silently ignores in/out
311
324
  const modified2 = original.withTag('in', 'media:string');
@@ -357,6 +370,7 @@ function testConvenienceMethods() {
357
370
  console.log(' ✓ Convenience methods');
358
371
  }
359
372
 
373
+ // TEST012: Test that CapMatcher finds best and all matches from cap list
360
374
  function testCapMatcher() {
361
375
  console.log('Testing CapMatcher...');
362
376
 
@@ -371,7 +385,8 @@ function testCapMatcher() {
371
385
 
372
386
  // Most specific cap that can handle the request (ext=pdf is more specific)
373
387
  // Canonical order is alphabetical: ext, in, op, out
374
- assertEqual(best.toString(), 'cap:ext=pdf;in=media:void;op=generate;out=media:object', 'Should find most specific match');
388
+ // Note: media:form=map needs quotes because it contains '='
389
+ assertEqual(best.toString(), 'cap:ext=pdf;in=media:void;op=generate;out="media:form=map"', 'Should find most specific match');
375
390
 
376
391
  // Test findAllMatches - now only 2 match because first has wildcard in/out
377
392
  const matches = CapMatcher.findAllMatches(caps, request);
@@ -382,6 +397,7 @@ function testCapMatcher() {
382
397
  console.log(' ✓ CapMatcher');
383
398
  }
384
399
 
400
+ // TEST013: Test that cap URN serializes to and deserializes from JSON
385
401
  function testJSONSerialization() {
386
402
  console.log('Testing JSON serialization...');
387
403
 
@@ -392,11 +408,12 @@ function testJSONSerialization() {
392
408
 
393
409
  assert(original.equals(restored), 'Should serialize/deserialize correctly');
394
410
  assertEqual(restored.getInSpec(), 'media:void', 'Should preserve inSpec');
395
- assertEqual(restored.getOutSpec(), 'media:object', 'Should preserve outSpec');
411
+ assertEqual(restored.getOutSpec(), 'media:form=map', 'Should preserve outSpec');
396
412
 
397
413
  console.log(' ✓ JSON serialization');
398
414
  }
399
415
 
416
+ // TEST014: Test that empty cap URN without in/out fails, minimal URN with just in/out succeeds
400
417
  function testEmptyCapUrn() {
401
418
  console.log('Testing empty cap URN now fails (in/out required)...');
402
419
 
@@ -418,7 +435,7 @@ function testEmptyCapUrn() {
418
435
  const minimal = CapUrn.fromString(testUrn(''));
419
436
  assertEqual(Object.keys(minimal.tags).length, 0, 'Should have no other tags');
420
437
  assertEqual(minimal.getInSpec(), 'media:void', 'Should have inSpec');
421
- assertEqual(minimal.getOutSpec(), 'media:object', 'Should have outSpec');
438
+ assertEqual(minimal.getOutSpec(), 'media:form=map', 'Should have outSpec');
422
439
 
423
440
  // For "match anything" behavior, use wildcards
424
441
  const wildcard = CapUrn.fromString('cap:in=*;out=*');
@@ -429,6 +446,7 @@ function testEmptyCapUrn() {
429
446
  console.log(' ✓ Empty cap URN now fails (in/out required)');
430
447
  }
431
448
 
449
+ // TEST015: Test that URN supports forward slashes and colons in tag values
432
450
  function testExtendedCharacterSupport() {
433
451
  console.log('Testing extended character support...');
434
452
 
@@ -440,6 +458,7 @@ function testExtendedCharacterSupport() {
440
458
  console.log(' ✓ Extended character support');
441
459
  }
442
460
 
461
+ // TEST016: Test that wildcard is rejected in keys but accepted in values
443
462
  function testWildcardRestrictions() {
444
463
  console.log('Testing wildcard restrictions...');
445
464
 
@@ -462,6 +481,7 @@ function testWildcardRestrictions() {
462
481
  console.log(' ✓ Wildcard restrictions');
463
482
  }
464
483
 
484
+ // TEST017: Test that duplicate keys in URN are rejected with DUPLICATE_KEY error
465
485
  function testDuplicateKeyRejection() {
466
486
  console.log('Testing duplicate key rejection...');
467
487
 
@@ -475,6 +495,7 @@ function testDuplicateKeyRejection() {
475
495
  console.log(' ✓ Duplicate key rejection');
476
496
  }
477
497
 
498
+ // TEST018: Test that pure numeric keys are rejected but mixed alphanumeric keys are allowed
478
499
  function testNumericKeyRestriction() {
479
500
  console.log('Testing numeric key restriction...');
480
501
 
@@ -503,6 +524,7 @@ function testNumericKeyRestriction() {
503
524
  // NEW FORMAT TESTS - Spec ID Resolution and MediaSpec
504
525
  // ============================================================================
505
526
 
527
+ // TEST057: Test MediaSpec parses canonical format without content-type prefix
506
528
  function testMediaSpecCanonicalFormat() {
507
529
  console.log('Testing MediaSpec canonical format...');
508
530
 
@@ -523,6 +545,7 @@ function testMediaSpecCanonicalFormat() {
523
545
  console.log(' ✓ MediaSpec canonical format');
524
546
  }
525
547
 
548
+ // TEST058: Test MediaSpec fails hard on legacy content-type prefix format
526
549
  function testMediaSpecLegacyFormatRejection() {
527
550
  console.log('Testing legacy format rejection...');
528
551
 
@@ -542,6 +565,7 @@ function testMediaSpecLegacyFormatRejection() {
542
565
  console.log(' ✓ Legacy format rejection');
543
566
  }
544
567
 
568
+ // TEST059: Test built-in media URNs are recognized by isBuiltinMediaUrn
545
569
  function testBuiltinSpecIds() {
546
570
  console.log('Testing built-in spec IDs...');
547
571
 
@@ -559,6 +583,7 @@ function testBuiltinSpecIds() {
559
583
  console.log(' ✓ Built-in spec IDs');
560
584
  }
561
585
 
586
+ // TEST060: Test resolveMediaUrn resolves built-in media URNs to MediaSpec
562
587
  function testSpecIdResolution() {
563
588
  console.log('Testing spec ID resolution...');
564
589
 
@@ -581,6 +606,7 @@ function testSpecIdResolution() {
581
606
  console.log(' ✓ Spec ID resolution');
582
607
  }
583
608
 
609
+ // TEST061: Test resolveMediaUrn resolves custom media URNs from mediaSpecs table
584
610
  function testMediaUrnResolutionWithMediaSpecs() {
585
611
  console.log('Testing media URN resolution with custom mediaSpecs...');
586
612
 
@@ -612,6 +638,7 @@ function testMediaUrnResolutionWithMediaSpecs() {
612
638
  console.log(' ✓ Media URN resolution with custom mediaSpecs');
613
639
  }
614
640
 
641
+ // TEST062: Test resolveMediaUrn fails hard on unresolvable media URN
615
642
  function testMediaUrnResolutionFailHard() {
616
643
  console.log('Testing media URN resolution fail hard...');
617
644
 
@@ -631,6 +658,7 @@ function testMediaUrnResolutionFailHard() {
631
658
  console.log(' ✓ Media URN resolution fail hard');
632
659
  }
633
660
 
661
+ // TEST063: Test metadata is propagated from object form media spec definition
634
662
  function testMetadataPropagation() {
635
663
  console.log('Testing metadata propagation...');
636
664
 
@@ -661,6 +689,7 @@ function testMetadataPropagation() {
661
689
  console.log(' ✓ Metadata propagation from object definition');
662
690
  }
663
691
 
692
+ // TEST064: Test string form media spec definition has no metadata
664
693
  function testMetadataForStringDef() {
665
694
  console.log('Testing metadata for string definition...');
666
695
 
@@ -675,6 +704,7 @@ function testMetadataForStringDef() {
675
704
  console.log(' ✓ String form has no metadata');
676
705
  }
677
706
 
707
+ // TEST065: Test built-in media URNs have no metadata
678
708
  function testMetadataForBuiltin() {
679
709
  console.log('Testing metadata for built-in...');
680
710
 
@@ -685,6 +715,7 @@ function testMetadataForBuiltin() {
685
715
  console.log(' ✓ Built-in has no metadata');
686
716
  }
687
717
 
718
+ // TEST066: Test metadata and validation coexist in media spec definition
688
719
  function testMetadataWithValidation() {
689
720
  console.log('Testing metadata with validation...');
690
721
 
@@ -720,6 +751,7 @@ function testMetadataWithValidation() {
720
751
  console.log(' ✓ Metadata coexists with validation');
721
752
  }
722
753
 
754
+ // TEST108: Test Cap with mediaSpecs resolves custom and built-in media URNs
723
755
  function testCapWithMediaSpecs() {
724
756
  console.log('Testing Cap with mediaSpecs...');
725
757
 
@@ -754,6 +786,7 @@ function testCapWithMediaSpecs() {
754
786
  console.log(' ✓ Cap with mediaSpecs');
755
787
  }
756
788
 
789
+ // TEST109: Test Cap JSON serialization includes mediaSpecs and direction specs
757
790
  function testCapJSONSerialization() {
758
791
  console.log('Testing Cap JSON serialization with mediaSpecs...');
759
792
 
@@ -774,18 +807,19 @@ function testCapJSONSerialization() {
774
807
  assertEqual(json.media_specs['media:custom'], 'text/plain; profile=https://example.com', 'Should serialize mediaSpecs');
775
808
  // URN tags should include in and out
776
809
  assertEqual(json.urn.tags['in'], 'media:void', 'Should serialize inSpec in tags');
777
- assertEqual(json.urn.tags['out'], 'media:object', 'Should serialize outSpec in tags');
810
+ assertEqual(json.urn.tags['out'], 'media:form=map', 'Should serialize outSpec in tags');
778
811
 
779
812
  // Deserialize from JSON
780
813
  const restored = Cap.fromJSON(json);
781
814
  assert(restored.mediaSpecs !== undefined, 'Should restore mediaSpecs');
782
815
  assertEqual(restored.mediaSpecs['media:custom'], 'text/plain; profile=https://example.com', 'Should restore mediaSpecs content');
783
816
  assertEqual(restored.urn.getInSpec(), 'media:void', 'Should restore inSpec');
784
- assertEqual(restored.urn.getOutSpec(), 'media:object', 'Should restore outSpec');
817
+ assertEqual(restored.urn.getOutSpec(), 'media:form=map', 'Should restore outSpec');
785
818
 
786
819
  console.log(' ✓ Cap JSON serialization with mediaSpecs');
787
820
  }
788
821
 
822
+ // TEST019: Test op tag is used instead of deprecated action tag
789
823
  function testOpTagRename() {
790
824
  console.log('Testing op tag (renamed from action)...');
791
825
 
@@ -812,6 +846,7 @@ function testOpTagRename() {
812
846
  // All implementations (Rust, Go, JS, ObjC) must pass these identically
813
847
  // ============================================================================
814
848
 
849
+ // TEST020: Test matching semantics - exact match including in/out
815
850
  function testMatchingSemantics_Test1_ExactMatch() {
816
851
  console.log('Testing Matching Semantics Test 1: Exact match...');
817
852
  // Test 1: Exact match (including in/out)
@@ -824,6 +859,7 @@ function testMatchingSemantics_Test1_ExactMatch() {
824
859
  console.log(' ✓ Test 1: Exact match');
825
860
  }
826
861
 
862
+ // TEST021: Test matching semantics - cap missing tag treated as implicit wildcard
827
863
  function testMatchingSemantics_Test2_CapMissingTag() {
828
864
  console.log('Testing Matching Semantics Test 2: Cap missing tag...');
829
865
  // Test 2: Cap missing tag (implicit wildcard for other tags, not in/out)
@@ -836,6 +872,7 @@ function testMatchingSemantics_Test2_CapMissingTag() {
836
872
  console.log(' ✓ Test 2: Cap missing tag');
837
873
  }
838
874
 
875
+ // TEST022: Test matching semantics - cap with extra tag matches request
839
876
  function testMatchingSemantics_Test3_CapHasExtraTag() {
840
877
  console.log('Testing Matching Semantics Test 3: Cap has extra tag...');
841
878
  // Test 3: Cap has extra tag
@@ -848,6 +885,7 @@ function testMatchingSemantics_Test3_CapHasExtraTag() {
848
885
  console.log(' ✓ Test 3: Cap has extra tag');
849
886
  }
850
887
 
888
+ // TEST023: Test matching semantics - request with wildcard matches specific cap
851
889
  function testMatchingSemantics_Test4_RequestHasWildcard() {
852
890
  console.log('Testing Matching Semantics Test 4: Request has wildcard...');
853
891
  // Test 4: Request has wildcard
@@ -860,6 +898,7 @@ function testMatchingSemantics_Test4_RequestHasWildcard() {
860
898
  console.log(' ✓ Test 4: Request has wildcard');
861
899
  }
862
900
 
901
+ // TEST024: Test matching semantics - cap with wildcard matches specific request
863
902
  function testMatchingSemantics_Test5_CapHasWildcard() {
864
903
  console.log('Testing Matching Semantics Test 5: Cap has wildcard...');
865
904
  // Test 5: Cap has wildcard
@@ -872,6 +911,7 @@ function testMatchingSemantics_Test5_CapHasWildcard() {
872
911
  console.log(' ✓ Test 5: Cap has wildcard');
873
912
  }
874
913
 
914
+ // TEST025: Test matching semantics - value mismatch does not match
875
915
  function testMatchingSemantics_Test6_ValueMismatch() {
876
916
  console.log('Testing Matching Semantics Test 6: Value mismatch...');
877
917
  // Test 6: Value mismatch
@@ -884,6 +924,7 @@ function testMatchingSemantics_Test6_ValueMismatch() {
884
924
  console.log(' ✓ Test 6: Value mismatch');
885
925
  }
886
926
 
927
+ // TEST026: Test matching semantics - fallback pattern with missing tag as implicit wildcard
887
928
  function testMatchingSemantics_Test7_FallbackPattern() {
888
929
  console.log('Testing Matching Semantics Test 7: Fallback pattern...');
889
930
  // Test 7: Fallback pattern
@@ -896,6 +937,7 @@ function testMatchingSemantics_Test7_FallbackPattern() {
896
937
  console.log(' ✓ Test 7: Fallback pattern');
897
938
  }
898
939
 
940
+ // TEST027: Test matching semantics - wildcard cap with in=* out=* matches anything
899
941
  function testMatchingSemantics_Test8_WildcardCapMatchesAnything() {
900
942
  console.log('Testing Matching Semantics Test 8: Wildcard cap matches anything...');
901
943
  // Test 8: Wildcard cap matches anything (replaces empty cap test)
@@ -908,6 +950,7 @@ function testMatchingSemantics_Test8_WildcardCapMatchesAnything() {
908
950
  console.log(' ✓ Test 8: Wildcard cap matches anything');
909
951
  }
910
952
 
953
+ // TEST028: Test matching semantics - cross-dimension independence for other tags
911
954
  function testMatchingSemantics_Test9_CrossDimensionIndependence() {
912
955
  console.log('Testing Matching Semantics Test 9: Cross-dimension independence...');
913
956
  // Test 9: Cross-dimension independence (for other tags, not in/out)
@@ -920,6 +963,7 @@ function testMatchingSemantics_Test9_CrossDimensionIndependence() {
920
963
  console.log(' ✓ Test 9: Cross-dimension independence');
921
964
  }
922
965
 
966
+ // TEST029: Test matching semantics - direction mismatch in/out does not match
923
967
  function testMatchingSemantics_Test10_DirectionMismatch() {
924
968
  console.log('Testing Matching Semantics Test 10: Direction mismatch...');
925
969
  // Test 10: Direction mismatch (in/out must match)
@@ -964,6 +1008,7 @@ function makeCap(urnString, title) {
964
1008
  return new Cap(capUrn, title, 'test', title);
965
1009
  }
966
1010
 
1011
+ // TEST117: Test CapCube finds more specific cap across registries
967
1012
  function testCapCubeMoreSpecificWins() {
968
1013
  console.log('Testing CapCube: More specific wins...');
969
1014
 
@@ -1002,6 +1047,7 @@ function testCapCubeMoreSpecificWins() {
1002
1047
  console.log(' ✓ More specific wins');
1003
1048
  }
1004
1049
 
1050
+ // TEST118: Test CapCube tie-breaking prefers first registry in order
1005
1051
  function testCapCubeTieGoesToFirst() {
1006
1052
  console.log('Testing CapCube: Tie goes to first...');
1007
1053
 
@@ -1029,6 +1075,7 @@ function testCapCubeTieGoesToFirst() {
1029
1075
  console.log(' ✓ Tie goes to first');
1030
1076
  }
1031
1077
 
1078
+ // TEST119: Test CapCube polls all registries to find best match
1032
1079
  function testCapCubePollsAll() {
1033
1080
  console.log('Testing CapCube: Polls all registries...');
1034
1081
 
@@ -1063,6 +1110,7 @@ function testCapCubePollsAll() {
1063
1110
  console.log(' ✓ Polls all registries');
1064
1111
  }
1065
1112
 
1113
+ // TEST120: Test CapCube returns error when no cap matches request
1066
1114
  function testCapCubeNoMatch() {
1067
1115
  console.log('Testing CapCube: No match error...');
1068
1116
 
@@ -1081,6 +1129,7 @@ function testCapCubeNoMatch() {
1081
1129
  console.log(' ✓ No match error');
1082
1130
  }
1083
1131
 
1132
+ // TEST121: Test CapCube fallback scenario where generic cap handles unknown file types
1084
1133
  function testCapCubeFallbackScenario() {
1085
1134
  console.log('Testing CapCube: Fallback scenario...');
1086
1135
 
@@ -1125,6 +1174,7 @@ function testCapCubeFallbackScenario() {
1125
1174
  console.log(' ✓ Fallback scenario');
1126
1175
  }
1127
1176
 
1177
+ // TEST122: Test CapCube can method returns execution info and canHandle checks capability
1128
1178
  function testCapCubeCanMethod() {
1129
1179
  console.log('Testing CapCube: can() method...');
1130
1180
 
@@ -1149,6 +1199,7 @@ function testCapCubeCanMethod() {
1149
1199
  console.log(' ✓ can() method');
1150
1200
  }
1151
1201
 
1202
+ // TEST123: Test CapCube registry management add, get, remove operations
1152
1203
  function testCapCubeRegistryManagement() {
1153
1204
  console.log('Testing CapCube: Registry management...');
1154
1205
 
@@ -1190,6 +1241,7 @@ function makeGraphCap(inUrn, outUrn, title) {
1190
1241
  return new Cap(capUrn, title, 'convert', title);
1191
1242
  }
1192
1243
 
1244
+ // TEST124: Test CapGraph basic construction builds nodes and edges from caps
1193
1245
  function testCapGraphBasicConstruction() {
1194
1246
  console.log('Testing CapGraph: Basic construction...');
1195
1247
 
@@ -1223,6 +1275,7 @@ function testCapGraphBasicConstruction() {
1223
1275
  console.log(' ✓ Basic construction');
1224
1276
  }
1225
1277
 
1278
+ // TEST125: Test CapGraph getOutgoing and getIncoming return correct edges for media URN
1226
1279
  function testCapGraphOutgoingIncoming() {
1227
1280
  console.log('Testing CapGraph: Outgoing and incoming edges...');
1228
1281
 
@@ -1255,6 +1308,7 @@ function testCapGraphOutgoingIncoming() {
1255
1308
  console.log(' ✓ Outgoing and incoming edges');
1256
1309
  }
1257
1310
 
1311
+ // TEST126: Test CapGraph canConvert checks direct and transitive conversion paths
1258
1312
  function testCapGraphCanConvert() {
1259
1313
  console.log('Testing CapGraph: Can convert...');
1260
1314
 
@@ -1289,6 +1343,7 @@ function testCapGraphCanConvert() {
1289
1343
  console.log(' ✓ Can convert');
1290
1344
  }
1291
1345
 
1346
+ // TEST127: Test CapGraph findPath returns shortest path between media URNs
1292
1347
  function testCapGraphFindPath() {
1293
1348
  console.log('Testing CapGraph: Find path...');
1294
1349
 
@@ -1330,6 +1385,7 @@ function testCapGraphFindPath() {
1330
1385
  console.log(' ✓ Find path');
1331
1386
  }
1332
1387
 
1388
+ // TEST128: Test CapGraph findAllPaths returns all paths sorted by length
1333
1389
  function testCapGraphFindAllPaths() {
1334
1390
  console.log('Testing CapGraph: Find all paths...');
1335
1391
 
@@ -1361,6 +1417,7 @@ function testCapGraphFindAllPaths() {
1361
1417
  console.log(' ✓ Find all paths');
1362
1418
  }
1363
1419
 
1420
+ // TEST129: Test CapGraph getDirectEdges returns edges sorted by specificity
1364
1421
  function testCapGraphGetDirectEdges() {
1365
1422
  console.log('Testing CapGraph: Get direct edges...');
1366
1423
 
@@ -1397,6 +1454,7 @@ function testCapGraphGetDirectEdges() {
1397
1454
  console.log(' ✓ Get direct edges');
1398
1455
  }
1399
1456
 
1457
+ // TEST130: Test CapGraph stats returns node count, edge count, input/output URN counts
1400
1458
  function testCapGraphStats() {
1401
1459
  console.log('Testing CapGraph: Stats...');
1402
1460
 
@@ -1432,6 +1490,7 @@ function testCapGraphStats() {
1432
1490
  console.log(' ✓ Stats');
1433
1491
  }
1434
1492
 
1493
+ // TEST131: Test CapGraph with CapCube builds graph from multiple registries
1435
1494
  function testCapGraphWithCapCube() {
1436
1495
  console.log('Testing CapGraph: With CapCube...');
1437
1496
 
@@ -1473,6 +1532,7 @@ function testCapGraphWithCapCube() {
1473
1532
  // StdinSource Tests
1474
1533
  // ============================================================================
1475
1534
 
1535
+ // TEST156: Test creating StdinSource Data variant with byte vector
1476
1536
  function testStdinSourceFromData() {
1477
1537
  console.log('Testing StdinSource: From data...');
1478
1538
 
@@ -1488,13 +1548,14 @@ function testStdinSourceFromData() {
1488
1548
  console.log(' ✓ From data');
1489
1549
  }
1490
1550
 
1551
+ // TEST157: Test creating StdinSource FileReference variant with all required fields
1491
1552
  function testStdinSourceFromFileReference() {
1492
1553
  console.log('Testing StdinSource: From file reference...');
1493
1554
 
1494
1555
  const trackedFileId = 'tracked-file-123';
1495
1556
  const originalPath = '/path/to/original.pdf';
1496
1557
  const securityBookmark = new Uint8Array([0x62, 0x6f, 0x6f, 0x6b]); // "book"
1497
- const mediaUrn = 'media:pdf;binary';
1558
+ const mediaUrn = 'media:pdf;bytes';
1498
1559
 
1499
1560
  const source = StdinSource.fromFileReference(trackedFileId, originalPath, securityBookmark, mediaUrn);
1500
1561
 
@@ -1510,6 +1571,7 @@ function testStdinSourceFromFileReference() {
1510
1571
  console.log(' ✓ From file reference');
1511
1572
  }
1512
1573
 
1574
+ // TEST158: Test StdinSource Data with empty vector stores and retrieves correctly
1513
1575
  function testStdinSourceWithEmptyData() {
1514
1576
  console.log('Testing StdinSource: With empty data...');
1515
1577
 
@@ -1523,6 +1585,7 @@ function testStdinSourceWithEmptyData() {
1523
1585
  console.log(' ✓ With empty data');
1524
1586
  }
1525
1587
 
1588
+ // TEST030: Test StdinSource with null data creates valid Data source
1526
1589
  function testStdinSourceWithNullData() {
1527
1590
  console.log('Testing StdinSource: With null data...');
1528
1591
 
@@ -1535,6 +1598,7 @@ function testStdinSourceWithNullData() {
1535
1598
  console.log(' ✓ With null data');
1536
1599
  }
1537
1600
 
1601
+ // TEST159: Test StdinSource Data with binary content like PNG header bytes
1538
1602
  function testStdinSourceWithBinaryContent() {
1539
1603
  console.log('Testing StdinSource: With binary content...');
1540
1604
 
@@ -1551,6 +1615,7 @@ function testStdinSourceWithBinaryContent() {
1551
1615
  console.log(' ✓ With binary content');
1552
1616
  }
1553
1617
 
1618
+ // TEST031: Test StdinSourceKind constants are defined and distinct
1554
1619
  function testStdinSourceKindConstants() {
1555
1620
  console.log('Testing StdinSource: Kind constants...');
1556
1621
 
@@ -1561,6 +1626,7 @@ function testStdinSourceKindConstants() {
1561
1626
  console.log(' ✓ Kind constants');
1562
1627
  }
1563
1628
 
1629
+ // TEST032: Test StdinSource Data is passed correctly to executeCap
1564
1630
  function testStdinSourcePassedToExecuteCap() {
1565
1631
  console.log('Testing StdinSource: Passed to executeCap...');
1566
1632
 
@@ -1605,6 +1671,7 @@ function testStdinSourcePassedToExecuteCap() {
1605
1671
  });
1606
1672
  }
1607
1673
 
1674
+ // TEST033: Test StdinSource FileReference is passed correctly to executeCap
1608
1675
  function testStdinSourceFileReferencePassedToExecuteCap() {
1609
1676
  console.log('Testing StdinSource: File reference passed to executeCap...');
1610
1677
 
@@ -1636,7 +1703,7 @@ function testStdinSourceFileReferencePassedToExecuteCap() {
1636
1703
  'tracked-123',
1637
1704
  '/path/to/file.pdf',
1638
1705
  new Uint8Array([0x42, 0x4f, 0x4f, 0x4b]),
1639
- 'media:pdf;binary'
1706
+ 'media:pdf;bytes'
1640
1707
  );
1641
1708
 
1642
1709
  const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
@@ -1652,11 +1719,76 @@ function testStdinSourceFileReferencePassedToExecuteCap() {
1652
1719
  assert(receivedSource.isFileReference(), 'Should receive file reference source');
1653
1720
  assertEqual(receivedSource.trackedFileId, 'tracked-123', 'Should have correct trackedFileId');
1654
1721
  assertEqual(receivedSource.originalPath, '/path/to/file.pdf', 'Should have correct originalPath');
1655
- assertEqual(receivedSource.mediaUrn, 'media:pdf;binary', 'Should have correct mediaUrn');
1722
+ assertEqual(receivedSource.mediaUrn, 'media:pdf;bytes', 'Should have correct mediaUrn');
1656
1723
  console.log(' ✓ File reference passed to executeCap');
1657
1724
  });
1658
1725
  }
1659
1726
 
1727
+ // ============================================================================
1728
+ // XV5 VALIDATION TESTS
1729
+ // TEST054-056: Validate that inline media_specs don't redefine registry specs
1730
+ // ============================================================================
1731
+
1732
+ // TEST054: Test XV5 validation detects inline media spec redefinition of built-in spec
1733
+ function testXV5InlineSpecRedefinitionDetected() {
1734
+ console.log('Testing XV5: Inline spec redefinition detected...');
1735
+
1736
+ // Try to redefine MEDIA_STRING which is a built-in spec
1737
+ const mediaSpecs = {
1738
+ [MEDIA_STRING]: {
1739
+ media_type: 'text/plain',
1740
+ title: 'My Custom String',
1741
+ description: 'Trying to redefine string'
1742
+ }
1743
+ };
1744
+
1745
+ const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
1746
+
1747
+ assert(!result.valid, 'Should fail validation when redefining built-in spec');
1748
+ assert(result.error && result.error.includes('XV5'), 'Error should mention XV5');
1749
+ assert(result.redefines && result.redefines.includes(MEDIA_STRING), 'Should identify MEDIA_STRING as redefined');
1750
+
1751
+ console.log(' ✓ Inline spec redefinition detected');
1752
+ }
1753
+
1754
+ // TEST055: Test XV5 validation allows new inline media spec not in built-ins
1755
+ function testXV5NewInlineSpecAllowed() {
1756
+ console.log('Testing XV5: New inline spec allowed...');
1757
+
1758
+ // Define a completely new media spec that doesn't exist in built-ins
1759
+ const mediaSpecs = {
1760
+ 'media:my-unique-custom-type-xyz123': {
1761
+ media_type: 'application/json',
1762
+ title: 'My Custom Output',
1763
+ description: 'A custom output type'
1764
+ }
1765
+ };
1766
+
1767
+ const result = validateNoMediaSpecRedefinitionSync(mediaSpecs);
1768
+
1769
+ assert(result.valid, 'Should pass validation for new spec not in built-ins');
1770
+ assert(!result.error, 'Should not have error message');
1771
+
1772
+ console.log(' ✓ New inline spec allowed');
1773
+ }
1774
+
1775
+ // TEST056: Test XV5 validation passes for empty or null media_specs
1776
+ function testXV5EmptyMediaSpecsAllowed() {
1777
+ console.log('Testing XV5: Empty media_specs allowed...');
1778
+
1779
+ // Empty or null media_specs should pass
1780
+ let result = validateNoMediaSpecRedefinitionSync({});
1781
+ assert(result.valid, 'Empty object should pass validation');
1782
+
1783
+ result = validateNoMediaSpecRedefinitionSync(null);
1784
+ assert(result.valid, 'Null should pass validation');
1785
+
1786
+ result = validateNoMediaSpecRedefinitionSync(undefined);
1787
+ assert(result.valid, 'Undefined should pass validation');
1788
+
1789
+ console.log(' ✓ Empty media_specs allowed');
1790
+ }
1791
+
1660
1792
  // Update runTests to include new tests
1661
1793
  async function runTests() {
1662
1794
  console.log('Running Cap URN JavaScript tests...\n');
@@ -1737,6 +1869,11 @@ async function runTests() {
1737
1869
  await testStdinSourcePassedToExecuteCap();
1738
1870
  await testStdinSourceFileReferencePassedToExecuteCap();
1739
1871
 
1872
+ // XV5 validation tests
1873
+ testXV5InlineSpecRedefinitionDetected();
1874
+ testXV5NewInlineSpecAllowed();
1875
+ testXV5EmptyMediaSpecsAllowed();
1876
+
1740
1877
  console.log('OK All tests passed!');
1741
1878
  }
1742
1879
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "Bahram Joharshamshiri",
3
3
  "dependencies": {
4
- "tagged-urn": "^0.16.3139"
4
+ "tagged-urn": "^0.18.2934"
5
5
  },
6
6
  "description": "JavaScript implementation of Cap URN (Capability Uniform Resource Names) with strict validation and matching",
7
7
  "engines": {
@@ -32,5 +32,5 @@
32
32
  "scripts": {
33
33
  "test": "node capns.test.js"
34
34
  },
35
- "version": "0.50.11396"
35
+ "version": "0.58.11575"
36
36
  }