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.
@@ -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` (capdag CapInput list) carries the data-flow graph: the main
778
- // input plus any convergence inputs. Optional on legacy payloads, but
779
- // when present it must be well-formed — the fan-in edges are drawn from
780
- // it, so a malformed entry is a hard error, not a silent skip.
781
- if (body.inputs !== undefined) {
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.isInteger(outcome.body_index) || outcome.body_index < 0) {
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.failed_cap !== undefined && outcome.failed_cap !== null
836
- && (typeof outcome.failed_cap !== 'string' || outcome.failed_cap.length === 0)) {
837
- throw new Error(`CapFabRenderer: ${path}.failed_cap must be a non-empty string when present`);
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 (!outcome.success && outcome.failed_cap === undefined) {
840
- // Failure without a failed_cap is allowed (e.g. infrastructure
841
- // failure before any cap ran) but we still expect the field to be
842
- // present — either null or a string. Rust's Option<String>
843
- // serializes as null or the string, never missing.
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.command || '');
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 = Array.isArray(body.inputs) ? body.inputs : [];
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) continue;
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) producerNodeIds.push(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 — or
1402
- // when the cap declares no inputs — keep the linear `prevNodeId`
1403
- // thread so the ForEach boundary handling and its (1→n) entry marker
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.failed_cap === 'string' && outcome.failed_cap.length > 0) {
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
- const candidate = CapUrn.fromString(capSteps[i].cap_urn);
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 cap = capSteps[i];
1895
- const targetMedia = canonicalMediaUrn(i === capSteps.length - 1 ? data.resolved_strand.target_media_urn : data.resolved_strand.steps.filter(step => Object.keys(step.step_type)[0] === 'Cap')[i].to_spec);
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 `failed_cap`. `CapUrn.isEquivalent`
2292
- // is used for the match — never string equality.
2293
- let traceEnd = bodyCapSteps.length;
2294
- if (!success && typeof outcome.failed_cap === 'string' && outcome.failed_cap.length > 0) {
2295
- const CapUrn = requireHostDependency('CapUrn');
2296
- const target = CapUrn.fromString(outcome.failed_cap);
2297
- for (let i = 0; i < bodyCapSteps.length; i++) {
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}`;