capdag 0.171.419 → 0.174.430

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 +175 -81
  2. package/capdag.test.js +44 -18
  3. package/package.json +1 -1
package/capdag.js CHANGED
@@ -1111,6 +1111,12 @@ const MEDIA_COLLECTION = 'media:collection;record';
1111
1111
  const MEDIA_COLLECTION_LIST = 'media:collection;list;record';
1112
1112
  // Media URN for adapter selection output - JSON record
1113
1113
  const MEDIA_ADAPTER_SELECTION = 'media:adapter-selection;json;record';
1114
+ // Fabric registry lookup wire types (consumed/produced by cap:lookup-cap;fabric
1115
+ // and cap:lookup-media-spec;fabric, both implemented by netaccesscartridge).
1116
+ const MEDIA_CAP_URN = 'media:cap-urn;textable';
1117
+ const MEDIA_MEDIA_URN = 'media:media-urn;textable';
1118
+ const MEDIA_CAP_DEFINITION = 'media:cap-definition;json;record;textable';
1119
+ const MEDIA_MEDIA_SPEC_DEFINITION = 'media:media-spec-definition;json;record;textable';
1114
1120
 
1115
1121
  // =============================================================================
1116
1122
  // STANDARD CAP URN CONSTANTS
@@ -1124,6 +1130,12 @@ const CAP_IDENTITY = 'cap:in=media:;out=media:';
1124
1130
  // Cartridges that inspect file content override this with a handler that returns {"media_urns": [...]}.
1125
1131
  const CAP_ADAPTER_SELECTION = 'cap:in="media:";out="media:adapter-selection;json;record"';
1126
1132
 
1133
+ // Fabric registry lookup caps. Implemented by netaccesscartridge.
1134
+ // CAP_LOOKUP_CAP_FABRIC resolves a canonical cap URN to its full flattened
1135
+ // cap definition; CAP_LOOKUP_MEDIA_SPEC_FABRIC does the same for media specs.
1136
+ const CAP_LOOKUP_CAP_FABRIC = 'cap:in="media:cap-urn;textable";fabric;lookup-cap;out="media:cap-definition;json;record;textable"';
1137
+ const CAP_LOOKUP_MEDIA_SPEC_FABRIC = 'cap:in="media:media-urn;textable";fabric;lookup-media-spec;out="media:media-spec-definition;json;record;textable"';
1138
+
1127
1139
  // =============================================================================
1128
1140
  // MEDIA URN CLASS
1129
1141
  // =============================================================================
@@ -2227,7 +2239,6 @@ class Cap {
2227
2239
  this.cap_description = capDescription;
2228
2240
  this.documentation = documentation;
2229
2241
  this.metadata = metadata || {};
2230
- this.mediaSpecs = []; // Media spec definitions array
2231
2242
  this.args = []; // Array of CapArg - unified argument format
2232
2243
  this.output = null;
2233
2244
  this.metadata_json = metadataJson;
@@ -2289,16 +2300,6 @@ class Cap {
2289
2300
  return this.getStdinMediaUrn() !== null;
2290
2301
  }
2291
2302
 
2292
- /**
2293
- * Resolve a media URN to a MediaSpec using this cap's mediaSpecs table
2294
- * @param {string} mediaUrn - The media URN (e.g., "media:string")
2295
- * @returns {MediaSpec} The resolved MediaSpec
2296
- * @throws {MediaSpecError} If media URN cannot be resolved
2297
- */
2298
- resolveMediaUrn(mediaUrn) {
2299
- return resolveMediaUrn(mediaUrn, this.mediaSpecs);
2300
- }
2301
-
2302
2303
  /**
2303
2304
  * Get the URN as a string
2304
2305
  * @returns {string} The URN string representation
@@ -2446,7 +2447,6 @@ class Cap {
2446
2447
  this.cap_description === other.cap_description &&
2447
2448
  this.documentation === other.documentation &&
2448
2449
  JSON.stringify(this.metadata) === JSON.stringify(other.metadata) &&
2449
- JSON.stringify(this.mediaSpecs) === JSON.stringify(other.mediaSpecs) &&
2450
2450
  JSON.stringify(this.args.map(a => a.toJSON())) === JSON.stringify(other.args.map(a => a.toJSON())) &&
2451
2451
  JSON.stringify(this.output) === JSON.stringify(other.output) &&
2452
2452
  JSON.stringify(this.metadata_json) === JSON.stringify(other.metadata_json) &&
@@ -2466,7 +2466,6 @@ class Cap {
2466
2466
  command: this.command,
2467
2467
  cap_description: this.cap_description,
2468
2468
  metadata: this.metadata,
2469
- media_specs: this.mediaSpecs,
2470
2469
  args: this.args.map(a => a.toJSON()),
2471
2470
  output: this.output
2472
2471
  };
@@ -2510,7 +2509,6 @@ class Cap {
2510
2509
  ? json.documentation
2511
2510
  : null;
2512
2511
  const cap = new Cap(urn, json.title, json.command, json.cap_description, json.metadata, json.metadata_json, documentation);
2513
- cap.mediaSpecs = json.media_specs || json.mediaSpecs || [];
2514
2512
  // Parse args (new format)
2515
2513
  if (json.args && Array.isArray(json.args)) {
2516
2514
  cap.args = json.args.map(a => CapArg.fromJSON(a));
@@ -2951,9 +2949,16 @@ function validateCapArgs(cap) {
2951
2949
  */
2952
2950
  class InputValidator {
2953
2951
  /**
2954
- * Validate positional arguments against cap input schema
2955
- */
2956
- static validatePositionalArguments(cap, argValues) {
2952
+ * Validate positional arguments against cap input schema.
2953
+ *
2954
+ * @param {Cap} cap
2955
+ * @param {Array} argValues
2956
+ * @param {Array} mediaSpecs - Media specs the cap's args reference;
2957
+ * threaded through to `resolveMediaUrn` for schema resolution.
2958
+ * Required for any cap whose args reference media URNs that
2959
+ * resolve through the registry.
2960
+ */
2961
+ static validatePositionalArguments(cap, argValues, mediaSpecs = []) {
2957
2962
  const capUrn = cap.urnString();
2958
2963
  const args = cap.arguments;
2959
2964
 
@@ -2974,7 +2979,7 @@ class InputValidator {
2974
2979
  });
2975
2980
  }
2976
2981
 
2977
- InputValidator.validateSingleArgument(cap, args.required[i], argValues[i]);
2982
+ InputValidator.validateSingleArgument(cap, args.required[i], argValues[i], mediaSpecs);
2978
2983
  }
2979
2984
 
2980
2985
  // Validate optional arguments if provided
@@ -2982,15 +2987,20 @@ class InputValidator {
2982
2987
  for (let i = 0; i < args.optional.length; i++) {
2983
2988
  const argIndex = requiredCount + i;
2984
2989
  if (argIndex < argValues.length) {
2985
- InputValidator.validateSingleArgument(cap, args.optional[i], argValues[argIndex]);
2990
+ InputValidator.validateSingleArgument(cap, args.optional[i], argValues[argIndex], mediaSpecs);
2986
2991
  }
2987
2992
  }
2988
2993
  }
2989
2994
 
2990
2995
  /**
2991
- * Validate named arguments against cap input schema
2996
+ * Validate named arguments against cap input schema.
2997
+ *
2998
+ * @param {Cap} cap
2999
+ * @param {Array} namedArgs
3000
+ * @param {Array} mediaSpecs - Media specs the cap's args reference;
3001
+ * threaded through to `resolveMediaUrn` for schema resolution.
2992
3002
  */
2993
- static validateNamedArguments(cap, namedArgs) {
3003
+ static validateNamedArguments(cap, namedArgs, mediaSpecs = []) {
2994
3004
  const capUrn = cap.urnString();
2995
3005
  const args = cap.arguments;
2996
3006
 
@@ -3012,14 +3022,14 @@ class InputValidator {
3012
3022
 
3013
3023
  // Validate the provided argument value
3014
3024
  const providedValue = providedArgs.get(reqArg.name);
3015
- InputValidator.validateSingleArgument(cap, reqArg, providedValue);
3025
+ InputValidator.validateSingleArgument(cap, reqArg, providedValue, mediaSpecs);
3016
3026
  }
3017
3027
 
3018
3028
  // Validate optional arguments if provided
3019
3029
  for (const optArg of args.optional) {
3020
3030
  if (providedArgs.has(optArg.name)) {
3021
3031
  const providedValue = providedArgs.get(optArg.name);
3022
- InputValidator.validateSingleArgument(cap, optArg, providedValue);
3032
+ InputValidator.validateSingleArgument(cap, optArg, providedValue, mediaSpecs);
3023
3033
  }
3024
3034
  }
3025
3035
 
@@ -3043,9 +3053,9 @@ class InputValidator {
3043
3053
  * Two-pass validation:
3044
3054
  * 1. Type validation + media spec validation rules (inherent to semantic type)
3045
3055
  */
3046
- static validateSingleArgument(cap, argDef, value) {
3056
+ static validateSingleArgument(cap, argDef, value, mediaSpecs = []) {
3047
3057
  // Type validation - returns the resolved MediaSpec
3048
- const mediaSpec = InputValidator.validateArgumentType(cap, argDef, value);
3058
+ const mediaSpec = InputValidator.validateArgumentType(cap, argDef, value, mediaSpecs);
3049
3059
 
3050
3060
  // Media spec validation rules (inherent to the semantic type)
3051
3061
  if (mediaSpec && mediaSpec.validation) {
@@ -3058,7 +3068,7 @@ class InputValidator {
3058
3068
  * Resolves spec ID to MediaSpec before validation
3059
3069
  * @returns {MediaSpec|null} The resolved MediaSpec
3060
3070
  */
3061
- static validateArgumentType(cap, argDef, value) {
3071
+ static validateArgumentType(cap, argDef, value, mediaSpecs = []) {
3062
3072
  const capUrn = cap.urnString();
3063
3073
 
3064
3074
  // Get mediaUrn field (now contains a media URN)
@@ -3071,7 +3081,7 @@ class InputValidator {
3071
3081
  // Resolve media URN to MediaSpec - FAIL HARD if unresolvable
3072
3082
  let mediaSpec;
3073
3083
  try {
3074
- mediaSpec = cap.resolveMediaUrn(mediaUrn);
3084
+ mediaSpec = resolveMediaUrn(mediaUrn, mediaSpecs);
3075
3085
  } catch (e) {
3076
3086
  throw new ValidationError('InvalidCapSchema', capUrn, {
3077
3087
  issue: `Cannot resolve media URN '${mediaUrn}' for argument '${argDef.name}': ${e.message}`
@@ -3253,17 +3263,20 @@ class InputValidator {
3253
3263
  */
3254
3264
  class OutputValidator {
3255
3265
  /**
3256
- * Validate output against cap output schema using MediaSpec
3257
- * Resolves spec ID to MediaSpec before validation
3266
+ * Validate output against cap output schema using MediaSpec.
3267
+ *
3268
+ * @param {Cap} cap
3269
+ * @param {*} output
3270
+ * @param {Array} mediaSpecs - Media specs the cap output references;
3271
+ * threaded through to `resolveMediaUrn` for schema resolution.
3258
3272
  */
3259
- static validateOutput(cap, output) {
3260
- const capUrn = cap.urnString();
3273
+ static validateOutput(cap, output, mediaSpecs = []) {
3261
3274
  const outputDef = cap.output;
3262
3275
 
3263
3276
  if (!outputDef) return; // No output definition to validate against
3264
3277
 
3265
3278
  // Type validation - returns the resolved MediaSpec
3266
- const mediaSpec = OutputValidator.validateOutputType(cap, outputDef, output);
3279
+ const mediaSpec = OutputValidator.validateOutputType(cap, outputDef, output, mediaSpecs);
3267
3280
 
3268
3281
  // Media spec validation rules (inherent to the semantic type)
3269
3282
  if (mediaSpec && mediaSpec.validation) {
@@ -3275,7 +3288,7 @@ class OutputValidator {
3275
3288
  * Validate output type using MediaSpec
3276
3289
  * @returns {MediaSpec|null} The resolved MediaSpec
3277
3290
  */
3278
- static validateOutputType(cap, outputDef, value) {
3291
+ static validateOutputType(cap, outputDef, value, mediaSpecs = []) {
3279
3292
  const capUrn = cap.urnString();
3280
3293
 
3281
3294
  // Get mediaUrn field (now contains a media URN)
@@ -3288,7 +3301,7 @@ class OutputValidator {
3288
3301
  // Resolve media URN to MediaSpec - FAIL HARD if unresolvable
3289
3302
  let mediaSpec;
3290
3303
  try {
3291
- mediaSpec = cap.resolveMediaUrn(mediaUrn);
3304
+ mediaSpec = resolveMediaUrn(mediaUrn, mediaSpecs);
3292
3305
  } catch (e) {
3293
3306
  throw new ValidationError('InvalidCapSchema', capUrn, {
3294
3307
  issue: `Cannot resolve media URN '${mediaUrn}' for output: ${e.message}`
@@ -5823,9 +5836,21 @@ class MachineBuilder {
5823
5836
 
5824
5837
  /**
5825
5838
  * A capability entry from the registry.
5826
- * Matches the denormalized view format from capdag.com /api/capabilities.
5839
+ *
5840
+ * Wire shape mirrors the flattened entries published at
5841
+ * <base>/api/capabilities (the flat array)
5842
+ * <base>/views/capabilities (alias)
5843
+ * <base>/views/capabilities-by-urn (map keyed by canonical URN)
5844
+ * <base>/caps/<sha256(canonical-urn)> (per-URN object)
5845
+ *
5846
+ * Equality between an entry's URN and any caller-supplied URN MUST go
5847
+ * through the CapUrn parser's `isEquivalent()` predicate — never via
5848
+ * string comparison. The wire form is already canonical (the writer
5849
+ * canonicalises before publishing), but a caller's URN may not be, so
5850
+ * lookups parse both sides and compare via the parser's order-theoretic
5851
+ * relations.
5827
5852
  */
5828
- class CapRegistryEntry {
5853
+ class FabricRegistryEntry {
5829
5854
  constructor(data) {
5830
5855
  this.urn = data.urn;
5831
5856
  this.title = data.title || '';
@@ -5833,7 +5858,6 @@ class CapRegistryEntry {
5833
5858
  this.description = data.cap_description || '';
5834
5859
  this.args = data.args || [];
5835
5860
  this.output = data.output || null;
5836
- this.mediaSpecs = data.media_specs || [];
5837
5861
  this.urnTags = data.urn_tags || {};
5838
5862
  this.inSpec = data.in_spec || '';
5839
5863
  this.outSpec = data.out_spec || '';
@@ -5844,7 +5868,10 @@ class CapRegistryEntry {
5844
5868
 
5845
5869
  /**
5846
5870
  * A media spec entry from the registry.
5847
- * Matches the media lookup format from capdag.com /media:*.
5871
+ *
5872
+ * Wire shape mirrors the per-URN objects published at
5873
+ * <base>/media/<sha256(canonical-urn)>
5874
+ * and the values of `<base>/views/media-by-urn`.
5848
5875
  */
5849
5876
  class MediaRegistryEntry {
5850
5877
  constructor(data) {
@@ -5856,35 +5883,77 @@ class MediaRegistryEntry {
5856
5883
  }
5857
5884
 
5858
5885
  /**
5859
- * Client for fetching and caching capability and media registries from capdag.com.
5886
+ * SHA-256 hex digest of `s` UTF-8 bytes.
5887
+ *
5888
+ * Used to derive registry object keys from canonical URN strings, so
5889
+ * the URL surface is colon/quote/semicolon-free. All capdag
5890
+ * implementations (capdag, capdag-js, capdag-py, capdag-go, capdag-objc)
5891
+ * use this same algorithm so a URN's key is identical across languages.
5860
5892
  *
5861
- * Uses a time-based cache with configurable TTL. All methods are async.
5862
- * Fails hard on network errors no silent degradation.
5893
+ * Runtime detection: `crypto.subtle` is available in browsers and
5894
+ * modern Node (≥ 16, exposed via `globalThis.crypto`); CommonJS Node
5895
+ * also has the synchronous `crypto` module. We prefer subtle for
5896
+ * portability and fall back to the Node module when subtle is absent.
5863
5897
  */
5864
- class CapRegistryClient {
5898
+ async function sha256Hex(s) {
5899
+ const utf8 = new TextEncoder().encode(s);
5900
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.subtle) {
5901
+ const buf = await globalThis.crypto.subtle.digest('SHA-256', utf8);
5902
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
5903
+ }
5904
+ // Node CommonJS fallback. require() may throw in strict ESM contexts;
5905
+ // we let it propagate because every supported runtime has a crypto API.
5906
+ // eslint-disable-next-line global-require
5907
+ const nodeCrypto = require('crypto');
5908
+ return nodeCrypto.createHash('sha256').update(utf8).digest('hex');
5909
+ }
5910
+
5911
+ /**
5912
+ * Client for fetching and caching capabilities and media specs.
5913
+ *
5914
+ * Reads from `<baseUrl>/api/capabilities` (the flat catalogue) and
5915
+ * `<baseUrl>/{caps,media}/<sha256>` (per-URN).
5916
+ *
5917
+ * URL safety: per-URN lookups hash the canonical URN string with SHA-256
5918
+ * before constructing the URL, so the path contains only the literal
5919
+ * prefix and a 64-character hex digest — no colons, quotes, semicolons,
5920
+ * or equals signs to percent-encode.
5921
+ *
5922
+ * URN comparison: the cache lookup parses both sides through CapUrn /
5923
+ * MediaUrn and uses the parser's `isEquivalent()` predicate. Two
5924
+ * spellings of the same URN (different tag order, etc.) resolve to the
5925
+ * same entry.
5926
+ */
5927
+ class FabricRegistryClient {
5865
5928
  /**
5866
- * @param {string} [baseUrl='https://capdag.com'] - Registry base URL
5929
+ * @param {string} [baseUrl='https://fabric.capdag.com'] - Registry base URL
5867
5930
  * @param {number} [cacheTtlSeconds=300] - Cache TTL in seconds
5868
5931
  */
5869
- constructor(baseUrl = 'https://capdag.com', cacheTtlSeconds = 300) {
5932
+ constructor(baseUrl = 'https://fabric.capdag.com', cacheTtlSeconds = 300) {
5870
5933
  this._baseUrl = baseUrl.replace(/\/$/, '');
5871
5934
  this._cacheTtl = cacheTtlSeconds * 1000;
5872
- this._capCache = null; // { entries: CapRegistryEntry[], fetchedAt: number }
5873
- this._mediaCache = new Map(); // media_urn_string → { entry: MediaRegistryEntry, fetchedAt: number }
5935
+ // The full-catalogue cache is keyed by no key — there's only one.
5936
+ this._capCache = null;
5937
+ // Per-URN media cache, keyed by the canonical URN string. The
5938
+ // canonical key only needs to be unique per equivalence class; we
5939
+ // store one entry per equivalence class.
5940
+ this._mediaCache = new Map();
5874
5941
  }
5875
5942
 
5876
5943
  /**
5877
- * Fetch all capabilities from the registry (cached).
5878
- * @returns {Promise<CapRegistryEntry[]>}
5944
+ * Fetch the full flat capability catalogue (cached).
5945
+ *
5946
+ * @returns {Promise<FabricRegistryEntry[]>}
5879
5947
  */
5880
5948
  async fetchCapabilities() {
5881
5949
  if (this._capCache && (Date.now() - this._capCache.fetchedAt) < this._cacheTtl) {
5882
5950
  return this._capCache.entries;
5883
5951
  }
5884
5952
 
5885
- const response = await fetch(`${this._baseUrl}/api/capabilities`);
5953
+ const url = `${this._baseUrl}/api/capabilities`;
5954
+ const response = await fetch(url);
5886
5955
  if (!response.ok) {
5887
- throw new Error(`Cap registry request failed: HTTP ${response.status} from ${this._baseUrl}/api/capabilities`);
5956
+ throw new Error(`Cap registry request failed: HTTP ${response.status} from ${url}`);
5888
5957
  }
5889
5958
 
5890
5959
  const data = await response.json();
@@ -5892,68 +5961,80 @@ class CapRegistryClient {
5892
5961
  throw new Error(`Invalid cap registry response: expected array, got ${typeof data}`);
5893
5962
  }
5894
5963
 
5895
- const entries = data.map(d => new CapRegistryEntry(d));
5964
+ const entries = data.map(d => new FabricRegistryEntry(d));
5896
5965
  this._capCache = { entries, fetchedAt: Date.now() };
5897
5966
  return entries;
5898
5967
  }
5899
5968
 
5900
5969
  /**
5901
5970
  * Lookup a single capability by URN.
5902
- * Uses the capabilities cache if available, otherwise falls back to direct lookup.
5903
- * @param {string} capUrnStr - Cap URN string
5904
- * @returns {Promise<CapRegistryEntry|null>}
5971
+ *
5972
+ * Canonicalises the input URN through CapUrn, then either:
5973
+ * - returns the cache entry whose URN `isEquivalent()` to the
5974
+ * canonical input, OR
5975
+ * - GETs `<base>/caps/<sha256(canonical)>` and returns its body.
5976
+ *
5977
+ * @param {string} capUrnStr — caller's cap URN (any valid form)
5978
+ * @returns {Promise<FabricRegistryEntry|null>}
5905
5979
  */
5906
5980
  async lookupCap(capUrnStr) {
5907
- // Try cache first
5981
+ const requested = CapUrn.fromString(capUrnStr);
5982
+
5908
5983
  if (this._capCache && (Date.now() - this._capCache.fetchedAt) < this._cacheTtl) {
5909
- const found = this._capCache.entries.find(e => e.urn === capUrnStr);
5910
- if (found) return found;
5984
+ for (const entry of this._capCache.entries) {
5985
+ const entryUrn = CapUrn.fromString(entry.urn);
5986
+ if (entryUrn.isEquivalent(requested)) return entry;
5987
+ }
5911
5988
  }
5912
5989
 
5913
- // Direct lookup
5914
- const encoded = encodeURIComponent(capUrnStr);
5915
- const response = await fetch(`${this._baseUrl}/${encoded}`);
5916
- if (response.status === 404) {
5917
- return null;
5918
- }
5990
+ const canonical = requested.toString();
5991
+ const hash = await sha256Hex(canonical);
5992
+ const url = `${this._baseUrl}/caps/${hash}`;
5993
+ const response = await fetch(url);
5994
+ if (response.status === 404) return null;
5919
5995
  if (!response.ok) {
5920
- throw new Error(`Cap lookup failed: HTTP ${response.status} for ${capUrnStr}`);
5996
+ throw new Error(`Cap lookup failed: HTTP ${response.status} for ${canonical} (${url})`);
5921
5997
  }
5922
5998
 
5923
5999
  const data = await response.json();
5924
- return new CapRegistryEntry(data);
6000
+ return new FabricRegistryEntry(data);
5925
6001
  }
5926
6002
 
5927
6003
  /**
5928
6004
  * Lookup a single media spec by URN.
5929
- * @param {string} mediaUrnStr - Media URN string
6005
+ *
6006
+ * Canonicalises through MediaUrn and looks up by SHA-256 hash.
6007
+ *
6008
+ * @param {string} mediaUrnStr
5930
6009
  * @returns {Promise<MediaRegistryEntry|null>}
5931
6010
  */
5932
6011
  async lookupMedia(mediaUrnStr) {
5933
- // Check cache
5934
- const cached = this._mediaCache.get(mediaUrnStr);
6012
+ const requested = MediaUrn.fromString(mediaUrnStr);
6013
+ const canonical = requested.toString();
6014
+
6015
+ const cached = this._mediaCache.get(canonical);
5935
6016
  if (cached && (Date.now() - cached.fetchedAt) < this._cacheTtl) {
5936
6017
  return cached.entry;
5937
6018
  }
5938
6019
 
5939
- const encoded = encodeURIComponent(mediaUrnStr);
5940
- const response = await fetch(`${this._baseUrl}/${encoded}`);
5941
- if (response.status === 404) {
5942
- return null;
5943
- }
6020
+ const hash = await sha256Hex(canonical);
6021
+ const url = `${this._baseUrl}/media/${hash}`;
6022
+ const response = await fetch(url);
6023
+ if (response.status === 404) return null;
5944
6024
  if (!response.ok) {
5945
- throw new Error(`Media lookup failed: HTTP ${response.status} for ${mediaUrnStr}`);
6025
+ throw new Error(`Media lookup failed: HTTP ${response.status} for ${canonical} (${url})`);
5946
6026
  }
5947
6027
 
5948
6028
  const data = await response.json();
5949
6029
  const entry = new MediaRegistryEntry(data);
5950
- this._mediaCache.set(mediaUrnStr, { entry, fetchedAt: Date.now() });
6030
+ this._mediaCache.set(canonical, { entry, fetchedAt: Date.now() });
5951
6031
  return entry;
5952
6032
  }
5953
6033
 
5954
6034
  /**
5955
- * Get all known media URNs from cached capabilities (in and out specs).
5956
- * Fetches capabilities if not cached.
6035
+ * All canonical media URNs referenced as in/out specs by any cap in
6036
+ * the cached catalogue.
6037
+ *
5957
6038
  * @returns {Promise<string[]>}
5958
6039
  */
5959
6040
  async getKnownMediaUrns() {
@@ -5967,7 +6048,13 @@ class CapRegistryClient {
5967
6048
  }
5968
6049
 
5969
6050
  /**
5970
- * Get all known op= tag values from cached capabilities.
6051
+ * All distinct `op=` tag values present on any cap in the cached
6052
+ * catalogue.
6053
+ *
6054
+ * `op` is just another arbitrary tag; this helper exists for the
6055
+ * cap-navigator UI which surfaces operation labels. It is NOT part of
6056
+ * dispatch — only the in/out tags carry functional meaning.
6057
+ *
5971
6058
  * @returns {Promise<string[]>}
5972
6059
  */
5973
6060
  async getKnownOps() {
@@ -5981,7 +6068,7 @@ class CapRegistryClient {
5981
6068
  }
5982
6069
 
5983
6070
  /**
5984
- * Invalidate all caches. Next call to any method will fetch fresh data.
6071
+ * Invalidate all caches. Next call to any method fetches fresh data.
5985
6072
  */
5986
6073
  invalidate() {
5987
6074
  this._capCache = null;
@@ -6121,8 +6208,15 @@ module.exports = {
6121
6208
  MEDIA_COLLECTION,
6122
6209
  MEDIA_COLLECTION_LIST,
6123
6210
  MEDIA_ADAPTER_SELECTION,
6211
+ // Fabric registry lookup wire types
6212
+ MEDIA_CAP_URN,
6213
+ MEDIA_MEDIA_URN,
6214
+ MEDIA_CAP_DEFINITION,
6215
+ MEDIA_MEDIA_SPEC_DEFINITION,
6124
6216
  // Standard cap URN constants
6125
6217
  CAP_ADAPTER_SELECTION,
6218
+ CAP_LOOKUP_CAP_FABRIC,
6219
+ CAP_LOOKUP_MEDIA_SPEC_FABRIC,
6126
6220
  // Cap execution result
6127
6221
  CapResult,
6128
6222
  // Unified argument type
@@ -6167,7 +6261,7 @@ module.exports = {
6167
6261
  parseMachine,
6168
6262
  parseMachineWithAST,
6169
6263
  // Cap & Media Registry
6170
- CapRegistryEntry,
6264
+ FabricRegistryEntry,
6171
6265
  MediaRegistryEntry,
6172
- CapRegistryClient,
6266
+ FabricRegistryClient,
6173
6267
  };
package/capdag.test.js CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  CapArgumentValue, CapArg, ArgSource, validateCapArgs, ValidationError,
15
15
  llmGenerateTextUrn, modelAvailabilityUrn, modelPathUrn,
16
16
  MachineSyntaxError, MachineSyntaxErrorCodes, MachineEdge, Machine, MachineBuilder, parseMachine, parseMachineWithAST,
17
- CapRegistryEntry, MediaRegistryEntry, CapRegistryClient,
17
+ FabricRegistryEntry, MediaRegistryEntry, FabricRegistryClient,
18
18
  MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN,
19
19
  MEDIA_OBJECT, MEDIA_STRING_LIST, MEDIA_INTEGER_LIST,
20
20
  MEDIA_NUMBER_LIST, MEDIA_BOOLEAN_LIST, MEDIA_OBJECT_LIST,
@@ -283,6 +283,37 @@ function test016_trailingSemicolonEquivalence() {
283
283
  assertEqual(cap1.toString(), cap2.toString(), 'Canonical forms should match');
284
284
  }
285
285
 
286
+ // TEST939: The canonical form drops `in=media:` and `out=media:`
287
+ // segments. Every spelling of "the same cap with wildcard in/out"
288
+ // collapses to one byte-identical canonical string. This is the
289
+ // contract that makes registry lookups work: the cap-publisher hashes
290
+ // `<canonical-urn>` to compute the cache key, and every language port
291
+ // (Rust, Go, Python, JS, ObjC) must agree on the canonical form for
292
+ // cross-language lookups to land on the same key. A regression that
293
+ // emitted the wildcard segments would silently move the published cap
294
+ // to a different SHA-256 bucket, 404'ing every reader that hashes the
295
+ // canonical form.
296
+ function test939_capUrnCanonicalFormDropsWildcardInOut() {
297
+ const canonical = 'cap:decimate-sequence';
298
+ const variants = [
299
+ 'cap:decimate-sequence',
300
+ 'cap:decimate-sequence;in=media:;out=media:',
301
+ 'cap:in=media:;out=media:;decimate-sequence',
302
+ 'cap:in=media:;decimate-sequence;out=media:',
303
+ ];
304
+ for (const v of variants) {
305
+ const parsed = CapUrn.fromString(v);
306
+ assertEqual(
307
+ parsed.toString(),
308
+ canonical,
309
+ `input ${JSON.stringify(v)} canonicalized to ${JSON.stringify(parsed.toString())}, expected ${JSON.stringify(canonical)} — wildcard in/out segments must be elided so the registry SHA-256 key is stable across input spellings`
310
+ );
311
+ }
312
+ // Bare-identity round-trip.
313
+ const identity = CapUrn.fromString('cap:in=media:;out=media:');
314
+ assertEqual(identity.toString(), 'cap:', 'cap with wildcard in/out and no other tags must canonicalize to bare "cap:"');
315
+ }
316
+
286
317
  // TEST017: Test tag matching: exact match, subset match, wildcard match, value mismatch
287
318
  function test017_tagMatching() {
288
319
  const cap = CapUrn.fromString(testUrn('generate;ext=pdf;target=thumbnail'));
@@ -1528,16 +1559,14 @@ function testJS_getExtensionMappings() {
1528
1559
  assertEqual(mappings.length, 3, 'Should have 3 mappings');
1529
1560
  }
1530
1561
 
1531
- function testJS_capWithMediaSpecs() {
1532
- const urn = CapUrn.fromString('cap:in="media:string";test;out="media:custom"');
1533
- const cap = new Cap(urn, 'Test Cap', 'test_command');
1534
- cap.mediaSpecs = [
1562
+ function testJS_resolveMediaUrnFromSpecs() {
1563
+ const mediaSpecs = [
1535
1564
  { urn: MEDIA_STRING, media_type: 'text/plain', title: 'String', profile_uri: 'https://capdag.com/schema/str' },
1536
1565
  { urn: 'media:custom', media_type: 'application/json', title: 'Custom Output', schema: { type: 'object' } }
1537
1566
  ];
1538
- const strSpec = cap.resolveMediaUrn(MEDIA_STRING);
1567
+ const strSpec = resolveMediaUrn(MEDIA_STRING, mediaSpecs);
1539
1568
  assertEqual(strSpec.contentType, 'text/plain', 'Should resolve string spec');
1540
- const outputSpec = cap.resolveMediaUrn('media:custom');
1569
+ const outputSpec = resolveMediaUrn('media:custom', mediaSpecs);
1541
1570
  assertEqual(outputSpec.contentType, 'application/json', 'Should resolve custom spec');
1542
1571
  assert(outputSpec.schema !== null, 'Should have schema');
1543
1572
  }
@@ -1545,9 +1574,6 @@ function testJS_capWithMediaSpecs() {
1545
1574
  function testJS_capJSONSerialization() {
1546
1575
  const urn = CapUrn.fromString(testUrn('test'));
1547
1576
  const cap = new Cap(urn, 'Test Cap', 'test_command');
1548
- cap.mediaSpecs = [
1549
- { urn: 'media:custom', media_type: 'text/plain', title: 'Custom' }
1550
- ];
1551
1577
  cap.arguments = {
1552
1578
  required: [{ name: 'input', media_urn: MEDIA_STRING }],
1553
1579
  optional: []
@@ -1555,11 +1581,10 @@ function testJS_capJSONSerialization() {
1555
1581
  cap.output = { media_urn: 'media:custom', output_description: 'Test output' };
1556
1582
 
1557
1583
  const json = cap.toJSON();
1558
- assert(json.media_specs !== undefined, 'Should have media_specs');
1559
1584
  assertEqual(typeof json.urn, 'string', 'URN should be string');
1585
+ assert(json.media_specs === undefined, 'Cap JSON must not contain media_specs (registry-resolved)');
1560
1586
 
1561
1587
  const restored = Cap.fromJSON(json);
1562
- assert(restored.mediaSpecs !== undefined, 'Should restore mediaSpecs');
1563
1588
  assertEqual(restored.urn.getInSpec(), MEDIA_VOID, 'Should restore inSpec');
1564
1589
  assertEqual(restored.urn.getOutSpec(), MEDIA_OBJECT, 'Should restore outSpec');
1565
1590
  }
@@ -3638,11 +3663,11 @@ function testMachine_toMermaid_fanOut() {
3638
3663
  }
3639
3664
 
3640
3665
  // ============================================================================
3641
- // Phase 0B: CapRegistryClient tests
3666
+ // Phase 0B: FabricRegistryClient tests
3642
3667
  // ============================================================================
3643
3668
 
3644
3669
  function testMachine_capRegistryEntry_construction() {
3645
- const entry = new CapRegistryEntry({
3670
+ const entry = new FabricRegistryEntry({
3646
3671
  urn: 'cap:in="media:pdf";extract;out="media:txt;textable"',
3647
3672
  title: 'PDF Extractor',
3648
3673
  command: 'extract',
@@ -3678,7 +3703,7 @@ function testMachine_mediaRegistryEntry_construction() {
3678
3703
  }
3679
3704
 
3680
3705
  function testMachine_capRegistryClient_construction() {
3681
- const client = new CapRegistryClient('https://example.com', 600);
3706
+ const client = new FabricRegistryClient('https://example.com', 600);
3682
3707
  assert(client !== null, 'Client should be constructed');
3683
3708
  // Invalidate should not throw
3684
3709
  client.invalidate();
@@ -3686,7 +3711,7 @@ function testMachine_capRegistryClient_construction() {
3686
3711
 
3687
3712
  function testMachine_capRegistryEntry_defaults() {
3688
3713
  // Verify that missing fields default gracefully
3689
- const entry = new CapRegistryEntry({ urn: 'cap:in=media:;test;out=media:' });
3714
+ const entry = new FabricRegistryEntry({ urn: 'cap:in=media:;test;out=media:' });
3690
3715
  assertEqual(entry.urn, 'cap:in=media:;test;out=media:', 'URN should match');
3691
3716
  assertEqual(entry.title, '', 'Title should default to empty');
3692
3717
  assertEqual(entry.description, '', 'Description should default to empty');
@@ -5674,6 +5699,7 @@ async function runTests() {
5674
5699
  runTest('TEST014: round_trip_escapes', test014_roundTripEscapes);
5675
5700
  runTest('TEST015: cap_prefix_required', test015_capPrefixRequired);
5676
5701
  runTest('TEST016: trailing_semicolon_equivalence', test016_trailingSemicolonEquivalence);
5702
+ runTest('TEST939: cap_urn_canonical_form_drops_wildcard_in_out', test939_capUrnCanonicalFormDropsWildcardInOut);
5677
5703
  runTest('TEST017: tag_matching', test017_tagMatching);
5678
5704
  runTest('TEST018: matching_case_sensitive_values', test018_matchingCaseSensitiveValues);
5679
5705
  runTest('TEST019: missing_tag_handling', test019_missingTagHandling);
@@ -5803,7 +5829,7 @@ async function runTests() {
5803
5829
  runTest('JS: build_extension_index', testJS_buildExtensionIndex);
5804
5830
  runTest('JS: media_urns_for_extension', testJS_mediaUrnsForExtension);
5805
5831
  runTest('JS: get_extension_mappings', testJS_getExtensionMappings);
5806
- runTest('JS: cap_with_media_specs', testJS_capWithMediaSpecs);
5832
+ runTest('JS: resolve_media_urn_from_specs', testJS_resolveMediaUrnFromSpecs);
5807
5833
  runTest('JS: cap_json_serialization', testJS_capJSONSerialization);
5808
5834
  runTest('JS: cap_documentation_round_trip', testJS_capDocumentationRoundTrip);
5809
5835
  runTest('JS: cap_documentation_omitted_when_null', testJS_capDocumentationOmittedWhenNull);
@@ -5975,7 +6001,7 @@ async function runTests() {
5975
6001
  runTest('MACHINE:toMermaid_fanIn', testMachine_toMermaid_fanIn);
5976
6002
  runTest('MACHINE:toMermaid_fanOut', testMachine_toMermaid_fanOut);
5977
6003
 
5978
- // Phase 0B: CapRegistryClient
6004
+ // Phase 0B: FabricRegistryClient
5979
6005
  console.log('\n--- registry/client ---');
5980
6006
  runTest('REGISTRY: capRegistryEntry_construction', testMachine_capRegistryEntry_construction);
5981
6007
  runTest('REGISTRY: mediaRegistryEntry_construction', testMachine_mediaRegistryEntry_construction);
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.171.419"
43
+ "version": "0.174.430"
44
44
  }