@telorun/analyzer 0.12.1 → 0.14.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/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +18 -0
- package/dist/adapters/node-adapter.d.ts +17 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +71 -0
- package/dist/adapters/registry-adapter.d.ts +15 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +53 -0
- package/dist/alias-resolver.d.ts +6 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +8 -0
- 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 +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +176 -88
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +86 -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/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +13 -0
- 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-ref-sentinels.d.ts +15 -17
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +94 -40
- 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 +215 -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/dist/analyzer.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { defaultRegistry,
|
|
1
|
+
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
2
2
|
import { AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import { buildCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
|
|
4
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
6
6
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
7
7
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
8
|
+
import { visitManifest } from "./manifest-visitor.js";
|
|
8
9
|
import { isModuleKind } from "./module-kinds.js";
|
|
9
10
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
10
11
|
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
@@ -12,11 +13,12 @@ import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
|
12
13
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
13
14
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
14
15
|
import { DiagnosticSeverity } from "./types.js";
|
|
15
|
-
import { getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
|
|
16
|
+
import { extractContextsFromSchema, getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
|
|
16
17
|
import { validateExtends } from "./validate-extends.js";
|
|
17
18
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
18
19
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
19
20
|
import { validateReferences } from "./validate-references.js";
|
|
21
|
+
import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
|
|
20
22
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
21
23
|
const SELF_PREFIX = "Self.";
|
|
22
24
|
/**
|
|
@@ -142,44 +144,6 @@ function manifestRootForResolver(m, defs, aliases, allManifests) {
|
|
|
142
144
|
...(inputs ? { inputType: inputs } : {}),
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
147
|
-
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
148
|
-
* the same format the analyzer uses for CEL context validation.
|
|
149
|
-
*
|
|
150
|
-
* Result is sorted by scope specificity (longer scope first) so that the
|
|
151
|
-
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
152
|
-
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
153
|
-
* could shadow a narrower descendant scope whose activation differs.
|
|
154
|
-
*/
|
|
155
|
-
function extractContextsFromSchema(schema, path = "$") {
|
|
156
|
-
const all = collectContexts(schema, path);
|
|
157
|
-
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
158
|
-
}
|
|
159
|
-
function collectContexts(schema, path) {
|
|
160
|
-
if (!schema || typeof schema !== "object")
|
|
161
|
-
return [];
|
|
162
|
-
const results = [];
|
|
163
|
-
if (schema["x-telo-context"]) {
|
|
164
|
-
results.push({ scope: path, schema: schema["x-telo-context"] });
|
|
165
|
-
}
|
|
166
|
-
if (schema.properties) {
|
|
167
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
168
|
-
results.push(...collectContexts(value, `${path}.${key}`));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (schema.items && typeof schema.items === "object") {
|
|
172
|
-
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
173
|
-
}
|
|
174
|
-
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
175
|
-
if (Array.isArray(schema[key])) {
|
|
176
|
-
for (const subschema of schema[key]) {
|
|
177
|
-
results.push(...collectContexts(subschema, path));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return results;
|
|
182
|
-
}
|
|
183
147
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
184
148
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
185
149
|
function resolveLocalRef(schema, root) {
|
|
@@ -343,23 +307,101 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
|
|
|
343
307
|
}
|
|
344
308
|
return undefined;
|
|
345
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Collect every field annotated with `x-telo-error-context` anywhere in a
|
|
312
|
+
* definition schema (resolving local `$ref`s into `$defs`, cycle-safe), mapping
|
|
313
|
+
* the annotated field name to its declared error-shape schema. The field name
|
|
314
|
+
* is matched against CEL paths so the context applies at any nesting depth under
|
|
315
|
+
* that field — e.g. `error` inside a `catch:` nested inside another `try:`. No
|
|
316
|
+
* specific field name (or `Run.Sequence`) is hardcoded; any composer that tags
|
|
317
|
+
* its error-bearing branch fields opts in the same way.
|
|
318
|
+
*/
|
|
319
|
+
function collectErrorContextScopes(defSchema) {
|
|
320
|
+
const out = new Map();
|
|
321
|
+
if (!defSchema || typeof defSchema !== "object")
|
|
322
|
+
return out;
|
|
323
|
+
const seen = new Set();
|
|
324
|
+
const walk = (schema) => {
|
|
325
|
+
if (!schema || typeof schema !== "object" || seen.has(schema))
|
|
326
|
+
return;
|
|
327
|
+
seen.add(schema);
|
|
328
|
+
const props = schema.properties;
|
|
329
|
+
if (props) {
|
|
330
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
331
|
+
if (fieldSchema && typeof fieldSchema === "object") {
|
|
332
|
+
const errCtx = fieldSchema["x-telo-error-context"];
|
|
333
|
+
if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
|
|
334
|
+
out.set(fieldName, errCtx);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
walk(resolveLocalRef(fieldSchema, defSchema));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (schema.items)
|
|
341
|
+
walk(resolveLocalRef(schema.items, defSchema));
|
|
342
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
343
|
+
const arr = schema[key];
|
|
344
|
+
if (Array.isArray(arr))
|
|
345
|
+
for (const sub of arr)
|
|
346
|
+
walk(resolveLocalRef(sub, defSchema));
|
|
347
|
+
}
|
|
348
|
+
if (schema.$defs && typeof schema.$defs === "object") {
|
|
349
|
+
for (const sub of Object.values(schema.$defs)) {
|
|
350
|
+
walk(sub);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
walk(defSchema);
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Return the error-context schema for a CEL `path` when the path lies within
|
|
359
|
+
* (any depth under) one of the error-bearing fields, else undefined. A path is
|
|
360
|
+
* "within" field `f` when it contains a segment `f[<index>]`. When multiple
|
|
361
|
+
* error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
|
|
362
|
+
* deepest — the one whose segment appears latest in the path — wins, so the
|
|
363
|
+
* innermost branch's schema governs.
|
|
364
|
+
*/
|
|
365
|
+
function errorContextForPath(path, scopes) {
|
|
366
|
+
let best;
|
|
367
|
+
for (const [fieldName, schema] of scopes) {
|
|
368
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
369
|
+
for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
|
|
370
|
+
if (best === undefined || match.index > best.index) {
|
|
371
|
+
best = { index: match.index, schema };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return best?.schema;
|
|
376
|
+
}
|
|
346
377
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
347
378
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
348
379
|
/** Recursively walk `data`+`schema` together, type-checking every pure CEL template
|
|
349
380
|
* string via `env.check()`. Returns `SchemaIssue[]` for any type mismatches found. */
|
|
350
|
-
function collectCelTypeIssues(data, schema, path, definition, manifest, baseTypedEnv, rootEnv) {
|
|
381
|
+
function collectCelTypeIssues(data, schema, path, definition, manifest, baseTypedEnv, rootEnv, rootModuleManifest) {
|
|
351
382
|
const issues = [];
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
383
|
+
// A pure CEL value type-checks the same regardless of surface form: a
|
|
384
|
+
// `${{ … }}` string and a `!cel`-tagged sentinel must behave identically.
|
|
385
|
+
let celExpr;
|
|
386
|
+
if (isTaggedSentinel(data)) {
|
|
387
|
+
// Non-CEL engines (e.g. `!literal`) are analyzed by their own engine pass.
|
|
388
|
+
if (data.engine !== "cel")
|
|
389
|
+
return issues;
|
|
390
|
+
celExpr = data.source;
|
|
391
|
+
}
|
|
392
|
+
else if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
393
|
+
celExpr = data.match(CEL_EXPR_RE)?.[1]?.trim();
|
|
394
|
+
}
|
|
395
|
+
if (celExpr !== undefined) {
|
|
396
|
+
{
|
|
397
|
+
const expr = celExpr;
|
|
356
398
|
// Merge x-telo-context variables for this path if applicable
|
|
357
399
|
let typedEnv = baseTypedEnv;
|
|
358
400
|
if (definition.schema) {
|
|
359
401
|
for (const ctx of extractContextsFromSchema(definition.schema)) {
|
|
360
402
|
if (!pathMatchesScope(path, ctx.scope))
|
|
361
403
|
continue;
|
|
362
|
-
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema);
|
|
404
|
+
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema, rootModuleManifest);
|
|
363
405
|
break;
|
|
364
406
|
}
|
|
365
407
|
}
|
|
@@ -382,8 +424,9 @@ function collectCelTypeIssues(data, schema, path, definition, manifest, baseType
|
|
|
382
424
|
else if (checkResult?.valid && checkResult.type && schema) {
|
|
383
425
|
const celType = checkResult.type.split("<")[0];
|
|
384
426
|
if (!celTypeSatisfiesJsonSchema(celType, schema)) {
|
|
427
|
+
const expected = schema["x-telo-type"] ?? schema.type ?? "unknown";
|
|
385
428
|
issues.push({
|
|
386
|
-
message: `CEL returns '${checkResult.type}' but field expects '${
|
|
429
|
+
message: `CEL returns '${checkResult.type}' but field expects '${expected}'`,
|
|
387
430
|
path,
|
|
388
431
|
});
|
|
389
432
|
}
|
|
@@ -394,13 +437,13 @@ function collectCelTypeIssues(data, schema, path, definition, manifest, baseType
|
|
|
394
437
|
if (Array.isArray(data)) {
|
|
395
438
|
const itemSchema = (schema.items ?? {});
|
|
396
439
|
for (let i = 0; i < data.length; i++) {
|
|
397
|
-
issues.push(...collectCelTypeIssues(data[i], itemSchema, `${path}[${i}]`, definition, manifest, baseTypedEnv, rootEnv));
|
|
440
|
+
issues.push(...collectCelTypeIssues(data[i], itemSchema, `${path}[${i}]`, definition, manifest, baseTypedEnv, rootEnv, rootModuleManifest));
|
|
398
441
|
}
|
|
399
442
|
}
|
|
400
443
|
else if (data !== null && typeof data === "object") {
|
|
401
444
|
const props = (schema.properties ?? {});
|
|
402
445
|
for (const [k, v] of Object.entries(data)) {
|
|
403
|
-
issues.push(...collectCelTypeIssues(v, (props[k] ?? {}), path ? `${path}.${k}` : k, definition, manifest, baseTypedEnv, rootEnv));
|
|
446
|
+
issues.push(...collectCelTypeIssues(v, (props[k] ?? {}), path ? `${path}.${k}` : k, definition, manifest, baseTypedEnv, rootEnv, rootModuleManifest));
|
|
404
447
|
}
|
|
405
448
|
}
|
|
406
449
|
return issues;
|
|
@@ -468,9 +511,12 @@ export class StaticAnalyzer {
|
|
|
468
511
|
// through the same alias machinery as user-declared Telo.Imports —
|
|
469
512
|
// honours the library's `exports.kinds` list, no special cases.
|
|
470
513
|
if (moduleName) {
|
|
471
|
-
|
|
514
|
+
// `Self` resolves the library's own kinds UNGATED — a library may reference
|
|
515
|
+
// its own kinds regardless of `exports.kinds`, which gates importers, not
|
|
516
|
+
// internal use. This is what lets a library declare an instance of a kind it
|
|
517
|
+
// does not export (e.g. console's `writeLine`) to enforce a singleton.
|
|
472
518
|
if (rootModules.has(moduleName)) {
|
|
473
|
-
aliases.registerImport("Self", moduleName,
|
|
519
|
+
aliases.registerImport("Self", moduleName, []);
|
|
474
520
|
}
|
|
475
521
|
else {
|
|
476
522
|
let libResolver = aliasesByModule.get(moduleName);
|
|
@@ -478,7 +524,7 @@ export class StaticAnalyzer {
|
|
|
478
524
|
libResolver = new AliasResolver();
|
|
479
525
|
aliasesByModule.set(moduleName, libResolver);
|
|
480
526
|
}
|
|
481
|
-
libResolver.registerImport("Self", moduleName,
|
|
527
|
+
libResolver.registerImport("Self", moduleName, []);
|
|
482
528
|
}
|
|
483
529
|
}
|
|
484
530
|
}
|
|
@@ -625,6 +671,11 @@ export class StaticAnalyzer {
|
|
|
625
671
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
626
672
|
// recognises variables, secrets, resources, env automatically
|
|
627
673
|
const kernelGlobals = buildKernelGlobalsSchema(allManifests);
|
|
674
|
+
// The module doc (Application/Library) carries the Application-only `ports`
|
|
675
|
+
// namespace; threaded into per-resource CEL typing so `${{ ports.X }}`
|
|
676
|
+
// resolves its nominal brand cross-doc.
|
|
677
|
+
const moduleManifest = allManifests.find((mm) => mm.kind === "Telo.Application") ??
|
|
678
|
+
allManifests.find((mm) => mm.kind === "Telo.Library");
|
|
628
679
|
// Validate each non-definition, non-system resource
|
|
629
680
|
for (const m of allManifests) {
|
|
630
681
|
const filePath = m.metadata?.source;
|
|
@@ -645,6 +696,16 @@ export class StaticAnalyzer {
|
|
|
645
696
|
if (m.kind === "Telo.Abstract") {
|
|
646
697
|
continue;
|
|
647
698
|
}
|
|
699
|
+
// Forwarded foreign exports (an imported library's exported instances, whose
|
|
700
|
+
// metadata.module isn't a root module here) were already validated in their own
|
|
701
|
+
// module's standalone analysis, and their `kind`/CEL are authored in that module's
|
|
702
|
+
// scope (e.g. `Self.X` resolves to that module, not the consumer). Re-validating them
|
|
703
|
+
// against the consumer's scope yields false UNDEFINED_KIND / scope-mismatch errors, so
|
|
704
|
+
// skip — they participate in this analysis only as cross-module resolution targets.
|
|
705
|
+
const mModule = m.metadata?.module;
|
|
706
|
+
if (typeof mModule === "string" && !rootModules.has(mModule)) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
648
709
|
const resource = { kind: m.kind, name: m.metadata?.name };
|
|
649
710
|
// Resolve kind through alias if needed; direct lookup takes priority so that
|
|
650
711
|
// aliases whose name matches the module name (the common case) work without
|
|
@@ -678,8 +739,8 @@ export class StaticAnalyzer {
|
|
|
678
739
|
}
|
|
679
740
|
: definition.schema;
|
|
680
741
|
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
681
|
-
const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m);
|
|
682
|
-
const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseTypedEnv, this.celEnv);
|
|
742
|
+
const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
743
|
+
const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseTypedEnv, this.celEnv, moduleManifest);
|
|
683
744
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
684
745
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
685
746
|
const issues = [...celIssues, ...ajvIssues];
|
|
@@ -791,42 +852,54 @@ export class StaticAnalyzer {
|
|
|
791
852
|
}
|
|
792
853
|
}
|
|
793
854
|
}
|
|
794
|
-
// Validate CEL syntax and context variable access in all manifests
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
855
|
+
// Validate CEL syntax and context variable access in all manifests. The
|
|
856
|
+
// walker discovers every compiled CEL node by scanning the value tree and
|
|
857
|
+
// hands back the `x-telo-context` schema matched at the enclosing path; the
|
|
858
|
+
// per-path resolution (step context, kernel-globals merge, x-telo-context-*
|
|
859
|
+
// annotation resolution) stays here because it depends on analyzer-internal
|
|
860
|
+
// state (definitions, aliases, the typed CEL env).
|
|
861
|
+
// Per-resource state computed at enter and read by that resource's CEL
|
|
862
|
+
// sites. The manifest / resource / filePath come straight off each CelSite's
|
|
863
|
+
// `source` (no need to capture them); only the derived step / invocation
|
|
864
|
+
// context — which require analyzer state to build — are stashed here.
|
|
865
|
+
let celStepContextSchema;
|
|
866
|
+
let celInvocationContext;
|
|
867
|
+
let celErrorScopes = new Map();
|
|
868
|
+
visitManifest(allManifests, defs, {
|
|
869
|
+
onResourceEnter: (e) => {
|
|
870
|
+
const m = e.source;
|
|
871
|
+
celInvocationContext = m.metadata?.xTeloInvocationContext;
|
|
872
|
+
celStepContextSchema = e.definition?.schema
|
|
873
|
+
? buildStepContextSchema(m, e.definition.schema, allManifests, defs, aliases)
|
|
874
|
+
: undefined;
|
|
875
|
+
celErrorScopes = collectErrorContextScopes(e.definition?.schema);
|
|
876
|
+
},
|
|
877
|
+
onCel: (e) => {
|
|
878
|
+
const m = e.source;
|
|
879
|
+
const resource = { kind: m.kind, name: m.metadata?.name };
|
|
880
|
+
const filePath = m.metadata?.source;
|
|
881
|
+
const { expr, path, engineName, matchedScope } = e;
|
|
882
|
+
let matchedContext = e.contextSchema ?? celInvocationContext;
|
|
883
|
+
if (celStepContextSchema) {
|
|
884
|
+
const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
885
|
+
matchedContext = {
|
|
886
|
+
...base,
|
|
887
|
+
properties: {
|
|
888
|
+
...(base.properties ?? {}),
|
|
889
|
+
steps: celStepContextSchema,
|
|
890
|
+
},
|
|
891
|
+
};
|
|
820
892
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
893
|
+
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
894
|
+
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
895
|
+
const errorSchema = celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
|
|
896
|
+
if (errorSchema) {
|
|
824
897
|
const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
825
898
|
matchedContext = {
|
|
826
899
|
...base,
|
|
827
900
|
properties: {
|
|
828
901
|
...(base.properties ?? {}),
|
|
829
|
-
|
|
902
|
+
error: errorSchema,
|
|
830
903
|
},
|
|
831
904
|
};
|
|
832
905
|
}
|
|
@@ -877,6 +950,15 @@ export class StaticAnalyzer {
|
|
|
877
950
|
data: { resource, filePath, path },
|
|
878
951
|
});
|
|
879
952
|
}
|
|
953
|
+
else if (f.code === "CEL_NULLABLE_ACCESS") {
|
|
954
|
+
diagnostics.push({
|
|
955
|
+
severity: DiagnosticSeverity.Error,
|
|
956
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
957
|
+
source: SOURCE,
|
|
958
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
959
|
+
data: { resource, filePath, path },
|
|
960
|
+
});
|
|
961
|
+
}
|
|
880
962
|
else {
|
|
881
963
|
// Unknown code from a future engine — pass the message through,
|
|
882
964
|
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
|
@@ -890,8 +972,8 @@ export class StaticAnalyzer {
|
|
|
890
972
|
});
|
|
891
973
|
}
|
|
892
974
|
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
975
|
+
},
|
|
976
|
+
}, { aliases });
|
|
895
977
|
// Validate resource references (Phase 3)
|
|
896
978
|
diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs, aliasesByModule }));
|
|
897
979
|
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
@@ -900,6 +982,8 @@ export class StaticAnalyzer {
|
|
|
900
982
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
901
983
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
902
984
|
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
985
|
+
// Warn about declared variables / secrets / ports that no CEL references.
|
|
986
|
+
diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
|
|
903
987
|
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
904
988
|
// the chain root so position-index lookups land on the parent doc.
|
|
905
989
|
return rewriteSyntheticOrigins(diagnostics, allManifests);
|
|
@@ -907,13 +991,17 @@ export class StaticAnalyzer {
|
|
|
907
991
|
analyzeErrors(manifests, options, registry) {
|
|
908
992
|
return this.analyze(manifests, options, registry).filter((d) => d.severity === DiagnosticSeverity.Error);
|
|
909
993
|
}
|
|
910
|
-
normalize(manifests, registry
|
|
994
|
+
normalize(manifests, registry,
|
|
995
|
+
// Forwarded foreign exports used only as cross-module resolution targets (see
|
|
996
|
+
// resolveRefSentinels). The kernel passes its analyzer-flattened set so the
|
|
997
|
+
// entry-only runtime pass can still resolve `!ref Alias.name`.
|
|
998
|
+
crossModuleTargets) {
|
|
911
999
|
const ctx = registry._context();
|
|
912
1000
|
const normalized = normalizeInlineResources(manifests, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
|
|
913
1001
|
// Resolve !ref sentinels after normalize so both the original and
|
|
914
1002
|
// inline-extracted manifests get their refs canonicalized to
|
|
915
1003
|
// {kind, name} for the kernel that consumes this output.
|
|
916
|
-
resolveRefSentinels(normalized, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
|
|
1004
|
+
resolveRefSentinels(normalized, ctx.definitions, ctx.aliases, ctx.aliasesByModule, crossModuleTargets ?? []);
|
|
917
1005
|
return normalized;
|
|
918
1006
|
}
|
|
919
1007
|
prepare(manifests, registry) {
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAuZ/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -232,6 +232,66 @@ export const KERNEL_BUILTINS = [
|
|
|
232
232
|
},
|
|
233
233
|
additionalProperties: true,
|
|
234
234
|
},
|
|
235
|
+
// Gated reference: run() a Runnable/Service only when the
|
|
236
|
+
// `when` CEL guard holds. Discriminated by the `ref` key. `ref`
|
|
237
|
+
// is a bare name or a resolved `!ref` (`{ kind, name }`).
|
|
238
|
+
{
|
|
239
|
+
type: "object",
|
|
240
|
+
required: ["ref"],
|
|
241
|
+
properties: {
|
|
242
|
+
ref: {
|
|
243
|
+
anyOf: [
|
|
244
|
+
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
245
|
+
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
246
|
+
{
|
|
247
|
+
type: "object",
|
|
248
|
+
required: ["kind", "name"],
|
|
249
|
+
properties: {
|
|
250
|
+
kind: { type: "string" },
|
|
251
|
+
name: { type: "string" },
|
|
252
|
+
},
|
|
253
|
+
additionalProperties: true,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
when: { type: "string" },
|
|
258
|
+
},
|
|
259
|
+
additionalProperties: false,
|
|
260
|
+
},
|
|
261
|
+
// Inline flat invoke step: invoke an Invocable / Runnable on boot
|
|
262
|
+
// with an optional `name` (for steps.<name>.result plumbing),
|
|
263
|
+
// `when` guard, and `inputs`. Discriminated by the `invoke` key.
|
|
264
|
+
// Control flow (if/while/switch/try) is not available here —
|
|
265
|
+
// reach for Run.Sequence. `invoke` is ref-only and must resolve
|
|
266
|
+
// to a `{ kind, name }` reference (a `!ref` / `{kind,name}`):
|
|
267
|
+
// requiring `name` rejects an inline `{ kind }` definition (no
|
|
268
|
+
// name) at analysis instead of failing at boot with an undefined
|
|
269
|
+
// resource name. The Invocable/Runnable kind set mirrors
|
|
270
|
+
// Run.Sequence invoke steps.
|
|
271
|
+
{
|
|
272
|
+
type: "object",
|
|
273
|
+
required: ["invoke"],
|
|
274
|
+
properties: {
|
|
275
|
+
name: { type: "string" },
|
|
276
|
+
invoke: {
|
|
277
|
+
"x-telo-topology-role": "invoke",
|
|
278
|
+
type: "object",
|
|
279
|
+
required: ["kind", "name"],
|
|
280
|
+
properties: {
|
|
281
|
+
kind: { type: "string" },
|
|
282
|
+
name: { type: "string" },
|
|
283
|
+
},
|
|
284
|
+
additionalProperties: true,
|
|
285
|
+
anyOf: [
|
|
286
|
+
{ "x-telo-ref": "telo#Invocable" },
|
|
287
|
+
{ "x-telo-ref": "telo#Runnable" },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
inputs: { type: "object", additionalProperties: true },
|
|
291
|
+
when: { type: "string" },
|
|
292
|
+
},
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
},
|
|
235
295
|
],
|
|
236
296
|
},
|
|
237
297
|
},
|
|
@@ -277,6 +337,31 @@ export const KERNEL_BUILTINS = [
|
|
|
277
337
|
},
|
|
278
338
|
},
|
|
279
339
|
},
|
|
340
|
+
// Inbound ports the Application listens on. A name-keyed map mirroring
|
|
341
|
+
// `variables`: each entry binds a host env var (`env:`) that supplies a
|
|
342
|
+
// port integer (implicitly typed `integer`, 1–65535), with an optional
|
|
343
|
+
// `default:` used when the env var is unset. `protocol:` (default `tcp`)
|
|
344
|
+
// selects the transport — the runner reads this list to know the
|
|
345
|
+
// exposed ports before launch, and the analyzer brands the resolved
|
|
346
|
+
// `ports.<name>` value (tcp → TcpPort, udp → UdpPort) for static wiring
|
|
347
|
+
// checks. Application-only. See kernel/nodejs/src/application-env.ts.
|
|
348
|
+
ports: {
|
|
349
|
+
type: "object",
|
|
350
|
+
additionalProperties: {
|
|
351
|
+
type: "object",
|
|
352
|
+
required: ["env"],
|
|
353
|
+
properties: {
|
|
354
|
+
env: { type: "string" },
|
|
355
|
+
protocol: {
|
|
356
|
+
type: "string",
|
|
357
|
+
enum: ["tcp", "udp"],
|
|
358
|
+
default: "tcp",
|
|
359
|
+
},
|
|
360
|
+
default: { type: "integer", minimum: 1, maximum: 65535 },
|
|
361
|
+
},
|
|
362
|
+
additionalProperties: false,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
280
365
|
},
|
|
281
366
|
required: ["metadata"],
|
|
282
367
|
additionalProperties: false,
|
|
@@ -311,6 +396,7 @@ export const KERNEL_BUILTINS = [
|
|
|
311
396
|
type: "object",
|
|
312
397
|
properties: {
|
|
313
398
|
kinds: { type: "array", items: { type: "string" } },
|
|
399
|
+
resources: { type: "array", items: { type: "string" } },
|
|
314
400
|
},
|
|
315
401
|
additionalProperties: true,
|
|
316
402
|
},
|
|
@@ -12,5 +12,5 @@ export type { CelHandlers } from "@telorun/templating";
|
|
|
12
12
|
*
|
|
13
13
|
* NOTE: The set of kernel globals registered here must match `KERNEL_GLOBAL_NAMES`
|
|
14
14
|
* in kernel-globals.ts, which is used for chain-access validation. */
|
|
15
|
-
export declare function buildTypedCelEnvironment(baseEnv: Environment, manifest: ResourceManifest, extraContextSchema?: Record<string, any> | null): Environment;
|
|
15
|
+
export declare function buildTypedCelEnvironment(baseEnv: Environment, manifest: ResourceManifest, extraContextSchema?: Record<string, any> | null, rootModuleManifest?: ResourceManifest): Environment;
|
|
16
16
|
//# sourceMappingURL=cel-environment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;uEASuE;AACvE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAI/C,kBAAkB,CAAC,EAAE,gBAAgB,GACpC,WAAW,CAuEb"}
|
package/dist/cel-environment.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
1
|
+
import { jsonSchemaToCelType, VALUE_BRAND_BASE } from "./schema-compat.js";
|
|
2
|
+
/** Transport protocol on a `ports` entry → the nominal CEL brand its resolved
|
|
3
|
+
* value carries. Mirrors the `protocol` enum in the Application schema. */
|
|
4
|
+
const PORT_PROTOCOL_BRAND = {
|
|
5
|
+
tcp: "TcpPort",
|
|
6
|
+
udp: "UdpPort",
|
|
7
|
+
};
|
|
2
8
|
export { buildCelEnvironment } from "@telorun/templating";
|
|
3
9
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
4
10
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
@@ -10,9 +16,21 @@ export { buildCelEnvironment } from "@telorun/templating";
|
|
|
10
16
|
*
|
|
11
17
|
* NOTE: The set of kernel globals registered here must match `KERNEL_GLOBAL_NAMES`
|
|
12
18
|
* in kernel-globals.ts, which is used for chain-access validation. */
|
|
13
|
-
export function buildTypedCelEnvironment(baseEnv, manifest, extraContextSchema
|
|
19
|
+
export function buildTypedCelEnvironment(baseEnv, manifest, extraContextSchema,
|
|
20
|
+
// The `ports` namespace is Application-only and lives on the module doc, not
|
|
21
|
+
// on the resource being analyzed. When validating a resource, the caller
|
|
22
|
+
// passes the module manifest here so `${{ ports.X }}` types cross-doc.
|
|
23
|
+
rootModuleManifest) {
|
|
14
24
|
try {
|
|
15
25
|
const env = baseEnv.clone();
|
|
26
|
+
// Register nominal value brands (TcpPort/UdpPort/…) on the *clone* so the
|
|
27
|
+
// type-checker can distinguish structurally-identical values. The base env
|
|
28
|
+
// (shared with the kernel runtime) is untouched — a branded value flows as
|
|
29
|
+
// a plain integer at runtime, so only static checking needs these. cel-js
|
|
30
|
+
// auto-generates a field-less wrapper class; no runtime constructor needed.
|
|
31
|
+
for (const brand of Object.keys(VALUE_BRAND_BASE)) {
|
|
32
|
+
env.registerType(brand, { fields: {} });
|
|
33
|
+
}
|
|
16
34
|
// Build typed ObjectSchema from manifest.variables if it looks like a schema map
|
|
17
35
|
const vars = manifest.variables;
|
|
18
36
|
if (vars !== null && typeof vars === "object" && !Array.isArray(vars)) {
|
|
@@ -31,6 +49,26 @@ export function buildTypedCelEnvironment(baseEnv, manifest, extraContextSchema)
|
|
|
31
49
|
else {
|
|
32
50
|
env.registerVariable("variables", "map");
|
|
33
51
|
}
|
|
52
|
+
// `ports` namespace: each entry types as the brand its `protocol` selects
|
|
53
|
+
// (tcp → TcpPort, udp → UdpPort), so `${{ ports.http }}` carries a nominal
|
|
54
|
+
// type that consuming fields can check against.
|
|
55
|
+
const portsManifest = (rootModuleManifest ?? manifest).ports;
|
|
56
|
+
if (portsManifest !== null && typeof portsManifest === "object" && !Array.isArray(portsManifest)) {
|
|
57
|
+
const portEntries = Object.entries(portsManifest).filter(([, v]) => v !== null && typeof v === "object" && !Array.isArray(v));
|
|
58
|
+
if (portEntries.length > 0) {
|
|
59
|
+
const schema = {};
|
|
60
|
+
for (const [k, v] of portEntries) {
|
|
61
|
+
schema[k] = PORT_PROTOCOL_BRAND[v.protocol ?? "tcp"] ?? "int";
|
|
62
|
+
}
|
|
63
|
+
env.registerVariable({ name: "ports", schema });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
env.registerVariable("ports", "map");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
env.registerVariable("ports", "map");
|
|
71
|
+
}
|
|
34
72
|
env.registerVariable("secrets", "map");
|
|
35
73
|
env.registerVariable("resources", "map");
|
|
36
74
|
env.registerVariable("env", "map");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B;kDAC8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACpC;oFACgF;IAChF,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrC;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,eAAe,
|
|
1
|
+
{"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B;kDAC8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACpC;oFACgF;IAChF,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrC;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,eAAe,CAmGjB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAOtE"}
|