@telorun/analyzer 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +3 -3
  2. package/dist/analyzer.d.ts +6 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +45 -25
  5. package/dist/builtins.d.ts.map +1 -1
  6. package/dist/builtins.js +52 -24
  7. package/dist/cel-environment.d.ts +12 -5
  8. package/dist/cel-environment.d.ts.map +1 -1
  9. package/dist/cel-environment.js +31 -17
  10. package/dist/definition-registry.d.ts +5 -5
  11. package/dist/definition-registry.d.ts.map +1 -1
  12. package/dist/definition-registry.js +10 -10
  13. package/dist/dependency-graph.d.ts.map +1 -1
  14. package/dist/dependency-graph.js +9 -2
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/kernel-globals.d.ts +6 -2
  19. package/dist/kernel-globals.d.ts.map +1 -1
  20. package/dist/kernel-globals.js +14 -8
  21. package/dist/manifest-loader.d.ts +1 -0
  22. package/dist/manifest-loader.d.ts.map +1 -1
  23. package/dist/manifest-loader.js +36 -14
  24. package/dist/module-kinds.d.ts +4 -0
  25. package/dist/module-kinds.d.ts.map +1 -0
  26. package/dist/module-kinds.js +4 -0
  27. package/dist/normalize-inline-resources.d.ts.map +1 -1
  28. package/dist/normalize-inline-resources.js +6 -1
  29. package/dist/precompile.d.ts +3 -2
  30. package/dist/precompile.d.ts.map +1 -1
  31. package/dist/precompile.js +13 -11
  32. package/dist/reference-field-map.d.ts +1 -1
  33. package/dist/resolve-throws-union.d.ts +30 -0
  34. package/dist/resolve-throws-union.d.ts.map +1 -0
  35. package/dist/resolve-throws-union.js +252 -0
  36. package/dist/types.d.ts +3 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/validate-cel-context.js +1 -1
  39. package/dist/validate-references.d.ts.map +1 -1
  40. package/dist/validate-references.js +19 -12
  41. package/dist/validate-throws-coverage.d.ts +8 -0
  42. package/dist/validate-throws-coverage.d.ts.map +1 -0
  43. package/dist/validate-throws-coverage.js +461 -0
  44. package/package.json +2 -2
  45. package/src/analyzer.ts +60 -26
  46. package/src/builtins.ts +52 -24
  47. package/src/cel-environment.ts +40 -17
  48. package/src/definition-registry.ts +10 -10
  49. package/src/dependency-graph.ts +9 -2
  50. package/src/index.ts +2 -0
  51. package/src/kernel-globals.ts +19 -10
  52. package/src/manifest-loader.ts +40 -14
  53. package/src/module-kinds.ts +6 -0
  54. package/src/normalize-inline-resources.ts +6 -1
  55. package/src/precompile.ts +14 -11
  56. package/src/reference-field-map.ts +1 -1
  57. package/src/resolve-throws-union.ts +345 -0
  58. package/src/types.ts +3 -0
  59. package/src/validate-cel-context.ts +1 -1
  60. package/src/validate-references.ts +19 -12
  61. package/src/validate-throws-coverage.ts +565 -0
  62. package/dist/adapters/node-adapter.d.ts +0 -17
  63. package/dist/adapters/node-adapter.d.ts.map +0 -1
  64. package/dist/adapters/node-adapter.js +0 -71
package/dist/types.d.ts CHANGED
@@ -67,6 +67,9 @@ export interface LoaderInitOptions {
67
67
  includeRegistryAdapter?: boolean;
68
68
  /** Base URL used by built-in RegistryAdapter when enabled. */
69
69
  registryUrl?: string;
70
+ /** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
71
+ * omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
72
+ celHandlers?: import("./cel-environment.js").CelHandlers;
70
73
  }
71
74
  export interface AnalysisOptions {
72
75
  strictContexts?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;sEAEkE;IAClE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;sEAEkE;IAClE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;CACrE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;sEAEkE;IAClE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;sEAEkE;IAClE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;CACrE"}
@@ -8,7 +8,7 @@ export function resolveTypeFieldToSchema(value, allManifests) {
8
8
  if (!value)
9
9
  return undefined;
10
10
  if (typeof value === "string") {
11
- // Named type reference — find a Kernel.Type resource by name
11
+ // Named type reference — find a Telo.Type resource by name
12
12
  const typeManifest = allManifests.find((m) => m.metadata?.name === value &&
13
13
  typeof m.kind === "string" &&
14
14
  /\bType\b/.test(m.kind) &&
@@ -1 +1 @@
1
- {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA6C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAgQtB"}
1
+ {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAkQtB"}
@@ -2,7 +2,12 @@ import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveF
2
2
  import { navigateJsonPointer } from "./schema-compat.js";
3
3
  import { DiagnosticSeverity } from "./types.js";
4
4
  const SOURCE = "telo-analyzer";
5
- const SYSTEM_KINDS = new Set(["Kernel.Definition", "Kernel.Abstract"]);
5
+ /** Kinds skipped by reference validation. Telo.Application and Telo.Library
6
+ * are intentionally not here: Application has `targets` with x-telo-ref that
7
+ * must be validated, and Library has no ref-bearing fields so flows through
8
+ * harmlessly. Telo.Import is also not here for the same reason — its
9
+ * `source` field isn't x-telo-ref, so nothing gets checked. */
10
+ const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
6
11
  /**
7
12
  * Checks whether `kind` satisfies the ref constraint in `entry`.
8
13
  * Returns an empty array when valid, or mismatch error strings when not.
@@ -19,7 +24,7 @@ function checkKind(kind, entry, registry, aliases) {
19
24
  const targetDef = registry.resolve(targetKind);
20
25
  if (!targetDef)
21
26
  return [];
22
- if (targetDef.kind === "Kernel.Abstract") {
27
+ if (targetDef.kind === "Telo.Abstract") {
23
28
  const implementing = registry.getByExtends(targetKind);
24
29
  if (implementing.length === 0)
25
30
  return []; // partial context — no implementations loaded yet
@@ -59,7 +64,7 @@ export function validateReferences(resources, context) {
59
64
  if (!aliases || !registry)
60
65
  return diagnostics;
61
66
  // Build outer resource lookup by name for resolution check.
62
- // Exclude system kinds (Kernel.Definition) — they are type blueprints, not instances,
67
+ // Exclude system kinds (Telo.Definition) — they are type blueprints, not instances,
63
68
  // and their names (e.g. "Server", "Job") would shadow user-defined resource instances.
64
69
  const byName = new Map();
65
70
  for (const r of resources) {
@@ -74,6 +79,7 @@ export function validateReferences(resources, context) {
74
79
  continue;
75
80
  const resourceLabel = `${r.kind}/${r.metadata.name}`;
76
81
  const resourceData = { kind: r.kind, name: r.metadata.name };
82
+ const filePath = r.metadata?.source;
77
83
  // Collect scope visibility prefixes (JSON Pointer → dot prefix) and their manifests.
78
84
  // scope field path → flat array of ResourceManifest declared in that scope.
79
85
  const scopeManifestsByPointer = new Map();
@@ -131,7 +137,7 @@ export function validateReferences(resources, context) {
131
137
  code: "UNRESOLVED_REFERENCE",
132
138
  source: SOURCE,
133
139
  message: `${resourceLabel}: reference at '${fieldPath}' → resource '${val}' not found`,
134
- data: { resource: resourceData, path: fieldPath },
140
+ data: { resource: resourceData, filePath, path: fieldPath },
135
141
  });
136
142
  continue;
137
143
  }
@@ -142,7 +148,7 @@ export function validateReferences(resources, context) {
142
148
  code: "REFERENCE_KIND_MISMATCH",
143
149
  source: SOURCE,
144
150
  message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
145
- data: { resource: resourceData, path: fieldPath },
151
+ data: { resource: resourceData, filePath, path: fieldPath },
146
152
  });
147
153
  }
148
154
  continue;
@@ -160,7 +166,7 @@ export function validateReferences(resources, context) {
160
166
  code: "INVALID_REFERENCE",
161
167
  source: SOURCE,
162
168
  message: `${resourceLabel}: reference at '${fieldPath}' must have string 'kind' and 'name' fields`,
163
- data: { resource: resourceData, path: fieldPath },
169
+ data: { resource: resourceData, filePath, path: fieldPath },
164
170
  });
165
171
  continue;
166
172
  }
@@ -172,7 +178,7 @@ export function validateReferences(resources, context) {
172
178
  code: "REFERENCE_KIND_MISMATCH",
173
179
  source: SOURCE,
174
180
  message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
175
- data: { resource: resourceData, path: fieldPath },
181
+ data: { resource: resourceData, filePath, path: fieldPath },
176
182
  });
177
183
  }
178
184
  // 3. Resolution check — resource with this name must exist.
@@ -184,7 +190,7 @@ export function validateReferences(resources, context) {
184
190
  code: "UNRESOLVED_REFERENCE",
185
191
  source: SOURCE,
186
192
  message: `${resourceLabel}: reference at '${fieldPath}' → resource '${refVal.name}' not found`,
187
- data: { resource: resourceData, path: fieldPath },
193
+ data: { resource: resourceData, filePath, path: fieldPath },
188
194
  });
189
195
  }
190
196
  }
@@ -202,6 +208,7 @@ export function validateReferences(resources, context) {
202
208
  continue;
203
209
  const resourceLabel = `${r.kind}/${r.metadata.name}`;
204
210
  const resourceData = { kind: r.kind, name: r.metadata.name };
211
+ const filePath = r.metadata?.source;
205
212
  for (const [fieldPath, entry] of fieldMap) {
206
213
  if (!isSchemaFromEntry(entry))
207
214
  continue;
@@ -215,7 +222,7 @@ export function validateReferences(resources, context) {
215
222
  code: "INVALID_SCHEMA_FROM",
216
223
  source: SOURCE,
217
224
  message: `${resourceLabel}: x-telo-schema-from "${schemaFrom}" must contain at least one "/" to separate anchor from JSON Pointer`,
218
- data: { resource: resourceData, path: fieldPath },
225
+ data: { resource: resourceData, filePath, path: fieldPath },
219
226
  });
220
227
  continue;
221
228
  }
@@ -255,7 +262,7 @@ export function validateReferences(resources, context) {
255
262
  code: "SCHEMA_FROM_MISSING_PATH",
256
263
  source: SOURCE,
257
264
  message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema`,
258
- data: { resource: resourceData, path: fieldPath },
265
+ data: { resource: resourceData, filePath, path: fieldPath },
259
266
  });
260
267
  continue;
261
268
  }
@@ -266,7 +273,7 @@ export function validateReferences(resources, context) {
266
273
  code: "SCHEMA_FROM_MISSING_PATH",
267
274
  source: SOURCE,
268
275
  message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
269
- data: { resource: resourceData, path: fieldPath },
276
+ data: { resource: resourceData, filePath, path: fieldPath },
270
277
  });
271
278
  continue;
272
279
  }
@@ -277,7 +284,7 @@ export function validateReferences(resources, context) {
277
284
  code: "DEPENDENT_SCHEMA_MISMATCH",
278
285
  source: SOURCE,
279
286
  message: `${resourceLabel}: '${fieldPath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
280
- data: { resource: resourceData, path: fieldPath },
287
+ data: { resource: resourceData, filePath, path: fieldPath },
281
288
  });
282
289
  }
283
290
  }
@@ -0,0 +1,8 @@
1
+ import type { Environment } from "@marcbachmann/cel-js";
2
+ import type { ResourceManifest } from "@telorun/sdk";
3
+ import type { AliasResolver } from "./alias-resolver.js";
4
+ import type { DefinitionRegistry } from "./definition-registry.js";
5
+ import { type AnalysisDiagnostic } from "./types.js";
6
+ /** Entry point — invoked once per analyze() run. */
7
+ export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment): AnalysisDiagnostic[];
8
+ //# sourceMappingURL=validate-throws-coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4dzE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,GACf,kBAAkB,EAAE,CAwCtB"}
@@ -0,0 +1,461 @@
1
+ import { createResolveCtx, resolveThrowsUnion, } from "./resolve-throws-union.js";
2
+ import { DiagnosticSeverity } from "./types.js";
3
+ import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
4
+ const SOURCE = "telo-analyzer";
5
+ const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
6
+ /** Walk `definition.schema` and `data` in tandem, invoking `onOutcome` each
7
+ * time an array schema annotated with `x-telo-outcome-list` is encountered.
8
+ * Keeps schema/data in lockstep so callers can resolve sibling fields. */
9
+ function collectOutcomeLists(manifest, schema, onReturns, onCatches) {
10
+ if (!schema)
11
+ return;
12
+ walkSchemaData(schema, manifest, "", {
13
+ manifest,
14
+ onReturns,
15
+ onCatches,
16
+ });
17
+ }
18
+ function walkSchemaData(schema, data, path, ctx) {
19
+ if (!schema || typeof schema !== "object")
20
+ return;
21
+ const outcomeKind = schema["x-telo-outcome-list"];
22
+ if (outcomeKind && Array.isArray(data)) {
23
+ // The sibling data is the parent object (not reachable here without tracking).
24
+ // collectOutcomeListsInObject passes the parent; this branch is a fallback
25
+ // for top-level outcome lists (never occurs in practice).
26
+ if (outcomeKind === "returns") {
27
+ ctx.onReturns({ manifest: ctx.manifest, entries: data, arrayPath: path });
28
+ }
29
+ return;
30
+ }
31
+ if (schema.properties && typeof data === "object" && data !== null && !Array.isArray(data)) {
32
+ const dataObj = data;
33
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
34
+ const nextPath = path ? `${path}.${key}` : key;
35
+ const child = dataObj[key];
36
+ const outcome = propSchema["x-telo-outcome-list"];
37
+ if (outcome) {
38
+ const entries = Array.isArray(child) ? child : [];
39
+ if (outcome === "returns") {
40
+ // Only fire for present arrays — rule 6 (missing returns) is
41
+ // enforced by schema required fields, not here.
42
+ if (Array.isArray(child)) {
43
+ ctx.onReturns({ manifest: ctx.manifest, entries, arrayPath: nextPath });
44
+ }
45
+ }
46
+ else {
47
+ const catchesFor = propSchema["x-telo-catches-for"];
48
+ if (catchesFor) {
49
+ // Fire even when absent so the coverage check can flag handlers
50
+ // whose declared union is non-empty but the list is missing.
51
+ ctx.onCatches(entries, nextPath, dataObj, catchesFor);
52
+ }
53
+ }
54
+ continue;
55
+ }
56
+ if (child !== undefined)
57
+ walkSchemaData(propSchema, child, nextPath, ctx);
58
+ }
59
+ }
60
+ if (schema.items && Array.isArray(data)) {
61
+ for (const [i, item] of data.entries()) {
62
+ walkSchemaData(schema.items, item, `${path}[${i}]`, ctx);
63
+ }
64
+ }
65
+ }
66
+ /** Read a referenced handler's `{kind, name}` from a sibling field. Handles
67
+ * both `"Alias.Kind"` strings and `{ kind, name? }` objects. */
68
+ function resolveHandlerRef(sibling) {
69
+ if (!sibling)
70
+ return null;
71
+ if (typeof sibling === "string")
72
+ return { kind: sibling };
73
+ if (typeof sibling === "object") {
74
+ const obj = sibling;
75
+ if (typeof obj.kind === "string") {
76
+ return { kind: obj.kind, name: obj.name };
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ /** Parse a `when:` CEL expression and extract the set of `error.code` literals
82
+ * it covers. Recognised forms (per the plan's "coverage-proving" list):
83
+ * - `error.code == 'FOO'`
84
+ * - a disjunction of the above (`||`)
85
+ * - `error.code in ['FOO', 'BAR']`
86
+ * Parenthesised nestings of `||` over equality/in are flattened.
87
+ * Any non-matching sub-expression forfeits coverage for the whole `when:`. */
88
+ function extractCoveredCodes(whenExpr, env) {
89
+ const match = whenExpr.match(/\$\{\{\s*([^}]+?)\s*\}\}/);
90
+ if (!match)
91
+ return { proven: false, codes: new Set() };
92
+ let ast;
93
+ try {
94
+ ast = env.parse(match[1].trim()).ast;
95
+ }
96
+ catch {
97
+ return { proven: false, codes: new Set() };
98
+ }
99
+ const codes = new Set();
100
+ const proven = extractFromNode(ast, codes);
101
+ return { proven, codes };
102
+ }
103
+ function extractFromNode(node, codes) {
104
+ if (node.op === "||") {
105
+ const [l, r] = node.args;
106
+ return extractFromNode(l, codes) && extractFromNode(r, codes);
107
+ }
108
+ if (node.op === "==") {
109
+ const [l, r] = node.args;
110
+ const lit = readErrorCodeEq(l, r) ?? readErrorCodeEq(r, l);
111
+ if (lit === null)
112
+ return false;
113
+ codes.add(lit);
114
+ return true;
115
+ }
116
+ if (node.op === "in") {
117
+ const [l, r] = node.args;
118
+ if (!isErrorCodeRef(l) || r.op !== "list")
119
+ return false;
120
+ for (const item of r.args) {
121
+ if (item.op !== "value" || typeof item.args !== "string")
122
+ return false;
123
+ codes.add(item.args);
124
+ }
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ function readErrorCodeEq(ref, lit) {
130
+ if (!isErrorCodeRef(ref))
131
+ return null;
132
+ if (lit.op !== "value" || typeof lit.args !== "string")
133
+ return null;
134
+ return lit.args;
135
+ }
136
+ function isErrorCodeRef(node) {
137
+ if (node.op !== ".")
138
+ return false;
139
+ const [obj, field] = node.args;
140
+ if (field !== "code")
141
+ return false;
142
+ return obj.op === "id" && obj.args === "error";
143
+ }
144
+ /** Rule 7: within an outcome list, a no-`when:` entry must be the last entry. */
145
+ function checkCatchAllPlacement(entries, resource, channel, filePath, arrayPath) {
146
+ const diagnostics = [];
147
+ for (let i = 0; i < entries.length - 1; i++) {
148
+ const e = entries[i];
149
+ if (!e?.when) {
150
+ diagnostics.push({
151
+ severity: DiagnosticSeverity.Error,
152
+ code: "CATCHALL_NOT_LAST",
153
+ source: SOURCE,
154
+ message: `${channel}: catch-all entry (no \`when:\`) at index ${i} must be last — entries after it are unreachable.`,
155
+ data: { resource, filePath, path: `${arrayPath}[${i}]` },
156
+ });
157
+ }
158
+ }
159
+ return diagnostics;
160
+ }
161
+ /** Rule 1 + Rule 4: check declared-union coverage and reject undeclared codes
162
+ * in coverage-proving `when:` clauses. Phase 2 accepts inherit/passthrough
163
+ * handler unions too — when the resolved union is unbounded, a catch-all is
164
+ * required (rule 4 extension). */
165
+ function checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env) {
166
+ const diagnostics = [];
167
+ const declaredCodes = new Set(union.codes.keys());
168
+ const covered = new Set();
169
+ let hasCatchAll = false;
170
+ for (let i = 0; i < entries.length; i++) {
171
+ const e = entries[i];
172
+ if (!e)
173
+ continue;
174
+ if (!e.when) {
175
+ hasCatchAll = true;
176
+ continue;
177
+ }
178
+ const { proven, codes } = extractCoveredCodes(e.when, env);
179
+ if (proven) {
180
+ for (const c of codes) {
181
+ if (!declaredCodes.has(c)) {
182
+ diagnostics.push({
183
+ severity: DiagnosticSeverity.Error,
184
+ code: "UNDECLARED_THROW_CODE",
185
+ source: SOURCE,
186
+ message: `catches[${i}] references code '${c}' which is not in the handler's declared throw union {${[...declaredCodes].sort().join(", ") || "∅"}}${union.unbounded ? " (union is unbounded — a catch-all is required)" : ""}.`,
187
+ data: { resource, filePath, path: `${arrayPath}[${i}].when` },
188
+ });
189
+ }
190
+ else {
191
+ covered.add(c);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // Unbounded union (passthrough or transitive): authors can't enumerate the
197
+ // codes, so a catch-all is mandatory.
198
+ if (union.unbounded && !hasCatchAll) {
199
+ diagnostics.push({
200
+ severity: DiagnosticSeverity.Error,
201
+ code: "UNBOUNDED_UNION_NEEDS_CATCHALL",
202
+ source: SOURCE,
203
+ message: `The handler's throw union is unbounded (inherit/passthrough resolution couldn't enumerate all codes). The catches: list must include a catch-all entry (no \`when:\`).`,
204
+ data: { resource, filePath, path: arrayPath },
205
+ });
206
+ }
207
+ if (!hasCatchAll) {
208
+ for (const code of declaredCodes) {
209
+ if (!covered.has(code)) {
210
+ diagnostics.push({
211
+ severity: DiagnosticSeverity.Error,
212
+ code: "UNCOVERED_THROW_CODE",
213
+ source: SOURCE,
214
+ message: `Code '${code}' is declared by the handler but not covered by any catches: entry (no matching \`when:\` and no catch-all).`,
215
+ data: { resource, filePath, path: arrayPath },
216
+ });
217
+ }
218
+ }
219
+ }
220
+ return diagnostics;
221
+ }
222
+ /** Rule 2: for each `error.data.<field>` chain in a catches entry's expressions,
223
+ * type-check against the data schema declared for the matched code(s). When the
224
+ * matching `when:` disjunctively covers multiple codes, use the intersection
225
+ * of their data schemas so only fields present on every code narrow through. */
226
+ function checkTypedErrorData(entries, union, resource, filePath, arrayPath, env) {
227
+ const diagnostics = [];
228
+ // If the union is unbounded we can't narrow data schemas reliably — skip
229
+ // typed-data checks for those entries. The catch-all path still provides
230
+ // runtime access to error.data as an opaque value.
231
+ if (union.unbounded || union.codes.size === 0)
232
+ return diagnostics;
233
+ const dataByCode = {};
234
+ for (const [code, meta] of union.codes) {
235
+ dataByCode[code] = meta.data;
236
+ }
237
+ const allCodes = Object.keys(dataByCode);
238
+ for (let i = 0; i < entries.length; i++) {
239
+ const e = entries[i];
240
+ if (!e)
241
+ continue;
242
+ const covered = e.when
243
+ ? extractCoveredCodes(e.when, env)
244
+ : { proven: false, codes: new Set() };
245
+ // Codes applicable to this entry:
246
+ // - coverage-proving `when:` → exactly those codes
247
+ // - catch-all (no `when:`) or non-proven expr → all declared codes
248
+ const applicable = covered.proven
249
+ ? [...covered.codes].filter((c) => c in dataByCode)
250
+ : allCodes;
251
+ if (applicable.length === 0)
252
+ continue;
253
+ const schemas = applicable.map((c) => dataByCode[c]).filter(Boolean);
254
+ if (schemas.length === 0)
255
+ continue;
256
+ const dataSchema = intersectDataSchemas(schemas);
257
+ // Walk CEL expressions inside this entry's body / headers — only
258
+ // string-valued fields can contain CEL templates.
259
+ collectCelStrings(e.body, `${arrayPath}[${i}].body`).forEach((entry) => {
260
+ diagnostics.push(...checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env));
261
+ });
262
+ if (e.headers) {
263
+ collectCelStrings(e.headers, `${arrayPath}[${i}].headers`).forEach((entry) => {
264
+ diagnostics.push(...checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env));
265
+ });
266
+ }
267
+ }
268
+ return diagnostics;
269
+ }
270
+ /** Intersection of JSON Schemas (object type, explicit properties only). Only
271
+ * retains properties present in every input; picks the most-specific of the
272
+ * sub-schemas when they agree on `type`, else widens to `{}`. */
273
+ function intersectDataSchemas(schemas) {
274
+ if (schemas.length === 1)
275
+ return schemas[0];
276
+ const commonProps = {};
277
+ const firstProps = (schemas[0].properties ?? {});
278
+ for (const propName of Object.keys(firstProps)) {
279
+ const sub = schemas.map((s) => (s.properties ?? {})[propName]);
280
+ if (sub.some((p) => p === undefined))
281
+ continue;
282
+ commonProps[propName] = intersectPropertySchemas(sub);
283
+ }
284
+ return {
285
+ type: "object",
286
+ properties: commonProps,
287
+ additionalProperties: false,
288
+ };
289
+ }
290
+ function intersectPropertySchemas(schemas) {
291
+ const types = new Set(schemas.map((s) => s?.type).filter(Boolean));
292
+ if (types.size === 1) {
293
+ const type = [...types][0];
294
+ if (type === "object")
295
+ return intersectDataSchemas(schemas);
296
+ return { type };
297
+ }
298
+ return {};
299
+ }
300
+ function collectCelStrings(value, path) {
301
+ const out = [];
302
+ if (typeof value === "string") {
303
+ for (const m of value.matchAll(TEMPLATE_REGEX)) {
304
+ out.push({ expr: m[1].trim(), path });
305
+ }
306
+ return out;
307
+ }
308
+ if (Array.isArray(value)) {
309
+ for (const [i, v] of value.entries()) {
310
+ out.push(...collectCelStrings(v, `${path}[${i}]`));
311
+ }
312
+ return out;
313
+ }
314
+ if (value !== null && typeof value === "object") {
315
+ for (const [k, v] of Object.entries(value)) {
316
+ out.push(...collectCelStrings(v, path ? `${path}.${k}` : k));
317
+ }
318
+ }
319
+ return out;
320
+ }
321
+ function checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env) {
322
+ let ast;
323
+ try {
324
+ ast = env.parse(entry.expr).ast;
325
+ }
326
+ catch {
327
+ return [];
328
+ }
329
+ const chains = extractAccessChains(ast);
330
+ const diagnostics = [];
331
+ for (const chain of chains) {
332
+ // Only interested in chains that start with error.data.*
333
+ if (chain[0] !== "error" || chain[1] !== "data" || chain.length <= 2)
334
+ continue;
335
+ const subChain = chain.slice(2); // strip "error", "data"
336
+ const err = validateChainAgainstSchema(subChain, dataSchema);
337
+ if (err) {
338
+ diagnostics.push({
339
+ severity: DiagnosticSeverity.Error,
340
+ code: "CEL_UNKNOWN_FIELD",
341
+ source: SOURCE,
342
+ message: `${resource.kind}/${resource.name}: CEL at '${entry.path}': error.data.${err}`,
343
+ data: { resource, filePath, path: entry.path },
344
+ });
345
+ }
346
+ }
347
+ return diagnostics;
348
+ }
349
+ /** Rule 8 extension: `inherit: true` only makes sense on a definition whose
350
+ * schema contains at least one `x-telo-step-context` array — the annotation
351
+ * that drives the resolver's generic step traversal. A definition with
352
+ * `inherit: true` and no such array has no invocables to inherit from. */
353
+ function validateThrowsDeclarations(manifests) {
354
+ const diagnostics = [];
355
+ for (const m of manifests) {
356
+ if (m.kind !== "Telo.Definition")
357
+ continue;
358
+ const throws = m.throws;
359
+ if (!throws)
360
+ continue;
361
+ const name = m.metadata?.name ?? "<unnamed>";
362
+ const filePath = m.metadata?.source;
363
+ if (throws.inherit === true) {
364
+ const schema = m.schema;
365
+ if (!schemaHasStepContext(schema)) {
366
+ diagnostics.push({
367
+ severity: DiagnosticSeverity.Error,
368
+ code: "INHERIT_WITHOUT_STEP_CONTEXT",
369
+ source: SOURCE,
370
+ message: `Telo.Definition '${name}' declares throws.inherit: true but its schema has no field annotated with x-telo-step-context. inherit is only meaningful on definitions that drive invocables via step arrays.`,
371
+ data: { resource: { kind: m.kind, name }, filePath, path: "throws.inherit" },
372
+ });
373
+ }
374
+ }
375
+ }
376
+ return diagnostics;
377
+ }
378
+ function schemaHasStepContext(schema) {
379
+ if (!schema || typeof schema !== "object")
380
+ return false;
381
+ if ("x-telo-step-context" in schema)
382
+ return true;
383
+ const props = schema.properties;
384
+ if (props && typeof props === "object") {
385
+ for (const v of Object.values(props)) {
386
+ if (schemaHasStepContext(v))
387
+ return true;
388
+ }
389
+ }
390
+ if (schema.items && schemaHasStepContext(schema.items))
391
+ return true;
392
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
393
+ const arr = schema[key];
394
+ if (Array.isArray(arr)) {
395
+ for (const sub of arr)
396
+ if (schemaHasStepContext(sub))
397
+ return true;
398
+ }
399
+ }
400
+ if (schema.$defs && typeof schema.$defs === "object") {
401
+ for (const v of Object.values(schema.$defs)) {
402
+ if (schemaHasStepContext(v))
403
+ return true;
404
+ }
405
+ }
406
+ return false;
407
+ }
408
+ /** Entry point — invoked once per analyze() run. */
409
+ export function validateThrowsCoverage(manifests, defs, aliases, env) {
410
+ const diagnostics = [];
411
+ diagnostics.push(...validateThrowsDeclarations(manifests));
412
+ const resolveCtx = createResolveCtx(manifests, defs, aliases);
413
+ for (const manifest of manifests) {
414
+ if (!manifest.kind || !manifest.metadata?.name)
415
+ continue;
416
+ if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract")
417
+ continue;
418
+ const resolvedKind = aliases.resolveKind(manifest.kind);
419
+ const definition = defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
420
+ if (!definition?.schema)
421
+ continue;
422
+ const resource = { kind: manifest.kind, name: manifest.metadata.name };
423
+ const filePath = manifest.metadata?.source;
424
+ collectOutcomeLists(manifest, definition.schema, (ret) => {
425
+ diagnostics.push(...checkCatchAllPlacement(ret.entries, resource, "returns", filePath, ret.arrayPath));
426
+ }, (entries, arrayPath, siblingData, catchesFor) => {
427
+ diagnostics.push(...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath));
428
+ const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
429
+ const union = handlerRefUnion(handlerRef, manifests, resolveCtx);
430
+ diagnostics.push(...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env));
431
+ diagnostics.push(...checkTypedErrorData(entries, union, resource, filePath, arrayPath, env));
432
+ });
433
+ }
434
+ return diagnostics;
435
+ }
436
+ /** Resolve a handler ref's effective throw union. Prefers the named manifest
437
+ * (so `inherit: true` handlers expose their transitive union); falls back to
438
+ * the definition's own codes when no name is given. */
439
+ function handlerRefUnion(handlerRef, manifests, ctx) {
440
+ if (!handlerRef)
441
+ return { codes: new Map(), unbounded: false };
442
+ if (handlerRef.name) {
443
+ const resolvedKind = ctx.aliases.resolveKind(handlerRef.kind);
444
+ const targetManifest = manifests.find((m) => m.metadata?.name === handlerRef.name &&
445
+ (m.kind === handlerRef.kind ||
446
+ m.kind === resolvedKind ||
447
+ ctx.aliases.resolveKind(m.kind) === handlerRef.kind));
448
+ if (targetManifest)
449
+ return resolveThrowsUnion(targetManifest, ctx);
450
+ }
451
+ const resolved = ctx.aliases.resolveKind(handlerRef.kind);
452
+ const def = ctx.defs.resolve(handlerRef.kind) ?? (resolved ? ctx.defs.resolve(resolved) : undefined);
453
+ if (!def?.throws)
454
+ return { codes: new Map(), unbounded: false };
455
+ const codes = new Map();
456
+ for (const [c, meta] of Object.entries(def.throws.codes ?? {})) {
457
+ codes.set(c, { data: meta.data });
458
+ }
459
+ const unbounded = def.throws.passthrough === true || def.throws.inherit === true;
460
+ return { codes, unbounded };
461
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -41,7 +41,7 @@
41
41
  "ajv-formats": "^3.0.1",
42
42
  "jsonpath-plus": "^10.3.0",
43
43
  "yaml": "^2.8.3",
44
- "@telorun/sdk": "0.2.8"
44
+ "@telorun/sdk": "0.3.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^20.0.0",