@telorun/analyzer 0.13.0 → 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 +1 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +91 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +60 -0
- package/dist/manifest-visitor.d.ts +15 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +73 -2
- 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/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +7 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +1 -1
- package/src/analyzer.ts +100 -0
- package/src/builtins.ts +60 -0
- package/src/manifest-visitor.ts +91 -2
- package/src/reference-field-map.ts +14 -0
- package/src/resolve-throws-union.ts +36 -8
- package/src/validate-references.ts +7 -0
|
@@ -33,6 +33,7 @@ export declare class AnalysisRegistry {
|
|
|
33
33
|
visitManifest(resources: ResourceManifest[], visitor: ManifestVisitor, opts?: {
|
|
34
34
|
skipKinds?: ReadonlySet<string>;
|
|
35
35
|
expand?: boolean;
|
|
36
|
+
discoverNestedRefs?: boolean;
|
|
36
37
|
}): void;
|
|
37
38
|
/**
|
|
38
39
|
* Returns the built-in kernel definitions. The underlying DefinitionRegistry already
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAqC,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;;;;;OAOG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAkBP;;;;;;OAMG;IACH,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,EACxB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,
|
|
1
|
+
{"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAqC,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEhG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;;;;;OAOG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAkBP;;;;;;OAMG;IACH,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,EACxB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GACzF,IAAI;IAQP;;;;OAIG;IACH,kBAAkB,IAAI,kBAAkB,EAAE;IAI1C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAM/D,QAAQ,IAAI,MAAM,EAAE;IAIpB;mEAC+D;IAC/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC;;;gCAG4B;IAC5B,oBAAoB,IAAI,MAAM,EAAE;IAIhC;wEACoE;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;;gEAK4D;IAC5D,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IA+B7D,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuhB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAqlBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAexF,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -307,6 +307,73 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
|
|
|
307
307
|
}
|
|
308
308
|
return undefined;
|
|
309
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
|
+
}
|
|
310
377
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
311
378
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
312
379
|
/** Recursively walk `data`+`schema` together, type-checking every pure CEL template
|
|
@@ -784,6 +851,7 @@ export class StaticAnalyzer {
|
|
|
784
851
|
// context — which require analyzer state to build — are stashed here.
|
|
785
852
|
let celStepContextSchema;
|
|
786
853
|
let celInvocationContext;
|
|
854
|
+
let celErrorScopes = new Map();
|
|
787
855
|
visitManifest(allManifests, defs, {
|
|
788
856
|
onResourceEnter: (e) => {
|
|
789
857
|
const m = e.source;
|
|
@@ -791,6 +859,7 @@ export class StaticAnalyzer {
|
|
|
791
859
|
celStepContextSchema = e.definition?.schema
|
|
792
860
|
? buildStepContextSchema(m, e.definition.schema, allManifests, defs, aliases)
|
|
793
861
|
: undefined;
|
|
862
|
+
celErrorScopes = collectErrorContextScopes(e.definition?.schema);
|
|
794
863
|
},
|
|
795
864
|
onCel: (e) => {
|
|
796
865
|
const m = e.source;
|
|
@@ -808,6 +877,19 @@ export class StaticAnalyzer {
|
|
|
808
877
|
},
|
|
809
878
|
};
|
|
810
879
|
}
|
|
880
|
+
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
881
|
+
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
882
|
+
const errorSchema = celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
|
|
883
|
+
if (errorSchema) {
|
|
884
|
+
const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
885
|
+
matchedContext = {
|
|
886
|
+
...base,
|
|
887
|
+
properties: {
|
|
888
|
+
...(base.properties ?? {}),
|
|
889
|
+
error: errorSchema,
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
}
|
|
811
893
|
let effectiveContext = null;
|
|
812
894
|
if (matchedContext) {
|
|
813
895
|
const manifestItem = matchedScope
|
|
@@ -855,6 +937,15 @@ export class StaticAnalyzer {
|
|
|
855
937
|
data: { resource, filePath, path },
|
|
856
938
|
});
|
|
857
939
|
}
|
|
940
|
+
else if (f.code === "CEL_NULLABLE_ACCESS") {
|
|
941
|
+
diagnostics.push({
|
|
942
|
+
severity: DiagnosticSeverity.Error,
|
|
943
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
944
|
+
source: SOURCE,
|
|
945
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
946
|
+
data: { resource, filePath, path },
|
|
947
|
+
});
|
|
948
|
+
}
|
|
858
949
|
else {
|
|
859
950
|
// Unknown code from a future engine — pass the message through,
|
|
860
951
|
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -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,
|
|
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,EAsZ/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
|
},
|
|
@@ -65,6 +65,13 @@ export interface RefSiteEvent {
|
|
|
65
65
|
inScope: boolean;
|
|
66
66
|
/** Scope manifests visible to this ref path (non-empty only when `inScope`). */
|
|
67
67
|
visibleScopeManifests: ResourceManifest[];
|
|
68
|
+
/** True when the site was found by value-tree scanning rather than the field
|
|
69
|
+
* map (only when `discoverNestedRefs` is set) — a ref nested behind a `$ref`
|
|
70
|
+
* the field map doesn't descend (e.g. `Run.Sequence` `steps[].invoke`).
|
|
71
|
+
* Nested sites carry no x-telo-ref constraint (`entry.refs` is empty) and no
|
|
72
|
+
* scope info; `concretePath` still points at the exact location, so consumers
|
|
73
|
+
* can anchor to it. */
|
|
74
|
+
nested?: boolean;
|
|
68
75
|
}
|
|
69
76
|
export interface SchemaFromSiteEvent {
|
|
70
77
|
source: ResourceManifest;
|
|
@@ -104,6 +111,14 @@ export interface VisitOptions {
|
|
|
104
111
|
* so refs nested behind `x-telo-schema-from` are surfaced. `SchemaFromSite`
|
|
105
112
|
* events are always emitted from the base map regardless of this flag. */
|
|
106
113
|
expand?: boolean;
|
|
114
|
+
/** When true, additionally discover refs by scanning each resource's value
|
|
115
|
+
* tree for `!ref` sentinels and `{kind, name}` reference objects — surfacing
|
|
116
|
+
* refs the field map never lists because they sit behind a `$ref` it doesn't
|
|
117
|
+
* descend (notably `Run.Sequence` step `invoke`s). Emitted as `RefSite`s with
|
|
118
|
+
* `nested: true`, deduped against the field-map sites by concrete path.
|
|
119
|
+
* Opt-in: the validators / dependency graph must NOT enable it (those refs
|
|
120
|
+
* are runtime-resolved, not boot dependencies). */
|
|
121
|
+
discoverNestedRefs?: boolean;
|
|
107
122
|
}
|
|
108
123
|
export declare function visitManifest(resources: ResourceManifest[], registry: DefinitionRegistry, visitor: ManifestVisitor, options?: VisitOptions): void;
|
|
109
124
|
//# sourceMappingURL=manifest-visitor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-visitor.d.ts","sourceRoot":"","sources":["../src/manifest-visitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,wEAAwE;IACxE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpD;iFAC6E;IAC7E,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAC;IACrB;iEAC6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest-visitor.d.ts","sourceRoot":"","sources":["../src/manifest-visitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,wEAAwE;IACxE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpD;iFAC6E;IAC7E,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAC;IACrB;iEAC6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;IAC1C;;;;;4BAKwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,gBAAgB,CAAC;IACzB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,gBAAgB,CAAC;IACzB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB;;mEAE+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACpC,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC9C,OAAO,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC,KAAK,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,YAAY,CAAC,CAAC,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC;;+EAE2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;wDAMoD;IACpD,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAsDD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,YAAiB,GACzB,IAAI,CA4IN"}
|
package/dist/manifest-visitor.js
CHANGED
|
@@ -1,16 +1,59 @@
|
|
|
1
|
-
import { walkCelExpressions } from "@telorun/templating";
|
|
1
|
+
import { isRefSentinel, isTaggedSentinel, walkCelExpressions } from "@telorun/templating";
|
|
2
2
|
import { isRefEntry, isSchemaFromEntry, isScopeEntry, resolveFieldEntries, resolveFieldValues, } from "./reference-field-map.js";
|
|
3
3
|
import { extractContextsFromSchema, pathMatchesScope } from "./validate-cel-context.js";
|
|
4
|
+
/** Synthetic entry for a value-tree-discovered ref — these carry no declared
|
|
5
|
+
* x-telo-ref constraint. */
|
|
6
|
+
const NESTED_REF_ENTRY = { refs: [], isArray: false };
|
|
7
|
+
/** Scans a value tree for ref-shaped values, emitting each with its concrete
|
|
8
|
+
* path. Recognizes `!ref <name>` sentinels and named `{kind, name}` reference
|
|
9
|
+
* objects. Other tagged sentinels (`!cel`, `!literal`) and precompiled nodes
|
|
10
|
+
* are leaves. Path format matches `resolveFieldEntries` / `walkCelExpressions`
|
|
11
|
+
* (`a.b[0].c`).
|
|
12
|
+
*
|
|
13
|
+
* Stops at every `{kind, …}` resource boundary: a named ref is emitted, an
|
|
14
|
+
* inline resource (`{kind}` with no name) is left alone, and **neither is
|
|
15
|
+
* descended into**. A nested resource's own refs belong to its inner topology,
|
|
16
|
+
* not the enclosing node — e.g. an inline `Sql.Exec` step's `connection` is the
|
|
17
|
+
* Exec's dependency, not the surrounding `Run.Sequence`'s. The scan is started
|
|
18
|
+
* per top-level field (not on the resource object) so the resource's own
|
|
19
|
+
* `kind` doesn't trip this boundary. */
|
|
20
|
+
function walkRefValues(value, path, cb) {
|
|
21
|
+
if (isRefSentinel(value)) {
|
|
22
|
+
cb(value, path);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (isTaggedSentinel(value))
|
|
26
|
+
return;
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
value.forEach((v, i) => walkRefValues(v, `${path}[${i}]`, cb));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (value === null || typeof value !== "object")
|
|
32
|
+
return;
|
|
33
|
+
if (value.__compiled)
|
|
34
|
+
return;
|
|
35
|
+
const obj = value;
|
|
36
|
+
if (typeof obj.kind === "string") {
|
|
37
|
+
// Resource boundary — emit if it's a named ref, then stop descending.
|
|
38
|
+
if (typeof obj.name === "string")
|
|
39
|
+
cb(value, path);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
43
|
+
walkRefValues(v, path ? `${path}.${k}` : k, cb);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
4
46
|
const scopePrefixOf = (pointer) => pointer.replace(/^\//, "").replace(/\//g, ".");
|
|
5
47
|
const pathUnderPrefix = (fieldPath, prefix) => fieldPath === prefix ||
|
|
6
48
|
fieldPath.startsWith(prefix + ".") ||
|
|
7
49
|
fieldPath.startsWith(prefix + "[");
|
|
8
50
|
export function visitManifest(resources, registry, visitor, options = {}) {
|
|
9
|
-
const { aliases, aliasesByModule, skipKinds, expand } = options;
|
|
51
|
+
const { aliases, aliasesByModule, skipKinds, expand, discoverNestedRefs } = options;
|
|
10
52
|
const wantsRefs = !!visitor.onRef;
|
|
11
53
|
const wantsScope = !!visitor.onScope;
|
|
12
54
|
const wantsSchemaFrom = !!visitor.onSchemaFrom;
|
|
13
55
|
const wantsCel = !!visitor.onCel;
|
|
56
|
+
const wantsNested = wantsRefs && !!discoverNestedRefs;
|
|
14
57
|
for (const r of resources) {
|
|
15
58
|
if (!r.metadata?.name || !r.kind)
|
|
16
59
|
continue;
|
|
@@ -20,6 +63,9 @@ export function visitManifest(resources, registry, visitor, options = {}) {
|
|
|
20
63
|
const definition = registry.resolve(r.kind) ??
|
|
21
64
|
(resolvedKind ? registry.resolve(resolvedKind) : undefined);
|
|
22
65
|
visitor.onResourceEnter?.({ source: r, definition });
|
|
66
|
+
// Concrete paths emitted from the field map — so the value-tree scan below
|
|
67
|
+
// doesn't re-emit a ref the field map already covered.
|
|
68
|
+
const emittedRefPaths = wantsNested ? new Set() : null;
|
|
23
69
|
if (wantsRefs || wantsScope || wantsSchemaFrom) {
|
|
24
70
|
const baseMap = aliases
|
|
25
71
|
? registry.getFieldMapForKind(r.kind, aliases)
|
|
@@ -69,6 +115,7 @@ export function visitManifest(resources, registry, visitor, options = {}) {
|
|
|
69
115
|
for (const { value, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
70
116
|
if (!value)
|
|
71
117
|
continue;
|
|
118
|
+
emittedRefPaths?.add(concretePath);
|
|
72
119
|
visitor.onRef({
|
|
73
120
|
source: r,
|
|
74
121
|
fieldPath,
|
|
@@ -90,6 +137,30 @@ export function visitManifest(resources, registry, visitor, options = {}) {
|
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
}
|
|
140
|
+
// Value-tree-driven nested ref discovery — refs the field map can't reach
|
|
141
|
+
// because they sit behind a `$ref` it doesn't descend (e.g. Run.Sequence
|
|
142
|
+
// step `invoke`s). Deduped against the field-map sites by concrete path.
|
|
143
|
+
// Scanned per top-level field so the resource's own `kind` isn't treated as
|
|
144
|
+
// a resource boundary by `walkRefValues`.
|
|
145
|
+
if (wantsNested) {
|
|
146
|
+
const emitNested = (value, path) => {
|
|
147
|
+
if (emittedRefPaths.has(path))
|
|
148
|
+
return;
|
|
149
|
+
visitor.onRef({
|
|
150
|
+
source: r,
|
|
151
|
+
fieldPath: path,
|
|
152
|
+
concretePath: path,
|
|
153
|
+
value,
|
|
154
|
+
entry: NESTED_REF_ENTRY,
|
|
155
|
+
inScope: false,
|
|
156
|
+
visibleScopeManifests: [],
|
|
157
|
+
nested: true,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
for (const [key, value] of Object.entries(r)) {
|
|
161
|
+
walkRefValues(value, key, emitNested);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
93
164
|
if (wantsCel) {
|
|
94
165
|
const contexts = definition?.schema ? extractContextsFromSchema(definition.schema) : [];
|
|
95
166
|
walkCelExpressions(r, "", (expr, path, engineName) => {
|
|
@@ -157,6 +157,22 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
|
|
|
157
157
|
if (node["x-telo-context"])
|
|
158
158
|
entry.context = node["x-telo-context"];
|
|
159
159
|
map.set(path, entry);
|
|
160
|
+
// A node can mix item-level ref branches (a bare string / `{kind, name}`)
|
|
161
|
+
// with object branches that carry their OWN nested refs — e.g. Application
|
|
162
|
+
// `targets`: a bare ref vs inline `{ invoke }` vs gated `{ ref }`. Descend
|
|
163
|
+
// into the variant objects so those nested slots register too (and their
|
|
164
|
+
// `!ref` sentinels resolve). Pure x-telo-ref branches have no properties
|
|
165
|
+
// and contribute nothing here.
|
|
166
|
+
for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
|
|
167
|
+
const variants = node[variantKey];
|
|
168
|
+
if (!Array.isArray(variants))
|
|
169
|
+
continue;
|
|
170
|
+
for (const variant of variants) {
|
|
171
|
+
if (!variant || typeof variant !== "object")
|
|
172
|
+
continue;
|
|
173
|
+
traverseVariant(variant, path, map, root, visitedRefs);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
160
176
|
return;
|
|
161
177
|
}
|
|
162
178
|
// Array — recurse into items
|
|
@@ -4,6 +4,10 @@ import type { DefinitionRegistry } from "./definition-registry.js";
|
|
|
4
4
|
export interface ThrowsCodeMeta {
|
|
5
5
|
data?: Record<string, any>;
|
|
6
6
|
}
|
|
7
|
+
/** Code a non-`InvokeError` failure surfaces as inside a `catch` block. Mirrors
|
|
8
|
+
* `PLAIN_ERROR_CODE` in `@telorun/run`'s `toSequenceError`: any invoke can throw
|
|
9
|
+
* a plain error, which the catch sees as `error.code === "INTERNAL_ERROR"`. */
|
|
10
|
+
export declare const PLAIN_ERROR_CODE = "INTERNAL_ERROR";
|
|
7
11
|
export interface ThrowsUnion {
|
|
8
12
|
/** Code → per-code metadata (data schema, etc). Keys are the declared codes. */
|
|
9
13
|
codes: Map<string, ThrowsCodeMeta>;
|
|
@@ -12,6 +16,12 @@ export interface ThrowsUnion {
|
|
|
12
16
|
* an unknown kind was encountered, or a cycle short-circuited resolution.
|
|
13
17
|
* Callers must treat unbounded unions as requiring a catch-all entry. */
|
|
14
18
|
unbounded: boolean;
|
|
19
|
+
/** True when the block can fail with a non-`InvokeError` (any `invoke:` step).
|
|
20
|
+
* Such a failure surfaces inside an enclosing `catch` as `PLAIN_ERROR_CODE`,
|
|
21
|
+
* so a `throw: { code: "${{ error.code }}" }` rethrow can propagate it. Not
|
|
22
|
+
* injected into `codes` — only seeds `enclosingTryCodes` at a try/catch site,
|
|
23
|
+
* leaving non-rethrow unions untouched. */
|
|
24
|
+
canThrowPlain?: boolean;
|
|
15
25
|
}
|
|
16
26
|
export interface ResolveCtx {
|
|
17
27
|
allManifests: ResourceManifest[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-throws-union.d.ts","sourceRoot":"","sources":["../src/resolve-throws-union.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve-throws-union.d.ts","sourceRoot":"","sources":["../src/resolve-throws-union.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED;;gFAEgF;AAChF,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACnC;;;8EAG0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;gDAI4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,gBAAgB,EAAE,EAChC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,GACrB,UAAU,CAQZ;AAgCD;;;;8CAI8C;AAC9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,EAAE,UAAU,GACd,WAAW,CA+Cb"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
/** Code a non-`InvokeError` failure surfaces as inside a `catch` block. Mirrors
|
|
3
|
+
* `PLAIN_ERROR_CODE` in `@telorun/run`'s `toSequenceError`: any invoke can throw
|
|
4
|
+
* a plain error, which the catch sees as `error.code === "INTERNAL_ERROR"`. */
|
|
5
|
+
export const PLAIN_ERROR_CODE = "INTERNAL_ERROR";
|
|
1
6
|
export function createResolveCtx(allManifests, defs, aliases) {
|
|
2
7
|
return {
|
|
3
8
|
allManifests,
|
|
@@ -17,6 +22,8 @@ function unionInto(target, src) {
|
|
|
17
22
|
}
|
|
18
23
|
if (src.unbounded)
|
|
19
24
|
target.unbounded = true;
|
|
25
|
+
if (src.canThrowPlain)
|
|
26
|
+
target.canThrowPlain = true;
|
|
20
27
|
}
|
|
21
28
|
function definitionFor(kind, defs, aliases) {
|
|
22
29
|
const resolved = aliases.resolveKind(kind);
|
|
@@ -115,7 +122,11 @@ function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx) {
|
|
|
115
122
|
* composers that reuse those shape conventions work without changes here. */
|
|
116
123
|
function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
117
124
|
if (step[invokeField]) {
|
|
118
|
-
|
|
125
|
+
// Any invoked resource can throw a non-InvokeError at runtime, which an
|
|
126
|
+
// enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
|
|
127
|
+
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
128
|
+
u.canThrowPlain = true;
|
|
129
|
+
return u;
|
|
119
130
|
}
|
|
120
131
|
if (step.throw && typeof step.throw === "object") {
|
|
121
132
|
return resolveThrowStepCode(step.throw, enclosingTryCodes);
|
|
@@ -128,6 +139,11 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
128
139
|
// out instead. Sequence-specific subtraction — the plan explicitly
|
|
129
140
|
// anchors this to Run.Sequence's try/catch schema shape.
|
|
130
141
|
const tryCodes = new Set(tryUnion.codes.keys());
|
|
142
|
+
// A plain (non-InvokeError) failure in the try block reaches the catch as
|
|
143
|
+
// `error.code === PLAIN_ERROR_CODE`, so a `throw: { code: error.code }`
|
|
144
|
+
// rethrow can propagate it — seed the set the catch resolves against.
|
|
145
|
+
if (tryUnion.canThrowPlain)
|
|
146
|
+
tryCodes.add(PLAIN_ERROR_CODE);
|
|
131
147
|
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
132
148
|
// Unbounded in the try block still signals the caller to expect
|
|
133
149
|
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
@@ -179,6 +195,8 @@ function cloneUnion(u) {
|
|
|
179
195
|
for (const [c, m] of u.codes)
|
|
180
196
|
out.codes.set(c, m);
|
|
181
197
|
out.unbounded = u.unbounded;
|
|
198
|
+
if (u.canThrowPlain)
|
|
199
|
+
out.canThrowPlain = true;
|
|
182
200
|
return out;
|
|
183
201
|
}
|
|
184
202
|
function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
@@ -226,14 +244,24 @@ function resolveThrowStepCode(throwSpec, enclosingTryCodes) {
|
|
|
226
244
|
return resolveCodeExpression(throwSpec.code, enclosingTryCodes);
|
|
227
245
|
}
|
|
228
246
|
function resolveCodeExpression(codeInput, enclosingTryCodes) {
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
// A `!cel`-tagged sentinel and a `${{ … }}` string must resolve identically —
|
|
248
|
+
// normalize both to the inner CEL expression (or a bare literal code).
|
|
249
|
+
let expr;
|
|
250
|
+
if (isTaggedSentinel(codeInput)) {
|
|
251
|
+
if (codeInput.engine !== "cel")
|
|
252
|
+
return { codes: new Map(), unbounded: true };
|
|
253
|
+
expr = codeInput.source.trim();
|
|
231
254
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
else if (typeof codeInput === "string" && codeInput.length > 0) {
|
|
256
|
+
const match = codeInput.match(/^\s*\$\{\{\s*([\s\S]+?)\s*\}\}\s*$/);
|
|
257
|
+
if (!match) {
|
|
258
|
+
return { codes: new Map([[codeInput, {}]]), unbounded: false };
|
|
259
|
+
}
|
|
260
|
+
expr = match[1].trim();
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
return { codes: new Map(), unbounded: true };
|
|
235
264
|
}
|
|
236
|
-
const expr = match[1].trim();
|
|
237
265
|
const litMatch = expr.match(/^'([^']+)'$|^"([^"]+)"$/);
|
|
238
266
|
if (litMatch) {
|
|
239
267
|
const code = litMatch[1] ?? litMatch[2];
|
|
@@ -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;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
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;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAiZtB"}
|
|
@@ -232,6 +232,13 @@ export function validateReferences(resources, context) {
|
|
|
232
232
|
// Skip inline resources — Phase 2 normalization hasn't run yet.
|
|
233
233
|
if (isInlineResource(refVal))
|
|
234
234
|
return;
|
|
235
|
+
// Polymorphic ref slots (Application `targets`) accept object forms
|
|
236
|
+
// whose references live in nested slots rather than being a `{kind,
|
|
237
|
+
// name}` ref themselves — inline `{ invoke }` and gated `{ ref }`.
|
|
238
|
+
// Those nested refs are validated via their own field-map entries, so
|
|
239
|
+
// skip the item-level structural check here.
|
|
240
|
+
if (typeof refVal.kind !== "string" && ("invoke" in refVal || "ref" in refVal))
|
|
241
|
+
return;
|
|
235
242
|
// 1. Structural check
|
|
236
243
|
if (typeof refVal.kind !== "string" || typeof refVal.name !== "string") {
|
|
237
244
|
diagnostics.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.
|
|
45
|
+
"@telorun/templating": "1.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"vitest": "^2.1.8"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@telorun/sdk": "0.
|
|
53
|
+
"@telorun/sdk": "1.0.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsc -p tsconfig.lib.json",
|
package/src/analysis-registry.ts
CHANGED
|
@@ -73,7 +73,7 @@ export class AnalysisRegistry {
|
|
|
73
73
|
visitManifest(
|
|
74
74
|
resources: ResourceManifest[],
|
|
75
75
|
visitor: ManifestVisitor,
|
|
76
|
-
opts?: { skipKinds?: ReadonlySet<string>; expand?: boolean },
|
|
76
|
+
opts?: { skipKinds?: ReadonlySet<string>; expand?: boolean; discoverNestedRefs?: boolean },
|
|
77
77
|
): void {
|
|
78
78
|
runVisitManifest(resources, this.defs, visitor, {
|
|
79
79
|
aliases: this.aliases,
|
package/src/analyzer.ts
CHANGED
|
@@ -379,6 +379,78 @@ function buildStepContextSchema(
|
|
|
379
379
|
return undefined;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Collect every field annotated with `x-telo-error-context` anywhere in a
|
|
384
|
+
* definition schema (resolving local `$ref`s into `$defs`, cycle-safe), mapping
|
|
385
|
+
* the annotated field name to its declared error-shape schema. The field name
|
|
386
|
+
* is matched against CEL paths so the context applies at any nesting depth under
|
|
387
|
+
* that field — e.g. `error` inside a `catch:` nested inside another `try:`. No
|
|
388
|
+
* specific field name (or `Run.Sequence`) is hardcoded; any composer that tags
|
|
389
|
+
* its error-bearing branch fields opts in the same way.
|
|
390
|
+
*/
|
|
391
|
+
function collectErrorContextScopes(
|
|
392
|
+
defSchema: Record<string, any> | undefined,
|
|
393
|
+
): Map<string, Record<string, any>> {
|
|
394
|
+
const out = new Map<string, Record<string, any>>();
|
|
395
|
+
if (!defSchema || typeof defSchema !== "object") return out;
|
|
396
|
+
const seen = new Set<Record<string, any>>();
|
|
397
|
+
|
|
398
|
+
const walk = (schema: Record<string, any> | undefined): void => {
|
|
399
|
+
if (!schema || typeof schema !== "object" || seen.has(schema)) return;
|
|
400
|
+
seen.add(schema);
|
|
401
|
+
|
|
402
|
+
const props = schema.properties as Record<string, any> | undefined;
|
|
403
|
+
if (props) {
|
|
404
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
405
|
+
if (fieldSchema && typeof fieldSchema === "object") {
|
|
406
|
+
const errCtx = (fieldSchema as Record<string, any>)["x-telo-error-context"];
|
|
407
|
+
if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
|
|
408
|
+
out.set(fieldName, errCtx as Record<string, any>);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
walk(resolveLocalRef(fieldSchema as Record<string, any>, defSchema));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (schema.items) walk(resolveLocalRef(schema.items as Record<string, any>, defSchema));
|
|
415
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
416
|
+
const arr = schema[key];
|
|
417
|
+
if (Array.isArray(arr)) for (const sub of arr) walk(resolveLocalRef(sub, defSchema));
|
|
418
|
+
}
|
|
419
|
+
if (schema.$defs && typeof schema.$defs === "object") {
|
|
420
|
+
for (const sub of Object.values(schema.$defs as Record<string, any>)) {
|
|
421
|
+
walk(sub as Record<string, any>);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
walk(defSchema);
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Return the error-context schema for a CEL `path` when the path lies within
|
|
432
|
+
* (any depth under) one of the error-bearing fields, else undefined. A path is
|
|
433
|
+
* "within" field `f` when it contains a segment `f[<index>]`. When multiple
|
|
434
|
+
* error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
|
|
435
|
+
* deepest — the one whose segment appears latest in the path — wins, so the
|
|
436
|
+
* innermost branch's schema governs.
|
|
437
|
+
*/
|
|
438
|
+
function errorContextForPath(
|
|
439
|
+
path: string,
|
|
440
|
+
scopes: Map<string, Record<string, any>>,
|
|
441
|
+
): Record<string, any> | undefined {
|
|
442
|
+
let best: { index: number; schema: Record<string, any> } | undefined;
|
|
443
|
+
for (const [fieldName, schema] of scopes) {
|
|
444
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
445
|
+
for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
|
|
446
|
+
if (best === undefined || match.index > best.index) {
|
|
447
|
+
best = { index: match.index, schema };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return best?.schema;
|
|
452
|
+
}
|
|
453
|
+
|
|
382
454
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
383
455
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
384
456
|
|
|
@@ -954,6 +1026,7 @@ export class StaticAnalyzer {
|
|
|
954
1026
|
// context — which require analyzer state to build — are stashed here.
|
|
955
1027
|
let celStepContextSchema: Record<string, any> | undefined;
|
|
956
1028
|
let celInvocationContext: Record<string, any> | undefined;
|
|
1029
|
+
let celErrorScopes: Map<string, Record<string, any>> = new Map();
|
|
957
1030
|
|
|
958
1031
|
visitManifest(
|
|
959
1032
|
allManifests,
|
|
@@ -973,6 +1046,9 @@ export class StaticAnalyzer {
|
|
|
973
1046
|
aliases,
|
|
974
1047
|
)
|
|
975
1048
|
: undefined;
|
|
1049
|
+
celErrorScopes = collectErrorContextScopes(
|
|
1050
|
+
e.definition?.schema as Record<string, any> | undefined,
|
|
1051
|
+
);
|
|
976
1052
|
},
|
|
977
1053
|
onCel: (e) => {
|
|
978
1054
|
const m = e.source;
|
|
@@ -995,6 +1071,22 @@ export class StaticAnalyzer {
|
|
|
995
1071
|
};
|
|
996
1072
|
}
|
|
997
1073
|
|
|
1074
|
+
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
1075
|
+
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
1076
|
+
const errorSchema =
|
|
1077
|
+
celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
|
|
1078
|
+
if (errorSchema) {
|
|
1079
|
+
const base =
|
|
1080
|
+
matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
1081
|
+
matchedContext = {
|
|
1082
|
+
...base,
|
|
1083
|
+
properties: {
|
|
1084
|
+
...(base.properties ?? {}),
|
|
1085
|
+
error: errorSchema,
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
998
1090
|
let effectiveContext: Record<string, any> | null = null;
|
|
999
1091
|
if (matchedContext) {
|
|
1000
1092
|
const manifestItem = matchedScope
|
|
@@ -1046,6 +1138,14 @@ export class StaticAnalyzer {
|
|
|
1046
1138
|
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
1047
1139
|
data: { resource, filePath, path },
|
|
1048
1140
|
});
|
|
1141
|
+
} else if (f.code === "CEL_NULLABLE_ACCESS") {
|
|
1142
|
+
diagnostics.push({
|
|
1143
|
+
severity: DiagnosticSeverity.Error,
|
|
1144
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
1145
|
+
source: SOURCE,
|
|
1146
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
1147
|
+
data: { resource, filePath, path },
|
|
1148
|
+
});
|
|
1049
1149
|
} else {
|
|
1050
1150
|
// Unknown code from a future engine — pass the message through,
|
|
1051
1151
|
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
package/src/builtins.ts
CHANGED
|
@@ -234,6 +234,66 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
234
234
|
},
|
|
235
235
|
additionalProperties: true,
|
|
236
236
|
},
|
|
237
|
+
// Gated reference: run() a Runnable/Service only when the
|
|
238
|
+
// `when` CEL guard holds. Discriminated by the `ref` key. `ref`
|
|
239
|
+
// is a bare name or a resolved `!ref` (`{ kind, name }`).
|
|
240
|
+
{
|
|
241
|
+
type: "object",
|
|
242
|
+
required: ["ref"],
|
|
243
|
+
properties: {
|
|
244
|
+
ref: {
|
|
245
|
+
anyOf: [
|
|
246
|
+
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
247
|
+
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
248
|
+
{
|
|
249
|
+
type: "object",
|
|
250
|
+
required: ["kind", "name"],
|
|
251
|
+
properties: {
|
|
252
|
+
kind: { type: "string" },
|
|
253
|
+
name: { type: "string" },
|
|
254
|
+
},
|
|
255
|
+
additionalProperties: true,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
when: { type: "string" },
|
|
260
|
+
},
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
},
|
|
263
|
+
// Inline flat invoke step: invoke an Invocable / Runnable on boot
|
|
264
|
+
// with an optional `name` (for steps.<name>.result plumbing),
|
|
265
|
+
// `when` guard, and `inputs`. Discriminated by the `invoke` key.
|
|
266
|
+
// Control flow (if/while/switch/try) is not available here —
|
|
267
|
+
// reach for Run.Sequence. `invoke` is ref-only and must resolve
|
|
268
|
+
// to a `{ kind, name }` reference (a `!ref` / `{kind,name}`):
|
|
269
|
+
// requiring `name` rejects an inline `{ kind }` definition (no
|
|
270
|
+
// name) at analysis instead of failing at boot with an undefined
|
|
271
|
+
// resource name. The Invocable/Runnable kind set mirrors
|
|
272
|
+
// Run.Sequence invoke steps.
|
|
273
|
+
{
|
|
274
|
+
type: "object",
|
|
275
|
+
required: ["invoke"],
|
|
276
|
+
properties: {
|
|
277
|
+
name: { type: "string" },
|
|
278
|
+
invoke: {
|
|
279
|
+
"x-telo-topology-role": "invoke",
|
|
280
|
+
type: "object",
|
|
281
|
+
required: ["kind", "name"],
|
|
282
|
+
properties: {
|
|
283
|
+
kind: { type: "string" },
|
|
284
|
+
name: { type: "string" },
|
|
285
|
+
},
|
|
286
|
+
additionalProperties: true,
|
|
287
|
+
anyOf: [
|
|
288
|
+
{ "x-telo-ref": "telo#Invocable" },
|
|
289
|
+
{ "x-telo-ref": "telo#Runnable" },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
inputs: { type: "object", additionalProperties: true },
|
|
293
|
+
when: { type: "string" },
|
|
294
|
+
},
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
},
|
|
237
297
|
],
|
|
238
298
|
},
|
|
239
299
|
},
|
package/src/manifest-visitor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import { walkCelExpressions } from "@telorun/templating";
|
|
2
|
+
import { isRefSentinel, isTaggedSentinel, walkCelExpressions } from "@telorun/templating";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import {
|
|
@@ -80,6 +80,13 @@ export interface RefSiteEvent {
|
|
|
80
80
|
inScope: boolean;
|
|
81
81
|
/** Scope manifests visible to this ref path (non-empty only when `inScope`). */
|
|
82
82
|
visibleScopeManifests: ResourceManifest[];
|
|
83
|
+
/** True when the site was found by value-tree scanning rather than the field
|
|
84
|
+
* map (only when `discoverNestedRefs` is set) — a ref nested behind a `$ref`
|
|
85
|
+
* the field map doesn't descend (e.g. `Run.Sequence` `steps[].invoke`).
|
|
86
|
+
* Nested sites carry no x-telo-ref constraint (`entry.refs` is empty) and no
|
|
87
|
+
* scope info; `concretePath` still points at the exact location, so consumers
|
|
88
|
+
* can anchor to it. */
|
|
89
|
+
nested?: boolean;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface SchemaFromSiteEvent {
|
|
@@ -123,6 +130,58 @@ export interface VisitOptions {
|
|
|
123
130
|
* so refs nested behind `x-telo-schema-from` are surfaced. `SchemaFromSite`
|
|
124
131
|
* events are always emitted from the base map regardless of this flag. */
|
|
125
132
|
expand?: boolean;
|
|
133
|
+
/** When true, additionally discover refs by scanning each resource's value
|
|
134
|
+
* tree for `!ref` sentinels and `{kind, name}` reference objects — surfacing
|
|
135
|
+
* refs the field map never lists because they sit behind a `$ref` it doesn't
|
|
136
|
+
* descend (notably `Run.Sequence` step `invoke`s). Emitted as `RefSite`s with
|
|
137
|
+
* `nested: true`, deduped against the field-map sites by concrete path.
|
|
138
|
+
* Opt-in: the validators / dependency graph must NOT enable it (those refs
|
|
139
|
+
* are runtime-resolved, not boot dependencies). */
|
|
140
|
+
discoverNestedRefs?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Synthetic entry for a value-tree-discovered ref — these carry no declared
|
|
144
|
+
* x-telo-ref constraint. */
|
|
145
|
+
const NESTED_REF_ENTRY: RefFieldEntry = { refs: [], isArray: false };
|
|
146
|
+
|
|
147
|
+
/** Scans a value tree for ref-shaped values, emitting each with its concrete
|
|
148
|
+
* path. Recognizes `!ref <name>` sentinels and named `{kind, name}` reference
|
|
149
|
+
* objects. Other tagged sentinels (`!cel`, `!literal`) and precompiled nodes
|
|
150
|
+
* are leaves. Path format matches `resolveFieldEntries` / `walkCelExpressions`
|
|
151
|
+
* (`a.b[0].c`).
|
|
152
|
+
*
|
|
153
|
+
* Stops at every `{kind, …}` resource boundary: a named ref is emitted, an
|
|
154
|
+
* inline resource (`{kind}` with no name) is left alone, and **neither is
|
|
155
|
+
* descended into**. A nested resource's own refs belong to its inner topology,
|
|
156
|
+
* not the enclosing node — e.g. an inline `Sql.Exec` step's `connection` is the
|
|
157
|
+
* Exec's dependency, not the surrounding `Run.Sequence`'s. The scan is started
|
|
158
|
+
* per top-level field (not on the resource object) so the resource's own
|
|
159
|
+
* `kind` doesn't trip this boundary. */
|
|
160
|
+
function walkRefValues(
|
|
161
|
+
value: unknown,
|
|
162
|
+
path: string,
|
|
163
|
+
cb: (value: unknown, path: string) => void,
|
|
164
|
+
): void {
|
|
165
|
+
if (isRefSentinel(value)) {
|
|
166
|
+
cb(value, path);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (isTaggedSentinel(value)) return;
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
value.forEach((v, i) => walkRefValues(v, `${path}[${i}]`, cb));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (value === null || typeof value !== "object") return;
|
|
175
|
+
if ((value as { __compiled?: unknown }).__compiled) return;
|
|
176
|
+
const obj = value as Record<string, unknown>;
|
|
177
|
+
if (typeof obj.kind === "string") {
|
|
178
|
+
// Resource boundary — emit if it's a named ref, then stop descending.
|
|
179
|
+
if (typeof obj.name === "string") cb(value, path);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
183
|
+
walkRefValues(v, path ? `${path}.${k}` : k, cb);
|
|
184
|
+
}
|
|
126
185
|
}
|
|
127
186
|
|
|
128
187
|
const scopePrefixOf = (pointer: string): string =>
|
|
@@ -139,12 +198,13 @@ export function visitManifest(
|
|
|
139
198
|
visitor: ManifestVisitor,
|
|
140
199
|
options: VisitOptions = {},
|
|
141
200
|
): void {
|
|
142
|
-
const { aliases, aliasesByModule, skipKinds, expand } = options;
|
|
201
|
+
const { aliases, aliasesByModule, skipKinds, expand, discoverNestedRefs } = options;
|
|
143
202
|
|
|
144
203
|
const wantsRefs = !!visitor.onRef;
|
|
145
204
|
const wantsScope = !!visitor.onScope;
|
|
146
205
|
const wantsSchemaFrom = !!visitor.onSchemaFrom;
|
|
147
206
|
const wantsCel = !!visitor.onCel;
|
|
207
|
+
const wantsNested = wantsRefs && !!discoverNestedRefs;
|
|
148
208
|
|
|
149
209
|
for (const r of resources) {
|
|
150
210
|
if (!r.metadata?.name || !r.kind) continue;
|
|
@@ -157,6 +217,10 @@ export function visitManifest(
|
|
|
157
217
|
|
|
158
218
|
visitor.onResourceEnter?.({ source: r, definition });
|
|
159
219
|
|
|
220
|
+
// Concrete paths emitted from the field map — so the value-tree scan below
|
|
221
|
+
// doesn't re-emit a ref the field map already covered.
|
|
222
|
+
const emittedRefPaths = wantsNested ? new Set<string>() : null;
|
|
223
|
+
|
|
160
224
|
if (wantsRefs || wantsScope || wantsSchemaFrom) {
|
|
161
225
|
const baseMap = aliases
|
|
162
226
|
? registry.getFieldMapForKind(r.kind, aliases)
|
|
@@ -208,6 +272,7 @@ export function visitManifest(
|
|
|
208
272
|
|
|
209
273
|
for (const { value, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
210
274
|
if (!value) continue;
|
|
275
|
+
emittedRefPaths?.add(concretePath);
|
|
211
276
|
visitor.onRef!({
|
|
212
277
|
source: r,
|
|
213
278
|
fieldPath,
|
|
@@ -230,6 +295,30 @@ export function visitManifest(
|
|
|
230
295
|
}
|
|
231
296
|
}
|
|
232
297
|
|
|
298
|
+
// Value-tree-driven nested ref discovery — refs the field map can't reach
|
|
299
|
+
// because they sit behind a `$ref` it doesn't descend (e.g. Run.Sequence
|
|
300
|
+
// step `invoke`s). Deduped against the field-map sites by concrete path.
|
|
301
|
+
// Scanned per top-level field so the resource's own `kind` isn't treated as
|
|
302
|
+
// a resource boundary by `walkRefValues`.
|
|
303
|
+
if (wantsNested) {
|
|
304
|
+
const emitNested = (value: unknown, path: string) => {
|
|
305
|
+
if (emittedRefPaths!.has(path)) return;
|
|
306
|
+
visitor.onRef!({
|
|
307
|
+
source: r,
|
|
308
|
+
fieldPath: path,
|
|
309
|
+
concretePath: path,
|
|
310
|
+
value,
|
|
311
|
+
entry: NESTED_REF_ENTRY,
|
|
312
|
+
inScope: false,
|
|
313
|
+
visibleScopeManifests: [],
|
|
314
|
+
nested: true,
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
for (const [key, value] of Object.entries(r as Record<string, unknown>)) {
|
|
318
|
+
walkRefValues(value, key, emitNested);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
233
322
|
if (wantsCel) {
|
|
234
323
|
const contexts = definition?.schema ? extractContextsFromSchema(definition.schema) : [];
|
|
235
324
|
walkCelExpressions(r, "", (expr, path, engineName) => {
|
|
@@ -213,6 +213,20 @@ function traverseNode(
|
|
|
213
213
|
const entry: RefFieldEntry = { refs, isArray: path.includes("[]") };
|
|
214
214
|
if (node["x-telo-context"]) entry.context = node["x-telo-context"] as Record<string, any>;
|
|
215
215
|
map.set(path, entry);
|
|
216
|
+
// A node can mix item-level ref branches (a bare string / `{kind, name}`)
|
|
217
|
+
// with object branches that carry their OWN nested refs — e.g. Application
|
|
218
|
+
// `targets`: a bare ref vs inline `{ invoke }` vs gated `{ ref }`. Descend
|
|
219
|
+
// into the variant objects so those nested slots register too (and their
|
|
220
|
+
// `!ref` sentinels resolve). Pure x-telo-ref branches have no properties
|
|
221
|
+
// and contribute nothing here.
|
|
222
|
+
for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
|
|
223
|
+
const variants = node[variantKey];
|
|
224
|
+
if (!Array.isArray(variants)) continue;
|
|
225
|
+
for (const variant of variants) {
|
|
226
|
+
if (!variant || typeof variant !== "object") continue;
|
|
227
|
+
traverseVariant(variant as Record<string, any>, path, map, root, visitedRefs);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
216
230
|
return;
|
|
217
231
|
}
|
|
218
232
|
|
|
@@ -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]!;
|
|
@@ -248,6 +248,13 @@ export function validateReferences(
|
|
|
248
248
|
// Skip inline resources — Phase 2 normalization hasn't run yet.
|
|
249
249
|
if (isInlineResource(refVal)) return;
|
|
250
250
|
|
|
251
|
+
// Polymorphic ref slots (Application `targets`) accept object forms
|
|
252
|
+
// whose references live in nested slots rather than being a `{kind,
|
|
253
|
+
// name}` ref themselves — inline `{ invoke }` and gated `{ ref }`.
|
|
254
|
+
// Those nested refs are validated via their own field-map entries, so
|
|
255
|
+
// skip the item-level structural check here.
|
|
256
|
+
if (typeof refVal.kind !== "string" && ("invoke" in refVal || "ref" in refVal)) return;
|
|
257
|
+
|
|
251
258
|
// 1. Structural check
|
|
252
259
|
if (typeof refVal.kind !== "string" || typeof refVal.name !== "string") {
|
|
253
260
|
diagnostics.push({
|