@telorun/analyzer 0.1.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.
Files changed (82) hide show
  1. package/LICENSE +17 -0
  2. package/dist/adapters/http-adapter.d.ts +10 -0
  3. package/dist/adapters/http-adapter.d.ts.map +1 -0
  4. package/dist/adapters/http-adapter.js +17 -0
  5. package/dist/adapters/node-adapter.d.ts +15 -0
  6. package/dist/adapters/node-adapter.d.ts.map +1 -0
  7. package/dist/adapters/node-adapter.js +32 -0
  8. package/dist/adapters/registry-adapter.d.ts +11 -0
  9. package/dist/adapters/registry-adapter.d.ts.map +1 -0
  10. package/dist/adapters/registry-adapter.js +33 -0
  11. package/dist/alias-resolver.d.ts +12 -0
  12. package/dist/alias-resolver.d.ts.map +1 -0
  13. package/dist/alias-resolver.js +36 -0
  14. package/dist/analysis-registry.d.ts +29 -0
  15. package/dist/analysis-registry.d.ts.map +1 -0
  16. package/dist/analysis-registry.js +55 -0
  17. package/dist/analyzer.d.ts +14 -0
  18. package/dist/analyzer.d.ts.map +1 -0
  19. package/dist/analyzer.js +314 -0
  20. package/dist/builtins.d.ts +3 -0
  21. package/dist/builtins.d.ts.map +1 -0
  22. package/dist/builtins.js +109 -0
  23. package/dist/cel-environment.d.ts +12 -0
  24. package/dist/cel-environment.d.ts.map +1 -0
  25. package/dist/cel-environment.js +59 -0
  26. package/dist/definition-registry.d.ts +58 -0
  27. package/dist/definition-registry.d.ts.map +1 -0
  28. package/dist/definition-registry.js +155 -0
  29. package/dist/dependency-graph.d.ts +38 -0
  30. package/dist/dependency-graph.d.ts.map +1 -0
  31. package/dist/dependency-graph.js +155 -0
  32. package/dist/index.d.ts +9 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +7 -0
  35. package/dist/manifest-loader.d.ts +11 -0
  36. package/dist/manifest-loader.d.ts.map +1 -0
  37. package/dist/manifest-loader.js +194 -0
  38. package/dist/normalize-inline-resources.d.ts +22 -0
  39. package/dist/normalize-inline-resources.d.ts.map +1 -0
  40. package/dist/normalize-inline-resources.js +136 -0
  41. package/dist/precompile.d.ts +9 -0
  42. package/dist/precompile.d.ts.map +1 -0
  43. package/dist/precompile.js +51 -0
  44. package/dist/reference-field-map.d.ts +53 -0
  45. package/dist/reference-field-map.d.ts.map +1 -0
  46. package/dist/reference-field-map.js +107 -0
  47. package/dist/schema-compat.d.ts +42 -0
  48. package/dist/schema-compat.d.ts.map +1 -0
  49. package/dist/schema-compat.js +234 -0
  50. package/dist/scope-resolver.d.ts +5 -0
  51. package/dist/scope-resolver.d.ts.map +1 -0
  52. package/dist/scope-resolver.js +13 -0
  53. package/dist/types.d.ts +64 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +8 -0
  56. package/dist/validate-cel-context.d.ts +24 -0
  57. package/dist/validate-cel-context.d.ts.map +1 -0
  58. package/dist/validate-cel-context.js +136 -0
  59. package/dist/validate-references.d.ts +19 -0
  60. package/dist/validate-references.d.ts.map +1 -0
  61. package/dist/validate-references.js +275 -0
  62. package/package.json +34 -0
  63. package/src/adapters/http-adapter.ts +23 -0
  64. package/src/adapters/node-adapter.ts +38 -0
  65. package/src/adapters/registry-adapter.ts +43 -0
  66. package/src/alias-resolver.ts +37 -0
  67. package/src/analysis-registry.ts +68 -0
  68. package/src/analyzer.ts +399 -0
  69. package/src/builtins.ts +111 -0
  70. package/src/cel-environment.ts +70 -0
  71. package/src/definition-registry.ts +170 -0
  72. package/src/dependency-graph.ts +187 -0
  73. package/src/index.ts +17 -0
  74. package/src/manifest-loader.ts +203 -0
  75. package/src/normalize-inline-resources.ts +170 -0
  76. package/src/precompile.ts +54 -0
  77. package/src/reference-field-map.ts +147 -0
  78. package/src/schema-compat.ts +264 -0
  79. package/src/scope-resolver.ts +13 -0
  80. package/src/types.ts +68 -0
  81. package/src/validate-cel-context.ts +142 -0
  82. package/src/validate-references.ts +311 -0
@@ -0,0 +1,399 @@
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import { AliasResolver } from "./alias-resolver.js";
3
+ import { AnalysisRegistry } from "./analysis-registry.js";
4
+ import { buildTypedCelEnvironment, celEnvironment } from "./cel-environment.js";
5
+ import { DefinitionRegistry } from "./definition-registry.js";
6
+ import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
7
+ import { normalizeInlineResources } from "./normalize-inline-resources.js";
8
+ import {
9
+ type SchemaIssue,
10
+ celTypeSatisfiesJsonSchema,
11
+ substituteCelFields,
12
+ validateAgainstSchema,
13
+ } from "./schema-compat.js";
14
+ import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
15
+ import {
16
+ extractAccessChains,
17
+ pathMatchesScope,
18
+ validateChainAgainstSchema,
19
+ } from "./validate-cel-context.js";
20
+ import { validateReferences } from "./validate-references.js";
21
+
22
+ const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
23
+
24
+ function walkCelExpressions(
25
+ value: unknown,
26
+ path: string,
27
+ cb: (expr: string, path: string) => void,
28
+ ): void {
29
+ if (typeof value === "string") {
30
+ for (const m of value.matchAll(TEMPLATE_REGEX)) {
31
+ cb(m[1].trim(), path);
32
+ }
33
+ } else if (Array.isArray(value)) {
34
+ value.forEach((v, i) => walkCelExpressions(v, `${path}[${i}]`, cb));
35
+ } else if (value !== null && typeof value === "object") {
36
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
37
+ walkCelExpressions(v, path ? `${path}.${k}` : k, cb);
38
+ }
39
+ }
40
+ }
41
+
42
+ const SOURCE = "telo-analyzer";
43
+
44
+ /**
45
+ * Walk a JSON Schema tree and collect all `x-telo-context` annotations,
46
+ * returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
47
+ * the same format the analyzer uses for CEL context validation.
48
+ */
49
+ function extractContextsFromSchema(
50
+ schema: Record<string, any>,
51
+ path = "$",
52
+ ): Array<{ scope: string; schema: Record<string, any> }> {
53
+ if (!schema || typeof schema !== "object") return [];
54
+ const results: Array<{ scope: string; schema: Record<string, any> }> = [];
55
+
56
+ if (schema["x-telo-context"]) {
57
+ results.push({ scope: path, schema: schema["x-telo-context"] });
58
+ }
59
+
60
+ if (schema.properties) {
61
+ for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
62
+ results.push(...extractContextsFromSchema(value, `${path}.${key}`));
63
+ }
64
+ }
65
+
66
+ if (schema.items && typeof schema.items === "object") {
67
+ results.push(...extractContextsFromSchema(schema.items, `${path}[*]`));
68
+ }
69
+
70
+ for (const key of ["oneOf", "anyOf", "allOf"] as const) {
71
+ if (Array.isArray(schema[key])) {
72
+ for (const subschema of schema[key]) {
73
+ results.push(...extractContextsFromSchema(subschema, path));
74
+ }
75
+ }
76
+ }
77
+
78
+ return results;
79
+ }
80
+
81
+ const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
82
+ const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
83
+
84
+ /** Recursively walk `data`+`schema` together, type-checking every pure CEL template
85
+ * string via `env.check()`. Returns `SchemaIssue[]` for any type mismatches found. */
86
+ function collectCelTypeIssues(
87
+ data: unknown,
88
+ schema: Record<string, any>,
89
+ path: string,
90
+ definition: { schema?: Record<string, any> },
91
+ manifest: ResourceManifest,
92
+ baseEnv: ReturnType<typeof buildTypedCelEnvironment>,
93
+ ): SchemaIssue[] {
94
+ const issues: SchemaIssue[] = [];
95
+
96
+ if (typeof data === "string" && CEL_PURE_RE.test(data)) {
97
+ const exprMatch = data.match(CEL_EXPR_RE);
98
+ if (exprMatch) {
99
+ const expr = exprMatch[1].trim();
100
+
101
+ // Merge x-telo-context variables for this path if applicable
102
+ let typedEnv = baseEnv;
103
+ if (definition.schema) {
104
+ for (const ctx of extractContextsFromSchema(definition.schema)) {
105
+ if (!pathMatchesScope(path, ctx.scope)) continue;
106
+ typedEnv = buildTypedCelEnvironment(manifest, ctx.schema);
107
+ break;
108
+ }
109
+ }
110
+
111
+ let checkResult: ReturnType<typeof typedEnv.check> | undefined;
112
+ try {
113
+ checkResult = typedEnv.check(expr);
114
+ } catch {
115
+ /* degrade gracefully */
116
+ }
117
+
118
+ if (checkResult?.valid && checkResult.type && schema) {
119
+ const celType = checkResult.type.split("<")[0]!;
120
+ if (!celTypeSatisfiesJsonSchema(celType, schema)) {
121
+ issues.push({
122
+ message: `CEL returns '${checkResult.type}' but field expects '${schema.type ?? "unknown"}'`,
123
+ path,
124
+ });
125
+ }
126
+ }
127
+ }
128
+ return issues;
129
+ }
130
+
131
+ if (Array.isArray(data)) {
132
+ const itemSchema = (schema.items ?? {}) as Record<string, any>;
133
+ for (let i = 0; i < data.length; i++) {
134
+ issues.push(
135
+ ...collectCelTypeIssues(data[i], itemSchema, `${path}[${i}]`, definition, manifest, baseEnv),
136
+ );
137
+ }
138
+ } else if (data !== null && typeof data === "object") {
139
+ const props = (schema.properties ?? {}) as Record<string, any>;
140
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
141
+ issues.push(
142
+ ...collectCelTypeIssues(
143
+ v,
144
+ (props[k] ?? {}) as Record<string, any>,
145
+ path ? `${path}.${k}` : k,
146
+ definition,
147
+ manifest,
148
+ baseEnv,
149
+ ),
150
+ );
151
+ }
152
+ }
153
+
154
+ return issues;
155
+ }
156
+
157
+ export class StaticAnalyzer {
158
+ analyze(
159
+ manifests: ResourceManifest[],
160
+ options?: AnalysisOptions,
161
+ registry?: AnalysisRegistry,
162
+ ): AnalysisDiagnostic[] {
163
+ const diagnostics: AnalysisDiagnostic[] = [];
164
+
165
+ // Use pre-seeded registries from the provided AnalysisRegistry, or create fresh ones.
166
+ // New aliases/definitions found in the manifests are accumulated into the provided instance
167
+ // so state builds up across successive calls (e.g. incremental editor validation).
168
+ const ctx = registry?._context();
169
+ const aliases = ctx?.aliases ?? new AliasResolver();
170
+ const defs = ctx?.definitions ?? new DefinitionRegistry();
171
+
172
+ // Register module identities and aliases.
173
+ // The root Kernel.Module provides its own identity; imported modules surface their
174
+ // identity via resolvedModuleName/resolvedNamespace stamped onto the Kernel.Import
175
+ // by the loader (so we don't need to include imported Kernel.Module manifests in
176
+ // the analysis set, avoiding false reference errors in the parent context).
177
+ for (const m of manifests) {
178
+ if (m.kind === "Kernel.Module") {
179
+ const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
180
+ const moduleName = m.metadata.name as string;
181
+ if (moduleName) defs.registerModuleIdentity(namespace, moduleName);
182
+ }
183
+ if (m.kind === "Kernel.Import") {
184
+ const alias = m.metadata.name as string;
185
+ const source = (m as any).source as string | undefined;
186
+ const exportedKinds: string[] = (m as any).exports?.kinds ?? [];
187
+ const resolvedModuleName = (m.metadata as any).resolvedModuleName as string | undefined;
188
+ const resolvedNamespace = (m.metadata as any).resolvedNamespace as
189
+ | string
190
+ | null
191
+ | undefined;
192
+ if (alias && source) {
193
+ const targetModule =
194
+ resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
195
+ aliases.registerImport(alias, targetModule, exportedKinds);
196
+ if (resolvedModuleName) {
197
+ defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ // Register definitions from Kernel.Definition resources.
204
+ // Normalize alias-prefixed `capability` to canonical form so extendedBy lookup works
205
+ // (e.g. "Workflow.Backend" → "workflow.Backend" when "Workflow" is a known alias).
206
+ for (const m of manifests) {
207
+ if (m.kind === "Kernel.Definition") {
208
+ const def = m as unknown as ResourceDefinition;
209
+ const resolvedCapability = def.capability
210
+ ? (aliases.resolveKind(def.capability) ?? def.capability)
211
+ : def.capability;
212
+ defs.register(
213
+ resolvedCapability !== def.capability ? { ...def, capability: resolvedCapability } : def,
214
+ );
215
+ }
216
+ }
217
+
218
+ // Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
219
+ const allManifests = normalizeInlineResources(manifests, defs, aliases);
220
+
221
+ // Build a name→manifest map for looking up referenced resources
222
+ const byName = new Map<string, ResourceManifest>();
223
+ for (const m of allManifests) {
224
+ if (m.metadata?.name) {
225
+ byName.set(m.metadata.name as string, m);
226
+ }
227
+ }
228
+
229
+ // Validate each non-definition, non-system resource
230
+ for (const m of allManifests) {
231
+ if (!m.kind || !m.metadata?.name) {
232
+ diagnostics.push({
233
+ severity: DiagnosticSeverity.Error,
234
+ code: "MISSING_KIND_OR_NAME",
235
+ source: SOURCE,
236
+ message: "Resource is missing required 'kind' or 'metadata.name' field.",
237
+ data: { path: !m.kind ? "kind" : "metadata.name" },
238
+ });
239
+ continue;
240
+ }
241
+ if (m.kind === "Kernel.Definition" || m.kind === "Kernel.Abstract") {
242
+ continue;
243
+ }
244
+
245
+ const resource = { kind: m.kind, name: m.metadata?.name as string };
246
+
247
+ // Resolve kind through alias if needed; direct lookup takes priority so that
248
+ // aliases whose name matches the module name (the common case) work without
249
+ // path-derived name mangling.
250
+ const resolvedKind = aliases.resolveKind(m.kind);
251
+ const definition =
252
+ defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
253
+ if (!definition) {
254
+ const knownAliases = aliases.knownAliases();
255
+ const knownKinds = defs.kinds();
256
+ const parts: string[] = [];
257
+ if (knownAliases.length > 0) parts.push(`imports: ${knownAliases.join(", ")}`);
258
+ if (knownKinds.length > 0) parts.push(`kinds: ${knownKinds.join(", ")}`);
259
+ const hint = parts.length > 0 ? ` Known ${parts.join(" | ")}` : "";
260
+ diagnostics.push({
261
+ severity: DiagnosticSeverity.Error,
262
+ code: "UNDEFINED_KIND",
263
+ source: SOURCE,
264
+ message: `No Kernel.Definition found for kind '${m.kind}'.${hint}`,
265
+ data: { resource, path: "kind" },
266
+ });
267
+ continue;
268
+ }
269
+
270
+ // Validate resource config against definition schema.
271
+ // `kind` and `metadata` are implicit on every resource — inject them so module
272
+ // authors don't have to repeat them when using additionalProperties: false.
273
+ if (definition.schema) {
274
+ const schema =
275
+ definition.schema.additionalProperties === false
276
+ ? {
277
+ ...definition.schema,
278
+ properties: {
279
+ kind: { type: "string" },
280
+ metadata: { type: "object" },
281
+ ...definition.schema.properties,
282
+ },
283
+ }
284
+ : definition.schema;
285
+ // Phase 1: CEL type checking — walk data+schema together, check env.check() return types
286
+ const baseEnv = buildTypedCelEnvironment(m);
287
+ const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseEnv);
288
+ // Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
289
+ const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
290
+ const issues = [...celIssues, ...ajvIssues];
291
+ for (const issue of issues) {
292
+ diagnostics.push({
293
+ severity: DiagnosticSeverity.Error,
294
+ code: "SCHEMA_VIOLATION",
295
+ source: SOURCE,
296
+ message: `${m.kind}/${resource.name}: ${issue.message}`,
297
+ data: { resource, path: issue.path },
298
+ });
299
+ }
300
+ }
301
+
302
+ // (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
303
+ }
304
+
305
+ // Validate CEL syntax and context variable access in all manifests
306
+ for (const m of allManifests) {
307
+ const resource = { kind: m.kind, name: m.metadata?.name as string };
308
+
309
+ const resolvedKind = aliases.resolveKind(m.kind);
310
+ const mDefinition =
311
+ defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
312
+
313
+ walkCelExpressions(m, "", (expr, path) => {
314
+ let parsed: ReturnType<typeof celEnvironment.parse> | undefined;
315
+ try {
316
+ parsed = celEnvironment.parse(expr);
317
+ } catch (e) {
318
+ diagnostics.push({
319
+ severity: DiagnosticSeverity.Error,
320
+ code: "CEL_SYNTAX_ERROR",
321
+ source: SOURCE,
322
+ message: `CEL syntax error at ${path}: ${e instanceof Error ? e.message : String(e)}`,
323
+ data: { resource, path },
324
+ });
325
+ return;
326
+ }
327
+
328
+ const contexts = mDefinition?.schema ? extractContextsFromSchema(mDefinition.schema) : [];
329
+ const invocationContext = (m.metadata as any)?.xTeloInvocationContext as
330
+ | Record<string, any>
331
+ | undefined;
332
+ if (contexts.length === 0 && !invocationContext) return;
333
+
334
+ let matchedContext: Record<string, any> | undefined;
335
+ for (const ctx of contexts) {
336
+ if (pathMatchesScope(path, ctx.scope)) {
337
+ matchedContext = ctx.schema;
338
+ break;
339
+ }
340
+ }
341
+ if (!matchedContext) matchedContext = invocationContext;
342
+ if (!matchedContext) return;
343
+
344
+ for (const chain of extractAccessChains(parsed.ast)) {
345
+ const err = validateChainAgainstSchema(chain, matchedContext);
346
+ if (!err) continue;
347
+ diagnostics.push({
348
+ severity: DiagnosticSeverity.Error,
349
+ code: "CEL_UNKNOWN_FIELD",
350
+ source: SOURCE,
351
+ message: `${m.kind}/${resource.name}: CEL at '${path}': ${err}`,
352
+ data: { resource, path },
353
+ });
354
+ }
355
+ });
356
+ }
357
+
358
+ // Validate resource references (Phase 3)
359
+ diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs }));
360
+
361
+ return diagnostics;
362
+ }
363
+
364
+ analyzeErrors(
365
+ manifests: ResourceManifest[],
366
+ options?: AnalysisOptions,
367
+ registry?: AnalysisRegistry,
368
+ ): AnalysisDiagnostic[] {
369
+ return this.analyze(manifests, options, registry).filter(
370
+ (d) => d.severity === DiagnosticSeverity.Error,
371
+ );
372
+ }
373
+
374
+ normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[] {
375
+ const ctx = registry._context();
376
+ return normalizeInlineResources(manifests, ctx.definitions!, ctx.aliases);
377
+ }
378
+
379
+ prepare(
380
+ manifests: ResourceManifest[],
381
+ registry: AnalysisRegistry,
382
+ ): { diagnostics: AnalysisDiagnostic[]; order: string[] | null; cycleError: string | null } {
383
+ const ctx = registry._context();
384
+ const diagnostics = validateReferences(manifests, ctx);
385
+ const errors = diagnostics.filter((d) => d.severity === DiagnosticSeverity.Error);
386
+ if (errors.length > 0) {
387
+ return { diagnostics: errors, order: null, cycleError: null };
388
+ }
389
+ const graph = buildDependencyGraph(manifests, ctx.definitions!, ctx.aliases);
390
+ if (graph.cycle) {
391
+ return { diagnostics: [], order: null, cycleError: formatCycle(graph.cycle) };
392
+ }
393
+ return {
394
+ diagnostics: [],
395
+ order: graph.order ? graph.order.map((n) => n.name) : null,
396
+ cycleError: null,
397
+ };
398
+ }
399
+ }
@@ -0,0 +1,111 @@
1
+ import type { ResourceDefinition } from "@telorun/sdk";
2
+
3
+ export const KERNEL_BUILTINS: ResourceDefinition[] = [
4
+ { kind: "Kernel.Abstract", metadata: { name: "Template", module: "Kernel" } },
5
+ { kind: "Kernel.Abstract", metadata: { name: "Runnable", module: "Kernel" } },
6
+ { kind: "Kernel.Abstract", metadata: { name: "Service", module: "Kernel" } },
7
+ { kind: "Kernel.Abstract", metadata: { name: "Invocable", module: "Kernel" } },
8
+ { kind: "Kernel.Abstract", metadata: { name: "Mount", module: "Kernel" } },
9
+ { kind: "Kernel.Abstract", metadata: { name: "Type", module: "Kernel" } },
10
+ {
11
+ kind: "Kernel.Abstract",
12
+ metadata: { name: "Provider", module: "Kernel" },
13
+ schema: { "x-telo-eval": "compile" },
14
+ },
15
+ {
16
+ kind: "Kernel.Definition",
17
+ metadata: { name: "Abstract", module: "Kernel" },
18
+ capability: "Kernel.Template",
19
+ schema: {
20
+ type: "object",
21
+ properties: {
22
+ kind: { type: "string" },
23
+ metadata: {
24
+ type: "object",
25
+ properties: { name: { type: "string" } },
26
+ required: ["name"],
27
+ additionalProperties: true,
28
+ },
29
+ capability: { type: "string" },
30
+ },
31
+ required: ["metadata"],
32
+ additionalProperties: false,
33
+ },
34
+ },
35
+ {
36
+ kind: "Kernel.Definition",
37
+ metadata: { name: "Definition", module: "Kernel" },
38
+ capability: "Kernel.Template",
39
+ schema: { type: "object" },
40
+ },
41
+ {
42
+ kind: "Kernel.Definition",
43
+ metadata: { name: "Import", module: "Kernel" },
44
+ capability: "Kernel.Template",
45
+ schema: {
46
+ type: "object",
47
+ properties: {
48
+ kind: { type: "string" },
49
+ metadata: {
50
+ type: "object",
51
+ properties: { name: { type: "string" } },
52
+ required: ["name"],
53
+ additionalProperties: true,
54
+ },
55
+ source: { type: "string" },
56
+ variables: { type: "object" },
57
+ secrets: { type: "object" },
58
+ },
59
+ required: ["metadata", "source"],
60
+ additionalProperties: false,
61
+ },
62
+ },
63
+ {
64
+ kind: "Kernel.Definition",
65
+ metadata: { name: "Module", module: "Kernel" },
66
+ capability: "Kernel.Template",
67
+ schema: {
68
+ type: "object",
69
+ properties: {
70
+ kind: { type: "string" },
71
+ metadata: {
72
+ type: "object",
73
+ properties: {
74
+ name: { type: "string" },
75
+ version: { type: "string" },
76
+ source: { type: "string" },
77
+ module: { type: "string" },
78
+ },
79
+ required: ["name"],
80
+ additionalProperties: true,
81
+ },
82
+ lifecycle: {
83
+ type: "string",
84
+ enum: ["shared", "isolated"],
85
+ default: "shared",
86
+ },
87
+ keepAlive: { type: "boolean", default: false },
88
+ variables: { type: "object" },
89
+ secrets: { type: "object" },
90
+ targets: {
91
+ type: "array",
92
+ items: {
93
+ anyOf: [
94
+ { type: "string", "x-telo-ref": "kernel#Runnable" },
95
+ { type: "string", "x-telo-ref": "kernel#Service" },
96
+ ],
97
+ },
98
+ },
99
+ exports: {
100
+ type: "object",
101
+ properties: {
102
+ kinds: { type: "array", items: { type: "string" } },
103
+ },
104
+ additionalProperties: true,
105
+ },
106
+ },
107
+ required: ["metadata"],
108
+ additionalProperties: false,
109
+ },
110
+ },
111
+ ];
@@ -0,0 +1,70 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import { Environment } from "@marcbachmann/cel-js";
3
+ import { jsonSchemaToCelType } from "./schema-compat.js";
4
+
5
+ export const celEnvironment = new Environment({ unlistedVariablesAreDyn: true })
6
+ .registerFunction("join(list, string): string", (list: unknown[], sep: string) =>
7
+ list.map(String).join(sep),
8
+ )
9
+ .registerFunction("keys(map): list", (map: unknown) => {
10
+ if (map instanceof Map) return [...map.keys()];
11
+ return Object.keys(map as Record<string, unknown>);
12
+ })
13
+ .registerFunction("values(map): list", (map: unknown) => {
14
+ if (map instanceof Map) return [...map.values()];
15
+ return Object.values(map as Record<string, unknown>);
16
+ });
17
+
18
+ /** Clone `celEnvironment` and register typed variable declarations so that
19
+ * `env.check(expr)` can infer return types for expressions referencing known variables.
20
+ *
21
+ * - `variables`: typed from the manifest's `variables` field if it is a schema map
22
+ * (only `Kernel.Module` resources carry this); otherwise registered as `map` (dyn).
23
+ * - `secrets`, `resources`, `imports`, `env`: always `map` (dyn — output schemas unknown).
24
+ * - `extraContextSchema`: additional variables from an `x-telo-context` annotation. */
25
+ export function buildTypedCelEnvironment(
26
+ manifest: ResourceManifest,
27
+ extraContextSchema?: Record<string, any> | null,
28
+ ): Environment {
29
+ try {
30
+ const env = celEnvironment.clone();
31
+
32
+ // Build typed ObjectSchema from manifest.variables if it looks like a schema map
33
+ const vars = (manifest as Record<string, unknown>).variables;
34
+ if (vars !== null && typeof vars === "object" && !Array.isArray(vars)) {
35
+ const entries = Object.entries(vars as Record<string, unknown>).filter(
36
+ ([, v]) => v !== null && typeof v === "object" && !Array.isArray(v),
37
+ );
38
+ if (entries.length > 0) {
39
+ const schema: Record<string, string> = {};
40
+ for (const [k, v] of entries) {
41
+ schema[k] = jsonSchemaToCelType(v as Record<string, any>);
42
+ }
43
+ (env as any).registerVariable({ name: "variables", schema });
44
+ } else {
45
+ env.registerVariable("variables", "map");
46
+ }
47
+ } else {
48
+ env.registerVariable("variables", "map");
49
+ }
50
+
51
+ env.registerVariable("secrets", "map");
52
+ env.registerVariable("resources", "map");
53
+ env.registerVariable("imports", "map");
54
+ env.registerVariable("env", "map");
55
+
56
+ if (extraContextSchema?.properties) {
57
+ for (const [name, propSchema] of Object.entries(
58
+ extraContextSchema.properties as Record<string, any>,
59
+ )) {
60
+ if (!env.hasVariable(name)) {
61
+ env.registerVariable(name, jsonSchemaToCelType(propSchema as Record<string, any>));
62
+ }
63
+ }
64
+ }
65
+
66
+ return env;
67
+ } catch {
68
+ return celEnvironment.clone();
69
+ }
70
+ }