capdag 1.470.4 → 1.474.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/capdag.js CHANGED
@@ -2062,9 +2062,10 @@ class MediaDef {
2062
2062
  * @param {Object|null} validation - Optional validation rules (min, max, min_length, max_length, pattern, allowed_values)
2063
2063
  * @param {Object|null} metadata - Optional metadata (arbitrary key-value pairs for display/categorization)
2064
2064
  * @param {string[]} extensions - File extensions for storing this media type (e.g., ['pdf'], ['jpg', 'jpeg'])
2065
- * @param {string|null} documentation - Optional long-form markdown documentation. Rendered in media info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the media def is useful.
2065
+ * @param {string|null} documentation - Optional long-form markdown documentation. Rendered in media info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the media def is useful. Only what is specific to this media def: what the dims it was built from mean is documented with each dim.
2066
+ * @param {DimRef[]} dims - The dims this media def was built from, and the value chosen from each, in the order its source references them. Each dim is published on its own (`FabricRegistryClient.getDim`); a reader shows its documentation beside `documentation`. Empty when built from no dim.
2066
2067
  */
2067
- constructor(contentType, profile = null, schema = null, title = null, description = null, mediaUrn = null, validation = null, metadata = null, extensions = [], documentation = null) {
2068
+ constructor(contentType, profile = null, schema = null, title = null, description = null, mediaUrn = null, validation = null, metadata = null, extensions = [], documentation = null, dims = []) {
2068
2069
  this.contentType = contentType;
2069
2070
  this.profile = profile;
2070
2071
  this.schema = schema;
@@ -2075,6 +2076,7 @@ class MediaDef {
2075
2076
  this.metadata = metadata;
2076
2077
  this.extensions = extensions;
2077
2078
  this.documentation = documentation;
2079
+ this.dims = dims;
2078
2080
  }
2079
2081
 
2080
2082
  /**
@@ -2224,7 +2226,7 @@ function resolveMediaUrn(mediaUrn, mediaDefs = []) {
2224
2226
  const def = mediaDefs.find(spec => spec.urn === mediaUrn);
2225
2227
 
2226
2228
  if (def) {
2227
- // Object form: { urn, media_type, title, profile_uri?, schema?, description?, documentation?, validation?, metadata?, extensions? }
2229
+ // Object form: { urn, media_type, title, profile_uri?, schema?, description?, documentation?, dims?, validation?, metadata?, extensions? }
2228
2230
  const mediaType = def.media_type || def.mediaType;
2229
2231
  const profileUri = def.profile_uri || def.profileUri || null;
2230
2232
  const schema = def.schema || null;
@@ -2240,6 +2242,9 @@ function resolveMediaUrn(mediaUrn, mediaDefs = []) {
2240
2242
  const validation = def.validation || null;
2241
2243
  const metadata = def.metadata || null;
2242
2244
  const extensions = Array.isArray(def.extensions) ? def.extensions : [];
2245
+ // The dims it was built from, each entry held to the dim reference
2246
+ // keys and each dim named once.
2247
+ const dims = dimRefsFromJSON(def.dims, `media def '${mediaUrn}'`);
2243
2248
 
2244
2249
  if (!mediaType) {
2245
2250
  throw new MediaDefError(
@@ -2248,7 +2253,7 @@ function resolveMediaUrn(mediaUrn, mediaDefs = []) {
2248
2253
  );
2249
2254
  }
2250
2255
 
2251
- return new MediaDef(mediaType, profileUri, schema, title, description, mediaUrn, validation, metadata, extensions, documentation);
2256
+ return new MediaDef(mediaType, profileUri, schema, title, description, mediaUrn, validation, metadata, extensions, documentation, dims);
2252
2257
  }
2253
2258
  }
2254
2259
 
@@ -3023,7 +3028,16 @@ class Cap {
3023
3028
  this.aliases = aliases;
3024
3029
  this.is_abstract = false;
3025
3030
  this.cap_description = capDescription;
3031
+ // Only what is specific to this cap: what the dims it was built from mean
3032
+ // is documented with each dim (see `dims`).
3026
3033
  this.documentation = documentation;
3034
+ // The dims this cap was built from, and the value chosen from each
3035
+ // (Array of DimRef), in the order its source references them. Each dim is
3036
+ // published on its own (`FabricRegistryClient.getDim`) with its
3037
+ // documentation and each value's; a reader shows that beside
3038
+ // `documentation`. Empty when the cap was built from no dim. Descriptive
3039
+ // only: matching and dispatch never read it.
3040
+ this.dims = [];
3027
3041
  this.metadata = metadata || {};
3028
3042
  this.args = []; // Array of CapArg - unified argument format
3029
3043
  // Every output the cap declares (Array of CapOutput). Exactly one is the
@@ -3317,6 +3331,7 @@ class Cap {
3317
3331
  this.is_abstract === other.is_abstract &&
3318
3332
  this.cap_description === other.cap_description &&
3319
3333
  this.documentation === other.documentation &&
3334
+ JSON.stringify(this.dims.map(d => d.toJSON())) === JSON.stringify(other.dims.map(d => d.toJSON())) &&
3320
3335
  JSON.stringify(this.metadata) === JSON.stringify(other.metadata) &&
3321
3336
  JSON.stringify(this.args.map(a => a.toJSON())) === JSON.stringify(other.args.map(a => a.toJSON())) &&
3322
3337
  JSON.stringify(this.outputs.map(o => o.toJSON())) === JSON.stringify(other.outputs.map(o => o.toJSON())) &&
@@ -3355,6 +3370,12 @@ class Cap {
3355
3370
  result.documentation = this.documentation;
3356
3371
  }
3357
3372
 
3373
+ // Emit `dims` only when the cap was built from some; absent means none,
3374
+ // exactly as the registry publishes it.
3375
+ if (this.dims.length > 0) {
3376
+ result.dims = this.dims.map(d => d.toJSON());
3377
+ }
3378
+
3358
3379
  if (this.metadata_json !== null && this.metadata_json !== undefined) {
3359
3380
  result.metadata_json = this.metadata_json;
3360
3381
  }
@@ -3393,6 +3414,9 @@ class Cap {
3393
3414
  const cap = new Cap(urn, json.title, json.aliases, json.cap_description, json.metadata, json.metadata_json, documentation);
3394
3415
  cap.is_abstract = json.abstract === true;
3395
3416
  cap.version = (typeof json.version === 'number' && json.version !== 0) ? json.version : 0;
3417
+ // A dim is named at most once: a definition cannot have been built with
3418
+ // two values of one dim.
3419
+ cap.dims = dimRefsFromJSON(json.dims, `cap '${urn}'`);
3396
3420
  // Parse args (new format)
3397
3421
  if (json.args && Array.isArray(json.args)) {
3398
3422
  cap.args = json.args.map(a => CapArg.fromJSON(a));
@@ -5564,6 +5588,23 @@ class CartridgeSuggestion {
5564
5588
  * `<registryUrl>:<channel>:<id>` so the same id can independently
5565
5589
  * coexist across multiple registries × both channels.
5566
5590
  */
5591
+ /**
5592
+ * The schema revision of the cartridge registry this build reads
5593
+ * (`schemas/cartridge-registry/v7.json`): its cap groups carry each
5594
+ * cartridge's cap definitions exactly as its manifest declares them,
5595
+ * including the dims each was built from.
5596
+ */
5597
+ const CARTRIDGE_REGISTRY_SCHEMA_VERSION = '7.0';
5598
+
5599
+ /**
5600
+ * The cartridge registry regime version this build speaks
5601
+ * (`schemas/cartridge-registry/registry-version.txt`), served at
5602
+ * `/v<version>/manifest`. v0 is the pre-versioning legacy at the bare
5603
+ * `/manifest` path; a registry of any other version is refused, never
5604
+ * reinterpreted.
5605
+ */
5606
+ const CARTRIDGE_REGISTRY_VERSION = 3;
5607
+
5567
5608
  class CartridgeRepoCache {
5568
5609
  constructor(repoUrl) {
5569
5610
  this.cartridges = new Map(); // "<registryUrl>:<channel>:<id>" -> CartridgeInfo
@@ -5587,7 +5628,8 @@ class CartridgeRepoClient {
5587
5628
  }
5588
5629
 
5589
5630
  /**
5590
- * Fetch a v5.0 channel-partitioned registry from a URL and flatten
5631
+ * Fetch a channel-partitioned registry (schema
5632
+ * CARTRIDGE_REGISTRY_SCHEMA_VERSION) from a URL and flatten
5591
5633
  * to a list of `CartridgeInfo`, one per `(channel, id)` pair.
5592
5634
  */
5593
5635
  async fetchRegistry(repoUrl) {
@@ -5603,14 +5645,13 @@ class CartridgeRepoClient {
5603
5645
  }
5604
5646
 
5605
5647
  const data = await response.json();
5606
- if (data.schemaVersion !== '5.0') {
5607
- throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
5648
+ if (data.schemaVersion !== CARTRIDGE_REGISTRY_SCHEMA_VERSION) {
5649
+ throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: ${CARTRIDGE_REGISTRY_SCHEMA_VERSION}`);
5608
5650
  }
5609
- // Cartridge registry regime version. v0 was the pre-versioning legacy at
5610
- // the bare /manifest path; this speaks only v1 at /v1/manifest. A manifest
5611
- // from a different regime version is rejected, not reinterpreted.
5612
- if (data.registryVersion !== 1) {
5613
- throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v1`);
5651
+ // Cartridge registry regime version (CARTRIDGE_REGISTRY_VERSION). A
5652
+ // manifest from a different regime version is rejected, not reinterpreted.
5653
+ if (data.registryVersion !== CARTRIDGE_REGISTRY_VERSION) {
5654
+ throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v${CARTRIDGE_REGISTRY_VERSION}`);
5614
5655
  }
5615
5656
  // A cartridge registry must declare the fabric its caps resolve against. The
5616
5657
  // client cross-checks this equals its own fabric (so every registry it uses
@@ -5877,7 +5918,8 @@ function _validChannel(c) {
5877
5918
  }
5878
5919
 
5879
5920
  /**
5880
- * Reads a v5.0 channel-partitioned cartridge registry. Both `release`
5921
+ * Reads a channel-partitioned cartridge registry (schema
5922
+ * CARTRIDGE_REGISTRY_SCHEMA_VERSION). Both `release`
5881
5923
  * and `nightly` channels are always present (possibly empty); every
5882
5924
  * `CartridgeInfo` returned carries the channel it came from so consumers
5883
5925
  * can render the release/nightly distinction without re-deriving.
@@ -5892,11 +5934,11 @@ class CartridgeRepoServer {
5892
5934
  if (!this.registry) {
5893
5935
  throw new Error('Registry is required');
5894
5936
  }
5895
- if (this.registry.schemaVersion !== '5.0') {
5896
- throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
5937
+ if (this.registry.schemaVersion !== CARTRIDGE_REGISTRY_SCHEMA_VERSION) {
5938
+ throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: ${CARTRIDGE_REGISTRY_SCHEMA_VERSION}`);
5897
5939
  }
5898
- if (this.registry.registryVersion !== 1) {
5899
- throw new Error(`Unsupported cartridge registry version: ${this.registry.registryVersion}. This build speaks v1.`);
5940
+ if (this.registry.registryVersion !== CARTRIDGE_REGISTRY_VERSION) {
5941
+ throw new Error(`Unsupported cartridge registry version: ${this.registry.registryVersion}. This build speaks v${CARTRIDGE_REGISTRY_VERSION}.`);
5900
5942
  }
5901
5943
  if (typeof this.registry.fabricRegistryUrl !== 'string' || this.registry.fabricRegistryUrl.length === 0) {
5902
5944
  throw new Error('Registry must declare a non-empty `fabricRegistryUrl` (the fabric its caps resolve against)');
@@ -11072,7 +11114,7 @@ Machine.prototype.toMermaid = function toMermaid(aliases = null) {
11072
11114
  };
11073
11115
 
11074
11116
  // ============================================================================
11075
- // The fabric registry: versioned definitions of caps, media and aliases
11117
+ // The fabric registry: versioned definitions of caps, media, aliases and dims
11076
11118
  // ============================================================================
11077
11119
 
11078
11120
  /**
@@ -11117,8 +11159,8 @@ const LOAD_ALL_AT_ONCE = 16;
11117
11159
 
11118
11160
  /**
11119
11161
  * Read a manifest as the registry serves it, refusing one that is not whole:
11120
- * `version` and `previous` are whole numbers, and `caps`, `media` and
11121
- * `aliases` map each name to a positive per-definition version.
11162
+ * `version` and `previous` are whole numbers, and `caps`, `media`, `aliases`
11163
+ * and `dims` map each name to a positive per-definition version.
11122
11164
  */
11123
11165
  function manifestFromJSON(obj, expectedVersion) {
11124
11166
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
@@ -11132,7 +11174,7 @@ function manifestFromJSON(obj, expectedVersion) {
11132
11174
  throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE,
11133
11175
  `manifest fetched as v${expectedVersion} reports version ${obj.version}`);
11134
11176
  }
11135
- for (const section of ['caps', 'media', 'aliases']) {
11177
+ for (const section of ['caps', 'media', 'aliases', 'dims']) {
11136
11178
  const held = obj[section];
11137
11179
  if (held === null || typeof held !== 'object' || Array.isArray(held)) {
11138
11180
  throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE, `manifest v${expectedVersion} has no '${section}' map`);
@@ -11144,7 +11186,7 @@ function manifestFromJSON(obj, expectedVersion) {
11144
11186
  }
11145
11187
  }
11146
11188
  }
11147
- return new Manifest(obj.version, obj.previous, { ...obj.caps }, { ...obj.media }, { ...obj.aliases });
11189
+ return new Manifest(obj.version, obj.previous, { ...obj.caps }, { ...obj.media }, { ...obj.aliases }, { ...obj.dims });
11148
11190
  }
11149
11191
 
11150
11192
  /**
@@ -11163,9 +11205,16 @@ function mediaDefFromStored(obj, expectedUrn, expectedVersion) {
11163
11205
  throw refused(`reports version ${JSON.stringify(obj.version)} but the manifest pins ${expectedVersion}`);
11164
11206
  }
11165
11207
  if (obj.extensions !== undefined && !Array.isArray(obj.extensions)) throw refused("has 'extensions' that are not a list");
11208
+ let dims;
11209
+ try {
11210
+ dims = dimRefsFromJSON(obj.dims, `media def '${expectedUrn}'`);
11211
+ } catch (error) {
11212
+ throw refused(`has dims this capdag cannot hold: ${error.message}`);
11213
+ }
11166
11214
  const def = new MediaDef(obj.media_type, obj.profile_uri || null, obj.schema === undefined ? null : obj.schema,
11167
11215
  obj.title, obj.description || null, expectedUrn, obj.validation || null, obj.metadata || null,
11168
- obj.extensions || [], typeof obj.documentation === 'string' && obj.documentation.length > 0 ? obj.documentation : null);
11216
+ obj.extensions || [], typeof obj.documentation === 'string' && obj.documentation.length > 0 ? obj.documentation : null,
11217
+ dims);
11169
11218
  def.version = obj.version;
11170
11219
  return def;
11171
11220
  }
@@ -11200,12 +11249,41 @@ function capFromStored(obj, expectedUrn, expectedVersion) {
11200
11249
  return cap;
11201
11250
  }
11202
11251
 
11252
+ /**
11253
+ * A dim body as a registry served it, for the dim `name` at `defver`: parsed
11254
+ * (unknown fields refused), named and versioned as requested, and holding
11255
+ * (`StoredDim.validate`).
11256
+ * @returns {StoredDim}
11257
+ */
11258
+ function checkedDim(obj, name, defver) {
11259
+ let dim;
11260
+ try {
11261
+ dim = StoredDim.fromJSON(obj);
11262
+ } catch (error) {
11263
+ throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE, `Failed to parse dim '${name}': ${error.message}`);
11264
+ }
11265
+ if (dim.name !== name) {
11266
+ throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE,
11267
+ `dim object name '${dim.name}' does not match requested name '${name}'`);
11268
+ }
11269
+ if (dim.version !== defver) {
11270
+ throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE,
11271
+ `dim '${dim.name}' object reports version ${dim.version} but manifest pins defver ${defver}`);
11272
+ }
11273
+ try {
11274
+ dim.validate();
11275
+ } catch (error) {
11276
+ throw new FabricRegistryError(FabricRegistryErrorCodes.VALIDATION, error.message);
11277
+ }
11278
+ return dim;
11279
+ }
11280
+
11203
11281
  /**
11204
11282
  * A client of the fabric registry, pinned to one manifest version.
11205
11283
  *
11206
- * The manifest names every cap URN, media URN and alias of its snapshot with
11207
- * the version of each definition; each definition is one object at
11208
- * `<base>/{caps,media,aliases}/<sha256 of its canonical name>/<version>.json`.
11284
+ * The manifest names every cap URN, media URN, alias and dim of its snapshot
11285
+ * with the version of each definition; each definition is one object at
11286
+ * `<base>/{caps,media,aliases,dims}/<sha256 of its canonical name>/<version>.json`.
11209
11287
  * Nothing outside the pinned manifest is fetched: a URN it does not name is
11210
11288
  * not found, never read from another snapshot.
11211
11289
  *
@@ -11214,7 +11292,10 @@ function capFromStored(obj, expectedUrn, expectedVersion) {
11214
11292
  * parsed against (`capDefinition`, `mediaDefinition`, `aliasTarget`) and the
11215
11293
  * alias source a machine is written with (`displayAliasForUrn`). A cap is
11216
11294
  * cached only once every media URN it names is: a cap whose media cannot be
11217
- * read is not held at all.
11295
+ * read is not held at all. A cap or media definition is likewise cached only
11296
+ * once every dim it names (`dims`) is, at the version the manifest pins,
11297
+ * with the value it names — so a reader holding a definition can always
11298
+ * read the documentation of the dims it was built from (`getCachedDim`).
11218
11299
  */
11219
11300
  class FabricRegistryClient {
11220
11301
  /**
@@ -11238,6 +11319,7 @@ class FabricRegistryClient {
11238
11319
  this._caps = new Map();
11239
11320
  this._media = new Map();
11240
11321
  this._aliases = new Map();
11322
+ this._dims = new Map();
11241
11323
  // A fetch under way, by what it fetches: one asked for again while it
11242
11324
  // runs is the same fetch.
11243
11325
  this._pending = new Map();
@@ -11325,6 +11407,7 @@ class FabricRegistryClient {
11325
11407
  const defver = await this._defver('media', canonical, `media definition '${canonical}'`);
11326
11408
  const url = `${this._baseUrl}/media/${await sha256Hex(canonical)}/${defver}.json`;
11327
11409
  const def = mediaDefFromStored(await this._get(url, `media definition '${canonical}'`), canonical, defver);
11410
+ await this._ensureDimsCached(`media def '${canonical}'`, def.dims);
11328
11411
  this._media.set(canonical, def);
11329
11412
  return def;
11330
11413
  });
@@ -11361,6 +11444,8 @@ class FabricRegistryClient {
11361
11444
  }
11362
11445
  for (const output of cap.getOutputs()) name(output.media_urn);
11363
11446
  for (const media of named) await this.getMediaDef(media);
11447
+ // Every dim the cap names, at its pinned version, with the value named.
11448
+ await this._ensureDimsCached(`cap '${canonical}'`, cap.dims);
11364
11449
  this._caps.set(canonical, cap);
11365
11450
  return cap;
11366
11451
  }
@@ -11400,6 +11485,88 @@ class FabricRegistryClient {
11400
11485
  return alias;
11401
11486
  }
11402
11487
 
11488
+ /**
11489
+ * A dim's definition — its title, documentation and values, each value
11490
+ * with its own — by name (held first, then the registry), at the version
11491
+ * the pinned manifest names.
11492
+ * @param {string} name
11493
+ * @returns {Promise<StoredDim>}
11494
+ */
11495
+ async getDim(name) {
11496
+ if (!isDimName(name)) {
11497
+ throw new FabricRegistryError(FabricRegistryErrorCodes.VALIDATION, `'${name}' is not a dim name`);
11498
+ }
11499
+ if (this._dims.has(name)) return this._dims.get(name);
11500
+ return this._once(`dim ${name}`, async () => this._fetchDim(name, await this._defver('dims', name, `dim '${name}'`)));
11501
+ }
11502
+
11503
+ async _fetchDim(name, defver) {
11504
+ const url = `${this._baseUrl}/dims/${await sha256Hex(name)}/${defver}.json`;
11505
+ const dim = checkedDim(await this._get(url, `dim '${name}'`), name, defver);
11506
+ this._dims.set(name, dim);
11507
+ return dim;
11508
+ }
11509
+
11510
+ /**
11511
+ * The held dim of that name, or null. Every dim a held cap or media
11512
+ * definition names is held with it.
11513
+ * @param {string} name
11514
+ * @returns {StoredDim|null}
11515
+ */
11516
+ getCachedDim(name) {
11517
+ return this._dims.get(name) || null;
11518
+ }
11519
+
11520
+ /**
11521
+ * The version the pinned manifest names for a dim, without fetching its
11522
+ * body.
11523
+ * @param {string} name
11524
+ * @returns {Promise<number>}
11525
+ */
11526
+ async dimDefverFor(name) {
11527
+ return this._defver('dims', name, `dim '${name}'`);
11528
+ }
11529
+
11530
+ /**
11531
+ * Insert a dim into the cache, keyed by its name: the seam a host or a
11532
+ * test seeds dims through without a registry behind it.
11533
+ * @param {StoredDim} dim
11534
+ */
11535
+ insertCachedDimForTest(dim) {
11536
+ this._dims.set(dim.name, dim);
11537
+ }
11538
+
11539
+ /**
11540
+ * Make sure every dim a definition names is held, fetching the ones that
11541
+ * are not at the version the manifest pins, and that each names a value
11542
+ * the dim declares. `owner` names the definition in errors. Nothing about
11543
+ * the definition itself is cached here: its fetcher caches it only once
11544
+ * this succeeds. That each dim is named once was held when the definition
11545
+ * was parsed.
11546
+ */
11547
+ async _ensureDimsCached(owner, dims) {
11548
+ for (const r of dims) {
11549
+ let dim = this._dims.get(r.dim);
11550
+ if (dim === undefined) {
11551
+ const defver = (await this.manifest()).dims[r.dim];
11552
+ if (defver === undefined) {
11553
+ throw new FabricRegistryError(FabricRegistryErrorCodes.NOT_FOUND,
11554
+ `${owner} names dim '${r.dim}', which is not in manifest v${this._manifestVersion}`);
11555
+ }
11556
+ try {
11557
+ dim = await this._once(`dim ${r.dim}`, () => this._fetchDim(r.dim, defver));
11558
+ } catch (error) {
11559
+ throw new FabricRegistryError(FabricRegistryErrorCodes.NOT_FOUND,
11560
+ `${owner} names dim '${r.dim}', which could not be fetched: ${error.message}`);
11561
+ }
11562
+ }
11563
+ if (dim.value(r.value) === null) {
11564
+ throw new FabricRegistryError(FabricRegistryErrorCodes.VALIDATION,
11565
+ `${owner} names value '${r.value}' of dim '${r.dim}', which the dim does not declare`);
11566
+ }
11567
+ }
11568
+ }
11569
+
11403
11570
  async _aliasTargetOfKind(name, kind) {
11404
11571
  const alias = await this.getAlias(name);
11405
11572
  if (classifyAliasTarget(alias.target) !== kind) {
@@ -11500,6 +11667,7 @@ class FabricRegistryClient {
11500
11667
  this._caps.clear();
11501
11668
  this._media.clear();
11502
11669
  this._aliases.clear();
11670
+ this._dims.clear();
11503
11671
  this._pending.clear();
11504
11672
  }
11505
11673
  }
@@ -11619,21 +11787,302 @@ class StoredAlias {
11619
11787
  }
11620
11788
  }
11621
11789
 
11790
+ // ===========================================================================
11791
+ // Dims (the axes definitions are built along)
11792
+ // ===========================================================================
11793
+ //
11794
+ // A dim (a language, a file type, a quantisation, …) is a fabric definition
11795
+ // like a cap, a media def or an alias: published on its own at
11796
+ // `dims/<sha256-of-name>/<defver>.json` and pinned by a manifest's `dims`
11797
+ // map. Its body says what the dim is and which values it may take, each with
11798
+ // its own documentation.
11799
+ //
11800
+ // A cap or media definition names the dims it was built from, and the value
11801
+ // chosen from each, in its `dims` (DimRef). Its own `documentation` is only
11802
+ // what is specific to it; what a dim and a value mean is read from the dim.
11803
+ // A reader shows both.
11804
+
11805
+ const DIM_NAME_RE = /^[a-z][a-z0-9-]*$/;
11806
+ /** The keys of an entry of a definition's `dims`. */
11807
+ const DIM_REF_KEYS = Object.freeze(['dim', 'value']);
11808
+ /** The keys of a tag a dim value contributes. */
11809
+ const DIM_TAG_KEYS = Object.freeze(['key', 'value']);
11810
+ /** The keys of a dim value (fabric/dim.schema.json). */
11811
+ const DIM_VALUE_KEYS = Object.freeze(['name', 'title', 'tags', 'documentation', 'media_type', 'extensions']);
11812
+ /** The keys of a dim body (fabric/dim.schema.json). */
11813
+ const DIM_KEYS = Object.freeze(['name', 'version', 'title', 'documentation', 'values']);
11814
+
11815
+ const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
11816
+
11817
+ /**
11818
+ * Whether `name` is a dim name: `^[a-z][a-z0-9-]*$`.
11819
+ * @param {string} name
11820
+ * @returns {boolean}
11821
+ */
11822
+ function isDimName(name) {
11823
+ return typeof name === 'string' && DIM_NAME_RE.test(name);
11824
+ }
11825
+
11826
+ /**
11827
+ * One dim a definition was built from, and the value chosen from it. The
11828
+ * entries of a definition's `dims`, in the order its source references them.
11829
+ *
11830
+ * Unknown fields are refused, as for arguments and outputs: a key this
11831
+ * library does not know would be dropped when the definition is written back
11832
+ * out, and the copy would no longer say what the registry said.
11833
+ */
11834
+ class DimRef {
11835
+ /**
11836
+ * @param {string} dim - The dim's name
11837
+ * @param {string} value - The name of the value chosen from that dim
11838
+ */
11839
+ constructor(dim, value) {
11840
+ this.dim = dim;
11841
+ this.value = value;
11842
+ }
11843
+
11844
+ toJSON() {
11845
+ return { dim: this.dim, value: this.value };
11846
+ }
11847
+
11848
+ /** @returns {DimRef} */
11849
+ static fromJSON(json) {
11850
+ if (!isPlainObject(json)) {
11851
+ throw new Error(`a dim reference is an object, not ${JSON.stringify(json)}`);
11852
+ }
11853
+ refuseUnknownDefinitionKeys(json, DIM_REF_KEYS, 'dim reference');
11854
+ for (const key of DIM_REF_KEYS) {
11855
+ if (typeof json[key] !== 'string') {
11856
+ throw new Error(`dim reference is missing field \`${key}\``);
11857
+ }
11858
+ }
11859
+ return new DimRef(json.dim, json.value);
11860
+ }
11861
+ }
11862
+
11863
+ /**
11864
+ * A definition's `dims` names each dim at most once, and only by dim and
11865
+ * value names. Returns why not, naming the offending entry, or null when it
11866
+ * holds.
11867
+ * @param {DimRef[]} dims
11868
+ * @returns {string|null}
11869
+ */
11870
+ function checkDimRefs(dims) {
11871
+ const seen = new Set();
11872
+ for (const r of dims) {
11873
+ if (!isDimName(r.dim)) return `'${r.dim}' is not a dim name`;
11874
+ if (typeof r.value !== 'string' || r.value.length === 0) return `dim '${r.dim}' is named without a value`;
11875
+ if (seen.has(r.dim)) return `dim '${r.dim}' is named more than once`;
11876
+ seen.add(r.dim);
11877
+ }
11878
+ return null;
11879
+ }
11880
+
11881
+ /**
11882
+ * A definition's `dims` as its JSON carries them: absent is none; otherwise
11883
+ * a list of dim references, each dim named once. `what` names the definition
11884
+ * in the error.
11885
+ * @returns {DimRef[]}
11886
+ */
11887
+ function dimRefsFromJSON(json, what) {
11888
+ if (json === undefined) return [];
11889
+ if (!Array.isArray(json)) {
11890
+ throw new Error(`${what}: 'dims' is a list of dim references`);
11891
+ }
11892
+ const dims = json.map(d => DimRef.fromJSON(d));
11893
+ const wrong = checkDimRefs(dims);
11894
+ if (wrong !== null) throw new Error(`${what} dims: ${wrong}`);
11895
+ return dims;
11896
+ }
11897
+
11898
+ /** A tag a dim value contributes to the URN of every definition built with it. `value` is `*` for a marker tag. */
11899
+ class DimTag {
11900
+ constructor(key, value) {
11901
+ this.key = key;
11902
+ this.value = value;
11903
+ }
11904
+
11905
+ toJSON() {
11906
+ return { key: this.key, value: this.value };
11907
+ }
11908
+
11909
+ /** @returns {DimTag} */
11910
+ static fromJSON(json) {
11911
+ if (!isPlainObject(json)) throw new Error(`a dim tag is an object, not ${JSON.stringify(json)}`);
11912
+ refuseUnknownDefinitionKeys(json, DIM_TAG_KEYS, 'dim tag');
11913
+ for (const key of DIM_TAG_KEYS) {
11914
+ if (typeof json[key] !== 'string') throw new Error(`dim tag is missing field \`${key}\``);
11915
+ }
11916
+ return new DimTag(json.key, json.value);
11917
+ }
11918
+ }
11919
+
11920
+ /** One value a dim may take. */
11921
+ class StoredDimValue {
11922
+ /**
11923
+ * @param {string} name - The value's name, unique within the dim
11924
+ * @param {string} title - Display-friendly title of the value
11925
+ * @param {DimTag[]} tags - The tags the value contributes; at least one
11926
+ * @param {string|null} documentation - Markdown documentation of what choosing this value means
11927
+ * @param {string|null} mediaType - The MIME media type the value gives the media defs built with it
11928
+ * @param {string[]} extensions - The file extensions the value gives the media defs built with it
11929
+ */
11930
+ constructor(name, title, tags, documentation = null, mediaType = null, extensions = []) {
11931
+ this.name = name;
11932
+ this.title = title;
11933
+ this.tags = tags;
11934
+ this.documentation = documentation;
11935
+ this.media_type = mediaType;
11936
+ this.extensions = extensions;
11937
+ }
11938
+
11939
+ toJSON() {
11940
+ const result = { name: this.name, title: this.title, tags: this.tags.map(t => t.toJSON()) };
11941
+ if (this.documentation !== null) result.documentation = this.documentation;
11942
+ if (this.media_type !== null) result.media_type = this.media_type;
11943
+ if (this.extensions.length > 0) result.extensions = this.extensions;
11944
+ return result;
11945
+ }
11946
+
11947
+ /** @returns {StoredDimValue} */
11948
+ static fromJSON(json) {
11949
+ if (!isPlainObject(json)) throw new Error(`a dim value is an object, not ${JSON.stringify(json)}`);
11950
+ refuseUnknownDefinitionKeys(json, DIM_VALUE_KEYS, 'dim value');
11951
+ for (const key of ['name', 'title']) {
11952
+ if (typeof json[key] !== 'string') throw new Error(`dim value is missing field \`${key}\``);
11953
+ }
11954
+ if (!Array.isArray(json.tags)) throw new Error(`dim value '${json.name}' is missing field \`tags\``);
11955
+ for (const key of ['documentation', 'media_type']) {
11956
+ if (json[key] !== undefined && typeof json[key] !== 'string') {
11957
+ throw new Error(`dim value '${json.name}': '${key}' is a string`);
11958
+ }
11959
+ }
11960
+ if (json.extensions !== undefined && !(Array.isArray(json.extensions) && json.extensions.every(e => typeof e === 'string'))) {
11961
+ throw new Error(`dim value '${json.name}': 'extensions' is a list of strings`);
11962
+ }
11963
+ return new StoredDimValue(json.name, json.title, json.tags.map(t => DimTag.fromJSON(t)),
11964
+ json.documentation === undefined ? null : json.documentation,
11965
+ json.media_type === undefined ? null : json.media_type,
11966
+ json.extensions === undefined ? [] : [...json.extensions]);
11967
+ }
11968
+ }
11969
+
11970
+ const DimErrorCodes = Object.freeze({
11971
+ INVALID_NAME: 'InvalidName',
11972
+ NO_VALUES: 'NoValues',
11973
+ DUPLICATE_VALUE: 'DuplicateValue',
11974
+ VALUE_WITHOUT_TAGS: 'ValueWithoutTags',
11975
+ });
11976
+
11977
+ /** Why a dim body is not one this library can hold. */
11978
+ class DimError extends Error {
11979
+ constructor(code, message) {
11980
+ super(message);
11981
+ this.name = 'DimError';
11982
+ this.code = code;
11983
+ }
11984
+ }
11985
+
11986
+ /**
11987
+ * Stored dim definition. Mirrors fabric/dim.schema.json on the wire and is
11988
+ * the body published at `dims/<sha256-of-name>/<defver>.json`.
11989
+ */
11990
+ class StoredDim {
11991
+ /**
11992
+ * @param {string} name - The dim's name, unique across the snapshot
11993
+ * @param {number} version - Per-definition version; dims exist only in the versioned regime
11994
+ * @param {string} title - Display-friendly title of the dim
11995
+ * @param {string} documentation - Markdown documentation of what the dim is
11996
+ * @param {StoredDimValue[]} values - The values the dim may take, in the order the dim declares them
11997
+ */
11998
+ constructor(name, version, title, documentation, values) {
11999
+ this.name = name;
12000
+ this.version = version;
12001
+ this.title = title;
12002
+ this.documentation = documentation;
12003
+ this.values = values;
12004
+ }
12005
+
12006
+ /**
12007
+ * The value named `name`, if the dim declares it.
12008
+ * @returns {StoredDimValue|null}
12009
+ */
12010
+ value(name) {
12011
+ return this.values.find(v => v.name === name) || null;
12012
+ }
12013
+
12014
+ /**
12015
+ * Hold the body to what dim.schema.json says beyond its field types: a
12016
+ * dim name, at least one value, value names unique, every value with a tag.
12017
+ * @throws {DimError}
12018
+ */
12019
+ validate() {
12020
+ if (!isDimName(this.name)) {
12021
+ throw new DimError(DimErrorCodes.INVALID_NAME,
12022
+ `dim name '${this.name}' is not a dim name (lowercase letters, digits and '-', starting with a letter)`);
12023
+ }
12024
+ if (this.values.length === 0) {
12025
+ throw new DimError(DimErrorCodes.NO_VALUES, `dim '${this.name}' declares no values`);
12026
+ }
12027
+ const seen = new Set();
12028
+ for (const v of this.values) {
12029
+ if (seen.has(v.name)) {
12030
+ throw new DimError(DimErrorCodes.DUPLICATE_VALUE, `dim '${this.name}' declares value '${v.name}' more than once`);
12031
+ }
12032
+ seen.add(v.name);
12033
+ if (v.tags.length === 0) {
12034
+ throw new DimError(DimErrorCodes.VALUE_WITHOUT_TAGS, `dim '${this.name}' value '${v.name}' contributes no tag`);
12035
+ }
12036
+ }
12037
+ }
12038
+
12039
+ toJSON() {
12040
+ return {
12041
+ name: this.name,
12042
+ version: this.version,
12043
+ title: this.title,
12044
+ documentation: this.documentation,
12045
+ values: this.values.map(v => v.toJSON()),
12046
+ };
12047
+ }
12048
+
12049
+ /**
12050
+ * Read a dim body, refusing a field this library does not know.
12051
+ * @returns {StoredDim}
12052
+ */
12053
+ static fromJSON(json) {
12054
+ if (!isPlainObject(json)) throw new Error(`a dim is an object, not ${JSON.stringify(json)}`);
12055
+ refuseUnknownDefinitionKeys(json, DIM_KEYS, 'dim');
12056
+ for (const key of ['name', 'title', 'documentation']) {
12057
+ if (typeof json[key] !== 'string') throw new Error(`dim is missing field \`${key}\``);
12058
+ }
12059
+ if (!Number.isInteger(json.version) || json.version < 0) {
12060
+ throw new Error(`dim '${json.name}' has no whole \`version\``);
12061
+ }
12062
+ if (!Array.isArray(json.values)) throw new Error(`dim '${json.name}' is missing field \`values\``);
12063
+ return new StoredDim(json.name, json.version, json.title, json.documentation,
12064
+ json.values.map(v => StoredDimValue.fromJSON(v)));
12065
+ }
12066
+ }
12067
+
11622
12068
  /**
11623
12069
  * A versioned registry snapshot. Mirrors fabric/manifest.schema.json:
11624
- * { version, previous, caps, media, aliases } where each map is
11625
- * name/urn -> defver.
12070
+ * { version, previous, caps, media, aliases, dims } where each map is
12071
+ * name/urn -> defver. `dims` is required: every dim a cap or media def of
12072
+ * the snapshot names is in it, and a manifest without it is not one this
12073
+ * library reads.
11626
12074
  */
11627
12075
  class Manifest {
11628
- constructor(version, previous, caps = {}, media = {}, aliases = {}) {
12076
+ constructor(version, previous, caps = {}, media = {}, aliases = {}, dims = {}) {
11629
12077
  this.version = version;
11630
12078
  this.previous = previous;
11631
12079
  this.caps = caps;
11632
12080
  this.media = media;
11633
12081
  this.aliases = aliases;
12082
+ this.dims = dims;
11634
12083
  }
11635
12084
  static empty(version) {
11636
- return new Manifest(version, Math.max(0, version - 1), {}, {}, {});
12085
+ return new Manifest(version, Math.max(0, version - 1), {}, {}, {}, {});
11637
12086
  }
11638
12087
  toJSON() {
11639
12088
  return {
@@ -11642,15 +12091,20 @@ class Manifest {
11642
12091
  caps: this.caps,
11643
12092
  media: this.media,
11644
12093
  aliases: this.aliases,
12094
+ dims: this.dims,
11645
12095
  };
11646
12096
  }
11647
12097
  static fromJSON(obj) {
12098
+ if (!isPlainObject(obj.dims)) {
12099
+ throw new Error(`manifest v${obj.version} has no 'dims' map`);
12100
+ }
11648
12101
  return new Manifest(
11649
12102
  obj.version,
11650
12103
  obj.previous,
11651
12104
  obj.caps || {},
11652
12105
  obj.media || {},
11653
12106
  obj.aliases || {},
12107
+ { ...obj.dims },
11654
12108
  );
11655
12109
  }
11656
12110
  }
@@ -11680,6 +12134,15 @@ export {
11680
12134
  selectDisplayAlias,
11681
12135
  StoredAlias,
11682
12136
  Manifest,
12137
+ // Dims
12138
+ isDimName,
12139
+ checkDimRefs,
12140
+ DimRef,
12141
+ DimTag,
12142
+ StoredDimValue,
12143
+ StoredDim,
12144
+ DimError,
12145
+ DimErrorCodes,
11683
12146
  CapUrn,
11684
12147
  CapKind,
11685
12148
  CapEffect,