@telorun/analyzer 0.1.4 → 0.2.1
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/README.md +3 -3
- package/dist/analyzer.d.ts +6 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +45 -25
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +52 -24
- package/dist/cel-environment.d.ts +12 -5
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +31 -17
- package/dist/definition-registry.d.ts +5 -5
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +10 -10
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel-globals.d.ts +6 -2
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +14 -8
- package/dist/manifest-loader.d.ts +1 -0
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +36 -14
- package/dist/module-kinds.d.ts +4 -0
- package/dist/module-kinds.d.ts.map +1 -0
- package/dist/module-kinds.js +4 -0
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +6 -1
- package/dist/precompile.d.ts +3 -2
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +13 -11
- package/dist/reference-field-map.d.ts +1 -1
- package/dist/resolve-throws-union.d.ts +30 -0
- package/dist/resolve-throws-union.d.ts.map +1 -0
- package/dist/resolve-throws-union.js +252 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.js +1 -1
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +19 -12
- package/dist/validate-throws-coverage.d.ts +8 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -0
- package/dist/validate-throws-coverage.js +461 -0
- package/package.json +2 -2
- package/src/analyzer.ts +60 -26
- package/src/builtins.ts +52 -24
- package/src/cel-environment.ts +40 -17
- package/src/definition-registry.ts +10 -10
- package/src/dependency-graph.ts +9 -2
- package/src/index.ts +2 -0
- package/src/kernel-globals.ts +19 -10
- package/src/manifest-loader.ts +40 -14
- package/src/module-kinds.ts +6 -0
- package/src/normalize-inline-resources.ts +6 -1
- package/src/precompile.ts +14 -11
- package/src/reference-field-map.ts +1 -1
- package/src/resolve-throws-union.ts +345 -0
- package/src/types.ts +3 -0
- package/src/validate-cel-context.ts +1 -1
- package/src/validate-references.ts +19 -12
- package/src/validate-throws-coverage.ts +565 -0
- package/dist/adapters/node-adapter.d.ts +0 -17
- package/dist/adapters/node-adapter.d.ts.map +0 -1
- package/dist/adapters/node-adapter.js +0 -71
package/src/manifest-loader.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
1
2
|
import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
|
|
2
3
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
|
|
3
4
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
5
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
6
|
+
import { buildCelEnvironment } from "./cel-environment.js";
|
|
7
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
5
8
|
import { precompileDoc } from "./precompile.js";
|
|
6
9
|
import {
|
|
7
10
|
DEFAULT_MANIFEST_FILENAME,
|
|
@@ -12,7 +15,12 @@ import {
|
|
|
12
15
|
type PositionIndex,
|
|
13
16
|
} from "./types.js";
|
|
14
17
|
|
|
15
|
-
const SYSTEM_KINDS = new Set([
|
|
18
|
+
const SYSTEM_KINDS = new Set([
|
|
19
|
+
"Telo.Application",
|
|
20
|
+
"Telo.Library",
|
|
21
|
+
"Telo.Import",
|
|
22
|
+
"Telo.Definition",
|
|
23
|
+
]);
|
|
16
24
|
|
|
17
25
|
export class Loader {
|
|
18
26
|
private static readonly moduleCache = new Map<
|
|
@@ -21,6 +29,7 @@ export class Loader {
|
|
|
21
29
|
>();
|
|
22
30
|
|
|
23
31
|
protected adapters: ManifestAdapter[];
|
|
32
|
+
private readonly celEnv: Environment;
|
|
24
33
|
|
|
25
34
|
constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
|
|
26
35
|
const options: LoaderInitOptions = Array.isArray(extraAdaptersOrOptions)
|
|
@@ -37,6 +46,8 @@ export class Loader {
|
|
|
37
46
|
if (options.extraAdapters?.length) {
|
|
38
47
|
this.adapters.unshift(...options.extraAdapters);
|
|
39
48
|
}
|
|
49
|
+
|
|
50
|
+
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
register(adapter: ManifestAdapter): this {
|
|
@@ -79,7 +90,7 @@ export class Loader {
|
|
|
79
90
|
let compiledDocs: unknown[];
|
|
80
91
|
if (options?.compile) {
|
|
81
92
|
try {
|
|
82
|
-
const result = precompileDoc(rawDoc);
|
|
93
|
+
const result = precompileDoc(rawDoc, this.celEnv);
|
|
83
94
|
compiledDocs = Array.isArray(result) ? result : [result];
|
|
84
95
|
} catch (error) {
|
|
85
96
|
throw new Error(
|
|
@@ -106,17 +117,19 @@ export class Loader {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
const moduleManifests = resolved.filter((m) => m.kind
|
|
120
|
+
const moduleManifests = resolved.filter((m) => isModuleKind(m.kind));
|
|
110
121
|
if (moduleManifests.length > 1) {
|
|
122
|
+
const kinds = moduleManifests.map((m) => m.kind).join(", ");
|
|
111
123
|
throw new Error(
|
|
112
|
-
`File '${source}' contains ${moduleManifests.length}
|
|
124
|
+
`File '${source}' contains ${moduleManifests.length} module declarations (${kinds}). ` +
|
|
125
|
+
`A file may declare at most one Telo.Application or Telo.Library.`,
|
|
113
126
|
);
|
|
114
127
|
}
|
|
115
128
|
const moduleManifest = moduleManifests[0];
|
|
116
129
|
const moduleName = moduleManifest?.metadata?.name as string | undefined;
|
|
117
130
|
if (moduleName) {
|
|
118
131
|
for (const manifest of resolved) {
|
|
119
|
-
if (manifest.kind
|
|
132
|
+
if (!isModuleKind(manifest.kind) && !manifest.metadata?.module) {
|
|
120
133
|
const pi = (manifest.metadata as any)?.positionIndex;
|
|
121
134
|
manifest.metadata = { ...manifest.metadata, module: moduleName };
|
|
122
135
|
if (pi) {
|
|
@@ -205,7 +218,7 @@ export class Loader {
|
|
|
205
218
|
let compiledDocs: unknown[];
|
|
206
219
|
if (options?.compile) {
|
|
207
220
|
try {
|
|
208
|
-
const result = precompileDoc(rawDoc);
|
|
221
|
+
const result = precompileDoc(rawDoc, this.celEnv);
|
|
209
222
|
compiledDocs = Array.isArray(result) ? result : [result];
|
|
210
223
|
} catch (error) {
|
|
211
224
|
throw new Error(
|
|
@@ -247,10 +260,10 @@ export class Loader {
|
|
|
247
260
|
} | null> {
|
|
248
261
|
// Try loading as a regular module first (it might be a telo.yaml itself).
|
|
249
262
|
// Use loadManifests (not loadModule) so imported definitions are included —
|
|
250
|
-
// otherwise the analyzer won't know about kinds from
|
|
263
|
+
// otherwise the analyzer won't know about kinds from Telo.Import sources.
|
|
251
264
|
try {
|
|
252
265
|
const docs = await this.loadModule(fileUrl);
|
|
253
|
-
const hasModule = docs.some((d) => d.kind
|
|
266
|
+
const hasModule = docs.some((d) => isModuleKind(d.kind));
|
|
254
267
|
if (hasModule) {
|
|
255
268
|
const { source } = await this.pick(fileUrl).read(fileUrl);
|
|
256
269
|
const manifests = await this.loadManifests(fileUrl);
|
|
@@ -291,7 +304,7 @@ export class Loader {
|
|
|
291
304
|
|
|
292
305
|
while (queue.length > 0) {
|
|
293
306
|
const m = queue.shift()!;
|
|
294
|
-
if (m.kind !== "
|
|
307
|
+
if (m.kind !== "Telo.Import") continue;
|
|
295
308
|
const importSource = (m as any).source as string | undefined;
|
|
296
309
|
if (!importSource) continue;
|
|
297
310
|
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
@@ -311,7 +324,7 @@ export class Loader {
|
|
|
311
324
|
}
|
|
312
325
|
result.set(importUrl, imported);
|
|
313
326
|
for (const im of imported) {
|
|
314
|
-
if (im.kind === "
|
|
327
|
+
if (im.kind === "Telo.Import") queue.push(im);
|
|
315
328
|
}
|
|
316
329
|
}
|
|
317
330
|
|
|
@@ -327,7 +340,7 @@ export class Loader {
|
|
|
327
340
|
|
|
328
341
|
while (queue.length > 0) {
|
|
329
342
|
const m = queue.shift()!;
|
|
330
|
-
if (m.kind !== "
|
|
343
|
+
if (m.kind !== "Telo.Import") continue;
|
|
331
344
|
const importSource = (m as any).source as string | undefined;
|
|
332
345
|
if (!importSource) continue;
|
|
333
346
|
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
@@ -345,7 +358,20 @@ export class Loader {
|
|
|
345
358
|
(e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
|
|
346
359
|
throw e;
|
|
347
360
|
}
|
|
348
|
-
|
|
361
|
+
// Import target must be a Telo.Library. Check the Library branch
|
|
362
|
+
// explicitly rather than "anything that's a module kind" so that a
|
|
363
|
+
// future third kind can't silently slip past as a valid import target.
|
|
364
|
+
const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
|
|
365
|
+
const importedApplication = imported.find((im) => im.kind === "Telo.Application");
|
|
366
|
+
if (importedApplication) {
|
|
367
|
+
const e = new Error(
|
|
368
|
+
`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
369
|
+
`Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
|
|
370
|
+
);
|
|
371
|
+
(e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
const importedModule = importedLibrary;
|
|
349
375
|
if (importedModule?.metadata?.name) {
|
|
350
376
|
const pi = (m.metadata as any)?.positionIndex;
|
|
351
377
|
m.metadata = {
|
|
@@ -363,8 +389,8 @@ export class Loader {
|
|
|
363
389
|
}
|
|
364
390
|
}
|
|
365
391
|
for (const im of imported) {
|
|
366
|
-
if (im.kind === "
|
|
367
|
-
if (im.kind === "
|
|
392
|
+
if (im.kind === "Telo.Definition") importedDefs.push(im);
|
|
393
|
+
if (im.kind === "Telo.Import") queue.push(im);
|
|
368
394
|
}
|
|
369
395
|
}
|
|
370
396
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const MODULE_KINDS = ["Telo.Application", "Telo.Library"] as const;
|
|
2
|
+
export type ModuleKind = (typeof MODULE_KINDS)[number];
|
|
3
|
+
|
|
4
|
+
export function isModuleKind(kind: string | undefined): kind is ModuleKind {
|
|
5
|
+
return kind === "Telo.Application" || kind === "Telo.Library";
|
|
6
|
+
}
|
|
@@ -3,7 +3,12 @@ import { isRefEntry, isScopeEntry, isInlineResource } from "./reference-field-ma
|
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
5
5
|
|
|
6
|
-
const SYSTEM_KINDS = new Set([
|
|
6
|
+
const SYSTEM_KINDS = new Set([
|
|
7
|
+
"Telo.Definition",
|
|
8
|
+
"Telo.Application",
|
|
9
|
+
"Telo.Library",
|
|
10
|
+
"Telo.Import",
|
|
11
|
+
]);
|
|
7
12
|
|
|
8
13
|
/** Replaces characters outside [a-zA-Z0-9_] with underscores. */
|
|
9
14
|
function sanitizeName(raw: string): string {
|
package/src/precompile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompiledValue } from "@telorun/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
|
|
4
4
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
5
5
|
const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
@@ -8,31 +8,32 @@ const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
|
8
8
|
* Walks a raw YAML document and replaces all "${{ expr }}" strings with
|
|
9
9
|
* CompiledValue wrappers. Throws on CEL syntax errors.
|
|
10
10
|
* Intended to be called once per document at load time.
|
|
11
|
-
*
|
|
11
|
+
* Telo.Definition documents are returned unchanged — their schema fields
|
|
12
12
|
* are static metadata and must not be treated as CEL templates.
|
|
13
13
|
*/
|
|
14
|
-
export function precompileDoc(doc: unknown): unknown {
|
|
15
|
-
if (typeof doc === "string") return compileString(doc);
|
|
16
|
-
if (Array.isArray(doc)) return doc.map(precompileDoc);
|
|
14
|
+
export function precompileDoc(doc: unknown, env: Environment): unknown {
|
|
15
|
+
if (typeof doc === "string") return compileString(doc, env);
|
|
16
|
+
if (Array.isArray(doc)) return doc.map((item) => precompileDoc(item, env));
|
|
17
17
|
// Only recurse into plain objects. Class instances (ResourceInstance, ScopeHandle, etc.)
|
|
18
18
|
// are returned as-is — their prototype methods must not be lost by object reconstruction.
|
|
19
19
|
if (doc !== null && typeof doc === "object" && Object.getPrototypeOf(doc) === Object.prototype) {
|
|
20
20
|
const result: Record<string, unknown> = {};
|
|
21
21
|
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
|
|
22
|
-
result[k] = precompileDoc(v);
|
|
22
|
+
result[k] = precompileDoc(v, env);
|
|
23
23
|
}
|
|
24
24
|
return result;
|
|
25
25
|
}
|
|
26
26
|
return doc;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function compileString(s: string): unknown {
|
|
29
|
+
function compileString(s: string, env: Environment): unknown {
|
|
30
30
|
if (!s.includes("${{")) return s;
|
|
31
31
|
|
|
32
32
|
const exact = s.match(EXACT_TEMPLATE_REGEX);
|
|
33
33
|
if (exact) {
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const expr = exact[1].trim();
|
|
35
|
+
const fn = env.parse(expr);
|
|
36
|
+
return { __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// Interpolated template — collect literal parts + compiled sub-expressions
|
|
@@ -40,14 +41,16 @@ function compileString(s: string): unknown {
|
|
|
40
41
|
let last = 0;
|
|
41
42
|
for (const m of s.matchAll(TEMPLATE_REGEX)) {
|
|
42
43
|
if (m.index! > last) parts.push(s.slice(last, m.index));
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const expr = m[1].trim();
|
|
45
|
+
const fn = env.parse(expr);
|
|
46
|
+
parts.push({ __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue);
|
|
45
47
|
last = m.index! + m[0].length;
|
|
46
48
|
}
|
|
47
49
|
if (last < s.length) parts.push(s.slice(last));
|
|
48
50
|
|
|
49
51
|
return {
|
|
50
52
|
__compiled: true,
|
|
53
|
+
source: s,
|
|
51
54
|
call: (ctx: Record<string, unknown>) =>
|
|
52
55
|
parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
53
56
|
} satisfies CompiledValue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** An entry for a field that carries one or more x-telo-ref constraints. */
|
|
2
2
|
export interface RefFieldEntry {
|
|
3
|
-
/** One or more canonical ref strings ("namespace/module#TypeName" or "
|
|
3
|
+
/** One or more canonical ref strings ("namespace/module#TypeName" or "telo#TypeName").
|
|
4
4
|
* Multiple entries arise from anyOf branches. */
|
|
5
5
|
refs: string[];
|
|
6
6
|
/** True when the field path traversed through at least one array (path contains "[]"). */
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
|
|
5
|
+
export interface ThrowsCodeMeta {
|
|
6
|
+
data?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ThrowsUnion {
|
|
10
|
+
/** Code → per-code metadata (data schema, etc). Keys are the declared codes. */
|
|
11
|
+
codes: Map<string, ThrowsCodeMeta>;
|
|
12
|
+
/** True when the union cannot be fully resolved statically — e.g. a
|
|
13
|
+
* `passthrough` call site uses a CEL expression the analyzer can't narrow,
|
|
14
|
+
* an unknown kind was encountered, or a cycle short-circuited resolution.
|
|
15
|
+
* Callers must treat unbounded unions as requiring a catch-all entry. */
|
|
16
|
+
unbounded: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResolveCtx {
|
|
20
|
+
allManifests: ResourceManifest[];
|
|
21
|
+
defs: DefinitionRegistry;
|
|
22
|
+
aliases: AliasResolver;
|
|
23
|
+
memo: Map<string, ThrowsUnion>;
|
|
24
|
+
inProgress: Set<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createResolveCtx(
|
|
28
|
+
allManifests: ResourceManifest[],
|
|
29
|
+
defs: DefinitionRegistry,
|
|
30
|
+
aliases: AliasResolver,
|
|
31
|
+
): ResolveCtx {
|
|
32
|
+
return {
|
|
33
|
+
allManifests,
|
|
34
|
+
defs,
|
|
35
|
+
aliases,
|
|
36
|
+
memo: new Map(),
|
|
37
|
+
inProgress: new Set(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function emptyUnion(): ThrowsUnion {
|
|
42
|
+
return { codes: new Map(), unbounded: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function unionInto(target: ThrowsUnion, src: ThrowsUnion): void {
|
|
46
|
+
for (const [code, meta] of src.codes) {
|
|
47
|
+
if (!target.codes.has(code)) target.codes.set(code, meta);
|
|
48
|
+
}
|
|
49
|
+
if (src.unbounded) target.unbounded = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function definitionFor(
|
|
53
|
+
kind: string,
|
|
54
|
+
defs: DefinitionRegistry,
|
|
55
|
+
aliases: AliasResolver,
|
|
56
|
+
): ResourceDefinition | undefined {
|
|
57
|
+
const resolved = aliases.resolveKind(kind);
|
|
58
|
+
return defs.resolve(kind) ?? (resolved ? defs.resolve(resolved) : undefined);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function codesFromDefinition(definition: ResourceDefinition): Map<string, ThrowsCodeMeta> {
|
|
62
|
+
const out = new Map<string, ThrowsCodeMeta>();
|
|
63
|
+
const raw = definition.throws?.codes ?? {};
|
|
64
|
+
for (const [code, meta] of Object.entries(raw)) {
|
|
65
|
+
out.set(code, { data: (meta as { data?: Record<string, any> }).data });
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve the effective throw union for a named manifest. The result combines
|
|
71
|
+
* explicit `throws.codes`, `throws.inherit: true` dataflow (step-context
|
|
72
|
+
* traversal with try/catch subtraction), and unbounded markers for
|
|
73
|
+
* unresolvable passthrough call sites. Cycles short-circuit to an empty
|
|
74
|
+
* result so resolution always terminates. */
|
|
75
|
+
export function resolveThrowsUnion(
|
|
76
|
+
manifest: ResourceManifest,
|
|
77
|
+
ctx: ResolveCtx,
|
|
78
|
+
): ThrowsUnion {
|
|
79
|
+
const name = manifest.metadata?.name as string | undefined;
|
|
80
|
+
|
|
81
|
+
if (name) {
|
|
82
|
+
const cached = ctx.memo.get(name);
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
if (ctx.inProgress.has(name)) return emptyUnion();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases);
|
|
88
|
+
if (!definition) {
|
|
89
|
+
const u: ThrowsUnion = { codes: new Map(), unbounded: true };
|
|
90
|
+
if (name) ctx.memo.set(name, u);
|
|
91
|
+
return u;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const throws = definition.throws;
|
|
95
|
+
if (!throws) {
|
|
96
|
+
const u = emptyUnion();
|
|
97
|
+
if (name) ctx.memo.set(name, u);
|
|
98
|
+
return u;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (name) ctx.inProgress.add(name);
|
|
102
|
+
try {
|
|
103
|
+
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
104
|
+
|
|
105
|
+
for (const [code, meta] of codesFromDefinition(definition)) {
|
|
106
|
+
result.codes.set(code, meta);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (throws.passthrough) {
|
|
110
|
+
// Definition-level passthrough can't be resolved without a call site.
|
|
111
|
+
// resolveStepInvokeThrows handles passthrough call sites directly.
|
|
112
|
+
result.unbounded = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (throws.inherit) {
|
|
116
|
+
const inherited = resolveInherited(manifest, definition, ctx);
|
|
117
|
+
unionInto(result, inherited);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (name) ctx.memo.set(name, result);
|
|
121
|
+
return result;
|
|
122
|
+
} finally {
|
|
123
|
+
if (name) ctx.inProgress.delete(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveInherited(
|
|
128
|
+
manifest: ResourceManifest,
|
|
129
|
+
definition: ResourceDefinition,
|
|
130
|
+
ctx: ResolveCtx,
|
|
131
|
+
): ThrowsUnion {
|
|
132
|
+
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
133
|
+
const props = definition.schema?.properties as Record<string, any> | undefined;
|
|
134
|
+
if (!props) return result;
|
|
135
|
+
|
|
136
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
137
|
+
const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
|
|
138
|
+
if (!stepCtx?.invoke) continue;
|
|
139
|
+
const steps = (manifest as Record<string, any>)[fieldName];
|
|
140
|
+
if (!Array.isArray(steps)) continue;
|
|
141
|
+
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function collectStepArrayThrows(
|
|
148
|
+
steps: unknown[],
|
|
149
|
+
invokeField: string,
|
|
150
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
151
|
+
ctx: ResolveCtx,
|
|
152
|
+
): ThrowsUnion {
|
|
153
|
+
const result = emptyUnion();
|
|
154
|
+
for (const step of steps) {
|
|
155
|
+
if (!step || typeof step !== "object") continue;
|
|
156
|
+
unionInto(
|
|
157
|
+
result,
|
|
158
|
+
collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Walk one step, dispatching by shape. Generic for any Run.Sequence-style
|
|
165
|
+
* composer: the step keys it recognises (`try` / `catch` / `finally` / `then`
|
|
166
|
+
* / `else` / `elseif` / `do` / `cases` / `default`) are the same set already
|
|
167
|
+
* traversed by the analyzer's `x-telo-step-context` schema builder, so future
|
|
168
|
+
* composers that reuse those shape conventions work without changes here. */
|
|
169
|
+
function collectStepThrows(
|
|
170
|
+
step: Record<string, any>,
|
|
171
|
+
invokeField: string,
|
|
172
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
173
|
+
ctx: ResolveCtx,
|
|
174
|
+
): ThrowsUnion {
|
|
175
|
+
if (step[invokeField]) {
|
|
176
|
+
return resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (step.throw && typeof step.throw === "object") {
|
|
180
|
+
return resolveThrowStepCode(step.throw as Record<string, any>, enclosingTryCodes);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Array.isArray(step.try)) {
|
|
184
|
+
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
|
|
185
|
+
let propagated: ThrowsUnion;
|
|
186
|
+
if (Array.isArray(step.catch)) {
|
|
187
|
+
// Catch absorbs the try block's codes; the catch's own throws propagate
|
|
188
|
+
// out instead. Sequence-specific subtraction — the plan explicitly
|
|
189
|
+
// anchors this to Run.Sequence's try/catch schema shape.
|
|
190
|
+
const tryCodes = new Set(tryUnion.codes.keys());
|
|
191
|
+
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
192
|
+
// Unbounded in the try block still signals the caller to expect
|
|
193
|
+
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
194
|
+
if (tryUnion.unbounded) propagated.unbounded = true;
|
|
195
|
+
} else {
|
|
196
|
+
propagated = cloneUnion(tryUnion);
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(step.finally)) {
|
|
199
|
+
unionInto(
|
|
200
|
+
propagated,
|
|
201
|
+
collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return propagated;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(step.then)) {
|
|
208
|
+
const result = emptyUnion();
|
|
209
|
+
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
|
|
210
|
+
if (Array.isArray(step.else)) {
|
|
211
|
+
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(step.elseif)) {
|
|
214
|
+
for (const branch of step.elseif) {
|
|
215
|
+
if (Array.isArray(branch?.then)) {
|
|
216
|
+
unionInto(
|
|
217
|
+
result,
|
|
218
|
+
collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(step.do)) {
|
|
227
|
+
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (step.cases && typeof step.cases === "object") {
|
|
231
|
+
const result = emptyUnion();
|
|
232
|
+
for (const arr of Object.values(step.cases as Record<string, unknown>)) {
|
|
233
|
+
if (Array.isArray(arr)) {
|
|
234
|
+
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(step.default)) {
|
|
238
|
+
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return emptyUnion();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function cloneUnion(u: ThrowsUnion): ThrowsUnion {
|
|
247
|
+
const out = emptyUnion();
|
|
248
|
+
for (const [c, m] of u.codes) out.codes.set(c, m);
|
|
249
|
+
out.unbounded = u.unbounded;
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resolveStepInvokeThrows(
|
|
254
|
+
step: Record<string, any>,
|
|
255
|
+
invokeField: string,
|
|
256
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
257
|
+
ctx: ResolveCtx,
|
|
258
|
+
): ThrowsUnion {
|
|
259
|
+
const invokeRef = step[invokeField];
|
|
260
|
+
if (!invokeRef || typeof invokeRef !== "object") return emptyUnion();
|
|
261
|
+
const invokedKind = invokeRef.kind as string | undefined;
|
|
262
|
+
if (!invokedKind) return emptyUnion();
|
|
263
|
+
|
|
264
|
+
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases);
|
|
265
|
+
if (!definition) return { codes: new Map(), unbounded: true };
|
|
266
|
+
|
|
267
|
+
if (definition.throws?.passthrough) {
|
|
268
|
+
return resolvePassthroughAtCallSite(step, enclosingTryCodes);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
272
|
+
const invokeName = invokeRef.name as string | undefined;
|
|
273
|
+
if (invokeName) {
|
|
274
|
+
const target = ctx.allManifests.find(
|
|
275
|
+
(m) =>
|
|
276
|
+
m.metadata?.name === invokeName &&
|
|
277
|
+
(m.kind === invokedKind ||
|
|
278
|
+
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
279
|
+
m.kind === ctx.aliases.resolveKind(invokedKind)),
|
|
280
|
+
);
|
|
281
|
+
if (target) return resolveThrowsUnion(target, ctx);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Fall back to the definition's own explicit codes. Mark unbounded when the
|
|
285
|
+
// definition depends on call-site or transitive resolution we couldn't
|
|
286
|
+
// perform (no specific target manifest to recurse into).
|
|
287
|
+
const codes = codesFromDefinition(definition);
|
|
288
|
+
const unbounded =
|
|
289
|
+
definition.throws?.inherit === true || definition.throws?.passthrough === true;
|
|
290
|
+
return { codes, unbounded };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Resolve a passthrough-style invocable at a specific call site. Recognised forms
|
|
294
|
+
* (see "passthrough: true" in the plan):
|
|
295
|
+
* - constant literal (no template) → `{ <literal> }`
|
|
296
|
+
* - `${{ 'FOO' }}` constant expression → `{ FOO }`
|
|
297
|
+
* - `${{ error.code }}` inside a catch → enclosing try's propagated union
|
|
298
|
+
* Anything else is unbounded; the analyzer flags it downstream. */
|
|
299
|
+
function resolvePassthroughAtCallSite(
|
|
300
|
+
step: Record<string, any>,
|
|
301
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
302
|
+
): ThrowsUnion {
|
|
303
|
+
return resolveCodeExpression(step.inputs?.code, enclosingTryCodes);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Resolve the `code:` of a `throw:` step to a throws union. Uses the same
|
|
307
|
+
* recognised forms as passthrough call sites. */
|
|
308
|
+
function resolveThrowStepCode(
|
|
309
|
+
throwSpec: Record<string, any>,
|
|
310
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
311
|
+
): ThrowsUnion {
|
|
312
|
+
return resolveCodeExpression(throwSpec.code, enclosingTryCodes);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resolveCodeExpression(
|
|
316
|
+
codeInput: unknown,
|
|
317
|
+
enclosingTryCodes: Set<string> | undefined,
|
|
318
|
+
): ThrowsUnion {
|
|
319
|
+
if (typeof codeInput !== "string" || codeInput.length === 0) {
|
|
320
|
+
return { codes: new Map(), unbounded: true };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const match = codeInput.match(/^\s*\$\{\{\s*([\s\S]+?)\s*\}\}\s*$/);
|
|
324
|
+
if (!match) {
|
|
325
|
+
return { codes: new Map([[codeInput, {}]]), unbounded: false };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const expr = match[1].trim();
|
|
329
|
+
const litMatch = expr.match(/^'([^']+)'$|^"([^"]+)"$/);
|
|
330
|
+
if (litMatch) {
|
|
331
|
+
const code = litMatch[1] ?? litMatch[2]!;
|
|
332
|
+
return { codes: new Map([[code, {}]]), unbounded: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (expr === "error.code") {
|
|
336
|
+
if (enclosingTryCodes) {
|
|
337
|
+
const codes = new Map<string, ThrowsCodeMeta>();
|
|
338
|
+
for (const c of enclosingTryCodes) codes.set(c, {});
|
|
339
|
+
return { codes, unbounded: false };
|
|
340
|
+
}
|
|
341
|
+
return { codes: new Map(), unbounded: true };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { codes: new Map(), unbounded: true };
|
|
345
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -74,6 +74,9 @@ export interface LoaderInitOptions {
|
|
|
74
74
|
includeRegistryAdapter?: boolean;
|
|
75
75
|
/** Base URL used by built-in RegistryAdapter when enabled. */
|
|
76
76
|
registryUrl?: string;
|
|
77
|
+
/** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
|
|
78
|
+
* omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
|
|
79
|
+
celHandlers?: import("./cel-environment.js").CelHandlers;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export interface AnalysisOptions {
|
|
@@ -13,7 +13,7 @@ export function resolveTypeFieldToSchema(
|
|
|
13
13
|
if (!value) return undefined;
|
|
14
14
|
|
|
15
15
|
if (typeof value === "string") {
|
|
16
|
-
// Named type reference — find a
|
|
16
|
+
// Named type reference — find a Telo.Type resource by name
|
|
17
17
|
const typeManifest = allManifests.find(
|
|
18
18
|
(m) =>
|
|
19
19
|
(m.metadata as any)?.name === value &&
|