@telorun/analyzer 0.1.3 → 0.2.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 (65) hide show
  1. package/README.md +3 -3
  2. package/dist/analyzer.d.ts +6 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +45 -25
  5. package/dist/builtins.d.ts.map +1 -1
  6. package/dist/builtins.js +56 -24
  7. package/dist/cel-environment.d.ts +12 -5
  8. package/dist/cel-environment.d.ts.map +1 -1
  9. package/dist/cel-environment.js +31 -17
  10. package/dist/definition-registry.d.ts +5 -5
  11. package/dist/definition-registry.d.ts.map +1 -1
  12. package/dist/definition-registry.js +10 -10
  13. package/dist/dependency-graph.d.ts.map +1 -1
  14. package/dist/dependency-graph.js +9 -2
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/kernel-globals.d.ts +6 -2
  19. package/dist/kernel-globals.d.ts.map +1 -1
  20. package/dist/kernel-globals.js +14 -8
  21. package/dist/manifest-loader.d.ts +9 -1
  22. package/dist/manifest-loader.d.ts.map +1 -1
  23. package/dist/manifest-loader.js +165 -11
  24. package/dist/module-kinds.d.ts +4 -0
  25. package/dist/module-kinds.d.ts.map +1 -0
  26. package/dist/module-kinds.js +4 -0
  27. package/dist/normalize-inline-resources.d.ts.map +1 -1
  28. package/dist/normalize-inline-resources.js +6 -1
  29. package/dist/precompile.d.ts +3 -2
  30. package/dist/precompile.d.ts.map +1 -1
  31. package/dist/precompile.js +13 -11
  32. package/dist/reference-field-map.d.ts +1 -1
  33. package/dist/resolve-throws-union.d.ts +30 -0
  34. package/dist/resolve-throws-union.d.ts.map +1 -0
  35. package/dist/resolve-throws-union.js +252 -0
  36. package/dist/types.d.ts +11 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/validate-cel-context.js +1 -1
  39. package/dist/validate-references.d.ts.map +1 -1
  40. package/dist/validate-references.js +19 -12
  41. package/dist/validate-throws-coverage.d.ts +8 -0
  42. package/dist/validate-throws-coverage.d.ts.map +1 -0
  43. package/dist/validate-throws-coverage.js +461 -0
  44. package/package.json +3 -3
  45. package/src/analyzer.ts +60 -26
  46. package/src/builtins.ts +56 -24
  47. package/src/cel-environment.ts +40 -17
  48. package/src/definition-registry.ts +10 -10
  49. package/src/dependency-graph.ts +9 -2
  50. package/src/index.ts +2 -1
  51. package/src/kernel-globals.ts +19 -10
  52. package/src/manifest-loader.ts +202 -17
  53. package/src/module-kinds.ts +6 -0
  54. package/src/normalize-inline-resources.ts +6 -1
  55. package/src/precompile.ts +14 -11
  56. package/src/reference-field-map.ts +1 -1
  57. package/src/resolve-throws-union.ts +345 -0
  58. package/src/types.ts +13 -0
  59. package/src/validate-cel-context.ts +1 -1
  60. package/src/validate-references.ts +19 -12
  61. package/src/validate-throws-coverage.ts +565 -0
  62. package/dist/adapters/node-adapter.d.ts +0 -15
  63. package/dist/adapters/node-adapter.d.ts.map +0 -1
  64. package/dist/adapters/node-adapter.js +0 -33
  65. package/src/adapters/node-adapter.ts +0 -38
@@ -1,16 +1,27 @@
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
- import type {
7
- LoadOptions,
8
- LoaderInitOptions,
9
- ManifestAdapter,
10
- Position,
11
- PositionIndex,
9
+ import {
10
+ DEFAULT_MANIFEST_FILENAME,
11
+ type LoadOptions,
12
+ type LoaderInitOptions,
13
+ type ManifestAdapter,
14
+ type Position,
15
+ type PositionIndex,
12
16
  } from "./types.js";
13
17
 
18
+ const SYSTEM_KINDS = new Set([
19
+ "Telo.Application",
20
+ "Telo.Library",
21
+ "Telo.Import",
22
+ "Telo.Definition",
23
+ ]);
24
+
14
25
  export class Loader {
15
26
  private static readonly moduleCache = new Map<
16
27
  string,
@@ -18,6 +29,7 @@ export class Loader {
18
29
  >();
19
30
 
20
31
  protected adapters: ManifestAdapter[];
32
+ private readonly celEnv: Environment;
21
33
 
22
34
  constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
23
35
  const options: LoaderInitOptions = Array.isArray(extraAdaptersOrOptions)
@@ -34,6 +46,8 @@ export class Loader {
34
46
  if (options.extraAdapters?.length) {
35
47
  this.adapters.unshift(...options.extraAdapters);
36
48
  }
49
+
50
+ this.celEnv = buildCelEnvironment(options.celHandlers);
37
51
  }
38
52
 
39
53
  register(adapter: ManifestAdapter): this {
@@ -76,7 +90,7 @@ export class Loader {
76
90
  let compiledDocs: unknown[];
77
91
  if (options?.compile) {
78
92
  try {
79
- const result = precompileDoc(rawDoc);
93
+ const result = precompileDoc(rawDoc, this.celEnv);
80
94
  compiledDocs = Array.isArray(result) ? result : [result];
81
95
  } catch (error) {
82
96
  throw new Error(
@@ -103,17 +117,19 @@ export class Loader {
103
117
  }
104
118
  }
105
119
 
106
- const moduleManifests = resolved.filter((m) => m.kind === "Kernel.Module");
120
+ const moduleManifests = resolved.filter((m) => isModuleKind(m.kind));
107
121
  if (moduleManifests.length > 1) {
122
+ const kinds = moduleManifests.map((m) => m.kind).join(", ");
108
123
  throw new Error(
109
- `File '${source}' contains ${moduleManifests.length} Kernel.Module declarations. Maximum one is allowed.`,
124
+ `File '${source}' contains ${moduleManifests.length} module declarations (${kinds}). ` +
125
+ `A file may declare at most one Telo.Application or Telo.Library.`,
110
126
  );
111
127
  }
112
128
  const moduleManifest = moduleManifests[0];
113
129
  const moduleName = moduleManifest?.metadata?.name as string | undefined;
114
130
  if (moduleName) {
115
131
  for (const manifest of resolved) {
116
- if (manifest.kind !== "Kernel.Module" && !manifest.metadata?.module) {
132
+ if (!isModuleKind(manifest.kind) && !manifest.metadata?.module) {
117
133
  const pi = (manifest.metadata as any)?.positionIndex;
118
134
  manifest.metadata = { ...manifest.metadata, module: moduleName };
119
135
  if (pi) {
@@ -128,10 +144,152 @@ export class Loader {
128
144
  }
129
145
  }
130
146
 
131
- Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
147
+ // Expand include directives load partial files into the same module scope.
148
+ // Results with includes are NOT cached because partial file content is not
149
+ // tracked in the cache key — the cache would serve stale data if a partial changes.
150
+ let hasIncludes = false;
151
+ if (moduleManifest) {
152
+ const includePatterns = (moduleManifest as any).include as string[] | undefined;
153
+ if (includePatterns?.length) {
154
+ hasIncludes = true;
155
+ const adapter = this.pick(source);
156
+ const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
157
+ for (const includedUrl of includedFiles) {
158
+ const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
159
+ resolved.push(...partialManifests);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (!hasIncludes) {
165
+ Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
166
+ }
132
167
  return cloneManifestArray(resolved);
133
168
  }
134
169
 
170
+ private async resolveIncludes(
171
+ ownerSource: string,
172
+ patterns: string[],
173
+ adapter: ManifestAdapter,
174
+ ): Promise<string[]> {
175
+ const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
176
+ if (hasGlobs) {
177
+ if (!adapter.expandGlob) {
178
+ throw new Error(
179
+ `Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
180
+ `does not support glob expansion. Use explicit file paths instead of patterns like: ` +
181
+ patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "),
182
+ );
183
+ }
184
+ return adapter.expandGlob(ownerSource, patterns);
185
+ }
186
+ // Literal relative paths — deduplicate in case the same file appears under multiple patterns.
187
+ return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
188
+ }
189
+
190
+ private async loadPartialFile(
191
+ url: string,
192
+ ownerModuleName: string | undefined,
193
+ options?: LoadOptions,
194
+ ): Promise<ResourceManifest[]> {
195
+ const { text, source } = await this.pick(url).read(url);
196
+
197
+ const parsedDocuments = parseAllDocuments(text);
198
+ const rawDocs = parsedDocuments.map((d) => d.toJSON());
199
+ const offsets = documentLineOffsets(text);
200
+ const lineOffsets = buildLineOffsets(text);
201
+ const resolved: ResourceManifest[] = [];
202
+ let docIdx = 0;
203
+
204
+ for (const rawDoc of rawDocs) {
205
+ const currentDocIdx = docIdx++;
206
+ const sourceLine = offsets[currentDocIdx] ?? 0;
207
+ const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
208
+ if (rawDoc === null || rawDoc === undefined) continue;
209
+
210
+ const kind = rawDoc.kind as string | undefined;
211
+ if (kind && SYSTEM_KINDS.has(kind)) {
212
+ throw new Error(
213
+ `Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
214
+ `Only the owner telo.yaml may declare ${kind} resources.`,
215
+ );
216
+ }
217
+
218
+ let compiledDocs: unknown[];
219
+ if (options?.compile) {
220
+ try {
221
+ const result = precompileDoc(rawDoc, this.celEnv);
222
+ compiledDocs = Array.isArray(result) ? result : [result];
223
+ } catch (error) {
224
+ throw new Error(
225
+ `Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`,
226
+ );
227
+ }
228
+ } else {
229
+ compiledDocs = [rawDoc];
230
+ }
231
+
232
+ for (const doc of compiledDocs) {
233
+ if (doc === null || doc === undefined) continue;
234
+ const manifest = doc as ResourceManifest;
235
+ const metadata = {
236
+ ...manifest.metadata,
237
+ source,
238
+ sourceLine,
239
+ ...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
240
+ };
241
+ Object.defineProperty(metadata, "positionIndex", {
242
+ value: positionIndex,
243
+ enumerable: false,
244
+ writable: true,
245
+ configurable: true,
246
+ });
247
+ resolved.push({ ...manifest, metadata });
248
+ }
249
+ }
250
+
251
+ return resolved;
252
+ }
253
+
254
+ async loadModuleForFile(
255
+ fileUrl: string,
256
+ ): Promise<{
257
+ ownerUrl: string;
258
+ manifests: ResourceManifest[];
259
+ sourceManifests: Map<string, ResourceManifest[]>;
260
+ } | null> {
261
+ // Try loading as a regular module first (it might be a telo.yaml itself).
262
+ // Use loadManifests (not loadModule) so imported definitions are included —
263
+ // otherwise the analyzer won't know about kinds from Telo.Import sources.
264
+ try {
265
+ const docs = await this.loadModule(fileUrl);
266
+ const hasModule = docs.some((d) => isModuleKind(d.kind));
267
+ if (hasModule) {
268
+ const { source } = await this.pick(fileUrl).read(fileUrl);
269
+ const manifests = await this.loadManifests(fileUrl);
270
+ return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
271
+ }
272
+ } catch (err) {
273
+ // If the file looks like an owner manifest (named telo.yaml), rethrow —
274
+ // a broken owner shouldn't silently fall through to parent lookup.
275
+ const normalized = fileUrl.replace(/\\/g, "/");
276
+ if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
277
+ throw err;
278
+ }
279
+ // Otherwise fall through to owner lookup — this is likely a partial file
280
+ }
281
+
282
+ // Find the owning telo.yaml via parent-directory traversal
283
+ const adapter = this.pick(fileUrl);
284
+ if (!adapter.resolveOwnerOf) return null;
285
+ const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
286
+ if (!ownerUrl) return null;
287
+
288
+ // Load the owner module (which will load included files via include expansion)
289
+ const manifests = await this.loadManifests(ownerUrl);
290
+ return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
291
+ }
292
+
135
293
  async loadModuleGraph(
136
294
  entryUrl: string,
137
295
  onError?: (url: string, error: Error) => void,
@@ -146,7 +304,7 @@ export class Loader {
146
304
 
147
305
  while (queue.length > 0) {
148
306
  const m = queue.shift()!;
149
- if (m.kind !== "Kernel.Import") continue;
307
+ if (m.kind !== "Telo.Import") continue;
150
308
  const importSource = (m as any).source as string | undefined;
151
309
  if (!importSource) continue;
152
310
  const base = (m.metadata as any)?.source ?? entryUrl;
@@ -166,7 +324,7 @@ export class Loader {
166
324
  }
167
325
  result.set(importUrl, imported);
168
326
  for (const im of imported) {
169
- if (im.kind === "Kernel.Import") queue.push(im);
327
+ if (im.kind === "Telo.Import") queue.push(im);
170
328
  }
171
329
  }
172
330
 
@@ -182,7 +340,7 @@ export class Loader {
182
340
 
183
341
  while (queue.length > 0) {
184
342
  const m = queue.shift()!;
185
- if (m.kind !== "Kernel.Import") continue;
343
+ if (m.kind !== "Telo.Import") continue;
186
344
  const importSource = (m as any).source as string | undefined;
187
345
  if (!importSource) continue;
188
346
  const base = (m.metadata as any)?.source ?? entryUrl;
@@ -200,7 +358,20 @@ export class Loader {
200
358
  (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
201
359
  throw e;
202
360
  }
203
- const importedModule = imported.find((im) => im.kind === "Kernel.Module");
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;
204
375
  if (importedModule?.metadata?.name) {
205
376
  const pi = (m.metadata as any)?.positionIndex;
206
377
  m.metadata = {
@@ -218,8 +389,8 @@ export class Loader {
218
389
  }
219
390
  }
220
391
  for (const im of imported) {
221
- if (im.kind === "Kernel.Definition") importedDefs.push(im);
222
- if (im.kind === "Kernel.Import") queue.push(im);
392
+ if (im.kind === "Telo.Definition") importedDefs.push(im);
393
+ if (im.kind === "Telo.Import") queue.push(im);
223
394
  }
224
395
  }
225
396
 
@@ -253,6 +424,20 @@ function cloneManifestValue<T>(value: T): T {
253
424
  return value;
254
425
  }
255
426
 
427
+ function groupBySource(manifests: ResourceManifest[]): Map<string, ResourceManifest[]> {
428
+ const map = new Map<string, ResourceManifest[]>();
429
+ for (const m of manifests) {
430
+ const src = (m.metadata?.source as string) ?? "unknown";
431
+ let list = map.get(src);
432
+ if (!list) {
433
+ list = [];
434
+ map.set(src, list);
435
+ }
436
+ list.push(m);
437
+ }
438
+ return map;
439
+ }
440
+
256
441
  function documentLineOffsets(text: string): number[] {
257
442
  const offsets = [0];
258
443
  const lines = text.split("\n");
@@ -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(["Kernel.Definition", "Kernel.Module", "Kernel.Import"]);
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 { celEnvironment } from "./cel-environment.js";
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
- * Kernel.Definition documents are returned unchanged — their schema fields
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 fn = celEnvironment.parse(exact[1].trim());
35
- return { __compiled: true, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue;
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 fn = celEnvironment.parse(m[1].trim());
44
- parts.push({ __compiled: true, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue);
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 "kernel#TypeName").
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 "[]"). */