@telorun/analyzer 0.13.0 → 0.14.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/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +18 -0
- package/dist/adapters/node-adapter.d.ts +17 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +71 -0
- package/dist/adapters/registry-adapter.d.ts +15 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +53 -0
- package/dist/alias-resolver.d.ts +6 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +8 -0
- package/dist/analysis-registry.d.ts +1 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +113 -5
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +61 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +13 -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-ref-sentinels.d.ts +15 -17
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +94 -40
- 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 +105 -7
- 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
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
|
|
@@ -3,25 +3,23 @@ import type { AliasResolver } from "./alias-resolver.js";
|
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
/**
|
|
5
5
|
* Walks every `x-telo-ref` slot in every non-system resource and rewrites
|
|
6
|
-
* `!ref <name>` sentinels in-place to `{kind
|
|
6
|
+
* `!ref <name>` sentinels in-place to `{kind, name}` (local) or
|
|
7
|
+
* `{kind, name, alias}` (cross-module).
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Reference grammar — the tag's source string is split on the FIRST dot:
|
|
10
|
+
* - `!ref writeLine` → local resource `writeLine`
|
|
11
|
+
* - `!ref Self.writeLine` → local resource `writeLine` (explicit self-qualifier)
|
|
12
|
+
* - `!ref Console.writeLine` → instance `writeLine` exported by the import aliased
|
|
13
|
+
* `Console`, resolved against the forwarded foreign set
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Aliases are PascalCase identifiers without dots and resource names carry no dots
|
|
16
|
+
* (enforced as a hard diagnostic), so the first-dot split is unambiguous. When the
|
|
17
|
+
* name doesn't resolve, the sentinel is left in place so `validateReferences` emits the
|
|
18
|
+
* `UNRESOLVED_REFERENCE` diagnostic with full context.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* to the path-encoding rules of `resolveFieldEntries` — any new path
|
|
23
|
-
* marker would silently break this writer. Descending directly avoids
|
|
24
|
-
* that coupling.
|
|
20
|
+
* Forwarded foreign resources (an imported library's exported instances, carrying a
|
|
21
|
+
* `metadata.module` that isn't a root module) are resolution TARGETS only — they are not
|
|
22
|
+
* re-walked as sources here, since their own ref slots belong to their own module scope.
|
|
25
23
|
*/
|
|
26
|
-
export declare function resolveRefSentinels(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver
|
|
24
|
+
export declare function resolveRefSentinels(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>, crossModuleTargets?: ResourceManifest[]): void;
|
|
27
25
|
//# sourceMappingURL=resolve-ref-sentinels.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AASnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAK5C,kBAAkB,GAAE,gBAAgB,EAAO,GAC1C,IAAI,CA8EN"}
|
|
@@ -1,38 +1,99 @@
|
|
|
1
1
|
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
2
3
|
import { isRefEntry } from "./reference-field-map.js";
|
|
3
4
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
4
5
|
/**
|
|
5
6
|
* Walks every `x-telo-ref` slot in every non-system resource and rewrites
|
|
6
|
-
* `!ref <name>` sentinels in-place to `{kind
|
|
7
|
+
* `!ref <name>` sentinels in-place to `{kind, name}` (local) or
|
|
8
|
+
* `{kind, name, alias}` (cross-module).
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Reference grammar — the tag's source string is split on the FIRST dot:
|
|
11
|
+
* - `!ref writeLine` → local resource `writeLine`
|
|
12
|
+
* - `!ref Self.writeLine` → local resource `writeLine` (explicit self-qualifier)
|
|
13
|
+
* - `!ref Console.writeLine` → instance `writeLine` exported by the import aliased
|
|
14
|
+
* `Console`, resolved against the forwarded foreign set
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Aliases are PascalCase identifiers without dots and resource names carry no dots
|
|
17
|
+
* (enforced as a hard diagnostic), so the first-dot split is unambiguous. When the
|
|
18
|
+
* name doesn't resolve, the sentinel is left in place so `validateReferences` emits the
|
|
19
|
+
* `UNRESOLVED_REFERENCE` diagnostic with full context.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* to the path-encoding rules of `resolveFieldEntries` — any new path
|
|
23
|
-
* marker would silently break this writer. Descending directly avoids
|
|
24
|
-
* that coupling.
|
|
21
|
+
* Forwarded foreign resources (an imported library's exported instances, carrying a
|
|
22
|
+
* `metadata.module` that isn't a root module) are resolution TARGETS only — they are not
|
|
23
|
+
* re-walked as sources here, since their own ref slots belong to their own module scope.
|
|
25
24
|
*/
|
|
26
|
-
export function resolveRefSentinels(resources, registry, aliases, aliasesByModule
|
|
25
|
+
export function resolveRefSentinels(resources, registry, aliases, aliasesByModule,
|
|
26
|
+
// Extra foreign resources used only as cross-module resolution TARGETS (not mutated, not
|
|
27
|
+
// walked as sources). The kernel passes the analyzer-flattened set here so the runtime
|
|
28
|
+
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
29
|
+
// imported libraries' exported instances.
|
|
30
|
+
crossModuleTargets = []) {
|
|
31
|
+
const rootModules = new Set();
|
|
32
|
+
for (const r of resources) {
|
|
33
|
+
if (isModuleKind(r.kind) && typeof r.metadata?.name === "string") {
|
|
34
|
+
rootModules.add(r.metadata.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const moduleOf = (r) => r.metadata?.module;
|
|
38
|
+
const isForeign = (r) => {
|
|
39
|
+
const mod = moduleOf(r);
|
|
40
|
+
return typeof mod === "string" && !rootModules.has(mod);
|
|
41
|
+
};
|
|
42
|
+
// Local resources resolve a bare / `Self.`-qualified name; forwarded foreign exports
|
|
43
|
+
// resolve an `Alias.`-qualified name keyed by (module, name).
|
|
27
44
|
const byName = new Map();
|
|
45
|
+
const byModuleName = new Map();
|
|
28
46
|
for (const r of resources) {
|
|
29
|
-
if (r.metadata?.name
|
|
30
|
-
|
|
47
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind))
|
|
48
|
+
continue;
|
|
49
|
+
const name = r.metadata.name;
|
|
50
|
+
if (isForeign(r)) {
|
|
51
|
+
byModuleName.set(`${moduleOf(r)}\0${name}`, r);
|
|
31
52
|
}
|
|
53
|
+
else {
|
|
54
|
+
byName.set(name, r);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const r of crossModuleTargets) {
|
|
58
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForeign(r))
|
|
59
|
+
continue;
|
|
60
|
+
byModuleName.set(`${moduleOf(r)}\0${r.metadata.name}`, r);
|
|
32
61
|
}
|
|
62
|
+
const resolveTarget = (source) => {
|
|
63
|
+
const dot = source.indexOf(".");
|
|
64
|
+
if (dot === -1) {
|
|
65
|
+
const t = byName.get(source);
|
|
66
|
+
return t ? { kind: t.kind, name: source } : undefined;
|
|
67
|
+
}
|
|
68
|
+
const alias = source.slice(0, dot);
|
|
69
|
+
const name = source.slice(dot + 1);
|
|
70
|
+
if (alias === "Self") {
|
|
71
|
+
const t = byName.get(name);
|
|
72
|
+
return t ? { kind: t.kind, name } : undefined;
|
|
73
|
+
}
|
|
74
|
+
const module = aliases?.moduleForAlias(alias);
|
|
75
|
+
if (module) {
|
|
76
|
+
const t = byModuleName.get(`${module}\0${name}`);
|
|
77
|
+
if (t) {
|
|
78
|
+
// The foreign instance's `kind` is authored in ITS module's scope (e.g.
|
|
79
|
+
// `Self.WriteLine`); canonicalize to a scope-independent `<module>.<Kind>` for the
|
|
80
|
+
// consumer's kind check. `Self.` maps to the owning module directly — the forwarded
|
|
81
|
+
// library's Library doc (hence its `Self` alias) isn't in the consumer's manifest
|
|
82
|
+
// set — while other alias prefixes resolve via that module's forwarded import scope.
|
|
83
|
+
const rawKind = t.kind;
|
|
84
|
+
const foreignKind = rawKind.startsWith("Self.")
|
|
85
|
+
? `${module}.${rawKind.slice("Self.".length)}`
|
|
86
|
+
: aliasesByModule?.get(module)?.resolveKind(rawKind) ?? rawKind;
|
|
87
|
+
return { kind: foreignKind, name, alias };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
33
92
|
for (const r of resources) {
|
|
34
93
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
35
94
|
continue;
|
|
95
|
+
if (isForeign(r))
|
|
96
|
+
continue;
|
|
36
97
|
const fieldMap = aliases && aliasesByModule
|
|
37
98
|
? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
|
|
38
99
|
: registry.getFieldMapForKind(r.kind, aliases);
|
|
@@ -41,37 +102,30 @@ export function resolveRefSentinels(resources, registry, aliases, aliasesByModul
|
|
|
41
102
|
for (const [fieldPath, entry] of fieldMap) {
|
|
42
103
|
if (!isRefEntry(entry))
|
|
43
104
|
continue;
|
|
44
|
-
|
|
105
|
+
descend(r, fieldPath.split("."), resolveTarget);
|
|
45
106
|
}
|
|
46
107
|
}
|
|
47
108
|
}
|
|
48
|
-
/** Walks `obj` along `fieldPath` (dot notation with `[]` for arrays and
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
* round-trip. */
|
|
53
|
-
function replaceSentinelsAtPath(obj, fieldPath, byName) {
|
|
54
|
-
const parts = fieldPath.split(".");
|
|
55
|
-
descend(obj, parts, byName);
|
|
56
|
-
}
|
|
57
|
-
function descend(obj, parts, byName) {
|
|
109
|
+
/** Walks `obj` along `fieldPath` parts (dot notation with `[]` for arrays and `{}` for
|
|
110
|
+
* additionalProperties-typed maps) and replaces any `!ref` sentinel at the terminal slot
|
|
111
|
+
* with its resolved `{kind, name, alias?}`. Mutates the parent container in place. */
|
|
112
|
+
function descend(obj, parts, resolve) {
|
|
58
113
|
if (obj == null || typeof obj !== "object" || parts.length === 0)
|
|
59
114
|
return;
|
|
60
115
|
const [head, ...rest] = parts;
|
|
61
|
-
// Map iteration: descend into every value of the current object.
|
|
62
116
|
if (head === "{}") {
|
|
63
117
|
const container = obj;
|
|
64
118
|
for (const key of Object.keys(container)) {
|
|
65
119
|
const child = container[key];
|
|
66
120
|
if (rest.length === 0) {
|
|
67
121
|
if (isRefSentinel(child)) {
|
|
68
|
-
const target =
|
|
122
|
+
const target = resolve(child.source);
|
|
69
123
|
if (target)
|
|
70
|
-
container[key] =
|
|
124
|
+
container[key] = target;
|
|
71
125
|
}
|
|
72
126
|
}
|
|
73
127
|
else {
|
|
74
|
-
descend(child, rest,
|
|
128
|
+
descend(child, rest, resolve);
|
|
75
129
|
}
|
|
76
130
|
}
|
|
77
131
|
return;
|
|
@@ -89,26 +143,26 @@ function descend(obj, parts, byName) {
|
|
|
89
143
|
if (rest.length === 0) {
|
|
90
144
|
const elem = val[i];
|
|
91
145
|
if (isRefSentinel(elem)) {
|
|
92
|
-
const target =
|
|
146
|
+
const target = resolve(elem.source);
|
|
93
147
|
if (target)
|
|
94
|
-
val[i] =
|
|
148
|
+
val[i] = target;
|
|
95
149
|
}
|
|
96
150
|
}
|
|
97
151
|
else {
|
|
98
|
-
descend(val[i], rest,
|
|
152
|
+
descend(val[i], rest, resolve);
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
155
|
}
|
|
102
156
|
else {
|
|
103
157
|
if (rest.length === 0) {
|
|
104
158
|
if (isRefSentinel(val)) {
|
|
105
|
-
const target =
|
|
159
|
+
const target = resolve(val.source);
|
|
106
160
|
if (target)
|
|
107
|
-
container[key] =
|
|
161
|
+
container[key] = target;
|
|
108
162
|
}
|
|
109
163
|
}
|
|
110
164
|
else {
|
|
111
|
-
descend(val, rest,
|
|
165
|
+
descend(val, rest, resolve);
|
|
112
166
|
}
|
|
113
167
|
}
|
|
114
168
|
}
|
|
@@ -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;
|
|
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;AA6C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2etB"}
|