@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.
- package/dist/analysis-registry.d.ts +12 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +26 -0
- package/dist/call-graph.d.ts +80 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +145 -12
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +22 -8
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/loaded-types.d.ts +4 -4
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-analysis.d.ts +19 -0
- package/dist/manifest-analysis.d.ts.map +1 -1
- package/dist/manifest-analysis.js +27 -0
- package/dist/manifest-loader.d.ts +3 -4
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +4 -5
- package/dist/manifest-schemas.d.ts +2 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +4 -0
- package/dist/module-graph.d.ts +500 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/module-graph.js +1411 -0
- package/dist/reconcile-module-versions.d.ts.map +1 -1
- package/dist/reconcile-module-versions.js +10 -11
- package/dist/release/release-plan.d.ts +1 -1
- package/dist/resolve-zone-containment.d.ts +9 -1
- package/dist/resolve-zone-containment.d.ts.map +1 -1
- package/dist/resolve-zone-containment.js +34 -6
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +4 -2
- package/dist/sources/default-sources.d.ts +6 -6
- package/dist/sources/default-sources.d.ts.map +1 -1
- package/dist/sources/default-sources.js +7 -8
- package/dist/sources/integrity.d.ts +3 -2
- package/dist/sources/integrity.d.ts.map +1 -1
- package/dist/sources/integrity.js +26 -3
- package/dist/sources/versioned-ref.d.ts +17 -12
- package/dist/sources/versioned-ref.d.ts.map +1 -1
- package/dist/sources/versioned-ref.js +22 -24
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/package.json +1 -1
- package/src/analysis-registry.ts +37 -0
- package/src/call-graph.ts +207 -14
- package/src/import-resolution-diagnostics.ts +24 -7
- package/src/index.ts +38 -5
- package/src/loaded-types.ts +4 -4
- package/src/manifest-analysis.ts +39 -0
- package/src/manifest-loader.ts +4 -5
- package/src/manifest-schemas.ts +4 -0
- package/src/module-graph.ts +2005 -0
- package/src/reconcile-module-versions.ts +10 -11
- package/src/release/release-plan.ts +1 -1
- package/src/resolve-zone-containment.ts +49 -9
- package/src/resolve-zone-requirements.ts +7 -2
- package/src/sources/default-sources.ts +7 -8
- package/src/sources/integrity.ts +28 -3
- package/src/sources/versioned-ref.ts +26 -28
- package/src/telo-version.ts +1 -1
- package/dist/sources/module-ref.d.ts +0 -21
- package/dist/sources/module-ref.d.ts.map +0 -1
- package/dist/sources/module-ref.js +0 -36
- package/dist/sources/registry-source.d.ts +0 -14
- package/dist/sources/registry-source.d.ts.map +0 -1
- package/dist/sources/registry-source.js +0 -45
- package/src/sources/module-ref.ts +0 -49
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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) =>
|
|
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 =
|
|
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) ??
|
|
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 =
|
|
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) ??
|
|
724
|
-
:
|
|
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
|
-
|
|
762
|
-
|
|
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 —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
`'
|
|
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 {
|
|
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 {
|
|
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";
|
package/src/loaded-types.ts
CHANGED
|
@@ -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
|
|
15
|
+
* HTTPS for a remote module, an absolute path for local. */
|
|
16
16
|
source: string;
|
|
17
|
-
/** The URL the caller supplied (e.g.
|
|
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 —
|
|
69
|
-
*
|
|
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;
|
package/src/manifest-analysis.ts
CHANGED
|
@@ -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);
|
package/src/manifest-loader.ts
CHANGED
|
@@ -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 `
|
|
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
|
|
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 `
|
|
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`;
|
|
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
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -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: {
|