@telorun/analyzer 0.70.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/analysis-registry.d.ts.map +1 -1
  2. package/dist/analysis-registry.js +4 -5
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +92 -10
  5. package/dist/call-graph.d.ts.map +1 -1
  6. package/dist/call-graph.js +10 -4
  7. package/dist/catch-scope.d.ts +72 -0
  8. package/dist/catch-scope.d.ts.map +1 -0
  9. package/dist/catch-scope.js +102 -0
  10. package/dist/cel-scope-query.d.ts +14 -0
  11. package/dist/cel-scope-query.d.ts.map +1 -1
  12. package/dist/cel-scope-query.js +36 -6
  13. package/dist/definition-registry.d.ts.map +1 -1
  14. package/dist/definition-registry.js +3 -4
  15. package/dist/deprecation.d.ts +21 -0
  16. package/dist/deprecation.d.ts.map +1 -0
  17. package/dist/deprecation.js +26 -0
  18. package/dist/flatten-for-analyzer.d.ts +2 -2
  19. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  20. package/dist/flatten-for-analyzer.js +1 -1
  21. package/dist/index.d.ts +5 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -2
  24. package/dist/manifest-visitor.d.ts +17 -1
  25. package/dist/manifest-visitor.d.ts.map +1 -1
  26. package/dist/manifest-visitor.js +16 -4
  27. package/dist/migrations/report.d.ts +1 -1
  28. package/dist/migrations/report.d.ts.map +1 -1
  29. package/dist/migrations/report.js +5 -0
  30. package/dist/module-alias-scope.d.ts +65 -0
  31. package/dist/module-alias-scope.d.ts.map +1 -0
  32. package/dist/module-alias-scope.js +25 -0
  33. package/dist/{zone-module-documents.d.ts → module-documents.d.ts} +11 -7
  34. package/dist/module-documents.d.ts.map +1 -0
  35. package/dist/ref-sentinel-target.d.ts +38 -0
  36. package/dist/ref-sentinel-target.d.ts.map +1 -0
  37. package/dist/ref-sentinel-target.js +13 -0
  38. package/dist/ref-slot.d.ts +15 -0
  39. package/dist/ref-slot.d.ts.map +1 -1
  40. package/dist/ref-slot.js +7 -0
  41. package/dist/resolve-schema-type-refs.d.ts.map +1 -1
  42. package/dist/resolve-schema-type-refs.js +2 -1
  43. package/dist/resolve-throws-union.d.ts +47 -2
  44. package/dist/resolve-throws-union.d.ts.map +1 -1
  45. package/dist/resolve-throws-union.js +199 -20
  46. package/dist/resolve-zone-requirements.d.ts +3 -3
  47. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  48. package/dist/resolve-zone-requirements.js +12 -12
  49. package/dist/schema-compat.d.ts.map +1 -1
  50. package/dist/schema-compat.js +13 -1
  51. package/dist/schema-error-report.d.ts.map +1 -1
  52. package/dist/schema-error-report.js +48 -4
  53. package/dist/schema-keywords.d.ts.map +1 -1
  54. package/dist/schema-keywords.js +3 -1
  55. package/dist/schema-walk.d.ts +27 -0
  56. package/dist/schema-walk.d.ts.map +1 -1
  57. package/dist/schema-walk.js +44 -0
  58. package/dist/telo-version.d.ts +1 -1
  59. package/dist/telo-version.js +1 -1
  60. package/dist/template-body.d.ts.map +1 -1
  61. package/dist/template-body.js +2 -4
  62. package/dist/types.d.ts +20 -3
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +11 -0
  65. package/dist/validate-identifier-names.d.ts +2 -2
  66. package/dist/validate-identifier-names.d.ts.map +1 -1
  67. package/dist/validate-identifier-names.js +22 -7
  68. package/dist/validate-invocation-contract.d.ts +5 -0
  69. package/dist/validate-invocation-contract.d.ts.map +1 -1
  70. package/dist/validate-invocation-contract.js +114 -3
  71. package/dist/validate-logging.d.ts.map +1 -1
  72. package/dist/validate-logging.js +2 -2
  73. package/dist/validate-ref-slots.d.ts +1 -1
  74. package/dist/validate-ref-slots.d.ts.map +1 -1
  75. package/dist/validate-ref-slots.js +34 -0
  76. package/dist/validate-references.d.ts +16 -5
  77. package/dist/validate-references.d.ts.map +1 -1
  78. package/dist/validate-references.js +57 -15
  79. package/dist/validate-resource-inputs.d.ts +1 -26
  80. package/dist/validate-resource-inputs.d.ts.map +1 -1
  81. package/dist/validate-resource-inputs.js +12 -2
  82. package/dist/validate-schema-type-refs.d.ts.map +1 -1
  83. package/dist/validate-schema-type-refs.js +2 -1
  84. package/dist/validate-throws-coverage.d.ts +5 -1
  85. package/dist/validate-throws-coverage.d.ts.map +1 -1
  86. package/dist/validate-throws-coverage.js +241 -86
  87. package/package.json +3 -3
  88. package/src/analysis-registry.ts +4 -5
  89. package/src/analyzer.ts +115 -11
  90. package/src/call-graph.ts +9 -3
  91. package/src/catch-scope.ts +157 -0
  92. package/src/cel-scope-query.ts +45 -8
  93. package/src/definition-registry.ts +3 -6
  94. package/src/deprecation.ts +36 -0
  95. package/src/flatten-for-analyzer.ts +3 -3
  96. package/src/index.ts +10 -3
  97. package/src/manifest-visitor.ts +30 -5
  98. package/src/migrations/report.ts +5 -1
  99. package/src/module-alias-scope.ts +94 -0
  100. package/src/{zone-module-documents.ts → module-documents.ts} +10 -6
  101. package/src/ref-sentinel-target.ts +46 -0
  102. package/src/ref-slot.ts +19 -0
  103. package/src/resolve-schema-type-refs.ts +2 -1
  104. package/src/resolve-throws-union.ts +253 -20
  105. package/src/resolve-zone-requirements.ts +14 -14
  106. package/src/schema-compat.ts +13 -0
  107. package/src/schema-error-report.ts +50 -6
  108. package/src/schema-keywords.ts +4 -1
  109. package/src/schema-walk.ts +56 -0
  110. package/src/telo-version.ts +1 -1
  111. package/src/template-body.ts +2 -5
  112. package/src/types.ts +21 -3
  113. package/src/validate-identifier-names.ts +28 -9
  114. package/src/validate-invocation-contract.ts +128 -2
  115. package/src/validate-logging.ts +2 -3
  116. package/src/validate-ref-slots.ts +41 -1
  117. package/src/validate-references.ts +74 -14
  118. package/src/validate-resource-inputs.ts +26 -3
  119. package/src/validate-schema-type-refs.ts +2 -1
  120. package/src/validate-throws-coverage.ts +344 -92
  121. package/dist/zone-module-documents.d.ts.map +0 -1
  122. /package/dist/{zone-module-documents.js → module-documents.js} +0 -0
@@ -17,6 +17,7 @@ import {
17
17
  type SchemaFromFieldEntry,
18
18
  } from "./reference-field-map.js";
19
19
  import type { ModuleScopes } from "./alias-resolver.js";
20
+ import { moduleAliasScope } from "./module-alias-scope.js";
20
21
  import { templateBodies, withTemplateSelf } from "./template-body.js";
21
22
  import { extractContextsFromSchema, pathMatchesScope } from "./validate-cel-context.js";
22
23
 
@@ -65,8 +66,21 @@ export interface ScopeBoundaryEvent {
65
66
  source: ResourceManifest;
66
67
  /** Dot-form prefixes of every `x-telo-scope` field on this resource. */
67
68
  scopePrefixes: string[];
68
- /** Scope-field JSON Pointer manifests declared within that scope. */
69
+ /** VISIBILITY pointer (an `x-telo-scope` entry, e.g. `/steps`) the manifests
70
+ * visible there. Keyed by where scoped names may be REFERENCED, which is not
71
+ * where they are DECLARED: `Run.Sequence` declares `x-telo-scope:
72
+ * ["/steps", "/targets"]` on its `with:` field, so one declaration appears
73
+ * under two keys and neither names `with`. Use {@link declarations} for the
74
+ * declaration site. */
69
75
  manifestsByPointer: Map<string, ResourceManifest[]>;
76
+ /** Every inline declaration inside this resource's scopes, ONCE each, with the
77
+ * concrete path it is written at in the owner's document (`with[0]`).
78
+ *
79
+ * A diagnostic about a scoped resource has to anchor there: the resource is
80
+ * not a top-level document, so position lookup finds the OWNER and then walks
81
+ * this path into it. Deriving one from a visibility pointer instead names a
82
+ * region the declaration is not in (`steps[0]`). */
83
+ declarations: { manifest: ResourceManifest; path: string }[];
70
84
  /** Names of every resource declared inside this resource's scopes. Used by
71
85
  * the dependency graph to drop boot edges to scoped (on-demand) targets. */
72
86
  enclosedNames: Set<string>;
@@ -237,7 +251,14 @@ export function visitManifest(
237
251
  if (!r.metadata?.name || !r.kind) continue;
238
252
  if (skipKinds?.has(r.kind)) continue;
239
253
 
240
- const resolvedKind = aliases?.resolveKind(r.kind);
254
+ // A resource's own kind is written in the alias scope of the module that
255
+ // DECLARED it — see `moduleAliasScope`, which is the one place that rule
256
+ // lives. Resolving it through the entry's aliases alone left `definition`
257
+ // undefined for every forwarded resource, so its `x-telo-context` regions
258
+ // contributed nothing and `request` / `result` read as unknown identifiers
259
+ // inside the library's own routes, on a file the consumer cannot fix.
260
+ const moduleScope = moduleAliasScope(r.metadata, aliases, aliasesByModule);
261
+ const resolvedKind = moduleScope?.resolveKind(r.kind);
241
262
  const definition =
242
263
  registry.resolve(r.kind) ??
243
264
  (resolvedKind ? registry.resolve(resolvedKind) : undefined);
@@ -249,8 +270,8 @@ export function visitManifest(
249
270
  const emittedRefPaths = wantsNested ? new Set<string>() : null;
250
271
 
251
272
  if (wantsRefs || wantsScope || wantsSchemaFrom) {
252
- const baseMap = aliases
253
- ? registry.getFieldMapForKind(r.kind, aliases)
273
+ const baseMap = moduleScope
274
+ ? registry.getFieldMapForKind(r.kind, moduleScope)
254
275
  : registry.getFieldMap(r.kind);
255
276
 
256
277
  // Expanded map drives ref/scope sites when requested; schema-from sites
@@ -263,6 +284,7 @@ export function visitManifest(
263
284
  if (refScopeMap && (wantsRefs || wantsScope)) {
264
285
  const manifestsByPointer = new Map<string, ResourceManifest[]>();
265
286
  const scopeRefEntries: { path: string; refName: string }[] = [];
287
+ const declarations: { manifest: ResourceManifest; path: string }[] = [];
266
288
  for (const [fieldPath, entry] of refScopeMap) {
267
289
  if (!isScopeEntry(entry)) continue;
268
290
  const raw: ResourceManifest[] = [];
@@ -270,6 +292,7 @@ export function visitManifest(
270
292
  const items = Array.isArray(fe.value) ? fe.value : [fe.value];
271
293
  items.forEach((v, i) => {
272
294
  if (!v || typeof v !== "object") return;
295
+ const declarationPath = Array.isArray(fe.value) ? `${fe.path}[${i}]` : fe.path;
273
296
  // A scope entry must be an inline resource definition; a `!ref`
274
297
  // (tagged sentinel or resolved `{kind, name}`) is not — record it
275
298
  // so a static diagnostic flags it instead of registering a
@@ -280,12 +303,13 @@ export function visitManifest(
280
303
  (typeof rec.kind === "string" && typeof rec.name === "string")
281
304
  ) {
282
305
  scopeRefEntries.push({
283
- path: Array.isArray(fe.value) ? `${fe.path}[${i}]` : fe.path,
306
+ path: declarationPath,
284
307
  refName: isRefSentinel(v) ? v.source : String(rec.name),
285
308
  });
286
309
  return;
287
310
  }
288
311
  raw.push(v as ResourceManifest);
312
+ declarations.push({ manifest: v as ResourceManifest, path: declarationPath });
289
313
  });
290
314
  }
291
315
  const pointers = Array.isArray(entry.scope) ? entry.scope : [entry.scope];
@@ -305,6 +329,7 @@ export function visitManifest(
305
329
  source: r,
306
330
  scopePrefixes,
307
331
  manifestsByPointer,
332
+ declarations,
308
333
  enclosedNames,
309
334
  scopeRefEntries,
310
335
  });
@@ -21,7 +21,7 @@ import type {
21
21
  MigrationPath,
22
22
  MigrationRewrite,
23
23
  } from "./types.js";
24
- import type { AnalysisDiagnostic, DiagnosticFix } from "../types.js";
24
+ import { DiagnosticTag, type AnalysisDiagnostic, type DiagnosticFix } from "../types.js";
25
25
 
26
26
  /** One patch that applied, as the reporting side needs to see it. */
27
27
  export interface AppliedPatch {
@@ -63,6 +63,10 @@ export function toDiagnostic(
63
63
  severity: applied.entry.severity,
64
64
  code: applied.entry.code,
65
65
  source: "telo-analyzer",
66
+ // A migration IS a deprecation — a legacy spelling still read, and the one
67
+ // an author is being asked to stop writing — so the range carries the tag
68
+ // whatever severity the entry chose for itself.
69
+ tags: [DiagnosticTag.Deprecated],
66
70
  message: `${rewrite.summary}\n${applied.entry.reason}\n${closing}`,
67
71
  data: {
68
72
  filePath: source,
@@ -0,0 +1,94 @@
1
+ import { AliasResolver, scopeResolverForModule, type ModuleScopes } from "./alias-resolver.js";
2
+
3
+ /**
4
+ * WHICH ALIAS TABLE A MANIFEST'S OWN NAMES ARE WRITTEN IN.
5
+ *
6
+ * An application analysis is flattened, so an imported library's definitions and
7
+ * exported instances are checked in the CONSUMER's pass. But every alias-qualified
8
+ * name on such a manifest — its `kind:`, its `extends:`, an `x-telo-schema-from`
9
+ * anchor — was written against the alias map of the module that DECLARED it. A
10
+ * library writes `kind: Http.Api` through an import the consumer has no reason to
11
+ * have, so resolving it through the entry's table finds nothing, and everything
12
+ * derived from the definition silently goes missing: the field map (Phase-5
13
+ * injection blind to a forwarded resource's ref slots) and the `x-telo-context`
14
+ * regions (`request` / `result` reported as unknown identifiers on a file the
15
+ * consumer cannot edit).
16
+ *
17
+ * **There are TWO fallbacks, not one, and this file is where they are told
18
+ * apart** — that is the whole reason they live together. They agree on every
19
+ * module that has a table, and differ on a non-root module that has none:
20
+ *
21
+ * - {@link moduleAliasScope} falls back to the GLOBAL table. For a question
22
+ * asked about an arbitrary manifest, where a missing entry means "this is the
23
+ * entry's own", which is what the global table answers correctly.
24
+ * - {@link declaringModuleScope} falls back to an EMPTY table for a non-root
25
+ * module. For canonicalizing what a library DECLARED, where resolving its
26
+ * `Http.Api` through the consumer's imports would silently bind a library's
27
+ * kind to whatever the app happens to import under that alias — a wrong
28
+ * answer, which is worse than none.
29
+ *
30
+ * Both rest on one invariant neither states inline: **a root module is never a
31
+ * key in `aliasesByModule`** — a root's imports are registered into the global
32
+ * table instead (see the `rootModules` guards in `analyze()`) — so for a
33
+ * consumer-owned manifest the two rules coincide and neither fallback is a
34
+ * degradation.
35
+ *
36
+ * Every READ of the rule goes through one of these two. What legitimately does
37
+ * not, and why, so the next reader does not have to re-derive it: the three
38
+ * sites in `analyze()` that POPULATE `aliasesByModule` (a write, not a lookup),
39
+ * and `resolve-ref-sentinels`, whose module is known non-root by construction and
40
+ * whose fallback is the raw kind rather than another table — a different rule
41
+ * that happens to read the same map.
42
+ */
43
+ export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
44
+ metadata: { module?: unknown } | undefined,
45
+ aliases: A,
46
+ aliasesByModule: ReadonlyMap<string, M> | undefined,
47
+ ): A | M;
48
+ export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
49
+ metadata: { module?: unknown } | undefined,
50
+ aliases: A | undefined,
51
+ aliasesByModule: ReadonlyMap<string, M> | undefined,
52
+ ): A | M | undefined;
53
+ export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
54
+ metadata: { module?: unknown } | undefined,
55
+ aliases: A | undefined,
56
+ aliasesByModule: ReadonlyMap<string, M> | undefined,
57
+ ): A | M | undefined {
58
+ const declaringModule = metadata?.module;
59
+ if (typeof declaringModule !== "string") return aliases;
60
+ return aliasesByModule?.get(declaringModule) ?? aliases;
61
+ }
62
+
63
+ /** All this rule needs of a resolver, and deliberately all it asks for:
64
+ * `ModuleScopes` already types its map this way so a caller can hand over a
65
+ * lighter table, and requiring the full `AliasResolver` here would have made
66
+ * the one site that does (a template body) reach for a cast. */
67
+ interface KindResolver {
68
+ resolveKind(kind: string): string | undefined;
69
+ }
70
+
71
+ /** An empty table, shared: a non-root module with no aliases of its own resolves
72
+ * nothing rather than resolving through the consumer's. One instance because it
73
+ * is immutable in use and allocating one per call put a resolver on a per-schema
74
+ * loop. */
75
+ const NO_ALIASES = new AliasResolver();
76
+
77
+ /**
78
+ * The scope a DECLARING module's own alias-form names are canonicalized in — the
79
+ * root-aware half of the rule above, and the one that must never fall through to
80
+ * the consumer's table. See this file's header for why the two differ.
81
+ */
82
+ export function declaringModuleScope(
83
+ ownModule: string | undefined,
84
+ aliases: AliasResolver,
85
+ scopes: { aliasesByModule: Map<string, AliasResolver>; rootModules: Set<string> },
86
+ ): AliasResolver {
87
+ const own = scopeResolverForModule(ownModule, scopes.rootModules, scopes.aliasesByModule);
88
+ if (own) return own;
89
+ // Root (or unknown-and-therefore-treated-as-root): the global table IS its
90
+ // import map. A non-root module with no table of its own resolves nothing.
91
+ return ownModule && !scopes.rootModules.has(ownModule) ? NO_ALIASES : aliases;
92
+ }
93
+
94
+ export type { ModuleScopes };
@@ -1,19 +1,23 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
 
3
3
  /**
4
- * One imported library's FULL document set, for the zone stage's per-library
5
- * export derivation what the flattened analysis view no longer holds, since
6
- * it forwards only each library's export surface and never its internal
7
- * dispatch chain.
4
+ * One imported library's FULL document set what the flattened analysis view
5
+ * no longer holds, since it forwards only each library's export surface and
6
+ * never its internal dispatch chain.
7
+ *
8
+ * Named for what it CARRIES rather than for who asked first: the zone stage's
9
+ * per-library export derivation was the original consumer, and the throws walk
10
+ * is the second — a third should not have to import "zone" to ask about
11
+ * something else again.
8
12
  *
9
13
  * Plain data in a module of its own, deliberately. It is produced by the
10
- * loading side (`collectZoneModuleDocuments`), named in `AnalysisOptions`, and
14
+ * loading side (`collectModuleDocuments`), named in `AnalysisOptions`, and
11
15
  * consumed by the projection; putting it in any of the three would make the
12
16
  * other two import that one, and `types.ts` ↔ the projection is a genuine
13
17
  * cycle. A leaf module with no imports of its own breaks it without an inline
14
18
  * `import(...)` type expression standing in for the dependency nobody wanted.
15
19
  */
16
- export interface ZoneModuleDocuments {
20
+ export interface ModuleDocuments {
17
21
  /** The library's module name (its `Telo.Library` doc's `metadata.name`). */
18
22
  module: string;
19
23
  /** Stable source identity of the library's owner file — the cache key. */
@@ -0,0 +1,46 @@
1
+ import { isRefSentinel } from "@telorun/templating";
2
+
3
+ /**
4
+ * What a `!ref` names, parsed once.
5
+ *
6
+ * THE single reader of the tag's grammar, on the `ref-slot.ts` / `zone-slot.ts`
7
+ * precedent. Three passes had grown their own parse of the same scalar and they
8
+ * disagreed: one took the source verbatim, one split on the first dot and
9
+ * dropped every alias but `Self`, one split on the LAST dot and dropped the
10
+ * alias entirely — so the same `!ref Alias.name` named three different things
11
+ * depending on which pass was asking, and the loosest of the three resolved a
12
+ * bare name against whatever manifest happened to share it.
13
+ *
14
+ * The grammar itself is one line and is not in dispute: `!ref <name>` or
15
+ * `!ref <Alias>.<name>`, split on the FIRST dot, because that is what
16
+ * `resolveRefSentinels` does and a name may not contain one
17
+ * (`INVALID_NAME` rejects it at every declaration site).
18
+ *
19
+ * What this deliberately does NOT do is decide what to look up. A reduction is
20
+ * the caller's, and the three genuinely differ: an edge in the call graph points
21
+ * at a cross-module target and tolerates not resolving it; a zone correlation
22
+ * refuses to bind anything it cannot resolve exactly; a throws walk resolves in
23
+ * the DECLARING library first. Each states its own, over one parse.
24
+ *
25
+ * Browser-safe.
26
+ */
27
+ export interface RefSentinelTarget {
28
+ /** The scalar as written, for a caller that wants the author's spelling. */
29
+ source: string;
30
+ /** The prefix before the first dot, `Self` included and not normalized away —
31
+ * a caller that treats `Self` as "no alias" says so itself. */
32
+ alias?: string;
33
+ /** The segment after the first dot, or the whole scalar when there is none. */
34
+ name: string;
35
+ }
36
+
37
+ /** Parse a `!ref` sentinel. Returns `undefined` for anything that is not one —
38
+ * including the `{kind, name}` object `resolveRefSentinels` rewrites it into,
39
+ * which is a different shape with a different reader. */
40
+ export function refSentinelTarget(value: unknown): RefSentinelTarget | undefined {
41
+ if (!isRefSentinel(value)) return undefined;
42
+ const source = value.source;
43
+ const dot = source.indexOf(".");
44
+ if (dot <= 0) return { source, name: source };
45
+ return { source, alias: source.slice(0, dot), name: source.slice(dot + 1) };
46
+ }
package/src/ref-slot.ts CHANGED
@@ -94,6 +94,21 @@ export interface RefSlot {
94
94
  * that carries this call's arguments. Replaces
95
95
  * `x-telo-topology-role: inputs`. */
96
96
  inputs?: string;
97
+ /** Throws raised by the target — or by anything the target drives — surface
98
+ * through the DECLARING resource, so its own throws union includes the
99
+ * target's and its catch scope encloses the target's.
100
+ *
101
+ * It exists because the relation is real where `use` correctly says nothing:
102
+ * `Http.Server.mounts[].mount` is a `dependency` (the server holds the mount
103
+ * and calls a convention method on it — control reaches a route through the
104
+ * MOUNT's own `trigger.inbound` slot, not through this one), yet a throw from
105
+ * that route is exactly what the server renders. Following every `dependency`
106
+ * edge instead would drag a connection's throws into a router's denominator.
107
+ *
108
+ * Declared by the kind that HOLDS, because only it knows the throws surface
109
+ * through it, and it is the same fact its runtime establishes by rethrowing —
110
+ * so the annotation and the behaviour cannot disagree. */
111
+ throwsThrough?: boolean;
97
112
  /** `x-telo-inline: true` on the slot or any `anyOf` branch — accepts an inline
98
113
  * `{kind, ...config}` definition, not only a `!ref`. */
99
114
  inline: boolean;
@@ -228,6 +243,7 @@ export function readRefSlot(node: Record<string, any> | undefined): RefSlot | un
228
243
  const uses = new Set<RefUse>();
229
244
  let useCases: RefUseCases | undefined;
230
245
  let inputs: string | undefined;
246
+ let throwsThrough = false;
231
247
 
232
248
  for (const carrier of nodes) {
233
249
  const annotation = carrier["x-telo-ref"];
@@ -239,6 +255,7 @@ export function readRefSlot(node: Record<string, any> | undefined): RefSlot | un
239
255
  for (const use of normalizeUses(obj.use)) uses.add(use);
240
256
  useCases ??= readUseCases(obj.use);
241
257
  if (typeof obj.inputs === "string") inputs ??= obj.inputs;
258
+ if (obj.throwsThrough === true) throwsThrough = true;
242
259
  }
243
260
 
244
261
  const slot: RefSlot = {
@@ -249,6 +266,7 @@ export function readRefSlot(node: Record<string, any> | undefined): RefSlot | un
249
266
  };
250
267
  if (useCases) slot.useCases = useCases;
251
268
  if (inputs !== undefined) slot.inputs = inputs;
269
+ if (throwsThrough) slot.throwsThrough = true;
252
270
  return slot;
253
271
  }
254
272
 
@@ -274,6 +292,7 @@ export function refSlotAnnotation(slot: RefSlot): Record<string, unknown> {
274
292
  else if (slot.uses.length === 1) annotation.use = slot.uses[0];
275
293
  else if (slot.uses.length > 1) annotation.use = slot.uses;
276
294
  if (slot.inputs !== undefined) annotation.inputs = slot.inputs;
295
+ if (slot.throwsThrough) annotation.throwsThrough = true;
277
296
  return annotation;
278
297
  }
279
298
 
@@ -1,6 +1,7 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
3
3
  import type { AliasResolver } from "./alias-resolver.js";
4
+ import { moduleAliasScope } from "./module-alias-scope.js";
4
5
 
5
6
  /** Schema-bearing fields on a Telo.Definition / Telo.Type resource. */
6
7
  const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
@@ -96,7 +97,7 @@ export function resolveSchemaTypeRefs(
96
97
 
97
98
  for (const r of resources) {
98
99
  const ownModule = (r.metadata as { module?: string } | undefined)?.module;
99
- const resolver = (ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
100
+ const resolver = moduleAliasScope(r.metadata, aliases, aliasesByModule);
100
101
  const resolveAuthority = (authority: string): string | undefined =>
101
102
  authority === "Self" ? ownModule : resolver?.moduleForAlias(authority);
102
103
  for (const field of SCHEMA_FIELDS) {