capdag 0.138.305 → 0.140.310

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.
@@ -108,24 +108,24 @@ function canonicalMediaUrn(mediaUrnString) {
108
108
  return MediaUrn.fromString(mediaUrnString).toString();
109
109
  }
110
110
 
111
- // Produce a multi-line node label from a canonical media URN, one line
112
- // per tag. Marker tags render as bare keys; value tags render as
113
- // `key: value`. TaggedUrn.getTags() is iterated in sorted order matching
114
- // the canonical serialization.
115
- function mediaNodeLabel(canonicalUrn) {
116
- const TaggedUrn = requireHostDependency('TaggedUrn');
117
- const parsed = TaggedUrn.fromString(canonicalUrn);
118
- const tags = parsed.tags;
119
- const lines = [];
120
- for (const key of Object.keys(tags).sort()) {
121
- const value = tags[key];
122
- if (value === '*') {
123
- lines.push(key);
124
- } else {
125
- lines.push(`${key}: ${value}`);
126
- }
111
+ // Graph labels must be provided explicitly by the host. The renderer is not
112
+ // allowed to synthesize user-facing labels from URNs.
113
+ function mediaNodeLabel() {
114
+ throw new Error(
115
+ 'CapGraphRenderer: mediaNodeLabel() is no longer supported. ' +
116
+ 'Pass explicit media titles/display names to the renderer.'
117
+ );
118
+ }
119
+
120
+ function requireExplicitDisplayName(canonicalUrn, displayEntries, context) {
121
+ const MediaUrn = requireHostDependency('MediaUrn');
122
+ const candidate = MediaUrn.fromString(canonicalUrn);
123
+ for (const entry of displayEntries) {
124
+ if (candidate.isEquivalent(entry.media)) return entry.display;
127
125
  }
128
- return lines.join('\n');
126
+ throw new Error(
127
+ `CapGraphRenderer: missing explicit display name for ${context} '${canonicalUrn}'`
128
+ );
129
129
  }
130
130
 
131
131
  // =============================================================================
@@ -461,6 +461,9 @@ function validateBrowseData(data) {
461
461
  assertString(cap.urn, `browse mode data[${idx}].urn`);
462
462
  assertString(cap.in_spec, `browse mode data[${idx}].in_spec (cap urn: ${cap.urn})`);
463
463
  assertString(cap.out_spec, `browse mode data[${idx}].out_spec (cap urn: ${cap.urn})`);
464
+ assertString(cap.title, `browse mode data[${idx}].title (cap urn: ${cap.urn})`);
465
+ assertString(cap.in_media_title, `browse mode data[${idx}].in_media_title (cap urn: ${cap.urn})`);
466
+ assertString(cap.out_media_title, `browse mode data[${idx}].out_media_title (cap urn: ${cap.urn})`);
464
467
  });
465
468
  }
466
469
 
@@ -631,6 +634,7 @@ function validateResolvedMachinePayload(data) {
631
634
  }
632
635
  assertString(n.id, `machine mode data.strands[${sIdx}].nodes[${nIdx}].id`);
633
636
  assertString(n.urn, `machine mode data.strands[${sIdx}].nodes[${nIdx}].urn`);
637
+ assertString(n.title, `machine mode data.strands[${sIdx}].nodes[${nIdx}].title`);
634
638
  });
635
639
  assertArray(strand.edges, `machine mode data.strands[${sIdx}].edges`);
636
640
  strand.edges.forEach((e, eIdx) => {
@@ -639,6 +643,7 @@ function validateResolvedMachinePayload(data) {
639
643
  }
640
644
  assertString(e.alias, `machine mode data.strands[${sIdx}].edges[${eIdx}].alias`);
641
645
  assertString(e.cap_urn, `machine mode data.strands[${sIdx}].edges[${eIdx}].cap_urn`);
646
+ assertString(e.title, `machine mode data.strands[${sIdx}].edges[${eIdx}].title`);
642
647
  if (typeof e.is_loop !== 'boolean') {
643
648
  throw new Error(`CapGraphRenderer machine mode: data.strands[${sIdx}].edges[${eIdx}].is_loop must be boolean`);
644
649
  }
@@ -744,6 +749,13 @@ function buildBrowseGraphData(capabilities) {
744
749
  });
745
750
 
746
751
  const nodes = Array.from(nodesMap.values());
752
+ for (const node of nodes) {
753
+ if (!mediaTitles.has(node.id)) {
754
+ throw new Error(
755
+ `CapGraphRenderer browse mode: missing explicit media title for '${node.id}'`
756
+ );
757
+ }
758
+ }
747
759
 
748
760
  const adjacency = new Map();
749
761
  const reverseAdj = new Map();
@@ -762,8 +774,8 @@ function browseCytoscapeElements(built) {
762
774
  group: 'nodes',
763
775
  data: {
764
776
  id: node.id,
765
- label: mediaNodeLabel(node.id),
766
- mediaTitle: built.mediaTitles.get(node.id) || '',
777
+ label: built.mediaTitles.get(node.id),
778
+ mediaTitle: built.mediaTitles.get(node.id),
767
779
  fullUrn: node.id,
768
780
  },
769
781
  }));
@@ -863,11 +875,7 @@ function buildStrandGraphData(data) {
863
875
  }
864
876
  }
865
877
  function displayNameFor(canonicalUrn) {
866
- const candidate = MediaUrn.fromString(canonicalUrn);
867
- for (const entry of displayEntries) {
868
- if (candidate.isEquivalent(entry.media)) return entry.display;
869
- }
870
- return mediaNodeLabel(canonicalUrn);
878
+ return requireExplicitDisplayName(canonicalUrn, displayEntries, 'strand node');
871
879
  }
872
880
 
873
881
  const nodes = [];
@@ -1497,11 +1505,7 @@ function buildRunGraphData(data) {
1497
1505
  } catch (_) { /* ignore malformed keys */ }
1498
1506
  }
1499
1507
  function displayNameFor(canonicalUrn) {
1500
- const candidate = MediaUrn.fromString(canonicalUrn);
1501
- for (const entry of displayEntries) {
1502
- if (candidate.isEquivalent(entry.media)) return entry.display;
1503
- }
1504
- return mediaNodeLabel(canonicalUrn);
1508
+ return requireExplicitDisplayName(canonicalUrn, displayEntries, 'run node');
1505
1509
  }
1506
1510
 
1507
1511
  // The per-body "entry" node represents one item of the
@@ -1946,7 +1950,7 @@ function buildResolvedMachineGraphData(data) {
1946
1950
  group: 'nodes',
1947
1951
  data: {
1948
1952
  id: node.id,
1949
- label: mediaNodeLabel(canonicalMediaUrn(node.urn)),
1953
+ label: node.title,
1950
1954
  fullUrn: node.urn,
1951
1955
  strandIndex: strandIdx,
1952
1956
  },
@@ -1957,11 +1961,7 @@ function buildResolvedMachineGraphData(data) {
1957
1961
  for (const edge of strand.edges) {
1958
1962
  const cardinality = edge.is_loop ? 'n\u21921' : '1\u21921';
1959
1963
  const capUrn = edge.cap_urn;
1960
- // Title falls back to the canonical cap URN. The host can
1961
- // pre-resolve a friendlier title via `step_titles` on the
1962
- // proto message and use it elsewhere in the UI; the graph
1963
- // renderer is intentionally registry-free.
1964
- const capTitle = capUrn;
1964
+ const capTitle = edge.title;
1965
1965
  const label = cardinality === '1\u21921'
1966
1966
  ? edge.alias
1967
1967
  : `${edge.alias} (${cardinality})`;
package/capdag.js CHANGED
@@ -890,11 +890,10 @@ const MEDIA_YAML_LIST_RECORD = 'media:list;record;textable;yaml';
890
890
  const MEDIA_CSV = 'media:csv;list;record;textable';
891
891
  const MEDIA_CSV_LIST = 'media:csv;list;textable';
892
892
 
893
- // File path types - for arguments that represent filesystem paths
894
- // Media URN for a single file path - textable, scalar by default (no list marker)
893
+ // File path type for arguments that represent filesystem paths.
894
+ // There is a single media URN; cardinality (single file vs many files)
895
+ // is carried on the wire via is_sequence, not via URN tags.
895
896
  const MEDIA_FILE_PATH = 'media:file-path;textable';
896
- // Media URN for an array of file paths - textable with list marker
897
- const MEDIA_FILE_PATH_ARRAY = 'media:file-path;list;textable';
898
897
 
899
898
  // Semantic text input types - distinguished by their purpose/context
900
899
  // Media URN for model spec (provider:model format, HuggingFace name, etc.) - scalar by default
@@ -1095,25 +1094,12 @@ class MediaUrn {
1095
1094
  }
1096
1095
 
1097
1096
  /**
1098
- * Check if this represents a single file path type (not array).
1099
- * Returns true if the "file-path" marker tag is present AND no list marker.
1097
+ * True if this URN specializes `media:file-path`. There is a single
1098
+ * file-path media URN; cardinality (single file vs many) is carried on
1099
+ * the wire via `is_sequence`, not via URN tags.
1100
1100
  * @returns {boolean}
1101
1101
  */
1102
- isFilePath() { return this._hasMarkerTag('file-path') && !this.isList(); }
1103
-
1104
- /**
1105
- * Check if this represents a file path array type.
1106
- * Returns true if the "file-path" marker tag is present AND has list marker.
1107
- * @returns {boolean}
1108
- */
1109
- isFilePathArray() { return this._hasMarkerTag('file-path') && this.isList(); }
1110
-
1111
- /**
1112
- * Check if this represents any file path type (single or array).
1113
- * Returns true if "file-path" marker tag is present.
1114
- * @returns {boolean}
1115
- */
1116
- isAnyFilePath() { return this._hasMarkerTag('file-path'); }
1102
+ isFilePath() { return this._hasMarkerTag('file-path'); }
1117
1103
 
1118
1104
  /**
1119
1105
  * Check if this represents a collection type.
@@ -5675,9 +5661,8 @@ module.exports = {
5675
5661
  MEDIA_LLM_INFERENCE_OUTPUT,
5676
5662
  MEDIA_IMAGE_DESCRIPTION,
5677
5663
  MEDIA_TRANSCRIPTION_OUTPUT,
5678
- // File path types
5664
+ // File path type — single URN; cardinality lives on is_sequence.
5679
5665
  MEDIA_FILE_PATH,
5680
- MEDIA_FILE_PATH_ARRAY,
5681
5666
  MEDIA_MLX_MODEL_PATH,
5682
5667
  // Collection types
5683
5668
  MEDIA_COLLECTION,
package/capdag.test.js CHANGED
@@ -24,7 +24,7 @@ const {
24
24
  MEDIA_HTML, MEDIA_XML, MEDIA_JSON, MEDIA_YAML, MEDIA_JSON_SCHEMA,
25
25
  MEDIA_MODEL_SPEC, MEDIA_AVAILABILITY_OUTPUT, MEDIA_PATH_OUTPUT,
26
26
  MEDIA_LLM_INFERENCE_OUTPUT,
27
- MEDIA_FILE_PATH, MEDIA_FILE_PATH_ARRAY,
27
+ MEDIA_FILE_PATH,
28
28
  MEDIA_COLLECTION, MEDIA_COLLECTION_LIST,
29
29
  MEDIA_DECISION,
30
30
  MEDIA_AUDIO_SPEECH
@@ -2453,34 +2453,15 @@ function test1298_isBool() {
2453
2453
  assert(!MediaUrn.fromString(MEDIA_IDENTITY).isBool(), 'MEDIA_IDENTITY should not be bool');
2454
2454
  }
2455
2455
 
2456
- // TEST1299: is_file_path returns true for scalar file-path, false for array
2456
+ // TEST1299: isFilePath returns true for the single file-path media URN,
2457
+ // false for everything else. There is no "array" variant — cardinality is
2458
+ // carried by is_sequence on the wire, not by URN tags.
2457
2459
  function test1299_isFilePath() {
2458
2460
  assert(MediaUrn.fromString(MEDIA_FILE_PATH).isFilePath(), 'MEDIA_FILE_PATH should be file-path');
2459
- // Array file-path is NOT isFilePath (it's isFilePathArray)
2460
- assert(!MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isFilePath(), 'MEDIA_FILE_PATH_ARRAY should not be isFilePath');
2461
- // Non-file-path types
2462
2461
  assert(!MediaUrn.fromString(MEDIA_STRING).isFilePath(), 'MEDIA_STRING should not be file-path');
2463
2462
  assert(!MediaUrn.fromString(MEDIA_IDENTITY).isFilePath(), 'MEDIA_IDENTITY should not be file-path');
2464
2463
  }
2465
2464
 
2466
- // TEST1300: is_file_path_array returns true for list file-path, false for scalar
2467
- function test1300_isFilePathArray() {
2468
- assert(MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isFilePathArray(), 'MEDIA_FILE_PATH_ARRAY should be file-path-array');
2469
- // Scalar file-path is NOT isFilePathArray
2470
- assert(!MediaUrn.fromString(MEDIA_FILE_PATH).isFilePathArray(), 'MEDIA_FILE_PATH should not be isFilePathArray');
2471
- // Non-file-path types
2472
- assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isFilePathArray(), 'MEDIA_STRING_LIST should not be file-path-array');
2473
- }
2474
-
2475
- // TEST1301: is_any_file_path returns true for both scalar and array file-path
2476
- function test1301_isAnyFilePath() {
2477
- assert(MediaUrn.fromString(MEDIA_FILE_PATH).isAnyFilePath(), 'MEDIA_FILE_PATH should be any-file-path');
2478
- assert(MediaUrn.fromString(MEDIA_FILE_PATH_ARRAY).isAnyFilePath(), 'MEDIA_FILE_PATH_ARRAY should be any-file-path');
2479
- // Non-file-path types
2480
- assert(!MediaUrn.fromString(MEDIA_STRING).isAnyFilePath(), 'MEDIA_STRING should not be any-file-path');
2481
- assert(!MediaUrn.fromString(MEDIA_STRING_LIST).isAnyFilePath(), 'MEDIA_STRING_LIST should not be any-file-path');
2482
- }
2483
-
2484
2465
  // Mirror-specific coverage: isCollection returns true when collection marker tag is present
2485
2466
  // Mirror-specific coverage: N/A for JS (MEDIA_COLLECTION constants removed - no longer exists)
2486
2467
  function testisCollection() {
@@ -3872,6 +3853,7 @@ const {
3872
3853
  cardinalityFromCap: rendererCardinalityFromCap,
3873
3854
  canonicalMediaUrn: rendererCanonicalMediaUrn,
3874
3855
  mediaNodeLabel: rendererMediaNodeLabel,
3856
+ buildBrowseGraphData: rendererBuildBrowseGraphData,
3875
3857
  buildStrandGraphData: rendererBuildStrandGraphData,
3876
3858
  collapseStrandShapeTransitions: rendererCollapseStrandShapeTransitions,
3877
3859
  buildRunGraphData: rendererBuildRunGraphData,
@@ -4018,24 +4000,41 @@ function testRenderer_canonicalMediaUrn_rejectsCapUrn() {
4018
4000
  assert(threw, 'canonicalMediaUrn must reject non-media URNs');
4019
4001
  }
4020
4002
 
4021
- function testRenderer_mediaNodeLabel_oneLinePerTag_valueAndMarker() {
4022
- // A media URN with one value tag and one marker tag renders two lines:
4023
- // value tag as "key: value", marker tag as bare key. Order is canonical
4024
- // (alphabetical, matching TaggedUrn's sorted key iteration).
4025
- const label = rendererMediaNodeLabel('media:video;quant=q4');
4026
- const lines = label.split('\n');
4027
- assertEqual(lines.length, 2, 'two tags must produce two lines');
4028
- assert(lines.includes('quant: q4'), 'value tag rendered as key: value');
4029
- assert(lines.includes('video'), 'marker tag rendered as bare key');
4003
+ function testRenderer_mediaNodeLabel_rejectsUrnDerivedLabels() {
4004
+ let threw = false;
4005
+ let message = '';
4006
+ try {
4007
+ rendererMediaNodeLabel('media:video;quant=q4');
4008
+ } catch (e) {
4009
+ threw = true;
4010
+ message = e.message || '';
4011
+ }
4012
+ assert(threw, 'mediaNodeLabel must reject URN-derived labels');
4013
+ assert(message.includes('no longer supported'),
4014
+ 'error must explain that explicit titles are required');
4030
4015
  }
4031
4016
 
4032
- function testRenderer_mediaNodeLabel_stableAcrossTagOrder() {
4033
- // Labels must be tag-order-independent so that the same media URN
4034
- // produces the same multi-line label regardless of how the source
4035
- // happened to spell it.
4036
- const a = rendererMediaNodeLabel(rendererCanonicalMediaUrn('media:list;textable'));
4037
- const b = rendererMediaNodeLabel(rendererCanonicalMediaUrn('media:textable;list'));
4038
- assertEqual(a, b, 'label must be stable across tag orderings');
4017
+ function testRenderer_buildBrowseGraphData_rejectsMissingMediaTitles() {
4018
+ let threw = false;
4019
+ let message = '';
4020
+ try {
4021
+ rendererBuildBrowseGraphData([
4022
+ {
4023
+ urn: 'cap:in="media:pdf";op=extract;out="media:txt;textable"',
4024
+ title: 'Extract Text',
4025
+ in_spec: 'media:pdf',
4026
+ out_spec: 'media:txt;textable',
4027
+ in_media_title: 'PDF',
4028
+ out_media_title: '',
4029
+ },
4030
+ ]);
4031
+ } catch (e) {
4032
+ threw = true;
4033
+ message = e.message || '';
4034
+ }
4035
+ assert(threw, 'browse builder must reject missing explicit media titles');
4036
+ assert(message.includes('out_media_title'),
4037
+ 'error must identify the missing explicit media title field');
4039
4038
  }
4040
4039
 
4041
4040
  // ---------------- strand builder ----------------
@@ -4156,18 +4155,27 @@ function findEdge(edges, source, target) {
4156
4155
  return edges.find(e => e.source === source && e.target === target);
4157
4156
  }
4158
4157
 
4158
+ function withMediaDisplayNames(payload, mediaDisplayNames) {
4159
+ return Object.assign({}, payload, {
4160
+ media_display_names: Object.assign({}, mediaDisplayNames),
4161
+ });
4162
+ }
4163
+
4159
4164
  function testRenderer_buildStrandGraphData_singleCapPlain() {
4160
4165
  // Minimal strand with one plain 1→1 cap. Plan builder produces:
4161
4166
  // input_slot → step_0 (cap) → output
4162
4167
  // (two edges, three nodes). No cardinality marker in the cap label
4163
4168
  // because input_is_sequence == output_is_sequence == false.
4164
- const payload = {
4169
+ const payload = withMediaDisplayNames({
4165
4170
  source_spec: 'media:a',
4166
4171
  target_spec: 'media:b',
4167
4172
  steps: [
4168
4173
  makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4169
4174
  ],
4170
- };
4175
+ }, {
4176
+ 'media:a': 'Source A',
4177
+ 'media:b': 'Target B',
4178
+ });
4171
4179
  const built = rendererBuildStrandGraphData(payload);
4172
4180
  const nodeIds = built.nodes.map(n => n.id).sort();
4173
4181
  assertEqual(JSON.stringify(nodeIds), JSON.stringify(['input_slot', 'output', 'step_0']),
@@ -4183,13 +4191,16 @@ function testRenderer_buildStrandGraphData_singleCapPlain() {
4183
4191
  function testRenderer_buildStrandGraphData_sequenceShowsCardinality() {
4184
4192
  // A cap with input_is_sequence=true MUST emit "(n→1)" on its edge
4185
4193
  // label.
4186
- const payload = {
4194
+ const payload = withMediaDisplayNames({
4187
4195
  source_spec: 'media:a;list',
4188
4196
  target_spec: 'media:b',
4189
4197
  steps: [
4190
4198
  makeCapStep('cap:in="media:a;list";op=x;out="media:b"', 'x', 'media:a;list', 'media:b', true, false),
4191
4199
  ],
4192
- };
4200
+ }, {
4201
+ 'media:a;list': 'Source A List',
4202
+ 'media:b': 'Target B',
4203
+ });
4193
4204
  const built = rendererBuildStrandGraphData(payload);
4194
4205
  const capEdge = findEdge(built.edges, 'input_slot', 'step_0');
4195
4206
  assert(capEdge !== undefined, 'cap edge exists');
@@ -4210,7 +4221,7 @@ function testRenderer_buildStrandGraphData_foreachCollectSpan() {
4210
4221
  // edges.) ForEach and Collect are REAL nodes in the graph, not
4211
4222
  // labels on cap edges — they're distinct processing units in the
4212
4223
  // plan. This mirrors capdag's plan_builder.rs exactly.
4213
- const payload = {
4224
+ const payload = withMediaDisplayNames({
4214
4225
  source_spec: 'media:pdf;list',
4215
4226
  target_spec: 'media:txt;list',
4216
4227
  steps: [
@@ -4218,7 +4229,11 @@ function testRenderer_buildStrandGraphData_foreachCollectSpan() {
4218
4229
  makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
4219
4230
  makeCollectStep('media:txt'),
4220
4231
  ],
4221
- };
4232
+ }, {
4233
+ 'media:pdf;list': 'PDF List',
4234
+ 'media:txt': 'Plain Text',
4235
+ 'media:txt;list': 'Text List',
4236
+ });
4222
4237
  const built = rendererBuildStrandGraphData(payload);
4223
4238
  const nodeIds = built.nodes.map(n => n.id).sort();
4224
4239
  assertEqual(JSON.stringify(nodeIds),
@@ -4248,14 +4263,18 @@ function testRenderer_buildStrandGraphData_standaloneCollect() {
4248
4263
  // Strand with a standalone Collect (no enclosing ForEach). Plan
4249
4264
  // builder creates a Collect node consuming prev directly — plain
4250
4265
  // direct edge, no iteration/collection semantics.
4251
- const payload = {
4266
+ const payload = withMediaDisplayNames({
4252
4267
  source_spec: 'media:a',
4253
4268
  target_spec: 'media:b;list',
4254
4269
  steps: [
4255
4270
  makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4256
4271
  makeCollectStep('media:b'),
4257
4272
  ],
4258
- };
4273
+ }, {
4274
+ 'media:a': 'Source A',
4275
+ 'media:b': 'Target B',
4276
+ 'media:b;list': 'Target B List',
4277
+ });
4259
4278
  const built = rendererBuildStrandGraphData(payload);
4260
4279
  assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
4261
4280
  'cap edge input_slot → step_0');
@@ -4272,7 +4291,7 @@ function testRenderer_buildStrandGraphData_unclosedForEachBody() {
4272
4291
  // builder's "unclosed ForEach" branch creates a ForEach node
4273
4292
  // connecting Cap_a to Cap_b via iteration, with prev becoming the
4274
4293
  // body exit (Cap_b).
4275
- const payload = {
4294
+ const payload = withMediaDisplayNames({
4276
4295
  source_spec: 'media:a',
4277
4296
  target_spec: 'media:c',
4278
4297
  steps: [
@@ -4280,7 +4299,11 @@ function testRenderer_buildStrandGraphData_unclosedForEachBody() {
4280
4299
  makeForEachStep('media:b'),
4281
4300
  makeCapStep('cap:in="media:b";op=b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
4282
4301
  ],
4283
- };
4302
+ }, {
4303
+ 'media:a': 'Source A',
4304
+ 'media:b': 'Intermediate B',
4305
+ 'media:c': 'Target C',
4306
+ });
4284
4307
  const built = rendererBuildStrandGraphData(payload);
4285
4308
  // Cap_a connects from input_slot.
4286
4309
  assert(findEdge(built.edges, 'input_slot', 'step_0') !== undefined,
@@ -4305,7 +4328,7 @@ function testRenderer_buildStrandGraphData_nestedForEachThrows() {
4305
4328
  // ForEach is an illegal nesting per plan_builder. The renderer
4306
4329
  // must throw the same error to surface the issue rather than
4307
4330
  // render a malformed graph.
4308
- const payload = {
4331
+ const payload = withMediaDisplayNames({
4309
4332
  source_spec: 'media:a;list',
4310
4333
  target_spec: 'media:a',
4311
4334
  steps: [
@@ -4313,7 +4336,10 @@ function testRenderer_buildStrandGraphData_nestedForEachThrows() {
4313
4336
  makeForEachStep('media:a'),
4314
4337
  makeCapStep('cap:in="media:a";op=x;out="media:a"', 'x', 'media:a', 'media:a', false, false),
4315
4338
  ],
4316
- };
4339
+ }, {
4340
+ 'media:a;list': 'Source A List',
4341
+ 'media:a': 'Source A',
4342
+ });
4317
4343
  let threw = false;
4318
4344
  try {
4319
4345
  rendererBuildStrandGraphData(payload);
@@ -4339,7 +4365,7 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4339
4365
  // Expected render shape: 3 nodes (input_slot, step_1, output),
4340
4366
  // with the entry edge labeled "extract" and an unlabeled
4341
4367
  // connector bridge to the output.
4342
- const payload = {
4368
+ const payload = withMediaDisplayNames({
4343
4369
  source_spec: 'media:pdf;list',
4344
4370
  target_spec: 'media:txt;list',
4345
4371
  steps: [
@@ -4347,7 +4373,11 @@ function testRenderer_collapseStrand_singleCapBodyKeepsCapOwnLabel() {
4347
4373
  makeCapStep('cap:in="media:pdf";op=extract;out="media:txt"', 'extract', 'media:pdf', 'media:txt', false, false),
4348
4374
  makeCollectStep('media:txt'),
4349
4375
  ],
4350
- };
4376
+ }, {
4377
+ 'media:pdf;list': 'PDF List',
4378
+ 'media:txt': 'Plain Text',
4379
+ 'media:txt;list': 'Text List',
4380
+ });
4351
4381
  const built = rendererBuildStrandGraphData(payload);
4352
4382
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4353
4383
 
@@ -4384,7 +4414,7 @@ function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
4384
4414
  // no relabeling.
4385
4415
  //
4386
4416
  // Final: 3 nodes (input_slot, step_0, step_2), 2 edges.
4387
- const payload = {
4417
+ const payload = withMediaDisplayNames({
4388
4418
  source_spec: 'media:a',
4389
4419
  target_spec: 'media:c',
4390
4420
  steps: [
@@ -4392,7 +4422,11 @@ function testRenderer_collapseStrand_unclosedForEachBodyCollapses() {
4392
4422
  makeForEachStep('media:b'),
4393
4423
  makeCapStep('cap:in="media:b";op=b;out="media:c"', 'b', 'media:b', 'media:c', false, false),
4394
4424
  ],
4395
- };
4425
+ }, {
4426
+ 'media:a': 'Source A',
4427
+ 'media:b': 'Intermediate B',
4428
+ 'media:c': 'Target C',
4429
+ });
4396
4430
  const built = rendererBuildStrandGraphData(payload);
4397
4431
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4398
4432
 
@@ -4437,14 +4471,18 @@ function testRenderer_collapseStrand_standaloneCollectCollapses() {
4437
4471
  // cap is not inside any foreach body.
4438
4472
  //
4439
4473
  // Final: 3 nodes (input_slot, step_0, output), 2 edges.
4440
- const payload = {
4474
+ const payload = withMediaDisplayNames({
4441
4475
  source_spec: 'media:a',
4442
4476
  target_spec: 'media:b;list',
4443
4477
  steps: [
4444
4478
  makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4445
4479
  makeCollectStep('media:b'),
4446
4480
  ],
4447
- };
4481
+ }, {
4482
+ 'media:a': 'Source A',
4483
+ 'media:b': 'Target B',
4484
+ 'media:b;list': 'Target B List',
4485
+ });
4448
4486
  const built = rendererBuildStrandGraphData(payload);
4449
4487
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4450
4488
 
@@ -4481,7 +4519,7 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4481
4519
  // (Disbind), NOT the cap that consumes one item from it.
4482
4520
  // No separate output node because step_2's to_spec equals the
4483
4521
  // strand target.
4484
- const payload = {
4522
+ const payload = withMediaDisplayNames({
4485
4523
  source_spec: 'media:pdf',
4486
4524
  target_spec: 'media:decision',
4487
4525
  steps: [
@@ -4489,7 +4527,11 @@ function testRenderer_collapseStrand_sequenceProducingCapBeforeForeach() {
4489
4527
  makeForEachStep('media:page'),
4490
4528
  makeCapStep('cap:in="media:page";op=decide;out="media:decision"', 'Make a Decision', 'media:page', 'media:decision', false, false),
4491
4529
  ],
4492
- };
4530
+ }, {
4531
+ 'media:pdf': 'PDF',
4532
+ 'media:page': 'Page',
4533
+ 'media:decision': 'Decision',
4534
+ });
4493
4535
  const built = rendererBuildStrandGraphData(payload);
4494
4536
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4495
4537
 
@@ -4532,13 +4574,16 @@ function testRenderer_collapseStrand_plainCapMergesTrailingOutput() {
4532
4574
  // step_0 and output represent the same URN (media:b).
4533
4575
  //
4534
4576
  // Final: 2 nodes (input_slot, step_0), 1 edge.
4535
- const payload = {
4577
+ const payload = withMediaDisplayNames({
4536
4578
  source_spec: 'media:a',
4537
4579
  target_spec: 'media:b',
4538
4580
  steps: [
4539
4581
  makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4540
4582
  ],
4541
- };
4583
+ }, {
4584
+ 'media:a': 'Source A',
4585
+ 'media:b': 'Target B',
4586
+ });
4542
4587
  const built = rendererBuildStrandGraphData(payload);
4543
4588
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4544
4589
 
@@ -4561,13 +4606,17 @@ function testRenderer_collapseStrand_plainCapDistinctTargetNoMerge() {
4561
4606
  // A strand with a single plain cap whose to_spec is NOT
4562
4607
  // equivalent to target_spec. The output node must be retained
4563
4608
  // and the trailing connector edge preserved.
4564
- const payload = {
4609
+ const payload = withMediaDisplayNames({
4565
4610
  source_spec: 'media:a',
4566
4611
  target_spec: 'media:b;list',
4567
4612
  steps: [
4568
4613
  makeCapStep('cap:in="media:a";op=x;out="media:b"', 'x', 'media:a', 'media:b', false, false),
4569
4614
  ],
4570
- };
4615
+ }, {
4616
+ 'media:a': 'Source A',
4617
+ 'media:b': 'Target B',
4618
+ 'media:b;list': 'Target B List',
4619
+ });
4571
4620
  const built = rendererBuildStrandGraphData(payload);
4572
4621
  const collapsed = rendererCollapseStrandShapeTransitions(built);
4573
4622
 
@@ -4641,6 +4690,12 @@ function testRenderer_buildRunGraphData_pagesSuccessesAndFailures() {
4641
4690
  }
4642
4691
  const payload = {
4643
4692
  resolved_strand: strand,
4693
+ media_display_names: {
4694
+ 'media:pdf;list': 'PDF List',
4695
+ 'media:pdf': 'PDF',
4696
+ 'media:png': 'PNG',
4697
+ 'media:txt': 'Text',
4698
+ },
4644
4699
  body_outcomes: outcomes,
4645
4700
  visible_success_count: 3,
4646
4701
  visible_failure_count: 2,
@@ -4684,6 +4739,11 @@ function testRenderer_buildRunGraphData_failureWithoutFailedCapRendersFullTrace(
4684
4739
  };
4685
4740
  const payload = {
4686
4741
  resolved_strand: strand,
4742
+ media_display_names: {
4743
+ 'media:pdf;list': 'PDF List',
4744
+ 'media:pdf': 'PDF',
4745
+ 'media:txt': 'Text',
4746
+ },
4687
4747
  body_outcomes: [
4688
4748
  { body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, error: 'unknown' },
4689
4749
  ],
@@ -4726,6 +4786,12 @@ function testRenderer_buildRunGraphData_usesCapUrnIsEquivalentForFailedCap() {
4726
4786
  // will match. Feed a fully-specified form that is equivalent.
4727
4787
  const payload = {
4728
4788
  resolved_strand: strand,
4789
+ media_display_names: {
4790
+ 'media:a': 'Source A',
4791
+ 'media:a;list': 'Source A List',
4792
+ 'media:b': 'Intermediate B',
4793
+ 'media:c': 'Target C',
4794
+ },
4729
4795
  body_outcomes: [
4730
4796
  {
4731
4797
  body_index: 0,
@@ -4775,6 +4841,11 @@ function testRenderer_buildRunGraphData_backboneHasNoForeachNode() {
4775
4841
  };
4776
4842
  const payload = {
4777
4843
  resolved_strand: strand,
4844
+ media_display_names: {
4845
+ 'media:pdf': 'PDF',
4846
+ 'media:page': 'Page',
4847
+ 'media:decision': 'Decision',
4848
+ },
4778
4849
  body_outcomes: [],
4779
4850
  visible_success_count: 0,
4780
4851
  visible_failure_count: 0,
@@ -4819,6 +4890,11 @@ function testRenderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
4819
4890
  const failedCapUrn = 'cap:in="media:page";op=decide;out="media:decision"';
4820
4891
  const payload = {
4821
4892
  resolved_strand: strand,
4893
+ media_display_names: {
4894
+ 'media:pdf': 'PDF',
4895
+ 'media:page': 'Page',
4896
+ 'media:decision': 'Decision',
4897
+ },
4822
4898
  body_outcomes: [
4823
4899
  { body_index: 0, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
4824
4900
  { body_index: 1, success: false, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0, failed_cap: failedCapUrn, error: 'boom' },
@@ -4883,6 +4959,11 @@ function testRenderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
4883
4959
  };
4884
4960
  const payload = {
4885
4961
  resolved_strand: strand,
4962
+ media_display_names: {
4963
+ 'media:pdf': 'PDF',
4964
+ 'media:page': 'Page',
4965
+ 'media:decision': 'Decision',
4966
+ },
4886
4967
  body_outcomes: [
4887
4968
  { body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
4888
4969
  ],
@@ -4937,6 +5018,12 @@ function testRenderer_buildRunGraphData_closedForeachSuccessMergesAtCollectTarge
4937
5018
  };
4938
5019
  const payload = {
4939
5020
  resolved_strand: strand,
5021
+ media_display_names: {
5022
+ 'media:pdf;list': 'PDF List',
5023
+ 'media:pdf': 'PDF',
5024
+ 'media:txt': 'Text',
5025
+ 'media:txt;list': 'Text List',
5026
+ },
4940
5027
  body_outcomes: [
4941
5028
  { body_index: 0, success: true, cap_urns: [], saved_paths: [], total_bytes: 0, duration_ms: 0 },
4942
5029
  ],
@@ -5098,14 +5185,15 @@ function testRenderer_buildResolvedMachineGraphData_singleStrandLinearChain() {
5098
5185
  strands: [
5099
5186
  {
5100
5187
  nodes: [
5101
- { id: 'n0', urn: 'media:pdf' },
5102
- { id: 'n1', urn: 'media:txt;textable' },
5103
- { id: 'n2', urn: 'media:embedding;record' },
5188
+ { id: 'n0', urn: 'media:pdf', title: 'PDF' },
5189
+ { id: 'n1', urn: 'media:txt;textable', title: 'Plain Text' },
5190
+ { id: 'n2', urn: 'media:embedding;record', title: 'Embedding Record' },
5104
5191
  ],
5105
5192
  edges: [
5106
5193
  {
5107
5194
  alias: 'edge_0',
5108
5195
  cap_urn: 'cap:in=media:pdf;op=extract;out=media:txt;textable',
5196
+ title: 'Extract Text',
5109
5197
  is_loop: false,
5110
5198
  assignment: [
5111
5199
  { cap_arg_media_urn: 'media:pdf', source_node: 'n0' },
@@ -5115,6 +5203,7 @@ function testRenderer_buildResolvedMachineGraphData_singleStrandLinearChain() {
5115
5203
  {
5116
5204
  alias: 'edge_1',
5117
5205
  cap_urn: 'cap:in=media:textable;op=embed;out=media:embedding;record',
5206
+ title: 'Generate Embedding',
5118
5207
  is_loop: false,
5119
5208
  assignment: [
5120
5209
  { cap_arg_media_urn: 'media:textable', source_node: 'n1' },
@@ -5137,6 +5226,7 @@ function testRenderer_buildResolvedMachineGraphData_singleStrandLinearChain() {
5137
5226
  // Anchor nodes carry the strand-source / strand-target classes.
5138
5227
  const n0 = built.nodes.find(n => n.data.id === 'n0');
5139
5228
  const n2 = built.nodes.find(n => n.data.id === 'n2');
5229
+ assertEqual(n0.data.label, 'PDF', 'node label must come from explicit title');
5140
5230
  assert(n0.classes.indexOf('strand-source') >= 0,
5141
5231
  'input anchor node carries strand-source class');
5142
5232
  assert(n2.classes.indexOf('strand-target') >= 0,
@@ -5151,13 +5241,14 @@ function testRenderer_buildResolvedMachineGraphData_loopEdgeGetsLoopClass() {
5151
5241
  strands: [
5152
5242
  {
5153
5243
  nodes: [
5154
- { id: 'n0', urn: 'media:page;textable' },
5155
- { id: 'n1', urn: 'media:decision;json;record;textable' },
5244
+ { id: 'n0', urn: 'media:page;textable', title: 'Page' },
5245
+ { id: 'n1', urn: 'media:decision;json;record;textable', title: 'Decision Record' },
5156
5246
  ],
5157
5247
  edges: [
5158
5248
  {
5159
5249
  alias: 'edge_0',
5160
5250
  cap_urn: 'cap:in=media:textable;op=make_decision;out=media:decision;json;record;textable',
5251
+ title: 'Make Decision',
5161
5252
  is_loop: true,
5162
5253
  assignment: [
5163
5254
  { cap_arg_media_urn: 'media:textable', source_node: 'n0' },
@@ -5185,14 +5276,15 @@ function testRenderer_buildResolvedMachineGraphData_fanInProducesEdgePerAssignme
5185
5276
  strands: [
5186
5277
  {
5187
5278
  nodes: [
5188
- { id: 'n0', urn: 'media:image;png' },
5189
- { id: 'n1', urn: 'media:model-spec;textable' },
5190
- { id: 'n2', urn: 'media:image-description;textable' },
5279
+ { id: 'n0', urn: 'media:image;png', title: 'PNG Image' },
5280
+ { id: 'n1', urn: 'media:model-spec;textable', title: 'Model Spec' },
5281
+ { id: 'n2', urn: 'media:image-description;textable', title: 'Image Description' },
5191
5282
  ],
5192
5283
  edges: [
5193
5284
  {
5194
5285
  alias: 'edge_0',
5195
5286
  cap_urn: 'cap:in=media:image;png;op=describe_image;out=media:image-description;textable',
5287
+ title: 'Describe Image',
5196
5288
  is_loop: false,
5197
5289
  assignment: [
5198
5290
  { cap_arg_media_urn: 'media:image;png', source_node: 'n0' },
@@ -5225,13 +5317,14 @@ function testRenderer_buildResolvedMachineGraphData_multiStrandKeepsStrandsDisjo
5225
5317
  strands: [
5226
5318
  {
5227
5319
  nodes: [
5228
- { id: 'n0', urn: 'media:pdf' },
5229
- { id: 'n1', urn: 'media:txt;textable' },
5320
+ { id: 'n0', urn: 'media:pdf', title: 'PDF' },
5321
+ { id: 'n1', urn: 'media:txt;textable', title: 'Plain Text' },
5230
5322
  ],
5231
5323
  edges: [
5232
5324
  {
5233
5325
  alias: 'edge_0',
5234
5326
  cap_urn: 'cap:in=media:pdf;op=extract;out=media:txt;textable',
5327
+ title: 'Extract Text',
5235
5328
  is_loop: false,
5236
5329
  assignment: [
5237
5330
  { cap_arg_media_urn: 'media:pdf', source_node: 'n0' },
@@ -5244,13 +5337,14 @@ function testRenderer_buildResolvedMachineGraphData_multiStrandKeepsStrandsDisjo
5244
5337
  },
5245
5338
  {
5246
5339
  nodes: [
5247
- { id: 'n2', urn: 'media:json;record;textable' },
5248
- { id: 'n3', urn: 'media:csv;list;record;textable' },
5340
+ { id: 'n2', urn: 'media:json;record;textable', title: 'JSON Record' },
5341
+ { id: 'n3', urn: 'media:csv;list;record;textable', title: 'CSV Rows' },
5249
5342
  ],
5250
5343
  edges: [
5251
5344
  {
5252
5345
  alias: 'edge_1',
5253
5346
  cap_urn: 'cap:in=media:json;record;textable;op=convert_format;out=media:csv;list;record;textable',
5347
+ title: 'Convert Format',
5254
5348
  is_loop: false,
5255
5349
  assignment: [
5256
5350
  { cap_arg_media_urn: 'media:json;record;textable', source_node: 'n2' },
@@ -5284,13 +5378,13 @@ function testRenderer_buildResolvedMachineGraphData_duplicateNodeIdAcrossStrands
5284
5378
  const payload = {
5285
5379
  strands: [
5286
5380
  {
5287
- nodes: [{ id: 'n0', urn: 'media:pdf' }],
5381
+ nodes: [{ id: 'n0', urn: 'media:pdf', title: 'PDF' }],
5288
5382
  edges: [],
5289
5383
  input_anchor_nodes: ['n0'],
5290
5384
  output_anchor_nodes: ['n0'],
5291
5385
  },
5292
5386
  {
5293
- nodes: [{ id: 'n0', urn: 'media:html' }],
5387
+ nodes: [{ id: 'n0', urn: 'media:html', title: 'HTML' }],
5294
5388
  edges: [],
5295
5389
  input_anchor_nodes: ['n0'],
5296
5390
  output_anchor_nodes: ['n0'],
@@ -5318,13 +5412,14 @@ function testRenderer_validateResolvedMachinePayload_rejectsMissingFields() {
5318
5412
  const cases = [
5319
5413
  { strands: 'not-an-array' },
5320
5414
  { strands: [{ nodes: [], edges: [], input_anchor_nodes: [] /* missing output_anchor_nodes */ }] },
5321
- { strands: [{ nodes: [{ id: 'n0' /* missing urn */ }], edges: [], input_anchor_nodes: [], output_anchor_nodes: [] }] },
5415
+ { strands: [{ nodes: [{ id: 'n0' /* missing urn/title */ }], edges: [], input_anchor_nodes: [], output_anchor_nodes: [] }] },
5322
5416
  {
5323
5417
  strands: [{
5324
- nodes: [{ id: 'n0', urn: 'media:x' }],
5418
+ nodes: [{ id: 'n0', urn: 'media:x', title: 'X' }],
5325
5419
  edges: [{
5326
5420
  alias: 'edge_0',
5327
5421
  cap_urn: 'cap:in=...;out=...',
5422
+ title: 'Edge 0',
5328
5423
  is_loop: false,
5329
5424
  assignment: [{ cap_arg_media_urn: 'media:x' /* missing source_node */ }],
5330
5425
  target_node: 'n0',
@@ -5546,8 +5641,6 @@ async function runTests() {
5546
5641
  runTest('TEST1315: is_numeric', test1315_isNumeric);
5547
5642
  runTest('TEST1298: is_bool', test1298_isBool);
5548
5643
  runTest('TEST1299: is_file_path', test1299_isFilePath);
5549
- runTest('TEST1300: is_file_path_array', test1300_isFilePathArray);
5550
- runTest('TEST1301: is_any_file_path', test1301_isAnyFilePath);
5551
5644
  runTest('TEST1302: predicate_constant_consistency', test1302_predicateConstantConsistency);
5552
5645
 
5553
5646
  // cap_urn.rs: TEST1303-TEST1307 (CapUrn tier tests)
@@ -5703,8 +5796,8 @@ async function runTests() {
5703
5796
  runTest('RENDERER: canonicalMediaUrn_normalizesTagOrder', testRenderer_canonicalMediaUrn_normalizesTagOrder);
5704
5797
  runTest('RENDERER: canonicalMediaUrn_preservesValueTags', testRenderer_canonicalMediaUrn_preservesValueTags);
5705
5798
  runTest('RENDERER: canonicalMediaUrn_rejectsCapUrn', testRenderer_canonicalMediaUrn_rejectsCapUrn);
5706
- runTest('RENDERER: mediaNodeLabel_valueAndMarker', testRenderer_mediaNodeLabel_oneLinePerTag_valueAndMarker);
5707
- runTest('RENDERER: mediaNodeLabel_stableAcrossTagOrder', testRenderer_mediaNodeLabel_stableAcrossTagOrder);
5799
+ runTest('RENDERER: mediaNodeLabel_rejectsUrnDerived', testRenderer_mediaNodeLabel_rejectsUrnDerivedLabels);
5800
+ runTest('RENDERER: buildBrowse_rejectsMissingMediaTitles', testRenderer_buildBrowseGraphData_rejectsMissingMediaTitles);
5708
5801
 
5709
5802
  console.log('\n--- cap-graph-renderer strand builder ---');
5710
5803
  runTest('RENDERER: validateStrandStep_unknownVariant', testRenderer_validateStrandStep_rejectsUnknownVariant);
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.138.305"
43
+ "version": "0.140.310"
44
44
  }