@telorun/analyzer 0.12.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts +13 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +154 -83
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +85 -0
- package/dist/cel-environment.d.ts +1 -1
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +40 -2
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +41 -62
- 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 +1 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +19 -1
- package/dist/manifest-visitor.d.ts +124 -0
- package/dist/manifest-visitor.d.ts.map +1 -0
- package/dist/manifest-visitor.js +181 -0
- package/dist/reference-field-map.js +16 -0
- package/dist/resolve-throws-union.d.ts +10 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +35 -7
- package/dist/schema-compat.d.ts +10 -0
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +32 -0
- package/dist/validate-cel-context.d.ts +14 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +38 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +124 -160
- package/dist/validate-unused-declarations.d.ts +25 -0
- package/dist/validate-unused-declarations.d.ts.map +1 -0
- package/dist/validate-unused-declarations.js +91 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +20 -0
- package/src/analyzer.ts +256 -168
- package/src/builtins.ts +85 -0
- package/src/cel-environment.ts +42 -1
- package/src/dependency-graph.ts +37 -52
- package/src/index.ts +11 -0
- package/src/kernel-globals.ts +22 -1
- package/src/manifest-visitor.ts +340 -0
- package/src/reference-field-map.ts +14 -0
- package/src/resolve-throws-union.ts +36 -8
- package/src/schema-compat.ts +32 -0
- package/src/validate-cel-context.ts +50 -0
- package/src/validate-references.ts +175 -211
- package/src/validate-unused-declarations.ts +95 -0
package/src/analyzer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
|
-
import { defaultRegistry,
|
|
3
|
+
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
4
4
|
import { AliasResolver } from "./alias-resolver.js";
|
|
5
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
6
6
|
import {
|
|
@@ -12,6 +12,7 @@ import { DefinitionRegistry } from "./definition-registry.js";
|
|
|
12
12
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
13
13
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
14
14
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
15
|
+
import { visitManifest } from "./manifest-visitor.js";
|
|
15
16
|
import { isModuleKind } from "./module-kinds.js";
|
|
16
17
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
17
18
|
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
} from "./schema-compat.js";
|
|
26
27
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
|
|
27
28
|
import {
|
|
29
|
+
extractContextsFromSchema,
|
|
28
30
|
getManifestItem,
|
|
29
31
|
pathMatchesScope,
|
|
30
32
|
resolveContextAnnotations,
|
|
@@ -34,6 +36,7 @@ import { validateExtends } from "./validate-extends.js";
|
|
|
34
36
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
35
37
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
36
38
|
import { validateReferences } from "./validate-references.js";
|
|
39
|
+
import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
|
|
37
40
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
38
41
|
|
|
39
42
|
const SELF_PREFIX = "Self.";
|
|
@@ -187,56 +190,6 @@ function manifestRootForResolver(
|
|
|
187
190
|
};
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
/**
|
|
191
|
-
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
192
|
-
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
193
|
-
* the same format the analyzer uses for CEL context validation.
|
|
194
|
-
*
|
|
195
|
-
* Result is sorted by scope specificity (longer scope first) so that the
|
|
196
|
-
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
197
|
-
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
198
|
-
* could shadow a narrower descendant scope whose activation differs.
|
|
199
|
-
*/
|
|
200
|
-
function extractContextsFromSchema(
|
|
201
|
-
schema: Record<string, any>,
|
|
202
|
-
path = "$",
|
|
203
|
-
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
204
|
-
const all = collectContexts(schema, path);
|
|
205
|
-
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function collectContexts(
|
|
209
|
-
schema: Record<string, any>,
|
|
210
|
-
path: string,
|
|
211
|
-
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
212
|
-
if (!schema || typeof schema !== "object") return [];
|
|
213
|
-
const results: Array<{ scope: string; schema: Record<string, any> }> = [];
|
|
214
|
-
|
|
215
|
-
if (schema["x-telo-context"]) {
|
|
216
|
-
results.push({ scope: path, schema: schema["x-telo-context"] });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (schema.properties) {
|
|
220
|
-
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
221
|
-
results.push(...collectContexts(value, `${path}.${key}`));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (schema.items && typeof schema.items === "object") {
|
|
226
|
-
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
230
|
-
if (Array.isArray(schema[key])) {
|
|
231
|
-
for (const subschema of schema[key]) {
|
|
232
|
-
results.push(...collectContexts(subschema, path));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return results;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
193
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
241
194
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
242
195
|
function resolveLocalRef(
|
|
@@ -426,6 +379,78 @@ function buildStepContextSchema(
|
|
|
426
379
|
return undefined;
|
|
427
380
|
}
|
|
428
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Collect every field annotated with `x-telo-error-context` anywhere in a
|
|
384
|
+
* definition schema (resolving local `$ref`s into `$defs`, cycle-safe), mapping
|
|
385
|
+
* the annotated field name to its declared error-shape schema. The field name
|
|
386
|
+
* is matched against CEL paths so the context applies at any nesting depth under
|
|
387
|
+
* that field — e.g. `error` inside a `catch:` nested inside another `try:`. No
|
|
388
|
+
* specific field name (or `Run.Sequence`) is hardcoded; any composer that tags
|
|
389
|
+
* its error-bearing branch fields opts in the same way.
|
|
390
|
+
*/
|
|
391
|
+
function collectErrorContextScopes(
|
|
392
|
+
defSchema: Record<string, any> | undefined,
|
|
393
|
+
): Map<string, Record<string, any>> {
|
|
394
|
+
const out = new Map<string, Record<string, any>>();
|
|
395
|
+
if (!defSchema || typeof defSchema !== "object") return out;
|
|
396
|
+
const seen = new Set<Record<string, any>>();
|
|
397
|
+
|
|
398
|
+
const walk = (schema: Record<string, any> | undefined): void => {
|
|
399
|
+
if (!schema || typeof schema !== "object" || seen.has(schema)) return;
|
|
400
|
+
seen.add(schema);
|
|
401
|
+
|
|
402
|
+
const props = schema.properties as Record<string, any> | undefined;
|
|
403
|
+
if (props) {
|
|
404
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
405
|
+
if (fieldSchema && typeof fieldSchema === "object") {
|
|
406
|
+
const errCtx = (fieldSchema as Record<string, any>)["x-telo-error-context"];
|
|
407
|
+
if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
|
|
408
|
+
out.set(fieldName, errCtx as Record<string, any>);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
walk(resolveLocalRef(fieldSchema as Record<string, any>, defSchema));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (schema.items) walk(resolveLocalRef(schema.items as Record<string, any>, defSchema));
|
|
415
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
416
|
+
const arr = schema[key];
|
|
417
|
+
if (Array.isArray(arr)) for (const sub of arr) walk(resolveLocalRef(sub, defSchema));
|
|
418
|
+
}
|
|
419
|
+
if (schema.$defs && typeof schema.$defs === "object") {
|
|
420
|
+
for (const sub of Object.values(schema.$defs as Record<string, any>)) {
|
|
421
|
+
walk(sub as Record<string, any>);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
walk(defSchema);
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Return the error-context schema for a CEL `path` when the path lies within
|
|
432
|
+
* (any depth under) one of the error-bearing fields, else undefined. A path is
|
|
433
|
+
* "within" field `f` when it contains a segment `f[<index>]`. When multiple
|
|
434
|
+
* error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
|
|
435
|
+
* deepest — the one whose segment appears latest in the path — wins, so the
|
|
436
|
+
* innermost branch's schema governs.
|
|
437
|
+
*/
|
|
438
|
+
function errorContextForPath(
|
|
439
|
+
path: string,
|
|
440
|
+
scopes: Map<string, Record<string, any>>,
|
|
441
|
+
): Record<string, any> | undefined {
|
|
442
|
+
let best: { index: number; schema: Record<string, any> } | undefined;
|
|
443
|
+
for (const [fieldName, schema] of scopes) {
|
|
444
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
445
|
+
for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
|
|
446
|
+
if (best === undefined || match.index > best.index) {
|
|
447
|
+
best = { index: match.index, schema };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return best?.schema;
|
|
452
|
+
}
|
|
453
|
+
|
|
429
454
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
430
455
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
431
456
|
|
|
@@ -439,20 +464,31 @@ function collectCelTypeIssues(
|
|
|
439
464
|
manifest: ResourceManifest,
|
|
440
465
|
baseTypedEnv: Environment,
|
|
441
466
|
rootEnv: Environment,
|
|
467
|
+
rootModuleManifest?: ResourceManifest,
|
|
442
468
|
): SchemaIssue[] {
|
|
443
469
|
const issues: SchemaIssue[] = [];
|
|
444
470
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
471
|
+
// A pure CEL value type-checks the same regardless of surface form: a
|
|
472
|
+
// `${{ … }}` string and a `!cel`-tagged sentinel must behave identically.
|
|
473
|
+
let celExpr: string | undefined;
|
|
474
|
+
if (isTaggedSentinel(data)) {
|
|
475
|
+
// Non-CEL engines (e.g. `!literal`) are analyzed by their own engine pass.
|
|
476
|
+
if (data.engine !== "cel") return issues;
|
|
477
|
+
celExpr = data.source;
|
|
478
|
+
} else if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
479
|
+
celExpr = data.match(CEL_EXPR_RE)?.[1]?.trim();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (celExpr !== undefined) {
|
|
483
|
+
{
|
|
484
|
+
const expr = celExpr;
|
|
449
485
|
|
|
450
486
|
// Merge x-telo-context variables for this path if applicable
|
|
451
487
|
let typedEnv = baseTypedEnv;
|
|
452
488
|
if (definition.schema) {
|
|
453
489
|
for (const ctx of extractContextsFromSchema(definition.schema)) {
|
|
454
490
|
if (!pathMatchesScope(path, ctx.scope)) continue;
|
|
455
|
-
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema);
|
|
491
|
+
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema, rootModuleManifest);
|
|
456
492
|
break;
|
|
457
493
|
}
|
|
458
494
|
}
|
|
@@ -475,8 +511,9 @@ function collectCelTypeIssues(
|
|
|
475
511
|
} else if (checkResult?.valid && checkResult.type && schema) {
|
|
476
512
|
const celType = checkResult.type.split("<")[0]!;
|
|
477
513
|
if (!celTypeSatisfiesJsonSchema(celType, schema)) {
|
|
514
|
+
const expected = schema["x-telo-type"] ?? schema.type ?? "unknown";
|
|
478
515
|
issues.push({
|
|
479
|
-
message: `CEL returns '${checkResult.type}' but field expects '${
|
|
516
|
+
message: `CEL returns '${checkResult.type}' but field expects '${expected}'`,
|
|
480
517
|
path,
|
|
481
518
|
});
|
|
482
519
|
}
|
|
@@ -497,6 +534,7 @@ function collectCelTypeIssues(
|
|
|
497
534
|
manifest,
|
|
498
535
|
baseTypedEnv,
|
|
499
536
|
rootEnv,
|
|
537
|
+
rootModuleManifest,
|
|
500
538
|
),
|
|
501
539
|
);
|
|
502
540
|
}
|
|
@@ -512,6 +550,7 @@ function collectCelTypeIssues(
|
|
|
512
550
|
manifest,
|
|
513
551
|
baseTypedEnv,
|
|
514
552
|
rootEnv,
|
|
553
|
+
rootModuleManifest,
|
|
515
554
|
),
|
|
516
555
|
);
|
|
517
556
|
}
|
|
@@ -758,6 +797,13 @@ export class StaticAnalyzer {
|
|
|
758
797
|
// recognises variables, secrets, resources, env automatically
|
|
759
798
|
const kernelGlobals = buildKernelGlobalsSchema(allManifests);
|
|
760
799
|
|
|
800
|
+
// The module doc (Application/Library) carries the Application-only `ports`
|
|
801
|
+
// namespace; threaded into per-resource CEL typing so `${{ ports.X }}`
|
|
802
|
+
// resolves its nominal brand cross-doc.
|
|
803
|
+
const moduleManifest =
|
|
804
|
+
allManifests.find((mm) => mm.kind === "Telo.Application") ??
|
|
805
|
+
allManifests.find((mm) => mm.kind === "Telo.Library");
|
|
806
|
+
|
|
761
807
|
// Validate each non-definition, non-system resource
|
|
762
808
|
for (const m of allManifests) {
|
|
763
809
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
@@ -816,8 +862,17 @@ export class StaticAnalyzer {
|
|
|
816
862
|
}
|
|
817
863
|
: definition.schema;
|
|
818
864
|
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
819
|
-
const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m);
|
|
820
|
-
const celIssues = collectCelTypeIssues(
|
|
865
|
+
const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
866
|
+
const celIssues = collectCelTypeIssues(
|
|
867
|
+
m,
|
|
868
|
+
schema,
|
|
869
|
+
"",
|
|
870
|
+
definition,
|
|
871
|
+
m,
|
|
872
|
+
baseTypedEnv,
|
|
873
|
+
this.celEnv,
|
|
874
|
+
moduleManifest,
|
|
875
|
+
);
|
|
821
876
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
822
877
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
823
878
|
const issues = [...celIssues, ...ajvIssues];
|
|
@@ -959,125 +1014,155 @@ export class StaticAnalyzer {
|
|
|
959
1014
|
}
|
|
960
1015
|
}
|
|
961
1016
|
|
|
962
|
-
// Validate CEL syntax and context variable access in all manifests
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
:
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1017
|
+
// Validate CEL syntax and context variable access in all manifests. The
|
|
1018
|
+
// walker discovers every compiled CEL node by scanning the value tree and
|
|
1019
|
+
// hands back the `x-telo-context` schema matched at the enclosing path; the
|
|
1020
|
+
// per-path resolution (step context, kernel-globals merge, x-telo-context-*
|
|
1021
|
+
// annotation resolution) stays here because it depends on analyzer-internal
|
|
1022
|
+
// state (definitions, aliases, the typed CEL env).
|
|
1023
|
+
// Per-resource state computed at enter and read by that resource's CEL
|
|
1024
|
+
// sites. The manifest / resource / filePath come straight off each CelSite's
|
|
1025
|
+
// `source` (no need to capture them); only the derived step / invocation
|
|
1026
|
+
// context — which require analyzer state to build — are stashed here.
|
|
1027
|
+
let celStepContextSchema: Record<string, any> | undefined;
|
|
1028
|
+
let celInvocationContext: Record<string, any> | undefined;
|
|
1029
|
+
let celErrorScopes: Map<string, Record<string, any>> = new Map();
|
|
1030
|
+
|
|
1031
|
+
visitManifest(
|
|
1032
|
+
allManifests,
|
|
1033
|
+
defs,
|
|
1034
|
+
{
|
|
1035
|
+
onResourceEnter: (e) => {
|
|
1036
|
+
const m = e.source;
|
|
1037
|
+
celInvocationContext = (m.metadata as any)?.xTeloInvocationContext as
|
|
1038
|
+
| Record<string, any>
|
|
1039
|
+
| undefined;
|
|
1040
|
+
celStepContextSchema = e.definition?.schema
|
|
1041
|
+
? buildStepContextSchema(
|
|
1042
|
+
m as Record<string, any>,
|
|
1043
|
+
e.definition.schema as Record<string, any>,
|
|
1044
|
+
allManifests as Record<string, any>[],
|
|
1045
|
+
defs,
|
|
1046
|
+
aliases,
|
|
1047
|
+
)
|
|
1048
|
+
: undefined;
|
|
1049
|
+
celErrorScopes = collectErrorContextScopes(
|
|
1050
|
+
e.definition?.schema as Record<string, any> | undefined,
|
|
1051
|
+
);
|
|
1052
|
+
},
|
|
1053
|
+
onCel: (e) => {
|
|
1054
|
+
const m = e.source;
|
|
1055
|
+
const resource = { kind: m.kind, name: m.metadata?.name as string };
|
|
1056
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
1057
|
+
const { expr, path, engineName, matchedScope } = e;
|
|
1058
|
+
|
|
1059
|
+
let matchedContext: Record<string, any> | undefined =
|
|
1060
|
+
e.contextSchema ?? celInvocationContext;
|
|
1061
|
+
|
|
1062
|
+
if (celStepContextSchema) {
|
|
1063
|
+
const base =
|
|
1064
|
+
matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
1065
|
+
matchedContext = {
|
|
1066
|
+
...base,
|
|
1067
|
+
properties: {
|
|
1068
|
+
...(base.properties ?? {}),
|
|
1069
|
+
steps: celStepContextSchema,
|
|
1070
|
+
},
|
|
1071
|
+
};
|
|
1000
1072
|
}
|
|
1001
|
-
}
|
|
1002
|
-
if (!matchedContext) matchedContext = invocationContext;
|
|
1003
1073
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
: (m as Record<string, any>);
|
|
1020
|
-
const rootForResolver = manifestRootForResolver(
|
|
1021
|
-
m as Record<string, any>,
|
|
1022
|
-
defs,
|
|
1023
|
-
aliases,
|
|
1024
|
-
allManifests as Record<string, any>[],
|
|
1025
|
-
);
|
|
1026
|
-
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
1027
|
-
manifestRoot: rootForResolver,
|
|
1028
|
-
defs,
|
|
1029
|
-
aliases,
|
|
1030
|
-
allManifests: allManifests as Record<string, any>[],
|
|
1031
|
-
});
|
|
1032
|
-
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
1033
|
-
}
|
|
1074
|
+
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
1075
|
+
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
1076
|
+
const errorSchema =
|
|
1077
|
+
celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
|
|
1078
|
+
if (errorSchema) {
|
|
1079
|
+
const base =
|
|
1080
|
+
matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
1081
|
+
matchedContext = {
|
|
1082
|
+
...base,
|
|
1083
|
+
properties: {
|
|
1084
|
+
...(base.properties ?? {}),
|
|
1085
|
+
error: errorSchema,
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1034
1089
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
diagnostics.push({
|
|
1052
|
-
severity: DiagnosticSeverity.Error,
|
|
1053
|
-
code: "CEL_SYNTAX_ERROR",
|
|
1054
|
-
source: SOURCE,
|
|
1055
|
-
message: `CEL syntax error at ${path}: ${f.message}`,
|
|
1056
|
-
data: { resource, filePath, path },
|
|
1057
|
-
});
|
|
1058
|
-
} else if (f.code === "CEL_UNKNOWN_FIELD") {
|
|
1059
|
-
diagnostics.push({
|
|
1060
|
-
severity: DiagnosticSeverity.Error,
|
|
1061
|
-
code: "CEL_UNKNOWN_FIELD",
|
|
1062
|
-
source: SOURCE,
|
|
1063
|
-
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
1064
|
-
data: { resource, filePath, path },
|
|
1090
|
+
let effectiveContext: Record<string, any> | null = null;
|
|
1091
|
+
if (matchedContext) {
|
|
1092
|
+
const manifestItem = matchedScope
|
|
1093
|
+
? getManifestItem(path, matchedScope, m as Record<string, any>)
|
|
1094
|
+
: (m as Record<string, any>);
|
|
1095
|
+
const rootForResolver = manifestRootForResolver(
|
|
1096
|
+
m as Record<string, any>,
|
|
1097
|
+
defs,
|
|
1098
|
+
aliases,
|
|
1099
|
+
allManifests as Record<string, any>[],
|
|
1100
|
+
);
|
|
1101
|
+
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
1102
|
+
manifestRoot: rootForResolver,
|
|
1103
|
+
defs,
|
|
1104
|
+
aliases,
|
|
1105
|
+
allManifests: allManifests as Record<string, any>[],
|
|
1065
1106
|
});
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1107
|
+
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const engine = defaultRegistry().get(engineName);
|
|
1111
|
+
if (!engine) {
|
|
1112
|
+
// No registered engine owns this tag — the expression would go
|
|
1113
|
+
// entirely unanalyzed. Surface it rather than skipping silently.
|
|
1070
1114
|
diagnostics.push({
|
|
1071
1115
|
severity: DiagnosticSeverity.Error,
|
|
1072
|
-
code:
|
|
1116
|
+
code: "UNKNOWN_ENGINE",
|
|
1073
1117
|
source: SOURCE,
|
|
1074
|
-
message: `${m.kind}/${resource.name}: !${engineName} at '${path}'
|
|
1118
|
+
message: `${m.kind}/${resource.name}: no templating engine registered for '!${engineName}' at '${path}'.`,
|
|
1075
1119
|
data: { resource, filePath, path },
|
|
1076
1120
|
});
|
|
1121
|
+
return;
|
|
1077
1122
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1123
|
+
const findings = engine.analyze(expr, { celEnv: this.celEnv, contextSchema: effectiveContext });
|
|
1124
|
+
for (const f of findings) {
|
|
1125
|
+
if (f.code === "CEL_SYNTAX_ERROR") {
|
|
1126
|
+
diagnostics.push({
|
|
1127
|
+
severity: DiagnosticSeverity.Error,
|
|
1128
|
+
code: "CEL_SYNTAX_ERROR",
|
|
1129
|
+
source: SOURCE,
|
|
1130
|
+
message: `CEL syntax error at ${path}: ${f.message}`,
|
|
1131
|
+
data: { resource, filePath, path },
|
|
1132
|
+
});
|
|
1133
|
+
} else if (f.code === "CEL_UNKNOWN_FIELD") {
|
|
1134
|
+
diagnostics.push({
|
|
1135
|
+
severity: DiagnosticSeverity.Error,
|
|
1136
|
+
code: "CEL_UNKNOWN_FIELD",
|
|
1137
|
+
source: SOURCE,
|
|
1138
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
1139
|
+
data: { resource, filePath, path },
|
|
1140
|
+
});
|
|
1141
|
+
} else if (f.code === "CEL_NULLABLE_ACCESS") {
|
|
1142
|
+
diagnostics.push({
|
|
1143
|
+
severity: DiagnosticSeverity.Error,
|
|
1144
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
1145
|
+
source: SOURCE,
|
|
1146
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
1147
|
+
data: { resource, filePath, path },
|
|
1148
|
+
});
|
|
1149
|
+
} else {
|
|
1150
|
+
// Unknown code from a future engine — pass the message through,
|
|
1151
|
+
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
|
1152
|
+
// filters can still bucket it.
|
|
1153
|
+
diagnostics.push({
|
|
1154
|
+
severity: DiagnosticSeverity.Error,
|
|
1155
|
+
code: f.code ?? "ENGINE_DIAGNOSTIC",
|
|
1156
|
+
source: SOURCE,
|
|
1157
|
+
message: `${m.kind}/${resource.name}: !${engineName} at '${path}': ${f.message}`,
|
|
1158
|
+
data: { resource, filePath, path },
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
{ aliases },
|
|
1165
|
+
);
|
|
1081
1166
|
|
|
1082
1167
|
// Validate resource references (Phase 3)
|
|
1083
1168
|
diagnostics.push(
|
|
@@ -1093,6 +1178,9 @@ export class StaticAnalyzer {
|
|
|
1093
1178
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
1094
1179
|
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
1095
1180
|
|
|
1181
|
+
// Warn about declared variables / secrets / ports that no CEL references.
|
|
1182
|
+
diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
|
|
1183
|
+
|
|
1096
1184
|
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
1097
1185
|
// the chain root so position-index lookups land on the parent doc.
|
|
1098
1186
|
return rewriteSyntheticOrigins(diagnostics, allManifests);
|
package/src/builtins.ts
CHANGED
|
@@ -234,6 +234,66 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
234
234
|
},
|
|
235
235
|
additionalProperties: true,
|
|
236
236
|
},
|
|
237
|
+
// Gated reference: run() a Runnable/Service only when the
|
|
238
|
+
// `when` CEL guard holds. Discriminated by the `ref` key. `ref`
|
|
239
|
+
// is a bare name or a resolved `!ref` (`{ kind, name }`).
|
|
240
|
+
{
|
|
241
|
+
type: "object",
|
|
242
|
+
required: ["ref"],
|
|
243
|
+
properties: {
|
|
244
|
+
ref: {
|
|
245
|
+
anyOf: [
|
|
246
|
+
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
247
|
+
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
248
|
+
{
|
|
249
|
+
type: "object",
|
|
250
|
+
required: ["kind", "name"],
|
|
251
|
+
properties: {
|
|
252
|
+
kind: { type: "string" },
|
|
253
|
+
name: { type: "string" },
|
|
254
|
+
},
|
|
255
|
+
additionalProperties: true,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
when: { type: "string" },
|
|
260
|
+
},
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
},
|
|
263
|
+
// Inline flat invoke step: invoke an Invocable / Runnable on boot
|
|
264
|
+
// with an optional `name` (for steps.<name>.result plumbing),
|
|
265
|
+
// `when` guard, and `inputs`. Discriminated by the `invoke` key.
|
|
266
|
+
// Control flow (if/while/switch/try) is not available here —
|
|
267
|
+
// reach for Run.Sequence. `invoke` is ref-only and must resolve
|
|
268
|
+
// to a `{ kind, name }` reference (a `!ref` / `{kind,name}`):
|
|
269
|
+
// requiring `name` rejects an inline `{ kind }` definition (no
|
|
270
|
+
// name) at analysis instead of failing at boot with an undefined
|
|
271
|
+
// resource name. The Invocable/Runnable kind set mirrors
|
|
272
|
+
// Run.Sequence invoke steps.
|
|
273
|
+
{
|
|
274
|
+
type: "object",
|
|
275
|
+
required: ["invoke"],
|
|
276
|
+
properties: {
|
|
277
|
+
name: { type: "string" },
|
|
278
|
+
invoke: {
|
|
279
|
+
"x-telo-topology-role": "invoke",
|
|
280
|
+
type: "object",
|
|
281
|
+
required: ["kind", "name"],
|
|
282
|
+
properties: {
|
|
283
|
+
kind: { type: "string" },
|
|
284
|
+
name: { type: "string" },
|
|
285
|
+
},
|
|
286
|
+
additionalProperties: true,
|
|
287
|
+
anyOf: [
|
|
288
|
+
{ "x-telo-ref": "telo#Invocable" },
|
|
289
|
+
{ "x-telo-ref": "telo#Runnable" },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
inputs: { type: "object", additionalProperties: true },
|
|
293
|
+
when: { type: "string" },
|
|
294
|
+
},
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
},
|
|
237
297
|
],
|
|
238
298
|
},
|
|
239
299
|
},
|
|
@@ -279,6 +339,31 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
279
339
|
},
|
|
280
340
|
},
|
|
281
341
|
},
|
|
342
|
+
// Inbound ports the Application listens on. A name-keyed map mirroring
|
|
343
|
+
// `variables`: each entry binds a host env var (`env:`) that supplies a
|
|
344
|
+
// port integer (implicitly typed `integer`, 1–65535), with an optional
|
|
345
|
+
// `default:` used when the env var is unset. `protocol:` (default `tcp`)
|
|
346
|
+
// selects the transport — the runner reads this list to know the
|
|
347
|
+
// exposed ports before launch, and the analyzer brands the resolved
|
|
348
|
+
// `ports.<name>` value (tcp → TcpPort, udp → UdpPort) for static wiring
|
|
349
|
+
// checks. Application-only. See kernel/nodejs/src/application-env.ts.
|
|
350
|
+
ports: {
|
|
351
|
+
type: "object",
|
|
352
|
+
additionalProperties: {
|
|
353
|
+
type: "object",
|
|
354
|
+
required: ["env"],
|
|
355
|
+
properties: {
|
|
356
|
+
env: { type: "string" },
|
|
357
|
+
protocol: {
|
|
358
|
+
type: "string",
|
|
359
|
+
enum: ["tcp", "udp"],
|
|
360
|
+
default: "tcp",
|
|
361
|
+
},
|
|
362
|
+
default: { type: "integer", minimum: 1, maximum: 65535 },
|
|
363
|
+
},
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
282
367
|
},
|
|
283
368
|
required: ["metadata"],
|
|
284
369
|
additionalProperties: false,
|