capdag 0.124.274 → 0.127.280

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/capdag.js +230 -87
  2. package/capdag.test.js +112 -73
  3. package/package.json +1 -1
package/capdag.js CHANGED
@@ -789,18 +789,22 @@ const MEDIA_OBJECT = 'media:record';
789
789
  // Media URN for binary data - the most general media type (no constraints)
790
790
  const MEDIA_IDENTITY = 'media:';
791
791
 
792
- // Array types - URNs must match base.toml definitions
793
- // Media URN for string array type - textable with list marker
794
- const MEDIA_STRING_ARRAY = 'media:list;textable';
795
- // Media URN for integer array type - textable, numeric with list marker
796
- const MEDIA_INTEGER_ARRAY = 'media:integer;list;textable;numeric';
797
- // Media URN for number array type - textable, numeric with list marker
798
- const MEDIA_NUMBER_ARRAY = 'media:list;textable;numeric';
799
- // Media URN for boolean array type - uses "bool" with list marker
800
- const MEDIA_BOOLEAN_ARRAY = 'media:bool;list;textable';
801
- // Media URN for object array type - list of records (NOT textable)
802
- // Use a specific format like JSON array for textable object arrays.
803
- const MEDIA_OBJECT_ARRAY = 'media:list;record';
792
+ // List types - URNs must match base.toml definitions
793
+ // Media URN for generic list type
794
+ const MEDIA_LIST = 'media:list';
795
+ // Media URN for textable list type
796
+ const MEDIA_TEXTABLE_LIST = 'media:list;textable';
797
+ // Media URN for string list type - textable with list marker
798
+ const MEDIA_STRING_LIST = 'media:list;textable';
799
+ // Media URN for integer list type - textable, numeric with list marker
800
+ const MEDIA_INTEGER_LIST = 'media:integer;list;textable;numeric';
801
+ // Media URN for number list type - textable, numeric with list marker
802
+ const MEDIA_NUMBER_LIST = 'media:list;numeric;textable';
803
+ // Media URN for boolean list type - uses "bool" with list marker
804
+ const MEDIA_BOOLEAN_LIST = 'media:bool;list;textable';
805
+ // Media URN for object list type - list of records (NOT textable)
806
+ // Use a specific format like JSON array for textable object lists.
807
+ const MEDIA_OBJECT_LIST = 'media:list;record';
804
808
 
805
809
  // Semantic media types for specialized content
806
810
  // Media URN for PNG image data
@@ -813,8 +817,6 @@ const MEDIA_VIDEO = 'media:video';
813
817
  // Semantic AI input types - distinguished by their purpose/context
814
818
  // Media URN for audio input containing speech for transcription (Whisper)
815
819
  const MEDIA_AUDIO_SPEECH = 'media:audio;wav;speech';
816
- // Media URN for thumbnail image output
817
- const MEDIA_IMAGE_THUMBNAIL = 'media:image;png;thumbnail';
818
820
 
819
821
  // Document types (PRIMARY naming - type IS the format)
820
822
  // Media URN for PDF documents
@@ -842,6 +844,18 @@ const MEDIA_JSON_SCHEMA = 'media:json;json-schema;record;textable';
842
844
  // Media URN for YAML data - has record marker (structured key-value)
843
845
  const MEDIA_YAML = 'media:record;textable;yaml';
844
846
 
847
+ // Format-specific variants for JSON, YAML, CSV
848
+ const MEDIA_JSON_VALUE = 'media:json;textable';
849
+ const MEDIA_JSON_RECORD = 'media:json;record;textable';
850
+ const MEDIA_JSON_LIST = 'media:json;list;textable';
851
+ const MEDIA_JSON_LIST_RECORD = 'media:json;list;record;textable';
852
+ const MEDIA_YAML_VALUE = 'media:textable;yaml';
853
+ const MEDIA_YAML_RECORD = 'media:record;textable;yaml';
854
+ const MEDIA_YAML_LIST = 'media:list;textable;yaml';
855
+ const MEDIA_YAML_LIST_RECORD = 'media:list;record;textable;yaml';
856
+ const MEDIA_CSV = 'media:csv;list;record;textable';
857
+ const MEDIA_CSV_LIST = 'media:csv;list;textable';
858
+
845
859
  // File path types - for arguments that represent filesystem paths
846
860
  // Media URN for a single file path - textable, scalar by default (no list marker)
847
861
  const MEDIA_FILE_PATH = 'media:file-path;textable';
@@ -849,8 +863,6 @@ const MEDIA_FILE_PATH = 'media:file-path;textable';
849
863
  const MEDIA_FILE_PATH_ARRAY = 'media:file-path;list;textable';
850
864
 
851
865
  // Semantic text input types - distinguished by their purpose/context
852
- // Media URN for frontmatter text (book metadata) - scalar by default
853
- const MEDIA_FRONTMATTER_TEXT = 'media:frontmatter;textable';
854
866
  // Media URN for model spec (provider:model format, HuggingFace name, etc.) - scalar by default
855
867
  const MEDIA_MODEL_SPEC = 'media:model-spec;textable';
856
868
  // Media URN for MLX model path - scalar by default
@@ -877,20 +889,17 @@ const MEDIA_PATH_OUTPUT = 'media:model-path;record;textable';
877
889
  const MEDIA_EMBEDDING_VECTOR = 'media:embedding-vector;record;textable';
878
890
  // Media URN for LLM inference output - has record marker
879
891
  const MEDIA_LLM_INFERENCE_OUTPUT = 'media:generated-text;record;textable';
880
- // Media URN for extracted metadata - has record marker
881
- const MEDIA_FILE_METADATA = 'media:file-metadata;record;textable';
882
- // Media URN for extracted outline - has record marker
883
- const MEDIA_DOCUMENT_OUTLINE = 'media:document-outline;record;textable';
884
- // Media URN for disbound page - has list marker (array of page objects)
885
- const MEDIA_DISBOUND_PAGE = 'media:disbound-page;list;textable';
886
892
  // Media URN for vision inference output - textable, scalar by default
887
893
  const MEDIA_IMAGE_DESCRIPTION = 'media:image-description;textable';
888
894
  // Media URN for transcription output - has record marker
889
895
  const MEDIA_TRANSCRIPTION_OUTPUT = 'media:record;textable;transcription';
890
- // Media URN for decision output (bit choice) - scalar by default
891
- const MEDIA_DECISION = 'media:bool;decision;textable';
892
- // Media URN for decision array output (bit choices) - has list marker
893
- const MEDIA_DECISION_ARRAY = 'media:bool;decision;list;textable';
896
+ // Media URN for decision output - JSON record with textable
897
+ const MEDIA_DECISION = 'media:decision;json;record;textable';
898
+ // Media URN for textable page output
899
+ const MEDIA_TEXTABLE_PAGE = 'media:textable;page';
900
+ // Collection types
901
+ const MEDIA_COLLECTION = 'media:collection;record';
902
+ const MEDIA_COLLECTION_LIST = 'media:collection;list;record';
894
903
 
895
904
  // =============================================================================
896
905
  // STANDARD CAP URN CONSTANTS
@@ -956,8 +965,10 @@ class MediaUrn {
956
965
  // =========================================================================
957
966
 
958
967
  /**
959
- * Returns true if this media is a list (has `list` marker tag).
960
- * Returns false if scalar (no `list` marker = default).
968
+ * Returns true if this media URN describes list-type data (has `list` marker tag).
969
+ * This is a semantic type check it means "the data format IS a list/array".
970
+ * This does NOT indicate input cardinality (single vs multiple items).
971
+ * Cardinality is tracked by is_sequence on the wire protocol, not by URN tags.
961
972
  * @returns {boolean}
962
973
  */
963
974
  isList() { return this._hasMarkerTag('list'); }
@@ -1027,6 +1038,22 @@ class MediaUrn {
1027
1038
  /** @returns {boolean} True if the "bool" marker tag is present */
1028
1039
  isBool() { return this._urn.getTag('bool') !== undefined; }
1029
1040
 
1041
+ /**
1042
+ * Returns true if this media URN describes YAML representation.
1043
+ * @returns {boolean}
1044
+ */
1045
+ isYaml() {
1046
+ return this._hasMarkerTag('yaml');
1047
+ }
1048
+
1049
+ /**
1050
+ * Returns true if this media URN describes CSV representation.
1051
+ * @returns {boolean}
1052
+ */
1053
+ isCsv() {
1054
+ return this._hasMarkerTag('csv');
1055
+ }
1056
+
1030
1057
  /**
1031
1058
  * Check if this represents a single file path type (not array).
1032
1059
  * Returns true if the "file-path" marker tag is present AND no list marker.
@@ -1048,6 +1075,13 @@ class MediaUrn {
1048
1075
  */
1049
1076
  isAnyFilePath() { return this._hasMarkerTag('file-path'); }
1050
1077
 
1078
+ /**
1079
+ * Check if this represents a collection type.
1080
+ * Returns true if the "collection" marker tag is present.
1081
+ * @returns {boolean}
1082
+ */
1083
+ isCollection() { return this._hasMarkerTag('collection'); }
1084
+
1051
1085
  /**
1052
1086
  * Check if this media URN conforms to another (pattern).
1053
1087
  * @param {MediaUrn} pattern
@@ -1123,20 +1157,67 @@ class MediaUrn {
1123
1157
  // =============================================================================
1124
1158
 
1125
1159
  /**
1126
- * Build URN for LLM conversation capability
1127
- * @param {string} langCode - Language code (e.g., "en", "fr")
1160
+ * Build URN for LLM generate-text capability
1128
1161
  * @returns {CapUrn}
1129
1162
  */
1130
- function llmConversationUrn(langCode) {
1163
+ function llmGenerateTextUrn() {
1131
1164
  return new CapUrnBuilder()
1132
- .tag('op', 'conversation')
1133
- .tag('unconstrained', '*')
1134
- .tag('language', langCode)
1165
+ .tag('op', 'generate_text')
1166
+ .tag('llm', '*')
1167
+ .tag('ml-model', '*')
1135
1168
  .inSpec(MEDIA_STRING)
1136
- .outSpec(MEDIA_LLM_INFERENCE_OUTPUT)
1169
+ .outSpec(MEDIA_STRING)
1137
1170
  .build();
1138
1171
  }
1139
1172
 
1173
+ /**
1174
+ * Build URN for render-page-image capability
1175
+ * @param {string} inputMedia - The input media URN string
1176
+ * @returns {CapUrn}
1177
+ */
1178
+ function renderPageImageUrn(inputMedia) {
1179
+ return new CapUrnBuilder()
1180
+ .tag('op', 'render_page_image')
1181
+ .inSpec(inputMedia)
1182
+ .outSpec(MEDIA_PNG)
1183
+ .build();
1184
+ }
1185
+
1186
+ /**
1187
+ * Build URN for format conversion capability
1188
+ * @param {string} inMedia - The input media URN string
1189
+ * @param {string} outMedia - The output media URN string
1190
+ * @returns {CapUrn}
1191
+ */
1192
+ function formatConversionUrn(inMedia, outMedia) {
1193
+ return new CapUrnBuilder()
1194
+ .tag('op', 'convert_format')
1195
+ .inSpec(inMedia)
1196
+ .outSpec(outMedia)
1197
+ .build();
1198
+ }
1199
+
1200
+ /**
1201
+ * Map a primitive type name to the corresponding media URN string.
1202
+ * @param {string} typeName - The type name (e.g., 'string', 'integer', 'string-list')
1203
+ * @returns {string|null} The media URN string, or null if not recognized
1204
+ */
1205
+ function mediaUrnForType(typeName) {
1206
+ switch (typeName) {
1207
+ case 'string': return MEDIA_STRING;
1208
+ case 'integer': return MEDIA_INTEGER;
1209
+ case 'number': return MEDIA_NUMBER;
1210
+ case 'boolean': return MEDIA_BOOLEAN;
1211
+ case 'object': return MEDIA_OBJECT;
1212
+ case 'string-list': return MEDIA_STRING_LIST;
1213
+ case 'integer-list': return MEDIA_INTEGER_LIST;
1214
+ case 'number-list': return MEDIA_NUMBER_LIST;
1215
+ case 'boolean-list': return MEDIA_BOOLEAN_LIST;
1216
+ case 'object-list': return MEDIA_OBJECT_LIST;
1217
+ default: return null;
1218
+ }
1219
+ }
1220
+
1140
1221
  /**
1141
1222
  * Build URN for model-availability capability
1142
1223
  * @returns {CapUrn}
@@ -2933,6 +3014,56 @@ class CapValidator {
2933
3014
  // CAP ARGUMENT VALUE - Unified argument type
2934
3015
  // ============================================================================
2935
3016
 
3017
+ /**
3018
+ * Result from a cap execution.
3019
+ *
3020
+ * Scalar outputs carry raw materialized bytes (e.g. UTF-8 text, raw binary).
3021
+ * List outputs carry a CBOR sequence of values, one per list item.
3022
+ * Empty represents a void cap with no output.
3023
+ */
3024
+ class CapResult {
3025
+ static KIND_SCALAR = 'scalar';
3026
+ static KIND_LIST = 'list';
3027
+ static KIND_EMPTY = 'empty';
3028
+
3029
+ /**
3030
+ * @param {'scalar'|'list'|'empty'} kind
3031
+ * @param {Uint8Array|null} data - Bytes for scalar or CBOR sequence for list, null for empty
3032
+ */
3033
+ constructor(kind, data = null) {
3034
+ this.kind = kind;
3035
+ this.data = data;
3036
+ }
3037
+
3038
+ /** Create a CapResult carrying raw bytes (scalar output). */
3039
+ static scalar(data) {
3040
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data || []);
3041
+ return new CapResult(CapResult.KIND_SCALAR, bytes);
3042
+ }
3043
+
3044
+ /** Create a CapResult carrying a CBOR sequence (list output). */
3045
+ static list(cborSequence) {
3046
+ const bytes = cborSequence instanceof Uint8Array ? cborSequence : new Uint8Array(cborSequence || []);
3047
+ return new CapResult(CapResult.KIND_LIST, bytes);
3048
+ }
3049
+
3050
+ /** Create a CapResult for void caps. */
3051
+ static empty() {
3052
+ return new CapResult(CapResult.KIND_EMPTY, null);
3053
+ }
3054
+
3055
+ /** Returns true if this is a scalar result. */
3056
+ isScalar() { return this.kind === CapResult.KIND_SCALAR; }
3057
+
3058
+ /** Returns true if this is a list result. */
3059
+ isList() { return this.kind === CapResult.KIND_LIST; }
3060
+
3061
+ /** Returns true if this is an empty result. */
3062
+ isEmpty() { return this.kind === CapResult.KIND_EMPTY; }
3063
+ }
3064
+
3065
+ // ============================================================================
3066
+
2936
3067
  /**
2937
3068
  * Unified argument type - arguments are identified by media_urn.
2938
3069
  * The cap definition's sources specify how to extract values (stdin, position, cli_flag).
@@ -3189,7 +3320,7 @@ class CompositeCapSet {
3189
3320
  * Execute a capability by finding the best match and delegating
3190
3321
  * @param {string} capUrn - The capability URN to execute
3191
3322
  * @param {CapArgumentValue[]} args - Arguments identified by media_urn
3192
- * @returns {Promise<{binaryOutput: Uint8Array|null, textOutput: string|null}>}
3323
+ * @returns {Promise<CapResult>}
3193
3324
  */
3194
3325
  async executeCap(capUrn, args) {
3195
3326
  let request;
@@ -3903,12 +4034,8 @@ class CartridgeInfo {
3903
4034
  this.caps = (data.caps || []).map(c => new CartridgeCapSummary(c.urn, c.title, c.description || ''));
3904
4035
  this.categories = data.categories || [];
3905
4036
  this.tags = data.tags || [];
3906
- this.changelog = data.changelog || {};
3907
- // Distribution fields
3908
- this.platform = data.platform || '';
3909
- this.packageName = data.packageName || '';
3910
- this.packageSha256 = data.packageSha256 || '';
3911
- this.packageSize = data.packageSize || 0;
4037
+ // Versions with platform-specific builds
4038
+ this.versions = data.versions || {};
3912
4039
  this.availableVersions = data.availableVersions || [];
3913
4040
  }
3914
4041
 
@@ -3920,10 +4047,25 @@ class CartridgeInfo {
3920
4047
  }
3921
4048
 
3922
4049
  /**
3923
- * Check if package download info is available
4050
+ * Get the build for a specific platform from the latest version
3924
4051
  */
3925
- hasPackage() {
3926
- return this.packageName.length > 0 && this.packageSha256.length > 0;
4052
+ buildForPlatform(platform) {
4053
+ const latestVersionData = this.versions[this.version];
4054
+ if (!latestVersionData) return null;
4055
+ return (latestVersionData.builds || []).find(b => b.platform === platform) || null;
4056
+ }
4057
+
4058
+ /**
4059
+ * Get all platforms available across all versions
4060
+ */
4061
+ availablePlatforms() {
4062
+ const platforms = new Set();
4063
+ for (const versionData of Object.values(this.versions)) {
4064
+ for (const build of (versionData.builds || [])) {
4065
+ platforms.add(build.platform);
4066
+ }
4067
+ }
4068
+ return Array.from(platforms).sort();
3927
4069
  }
3928
4070
  }
3929
4071
 
@@ -4145,8 +4287,8 @@ class CartridgeRepoServer {
4145
4287
  if (!this.registry) {
4146
4288
  throw new Error('Registry is required');
4147
4289
  }
4148
- if (this.registry.schemaVersion !== '3.0') {
4149
- throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 3.0`);
4290
+ if (this.registry.schemaVersion !== '4.0') {
4291
+ throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 4.0`);
4150
4292
  }
4151
4293
  if (!this.registry.cartridges || typeof this.registry.cartridges !== 'object') {
4152
4294
  throw new Error('Registry must have cartridges object');
@@ -4157,11 +4299,17 @@ class CartridgeRepoServer {
4157
4299
  * Validate version data has all required fields
4158
4300
  */
4159
4301
  validateVersionData(id, version, versionData) {
4160
- if (!versionData.platform) {
4161
- throw new Error(`Cartridge ${id} v${version}: missing required field 'platform'`);
4302
+ if (!Array.isArray(versionData.builds) || versionData.builds.length === 0) {
4303
+ throw new Error(`Cartridge ${id} v${version}: no builds`);
4162
4304
  }
4163
- if (!versionData.package || !versionData.package.name) {
4164
- throw new Error(`Cartridge ${id} v${version}: missing required field 'package'`);
4305
+ for (let i = 0; i < versionData.builds.length; i++) {
4306
+ const build = versionData.builds[i];
4307
+ if (!build.platform) {
4308
+ throw new Error(`Cartridge ${id} v${version}: build[${i}] missing platform`);
4309
+ }
4310
+ if (!build.package || !build.package.name) {
4311
+ throw new Error(`Cartridge ${id} v${version}: build[${i}] (${build.platform}) missing package.name`);
4312
+ }
4165
4313
  }
4166
4314
  }
4167
4315
 
@@ -4182,19 +4330,6 @@ class CartridgeRepoServer {
4182
4330
  return 0;
4183
4331
  }
4184
4332
 
4185
- /**
4186
- * Build changelog map from versions
4187
- */
4188
- buildChangelogMap(versions) {
4189
- const changelog = {};
4190
- for (const [version, versionData] of Object.entries(versions)) {
4191
- if (versionData.changelog && Array.isArray(versionData.changelog)) {
4192
- changelog[version] = versionData.changelog;
4193
- }
4194
- }
4195
- return changelog;
4196
- }
4197
-
4198
4333
  /**
4199
4334
  * Transform registry to flat cartridge array
4200
4335
  */
@@ -4218,28 +4353,20 @@ class CartridgeRepoServer {
4218
4353
  return this.compareVersions(b, a);
4219
4354
  });
4220
4355
 
4221
- // Build flat cartridge object with latest version data
4222
- const packageUrl = `https://machinefabric.com/cartridges/packages/${versionData.package.name}`;
4223
4356
  cartridges.push({
4224
4357
  id,
4225
4358
  name: cartridge.name,
4226
4359
  version: latestVersion,
4227
4360
  description: cartridge.description,
4228
4361
  author: cartridge.author,
4229
- pageUrl: cartridge.pageUrl || packageUrl,
4362
+ pageUrl: cartridge.pageUrl || '',
4230
4363
  teamId: cartridge.teamId,
4231
4364
  signedAt: versionData.releaseDate,
4232
4365
  minAppVersion: versionData.minAppVersion || cartridge.minAppVersion,
4233
4366
  caps: cartridge.caps || [],
4234
4367
  categories: cartridge.categories,
4235
4368
  tags: cartridge.tags,
4236
- changelog: this.buildChangelogMap(cartridge.versions),
4237
- // Distribution fields
4238
- platform: versionData.platform,
4239
- packageName: versionData.package.name,
4240
- packageSha256: versionData.package.sha256,
4241
- packageSize: versionData.package.size,
4242
- // All available versions
4369
+ versions: cartridge.versions,
4243
4370
  availableVersions
4244
4371
  });
4245
4372
  }
@@ -5439,18 +5566,20 @@ module.exports = {
5439
5566
  MEDIA_NUMBER,
5440
5567
  MEDIA_BOOLEAN,
5441
5568
  MEDIA_OBJECT,
5442
- MEDIA_STRING_ARRAY,
5443
- MEDIA_INTEGER_ARRAY,
5444
- MEDIA_NUMBER_ARRAY,
5445
- MEDIA_BOOLEAN_ARRAY,
5446
- MEDIA_OBJECT_ARRAY,
5569
+ // List types
5570
+ MEDIA_LIST,
5571
+ MEDIA_TEXTABLE_LIST,
5572
+ MEDIA_STRING_LIST,
5573
+ MEDIA_INTEGER_LIST,
5574
+ MEDIA_NUMBER_LIST,
5575
+ MEDIA_BOOLEAN_LIST,
5576
+ MEDIA_OBJECT_LIST,
5447
5577
  MEDIA_IDENTITY,
5448
5578
  MEDIA_VOID,
5449
5579
  MEDIA_PNG,
5450
5580
  MEDIA_AUDIO,
5451
5581
  MEDIA_VIDEO,
5452
5582
  MEDIA_AUDIO_SPEECH,
5453
- MEDIA_IMAGE_THUMBNAIL,
5454
5583
  // Document types (PRIMARY naming)
5455
5584
  MEDIA_PDF,
5456
5585
  MEDIA_EPUB,
@@ -5464,11 +5593,22 @@ module.exports = {
5464
5593
  MEDIA_JSON,
5465
5594
  MEDIA_JSON_SCHEMA,
5466
5595
  MEDIA_YAML,
5596
+ // Format-specific variants
5597
+ MEDIA_JSON_VALUE,
5598
+ MEDIA_JSON_RECORD,
5599
+ MEDIA_JSON_LIST,
5600
+ MEDIA_JSON_LIST_RECORD,
5601
+ MEDIA_YAML_VALUE,
5602
+ MEDIA_YAML_RECORD,
5603
+ MEDIA_YAML_LIST,
5604
+ MEDIA_YAML_LIST_RECORD,
5605
+ MEDIA_CSV,
5606
+ MEDIA_CSV_LIST,
5467
5607
  MEDIA_MODEL_SPEC,
5468
5608
  MEDIA_MODEL_REPO,
5469
5609
  MEDIA_MODEL_DIM,
5470
5610
  MEDIA_DECISION,
5471
- MEDIA_DECISION_ARRAY,
5611
+ MEDIA_TEXTABLE_PAGE,
5472
5612
  // Semantic output types - model management
5473
5613
  MEDIA_DOWNLOAD_OUTPUT,
5474
5614
  MEDIA_LIST_OUTPUT,
@@ -5479,21 +5619,24 @@ module.exports = {
5479
5619
  // Semantic output types - inference
5480
5620
  MEDIA_EMBEDDING_VECTOR,
5481
5621
  MEDIA_LLM_INFERENCE_OUTPUT,
5482
- MEDIA_FILE_METADATA,
5483
- MEDIA_DOCUMENT_OUTLINE,
5484
- MEDIA_DISBOUND_PAGE,
5485
5622
  MEDIA_IMAGE_DESCRIPTION,
5486
5623
  MEDIA_TRANSCRIPTION_OUTPUT,
5487
5624
  // File path types
5488
5625
  MEDIA_FILE_PATH,
5489
5626
  MEDIA_FILE_PATH_ARRAY,
5490
- // Semantic text input types
5491
- MEDIA_FRONTMATTER_TEXT,
5492
5627
  MEDIA_MLX_MODEL_PATH,
5628
+ // Collection types
5629
+ MEDIA_COLLECTION,
5630
+ MEDIA_COLLECTION_LIST,
5631
+ // Cap execution result
5632
+ CapResult,
5493
5633
  // Unified argument type
5494
5634
  CapArgumentValue,
5495
5635
  // Standard cap URN builders
5496
- llmConversationUrn,
5636
+ llmGenerateTextUrn,
5637
+ renderPageImageUrn,
5638
+ formatConversionUrn,
5639
+ mediaUrnForType,
5497
5640
  modelAvailabilityUrn,
5498
5641
  modelPathUrn,
5499
5642
  CapMatrixError,
package/capdag.test.js CHANGED
@@ -13,12 +13,12 @@ const {
13
13
  StdinSource, StdinSourceKind,
14
14
  validateNoMediaSpecRedefinitionSync,
15
15
  CapArgumentValue,
16
- llmConversationUrn, modelAvailabilityUrn, modelPathUrn,
16
+ llmGenerateTextUrn, modelAvailabilityUrn, modelPathUrn,
17
17
  MachineSyntaxError, MachineSyntaxErrorCodes, MachineEdge, Machine, MachineBuilder, parseMachine, parseMachineWithAST,
18
18
  CapRegistryEntry, MediaRegistryEntry, CapRegistryClient,
19
19
  MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN,
20
- MEDIA_OBJECT, MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY,
21
- MEDIA_NUMBER_ARRAY, MEDIA_BOOLEAN_ARRAY, MEDIA_OBJECT_ARRAY,
20
+ MEDIA_OBJECT, MEDIA_STRING_LIST, MEDIA_INTEGER_LIST,
21
+ MEDIA_NUMBER_LIST, MEDIA_BOOLEAN_LIST, MEDIA_OBJECT_LIST,
22
22
  MEDIA_IDENTITY, MEDIA_VOID, MEDIA_PNG, MEDIA_AUDIO, MEDIA_VIDEO,
23
23
  MEDIA_PDF, MEDIA_EPUB, MEDIA_MD, MEDIA_TXT, MEDIA_RST, MEDIA_LOG,
24
24
  MEDIA_HTML, MEDIA_XML, MEDIA_JSON, MEDIA_YAML, MEDIA_JSON_SCHEMA,
@@ -26,8 +26,8 @@ const {
26
26
  MEDIA_LLM_INFERENCE_OUTPUT,
27
27
  MEDIA_FILE_PATH, MEDIA_FILE_PATH_ARRAY,
28
28
  MEDIA_COLLECTION, MEDIA_COLLECTION_LIST,
29
- MEDIA_DECISION, MEDIA_DECISION_ARRAY,
30
- MEDIA_AUDIO_SPEECH, MEDIA_IMAGE_THUMBNAIL
29
+ MEDIA_DECISION,
30
+ MEDIA_AUDIO_SPEECH
31
31
  } = require('./capdag.js');
32
32
 
33
33
  // ============================================================================
@@ -827,7 +827,7 @@ function test061_isBinary() {
827
827
  assert(!MediaUrn.fromString(MEDIA_MD).isBinary(), 'MEDIA_MD should not be binary');
828
828
  }
829
829
 
830
- // TEST062: isMap true for MEDIA_OBJECT (record); false for MEDIA_STRING (form=scalar), MEDIA_STRING_ARRAY (list)
830
+ // TEST062: isMap true for MEDIA_OBJECT (record); false for MEDIA_STRING (form=scalar), MEDIA_STRING_LIST (list)
831
831
  // TEST062: is_record returns true if record marker tag is present (key-value structure)
832
832
  function test062_isRecord() {
833
833
  assert(MediaUrn.fromString(MEDIA_OBJECT).isRecord(), 'MEDIA_OBJECT should be record');
@@ -836,7 +836,7 @@ function test062_isRecord() {
836
836
  // Without record marker, is_record is false
837
837
  assert(!MediaUrn.fromString('media:textable').isRecord(), 'plain textable should not be record');
838
838
  assert(!MediaUrn.fromString(MEDIA_STRING).isRecord(), 'MEDIA_STRING should not be record');
839
- assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isRecord(), 'MEDIA_STRING_ARRAY should not be record');
839
+ assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isRecord(), 'MEDIA_STRING_LIST should not be record');
840
840
  }
841
841
 
842
842
  // TEST063: is_scalar returns true if NO list marker (scalar is default cardinality)
@@ -848,16 +848,16 @@ function test063_isScalar() {
848
848
  assert(MediaUrn.fromString(MEDIA_OBJECT).isScalar(), 'MEDIA_OBJECT (record but scalar) should be scalar');
849
849
  assert(MediaUrn.fromString('media:textable').isScalar(), 'plain textable should be scalar');
850
850
  // With list marker, is_scalar is false
851
- assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isScalar(), 'MEDIA_STRING_ARRAY should not be scalar');
852
- assert(!MediaUrn.fromString(MEDIA_OBJECT_ARRAY).isScalar(), 'MEDIA_OBJECT_ARRAY should not be scalar');
851
+ assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isScalar(), 'MEDIA_STRING_LIST should not be scalar');
852
+ assert(!MediaUrn.fromString(MEDIA_OBJECT_LIST).isScalar(), 'MEDIA_OBJECT_LIST should not be scalar');
853
853
  }
854
854
 
855
- // TEST064: isList true for MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY, MEDIA_OBJECT_ARRAY;
855
+ // TEST064: isList true for MEDIA_STRING_LIST, MEDIA_INTEGER_LIST, MEDIA_OBJECT_LIST;
856
856
  // false for MEDIA_STRING, MEDIA_OBJECT
857
857
  function test064_isList() {
858
- assert(MediaUrn.fromString(MEDIA_STRING_ARRAY).isList(), 'MEDIA_STRING_ARRAY should be list');
859
- assert(MediaUrn.fromString(MEDIA_INTEGER_ARRAY).isList(), 'MEDIA_INTEGER_ARRAY should be list');
860
- assert(MediaUrn.fromString(MEDIA_OBJECT_ARRAY).isList(), 'MEDIA_OBJECT_ARRAY should be list');
858
+ assert(MediaUrn.fromString(MEDIA_STRING_LIST).isList(), 'MEDIA_STRING_LIST should be list');
859
+ assert(MediaUrn.fromString(MEDIA_INTEGER_LIST).isList(), 'MEDIA_INTEGER_LIST should be list');
860
+ assert(MediaUrn.fromString(MEDIA_OBJECT_LIST).isList(), 'MEDIA_OBJECT_LIST should be list');
861
861
  assert(!MediaUrn.fromString(MEDIA_STRING).isList(), 'MEDIA_STRING should not be list');
862
862
  assert(!MediaUrn.fromString(MEDIA_OBJECT).isList(), 'MEDIA_OBJECT should not be list');
863
863
  }
@@ -865,7 +865,7 @@ function test064_isList() {
865
865
  // TEST065: is_opaque returns true if NO record marker (opaque is default structure)
866
866
  function test065_isOpaque() {
867
867
  assert(MediaUrn.fromString(MEDIA_STRING).isOpaque(), 'MEDIA_STRING should be opaque');
868
- assert(MediaUrn.fromString(MEDIA_STRING_ARRAY).isOpaque(), 'MEDIA_STRING_ARRAY (list but no record) should be opaque');
868
+ assert(MediaUrn.fromString(MEDIA_STRING_LIST).isOpaque(), 'MEDIA_STRING_LIST (list but no record) should be opaque');
869
869
  assert(MediaUrn.fromString(MEDIA_PDF).isOpaque(), 'MEDIA_PDF should be opaque');
870
870
  assert(MediaUrn.fromString('media:textable').isOpaque(), 'plain textable should be opaque');
871
871
  // With record marker, is_opaque is false
@@ -912,8 +912,8 @@ function test071_toStringRoundtrip() {
912
912
  function test072_constantsParse() {
913
913
  const constants = [
914
914
  MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN,
915
- MEDIA_OBJECT, MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY,
916
- MEDIA_NUMBER_ARRAY, MEDIA_BOOLEAN_ARRAY, MEDIA_OBJECT_ARRAY,
915
+ MEDIA_OBJECT, MEDIA_STRING_LIST, MEDIA_INTEGER_LIST,
916
+ MEDIA_NUMBER_LIST, MEDIA_BOOLEAN_LIST, MEDIA_OBJECT_LIST,
917
917
  MEDIA_IDENTITY, MEDIA_VOID, MEDIA_PNG, MEDIA_PDF, MEDIA_EPUB,
918
918
  MEDIA_MD, MEDIA_TXT, MEDIA_RST, MEDIA_LOG, MEDIA_HTML, MEDIA_XML,
919
919
  MEDIA_JSON, MEDIA_YAML, MEDIA_JSON_SCHEMA, MEDIA_AUDIO, MEDIA_VIDEO,
@@ -1052,8 +1052,8 @@ function test101_resolvedIsScalar() {
1052
1052
 
1053
1053
  // TEST102: MediaSpec with list -> isList() true
1054
1054
  function test102_resolvedIsList() {
1055
- const spec = new MediaSpec('text/plain', null, null, 'String Array', null, MEDIA_STRING_ARRAY);
1056
- assert(spec.isList(), 'Resolved string_array spec should be list');
1055
+ const spec = new MediaSpec('text/plain', null, null, 'String List', null, MEDIA_STRING_LIST);
1056
+ assert(spec.isList(), 'Resolved string_list spec should be list');
1057
1057
  }
1058
1058
 
1059
1059
  // TEST103: MediaSpec with json tag -> isJSON() true
@@ -1706,37 +1706,37 @@ function test309_modelAvailabilityAndPathAreDistinct() {
1706
1706
  assert(avail.toString() !== path.toString(), 'availability and path must be distinct');
1707
1707
  }
1708
1708
 
1709
- // TEST310: llm_conversation_urn uses unconstrained tag (not constrained)
1710
- function test310_llmConversationUrnUnconstrained() {
1711
- const urn = llmConversationUrn('en');
1712
- assert(urn.getTag('unconstrained') !== undefined, 'Must have unconstrained tag');
1713
- assert(urn.hasTag('op', 'conversation'), 'Must have op=conversation');
1714
- assert(urn.hasTag('language', 'en'), 'Must have language=en');
1709
+ // TEST310: llm_generate_text_urn has correct op and ml-model tags
1710
+ function test310_llmGenerateTextUrn() {
1711
+ const urn = llmGenerateTextUrn();
1712
+ assert(urn.hasTag('op', 'generate_text'), 'Must have op=generate_text');
1713
+ assert(urn.getTag('llm') !== undefined, 'Must have llm tag');
1714
+ assert(urn.getTag('ml-model') !== undefined, 'Must have ml-model tag');
1715
1715
  }
1716
1716
 
1717
- // TEST311: llm_conversation_urn in/out specs match the expected media URNs semantically
1718
- function test311_llmConversationUrnSpecs() {
1719
- const urn = llmConversationUrn('fr');
1717
+ // TEST311: llm_generate_text_urn in/out specs match MEDIA_STRING
1718
+ function test311_llmGenerateTextUrnSpecs() {
1719
+ const urn = llmGenerateTextUrn();
1720
1720
  const inSpec = TaggedUrn.fromString(urn.getInSpec());
1721
1721
  const expectedIn = TaggedUrn.fromString(MEDIA_STRING);
1722
1722
  assert(inSpec.conformsTo(expectedIn), 'in_spec must conform to MEDIA_STRING');
1723
1723
  const outSpec = TaggedUrn.fromString(urn.getOutSpec());
1724
- const expectedOut = TaggedUrn.fromString(MEDIA_LLM_INFERENCE_OUTPUT);
1725
- assert(outSpec.conformsTo(expectedOut), 'out_spec must conform to MEDIA_LLM_INFERENCE_OUTPUT');
1724
+ const expectedOut = TaggedUrn.fromString(MEDIA_STRING);
1725
+ assert(outSpec.conformsTo(expectedOut), 'out_spec must conform to MEDIA_STRING');
1726
1726
  }
1727
1727
 
1728
1728
  // TEST312: All URN builders produce parseable cap URNs
1729
1729
  function test312_allUrnBuildersProduceValidUrns() {
1730
1730
  const avail = modelAvailabilityUrn();
1731
1731
  const path = modelPathUrn();
1732
- const conv = llmConversationUrn('en');
1732
+ const llmGen = llmGenerateTextUrn();
1733
1733
 
1734
1734
  const parsedAvail = CapUrn.fromString(avail.toString());
1735
1735
  assert(parsedAvail !== null, 'modelAvailabilityUrn must be parseable');
1736
1736
  const parsedPath = CapUrn.fromString(path.toString());
1737
1737
  assert(parsedPath !== null, 'modelPathUrn must be parseable');
1738
- const parsedConv = CapUrn.fromString(conv.toString());
1739
- assert(parsedConv !== null, 'llmConversationUrn must be parseable');
1738
+ const parsedLlmGen = CapUrn.fromString(llmGen.toString());
1739
+ assert(parsedLlmGen !== null, 'llmGenerateTextUrn must be parseable');
1740
1740
  }
1741
1741
 
1742
1742
  // ============================================================================
@@ -2013,7 +2013,7 @@ function testJS_mediaSpecConstruction() {
2013
2013
 
2014
2014
  // Sample registry for testing
2015
2015
  const sampleRegistry = {
2016
- schemaVersion: '3.0',
2016
+ schemaVersion: '4.0',
2017
2017
  lastUpdated: '2026-02-07T16:48:28Z',
2018
2018
  cartridges: {
2019
2019
  pdfcartridge: {
@@ -2043,12 +2043,14 @@ const sampleRegistry = {
2043
2043
  releaseDate: '2026-02-07T16:40:28Z',
2044
2044
  changelog: ['Initial release'],
2045
2045
  minAppVersion: '1.0.0',
2046
- platform: 'darwin-arm64',
2047
- package: {
2048
- name: 'pdfcartridge-0.81.5325.pkg',
2049
- sha256: '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0',
2050
- size: 5187485
2051
- }
2046
+ builds: [{
2047
+ platform: 'darwin-arm64',
2048
+ package: {
2049
+ name: 'pdfcartridge-0.81.5325.pkg',
2050
+ sha256: '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0',
2051
+ size: 5187485
2052
+ }
2053
+ }]
2052
2054
  }
2053
2055
  }
2054
2056
  },
@@ -2074,12 +2076,14 @@ const sampleRegistry = {
2074
2076
  releaseDate: '2026-02-07T17:44:00Z',
2075
2077
  changelog: ['First version'],
2076
2078
  minAppVersion: '1.0.0',
2077
- platform: 'darwin-arm64',
2078
- package: {
2079
- name: 'txtcartridge-0.54.6408.pkg',
2080
- sha256: 'abc123',
2081
- size: 821000
2082
- }
2079
+ builds: [{
2080
+ platform: 'darwin-arm64',
2081
+ package: {
2082
+ name: 'txtcartridge-0.54.6408.pkg',
2083
+ sha256: 'abc123',
2084
+ size: 821000
2085
+ }
2086
+ }]
2083
2087
  }
2084
2088
  }
2085
2089
  }
@@ -2095,9 +2099,16 @@ function test320_cartridgeInfoConstruction() {
2095
2099
  description: 'A test',
2096
2100
  teamId: 'TEAM123',
2097
2101
  signedAt: '2026-01-01',
2098
- packageName: 'test-1.0.0.pkg',
2099
- packageSha256: 'abc123',
2100
- caps: [{urn: 'cap:in="media:void";op=test;out="media:void"', title: 'Test', description: ''}]
2102
+ caps: [{urn: 'cap:in="media:void";op=test;out="media:void"', title: 'Test', description: ''}],
2103
+ versions: {
2104
+ '1.0.0': {
2105
+ releaseDate: '2026-01-01',
2106
+ changelog: ['Initial'],
2107
+ minAppVersion: '1.0.0',
2108
+ builds: [{platform: 'darwin-arm64', package: {name: 'test-1.0.0.pkg', sha256: 'abc123', size: 100}}]
2109
+ }
2110
+ },
2111
+ availableVersions: ['1.0.0']
2101
2112
  };
2102
2113
  const cartridge = new CartridgeInfo(data);
2103
2114
  assert(cartridge.id === 'testcartridge', 'ID should match');
@@ -2118,28 +2129,50 @@ function test321_cartridgeInfoIsSigned() {
2118
2129
  assert(unsigned2.isSigned() === false, 'Cartridge without signedAt should not be signed');
2119
2130
  }
2120
2131
 
2121
- // TEST322: Cartridge info has package check
2122
- function test322_cartridgeInfoHasPackage() {
2123
- const withPkg = new CartridgeInfo({id: 'test', packageName: 'test.pkg', packageSha256: 'abc', caps: []});
2124
- assert(withPkg.hasPackage() === true, 'Cartridge with package info should return true');
2132
+ // TEST322: Cartridge info build for platform and available platforms
2133
+ function test322_cartridgeInfoBuildForPlatform() {
2134
+ const withBuilds = new CartridgeInfo({
2135
+ id: 'test', version: '1.0.0', caps: [],
2136
+ versions: {
2137
+ '1.0.0': {
2138
+ builds: [
2139
+ {platform: 'darwin-arm64', package: {name: 'test-darwin.pkg', sha256: 'abc', size: 100}},
2140
+ {platform: 'linux-x86_64', package: {name: 'test-linux.pkg', sha256: 'def', size: 200}}
2141
+ ]
2142
+ }
2143
+ },
2144
+ availableVersions: ['1.0.0']
2145
+ });
2146
+ const darwinBuild = withBuilds.buildForPlatform('darwin-arm64');
2147
+ assert(darwinBuild !== null, 'Should find darwin-arm64 build');
2148
+ assert(darwinBuild.package.name === 'test-darwin.pkg', 'Should have correct package name');
2149
+
2150
+ const linuxBuild = withBuilds.buildForPlatform('linux-x86_64');
2151
+ assert(linuxBuild !== null, 'Should find linux-x86_64 build');
2152
+
2153
+ const missingBuild = withBuilds.buildForPlatform('windows-x86_64');
2154
+ assert(missingBuild === null, 'Should return null for missing platform');
2125
2155
 
2126
- const noPkg1 = new CartridgeInfo({id: 'test', packageName: '', packageSha256: 'abc', caps: []});
2127
- assert(noPkg1.hasPackage() === false, 'Cartridge without packageName should return false');
2156
+ const platforms = withBuilds.availablePlatforms();
2157
+ assert(platforms.length === 2, 'Should have 2 platforms');
2158
+ assert(platforms.includes('darwin-arm64'), 'Should include darwin-arm64');
2159
+ assert(platforms.includes('linux-x86_64'), 'Should include linux-x86_64');
2128
2160
 
2129
- const noPkg2 = new CartridgeInfo({id: 'test', packageName: 'test.pkg', packageSha256: '', caps: []});
2130
- assert(noPkg2.hasPackage() === false, 'Cartridge without packageSha256 should return false');
2161
+ const noBuilds = new CartridgeInfo({id: 'test', version: '1.0.0', caps: [], versions: {}, availableVersions: []});
2162
+ assert(noBuilds.buildForPlatform('darwin-arm64') === null, 'Should return null when no versions');
2163
+ assert(noBuilds.availablePlatforms().length === 0, 'Should have no platforms');
2131
2164
  }
2132
2165
 
2133
2166
  // TEST323: CartridgeRepoServer validate registry
2134
2167
  function test323_cartridgeRepoServerValidateRegistry() {
2135
2168
  // Valid registry
2136
2169
  const server = new CartridgeRepoServer(sampleRegistry);
2137
- assert(server.registry.schemaVersion === '3.0', 'Should accept valid registry');
2170
+ assert(server.registry.schemaVersion === '4.0', 'Should accept valid registry');
2138
2171
 
2139
2172
  // Invalid schema version
2140
2173
  let threw = false;
2141
2174
  try {
2142
- new CartridgeRepoServer({schemaVersion: '2.0', cartridges: {}});
2175
+ new CartridgeRepoServer({schemaVersion: '3.0', cartridges: {}});
2143
2176
  } catch (e) {
2144
2177
  threw = true;
2145
2178
  assert(e.message.includes('schema version'), 'Should reject wrong schema version');
@@ -2149,7 +2182,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
2149
2182
  // Missing cartridges
2150
2183
  threw = false;
2151
2184
  try {
2152
- new CartridgeRepoServer({schemaVersion: '3.0'});
2185
+ new CartridgeRepoServer({schemaVersion: '4.0'});
2153
2186
  } catch (e) {
2154
2187
  threw = true;
2155
2188
  assert(e.message.includes('cartridges'), 'Should reject missing cartridges');
@@ -2170,8 +2203,14 @@ function test324_cartridgeRepoServerTransformToArray() {
2170
2203
  assert(pdf.version === '0.81.5325', 'Should have latest version');
2171
2204
  assert(pdf.teamId === 'P336JK947M', 'Should have teamId');
2172
2205
  assert(pdf.signedAt === '2026-02-07T16:40:28Z', 'Should have signedAt from releaseDate');
2173
- assert(pdf.packageName === 'pdfcartridge-0.81.5325.pkg', 'Should have package name');
2174
- assert(pdf.packageSha256 === '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0', 'Should have package SHA256');
2206
+ assert(pdf.versions !== undefined, 'Should have versions');
2207
+ assert(pdf.versions['0.81.5325'] !== undefined, 'Should have version data');
2208
+ assert(pdf.versions['0.81.5325'].builds.length === 1, 'Should have 1 build');
2209
+ assert(pdf.versions['0.81.5325'].builds[0].platform === 'darwin-arm64', 'Should have correct platform');
2210
+ assert(pdf.versions['0.81.5325'].builds[0].package.name === 'pdfcartridge-0.81.5325.pkg', 'Should have package name');
2211
+ assert(pdf.versions['0.81.5325'].builds[0].package.sha256 === '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0', 'Should have package SHA256');
2212
+ assert(Array.isArray(pdf.availableVersions), 'Should have availableVersions array');
2213
+ assert(pdf.availableVersions.includes('0.81.5325'), 'Should include latest version');
2175
2214
  assert(Array.isArray(pdf.caps), 'Should have caps array');
2176
2215
  assert(pdf.caps.length === 2, 'Should have 2 caps');
2177
2216
  }
@@ -2338,7 +2377,7 @@ function test335_cartridgeRepoServerClientIntegration() {
2338
2377
  const cartridge = client.getCartridge('pdfcartridge');
2339
2378
  assert(cartridge !== null, 'Client should find cartridge from server data');
2340
2379
  assert(cartridge.isSigned(), 'Cartridge should be signed');
2341
- assert(cartridge.hasPackage(), 'Cartridge should have package');
2380
+ assert(cartridge.buildForPlatform('darwin-arm64') !== null, 'Cartridge should have darwin-arm64 build');
2342
2381
 
2343
2382
  // Client can get suggestions
2344
2383
  const capUrn = 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"';
@@ -2359,7 +2398,7 @@ function test335_cartridgeRepoServerClientIntegration() {
2359
2398
  // TEST546: isImage returns true only when image marker tag is present
2360
2399
  function test546_isImage() {
2361
2400
  assert(MediaUrn.fromString(MEDIA_PNG).isImage(), 'MEDIA_PNG should be image');
2362
- assert(MediaUrn.fromString(MEDIA_IMAGE_THUMBNAIL).isImage(), 'MEDIA_IMAGE_THUMBNAIL should be image');
2401
+ assert(MediaUrn.fromString('media:image;png;thumbnail').isImage(), 'media:image;png;thumbnail should be image');
2363
2402
  assert(MediaUrn.fromString('media:image;jpg').isImage(), 'media:image;jpg should be image');
2364
2403
  // Non-image types
2365
2404
  assert(!MediaUrn.fromString(MEDIA_PDF).isImage(), 'MEDIA_PDF should not be image');
@@ -2393,8 +2432,8 @@ function test548_isVideo() {
2393
2432
  function test549_isNumeric() {
2394
2433
  assert(MediaUrn.fromString(MEDIA_INTEGER).isNumeric(), 'MEDIA_INTEGER should be numeric');
2395
2434
  assert(MediaUrn.fromString(MEDIA_NUMBER).isNumeric(), 'MEDIA_NUMBER should be numeric');
2396
- assert(MediaUrn.fromString(MEDIA_INTEGER_ARRAY).isNumeric(), 'MEDIA_INTEGER_ARRAY should be numeric');
2397
- assert(MediaUrn.fromString(MEDIA_NUMBER_ARRAY).isNumeric(), 'MEDIA_NUMBER_ARRAY should be numeric');
2435
+ assert(MediaUrn.fromString(MEDIA_INTEGER_LIST).isNumeric(), 'MEDIA_INTEGER_LIST should be numeric');
2436
+ assert(MediaUrn.fromString(MEDIA_NUMBER_LIST).isNumeric(), 'MEDIA_NUMBER_LIST should be numeric');
2398
2437
  // Non-numeric types
2399
2438
  assert(!MediaUrn.fromString(MEDIA_STRING).isNumeric(), 'MEDIA_STRING should not be numeric');
2400
2439
  assert(!MediaUrn.fromString(MEDIA_BOOLEAN).isNumeric(), 'MEDIA_BOOLEAN should not be numeric');
@@ -2404,9 +2443,9 @@ function test549_isNumeric() {
2404
2443
  // TEST550: isBool returns true only when bool marker tag is present
2405
2444
  function test550_isBool() {
2406
2445
  assert(MediaUrn.fromString(MEDIA_BOOLEAN).isBool(), 'MEDIA_BOOLEAN should be bool');
2407
- assert(MediaUrn.fromString(MEDIA_BOOLEAN_ARRAY).isBool(), 'MEDIA_BOOLEAN_ARRAY should be bool');
2408
- assert(MediaUrn.fromString(MEDIA_DECISION).isBool(), 'MEDIA_DECISION should be bool');
2409
- assert(MediaUrn.fromString(MEDIA_DECISION_ARRAY).isBool(), 'MEDIA_DECISION_ARRAY should be bool');
2446
+ assert(MediaUrn.fromString(MEDIA_BOOLEAN_LIST).isBool(), 'MEDIA_BOOLEAN_LIST should be bool');
2447
+ // MEDIA_DECISION is now a JSON record type (not bool)
2448
+ assert(!MediaUrn.fromString(MEDIA_DECISION).isBool(), 'MEDIA_DECISION should not be bool (it is a JSON record now)');
2410
2449
  // Non-bool types
2411
2450
  assert(!MediaUrn.fromString(MEDIA_STRING).isBool(), 'MEDIA_STRING should not be bool');
2412
2451
  assert(!MediaUrn.fromString(MEDIA_INTEGER).isBool(), 'MEDIA_INTEGER should not be bool');
@@ -2429,7 +2468,7 @@ function test552_isFilePathArray() {
2429
2468
  // Scalar file-path is NOT isFilePathArray
2430
2469
  assert(!MediaUrn.fromString(MEDIA_FILE_PATH).isFilePathArray(), 'MEDIA_FILE_PATH should not be isFilePathArray');
2431
2470
  // Non-file-path types
2432
- assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isFilePathArray(), 'MEDIA_STRING_ARRAY should not be file-path-array');
2471
+ assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isFilePathArray(), 'MEDIA_STRING_LIST should not be file-path-array');
2433
2472
  }
2434
2473
 
2435
2474
  // TEST553: isAnyFilePath returns true for both scalar and array file-path
@@ -2438,7 +2477,7 @@ function test553_isAnyFilePath() {
2438
2477
  assert(MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isAnyFilePath(), 'MEDIA_FILE_PATH_ARRAY should be any-file-path');
2439
2478
  // Non-file-path types
2440
2479
  assert(!MediaUrn.fromString(MEDIA_STRING).isAnyFilePath(), 'MEDIA_STRING should not be any-file-path');
2441
- assert(!MediaUrn.fromString(MEDIA_STRING_ARRAY).isAnyFilePath(), 'MEDIA_STRING_ARRAY should not be any-file-path');
2480
+ assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isAnyFilePath(), 'MEDIA_STRING_LIST should not be any-file-path');
2442
2481
  }
2443
2482
 
2444
2483
  // TEST554: isCollection returns true when collection marker tag is present
@@ -5369,8 +5408,8 @@ async function runTests() {
5369
5408
  runTest('TEST307: model_availability_urn', test307_modelAvailabilityUrn);
5370
5409
  runTest('TEST308: model_path_urn', test308_modelPathUrn);
5371
5410
  runTest('TEST309: model_availability_and_path_are_distinct', test309_modelAvailabilityAndPathAreDistinct);
5372
- runTest('TEST310: llm_conversation_urn_unconstrained', test310_llmConversationUrnUnconstrained);
5373
- runTest('TEST311: llm_conversation_urn_specs', test311_llmConversationUrnSpecs);
5411
+ runTest('TEST310: llm_generate_text_urn', test310_llmGenerateTextUrn);
5412
+ runTest('TEST311: llm_generate_text_urn_specs', test311_llmGenerateTextUrnSpecs);
5374
5413
  runTest('TEST312: all_urn_builders_produce_valid_urns', test312_allUrnBuildersProduceValidUrns);
5375
5414
 
5376
5415
  // JS-specific tests (no Rust number)
@@ -5395,7 +5434,7 @@ async function runTests() {
5395
5434
  console.log('\n--- cartridge_repo ---');
5396
5435
  runTest('TEST320: cartridge_info_construction', test320_cartridgeInfoConstruction);
5397
5436
  runTest('TEST321: cartridge_info_is_signed', test321_cartridgeInfoIsSigned);
5398
- runTest('TEST322: cartridge_info_has_package', test322_cartridgeInfoHasPackage);
5437
+ runTest('TEST322: cartridge_info_build_for_platform', test322_cartridgeInfoBuildForPlatform);
5399
5438
  runTest('TEST323: cartridge_repo_server_validate_registry', test323_cartridgeRepoServerValidateRegistry);
5400
5439
  runTest('TEST324: cartridge_repo_server_transform_to_array', test324_cartridgeRepoServerTransformToArray);
5401
5440
  runTest('TEST325: cartridge_repo_server_get_cartridges', test325_cartridgeRepoServerGetCartridges);
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.124.274"
43
+ "version": "0.127.280"
44
44
  }