@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.
@@ -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,GACnC,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"}
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 { AliasResolver } from "./alias-resolver.js";
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,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
+ {"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 defs.resolve(kind) ?? (resolved ? defs.resolve(resolved) : undefined);
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 definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases);
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
- const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases);
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 { AliasResolver } from "./alias-resolver.js";
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,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,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,GACf,kBAAkB,EAAE,CAwCtB"}
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 resolvedKind = aliases.resolveKind(manifest.kind);
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
- const resolved = ctx.aliases.resolveKind(handlerRef.kind);
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.22.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.23.0"
51
+ "@telorun/sdk": "0.26.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
@@ -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
- if (!ownModule || rootModules.has(ownModule)) {
711
- aliases.registerImport(alias, targetModule, exportedKinds);
712
- } else {
713
- let libResolver = aliasesByModule.get(ownModule);
714
- if (!libResolver) {
715
- libResolver = new AliasResolver();
716
- aliasesByModule.set(ownModule, libResolver);
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
- const resolvedKind = aliases.resolveKind(m.kind);
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
- const baseTypedEnv = buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
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
- const ownModule = (m.metadata as { module?: string } | undefined)?.module;
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(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
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
- // `variables` / `secrets` are reserved on the resources.<Alias> value-flow
459
- // surface, so a library may not export instances under those names.
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"] } },
@@ -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
+ }