@telorun/analyzer 0.12.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts +13 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +154 -83
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +85 -0
- package/dist/cel-environment.d.ts +1 -1
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +40 -2
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +41 -62
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel-globals.d.ts +1 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +19 -1
- package/dist/manifest-visitor.d.ts +124 -0
- package/dist/manifest-visitor.d.ts.map +1 -0
- package/dist/manifest-visitor.js +181 -0
- package/dist/reference-field-map.js +16 -0
- package/dist/resolve-throws-union.d.ts +10 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +35 -7
- package/dist/schema-compat.d.ts +10 -0
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +32 -0
- package/dist/validate-cel-context.d.ts +14 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +38 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +124 -160
- package/dist/validate-unused-declarations.d.ts +25 -0
- package/dist/validate-unused-declarations.d.ts.map +1 -0
- package/dist/validate-unused-declarations.js +91 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +20 -0
- package/src/analyzer.ts +256 -168
- package/src/builtins.ts +85 -0
- package/src/cel-environment.ts +42 -1
- package/src/dependency-graph.ts +37 -52
- package/src/index.ts +11 -0
- package/src/kernel-globals.ts +22 -1
- package/src/manifest-visitor.ts +340 -0
- package/src/reference-field-map.ts +14 -0
- package/src/resolve-throws-union.ts +36 -8
- package/src/schema-compat.ts +32 -0
- package/src/validate-cel-context.ts +50 -0
- package/src/validate-references.ts +175 -211
- package/src/validate-unused-declarations.ts +95 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
5
|
|
|
@@ -6,6 +7,11 @@ export interface ThrowsCodeMeta {
|
|
|
6
7
|
data?: Record<string, any>;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/** Code a non-`InvokeError` failure surfaces as inside a `catch` block. Mirrors
|
|
11
|
+
* `PLAIN_ERROR_CODE` in `@telorun/run`'s `toSequenceError`: any invoke can throw
|
|
12
|
+
* a plain error, which the catch sees as `error.code === "INTERNAL_ERROR"`. */
|
|
13
|
+
export const PLAIN_ERROR_CODE = "INTERNAL_ERROR";
|
|
14
|
+
|
|
9
15
|
export interface ThrowsUnion {
|
|
10
16
|
/** Code → per-code metadata (data schema, etc). Keys are the declared codes. */
|
|
11
17
|
codes: Map<string, ThrowsCodeMeta>;
|
|
@@ -14,6 +20,12 @@ export interface ThrowsUnion {
|
|
|
14
20
|
* an unknown kind was encountered, or a cycle short-circuited resolution.
|
|
15
21
|
* Callers must treat unbounded unions as requiring a catch-all entry. */
|
|
16
22
|
unbounded: boolean;
|
|
23
|
+
/** True when the block can fail with a non-`InvokeError` (any `invoke:` step).
|
|
24
|
+
* Such a failure surfaces inside an enclosing `catch` as `PLAIN_ERROR_CODE`,
|
|
25
|
+
* so a `throw: { code: "${{ error.code }}" }` rethrow can propagate it. Not
|
|
26
|
+
* injected into `codes` — only seeds `enclosingTryCodes` at a try/catch site,
|
|
27
|
+
* leaving non-rethrow unions untouched. */
|
|
28
|
+
canThrowPlain?: boolean;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export interface ResolveCtx {
|
|
@@ -47,6 +59,7 @@ function unionInto(target: ThrowsUnion, src: ThrowsUnion): void {
|
|
|
47
59
|
if (!target.codes.has(code)) target.codes.set(code, meta);
|
|
48
60
|
}
|
|
49
61
|
if (src.unbounded) target.unbounded = true;
|
|
62
|
+
if (src.canThrowPlain) target.canThrowPlain = true;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function definitionFor(
|
|
@@ -173,7 +186,11 @@ function collectStepThrows(
|
|
|
173
186
|
ctx: ResolveCtx,
|
|
174
187
|
): ThrowsUnion {
|
|
175
188
|
if (step[invokeField]) {
|
|
176
|
-
|
|
189
|
+
// Any invoked resource can throw a non-InvokeError at runtime, which an
|
|
190
|
+
// enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
|
|
191
|
+
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
192
|
+
u.canThrowPlain = true;
|
|
193
|
+
return u;
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
if (step.throw && typeof step.throw === "object") {
|
|
@@ -188,6 +205,10 @@ function collectStepThrows(
|
|
|
188
205
|
// out instead. Sequence-specific subtraction — the plan explicitly
|
|
189
206
|
// anchors this to Run.Sequence's try/catch schema shape.
|
|
190
207
|
const tryCodes = new Set(tryUnion.codes.keys());
|
|
208
|
+
// A plain (non-InvokeError) failure in the try block reaches the catch as
|
|
209
|
+
// `error.code === PLAIN_ERROR_CODE`, so a `throw: { code: error.code }`
|
|
210
|
+
// rethrow can propagate it — seed the set the catch resolves against.
|
|
211
|
+
if (tryUnion.canThrowPlain) tryCodes.add(PLAIN_ERROR_CODE);
|
|
191
212
|
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
192
213
|
// Unbounded in the try block still signals the caller to expect
|
|
193
214
|
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
@@ -247,6 +268,7 @@ function cloneUnion(u: ThrowsUnion): ThrowsUnion {
|
|
|
247
268
|
const out = emptyUnion();
|
|
248
269
|
for (const [c, m] of u.codes) out.codes.set(c, m);
|
|
249
270
|
out.unbounded = u.unbounded;
|
|
271
|
+
if (u.canThrowPlain) out.canThrowPlain = true;
|
|
250
272
|
return out;
|
|
251
273
|
}
|
|
252
274
|
|
|
@@ -316,16 +338,22 @@ function resolveCodeExpression(
|
|
|
316
338
|
codeInput: unknown,
|
|
317
339
|
enclosingTryCodes: Set<string> | undefined,
|
|
318
340
|
): ThrowsUnion {
|
|
319
|
-
|
|
341
|
+
// A `!cel`-tagged sentinel and a `${{ … }}` string must resolve identically —
|
|
342
|
+
// normalize both to the inner CEL expression (or a bare literal code).
|
|
343
|
+
let expr: string;
|
|
344
|
+
if (isTaggedSentinel(codeInput)) {
|
|
345
|
+
if (codeInput.engine !== "cel") return { codes: new Map(), unbounded: true };
|
|
346
|
+
expr = codeInput.source.trim();
|
|
347
|
+
} else if (typeof codeInput === "string" && codeInput.length > 0) {
|
|
348
|
+
const match = codeInput.match(/^\s*\$\{\{\s*([\s\S]+?)\s*\}\}\s*$/);
|
|
349
|
+
if (!match) {
|
|
350
|
+
return { codes: new Map([[codeInput, {}]]), unbounded: false };
|
|
351
|
+
}
|
|
352
|
+
expr = match[1].trim();
|
|
353
|
+
} else {
|
|
320
354
|
return { codes: new Map(), unbounded: true };
|
|
321
355
|
}
|
|
322
356
|
|
|
323
|
-
const match = codeInput.match(/^\s*\$\{\{\s*([\s\S]+?)\s*\}\}\s*$/);
|
|
324
|
-
if (!match) {
|
|
325
|
-
return { codes: new Map([[codeInput, {}]]), unbounded: false };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const expr = match[1].trim();
|
|
329
357
|
const litMatch = expr.match(/^'([^']+)'$|^"([^"]+)"$/);
|
|
330
358
|
if (litMatch) {
|
|
331
359
|
const code = litMatch[1] ?? litMatch[2]!;
|
package/src/schema-compat.ts
CHANGED
|
@@ -204,9 +204,29 @@ export function navigateSchemaToExprPath(
|
|
|
204
204
|
return current;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Recognized `x-telo-type` value brands and the CEL primitive each refines.
|
|
209
|
+
* A brand is a nominal type the analyzer registers (see cel-environment.ts) so
|
|
210
|
+
* structurally-identical values (a `TcpPort` and a `UdpPort` are both integers)
|
|
211
|
+
* stay distinct for static wiring checks. Brands carry no runtime effect — the
|
|
212
|
+
* value flows as its base type. Add new brands here (e.g. `Url: "string"`).
|
|
213
|
+
*/
|
|
214
|
+
export const VALUE_BRAND_BASE: Record<string, string> = {
|
|
215
|
+
TcpPort: "int",
|
|
216
|
+
UdpPort: "int",
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Read a recognized `x-telo-type` brand off a schema, or undefined. */
|
|
220
|
+
export function brandOfSchema(schema: Record<string, any> | undefined): string | undefined {
|
|
221
|
+
const brand = schema?.["x-telo-type"];
|
|
222
|
+
return typeof brand === "string" && brand in VALUE_BRAND_BASE ? brand : undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
/** Map a JSON Schema type annotation to a CEL type string. */
|
|
208
226
|
export function jsonSchemaToCelType(schema: Record<string, any> | undefined): string {
|
|
209
227
|
if (!schema || typeof schema !== "object") return "dyn";
|
|
228
|
+
const brand = brandOfSchema(schema);
|
|
229
|
+
if (brand) return brand;
|
|
210
230
|
if (schema.anyOf || schema.oneOf || schema.allOf) return "dyn";
|
|
211
231
|
if (Array.isArray(schema.type)) return "dyn";
|
|
212
232
|
switch (schema.type) {
|
|
@@ -233,6 +253,18 @@ export function jsonSchemaToCelType(schema: Record<string, any> | undefined): st
|
|
|
233
253
|
/** Check whether a CEL return type is compatible with a JSON Schema type constraint. */
|
|
234
254
|
export function celTypeSatisfiesJsonSchema(celType: string, schema: Record<string, any>): boolean {
|
|
235
255
|
if (celType === "dyn") return true;
|
|
256
|
+
// Nominal value brands: when the expression's type is a recognized brand,
|
|
257
|
+
// a branded consuming field must match exactly (a UdpPort wired into a
|
|
258
|
+
// TcpPort-branded field is the error we want). An unbranded field accepts
|
|
259
|
+
// the brand as its base type — gradual typing, so a TcpPort flows freely
|
|
260
|
+
// into a plain integer field. (A plain integer into a branded field is also
|
|
261
|
+
// allowed: only a *conflicting* brand is rejected.)
|
|
262
|
+
const sourceBase = VALUE_BRAND_BASE[celType];
|
|
263
|
+
if (sourceBase) {
|
|
264
|
+
const fieldBrand = brandOfSchema(schema);
|
|
265
|
+
if (fieldBrand) return fieldBrand === celType;
|
|
266
|
+
celType = sourceBase;
|
|
267
|
+
}
|
|
236
268
|
if (!schema.type && !schema.anyOf && !schema.oneOf && !schema.allOf) return true;
|
|
237
269
|
if (schema.anyOf || schema.oneOf || schema.allOf) return true;
|
|
238
270
|
const schemaTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
@@ -266,3 +266,53 @@ function navigatePath(obj: unknown, segments: string[]): unknown {
|
|
|
266
266
|
}
|
|
267
267
|
return cur;
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
272
|
+
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
273
|
+
* the same format the analyzer uses for CEL context validation.
|
|
274
|
+
*
|
|
275
|
+
* Result is sorted by scope specificity (longer scope first) so that the
|
|
276
|
+
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
277
|
+
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
278
|
+
* could shadow a narrower descendant scope whose activation differs.
|
|
279
|
+
*/
|
|
280
|
+
export function extractContextsFromSchema(
|
|
281
|
+
schema: Record<string, any>,
|
|
282
|
+
path = "$",
|
|
283
|
+
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
284
|
+
const all = collectContexts(schema, path);
|
|
285
|
+
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectContexts(
|
|
289
|
+
schema: Record<string, any>,
|
|
290
|
+
path: string,
|
|
291
|
+
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
292
|
+
if (!schema || typeof schema !== "object") return [];
|
|
293
|
+
const results: Array<{ scope: string; schema: Record<string, any> }> = [];
|
|
294
|
+
|
|
295
|
+
if (schema["x-telo-context"]) {
|
|
296
|
+
results.push({ scope: path, schema: schema["x-telo-context"] });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (schema.properties) {
|
|
300
|
+
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
301
|
+
results.push(...collectContexts(value, `${path}.${key}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (schema.items && typeof schema.items === "object") {
|
|
306
|
+
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
310
|
+
if (Array.isArray(schema[key])) {
|
|
311
|
+
for (const subschema of schema[key]) {
|
|
312
|
+
results.push(...collectContexts(subschema, path));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return results;
|
|
318
|
+
}
|