capdag 0.152.345 → 0.157.363

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
@@ -3142,13 +3142,13 @@ class CapArgumentValue {
3142
3142
 
3143
3143
 
3144
3144
  // ============================================================================
3145
- // CAP GRAPH - Directed graph of capability conversions
3145
+ // CAPFAB - Directed graph of capability conversions
3146
3146
  // ============================================================================
3147
3147
 
3148
3148
  /**
3149
3149
  * An edge in the capability graph representing a conversion from one media URN to another.
3150
3150
  */
3151
- class CapGraphEdge {
3151
+ class CapFabEdge {
3152
3152
  /**
3153
3153
  * @param {string} fromUrn - The input media URN
3154
3154
  * @param {string} toUrn - The output media URN
@@ -3168,7 +3168,7 @@ class CapGraphEdge {
3168
3168
  /**
3169
3169
  * Statistics about a capability graph.
3170
3170
  */
3171
- class CapGraphStats {
3171
+ class CapFabStats {
3172
3172
  /**
3173
3173
  * @param {number} nodeCount - Number of unique media URN nodes
3174
3174
  * @param {number} edgeCount - Number of edges (capabilities)
@@ -3187,7 +3187,7 @@ class CapGraphStats {
3187
3187
  * A directed graph where nodes are media URNs and edges are capabilities.
3188
3188
  * This graph enables discovering conversion paths between different media formats.
3189
3189
  */
3190
- class CapGraph {
3190
+ class CapFab {
3191
3191
  constructor() {
3192
3192
  this.edges = [];
3193
3193
  this.outgoing = new Map(); // fromUrn -> edge indices
@@ -3211,7 +3211,7 @@ class CapGraph {
3211
3211
 
3212
3212
  // Create edge
3213
3213
  const edgeIndex = this.edges.length;
3214
- const edge = new CapGraphEdge(fromUrn, toUrn, cap, registryName, specificity);
3214
+ const edge = new CapFabEdge(fromUrn, toUrn, cap, registryName, specificity);
3215
3215
  this.edges.push(edge);
3216
3216
 
3217
3217
  // Update outgoing index
@@ -3237,7 +3237,7 @@ class CapGraph {
3237
3237
 
3238
3238
  /**
3239
3239
  * Get all edges in the graph.
3240
- * @returns {CapGraphEdge[]}
3240
+ * @returns {CapFabEdge[]}
3241
3241
  */
3242
3242
  getEdges() {
3243
3243
  return [...this.edges];
@@ -3247,7 +3247,7 @@ class CapGraph {
3247
3247
  * Get all edges where the provided URN satisfies the edge's input requirement.
3248
3248
  * Uses conformsTo-based matching instead of exact string matching.
3249
3249
  * @param {string} urn - The media URN
3250
- * @returns {CapGraphEdge[]}
3250
+ * @returns {CapFabEdge[]}
3251
3251
  */
3252
3252
  getOutgoing(urn) {
3253
3253
  // Use TaggedUrn matching: find all edges where the provided URN (instance)
@@ -3268,7 +3268,7 @@ class CapGraph {
3268
3268
  /**
3269
3269
  * Get all edges targeting a media URN.
3270
3270
  * @param {string} urn - The media URN
3271
- * @returns {CapGraphEdge[]}
3271
+ * @returns {CapFabEdge[]}
3272
3272
  */
3273
3273
  getIncoming(urn) {
3274
3274
  const indices = this.incoming.get(urn) || [];
@@ -3289,7 +3289,7 @@ class CapGraph {
3289
3289
  * Get all direct edges from one URN to another, sorted by specificity (highest first).
3290
3290
  * @param {string} fromUrn - The source media URN
3291
3291
  * @param {string} toUrn - The target media URN
3292
- * @returns {CapGraphEdge[]}
3292
+ * @returns {CapFabEdge[]}
3293
3293
  */
3294
3294
  getDirectEdges(fromUrn, toUrn) {
3295
3295
  const edges = this.getOutgoing(fromUrn).filter(edge => edge.toUrn === toUrn);
@@ -3338,7 +3338,7 @@ class CapGraph {
3338
3338
  * Find the shortest conversion path from one URN to another.
3339
3339
  * @param {string} fromUrn - The source media URN
3340
3340
  * @param {string} toUrn - The target media URN
3341
- * @returns {CapGraphEdge[]|null} Array of edges representing the path, or null if no path exists
3341
+ * @returns {CapFabEdge[]|null} Array of edges representing the path, or null if no path exists
3342
3342
  */
3343
3343
  findPath(fromUrn, toUrn) {
3344
3344
  if (fromUrn === toUrn) {
@@ -3393,7 +3393,7 @@ class CapGraph {
3393
3393
  * @param {string} fromUrn - The source media URN
3394
3394
  * @param {string} toUrn - The target media URN
3395
3395
  * @param {number} maxDepth - Maximum path length to search
3396
- * @returns {CapGraphEdge[][]} Array of paths (each path is an array of edges)
3396
+ * @returns {CapFabEdge[][]} Array of paths (each path is an array of edges)
3397
3397
  */
3398
3398
  findAllPaths(fromUrn, toUrn, maxDepth) {
3399
3399
  if (!this.nodes.has(fromUrn) || !this.nodes.has(toUrn)) {
@@ -3447,7 +3447,7 @@ class CapGraph {
3447
3447
  * @param {string} fromUrn - The source media URN
3448
3448
  * @param {string} toUrn - The target media URN
3449
3449
  * @param {number} maxDepth - Maximum path length to search
3450
- * @returns {CapGraphEdge[]|null} Array of edges representing the best path, or null if no path exists
3450
+ * @returns {CapFabEdge[]|null} Array of edges representing the best path, or null if no path exists
3451
3451
  */
3452
3452
  findBestPath(fromUrn, toUrn, maxDepth) {
3453
3453
  const allPaths = this.findAllPaths(fromUrn, toUrn, maxDepth);
@@ -3488,10 +3488,10 @@ class CapGraph {
3488
3488
 
3489
3489
  /**
3490
3490
  * Get statistics about the graph.
3491
- * @returns {CapGraphStats}
3491
+ * @returns {CapFabStats}
3492
3492
  */
3493
3493
  stats() {
3494
- return new CapGraphStats(
3494
+ return new CapFabStats(
3495
3495
  this.nodes.size,
3496
3496
  this.edges.length,
3497
3497
  this.outgoing.size,
@@ -3613,6 +3613,54 @@ class CartridgeCapGroup {
3613
3613
  }
3614
3614
  }
3615
3615
 
3616
+ // =============================================================================
3617
+ // Cartridge registry slug
3618
+ // =============================================================================
3619
+ //
3620
+ // Deterministic mapping from a registry URL to a top-level folder
3621
+ // name under the cartridges install root. Mirrors
3622
+ // capdag::cartridge_slug byte-for-byte: SHA-256 of the URL bytes,
3623
+ // lowercase hex, first 16 chars. The literal string "dev" is
3624
+ // reserved for dev cartridges that have no registry.
3625
+ //
3626
+ // JS uses Web Crypto's SubtleCrypto for SHA-256. The function is
3627
+ // async because `crypto.subtle.digest` returns a Promise; consumers
3628
+ // in synchronous contexts must await.
3629
+
3630
+ const DEV_SLUG = "dev";
3631
+ const SLUG_HEX_LEN = 16;
3632
+
3633
+ /**
3634
+ * Compute the on-disk slug for a registry URL.
3635
+ *
3636
+ * @param {string|null|undefined} registryUrl - The registry URL, or
3637
+ * null/undefined for dev installs.
3638
+ * @returns {Promise<string>} The slug — `DEV_SLUG` for null,
3639
+ * otherwise the first 16 lowercase-hex characters of
3640
+ * sha256(registryUrl).
3641
+ */
3642
+ async function slugForRegistryUrl(registryUrl) {
3643
+ if (registryUrl === null || registryUrl === undefined) {
3644
+ return DEV_SLUG;
3645
+ }
3646
+ const bytes = new TextEncoder().encode(registryUrl);
3647
+ // Web Crypto exposes SHA-256 via crypto.subtle. Node 16+ exposes
3648
+ // it through globalThis.crypto.subtle.
3649
+ const digestBuffer = await crypto.subtle.digest("SHA-256", bytes);
3650
+ const digestBytes = new Uint8Array(digestBuffer);
3651
+ let hex = "";
3652
+ for (const b of digestBytes) {
3653
+ hex += b.toString(16).padStart(2, "0");
3654
+ }
3655
+ return hex.slice(0, SLUG_HEX_LEN);
3656
+ }
3657
+
3658
+ function isRegistrySlug(s) {
3659
+ return typeof s === "string"
3660
+ && s.length === SLUG_HEX_LEN
3661
+ && /^[0-9a-f]+$/.test(s);
3662
+ }
3663
+
3616
3664
  /**
3617
3665
  * Cartridge information from registry
3618
3666
  */
@@ -3640,6 +3688,16 @@ class CartridgeInfo {
3640
3688
  throw new Error(`CartridgeInfo ${data.id || '?'}: invalid or missing channel '${data.channel}'`);
3641
3689
  }
3642
3690
  this.channel = data.channel;
3691
+ // Registry URL: verbatim string the registry was fetched from.
3692
+ // Required and non-empty — every CartridgeInfo carries the URL of
3693
+ // the registry that served it so downstream consumers can build
3694
+ // the (registryUrl, channel, id) identity tuple without
3695
+ // re-deriving it. The registry transformer stamps this onto every
3696
+ // entry at flatten time.
3697
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3698
+ throw new Error(`CartridgeInfo ${data.id || '?'}: registryUrl is required and must be a non-empty string`);
3699
+ }
3700
+ this.registryUrl = data.registryUrl;
3643
3701
  }
3644
3702
 
3645
3703
  /** All caps flattened across all cap_groups, deduplicated by URN */
@@ -3688,9 +3746,13 @@ class CartridgeInfo {
3688
3746
  }
3689
3747
 
3690
3748
  /**
3691
- * Cartridge suggestion for a missing cap. `channel` reports which
3692
- * channel the suggesting cartridge lives in so consumers can render
3693
- * the release/nightly distinction.
3749
+ * Cartridge suggestion for a missing cap.
3750
+ *
3751
+ * `(registryUrl, channel, cartridgeId)` is the suggesting
3752
+ * cartridge's full identity — installs of the same id from
3753
+ * different registries × channels are independent records and the
3754
+ * client keeps both visible. `registryUrl` is required and
3755
+ * non-empty; suggestions never come from dev installs.
3694
3756
  */
3695
3757
  class CartridgeSuggestion {
3696
3758
  constructor(data) {
@@ -3706,25 +3768,29 @@ class CartridgeSuggestion {
3706
3768
  throw new Error(`CartridgeSuggestion: invalid or missing channel '${data.channel}'`);
3707
3769
  }
3708
3770
  this.channel = data.channel;
3771
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3772
+ throw new Error("CartridgeSuggestion: registryUrl is required and must be a non-empty string");
3773
+ }
3774
+ this.registryUrl = data.registryUrl;
3709
3775
  }
3710
3776
  }
3711
3777
 
3712
3778
  /**
3713
3779
  * Cartridge registry cache entry. The cartridges map is keyed by
3714
- * `<channel>:<id>` so the same id can independently coexist in both
3715
- * channels.
3780
+ * `<registryUrl>:<channel>:<id>` so the same id can independently
3781
+ * coexist across multiple registries × both channels.
3716
3782
  */
3717
3783
  class CartridgeRepoCache {
3718
3784
  constructor(repoUrl) {
3719
- this.cartridges = new Map(); // "<channel>:<id>" -> CartridgeInfo
3720
- this.capToCartridges = new Map(); // cap_urn -> [{channel, id}]
3785
+ this.cartridges = new Map(); // "<registryUrl>:<channel>:<id>" -> CartridgeInfo
3786
+ this.capToCartridges = new Map(); // cap_urn -> [{registryUrl, channel, id}]
3721
3787
  this.lastUpdated = Date.now();
3722
3788
  this.repoUrl = repoUrl;
3723
3789
  }
3724
3790
  }
3725
3791
 
3726
- function _cacheKey(channel, id) {
3727
- return `${channel}:${id}`;
3792
+ function _cacheKey(registryUrl, channel, id) {
3793
+ return `${registryUrl}:${channel}:${id}`;
3728
3794
  }
3729
3795
 
3730
3796
  /**
@@ -3756,6 +3822,21 @@ class CartridgeRepoClient {
3756
3822
  if (data.schemaVersion !== '5.0') {
3757
3823
  throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
3758
3824
  }
3825
+ // Self-referential check: the manifest declares its own URL via
3826
+ // `registryUrl`. It must match the URL we just fetched from
3827
+ // byte-for-byte — a mismatch is a manifest-corruption signal
3828
+ // (publisher wrote the wrong URL, or manifest is being served
3829
+ // from an unexpected mirror). Identity downstream depends on
3830
+ // this string; refuse to ingest on mismatch.
3831
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3832
+ throw new Error(`Cartridge registry from ${repoUrl}: missing required top-level 'registryUrl' field`);
3833
+ }
3834
+ if (data.registryUrl !== repoUrl) {
3835
+ throw new Error(
3836
+ `Cartridge registry from ${repoUrl}: declared registryUrl '${data.registryUrl}' ` +
3837
+ `does not match the URL it was fetched from. These must match byte-for-byte.`
3838
+ );
3839
+ }
3759
3840
  if (!data.channels || typeof data.channels !== 'object') {
3760
3841
  throw new Error(`Cartridge registry from ${repoUrl}: missing channels object`);
3761
3842
  }
@@ -3771,7 +3852,12 @@ class CartridgeRepoClient {
3771
3852
  ...c,
3772
3853
  id,
3773
3854
  version: c.latestVersion,
3774
- channel
3855
+ channel,
3856
+ // Stamp registryUrl onto every entry — verbatim from the
3857
+ // registry self-reference (which we just verified equals
3858
+ // the fetched URL). Identity comparison downstream is
3859
+ // byte equality.
3860
+ registryUrl: data.registryUrl
3775
3861
  }));
3776
3862
  }
3777
3863
  }
@@ -3794,7 +3880,10 @@ class CartridgeRepoClient {
3794
3880
  const cache = new CartridgeRepoCache(repoUrl);
3795
3881
 
3796
3882
  for (const cartridge of cartridges) {
3797
- cache.cartridges.set(_cacheKey(cartridge.channel, cartridge.id), cartridge);
3883
+ cache.cartridges.set(
3884
+ _cacheKey(cartridge.registryUrl, cartridge.channel, cartridge.id),
3885
+ cartridge
3886
+ );
3798
3887
 
3799
3888
  for (const cap of cartridge.allCaps()) {
3800
3889
  const normalized = CapUrn.fromString(cap.urn).toString();
@@ -3802,6 +3891,7 @@ class CartridgeRepoClient {
3802
3891
  cache.capToCartridges.set(normalized, []);
3803
3892
  }
3804
3893
  cache.capToCartridges.get(normalized).push({
3894
+ registryUrl: cartridge.registryUrl,
3805
3895
  channel: cartridge.channel,
3806
3896
  id: cartridge.id
3807
3897
  });
@@ -3866,7 +3956,7 @@ class CartridgeRepoClient {
3866
3956
  if (!refs) continue;
3867
3957
 
3868
3958
  for (const ref of refs) {
3869
- const cartridge = cache.cartridges.get(_cacheKey(ref.channel, ref.id));
3959
+ const cartridge = cache.cartridges.get(_cacheKey(ref.registryUrl, ref.channel, ref.id));
3870
3960
  if (!cartridge) continue;
3871
3961
 
3872
3962
  const capInfo = cartridge.allCaps().find(c => {
@@ -3891,7 +3981,8 @@ class CartridgeRepoClient {
3891
3981
  latestVersion: cartridge.version,
3892
3982
  repoUrl: cache.repoUrl,
3893
3983
  pageUrl: pageUrl,
3894
- channel: cartridge.channel
3984
+ channel: cartridge.channel,
3985
+ registryUrl: cartridge.registryUrl
3895
3986
  }));
3896
3987
  }
3897
3988
  }
@@ -3928,16 +4019,23 @@ class CartridgeRepoClient {
3928
4019
  }
3929
4020
 
3930
4021
  /**
3931
- * Get cartridge info by `(channel, id)`. Channel is required —
3932
- * the same id can independently exist in both channels with
3933
- * different metadata. Returns `null` when not found.
4022
+ * Get cartridge info by `(registryUrl, channel, id)`. All three
4023
+ * are required — the same id can independently exist across
4024
+ * multiple registries × both channels with different metadata.
4025
+ * Returns `null` when not found. `registryUrl` is the verbatim
4026
+ * URL the cache was indexed under.
3934
4027
  */
3935
- getCartridge(channel, cartridgeId) {
4028
+ getCartridge(registryUrl, channel, cartridgeId) {
4029
+ if (typeof registryUrl !== 'string' || registryUrl.length === 0) {
4030
+ throw new Error('getCartridge: registryUrl must be a non-empty string');
4031
+ }
3936
4032
  if (channel !== 'release' && channel !== 'nightly') {
3937
4033
  throw new Error(`Invalid channel '${channel}' — must be 'release' or 'nightly'`);
3938
4034
  }
3939
- const key = _cacheKey(channel, cartridgeId);
3940
- for (const cache of this.caches.values()) {
4035
+ const key = _cacheKey(registryUrl, channel, cartridgeId);
4036
+ // Cache outer key is also the registry URL, so look up directly.
4037
+ const cache = this.caches.get(registryUrl);
4038
+ if (cache) {
3941
4039
  const cartridge = cache.cartridges.get(key);
3942
4040
  if (cartridge) {
3943
4041
  return cartridge;
@@ -4001,6 +4099,9 @@ class CartridgeRepoServer {
4001
4099
  if (this.registry.schemaVersion !== '5.0') {
4002
4100
  throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
4003
4101
  }
4102
+ if (typeof this.registry.registryUrl !== 'string' || this.registry.registryUrl.length === 0) {
4103
+ throw new Error('Registry must have a non-empty top-level `registryUrl` field (self-referential URL)');
4104
+ }
4004
4105
  const channels = this.registry.channels;
4005
4106
  if (!channels || typeof channels !== 'object') {
4006
4107
  throw new Error('Registry must have a channels object');
@@ -4082,7 +4183,11 @@ class CartridgeRepoServer {
4082
4183
  tags: cartridge.tags,
4083
4184
  versions: cartridge.versions,
4084
4185
  availableVersions,
4085
- channel
4186
+ channel,
4187
+ // Stamp the manifest's self-referential URL onto every entry —
4188
+ // verbatim from the registry. Identity comparison downstream is
4189
+ // byte equality.
4190
+ registryUrl: this.registry.registryUrl
4086
4191
  };
4087
4192
  }
4088
4193
 
@@ -5078,16 +5183,16 @@ class MachineBuilder {
5078
5183
  }
5079
5184
 
5080
5185
  /**
5081
- * Add a linear chain of edges from CapGraphEdge[] (from CapGraph.findAllPaths).
5186
+ * Add a linear chain of edges from CapFabEdge[] (from CapFab.findAllPaths).
5082
5187
  *
5083
- * Each CapGraphEdge has fromUrn, toUrn, and cap (with cap.urn).
5188
+ * Each CapFabEdge has fromUrn, toUrn, and cap (with cap.urn).
5084
5189
  * This converts the path into a series of MachineEdges.
5085
5190
  *
5086
- * @param {CapGraphEdge[]} capGraphEdges - Array of CapGraphEdge from pathfinding
5191
+ * @param {CapFabEdge[]} capFabEdges - Array of CapFabEdge from pathfinding
5087
5192
  * @returns {MachineBuilder} this (for chaining)
5088
5193
  */
5089
- addCapGraphPath(capGraphEdges) {
5090
- for (const edge of capGraphEdges) {
5194
+ addCapFabPath(capFabEdges) {
5195
+ for (const edge of capFabEdges) {
5091
5196
  const source = MediaUrn.fromString(edge.fromUrn);
5092
5197
  const target = MediaUrn.fromString(edge.toUrn);
5093
5198
  this._edges.push(new MachineEdge([source], edge.cap.urn, target, false));
@@ -5396,9 +5501,9 @@ module.exports = {
5396
5501
  mediaUrnForType,
5397
5502
  modelAvailabilityUrn,
5398
5503
  modelPathUrn,
5399
- CapGraphEdge,
5400
- CapGraphStats,
5401
- CapGraph,
5504
+ CapFabEdge,
5505
+ CapFabStats,
5506
+ CapFab,
5402
5507
  StdinSource,
5403
5508
  StdinSourceKind,
5404
5509
  // Cartridge Repository
@@ -5409,6 +5514,11 @@ module.exports = {
5409
5514
  CartridgeRepoClient,
5410
5515
  CartridgeRepoServer,
5411
5516
  CartridgeChannel,
5517
+ // Registry slug
5518
+ DEV_SLUG,
5519
+ SLUG_HEX_LEN,
5520
+ slugForRegistryUrl,
5521
+ isRegistrySlug,
5412
5522
  // Machine notation
5413
5523
  MachineSyntaxError,
5414
5524
  MachineSyntaxErrorCodes,