capdag 0.140.310 → 0.145.321
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/cap-graph-renderer.js +4 -5
- package/capdag.js +0 -466
- package/capdag.test.js +58 -448
- package/package.json +1 -1
package/cap-graph-renderer.js
CHANGED
|
@@ -313,11 +313,10 @@ function buildStylesheet() {
|
|
|
313
313
|
'text-background-opacity': 1,
|
|
314
314
|
'text-background-padding': '4px',
|
|
315
315
|
'text-background-shape': 'roundrectangle',
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
'text-
|
|
320
|
-
'text-margin-y': 0,
|
|
316
|
+
// Align labels along their edges so perpendicular/misaligned
|
|
317
|
+
// text no longer reads as floating metadata.
|
|
318
|
+
'text-rotation': 'autorotate',
|
|
319
|
+
'text-margin-y': -6,
|
|
321
320
|
'curve-style': 'bezier',
|
|
322
321
|
'control-point-step-size': 40,
|
|
323
322
|
'width': 1.5,
|
package/capdag.js
CHANGED
|
@@ -3140,448 +3140,6 @@ class CapArgumentValue {
|
|
|
3140
3140
|
}
|
|
3141
3141
|
}
|
|
3142
3142
|
|
|
3143
|
-
// ============================================================================
|
|
3144
|
-
// CAP MATRIX - Registry for Capability Hosts
|
|
3145
|
-
// ============================================================================
|
|
3146
|
-
|
|
3147
|
-
/**
|
|
3148
|
-
* Error types for capability host registry operations
|
|
3149
|
-
*/
|
|
3150
|
-
class CapMatrixError extends Error {
|
|
3151
|
-
constructor(type, message) {
|
|
3152
|
-
super(message);
|
|
3153
|
-
this.name = 'CapMatrixError';
|
|
3154
|
-
this.type = type;
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
static noSetsFound(capability) {
|
|
3158
|
-
return new CapMatrixError('NoSetsFound', `No cap sets found for capability: ${capability}`);
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
static invalidUrn(urn, reason) {
|
|
3162
|
-
return new CapMatrixError('InvalidUrn', `Invalid capability URN: ${urn}: ${reason}`);
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
static registryError(message) {
|
|
3166
|
-
return new CapMatrixError('RegistryError', message);
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
/**
|
|
3171
|
-
* Internal entry for a registered capability host
|
|
3172
|
-
*/
|
|
3173
|
-
class CapSetEntry {
|
|
3174
|
-
constructor(name, host, capabilities) {
|
|
3175
|
-
this.name = name;
|
|
3176
|
-
this.host = host; // Object implementing executeCap(capUrn, arguments) -> Promise
|
|
3177
|
-
this.capabilities = capabilities; // Array<Cap>
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
/**
|
|
3182
|
-
* Unified registry for cap sets (providers and cartridges)
|
|
3183
|
-
* Provides capability host discovery using subset matching.
|
|
3184
|
-
*/
|
|
3185
|
-
class CapMatrix {
|
|
3186
|
-
constructor() {
|
|
3187
|
-
this.sets = new Map(); // Map<string, CapSetEntry>
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
/**
|
|
3191
|
-
* Register a capability host with its supported capabilities
|
|
3192
|
-
* @param {string} name - Unique name for the capability host
|
|
3193
|
-
* @param {object} host - Object with executeCap method
|
|
3194
|
-
* @param {Cap[]} capabilities - Array of capabilities this host supports
|
|
3195
|
-
*/
|
|
3196
|
-
registerCapSet(name, host, capabilities) {
|
|
3197
|
-
const entry = new CapSetEntry(name, host, capabilities);
|
|
3198
|
-
this.sets.set(name, entry);
|
|
3199
|
-
}
|
|
3200
|
-
|
|
3201
|
-
/**
|
|
3202
|
-
* Find cap sets that can handle the requested capability
|
|
3203
|
-
* Uses subset matching: host capabilities must be a subset of or match the request
|
|
3204
|
-
* @param {string} requestUrn - The capability URN to find sets for
|
|
3205
|
-
* @returns {object[]} Array of hosts that can handle the request
|
|
3206
|
-
* @throws {CapMatrixError} If URN is invalid or no sets found
|
|
3207
|
-
*/
|
|
3208
|
-
findCapSets(requestUrn) {
|
|
3209
|
-
let request;
|
|
3210
|
-
try {
|
|
3211
|
-
request = CapUrn.fromString(requestUrn);
|
|
3212
|
-
} catch (e) {
|
|
3213
|
-
throw CapMatrixError.invalidUrn(requestUrn, e.message);
|
|
3214
|
-
}
|
|
3215
|
-
|
|
3216
|
-
const matchingHosts = [];
|
|
3217
|
-
|
|
3218
|
-
for (const entry of this.sets.values()) {
|
|
3219
|
-
for (const cap of entry.capabilities) {
|
|
3220
|
-
if (cap.urn.accepts(request)) {
|
|
3221
|
-
matchingHosts.push(entry.host);
|
|
3222
|
-
break; // Found a matching capability for this host
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3227
|
-
if (matchingHosts.length === 0) {
|
|
3228
|
-
throw CapMatrixError.noSetsFound(requestUrn);
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
return matchingHosts;
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
/**
|
|
3235
|
-
* Find the best capability host for the request using specificity ranking
|
|
3236
|
-
* @param {string} requestUrn - The capability URN to find the best host for
|
|
3237
|
-
* @returns {{host: object, cap: Cap}} The best host and matching cap definition
|
|
3238
|
-
* @throws {CapMatrixError} If URN is invalid or no sets found
|
|
3239
|
-
*/
|
|
3240
|
-
findBestCapSet(requestUrn) {
|
|
3241
|
-
let request;
|
|
3242
|
-
try {
|
|
3243
|
-
request = CapUrn.fromString(requestUrn);
|
|
3244
|
-
} catch (e) {
|
|
3245
|
-
throw CapMatrixError.invalidUrn(requestUrn, e.message);
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
let bestHost = null;
|
|
3249
|
-
let bestCap = null;
|
|
3250
|
-
let bestSpecificity = -1;
|
|
3251
|
-
|
|
3252
|
-
for (const entry of this.sets.values()) {
|
|
3253
|
-
for (const cap of entry.capabilities) {
|
|
3254
|
-
if (cap.urn.accepts(request)) {
|
|
3255
|
-
const specificity = cap.urn.specificity();
|
|
3256
|
-
if (bestSpecificity === -1 || specificity > bestSpecificity) {
|
|
3257
|
-
bestHost = entry.host;
|
|
3258
|
-
bestCap = cap;
|
|
3259
|
-
bestSpecificity = specificity;
|
|
3260
|
-
}
|
|
3261
|
-
break; // Found match for this entry, check next
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
if (bestHost === null) {
|
|
3267
|
-
throw CapMatrixError.noSetsFound(requestUrn);
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
return { host: bestHost, cap: bestCap };
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/**
|
|
3274
|
-
* Get all registered capability host names
|
|
3275
|
-
* @returns {string[]} Array of host names
|
|
3276
|
-
*/
|
|
3277
|
-
getHostNames() {
|
|
3278
|
-
return Array.from(this.sets.keys());
|
|
3279
|
-
}
|
|
3280
|
-
|
|
3281
|
-
/**
|
|
3282
|
-
* Get all capabilities from all registered sets
|
|
3283
|
-
* @returns {Cap[]} Array of all capabilities
|
|
3284
|
-
*/
|
|
3285
|
-
getAllCapabilities() {
|
|
3286
|
-
const capabilities = [];
|
|
3287
|
-
for (const entry of this.sets.values()) {
|
|
3288
|
-
capabilities.push(...entry.capabilities);
|
|
3289
|
-
}
|
|
3290
|
-
return capabilities;
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
/**
|
|
3294
|
-
* Check if any host accepts the specified capability request
|
|
3295
|
-
* @param {string} requestUrn - The capability URN to check
|
|
3296
|
-
* @returns {boolean} Whether the capability request is accepted
|
|
3297
|
-
*/
|
|
3298
|
-
acceptsRequest(requestUrn) {
|
|
3299
|
-
try {
|
|
3300
|
-
this.findCapSets(requestUrn);
|
|
3301
|
-
return true;
|
|
3302
|
-
} catch (e) {
|
|
3303
|
-
if (e instanceof CapMatrixError) {
|
|
3304
|
-
return false;
|
|
3305
|
-
}
|
|
3306
|
-
throw e;
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
/**
|
|
3311
|
-
* Unregister a capability host
|
|
3312
|
-
* @param {string} name - The name of the host to unregister
|
|
3313
|
-
* @returns {boolean} Whether the host was found and removed
|
|
3314
|
-
*/
|
|
3315
|
-
unregisterCapSet(name) {
|
|
3316
|
-
return this.sets.delete(name);
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
/**
|
|
3320
|
-
* Clear all registered sets
|
|
3321
|
-
*/
|
|
3322
|
-
clear() {
|
|
3323
|
-
this.sets.clear();
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
// ============================================================================
|
|
3328
|
-
// CAP BLOCK - Composite Registry
|
|
3329
|
-
// ============================================================================
|
|
3330
|
-
|
|
3331
|
-
/**
|
|
3332
|
-
* Result of finding the best match across registries
|
|
3333
|
-
*/
|
|
3334
|
-
class BestCapSetMatch {
|
|
3335
|
-
/**
|
|
3336
|
-
* @param {Cap} cap - The Cap definition that matched
|
|
3337
|
-
* @param {number} specificity - The specificity score of the match
|
|
3338
|
-
* @param {string} registryName - The name of the registry that provided this match
|
|
3339
|
-
*/
|
|
3340
|
-
constructor(cap, specificity, registryName) {
|
|
3341
|
-
this.cap = cap;
|
|
3342
|
-
this.specificity = specificity;
|
|
3343
|
-
this.registryName = registryName;
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
/**
|
|
3348
|
-
* Composite CapSet that wraps multiple registries
|
|
3349
|
-
* and delegates execution to the best matching one.
|
|
3350
|
-
*/
|
|
3351
|
-
class CompositeCapSet {
|
|
3352
|
-
/**
|
|
3353
|
-
* @param {Array<{name: string, registry: CapMatrix}>} registries
|
|
3354
|
-
*/
|
|
3355
|
-
constructor(registries) {
|
|
3356
|
-
this.registries = registries;
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
/**
|
|
3360
|
-
* Execute a capability by finding the best match and delegating
|
|
3361
|
-
* @param {string} capUrn - The capability URN to execute
|
|
3362
|
-
* @param {CapArgumentValue[]} args - Arguments identified by media_urn
|
|
3363
|
-
* @returns {Promise<CapResult>}
|
|
3364
|
-
*/
|
|
3365
|
-
async executeCap(capUrn, args) {
|
|
3366
|
-
let request;
|
|
3367
|
-
try {
|
|
3368
|
-
request = CapUrn.fromString(capUrn);
|
|
3369
|
-
} catch (e) {
|
|
3370
|
-
throw new Error(`Invalid cap URN '${capUrn}': ${e.message}`);
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
// Find the best matching host across all registries
|
|
3374
|
-
let bestHost = null;
|
|
3375
|
-
let bestSpecificity = -1;
|
|
3376
|
-
|
|
3377
|
-
for (const { registry } of this.registries) {
|
|
3378
|
-
for (const entry of registry.sets.values()) {
|
|
3379
|
-
for (const cap of entry.capabilities) {
|
|
3380
|
-
if (cap.urn.accepts(request)) {
|
|
3381
|
-
const specificity = cap.urn.specificity();
|
|
3382
|
-
if (bestSpecificity === -1 || specificity > bestSpecificity) {
|
|
3383
|
-
bestHost = entry.host;
|
|
3384
|
-
bestSpecificity = specificity;
|
|
3385
|
-
}
|
|
3386
|
-
break; // Found match for this entry
|
|
3387
|
-
}
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
if (bestHost === null) {
|
|
3393
|
-
throw new Error(`No capability host found for '${capUrn}'`);
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
|
-
// Delegate execution to the best matching host
|
|
3397
|
-
return bestHost.executeCap(capUrn, args);
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
/**
|
|
3401
|
-
* Build a directed graph from all capabilities in the registries.
|
|
3402
|
-
* @returns {CapGraph}
|
|
3403
|
-
*/
|
|
3404
|
-
graph() {
|
|
3405
|
-
return CapGraph.buildFromRegistries(this.registries);
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
|
|
3409
|
-
/**
|
|
3410
|
-
* Composite registry that wraps multiple CapMatrix instances
|
|
3411
|
-
* and finds the best match across all of them by specificity.
|
|
3412
|
-
*
|
|
3413
|
-
* When multiple registries can handle a request, this registry compares
|
|
3414
|
-
* specificity scores and returns the most specific match.
|
|
3415
|
-
* On tie, defaults to the first registry that was added (priority order).
|
|
3416
|
-
*/
|
|
3417
|
-
class CapBlock {
|
|
3418
|
-
constructor() {
|
|
3419
|
-
this.registries = []; // Array of {name: string, registry: CapMatrix}
|
|
3420
|
-
}
|
|
3421
|
-
|
|
3422
|
-
/**
|
|
3423
|
-
* Add a child registry with a name.
|
|
3424
|
-
* Registries are checked in order of addition for tie-breaking.
|
|
3425
|
-
* @param {string} name - Unique name for this registry
|
|
3426
|
-
* @param {CapMatrix} registry - The CapMatrix to add
|
|
3427
|
-
*/
|
|
3428
|
-
addRegistry(name, registry) {
|
|
3429
|
-
this.registries.push({ name, registry });
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
/**
|
|
3433
|
-
* Remove a child registry by name
|
|
3434
|
-
* @param {string} name - The name of the registry to remove
|
|
3435
|
-
* @returns {CapMatrix|null} The removed registry, or null if not found
|
|
3436
|
-
*/
|
|
3437
|
-
removeRegistry(name) {
|
|
3438
|
-
const index = this.registries.findIndex(entry => entry.name === name);
|
|
3439
|
-
if (index !== -1) {
|
|
3440
|
-
const removed = this.registries[index].registry;
|
|
3441
|
-
this.registries.splice(index, 1);
|
|
3442
|
-
return removed;
|
|
3443
|
-
}
|
|
3444
|
-
return null;
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
|
-
/**
|
|
3448
|
-
* Get a child registry by name
|
|
3449
|
-
* @param {string} name - The name of the registry
|
|
3450
|
-
* @returns {CapMatrix|null} The registry, or null if not found
|
|
3451
|
-
*/
|
|
3452
|
-
getRegistry(name) {
|
|
3453
|
-
const entry = this.registries.find(e => e.name === name);
|
|
3454
|
-
return entry ? entry.registry : null;
|
|
3455
|
-
}
|
|
3456
|
-
|
|
3457
|
-
/**
|
|
3458
|
-
* Get names of all child registries
|
|
3459
|
-
* @returns {string[]} Array of registry names in priority order
|
|
3460
|
-
*/
|
|
3461
|
-
getRegistryNames() {
|
|
3462
|
-
return this.registries.map(entry => entry.name);
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
/**
|
|
3466
|
-
* Check if a capability is available and return execution info.
|
|
3467
|
-
* This is the main entry point for capability lookup.
|
|
3468
|
-
* @param {string} capUrn - The capability URN to look up
|
|
3469
|
-
* @returns {{cap: Cap, compositeHost: CompositeCapSet}} The cap and composite host for execution
|
|
3470
|
-
* @throws {CapMatrixError} If URN is invalid or no match found
|
|
3471
|
-
*/
|
|
3472
|
-
can(capUrn) {
|
|
3473
|
-
// Find the best match to get the cap definition
|
|
3474
|
-
const bestMatch = this.findBestCapSet(capUrn);
|
|
3475
|
-
|
|
3476
|
-
// Create a CompositeCapSet that will delegate execution
|
|
3477
|
-
const compositeHost = new CompositeCapSet([...this.registries]);
|
|
3478
|
-
|
|
3479
|
-
return {
|
|
3480
|
-
cap: bestMatch.cap,
|
|
3481
|
-
compositeHost: compositeHost
|
|
3482
|
-
};
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
/**
|
|
3486
|
-
* Find the best capability host across ALL child registries.
|
|
3487
|
-
* Polls all registries and compares their best matches by specificity.
|
|
3488
|
-
* On specificity tie, returns the match from the first registry.
|
|
3489
|
-
* @param {string} requestUrn - The capability URN to find the best host for
|
|
3490
|
-
* @returns {BestCapSetMatch} The best match
|
|
3491
|
-
* @throws {CapMatrixError} If URN is invalid or no match found
|
|
3492
|
-
*/
|
|
3493
|
-
findBestCapSet(requestUrn) {
|
|
3494
|
-
let request;
|
|
3495
|
-
try {
|
|
3496
|
-
request = CapUrn.fromString(requestUrn);
|
|
3497
|
-
} catch (e) {
|
|
3498
|
-
throw CapMatrixError.invalidUrn(requestUrn, e.message);
|
|
3499
|
-
}
|
|
3500
|
-
|
|
3501
|
-
let bestOverall = null;
|
|
3502
|
-
|
|
3503
|
-
for (const { name, registry } of this.registries) {
|
|
3504
|
-
// Find the best match within this registry
|
|
3505
|
-
const result = this._findBestInRegistry(registry, request);
|
|
3506
|
-
if (result) {
|
|
3507
|
-
const { cap, specificity } = result;
|
|
3508
|
-
const candidate = new BestCapSetMatch(cap, specificity, name);
|
|
3509
|
-
|
|
3510
|
-
if (bestOverall === null) {
|
|
3511
|
-
bestOverall = candidate;
|
|
3512
|
-
} else if (specificity > bestOverall.specificity) {
|
|
3513
|
-
// Only replace if strictly more specific
|
|
3514
|
-
// On tie, keep the first one (priority order)
|
|
3515
|
-
bestOverall = candidate;
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
if (bestOverall === null) {
|
|
3521
|
-
throw CapMatrixError.noSetsFound(requestUrn);
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
return bestOverall;
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
/**
|
|
3528
|
-
* Check if any registry accepts the specified capability request
|
|
3529
|
-
* @param {string} requestUrn - The capability URN to check
|
|
3530
|
-
* @returns {boolean} Whether the capability request is accepted
|
|
3531
|
-
*/
|
|
3532
|
-
acceptsRequest(requestUrn) {
|
|
3533
|
-
try {
|
|
3534
|
-
this.findBestCapSet(requestUrn);
|
|
3535
|
-
return true;
|
|
3536
|
-
} catch (e) {
|
|
3537
|
-
if (e instanceof CapMatrixError) {
|
|
3538
|
-
return false;
|
|
3539
|
-
}
|
|
3540
|
-
throw e;
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
|
-
/**
|
|
3545
|
-
* Find the best match within a single registry
|
|
3546
|
-
* @private
|
|
3547
|
-
* @param {CapMatrix} registry - The registry to search
|
|
3548
|
-
* @param {CapUrn} request - The parsed request URN
|
|
3549
|
-
* @returns {{cap: Cap, specificity: number}|null} The best match or null
|
|
3550
|
-
*/
|
|
3551
|
-
_findBestInRegistry(registry, request) {
|
|
3552
|
-
let bestCap = null;
|
|
3553
|
-
let bestSpecificity = -1;
|
|
3554
|
-
|
|
3555
|
-
for (const entry of registry.sets.values()) {
|
|
3556
|
-
for (const cap of entry.capabilities) {
|
|
3557
|
-
if (cap.urn.accepts(request)) {
|
|
3558
|
-
const specificity = cap.urn.specificity();
|
|
3559
|
-
if (bestSpecificity === -1 || specificity > bestSpecificity) {
|
|
3560
|
-
bestCap = cap;
|
|
3561
|
-
bestSpecificity = specificity;
|
|
3562
|
-
}
|
|
3563
|
-
break; // Found match for this entry
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
if (bestCap === null) {
|
|
3569
|
-
return null;
|
|
3570
|
-
}
|
|
3571
|
-
return { cap: bestCap, specificity: bestSpecificity };
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
/**
|
|
3575
|
-
* Build a directed graph from all capabilities across all registries.
|
|
3576
|
-
* The graph represents all possible conversions where:
|
|
3577
|
-
* - Nodes are media URNs (e.g., "media:string", "media:binary")
|
|
3578
|
-
* - Edges are capabilities that convert from one media URN to another
|
|
3579
|
-
* @returns {CapGraph} The capability graph
|
|
3580
|
-
*/
|
|
3581
|
-
graph() {
|
|
3582
|
-
return CapGraph.buildFromRegistries(this.registries);
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
3143
|
|
|
3586
3144
|
// ============================================================================
|
|
3587
3145
|
// CAP GRAPH - Directed graph of capability conversions
|
|
@@ -3669,25 +3227,6 @@ class CapGraph {
|
|
|
3669
3227
|
this.incoming.get(toUrn).push(edgeIndex);
|
|
3670
3228
|
}
|
|
3671
3229
|
|
|
3672
|
-
/**
|
|
3673
|
-
* Build a graph from multiple registries.
|
|
3674
|
-
* @param {Array<{name: string, registry: CapMatrix}>} registries
|
|
3675
|
-
* @returns {CapGraph}
|
|
3676
|
-
*/
|
|
3677
|
-
static buildFromRegistries(registries) {
|
|
3678
|
-
const graph = new CapGraph();
|
|
3679
|
-
|
|
3680
|
-
for (const { name, registry } of registries) {
|
|
3681
|
-
for (const entry of registry.sets.values()) {
|
|
3682
|
-
for (const cap of entry.capabilities) {
|
|
3683
|
-
graph.addCap(cap, name);
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
return graph;
|
|
3689
|
-
}
|
|
3690
|
-
|
|
3691
3230
|
/**
|
|
3692
3231
|
* Get all nodes (media URNs) in the graph.
|
|
3693
3232
|
* @returns {Set<string>}
|
|
@@ -5681,11 +5220,6 @@ module.exports = {
|
|
|
5681
5220
|
mediaUrnForType,
|
|
5682
5221
|
modelAvailabilityUrn,
|
|
5683
5222
|
modelPathUrn,
|
|
5684
|
-
CapMatrixError,
|
|
5685
|
-
CapMatrix,
|
|
5686
|
-
BestCapSetMatch,
|
|
5687
|
-
CompositeCapSet,
|
|
5688
|
-
CapBlock,
|
|
5689
5223
|
CapGraphEdge,
|
|
5690
5224
|
CapGraphStats,
|
|
5691
5225
|
CapGraph,
|
package/capdag.test.js
CHANGED
|
@@ -7,7 +7,6 @@ const {
|
|
|
7
7
|
MediaUrn, MediaUrnError, MediaUrnErrorCodes,
|
|
8
8
|
Cap, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
|
|
9
9
|
resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
|
|
10
|
-
CapMatrixError, CapMatrix, BestCapSetMatch, CompositeCapSet, CapBlock,
|
|
11
10
|
CartridgeInfo, CartridgeCapSummary, CartridgeSuggestion, CartridgeRepoClient, CartridgeRepoServer,
|
|
12
11
|
CapGraphEdge, CapGraphStats, CapGraph,
|
|
13
12
|
StdinSource, StdinSourceKind,
|
|
@@ -112,20 +111,6 @@ function testUrn(tags) {
|
|
|
112
111
|
return `cap:in="${MEDIA_VOID}";${tags};out="${MEDIA_OBJECT}"`;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
// Mock CapSet for testing
|
|
116
|
-
class MockCapSet {
|
|
117
|
-
constructor(name) {
|
|
118
|
-
this.name = name;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async executeCap(capUrn, args) {
|
|
122
|
-
return {
|
|
123
|
-
binaryOutput: null,
|
|
124
|
-
textOutput: `Mock response from ${this.name}`
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
114
|
// Helper to create a Cap for testing
|
|
130
115
|
function makeCap(urnString, title) {
|
|
131
116
|
const capUrn = CapUrn.fromString(urnString);
|
|
@@ -139,14 +124,6 @@ function makeGraphCap(inUrn, outUrn, title) {
|
|
|
139
124
|
return new Cap(capUrn, title, 'convert', title);
|
|
140
125
|
}
|
|
141
126
|
|
|
142
|
-
// Helper to create a test URN for matrix tests
|
|
143
|
-
function matrixTestUrn(tags) {
|
|
144
|
-
if (!tags) {
|
|
145
|
-
return 'cap:in="media:void";out="media:object"';
|
|
146
|
-
}
|
|
147
|
-
return `cap:in="media:void";out="media:object";${tags}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
127
|
// ============================================================================
|
|
151
128
|
// cap_urn.rs: TEST001-TEST050, TEST890-TEST891
|
|
152
129
|
// ============================================================================
|
|
@@ -1167,357 +1144,72 @@ function test110_multipleExtensions() {
|
|
|
1167
1144
|
}
|
|
1168
1145
|
|
|
1169
1146
|
// ============================================================================
|
|
1170
|
-
//
|
|
1147
|
+
// cap_graph: browse-mode API used by cap-graph-renderer.js
|
|
1148
|
+
//
|
|
1149
|
+
// The renderer builds its browse graph by:
|
|
1150
|
+
// const capGraph = new CapGraph();
|
|
1151
|
+
// for each cap in /api/capabilities: capGraph.addCap(cap, 'registry');
|
|
1152
|
+
// ... then reads capGraph.edges / getOutgoing(urn) / etc.
|
|
1153
|
+
//
|
|
1154
|
+
// These tests lock in that specific contract. They do NOT cover
|
|
1155
|
+
// buildFromRegistries / CapMatrix / CapBlock — all deleted with the dead
|
|
1156
|
+
// in-process dispatch stack.
|
|
1171
1157
|
// ============================================================================
|
|
1172
1158
|
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const providerCap = makeCap(
|
|
1180
|
-
'cap:in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1181
|
-
'Provider Thumbnail Generator (generic)'
|
|
1182
|
-
);
|
|
1183
|
-
providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
|
|
1184
|
-
|
|
1185
|
-
const cartridgeHost = new MockCapSet('cartridge');
|
|
1186
|
-
const cartridgeCap = makeCap(
|
|
1187
|
-
'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1188
|
-
'Cartridge PDF Thumbnail Generator (specific)'
|
|
1189
|
-
);
|
|
1190
|
-
cartridgeRegistry.registerCapSet('cartridge', cartridgeHost, [cartridgeCap]);
|
|
1191
|
-
|
|
1192
|
-
const composite = new CapBlock();
|
|
1193
|
-
composite.addRegistry('providers', providerRegistry);
|
|
1194
|
-
composite.addRegistry('cartridges', cartridgeRegistry);
|
|
1195
|
-
|
|
1196
|
-
const request = 'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"';
|
|
1197
|
-
const best = composite.findBestCapSet(request);
|
|
1198
|
-
|
|
1199
|
-
assertEqual(best.registryName, 'cartridges', 'More specific cartridge should win');
|
|
1200
|
-
assertEqual(best.cap.title, 'Cartridge PDF Thumbnail Generator (specific)', 'Should get cartridge cap');
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// TEST118: Test selecting best cap set based on specificity ranking With is_dispatchable semantics: - Provider must satisfy ALL request constraints - General request matches specific provider (provider refines request) - Specific request does NOT match general provider (provider lacks constraints)
|
|
1204
|
-
function test118_capBlockTieGoesToFirst() {
|
|
1205
|
-
const registry1 = new CapMatrix();
|
|
1206
|
-
const registry2 = new CapMatrix();
|
|
1207
|
-
|
|
1208
|
-
const host1 = new MockCapSet('host1');
|
|
1209
|
-
const cap1 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 1 Cap');
|
|
1210
|
-
registry1.registerCapSet('host1', host1, [cap1]);
|
|
1211
|
-
|
|
1212
|
-
const host2 = new MockCapSet('host2');
|
|
1213
|
-
const cap2 = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Registry 2 Cap');
|
|
1214
|
-
registry2.registerCapSet('host2', host2, [cap2]);
|
|
1215
|
-
|
|
1216
|
-
const composite = new CapBlock();
|
|
1217
|
-
composite.addRegistry('first', registry1);
|
|
1218
|
-
composite.addRegistry('second', registry2);
|
|
1219
|
-
|
|
1220
|
-
const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;op=generate'));
|
|
1221
|
-
assertEqual(best.registryName, 'first', 'On tie, first registry should win');
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// TEST119: Test invalid URN returns InvalidUrn error
|
|
1225
|
-
function test119_capBlockPollsAll() {
|
|
1226
|
-
const registry1 = new CapMatrix();
|
|
1227
|
-
const registry2 = new CapMatrix();
|
|
1228
|
-
const registry3 = new CapMatrix();
|
|
1229
|
-
|
|
1230
|
-
const host1 = new MockCapSet('host1');
|
|
1231
|
-
const cap1 = makeCap(matrixTestUrn('op=different'), 'Registry 1');
|
|
1232
|
-
registry1.registerCapSet('host1', host1, [cap1]);
|
|
1233
|
-
|
|
1234
|
-
const host2 = new MockCapSet('host2');
|
|
1235
|
-
const cap2 = makeCap(matrixTestUrn('op=generate'), 'Registry 2');
|
|
1236
|
-
registry2.registerCapSet('host2', host2, [cap2]);
|
|
1237
|
-
|
|
1238
|
-
const host3 = new MockCapSet('host3');
|
|
1239
|
-
const cap3 = makeCap(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'), 'Registry 3');
|
|
1240
|
-
registry3.registerCapSet('host3', host3, [cap3]);
|
|
1241
|
-
|
|
1242
|
-
const composite = new CapBlock();
|
|
1243
|
-
composite.addRegistry('r1', registry1);
|
|
1244
|
-
composite.addRegistry('r2', registry2);
|
|
1245
|
-
composite.addRegistry('r3', registry3);
|
|
1246
|
-
|
|
1247
|
-
const best = composite.findBestCapSet(matrixTestUrn('ext=pdf;format=thumbnail;op=generate'));
|
|
1248
|
-
assertEqual(best.registryName, 'r3', 'Most specific registry should win');
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// TEST120: Test accepts_request checks if registry can handle a capability request
|
|
1252
|
-
function test120_capBlockNoMatch() {
|
|
1253
|
-
const registry = new CapMatrix();
|
|
1254
|
-
const composite = new CapBlock();
|
|
1255
|
-
composite.addRegistry('empty', registry);
|
|
1256
|
-
|
|
1257
|
-
try {
|
|
1258
|
-
composite.findBestCapSet(matrixTestUrn('op=nonexistent'));
|
|
1259
|
-
throw new Error('Expected error for non-matching capability');
|
|
1260
|
-
} catch (e) {
|
|
1261
|
-
assert(e instanceof CapMatrixError, 'Should be CapMatrixError');
|
|
1262
|
-
assertEqual(e.type, 'NoSetsFound', 'Should be NoSetsFound error');
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// TEST121: Test CapBlock selects more specific cap over less specific regardless of registry order
|
|
1267
|
-
function test121_capBlockFallbackScenario() {
|
|
1268
|
-
const providerRegistry = new CapMatrix();
|
|
1269
|
-
const cartridgeRegistry = new CapMatrix();
|
|
1270
|
-
|
|
1271
|
-
const providerHost = new MockCapSet('provider_fallback');
|
|
1272
|
-
const providerCap = makeCap(
|
|
1273
|
-
'cap:in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1274
|
-
'Generic Thumbnail Provider'
|
|
1275
|
-
);
|
|
1276
|
-
providerRegistry.registerCapSet('provider_fallback', providerHost, [providerCap]);
|
|
1277
|
-
|
|
1278
|
-
const cartridgeHost = new MockCapSet('pdf_cartridge');
|
|
1279
|
-
const cartridgeCap = makeCap(
|
|
1280
|
-
'cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"',
|
|
1281
|
-
'PDF Thumbnail Cartridge'
|
|
1282
|
-
);
|
|
1283
|
-
cartridgeRegistry.registerCapSet('pdf_cartridge', cartridgeHost, [cartridgeCap]);
|
|
1284
|
-
|
|
1285
|
-
const composite = new CapBlock();
|
|
1286
|
-
composite.addRegistry('providers', providerRegistry);
|
|
1287
|
-
composite.addRegistry('cartridges', cartridgeRegistry);
|
|
1288
|
-
|
|
1289
|
-
// PDF request -> cartridge wins
|
|
1290
|
-
const best = composite.findBestCapSet('cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
1291
|
-
assertEqual(best.registryName, 'cartridges', 'Cartridge should win for PDF');
|
|
1292
|
-
|
|
1293
|
-
// WAV request -> provider wins (fallback)
|
|
1294
|
-
const bestWav = composite.findBestCapSet('cap:ext=wav;in="media:binary";op=generate_thumbnail;out="media:binary"');
|
|
1295
|
-
assertEqual(bestWav.registryName, 'providers', 'Provider should win for wav (fallback)');
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// TEST122: Test CapBlock breaks specificity ties by first registered registry
|
|
1299
|
-
function test122_capBlockCanMethod() {
|
|
1300
|
-
const providerRegistry = new CapMatrix();
|
|
1301
|
-
const providerHost = new MockCapSet('test_provider');
|
|
1302
|
-
const providerCap = makeCap(matrixTestUrn('ext=pdf;op=generate'), 'Test Provider');
|
|
1303
|
-
providerRegistry.registerCapSet('test_provider', providerHost, [providerCap]);
|
|
1304
|
-
|
|
1305
|
-
const composite = new CapBlock();
|
|
1306
|
-
composite.addRegistry('providers', providerRegistry);
|
|
1307
|
-
|
|
1308
|
-
const result = composite.can(matrixTestUrn('ext=pdf;op=generate'));
|
|
1309
|
-
assert(result.cap !== null, 'Should return cap');
|
|
1310
|
-
assert(result.compositeHost instanceof CompositeCapSet, 'Should return CompositeCapSet');
|
|
1311
|
-
assert(composite.acceptsRequest(matrixTestUrn('ext=pdf;op=generate')), 'Should accept matching cap');
|
|
1312
|
-
assert(!composite.acceptsRequest(matrixTestUrn('op=nonexistent')), 'Should not accept non-matching cap');
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// TEST123: Test CapBlock polls all registries to find most specific match
|
|
1316
|
-
function test123_capBlockRegistryManagement() {
|
|
1317
|
-
const composite = new CapBlock();
|
|
1318
|
-
const registry1 = new CapMatrix();
|
|
1319
|
-
const registry2 = new CapMatrix();
|
|
1320
|
-
|
|
1321
|
-
composite.addRegistry('r1', registry1);
|
|
1322
|
-
composite.addRegistry('r2', registry2);
|
|
1323
|
-
assertEqual(composite.getRegistryNames().length, 2, 'Should have 2 registries');
|
|
1159
|
+
// Add a cap and check it becomes an edge with from/to nodes and carries the
|
|
1160
|
+
// registry name we passed. This is exactly the shape the renderer depends on.
|
|
1161
|
+
function testCapGraphAddCapPopulatesEdgesAndNodes() {
|
|
1162
|
+
const graph = new CapGraph();
|
|
1163
|
+
const cap = makeGraphCap('media:pdf', 'media:textable', 'PDF to Text');
|
|
1164
|
+
graph.addCap(cap, 'registry');
|
|
1324
1165
|
|
|
1325
|
-
|
|
1166
|
+
const edges = graph.getEdges();
|
|
1167
|
+
assertEqual(edges.length, 1, 'Graph must have one edge after a single addCap');
|
|
1168
|
+
assertEqual(edges[0].fromUrn, 'media:pdf', 'Edge fromUrn must be cap in_spec');
|
|
1169
|
+
assertEqual(edges[0].toUrn, 'media:textable', 'Edge toUrn must be cap out_spec');
|
|
1170
|
+
assertEqual(edges[0].registryName, 'registry', 'Edge must carry the registry name passed to addCap');
|
|
1326
1171
|
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
assertEqual(composite.getRegistry('nonexistent'), null, 'Should return null for non-existent');
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// TEST124: Test CapBlock returns error when no registries match the request
|
|
1335
|
-
function test124_capGraphBasicConstruction() {
|
|
1336
|
-
const registry = new CapMatrix();
|
|
1337
|
-
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1338
|
-
|
|
1339
|
-
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1340
|
-
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1341
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1342
|
-
|
|
1343
|
-
const cube = new CapBlock();
|
|
1344
|
-
cube.addRegistry('converters', registry);
|
|
1345
|
-
|
|
1346
|
-
const graph = cube.graph();
|
|
1347
|
-
assertEqual(graph.getNodes().size, 3, 'Expected 3 nodes');
|
|
1348
|
-
assertEqual(graph.getEdges().length, 2, 'Expected 2 edges');
|
|
1349
|
-
assertEqual(graph.stats().nodeCount, 3, 'Expected 3 nodes in stats');
|
|
1350
|
-
assertEqual(graph.stats().edgeCount, 2, 'Expected 2 edges in stats');
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// TEST125: Test CapBlock prefers specific cartridge over generic provider fallback
|
|
1354
|
-
function test125_capGraphOutgoingIncoming() {
|
|
1355
|
-
const registry = new CapMatrix();
|
|
1356
|
-
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1357
|
-
|
|
1358
|
-
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1359
|
-
const cap2 = makeGraphCap('media:binary', 'media:object', 'Binary to Object');
|
|
1360
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1361
|
-
|
|
1362
|
-
const cube = new CapBlock();
|
|
1363
|
-
cube.addRegistry('converters', registry);
|
|
1364
|
-
const graph = cube.graph();
|
|
1365
|
-
|
|
1366
|
-
assertEqual(graph.getOutgoing('media:binary').length, 2, 'binary should have 2 outgoing');
|
|
1367
|
-
assertEqual(graph.getIncoming('media:string').length, 1, 'string should have 1 incoming');
|
|
1368
|
-
assertEqual(graph.getIncoming('media:object').length, 1, 'object should have 1 incoming');
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// TEST126: Test composite can method returns CapCaller for capability execution
|
|
1372
|
-
function test126_capGraphCanConvert() {
|
|
1373
|
-
const registry = new CapMatrix();
|
|
1374
|
-
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1375
|
-
|
|
1376
|
-
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1377
|
-
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1378
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1379
|
-
|
|
1380
|
-
const cube = new CapBlock();
|
|
1381
|
-
cube.addRegistry('converters', registry);
|
|
1382
|
-
const graph = cube.graph();
|
|
1383
|
-
|
|
1384
|
-
assert(graph.canConvert('media:binary', 'media:string'), 'Direct conversion');
|
|
1385
|
-
assert(graph.canConvert('media:string', 'media:object'), 'Direct conversion');
|
|
1386
|
-
assert(graph.canConvert('media:binary', 'media:object'), 'Transitive conversion');
|
|
1387
|
-
assert(graph.canConvert('media:binary', 'media:binary'), 'Same spec');
|
|
1388
|
-
assert(!graph.canConvert('media:object', 'media:binary'), 'Impossible conversion');
|
|
1389
|
-
assert(!graph.canConvert('media:nonexistent', 'media:string'), 'Nonexistent node');
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// TEST127: Test CapGraph adds nodes and edges from capability definitions
|
|
1393
|
-
function test127_capGraphFindPath() {
|
|
1394
|
-
const registry = new CapMatrix();
|
|
1395
|
-
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1396
|
-
|
|
1397
|
-
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1398
|
-
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1399
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2]);
|
|
1400
|
-
|
|
1401
|
-
const cube = new CapBlock();
|
|
1402
|
-
cube.addRegistry('converters', registry);
|
|
1403
|
-
const graph = cube.graph();
|
|
1404
|
-
|
|
1405
|
-
// Direct path
|
|
1406
|
-
let path = graph.findPath('media:binary', 'media:string');
|
|
1407
|
-
assert(path !== null, 'Should find direct path');
|
|
1408
|
-
assertEqual(path.length, 1, 'Direct path length should be 1');
|
|
1409
|
-
|
|
1410
|
-
// Transitive path
|
|
1411
|
-
path = graph.findPath('media:binary', 'media:object');
|
|
1412
|
-
assert(path !== null, 'Should find transitive path');
|
|
1413
|
-
assertEqual(path.length, 2, 'Transitive path length should be 2');
|
|
1414
|
-
|
|
1415
|
-
// No path
|
|
1416
|
-
path = graph.findPath('media:object', 'media:binary');
|
|
1417
|
-
assertEqual(path, null, 'Should not find impossible path');
|
|
1418
|
-
|
|
1419
|
-
// Same spec
|
|
1420
|
-
path = graph.findPath('media:binary', 'media:binary');
|
|
1421
|
-
assert(path !== null, 'Same spec should return empty path');
|
|
1422
|
-
assertEqual(path.length, 0, 'Same spec path should be empty');
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// TEST128: Test CapGraph tracks outgoing and incoming edges for spec conversions
|
|
1426
|
-
function test128_capGraphFindAllPaths() {
|
|
1427
|
-
const registry = new CapMatrix();
|
|
1428
|
-
const mockHost = { executeCap: async () => ({ textOutput: 'mock' }) };
|
|
1429
|
-
|
|
1430
|
-
const cap1 = makeGraphCap('media:binary', 'media:string', 'Binary to String');
|
|
1431
|
-
const cap2 = makeGraphCap('media:string', 'media:object', 'String to Object');
|
|
1432
|
-
const cap3 = makeGraphCap('media:binary', 'media:object', 'Binary to Object (direct)');
|
|
1433
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
|
|
1434
|
-
|
|
1435
|
-
const cube = new CapBlock();
|
|
1436
|
-
cube.addRegistry('converters', registry);
|
|
1437
|
-
const graph = cube.graph();
|
|
1438
|
-
|
|
1439
|
-
const paths = graph.findAllPaths('media:binary', 'media:object', 3);
|
|
1440
|
-
assertEqual(paths.length, 2, 'Should find 2 paths');
|
|
1441
|
-
assertEqual(paths[0].length, 1, 'Shortest path first (direct)');
|
|
1442
|
-
assertEqual(paths[1].length, 2, 'Longer path second (via string)');
|
|
1172
|
+
const nodes = graph.getNodes();
|
|
1173
|
+
assert(nodes.has('media:pdf'), 'from_spec must appear as a node');
|
|
1174
|
+
assert(nodes.has('media:textable'), 'to_spec must appear as a node');
|
|
1443
1175
|
}
|
|
1444
1176
|
|
|
1445
|
-
//
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
const mockHost2 = { executeCap: async () => ({ textOutput: 'mock2' }) };
|
|
1177
|
+
// getOutgoing takes a concrete source URN and returns edges whose from_spec
|
|
1178
|
+
// the source conforms to. It must NOT be a plain string lookup.
|
|
1179
|
+
function testCapGraphGetOutgoingConformsToMatching() {
|
|
1180
|
+
const graph = new CapGraph();
|
|
1181
|
+
graph.addCap(makeGraphCap('media:textable', 'media:embedding-vector', 'Embed text'), 'registry');
|
|
1451
1182
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1183
|
+
// 'media:txt;textable' conforms to 'media:textable' — renderer relies on
|
|
1184
|
+
// this for the browse-mode fan-out from specific source URNs to caps that
|
|
1185
|
+
// accept a broader media pattern.
|
|
1186
|
+
const outgoingFromSpecific = graph.getOutgoing('media:txt;textable');
|
|
1187
|
+
assertEqual(outgoingFromSpecific.length, 1, 'Specific URN must match broader cap input');
|
|
1455
1188
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1189
|
+
// The broad URN still matches its own edge.
|
|
1190
|
+
const outgoingFromBroad = graph.getOutgoing('media:textable');
|
|
1191
|
+
assertEqual(outgoingFromBroad.length, 1, 'Exact URN must match');
|
|
1458
1192
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
const graph = cube.graph();
|
|
1463
|
-
|
|
1464
|
-
const edges = graph.getDirectEdges('media:binary', 'media:string');
|
|
1465
|
-
assertEqual(edges.length, 2, 'Expected 2 direct edges');
|
|
1466
|
-
assertEqual(edges[0].cap.title, 'PDF Binary to String', 'More specific edge first');
|
|
1467
|
-
assert(edges[0].specificity > edges[1].specificity, 'First edge should have higher specificity');
|
|
1193
|
+
// A totally unrelated URN must not match.
|
|
1194
|
+
const outgoingFromUnrelated = graph.getOutgoing('media:image;png');
|
|
1195
|
+
assertEqual(outgoingFromUnrelated.length, 0, 'Unrelated URN must not match');
|
|
1468
1196
|
}
|
|
1469
1197
|
|
|
1470
|
-
//
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1198
|
+
// Each edge must carry the registry name it was added with. This is how
|
|
1199
|
+
// the renderer colours/groups edges by provenance in browse mode.
|
|
1200
|
+
function testCapGraphDistinctRegistryNames() {
|
|
1201
|
+
const graph = new CapGraph();
|
|
1202
|
+
graph.addCap(makeGraphCap('media:pdf', 'media:textable', 'PDF to Text'), 'providers');
|
|
1203
|
+
graph.addCap(makeGraphCap('media:textable', 'media:embedding-vector', 'Embed'), 'cartridges');
|
|
1474
1204
|
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
const cap3 = makeGraphCap('media:binary', 'media:json', 'Binary to JSON');
|
|
1478
|
-
registry.registerCapSet('converter', mockHost, [cap1, cap2, cap3]);
|
|
1205
|
+
const edges = graph.getEdges();
|
|
1206
|
+
assertEqual(edges.length, 2, 'Two caps must produce two edges');
|
|
1479
1207
|
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const stats = graph.stats();
|
|
1484
|
-
|
|
1485
|
-
assertEqual(stats.nodeCount, 4, '4 unique nodes');
|
|
1486
|
-
assertEqual(stats.edgeCount, 3, '3 edges');
|
|
1487
|
-
assertEqual(stats.inputUrnCount, 2, '2 input URNs');
|
|
1488
|
-
assertEqual(stats.outputUrnCount, 3, '3 output URNs');
|
|
1208
|
+
const names = new Set(edges.map(e => e.registryName));
|
|
1209
|
+
assert(names.has('providers'), 'providers registry name must be preserved');
|
|
1210
|
+
assert(names.has('cartridges'), 'cartridges registry name must be preserved');
|
|
1489
1211
|
}
|
|
1490
1212
|
|
|
1491
|
-
// TEST131: Test CapGraph finds all conversion paths sorted by length
|
|
1492
|
-
function test131_capGraphWithCapBlock() {
|
|
1493
|
-
const providerRegistry = new CapMatrix();
|
|
1494
|
-
const cartridgeRegistry = new CapMatrix();
|
|
1495
|
-
const providerHost = { executeCap: async () => ({ textOutput: 'provider' }) };
|
|
1496
|
-
const cartridgeHost = { executeCap: async () => ({ textOutput: 'cartridge' }) };
|
|
1497
|
-
|
|
1498
|
-
const providerCap = makeGraphCap('media:binary', 'media:string', 'Provider Binary to String');
|
|
1499
|
-
providerRegistry.registerCapSet('provider', providerHost, [providerCap]);
|
|
1500
|
-
|
|
1501
|
-
const cartridgeCap = makeGraphCap('media:string', 'media:object', 'Cartridge String to Object');
|
|
1502
|
-
cartridgeRegistry.registerCapSet('cartridge', cartridgeHost, [cartridgeCap]);
|
|
1503
|
-
|
|
1504
|
-
const cube = new CapBlock();
|
|
1505
|
-
cube.addRegistry('providers', providerRegistry);
|
|
1506
|
-
cube.addRegistry('cartridges', cartridgeRegistry);
|
|
1507
|
-
const graph = cube.graph();
|
|
1508
|
-
|
|
1509
|
-
assert(graph.canConvert('media:binary', 'media:object'), 'Should convert across registries');
|
|
1510
|
-
const path = graph.findPath('media:binary', 'media:object');
|
|
1511
|
-
assert(path !== null, 'Should find path');
|
|
1512
|
-
assertEqual(path.length, 2, 'Path through 2 registries');
|
|
1513
|
-
assertEqual(path[0].registryName, 'providers', 'First edge from providers');
|
|
1514
|
-
assertEqual(path[1].registryName, 'cartridges', 'Second edge from cartridges');
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// TEST132: N/A (already covered by TEST129)
|
|
1518
|
-
// TEST133: N/A (already covered by TEST131)
|
|
1519
|
-
// TEST134: N/A (already covered by TEST130)
|
|
1520
|
-
|
|
1521
1213
|
// ============================================================================
|
|
1522
1214
|
// caller.rs: TEST156-TEST159
|
|
1523
1215
|
// ============================================================================
|
|
@@ -1930,73 +1622,6 @@ function testJS_stdinSourceNullData() {
|
|
|
1930
1622
|
assertEqual(source.data, null, 'Data should be null');
|
|
1931
1623
|
}
|
|
1932
1624
|
|
|
1933
|
-
function testJS_argsPassedToExecuteCap() {
|
|
1934
|
-
let receivedArgs = null;
|
|
1935
|
-
const mockHost = {
|
|
1936
|
-
executeCap: async (capUrn, args) => {
|
|
1937
|
-
receivedArgs = args;
|
|
1938
|
-
return { textOutput: 'ok' };
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
|
|
1942
|
-
const cap = new Cap(
|
|
1943
|
-
CapUrn.fromString('cap:in="media:void";op=test;out="media:string"'),
|
|
1944
|
-
'Test Cap',
|
|
1945
|
-
'test-command'
|
|
1946
|
-
);
|
|
1947
|
-
const registry = new CapMatrix();
|
|
1948
|
-
registry.registerCapSet('test', mockHost, [cap]);
|
|
1949
|
-
const cube = new CapBlock();
|
|
1950
|
-
cube.addRegistry('test', registry);
|
|
1951
|
-
|
|
1952
|
-
const args = [new CapArgumentValue('media:void', new Uint8Array([1, 2, 3]))];
|
|
1953
|
-
const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
|
|
1954
|
-
|
|
1955
|
-
return compositeHost.executeCap(
|
|
1956
|
-
'cap:in="media:void";op=test;out="media:string"',
|
|
1957
|
-
args
|
|
1958
|
-
).then(() => {
|
|
1959
|
-
assert(receivedArgs !== null, 'Should receive arguments');
|
|
1960
|
-
assert(Array.isArray(receivedArgs), 'Should receive array');
|
|
1961
|
-
assertEqual(receivedArgs.length, 1, 'Should have one argument');
|
|
1962
|
-
assertEqual(receivedArgs[0].mediaUrn, 'media:void', 'Correct mediaUrn');
|
|
1963
|
-
assertEqual(receivedArgs[0].value.length, 3, 'Correct data length');
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
function testJS_binaryArgPassedToExecuteCap() {
|
|
1968
|
-
let receivedArgs = null;
|
|
1969
|
-
const mockHost = {
|
|
1970
|
-
executeCap: async (capUrn, args) => {
|
|
1971
|
-
receivedArgs = args;
|
|
1972
|
-
return { textOutput: 'ok' };
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
|
-
const cap = new Cap(
|
|
1977
|
-
CapUrn.fromString('cap:in="media:void";op=test;out="media:string"'),
|
|
1978
|
-
'Test Cap',
|
|
1979
|
-
'test-command'
|
|
1980
|
-
);
|
|
1981
|
-
const registry = new CapMatrix();
|
|
1982
|
-
registry.registerCapSet('test', mockHost, [cap]);
|
|
1983
|
-
const cube = new CapBlock();
|
|
1984
|
-
cube.addRegistry('test', registry);
|
|
1985
|
-
|
|
1986
|
-
const binaryArg = new CapArgumentValue('media:pdf', new Uint8Array([0x89, 0x50, 0x4E, 0x47]));
|
|
1987
|
-
const { compositeHost } = cube.can('cap:in="media:void";op=test;out="media:string"');
|
|
1988
|
-
|
|
1989
|
-
return compositeHost.executeCap(
|
|
1990
|
-
'cap:in="media:void";op=test;out="media:string"',
|
|
1991
|
-
[binaryArg]
|
|
1992
|
-
).then(() => {
|
|
1993
|
-
assert(receivedArgs !== null, 'Should receive arguments');
|
|
1994
|
-
assertEqual(receivedArgs[0].mediaUrn, 'media:pdf', 'Correct mediaUrn');
|
|
1995
|
-
assertEqual(receivedArgs[0].value[0], 0x89, 'First byte check');
|
|
1996
|
-
assertEqual(receivedArgs[0].value.length, 4, 'Correct data length');
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
1625
|
function testJS_mediaSpecConstruction() {
|
|
2001
1626
|
const spec1 = new MediaSpec('text/plain', 'https://capdag.com/schema/str', null, 'String', null, 'media:string');
|
|
2002
1627
|
assertEqual(spec1.contentType, 'text/plain', 'Should have content type');
|
|
@@ -5547,24 +5172,13 @@ async function runTests() {
|
|
|
5547
5172
|
runTest('TEST109: extensions_with_metadata_and_validation', test109_extensionsWithMetadataAndValidation);
|
|
5548
5173
|
runTest('TEST110: multiple_extensions', test110_multipleExtensions);
|
|
5549
5174
|
|
|
5550
|
-
//
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
runTest('
|
|
5555
|
-
runTest('
|
|
5556
|
-
runTest('
|
|
5557
|
-
runTest('TEST122: cap_block_can_method', test122_capBlockCanMethod);
|
|
5558
|
-
runTest('TEST123: cap_block_registry_management', test123_capBlockRegistryManagement);
|
|
5559
|
-
runTest('TEST124: cap_graph_basic_construction', test124_capGraphBasicConstruction);
|
|
5560
|
-
runTest('TEST125: cap_graph_outgoing_incoming', test125_capGraphOutgoingIncoming);
|
|
5561
|
-
runTest('TEST126: cap_graph_can_convert', test126_capGraphCanConvert);
|
|
5562
|
-
runTest('TEST127: cap_graph_find_path', test127_capGraphFindPath);
|
|
5563
|
-
runTest('TEST128: cap_graph_find_all_paths', test128_capGraphFindAllPaths);
|
|
5564
|
-
runTest('TEST129: cap_graph_direct_edges_sorted_by_specificity', test129_capGraphGetDirectEdges);
|
|
5565
|
-
runTest('TEST130: cap_graph_stats', test130_capGraphStats);
|
|
5566
|
-
runTest('TEST131: cap_block_graph_integration', test131_capGraphWithCapBlock);
|
|
5567
|
-
console.log(' SKIP TEST132-134: N/A (already covered by TEST129-131)');
|
|
5175
|
+
// cap-graph-renderer.js uses CapGraph in browse mode (static registry from
|
|
5176
|
+
// /api/capabilities). These tests guard the minimal API the renderer relies
|
|
5177
|
+
// on: new CapGraph(), addCap(cap, registryName), getEdges(), getOutgoing().
|
|
5178
|
+
console.log('\n--- cap_graph (browse-mode API used by cap-graph-renderer) ---');
|
|
5179
|
+
runTest('cap_graph: add_cap_populates_edges_and_nodes', testCapGraphAddCapPopulatesEdgesAndNodes);
|
|
5180
|
+
runTest('cap_graph: get_outgoing_conforms_to_matching', testCapGraphGetOutgoingConformsToMatching);
|
|
5181
|
+
runTest('cap_graph: distinct_registry_names_recorded_per_edge', testCapGraphDistinctRegistryNames);
|
|
5568
5182
|
|
|
5569
5183
|
// caller.rs: TEST156-TEST159
|
|
5570
5184
|
console.log('\n--- caller.rs (StdinSource) ---');
|
|
@@ -5608,10 +5222,6 @@ async function runTests() {
|
|
|
5608
5222
|
runTest('JS: media_spec_documentation_propagates_through_resolve', testJS_mediaSpecDocumentationPropagatesThroughResolve);
|
|
5609
5223
|
runTest('JS: stdin_source_kind_constants', testJS_stdinSourceKindConstants);
|
|
5610
5224
|
runTest('JS: stdin_source_null_data', testJS_stdinSourceNullData);
|
|
5611
|
-
const p1 = runTest('JS: args_passed_to_executeCap', testJS_argsPassedToExecuteCap);
|
|
5612
|
-
if (p1) await p1;
|
|
5613
|
-
const p2 = runTest('JS: binary_arg_passed_to_executeCap', testJS_binaryArgPassedToExecuteCap);
|
|
5614
|
-
if (p2) await p2;
|
|
5615
5225
|
runTest('JS: media_spec_construction', testJS_mediaSpecConstruction);
|
|
5616
5226
|
|
|
5617
5227
|
// cartridge_repo: CartridgeRepoServer and CartridgeRepoClient tests
|
package/package.json
CHANGED