@telorun/analyzer 0.23.0 → 0.23.2

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.
@@ -26,4 +26,14 @@ export declare class AliasResolver {
26
26
  * back into its user-facing alias form (e.g. "Http.Server"). */
27
27
  aliasesFor(targetModule: string): string[];
28
28
  }
29
+ /**
30
+ * The alias resolver for a resource's own lexical scope. A resource that
31
+ * originated in an imported library (its `ownModule` names a non-root module —
32
+ * e.g. an inline handler extracted from an imported Http.Api) resolves its kind
33
+ * aliases against THAT library's import map, so an anonymous child inherits the
34
+ * lexical scope of the document that declares it. Returns undefined for
35
+ * root/consumer-owned resources (and unknown modules), so callers fall back to
36
+ * the root `aliases`.
37
+ */
38
+ export declare function scopeResolverForModule(ownModule: string | undefined, rootModules: Set<string>, aliasesByModule: Map<string, AliasResolver>): AliasResolver | undefined;
29
39
  //# sourceMappingURL=alias-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE;;4FAEwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAE7D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF;sFACkF;IAClF,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAIhF;;;;qDAIiD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjD,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAmB7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C"}
1
+ {"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE;;4FAEwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAE7D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF;sFACkF;IAClF,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAIhF;;;;qDAIiD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjD,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAmB7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,aAAa,GAAG,SAAS,CAI3B"}
@@ -67,3 +67,17 @@ export class AliasResolver {
67
67
  return result;
68
68
  }
69
69
  }
70
+ /**
71
+ * The alias resolver for a resource's own lexical scope. A resource that
72
+ * originated in an imported library (its `ownModule` names a non-root module —
73
+ * e.g. an inline handler extracted from an imported Http.Api) resolves its kind
74
+ * aliases against THAT library's import map, so an anonymous child inherits the
75
+ * lexical scope of the document that declares it. Returns undefined for
76
+ * root/consumer-owned resources (and unknown modules), so callers fall back to
77
+ * the root `aliases`.
78
+ */
79
+ export function scopeResolverForModule(ownModule, rootModules, aliasesByModule) {
80
+ return ownModule && !rootModules.has(ownModule)
81
+ ? aliasesByModule.get(ownModule)
82
+ : undefined;
83
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IA+sBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAerB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAstBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAerB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
package/dist/analyzer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
2
- import { AliasResolver } from "./alias-resolver.js";
2
+ import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
3
3
  import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
4
4
  import { DefinitionRegistry } from "./definition-registry.js";
5
5
  import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
@@ -796,8 +796,15 @@ export class StaticAnalyzer {
796
796
  const resource = { kind: m.kind, name: m.metadata?.name };
797
797
  // Resolve kind through alias if needed; direct lookup takes priority so that
798
798
  // aliases whose name matches the module name (the common case) work without
799
- // path-derived name mangling.
800
- const resolvedKind = aliases.resolveKind(m.kind);
799
+ // path-derived name mangling. A resource that originated in an imported library
800
+ // (its `metadata.module` names a non-root module — e.g. an inline route handler
801
+ // extracted from an imported Http.Api) must resolve its kind alias against THAT
802
+ // library's import map, not the consumer's; an anonymous child inherits the
803
+ // lexical scope of the document that declares it. Mirrors the nested-inline and
804
+ // reference-resolution paths: own-module scope first, root/consumer aliases last.
805
+ const ownModule = m.metadata?.module;
806
+ const scopeResolver = scopeResolverForModule(ownModule, rootModules, aliasesByModule);
807
+ const resolvedKind = scopeResolver?.resolveKind(m.kind) ?? aliases.resolveKind(m.kind);
801
808
  const definition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
802
809
  if (!definition) {
803
810
  const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
@@ -865,9 +872,8 @@ export class StaticAnalyzer {
865
872
  // first, then the parent module's own aliases (for resources declared
866
873
  // inside an imported module), then the root aliases. Mirrors how the
867
874
  // analyzer resolves kinds elsewhere so module-scoped aliases don't
868
- // produce false UNDEFINED_KIND diagnostics.
869
- const ownModule = m.metadata?.module;
870
- const scopeResolver = ownModule && !rootModules.has(ownModule) ? aliasesByModule.get(ownModule) : undefined;
875
+ // produce false UNDEFINED_KIND diagnostics. `scopeResolver` is the
876
+ // owning module's resolver computed above.
871
877
  diagnostics.push(...validateNestedInlineResources(m, definition.schema, (kind) => {
872
878
  const direct = defs.resolve(kind);
873
879
  if (direct)
@@ -1082,7 +1088,7 @@ export class StaticAnalyzer {
1082
1088
  // Validate provider coherence rules for `provide:` template-target definitions.
1083
1089
  diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
1084
1090
  // Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
1085
- diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
1091
+ diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules));
1086
1092
  // Warn about declared variables / secrets / ports that no CEL references.
1087
1093
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
1088
1094
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
@@ -15,8 +15,9 @@ import type { AliasResolver } from "./alias-resolver.js";
15
15
  * e.g. TestBasicAddition_steps_AddTwoNumbers_invoke
16
16
  * TestBasicAddition_steps_0_invoke (when step has no name)
17
17
  *
18
- * Returns a new array containing the original manifests (mutated in-place) plus all
19
- * extracted manifests. The original array is not mutated.
18
+ * Returns a new array of deep-cloned manifests (the rewrites land on the clones)
19
+ * plus all extracted manifests. The caller's manifests — array and elements — are
20
+ * never mutated; this is the analyzer's immutability boundary.
20
21
  */
21
22
  export declare function normalizeInlineResources(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): ResourceManifest[];
22
23
  //# sourceMappingURL=normalize-inline-resources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"normalize-inline-resources.d.ts","sourceRoot":"","sources":["../src/normalize-inline-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,gBAAgB,EAAE,CA+DpB"}
1
+ {"version":3,"file":"normalize-inline-resources.d.ts","sourceRoot":"","sources":["../src/normalize-inline-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA8BzD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,gBAAgB,EAAE,CAsEpB"}
@@ -9,6 +9,24 @@ const SYSTEM_KINDS = new Set([
9
9
  function sanitizeName(raw) {
10
10
  return raw.replace(/[^a-zA-Z0-9_]/g, "_");
11
11
  }
12
+ /** Deep-clones a manifest value tree so the in-place rewrites this module and
13
+ * `resolveRefSentinels` perform never reach caller-owned objects. Compiled-CEL
14
+ * nodes (`{__compiled, source, call?}`) are opaque leaves carrying functions —
15
+ * copied by reference, never descended into — matching how `resolveRefSentinels`
16
+ * and `manifest-visitor` short-circuit on `__compiled`. */
17
+ function cloneForMutation(value) {
18
+ if (value === null || typeof value !== "object")
19
+ return value;
20
+ if (value.__compiled)
21
+ return value;
22
+ if (Array.isArray(value))
23
+ return value.map(cloneForMutation);
24
+ const out = {};
25
+ for (const key of Object.keys(value)) {
26
+ out[key] = cloneForMutation(value[key]);
27
+ }
28
+ return out;
29
+ }
12
30
  /**
13
31
  * Phase 2 — Inline resource normalization.
14
32
  *
@@ -23,13 +41,21 @@ function sanitizeName(raw) {
23
41
  * e.g. TestBasicAddition_steps_AddTwoNumbers_invoke
24
42
  * TestBasicAddition_steps_0_invoke (when step has no name)
25
43
  *
26
- * Returns a new array containing the original manifests (mutated in-place) plus all
27
- * extracted manifests. The original array is not mutated.
44
+ * Returns a new array of deep-cloned manifests (the rewrites land on the clones)
45
+ * plus all extracted manifests. The caller's manifests — array and elements — are
46
+ * never mutated; this is the analyzer's immutability boundary.
28
47
  */
29
48
  export function normalizeInlineResources(resources, registry, aliases, aliasesByModule) {
30
- const result = [...resources];
49
+ // Deep-clone the input so this pass — and `resolveRefSentinels`, which runs on
50
+ // this output — never mutate caller-owned manifests. `extractInlinesAtPath`
51
+ // rewrites inline ref slots in place, so without cloning we'd corrupt shared
52
+ // state such as the editor's `LoadedFile.manifests` parse cache (a reused
53
+ // sentinel would be rewritten to `{kind, name}`, later misread as an authored
54
+ // reference form). The clone is the analyzer's immutability boundary.
55
+ const result = resources.map(cloneForMutation);
31
56
  // Queue: all non-system resources with a name. Extracted resources are appended.
32
- const queue = resources.filter((r) => typeof r.metadata?.name === "string" && !!r.kind && !SYSTEM_KINDS.has(r.kind));
57
+ // Filter the CLONES (not the originals) so traversal mutates copies.
58
+ const queue = result.filter((r) => typeof r.metadata?.name === "string" && !!r.kind && !SYSTEM_KINDS.has(r.kind));
33
59
  let i = 0;
34
60
  while (i < queue.length) {
35
61
  const resource = queue[i++];
@@ -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.23.0",
3
+ "version": "0.23.2",
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": "*"
@@ -69,3 +69,22 @@ export class AliasResolver {
69
69
  return result;
70
70
  }
71
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,7 +1,7 @@
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,
@@ -938,8 +938,15 @@ export class StaticAnalyzer {
938
938
 
939
939
  // Resolve kind through alias if needed; direct lookup takes priority so that
940
940
  // aliases whose name matches the module name (the common case) work without
941
- // path-derived name mangling.
942
- 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);
943
950
  const definition =
944
951
  defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
945
952
  if (!definition) {
@@ -1025,10 +1032,8 @@ export class StaticAnalyzer {
1025
1032
  // first, then the parent module's own aliases (for resources declared
1026
1033
  // inside an imported module), then the root aliases. Mirrors how the
1027
1034
  // analyzer resolves kinds elsewhere so module-scoped aliases don't
1028
- // produce false UNDEFINED_KIND diagnostics.
1029
- const ownModule = (m.metadata as { module?: string } | undefined)?.module;
1030
- const scopeResolver =
1031
- ownModule && !rootModules.has(ownModule) ? aliasesByModule.get(ownModule) : undefined;
1035
+ // produce false UNDEFINED_KIND diagnostics. `scopeResolver` is the
1036
+ // owning module's resolver computed above.
1032
1037
  diagnostics.push(
1033
1038
  ...validateNestedInlineResources(
1034
1039
  m,
@@ -1304,7 +1309,9 @@ export class StaticAnalyzer {
1304
1309
  diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
1305
1310
 
1306
1311
  // Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
1307
- diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
1312
+ diagnostics.push(
1313
+ ...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules),
1314
+ );
1308
1315
 
1309
1316
  // Warn about declared variables / secrets / ports that no CEL references.
1310
1317
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
@@ -15,6 +15,22 @@ function sanitizeName(raw: string): string {
15
15
  return raw.replace(/[^a-zA-Z0-9_]/g, "_");
16
16
  }
17
17
 
18
+ /** Deep-clones a manifest value tree so the in-place rewrites this module and
19
+ * `resolveRefSentinels` perform never reach caller-owned objects. Compiled-CEL
20
+ * nodes (`{__compiled, source, call?}`) are opaque leaves carrying functions —
21
+ * copied by reference, never descended into — matching how `resolveRefSentinels`
22
+ * and `manifest-visitor` short-circuit on `__compiled`. */
23
+ function cloneForMutation(value: unknown): unknown {
24
+ if (value === null || typeof value !== "object") return value;
25
+ if ((value as { __compiled?: unknown }).__compiled) return value;
26
+ if (Array.isArray(value)) return value.map(cloneForMutation);
27
+ const out: Record<string, unknown> = {};
28
+ for (const key of Object.keys(value)) {
29
+ out[key] = cloneForMutation((value as Record<string, unknown>)[key]);
30
+ }
31
+ return out;
32
+ }
33
+
18
34
  /**
19
35
  * Phase 2 — Inline resource normalization.
20
36
  *
@@ -29,8 +45,9 @@ function sanitizeName(raw: string): string {
29
45
  * e.g. TestBasicAddition_steps_AddTwoNumbers_invoke
30
46
  * TestBasicAddition_steps_0_invoke (when step has no name)
31
47
  *
32
- * Returns a new array containing the original manifests (mutated in-place) plus all
33
- * extracted manifests. The original array is not mutated.
48
+ * Returns a new array of deep-cloned manifests (the rewrites land on the clones)
49
+ * plus all extracted manifests. The caller's manifests — array and elements — are
50
+ * never mutated; this is the analyzer's immutability boundary.
34
51
  */
35
52
  export function normalizeInlineResources(
36
53
  resources: ResourceManifest[],
@@ -38,10 +55,17 @@ export function normalizeInlineResources(
38
55
  aliases?: AliasResolver,
39
56
  aliasesByModule?: Map<string, AliasResolver>,
40
57
  ): ResourceManifest[] {
41
- const result = [...resources];
58
+ // Deep-clone the input so this pass — and `resolveRefSentinels`, which runs on
59
+ // this output — never mutate caller-owned manifests. `extractInlinesAtPath`
60
+ // rewrites inline ref slots in place, so without cloning we'd corrupt shared
61
+ // state such as the editor's `LoadedFile.manifests` parse cache (a reused
62
+ // sentinel would be rewritten to `{kind, name}`, later misread as an authored
63
+ // reference form). The clone is the analyzer's immutability boundary.
64
+ const result = resources.map(cloneForMutation) as ResourceManifest[];
42
65
 
43
66
  // Queue: all non-system resources with a name. Extracted resources are appended.
44
- const queue = resources.filter(
67
+ // Filter the CLONES (not the originals) so traversal mutates copies.
68
+ const queue = result.filter(
45
69
  (r): r is ResourceManifest & { metadata: { name: string } } =>
46
70
  typeof r.metadata?.name === "string" && !!r.kind && !SYSTEM_KINDS.has(r.kind),
47
71
  );
@@ -1,6 +1,6 @@
1
1
  import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
2
  import { isTaggedSentinel } from "@telorun/templating";
3
- import type { AliasResolver } from "./alias-resolver.js";
3
+ import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
4
4
  import type { DefinitionRegistry } from "./definition-registry.js";
5
5
 
6
6
  export interface ThrowsCodeMeta {
@@ -32,6 +32,13 @@ export interface ResolveCtx {
32
32
  allManifests: ResourceManifest[];
33
33
  defs: DefinitionRegistry;
34
34
  aliases: AliasResolver;
35
+ /** Per-imported-library alias resolvers, keyed by module name. A manifest that
36
+ * originated in an imported library resolves its kind aliases against its own
37
+ * module's resolver, not the consumer's — an inline handler extracted from an
38
+ * imported Http.Api inherits the lexical scope of the library that declares it. */
39
+ aliasesByModule: Map<string, AliasResolver>;
40
+ /** The consumer/root module names; resources owned by these resolve against `aliases`. */
41
+ rootModules: Set<string>;
35
42
  memo: Map<string, ThrowsUnion>;
36
43
  inProgress: Set<string>;
37
44
  }
@@ -40,11 +47,15 @@ export function createResolveCtx(
40
47
  allManifests: ResourceManifest[],
41
48
  defs: DefinitionRegistry,
42
49
  aliases: AliasResolver,
50
+ aliasesByModule: Map<string, AliasResolver> = new Map(),
51
+ rootModules: Set<string> = new Set(),
43
52
  ): ResolveCtx {
44
53
  return {
45
54
  allManifests,
46
55
  defs,
47
56
  aliases,
57
+ aliasesByModule,
58
+ rootModules,
48
59
  memo: new Map(),
49
60
  inProgress: new Set(),
50
61
  };
@@ -54,6 +65,11 @@ function emptyUnion(): ThrowsUnion {
54
65
  return { codes: new Map(), unbounded: false };
55
66
  }
56
67
 
68
+ /** The owning module's alias resolver for a manifest in this resolve context. */
69
+ function scopeResolverFor(ctx: ResolveCtx, ownModule: string | undefined): AliasResolver | undefined {
70
+ return scopeResolverForModule(ownModule, ctx.rootModules, ctx.aliasesByModule);
71
+ }
72
+
57
73
  function unionInto(target: ThrowsUnion, src: ThrowsUnion): void {
58
74
  for (const [code, meta] of src.codes) {
59
75
  if (!target.codes.has(code)) target.codes.set(code, meta);
@@ -66,9 +82,17 @@ function definitionFor(
66
82
  kind: string,
67
83
  defs: DefinitionRegistry,
68
84
  aliases: AliasResolver,
85
+ scopeResolver?: AliasResolver,
69
86
  ): ResourceDefinition | undefined {
87
+ const direct = defs.resolve(kind);
88
+ if (direct) return direct;
89
+ const scoped = scopeResolver?.resolveKind(kind);
90
+ if (scoped) {
91
+ const d = defs.resolve(scoped);
92
+ if (d) return d;
93
+ }
70
94
  const resolved = aliases.resolveKind(kind);
71
- return defs.resolve(kind) ?? (resolved ? defs.resolve(resolved) : undefined);
95
+ return resolved ? defs.resolve(resolved) : undefined;
72
96
  }
73
97
 
74
98
  function codesFromDefinition(definition: ResourceDefinition): Map<string, ThrowsCodeMeta> {
@@ -97,7 +121,9 @@ export function resolveThrowsUnion(
97
121
  if (ctx.inProgress.has(name)) return emptyUnion();
98
122
  }
99
123
 
100
- const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases);
124
+ const ownModule = (manifest.metadata as { module?: string } | undefined)?.module;
125
+ const scopeResolver = scopeResolverFor(ctx, ownModule);
126
+ const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases, scopeResolver);
101
127
  if (!definition) {
102
128
  const u: ThrowsUnion = { codes: new Map(), unbounded: true };
103
129
  if (name) ctx.memo.set(name, u);
@@ -126,7 +152,7 @@ export function resolveThrowsUnion(
126
152
  }
127
153
 
128
154
  if (throws.inherit) {
129
- const inherited = resolveInherited(manifest, definition, ctx);
155
+ const inherited = resolveInherited(manifest, definition, ctx, ownModule);
130
156
  unionInto(result, inherited);
131
157
  }
132
158
 
@@ -141,6 +167,7 @@ function resolveInherited(
141
167
  manifest: ResourceManifest,
142
168
  definition: ResourceDefinition,
143
169
  ctx: ResolveCtx,
170
+ ownerModule: string | undefined,
144
171
  ): ThrowsUnion {
145
172
  const result: ThrowsUnion = { codes: new Map(), unbounded: false };
146
173
  const props = definition.schema?.properties as Record<string, any> | undefined;
@@ -151,7 +178,7 @@ function resolveInherited(
151
178
  if (!stepCtx?.invoke) continue;
152
179
  const steps = (manifest as Record<string, any>)[fieldName];
153
180
  if (!Array.isArray(steps)) continue;
154
- unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
181
+ unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx, ownerModule));
155
182
  }
156
183
 
157
184
  return result;
@@ -162,13 +189,14 @@ function collectStepArrayThrows(
162
189
  invokeField: string,
163
190
  enclosingTryCodes: Set<string> | undefined,
164
191
  ctx: ResolveCtx,
192
+ ownerModule: string | undefined,
165
193
  ): ThrowsUnion {
166
194
  const result = emptyUnion();
167
195
  for (const step of steps) {
168
196
  if (!step || typeof step !== "object") continue;
169
197
  unionInto(
170
198
  result,
171
- collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx),
199
+ collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx, ownerModule),
172
200
  );
173
201
  }
174
202
  return result;
@@ -184,11 +212,14 @@ function collectStepThrows(
184
212
  invokeField: string,
185
213
  enclosingTryCodes: Set<string> | undefined,
186
214
  ctx: ResolveCtx,
215
+ ownerModule: string | undefined,
187
216
  ): ThrowsUnion {
188
217
  if (step[invokeField]) {
189
218
  // Any invoked resource can throw a non-InvokeError at runtime, which an
190
219
  // enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
191
- const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx));
220
+ const u = cloneUnion(
221
+ resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule),
222
+ );
192
223
  u.canThrowPlain = true;
193
224
  return u;
194
225
  }
@@ -198,7 +229,7 @@ function collectStepThrows(
198
229
  }
199
230
 
200
231
  if (Array.isArray(step.try)) {
201
- const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
232
+ const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx, ownerModule);
202
233
  let propagated: ThrowsUnion;
203
234
  if (Array.isArray(step.catch)) {
204
235
  // Catch absorbs the try block's codes; the catch's own throws propagate
@@ -209,7 +240,7 @@ function collectStepThrows(
209
240
  // `error.code === PLAIN_ERROR_CODE`, so a `throw: { code: error.code }`
210
241
  // rethrow can propagate it — seed the set the catch resolves against.
211
242
  if (tryUnion.canThrowPlain) tryCodes.add(PLAIN_ERROR_CODE);
212
- propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
243
+ propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx, ownerModule);
213
244
  // Unbounded in the try block still signals the caller to expect
214
245
  // arbitrary codes to flow through the catch (e.g. via passthrough).
215
246
  if (tryUnion.unbounded) propagated.unbounded = true;
@@ -219,7 +250,7 @@ function collectStepThrows(
219
250
  if (Array.isArray(step.finally)) {
220
251
  unionInto(
221
252
  propagated,
222
- collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx),
253
+ collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx, ownerModule),
223
254
  );
224
255
  }
225
256
  return propagated;
@@ -227,16 +258,16 @@ function collectStepThrows(
227
258
 
228
259
  if (Array.isArray(step.then)) {
229
260
  const result = emptyUnion();
230
- unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
261
+ unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx, ownerModule));
231
262
  if (Array.isArray(step.else)) {
232
- unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
263
+ unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx, ownerModule));
233
264
  }
234
265
  if (Array.isArray(step.elseif)) {
235
266
  for (const branch of step.elseif) {
236
267
  if (Array.isArray(branch?.then)) {
237
268
  unionInto(
238
269
  result,
239
- collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx),
270
+ collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx, ownerModule),
240
271
  );
241
272
  }
242
273
  }
@@ -245,18 +276,18 @@ function collectStepThrows(
245
276
  }
246
277
 
247
278
  if (Array.isArray(step.do)) {
248
- return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
279
+ return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx, ownerModule);
249
280
  }
250
281
 
251
282
  if (step.cases && typeof step.cases === "object") {
252
283
  const result = emptyUnion();
253
284
  for (const arr of Object.values(step.cases as Record<string, unknown>)) {
254
285
  if (Array.isArray(arr)) {
255
- unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
286
+ unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx, ownerModule));
256
287
  }
257
288
  }
258
289
  if (Array.isArray(step.default)) {
259
- unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
290
+ unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx, ownerModule));
260
291
  }
261
292
  return result;
262
293
  }
@@ -277,13 +308,18 @@ function resolveStepInvokeThrows(
277
308
  invokeField: string,
278
309
  enclosingTryCodes: Set<string> | undefined,
279
310
  ctx: ResolveCtx,
311
+ ownerModule: string | undefined,
280
312
  ): ThrowsUnion {
281
313
  const invokeRef = step[invokeField];
282
314
  if (!invokeRef || typeof invokeRef !== "object") return emptyUnion();
283
315
  const invokedKind = invokeRef.kind as string | undefined;
284
316
  if (!invokedKind) return emptyUnion();
285
317
 
286
- const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases);
318
+ // The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
319
+ // composer that declares the step), so a library's step referencing its own
320
+ // import resolves against that library, not the consumer.
321
+ const scopeResolver = scopeResolverFor(ctx, ownerModule);
322
+ const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
287
323
  if (!definition) return { codes: new Map(), unbounded: true };
288
324
 
289
325
  if (definition.throws?.passthrough) {
@@ -293,12 +329,14 @@ function resolveStepInvokeThrows(
293
329
  // Named manifest: resolve the full chain (covers transitive inherit).
294
330
  const invokeName = invokeRef.name as string | undefined;
295
331
  if (invokeName) {
332
+ const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
296
333
  const target = ctx.allManifests.find(
297
334
  (m) =>
298
335
  m.metadata?.name === invokeName &&
299
336
  (m.kind === invokedKind ||
300
337
  ctx.aliases.resolveKind(m.kind) === invokedKind ||
301
- m.kind === ctx.aliases.resolveKind(invokedKind)),
338
+ m.kind === ctx.aliases.resolveKind(invokedKind) ||
339
+ (scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)),
302
340
  );
303
341
  if (target) return resolveThrowsUnion(target, ctx);
304
342
  }
@@ -1,6 +1,6 @@
1
1
  import type { ASTNode, Environment } from "@marcbachmann/cel-js";
2
2
  import type { ResourceManifest } from "@telorun/sdk";
3
- import type { AliasResolver } from "./alias-resolver.js";
3
+ import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
4
4
  import type { DefinitionRegistry } from "./definition-registry.js";
5
5
  import {
6
6
  createResolveCtx,
@@ -490,16 +490,28 @@ export function validateThrowsCoverage(
490
490
  defs: DefinitionRegistry,
491
491
  aliases: AliasResolver,
492
492
  env: Environment,
493
+ aliasesByModule: Map<string, AliasResolver> = new Map(),
494
+ rootModules: Set<string> = new Set(),
493
495
  ): AnalysisDiagnostic[] {
494
496
  const diagnostics: AnalysisDiagnostic[] = [];
495
497
  diagnostics.push(...validateThrowsDeclarations(manifests));
496
498
 
497
- const resolveCtx = createResolveCtx(manifests, defs, aliases);
499
+ const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules);
500
+
501
+ // The alias resolver for a manifest's own lexical scope — an imported library's
502
+ // resolver when it owns the manifest, else undefined (fall back to root aliases).
503
+ const scopeResolverFor = (m: ResourceManifest): AliasResolver | undefined =>
504
+ scopeResolverForModule(
505
+ (m.metadata as { module?: string } | undefined)?.module,
506
+ rootModules,
507
+ aliasesByModule,
508
+ );
498
509
 
499
510
  for (const manifest of manifests) {
500
511
  if (!manifest.kind || !manifest.metadata?.name) continue;
501
512
  if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract") continue;
502
- const resolvedKind = aliases.resolveKind(manifest.kind);
513
+ const scopeResolver = scopeResolverFor(manifest);
514
+ const resolvedKind = scopeResolver?.resolveKind(manifest.kind) ?? aliases.resolveKind(manifest.kind);
503
515
  const definition =
504
516
  defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
505
517
  if (!definition?.schema) continue;
@@ -519,7 +531,7 @@ export function validateThrowsCoverage(
519
531
  ...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath),
520
532
  );
521
533
  const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
522
- const union = handlerRefUnion(handlerRef, manifests, resolveCtx);
534
+ const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
523
535
  diagnostics.push(
524
536
  ...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env),
525
537
  );
@@ -539,20 +551,24 @@ function handlerRefUnion(
539
551
  handlerRef: { kind: string; name?: string } | null,
540
552
  manifests: ResourceManifest[],
541
553
  ctx: ReturnType<typeof createResolveCtx>,
554
+ scopeResolver: AliasResolver | undefined,
542
555
  ): ThrowsUnion {
543
556
  if (!handlerRef) return { codes: new Map(), unbounded: false };
544
557
  if (handlerRef.name) {
545
- const resolvedKind = ctx.aliases.resolveKind(handlerRef.kind);
558
+ const resolvedKind = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
546
559
  const targetManifest = manifests.find(
547
560
  (m) =>
548
561
  m.metadata?.name === handlerRef.name &&
549
562
  (m.kind === handlerRef.kind ||
550
563
  m.kind === resolvedKind ||
564
+ scopeResolver?.resolveKind(m.kind) === handlerRef.kind ||
551
565
  ctx.aliases.resolveKind(m.kind) === handlerRef.kind),
552
566
  );
553
567
  if (targetManifest) return resolveThrowsUnion(targetManifest, ctx);
554
568
  }
555
- const resolved = ctx.aliases.resolveKind(handlerRef.kind);
569
+ // No named target — fall back to the handler kind's own declared codes,
570
+ // resolving the kind in the owner's lexical scope first, then root aliases.
571
+ const resolved = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
556
572
  const def =
557
573
  ctx.defs.resolve(handlerRef.kind) ?? (resolved ? ctx.defs.resolve(resolved) : undefined);
558
574
  if (!def?.throws) return { codes: new Map(), unbounded: false };