@telorun/analyzer 0.1.3 → 0.2.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 (65) hide show
  1. package/README.md +3 -3
  2. package/dist/analyzer.d.ts +6 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +45 -25
  5. package/dist/builtins.d.ts.map +1 -1
  6. package/dist/builtins.js +56 -24
  7. package/dist/cel-environment.d.ts +12 -5
  8. package/dist/cel-environment.d.ts.map +1 -1
  9. package/dist/cel-environment.js +31 -17
  10. package/dist/definition-registry.d.ts +5 -5
  11. package/dist/definition-registry.d.ts.map +1 -1
  12. package/dist/definition-registry.js +10 -10
  13. package/dist/dependency-graph.d.ts.map +1 -1
  14. package/dist/dependency-graph.js +9 -2
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/kernel-globals.d.ts +6 -2
  19. package/dist/kernel-globals.d.ts.map +1 -1
  20. package/dist/kernel-globals.js +14 -8
  21. package/dist/manifest-loader.d.ts +9 -1
  22. package/dist/manifest-loader.d.ts.map +1 -1
  23. package/dist/manifest-loader.js +165 -11
  24. package/dist/module-kinds.d.ts +4 -0
  25. package/dist/module-kinds.d.ts.map +1 -0
  26. package/dist/module-kinds.js +4 -0
  27. package/dist/normalize-inline-resources.d.ts.map +1 -1
  28. package/dist/normalize-inline-resources.js +6 -1
  29. package/dist/precompile.d.ts +3 -2
  30. package/dist/precompile.d.ts.map +1 -1
  31. package/dist/precompile.js +13 -11
  32. package/dist/reference-field-map.d.ts +1 -1
  33. package/dist/resolve-throws-union.d.ts +30 -0
  34. package/dist/resolve-throws-union.d.ts.map +1 -0
  35. package/dist/resolve-throws-union.js +252 -0
  36. package/dist/types.d.ts +11 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/validate-cel-context.js +1 -1
  39. package/dist/validate-references.d.ts.map +1 -1
  40. package/dist/validate-references.js +19 -12
  41. package/dist/validate-throws-coverage.d.ts +8 -0
  42. package/dist/validate-throws-coverage.d.ts.map +1 -0
  43. package/dist/validate-throws-coverage.js +461 -0
  44. package/package.json +3 -3
  45. package/src/analyzer.ts +60 -26
  46. package/src/builtins.ts +56 -24
  47. package/src/cel-environment.ts +40 -17
  48. package/src/definition-registry.ts +10 -10
  49. package/src/dependency-graph.ts +9 -2
  50. package/src/index.ts +2 -1
  51. package/src/kernel-globals.ts +19 -10
  52. package/src/manifest-loader.ts +202 -17
  53. package/src/module-kinds.ts +6 -0
  54. package/src/normalize-inline-resources.ts +6 -1
  55. package/src/precompile.ts +14 -11
  56. package/src/reference-field-map.ts +1 -1
  57. package/src/resolve-throws-union.ts +345 -0
  58. package/src/types.ts +13 -0
  59. package/src/validate-cel-context.ts +1 -1
  60. package/src/validate-references.ts +19 -12
  61. package/src/validate-throws-coverage.ts +565 -0
  62. package/dist/adapters/node-adapter.d.ts +0 -15
  63. package/dist/adapters/node-adapter.d.ts.map +0 -1
  64. package/dist/adapters/node-adapter.js +0 -33
  65. package/src/adapters/node-adapter.ts +0 -38
@@ -0,0 +1,345 @@
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+
5
+ export interface ThrowsCodeMeta {
6
+ data?: Record<string, any>;
7
+ }
8
+
9
+ export interface ThrowsUnion {
10
+ /** Code → per-code metadata (data schema, etc). Keys are the declared codes. */
11
+ codes: Map<string, ThrowsCodeMeta>;
12
+ /** True when the union cannot be fully resolved statically — e.g. a
13
+ * `passthrough` call site uses a CEL expression the analyzer can't narrow,
14
+ * an unknown kind was encountered, or a cycle short-circuited resolution.
15
+ * Callers must treat unbounded unions as requiring a catch-all entry. */
16
+ unbounded: boolean;
17
+ }
18
+
19
+ export interface ResolveCtx {
20
+ allManifests: ResourceManifest[];
21
+ defs: DefinitionRegistry;
22
+ aliases: AliasResolver;
23
+ memo: Map<string, ThrowsUnion>;
24
+ inProgress: Set<string>;
25
+ }
26
+
27
+ export function createResolveCtx(
28
+ allManifests: ResourceManifest[],
29
+ defs: DefinitionRegistry,
30
+ aliases: AliasResolver,
31
+ ): ResolveCtx {
32
+ return {
33
+ allManifests,
34
+ defs,
35
+ aliases,
36
+ memo: new Map(),
37
+ inProgress: new Set(),
38
+ };
39
+ }
40
+
41
+ function emptyUnion(): ThrowsUnion {
42
+ return { codes: new Map(), unbounded: false };
43
+ }
44
+
45
+ function unionInto(target: ThrowsUnion, src: ThrowsUnion): void {
46
+ for (const [code, meta] of src.codes) {
47
+ if (!target.codes.has(code)) target.codes.set(code, meta);
48
+ }
49
+ if (src.unbounded) target.unbounded = true;
50
+ }
51
+
52
+ function definitionFor(
53
+ kind: string,
54
+ defs: DefinitionRegistry,
55
+ aliases: AliasResolver,
56
+ ): ResourceDefinition | undefined {
57
+ const resolved = aliases.resolveKind(kind);
58
+ return defs.resolve(kind) ?? (resolved ? defs.resolve(resolved) : undefined);
59
+ }
60
+
61
+ function codesFromDefinition(definition: ResourceDefinition): Map<string, ThrowsCodeMeta> {
62
+ const out = new Map<string, ThrowsCodeMeta>();
63
+ const raw = definition.throws?.codes ?? {};
64
+ for (const [code, meta] of Object.entries(raw)) {
65
+ out.set(code, { data: (meta as { data?: Record<string, any> }).data });
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Resolve the effective throw union for a named manifest. The result combines
71
+ * explicit `throws.codes`, `throws.inherit: true` dataflow (step-context
72
+ * traversal with try/catch subtraction), and unbounded markers for
73
+ * unresolvable passthrough call sites. Cycles short-circuit to an empty
74
+ * result so resolution always terminates. */
75
+ export function resolveThrowsUnion(
76
+ manifest: ResourceManifest,
77
+ ctx: ResolveCtx,
78
+ ): ThrowsUnion {
79
+ const name = manifest.metadata?.name as string | undefined;
80
+
81
+ if (name) {
82
+ const cached = ctx.memo.get(name);
83
+ if (cached) return cached;
84
+ if (ctx.inProgress.has(name)) return emptyUnion();
85
+ }
86
+
87
+ const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases);
88
+ if (!definition) {
89
+ const u: ThrowsUnion = { codes: new Map(), unbounded: true };
90
+ if (name) ctx.memo.set(name, u);
91
+ return u;
92
+ }
93
+
94
+ const throws = definition.throws;
95
+ if (!throws) {
96
+ const u = emptyUnion();
97
+ if (name) ctx.memo.set(name, u);
98
+ return u;
99
+ }
100
+
101
+ if (name) ctx.inProgress.add(name);
102
+ try {
103
+ const result: ThrowsUnion = { codes: new Map(), unbounded: false };
104
+
105
+ for (const [code, meta] of codesFromDefinition(definition)) {
106
+ result.codes.set(code, meta);
107
+ }
108
+
109
+ if (throws.passthrough) {
110
+ // Definition-level passthrough can't be resolved without a call site.
111
+ // resolveStepInvokeThrows handles passthrough call sites directly.
112
+ result.unbounded = true;
113
+ }
114
+
115
+ if (throws.inherit) {
116
+ const inherited = resolveInherited(manifest, definition, ctx);
117
+ unionInto(result, inherited);
118
+ }
119
+
120
+ if (name) ctx.memo.set(name, result);
121
+ return result;
122
+ } finally {
123
+ if (name) ctx.inProgress.delete(name);
124
+ }
125
+ }
126
+
127
+ function resolveInherited(
128
+ manifest: ResourceManifest,
129
+ definition: ResourceDefinition,
130
+ ctx: ResolveCtx,
131
+ ): ThrowsUnion {
132
+ const result: ThrowsUnion = { codes: new Map(), unbounded: false };
133
+ const props = definition.schema?.properties as Record<string, any> | undefined;
134
+ if (!props) return result;
135
+
136
+ for (const [fieldName, fieldSchema] of Object.entries(props)) {
137
+ const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
138
+ if (!stepCtx?.invoke) continue;
139
+ const steps = (manifest as Record<string, any>)[fieldName];
140
+ if (!Array.isArray(steps)) continue;
141
+ unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ function collectStepArrayThrows(
148
+ steps: unknown[],
149
+ invokeField: string,
150
+ enclosingTryCodes: Set<string> | undefined,
151
+ ctx: ResolveCtx,
152
+ ): ThrowsUnion {
153
+ const result = emptyUnion();
154
+ for (const step of steps) {
155
+ if (!step || typeof step !== "object") continue;
156
+ unionInto(
157
+ result,
158
+ collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx),
159
+ );
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /** Walk one step, dispatching by shape. Generic for any Run.Sequence-style
165
+ * composer: the step keys it recognises (`try` / `catch` / `finally` / `then`
166
+ * / `else` / `elseif` / `do` / `cases` / `default`) are the same set already
167
+ * traversed by the analyzer's `x-telo-step-context` schema builder, so future
168
+ * composers that reuse those shape conventions work without changes here. */
169
+ function collectStepThrows(
170
+ step: Record<string, any>,
171
+ invokeField: string,
172
+ enclosingTryCodes: Set<string> | undefined,
173
+ ctx: ResolveCtx,
174
+ ): ThrowsUnion {
175
+ if (step[invokeField]) {
176
+ return resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx);
177
+ }
178
+
179
+ if (step.throw && typeof step.throw === "object") {
180
+ return resolveThrowStepCode(step.throw as Record<string, any>, enclosingTryCodes);
181
+ }
182
+
183
+ if (Array.isArray(step.try)) {
184
+ const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
185
+ let propagated: ThrowsUnion;
186
+ if (Array.isArray(step.catch)) {
187
+ // Catch absorbs the try block's codes; the catch's own throws propagate
188
+ // out instead. Sequence-specific subtraction — the plan explicitly
189
+ // anchors this to Run.Sequence's try/catch schema shape.
190
+ const tryCodes = new Set(tryUnion.codes.keys());
191
+ propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
192
+ // Unbounded in the try block still signals the caller to expect
193
+ // arbitrary codes to flow through the catch (e.g. via passthrough).
194
+ if (tryUnion.unbounded) propagated.unbounded = true;
195
+ } else {
196
+ propagated = cloneUnion(tryUnion);
197
+ }
198
+ if (Array.isArray(step.finally)) {
199
+ unionInto(
200
+ propagated,
201
+ collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx),
202
+ );
203
+ }
204
+ return propagated;
205
+ }
206
+
207
+ if (Array.isArray(step.then)) {
208
+ const result = emptyUnion();
209
+ unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
210
+ if (Array.isArray(step.else)) {
211
+ unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
212
+ }
213
+ if (Array.isArray(step.elseif)) {
214
+ for (const branch of step.elseif) {
215
+ if (Array.isArray(branch?.then)) {
216
+ unionInto(
217
+ result,
218
+ collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx),
219
+ );
220
+ }
221
+ }
222
+ }
223
+ return result;
224
+ }
225
+
226
+ if (Array.isArray(step.do)) {
227
+ return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
228
+ }
229
+
230
+ if (step.cases && typeof step.cases === "object") {
231
+ const result = emptyUnion();
232
+ for (const arr of Object.values(step.cases as Record<string, unknown>)) {
233
+ if (Array.isArray(arr)) {
234
+ unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
235
+ }
236
+ }
237
+ if (Array.isArray(step.default)) {
238
+ unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
239
+ }
240
+ return result;
241
+ }
242
+
243
+ return emptyUnion();
244
+ }
245
+
246
+ function cloneUnion(u: ThrowsUnion): ThrowsUnion {
247
+ const out = emptyUnion();
248
+ for (const [c, m] of u.codes) out.codes.set(c, m);
249
+ out.unbounded = u.unbounded;
250
+ return out;
251
+ }
252
+
253
+ function resolveStepInvokeThrows(
254
+ step: Record<string, any>,
255
+ invokeField: string,
256
+ enclosingTryCodes: Set<string> | undefined,
257
+ ctx: ResolveCtx,
258
+ ): ThrowsUnion {
259
+ const invokeRef = step[invokeField];
260
+ if (!invokeRef || typeof invokeRef !== "object") return emptyUnion();
261
+ const invokedKind = invokeRef.kind as string | undefined;
262
+ if (!invokedKind) return emptyUnion();
263
+
264
+ const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases);
265
+ if (!definition) return { codes: new Map(), unbounded: true };
266
+
267
+ if (definition.throws?.passthrough) {
268
+ return resolvePassthroughAtCallSite(step, enclosingTryCodes);
269
+ }
270
+
271
+ // Named manifest: resolve the full chain (covers transitive inherit).
272
+ const invokeName = invokeRef.name as string | undefined;
273
+ if (invokeName) {
274
+ const target = ctx.allManifests.find(
275
+ (m) =>
276
+ m.metadata?.name === invokeName &&
277
+ (m.kind === invokedKind ||
278
+ ctx.aliases.resolveKind(m.kind) === invokedKind ||
279
+ m.kind === ctx.aliases.resolveKind(invokedKind)),
280
+ );
281
+ if (target) return resolveThrowsUnion(target, ctx);
282
+ }
283
+
284
+ // Fall back to the definition's own explicit codes. Mark unbounded when the
285
+ // definition depends on call-site or transitive resolution we couldn't
286
+ // perform (no specific target manifest to recurse into).
287
+ const codes = codesFromDefinition(definition);
288
+ const unbounded =
289
+ definition.throws?.inherit === true || definition.throws?.passthrough === true;
290
+ return { codes, unbounded };
291
+ }
292
+
293
+ /** Resolve a passthrough-style invocable at a specific call site. Recognised forms
294
+ * (see "passthrough: true" in the plan):
295
+ * - constant literal (no template) → `{ <literal> }`
296
+ * - `${{ 'FOO' }}` constant expression → `{ FOO }`
297
+ * - `${{ error.code }}` inside a catch → enclosing try's propagated union
298
+ * Anything else is unbounded; the analyzer flags it downstream. */
299
+ function resolvePassthroughAtCallSite(
300
+ step: Record<string, any>,
301
+ enclosingTryCodes: Set<string> | undefined,
302
+ ): ThrowsUnion {
303
+ return resolveCodeExpression(step.inputs?.code, enclosingTryCodes);
304
+ }
305
+
306
+ /** Resolve the `code:` of a `throw:` step to a throws union. Uses the same
307
+ * recognised forms as passthrough call sites. */
308
+ function resolveThrowStepCode(
309
+ throwSpec: Record<string, any>,
310
+ enclosingTryCodes: Set<string> | undefined,
311
+ ): ThrowsUnion {
312
+ return resolveCodeExpression(throwSpec.code, enclosingTryCodes);
313
+ }
314
+
315
+ function resolveCodeExpression(
316
+ codeInput: unknown,
317
+ enclosingTryCodes: Set<string> | undefined,
318
+ ): ThrowsUnion {
319
+ if (typeof codeInput !== "string" || codeInput.length === 0) {
320
+ return { codes: new Map(), unbounded: true };
321
+ }
322
+
323
+ const match = codeInput.match(/^\s*\$\{\{\s*([\s\S]+?)\s*\}\}\s*$/);
324
+ if (!match) {
325
+ return { codes: new Map([[codeInput, {}]]), unbounded: false };
326
+ }
327
+
328
+ const expr = match[1].trim();
329
+ const litMatch = expr.match(/^'([^']+)'$|^"([^"]+)"$/);
330
+ if (litMatch) {
331
+ const code = litMatch[1] ?? litMatch[2]!;
332
+ return { codes: new Map([[code, {}]]), unbounded: false };
333
+ }
334
+
335
+ if (expr === "error.code") {
336
+ if (enclosingTryCodes) {
337
+ const codes = new Map<string, ThrowsCodeMeta>();
338
+ for (const c of enclosingTryCodes) codes.set(c, {});
339
+ return { codes, unbounded: false };
340
+ }
341
+ return { codes: new Map(), unbounded: true };
342
+ }
343
+
344
+ return { codes: new Map(), unbounded: true };
345
+ }
package/src/types.ts CHANGED
@@ -45,6 +45,16 @@ export interface ManifestAdapter {
45
45
  supports(url: string): boolean;
46
46
  read(url: string): Promise<{ text: string; source: string }>;
47
47
  resolveRelative(base: string, relative: string): string;
48
+
49
+ /** Expand glob patterns relative to a base source. Returns sources in the same
50
+ * format as read().source — suitable to pass back into read() / resolveRelative().
51
+ * Optional — only filesystem-capable adapters implement this. */
52
+ expandGlob?(base: string, patterns: string[]): Promise<string[]>;
53
+
54
+ /** Walk parent directories from fileUrl looking for the nearest telo.yaml.
55
+ * Returns the source in the same format as read().source, or null if none found.
56
+ * Optional — only filesystem-capable adapters implement this. */
57
+ resolveOwnerOf?(fileUrl: string): Promise<string | null>;
48
58
  }
49
59
 
50
60
  export interface LoadOptions {
@@ -64,6 +74,9 @@ export interface LoaderInitOptions {
64
74
  includeRegistryAdapter?: boolean;
65
75
  /** Base URL used by built-in RegistryAdapter when enabled. */
66
76
  registryUrl?: string;
77
+ /** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
78
+ * omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
79
+ celHandlers?: import("./cel-environment.js").CelHandlers;
67
80
  }
68
81
 
69
82
  export interface AnalysisOptions {
@@ -13,7 +13,7 @@ export function resolveTypeFieldToSchema(
13
13
  if (!value) return undefined;
14
14
 
15
15
  if (typeof value === "string") {
16
- // Named type reference — find a Kernel.Type resource by name
16
+ // Named type reference — find a Telo.Type resource by name
17
17
  const typeManifest = allManifests.find(
18
18
  (m) =>
19
19
  (m.metadata as any)?.name === value &&
@@ -6,7 +6,12 @@ import type { AliasResolver } from "./alias-resolver.js";
6
6
  import type { DefinitionRegistry } from "./definition-registry.js";
7
7
 
8
8
  const SOURCE = "telo-analyzer";
9
- const SYSTEM_KINDS = new Set(["Kernel.Definition", "Kernel.Abstract"]);
9
+ /** Kinds skipped by reference validation. Telo.Application and Telo.Library
10
+ * are intentionally not here: Application has `targets` with x-telo-ref that
11
+ * must be validated, and Library has no ref-bearing fields so flows through
12
+ * harmlessly. Telo.Import is also not here for the same reason — its
13
+ * `source` field isn't x-telo-ref, so nothing gets checked. */
14
+ const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
10
15
 
11
16
  /**
12
17
  * Checks whether `kind` satisfies the ref constraint in `entry`.
@@ -27,7 +32,7 @@ function checkKind(
27
32
  if (!targetKind) return [];
28
33
  const targetDef = registry.resolve(targetKind);
29
34
  if (!targetDef) return [];
30
- if (targetDef.kind === "Kernel.Abstract") {
35
+ if (targetDef.kind === "Telo.Abstract") {
31
36
  const implementing = registry.getByExtends(targetKind);
32
37
  if (implementing.length === 0) return []; // partial context — no implementations loaded yet
33
38
  const implementingKinds = new Set(
@@ -71,7 +76,7 @@ export function validateReferences(
71
76
  if (!aliases || !registry) return diagnostics;
72
77
 
73
78
  // Build outer resource lookup by name for resolution check.
74
- // Exclude system kinds (Kernel.Definition) — they are type blueprints, not instances,
79
+ // Exclude system kinds (Telo.Definition) — they are type blueprints, not instances,
75
80
  // and their names (e.g. "Server", "Job") would shadow user-defined resource instances.
76
81
  const byName = new Map<string, ResourceManifest>();
77
82
  for (const r of resources) {
@@ -86,6 +91,7 @@ export function validateReferences(
86
91
 
87
92
  const resourceLabel = `${r.kind}/${r.metadata.name as string}`;
88
93
  const resourceData = { kind: r.kind, name: r.metadata.name as string };
94
+ const filePath = (r.metadata as { source?: string } | undefined)?.source;
89
95
 
90
96
  // Collect scope visibility prefixes (JSON Pointer → dot prefix) and their manifests.
91
97
  // scope field path → flat array of ResourceManifest declared in that scope.
@@ -155,7 +161,7 @@ export function validateReferences(
155
161
  code: "UNRESOLVED_REFERENCE",
156
162
  source: SOURCE,
157
163
  message: `${resourceLabel}: reference at '${fieldPath}' → resource '${val}' not found`,
158
- data: { resource: resourceData, path: fieldPath },
164
+ data: { resource: resourceData, filePath, path: fieldPath },
159
165
  });
160
166
  continue;
161
167
  }
@@ -166,7 +172,7 @@ export function validateReferences(
166
172
  code: "REFERENCE_KIND_MISMATCH",
167
173
  source: SOURCE,
168
174
  message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
169
- data: { resource: resourceData, path: fieldPath },
175
+ data: { resource: resourceData, filePath, path: fieldPath },
170
176
  });
171
177
  }
172
178
  continue;
@@ -185,7 +191,7 @@ export function validateReferences(
185
191
  code: "INVALID_REFERENCE",
186
192
  source: SOURCE,
187
193
  message: `${resourceLabel}: reference at '${fieldPath}' must have string 'kind' and 'name' fields`,
188
- data: { resource: resourceData, path: fieldPath },
194
+ data: { resource: resourceData, filePath, path: fieldPath },
189
195
  });
190
196
  continue;
191
197
  }
@@ -198,7 +204,7 @@ export function validateReferences(
198
204
  code: "REFERENCE_KIND_MISMATCH",
199
205
  source: SOURCE,
200
206
  message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
201
- data: { resource: resourceData, path: fieldPath },
207
+ data: { resource: resourceData, filePath, path: fieldPath },
202
208
  });
203
209
  }
204
210
 
@@ -212,7 +218,7 @@ export function validateReferences(
212
218
  code: "UNRESOLVED_REFERENCE",
213
219
  source: SOURCE,
214
220
  message: `${resourceLabel}: reference at '${fieldPath}' → resource '${refVal.name}' not found`,
215
- data: { resource: resourceData, path: fieldPath },
221
+ data: { resource: resourceData, filePath, path: fieldPath },
216
222
  });
217
223
  }
218
224
  }
@@ -231,6 +237,7 @@ export function validateReferences(
231
237
 
232
238
  const resourceLabel = `${r.kind}/${r.metadata.name as string}`;
233
239
  const resourceData = { kind: r.kind, name: r.metadata.name as string };
240
+ const filePath = (r.metadata as { source?: string } | undefined)?.source;
234
241
 
235
242
  for (const [fieldPath, entry] of fieldMap) {
236
243
  if (!isSchemaFromEntry(entry)) continue;
@@ -245,7 +252,7 @@ export function validateReferences(
245
252
  code: "INVALID_SCHEMA_FROM",
246
253
  source: SOURCE,
247
254
  message: `${resourceLabel}: x-telo-schema-from "${schemaFrom}" must contain at least one "/" to separate anchor from JSON Pointer`,
248
- data: { resource: resourceData, path: fieldPath },
255
+ data: { resource: resourceData, filePath, path: fieldPath },
249
256
  });
250
257
  continue;
251
258
  }
@@ -288,7 +295,7 @@ export function validateReferences(
288
295
  code: "SCHEMA_FROM_MISSING_PATH",
289
296
  source: SOURCE,
290
297
  message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema`,
291
- data: { resource: resourceData, path: fieldPath },
298
+ data: { resource: resourceData, filePath, path: fieldPath },
292
299
  });
293
300
  continue;
294
301
  }
@@ -300,7 +307,7 @@ export function validateReferences(
300
307
  code: "SCHEMA_FROM_MISSING_PATH",
301
308
  source: SOURCE,
302
309
  message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
303
- data: { resource: resourceData, path: fieldPath },
310
+ data: { resource: resourceData, filePath, path: fieldPath },
304
311
  });
305
312
  continue;
306
313
  }
@@ -312,7 +319,7 @@ export function validateReferences(
312
319
  code: "DEPENDENT_SCHEMA_MISMATCH",
313
320
  source: SOURCE,
314
321
  message: `${resourceLabel}: '${fieldPath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
315
- data: { resource: resourceData, path: fieldPath },
322
+ data: { resource: resourceData, filePath, path: fieldPath },
316
323
  });
317
324
  }
318
325
  }