@telorun/analyzer 0.47.0 → 0.49.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 (58) hide show
  1. package/dist/analysis-registry.d.ts +22 -11
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +36 -39
  4. package/dist/analyzer.d.ts +38 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +121 -83
  7. package/dist/artifact-layer-index.d.ts +55 -0
  8. package/dist/artifact-layer-index.d.ts.map +1 -0
  9. package/dist/artifact-layer-index.js +116 -0
  10. package/dist/artifact-selector.d.ts +81 -0
  11. package/dist/artifact-selector.d.ts.map +1 -0
  12. package/dist/artifact-selector.js +122 -0
  13. package/dist/builtins.d.ts.map +1 -1
  14. package/dist/builtins.js +130 -20
  15. package/dist/extends-resolution.d.ts +41 -0
  16. package/dist/extends-resolution.d.ts.map +1 -1
  17. package/dist/extends-resolution.js +68 -0
  18. package/dist/index.d.ts +9 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -1
  21. package/dist/invocation-contract.d.ts +100 -0
  22. package/dist/invocation-contract.d.ts.map +1 -0
  23. package/dist/invocation-contract.js +208 -0
  24. package/dist/schema-compat.d.ts +12 -4
  25. package/dist/schema-compat.d.ts.map +1 -1
  26. package/dist/schema-compat.js +185 -9
  27. package/dist/validate-base-mapping.js +11 -1
  28. package/dist/validate-cel-context.d.ts +0 -6
  29. package/dist/validate-cel-context.d.ts.map +1 -1
  30. package/dist/validate-cel-context.js +51 -4
  31. package/dist/validate-invocation-contract.d.ts +30 -0
  32. package/dist/validate-invocation-contract.d.ts.map +1 -0
  33. package/dist/validate-invocation-contract.js +394 -0
  34. package/dist/validate-module-artifact.d.ts +27 -0
  35. package/dist/validate-module-artifact.d.ts.map +1 -0
  36. package/dist/validate-module-artifact.js +131 -0
  37. package/dist/validate-step-inputs.d.ts +24 -0
  38. package/dist/validate-step-inputs.d.ts.map +1 -0
  39. package/dist/validate-step-inputs.js +87 -0
  40. package/dist/validate-throws-coverage.d.ts +1 -1
  41. package/dist/validate-throws-coverage.d.ts.map +1 -1
  42. package/dist/validate-throws-coverage.js +9 -1
  43. package/package.json +2 -2
  44. package/src/analysis-registry.ts +44 -34
  45. package/src/analyzer.ts +177 -100
  46. package/src/artifact-layer-index.ts +162 -0
  47. package/src/artifact-selector.ts +171 -0
  48. package/src/builtins.ts +135 -20
  49. package/src/extends-resolution.ts +86 -0
  50. package/src/index.ts +38 -1
  51. package/src/invocation-contract.ts +275 -0
  52. package/src/schema-compat.ts +191 -8
  53. package/src/validate-base-mapping.ts +14 -1
  54. package/src/validate-cel-context.ts +49 -4
  55. package/src/validate-invocation-contract.ts +450 -0
  56. package/src/validate-module-artifact.ts +141 -0
  57. package/src/validate-step-inputs.ts +117 -0
  58. package/src/validate-throws-coverage.ts +12 -2
@@ -0,0 +1,117 @@
1
+ import type { AliasResolver, ModuleScopes } from "./alias-resolver.js";
2
+ import type { DefinitionRegistry } from "./definition-registry.js";
3
+ import type { ContractDirection } from "./extends-resolution.js";
4
+ import { resolveContract } from "./invocation-contract.js";
5
+ import { substituteCelFields, validateAgainstSchema } from "./schema-compat.js";
6
+ import {
7
+ analyzerContractScope,
8
+ containerOf,
9
+ gatherPropertySchemas,
10
+ missingRequired,
11
+ resolveLocalRef,
12
+ walkStepArray,
13
+ } from "./analyzer.js";
14
+
15
+ export interface StepInputIssue {
16
+ path: string;
17
+ targetLabel: string;
18
+ message: string;
19
+ }
20
+
21
+ /**
22
+ * Validate every step's `inputs:` against the invoked target's declared input
23
+ * contract — the static half of what the kernel enforces at dispatch.
24
+ *
25
+ * Worth doing statically because a call site is where the mistake is made and
26
+ * where the author can see both sides: a misspelled key or a wrong-shaped value
27
+ * would otherwise surface at runtime inside the callee, several steps from its
28
+ * cause, naming a resource the author may not have written.
29
+ *
30
+ * CEL leaves are replaced by schema-shaped placeholders first (`substituteCelFields`),
31
+ * so an expression is never a false positive — only structural disagreement is
32
+ * reported. Nothing is hardcoded about `Run.Sequence`: the invoke field comes
33
+ * from `x-telo-step-context`, and the paired inputs field from whichever sibling
34
+ * property carries `x-telo-topology-role: inputs`.
35
+ */
36
+ export function collectStepInputIssues(
37
+ manifest: Record<string, any>,
38
+ defSchema: Record<string, any>,
39
+ allManifests: Record<string, any>[],
40
+ defs: DefinitionRegistry,
41
+ aliases: AliasResolver,
42
+ scopes: ModuleScopes,
43
+ ): StepInputIssue[] {
44
+ const out: StepInputIssue[] = [];
45
+ const props = defSchema.properties as Record<string, any> | undefined;
46
+ if (!props) return out;
47
+
48
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
49
+ const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
50
+
51
+ for (const [fieldName, fieldSchema] of Object.entries(props)) {
52
+ const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
53
+ if (!stepCtx?.invoke) continue;
54
+ const steps = manifest[fieldName];
55
+ if (!Array.isArray(steps)) continue;
56
+
57
+ const stepItemSchema = resolveLocalRef(
58
+ fieldSchema.items as Record<string, any> | undefined,
59
+ defSchema,
60
+ );
61
+ if (!stepItemSchema) continue;
62
+
63
+ // The inputs field is whichever sibling declares the role — never the literal
64
+ // name, so a composer that spells it differently still gets checked.
65
+ let inputsField: string | undefined;
66
+ for (const [key, sub] of gatherPropertySchemas(stepItemSchema)) {
67
+ if (sub?.["x-telo-topology-role"] === "inputs") inputsField = key;
68
+ }
69
+ if (!inputsField) continue;
70
+
71
+ walkStepArray(steps, stepItemSchema, defSchema, fieldName, (step, stepPath) => {
72
+ const invoke = step[stepCtx.invoke] as Record<string, any> | undefined;
73
+ const values = step[inputsField!];
74
+ if (!invoke || typeof invoke !== "object") return;
75
+ if (!values || typeof values !== "object" || Array.isArray(values)) return;
76
+
77
+ const invokedKind = invoke.kind as string | undefined;
78
+ const invokedName = invoke.name as string | undefined;
79
+ const invokedManifest = invokedName
80
+ ? (allManifests.find(
81
+ (m) =>
82
+ (m.metadata as any)?.name === invokedName && (!invokedKind || m.kind === invokedKind),
83
+ ) as Record<string, any> | undefined)
84
+ : (invoke as Record<string, any>);
85
+ const invokedDef = invokedKind
86
+ ? contractScope.resolveIn(invokedKind, readingModule)
87
+ : undefined;
88
+ const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
89
+ if (!contract) return;
90
+
91
+ // Findings AT a substituted path are about a placeholder, not about
92
+ // anything the author wrote — a `pattern`-constrained string or a `oneOf`
93
+ // of unrelated shapes cannot be satisfied by any stand-in. Structural
94
+ // findings (missing required, unknown property) are located at the
95
+ // container and survive the filter.
96
+ const celPaths = new Set<string>();
97
+ const substituted = substituteCelFields(values, contract.schema, undefined, (p) =>
98
+ celPaths.add(p),
99
+ );
100
+ for (const issue of validateAgainstSchema(substituted, contract.schema)) {
101
+ if (celPaths.has(issue.path)) continue;
102
+ // A missing-required issue names the property that ISN'T there, so
103
+ // anchoring on it finds no node and the diagnostic degrades to 1:1 —
104
+ // losing the location of the most common contract mistake. Anchor on the
105
+ // container that should have held it, which does exist.
106
+ const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
107
+ out.push({
108
+ path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
109
+ targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
110
+ message: issue.message,
111
+ });
112
+ }
113
+ });
114
+ }
115
+ return out;
116
+ }
117
+
@@ -1,6 +1,10 @@
1
1
  import type { ASTNode, Environment } from "@marcbachmann/cel-js";
2
2
  import { isTaggedSentinel } from "@telorun/templating";
3
- import type { ResourceManifest } from "@telorun/sdk";
3
+ import {
4
+ AMBIENT_CONTRACT_ERROR_CODES,
5
+ isAmbientContractErrorCode,
6
+ type ResourceManifest,
7
+ } from "@telorun/sdk";
4
8
  import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
5
9
  import type { DefinitionRegistry } from "./definition-registry.js";
6
10
  import {
@@ -250,12 +254,18 @@ function checkCatchesCoverage(
250
254
  const { proven, codes } = extractCoveredCodes(e.when, env);
251
255
  if (proven) {
252
256
  for (const c of codes) {
257
+ // An ambient kernel code (contract violations) is raised by the kernel,
258
+ // not declared by the kind, so naming it is legal and still typo-checked
259
+ // — but it is NOT part of the declared union, so it never counts toward
260
+ // coverage. Folding these into every union would make every bounded
261
+ // catches: block in the standard library incomplete overnight.
262
+ if (isAmbientContractErrorCode(c)) continue;
253
263
  if (!declaredCodes.has(c)) {
254
264
  diagnostics.push({
255
265
  severity: DiagnosticSeverity.Error,
256
266
  code: "UNDECLARED_THROW_CODE",
257
267
  source: SOURCE,
258
- 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)" : ""}.`,
268
+ message: `catches[${i}] references code '${c}' which is not in the handler's declared throw union {${[...declaredCodes].sort().join(", ") || "∅"}} (ambient kernel codes ${AMBIENT_CONTRACT_ERROR_CODES.join(", ")} may also be named)${union.unbounded ? "; the union is unbounded, so a catch-all is required" : ""}.`,
259
269
  data: { resource, filePath, path: `${arrayPath}[${i}].when` },
260
270
  });
261
271
  } else {