@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.
Files changed (69) hide show
  1. package/dist/adapters/http-adapter.d.ts +10 -0
  2. package/dist/adapters/http-adapter.d.ts.map +1 -0
  3. package/dist/adapters/http-adapter.js +18 -0
  4. package/dist/adapters/node-adapter.d.ts +17 -0
  5. package/dist/adapters/node-adapter.d.ts.map +1 -0
  6. package/dist/adapters/node-adapter.js +71 -0
  7. package/dist/adapters/registry-adapter.d.ts +15 -0
  8. package/dist/adapters/registry-adapter.d.ts.map +1 -0
  9. package/dist/adapters/registry-adapter.js +53 -0
  10. package/dist/alias-resolver.d.ts +6 -0
  11. package/dist/alias-resolver.d.ts.map +1 -1
  12. package/dist/alias-resolver.js +8 -0
  13. package/dist/analysis-registry.d.ts +13 -0
  14. package/dist/analysis-registry.d.ts.map +1 -1
  15. package/dist/analysis-registry.js +15 -0
  16. package/dist/analyzer.d.ts +1 -1
  17. package/dist/analyzer.d.ts.map +1 -1
  18. package/dist/analyzer.js +176 -88
  19. package/dist/builtins.d.ts.map +1 -1
  20. package/dist/builtins.js +86 -0
  21. package/dist/cel-environment.d.ts +1 -1
  22. package/dist/cel-environment.d.ts.map +1 -1
  23. package/dist/cel-environment.js +40 -2
  24. package/dist/dependency-graph.d.ts.map +1 -1
  25. package/dist/dependency-graph.js +41 -62
  26. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  27. package/dist/flatten-for-analyzer.js +13 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/kernel-globals.d.ts +1 -1
  32. package/dist/kernel-globals.d.ts.map +1 -1
  33. package/dist/kernel-globals.js +19 -1
  34. package/dist/manifest-visitor.d.ts +124 -0
  35. package/dist/manifest-visitor.d.ts.map +1 -0
  36. package/dist/manifest-visitor.js +181 -0
  37. package/dist/reference-field-map.js +16 -0
  38. package/dist/resolve-ref-sentinels.d.ts +15 -17
  39. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  40. package/dist/resolve-ref-sentinels.js +94 -40
  41. package/dist/resolve-throws-union.d.ts +10 -0
  42. package/dist/resolve-throws-union.d.ts.map +1 -1
  43. package/dist/resolve-throws-union.js +35 -7
  44. package/dist/schema-compat.d.ts +10 -0
  45. package/dist/schema-compat.d.ts.map +1 -1
  46. package/dist/schema-compat.js +32 -0
  47. package/dist/validate-cel-context.d.ts +14 -0
  48. package/dist/validate-cel-context.d.ts.map +1 -1
  49. package/dist/validate-cel-context.js +38 -0
  50. package/dist/validate-references.d.ts.map +1 -1
  51. package/dist/validate-references.js +215 -160
  52. package/dist/validate-unused-declarations.d.ts +25 -0
  53. package/dist/validate-unused-declarations.d.ts.map +1 -0
  54. package/dist/validate-unused-declarations.js +91 -0
  55. package/package.json +3 -3
  56. package/src/analysis-registry.ts +20 -0
  57. package/src/analyzer.ts +256 -168
  58. package/src/builtins.ts +85 -0
  59. package/src/cel-environment.ts +42 -1
  60. package/src/dependency-graph.ts +37 -52
  61. package/src/index.ts +11 -0
  62. package/src/kernel-globals.ts +22 -1
  63. package/src/manifest-visitor.ts +340 -0
  64. package/src/reference-field-map.ts +14 -0
  65. package/src/resolve-throws-union.ts +36 -8
  66. package/src/schema-compat.ts +32 -0
  67. package/src/validate-cel-context.ts +50 -0
  68. package/src/validate-references.ts +175 -211
  69. package/src/validate-unused-declarations.ts +95 -0
package/dist/analyzer.js CHANGED
@@ -1,10 +1,11 @@
1
- import { defaultRegistry, walkCelExpressions } from "@telorun/templating";
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
- if (typeof data === "string" && CEL_PURE_RE.test(data)) {
353
- const exprMatch = data.match(CEL_EXPR_RE);
354
- if (exprMatch) {
355
- const expr = exprMatch[1].trim();
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 '${schema.type ?? "unknown"}'`,
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
- const exportedKinds = m.exports?.kinds ?? [];
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, exportedKinds);
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, exportedKinds);
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
- for (const m of allManifests) {
796
- const resource = { kind: m.kind, name: m.metadata?.name };
797
- const filePath = m.metadata?.source;
798
- const resolvedKind = aliases.resolveKind(m.kind);
799
- const mDefinition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
800
- // Pre-compute step context for manifests with x-telo-step-context
801
- const stepContextSchema = mDefinition?.schema
802
- ? buildStepContextSchema(m, mDefinition.schema, allManifests, defs, aliases)
803
- : undefined;
804
- walkCelExpressions(m, "", (expr, path, engineName) => {
805
- // Resolve the effective context for this expression's path. The
806
- // engine receives a single closed schema and validates member-access
807
- // chains against it; per-path resolution (step context, x-telo-context,
808
- // kernel-globals merge) stays on the analyzer side because it depends
809
- // on analyzer-internal state (definitions, aliases).
810
- const contexts = mDefinition?.schema ? extractContextsFromSchema(mDefinition.schema) : [];
811
- const invocationContext = m.metadata?.xTeloInvocationContext;
812
- let matchedContext;
813
- let matchedScope;
814
- for (const ctx of contexts) {
815
- if (pathMatchesScope(path, ctx.scope)) {
816
- matchedContext = ctx.schema;
817
- matchedScope = ctx.scope;
818
- break;
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
- if (!matchedContext)
822
- matchedContext = invocationContext;
823
- if (stepContextSchema) {
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
- steps: stepContextSchema,
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) {
@@ -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,EAiU/C,CAAC"}
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;AAGrD,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,GAC9C,WAAW,CAyCb"}
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"}
@@ -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,CAkHjB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAOtE"}
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"}