capdag 0.183.463 → 0.186.476
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/RULES.md +8 -8
- package/build-browser.js +3 -3
- package/cap-fab-renderer.js +44 -33
- package/capdag.js +198 -188
- package/capdag.test.js +153 -149
- package/package.json +1 -1
package/RULES.md
CHANGED
|
@@ -72,33 +72,33 @@ Examples:
|
|
|
72
72
|
|
|
73
73
|
## Validation Rules
|
|
74
74
|
|
|
75
|
-
### XV5: No Redefinition of Registry Media
|
|
75
|
+
### XV5: No Redefinition of Registry Media Defs
|
|
76
76
|
|
|
77
|
-
Inline media
|
|
77
|
+
Inline media defs in a capability's `media_defs` table must not redefine media defs that already exist in the global registry or built-in specs.
|
|
78
78
|
|
|
79
79
|
```javascript
|
|
80
|
-
const {
|
|
80
|
+
const { validateNoMediaDefRedefinitionSync, MEDIA_STRING } = require('capdag');
|
|
81
81
|
|
|
82
82
|
// This will fail - MEDIA_STRING is a built-in spec
|
|
83
|
-
const
|
|
83
|
+
const mediaDefs = {
|
|
84
84
|
[MEDIA_STRING]: { media_type: 'text/plain', title: 'My String' }
|
|
85
85
|
};
|
|
86
|
-
const result =
|
|
86
|
+
const result = validateNoMediaDefRedefinitionSync(mediaDefs);
|
|
87
87
|
// result: { valid: false, error: 'XV5: ...', redefines: ['media:textable'] }
|
|
88
88
|
|
|
89
89
|
// This is allowed - custom spec that doesn't exist
|
|
90
90
|
const customSpecs = {
|
|
91
91
|
'media:my-custom-type': { media_type: 'application/json', title: 'My Type' }
|
|
92
92
|
};
|
|
93
|
-
const customResult =
|
|
93
|
+
const customResult = validateNoMediaDefRedefinitionSync(customSpecs);
|
|
94
94
|
// result: { valid: true }
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
For server-side validation with registry access, use the async version:
|
|
98
98
|
```javascript
|
|
99
|
-
const {
|
|
99
|
+
const { validateNoMediaDefRedefinition } = require('capdag');
|
|
100
100
|
|
|
101
|
-
const result = await
|
|
101
|
+
const result = await validateNoMediaDefRedefinition(mediaDefs, {
|
|
102
102
|
registryLookup: async (urn) => await mediaStore.get(urn) !== null
|
|
103
103
|
});
|
|
104
104
|
```
|
package/build-browser.js
CHANGED
|
@@ -141,9 +141,9 @@ window.OutputValidator = OutputValidator;
|
|
|
141
141
|
window.CapValidator = CapValidator;
|
|
142
142
|
window.validateCapArgs = validateCapArgs;
|
|
143
143
|
window.RESERVED_CLI_FLAGS = RESERVED_CLI_FLAGS;
|
|
144
|
-
window.
|
|
145
|
-
window.
|
|
146
|
-
window.
|
|
144
|
+
window.MediaDef = MediaDef;
|
|
145
|
+
window.MediaDefError = MediaDefError;
|
|
146
|
+
window.MediaDefErrorCodes = MediaDefErrorCodes;
|
|
147
147
|
window.isBinaryCapUrn = isBinaryCapUrn;
|
|
148
148
|
window.isJSONCapUrn = isJSONCapUrn;
|
|
149
149
|
window.isStructuredCapUrn = isStructuredCapUrn;
|
package/cap-fab-renderer.js
CHANGED
|
@@ -742,7 +742,7 @@ function validateStrandStep(step, path) {
|
|
|
742
742
|
throw new Error(`CapFabRenderer: ${path}.step_type.Cap.output_is_sequence must be a boolean`);
|
|
743
743
|
}
|
|
744
744
|
} else {
|
|
745
|
-
assertString(body.
|
|
745
|
+
assertString(body.media_def, `${path}.step_type.${variant}.media_def`);
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
748
|
|
|
@@ -750,8 +750,8 @@ function validateStrandPayload(data) {
|
|
|
750
750
|
if (!data || typeof data !== 'object') {
|
|
751
751
|
throw new Error('CapFabRenderer strand mode: data must be an object');
|
|
752
752
|
}
|
|
753
|
-
assertString(data.
|
|
754
|
-
assertString(data.
|
|
753
|
+
assertString(data.source_media_urn, 'strand mode data.source_media_urn');
|
|
754
|
+
assertString(data.target_media_urn, 'strand mode data.target_media_urn');
|
|
755
755
|
assertArray(data.steps, 'strand mode data.steps');
|
|
756
756
|
data.steps.forEach((step, idx) => {
|
|
757
757
|
validateStrandStep(step, `strand mode data.steps[${idx}]`);
|
|
@@ -1087,7 +1087,7 @@ function classifyStrandCapSteps(steps) {
|
|
|
1087
1087
|
// edge to body_entry, and prev_node_id becomes body_exit.
|
|
1088
1088
|
//
|
|
1089
1089
|
// Node labels come from the `media_display_names` map keyed by the
|
|
1090
|
-
// step's canonical URN (or
|
|
1090
|
+
// step's canonical URN (or source_media_urn/target_media_urn for the boundary
|
|
1091
1091
|
// nodes). ForEach and Collect nodes display "for each" / "collect".
|
|
1092
1092
|
// Cap edges carry the cap title plus cardinality marker when either
|
|
1093
1093
|
// input or output is a sequence.
|
|
@@ -1095,8 +1095,8 @@ function buildStrandGraphData(data) {
|
|
|
1095
1095
|
validateStrandPayload(data);
|
|
1096
1096
|
|
|
1097
1097
|
const mediaDisplayNames = data.media_display_names || {};
|
|
1098
|
-
const
|
|
1099
|
-
const
|
|
1098
|
+
const sourceMediaUrn = canonicalMediaUrn(data.source_media_urn);
|
|
1099
|
+
const targetMediaUrn = canonicalMediaUrn(data.target_media_urn);
|
|
1100
1100
|
|
|
1101
1101
|
// Look up a display name for a media URN via the host-supplied map.
|
|
1102
1102
|
// Uses `MediaUrn.isEquivalent` so tag-order variation doesn't defeat
|
|
@@ -1151,9 +1151,9 @@ function buildStrandGraphData(data) {
|
|
|
1151
1151
|
edgeCounter++;
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
-
// Entry node — the strand's source media
|
|
1154
|
+
// Entry node — the strand's source media def.
|
|
1155
1155
|
const inputSlotId = 'input_slot';
|
|
1156
|
-
addNode(inputSlotId, displayNameFor(
|
|
1156
|
+
addNode(inputSlotId, displayNameFor(sourceMediaUrn), sourceMediaUrn, 'strand-source');
|
|
1157
1157
|
|
|
1158
1158
|
let prevNodeId = inputSlotId;
|
|
1159
1159
|
|
|
@@ -1311,7 +1311,7 @@ function buildStrandGraphData(data) {
|
|
|
1311
1311
|
|
|
1312
1312
|
// Final output node. Mirrors plan_builder.rs:430-432.
|
|
1313
1313
|
const outputId = 'output';
|
|
1314
|
-
addNode(outputId, displayNameFor(
|
|
1314
|
+
addNode(outputId, displayNameFor(targetMediaUrn), targetMediaUrn, 'strand-target');
|
|
1315
1315
|
addEdge(prevNodeId, outputId, '', '', '', 'strand-cap-edge');
|
|
1316
1316
|
|
|
1317
1317
|
// Return the raw plan-builder topology. Strand mode collapses
|
|
@@ -1319,7 +1319,7 @@ function buildStrandGraphData(data) {
|
|
|
1319
1319
|
// `strandCytoscapeElements`); run mode keeps them as explicit
|
|
1320
1320
|
// nodes because body replicas anchor at the ForEach/Collect
|
|
1321
1321
|
// junctions.
|
|
1322
|
-
return { nodes, edges,
|
|
1322
|
+
return { nodes, edges, sourceMediaUrn, targetMediaUrn };
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
1325
|
// Transform the plan-builder strand topology into the render shape
|
|
@@ -1349,7 +1349,7 @@ function buildStrandGraphData(data) {
|
|
|
1349
1349
|
// post-collect cap's `input_is_sequence=true` flag.
|
|
1350
1350
|
//
|
|
1351
1351
|
// 4. If the last cap step's `to_spec` is semantically equivalent
|
|
1352
|
-
// to the strand's `
|
|
1352
|
+
// to the strand's `target_media_urn` (via MediaUrn.isEquivalent),
|
|
1353
1353
|
// the separate `output` target node is dropped and the last
|
|
1354
1354
|
// cap edge lands on that merged endpoint. Removes the visible
|
|
1355
1355
|
// duplicate node.
|
|
@@ -1548,8 +1548,8 @@ function stripRunBackboneReplicaNodes(built, dropStepIds) {
|
|
|
1548
1548
|
return {
|
|
1549
1549
|
nodes: keptNodes,
|
|
1550
1550
|
edges: keptEdges,
|
|
1551
|
-
|
|
1552
|
-
|
|
1551
|
+
sourceMediaUrn: built.sourceMediaUrn,
|
|
1552
|
+
targetMediaUrn: built.targetMediaUrn,
|
|
1553
1553
|
};
|
|
1554
1554
|
}
|
|
1555
1555
|
|
|
@@ -1569,14 +1569,14 @@ function buildRunGraphData(data) {
|
|
|
1569
1569
|
// Run mode overrides the input_slot node's label with the
|
|
1570
1570
|
// host-supplied `source_display` (runtime input filename).
|
|
1571
1571
|
// Strand mode ignores this field so the abstract graph shows
|
|
1572
|
-
// the media
|
|
1572
|
+
// the media def's title from `media_display_names`.
|
|
1573
1573
|
if (typeof data.source_display === 'string' && data.source_display.length > 0) {
|
|
1574
1574
|
strandBuiltCollapsed = {
|
|
1575
1575
|
nodes: strandBuiltCollapsed.nodes.map(n =>
|
|
1576
1576
|
n.id === 'input_slot' ? Object.assign({}, n, { label: data.source_display }) : n),
|
|
1577
1577
|
edges: strandBuiltCollapsed.edges,
|
|
1578
|
-
|
|
1579
|
-
|
|
1578
|
+
sourceMediaUrn: strandBuiltCollapsed.sourceMediaUrn,
|
|
1579
|
+
targetMediaUrn: strandBuiltCollapsed.targetMediaUrn,
|
|
1580
1580
|
};
|
|
1581
1581
|
}
|
|
1582
1582
|
|
|
@@ -1664,7 +1664,7 @@ function buildRunGraphData(data) {
|
|
|
1664
1664
|
// labels the anchor→entry edge on the first body.
|
|
1665
1665
|
//
|
|
1666
1666
|
// Without such a preceding sequence cap, the source itself is
|
|
1667
|
-
// already a list (e.g. `media:pdf;list`
|
|
1667
|
+
// already a list (e.g. `media:pdf;list` source_media_urn) and the
|
|
1668
1668
|
// ForEach iterates it directly.
|
|
1669
1669
|
let seqProducerStepIdx = -1;
|
|
1670
1670
|
let seqProducerStep = null;
|
|
@@ -2590,6 +2590,22 @@ class CapFabRenderer {
|
|
|
2590
2590
|
// viewport (≈15% slack at 800×600).
|
|
2591
2591
|
static get ZOOM_OUT_FIT_SLACK() { return 0.15; }
|
|
2592
2592
|
|
|
2593
|
+
// Fit padding must scale down in very short viewports. The compact
|
|
2594
|
+
// machine-select graph pane is only ~100px tall; a fixed 50px inset
|
|
2595
|
+
// leaves no vertical room, so the bootstrap fit degenerates and the
|
|
2596
|
+
// graph opens at Cytoscape's fallback zoom instead of a true fit.
|
|
2597
|
+
// Keep the requested desktop padding where there is room, but cap it
|
|
2598
|
+
// to a small fraction of the actual visible viewport in compact
|
|
2599
|
+
// hosts.
|
|
2600
|
+
_resolvedFitPadding(requestedPadding, containerWidth, containerHeight, excluded) {
|
|
2601
|
+
const requested = Math.max(0, requestedPadding | 0);
|
|
2602
|
+
const availableHeight = Math.max(0, containerHeight - excluded);
|
|
2603
|
+
const limitingDim = Math.max(0, Math.min(containerWidth, availableHeight));
|
|
2604
|
+
if (limitingDim <= 0) return 0;
|
|
2605
|
+
const capped = Math.floor(limitingDim * 0.12);
|
|
2606
|
+
return Math.max(0, Math.min(requested, capped));
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2593
2609
|
// Bootstrap fit: snap (no animation) to a centered, padded fit of
|
|
2594
2610
|
// the entire graph. Used during the first paint and the post-paint
|
|
2595
2611
|
// resize ticks while `_initialFitDone` is false. The padding here
|
|
@@ -2612,11 +2628,11 @@ class CapFabRenderer {
|
|
|
2612
2628
|
if (containerWidth <= 0 || containerHeight <= 0) return;
|
|
2613
2629
|
const elements = cy.elements();
|
|
2614
2630
|
if (elements.length === 0) return;
|
|
2615
|
-
const bb = elements.boundingBox();
|
|
2631
|
+
const bb = elements.boundingBox({ includeLabels: true, includeOverlays: false });
|
|
2616
2632
|
if (bb.w === 0 && bb.h === 0) return;
|
|
2617
2633
|
|
|
2618
|
-
const padding = 50;
|
|
2619
2634
|
const excluded = Math.max(0, this.bottomExcludedRegion() | 0);
|
|
2635
|
+
const padding = this._resolvedFitPadding(50, containerWidth, containerHeight, excluded);
|
|
2620
2636
|
const visibleWidth = containerWidth - padding * 2;
|
|
2621
2637
|
const visibleHeight = containerHeight - excluded - padding * 2;
|
|
2622
2638
|
if (visibleWidth <= 0 || visibleHeight <= 0) return;
|
|
@@ -2642,12 +2658,9 @@ class CapFabRenderer {
|
|
|
2642
2658
|
// our zoom/pan write and leave the graph drifting.
|
|
2643
2659
|
cy.stop(true);
|
|
2644
2660
|
this._internalPanZoom = true;
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
} finally {
|
|
2649
|
-
this._internalPanZoom = false;
|
|
2650
|
-
}
|
|
2661
|
+
cy.zoom(clampedZoom);
|
|
2662
|
+
cy.pan({ x: panX, y: panY });
|
|
2663
|
+
this._internalPanZoom = false;
|
|
2651
2664
|
}
|
|
2652
2665
|
|
|
2653
2666
|
// Mark the bootstrap centering as complete so subsequent refits
|
|
@@ -2687,7 +2700,7 @@ class CapFabRenderer {
|
|
|
2687
2700
|
// that sits comfortably above this relaxed minimum, so opening
|
|
2688
2701
|
// the view shows the graph centred with margin rather than
|
|
2689
2702
|
// bleeding to all four edges.
|
|
2690
|
-
const bb = this.cy.elements().boundingBox();
|
|
2703
|
+
const bb = this.cy.elements().boundingBox({ includeLabels: true, includeOverlays: false });
|
|
2691
2704
|
let zoomMin;
|
|
2692
2705
|
if (bb.w > 0 && bb.h > 0) {
|
|
2693
2706
|
const strictFit = Math.min(w / bb.w, h / bb.h);
|
|
@@ -3392,12 +3405,13 @@ class CapFabRenderer {
|
|
|
3392
3405
|
this.cy.stop(true);
|
|
3393
3406
|
if (!eles || eles.length === 0) eles = this.cy.elements();
|
|
3394
3407
|
|
|
3395
|
-
const bb = eles.boundingBox();
|
|
3408
|
+
const bb = eles.boundingBox({ includeLabels: true, includeOverlays: false });
|
|
3396
3409
|
if (bb.w === 0 && bb.h === 0) return;
|
|
3397
3410
|
|
|
3398
3411
|
const containerWidth = this.cy.width();
|
|
3399
3412
|
const containerHeight = this.cy.height();
|
|
3400
3413
|
const excluded = Math.max(0, this.bottomExcludedRegion() | 0);
|
|
3414
|
+
padding = this._resolvedFitPadding(padding, containerWidth, containerHeight, excluded);
|
|
3401
3415
|
|
|
3402
3416
|
const visibleWidth = containerWidth - padding * 2;
|
|
3403
3417
|
const visibleHeight = containerHeight - excluded - padding * 2;
|
|
@@ -3428,12 +3442,9 @@ class CapFabRenderer {
|
|
|
3428
3442
|
stop: () => { this._internalPanZoom = false; },
|
|
3429
3443
|
});
|
|
3430
3444
|
} else {
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
} finally {
|
|
3435
|
-
this._internalPanZoom = false;
|
|
3436
|
-
}
|
|
3445
|
+
this.cy.zoom(clampedZoom);
|
|
3446
|
+
this.cy.pan({ x: panX, y: panY });
|
|
3447
|
+
this._internalPanZoom = false;
|
|
3437
3448
|
}
|
|
3438
3449
|
}
|
|
3439
3450
|
|