@telorun/analyzer 0.11.0 → 1.1.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 (48) hide show
  1. package/README.md +3 -3
  2. package/dist/analysis-registry.d.ts +7 -0
  3. package/dist/analysis-registry.d.ts.map +1 -1
  4. package/dist/analysis-registry.js +38 -0
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +44 -9
  7. package/dist/builtins.d.ts.map +1 -1
  8. package/dist/builtins.js +44 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/kernel-globals.d.ts.map +1 -1
  13. package/dist/kernel-globals.js +9 -11
  14. package/dist/normalize-inline-resources.d.ts.map +1 -1
  15. package/dist/normalize-inline-resources.js +26 -14
  16. package/dist/position-metadata.d.ts +5 -1
  17. package/dist/position-metadata.d.ts.map +1 -1
  18. package/dist/position-metadata.js +8 -1
  19. package/dist/reference-field-map.d.ts +21 -4
  20. package/dist/reference-field-map.d.ts.map +1 -1
  21. package/dist/reference-field-map.js +35 -19
  22. package/dist/residual-schema.d.ts +23 -0
  23. package/dist/residual-schema.d.ts.map +1 -0
  24. package/dist/residual-schema.js +45 -0
  25. package/dist/rewrite-synthetic-origins.d.ts +10 -0
  26. package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
  27. package/dist/rewrite-synthetic-origins.js +55 -0
  28. package/dist/validate-cel-context.d.ts +5 -0
  29. package/dist/validate-cel-context.d.ts.map +1 -1
  30. package/dist/validate-cel-context.js +27 -15
  31. package/dist/validate-provider-coherence.d.ts +23 -0
  32. package/dist/validate-provider-coherence.d.ts.map +1 -0
  33. package/dist/validate-provider-coherence.js +148 -0
  34. package/dist/validate-references.js +24 -24
  35. package/package.json +5 -3
  36. package/src/analysis-registry.ts +37 -0
  37. package/src/analyzer.ts +45 -11
  38. package/src/builtins.ts +44 -1
  39. package/src/index.ts +1 -0
  40. package/src/kernel-globals.ts +9 -11
  41. package/src/normalize-inline-resources.ts +48 -13
  42. package/src/position-metadata.ts +8 -1
  43. package/src/reference-field-map.ts +46 -18
  44. package/src/residual-schema.ts +49 -0
  45. package/src/rewrite-synthetic-origins.ts +75 -0
  46. package/src/validate-cel-context.ts +28 -15
  47. package/src/validate-provider-coherence.ts +166 -0
  48. package/src/validate-references.ts +24 -24
@@ -0,0 +1,166 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
5
+
6
+ const SOURCE = "telo-analyzer";
7
+
8
+ /**
9
+ * Validates coherence rules for `Telo.Definition` documents that use the `provide:`
10
+ * template target, plus the implementation-presence rule on `Telo.Provider`
11
+ * definitions.
12
+ *
13
+ * Diagnostics:
14
+ * - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
15
+ * `capability` is not `Telo.Provider`.
16
+ * - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
17
+ * on the same definition.
18
+ * - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
19
+ * `resources:`.
20
+ * - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
21
+ * kind is registered but not a `Telo.Invocable`.
22
+ * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
23
+ * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
24
+ */
25
+ export function validateProviderCoherence(
26
+ manifests: ResourceManifest[],
27
+ registry: DefinitionRegistry,
28
+ aliases: AliasResolver,
29
+ ): AnalysisDiagnostic[] {
30
+ const diagnostics: AnalysisDiagnostic[] = [];
31
+
32
+ const importedModules = new Set<string>();
33
+ for (const m of manifests) {
34
+ if (m.kind !== "Telo.Import") continue;
35
+ const resolved = (m.metadata as { resolvedModuleName?: string } | undefined)
36
+ ?.resolvedModuleName;
37
+ if (resolved) importedModules.add(resolved);
38
+ }
39
+
40
+ for (const m of manifests) {
41
+ if (m.kind !== "Telo.Definition") continue;
42
+ const name = m.metadata?.name as string | undefined;
43
+ if (!name) continue;
44
+ const ownModule = (m.metadata as { module?: string } | undefined)?.module;
45
+ if (ownModule && importedModules.has(ownModule)) continue;
46
+ const filePath = (m.metadata as { source?: string } | undefined)?.source;
47
+ const resource = { kind: m.kind, name };
48
+ const label = `${m.kind}/${name}`;
49
+
50
+ const md = m as Record<string, unknown>;
51
+ const capability = typeof md.capability === "string" ? md.capability : undefined;
52
+ const provide = md.provide;
53
+ const invoke = md.invoke;
54
+ const run = md.run;
55
+ const controllers = md.controllers;
56
+ const resources = md.resources;
57
+
58
+ const hasProvide = provide !== undefined && provide !== null;
59
+ const hasInvoke = invoke !== undefined && invoke !== null;
60
+ const hasRun = run !== undefined && run !== null;
61
+ const hasControllers = Array.isArray(controllers) && controllers.length > 0;
62
+
63
+ if (hasProvide && capability !== "Telo.Provider") {
64
+ diagnostics.push({
65
+ severity: DiagnosticSeverity.Error,
66
+ code: "PROVIDE_ON_NON_PROVIDER",
67
+ source: SOURCE,
68
+ message:
69
+ `${label}: 'provide:' is only valid on definitions with 'capability: Telo.Provider' ` +
70
+ `(found '${capability ?? "<unset>"}'). Use 'invoke:' or 'run:' for other capabilities.`,
71
+ data: { resource, filePath, path: "provide" },
72
+ });
73
+ }
74
+
75
+ if (hasProvide && (hasInvoke || hasRun)) {
76
+ const conflict = hasInvoke ? "invoke" : "run";
77
+ diagnostics.push({
78
+ severity: DiagnosticSeverity.Error,
79
+ code: "PROVIDE_DISPATCHER_CONFLICT",
80
+ source: SOURCE,
81
+ message:
82
+ `${label}: 'provide:' cannot co-exist with '${conflict}:'. ` +
83
+ `A definition declares exactly one dispatch entry-point.`,
84
+ data: { resource, filePath, path: "provide" },
85
+ });
86
+ }
87
+
88
+ if (hasProvide && typeof provide === "object" && !Array.isArray(provide)) {
89
+ const provideObj = provide as { kind?: unknown; name?: unknown };
90
+ const providedName = typeof provideObj.name === "string" ? provideObj.name : undefined;
91
+ const providedKind = typeof provideObj.kind === "string" ? provideObj.kind : undefined;
92
+ if (providedName && Array.isArray(resources)) {
93
+ const match = resources.find((r) => {
94
+ const meta = (r as { metadata?: { name?: unknown } })?.metadata;
95
+ return typeof meta?.name === "string" && meta.name === providedName;
96
+ }) as { kind?: unknown } | undefined;
97
+ if (!match) {
98
+ diagnostics.push({
99
+ severity: DiagnosticSeverity.Error,
100
+ code: "PROVIDE_TARGET_UNKNOWN",
101
+ source: SOURCE,
102
+ message:
103
+ `${label}: 'provide.name: ${providedName}' does not match any entry's ` +
104
+ `metadata.name in 'resources:'.`,
105
+ data: { resource, filePath, path: "provide.name" },
106
+ });
107
+ } else if (typeof match.kind === "string") {
108
+ // `provide.kind` is the type contract the analyzer uses to type
109
+ // `result` CEL against the target's `outputType`. The runtime
110
+ // dispatches on `provide.name` and ignores `provide.kind`, so a
111
+ // mismatch silently degrades `result` typing to an open schema
112
+ // (and at runtime quietly invokes the actually-matched resource).
113
+ // Flag the divergence so result-typing never lies.
114
+ if (providedKind) {
115
+ const providedCanonical = aliases.resolveKind(providedKind) ?? providedKind;
116
+ const matchCanonical = aliases.resolveKind(match.kind) ?? match.kind;
117
+ if (providedCanonical !== matchCanonical) {
118
+ diagnostics.push({
119
+ severity: DiagnosticSeverity.Error,
120
+ code: "PROVIDE_KIND_MISMATCH",
121
+ source: SOURCE,
122
+ message:
123
+ `${label}: 'provide.kind: ${providedKind}' disagrees with the matched ` +
124
+ `'resources:' entry's kind '${match.kind}' (matched by metadata.name ` +
125
+ `'${providedName}'). The runtime dispatches by name, so 'provide.kind' ` +
126
+ `is decorative — but the analyzer types 'result:' against it, and a ` +
127
+ `mismatch silently turns off that typing.`,
128
+ data: { resource, filePath, path: "provide.kind" },
129
+ });
130
+ }
131
+ }
132
+ const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
133
+ const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
134
+ if (targetDef && targetDef.kind === "Telo.Definition") {
135
+ const targetCap = (targetDef as { capability?: unknown }).capability;
136
+ if (typeof targetCap === "string" && targetCap !== "Telo.Invocable") {
137
+ diagnostics.push({
138
+ severity: DiagnosticSeverity.Error,
139
+ code: "PROVIDE_TARGET_NOT_INVOCABLE",
140
+ source: SOURCE,
141
+ message:
142
+ `${label}: 'provide.name: ${providedName}' resolves to a ${match.kind} ` +
143
+ `(capability '${targetCap}'); 'provide:' requires a Telo.Invocable target.`,
144
+ data: { resource, filePath, path: "provide.name" },
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
153
+ diagnostics.push({
154
+ severity: DiagnosticSeverity.Error,
155
+ code: "PROVIDER_MISSING_IMPLEMENTATION",
156
+ source: SOURCE,
157
+ message:
158
+ `${label}: 'capability: Telo.Provider' requires either 'controllers:' ` +
159
+ `(TS-backed) or 'provide:' (template-backed) to declare an implementation.`,
160
+ data: { resource, filePath, path: "capability" },
161
+ });
162
+ }
163
+ }
164
+
165
+ return diagnostics;
166
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
- import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldValues, type RefFieldEntry } from "./reference-field-map.js";
2
+ import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues, type RefFieldEntry } from "./reference-field-map.js";
3
3
  import { navigateJsonPointer } from "./schema-compat.js";
4
4
  import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisContext } from "./types.js";
5
5
  import type { AliasResolver } from "./alias-resolver.js";
@@ -142,7 +142,7 @@ export function validateReferences(
142
142
  }
143
143
  }
144
144
 
145
- for (const val of resolveFieldValues(r, fieldPath)) {
145
+ for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
146
146
  if (!val) continue;
147
147
 
148
148
  // Name-only reference (plain string) — look up by name to validate.
@@ -166,8 +166,8 @@ export function validateReferences(
166
166
  severity: DiagnosticSeverity.Error,
167
167
  code: "UNRESOLVED_REFERENCE",
168
168
  source: SOURCE,
169
- message: `${resourceLabel}: reference at '${fieldPath}' → resource '${val}' not found`,
170
- data: { resource: resourceData, filePath, path: fieldPath },
169
+ message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
170
+ data: { resource: resourceData, filePath, path: concretePath },
171
171
  });
172
172
  continue;
173
173
  }
@@ -177,8 +177,8 @@ export function validateReferences(
177
177
  severity: DiagnosticSeverity.Error,
178
178
  code: "REFERENCE_KIND_MISMATCH",
179
179
  source: SOURCE,
180
- message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
181
- data: { resource: resourceData, filePath, path: fieldPath },
180
+ message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
181
+ data: { resource: resourceData, filePath, path: concretePath },
182
182
  });
183
183
  }
184
184
  continue;
@@ -196,8 +196,8 @@ export function validateReferences(
196
196
  severity: DiagnosticSeverity.Error,
197
197
  code: "INVALID_REFERENCE",
198
198
  source: SOURCE,
199
- message: `${resourceLabel}: reference at '${fieldPath}' must have string 'kind' and 'name' fields`,
200
- data: { resource: resourceData, filePath, path: fieldPath },
199
+ message: `${resourceLabel}: reference at '${concretePath}' must have string 'kind' and 'name' fields`,
200
+ data: { resource: resourceData, filePath, path: concretePath },
201
201
  });
202
202
  continue;
203
203
  }
@@ -209,8 +209,8 @@ export function validateReferences(
209
209
  severity: DiagnosticSeverity.Error,
210
210
  code: "REFERENCE_KIND_MISMATCH",
211
211
  source: SOURCE,
212
- message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
213
- data: { resource: resourceData, filePath, path: fieldPath },
212
+ message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
213
+ data: { resource: resourceData, filePath, path: concretePath },
214
214
  });
215
215
  }
216
216
 
@@ -223,8 +223,8 @@ export function validateReferences(
223
223
  severity: DiagnosticSeverity.Error,
224
224
  code: "UNRESOLVED_REFERENCE",
225
225
  source: SOURCE,
226
- message: `${resourceLabel}: reference at '${fieldPath}' → resource '${refVal.name}' not found`,
227
- data: { resource: resourceData, filePath, path: fieldPath },
226
+ message: `${resourceLabel}: reference at '${concretePath}' → resource '${refVal.name}' not found`,
227
+ data: { resource: resourceData, filePath, path: concretePath },
228
228
  });
229
229
  }
230
230
  }
@@ -317,7 +317,7 @@ export function validateReferences(
317
317
  continue;
318
318
  }
319
319
 
320
- for (const fieldValue of resolveFieldValues(r, fieldPath)) {
320
+ for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
321
321
  if (fieldValue == null) continue;
322
322
  const issues = registry.validateWithRefs(fieldValue, subSchema as Record<string, any>);
323
323
  for (const issue of issues) {
@@ -325,8 +325,8 @@ export function validateReferences(
325
325
  severity: DiagnosticSeverity.Error,
326
326
  code: "DEPENDENT_SCHEMA_MISMATCH",
327
327
  source: SOURCE,
328
- message: `${resourceLabel}: '${fieldPath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
329
- data: { resource: resourceData, filePath, path: fieldPath },
328
+ message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
329
+ data: { resource: resourceData, filePath, path: concretePath },
330
330
  });
331
331
  }
332
332
  }
@@ -347,10 +347,10 @@ export function validateReferences(
347
347
  const anchorValues = resolveFieldValues(r, anchorPath);
348
348
  if (anchorValues.length === 0) continue; // anchor field not set — nothing to validate
349
349
 
350
- const fieldValues = resolveFieldValues(r, fieldPath);
350
+ const fieldEntries = resolveFieldEntries(r, fieldPath);
351
351
 
352
- for (let i = 0; i < fieldValues.length; i++) {
353
- const fieldValue = fieldValues[i];
352
+ for (let i = 0; i < fieldEntries.length; i++) {
353
+ const { value: fieldValue, path: concretePath } = fieldEntries[i];
354
354
  if (fieldValue == null) continue;
355
355
 
356
356
  // For absolute paths, the single anchor applies to all field values.
@@ -367,8 +367,8 @@ export function validateReferences(
367
367
  severity: DiagnosticSeverity.Error,
368
368
  code: "SCHEMA_FROM_MISSING_PATH",
369
369
  source: SOURCE,
370
- message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema`,
371
- data: { resource: resourceData, filePath, path: fieldPath },
370
+ message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema`,
371
+ data: { resource: resourceData, filePath, path: concretePath },
372
372
  });
373
373
  continue;
374
374
  }
@@ -379,8 +379,8 @@ export function validateReferences(
379
379
  severity: DiagnosticSeverity.Error,
380
380
  code: "SCHEMA_FROM_MISSING_PATH",
381
381
  source: SOURCE,
382
- message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
383
- data: { resource: resourceData, filePath, path: fieldPath },
382
+ message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
383
+ data: { resource: resourceData, filePath, path: concretePath },
384
384
  });
385
385
  continue;
386
386
  }
@@ -391,8 +391,8 @@ export function validateReferences(
391
391
  severity: DiagnosticSeverity.Error,
392
392
  code: "DEPENDENT_SCHEMA_MISMATCH",
393
393
  source: SOURCE,
394
- message: `${resourceLabel}: '${fieldPath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
395
- data: { resource: resourceData, filePath, path: fieldPath },
394
+ message: `${resourceLabel}: '${concretePath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
395
+ data: { resource: resourceData, filePath, path: concretePath },
396
396
  });
397
397
  }
398
398
  }