capdag 0.180.452 → 0.182.459
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-fab-renderer.js +31 -10
- package/capdag.js +338 -79
- package/capdag.test.js +213 -115
- package/package.json +2 -2
package/cap-fab-renderer.js
CHANGED
|
@@ -555,6 +555,26 @@ function buildStylesheet() {
|
|
|
555
555
|
selector: 'edge.active',
|
|
556
556
|
style: { 'width': 3, 'z-index': 1000 },
|
|
557
557
|
},
|
|
558
|
+
{
|
|
559
|
+
selector: 'edge.strand-shape-edge',
|
|
560
|
+
style: {
|
|
561
|
+
'line-style': 'dashed',
|
|
562
|
+
'width': 2,
|
|
563
|
+
'text-background-opacity': 0.92,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
selector: 'edge.strand-foreach-edge',
|
|
568
|
+
style: {
|
|
569
|
+
'target-arrow-shape': 'triangle',
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
selector: 'edge.strand-collect-edge',
|
|
574
|
+
style: {
|
|
575
|
+
'target-arrow-shape': 'tee',
|
|
576
|
+
},
|
|
577
|
+
},
|
|
558
578
|
{
|
|
559
579
|
selector: 'edge.faded',
|
|
560
580
|
style: { 'opacity': fadedEdgeOpacity },
|
|
@@ -1374,7 +1394,7 @@ function collapseStrandShapeTransitions(built) {
|
|
|
1374
1394
|
label: '',
|
|
1375
1395
|
title: '',
|
|
1376
1396
|
fullUrn: '',
|
|
1377
|
-
edgeClass: 'strand-cap-edge',
|
|
1397
|
+
edgeClass: 'strand-cap-edge strand-shape-edge strand-collect-edge',
|
|
1378
1398
|
color: bodyExitCapEdge ? bodyExitCapEdge.color : inEdge.color,
|
|
1379
1399
|
foreachEntry: false,
|
|
1380
1400
|
});
|
|
@@ -1399,15 +1419,16 @@ function collapseStrandShapeTransitions(built) {
|
|
|
1399
1419
|
e.edgeClass !== 'strand-collection');
|
|
1400
1420
|
edges = edges.concat(synthesizedExitEdges);
|
|
1401
1421
|
|
|
1402
|
-
// Step 2:
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1422
|
+
// Step 2: surface shape transitions as distinct edge semantics.
|
|
1423
|
+
// The cap edge that enters a foreach body keeps its original cap
|
|
1424
|
+
// label; only the edge styling changes. Collect bridges are
|
|
1425
|
+
// synthesized above as dedicated dashed edges.
|
|
1426
|
+
edges = edges.map(edge => {
|
|
1427
|
+
if (!edge.foreachEntry) return edge;
|
|
1428
|
+
return Object.assign({}, edge, {
|
|
1429
|
+
edgeClass: `${edge.edgeClass} strand-shape-edge strand-foreach-edge`,
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1411
1432
|
|
|
1412
1433
|
// Step 3: merge the trailing `step_N → output` edge when step_N
|
|
1413
1434
|
// and output represent the same media URN. The strand builder
|
package/capdag.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// Follows the exact same rules as Rust, Go, and Objective-C implementations
|
|
3
3
|
|
|
4
4
|
// Import TaggedUrn from the tagged-urn package
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
TaggedUrn,
|
|
7
|
+
valuesMatch: taggedUrnValuesMatch,
|
|
8
|
+
scoreTagValue
|
|
9
|
+
} = require('tagged-urn');
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Error types for Cap URN operations
|
|
@@ -30,7 +34,10 @@ const ErrorCodes = {
|
|
|
30
34
|
MISSING_OUT_SPEC: 11,
|
|
31
35
|
EMPTY_VALUE: 12,
|
|
32
36
|
INVALID_IN_SPEC: 13,
|
|
33
|
-
INVALID_OUT_SPEC: 14
|
|
37
|
+
INVALID_OUT_SPEC: 14,
|
|
38
|
+
INVALID_EFFECT: 15,
|
|
39
|
+
INVALID_EFFECT_APPLICATION: 16,
|
|
40
|
+
ILLEGAL_DECLARATION: 17
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
// Note: All parsing is delegated to TaggedUrn from tagged-urn-js
|
|
@@ -114,8 +121,8 @@ function validatePreservedDirectionSpec(spec, tagName) {
|
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
/**
|
|
117
|
-
* Functional category of a cap, derived from all
|
|
118
|
-
* `out`, and the remaining tags). The classification is **logical** —
|
|
124
|
+
* Functional category of a cap, derived from all four structural axes
|
|
125
|
+
* (`in`, `out`, `effect`, and the remaining tags). The classification is **logical** —
|
|
119
126
|
* the dispatch protocol does not branch on CapKind. Exposed so tools,
|
|
120
127
|
* UIs, planners, and tests can reason about a cap's role without
|
|
121
128
|
* re-deriving the rules.
|
|
@@ -124,15 +131,13 @@ function validatePreservedDirectionSpec(spec, tagName) {
|
|
|
124
131
|
* is the **top type** (universal wildcard). With those anchors the
|
|
125
132
|
* five kinds fall out:
|
|
126
133
|
*
|
|
127
|
-
* IDENTITY in=media:, out=media:, no other tags → A → A
|
|
134
|
+
* IDENTITY in=media:, out=media:, effect=none, no other tags → A → A
|
|
128
135
|
* SOURCE in=media:void, out!=void → () → B
|
|
129
136
|
* SINK in!=void, out=media:void → A → ()
|
|
130
137
|
* EFFECT in=media:void, out=media:void → () → ()
|
|
131
138
|
* TRANSFORM anything else
|
|
132
139
|
*
|
|
133
|
-
*
|
|
134
|
-
* tag specifies something on the third axis and demotes the morphism
|
|
135
|
-
* to a TRANSFORM whose in/out happen to be the wildcards.
|
|
140
|
+
* `cap:effect=none` is the categorical identity morphism.
|
|
136
141
|
*
|
|
137
142
|
* String values are snake_case to match other capdag enum
|
|
138
143
|
* serializations on the wire.
|
|
@@ -145,6 +150,50 @@ const CapKind = Object.freeze({
|
|
|
145
150
|
TRANSFORM: 'transform',
|
|
146
151
|
});
|
|
147
152
|
|
|
153
|
+
const CapEffect = Object.freeze({
|
|
154
|
+
DECLARED: 'declared',
|
|
155
|
+
NONE: 'none',
|
|
156
|
+
PATCH: 'patch',
|
|
157
|
+
ANY: '?',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function normalizeEffectValue(rawValue) {
|
|
161
|
+
if (rawValue === undefined || rawValue === null) return CapEffect.DECLARED;
|
|
162
|
+
if (rawValue === '?' || rawValue === '*') return CapEffect.ANY;
|
|
163
|
+
if (rawValue === CapEffect.DECLARED) return CapEffect.DECLARED;
|
|
164
|
+
if (rawValue === CapEffect.NONE) return CapEffect.NONE;
|
|
165
|
+
if (rawValue === CapEffect.PATCH) return CapEffect.PATCH;
|
|
166
|
+
if (rawValue === '') {
|
|
167
|
+
throw new CapUrnError(ErrorCodes.INVALID_EFFECT, "Empty value for 'effect' tag is not allowed");
|
|
168
|
+
}
|
|
169
|
+
throw new CapUrnError(
|
|
170
|
+
ErrorCodes.INVALID_EFFECT,
|
|
171
|
+
`Unsupported effect '${rawValue}'. Supported values are declared, none, patch, or explicit unconstrained ?effect/effect=*`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateNonStructuralTags(tags) {
|
|
176
|
+
try {
|
|
177
|
+
new TaggedUrn('cap', tags, true);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const msg = error && error.message ? error.message : String(error);
|
|
180
|
+
const msgLower = msg.toLowerCase();
|
|
181
|
+
if (msgLower.includes('duplicate')) {
|
|
182
|
+
throw new CapUrnError(ErrorCodes.DUPLICATE_KEY, msg);
|
|
183
|
+
}
|
|
184
|
+
if (msgLower.includes('numeric') || msgLower.includes('purely numeric')) {
|
|
185
|
+
throw new CapUrnError(ErrorCodes.NUMERIC_KEY, msg);
|
|
186
|
+
}
|
|
187
|
+
if (msgLower.includes('invalid character')) {
|
|
188
|
+
throw new CapUrnError(ErrorCodes.INVALID_CHARACTER, msg);
|
|
189
|
+
}
|
|
190
|
+
if (msgLower.includes('escape')) {
|
|
191
|
+
throw new CapUrnError(ErrorCodes.INVALID_ESCAPE_SEQUENCE, msg);
|
|
192
|
+
}
|
|
193
|
+
throw new CapUrnError(ErrorCodes.INVALID_TAG_FORMAT, msg);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
148
197
|
class CapUrn {
|
|
149
198
|
// Per-axis weights for cap-URN specificity. Two orders of
|
|
150
199
|
// magnitude separate each axis to keep them in distinct digit
|
|
@@ -157,19 +206,23 @@ class CapUrn {
|
|
|
157
206
|
* Create a new CapUrn with direction specs.
|
|
158
207
|
* @param {string} inSpec - Input media URN (e.g., "media:void")
|
|
159
208
|
* @param {string} outSpec - Output media URN (e.g., "media:object")
|
|
160
|
-
* @param {
|
|
209
|
+
* @param {string} effect - Runtime media identity effect
|
|
210
|
+
* @param {Object} tags - Other tags (must NOT contain 'in', 'out', or 'effect')
|
|
161
211
|
*/
|
|
162
|
-
constructor(inSpec, outSpec, tags = {}) {
|
|
212
|
+
constructor(inSpec, outSpec, effect = CapEffect.DECLARED, tags = {}) {
|
|
163
213
|
this.inSpec = canonicalizeDirectionSpec(inSpec, 'in');
|
|
164
214
|
this.outSpec = canonicalizeDirectionSpec(outSpec, 'out');
|
|
215
|
+
this.effectValue = normalizeEffectValue(effect);
|
|
165
216
|
this.tags = {};
|
|
166
|
-
// Copy tags, filtering out any
|
|
217
|
+
// Copy tags, filtering out any structural coordinates that might have slipped through
|
|
167
218
|
for (const [key, value] of Object.entries(tags)) {
|
|
168
219
|
const keyLower = key.toLowerCase();
|
|
169
|
-
if (keyLower !== 'in' && keyLower !== 'out') {
|
|
220
|
+
if (keyLower !== 'in' && keyLower !== 'out' && keyLower !== 'effect') {
|
|
170
221
|
this.tags[keyLower] = value;
|
|
171
222
|
}
|
|
172
223
|
}
|
|
224
|
+
validateNonStructuralTags(this.tags);
|
|
225
|
+
this._validateAdmissible();
|
|
173
226
|
}
|
|
174
227
|
|
|
175
228
|
/**
|
|
@@ -188,6 +241,14 @@ class CapUrn {
|
|
|
188
241
|
return this.outSpec;
|
|
189
242
|
}
|
|
190
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Get the canonical effect coordinate.
|
|
246
|
+
* @returns {string}
|
|
247
|
+
*/
|
|
248
|
+
getEffect() {
|
|
249
|
+
return this.effectValue;
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
/**
|
|
192
253
|
* Parse the in= spec into a MediaUrn.
|
|
193
254
|
* @returns {MediaUrn} The input media URN
|
|
@@ -207,15 +268,11 @@ class CapUrn {
|
|
|
207
268
|
}
|
|
208
269
|
|
|
209
270
|
/**
|
|
210
|
-
* Functional category of this cap, derived from all
|
|
211
|
-
* `in
|
|
212
|
-
* the tags (the operation/metadata axis — `this.tags` does NOT
|
|
213
|
-
* include in/out, those live in this.inSpec/this.outSpec).
|
|
271
|
+
* Functional category of this cap, derived from all four axes:
|
|
272
|
+
* `in`, `out`, `effect`, and the remaining y-axis tags.
|
|
214
273
|
*
|
|
215
|
-
* Identity requires
|
|
216
|
-
*
|
|
217
|
-
* there are no other tags. Source/Sink/Effect are decided by void
|
|
218
|
-
* on either directional axis. Anything else is Transform.
|
|
274
|
+
* Identity requires top/top, no other tags, and explicit
|
|
275
|
+
* `effect=none`.
|
|
219
276
|
*
|
|
220
277
|
* @returns {string} A {@link CapKind} value (snake_case string).
|
|
221
278
|
* @throws {MediaUrnError} If either side is not a valid media URN
|
|
@@ -232,7 +289,9 @@ class CapUrn {
|
|
|
232
289
|
const outTop = outMedia.isTop();
|
|
233
290
|
const noExtraTags = Object.keys(this.tags).length === 0;
|
|
234
291
|
|
|
235
|
-
if (inTop && outTop && noExtraTags)
|
|
292
|
+
if (inTop && outTop && noExtraTags) {
|
|
293
|
+
if (this.effectValue === CapEffect.NONE) return CapKind.IDENTITY;
|
|
294
|
+
}
|
|
236
295
|
if (inVoid && outVoid) return CapKind.EFFECT;
|
|
237
296
|
if (inVoid) return CapKind.SOURCE;
|
|
238
297
|
if (outVoid) return CapKind.SINK;
|
|
@@ -293,20 +352,23 @@ class CapUrn {
|
|
|
293
352
|
const inSpec = processDirectionTag(taggedUrn, 'in');
|
|
294
353
|
const outSpec = processDirectionTag(taggedUrn, 'out');
|
|
295
354
|
|
|
296
|
-
|
|
355
|
+
const effect = normalizeEffectValue(taggedUrn.getTag('effect'));
|
|
356
|
+
|
|
357
|
+
// Build remaining tags (excluding in/out/effect)
|
|
297
358
|
const remainingTags = {};
|
|
298
359
|
for (const [key, value] of Object.entries(taggedUrn.tags)) {
|
|
299
|
-
if (key !== 'in' && key !== 'out') {
|
|
360
|
+
if (key !== 'in' && key !== 'out' && key !== 'effect') {
|
|
300
361
|
remainingTags[key] = value;
|
|
301
362
|
}
|
|
302
363
|
}
|
|
303
364
|
|
|
304
|
-
return new CapUrn(inSpec, outSpec, remainingTags);
|
|
365
|
+
return new CapUrn(inSpec, outSpec, effect, remainingTags);
|
|
305
366
|
}
|
|
306
367
|
|
|
307
368
|
/**
|
|
308
369
|
* Create a Cap URN from a tags object.
|
|
309
|
-
*
|
|
370
|
+
* Missing structural coordinates default exactly as they do in string
|
|
371
|
+
* parsing (`in=media:`, `out=media:`, `effect=declared`).
|
|
310
372
|
*
|
|
311
373
|
* @param {Object} tags - Object containing all tags including 'in' and 'out'
|
|
312
374
|
* @returns {CapUrn} The parsed Cap URN
|
|
@@ -330,16 +392,18 @@ class CapUrn {
|
|
|
330
392
|
throw new CapUrnError(ErrorCodes.INVALID_OUT_SPEC, "Empty value for 'out' tag is not allowed");
|
|
331
393
|
}
|
|
332
394
|
|
|
333
|
-
|
|
395
|
+
const effect = normalizeEffectValue(tags['effect'] || tags['EFFECT']);
|
|
396
|
+
|
|
397
|
+
// Build remaining tags (excluding in/out/effect)
|
|
334
398
|
const remainingTags = {};
|
|
335
399
|
for (const [key, value] of Object.entries(tags)) {
|
|
336
400
|
const keyLower = key.toLowerCase();
|
|
337
|
-
if (keyLower !== 'in' && keyLower !== 'out') {
|
|
401
|
+
if (keyLower !== 'in' && keyLower !== 'out' && keyLower !== 'effect') {
|
|
338
402
|
remainingTags[keyLower] = value;
|
|
339
403
|
}
|
|
340
404
|
}
|
|
341
405
|
|
|
342
|
-
return new CapUrn(inSpec, outSpec, remainingTags);
|
|
406
|
+
return new CapUrn(inSpec, outSpec, effect, remainingTags);
|
|
343
407
|
}
|
|
344
408
|
|
|
345
409
|
/**
|
|
@@ -352,11 +416,8 @@ class CapUrn {
|
|
|
352
416
|
*/
|
|
353
417
|
toString() {
|
|
354
418
|
// `in` and `out` segments are emitted only when they refine beyond
|
|
355
|
-
// the trivial wildcard `media:`.
|
|
356
|
-
// `
|
|
357
|
-
// `cap:` — the bare identity URN. The canonicalizer collapses both
|
|
358
|
-
// written forms (`cap:` and `cap:in=media:;out=media:`) to one
|
|
359
|
-
// representative so byte-equality matches semantic identity.
|
|
419
|
+
// the trivial wildcard `media:`. Missing `effect` means the default
|
|
420
|
+
// `declared`; `effect=none` is preserved.
|
|
360
421
|
const allTags = { ...this.tags };
|
|
361
422
|
if (this.inSpec !== 'media:') {
|
|
362
423
|
allTags['in'] = this.inSpec;
|
|
@@ -364,6 +425,9 @@ class CapUrn {
|
|
|
364
425
|
if (this.outSpec !== 'media:') {
|
|
365
426
|
allTags['out'] = this.outSpec;
|
|
366
427
|
}
|
|
428
|
+
if (this.effectValue !== CapEffect.DECLARED) {
|
|
429
|
+
allTags['effect'] = this.effectValue;
|
|
430
|
+
}
|
|
367
431
|
|
|
368
432
|
const taggedUrn = new TaggedUrn('cap', allTags, true);
|
|
369
433
|
return taggedUrn.toString();
|
|
@@ -372,7 +436,8 @@ class CapUrn {
|
|
|
372
436
|
/**
|
|
373
437
|
* Get the value of a specific tag
|
|
374
438
|
* Key is normalized to lowercase for lookup
|
|
375
|
-
* Returns inSpec for "in" key, outSpec for "out" key
|
|
439
|
+
* Returns inSpec for "in" key, outSpec for "out" key, and the effect
|
|
440
|
+
* coordinate for "effect".
|
|
376
441
|
*
|
|
377
442
|
* @param {string} key - The tag key
|
|
378
443
|
* @returns {string|undefined} The tag value or undefined if not found
|
|
@@ -385,13 +450,16 @@ class CapUrn {
|
|
|
385
450
|
if (keyLower === 'out') {
|
|
386
451
|
return this.outSpec;
|
|
387
452
|
}
|
|
453
|
+
if (keyLower === 'effect') {
|
|
454
|
+
return this.effectValue;
|
|
455
|
+
}
|
|
388
456
|
return this.tags[keyLower];
|
|
389
457
|
}
|
|
390
458
|
|
|
391
459
|
/**
|
|
392
460
|
* Check if this cap has a specific tag with a specific value
|
|
393
461
|
* Key is normalized to lowercase; value comparison is case-sensitive
|
|
394
|
-
* Checks inSpec for "in" key, outSpec for "out" key
|
|
462
|
+
* Checks inSpec for "in" key, outSpec for "out" key, and effect for "effect"
|
|
395
463
|
*
|
|
396
464
|
* @param {string} key - The tag key
|
|
397
465
|
* @param {string} value - The tag value to check
|
|
@@ -405,6 +473,9 @@ class CapUrn {
|
|
|
405
473
|
if (keyLower === 'out') {
|
|
406
474
|
return this.outSpec === value;
|
|
407
475
|
}
|
|
476
|
+
if (keyLower === 'effect') {
|
|
477
|
+
return this.effectValue === value;
|
|
478
|
+
}
|
|
408
479
|
const tagValue = this.tags[keyLower];
|
|
409
480
|
return tagValue !== undefined && tagValue === value;
|
|
410
481
|
}
|
|
@@ -422,7 +493,7 @@ class CapUrn {
|
|
|
422
493
|
*/
|
|
423
494
|
hasMarkerTag(tagName) {
|
|
424
495
|
const keyLower = tagName.toLowerCase();
|
|
425
|
-
if (keyLower === 'in' || keyLower === 'out') {
|
|
496
|
+
if (keyLower === 'in' || keyLower === 'out' || keyLower === 'effect') {
|
|
426
497
|
return false;
|
|
427
498
|
}
|
|
428
499
|
return this.tags[keyLower] === '*';
|
|
@@ -430,8 +501,8 @@ class CapUrn {
|
|
|
430
501
|
|
|
431
502
|
/**
|
|
432
503
|
* Create a new cap URN with an added or updated tag.
|
|
433
|
-
*
|
|
434
|
-
*
|
|
504
|
+
* Reserved structural coordinates must be changed through dedicated
|
|
505
|
+
* accessors.
|
|
435
506
|
*
|
|
436
507
|
* @param {string} key - The tag key
|
|
437
508
|
* @param {string} value - The tag value
|
|
@@ -442,13 +513,15 @@ class CapUrn {
|
|
|
442
513
|
throw new CapUrnError(ErrorCodes.EMPTY_VALUE, `Empty value for key '${key}' (use '*' for wildcard)`);
|
|
443
514
|
}
|
|
444
515
|
const keyLower = key.toLowerCase();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
516
|
+
if (keyLower === 'in' || keyLower === 'out' || keyLower === 'effect') {
|
|
517
|
+
throw new CapUrnError(
|
|
518
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
519
|
+
`Reserved structural key '${keyLower}' must be changed via withInSpec(), withOutSpec(), or withEffect()`
|
|
520
|
+
);
|
|
448
521
|
}
|
|
449
522
|
const newTags = { ...this.tags };
|
|
450
523
|
newTags[keyLower] = value;
|
|
451
|
-
return new CapUrn(this.inSpec, this.outSpec, newTags);
|
|
524
|
+
return new CapUrn(this.inSpec, this.outSpec, this.effectValue, newTags);
|
|
452
525
|
}
|
|
453
526
|
|
|
454
527
|
/**
|
|
@@ -458,9 +531,12 @@ class CapUrn {
|
|
|
458
531
|
* @returns {CapUrn} A new CapUrn instance with the updated inSpec
|
|
459
532
|
*/
|
|
460
533
|
withInSpec(inSpec) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
534
|
+
return new CapUrn(
|
|
535
|
+
validatePreservedDirectionSpec(inSpec, 'in'),
|
|
536
|
+
this.outSpec,
|
|
537
|
+
this.effectValue,
|
|
538
|
+
this.tags
|
|
539
|
+
);
|
|
464
540
|
}
|
|
465
541
|
|
|
466
542
|
/**
|
|
@@ -470,28 +546,43 @@ class CapUrn {
|
|
|
470
546
|
* @returns {CapUrn} A new CapUrn instance with the updated outSpec
|
|
471
547
|
*/
|
|
472
548
|
withOutSpec(outSpec) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
549
|
+
return new CapUrn(
|
|
550
|
+
this.inSpec,
|
|
551
|
+
validatePreservedDirectionSpec(outSpec, 'out'),
|
|
552
|
+
this.effectValue,
|
|
553
|
+
this.tags
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Create a new cap URN with a different effect coordinate.
|
|
559
|
+
*
|
|
560
|
+
* @param {string} effect
|
|
561
|
+
* @returns {CapUrn}
|
|
562
|
+
*/
|
|
563
|
+
withEffect(effect) {
|
|
564
|
+
return new CapUrn(this.inSpec, this.outSpec, normalizeEffectValue(effect), this.tags);
|
|
476
565
|
}
|
|
477
566
|
|
|
478
567
|
/**
|
|
479
568
|
* Create a new cap URN with a tag removed
|
|
480
569
|
* Key is normalized to lowercase for case-insensitive removal
|
|
481
|
-
*
|
|
570
|
+
* Reserved structural coordinates must be changed through dedicated accessors.
|
|
482
571
|
*
|
|
483
572
|
* @param {string} key - The tag key to remove
|
|
484
573
|
* @returns {CapUrn} A new CapUrn instance with the tag removed
|
|
485
574
|
*/
|
|
486
575
|
withoutTag(key) {
|
|
487
576
|
const keyLower = key.toLowerCase();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
577
|
+
if (keyLower === 'in' || keyLower === 'out' || keyLower === 'effect') {
|
|
578
|
+
throw new CapUrnError(
|
|
579
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
580
|
+
`Reserved structural key '${keyLower}' cannot be removed via withoutTag()`
|
|
581
|
+
);
|
|
491
582
|
}
|
|
492
583
|
const newTags = { ...this.tags };
|
|
493
584
|
delete newTags[keyLower];
|
|
494
|
-
return new CapUrn(this.inSpec, this.outSpec, newTags);
|
|
585
|
+
return new CapUrn(this.inSpec, this.outSpec, this.effectValue, newTags);
|
|
495
586
|
}
|
|
496
587
|
|
|
497
588
|
/**
|
|
@@ -516,8 +607,8 @@ class CapUrn {
|
|
|
516
607
|
// Input direction: pattern accepts instance. `media:` on the pattern side is
|
|
517
608
|
// the wildcard top and skips the check.
|
|
518
609
|
if (this.inSpec !== 'media:' && this.inSpec !== '*') {
|
|
519
|
-
const capIn =
|
|
520
|
-
const requestIn =
|
|
610
|
+
const capIn = MediaUrn.fromString(this.inSpec);
|
|
611
|
+
const requestIn = MediaUrn.fromString(request.inSpec);
|
|
521
612
|
if (!capIn.accepts(requestIn)) {
|
|
522
613
|
return false;
|
|
523
614
|
}
|
|
@@ -526,13 +617,17 @@ class CapUrn {
|
|
|
526
617
|
// Output direction: provider output must conform to requested output.
|
|
527
618
|
// `media:` on the pattern side is wildcard top and skips the check.
|
|
528
619
|
if (this.outSpec !== 'media:' && this.outSpec !== '*') {
|
|
529
|
-
const capOut =
|
|
530
|
-
const requestOut =
|
|
620
|
+
const capOut = MediaUrn.fromString(this.outSpec);
|
|
621
|
+
const requestOut = MediaUrn.fromString(request.outSpec);
|
|
531
622
|
if (!capOut.conformsTo(requestOut)) {
|
|
532
623
|
return false;
|
|
533
624
|
}
|
|
534
625
|
}
|
|
535
626
|
|
|
627
|
+
if (this.effectValue !== CapEffect.ANY && this.effectValue !== request.effectValue) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
536
631
|
// Y-axis: every tag's per-key match runs through the six-form
|
|
537
632
|
// truth table (taggedUrnValuesMatch). Walk the union of all keys
|
|
538
633
|
// appearing on either side so missing-on-pattern and
|
|
@@ -593,8 +688,8 @@ class CapUrn {
|
|
|
593
688
|
* @returns {number} The specificity score
|
|
594
689
|
*/
|
|
595
690
|
specificity() {
|
|
596
|
-
const inUrn =
|
|
597
|
-
const outUrn =
|
|
691
|
+
const inUrn = MediaUrn.fromString(this.inSpec);
|
|
692
|
+
const outUrn = MediaUrn.fromString(this.outSpec);
|
|
598
693
|
|
|
599
694
|
let yScore = 0;
|
|
600
695
|
for (const value of Object.values(this.tags)) {
|
|
@@ -629,10 +724,13 @@ class CapUrn {
|
|
|
629
724
|
withWildcardTag(key) {
|
|
630
725
|
const keyLower = key.toLowerCase();
|
|
631
726
|
if (keyLower === 'in') {
|
|
632
|
-
return this.withInSpec('
|
|
727
|
+
return this.withInSpec('media:');
|
|
633
728
|
}
|
|
634
729
|
if (keyLower === 'out') {
|
|
635
|
-
return this.withOutSpec('
|
|
730
|
+
return this.withOutSpec('media:');
|
|
731
|
+
}
|
|
732
|
+
if (keyLower === 'effect') {
|
|
733
|
+
return this.withEffect(CapEffect.ANY);
|
|
636
734
|
}
|
|
637
735
|
if (this.tags.hasOwnProperty(keyLower)) {
|
|
638
736
|
return this.withTag(key, '*');
|
|
@@ -642,7 +740,7 @@ class CapUrn {
|
|
|
642
740
|
|
|
643
741
|
/**
|
|
644
742
|
* Create a new cap with only specified tags
|
|
645
|
-
* Always preserves inSpec
|
|
743
|
+
* Always preserves inSpec, outSpec, and effect.
|
|
646
744
|
*
|
|
647
745
|
* @param {string[]} keys - Array of tag keys to include
|
|
648
746
|
* @returns {CapUrn} A new CapUrn instance with only the specified tags (plus in/out)
|
|
@@ -651,29 +749,28 @@ class CapUrn {
|
|
|
651
749
|
const newTags = {};
|
|
652
750
|
for (const key of keys) {
|
|
653
751
|
const normalizedKey = key.toLowerCase();
|
|
654
|
-
|
|
655
|
-
if (normalizedKey !== 'in' && normalizedKey !== 'out') {
|
|
752
|
+
if (normalizedKey !== 'in' && normalizedKey !== 'out' && normalizedKey !== 'effect') {
|
|
656
753
|
if (this.tags.hasOwnProperty(normalizedKey)) {
|
|
657
754
|
newTags[normalizedKey] = this.tags[normalizedKey];
|
|
658
755
|
}
|
|
659
756
|
}
|
|
660
757
|
}
|
|
661
|
-
return new CapUrn(this.inSpec, this.outSpec, newTags);
|
|
758
|
+
return new CapUrn(this.inSpec, this.outSpec, this.effectValue, newTags);
|
|
662
759
|
}
|
|
663
760
|
|
|
664
761
|
/**
|
|
665
762
|
* Merge with another cap (other takes precedence for conflicts)
|
|
666
|
-
*
|
|
763
|
+
* Structural coordinates are taken from other.
|
|
667
764
|
*
|
|
668
765
|
* @param {CapUrn} other - The cap to merge with
|
|
669
766
|
* @returns {CapUrn} A new CapUrn instance with merged tags
|
|
670
767
|
*/
|
|
671
768
|
merge(other) {
|
|
672
769
|
if (!other) {
|
|
673
|
-
return new CapUrn(this.inSpec, this.outSpec, this.tags);
|
|
770
|
+
return new CapUrn(this.inSpec, this.outSpec, this.effectValue, this.tags);
|
|
674
771
|
}
|
|
675
772
|
const newTags = { ...this.tags, ...other.tags };
|
|
676
|
-
return new CapUrn(other.inSpec, other.outSpec, newTags);
|
|
773
|
+
return new CapUrn(other.inSpec, other.outSpec, other.effectValue, newTags);
|
|
677
774
|
}
|
|
678
775
|
|
|
679
776
|
/**
|
|
@@ -710,7 +807,7 @@ class CapUrn {
|
|
|
710
807
|
}
|
|
711
808
|
|
|
712
809
|
// Compare direction specs
|
|
713
|
-
if (this.inSpec !== other.inSpec || this.outSpec !== other.outSpec) {
|
|
810
|
+
if (this.inSpec !== other.inSpec || this.outSpec !== other.outSpec || this.effectValue !== other.effectValue) {
|
|
714
811
|
return false;
|
|
715
812
|
}
|
|
716
813
|
|
|
@@ -751,6 +848,139 @@ class CapUrn {
|
|
|
751
848
|
}
|
|
752
849
|
return hash.toString(16);
|
|
753
850
|
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Check if this provider can dispatch the given request.
|
|
854
|
+
*
|
|
855
|
+
* @param {CapUrn} request
|
|
856
|
+
* @returns {boolean}
|
|
857
|
+
*/
|
|
858
|
+
isDispatchable(request) {
|
|
859
|
+
return this._inputDispatchable(request)
|
|
860
|
+
&& this._outputDispatchable(request)
|
|
861
|
+
&& this._effectDispatchable(request)
|
|
862
|
+
&& this._capTagsDispatchable(request);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
_inputDispatchable(request) {
|
|
866
|
+
if (request.inSpec === 'media:') return true;
|
|
867
|
+
if (this.inSpec === 'media:') return true;
|
|
868
|
+
return MediaUrn.fromString(request.inSpec).conformsTo(MediaUrn.fromString(this.inSpec));
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
_outputDispatchable(request) {
|
|
872
|
+
if (request.outSpec === 'media:') return true;
|
|
873
|
+
if (this.outSpec === 'media:') return false;
|
|
874
|
+
return MediaUrn.fromString(this.outSpec).conformsTo(MediaUrn.fromString(request.outSpec));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
_effectDispatchable(request) {
|
|
878
|
+
return request.effectValue === CapEffect.ANY || this.effectValue === request.effectValue;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
_capTagsDispatchable(request) {
|
|
882
|
+
const allKeys = new Set([
|
|
883
|
+
...Object.keys(this.tags),
|
|
884
|
+
...Object.keys(request.tags),
|
|
885
|
+
]);
|
|
886
|
+
for (const key of allKeys) {
|
|
887
|
+
const patt = Object.prototype.hasOwnProperty.call(request.tags, key)
|
|
888
|
+
? request.tags[key]
|
|
889
|
+
: undefined;
|
|
890
|
+
const inst = Object.prototype.hasOwnProperty.call(this.tags, key)
|
|
891
|
+
? this.tags[key]
|
|
892
|
+
: undefined;
|
|
893
|
+
if (!taggedUrnValuesMatch(inst, patt)) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Infer the runtime output media for a concrete runtime input.
|
|
902
|
+
*
|
|
903
|
+
* @param {MediaUrn} runtimeInput
|
|
904
|
+
* @returns {MediaUrn}
|
|
905
|
+
*/
|
|
906
|
+
inferRuntimeOutputMedia(runtimeInput) {
|
|
907
|
+
const declaredIn = this.inMediaUrn();
|
|
908
|
+
const declaredOut = this.outMediaUrn();
|
|
909
|
+
|
|
910
|
+
if (!runtimeInput.conformsTo(declaredIn)) {
|
|
911
|
+
throw new CapUrnError(
|
|
912
|
+
ErrorCodes.INVALID_EFFECT_APPLICATION,
|
|
913
|
+
`Runtime input '${runtimeInput}' does not conform to declared input '${declaredIn}'`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
let runtimeOut;
|
|
918
|
+
switch (this.effectValue) {
|
|
919
|
+
case CapEffect.DECLARED:
|
|
920
|
+
runtimeOut = declaredOut;
|
|
921
|
+
break;
|
|
922
|
+
case CapEffect.NONE:
|
|
923
|
+
runtimeOut = runtimeInput;
|
|
924
|
+
break;
|
|
925
|
+
case CapEffect.PATCH: {
|
|
926
|
+
const delta = declaredOut.deltaFrom(declaredIn);
|
|
927
|
+
runtimeOut = runtimeInput.applyDelta(delta);
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case CapEffect.ANY:
|
|
931
|
+
throw new CapUrnError(
|
|
932
|
+
ErrorCodes.INVALID_EFFECT_APPLICATION,
|
|
933
|
+
'Cannot infer runtime output for an unconstrained effect request'
|
|
934
|
+
);
|
|
935
|
+
default:
|
|
936
|
+
throw new CapUrnError(
|
|
937
|
+
ErrorCodes.INVALID_EFFECT_APPLICATION,
|
|
938
|
+
`Unexpected effect '${this.effectValue}' during runtime output inference`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (!runtimeOut.conformsTo(declaredOut)) {
|
|
943
|
+
throw new CapUrnError(
|
|
944
|
+
ErrorCodes.INVALID_EFFECT_APPLICATION,
|
|
945
|
+
`Inferred runtime output '${runtimeOut}' does not conform to declared output '${declaredOut}'`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
return runtimeOut;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
_validateAdmissible() {
|
|
952
|
+
const inMedia = this.inMediaUrn();
|
|
953
|
+
const outMedia = this.outMediaUrn();
|
|
954
|
+
const noExtraTags = Object.keys(this.tags).length === 0;
|
|
955
|
+
|
|
956
|
+
if (inMedia.isTop() && outMedia.isTop() && noExtraTags && this.effectValue === CapEffect.DECLARED) {
|
|
957
|
+
throw new CapUrnError(
|
|
958
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
959
|
+
'illegal bare top cap; use cap:effect=none for identity, or declare a non-vacuous input/output/effect/tag'
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (this.effectValue === CapEffect.NONE) {
|
|
964
|
+
if (!inMedia.conformsTo(outMedia)) {
|
|
965
|
+
throw new CapUrnError(
|
|
966
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
967
|
+
`effect=none requires declared input '${inMedia}' to conform to declared output '${outMedia}'`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (this.effectValue === CapEffect.PATCH) {
|
|
974
|
+
const delta = outMedia.deltaFrom(inMedia);
|
|
975
|
+
const witness = inMedia.applyDelta(delta);
|
|
976
|
+
if (!witness.conformsTo(outMedia)) {
|
|
977
|
+
throw new CapUrnError(
|
|
978
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
979
|
+
`effect=patch witness '${witness}' does not conform to declared output '${outMedia}'`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
754
984
|
}
|
|
755
985
|
|
|
756
986
|
/**
|
|
@@ -760,6 +990,7 @@ class CapUrnBuilder {
|
|
|
760
990
|
constructor() {
|
|
761
991
|
this._inSpec = null;
|
|
762
992
|
this._outSpec = null;
|
|
993
|
+
this._effect = CapEffect.DECLARED;
|
|
763
994
|
this._tags = {};
|
|
764
995
|
}
|
|
765
996
|
|
|
@@ -785,10 +1016,15 @@ class CapUrnBuilder {
|
|
|
785
1016
|
return this;
|
|
786
1017
|
}
|
|
787
1018
|
|
|
1019
|
+
effect(effect) {
|
|
1020
|
+
this._effect = normalizeEffectValue(effect);
|
|
1021
|
+
return this;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
788
1024
|
/**
|
|
789
1025
|
* Add or update a tag
|
|
790
1026
|
* Key is normalized to lowercase; value is preserved as-is
|
|
791
|
-
*
|
|
1027
|
+
* Structural coordinates are reserved; use dedicated methods instead.
|
|
792
1028
|
*
|
|
793
1029
|
* @param {string} key - The tag key
|
|
794
1030
|
* @param {string} value - The tag value
|
|
@@ -796,10 +1032,13 @@ class CapUrnBuilder {
|
|
|
796
1032
|
*/
|
|
797
1033
|
tag(key, value) {
|
|
798
1034
|
const keyLower = key.toLowerCase();
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1035
|
+
if (keyLower === 'in' || keyLower === 'out' || keyLower === 'effect') {
|
|
1036
|
+
throw new CapUrnError(
|
|
1037
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
1038
|
+
`Reserved structural key '${keyLower}' must be set via inSpec(), outSpec(), or effect()`
|
|
1039
|
+
);
|
|
802
1040
|
}
|
|
1041
|
+
this._tags[keyLower] = value;
|
|
803
1042
|
return this;
|
|
804
1043
|
}
|
|
805
1044
|
|
|
@@ -813,9 +1052,13 @@ class CapUrnBuilder {
|
|
|
813
1052
|
*/
|
|
814
1053
|
marker(key) {
|
|
815
1054
|
const keyLower = key.toLowerCase();
|
|
816
|
-
if (keyLower
|
|
817
|
-
|
|
1055
|
+
if (keyLower === 'in' || keyLower === 'out' || keyLower === 'effect') {
|
|
1056
|
+
throw new CapUrnError(
|
|
1057
|
+
ErrorCodes.INVALID_TAG_FORMAT,
|
|
1058
|
+
`Reserved structural key '${keyLower}' cannot be used as a marker`
|
|
1059
|
+
);
|
|
818
1060
|
}
|
|
1061
|
+
this._tags[keyLower] = '*';
|
|
819
1062
|
return this;
|
|
820
1063
|
}
|
|
821
1064
|
|
|
@@ -832,7 +1075,7 @@ class CapUrnBuilder {
|
|
|
832
1075
|
if (!this._outSpec) {
|
|
833
1076
|
throw new CapUrnError(ErrorCodes.MISSING_OUT_SPEC, "Cap URN requires 'out' spec - call outSpec() before build()");
|
|
834
1077
|
}
|
|
835
|
-
return new CapUrn(this._inSpec, this._outSpec, this._tags);
|
|
1078
|
+
return new CapUrn(this._inSpec, this._outSpec, this._effect, this._tags);
|
|
836
1079
|
}
|
|
837
1080
|
}
|
|
838
1081
|
|
|
@@ -1128,9 +1371,9 @@ const MEDIA_MEDIA_SPEC_DEFINITION = 'media:media-spec-definition;json;record;tex
|
|
|
1128
1371
|
// STANDARD CAP URN CONSTANTS
|
|
1129
1372
|
// =============================================================================
|
|
1130
1373
|
|
|
1131
|
-
// Standard
|
|
1132
|
-
// Accepts any media type as input and
|
|
1133
|
-
const CAP_IDENTITY = 'cap:
|
|
1374
|
+
// Standard identity capability URN.
|
|
1375
|
+
// Accepts any media type as input and preserves the runtime media identity.
|
|
1376
|
+
const CAP_IDENTITY = 'cap:effect=none';
|
|
1134
1377
|
|
|
1135
1378
|
// Adapter-selection capability. Default implementation returns empty END (no match).
|
|
1136
1379
|
// Cartridges that inspect file content override this with a handler that returns {"media_urns": [...]}.
|
|
@@ -1390,6 +1633,20 @@ class MediaUrn {
|
|
|
1390
1633
|
*/
|
|
1391
1634
|
isComparable(other) { return this._urn.isComparable(other._urn); }
|
|
1392
1635
|
|
|
1636
|
+
/**
|
|
1637
|
+
* Compute the coordinate-space delta from `base` to this media URN.
|
|
1638
|
+
* @param {MediaUrn} base
|
|
1639
|
+
* @returns {TaggedUrnCoordinateDelta}
|
|
1640
|
+
*/
|
|
1641
|
+
deltaFrom(base) { return this._urn.deltaFrom(base._urn); }
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Apply a coordinate-space delta to this media URN.
|
|
1645
|
+
* @param {TaggedUrnCoordinateDelta} delta
|
|
1646
|
+
* @returns {MediaUrn}
|
|
1647
|
+
*/
|
|
1648
|
+
applyDelta(delta) { return new MediaUrn(this._urn.applyDelta(delta)); }
|
|
1649
|
+
|
|
1393
1650
|
/**
|
|
1394
1651
|
* @param {MediaUrn} other
|
|
1395
1652
|
* @returns {boolean}
|
|
@@ -6096,6 +6353,7 @@ class FabricRegistryClient {
|
|
|
6096
6353
|
module.exports = {
|
|
6097
6354
|
CapUrn,
|
|
6098
6355
|
CapKind,
|
|
6356
|
+
CapEffect,
|
|
6099
6357
|
CapUrnBuilder,
|
|
6100
6358
|
CapMatcher,
|
|
6101
6359
|
CapUrnError,
|
|
@@ -6230,6 +6488,7 @@ module.exports = {
|
|
|
6230
6488
|
MEDIA_CAP_DEFINITION,
|
|
6231
6489
|
MEDIA_MEDIA_SPEC_DEFINITION,
|
|
6232
6490
|
// Standard cap URN constants
|
|
6491
|
+
CAP_IDENTITY,
|
|
6233
6492
|
CAP_ADAPTER_SELECTION,
|
|
6234
6493
|
CAP_LOOKUP_CAP_FABRIC,
|
|
6235
6494
|
CAP_LOOKUP_MEDIA_SPEC_FABRIC,
|
package/capdag.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// All implementations (Rust, Go, JS, ObjC, Python) must pass these identically.
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
|
-
CapUrn, CapKind, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
|
|
6
|
+
CapUrn, CapKind, CapEffect, CapUrnBuilder, CapMatcher, CapUrnError, ErrorCodes,
|
|
7
7
|
MediaUrn, MediaUrnError, MediaUrnErrorCodes,
|
|
8
8
|
Cap, CapGroup, CapManifest, MediaSpec, MediaSpecError, MediaSpecErrorCodes,
|
|
9
9
|
resolveMediaUrn, buildExtensionIndex, mediaUrnsForExtension, getExtensionMappings,
|
|
@@ -26,7 +26,8 @@ const {
|
|
|
26
26
|
MEDIA_FILE_PATH,
|
|
27
27
|
MEDIA_COLLECTION, MEDIA_COLLECTION_LIST,
|
|
28
28
|
MEDIA_DECISION,
|
|
29
|
-
MEDIA_AUDIO_SPEECH
|
|
29
|
+
MEDIA_AUDIO_SPEECH,
|
|
30
|
+
CAP_IDENTITY
|
|
30
31
|
} = require('./capdag.js');
|
|
31
32
|
|
|
32
33
|
// ============================================================================
|
|
@@ -309,9 +310,15 @@ function test939_capUrnCanonicalFormDropsWildcardInOut() {
|
|
|
309
310
|
`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
|
);
|
|
311
312
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
assertThrows(
|
|
314
|
+
() => CapUrn.fromString('cap:in=media:;out=media:'),
|
|
315
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
316
|
+
'declared top-to-top cap must be rejected as inadmissible'
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const identity = CapUrn.fromString('cap:effect=none');
|
|
320
|
+
assertEqual(identity.toString(), 'cap:effect=none', 'true identity must preserve explicit effect=none');
|
|
321
|
+
assert(identity.toString() !== generic.toString(), 'cap: and cap:effect=none must not collapse');
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
// TEST017: Test tag matching: exact match, subset match, wildcard match, value mismatch
|
|
@@ -494,17 +501,19 @@ function test027_wildcardTag() {
|
|
|
494
501
|
assertEqual(wildcardExt.getTag('ext'), '*', 'Should set ext to wildcard');
|
|
495
502
|
|
|
496
503
|
const wildcardIn = cap.withWildcardTag('in');
|
|
497
|
-
assertEqual(wildcardIn.getInSpec(), '
|
|
504
|
+
assertEqual(wildcardIn.getInSpec(), 'media:', 'Should set in to canonical top media:');
|
|
498
505
|
|
|
499
506
|
const wildcardOut = cap.withWildcardTag('out');
|
|
500
|
-
assertEqual(wildcardOut.getOutSpec(), '
|
|
507
|
+
assertEqual(wildcardOut.getOutSpec(), 'media:', 'Should set out to canonical top media:');
|
|
501
508
|
}
|
|
502
509
|
|
|
503
|
-
// TEST028: Test empty cap URN
|
|
510
|
+
// TEST028: Test empty cap URN is illegal
|
|
504
511
|
function test028_emptyCapUrnNotAllowed() {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
512
|
+
assertThrows(
|
|
513
|
+
() => CapUrn.fromString('cap:'),
|
|
514
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
515
|
+
'Empty cap must be rejected as inadmissible'
|
|
516
|
+
);
|
|
508
517
|
}
|
|
509
518
|
|
|
510
519
|
// TEST029: Test minimal valid cap URN has just in and out, empty tags
|
|
@@ -683,7 +692,7 @@ function test047_matchingSemanticsThumbnailVoidInput() {
|
|
|
683
692
|
|
|
684
693
|
// TEST048: Matching semantics - wildcard direction matches anything
|
|
685
694
|
function test048_matchingSemanticsWildcardDirection() {
|
|
686
|
-
const cap = CapUrn.fromString('cap:in=*;out
|
|
695
|
+
const cap = CapUrn.fromString('cap:in=*;out=*;op');
|
|
687
696
|
const request = CapUrn.fromString(testUrn('generate;ext=pdf'));
|
|
688
697
|
assert(cap.accepts(request), 'Wildcard cap should accept any request');
|
|
689
698
|
}
|
|
@@ -2542,7 +2551,7 @@ function test1302_predicateConstantConsistency() {
|
|
|
2542
2551
|
// cap_urn.rs: TEST1303-TEST1307 (CapUrn tier tests)
|
|
2543
2552
|
// ============================================================================
|
|
2544
2553
|
|
|
2545
|
-
// TEST1303: without_tag removes tag,
|
|
2554
|
+
// TEST1303: without_tag removes tag, rejects structural keys, case-insensitive for keys
|
|
2546
2555
|
function test1303_withoutTag() {
|
|
2547
2556
|
const cap = CapUrn.fromString('cap:in="media:void";test;ext=pdf;out="media:void"');
|
|
2548
2557
|
const removed = cap.withoutTag('ext');
|
|
@@ -2553,11 +2562,9 @@ function test1303_withoutTag() {
|
|
|
2553
2562
|
const removed2 = cap.withoutTag('EXT');
|
|
2554
2563
|
assertEqual(removed2.getTag('ext'), undefined, 'withoutTag should be case-insensitive');
|
|
2555
2564
|
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
const same2 = cap.withoutTag('out');
|
|
2560
|
-
assertEqual(same2.getOutSpec(), 'media:void', 'withoutTag must not remove out');
|
|
2565
|
+
assertThrows(() => cap.withoutTag('in'), 'withoutTag must reject in');
|
|
2566
|
+
assertThrows(() => cap.withoutTag('out'), 'withoutTag must reject out');
|
|
2567
|
+
assertThrows(() => cap.withoutTag('effect'), 'withoutTag must reject effect');
|
|
2561
2568
|
|
|
2562
2569
|
// Removing non-existent tag is no-op
|
|
2563
2570
|
const same3 = cap.withoutTag('nonexistent');
|
|
@@ -2581,6 +2588,12 @@ function test1304_withInOutSpec() {
|
|
|
2581
2588
|
const changedBoth = cap.withInSpec('media:pdf').withOutSpec(MEDIA_TXT);
|
|
2582
2589
|
assertEqual(changedBoth.getInSpec(), 'media:pdf', 'Chain should set inSpec');
|
|
2583
2590
|
assertEqual(changedBoth.getOutSpec(), MEDIA_TXT, 'Chain should set outSpec');
|
|
2591
|
+
|
|
2592
|
+
const identity = CapUrn.fromString('cap:effect=none');
|
|
2593
|
+
assertThrows(
|
|
2594
|
+
() => identity.withOutSpec('media:pdf'),
|
|
2595
|
+
'withOutSpec must revalidate admissibility'
|
|
2596
|
+
);
|
|
2584
2597
|
}
|
|
2585
2598
|
|
|
2586
2599
|
// TEST561: N/A for JS (in_media_urn/out_media_urn not in JS CapUrn)
|
|
@@ -2630,15 +2643,28 @@ function test1306_areCompatible() {
|
|
|
2630
2643
|
|
|
2631
2644
|
// TEST565: N/A for JS (tags_to_string not in JS CapUrn)
|
|
2632
2645
|
|
|
2633
|
-
// TEST1307: with_tag
|
|
2634
|
-
function
|
|
2646
|
+
// TEST1307: with_tag rejects structural keys
|
|
2647
|
+
function test1307_withTagRejectsStructuralKeys() {
|
|
2635
2648
|
const cap = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2649
|
+
assertThrows(() => cap.withTag('in', 'media:'), 'withTag must reject in');
|
|
2650
|
+
assertThrows(() => cap.withTag('out', 'media:'), 'withTag must reject out');
|
|
2651
|
+
assertThrows(() => cap.withTag('effect', 'none'), 'withTag must reject effect');
|
|
2652
|
+
}
|
|
2639
2653
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2654
|
+
// TEST1308: builder rejects structural keys on tag/marker
|
|
2655
|
+
function test1308_builderRejectsStructuralKeys() {
|
|
2656
|
+
assertThrows(
|
|
2657
|
+
() => new CapUrnBuilder().tag('in', 'media:void'),
|
|
2658
|
+
'builder.tag must reject structural in'
|
|
2659
|
+
);
|
|
2660
|
+
assertThrows(
|
|
2661
|
+
() => new CapUrnBuilder().marker('effect'),
|
|
2662
|
+
'builder.marker must reject structural effect'
|
|
2663
|
+
);
|
|
2664
|
+
assertThrows(
|
|
2665
|
+
() => new CapUrnBuilder().inSpec('media:void').outSpec('media:record').tag('123', 'value').build(),
|
|
2666
|
+
'builder.build must reject invalid non-structural tags'
|
|
2667
|
+
);
|
|
2642
2668
|
}
|
|
2643
2669
|
|
|
2644
2670
|
// TEST1294: RULE11 - void-input cap with stdin source rejected
|
|
@@ -2697,47 +2723,58 @@ function test1297_rule11NonVoidInputWithStdin() {
|
|
|
2697
2723
|
// cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
|
|
2698
2724
|
// ============================================================================
|
|
2699
2725
|
|
|
2700
|
-
// TEST639: cap: (empty)
|
|
2701
|
-
function
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2726
|
+
// TEST639: cap: (empty) is the illegal bare top form
|
|
2727
|
+
function test639_emptyCapIsIllegal() {
|
|
2728
|
+
assertThrows(
|
|
2729
|
+
() => CapUrn.fromString('cap:'),
|
|
2730
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2731
|
+
'Empty cap must be rejected as inadmissible'
|
|
2732
|
+
);
|
|
2706
2733
|
}
|
|
2707
2734
|
|
|
2708
|
-
// TEST640: cap:in
|
|
2709
|
-
function
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2735
|
+
// TEST640: cap:in collapses to the same illegal bare top form
|
|
2736
|
+
function test640_inOnlyIsIllegal() {
|
|
2737
|
+
assertThrows(
|
|
2738
|
+
() => CapUrn.fromString('cap:in'),
|
|
2739
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2740
|
+
'Bare in must be rejected as inadmissible'
|
|
2741
|
+
);
|
|
2713
2742
|
}
|
|
2714
2743
|
|
|
2715
|
-
// TEST641: cap:out
|
|
2716
|
-
function
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2744
|
+
// TEST641: cap:out collapses to the same illegal bare top form
|
|
2745
|
+
function test641_outOnlyIsIllegal() {
|
|
2746
|
+
assertThrows(
|
|
2747
|
+
() => CapUrn.fromString('cap:out'),
|
|
2748
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2749
|
+
'Bare out must be rejected as inadmissible'
|
|
2750
|
+
);
|
|
2720
2751
|
}
|
|
2721
2752
|
|
|
2722
|
-
// TEST642: cap:in;out
|
|
2723
|
-
function
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2753
|
+
// TEST642: cap:in;out collapses to the same illegal bare top form
|
|
2754
|
+
function test642_inOutWithoutValuesAreIllegal() {
|
|
2755
|
+
assertThrows(
|
|
2756
|
+
() => CapUrn.fromString('cap:in;out'),
|
|
2757
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2758
|
+
'Bare in/out must be rejected as inadmissible'
|
|
2759
|
+
);
|
|
2727
2760
|
}
|
|
2728
2761
|
|
|
2729
|
-
// TEST643: cap:in=*;out=*
|
|
2730
|
-
function
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2762
|
+
// TEST643: cap:in=*;out=* is the same illegal bare top form
|
|
2763
|
+
function test643_explicitAsteriskIsIllegal() {
|
|
2764
|
+
assertThrows(
|
|
2765
|
+
() => CapUrn.fromString('cap:in=*;out=*'),
|
|
2766
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2767
|
+
'Explicit wildcard top-to-top must be rejected as inadmissible'
|
|
2768
|
+
);
|
|
2734
2769
|
}
|
|
2735
2770
|
|
|
2736
|
-
// TEST644: cap:in=media:;out=*
|
|
2737
|
-
function
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2771
|
+
// TEST644: cap:in=media:;out=* is the same illegal bare top form
|
|
2772
|
+
function test644_specificInWildcardOutIsIllegal() {
|
|
2773
|
+
assertThrows(
|
|
2774
|
+
() => CapUrn.fromString('cap:in=media:;out=*'),
|
|
2775
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2776
|
+
'Top-to-top declared form must be rejected as inadmissible'
|
|
2777
|
+
);
|
|
2741
2778
|
}
|
|
2742
2779
|
|
|
2743
2780
|
// TEST645: cap:in=*;out=media:text has wildcard in, specific out
|
|
@@ -2767,8 +2804,8 @@ function test647_invalidOutSpecFails() {
|
|
|
2767
2804
|
|
|
2768
2805
|
// TEST648: Wildcard in/out match specific caps
|
|
2769
2806
|
function test648_wildcardAcceptsSpecific() {
|
|
2770
|
-
const wildcard = CapUrn.fromString('cap:in=*;out
|
|
2771
|
-
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2807
|
+
const wildcard = CapUrn.fromString('cap:in=*;out=*;raw');
|
|
2808
|
+
const specific = CapUrn.fromString('cap:in="media:";out="media:text";raw');
|
|
2772
2809
|
|
|
2773
2810
|
assert(wildcard.accepts(specific), 'Wildcard should accept specific');
|
|
2774
2811
|
assert(specific.conformsTo(wildcard), 'Specific should conform to wildcard');
|
|
@@ -2776,52 +2813,105 @@ function test648_wildcardAcceptsSpecific() {
|
|
|
2776
2813
|
|
|
2777
2814
|
// TEST649: Specificity - wildcard has 0, specific has tag count
|
|
2778
2815
|
function test649_specificityScoring() {
|
|
2779
|
-
const wildcard = CapUrn.fromString('cap:in=*;out
|
|
2780
|
-
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2816
|
+
const wildcard = CapUrn.fromString('cap:in=*;out=*;raw');
|
|
2817
|
+
const specific = CapUrn.fromString('cap:in="media:";out="media:text";raw');
|
|
2781
2818
|
|
|
2782
|
-
assertEqual(wildcard.specificity(),
|
|
2819
|
+
assertEqual(wildcard.specificity(), 2, 'Marker-only wildcard cap should have y-axis specificity only');
|
|
2783
2820
|
assert(specific.specificity() > 0, 'Specific cap should have non-zero specificity');
|
|
2784
2821
|
}
|
|
2785
2822
|
|
|
2786
|
-
// TEST650:
|
|
2823
|
+
// TEST650: cap:in=media:;out=media:;test preserves other tags
|
|
2824
|
+
function test650_wildcardPreserveOtherTags() {
|
|
2825
|
+
const cap = CapUrn.fromString('cap:in=media:;out=media:;test');
|
|
2826
|
+
assertEqual(cap.getInSpec(), 'media:', 'in spec should remain media:');
|
|
2827
|
+
assertEqual(cap.getOutSpec(), 'media:', 'out spec should remain media:');
|
|
2828
|
+
assertEqual(cap.getEffect(), CapEffect.DECLARED, 'missing effect should default to declared');
|
|
2829
|
+
assert(cap.hasMarkerTag('test'), 'marker tag should be preserved');
|
|
2830
|
+
}
|
|
2787
2831
|
|
|
2788
|
-
// TEST651:
|
|
2789
|
-
function
|
|
2832
|
+
// TEST651: Generic top-to-top spellings are all rejected.
|
|
2833
|
+
function test651_wildcardGenericFormsRejected() {
|
|
2790
2834
|
const forms = [
|
|
2835
|
+
'cap:',
|
|
2836
|
+
'cap:in;out',
|
|
2791
2837
|
'cap:in=*;out=*',
|
|
2792
|
-
'cap:in=
|
|
2838
|
+
'cap:in=media:;out=media:',
|
|
2839
|
+
'cap:in;out=media:',
|
|
2840
|
+
'cap:in=*;out=media:',
|
|
2841
|
+
'cap:in=media:;out',
|
|
2842
|
+
'cap:in=media:;out=*',
|
|
2793
2843
|
];
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
const specific = CapUrn.fromString('cap:in="media:";out="media:text"');
|
|
2801
|
-
assert(first.accepts(specific), `Form 0 should accept specific`);
|
|
2802
|
-
assert(cap.accepts(specific), `Form ${i} should accept specific`);
|
|
2844
|
+
for (const form of forms) {
|
|
2845
|
+
assertThrows(
|
|
2846
|
+
() => CapUrn.fromString(form),
|
|
2847
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2848
|
+
`${form} must be rejected as inadmissible`
|
|
2849
|
+
);
|
|
2803
2850
|
}
|
|
2804
2851
|
}
|
|
2805
2852
|
|
|
2806
|
-
// TEST652:
|
|
2853
|
+
// TEST652: CAP_IDENTITY constant names the true identity cap, not bare cap:
|
|
2854
|
+
function test652_capIdentityConstantWorks() {
|
|
2855
|
+
const identity = CapUrn.fromString(CAP_IDENTITY);
|
|
2856
|
+
assertEqual(identity.toString(), 'cap:effect=none', 'CAP_IDENTITY must be explicit effect=none');
|
|
2857
|
+
assertEqual(identity.kind(), CapKind.IDENTITY, 'CAP_IDENTITY must classify as identity');
|
|
2807
2858
|
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const specificRequest = CapUrn.fromString('cap:in="media:void";test;out="media:void"');
|
|
2859
|
+
const longForm = CapUrn.fromString('cap:in=media:;out=media:;effect=none');
|
|
2860
|
+
assert(identity.accepts(longForm), 'identity should accept its long form');
|
|
2861
|
+
assert(longForm.accepts(identity), 'long form should accept canonical identity');
|
|
2812
2862
|
|
|
2813
|
-
|
|
2814
|
-
|
|
2863
|
+
assertThrows(
|
|
2864
|
+
() => CapUrn.fromString('cap:'),
|
|
2865
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2866
|
+
'bare cap must be rejected as inadmissible'
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2815
2869
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2870
|
+
// TEST653: invalid effect=none declarations fail at construction.
|
|
2871
|
+
function test653_invalidEffectNoneDeclarationRejected() {
|
|
2872
|
+
assertThrows(
|
|
2873
|
+
() => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
|
|
2874
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2875
|
+
'invalid effect=none declaration must fail at construction'
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2819
2878
|
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2879
|
+
// TEST654: effect=none preserves runtime media identity.
|
|
2880
|
+
function test654_effectNonePreservesRuntimeMedia() {
|
|
2881
|
+
const decimate = CapUrn.fromString('cap:decimate-sequence;effect=none');
|
|
2882
|
+
const png = MediaUrn.fromString('media:image;png');
|
|
2883
|
+
const pdf = MediaUrn.fromString('media:pdf');
|
|
2884
|
+
assertEqual(decimate.inferRuntimeOutputMedia(png).toString(), png.toString(), 'effect=none should preserve png');
|
|
2885
|
+
assertEqual(decimate.inferRuntimeOutputMedia(pdf).toString(), pdf.toString(), 'effect=none should preserve pdf');
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// TEST655: default effect=declared does not preserve runtime refinements.
|
|
2889
|
+
function test655_effectDeclaredUsesDeclaredOutput() {
|
|
2890
|
+
const resize = CapUrn.fromString('cap:in=media:image;out=media:image;resize');
|
|
2891
|
+
const png = MediaUrn.fromString('media:image;png;width=4000');
|
|
2892
|
+
assertEqual(
|
|
2893
|
+
resize.inferRuntimeOutputMedia(png).toString(),
|
|
2894
|
+
'media:image',
|
|
2895
|
+
'default declared effect should collapse to declared output'
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// TEST656: invalid effect=none declarations fail hard at construction.
|
|
2900
|
+
function test656_invalidEffectNoneFailsHard() {
|
|
2901
|
+
assertThrows(
|
|
2902
|
+
() => CapUrn.fromString('cap:in=media:pdf;out=media:textable;effect=none'),
|
|
2903
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
2904
|
+
'invalid effect=none declaration must fail at construction'
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// TEST657: omitted effect means declared; unconstrained effect must be explicit.
|
|
2909
|
+
function test657_effectDispatchRequiresExplicitWildcard() {
|
|
2910
|
+
const noneProvider = CapUrn.fromString('cap:effect=none');
|
|
2911
|
+
const declaredRequest = CapUrn.fromString('cap:raw');
|
|
2912
|
+
const anyRequest = CapUrn.fromString('cap:?effect');
|
|
2913
|
+
assert(!noneProvider.isDispatchable(declaredRequest), 'effect=none should not silently satisfy declared request');
|
|
2914
|
+
assert(noneProvider.isDispatchable(anyRequest), 'explicit ?effect should accept any provider effect');
|
|
2825
2915
|
}
|
|
2826
2916
|
|
|
2827
2917
|
// ============================================================================
|
|
@@ -5481,25 +5571,28 @@ function testRenderer_validateResolvedMachinePayload_rejectsMissingFields() {
|
|
|
5481
5571
|
// surface, not a per-port detail.
|
|
5482
5572
|
// ============================================================================
|
|
5483
5573
|
|
|
5484
|
-
// TEST1800: Identity classifier — only
|
|
5485
|
-
// Adding any tag (even one that doesn't constrain in/out) demotes
|
|
5486
|
-
// the cap to Transform because the operation/metadata axis is no
|
|
5487
|
-
// longer fully generic.
|
|
5574
|
+
// TEST1800: Identity classifier — only explicit effect=none qualifies.
|
|
5488
5575
|
function test1800_kindIdentityOnlyForBareCap() {
|
|
5489
|
-
const identity = CapUrn.fromString('cap:');
|
|
5490
|
-
assertEqual(identity.kind(), CapKind.IDENTITY, 'cap: should be Identity');
|
|
5576
|
+
const identity = CapUrn.fromString('cap:effect=none');
|
|
5577
|
+
assertEqual(identity.kind(), CapKind.IDENTITY, 'cap:effect=none should be Identity');
|
|
5491
5578
|
|
|
5492
5579
|
for (const spelling of [
|
|
5493
|
-
'cap:in=media:;out=media
|
|
5494
|
-
'cap:in=*;out=*',
|
|
5495
|
-
'cap:in=media:',
|
|
5496
|
-
'cap:out=media:',
|
|
5580
|
+
'cap:in=media:;out=media:;effect=none',
|
|
5581
|
+
'cap:effect=none;in=*;out=*',
|
|
5582
|
+
'cap:effect=none;in=media:',
|
|
5583
|
+
'cap:effect=none;out=media:',
|
|
5497
5584
|
]) {
|
|
5498
5585
|
const cap = CapUrn.fromString(spelling);
|
|
5499
5586
|
assertEqual(cap.kind(), CapKind.IDENTITY,
|
|
5500
|
-
`${spelling} should classify as Identity
|
|
5587
|
+
`${spelling} should classify as Identity`);
|
|
5501
5588
|
}
|
|
5502
5589
|
|
|
5590
|
+
assertThrows(
|
|
5591
|
+
() => CapUrn.fromString('cap:'),
|
|
5592
|
+
ErrorCodes.ILLEGAL_DECLARATION,
|
|
5593
|
+
'bare cap must be rejected as inadmissible'
|
|
5594
|
+
);
|
|
5595
|
+
|
|
5503
5596
|
const withOp = CapUrn.fromString('cap:passthrough');
|
|
5504
5597
|
assertEqual(withOp.kind(), CapKind.TRANSFORM,
|
|
5505
5598
|
'cap:passthrough specifies the operation axis — not Identity');
|
|
@@ -5587,7 +5680,7 @@ function test1810_mediaVoidIsAtomic() {
|
|
|
5587
5680
|
// once parsed.
|
|
5588
5681
|
function test1805_kindInvariantUnderCanonicalSpellings() {
|
|
5589
5682
|
const cases = [
|
|
5590
|
-
{ a: 'cap:', b: 'cap:in=media:;out=media
|
|
5683
|
+
{ a: 'cap:effect=none', b: 'cap:in=media:;out=media:;effect=none', expected: CapKind.IDENTITY },
|
|
5591
5684
|
{
|
|
5592
5685
|
a: 'cap:extract;in=media:pdf;out=media:textable',
|
|
5593
5686
|
b: 'cap:extract;in="media:pdf";out="media:textable"',
|
|
@@ -5627,8 +5720,8 @@ function test1805_kindInvariantUnderCanonicalSpellings() {
|
|
|
5627
5720
|
|
|
5628
5721
|
// TEST1820: A `?`-valued cap-tag scores 0. Same as missing.
|
|
5629
5722
|
function test1820_specificityQuestionIsZero() {
|
|
5630
|
-
const bare = CapUrn.fromString('cap
|
|
5631
|
-
assertEqual(bare.specificity(), 0, 'cap
|
|
5723
|
+
const bare = CapUrn.fromString('cap:?effect');
|
|
5724
|
+
assertEqual(bare.specificity(), 0, 'cap:?effect must score 0 (fully unconstrained request)');
|
|
5632
5725
|
|
|
5633
5726
|
const withQ = CapUrn.fromString('cap:?target');
|
|
5634
5727
|
assertEqual(withQ.specificity(), 0,
|
|
@@ -6023,7 +6116,8 @@ async function runTests() {
|
|
|
6023
6116
|
runTest('TEST1304: with_in_out_spec', test1304_withInOutSpec);
|
|
6024
6117
|
runTest('TEST1305: find_all_matches', test1305_findAllMatches);
|
|
6025
6118
|
runTest('TEST1306: are_compatible', test1306_areCompatible);
|
|
6026
|
-
runTest('TEST1307:
|
|
6119
|
+
runTest('TEST1307: with_tag_rejects_structural_keys', test1307_withTagRejectsStructuralKeys);
|
|
6120
|
+
runTest('TEST1308: builder_rejects_structural_keys', test1308_builderRejectsStructuralKeys);
|
|
6027
6121
|
runTest('TEST1294: rule11_void_input_with_stdin_rejected', test1294_rule11VoidInputWithStdinRejected);
|
|
6028
6122
|
runTest('TEST1295: rule11_non_void_input_without_stdin_rejected', test1295_rule11NonVoidInputWithoutStdinRejected);
|
|
6029
6123
|
runTest('TEST1296: rule11_void_input_cli_flag_only', test1296_rule11VoidInputCliFlagOnly);
|
|
@@ -6031,21 +6125,25 @@ async function runTests() {
|
|
|
6031
6125
|
|
|
6032
6126
|
// cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
|
|
6033
6127
|
console.log('\n--- cap_urn.rs (wildcard tests) ---');
|
|
6034
|
-
runTest('TEST639:
|
|
6035
|
-
runTest('TEST640:
|
|
6036
|
-
runTest('TEST641:
|
|
6037
|
-
runTest('TEST642:
|
|
6038
|
-
runTest('TEST643:
|
|
6039
|
-
runTest('TEST644:
|
|
6128
|
+
runTest('TEST639: empty_cap_is_illegal', test639_emptyCapIsIllegal);
|
|
6129
|
+
runTest('TEST640: in_only_is_illegal', test640_inOnlyIsIllegal);
|
|
6130
|
+
runTest('TEST641: out_only_is_illegal', test641_outOnlyIsIllegal);
|
|
6131
|
+
runTest('TEST642: in_out_without_values_are_illegal', test642_inOutWithoutValuesAreIllegal);
|
|
6132
|
+
runTest('TEST643: explicit_asterisk_is_illegal', test643_explicitAsteriskIsIllegal);
|
|
6133
|
+
runTest('TEST644: specific_in_wildcard_out_is_illegal', test644_specificInWildcardOutIsIllegal);
|
|
6040
6134
|
runTest('TEST645: wildcard_in_specific_out', test645_wildcardInSpecificOut);
|
|
6041
6135
|
runTest('TEST646: invalid_in_spec_fails', test646_invalidInSpecFails);
|
|
6042
6136
|
runTest('TEST647: invalid_out_spec_fails', test647_invalidOutSpecFails);
|
|
6043
6137
|
runTest('TEST648: wildcard_accepts_specific', test648_wildcardAcceptsSpecific);
|
|
6044
6138
|
runTest('TEST649: specificity_scoring', test649_specificityScoring);
|
|
6045
|
-
|
|
6046
|
-
runTest('TEST651:
|
|
6047
|
-
|
|
6048
|
-
runTest('TEST653:
|
|
6139
|
+
runTest('TEST650: wildcard_preserve_other_tags', test650_wildcardPreserveOtherTags);
|
|
6140
|
+
runTest('TEST651: wildcard_generic_forms_rejected', test651_wildcardGenericFormsRejected);
|
|
6141
|
+
runTest('TEST652: cap_identity_constant_works', test652_capIdentityConstantWorks);
|
|
6142
|
+
runTest('TEST653: invalid_effect_none_declaration_rejected', test653_invalidEffectNoneDeclarationRejected);
|
|
6143
|
+
runTest('TEST654: effect_none_preserves_runtime_media', test654_effectNonePreservesRuntimeMedia);
|
|
6144
|
+
runTest('TEST655: effect_declared_uses_declared_output', test655_effectDeclaredUsesDeclaredOutput);
|
|
6145
|
+
runTest('TEST656: invalid_effect_none_fails_hard', test656_invalidEffectNoneFailsHard);
|
|
6146
|
+
runTest('TEST657: effect_dispatch_requires_explicit_wildcard', test657_effectDispatchRequiresExplicitWildcard);
|
|
6049
6147
|
|
|
6050
6148
|
// machine module: parser tests (mirrors parser.rs)
|
|
6051
6149
|
console.log('\n--- machine/parser.rs ---');
|
|
@@ -6219,7 +6317,7 @@ async function runTests() {
|
|
|
6219
6317
|
runTest('RENDERER: validateResolvedMachine_rejectsMissingFields', testRenderer_validateResolvedMachinePayload_rejectsMissingFields);
|
|
6220
6318
|
|
|
6221
6319
|
console.log('\n--- CapKind classifier (test1800–test1805) ---');
|
|
6222
|
-
runTest('TEST1800:
|
|
6320
|
+
runTest('TEST1800: kind_identity_requires_effect_none', test1800_kindIdentityOnlyForBareCap);
|
|
6223
6321
|
runTest('TEST1801: kind_source_when_input_is_void', test1801_kindSourceWhenInputIsVoid);
|
|
6224
6322
|
runTest('TEST1802: kind_sink_when_output_is_void', test1802_kindSinkWhenOutputIsVoid);
|
|
6225
6323
|
runTest('TEST1803: kind_effect_when_both_sides_void', test1803_kindEffectWhenBothSidesVoid);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Bahram Joharshamshiri",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"peggy": "^5.1.0",
|
|
5
|
-
"tagged-urn": "^0.
|
|
5
|
+
"tagged-urn": "^0.42.114"
|
|
6
6
|
},
|
|
7
7
|
"description": "JavaScript implementation of Cap URN (Capability Uniform Resource Names) with strict validation and matching",
|
|
8
8
|
"engines": {
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"pretest": "npm run build:parser",
|
|
41
41
|
"test": "node capdag.test.js"
|
|
42
42
|
},
|
|
43
|
-
"version": "0.
|
|
43
|
+
"version": "0.182.459"
|
|
44
44
|
}
|