@telorun/analyzer 0.22.0 → 0.23.1
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/README.md +2 -2
- package/dist/alias-resolver.d.ts +17 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +28 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +61 -20
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +4 -2
- package/dist/cel-environment.d.ts +8 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +48 -0
- package/dist/flatten-for-analyzer.d.ts +52 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +192 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve-throws-union.d.ts +9 -2
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +47 -23
- package/dist/validate-throws-coverage.d.ts +2 -2
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +15 -7
- package/package.json +2 -2
- package/src/alias-resolver.ts +33 -0
- package/src/analyzer.ts +72 -19
- package/src/builtins.ts +4 -2
- package/src/cel-environment.ts +57 -0
- package/src/flatten-for-analyzer.ts +217 -4
- package/src/index.ts +7 -0
- package/src/resolve-throws-union.ts +56 -18
- package/src/validate-throws-coverage.ts +22 -6
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,gCAAgC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
2
2
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
3
|
-
export { flattenForAnalyzer, flattenLoadedModule, selectModuleManifestsForAnalysis, } from "./flatten-for-analyzer.js";
|
|
3
|
+
export { flattenForAnalyzer, flattenLoadedModule, forwardReExportManifests, parseExportEntry, reExportSpecsFromExports, resolveExportedKinds, selectModuleManifestsForAnalysis, stampReExportedKinds, } from "./flatten-for-analyzer.js";
|
|
4
4
|
export { visitManifest } from "./manifest-visitor.js";
|
|
5
5
|
export { Loader } from "./manifest-loader.js";
|
|
6
6
|
export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import type
|
|
2
|
+
import { type AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
export interface ThrowsCodeMeta {
|
|
5
5
|
data?: Record<string, any>;
|
|
@@ -27,10 +27,17 @@ export interface ResolveCtx {
|
|
|
27
27
|
allManifests: ResourceManifest[];
|
|
28
28
|
defs: DefinitionRegistry;
|
|
29
29
|
aliases: AliasResolver;
|
|
30
|
+
/** Per-imported-library alias resolvers, keyed by module name. A manifest that
|
|
31
|
+
* originated in an imported library resolves its kind aliases against its own
|
|
32
|
+
* module's resolver, not the consumer's — an inline handler extracted from an
|
|
33
|
+
* imported Http.Api inherits the lexical scope of the library that declares it. */
|
|
34
|
+
aliasesByModule: Map<string, AliasResolver>;
|
|
35
|
+
/** The consumer/root module names; resources owned by these resolve against `aliases`. */
|
|
36
|
+
rootModules: Set<string>;
|
|
30
37
|
memo: Map<string, ThrowsUnion>;
|
|
31
38
|
inProgress: Set<string>;
|
|
32
39
|
}
|
|
33
|
-
export declare function createResolveCtx(allManifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver): ResolveCtx;
|
|
40
|
+
export declare function createResolveCtx(allManifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string>): ResolveCtx;
|
|
34
41
|
/** Resolve the effective throw union for a named manifest. The result combines
|
|
35
42
|
* explicit `throws.codes`, `throws.inherit: true` dataflow (step-context
|
|
36
43
|
* traversal with try/catch subtraction), and unbounded markers for
|
|
@@ -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;AAEzE,OAAO,KAAK,
|
|
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,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,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;;;wFAGoF;IACpF,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC5C,0FAA0F;IAC1F,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,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,EACtB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,UAAU,CAUZ;AA6CD;;;;8CAI8C;AAC9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,EAAE,UAAU,GACd,WAAW,CAiDb"}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { scopeResolverForModule } from "./alias-resolver.js";
|
|
2
3
|
/** Code a non-`InvokeError` failure surfaces as inside a `catch` block. Mirrors
|
|
3
4
|
* `PLAIN_ERROR_CODE` in `@telorun/run`'s `toSequenceError`: any invoke can throw
|
|
4
5
|
* a plain error, which the catch sees as `error.code === "INTERNAL_ERROR"`. */
|
|
5
6
|
export const PLAIN_ERROR_CODE = "INTERNAL_ERROR";
|
|
6
|
-
export function createResolveCtx(allManifests, defs, aliases) {
|
|
7
|
+
export function createResolveCtx(allManifests, defs, aliases, aliasesByModule = new Map(), rootModules = new Set()) {
|
|
7
8
|
return {
|
|
8
9
|
allManifests,
|
|
9
10
|
defs,
|
|
10
11
|
aliases,
|
|
12
|
+
aliasesByModule,
|
|
13
|
+
rootModules,
|
|
11
14
|
memo: new Map(),
|
|
12
15
|
inProgress: new Set(),
|
|
13
16
|
};
|
|
@@ -15,6 +18,10 @@ export function createResolveCtx(allManifests, defs, aliases) {
|
|
|
15
18
|
function emptyUnion() {
|
|
16
19
|
return { codes: new Map(), unbounded: false };
|
|
17
20
|
}
|
|
21
|
+
/** The owning module's alias resolver for a manifest in this resolve context. */
|
|
22
|
+
function scopeResolverFor(ctx, ownModule) {
|
|
23
|
+
return scopeResolverForModule(ownModule, ctx.rootModules, ctx.aliasesByModule);
|
|
24
|
+
}
|
|
18
25
|
function unionInto(target, src) {
|
|
19
26
|
for (const [code, meta] of src.codes) {
|
|
20
27
|
if (!target.codes.has(code))
|
|
@@ -25,9 +32,18 @@ function unionInto(target, src) {
|
|
|
25
32
|
if (src.canThrowPlain)
|
|
26
33
|
target.canThrowPlain = true;
|
|
27
34
|
}
|
|
28
|
-
function definitionFor(kind, defs, aliases) {
|
|
35
|
+
function definitionFor(kind, defs, aliases, scopeResolver) {
|
|
36
|
+
const direct = defs.resolve(kind);
|
|
37
|
+
if (direct)
|
|
38
|
+
return direct;
|
|
39
|
+
const scoped = scopeResolver?.resolveKind(kind);
|
|
40
|
+
if (scoped) {
|
|
41
|
+
const d = defs.resolve(scoped);
|
|
42
|
+
if (d)
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
29
45
|
const resolved = aliases.resolveKind(kind);
|
|
30
|
-
return
|
|
46
|
+
return resolved ? defs.resolve(resolved) : undefined;
|
|
31
47
|
}
|
|
32
48
|
function codesFromDefinition(definition) {
|
|
33
49
|
const out = new Map();
|
|
@@ -51,7 +67,9 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
51
67
|
if (ctx.inProgress.has(name))
|
|
52
68
|
return emptyUnion();
|
|
53
69
|
}
|
|
54
|
-
const
|
|
70
|
+
const ownModule = manifest.metadata?.module;
|
|
71
|
+
const scopeResolver = scopeResolverFor(ctx, ownModule);
|
|
72
|
+
const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases, scopeResolver);
|
|
55
73
|
if (!definition) {
|
|
56
74
|
const u = { codes: new Map(), unbounded: true };
|
|
57
75
|
if (name)
|
|
@@ -78,7 +96,7 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
78
96
|
result.unbounded = true;
|
|
79
97
|
}
|
|
80
98
|
if (throws.inherit) {
|
|
81
|
-
const inherited = resolveInherited(manifest, definition, ctx);
|
|
99
|
+
const inherited = resolveInherited(manifest, definition, ctx, ownModule);
|
|
82
100
|
unionInto(result, inherited);
|
|
83
101
|
}
|
|
84
102
|
if (name)
|
|
@@ -90,7 +108,7 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
90
108
|
ctx.inProgress.delete(name);
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
|
-
function resolveInherited(manifest, definition, ctx) {
|
|
111
|
+
function resolveInherited(manifest, definition, ctx, ownerModule) {
|
|
94
112
|
const result = { codes: new Map(), unbounded: false };
|
|
95
113
|
const props = definition.schema?.properties;
|
|
96
114
|
if (!props)
|
|
@@ -102,16 +120,16 @@ function resolveInherited(manifest, definition, ctx) {
|
|
|
102
120
|
const steps = manifest[fieldName];
|
|
103
121
|
if (!Array.isArray(steps))
|
|
104
122
|
continue;
|
|
105
|
-
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
|
|
123
|
+
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx, ownerModule));
|
|
106
124
|
}
|
|
107
125
|
return result;
|
|
108
126
|
}
|
|
109
|
-
function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx) {
|
|
127
|
+
function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
110
128
|
const result = emptyUnion();
|
|
111
129
|
for (const step of steps) {
|
|
112
130
|
if (!step || typeof step !== "object")
|
|
113
131
|
continue;
|
|
114
|
-
unionInto(result, collectStepThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
132
|
+
unionInto(result, collectStepThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
115
133
|
}
|
|
116
134
|
return result;
|
|
117
135
|
}
|
|
@@ -120,11 +138,11 @@ function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx) {
|
|
|
120
138
|
* / `else` / `elseif` / `do` / `cases` / `default`) are the same set already
|
|
121
139
|
* traversed by the analyzer's `x-telo-step-context` schema builder, so future
|
|
122
140
|
* composers that reuse those shape conventions work without changes here. */
|
|
123
|
-
function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
141
|
+
function collectStepThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
124
142
|
if (step[invokeField]) {
|
|
125
143
|
// Any invoked resource can throw a non-InvokeError at runtime, which an
|
|
126
144
|
// enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
|
|
127
|
-
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
145
|
+
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
128
146
|
u.canThrowPlain = true;
|
|
129
147
|
return u;
|
|
130
148
|
}
|
|
@@ -132,7 +150,7 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
132
150
|
return resolveThrowStepCode(step.throw, enclosingTryCodes);
|
|
133
151
|
}
|
|
134
152
|
if (Array.isArray(step.try)) {
|
|
135
|
-
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
|
|
153
|
+
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
136
154
|
let propagated;
|
|
137
155
|
if (Array.isArray(step.catch)) {
|
|
138
156
|
// Catch absorbs the try block's codes; the catch's own throws propagate
|
|
@@ -144,7 +162,7 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
144
162
|
// rethrow can propagate it — seed the set the catch resolves against.
|
|
145
163
|
if (tryUnion.canThrowPlain)
|
|
146
164
|
tryCodes.add(PLAIN_ERROR_CODE);
|
|
147
|
-
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
165
|
+
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx, ownerModule);
|
|
148
166
|
// Unbounded in the try block still signals the caller to expect
|
|
149
167
|
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
150
168
|
if (tryUnion.unbounded)
|
|
@@ -154,37 +172,37 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
154
172
|
propagated = cloneUnion(tryUnion);
|
|
155
173
|
}
|
|
156
174
|
if (Array.isArray(step.finally)) {
|
|
157
|
-
unionInto(propagated, collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx));
|
|
175
|
+
unionInto(propagated, collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
158
176
|
}
|
|
159
177
|
return propagated;
|
|
160
178
|
}
|
|
161
179
|
if (Array.isArray(step.then)) {
|
|
162
180
|
const result = emptyUnion();
|
|
163
|
-
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
|
|
181
|
+
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
164
182
|
if (Array.isArray(step.else)) {
|
|
165
|
-
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
|
|
183
|
+
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
166
184
|
}
|
|
167
185
|
if (Array.isArray(step.elseif)) {
|
|
168
186
|
for (const branch of step.elseif) {
|
|
169
187
|
if (Array.isArray(branch?.then)) {
|
|
170
|
-
unionInto(result, collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx));
|
|
188
|
+
unionInto(result, collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
return result;
|
|
175
193
|
}
|
|
176
194
|
if (Array.isArray(step.do)) {
|
|
177
|
-
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
|
|
195
|
+
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
178
196
|
}
|
|
179
197
|
if (step.cases && typeof step.cases === "object") {
|
|
180
198
|
const result = emptyUnion();
|
|
181
199
|
for (const arr of Object.values(step.cases)) {
|
|
182
200
|
if (Array.isArray(arr)) {
|
|
183
|
-
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
|
|
201
|
+
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
if (Array.isArray(step.default)) {
|
|
187
|
-
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
|
|
205
|
+
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
188
206
|
}
|
|
189
207
|
return result;
|
|
190
208
|
}
|
|
@@ -199,14 +217,18 @@ function cloneUnion(u) {
|
|
|
199
217
|
out.canThrowPlain = true;
|
|
200
218
|
return out;
|
|
201
219
|
}
|
|
202
|
-
function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
220
|
+
function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
203
221
|
const invokeRef = step[invokeField];
|
|
204
222
|
if (!invokeRef || typeof invokeRef !== "object")
|
|
205
223
|
return emptyUnion();
|
|
206
224
|
const invokedKind = invokeRef.kind;
|
|
207
225
|
if (!invokedKind)
|
|
208
226
|
return emptyUnion();
|
|
209
|
-
|
|
227
|
+
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
228
|
+
// composer that declares the step), so a library's step referencing its own
|
|
229
|
+
// import resolves against that library, not the consumer.
|
|
230
|
+
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
231
|
+
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
|
|
210
232
|
if (!definition)
|
|
211
233
|
return { codes: new Map(), unbounded: true };
|
|
212
234
|
if (definition.throws?.passthrough) {
|
|
@@ -215,10 +237,12 @@ function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
215
237
|
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
216
238
|
const invokeName = invokeRef.name;
|
|
217
239
|
if (invokeName) {
|
|
240
|
+
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
218
241
|
const target = ctx.allManifests.find((m) => m.metadata?.name === invokeName &&
|
|
219
242
|
(m.kind === invokedKind ||
|
|
220
243
|
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
221
|
-
m.kind === ctx.aliases.resolveKind(invokedKind)
|
|
244
|
+
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
245
|
+
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)));
|
|
222
246
|
if (target)
|
|
223
247
|
return resolveThrowsUnion(target, ctx);
|
|
224
248
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
-
import type
|
|
3
|
+
import { type AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { type AnalysisDiagnostic } from "./types.js";
|
|
6
6
|
/** Entry point — invoked once per analyze() run. */
|
|
7
|
-
export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment): AnalysisDiagnostic[];
|
|
7
|
+
export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string>): AnalysisDiagnostic[];
|
|
8
8
|
//# sourceMappingURL=validate-throws-coverage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4dzE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,kBAAkB,EAAE,CAkDtB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scopeResolverForModule } from "./alias-resolver.js";
|
|
1
2
|
import { createResolveCtx, resolveThrowsUnion, } from "./resolve-throws-union.js";
|
|
2
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
3
4
|
import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
|
|
@@ -406,16 +407,20 @@ function schemaHasStepContext(schema) {
|
|
|
406
407
|
return false;
|
|
407
408
|
}
|
|
408
409
|
/** Entry point — invoked once per analyze() run. */
|
|
409
|
-
export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
410
|
+
export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByModule = new Map(), rootModules = new Set()) {
|
|
410
411
|
const diagnostics = [];
|
|
411
412
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
412
|
-
const resolveCtx = createResolveCtx(manifests, defs, aliases);
|
|
413
|
+
const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules);
|
|
414
|
+
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
415
|
+
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
416
|
+
const scopeResolverFor = (m) => scopeResolverForModule(m.metadata?.module, rootModules, aliasesByModule);
|
|
413
417
|
for (const manifest of manifests) {
|
|
414
418
|
if (!manifest.kind || !manifest.metadata?.name)
|
|
415
419
|
continue;
|
|
416
420
|
if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract")
|
|
417
421
|
continue;
|
|
418
|
-
const
|
|
422
|
+
const scopeResolver = scopeResolverFor(manifest);
|
|
423
|
+
const resolvedKind = scopeResolver?.resolveKind(manifest.kind) ?? aliases.resolveKind(manifest.kind);
|
|
419
424
|
const definition = defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
420
425
|
if (!definition?.schema)
|
|
421
426
|
continue;
|
|
@@ -426,7 +431,7 @@ export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
|
426
431
|
}, (entries, arrayPath, siblingData, catchesFor) => {
|
|
427
432
|
diagnostics.push(...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath));
|
|
428
433
|
const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
|
|
429
|
-
const union = handlerRefUnion(handlerRef, manifests, resolveCtx);
|
|
434
|
+
const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
|
|
430
435
|
diagnostics.push(...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env));
|
|
431
436
|
diagnostics.push(...checkTypedErrorData(entries, union, resource, filePath, arrayPath, env));
|
|
432
437
|
});
|
|
@@ -436,19 +441,22 @@ export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
|
436
441
|
/** Resolve a handler ref's effective throw union. Prefers the named manifest
|
|
437
442
|
* (so `inherit: true` handlers expose their transitive union); falls back to
|
|
438
443
|
* the definition's own codes when no name is given. */
|
|
439
|
-
function handlerRefUnion(handlerRef, manifests, ctx) {
|
|
444
|
+
function handlerRefUnion(handlerRef, manifests, ctx, scopeResolver) {
|
|
440
445
|
if (!handlerRef)
|
|
441
446
|
return { codes: new Map(), unbounded: false };
|
|
442
447
|
if (handlerRef.name) {
|
|
443
|
-
const resolvedKind = ctx.aliases.resolveKind(handlerRef.kind);
|
|
448
|
+
const resolvedKind = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
444
449
|
const targetManifest = manifests.find((m) => m.metadata?.name === handlerRef.name &&
|
|
445
450
|
(m.kind === handlerRef.kind ||
|
|
446
451
|
m.kind === resolvedKind ||
|
|
452
|
+
scopeResolver?.resolveKind(m.kind) === handlerRef.kind ||
|
|
447
453
|
ctx.aliases.resolveKind(m.kind) === handlerRef.kind));
|
|
448
454
|
if (targetManifest)
|
|
449
455
|
return resolveThrowsUnion(targetManifest, ctx);
|
|
450
456
|
}
|
|
451
|
-
|
|
457
|
+
// No named target — fall back to the handler kind's own declared codes,
|
|
458
|
+
// resolving the kind in the owner's lexical scope first, then root aliases.
|
|
459
|
+
const resolved = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
452
460
|
const def = ctx.defs.resolve(handlerRef.kind) ?? (resolved ? ctx.defs.resolve(resolved) : undefined);
|
|
453
461
|
if (!def?.throws)
|
|
454
462
|
return { codes: new Map(), unbounded: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.26.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/alias-resolver.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
export class AliasResolver {
|
|
4
4
|
private readonly importAliases = new Map<string, string>();
|
|
5
5
|
private readonly importedKinds = new Map<string, Set<string>>();
|
|
6
|
+
/** `${alias}.${suffix}` → canonical `<owningModule>.<Kind>` for kinds an import
|
|
7
|
+
* transitively RE-EXPORTS (`exports.kinds: [Alias.Kind]`), which don't live in the
|
|
8
|
+
* import's own module. Resolved before the normal `<module>.<suffix>` construction. */
|
|
9
|
+
private readonly reExportedKinds = new Map<string, string>();
|
|
6
10
|
|
|
7
11
|
registerImport(alias: string, targetModule: string, exportedKinds: string[]): void {
|
|
8
12
|
this.importAliases.set(alias, targetModule);
|
|
@@ -11,6 +15,12 @@ export class AliasResolver {
|
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** Register that `<alias>.<suffix>` re-exports the kind canonically named `canonicalKind`
|
|
19
|
+
* (owned by a module the alias's target imports, possibly several hops away). */
|
|
20
|
+
registerKindReExport(alias: string, suffix: string, canonicalKind: string): void {
|
|
21
|
+
this.reExportedKinds.set(`${alias}.${suffix}`, canonicalKind);
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
15
25
|
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
16
26
|
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
@@ -29,6 +39,10 @@ export class AliasResolver {
|
|
|
29
39
|
if (dot === -1) return undefined;
|
|
30
40
|
const prefix = kind.slice(0, dot);
|
|
31
41
|
const suffix = kind.slice(dot + 1);
|
|
42
|
+
// Re-export takes precedence: a re-exported kind resolves to its true owning module,
|
|
43
|
+
// not `${prefix-target}.${suffix}` (and bypasses the gate — it's explicitly re-exported).
|
|
44
|
+
const reExported = this.reExportedKinds.get(`${prefix}.${suffix}`);
|
|
45
|
+
if (reExported) return reExported;
|
|
32
46
|
const realModule = this.importAliases.get(prefix);
|
|
33
47
|
if (!realModule) return undefined;
|
|
34
48
|
const allowed = this.importedKinds.get(prefix);
|
|
@@ -55,3 +69,22 @@ export class AliasResolver {
|
|
|
55
69
|
return result;
|
|
56
70
|
}
|
|
57
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The alias resolver for a resource's own lexical scope. A resource that
|
|
75
|
+
* originated in an imported library (its `ownModule` names a non-root module —
|
|
76
|
+
* e.g. an inline handler extracted from an imported Http.Api) resolves its kind
|
|
77
|
+
* aliases against THAT library's import map, so an anonymous child inherits the
|
|
78
|
+
* lexical scope of the document that declares it. Returns undefined for
|
|
79
|
+
* root/consumer-owned resources (and unknown modules), so callers fall back to
|
|
80
|
+
* the root `aliases`.
|
|
81
|
+
*/
|
|
82
|
+
export function scopeResolverForModule(
|
|
83
|
+
ownModule: string | undefined,
|
|
84
|
+
rootModules: Set<string>,
|
|
85
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
86
|
+
): AliasResolver | undefined {
|
|
87
|
+
return ownModule && !rootModules.has(ownModule)
|
|
88
|
+
? aliasesByModule.get(ownModule)
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
4
|
-
import { AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
|
|
5
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
6
6
|
import {
|
|
7
7
|
buildCelEnvironment,
|
|
8
|
+
buildImportInputCelEnvironment,
|
|
8
9
|
buildTypedCelEnvironment,
|
|
9
10
|
type CelHandlers,
|
|
10
11
|
} from "./cel-environment.js";
|
|
@@ -706,16 +707,22 @@ export class StaticAnalyzer {
|
|
|
706
707
|
if (resolvedModuleName) {
|
|
707
708
|
defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
|
|
708
709
|
}
|
|
710
|
+
// `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
|
|
711
|
+
// maps an exported suffix to the true owning module's canonical kind for kinds this
|
|
712
|
+
// import transitively re-exports (`exports.kinds: [Alias.Kind]`).
|
|
713
|
+
const reExportedKinds = ((m.metadata as any)?.reExportedKinds ?? {}) as Record<
|
|
714
|
+
string,
|
|
715
|
+
string
|
|
716
|
+
>;
|
|
709
717
|
// Alias registration is scoped: consumer imports vs. imported-library imports.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
libResolver.registerImport(alias, targetModule, exportedKinds);
|
|
718
|
+
const resolver =
|
|
719
|
+
!ownModule || rootModules.has(ownModule)
|
|
720
|
+
? aliases
|
|
721
|
+
: (aliasesByModule.get(ownModule) ??
|
|
722
|
+
aliasesByModule.set(ownModule, new AliasResolver()).get(ownModule)!);
|
|
723
|
+
resolver.registerImport(alias, targetModule, exportedKinds);
|
|
724
|
+
for (const [suffix, canonical] of Object.entries(reExportedKinds)) {
|
|
725
|
+
resolver.registerKindReExport(alias, suffix, canonical);
|
|
719
726
|
}
|
|
720
727
|
}
|
|
721
728
|
}
|
|
@@ -864,6 +871,26 @@ export class StaticAnalyzer {
|
|
|
864
871
|
}
|
|
865
872
|
}
|
|
866
873
|
}
|
|
874
|
+
// `exports.resources` entries are plain names: `Db` (local) or `Alias.Name` (re-export),
|
|
875
|
+
// mirroring `exports.kinds`. The `!ref` tag is not accepted here — a `!ref` parses to a
|
|
876
|
+
// sentinel object that the schema's CEL/ref exemption would silently pass, so reject any
|
|
877
|
+
// non-string entry with an actionable message instead.
|
|
878
|
+
const exportsResources = (m as Record<string, any>).exports?.resources;
|
|
879
|
+
if (Array.isArray(exportsResources)) {
|
|
880
|
+
for (let i = 0; i < exportsResources.length; i++) {
|
|
881
|
+
if (typeof exportsResources[i] === "string") continue;
|
|
882
|
+
diagnostics.push({
|
|
883
|
+
severity: DiagnosticSeverity.Error,
|
|
884
|
+
code: "INVALID_EXPORT",
|
|
885
|
+
source: SOURCE,
|
|
886
|
+
message:
|
|
887
|
+
`Telo.Library exports.resources[${i}]: write the exported name as a plain string — ` +
|
|
888
|
+
`'Name' to export a local instance, or 'Alias.Name' to re-export an imported one. ` +
|
|
889
|
+
`The '!ref' tag is not allowed in exports.resources.`,
|
|
890
|
+
data: { resource, filePath, path: `exports.resources.${i}` },
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
867
894
|
}
|
|
868
895
|
|
|
869
896
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
@@ -911,8 +938,15 @@ export class StaticAnalyzer {
|
|
|
911
938
|
|
|
912
939
|
// Resolve kind through alias if needed; direct lookup takes priority so that
|
|
913
940
|
// aliases whose name matches the module name (the common case) work without
|
|
914
|
-
// path-derived name mangling.
|
|
915
|
-
|
|
941
|
+
// path-derived name mangling. A resource that originated in an imported library
|
|
942
|
+
// (its `metadata.module` names a non-root module — e.g. an inline route handler
|
|
943
|
+
// extracted from an imported Http.Api) must resolve its kind alias against THAT
|
|
944
|
+
// library's import map, not the consumer's; an anonymous child inherits the
|
|
945
|
+
// lexical scope of the document that declares it. Mirrors the nested-inline and
|
|
946
|
+
// reference-resolution paths: own-module scope first, root/consumer aliases last.
|
|
947
|
+
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
948
|
+
const scopeResolver = scopeResolverForModule(ownModule, rootModules, aliasesByModule);
|
|
949
|
+
const resolvedKind = scopeResolver?.resolveKind(m.kind) ?? aliases.resolveKind(m.kind);
|
|
916
950
|
const definition =
|
|
917
951
|
defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
918
952
|
if (!definition) {
|
|
@@ -943,8 +977,27 @@ export class StaticAnalyzer {
|
|
|
943
977
|
},
|
|
944
978
|
}
|
|
945
979
|
: definition.schema;
|
|
946
|
-
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
947
|
-
|
|
980
|
+
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types.
|
|
981
|
+
// A Telo.Import's variables/secrets are a config-only contract evaluated against the
|
|
982
|
+
// IMPORTING module's scope, so type them from the owning module doc (matched by
|
|
983
|
+
// `metadata.module`) and drop `resources`/`env` so referencing them is an error. A
|
|
984
|
+
// library's own internal import is validated against that library in the library's
|
|
985
|
+
// standalone analysis; in this flattened app pass the library doc is absent, so the
|
|
986
|
+
// importer is undefined here and variables/secrets fall back to a permissive `map`
|
|
987
|
+
// (no false positives) while resources/env stay rejected.
|
|
988
|
+
const importerModule =
|
|
989
|
+
m.kind === "Telo.Import"
|
|
990
|
+
? allManifests.find(
|
|
991
|
+
(mm) =>
|
|
992
|
+
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
993
|
+
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
994
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
995
|
+
)
|
|
996
|
+
: undefined;
|
|
997
|
+
const baseTypedEnv =
|
|
998
|
+
m.kind === "Telo.Import"
|
|
999
|
+
? buildImportInputCelEnvironment(this.celEnv, importerModule)
|
|
1000
|
+
: buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
948
1001
|
const celIssues = collectCelTypeIssues(
|
|
949
1002
|
m,
|
|
950
1003
|
schema,
|
|
@@ -979,10 +1032,8 @@ export class StaticAnalyzer {
|
|
|
979
1032
|
// first, then the parent module's own aliases (for resources declared
|
|
980
1033
|
// inside an imported module), then the root aliases. Mirrors how the
|
|
981
1034
|
// analyzer resolves kinds elsewhere so module-scoped aliases don't
|
|
982
|
-
// produce false UNDEFINED_KIND diagnostics.
|
|
983
|
-
|
|
984
|
-
const scopeResolver =
|
|
985
|
-
ownModule && !rootModules.has(ownModule) ? aliasesByModule.get(ownModule) : undefined;
|
|
1035
|
+
// produce false UNDEFINED_KIND diagnostics. `scopeResolver` is the
|
|
1036
|
+
// owning module's resolver computed above.
|
|
986
1037
|
diagnostics.push(
|
|
987
1038
|
...validateNestedInlineResources(
|
|
988
1039
|
m,
|
|
@@ -1258,7 +1309,9 @@ export class StaticAnalyzer {
|
|
|
1258
1309
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
1259
1310
|
|
|
1260
1311
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
1261
|
-
diagnostics.push(
|
|
1312
|
+
diagnostics.push(
|
|
1313
|
+
...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules),
|
|
1314
|
+
);
|
|
1262
1315
|
|
|
1263
1316
|
// Warn about declared variables / secrets / ports that no CEL references.
|
|
1264
1317
|
diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
|
package/src/builtins.ts
CHANGED
|
@@ -455,8 +455,10 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
455
455
|
type: "object",
|
|
456
456
|
properties: {
|
|
457
457
|
kinds: { type: "array", items: { type: "string" } },
|
|
458
|
-
//
|
|
459
|
-
//
|
|
458
|
+
// An entry is a bare name (`Db`, a locally-owned export) or a dotted `Alias.Name`
|
|
459
|
+
// (re-export of the instance reached via this library's import aliased `Alias`,
|
|
460
|
+
// under the name `Name`) — mirroring `exports.kinds`. `variables` / `secrets` are
|
|
461
|
+
// reserved on the resources.<Alias> value-flow surface, so they may not be exported.
|
|
460
462
|
resources: {
|
|
461
463
|
type: "array",
|
|
462
464
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
package/src/cel-environment.ts
CHANGED
|
@@ -102,3 +102,60 @@ export function buildTypedCelEnvironment(
|
|
|
102
102
|
return baseEnv.clone();
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
107
|
+
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
108
|
+
function registerConfigNamespace(
|
|
109
|
+
env: Environment,
|
|
110
|
+
block: unknown,
|
|
111
|
+
name: "variables" | "secrets",
|
|
112
|
+
): void {
|
|
113
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
114
|
+
const entries = Object.entries(block as Record<string, unknown>).filter(
|
|
115
|
+
([, v]) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
116
|
+
);
|
|
117
|
+
if (entries.length > 0) {
|
|
118
|
+
const schema: Record<string, string> = {};
|
|
119
|
+
for (const [k, v] of entries) schema[k] = jsonSchemaToCelType(v as Record<string, any>);
|
|
120
|
+
(env as any).registerVariable({ name, schema });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
env.registerVariable(name, "map");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** CEL environment for the `variables:`/`secrets:` expressions on a `Telo.Import`.
|
|
128
|
+
*
|
|
129
|
+
* Import inputs are a config-only contract: their expressions are evaluated
|
|
130
|
+
* against the IMPORTING module's `variables`/`secrets`, never the import's own
|
|
131
|
+
* values map (the bug) nor the imported child's. `resources`, `env`, and `ports`
|
|
132
|
+
* are registered as empty typed objects, so referencing them is a "No such key"
|
|
133
|
+
* error that steers authors to a typed `variables` entry. */
|
|
134
|
+
export function buildImportInputCelEnvironment(
|
|
135
|
+
baseEnv: Environment,
|
|
136
|
+
moduleManifest: ResourceManifest | undefined,
|
|
137
|
+
): Environment {
|
|
138
|
+
const env = baseEnv.clone();
|
|
139
|
+
for (const brand of Object.keys(VALUE_BRAND_BASE)) {
|
|
140
|
+
(env as any).registerType(brand, { fields: {} });
|
|
141
|
+
}
|
|
142
|
+
const mod = moduleManifest as Record<string, unknown> | undefined;
|
|
143
|
+
// Typing variables/secrets from the importer's schema can fail on a malformed
|
|
144
|
+
// schema; degrade those to permissive `map` if so — but never lose the
|
|
145
|
+
// resources/env/ports rejection registered below (the catch is scoped so a
|
|
146
|
+
// typing failure can't silently re-open the config-only contract).
|
|
147
|
+
try {
|
|
148
|
+
registerConfigNamespace(env, mod?.variables, "variables");
|
|
149
|
+
registerConfigNamespace(env, mod?.secrets, "secrets");
|
|
150
|
+
} catch {
|
|
151
|
+
env.registerVariable("variables", "map");
|
|
152
|
+
env.registerVariable("secrets", "map");
|
|
153
|
+
}
|
|
154
|
+
// Override the base env's dyn `resources`/`env`/`ports` with empty typed objects
|
|
155
|
+
// so any access (`resources.X`, `env.X`) is a "No such key" error — these
|
|
156
|
+
// surfaces are not part of the config-only import contract.
|
|
157
|
+
for (const name of ["resources", "env", "ports"]) {
|
|
158
|
+
(env as any).registerVariable({ name, schema: {} });
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|