@telorun/analyzer 0.68.0 → 0.69.0

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.
Files changed (70) hide show
  1. package/dist/analysis-registry.d.ts +12 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +26 -0
  4. package/dist/call-graph.d.ts +80 -1
  5. package/dist/call-graph.d.ts.map +1 -1
  6. package/dist/call-graph.js +145 -12
  7. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  8. package/dist/import-resolution-diagnostics.js +22 -8
  9. package/dist/index.d.ts +4 -5
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -4
  12. package/dist/loaded-types.d.ts +4 -4
  13. package/dist/loaded-types.d.ts.map +1 -1
  14. package/dist/manifest-analysis.d.ts +19 -0
  15. package/dist/manifest-analysis.d.ts.map +1 -1
  16. package/dist/manifest-analysis.js +27 -0
  17. package/dist/manifest-loader.d.ts +3 -4
  18. package/dist/manifest-loader.d.ts.map +1 -1
  19. package/dist/manifest-loader.js +4 -5
  20. package/dist/manifest-schemas.d.ts +2 -0
  21. package/dist/manifest-schemas.d.ts.map +1 -1
  22. package/dist/manifest-schemas.js +4 -0
  23. package/dist/module-graph.d.ts +500 -0
  24. package/dist/module-graph.d.ts.map +1 -0
  25. package/dist/module-graph.js +1411 -0
  26. package/dist/reconcile-module-versions.d.ts.map +1 -1
  27. package/dist/reconcile-module-versions.js +10 -11
  28. package/dist/release/release-plan.d.ts +1 -1
  29. package/dist/resolve-zone-containment.d.ts +9 -1
  30. package/dist/resolve-zone-containment.d.ts.map +1 -1
  31. package/dist/resolve-zone-containment.js +34 -6
  32. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  33. package/dist/resolve-zone-requirements.js +4 -2
  34. package/dist/sources/default-sources.d.ts +6 -6
  35. package/dist/sources/default-sources.d.ts.map +1 -1
  36. package/dist/sources/default-sources.js +7 -8
  37. package/dist/sources/integrity.d.ts +3 -2
  38. package/dist/sources/integrity.d.ts.map +1 -1
  39. package/dist/sources/integrity.js +26 -3
  40. package/dist/sources/versioned-ref.d.ts +17 -12
  41. package/dist/sources/versioned-ref.d.ts.map +1 -1
  42. package/dist/sources/versioned-ref.js +22 -24
  43. package/dist/telo-version.d.ts +1 -1
  44. package/dist/telo-version.js +1 -1
  45. package/package.json +1 -1
  46. package/src/analysis-registry.ts +37 -0
  47. package/src/call-graph.ts +207 -14
  48. package/src/import-resolution-diagnostics.ts +24 -7
  49. package/src/index.ts +38 -5
  50. package/src/loaded-types.ts +4 -4
  51. package/src/manifest-analysis.ts +39 -0
  52. package/src/manifest-loader.ts +4 -5
  53. package/src/manifest-schemas.ts +4 -0
  54. package/src/module-graph.ts +2005 -0
  55. package/src/reconcile-module-versions.ts +10 -11
  56. package/src/release/release-plan.ts +1 -1
  57. package/src/resolve-zone-containment.ts +49 -9
  58. package/src/resolve-zone-requirements.ts +7 -2
  59. package/src/sources/default-sources.ts +7 -8
  60. package/src/sources/integrity.ts +28 -3
  61. package/src/sources/versioned-ref.ts +26 -28
  62. package/src/telo-version.ts +1 -1
  63. package/dist/sources/module-ref.d.ts +0 -21
  64. package/dist/sources/module-ref.d.ts.map +0 -1
  65. package/dist/sources/module-ref.js +0 -36
  66. package/dist/sources/registry-source.d.ts +0 -14
  67. package/dist/sources/registry-source.d.ts.map +0 -1
  68. package/dist/sources/registry-source.js +0 -45
  69. package/src/sources/module-ref.ts +0 -49
  70. package/src/sources/registry-source.ts +0 -52
package/src/call-graph.ts CHANGED
@@ -56,7 +56,12 @@ import {
56
56
  type RefUseCases,
57
57
  } from "./ref-slot.js";
58
58
  import { isStepSlot } from "./step-slot.js";
59
- import { isRefEntry, resolveFieldEntries, type RefFieldEntry } from "./reference-field-map.js";
59
+ import {
60
+ isInlineResource,
61
+ isRefEntry,
62
+ resolveFieldEntries,
63
+ type RefFieldEntry,
64
+ } from "./reference-field-map.js";
60
65
  import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
61
66
 
62
67
  export interface ResourceGraphNode {
@@ -92,6 +97,42 @@ export interface StepGraphNode {
92
97
  index: number;
93
98
  /** The step value as written. */
94
99
  step: Record<string, unknown>;
100
+ /**
101
+ * Which branch of the step grammar this step IS — the branch's own title
102
+ * (`invoke`, `if/then/else`, `while/do`, `switch/cases/default`,
103
+ * `try/catch/finally`, `throw`, `value`).
104
+ *
105
+ * Matched on the branch's REQUIRED keys, and IS those keys — sorted and
106
+ * joined (`if+then`, `invoke`, `do+while`). A machine identity, so a consumer
107
+ * may branch on it; it was the branch's `title`, which is prose an author may
108
+ * reword at any time, and a view was splitting it on `/` to recover a keyword.
109
+ * Absent for a step matching no branch — one just added, and still empty.
110
+ */
111
+ variant?: string;
112
+ /** The branch's `title` as its author wrote it, for a view to render verbatim.
113
+ * Never parsed: it is a label, and the moment anything reads its SHAPE it
114
+ * becomes a vocabulary the author cannot change. */
115
+ variantLabel?: string;
116
+ /**
117
+ * The expression that decides whether or how this step runs, as written: an
118
+ * `if:`, a `while:`, a `switch:`, or the `when:` guard on a dispatch.
119
+ *
120
+ * Found through `x-telo-topology-role`, never by keyword, so a third-party
121
+ * composer annotating its own predicate is read the same way.
122
+ */
123
+ predicate?: string;
124
+ /**
125
+ * Every reference slot this step's grammar declares — filled, empty, or
126
+ * holding a declaration written at the site.
127
+ *
128
+ * Recorded here because this is the one place a step's item schema and its
129
+ * value are both in hand: a step array's items sit behind a local `$ref`, so
130
+ * the reference field map deliberately never reaches them, and a consumer
131
+ * asking "what may this step dispatch to, and where is that written" has
132
+ * nowhere else to look. An EMPTY slot is listed for exactly that reason — it
133
+ * is a site an editor can offer to fill, and an edge says nothing about one.
134
+ */
135
+ refSlots?: { key: string; path: string; kinds: string[]; inline?: boolean }[];
95
136
  }
96
137
 
97
138
  export type CallGraphNode = ResourceGraphNode | StepGraphNode;
@@ -154,7 +195,15 @@ export interface CallGraph {
154
195
  /** Edges arriving at a resource node. */
155
196
  edgesTo(id: string): CallGraphEdge[];
156
197
  resource(kind: string, name: string): ResourceGraphNode | undefined;
157
- resourceByName(name: string): ResourceGraphNode | undefined;
198
+ /**
199
+ * The resource a bare name addresses, read from `fromModule`'s scope.
200
+ *
201
+ * A name is module-scoped, so the module is how two declarations of one name
202
+ * are told apart — see {@link resolveScopedName}. Omitting it answers for a
203
+ * name declared once and, where several modules declare it, declines rather
204
+ * than taking whichever came first.
205
+ */
206
+ resourceByName(name: string, fromModule?: string): ResourceGraphNode | undefined;
158
207
  /** Step nodes declared by a resource, in lexical order. */
159
208
  steps(resourceId: string): StepGraphNode[];
160
209
  /** Every edge whose `use` includes at least one control transfer, plus every
@@ -164,6 +213,55 @@ export interface CallGraph {
164
213
 
165
214
  export const resourceId = (kind: string, name: string): string => `${kind}\0${name}`;
166
215
 
216
+ /**
217
+ * The id a manifest's node is keyed by.
218
+ *
219
+ * **A resource name is MODULE-SCOPED**, so `(kind, name)` is unique inside one
220
+ * module and not across a flattened set: two libraries each exporting an
221
+ * `Http.Api` named `routes` are two declarations, and keying both as
222
+ * `http.Api\0routes` silently made them one — the second overwrote the first,
223
+ * taking its edges and its steps with it. The module is part of the identity
224
+ * wherever the loader stamped one; a manifest with no stamp keeps the bare form,
225
+ * so nothing that never crossed a module boundary moves.
226
+ */
227
+ export function nodeIdFor(manifest: ResourceManifest): string {
228
+ const kind = manifest.kind as string;
229
+ const name = manifest.metadata?.name as string;
230
+ const module = (manifest.metadata as { module?: string } | undefined)?.module;
231
+ return module ? `${module}\0${resourceId(kind, name)}` : resourceId(kind, name);
232
+ }
233
+
234
+ /** The module a manifest was declared in, when the loader stamped one. */
235
+ const declaringModule = (manifest: ResourceManifest): string | undefined =>
236
+ (manifest.metadata as { module?: string } | undefined)?.module;
237
+
238
+ /**
239
+ * A bare `<name>` read from a resource declared in `fromModule`.
240
+ *
241
+ * **Same module first**, because that is what a bare name MEANS: it is resolved
242
+ * in the scope it was written in, and a cross-module reference is written with
243
+ * an alias. A name declared in exactly one module resolves to it whatever the
244
+ * reader's scope — which is what keeps an unstamped manifest set (every
245
+ * fixture, every single-module load) behaving as it did. A name declared in
246
+ * SEVERAL other modules and none of the reader's resolves to NOTHING: guessing
247
+ * between them attributes a reference to a resource the author never named, and
248
+ * some other pass reports the dangling name honestly.
249
+ *
250
+ * Exported because more than one pass resolves a bare name — the call graph's
251
+ * own sites, and the projection's inline declarations and CEL state reads. Two
252
+ * spellings of this rule is how a fix in one leaves a first-wins lookup in the
253
+ * other, which is exactly the collision it exists to prevent.
254
+ */
255
+ export function resolveScopedName<T>(
256
+ candidates: readonly T[] | undefined,
257
+ moduleOf: (item: T) => string | undefined,
258
+ fromModule: string | undefined,
259
+ ): T | undefined {
260
+ if (!candidates || candidates.length === 0) return undefined;
261
+ if (candidates.length === 1) return candidates[0];
262
+ return candidates.find((candidate) => moduleOf(candidate) === fromModule);
263
+ }
264
+
167
265
  /**
168
266
  * Does control reach this edge's target?
169
267
  *
@@ -323,6 +421,65 @@ interface StepWalkContext {
323
421
  stepEdgesByPath: Map<string, CallGraphEdge>;
324
422
  }
325
423
 
424
+ /**
425
+ * What a step IS, and what decides whether it runs.
426
+ *
427
+ * Both come off the schema rather than off a keyword list: the variant is the
428
+ * `oneOf` branch whose REQUIRED keys the step carries, and the predicate is the
429
+ * field annotated as one. A composer that declares a step body of its own is
430
+ * therefore described in its own words, and nothing here knows that `while`
431
+ * exists.
432
+ *
433
+ * A step matching several branches takes the FIRST — the grammar is a `oneOf`,
434
+ * so more than one match is a manifest the checker rejects, and guessing among
435
+ * them would be a second opinion on a question the checker already answers.
436
+ */
437
+ function classifyStep(
438
+ step: Record<string, unknown>,
439
+ itemSchema: Record<string, any> | undefined,
440
+ rootSchema: Record<string, any>,
441
+ ): { variant?: string; variantLabel?: string; predicate?: string } {
442
+ if (!itemSchema) return {};
443
+ const branches = Array.isArray(itemSchema.oneOf) ? itemSchema.oneOf : [];
444
+ let variant: string | undefined;
445
+ let variantLabel: string | undefined;
446
+ for (const raw of branches) {
447
+ const branch = resolveLocalRef(raw, rootSchema);
448
+ const required = Array.isArray(branch?.required) ? (branch.required as string[]) : [];
449
+ if (required.length === 0 || !required.every((key) => step[key] !== undefined)) continue;
450
+ // The keys are what the branch IS; the title is what it is called.
451
+ variant = [...required].sort().join("+");
452
+ if (typeof branch?.title === "string") variantLabel = branch.title;
453
+ break;
454
+ }
455
+
456
+ let predicate: string | undefined;
457
+ for (const [key, propSchema] of propertySchemas(itemSchema)) {
458
+ const role = propSchema?.["x-telo-topology-role"];
459
+ if (role !== "predicate" && role !== "discriminator") continue;
460
+ const written = expressionSource(step[key]);
461
+ if (written !== undefined) {
462
+ predicate = written;
463
+ break;
464
+ }
465
+ }
466
+ return {
467
+ ...(variant ? { variant } : {}),
468
+ ...(variantLabel ? { variantLabel } : {}),
469
+ ...(predicate ? { predicate } : {}),
470
+ };
471
+ }
472
+
473
+ /** A predicate as the author wrote it. A CEL value reaches here as a tagged
474
+ * sentinel or as an already-compiled node, and the SOURCE is the only part of
475
+ * either worth showing. */
476
+ function expressionSource(value: unknown): string | undefined {
477
+ if (typeof value === "string") return value;
478
+ if (value === null || typeof value !== "object") return undefined;
479
+ const record = value as { source?: unknown; __compiled?: unknown };
480
+ return typeof record.source === "string" ? record.source : undefined;
481
+ }
482
+
326
483
  /**
327
484
  * Emit the edges a single step's own ref slots declare.
328
485
  *
@@ -338,7 +495,21 @@ function emitStepEdges(node: StepGraphNode, ctx: StepWalkContext): void {
338
495
  for (const [key, propSchema] of propertySchemas(ctx.itemSchema)) {
339
496
  const slot = readRefSlot(propSchema);
340
497
  if (!slot || slot.kinds.length === 0) continue;
341
- const targetName = refTargetName(node.step[key]);
498
+ const written = node.step[key];
499
+ const inline =
500
+ written !== null &&
501
+ typeof written === "object" &&
502
+ !Array.isArray(written) &&
503
+ isInlineResource(written as Record<string, unknown>);
504
+ // Every declared slot is recorded, whatever is in it: an empty one is a
505
+ // site an editor can fill, and a declaration written at the site is a real
506
+ // dispatch that emits no edge — which is what made an inline step read
507
+ // exactly like one that dispatches nothing.
508
+ node.refSlots = [
509
+ ...(node.refSlots ?? []),
510
+ { key, path: `${node.path}.${key}`, kinds: slot.kinds, ...(inline ? { inline: true } : {}) },
511
+ ];
512
+ const targetName = refTargetName(written);
342
513
  if (targetName === undefined) continue;
343
514
  const entry: RefFieldEntry = {
344
515
  refs: slot.kinds,
@@ -441,6 +612,10 @@ function walkSteps(
441
612
  step: value,
442
613
  };
443
614
  if (typeof value.name === "string") node.name = value.name;
615
+ const { variant, variantLabel, predicate } = classifyStep(value, ctx.itemSchema, ctx.rootSchema);
616
+ if (variant) node.variant = variant;
617
+ if (variantLabel) node.variantLabel = variantLabel;
618
+ if (predicate) node.predicate = predicate;
444
619
  if (parent) node.parent = parent;
445
620
  ctx.nodes.set(id, node);
446
621
  ctx.order.push(node);
@@ -498,7 +673,9 @@ export function buildCallGraph(
498
673
  ): CallGraph {
499
674
  const nodes = new Map<string, CallGraphNode>();
500
675
  const edges: CallGraphEdge[] = [];
501
- const byName = new Map<string, ResourceGraphNode>();
676
+ /** Every declaration of a bare name, in declaration order — a name may be
677
+ * declared once per module, so this is a list rather than one node. */
678
+ const byName = new Map<string, ResourceGraphNode[]>();
502
679
  const stepsByOwner = new Map<string, StepGraphNode[]>();
503
680
  const stepEdgesByPath = new Map<string, CallGraphEdge>();
504
681
 
@@ -527,15 +704,22 @@ export function buildCallGraph(
527
704
  if (!name || !manifest.kind || SYSTEM_KINDS.has(manifest.kind)) continue;
528
705
  const node: ResourceGraphNode = {
529
706
  type: "resource",
530
- id: resourceId(manifest.kind, name as string),
707
+ id: nodeIdFor(manifest),
531
708
  kind: manifest.kind,
532
709
  name: name as string,
533
710
  manifest,
534
711
  };
712
+ if (nodes.has(node.id)) continue;
535
713
  nodes.set(node.id, node);
536
- byName.set(node.name, node);
714
+ byName.set(node.name, [...(byName.get(node.name) ?? []), node]);
537
715
  }
538
716
 
717
+ const resolveByName = (
718
+ name: string,
719
+ fromModule: string | undefined,
720
+ ): ResourceGraphNode | undefined =>
721
+ resolveScopedName(byName.get(name), (node) => declaringModule(node.manifest), fromModule);
722
+
539
723
  // --- step nodes ---
540
724
  const collectStepsFor = (
541
725
  node: ResourceGraphNode,
@@ -565,7 +749,7 @@ export function buildCallGraph(
565
749
  };
566
750
 
567
751
  for (const node of [...nodes.values()] as ResourceGraphNode[]) {
568
- collectStepsFor(node, (name) => byName.get(name));
752
+ collectStepsFor(node, (name) => resolveByName(name, declaringModule(node.manifest)));
569
753
  }
570
754
 
571
755
  // --- edges ---
@@ -593,7 +777,7 @@ export function buildCallGraph(
593
777
  scopeLocal = new Map();
594
778
  const ownerName = event.source.metadata?.name as string | undefined;
595
779
  if (!ownerName || !event.source.kind) return;
596
- const ownerId = resourceId(event.source.kind, ownerName);
780
+ const ownerId = nodeIdFor(event.source);
597
781
 
598
782
  // Inline declarations inside `x-telo-scope` arrays become nodes of
599
783
  // their own, keyed by their scope site — the declaration-site identity
@@ -620,8 +804,9 @@ export function buildCallGraph(
620
804
  }
621
805
  if (scopeLocal.size === 0) return;
622
806
 
807
+ const ownerModule = declaringModule(event.source);
623
808
  const resolveScoped = (name: string): ResourceGraphNode | undefined =>
624
- scopeLocal.get(name) ?? byName.get(name);
809
+ scopeLocal.get(name) ?? resolveByName(name, ownerModule);
625
810
 
626
811
  // The owner's own step edges were emitted before this scope was seen
627
812
  // (step collection precedes the visit), so their names resolved
@@ -680,7 +865,7 @@ export function buildCallGraph(
680
865
  onRef: (event) => {
681
866
  const sourceName = event.source.metadata?.name as string | undefined;
682
867
  if (!sourceName || !event.source.kind) return;
683
- const sourceId = resourceId(event.source.kind, sourceName);
868
+ const sourceId = nodeIdFor(event.source);
684
869
  if (!nodes.has(sourceId)) return;
685
870
 
686
871
  // A site inside a step was already emitted by the step walk, which
@@ -719,9 +904,10 @@ export function buildCallGraph(
719
904
  path: event.concretePath,
720
905
  use,
721
906
  };
907
+ const sourceModule = declaringModule(event.source);
722
908
  const target = scopedNames.has(targetName)
723
- ? (scopeLocal.get(targetName) ?? byName.get(targetName))
724
- : byName.get(targetName);
909
+ ? (scopeLocal.get(targetName) ?? resolveByName(targetName, sourceModule))
910
+ : resolveByName(targetName, sourceModule);
725
911
  if (target) edge.to = target.id;
726
912
  if (unresolved) edge.unresolved = unresolved;
727
913
  if (unresolvedReason) edge.unresolvedReason = unresolvedReason;
@@ -758,8 +944,15 @@ export function buildCallGraph(
758
944
  edges,
759
945
  edgesFrom: (id) => fromIndex.get(id) ?? [],
760
946
  edgesTo: (id) => toIndex.get(id) ?? [],
761
- resource: (kind, name) => nodes.get(resourceId(kind, name)) as ResourceGraphNode | undefined,
762
- resourceByName: (name) => byName.get(name),
947
+ // Both accessors take a name with no module, so both answer for the one
948
+ // declaration that name has — and, where a flattened set has several, the
949
+ // first in declaration order. A caller that must tell two apart holds a
950
+ // node id, which carries the module.
951
+ resource: (kind, name) =>
952
+ (nodes.get(resourceId(kind, name)) as ResourceGraphNode | undefined) ??
953
+ byName.get(name)?.find((n) => n.kind === kind),
954
+ resourceByName: (name, fromModule) =>
955
+ resolveScopedName(byName.get(name), (node) => declaringModule(node.manifest), fromModule),
763
956
  steps: (ownerId) => stepsByOwner.get(ownerId) ?? [],
764
957
  controlEdges: () => edges.filter(reachesTarget),
765
958
  };
@@ -1,18 +1,16 @@
1
1
  import type { GraphLoadError, LoadedGraph } from "./loaded-types.js";
2
2
  import { isLocalPathSource } from "./sources/local-path-ref.js";
3
- import { isRegistryRef } from "./sources/module-ref.js";
4
3
  import { isOciRef } from "./sources/oci-ref.js";
5
4
  import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
6
5
 
7
6
  const SOURCE = "telo-analyzer";
8
7
 
9
- /** True when `source` is a shape some transport claims — a registry ref, an OCI
10
- * ref, an HTTP(S) URL, or a relative/absolute path. A source matching none of
11
- * these is malformed (no transport can ever resolve it), which we report
12
- * differently from a well-formed ref that simply failed to fetch. */
8
+ /** True when `source` is a shape some transport claims — an OCI ref, an HTTP(S)
9
+ * URL, or a relative/absolute path. A source matching none of these is
10
+ * malformed (no transport can ever resolve it), which we report differently
11
+ * from a well-formed ref that simply failed to fetch. */
13
12
  function isRecognizedSourceShape(source: string): boolean {
14
13
  return (
15
- isRegistryRef(source) ||
16
14
  isOciRef(source) ||
17
15
  source.startsWith("http://") ||
18
16
  source.startsWith("https://") ||
@@ -27,13 +25,32 @@ function classify(e: GraphLoadError): "INVALID_IMPORT_TARGET" | "INVALID_IMPORT_
27
25
  return isRecognizedSourceShape(e.source ?? e.url) ? "IMPORT_UNRESOLVED" : "INVALID_IMPORT_SOURCE";
28
26
  }
29
27
 
28
+ /** The removed bare `<namespace>/<name>@<version>` ref: no scheme, not a path,
29
+ * carrying both a `/` and an `@`. Recognised only to explain itself — no
30
+ * migration can rewrite it, because the OCI host it moved to is not derivable
31
+ * from the ref, and the failing line is frequently inside a dependency the
32
+ * consumer cannot edit. The message is the whole remedy available to them. */
33
+ function isRemovedBareRef(source: string): boolean {
34
+ return (
35
+ !source.includes("://") &&
36
+ !source.startsWith("/") &&
37
+ !source.startsWith(".") &&
38
+ source.includes("@") &&
39
+ source.includes("/")
40
+ );
41
+ }
42
+
30
43
  function messageFor(e: GraphLoadError, code: ReturnType<typeof classify>): string {
31
44
  const authored = e.source ?? e.url;
32
45
  const via = e.alias ? `import '${e.alias}' → '${authored}'` : `'${authored}'`;
33
46
  if (code === "INVALID_IMPORT_SOURCE") {
47
+ const removedForm = isRemovedBareRef(authored)
48
+ ? ` The bare '<namespace>/<name>@<version>' form was removed — rewrite it to the ` +
49
+ `module's 'oci://' ref (\`telo search <name>\` reports it).`
50
+ : "";
34
51
  return (
35
52
  `Cannot resolve ${via}: not a recognized module reference. Expected ` +
36
- `'namespace/name@version', 'oci://host/repo@tag', 'https://…', or a relative path.`
53
+ `'oci://host/repo@tag', 'https://…', or a relative path.${removedForm}`
37
54
  );
38
55
  }
39
56
  // The target WAS obtained, so "cannot resolve" would name the wrong problem —
package/src/index.ts CHANGED
@@ -112,7 +112,13 @@ export {
112
112
  RedactionPathError,
113
113
  } from "./redaction-path.js";
114
114
  export type { RedactionSegment } from "./redaction-path.js";
115
- export { buildCallGraph, projectToPairs, resourceId } from "./call-graph.js";
115
+ export {
116
+ buildCallGraph,
117
+ nodeIdFor,
118
+ projectToPairs,
119
+ resolveScopedName,
120
+ resourceId,
121
+ } from "./call-graph.js";
116
122
  export type {
117
123
  BuildCallGraphOptions,
118
124
  CallGraph,
@@ -121,6 +127,31 @@ export type {
121
127
  ResourceGraphNode,
122
128
  StepGraphNode,
123
129
  } from "./call-graph.js";
130
+ export {
131
+ AMBIENT_CAPABILITIES,
132
+ buildModuleGraph,
133
+ contentKey,
134
+ edgeClassOf,
135
+ isAmbientCapability,
136
+ isAmbientHold,
137
+ isOrderedRow,
138
+ isUnwired,
139
+ } from "./module-graph.js";
140
+ export type {
141
+ BuildModuleGraphOptions,
142
+ EdgeClass,
143
+ GraphEdge,
144
+ GraphKind,
145
+ GraphNode,
146
+ GraphPort,
147
+ GraphRegion,
148
+ GraphRow,
149
+ ModuleGraph,
150
+ ModuleGraphDeps,
151
+ NodeOwnership,
152
+ PortSlot,
153
+ RowKind,
154
+ } from "./module-graph.js";
124
155
  export {
125
156
  buildReferenceFieldMap,
126
157
  isRefEntry,
@@ -304,7 +335,12 @@ export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validat
304
335
  export type { RefSlotIssue } from "./validate-ref-slots.js";
305
336
  export { validateValueTypeSlots } from "./validate-value-type-slots.js";
306
337
  export type { ValueTypeSlotIssue } from "./validate-value-type-slots.js";
307
- export { checkSchemaCompatibility, resolveRefIn, selectUnionBranch } from "./schema-compat.js";
338
+ export {
339
+ checkSchemaCompatibility,
340
+ collectProperties,
341
+ resolveRefIn,
342
+ selectUnionBranch,
343
+ } from "./schema-compat.js";
308
344
  export type { CompatibilityResult, ExternalSchemaResolver } from "./schema-compat.js";
309
345
  export {
310
346
  ajvErrorToPath,
@@ -362,7 +398,6 @@ export {
362
398
  } from "./position-metadata.js";
363
399
  export type { DocumentPosition } from "./position-metadata.js";
364
400
  export { HttpSource } from "./sources/http-source.js";
365
- export { RegistrySource } from "./sources/registry-source.js";
366
401
  export { defaultSources } from "./sources/default-sources.js";
367
402
  export {
368
403
  splitIntegrity,
@@ -373,8 +408,6 @@ export {
373
408
  sha256Base64Url,
374
409
  IntegrityError,
375
410
  } from "./sources/integrity.js";
376
- export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
377
- export type { ParsedModuleRef } from "./sources/module-ref.js";
378
411
  export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
379
412
  export type { ParsedOciRef } from "./sources/oci-ref.js";
380
413
  export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
@@ -12,9 +12,9 @@ import type { AstDocument } from "./yaml-ast.js";
12
12
  * cache uses `source` — the URL the source adapter's `read()` returned. */
13
13
  export interface LoadedFile {
14
14
  /** Canonical identity. The URL the source adapter's `read()` returned —
15
- * HTTPS for http/registry, an absolute path for local. */
15
+ * HTTPS for a remote module, an absolute path for local. */
16
16
  source: string;
17
- /** The URL the caller supplied (e.g. registry ref `std/javascript@0.3.0`).
17
+ /** The URL the caller supplied (e.g. `oci://ghcr.io/telorun/javascript@0.3.0`).
18
18
  * Differs from `source` only for adapter-resolved URLs. */
19
19
  requestedUrl: string;
20
20
  /** Raw text exactly as `read()` returned it. */
@@ -65,8 +65,8 @@ export interface LoadedModule {
65
65
  export interface ImportEdge {
66
66
  /** Canonical resolved URL of the target — a key into `modules`. */
67
67
  targetSource: string;
68
- /** The import's `source` exactly as authored — a registry ref, an `oci://`
69
- * or `https://` ref, or a relative path. Version reconciliation keys on
68
+ /** The import's `source` exactly as authored — an `oci://` or `https://`
69
+ * ref, or a relative path. Version reconciliation keys on
70
70
  * this (minus its version), since it names the module's location
71
71
  * independently of what the module declares about itself. */
72
72
  targetRef: string;
@@ -27,7 +27,14 @@ import { CelScopeQuery, type CelScopeQueryContext } from "./cel-scope-query.js";
27
27
  import { DefinitionRegistry } from "./definition-registry.js";
28
28
  import type { ContractDirection } from "./extends-resolution.js";
29
29
  import { analyzerContractScope, resolveContract } from "./invocation-contract.js";
30
+ import { buildCallGraph, type CallGraph } from "./call-graph.js";
30
31
  import { findManifest } from "./find-manifest.js";
32
+ import {
33
+ buildModuleGraph,
34
+ type BuildModuleGraphOptions,
35
+ type ModuleGraph,
36
+ type ModuleGraphDeps,
37
+ } from "./module-graph.js";
31
38
  import { isModuleKind } from "./module-kinds.js";
32
39
 
33
40
  /**
@@ -43,6 +50,8 @@ export interface ManifestRef {
43
50
  export class ManifestAnalysis {
44
51
  private readonly scopes: ModuleScopes;
45
52
  private celScopeQuery: CelScopeQuery | undefined;
53
+ private callGraphMemo: CallGraph | undefined;
54
+ private moduleGraphMemo: ModuleGraph | undefined;
46
55
 
47
56
  constructor(
48
57
  readonly manifests: ResourceManifest[],
@@ -62,6 +71,36 @@ export class ManifestAnalysis {
62
71
  return (this.celScopeQuery ??= new CelScopeQuery(this.manifests, this.ctx));
63
72
  }
64
73
 
74
+ /** What calls what. Built on first use and kept, so the projection and any
75
+ * other consumer over this set share one. */
76
+ private get callGraph(): CallGraph {
77
+ return (this.callGraphMemo ??= buildCallGraph(this.manifests, this.ctx.defs, {
78
+ aliases: this.ctx.aliases,
79
+ aliasesByModule: this.ctx.aliasesByModule,
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * The module graph: boxes, ordered rows and classed edges — the projection an
85
+ * editor draws.
86
+ *
87
+ * Here rather than on the registry because it is the registry AND a manifest
88
+ * set, which is the pairing this class exists to name; a fifth
89
+ * `registry.x(manifests, …)` factory is the accretion naming it stopped. It
90
+ * shares this analysis's call graph rather than building one of its own.
91
+ *
92
+ * `deps` is supplied by the registry, which owns capability resolution — this
93
+ * class holds the manifests, not the rules for reading a kind.
94
+ */
95
+ moduleGraph(deps: ModuleGraphDeps, options: BuildModuleGraphOptions = {}): ModuleGraph {
96
+ return (this.moduleGraphMemo ??= buildModuleGraph(
97
+ this.manifests,
98
+ this.callGraph,
99
+ deps,
100
+ options,
101
+ ));
102
+ }
103
+
65
104
  /** The manifest a `(kind, name)` pair addresses. */
66
105
  resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined {
67
106
  return findManifest(this.manifests, kind, name);
@@ -146,7 +146,7 @@ export class Loader {
146
146
  * when a URL it has already canonicalised is requested again — kernel
147
147
  * load → boot and the import-controller each ask the loader for the same
148
148
  * modules. Without this fast path every duplicate request re-runs the
149
- * source's `read()` (a `fetch` for `RegistrySource`, a disk read for
149
+ * source's `read()` (a `fetch` for `HttpSource`, a disk read for
150
150
  * `LocalFileSource`). */
151
151
  private readonly urlToSource = new Map<string, string>();
152
152
 
@@ -157,8 +157,7 @@ export class Loader {
157
157
  /** Sources are resolved in order — the first whose `supports(url)` matches
158
158
  * wins. The caller (composition root) decides which concrete sources exist
159
159
  * and supplies them; `defaultSources()` bundles the browser-safe built-ins
160
- * (HTTP + registry) for the common case. `register()` prepends a source at
161
- * runtime. */
160
+ * (HTTP) for the common case. `register()` prepends a source at runtime. */
162
161
  constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
163
162
  this.sources = [...sources];
164
163
  this.celEnv = buildCelEnvironment(options.celHandlers);
@@ -181,7 +180,7 @@ export class Loader {
181
180
  // entry are populated in `urlToSource` + `fileCache` in one read.
182
181
  // Callers (kernel.load) immediately call `loadGraph(entryUrl)`
183
182
  // afterwards — without this priming, the entry file would be read
184
- // twice (twice over the network for `RegistrySource`).
183
+ // twice (twice over the network for `HttpSource`).
185
184
  const file = await this.loadFile(url);
186
185
  return file.source;
187
186
  }
@@ -469,7 +468,7 @@ export class Loader {
469
468
 
470
469
  /** Resolve an `import` URL against the file it appears in. Relative /
471
470
  * absolute-path forms run through the owning `ManifestSource`'s
472
- * `resolveRelative`; registry refs and full URLs pass through
471
+ * `resolveRelative`; scheme-qualified refs and full URLs pass through
473
472
  * unchanged. Exposed so the import-controller (and any other
474
473
  * caller-side resolver) lands on the *exact same* canonical URL the
475
474
  * loader used when walking the entry graph — divergent resolution
@@ -241,6 +241,10 @@ export const InvokeStepSchema = {
241
241
  when: {
242
242
  title: "When",
243
243
  description: "CEL guard — the dispatch is skipped when it evaluates false.",
244
+ // The same role `if:` and `while:` carry: it is the expression that
245
+ // decides whether the statement runs, and a surface showing what a step
246
+ // is conditional on should not have to know which keyword spelled it.
247
+ "x-telo-topology-role": "predicate",
244
248
  type: "string",
245
249
  },
246
250
  retry: {