capdag 1.226.626 → 1.240.679
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 +126 -61
- package/capdag.js +209 -80
- package/capdag.test.js +622 -103
- package/package.json +1 -1
package/cap-fab-renderer.js
CHANGED
|
@@ -744,6 +744,7 @@ function validateStrandStep(step, path) {
|
|
|
744
744
|
if (!step || typeof step !== 'object') {
|
|
745
745
|
throw new Error(`CapFabRenderer: ${path} is not an object`);
|
|
746
746
|
}
|
|
747
|
+
assertString(step.token_id, `${path}.token_id`);
|
|
747
748
|
assertString(step.from_spec, `${path}.from_spec`);
|
|
748
749
|
assertString(step.to_spec, `${path}.to_spec`);
|
|
749
750
|
if (!step.step_type || typeof step.step_type !== 'object') {
|
|
@@ -774,28 +775,32 @@ function validateStrandStep(step, path) {
|
|
|
774
775
|
if (typeof body.output_is_sequence !== 'boolean') {
|
|
775
776
|
throw new Error(`CapFabRenderer: ${path}.step_type.Cap.output_is_sequence must be a boolean`);
|
|
776
777
|
}
|
|
777
|
-
// `inputs`
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
|
|
783
|
-
body.inputs.forEach((input, inputIdx) => {
|
|
784
|
-
const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
|
|
785
|
-
assertObject(input, inputPath);
|
|
786
|
-
assertString(input.arg_urn, `${inputPath}.arg_urn`);
|
|
787
|
-
// serde: unit variant → the string "StrandInput"; struct variant →
|
|
788
|
-
// { Step: { token_id } }. Anything else is malformed.
|
|
789
|
-
if (input.source === 'StrandInput') return;
|
|
790
|
-
if (input.source && typeof input.source === 'object' && input.source.Step) {
|
|
791
|
-
assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
throw new Error(
|
|
795
|
-
`CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
|
|
796
|
-
);
|
|
797
|
-
});
|
|
778
|
+
// `inputs` is the authoritative data-flow graph: the primary input plus
|
|
779
|
+
// every convergence input, with each producer named by stable token.
|
|
780
|
+
assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
|
|
781
|
+
if (body.inputs.length === 0) {
|
|
782
|
+
throw new Error(`CapFabRenderer: ${path}.step_type.Cap.inputs must include the primary input`);
|
|
798
783
|
}
|
|
784
|
+
const argUrns = new Set();
|
|
785
|
+
body.inputs.forEach((input, inputIdx) => {
|
|
786
|
+
const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
|
|
787
|
+
assertObject(input, inputPath);
|
|
788
|
+
assertString(input.arg_urn, `${inputPath}.arg_urn`);
|
|
789
|
+
if (argUrns.has(input.arg_urn)) {
|
|
790
|
+
throw new Error(`CapFabRenderer: ${inputPath}.arg_urn duplicates '${input.arg_urn}'`);
|
|
791
|
+
}
|
|
792
|
+
argUrns.add(input.arg_urn);
|
|
793
|
+
// serde: unit variant → the string "StrandInput"; struct variant →
|
|
794
|
+
// { Step: { token_id } }. Anything else is malformed.
|
|
795
|
+
if (input.source === 'StrandInput') return;
|
|
796
|
+
if (input.source && typeof input.source === 'object' && input.source.Step) {
|
|
797
|
+
assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
throw new Error(
|
|
801
|
+
`CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
|
|
802
|
+
);
|
|
803
|
+
});
|
|
799
804
|
} else {
|
|
800
805
|
assertString(body.media_def, `${path}.step_type.${variant}.media_def`);
|
|
801
806
|
}
|
|
@@ -808,8 +813,26 @@ function validateStrandPayload(data) {
|
|
|
808
813
|
assertString(data.source_media_urn, 'strand mode data.source_media_urn');
|
|
809
814
|
assertString(data.target_media_urn, 'strand mode data.target_media_urn');
|
|
810
815
|
assertArray(data.steps, 'strand mode data.steps');
|
|
816
|
+
const tokenIds = new Set();
|
|
811
817
|
data.steps.forEach((step, idx) => {
|
|
812
818
|
validateStrandStep(step, `strand mode data.steps[${idx}]`);
|
|
819
|
+
if (tokenIds.has(step.token_id)) {
|
|
820
|
+
throw new Error(`CapFabRenderer strand mode: duplicate step token_id '${step.token_id}'`);
|
|
821
|
+
}
|
|
822
|
+
tokenIds.add(step.token_id);
|
|
823
|
+
});
|
|
824
|
+
data.steps.forEach((step, idx) => {
|
|
825
|
+
if (!step.step_type.Cap) return;
|
|
826
|
+
step.step_type.Cap.inputs.forEach((input, inputIdx) => {
|
|
827
|
+
if (input.source === 'StrandInput') return;
|
|
828
|
+
const producerToken = input.source.Step.token_id;
|
|
829
|
+
if (!tokenIds.has(producerToken)) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`CapFabRenderer strand mode: data.steps[${idx}].step_type.Cap.inputs[${inputIdx}]` +
|
|
832
|
+
`.source.Step.token_id '${producerToken}' does not resolve to a strand step`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
813
836
|
});
|
|
814
837
|
if (data.media_display_names !== undefined
|
|
815
838
|
&& (data.media_display_names === null || typeof data.media_display_names !== 'object')) {
|
|
@@ -824,23 +847,43 @@ function validateBodyOutcome(outcome, path) {
|
|
|
824
847
|
if (!outcome || typeof outcome !== 'object') {
|
|
825
848
|
throw new Error(`CapFabRenderer: ${path} is not an object`);
|
|
826
849
|
}
|
|
827
|
-
if (typeof outcome.body_index !== 'number' || !Number.
|
|
828
|
-
throw new Error(`CapFabRenderer: ${path}.body_index must be a non-negative integer`);
|
|
850
|
+
if (typeof outcome.body_index !== 'number' || !Number.isSafeInteger(outcome.body_index) || outcome.body_index < 0) {
|
|
851
|
+
throw new Error(`CapFabRenderer: ${path}.body_index must be a non-negative safe integer`);
|
|
852
|
+
}
|
|
853
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'foreach_token_id')) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`CapFabRenderer: ${path}.foreach_token_id must be present as null or a stable ForEach step token`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (outcome.foreach_token_id !== null
|
|
859
|
+
&& (typeof outcome.foreach_token_id !== 'string' || outcome.foreach_token_id.length === 0)) {
|
|
860
|
+
throw new Error(`CapFabRenderer: ${path}.foreach_token_id must be null or a non-empty string`);
|
|
861
|
+
}
|
|
862
|
+
if (outcome.foreach_token_id === null && outcome.body_index !== 0) {
|
|
863
|
+
throw new Error(`CapFabRenderer: ${path} is linear and must use body_index 0`);
|
|
829
864
|
}
|
|
830
865
|
if (typeof outcome.success !== 'boolean') {
|
|
831
866
|
throw new Error(`CapFabRenderer: ${path}.success must be a boolean`);
|
|
832
867
|
}
|
|
833
868
|
assertArray(outcome.cap_urns, `${path}.cap_urns`);
|
|
834
869
|
outcome.cap_urns.forEach((u, i) => assertString(u, `${path}.cap_urns[${i}]`));
|
|
835
|
-
if (outcome.
|
|
836
|
-
&& (typeof outcome.
|
|
837
|
-
throw new Error(`CapFabRenderer: ${path}.
|
|
870
|
+
if (outcome.failed_token_id !== undefined && outcome.failed_token_id !== null
|
|
871
|
+
&& (typeof outcome.failed_token_id !== 'string' || outcome.failed_token_id.length === 0)) {
|
|
872
|
+
throw new Error(`CapFabRenderer: ${path}.failed_token_id must be a non-empty string when present`);
|
|
838
873
|
}
|
|
839
|
-
if (!
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
874
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'failed_token_id')) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
`CapFabRenderer: ${path}.failed_token_id must be present as null or a stable step token`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (!Object.prototype.hasOwnProperty.call(outcome, 'failed_arg_urn')) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`CapFabRenderer: ${path}.failed_arg_urn must be present as null or an attributed argument URN`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (outcome.failed_arg_urn !== null
|
|
885
|
+
&& (typeof outcome.failed_arg_urn !== 'string' || outcome.failed_arg_urn.length === 0)) {
|
|
886
|
+
throw new Error(`CapFabRenderer: ${path}.failed_arg_urn must be null or a non-empty string`);
|
|
844
887
|
}
|
|
845
888
|
}
|
|
846
889
|
|
|
@@ -867,8 +910,29 @@ function validateRunPayload(data) {
|
|
|
867
910
|
media_display_names: data.media_display_names,
|
|
868
911
|
}));
|
|
869
912
|
assertArray(data.body_outcomes, 'run mode data.body_outcomes');
|
|
913
|
+
const bodyCoordinates = new Set();
|
|
870
914
|
data.body_outcomes.forEach((o, idx) => {
|
|
871
915
|
validateBodyOutcome(o, `run mode data.body_outcomes[${idx}]`);
|
|
916
|
+
if (typeof o.foreach_token_id === 'string'
|
|
917
|
+
&& !data.resolved_strand.steps.some(step => (
|
|
918
|
+
step.token_id === o.foreach_token_id
|
|
919
|
+
&& Object.keys(step.step_type)[0] === 'ForEach'
|
|
920
|
+
))) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`CapFabRenderer run mode: body_outcomes[${idx}].foreach_token_id '${o.foreach_token_id}' does not resolve to a ForEach strand step`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
if (typeof o.failed_token_id === 'string'
|
|
926
|
+
&& !data.resolved_strand.steps.some(step => step.token_id === o.failed_token_id)) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`CapFabRenderer run mode: body_outcomes[${idx}].failed_token_id '${o.failed_token_id}' does not resolve to a strand step`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const coordinate = `${o.foreach_token_id === null ? 'linear' : o.foreach_token_id}:${o.body_index}`;
|
|
932
|
+
if (bodyCoordinates.has(coordinate)) {
|
|
933
|
+
throw new Error(`CapFabRenderer run mode: duplicate body coordinate '${coordinate}'`);
|
|
934
|
+
}
|
|
935
|
+
bodyCoordinates.add(coordinate);
|
|
872
936
|
});
|
|
873
937
|
if (typeof data.visible_success_count !== 'number' || data.visible_success_count < 0) {
|
|
874
938
|
throw new Error('CapFabRenderer run mode: data.visible_success_count must be a non-negative number');
|
|
@@ -1125,7 +1189,7 @@ function buildBrowseGraphData(capabilities) {
|
|
|
1125
1189
|
// Parsing the URN canonicalizes it and validates it — fail hard on
|
|
1126
1190
|
// malformed registry data.
|
|
1127
1191
|
const parsedUrn = CapUrn.fromString(capData.urn);
|
|
1128
|
-
const cap = createCap(parsedUrn, title, capData.
|
|
1192
|
+
const cap = createCap(parsedUrn, title, capData.aliases || []);
|
|
1129
1193
|
const capFabEdgeIndex = capFab.edges.length;
|
|
1130
1194
|
capFab.addCap(cap, 'registry');
|
|
1131
1195
|
|
|
@@ -1383,7 +1447,7 @@ function buildStrandGraphData(data) {
|
|
|
1383
1447
|
// ORDER is not — the realizer emits edges greedily in dependency
|
|
1384
1448
|
// order, so `prevNodeId` (the previously-emitted step) need not be
|
|
1385
1449
|
// one of this cap's inputs at all.
|
|
1386
|
-
const capInputs =
|
|
1450
|
+
const capInputs = body.inputs;
|
|
1387
1451
|
const producerNodeIds = [];
|
|
1388
1452
|
for (const input of capInputs) {
|
|
1389
1453
|
const src = input && input.source;
|
|
@@ -1392,16 +1456,22 @@ function buildStrandGraphData(data) {
|
|
|
1392
1456
|
continue;
|
|
1393
1457
|
}
|
|
1394
1458
|
const producerToken = src && typeof src === 'object' && src.Step && src.Step.token_id;
|
|
1395
|
-
if (typeof producerToken !== 'string' || producerToken.length === 0)
|
|
1459
|
+
if (typeof producerToken !== 'string' || producerToken.length === 0) {
|
|
1460
|
+
throw new Error(`CapFabRenderer strand: cap step '${step.token_id}' has an invalid producer token`);
|
|
1461
|
+
}
|
|
1396
1462
|
const producerNodeId = tokenToNodeId.get(producerToken);
|
|
1397
|
-
if (producerNodeId)
|
|
1463
|
+
if (!producerNodeId) {
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
`CapFabRenderer strand: cap step '${step.token_id}' references unknown producer '${producerToken}'`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
producerNodeIds.push(producerNodeId);
|
|
1398
1469
|
}
|
|
1399
1470
|
|
|
1400
1471
|
// Pick the source of the single labeled "backbone" edge (the one
|
|
1401
|
-
// carrying the cap title + cardinality). Inside a ForEach body
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
// are preserved exactly. Otherwise anchor the backbone on a REAL
|
|
1472
|
+
// carrying the cap title + cardinality). Inside a ForEach body, keep
|
|
1473
|
+
// the linear `prevNodeId` thread so the boundary handling and its
|
|
1474
|
+
// (1→n) entry marker are preserved exactly. Otherwise anchor it on a REAL
|
|
1405
1475
|
// input: `prevNodeId` if it is genuinely one, else the first actual
|
|
1406
1476
|
// producer — so a non-input step ordered just before this cap can
|
|
1407
1477
|
// never become a spurious backbone edge.
|
|
@@ -1803,14 +1873,12 @@ function buildExternalInputRunGraphData(
|
|
|
1803
1873
|
if (inputRuns.length <= 1 || allOutcomes.length === 0) return null;
|
|
1804
1874
|
|
|
1805
1875
|
const capSteps = data.resolved_strand.steps
|
|
1806
|
-
.filter(step => Object.keys(step.step_type)[0] === 'Cap')
|
|
1807
|
-
.map(step => step.step_type.Cap);
|
|
1876
|
+
.filter(step => Object.keys(step.step_type)[0] === 'Cap');
|
|
1808
1877
|
if (capSteps.length === 0) {
|
|
1809
1878
|
throw new Error('CapFabRenderer run mode: external multi-input runs require at least one Cap step in resolved_strand.');
|
|
1810
1879
|
}
|
|
1811
1880
|
|
|
1812
1881
|
const visibleOutcomes = visibleSuccess.concat(visibleFailure);
|
|
1813
|
-
const CapUrn = requireHostDependency('CapUrn');
|
|
1814
1882
|
const sourceCanonical = canonicalMediaUrn(data.resolved_strand.source_media_urn);
|
|
1815
1883
|
const targetCanonical = canonicalMediaUrn(data.resolved_strand.target_media_urn);
|
|
1816
1884
|
const anchorNodeId = 'external-input-anchor';
|
|
@@ -1874,12 +1942,10 @@ function buildExternalInputRunGraphData(
|
|
|
1874
1942
|
|
|
1875
1943
|
let traceEnd = capSteps.length;
|
|
1876
1944
|
if (!success) {
|
|
1877
|
-
if (typeof outcome.
|
|
1878
|
-
const failedCap = CapUrn.fromString(outcome.failed_cap);
|
|
1945
|
+
if (typeof outcome.failed_token_id === 'string' && outcome.failed_token_id.length > 0) {
|
|
1879
1946
|
traceEnd = 0;
|
|
1880
1947
|
for (let i = 0; i < capSteps.length; i++) {
|
|
1881
|
-
|
|
1882
|
-
if (candidate.isEquivalent(failedCap)) {
|
|
1948
|
+
if (capSteps[i].token_id === outcome.failed_token_id) {
|
|
1883
1949
|
traceEnd = i + 1;
|
|
1884
1950
|
break;
|
|
1885
1951
|
}
|
|
@@ -1891,8 +1957,13 @@ function buildExternalInputRunGraphData(
|
|
|
1891
1957
|
|
|
1892
1958
|
let prevNodeId = sourceNodeId;
|
|
1893
1959
|
for (let i = 0; i < traceEnd; i++) {
|
|
1894
|
-
const
|
|
1895
|
-
const
|
|
1960
|
+
const capStep = capSteps[i];
|
|
1961
|
+
const cap = capStep.step_type.Cap;
|
|
1962
|
+
const targetMedia = canonicalMediaUrn(
|
|
1963
|
+
i === capSteps.length - 1
|
|
1964
|
+
? data.resolved_strand.target_media_urn
|
|
1965
|
+
: capStep.to_spec
|
|
1966
|
+
);
|
|
1896
1967
|
const isLastExecutedStep = i === traceEnd - 1;
|
|
1897
1968
|
const outputs = success ? runDef.outputs : [];
|
|
1898
1969
|
|
|
@@ -2288,19 +2359,13 @@ function buildRunGraphData(data) {
|
|
|
2288
2359
|
const edgeClass = success ? 'body-success' : 'body-failure';
|
|
2289
2360
|
const colorVar = success ? '--graph-body-edge-success' : '--graph-body-edge-failure';
|
|
2290
2361
|
|
|
2291
|
-
// Trace end: failures stop at
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
const candidate = CapUrn.fromString(bodyCapSteps[i].step.step_type.Cap.cap_urn);
|
|
2299
|
-
if (candidate.isEquivalent(target)) {
|
|
2300
|
-
traceEnd = i + 1;
|
|
2301
|
-
break;
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2362
|
+
// Trace end: failures stop at the exact stable step token.
|
|
2363
|
+
let traceEnd = success ? bodyCapSteps.length : 0;
|
|
2364
|
+
if (!success && typeof outcome.failed_token_id === 'string' && outcome.failed_token_id.length > 0) {
|
|
2365
|
+
const failedIndex = bodyCapSteps.findIndex(
|
|
2366
|
+
bodyStep => bodyStep.step.token_id === outcome.failed_token_id
|
|
2367
|
+
);
|
|
2368
|
+
if (failedIndex >= 0) traceEnd = failedIndex + 1;
|
|
2304
2369
|
}
|
|
2305
2370
|
|
|
2306
2371
|
const bodyKey = `body-${outcome.body_index}`;
|