capdag 1.233.649 → 1.241.684

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/capdag.test.js CHANGED
@@ -89,6 +89,19 @@ function assertThrowsMediaUrn(fn, expectedErrorCode, message) {
89
89
  }
90
90
  }
91
91
 
92
+ function assertThrowsMessage(fn, expectedMessage, message) {
93
+ try {
94
+ fn();
95
+ } catch (error) {
96
+ assert(
97
+ typeof error.message === 'string' && error.message.includes(expectedMessage),
98
+ `${message}: expected error containing ${JSON.stringify(expectedMessage)}, got ${JSON.stringify(error.message)}`
99
+ );
100
+ return;
101
+ }
102
+ throw new Error(`Assertion failed: ${message}: expected function to throw`);
103
+ }
104
+
92
105
  function runTest(name, fn) {
93
106
  try {
94
107
  const result = fn();
@@ -715,7 +728,7 @@ function test050_matchingSemanticsDirectionMismatch() {
715
728
  assert(!cap.accepts(request), 'Incompatible direction types should not match');
716
729
  }
717
730
 
718
- // TEST890: Semantic direction matching - generic provider matches specific request
731
+ // TEST890: Semantic direction matching - generic candidate matches specific request
719
732
  function test890_directionSemanticMatching() {
720
733
  // Generic wildcard cap accepts specific pdf request
721
734
  const genericCap = CapUrn.fromString(
@@ -1409,14 +1422,14 @@ function test6208_CapFabGetOutgoingConformsToMatching() {
1409
1422
  // the renderer colours/groups edges by provenance in browse mode.
1410
1423
  function test6224_CapFabDistinctRegistryNames() {
1411
1424
  const graph = new CapFab();
1412
- graph.addCap(makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text'), 'providers');
1425
+ graph.addCap(makeGraphCap('media:ext=pdf', 'media:enc=utf-8', 'PDF to Text'), 'bundled-cartridges');
1413
1426
  graph.addCap(makeGraphCap('media:enc=utf-8', 'media:embedding-vector', 'Embed'), 'cartridges');
1414
1427
 
1415
1428
  const edges = graph.getEdges();
1416
1429
  assertEqual(edges.length, 2, 'Two caps must produce two edges');
1417
1430
 
1418
1431
  const names = new Set(edges.map(e => e.registryName));
1419
- assert(names.has('providers'), 'providers registry name must be preserved');
1432
+ assert(names.has('bundled-cartridges'), 'bundled-cartridges registry name must be preserved');
1420
1433
  assert(names.has('cartridges'), 'cartridges registry name must be preserved');
1421
1434
  }
1422
1435
 
@@ -2687,11 +2700,11 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
2687
2700
  }
2688
2701
 
2689
2702
  // TEST1878: a cartridge marked `installed_from: bundle` with no baked hash in
2690
- // BUNDLED_PROVIDER_HASHES (empty in this mirror) is rejected as BadInstallation
2703
+ // BUNDLED_CARTRIDGE_HASHES (empty in this mirror) is rejected as BadInstallation
2691
2704
  // — the bundled-integrity gate fires before the probe. Non-macOS only: on macOS
2692
2705
  // the baked-hash path is intentionally absent (OS code-signature is the guard),
2693
- // so a bundled provider is accepted there and would instead end at the probe.
2694
- async function test1878_bundledProviderWithoutBakedHashIsRejected() {
2706
+ // so a bundled cartridge is accepted there and would instead end at the probe.
2707
+ async function test1878_bundledCartridgeWithoutBakedHashIsRejected() {
2695
2708
  if (process.platform === 'darwin') {
2696
2709
  // Mirrors the Rust #[cfg(not(target_os = "macos"))] gate: on macOS the
2697
2710
  // baked-hash path does not exist, so this scenario is not exercised.
@@ -2709,7 +2722,7 @@ async function test1878_bundledProviderWithoutBakedHashIsRejected() {
2709
2722
  discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
2710
2723
  const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
2711
2724
  discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
2712
- assert(out[0].error.message.includes('bundled provider integrity'),
2725
+ assert(out[0].error.message.includes('bundled cartridge integrity'),
2713
2726
  `message should name the bundled-integrity failure: ${out[0].error.message}`);
2714
2727
  } finally {
2715
2728
  discoveryRemoveRoot(root);
@@ -3195,11 +3208,11 @@ function test127_invalidEffectNoneFailsHard() {
3195
3208
 
3196
3209
  // TEST128: omitted effect means declared; unconstrained effect must be explicit
3197
3210
  function test128_effectDispatchRequiresExplicitWildcard() {
3198
- const noneProvider = CapUrn.fromString('cap:effect=none');
3211
+ const noneCandidate = CapUrn.fromString('cap:effect=none');
3199
3212
  const declaredRequest = CapUrn.fromString('cap:raw');
3200
3213
  const anyRequest = CapUrn.fromString('cap:?effect');
3201
- assert(!noneProvider.isDispatchable(declaredRequest), 'effect=none should not silently satisfy declared request');
3202
- assert(noneProvider.isDispatchable(anyRequest), 'explicit ?effect should accept any provider effect');
3214
+ assert(!noneCandidate.isDispatchable(declaredRequest), 'effect=none should not silently satisfy declared request');
3215
+ assert(noneCandidate.isDispatchable(anyRequest), 'explicit ?effect should accept any candidate effect');
3203
3216
  }
3204
3217
 
3205
3218
  // ============================================================================
@@ -4296,7 +4309,7 @@ function test6467_Machine_capRegistryEntry_construction() {
4296
4309
  command: 'extract',
4297
4310
  cap_description: 'Extracts text from PDF',
4298
4311
  args: [{ media_urn: 'media:ext=pdf', required: true }],
4299
- output: { media_urn: 'media:enc=utf-8;ext=txt', output_description: 'Extracted text' },
4312
+ output: { media_urn: 'media:enc=utf-8;ext=txt', output_description: 'Digitized text' },
4300
4313
  media_defs: [],
4301
4314
  urn_tags: { op: 'extract' },
4302
4315
  in_spec: 'media:ext=pdf',
@@ -4567,8 +4580,16 @@ function test6483_Renderer_buildBrowseGraphData_rejectsMissingMediaTitles() {
4567
4580
 
4568
4581
  // ---------------- strand builder ----------------
4569
4582
 
4583
+ let fixtureStepToken = 0;
4584
+
4585
+ function nextFixtureStepToken() {
4586
+ fixtureStepToken += 1;
4587
+ return `fixture-step-${fixtureStepToken}`;
4588
+ }
4589
+
4570
4590
  function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
4571
4591
  return {
4592
+ token_id: nextFixtureStepToken(),
4572
4593
  step_type: {
4573
4594
  Cap: {
4574
4595
  cap_urn: capUrn,
@@ -4576,6 +4597,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
4576
4597
  specificity: 0,
4577
4598
  input_is_sequence: inSeq,
4578
4599
  output_is_sequence: outSeq,
4600
+ inputs: [{ arg_urn: fromSpec, source: 'StrandInput' }],
4579
4601
  },
4580
4602
  },
4581
4603
  from_spec: fromSpec,
@@ -4585,6 +4607,7 @@ function makeCapStep(capUrn, title, fromSpec, toSpec, inSeq, outSeq) {
4585
4607
 
4586
4608
  function makeForEachStep(mediaDef) {
4587
4609
  return {
4610
+ token_id: nextFixtureStepToken(),
4588
4611
  step_type: { ForEach: { media_def: mediaDef } },
4589
4612
  from_spec: mediaDef,
4590
4613
  to_spec: mediaDef,
@@ -4593,12 +4616,34 @@ function makeForEachStep(mediaDef) {
4593
4616
 
4594
4617
  function makeCollectStep(mediaDef) {
4595
4618
  return {
4619
+ token_id: nextFixtureStepToken(),
4596
4620
  step_type: { Collect: { media_def: mediaDef } },
4597
4621
  from_spec: mediaDef,
4598
4622
  to_spec: mediaDef,
4599
4623
  };
4600
4624
  }
4601
4625
 
4626
+ // Build an intentionally linear fixture while still spelling the canonical
4627
+ // stable-token producer graph that production payloads carry. ForEach changes
4628
+ // cardinality but does not replace the current value producer; Collect does.
4629
+ function wireLinearStepInputs(steps) {
4630
+ let producerToken = null;
4631
+ for (const step of steps) {
4632
+ if (step.step_type.Cap) {
4633
+ step.step_type.Cap.inputs = [{
4634
+ arg_urn: step.from_spec,
4635
+ source: producerToken === null
4636
+ ? 'StrandInput'
4637
+ : { Step: { token_id: producerToken } },
4638
+ }];
4639
+ producerToken = step.token_id;
4640
+ } else if (step.step_type.Collect) {
4641
+ producerToken = step.token_id;
4642
+ }
4643
+ }
4644
+ return steps;
4645
+ }
4646
+
4602
4647
  // TEST6484: Renderer validate strand step rejects unknown variant
4603
4648
  function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
4604
4649
  // A step with an unknown variant must fail hard at validation; no
@@ -4606,6 +4651,7 @@ function test6484_Renderer_validateStrandStep_rejectsUnknownVariant() {
4606
4651
  let threw = false;
4607
4652
  try {
4608
4653
  rendererValidateStrandStep({
4654
+ token_id: nextFixtureStepToken(),
4609
4655
  step_type: { WrongVariant: {} },
4610
4656
  from_spec: 'media:a',
4611
4657
  to_spec: 'media:a',
@@ -4624,6 +4670,7 @@ function test6486_Renderer_validateStrandStep_requiresBooleanIsSequence() {
4624
4670
  let threw = false;
4625
4671
  try {
4626
4672
  rendererValidateStrandStep({
4673
+ token_id: nextFixtureStepToken(),
4627
4674
  step_type: { Cap: {
4628
4675
  cap_urn: 'cap:in="media:a";x;out="media:b"',
4629
4676
  title: 't',
@@ -4645,13 +4692,13 @@ function test6487_Renderer_classifyStrandCapSteps_capFlags() {
4645
4692
  // Strand: ForEach → cap1 → cap2 → cap3 → Collect. cap1 should have
4646
4693
  // prevForEach=true; cap3 should have nextCollect=true; cap2 should
4647
4694
  // have neither.
4648
- const steps = [
4695
+ const steps = wireLinearStepInputs([
4649
4696
  makeForEachStep('media:ext=pdf;list'),
4650
4697
  makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
4651
4698
  makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=jpg"', 'b', 'media:ext=png;image', 'media:ext=jpg', false, false),
4652
4699
  makeCapStep('cap:in="media:ext=jpg";c;out="media:ext=txt"', 'c', 'media:ext=jpg', 'media:ext=txt', false, false),
4653
4700
  makeCollectStep('media:ext=txt'),
4654
- ];
4701
+ ]);
4655
4702
  const { capStepIndices, capFlags } = rendererClassifyStrandCapSteps(steps);
4656
4703
  assertEqual(capStepIndices.length, 3, 'three cap steps');
4657
4704
  assert(capFlags.get(1).prevForEach, 'cap1 has prevForEach');
@@ -4667,7 +4714,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
4667
4714
  // Nested strand: ForEach → cap1 → ForEach → cap2 → Collect → cap3 → Collect.
4668
4715
  // cap1 has prevForEach (outer), cap2 has prevForEach (inner) and
4669
4716
  // nextCollect (inner), cap3 has nextCollect (outer).
4670
- const steps = [
4717
+ const steps = wireLinearStepInputs([
4671
4718
  makeForEachStep('media:a;list'),
4672
4719
  makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4673
4720
  makeForEachStep('media:b;list'),
@@ -4675,7 +4722,7 @@ function test6488_Renderer_classifyStrandCapSteps_nestedForks() {
4675
4722
  makeCollectStep('media:c'),
4676
4723
  makeCapStep('cap:in="media:c";c;out="media:d"', 'c', 'media:c', 'media:d', false, false),
4677
4724
  makeCollectStep('media:d'),
4678
- ];
4725
+ ]);
4679
4726
  const { capFlags } = rendererClassifyStrandCapSteps(steps);
4680
4727
  assert(capFlags.get(1).prevForEach && !capFlags.get(1).nextCollect, 'cap1 outer entry');
4681
4728
  assert(capFlags.get(3).prevForEach && capFlags.get(3).nextCollect, 'cap2 inner both');
@@ -4702,9 +4749,9 @@ function test6489_Renderer_buildStrandGraphData_singleCapPlain() {
4702
4749
  const payload = withMediaDisplayNames({
4703
4750
  source_media_urn: 'media:a',
4704
4751
  target_media_urn: 'media:b',
4705
- steps: [
4752
+ steps: wireLinearStepInputs([
4706
4753
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4707
- ],
4754
+ ]),
4708
4755
  }, {
4709
4756
  'media:a': 'Source A',
4710
4757
  'media:b': 'Target B',
@@ -4728,9 +4775,9 @@ function test6491_Renderer_buildStrandGraphData_sequenceShowsCardinality() {
4728
4775
  const payload = withMediaDisplayNames({
4729
4776
  source_media_urn: 'media:a;list',
4730
4777
  target_media_urn: 'media:b',
4731
- steps: [
4778
+ steps: wireLinearStepInputs([
4732
4779
  makeCapStep('cap:in="media:a;list";x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
4733
- ],
4780
+ ]),
4734
4781
  }, {
4735
4782
  'media:a;list': 'Source A List',
4736
4783
  'media:b': 'Target B',
@@ -4759,11 +4806,11 @@ function test6492_Renderer_buildStrandGraphData_foreachCollectSpan() {
4759
4806
  const payload = withMediaDisplayNames({
4760
4807
  source_media_urn: 'media:ext=pdf;list',
4761
4808
  target_media_urn: 'media:txt;list',
4762
- steps: [
4809
+ steps: wireLinearStepInputs([
4763
4810
  makeForEachStep('media:ext=pdf;list'),
4764
4811
  makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
4765
4812
  makeCollectStep('media:ext=txt'),
4766
- ],
4813
+ ]),
4767
4814
  }, {
4768
4815
  'media:ext=pdf;list': 'PDF List',
4769
4816
  'media:ext=txt': 'Plain Text',
@@ -4802,10 +4849,10 @@ function test6493_Renderer_buildStrandGraphData_standaloneCollect() {
4802
4849
  const payload = withMediaDisplayNames({
4803
4850
  source_media_urn: 'media:a',
4804
4851
  target_media_urn: 'media:b;list',
4805
- steps: [
4852
+ steps: wireLinearStepInputs([
4806
4853
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4807
4854
  makeCollectStep('media:b'),
4808
- ],
4855
+ ]),
4809
4856
  }, {
4810
4857
  'media:a': 'Source A',
4811
4858
  'media:b': 'Target B',
@@ -4831,11 +4878,11 @@ function test6494_Renderer_buildStrandGraphData_unclosedForEachBody() {
4831
4878
  const payload = withMediaDisplayNames({
4832
4879
  source_media_urn: 'media:a',
4833
4880
  target_media_urn: 'media:c',
4834
- steps: [
4881
+ steps: wireLinearStepInputs([
4835
4882
  makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4836
4883
  makeForEachStep('media:b'),
4837
4884
  makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
4838
- ],
4885
+ ]),
4839
4886
  }, {
4840
4887
  'media:a': 'Source A',
4841
4888
  'media:b': 'Intermediate B',
@@ -4869,11 +4916,11 @@ function test6495_Renderer_buildStrandGraphData_nestedForEachThrows() {
4869
4916
  const payload = withMediaDisplayNames({
4870
4917
  source_media_urn: 'media:a;list',
4871
4918
  target_media_urn: 'media:a',
4872
- steps: [
4919
+ steps: wireLinearStepInputs([
4873
4920
  makeForEachStep('media:a;list'),
4874
4921
  makeForEachStep('media:a'),
4875
4922
  makeCapStep('cap:in="media:a";x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
4876
- ],
4923
+ ]),
4877
4924
  }, {
4878
4925
  'media:a;list': 'Source A List',
4879
4926
  'media:a': 'Source A',
@@ -4907,11 +4954,11 @@ function test6496_Renderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4907
4954
  const payload = withMediaDisplayNames({
4908
4955
  source_media_urn: 'media:ext=pdf;list',
4909
4956
  target_media_urn: 'media:txt;list',
4910
- steps: [
4957
+ steps: wireLinearStepInputs([
4911
4958
  makeForEachStep('media:ext=pdf;list'),
4912
4959
  makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
4913
4960
  makeCollectStep('media:ext=txt'),
4914
- ],
4961
+ ]),
4915
4962
  }, {
4916
4963
  'media:ext=pdf;list': 'PDF List',
4917
4964
  'media:ext=txt': 'Plain Text',
@@ -4957,11 +5004,11 @@ function test6497_Renderer_collapseStrand_unclosedForEachBodyCollapses() {
4957
5004
  const payload = withMediaDisplayNames({
4958
5005
  source_media_urn: 'media:a',
4959
5006
  target_media_urn: 'media:c',
4960
- steps: [
5007
+ steps: wireLinearStepInputs([
4961
5008
  makeCapStep('cap:in="media:a";a;out="media:b"', 'a', 'media:a', 'media:b', false, false),
4962
5009
  makeForEachStep('media:b'),
4963
5010
  makeCapStep('cap:in="media:b";b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
4964
- ],
5011
+ ]),
4965
5012
  }, {
4966
5013
  'media:a': 'Source A',
4967
5014
  'media:b': 'Intermediate B',
@@ -5015,10 +5062,10 @@ function test6498_Renderer_collapseStrand_standaloneCollectCollapses() {
5015
5062
  const payload = withMediaDisplayNames({
5016
5063
  source_media_urn: 'media:a',
5017
5064
  target_media_urn: 'media:b;list',
5018
- steps: [
5065
+ steps: wireLinearStepInputs([
5019
5066
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
5020
5067
  makeCollectStep('media:b'),
5021
- ],
5068
+ ]),
5022
5069
  }, {
5023
5070
  'media:a': 'Source A',
5024
5071
  'media:b': 'Target B',
@@ -5064,11 +5111,11 @@ function test6499_Renderer_collapseStrand_sequenceProducingCapBeforeForeach() {
5064
5111
  const payload = withMediaDisplayNames({
5065
5112
  source_media_urn: 'media:ext=pdf',
5066
5113
  target_media_urn: 'media:decision',
5067
- steps: [
5114
+ steps: wireLinearStepInputs([
5068
5115
  makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5069
5116
  makeForEachStep('media:page'),
5070
5117
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5071
- ],
5118
+ ]),
5072
5119
  }, {
5073
5120
  'media:ext=pdf': 'PDF',
5074
5121
  'media:page': 'Page',
@@ -5120,9 +5167,9 @@ function test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput() {
5120
5167
  const payload = withMediaDisplayNames({
5121
5168
  source_media_urn: 'media:a',
5122
5169
  target_media_urn: 'media:b',
5123
- steps: [
5170
+ steps: wireLinearStepInputs([
5124
5171
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
5125
- ],
5172
+ ]),
5126
5173
  }, {
5127
5174
  'media:a': 'Source A',
5128
5175
  'media:b': 'Target B',
@@ -5153,9 +5200,9 @@ function test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge() {
5153
5200
  const payload = withMediaDisplayNames({
5154
5201
  source_media_urn: 'media:a',
5155
5202
  target_media_urn: 'media:b;list',
5156
- steps: [
5203
+ steps: wireLinearStepInputs([
5157
5204
  makeCapStep('cap:in="media:a";x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
5158
- ],
5205
+ ]),
5159
5206
  }, {
5160
5207
  'media:a': 'Source A',
5161
5208
  'media:b': 'Target B',
@@ -5184,24 +5231,175 @@ function test6502_Renderer_validateStrandPayload_missingSourceMediaUrn() {
5184
5231
  assert(threw, 'missing source_media_urn must throw');
5185
5232
  }
5186
5233
 
5234
+ // TEST7120: Renderer requires the authoritative cap input graph
5235
+ function test7120_Renderer_validateStrandPayload_requiresAuthoritativeInputs() {
5236
+ const step = makeCapStep(
5237
+ 'cap:in="media:a";x;out="media:b"',
5238
+ 'x', 'media:a', 'media:b', false, false
5239
+ );
5240
+ const payload = { source_media_urn: 'media:a', target_media_urn: 'media:b', steps: [step] };
5241
+
5242
+ const missing = JSON.parse(JSON.stringify(payload));
5243
+ delete missing.steps[0].step_type.Cap.inputs;
5244
+ assertThrowsMessage(
5245
+ () => rendererValidateStrandPayload(missing),
5246
+ 'inputs must be an array',
5247
+ 'a cap cannot omit its data-flow graph'
5248
+ );
5249
+
5250
+ const empty = JSON.parse(JSON.stringify(payload));
5251
+ empty.steps[0].step_type.Cap.inputs = [];
5252
+ assertThrowsMessage(
5253
+ () => rendererValidateStrandPayload(empty),
5254
+ 'must include the primary input',
5255
+ 'a cap cannot omit its primary input'
5256
+ );
5257
+
5258
+ const duplicate = JSON.parse(JSON.stringify(payload));
5259
+ duplicate.steps[0].step_type.Cap.inputs.push(
5260
+ { arg_urn: 'media:a', source: 'StrandInput' }
5261
+ );
5262
+ assertThrowsMessage(
5263
+ () => rendererValidateStrandPayload(duplicate),
5264
+ "duplicates 'media:a'",
5265
+ 'one cap cannot bind the same argument slot twice'
5266
+ );
5267
+
5268
+ const unresolved = JSON.parse(JSON.stringify(payload));
5269
+ unresolved.steps[0].step_type.Cap.inputs[0].source = { Step: { token_id: 'missing-step' } };
5270
+ assertThrowsMessage(
5271
+ () => rendererValidateStrandPayload(unresolved),
5272
+ 'does not resolve to a strand step',
5273
+ 'a cap input cannot name an unknown producer'
5274
+ );
5275
+ }
5276
+
5187
5277
  // ---------------- run builder ----------------
5188
5278
 
5189
5279
  function test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex() {
5190
5280
  let threw = false;
5191
5281
  try {
5192
- rendererValidateBodyOutcome({ body_index: -1, success: true, cap_urns: [] }, 'test');
5282
+ rendererValidateBodyOutcome({ body_index: -1, foreach_token_id: null, success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null }, 'test');
5193
5283
  } catch (e) {
5194
5284
  threw = true;
5195
5285
  }
5196
5286
  assert(threw, 'negative body_index must throw');
5197
5287
  }
5198
5288
 
5289
+ // TEST7121: Renderer body outcomes require an explicit stable failure coordinate
5290
+ function test7121_Renderer_validateBodyOutcome_requiresExplicitFailureCoordinate() {
5291
+ rendererValidateBodyOutcome(
5292
+ { body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
5293
+ 'unattributed'
5294
+ );
5295
+ let threw = false;
5296
+ try {
5297
+ rendererValidateBodyOutcome({ body_index: 0, success: false, cap_urns: [] }, 'missing');
5298
+ } catch (error) {
5299
+ threw = true;
5300
+ assert(error.message.includes('foreach_token_id must be present as null or a stable ForEach step token'),
5301
+ 'missing execution-region coordinate error must state the required wire shape');
5302
+ }
5303
+ assert(threw, 'omitting foreach_token_id must fail hard');
5304
+
5305
+ threw = false;
5306
+ try {
5307
+ rendererValidateBodyOutcome(
5308
+ { body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_arg_urn: null },
5309
+ 'missing-failure-coordinate'
5310
+ );
5311
+ } catch (error) {
5312
+ threw = true;
5313
+ assert(error.message.includes('must be present as null or a stable step token'),
5314
+ 'missing failure coordinate error must state the required wire shape');
5315
+ }
5316
+ assert(threw, 'omitting failed_token_id must fail hard');
5317
+
5318
+ threw = false;
5319
+ try {
5320
+ rendererValidateBodyOutcome(
5321
+ { body_index: 0, foreach_token_id: null, success: false, cap_urns: [], failed_token_id: null },
5322
+ 'missing-argument-coordinate'
5323
+ );
5324
+ } catch (error) {
5325
+ threw = true;
5326
+ assert(error.message.includes('must be present as null or an attributed argument URN'),
5327
+ 'missing argument coordinate error must state the required wire shape');
5328
+ }
5329
+ assert(threw, 'omitting failed_arg_urn must fail hard');
5330
+
5331
+ assertThrowsMessage(
5332
+ () => rendererValidateBodyOutcome(
5333
+ { body_index: 1, foreach_token_id: null, success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
5334
+ 'nonzero-linear'
5335
+ ),
5336
+ 'linear and must use body_index 0',
5337
+ 'a bare ordinal cannot address a second linear body'
5338
+ );
5339
+
5340
+ assertThrowsMessage(
5341
+ () => rendererValidateBodyOutcome(
5342
+ { body_index: Number.MAX_SAFE_INTEGER + 1, foreach_token_id: 'tok-foreach', success: true, cap_urns: [], failed_token_id: null, failed_arg_urn: null },
5343
+ 'unsafe-coordinate'
5344
+ ),
5345
+ 'non-negative safe integer',
5346
+ 'JSON transport must not round a body coordinate'
5347
+ );
5348
+ }
5349
+
5350
+ // TEST7122: Renderer body outcomes reject execution-region tokens for non-ForEach steps
5351
+ function test7122_Renderer_validateRunPayload_requiresForEachRegionCoordinate() {
5352
+ const strand = {
5353
+ source_media_urn: 'media:a',
5354
+ target_media_urn: 'media:b',
5355
+ steps: wireLinearStepInputs([
5356
+ makeCapStep('cap:in="media:a";convert;out="media:b"', 'convert', 'media:a', 'media:b', false, false),
5357
+ ]),
5358
+ };
5359
+ const payload = {
5360
+ resolved_strand: strand,
5361
+ body_outcomes: [{
5362
+ body_index: 0,
5363
+ foreach_token_id: strand.steps[0].token_id,
5364
+ success: true,
5365
+ cap_urns: [],
5366
+ failed_token_id: null,
5367
+ failed_arg_urn: null,
5368
+ }],
5369
+ visible_success_count: 1,
5370
+ visible_failure_count: 0,
5371
+ total_body_count: 1,
5372
+ };
5373
+ assertThrowsMessage(
5374
+ () => rendererValidateRunPayload(payload),
5375
+ 'does not resolve to a ForEach strand step',
5376
+ 'a body outcome cannot use a cap token as its execution-region coordinate'
5377
+ );
5378
+
5379
+ const duplicateCoordinate = {
5380
+ ...payload,
5381
+ body_outcomes: [0, 1].map(() => ({
5382
+ body_index: 0,
5383
+ foreach_token_id: null,
5384
+ success: true,
5385
+ cap_urns: [],
5386
+ failed_token_id: null,
5387
+ failed_arg_urn: null,
5388
+ })),
5389
+ };
5390
+ assertThrowsMessage(
5391
+ () => rendererValidateRunPayload(duplicateCoordinate),
5392
+ "duplicate body coordinate 'linear:0'",
5393
+ 'one execution coordinate cannot acquire two outcomes'
5394
+ );
5395
+ }
5396
+
5199
5397
  // TEST6504: Renderer build run graph data pages successes and failures
5200
5398
  function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
5201
5399
  // 6 successes, 4 failures. visible=3+2, total=10. Body has 2
5202
5400
  // caps (a, b). Each body replica is a chain of:
5203
5401
  // entry node + body_step_0 (cap a) + body_step_1 (cap b)
5204
- // = 3 nodes per body. Failed bodies truncate at failed_cap
5402
+ // = 3 nodes per body. Failed bodies truncate at failed_token_id
5205
5403
  // (cap b, idx=1) so `traceEnd=2` — same 3-node chain.
5206
5404
  //
5207
5405
  // Total replica nodes: (3 success × 3) + (2 failure × 3) = 15.
@@ -5211,26 +5409,28 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
5211
5409
  const strand = {
5212
5410
  source_media_urn: 'media:ext=pdf;list',
5213
5411
  target_media_urn: 'media:ext=txt',
5214
- steps: [
5412
+ steps: wireLinearStepInputs([
5215
5413
  makeForEachStep('media:ext=pdf;list'),
5216
5414
  makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=png;image"', 'a', 'media:ext=pdf', 'media:ext=png;image', false, false),
5217
5415
  makeCapStep('cap:in="media:ext=png;image";b;out="media:ext=txt"', 'b', 'media:ext=png;image', 'media:ext=txt', false, false),
5218
5416
  makeCollectStep('media:ext=txt'),
5219
- ],
5417
+ ]),
5220
5418
  };
5221
5419
  const outcomes = [];
5222
5420
  for (let i = 0; i < 6; i++) {
5223
- outcomes.push({ body_index: i, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 });
5421
+ outcomes.push({ body_index: i, foreach_token_id: strand.steps[0].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null });
5224
5422
  }
5225
5423
  for (let i = 6; i < 10; i++) {
5226
5424
  outcomes.push({
5227
5425
  body_index: i,
5426
+ foreach_token_id: strand.steps[0].token_id,
5228
5427
  success: false,
5229
5428
  cap_urns: [],
5230
5429
  saved_paths: [],
5231
5430
  total_bytes: 0,
5232
5431
  duration_ms: 0,
5233
- failed_cap: 'cap:in="media:ext=png;image";b;out="media:ext=txt"',
5432
+ failed_token_id: strand.steps[2].token_id,
5433
+ failed_arg_urn: null,
5234
5434
  error: 'oom',
5235
5435
  });
5236
5436
  }
@@ -5267,22 +5467,21 @@ function test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures() {
5267
5467
  assertEqual(failureShowMore.data.hiddenCount, 2, 'failure hidden count = 2');
5268
5468
  }
5269
5469
 
5270
- // TEST6505: Renderer build run graph data failure without failed cap renders full trace
5271
- function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace() {
5272
- // A failure without failed_cap (infrastructure failure) must
5273
- // still render the full body trace — the builder must not
5274
- // crash or produce zero replicas.
5470
+ // TEST6505: Renderer run graph keeps an unattributed body failure at its entry
5471
+ function test6505_Renderer_buildRunGraphData_unattributedFailureStopsAtEntry() {
5472
+ // A failure without failed_token_id is machine-scoped. The body remains
5473
+ // visible, but no cap is falsely presented as the failure location.
5275
5474
  //
5276
5475
  // Strand [ForEach, Cap, Collect] → body has 1 cap. Each body
5277
5476
  // replica emits 1 entry node + 1 body cap node = 2 nodes.
5278
5477
  const strand = {
5279
5478
  source_media_urn: 'media:ext=pdf;list',
5280
5479
  target_media_urn: 'media:ext=txt',
5281
- steps: [
5480
+ steps: wireLinearStepInputs([
5282
5481
  makeForEachStep('media:ext=pdf;list'),
5283
5482
  makeCapStep('cap:in="media:ext=pdf";a;out="media:ext=txt"', 'a', 'media:ext=pdf', 'media:ext=txt', false, false),
5284
5483
  makeCollectStep('media:ext=txt'),
5285
- ],
5484
+ ]),
5286
5485
  };
5287
5486
  const payload = {
5288
5487
  resolved_strand: strand,
@@ -5292,7 +5491,7 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
5292
5491
  'media:ext=txt': 'Text',
5293
5492
  },
5294
5493
  body_outcomes: [
5295
- { body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, error: 'unknown' },
5494
+ { body_index: 0, foreach_token_id: strand.steps[0].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null, error: 'unknown' },
5296
5495
  ],
5297
5496
  visible_success_count: 0,
5298
5497
  visible_failure_count: 5,
@@ -5303,19 +5502,15 @@ function test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullT
5303
5502
  for (const n of built.replicaNodes) {
5304
5503
  if (n.classes === 'body-failure') failureNodes++;
5305
5504
  }
5306
- assertEqual(failureNodes, 2, 'entry + body cap = 2 failure replica nodes');
5505
+ assertEqual(failureNodes, 1, 'only the body entry is shown for an unattributed failure');
5307
5506
  }
5308
5507
 
5309
- // TEST6506: Renderer build run graph data uses cap urn is equivalent for failed cap
5310
- function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap() {
5311
- // The renderer matches failed_cap against step cap URNs via
5312
- // CapUrn.isEquivalent, NOT string equality. Feed a payload where
5313
- // failed_cap and the step's cap_urn differ only in tag order — they
5314
- // should still match, proving URNs are not treated as strings.
5508
+ // TEST6506: Renderer run graph resolves a failure by exact stable step token
5509
+ function test6506_Renderer_buildRunGraphData_resolvesExactFailedStepToken() {
5315
5510
  const strand = {
5316
5511
  source_media_urn: 'media:a',
5317
5512
  target_media_urn: 'media:c',
5318
- steps: [
5513
+ steps: wireLinearStepInputs([
5319
5514
  makeForEachStep('media:a;list'),
5320
5515
  // Canonical form places tags alphabetically: op after in/out.
5321
5516
  makeCapStep(
@@ -5327,11 +5522,8 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
5327
5522
  'y', 'media:b', 'media:c', false, false
5328
5523
  ),
5329
5524
  makeCollectStep('media:c'),
5330
- ],
5525
+ ]),
5331
5526
  };
5332
- // The failed_cap URN is semantically the same as step 1 (cap y). If
5333
- // CapUrn.fromString canonicalizes (it should), any equivalent form
5334
- // will match. Feed a fully-specified form that is equivalent.
5335
5527
  const payload = {
5336
5528
  resolved_strand: strand,
5337
5529
  media_display_names: {
@@ -5343,12 +5535,14 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
5343
5535
  body_outcomes: [
5344
5536
  {
5345
5537
  body_index: 0,
5538
+ foreach_token_id: strand.steps[0].token_id,
5346
5539
  success: false,
5347
5540
  cap_urns: [],
5348
5541
  saved_paths: [],
5349
5542
  total_bytes: 0,
5350
5543
  duration_ms: 0,
5351
- failed_cap: 'cap:in=media:b;out=media:c;y', // different tag order
5544
+ failed_token_id: strand.steps[2].token_id,
5545
+ failed_arg_urn: null,
5352
5546
  error: 'fail',
5353
5547
  },
5354
5548
  ],
@@ -5363,7 +5557,19 @@ function test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap(
5363
5557
  }
5364
5558
  // 1 entry + 2 body step nodes (cap x and cap y, truncated
5365
5559
  // at cap y) = 3 failure replica nodes.
5366
- assertEqual(failureNodes, 3, 'trace truncates at cap y via isEquivalent, yielding entry + 2 cap nodes');
5560
+ assertEqual(failureNodes, 3, 'trace truncates at cap y by stable token, yielding entry + 2 cap nodes');
5561
+
5562
+ const invalid = JSON.parse(JSON.stringify(payload));
5563
+ invalid.body_outcomes[0].failed_token_id = 'missing-step-token';
5564
+ let threw = false;
5565
+ try {
5566
+ rendererBuildRunGraphData(invalid);
5567
+ } catch (error) {
5568
+ threw = true;
5569
+ assert(error.message.includes('does not resolve to a strand step'),
5570
+ 'unknown token failure must identify the unresolved coordinate');
5571
+ }
5572
+ assert(threw, 'unknown failed_token_id must fail hard');
5367
5573
  }
5368
5574
 
5369
5575
  // TEST6507: Renderer build run graph data backbone has no foreach node
@@ -5382,11 +5588,11 @@ function test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode() {
5382
5588
  const strand = {
5383
5589
  source_media_urn: 'media:ext=pdf',
5384
5590
  target_media_urn: 'media:decision',
5385
- steps: [
5591
+ steps: wireLinearStepInputs([
5386
5592
  makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5387
5593
  makeForEachStep('media:page'),
5388
5594
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5389
- ],
5595
+ ]),
5390
5596
  };
5391
5597
  const payload = {
5392
5598
  resolved_strand: strand,
@@ -5431,13 +5637,12 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
5431
5637
  const strand = {
5432
5638
  source_media_urn: 'media:ext=pdf',
5433
5639
  target_media_urn: 'media:decision',
5434
- steps: [
5640
+ steps: wireLinearStepInputs([
5435
5641
  makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5436
5642
  makeForEachStep('media:page'),
5437
5643
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5438
- ],
5644
+ ]),
5439
5645
  };
5440
- const failedCapUrn = 'cap:in="media:page";decide;out="media:decision"';
5441
5646
  const payload = {
5442
5647
  resolved_strand: strand,
5443
5648
  media_display_names: {
@@ -5446,8 +5651,8 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
5446
5651
  'media:decision': 'Decision',
5447
5652
  },
5448
5653
  body_outcomes: [
5449
- { body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
5450
- { body_index: 1, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
5654
+ { body_index: 0, foreach_token_id: strand.steps[1].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: strand.steps[2].token_id, failed_arg_urn: null, error: 'boom' },
5655
+ { body_index: 1, foreach_token_id: strand.steps[1].token_id, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: strand.steps[2].token_id, failed_arg_urn: null, error: 'boom' },
5451
5656
  ],
5452
5657
  visible_success_count: 3,
5453
5658
  visible_failure_count: 3,
@@ -5502,11 +5707,11 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
5502
5707
  const strand = {
5503
5708
  source_media_urn: 'media:ext=pdf',
5504
5709
  target_media_urn: 'media:decision',
5505
- steps: [
5710
+ steps: wireLinearStepInputs([
5506
5711
  makeCapStep('cap:in="media:ext=pdf";disbind;out="media:page"', 'Disbind', 'media:ext=pdf', 'media:page', false, true),
5507
5712
  makeForEachStep('media:page'),
5508
5713
  makeCapStep('cap:in="media:page";decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
5509
- ],
5714
+ ]),
5510
5715
  };
5511
5716
  const payload = {
5512
5717
  resolved_strand: strand,
@@ -5516,7 +5721,7 @@ function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
5516
5721
  'media:decision': 'Decision',
5517
5722
  },
5518
5723
  body_outcomes: [
5519
- { body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
5724
+ { body_index: 0, foreach_token_id: strand.steps[1].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null },
5520
5725
  ],
5521
5726
  visible_success_count: 3,
5522
5727
  visible_failure_count: 3,
@@ -5562,11 +5767,11 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
5562
5767
  const strand = {
5563
5768
  source_media_urn: 'media:ext=pdf;list',
5564
5769
  target_media_urn: 'media:txt;list',
5565
- steps: [
5770
+ steps: wireLinearStepInputs([
5566
5771
  makeForEachStep('media:ext=pdf;list'),
5567
5772
  makeCapStep('cap:in="media:ext=pdf";extract;out="media:ext=txt"', 'extract', 'media:ext=pdf', 'media:ext=txt', false, false),
5568
5773
  makeCollectStep('media:ext=txt'),
5569
- ],
5774
+ ]),
5570
5775
  };
5571
5776
  const payload = {
5572
5777
  resolved_strand: strand,
@@ -5577,7 +5782,7 @@ function test6510_Renderer_buildRunGraphData_closedForeachSuccessMergesAtCollect
5577
5782
  'media:txt;list': 'Text List',
5578
5783
  },
5579
5784
  body_outcomes: [
5580
- { body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
5785
+ { body_index: 0, foreach_token_id: strand.steps[0].token_id, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_token_id: null, failed_arg_urn: null },
5581
5786
  ],
5582
5787
  visible_success_count: 3,
5583
5788
  visible_failure_count: 3,
@@ -6362,6 +6567,139 @@ function test1848_capVersionNonZeroOnWire() {
6362
6567
  assertEqual(restored.version, 42, 'Restored version must equal 42');
6363
6568
  }
6364
6569
 
6570
+ // ===========================================================================
6571
+ // Shared parity tests 7100-7104: CapArg.streamUrn / CapArg.isMainInput.
6572
+ // Same substantive assertions in every capdag mirror (rust, go, js, objc, py).
6573
+ // ===========================================================================
6574
+
6575
+ // TEST7100: streamUrn() returns the stdin source's URN when it differs from the declared slot media_urn — the stdin URN, not the slot URN, is what the runtime demuxes the arg's input stream by.
6576
+ function test7100_streamUrnReturnsStdinSourceUrnWhenItDiffersFromSlotUrn() {
6577
+ const arg = new CapArg(
6578
+ 'media:enc=utf-8;file-path',
6579
+ true,
6580
+ [new ArgSource({ stdin: 'media:ext=pdf;pdf-stream' })]
6581
+ );
6582
+ assertEqual(arg.streamUrn(), 'media:ext=pdf;pdf-stream',
6583
+ 'streamUrn must return the stdin source URN');
6584
+ assert(arg.streamUrn() !== arg.media_urn,
6585
+ 'stream URN must differ from the declared slot media_urn here');
6586
+ }
6587
+
6588
+ // TEST7101: streamUrn() falls back to the declared slot media_urn when the arg declares no stdin source — a producer-fed arg may be delivered by its declared URN without ever appearing on stdin.
6589
+ function test7101_streamUrnFallsBackToDeclaredMediaUrnWithoutStdinSource() {
6590
+ const arg = new CapArg(
6591
+ 'media:enc=utf-8;system-prompt',
6592
+ true,
6593
+ [new ArgSource({ cli_flag: '--system-prompt' })]
6594
+ );
6595
+ assertEqual(arg.streamUrn(), 'media:enc=utf-8;system-prompt',
6596
+ 'streamUrn must fall back to the declared media_urn');
6597
+ }
6598
+
6599
+ // TEST7102: isMainInput() is true when the stdin URN is order-theoretically EQUIVALENT to the cap's in= spec even when the two strings list their tags in a different order — the comparison is the MediaUrn equivalence predicate, never a string comparison.
6600
+ function test7102_isMainInputTrueOnTagOrderInsensitiveEquivalenceToInSpec() {
6601
+ const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
6602
+ const arg = new CapArg(
6603
+ 'media:enc=utf-8;file-path',
6604
+ true,
6605
+ [new ArgSource({ stdin: 'media:pdf-stream;ext=pdf' })] // same tags, different order
6606
+ );
6607
+ assert(arg.isMainInput(inSpec),
6608
+ 'tag-order-insensitive equivalent stdin URN must be the main input');
6609
+ // The raw strings genuinely differ — proves the match is equivalence,
6610
+ // not string equality.
6611
+ assert(inSpec.toString() !== 'media:pdf-stream;ext=pdf',
6612
+ 'raw spec strings must differ for this test to be meaningful');
6613
+ }
6614
+
6615
+ // TEST7103: isMainInput() is false for cli_flag-only and position-only args (no stdin source means never the main input, whatever the declared slot URN says), and false when the stdin URN is not equivalent to in=.
6616
+ function test7103_isMainInputFalseWithoutEquivalentStdinSource() {
6617
+ const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
6618
+
6619
+ const cliFlagOnly = new CapArg(
6620
+ 'media:ext=pdf;pdf-stream', // slot URN even matches in= — irrelevant
6621
+ true,
6622
+ [new ArgSource({ cli_flag: '--input' })]
6623
+ );
6624
+ assert(!cliFlagOnly.isMainInput(inSpec), 'cli_flag-only arg is never the main input');
6625
+
6626
+ const positionOnly = new CapArg(
6627
+ 'media:ext=pdf;pdf-stream',
6628
+ true,
6629
+ [new ArgSource({ position: 0 })]
6630
+ );
6631
+ assert(!positionOnly.isMainInput(inSpec), 'position-only arg is never the main input');
6632
+
6633
+ const nonEquivalentStdin = new CapArg(
6634
+ 'media:enc=utf-8;system-prompt',
6635
+ false,
6636
+ [new ArgSource({ stdin: 'media:enc=utf-8;system-prompt' })]
6637
+ );
6638
+ assert(!nonEquivalentStdin.isMainInput(inSpec),
6639
+ 'stdin URN not equivalent to in= must not be the main input');
6640
+ }
6641
+
6642
+ // TEST7104: A realistic multi-arg cap (one stdin main input; one required, defaultless cli_flag arg; several defaulted cli_flag args): exactly one arg is the main input, and partitioning the remaining args by required-without-default vs has-default yields the expected sets.
6643
+ function test7104_multiArgCapExactlyOneMainInputAndPartitionOfRest() {
6644
+ const inSpec = MediaUrn.fromString('media:ext=pdf;pdf-stream');
6645
+
6646
+ const args = [
6647
+ new CapArg(
6648
+ 'media:enc=utf-8;file-path',
6649
+ true,
6650
+ // Main input may ALSO be delivered by cli-flag; stdin is the
6651
+ // defining route.
6652
+ [new ArgSource({ stdin: 'media:pdf-stream;ext=pdf' }), new ArgSource({ cli_flag: '--input' })]
6653
+ ),
6654
+ new CapArg(
6655
+ 'media:enc=utf-8;question',
6656
+ true,
6657
+ [new ArgSource({ cli_flag: '--question' })]
6658
+ ),
6659
+ new CapArg(
6660
+ 'media:max-tokens;numeric',
6661
+ false,
6662
+ [new ArgSource({ cli_flag: '--max-tokens' })],
6663
+ { default_value: 1024 }
6664
+ ),
6665
+ new CapArg(
6666
+ 'media:numeric;temperature',
6667
+ false,
6668
+ [new ArgSource({ cli_flag: '--temperature' })],
6669
+ { default_value: 0.7 }
6670
+ ),
6671
+ new CapArg(
6672
+ 'media:enc=utf-8;system-prompt',
6673
+ false,
6674
+ [new ArgSource({ cli_flag: '--system-prompt' })],
6675
+ { default_value: 'You are a helpful assistant.' }
6676
+ ),
6677
+ ];
6678
+
6679
+ const mainInputs = args.filter(a => a.isMainInput(inSpec)).map(a => a.media_urn);
6680
+ const rest = args.filter(a => !a.isMainInput(inSpec));
6681
+ const requiredWithoutDefault = rest
6682
+ .filter(a => a.required && a.default_value === null)
6683
+ .map(a => a.media_urn);
6684
+ const withDefault = rest
6685
+ .filter(a => a.default_value !== null)
6686
+ .map(a => a.media_urn);
6687
+
6688
+ assertEqual(JSON.stringify(mainInputs),
6689
+ JSON.stringify(['media:enc=utf-8;file-path']),
6690
+ 'exactly one arg must be the main input');
6691
+ assertEqual(JSON.stringify(requiredWithoutDefault),
6692
+ JSON.stringify(['media:enc=utf-8;question']),
6693
+ 'required-without-default partition');
6694
+ assertEqual(JSON.stringify(withDefault),
6695
+ JSON.stringify([
6696
+ 'media:max-tokens;numeric',
6697
+ 'media:numeric;temperature',
6698
+ 'media:enc=utf-8;system-prompt',
6699
+ ]),
6700
+ 'has-default partition');
6701
+ }
6702
+
6365
6703
  // ===========================================================================
6366
6704
  // Fabric alias tests (shared numbers 1880-1882, 1887)
6367
6705
  //
@@ -6886,7 +7224,7 @@ async function runTests() {
6886
7224
  await runTest('TEST1875: scan_all_reaches_both_dev_and_registry_slugs', test1875_scanAllReachesBothDevAndRegistrySlugs);
6887
7225
  await runTest('TEST1876: other_channel_subtree_is_skipped', test1876_otherChannelSubtreeIsSkipped);
6888
7226
  await runTest('TEST1877: registry_cartridge_under_wrong_slug_is_bad_install', test1877_registryCartridgeUnderWrongSlugIsBadInstall);
6889
- await runTest('TEST1878: bundled_provider_without_baked_hash_is_rejected', test1878_bundledProviderWithoutBakedHashIsRejected);
7227
+ await runTest('TEST1878: bundled_cartridge_without_baked_hash_is_rejected', test1878_bundledCartridgeWithoutBakedHashIsRejected);
6890
7228
 
6891
7229
  // media_urn.rs: TEST1312-TEST1315, TEST1298-TEST1302 (MediaUrn predicates)
6892
7230
  console.log('\n--- media_urn.rs (predicates) ---');
@@ -7077,12 +7415,14 @@ async function runTests() {
7077
7415
  runTest('RENDERER: collapseStrand_plainCapMergesOutput', test6500_Renderer_collapseStrand_plainCapMergesTrailingOutput);
7078
7416
  runTest('RENDERER: collapseStrand_plainCapDistinctTarget', test6501_Renderer_collapseStrand_plainCapDistinctTargetNoMerge);
7079
7417
  runTest('RENDERER: validateStrand_missingSourceMediaUrn', test6502_Renderer_validateStrandPayload_missingSourceMediaUrn);
7418
+ runTest('RENDERER: validateStrand_requiresCapInputs', test7120_Renderer_validateStrandPayload_requiresAuthoritativeInputs);
7080
7419
 
7081
7420
  console.log('\n--- cap-fab-renderer run builder ---');
7082
7421
  runTest('RENDERER: validateBodyOutcome_negativeIndex', test6503_Renderer_validateBodyOutcome_rejectsNegativeIndex);
7422
+ runTest('RENDERER: validateBodyOutcome_failureCoordinate', test7121_Renderer_validateBodyOutcome_requiresExplicitFailureCoordinate);
7083
7423
  runTest('RENDERER: buildRun_pagesSuccessesAndFailures', test6504_Renderer_buildRunGraphData_pagesSuccessesAndFailures);
7084
- runTest('RENDERER: buildRun_failureWithoutFailedCap', test6505_Renderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace);
7085
- runTest('RENDERER: buildRun_usesIsEquivalentForFailedCap', test6506_Renderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap);
7424
+ runTest('RENDERER: buildRun_unattributedFailureAtEntry', test6505_Renderer_buildRunGraphData_unattributedFailureStopsAtEntry);
7425
+ runTest('RENDERER: buildRun_resolvesFailedStepToken', test6506_Renderer_buildRunGraphData_resolvesExactFailedStepToken);
7086
7426
  runTest('RENDERER: buildRun_backboneHasNoForeachNode', test6507_Renderer_buildRunGraphData_backboneHasNoForeachNode);
7087
7427
  runTest('RENDERER: buildRun_allFailedDropsPlaceholder', test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder);
7088
7428
  runTest('RENDERER: buildRun_unclosedForeachNoMerge', test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge);
@@ -7141,6 +7481,13 @@ async function runTests() {
7141
7481
  runTest('TEST1847: cap_version_zero_omitted_on_wire', test6737_capVersionZeroOmittedOnWire);
7142
7482
  runTest('TEST1848: cap_version_nonzero_on_wire', test1848_capVersionNonZeroOnWire);
7143
7483
 
7484
+ console.log('\n--- CapArg stream URN + main input (test7100–test7104) ---');
7485
+ runTest('TEST7100: stream_urn_prefers_stdin_source_urn', test7100_streamUrnReturnsStdinSourceUrnWhenItDiffersFromSlotUrn);
7486
+ runTest('TEST7101: stream_urn_falls_back_to_media_urn', test7101_streamUrnFallsBackToDeclaredMediaUrnWithoutStdinSource);
7487
+ runTest('TEST7102: is_main_input_by_urn_equivalence', test7102_isMainInputTrueOnTagOrderInsensitiveEquivalenceToInSpec);
7488
+ runTest('TEST7103: is_main_input_false_without_stdin', test7103_isMainInputFalseWithoutEquivalentStdinSource);
7489
+ runTest('TEST7104: multi_arg_main_input_partition', test7104_multiArgCapExactlyOneMainInputAndPartitionOfRest);
7490
+
7144
7491
  runTest('TEST1880: alias_name_normalization_rules', test1880_aliasNameNormalizationRules);
7145
7492
  runTest('TEST1881: token_urn_vs_alias_detection', test1881_tokenUrnVsAliasDetection);
7146
7493
  runTest('TEST1882: classify_alias_target_by_prefix', test1882_classifyAliasTargetByPrefix);