@telorun/analyzer 0.63.0 → 0.65.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 (64) hide show
  1. package/dist/analysis-registry.d.ts +24 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +35 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +76 -470
  7. package/dist/cel-scope-query.d.ts +109 -0
  8. package/dist/cel-scope-query.d.ts.map +1 -0
  9. package/dist/cel-scope-query.js +270 -0
  10. package/dist/cel-scope.d.ts +166 -0
  11. package/dist/cel-scope.d.ts.map +1 -0
  12. package/dist/cel-scope.js +377 -0
  13. package/dist/definition-registry.d.ts +38 -6
  14. package/dist/definition-registry.d.ts.map +1 -1
  15. package/dist/definition-registry.js +66 -22
  16. package/dist/find-manifest.d.ts +10 -0
  17. package/dist/find-manifest.d.ts.map +1 -0
  18. package/dist/find-manifest.js +12 -0
  19. package/dist/index.d.ts +11 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -1
  22. package/dist/invocation-contract.d.ts +11 -0
  23. package/dist/invocation-contract.d.ts.map +1 -1
  24. package/dist/invocation-contract.js +15 -0
  25. package/dist/manifest-analysis.d.ts +73 -0
  26. package/dist/manifest-analysis.d.ts.map +1 -0
  27. package/dist/manifest-analysis.js +78 -0
  28. package/dist/manifest-path.d.ts +18 -0
  29. package/dist/manifest-path.d.ts.map +1 -0
  30. package/dist/manifest-path.js +37 -0
  31. package/dist/schema-compat.d.ts +59 -22
  32. package/dist/schema-compat.d.ts.map +1 -1
  33. package/dist/schema-compat.js +60 -75
  34. package/dist/schema-error-report.d.ts +68 -0
  35. package/dist/schema-error-report.d.ts.map +1 -0
  36. package/dist/schema-error-report.js +356 -0
  37. package/dist/schema-walk.d.ts +25 -0
  38. package/dist/schema-walk.d.ts.map +1 -0
  39. package/dist/schema-walk.js +126 -0
  40. package/dist/telo-version.d.ts +1 -1
  41. package/dist/telo-version.js +1 -1
  42. package/dist/validate-nested-inline.d.ts +22 -1
  43. package/dist/validate-nested-inline.d.ts.map +1 -1
  44. package/dist/validate-nested-inline.js +17 -9
  45. package/dist/validate-step-inputs.d.ts +17 -0
  46. package/dist/validate-step-inputs.d.ts.map +1 -1
  47. package/dist/validate-step-inputs.js +108 -9
  48. package/package.json +2 -2
  49. package/src/analysis-registry.ts +37 -0
  50. package/src/analyzer.ts +83 -587
  51. package/src/cel-scope-query.ts +337 -0
  52. package/src/cel-scope.ts +570 -0
  53. package/src/definition-registry.ts +79 -24
  54. package/src/find-manifest.ts +19 -0
  55. package/src/index.ts +23 -2
  56. package/src/invocation-contract.ts +22 -0
  57. package/src/manifest-analysis.ts +132 -0
  58. package/src/manifest-path.ts +34 -0
  59. package/src/schema-compat.ts +92 -79
  60. package/src/schema-error-report.ts +417 -0
  61. package/src/schema-walk.ts +144 -0
  62. package/src/telo-version.ts +1 -1
  63. package/src/validate-nested-inline.ts +35 -14
  64. package/src/validate-step-inputs.ts +153 -11
@@ -9,7 +9,7 @@ import {
9
9
  validateAgainstSchema,
10
10
  } from "./schema-compat.js";
11
11
  import { plainChainOf } from "@telorun/templating";
12
- import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
12
+ import { isLiveSlot, valueTypeOf, type ResourceDefinition } from "@telorun/sdk";
13
13
  import { manifestFragmentOf } from "./manifest-schemas.js";
14
14
  import {
15
15
  analyzerContractScope,
@@ -20,6 +20,12 @@ import {
20
20
  walkStepArray,
21
21
  } from "./analyzer.js";
22
22
  import { readStepSlot } from "./step-slot.js";
23
+ import { navigateConcretePath } from "./manifest-path.js";
24
+ import {
25
+ isRefEntry,
26
+ resolveFieldEntries,
27
+ type ReferenceFieldMap,
28
+ } from "./reference-field-map.js";
23
29
 
24
30
  export interface StepInputIssue {
25
31
  path: string;
@@ -66,6 +72,15 @@ export function collectStepInputIssues(
66
72
  const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
67
73
  const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
68
74
 
75
+ const ctx: CallCheckContext = {
76
+ manifest,
77
+ allManifests,
78
+ defs,
79
+ contractScope,
80
+ readingModule,
81
+ stepContext,
82
+ };
83
+
69
84
  for (const [fieldName, fieldSchema] of Object.entries(props)) {
70
85
  const stepCtx = readStepSlot(fieldSchema);
71
86
  if (!stepCtx) continue;
@@ -92,6 +107,127 @@ export function collectStepInputIssues(
92
107
  if (!invoke || typeof invoke !== "object") return;
93
108
  if (!values || typeof values !== "object" || Array.isArray(values)) return;
94
109
 
110
+ out.push(
111
+ ...checkCallSite(
112
+ {
113
+ inputsPath: `${stepPath}.${inputsField}`,
114
+ values: values as Record<string, any>,
115
+ invoke,
116
+ // Only a step declares a re-attempt policy, so only a step can carry
117
+ // the live-value-retried finding.
118
+ declaredRetryFor: (invokedManifest, invokedDef) =>
119
+ declaredRetry(step, stepItemSchema, invokedManifest, invokedDef),
120
+ },
121
+ ctx,
122
+ ),
123
+ );
124
+ });
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Validate the argument map of every call this resource makes through a
131
+ * REFERENCE SLOT, as opposed to a step.
132
+ *
133
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
134
+ * (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
135
+ * annotation is the only thing tying an otherwise-open `inputs:` map to the
136
+ * resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
137
+ * is exactly this shape, and nothing about it is a step.
138
+ *
139
+ * Discovery is driven by the annotation rather than by any kind's topology, so
140
+ * a composer that names its argument slot gets its call sites checked without
141
+ * the analyzer learning what a route is. It is the same check the step driver
142
+ * runs, because it is the same question.
143
+ */
144
+ export function collectRefInputIssues(
145
+ manifest: Record<string, any>,
146
+ fieldMap: ReferenceFieldMap | undefined,
147
+ allManifests: Record<string, any>[],
148
+ defs: DefinitionRegistry,
149
+ aliases: AliasResolver,
150
+ scopes: ModuleScopes,
151
+ ): StepInputIssue[] {
152
+ const out: StepInputIssue[] = [];
153
+ if (!fieldMap) return out;
154
+
155
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
156
+ const ctx: CallCheckContext = {
157
+ manifest,
158
+ allManifests,
159
+ defs,
160
+ contractScope,
161
+ readingModule: (manifest.metadata as { module?: string } | undefined)?.module,
162
+ };
163
+
164
+ for (const [fieldPath, entry] of fieldMap) {
165
+ if (!isRefEntry(entry) || !entry.inputs) continue;
166
+ const pointer = pointerSegments(entry.inputs);
167
+ if (!pointer) continue;
168
+
169
+ for (const { value: invoke, path: slotPath } of resolveFieldEntries(manifest, fieldPath)) {
170
+ if (!invoke || typeof invoke !== "object" || Array.isArray(invoke)) continue;
171
+ // Relative to the object ENCLOSING the slot, which is the annotation's
172
+ // documented anchor.
173
+ const enclosing = slotPath.slice(0, Math.max(0, slotPath.lastIndexOf(".")));
174
+ const inputsPath = [enclosing, ...pointer].filter(Boolean).join(".");
175
+ const values = navigateConcretePath(manifest, inputsPath);
176
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
177
+
178
+ out.push(
179
+ ...checkCallSite(
180
+ { inputsPath, values: values as Record<string, any>, invoke: invoke as Record<string, any> },
181
+ ctx,
182
+ ),
183
+ );
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ /** A JSON Pointer naming a sibling FIELD path. An array index is not a field,
190
+ * so a pointer carrying one names nothing this can resolve. */
191
+ function pointerSegments(pointer: string): string[] | undefined {
192
+ if (!pointer.startsWith("/")) return undefined;
193
+ const segments = pointer
194
+ .slice(1)
195
+ .split("/")
196
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
197
+ return segments.every((s) => s.length > 0 && !/^\d+$/.test(s)) ? segments : undefined;
198
+ }
199
+
200
+ /** One call site: the arguments written, and the reference they are for. */
201
+ interface CallSite {
202
+ /** Concrete path of the argument map, for anchoring a diagnostic. */
203
+ inputsPath: string;
204
+ values: Record<string, any>;
205
+ invoke: Record<string, any>;
206
+ declaredRetryFor?(
207
+ invokedManifest: Record<string, any> | undefined,
208
+ invokedDef: ResourceDefinition | undefined,
209
+ ): string | undefined;
210
+ }
211
+
212
+ interface CallCheckContext {
213
+ manifest: Record<string, any>;
214
+ allManifests: Record<string, any>[];
215
+ defs: DefinitionRegistry;
216
+ contractScope: ReturnType<typeof analyzerContractScope>;
217
+ readingModule: string | undefined;
218
+ stepContext?: Record<string, any>;
219
+ }
220
+
221
+ /**
222
+ * The check itself, shared by both drivers: the arguments written at a call site
223
+ * against the contract the target declares.
224
+ */
225
+ function checkCallSite(site: CallSite, ctx: CallCheckContext): StepInputIssue[] {
226
+ const out: StepInputIssue[] = [];
227
+ const { manifest, allManifests, defs, contractScope, readingModule, stepContext } = ctx;
228
+ const { invoke, values } = site;
229
+ {
230
+ {
95
231
  const invokedKind = invoke.kind as string | undefined;
96
232
  const invokedName = invoke.name as string | undefined;
97
233
  const invokedManifest = invokedName
@@ -104,7 +240,7 @@ export function collectStepInputIssues(
104
240
  ? contractScope.resolveIn(invokedKind, readingModule)
105
241
  : undefined;
106
242
  const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
107
- if (!contract) return;
243
+ if (!contract) return out;
108
244
 
109
245
  // Findings AT a substituted path are about a placeholder, not about
110
246
  // anything the author wrote — a `pattern`-constrained string or a `oneOf`
@@ -112,9 +248,15 @@ export function collectStepInputIssues(
112
248
  // findings (missing required, unknown property) are located at the
113
249
  // container and survive the filter.
114
250
  const celPaths = new Set<string>();
115
- const substituted = substituteCelFields(values, contract.schema, undefined, (p) =>
116
- celPaths.add(p),
117
- );
251
+ const substituted = substituteCelFields(values, contract.schema, undefined, {
252
+ onSubstitute: (p) => celPaths.add(p),
253
+ // A contract may name a shape declared elsewhere. Both halves need the
254
+ // resolver or they disagree about the same slot: the stand-in walk hands
255
+ // its expressions a typeless value, and the check below compiles nothing
256
+ // at all — so a step's arguments went unchecked against exactly the
257
+ // contracts that describe them most precisely.
258
+ external: (ref) => defs.schemaForId(ref),
259
+ });
118
260
  // The type-argument check, at the one site where a produced value's schema
119
261
  // meets a consuming slot's. A CEL leaf's placeholder says nothing about
120
262
  // what the expression yields, so AJV above is silent here by design — and
@@ -163,10 +305,10 @@ export function collectStepInputIssues(
163
305
  // already declared: the value's liveness by its value type, and the
164
306
  // re-attempt by the retry policy. No kind is named.
165
307
  if (isLiveSlot(produced)) {
166
- const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
308
+ const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
167
309
  if (retry !== undefined) {
168
310
  out.push({
169
- path: `${stepPath}.${inputsField}.${inputName}`,
311
+ path: `${site.inputsPath}.${inputName}`,
170
312
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
171
313
  message:
172
314
  `'${inputName}' is a live value, which is consumed by reading and so exists ` +
@@ -184,7 +326,7 @@ export function collectStepInputIssues(
184
326
  );
185
327
  if (compatible) continue;
186
328
  out.push({
187
- path: `${stepPath}.${inputsField}.${inputName}`,
329
+ path: `${site.inputsPath}.${inputName}`,
188
330
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
189
331
  message: issues.join("; "),
190
332
  code: "CEL_TYPE_ARGUMENT_MISMATCH",
@@ -192,7 +334,7 @@ export function collectStepInputIssues(
192
334
  }
193
335
  }
194
336
 
195
- for (const issue of validateAgainstSchema(substituted, contract.schema)) {
337
+ for (const issue of defs.validateResourceConfig(substituted, contract.schema)) {
196
338
  if (celPaths.has(issue.path)) continue;
197
339
  // A missing-required issue names the property that ISN'T there, so
198
340
  // anchoring on it finds no node and the diagnostic degrades to 1:1 —
@@ -200,12 +342,12 @@ export function collectStepInputIssues(
200
342
  // container that should have held it, which does exist.
201
343
  const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
202
344
  out.push({
203
- path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
345
+ path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
204
346
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
205
347
  message: issue.message,
206
348
  });
207
349
  }
208
- });
350
+ }
209
351
  }
210
352
  return out;
211
353
  }