@telorun/analyzer 0.9.0 → 0.10.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.
@@ -16,6 +16,10 @@ export declare class AnalysisRegistry {
16
16
  /**
17
17
  * Iterates a resource's reference and scope fields as declared by its definition.
18
18
  * Calls onRef for each plain reference field and onScope for each scope field.
19
+ *
20
+ * Uses the expanded field map so x-telo-schema-from entries contribute their
21
+ * nested ref/scope slots — Phase 5 injection sees encoders that live inside a
22
+ * sub-schema (e.g. Server.notFoundHandler.returns[].content[mime].encoder).
19
23
  */
20
24
  iterateFieldEntries(resource: ResourceManifest, onRef: (fieldPath: string) => void, onScope: (fieldPath: string) => void): void;
21
25
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;OAGG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAcP;;;;OAIG;IACH,kBAAkB,IAAI,kBAAkB,EAAE;IAI1C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAM/D,QAAQ,IAAI,MAAM,EAAE;IAIpB;mEAC+D;IAC/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC;;;gCAG4B;IAC5B,oBAAoB,IAAI,MAAM,EAAE;IAIhC;wEACoE;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
1
+ {"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IAEpE,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;;;;;OAOG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAkBP;;;;OAIG;IACH,kBAAkB,IAAI,kBAAkB,EAAE;IAI1C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAM/D,QAAQ,IAAI,MAAM,EAAE;IAIpB;mEAC+D;IAC/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC;;;gCAG4B;IAC5B,oBAAoB,IAAI,MAAM,EAAE;IAIhC;wEACoE;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
@@ -27,9 +27,13 @@ export class AnalysisRegistry {
27
27
  /**
28
28
  * Iterates a resource's reference and scope fields as declared by its definition.
29
29
  * Calls onRef for each plain reference field and onScope for each scope field.
30
+ *
31
+ * Uses the expanded field map so x-telo-schema-from entries contribute their
32
+ * nested ref/scope slots — Phase 5 injection sees encoders that live inside a
33
+ * sub-schema (e.g. Server.notFoundHandler.returns[].content[mime].encoder).
30
34
  */
31
35
  iterateFieldEntries(resource, onRef, onScope) {
32
- const fieldMap = this.defs.getFieldMapForKind(resource.kind, this.aliases);
36
+ const fieldMap = this.defs.expandedFieldMapForResource(resource, this.aliases, this.aliasesByModule);
33
37
  if (!fieldMap)
34
38
  return;
35
39
  for (const [fieldPath, entry] of fieldMap) {
@@ -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,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgX/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,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAqUvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAKxF,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;CAiB5F"}
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,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgX/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,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAqUvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAUxF,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
@@ -412,7 +412,7 @@ export class StaticAnalyzer {
412
412
  defs.register(normalized);
413
413
  }
414
414
  // Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
415
- const allManifests = normalizeInlineResources(manifests, defs, aliases);
415
+ const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
416
416
  // Build a name→manifest map for looking up referenced resources
417
417
  const byName = new Map();
418
418
  for (const m of allManifests) {
@@ -587,7 +587,7 @@ export class StaticAnalyzer {
587
587
  }
588
588
  normalize(manifests, registry) {
589
589
  const ctx = registry._context();
590
- return normalizeInlineResources(manifests, ctx.definitions, ctx.aliases);
590
+ return normalizeInlineResources(manifests, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
591
591
  }
592
592
  prepare(manifests, registry) {
593
593
  const ctx = registry._context();
@@ -596,7 +596,7 @@ export class StaticAnalyzer {
596
596
  if (errors.length > 0) {
597
597
  return { diagnostics: errors, order: null, cycleError: null };
598
598
  }
599
- const graph = buildDependencyGraph(manifests, ctx.definitions, ctx.aliases);
599
+ const graph = buildDependencyGraph(manifests, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
600
600
  if (graph.cycle) {
601
601
  return { diagnostics: [], order: null, cycleError: formatCycle(graph.cycle) };
602
602
  }
@@ -1,4 +1,5 @@
1
- import type { ResourceDefinition } from "@telorun/sdk";
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
2
3
  import { type ReferenceFieldMap } from "./reference-field-map.js";
3
4
  /** Pure kind → ResourceDefinition map. No controller loading, no lifecycle. */
4
5
  export declare class DefinitionRegistry {
@@ -50,6 +51,17 @@ export declare class DefinitionRegistry {
50
51
  getFieldMapForKind(kind: string, aliases?: {
51
52
  resolveKind(k: string): string | undefined;
52
53
  }): ReferenceFieldMap | undefined;
54
+ /** Returns the field map for `resource.kind` with x-telo-schema-from entries replaced
55
+ * by their nested ref/scope slots — so Phase 2 inline normalization and Phase 5
56
+ * injection see encoders nested behind a schema-from indirection (e.g.
57
+ * http-server `Server.notFoundHandler.returns[].content[mime].encoder`).
58
+ *
59
+ * Only static absolute schema-from paths with a dotted alias anchor are expanded
60
+ * (e.g. "HttpDispatch.Outcomes/$defs/Returns"). Relative and unqualified absolute
61
+ * anchors depend on a sibling property at runtime and stay unexpanded; the
62
+ * analyzer's reference validation phase already flags the cases that matter. */
63
+ expandedFieldMapForResource(resource: ResourceManifest, aliases: AliasResolver, aliasesByModule: Map<string, AliasResolver>): ReferenceFieldMap | undefined;
64
+ private resolveSchemaFromSubMap;
53
65
  /** Returns all definitions that transitively extend the given abstract kind.
54
66
  * Follows the capability chain to any depth (equivalent to instanceof in OOP).
55
67
  * Definitions are included regardless of registration order. */
@@ -1 +1 @@
1
- {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG1F,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAgB1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;wFACoF;IACpF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
1
+ {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAGlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAgB1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;wFACoF;IACpF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;;;;;;;qFAQiF;IACjF,2BAA2B,CACzB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,iBAAiB,GAAG,SAAS;IAuBhC,OAAO,CAAC,uBAAuB;IA+B/B;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
@@ -1,6 +1,6 @@
1
1
  import { KERNEL_BUILTINS } from "./builtins.js";
2
- import { buildReferenceFieldMap } from "./reference-field-map.js";
3
- import { createAjv, formatSingleError } from "./schema-compat.js";
2
+ import { buildFieldMapAtPath, buildReferenceFieldMap, isSchemaFromEntry, } from "./reference-field-map.js";
3
+ import { createAjv, formatSingleError, navigateJsonPointer } from "./schema-compat.js";
4
4
  /** Pure kind → ResourceDefinition map. No controller loading, no lifecycle. */
5
5
  export class DefinitionRegistry {
6
6
  constructor() {
@@ -147,6 +147,62 @@ export class DefinitionRegistry {
147
147
  const resolved = aliases?.resolveKind(kind);
148
148
  return resolved ? this.getFieldMap(resolved) : undefined;
149
149
  }
150
+ /** Returns the field map for `resource.kind` with x-telo-schema-from entries replaced
151
+ * by their nested ref/scope slots — so Phase 2 inline normalization and Phase 5
152
+ * injection see encoders nested behind a schema-from indirection (e.g.
153
+ * http-server `Server.notFoundHandler.returns[].content[mime].encoder`).
154
+ *
155
+ * Only static absolute schema-from paths with a dotted alias anchor are expanded
156
+ * (e.g. "HttpDispatch.Outcomes/$defs/Returns"). Relative and unqualified absolute
157
+ * anchors depend on a sibling property at runtime and stay unexpanded; the
158
+ * analyzer's reference validation phase already flags the cases that matter. */
159
+ expandedFieldMapForResource(resource, aliases, aliasesByModule) {
160
+ const baseMap = this.getFieldMapForKind(resource.kind, aliases);
161
+ if (!baseMap)
162
+ return undefined;
163
+ const resolvedKind = aliases.resolveKind(resource.kind) ?? resource.kind;
164
+ const def = this.resolve(resource.kind) ?? this.resolve(resolvedKind);
165
+ const ownerModule = def?.metadata?.module;
166
+ const ownerScope = (ownerModule ? aliasesByModule.get(ownerModule) : undefined) ?? aliases;
167
+ const expanded = new Map();
168
+ for (const [path, entry] of baseMap) {
169
+ if (!isSchemaFromEntry(entry)) {
170
+ expanded.set(path, entry);
171
+ continue;
172
+ }
173
+ const sub = this.resolveSchemaFromSubMap(entry.schemaFrom, path, ownerScope);
174
+ if (!sub)
175
+ continue;
176
+ for (const [subPath, subEntry] of sub)
177
+ expanded.set(subPath, subEntry);
178
+ }
179
+ return expanded;
180
+ }
181
+ resolveSchemaFromSubMap(schemaFrom, fieldPath, ownerScope) {
182
+ const isAbsolute = schemaFrom.startsWith("/");
183
+ const expr = isAbsolute ? schemaFrom.slice(1) : schemaFrom;
184
+ const slashIdx = expr.indexOf("/");
185
+ if (slashIdx === -1)
186
+ return null;
187
+ const anchorName = expr.slice(0, slashIdx);
188
+ const jsonPointer = "/" + expr.slice(slashIdx + 1);
189
+ // Static form: absolute path whose anchor is a dotted alias (e.g.
190
+ // "HttpDispatch.Outcomes/$defs/Returns"). Polymorphic forms — relative
191
+ // anchors or single-segment absolute anchors — only resolve once we know a
192
+ // sibling property's value, which is per-resource.
193
+ if (!anchorName.includes("."))
194
+ return null;
195
+ const targetKind = ownerScope.resolveKind(anchorName);
196
+ if (!targetKind)
197
+ return null;
198
+ const targetDef = this.resolve(targetKind);
199
+ if (!targetDef?.schema)
200
+ return null;
201
+ const subSchema = navigateJsonPointer(targetDef.schema, jsonPointer);
202
+ if (!subSchema || typeof subSchema !== "object")
203
+ return null;
204
+ return buildFieldMapAtPath(subSchema, fieldPath);
205
+ }
150
206
  /** Returns all definitions that transitively extend the given abstract kind.
151
207
  * Follows the capability chain to any depth (equivalent to instanceof in OOP).
152
208
  * Definitions are included regardless of registration order. */
@@ -25,7 +25,7 @@ export interface DependencyGraph {
25
25
  * The registry is queried for each resource's field map by kind — callers do
26
26
  * not pre-compute or pass field maps separately.
27
27
  */
28
- export declare function buildDependencyGraph(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver): DependencyGraph;
28
+ export declare function buildDependencyGraph(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): DependencyGraph;
29
29
  /**
30
30
  * Formats a cycle result into a human-readable error string matching the spec:
31
31
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,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;AAGnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B;kDAC8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACpC;oFACgF;IAChF,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrC;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,GACtB,eAAe,CAsFjB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAOtE"}
1
+ {"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,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;AAGnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B;kDAC8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACpC;oFACgF;IAChF,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrC;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,eAAe,CA6FjB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAOtE"}
@@ -21,7 +21,7 @@ const nodeKey = (kind, name) => `${kind}\0${name}`;
21
21
  * The registry is queried for each resource's field map by kind — callers do
22
22
  * not pre-compute or pass field maps separately.
23
23
  */
24
- export function buildDependencyGraph(resources, registry, aliases) {
24
+ export function buildDependencyGraph(resources, registry, aliases, aliasesByModule) {
25
25
  // --- Build node set ---
26
26
  const nodes = new Map();
27
27
  for (const r of resources) {
@@ -38,7 +38,13 @@ export function buildDependencyGraph(resources, registry, aliases) {
38
38
  if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
39
39
  continue;
40
40
  const sourceKey = nodeKey(r.kind, r.metadata.name);
41
- const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
41
+ // Use the expanded map so refs nested behind x-telo-schema-from contribute
42
+ // edges to the DAG. Without these, a parent (e.g. Http.Server) can init
43
+ // before its extracted encoder and Phase 5 injection fires against a
44
+ // not-yet-created dependency.
45
+ const fieldMap = aliases && aliasesByModule
46
+ ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
47
+ : registry.getFieldMapForKind(r.kind, aliases);
42
48
  if (!fieldMap)
43
49
  continue;
44
50
  // Collect names of resources declared inside scope fields — these are initialized
@@ -18,5 +18,5 @@ import type { AliasResolver } from "./alias-resolver.js";
18
18
  * Returns a new array containing the original manifests (mutated in-place) plus all
19
19
  * extracted manifests. The original array is not mutated.
20
20
  */
21
- export declare function normalizeInlineResources(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver): ResourceManifest[];
21
+ export declare function normalizeInlineResources(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): ResourceManifest[];
22
22
  //# 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,GACtB,gBAAgB,EAAE,CAkDpB"}
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,CAwDpB"}
@@ -26,14 +26,19 @@ function sanitizeName(raw) {
26
26
  * Returns a new array containing the original manifests (mutated in-place) plus all
27
27
  * extracted manifests. The original array is not mutated.
28
28
  */
29
- export function normalizeInlineResources(resources, registry, aliases) {
29
+ export function normalizeInlineResources(resources, registry, aliases, aliasesByModule) {
30
30
  const result = [...resources];
31
31
  // Queue: all non-system resources with a name. Extracted resources are appended.
32
32
  const queue = resources.filter((r) => typeof r.metadata?.name === "string" && !!r.kind && !SYSTEM_KINDS.has(r.kind));
33
33
  let i = 0;
34
34
  while (i < queue.length) {
35
35
  const resource = queue[i++];
36
- const fieldMap = registry.getFieldMapForKind(resource.kind, aliases);
36
+ // When aliasesByModule is available, use the expanded map so inline refs
37
+ // hidden behind an x-telo-schema-from indirection (e.g. an encoder inside
38
+ // an HttpDispatch.Outcomes/$defs/Returns sub-schema) reach extraction.
39
+ const fieldMap = aliases && aliasesByModule
40
+ ? registry.expandedFieldMapForResource(resource, aliases, aliasesByModule)
41
+ : registry.getFieldMapForKind(resource.kind, aliases);
37
42
  if (!fieldMap)
38
43
  continue;
39
44
  const parentName = resource.metadata.name;
@@ -61,4 +61,9 @@ export declare function resolveFieldValues(obj: unknown, path: string): unknown[
61
61
  * - A node with `properties` → recurse into each property
62
62
  */
63
63
  export declare function buildReferenceFieldMap(schema: Record<string, any>): ReferenceFieldMap;
64
+ /** Traverses an arbitrary JSON Schema starting at the given path prefix. Used to
65
+ * expand x-telo-schema-from sub-schemas into nested ref/scope entries so Phase 2
66
+ * inline normalization and Phase 5 injection see slots that the local field map
67
+ * hid behind the schema-from indirection. */
68
+ export declare function buildFieldMapAtPath(schema: Record<string, any>, pathPrefix: string): ReferenceFieldMap;
64
69
  //# sourceMappingURL=reference-field-map.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;;kEAKkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA6BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF"}
1
+ {"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;;kEAKkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA6BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF;AAiBD;;;8CAG8C;AAC9C,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAInB"}
@@ -101,6 +101,15 @@ function collectRefs(node) {
101
101
  }
102
102
  return refs;
103
103
  }
104
+ /** Traverses an arbitrary JSON Schema starting at the given path prefix. Used to
105
+ * expand x-telo-schema-from sub-schemas into nested ref/scope entries so Phase 2
106
+ * inline normalization and Phase 5 injection see slots that the local field map
107
+ * hid behind the schema-from indirection. */
108
+ export function buildFieldMapAtPath(schema, pathPrefix) {
109
+ const map = new Map();
110
+ traverseNode(schema, pathPrefix, map);
111
+ return map;
112
+ }
104
113
  function traverseNode(node, path, map) {
105
114
  // Scope slot — record and stop; do not recurse into scope contents
106
115
  if ("x-telo-scope" in node) {
@@ -1 +1 @@
1
- {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAsUtB"}
1
+ {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2UtB"}
@@ -75,7 +75,12 @@ export function validateReferences(resources, context) {
75
75
  for (const r of resources) {
76
76
  if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
77
77
  continue;
78
- const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
78
+ // Use the expanded map so refs nested behind x-telo-schema-from get the
79
+ // same kind-check / unresolved-name validation as locally-declared refs.
80
+ // Falls back to the base map when aliasesByModule isn't supplied.
81
+ const fieldMap = aliasesByModule
82
+ ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
83
+ : registry.getFieldMapForKind(r.kind, aliases);
79
84
  if (!fieldMap)
80
85
  continue;
81
86
  const resourceLabel = `${r.kind}/${r.metadata.name}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -35,13 +35,21 @@ export class AnalysisRegistry {
35
35
  /**
36
36
  * Iterates a resource's reference and scope fields as declared by its definition.
37
37
  * Calls onRef for each plain reference field and onScope for each scope field.
38
+ *
39
+ * Uses the expanded field map so x-telo-schema-from entries contribute their
40
+ * nested ref/scope slots — Phase 5 injection sees encoders that live inside a
41
+ * sub-schema (e.g. Server.notFoundHandler.returns[].content[mime].encoder).
38
42
  */
39
43
  iterateFieldEntries(
40
44
  resource: ResourceManifest,
41
45
  onRef: (fieldPath: string) => void,
42
46
  onScope: (fieldPath: string) => void,
43
47
  ): void {
44
- const fieldMap = this.defs.getFieldMapForKind(resource.kind, this.aliases);
48
+ const fieldMap = this.defs.expandedFieldMapForResource(
49
+ resource,
50
+ this.aliases,
51
+ this.aliasesByModule,
52
+ );
45
53
  if (!fieldMap) return;
46
54
  for (const [fieldPath, entry] of fieldMap) {
47
55
  if (isScopeEntry(entry)) {
package/src/analyzer.ts CHANGED
@@ -525,7 +525,7 @@ export class StaticAnalyzer {
525
525
  }
526
526
 
527
527
  // Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
528
- const allManifests = normalizeInlineResources(manifests, defs, aliases);
528
+ const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
529
529
 
530
530
  // Build a name→manifest map for looking up referenced resources
531
531
  const byName = new Map<string, ResourceManifest>();
@@ -740,7 +740,12 @@ export class StaticAnalyzer {
740
740
 
741
741
  normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[] {
742
742
  const ctx = registry._context();
743
- return normalizeInlineResources(manifests, ctx.definitions!, ctx.aliases);
743
+ return normalizeInlineResources(
744
+ manifests,
745
+ ctx.definitions!,
746
+ ctx.aliases,
747
+ ctx.aliasesByModule,
748
+ );
744
749
  }
745
750
 
746
751
  prepare(
@@ -753,7 +758,12 @@ export class StaticAnalyzer {
753
758
  if (errors.length > 0) {
754
759
  return { diagnostics: errors, order: null, cycleError: null };
755
760
  }
756
- const graph = buildDependencyGraph(manifests, ctx.definitions!, ctx.aliases);
761
+ const graph = buildDependencyGraph(
762
+ manifests,
763
+ ctx.definitions!,
764
+ ctx.aliases,
765
+ ctx.aliasesByModule,
766
+ );
757
767
  if (graph.cycle) {
758
768
  return { diagnostics: [], order: null, cycleError: formatCycle(graph.cycle) };
759
769
  }
@@ -1,7 +1,13 @@
1
- import type { ResourceDefinition } from "@telorun/sdk";
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
2
3
  import { KERNEL_BUILTINS } from "./builtins.js";
3
- import { buildReferenceFieldMap, type ReferenceFieldMap } from "./reference-field-map.js";
4
- import { createAjv, formatSingleError } from "./schema-compat.js";
4
+ import {
5
+ buildFieldMapAtPath,
6
+ buildReferenceFieldMap,
7
+ isSchemaFromEntry,
8
+ type ReferenceFieldMap,
9
+ } from "./reference-field-map.js";
10
+ import { createAjv, formatSingleError, navigateJsonPointer } from "./schema-compat.js";
5
11
 
6
12
  /** Pure kind → ResourceDefinition map. No controller loading, no lifecycle. */
7
13
  export class DefinitionRegistry {
@@ -163,6 +169,73 @@ export class DefinitionRegistry {
163
169
  return resolved ? this.getFieldMap(resolved) : undefined;
164
170
  }
165
171
 
172
+ /** Returns the field map for `resource.kind` with x-telo-schema-from entries replaced
173
+ * by their nested ref/scope slots — so Phase 2 inline normalization and Phase 5
174
+ * injection see encoders nested behind a schema-from indirection (e.g.
175
+ * http-server `Server.notFoundHandler.returns[].content[mime].encoder`).
176
+ *
177
+ * Only static absolute schema-from paths with a dotted alias anchor are expanded
178
+ * (e.g. "HttpDispatch.Outcomes/$defs/Returns"). Relative and unqualified absolute
179
+ * anchors depend on a sibling property at runtime and stay unexpanded; the
180
+ * analyzer's reference validation phase already flags the cases that matter. */
181
+ expandedFieldMapForResource(
182
+ resource: ResourceManifest,
183
+ aliases: AliasResolver,
184
+ aliasesByModule: Map<string, AliasResolver>,
185
+ ): ReferenceFieldMap | undefined {
186
+ const baseMap = this.getFieldMapForKind(resource.kind, aliases);
187
+ if (!baseMap) return undefined;
188
+
189
+ const resolvedKind = aliases.resolveKind(resource.kind) ?? resource.kind;
190
+ const def = this.resolve(resource.kind) ?? this.resolve(resolvedKind);
191
+ const ownerModule = (def?.metadata as { module?: string } | undefined)?.module;
192
+ const ownerScope =
193
+ (ownerModule ? aliasesByModule.get(ownerModule) : undefined) ?? aliases;
194
+
195
+ const expanded: ReferenceFieldMap = new Map();
196
+ for (const [path, entry] of baseMap) {
197
+ if (!isSchemaFromEntry(entry)) {
198
+ expanded.set(path, entry);
199
+ continue;
200
+ }
201
+ const sub = this.resolveSchemaFromSubMap(entry.schemaFrom, path, ownerScope);
202
+ if (!sub) continue;
203
+ for (const [subPath, subEntry] of sub) expanded.set(subPath, subEntry);
204
+ }
205
+ return expanded;
206
+ }
207
+
208
+ private resolveSchemaFromSubMap(
209
+ schemaFrom: string,
210
+ fieldPath: string,
211
+ ownerScope: AliasResolver,
212
+ ): ReferenceFieldMap | null {
213
+ const isAbsolute = schemaFrom.startsWith("/");
214
+ const expr = isAbsolute ? schemaFrom.slice(1) : schemaFrom;
215
+ const slashIdx = expr.indexOf("/");
216
+ if (slashIdx === -1) return null;
217
+ const anchorName = expr.slice(0, slashIdx);
218
+ const jsonPointer = "/" + expr.slice(slashIdx + 1);
219
+
220
+ // Static form: absolute path whose anchor is a dotted alias (e.g.
221
+ // "HttpDispatch.Outcomes/$defs/Returns"). Polymorphic forms — relative
222
+ // anchors or single-segment absolute anchors — only resolve once we know a
223
+ // sibling property's value, which is per-resource.
224
+ if (!anchorName.includes(".")) return null;
225
+
226
+ const targetKind = ownerScope.resolveKind(anchorName);
227
+ if (!targetKind) return null;
228
+ const targetDef = this.resolve(targetKind);
229
+ if (!targetDef?.schema) return null;
230
+ const subSchema = navigateJsonPointer(
231
+ targetDef.schema as Record<string, unknown>,
232
+ jsonPointer,
233
+ );
234
+ if (!subSchema || typeof subSchema !== "object") return null;
235
+
236
+ return buildFieldMapAtPath(subSchema as Record<string, any>, fieldPath);
237
+ }
238
+
166
239
  /** Returns all definitions that transitively extend the given abstract kind.
167
240
  * Follows the capability chain to any depth (equivalent to instanceof in OOP).
168
241
  * Definitions are included regardless of registration order. */
@@ -45,6 +45,7 @@ export function buildDependencyGraph(
45
45
  resources: ResourceManifest[],
46
46
  registry: DefinitionRegistry,
47
47
  aliases?: AliasResolver,
48
+ aliasesByModule?: Map<string, AliasResolver>,
48
49
  ): DependencyGraph {
49
50
  // --- Build node set ---
50
51
  const nodes = new Map<string, ResourceNode>();
@@ -62,7 +63,14 @@ export function buildDependencyGraph(
62
63
  if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
63
64
 
64
65
  const sourceKey = nodeKey(r.kind, r.metadata.name as string);
65
- const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
66
+ // Use the expanded map so refs nested behind x-telo-schema-from contribute
67
+ // edges to the DAG. Without these, a parent (e.g. Http.Server) can init
68
+ // before its extracted encoder and Phase 5 injection fires against a
69
+ // not-yet-created dependency.
70
+ const fieldMap =
71
+ aliases && aliasesByModule
72
+ ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
73
+ : registry.getFieldMapForKind(r.kind, aliases);
66
74
  if (!fieldMap) continue;
67
75
 
68
76
  // Collect names of resources declared inside scope fields — these are initialized
@@ -36,6 +36,7 @@ export function normalizeInlineResources(
36
36
  resources: ResourceManifest[],
37
37
  registry: DefinitionRegistry,
38
38
  aliases?: AliasResolver,
39
+ aliasesByModule?: Map<string, AliasResolver>,
39
40
  ): ResourceManifest[] {
40
41
  const result = [...resources];
41
42
 
@@ -48,7 +49,13 @@ export function normalizeInlineResources(
48
49
  let i = 0;
49
50
  while (i < queue.length) {
50
51
  const resource = queue[i++];
51
- const fieldMap = registry.getFieldMapForKind(resource.kind, aliases);
52
+ // When aliasesByModule is available, use the expanded map so inline refs
53
+ // hidden behind an x-telo-schema-from indirection (e.g. an encoder inside
54
+ // an HttpDispatch.Outcomes/$defs/Returns sub-schema) reach extraction.
55
+ const fieldMap =
56
+ aliases && aliasesByModule
57
+ ? registry.expandedFieldMapForResource(resource, aliases, aliasesByModule)
58
+ : registry.getFieldMapForKind(resource.kind, aliases);
52
59
  if (!fieldMap) continue;
53
60
 
54
61
  const parentName = resource.metadata.name as string;
@@ -135,6 +135,19 @@ function collectRefs(node: Record<string, any>): string[] {
135
135
  return refs;
136
136
  }
137
137
 
138
+ /** Traverses an arbitrary JSON Schema starting at the given path prefix. Used to
139
+ * expand x-telo-schema-from sub-schemas into nested ref/scope entries so Phase 2
140
+ * inline normalization and Phase 5 injection see slots that the local field map
141
+ * hid behind the schema-from indirection. */
142
+ export function buildFieldMapAtPath(
143
+ schema: Record<string, any>,
144
+ pathPrefix: string,
145
+ ): ReferenceFieldMap {
146
+ const map: ReferenceFieldMap = new Map();
147
+ traverseNode(schema, pathPrefix, map);
148
+ return map;
149
+ }
150
+
138
151
  function traverseNode(node: Record<string, any>, path: string, map: ReferenceFieldMap): void {
139
152
  // Scope slot — record and stop; do not recurse into scope contents
140
153
  if ("x-telo-scope" in node) {
@@ -87,7 +87,12 @@ export function validateReferences(
87
87
  for (const r of resources) {
88
88
  if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
89
89
 
90
- const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
90
+ // Use the expanded map so refs nested behind x-telo-schema-from get the
91
+ // same kind-check / unresolved-name validation as locally-declared refs.
92
+ // Falls back to the base map when aliasesByModule isn't supplied.
93
+ const fieldMap = aliasesByModule
94
+ ? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
95
+ : registry.getFieldMapForKind(r.kind, aliases);
91
96
  if (!fieldMap) continue;
92
97
 
93
98
  const resourceLabel = `${r.kind}/${r.metadata.name as string}`;