@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/analyzer.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
3
|
import { AliasResolver } from "./alias-resolver.js";
|
|
3
4
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildCelEnvironment,
|
|
7
|
+
buildTypedCelEnvironment,
|
|
8
|
+
type CelHandlers,
|
|
9
|
+
} from "./cel-environment.js";
|
|
5
10
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
6
11
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
7
12
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
13
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
8
14
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
9
15
|
import {
|
|
10
16
|
celTypeSatisfiesJsonSchema,
|
|
@@ -22,6 +28,7 @@ import {
|
|
|
22
28
|
validateChainAgainstSchema,
|
|
23
29
|
} from "./validate-cel-context.js";
|
|
24
30
|
import { validateReferences } from "./validate-references.js";
|
|
31
|
+
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
25
32
|
|
|
26
33
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
27
34
|
|
|
@@ -174,7 +181,8 @@ function collectCelTypeIssues(
|
|
|
174
181
|
path: string,
|
|
175
182
|
definition: { schema?: Record<string, any> },
|
|
176
183
|
manifest: ResourceManifest,
|
|
177
|
-
|
|
184
|
+
baseTypedEnv: Environment,
|
|
185
|
+
rootEnv: Environment,
|
|
178
186
|
): SchemaIssue[] {
|
|
179
187
|
const issues: SchemaIssue[] = [];
|
|
180
188
|
|
|
@@ -184,11 +192,11 @@ function collectCelTypeIssues(
|
|
|
184
192
|
const expr = exprMatch[1].trim();
|
|
185
193
|
|
|
186
194
|
// Merge x-telo-context variables for this path if applicable
|
|
187
|
-
let typedEnv =
|
|
195
|
+
let typedEnv = baseTypedEnv;
|
|
188
196
|
if (definition.schema) {
|
|
189
197
|
for (const ctx of extractContextsFromSchema(definition.schema)) {
|
|
190
198
|
if (!pathMatchesScope(path, ctx.scope)) continue;
|
|
191
|
-
typedEnv = buildTypedCelEnvironment(manifest, ctx.schema);
|
|
199
|
+
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema);
|
|
192
200
|
break;
|
|
193
201
|
}
|
|
194
202
|
}
|
|
@@ -200,7 +208,15 @@ function collectCelTypeIssues(
|
|
|
200
208
|
/* degrade gracefully */
|
|
201
209
|
}
|
|
202
210
|
|
|
203
|
-
if (checkResult?.valid && checkResult.
|
|
211
|
+
if (checkResult?.valid === false && checkResult.error) {
|
|
212
|
+
// env.check() rejected the expression itself — e.g. wrong method, wrong
|
|
213
|
+
// argument types, wrong operator overload. Surface the first line of the
|
|
214
|
+
// error message; the tail is a source-code caret diagram we don't need.
|
|
215
|
+
const message = String((checkResult.error as { message?: string }).message ?? checkResult.error)
|
|
216
|
+
.split("\n")[0]
|
|
217
|
+
.trim();
|
|
218
|
+
issues.push({ message: `CEL type error: ${message}`, path });
|
|
219
|
+
} else if (checkResult?.valid && checkResult.type && schema) {
|
|
204
220
|
const celType = checkResult.type.split("<")[0]!;
|
|
205
221
|
if (!celTypeSatisfiesJsonSchema(celType, schema)) {
|
|
206
222
|
issues.push({
|
|
@@ -223,7 +239,8 @@ function collectCelTypeIssues(
|
|
|
223
239
|
`${path}[${i}]`,
|
|
224
240
|
definition,
|
|
225
241
|
manifest,
|
|
226
|
-
|
|
242
|
+
baseTypedEnv,
|
|
243
|
+
rootEnv,
|
|
227
244
|
),
|
|
228
245
|
);
|
|
229
246
|
}
|
|
@@ -237,7 +254,8 @@ function collectCelTypeIssues(
|
|
|
237
254
|
path ? `${path}.${k}` : k,
|
|
238
255
|
definition,
|
|
239
256
|
manifest,
|
|
240
|
-
|
|
257
|
+
baseTypedEnv,
|
|
258
|
+
rootEnv,
|
|
241
259
|
),
|
|
242
260
|
);
|
|
243
261
|
}
|
|
@@ -246,7 +264,17 @@ function collectCelTypeIssues(
|
|
|
246
264
|
return issues;
|
|
247
265
|
}
|
|
248
266
|
|
|
267
|
+
export interface StaticAnalyzerOptions {
|
|
268
|
+
celHandlers?: CelHandlers;
|
|
269
|
+
}
|
|
270
|
+
|
|
249
271
|
export class StaticAnalyzer {
|
|
272
|
+
private readonly celEnv: Environment;
|
|
273
|
+
|
|
274
|
+
constructor(options: StaticAnalyzerOptions = {}) {
|
|
275
|
+
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
analyze(
|
|
251
279
|
manifests: ResourceManifest[],
|
|
252
280
|
options?: AnalysisOptions,
|
|
@@ -262,17 +290,18 @@ export class StaticAnalyzer {
|
|
|
262
290
|
const defs = ctx?.definitions ?? new DefinitionRegistry();
|
|
263
291
|
|
|
264
292
|
// Register module identities and aliases.
|
|
265
|
-
// The root
|
|
266
|
-
// identity
|
|
267
|
-
// by the loader (so we don't
|
|
268
|
-
//
|
|
293
|
+
// The root module doc (Telo.Application or Telo.Library) provides its own
|
|
294
|
+
// identity; imported modules surface their identity via resolvedModuleName/
|
|
295
|
+
// resolvedNamespace stamped onto the Telo.Import by the loader (so we don't
|
|
296
|
+
// need to include imported module manifests in the analysis set, avoiding false
|
|
297
|
+
// reference errors in the parent context).
|
|
269
298
|
for (const m of manifests) {
|
|
270
|
-
if (m.kind
|
|
299
|
+
if (isModuleKind(m.kind)) {
|
|
271
300
|
const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
|
|
272
301
|
const moduleName = m.metadata.name as string;
|
|
273
302
|
if (moduleName) defs.registerModuleIdentity(namespace, moduleName);
|
|
274
303
|
}
|
|
275
|
-
if (m.kind === "
|
|
304
|
+
if (m.kind === "Telo.Import") {
|
|
276
305
|
const alias = m.metadata.name as string;
|
|
277
306
|
const source = (m as any).source as string | undefined;
|
|
278
307
|
const exportedKinds: string[] = (m as any).exports?.kinds ?? [];
|
|
@@ -292,11 +321,11 @@ export class StaticAnalyzer {
|
|
|
292
321
|
}
|
|
293
322
|
}
|
|
294
323
|
|
|
295
|
-
// Register definitions from
|
|
324
|
+
// Register definitions from Telo.Definition resources.
|
|
296
325
|
// Normalize alias-prefixed `capability` to canonical form so extendedBy lookup works
|
|
297
326
|
// (e.g. "Workflow.Backend" → "workflow.Backend" when "Workflow" is a known alias).
|
|
298
327
|
for (const m of manifests) {
|
|
299
|
-
if (m.kind === "
|
|
328
|
+
if (m.kind === "Telo.Definition") {
|
|
300
329
|
const def = m as unknown as ResourceDefinition;
|
|
301
330
|
const resolvedCapability = def.capability
|
|
302
331
|
? (aliases.resolveKind(def.capability) ?? def.capability)
|
|
@@ -324,17 +353,18 @@ export class StaticAnalyzer {
|
|
|
324
353
|
|
|
325
354
|
// Validate each non-definition, non-system resource
|
|
326
355
|
for (const m of allManifests) {
|
|
356
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
327
357
|
if (!m.kind || !m.metadata?.name) {
|
|
328
358
|
diagnostics.push({
|
|
329
359
|
severity: DiagnosticSeverity.Error,
|
|
330
360
|
code: "MISSING_KIND_OR_NAME",
|
|
331
361
|
source: SOURCE,
|
|
332
362
|
message: "Resource is missing required 'kind' or 'metadata.name' field.",
|
|
333
|
-
data: { path: !m.kind ? "kind" : "metadata.name" },
|
|
363
|
+
data: { filePath, path: !m.kind ? "kind" : "metadata.name" },
|
|
334
364
|
});
|
|
335
365
|
continue;
|
|
336
366
|
}
|
|
337
|
-
if (m.kind === "
|
|
367
|
+
if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") {
|
|
338
368
|
continue;
|
|
339
369
|
}
|
|
340
370
|
|
|
@@ -357,8 +387,8 @@ export class StaticAnalyzer {
|
|
|
357
387
|
severity: DiagnosticSeverity.Error,
|
|
358
388
|
code: "UNDEFINED_KIND",
|
|
359
389
|
source: SOURCE,
|
|
360
|
-
message: `No
|
|
361
|
-
data: { resource, path: "kind" },
|
|
390
|
+
message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
|
|
391
|
+
data: { resource, filePath, path: "kind" },
|
|
362
392
|
});
|
|
363
393
|
continue;
|
|
364
394
|
}
|
|
@@ -379,8 +409,8 @@ export class StaticAnalyzer {
|
|
|
379
409
|
}
|
|
380
410
|
: definition.schema;
|
|
381
411
|
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
382
|
-
const
|
|
383
|
-
const celIssues = collectCelTypeIssues(m, schema, "", definition, m,
|
|
412
|
+
const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m);
|
|
413
|
+
const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseTypedEnv, this.celEnv);
|
|
384
414
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
385
415
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
386
416
|
const issues = [...celIssues, ...ajvIssues];
|
|
@@ -390,7 +420,7 @@ export class StaticAnalyzer {
|
|
|
390
420
|
code: "SCHEMA_VIOLATION",
|
|
391
421
|
source: SOURCE,
|
|
392
422
|
message: `${m.kind}/${resource.name}: ${issue.message}`,
|
|
393
|
-
data: { resource, path: issue.path },
|
|
423
|
+
data: { resource, filePath, path: issue.path },
|
|
394
424
|
});
|
|
395
425
|
}
|
|
396
426
|
}
|
|
@@ -401,6 +431,7 @@ export class StaticAnalyzer {
|
|
|
401
431
|
// Validate CEL syntax and context variable access in all manifests
|
|
402
432
|
for (const m of allManifests) {
|
|
403
433
|
const resource = { kind: m.kind, name: m.metadata?.name as string };
|
|
434
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
404
435
|
|
|
405
436
|
const resolvedKind = aliases.resolveKind(m.kind);
|
|
406
437
|
const mDefinition =
|
|
@@ -416,16 +447,16 @@ export class StaticAnalyzer {
|
|
|
416
447
|
: undefined;
|
|
417
448
|
|
|
418
449
|
walkCelExpressions(m, "", (expr, path) => {
|
|
419
|
-
let parsed: ReturnType<typeof
|
|
450
|
+
let parsed: ReturnType<typeof this.celEnv.parse> | undefined;
|
|
420
451
|
try {
|
|
421
|
-
parsed =
|
|
452
|
+
parsed = this.celEnv.parse(expr);
|
|
422
453
|
} catch (e) {
|
|
423
454
|
diagnostics.push({
|
|
424
455
|
severity: DiagnosticSeverity.Error,
|
|
425
456
|
code: "CEL_SYNTAX_ERROR",
|
|
426
457
|
source: SOURCE,
|
|
427
458
|
message: `CEL syntax error at ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
428
|
-
data: { resource, path },
|
|
459
|
+
data: { resource, filePath, path },
|
|
429
460
|
});
|
|
430
461
|
return;
|
|
431
462
|
}
|
|
@@ -483,7 +514,7 @@ export class StaticAnalyzer {
|
|
|
483
514
|
code: "CEL_UNKNOWN_FIELD",
|
|
484
515
|
source: SOURCE,
|
|
485
516
|
message: `${m.kind}/${resource.name}: CEL at '${path}': ${err}`,
|
|
486
|
-
data: { resource, path },
|
|
517
|
+
data: { resource, filePath, path },
|
|
487
518
|
});
|
|
488
519
|
}
|
|
489
520
|
});
|
|
@@ -492,6 +523,9 @@ export class StaticAnalyzer {
|
|
|
492
523
|
// Validate resource references (Phase 3)
|
|
493
524
|
diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs }));
|
|
494
525
|
|
|
526
|
+
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
527
|
+
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
528
|
+
|
|
495
529
|
return diagnostics;
|
|
496
530
|
}
|
|
497
531
|
|
package/src/builtins.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import type { ResourceDefinition } from "@telorun/sdk";
|
|
2
2
|
|
|
3
3
|
export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
4
|
-
{ kind: "
|
|
5
|
-
{ kind: "
|
|
6
|
-
{ kind: "
|
|
7
|
-
{ kind: "
|
|
8
|
-
{ kind: "
|
|
9
|
-
{ kind: "
|
|
4
|
+
{ kind: "Telo.Abstract", metadata: { name: "Template", module: "Telo" } },
|
|
5
|
+
{ kind: "Telo.Abstract", metadata: { name: "Runnable", module: "Telo" } },
|
|
6
|
+
{ kind: "Telo.Abstract", metadata: { name: "Service", module: "Telo" } },
|
|
7
|
+
{ kind: "Telo.Abstract", metadata: { name: "Invocable", module: "Telo" } },
|
|
8
|
+
{ kind: "Telo.Abstract", metadata: { name: "Mount", module: "Telo" } },
|
|
9
|
+
{ kind: "Telo.Abstract", metadata: { name: "Type", module: "Telo" } },
|
|
10
10
|
{
|
|
11
|
-
kind: "
|
|
12
|
-
metadata: { name: "Provider", module: "
|
|
11
|
+
kind: "Telo.Abstract",
|
|
12
|
+
metadata: { name: "Provider", module: "Telo" },
|
|
13
13
|
schema: { "x-telo-eval": "compile" },
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
kind: "
|
|
17
|
-
metadata: { name: "Abstract", module: "
|
|
18
|
-
capability: "
|
|
16
|
+
kind: "Telo.Definition",
|
|
17
|
+
metadata: { name: "Abstract", module: "Telo" },
|
|
18
|
+
capability: "Telo.Template",
|
|
19
19
|
schema: {
|
|
20
20
|
type: "object",
|
|
21
21
|
properties: {
|
|
@@ -33,15 +33,15 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
kind: "
|
|
37
|
-
metadata: { name: "Definition", module: "
|
|
38
|
-
capability: "
|
|
36
|
+
kind: "Telo.Definition",
|
|
37
|
+
metadata: { name: "Definition", module: "Telo" },
|
|
38
|
+
capability: "Telo.Template",
|
|
39
39
|
schema: { type: "object" },
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
|
-
kind: "
|
|
43
|
-
metadata: { name: "Import", module: "
|
|
44
|
-
capability: "
|
|
42
|
+
kind: "Telo.Definition",
|
|
43
|
+
metadata: { name: "Import", module: "Telo" },
|
|
44
|
+
capability: "Telo.Template",
|
|
45
45
|
schema: {
|
|
46
46
|
type: "object",
|
|
47
47
|
properties: {
|
|
@@ -61,9 +61,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
|
-
kind: "
|
|
65
|
-
metadata: { name: "
|
|
66
|
-
capability: "
|
|
64
|
+
kind: "Telo.Definition",
|
|
65
|
+
metadata: { name: "Application", module: "Telo" },
|
|
66
|
+
capability: "Telo.Template",
|
|
67
67
|
schema: {
|
|
68
68
|
type: "object",
|
|
69
69
|
properties: {
|
|
@@ -85,14 +85,12 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
85
85
|
default: "shared",
|
|
86
86
|
},
|
|
87
87
|
keepAlive: { type: "boolean", default: false },
|
|
88
|
-
variables: { type: "object" },
|
|
89
|
-
secrets: { type: "object" },
|
|
90
88
|
targets: {
|
|
91
89
|
type: "array",
|
|
92
90
|
items: {
|
|
93
91
|
anyOf: [
|
|
94
|
-
{ type: "string", "x-telo-ref": "
|
|
95
|
-
{ type: "string", "x-telo-ref": "
|
|
92
|
+
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
93
|
+
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
96
94
|
],
|
|
97
95
|
},
|
|
98
96
|
},
|
|
@@ -100,6 +98,36 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
100
98
|
type: "array",
|
|
101
99
|
items: { type: "string" },
|
|
102
100
|
},
|
|
101
|
+
},
|
|
102
|
+
required: ["metadata"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: "Telo.Definition",
|
|
108
|
+
metadata: { name: "Library", module: "Telo" },
|
|
109
|
+
capability: "Telo.Template",
|
|
110
|
+
schema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
kind: { type: "string" },
|
|
114
|
+
metadata: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
name: { type: "string" },
|
|
118
|
+
version: { type: "string" },
|
|
119
|
+
source: { type: "string" },
|
|
120
|
+
module: { type: "string" },
|
|
121
|
+
},
|
|
122
|
+
required: ["name"],
|
|
123
|
+
additionalProperties: true,
|
|
124
|
+
},
|
|
125
|
+
variables: { type: "object" },
|
|
126
|
+
secrets: { type: "object" },
|
|
127
|
+
include: {
|
|
128
|
+
type: "array",
|
|
129
|
+
items: { type: "string" },
|
|
130
|
+
},
|
|
103
131
|
exports: {
|
|
104
132
|
type: "object",
|
|
105
133
|
properties: {
|
package/src/cel-environment.ts
CHANGED
|
@@ -1,36 +1,59 @@
|
|
|
1
|
-
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
3
|
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
export interface CelHandlers {
|
|
6
|
+
sha256: (s: string) => string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const stub = (name: string) => () => {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`${name}() is not available in this environment. ` +
|
|
12
|
+
`Construct StaticAnalyzer or Loader with celHandlers to enable it.`,
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STUB_HANDLERS: CelHandlers = {
|
|
17
|
+
sha256: stub("sha256"),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
21
|
+
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
22
|
+
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
23
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones. */
|
|
24
|
+
export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Environment {
|
|
25
|
+
return new Environment({ unlistedVariablesAreDyn: true })
|
|
26
|
+
.registerFunction("join(list, string): string", (list: unknown[], sep: string) =>
|
|
27
|
+
list.map(String).join(sep),
|
|
28
|
+
)
|
|
29
|
+
.registerFunction("keys(map): list", (map: unknown) => {
|
|
30
|
+
if (map instanceof Map) return [...map.keys()];
|
|
31
|
+
return Object.keys(map as Record<string, unknown>);
|
|
32
|
+
})
|
|
33
|
+
.registerFunction("values(map): list", (map: unknown) => {
|
|
34
|
+
if (map instanceof Map) return [...map.values()];
|
|
35
|
+
return Object.values(map as Record<string, unknown>);
|
|
36
|
+
})
|
|
37
|
+
.registerFunction("sha256(string): string", (s: string) => handlers.sha256(s));
|
|
38
|
+
}
|
|
17
39
|
|
|
18
|
-
/** Clone `
|
|
40
|
+
/** Clone `baseEnv` and register typed variable declarations so that
|
|
19
41
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
20
42
|
*
|
|
21
43
|
* - `variables`: typed from the manifest's `variables` field if it is a schema map
|
|
22
|
-
* (only `
|
|
44
|
+
* (only module-identity docs — `Telo.Application` / `Telo.Library` — carry this); otherwise registered as `map` (dyn).
|
|
23
45
|
* - `secrets`, `resources`, `env`: always `map` (dyn — output schemas unknown).
|
|
24
46
|
* - `extraContextSchema`: additional variables from an `x-telo-context` annotation.
|
|
25
47
|
*
|
|
26
48
|
* NOTE: The set of kernel globals registered here must match `KERNEL_GLOBAL_NAMES`
|
|
27
49
|
* in kernel-globals.ts, which is used for chain-access validation. */
|
|
28
50
|
export function buildTypedCelEnvironment(
|
|
51
|
+
baseEnv: Environment,
|
|
29
52
|
manifest: ResourceManifest,
|
|
30
53
|
extraContextSchema?: Record<string, any> | null,
|
|
31
54
|
): Environment {
|
|
32
55
|
try {
|
|
33
|
-
const env =
|
|
56
|
+
const env = baseEnv.clone();
|
|
34
57
|
|
|
35
58
|
// Build typed ObjectSchema from manifest.variables if it looks like a schema map
|
|
36
59
|
const vars = (manifest as Record<string, unknown>).variables;
|
|
@@ -67,6 +90,6 @@ export function buildTypedCelEnvironment(
|
|
|
67
90
|
|
|
68
91
|
return env;
|
|
69
92
|
} catch {
|
|
70
|
-
return
|
|
93
|
+
return baseEnv.clone();
|
|
71
94
|
}
|
|
72
95
|
}
|
|
@@ -20,10 +20,10 @@ export class DefinitionRegistry {
|
|
|
20
20
|
/** Reverse inheritance index: parent kind → direct child kinds. */
|
|
21
21
|
private readonly extendedBy = new Map<string, string[]>();
|
|
22
22
|
/** Module identity table: identity string → canonical module name.
|
|
23
|
-
* "
|
|
23
|
+
* "telo" → "Telo", "std/pipeline" → "pipeline", etc. */
|
|
24
24
|
private readonly identityMap = new Map<string, string>();
|
|
25
25
|
/** Reverse identity table: canonical module name → full identity string.
|
|
26
|
-
* "
|
|
26
|
+
* "Telo" → "telo", "pipeline" → "std/pipeline", etc.
|
|
27
27
|
* Used to compute definition $id values for the AJV schema store. */
|
|
28
28
|
private readonly reverseIdentityMap = new Map<string, string>();
|
|
29
29
|
|
|
@@ -40,10 +40,10 @@ export class DefinitionRegistry {
|
|
|
40
40
|
this.extendedBy.set(definition.capability, [key]);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
// Auto-register the
|
|
44
|
-
if (definition.kind === "
|
|
45
|
-
this.identityMap.set("
|
|
46
|
-
this.reverseIdentityMap.set("
|
|
43
|
+
// Auto-register the telo identity when any Telo built-in is registered.
|
|
44
|
+
if (definition.kind === "Telo.Abstract" && mod === "Telo") {
|
|
45
|
+
this.identityMap.set("telo", "Telo");
|
|
46
|
+
this.reverseIdentityMap.set("Telo", "telo");
|
|
47
47
|
}
|
|
48
48
|
// If identity is already known, register the schema in AJV immediately.
|
|
49
49
|
if (mod && definition.schema) {
|
|
@@ -52,11 +52,11 @@ export class DefinitionRegistry {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Register a module identity for x-telo-ref resolution.
|
|
55
|
-
* Call once per
|
|
56
|
-
* @param namespace The module's metadata.namespace (e.g. "std"), or null for
|
|
55
|
+
* Call once per module doc (Telo.Application or Telo.Library) when the manifest is loaded.
|
|
56
|
+
* @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
|
|
57
57
|
* @param moduleName The module's metadata.name (e.g. "pipeline", "http-server"). */
|
|
58
58
|
registerModuleIdentity(namespace: string | null, moduleName: string): void {
|
|
59
|
-
const identity = namespace ? `${namespace}/${moduleName}` : "
|
|
59
|
+
const identity = namespace ? `${namespace}/${moduleName}` : "telo";
|
|
60
60
|
this.identityMap.set(identity, moduleName);
|
|
61
61
|
this.reverseIdentityMap.set(moduleName, identity);
|
|
62
62
|
// Retroactively register AJV schemas for definitions of this module already in the registry.
|
|
@@ -110,7 +110,7 @@ export class DefinitionRegistry {
|
|
|
110
110
|
* Splits on "#", looks up the left side in the identity table, and returns
|
|
111
111
|
* "<canonicalModule>.<TypeName>".
|
|
112
112
|
*
|
|
113
|
-
* "
|
|
113
|
+
* "telo#Invocable" → "Telo.Invocable"
|
|
114
114
|
* "std/pipeline#Job" → "pipeline.Job"
|
|
115
115
|
* "std/http-server#Server" → "http-server.Server"
|
|
116
116
|
*
|
package/src/dependency-graph.ts
CHANGED
|
@@ -17,8 +17,15 @@ export interface DependencyGraph {
|
|
|
17
17
|
cycle?: ReadonlyArray<ResourceNode>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** System resource kinds that are not runtime nodes in the dependency graph.
|
|
21
|
-
|
|
20
|
+
/** System resource kinds that are not runtime nodes in the dependency graph.
|
|
21
|
+
* Module-identity docs (Telo.Application, Telo.Library) are intentionally
|
|
22
|
+
* not in this set: an Application's `targets` use `x-telo-ref` to real
|
|
23
|
+
* Runnable/Service resources, so the Application legitimately depends on
|
|
24
|
+
* them in boot order — modeling that as a graph edge is correct. A Library
|
|
25
|
+
* has no `targets`, so it becomes a zero-edge node, which is harmless.
|
|
26
|
+
* If the graph is ever consumed as "things to init", skip these kinds at
|
|
27
|
+
* the consumer site; the controller already runs them separately. */
|
|
28
|
+
const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Import"]);
|
|
22
29
|
|
|
23
30
|
const nodeKey = (kind: string, name: string) => `${kind}\0${name}`;
|
|
24
31
|
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
|
3
3
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
4
4
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
5
5
|
export { Loader } from "./manifest-loader.js";
|
|
6
|
+
export { MODULE_KINDS, isModuleKind } from "./module-kinds.js";
|
|
7
|
+
export type { ModuleKind } from "./module-kinds.js";
|
|
6
8
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
7
9
|
export type {
|
|
8
10
|
AnalysisDiagnostic,
|
package/src/kernel-globals.ts
CHANGED
|
@@ -7,16 +7,17 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
7
7
|
* must stay in sync with this list.
|
|
8
8
|
*
|
|
9
9
|
* Note: `env` is only available in the root module context. Child modules
|
|
10
|
-
* loaded via
|
|
10
|
+
* loaded via Telo.Import do not receive host environment variables.
|
|
11
11
|
* There is no `imports` namespace at runtime — import snapshots are stored
|
|
12
12
|
* under `resources.<alias>`.
|
|
13
13
|
*/
|
|
14
14
|
export const KERNEL_GLOBAL_NAMES = ["variables", "secrets", "resources", "env"] as const;
|
|
15
15
|
|
|
16
16
|
const SYSTEM_KINDS = new Set([
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
17
|
+
"Telo.Definition",
|
|
18
|
+
"Telo.Application",
|
|
19
|
+
"Telo.Library",
|
|
20
|
+
"Telo.Abstract",
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -25,22 +26,30 @@ const SYSTEM_KINDS = new Set([
|
|
|
25
26
|
* chain-access validation recognises kernel globals without module authors
|
|
26
27
|
* having to re-declare them.
|
|
27
28
|
*
|
|
28
|
-
* - `variables` / `secrets`: typed from the
|
|
29
|
+
* - `variables` / `secrets`: typed from the root module doc — prefer
|
|
30
|
+
* Telo.Application when present, otherwise fall back to Telo.Library.
|
|
31
|
+
* Applications are the root whose variables/secrets contract governs CEL
|
|
32
|
+
* in the outer module; Libraries are only relevant when the caller scoped
|
|
33
|
+
* the manifest list to a single library's file.
|
|
29
34
|
* - `resources`: enumerates all non-system resource names
|
|
30
35
|
* - `env`: dynamic (runtime env vars, root module only)
|
|
31
36
|
*/
|
|
32
37
|
export function buildKernelGlobalsSchema(
|
|
33
38
|
manifests: ResourceManifest[],
|
|
34
39
|
): Record<string, any> {
|
|
35
|
-
const moduleManifest =
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const moduleManifest =
|
|
41
|
+
(manifests.find((m) => m.kind === "Telo.Application") as
|
|
42
|
+
| Record<string, any>
|
|
43
|
+
| undefined) ??
|
|
44
|
+
(manifests.find((m) => m.kind === "Telo.Library") as
|
|
45
|
+
| Record<string, any>
|
|
46
|
+
| undefined);
|
|
38
47
|
|
|
39
48
|
const resourceProps: Record<string, any> = {};
|
|
40
49
|
for (const m of manifests) {
|
|
41
50
|
const name = m.metadata?.name as string | undefined;
|
|
42
51
|
if (!name || !m.kind) continue;
|
|
43
|
-
//
|
|
52
|
+
// Telo.Import snapshots are stored under resources.<alias> at runtime,
|
|
44
53
|
// so they appear here alongside regular resources.
|
|
45
54
|
if (!SYSTEM_KINDS.has(m.kind)) {
|
|
46
55
|
resourceProps[name] = { type: "object", additionalProperties: true };
|
|
@@ -62,7 +71,7 @@ export function buildKernelGlobalsSchema(
|
|
|
62
71
|
};
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
/** Wrap a JSON Schema property map (like `
|
|
74
|
+
/** Wrap a JSON Schema property map (like `Telo.Application.variables`) into a
|
|
66
75
|
* closed object schema suitable for chain-access validation. Falls back to
|
|
67
76
|
* an open map when the module declares no variables/secrets. */
|
|
68
77
|
function buildSchemaMapSchema(
|