capdag 0.170.416 → 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.
- package/build-browser.js +14 -7
- package/capdag.js +175 -81
- package/capdag.test.js +44 -18
- package/package.json +1 -1
package/build-browser.js
CHANGED
|
@@ -31,13 +31,19 @@ const outDir = process.argv[2]
|
|
|
31
31
|
fs.mkdirSync(outDir, { recursive: true });
|
|
32
32
|
|
|
33
33
|
function stripCJS(src) {
|
|
34
|
-
// Strip the CJS `
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
34
|
+
// Strip the CJS `Import TaggedUrn` comment and the trailing
|
|
35
|
+
// `module.exports = {...}` block, then rewrite any single-line
|
|
36
|
+
// `const { ... } = require('tagged-urn')` destructure into a
|
|
37
|
+
// destructure from `window`. The destructure may include any subset
|
|
38
|
+
// of names (TaggedUrn, valuesMatch, scoreTagValue, …) so long as it
|
|
39
|
+
// is one line; renames like `valuesMatch: taggedUrnValuesMatch` are
|
|
40
|
+
// preserved verbatim.
|
|
38
41
|
return src
|
|
39
42
|
.replace(/^\/\/.*Import TaggedUrn.*\n/m, '')
|
|
40
|
-
.replace(
|
|
43
|
+
.replace(
|
|
44
|
+
/^const\s*(\{[^}]*\})\s*=\s*require\s*\(\s*['"]tagged-urn['"]\s*\)\s*;?\s*$/gm,
|
|
45
|
+
'const $1 = window;'
|
|
46
|
+
)
|
|
41
47
|
.replace(/^module\.exports\s*=\s*\{[\s\S]*?\};?\s*$/m, '');
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -60,6 +66,8 @@ window.TaggedUrnBuilder = TaggedUrnBuilder;
|
|
|
60
66
|
window.UrnMatcher = UrnMatcher;
|
|
61
67
|
window.TaggedUrnError = TaggedUrnError;
|
|
62
68
|
window.TaggedUrnErrorCodes = ErrorCodes;
|
|
69
|
+
window.valuesMatch = valuesMatch;
|
|
70
|
+
window.scoreTagValue = scoreTagValue;
|
|
63
71
|
|
|
64
72
|
})();
|
|
65
73
|
`;
|
|
@@ -102,8 +110,7 @@ ${parserSrc}
|
|
|
102
110
|
(function() {
|
|
103
111
|
'use strict';
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
if (!TaggedUrn) {
|
|
113
|
+
if (!window.TaggedUrn) {
|
|
107
114
|
throw new Error('TaggedUrn global is not defined. Load tagged-urn.js before capdag.js.');
|
|
108
115
|
}
|
|
109
116
|
|
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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5862
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
5873
|
-
this.
|
|
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
|
|
5878
|
-
*
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
-
*
|
|
5903
|
-
*
|
|
5904
|
-
*
|
|
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
|
-
|
|
5981
|
+
const requested = CapUrn.fromString(capUrnStr);
|
|
5982
|
+
|
|
5908
5983
|
if (this._capCache && (Date.now() - this._capCache.fetchedAt) < this._cacheTtl) {
|
|
5909
|
-
const
|
|
5910
|
-
|
|
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
|
-
|
|
5914
|
-
const
|
|
5915
|
-
const
|
|
5916
|
-
|
|
5917
|
-
|
|
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 ${
|
|
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
|
|
6000
|
+
return new FabricRegistryEntry(data);
|
|
5925
6001
|
}
|
|
5926
6002
|
|
|
5927
6003
|
/**
|
|
5928
6004
|
* Lookup a single media spec by URN.
|
|
5929
|
-
*
|
|
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
|
-
|
|
5934
|
-
const
|
|
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
|
|
5940
|
-
const
|
|
5941
|
-
|
|
5942
|
-
|
|
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 ${
|
|
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(
|
|
6030
|
+
this._mediaCache.set(canonical, { entry, fetchedAt: Date.now() });
|
|
5951
6031
|
return entry;
|
|
5952
6032
|
}
|
|
5953
6033
|
|
|
5954
6034
|
/**
|
|
5955
|
-
*
|
|
5956
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
6264
|
+
FabricRegistryEntry,
|
|
6171
6265
|
MediaRegistryEntry,
|
|
6172
|
-
|
|
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
|
-
|
|
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
|
|
1532
|
-
const
|
|
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 =
|
|
1567
|
+
const strSpec = resolveMediaUrn(MEDIA_STRING, mediaSpecs);
|
|
1539
1568
|
assertEqual(strSpec.contentType, 'text/plain', 'Should resolve string spec');
|
|
1540
|
-
const outputSpec =
|
|
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:
|
|
3666
|
+
// Phase 0B: FabricRegistryClient tests
|
|
3642
3667
|
// ============================================================================
|
|
3643
3668
|
|
|
3644
3669
|
function testMachine_capRegistryEntry_construction() {
|
|
3645
|
-
const entry = new
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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