@telorun/analyzer 0.10.1 → 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 +198 -6
  7. package/dist/builtins.d.ts.map +1 -1
  8. package/dist/builtins.js +158 -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 +61 -7
  29. package/dist/validate-cel-context.d.ts.map +1 -1
  30. package/dist/validate-cel-context.js +90 -8
  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 +240 -8
  38. package/src/builtins.ts +158 -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 +111 -8
  47. package/src/validate-provider-coherence.ts +166 -0
  48. package/src/validate-references.ts +24 -24
@@ -0,0 +1,23 @@
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 { type AnalysisDiagnostic } from "./types.js";
5
+ /**
6
+ * Validates coherence rules for `Telo.Definition` documents that use the `provide:`
7
+ * template target, plus the implementation-presence rule on `Telo.Provider`
8
+ * definitions.
9
+ *
10
+ * Diagnostics:
11
+ * - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
12
+ * `capability` is not `Telo.Provider`.
13
+ * - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
14
+ * on the same definition.
15
+ * - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
16
+ * `resources:`.
17
+ * - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
18
+ * kind is registered but not a `Telo.Invocable`.
19
+ * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
20
+ * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
21
+ */
22
+ export declare function validateProviderCoherence(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
23
+ //# sourceMappingURL=validate-provider-coherence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-provider-coherence.d.ts","sourceRoot":"","sources":["../src/validate-provider-coherence.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;AACnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CAyItB"}
@@ -0,0 +1,148 @@
1
+ import { DiagnosticSeverity } from "./types.js";
2
+ const SOURCE = "telo-analyzer";
3
+ /**
4
+ * Validates coherence rules for `Telo.Definition` documents that use the `provide:`
5
+ * template target, plus the implementation-presence rule on `Telo.Provider`
6
+ * definitions.
7
+ *
8
+ * Diagnostics:
9
+ * - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
10
+ * `capability` is not `Telo.Provider`.
11
+ * - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
12
+ * on the same definition.
13
+ * - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
14
+ * `resources:`.
15
+ * - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
16
+ * kind is registered but not a `Telo.Invocable`.
17
+ * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
18
+ * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
19
+ */
20
+ export function validateProviderCoherence(manifests, registry, aliases) {
21
+ const diagnostics = [];
22
+ const importedModules = new Set();
23
+ for (const m of manifests) {
24
+ if (m.kind !== "Telo.Import")
25
+ continue;
26
+ const resolved = m.metadata
27
+ ?.resolvedModuleName;
28
+ if (resolved)
29
+ importedModules.add(resolved);
30
+ }
31
+ for (const m of manifests) {
32
+ if (m.kind !== "Telo.Definition")
33
+ continue;
34
+ const name = m.metadata?.name;
35
+ if (!name)
36
+ continue;
37
+ const ownModule = m.metadata?.module;
38
+ if (ownModule && importedModules.has(ownModule))
39
+ continue;
40
+ const filePath = m.metadata?.source;
41
+ const resource = { kind: m.kind, name };
42
+ const label = `${m.kind}/${name}`;
43
+ const md = m;
44
+ const capability = typeof md.capability === "string" ? md.capability : undefined;
45
+ const provide = md.provide;
46
+ const invoke = md.invoke;
47
+ const run = md.run;
48
+ const controllers = md.controllers;
49
+ const resources = md.resources;
50
+ const hasProvide = provide !== undefined && provide !== null;
51
+ const hasInvoke = invoke !== undefined && invoke !== null;
52
+ const hasRun = run !== undefined && run !== null;
53
+ const hasControllers = Array.isArray(controllers) && controllers.length > 0;
54
+ if (hasProvide && capability !== "Telo.Provider") {
55
+ diagnostics.push({
56
+ severity: DiagnosticSeverity.Error,
57
+ code: "PROVIDE_ON_NON_PROVIDER",
58
+ source: SOURCE,
59
+ message: `${label}: 'provide:' is only valid on definitions with 'capability: Telo.Provider' ` +
60
+ `(found '${capability ?? "<unset>"}'). Use 'invoke:' or 'run:' for other capabilities.`,
61
+ data: { resource, filePath, path: "provide" },
62
+ });
63
+ }
64
+ if (hasProvide && (hasInvoke || hasRun)) {
65
+ const conflict = hasInvoke ? "invoke" : "run";
66
+ diagnostics.push({
67
+ severity: DiagnosticSeverity.Error,
68
+ code: "PROVIDE_DISPATCHER_CONFLICT",
69
+ source: SOURCE,
70
+ message: `${label}: 'provide:' cannot co-exist with '${conflict}:'. ` +
71
+ `A definition declares exactly one dispatch entry-point.`,
72
+ data: { resource, filePath, path: "provide" },
73
+ });
74
+ }
75
+ if (hasProvide && typeof provide === "object" && !Array.isArray(provide)) {
76
+ const provideObj = provide;
77
+ const providedName = typeof provideObj.name === "string" ? provideObj.name : undefined;
78
+ const providedKind = typeof provideObj.kind === "string" ? provideObj.kind : undefined;
79
+ if (providedName && Array.isArray(resources)) {
80
+ const match = resources.find((r) => {
81
+ const meta = r?.metadata;
82
+ return typeof meta?.name === "string" && meta.name === providedName;
83
+ });
84
+ if (!match) {
85
+ diagnostics.push({
86
+ severity: DiagnosticSeverity.Error,
87
+ code: "PROVIDE_TARGET_UNKNOWN",
88
+ source: SOURCE,
89
+ message: `${label}: 'provide.name: ${providedName}' does not match any entry's ` +
90
+ `metadata.name in 'resources:'.`,
91
+ data: { resource, filePath, path: "provide.name" },
92
+ });
93
+ }
94
+ else if (typeof match.kind === "string") {
95
+ // `provide.kind` is the type contract the analyzer uses to type
96
+ // `result` CEL against the target's `outputType`. The runtime
97
+ // dispatches on `provide.name` and ignores `provide.kind`, so a
98
+ // mismatch silently degrades `result` typing to an open schema
99
+ // (and at runtime quietly invokes the actually-matched resource).
100
+ // Flag the divergence so result-typing never lies.
101
+ if (providedKind) {
102
+ const providedCanonical = aliases.resolveKind(providedKind) ?? providedKind;
103
+ const matchCanonical = aliases.resolveKind(match.kind) ?? match.kind;
104
+ if (providedCanonical !== matchCanonical) {
105
+ diagnostics.push({
106
+ severity: DiagnosticSeverity.Error,
107
+ code: "PROVIDE_KIND_MISMATCH",
108
+ source: SOURCE,
109
+ message: `${label}: 'provide.kind: ${providedKind}' disagrees with the matched ` +
110
+ `'resources:' entry's kind '${match.kind}' (matched by metadata.name ` +
111
+ `'${providedName}'). The runtime dispatches by name, so 'provide.kind' ` +
112
+ `is decorative — but the analyzer types 'result:' against it, and a ` +
113
+ `mismatch silently turns off that typing.`,
114
+ data: { resource, filePath, path: "provide.kind" },
115
+ });
116
+ }
117
+ }
118
+ const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
119
+ const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
120
+ if (targetDef && targetDef.kind === "Telo.Definition") {
121
+ const targetCap = targetDef.capability;
122
+ if (typeof targetCap === "string" && targetCap !== "Telo.Invocable") {
123
+ diagnostics.push({
124
+ severity: DiagnosticSeverity.Error,
125
+ code: "PROVIDE_TARGET_NOT_INVOCABLE",
126
+ source: SOURCE,
127
+ message: `${label}: 'provide.name: ${providedName}' resolves to a ${match.kind} ` +
128
+ `(capability '${targetCap}'); 'provide:' requires a Telo.Invocable target.`,
129
+ data: { resource, filePath, path: "provide.name" },
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
137
+ diagnostics.push({
138
+ severity: DiagnosticSeverity.Error,
139
+ code: "PROVIDER_MISSING_IMPLEMENTATION",
140
+ source: SOURCE,
141
+ message: `${label}: 'capability: Telo.Provider' requires either 'controllers:' ` +
142
+ `(TS-backed) or 'provide:' (template-backed) to declare an implementation.`,
143
+ data: { resource, filePath, path: "capability" },
144
+ });
145
+ }
146
+ }
147
+ return diagnostics;
148
+ }
@@ -1,4 +1,4 @@
1
- import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldValues } from "./reference-field-map.js";
1
+ import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
2
2
  import { navigateJsonPointer } from "./schema-compat.js";
3
3
  import { DiagnosticSeverity } from "./types.js";
4
4
  const SOURCE = "telo-analyzer";
@@ -119,7 +119,7 @@ export function validateReferences(resources, context) {
119
119
  }
120
120
  }
121
121
  }
122
- for (const val of resolveFieldValues(r, fieldPath)) {
122
+ for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
123
123
  if (!val)
124
124
  continue;
125
125
  // Name-only reference (plain string) — look up by name to validate.
@@ -142,8 +142,8 @@ export function validateReferences(resources, context) {
142
142
  severity: DiagnosticSeverity.Error,
143
143
  code: "UNRESOLVED_REFERENCE",
144
144
  source: SOURCE,
145
- message: `${resourceLabel}: reference at '${fieldPath}' → resource '${val}' not found`,
146
- data: { resource: resourceData, filePath, path: fieldPath },
145
+ message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
146
+ data: { resource: resourceData, filePath, path: concretePath },
147
147
  });
148
148
  continue;
149
149
  }
@@ -153,8 +153,8 @@ export function validateReferences(resources, context) {
153
153
  severity: DiagnosticSeverity.Error,
154
154
  code: "REFERENCE_KIND_MISMATCH",
155
155
  source: SOURCE,
156
- message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
157
- data: { resource: resourceData, filePath, path: fieldPath },
156
+ message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
157
+ data: { resource: resourceData, filePath, path: concretePath },
158
158
  });
159
159
  }
160
160
  continue;
@@ -171,8 +171,8 @@ export function validateReferences(resources, context) {
171
171
  severity: DiagnosticSeverity.Error,
172
172
  code: "INVALID_REFERENCE",
173
173
  source: SOURCE,
174
- message: `${resourceLabel}: reference at '${fieldPath}' must have string 'kind' and 'name' fields`,
175
- data: { resource: resourceData, filePath, path: fieldPath },
174
+ message: `${resourceLabel}: reference at '${concretePath}' must have string 'kind' and 'name' fields`,
175
+ data: { resource: resourceData, filePath, path: concretePath },
176
176
  });
177
177
  continue;
178
178
  }
@@ -183,8 +183,8 @@ export function validateReferences(resources, context) {
183
183
  severity: DiagnosticSeverity.Error,
184
184
  code: "REFERENCE_KIND_MISMATCH",
185
185
  source: SOURCE,
186
- message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
187
- data: { resource: resourceData, filePath, path: fieldPath },
186
+ message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
187
+ data: { resource: resourceData, filePath, path: concretePath },
188
188
  });
189
189
  }
190
190
  // 3. Resolution check — resource with this name must exist.
@@ -195,8 +195,8 @@ export function validateReferences(resources, context) {
195
195
  severity: DiagnosticSeverity.Error,
196
196
  code: "UNRESOLVED_REFERENCE",
197
197
  source: SOURCE,
198
- message: `${resourceLabel}: reference at '${fieldPath}' → resource '${refVal.name}' not found`,
199
- data: { resource: resourceData, filePath, path: fieldPath },
198
+ message: `${resourceLabel}: reference at '${concretePath}' → resource '${refVal.name}' not found`,
199
+ data: { resource: resourceData, filePath, path: concretePath },
200
200
  });
201
201
  }
202
202
  }
@@ -279,7 +279,7 @@ export function validateReferences(resources, context) {
279
279
  });
280
280
  continue;
281
281
  }
282
- for (const fieldValue of resolveFieldValues(r, fieldPath)) {
282
+ for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
283
283
  if (fieldValue == null)
284
284
  continue;
285
285
  const issues = registry.validateWithRefs(fieldValue, subSchema);
@@ -288,8 +288,8 @@ export function validateReferences(resources, context) {
288
288
  severity: DiagnosticSeverity.Error,
289
289
  code: "DEPENDENT_SCHEMA_MISMATCH",
290
290
  source: SOURCE,
291
- message: `${resourceLabel}: '${fieldPath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
292
- data: { resource: resourceData, filePath, path: fieldPath },
291
+ message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
292
+ data: { resource: resourceData, filePath, path: concretePath },
293
293
  });
294
294
  }
295
295
  }
@@ -309,9 +309,9 @@ export function validateReferences(resources, context) {
309
309
  const anchorValues = resolveFieldValues(r, anchorPath);
310
310
  if (anchorValues.length === 0)
311
311
  continue; // anchor field not set — nothing to validate
312
- const fieldValues = resolveFieldValues(r, fieldPath);
313
- for (let i = 0; i < fieldValues.length; i++) {
314
- const fieldValue = fieldValues[i];
312
+ const fieldEntries = resolveFieldEntries(r, fieldPath);
313
+ for (let i = 0; i < fieldEntries.length; i++) {
314
+ const { value: fieldValue, path: concretePath } = fieldEntries[i];
315
315
  if (fieldValue == null)
316
316
  continue;
317
317
  // For absolute paths, the single anchor applies to all field values.
@@ -328,8 +328,8 @@ export function validateReferences(resources, context) {
328
328
  severity: DiagnosticSeverity.Error,
329
329
  code: "SCHEMA_FROM_MISSING_PATH",
330
330
  source: SOURCE,
331
- message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema`,
332
- data: { resource: resourceData, filePath, path: fieldPath },
331
+ message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema`,
332
+ data: { resource: resourceData, filePath, path: concretePath },
333
333
  });
334
334
  continue;
335
335
  }
@@ -339,8 +339,8 @@ export function validateReferences(resources, context) {
339
339
  severity: DiagnosticSeverity.Error,
340
340
  code: "SCHEMA_FROM_MISSING_PATH",
341
341
  source: SOURCE,
342
- message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
343
- data: { resource: resourceData, filePath, path: fieldPath },
342
+ message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
343
+ data: { resource: resourceData, filePath, path: concretePath },
344
344
  });
345
345
  continue;
346
346
  }
@@ -350,8 +350,8 @@ export function validateReferences(resources, context) {
350
350
  severity: DiagnosticSeverity.Error,
351
351
  code: "DEPENDENT_SCHEMA_MISMATCH",
352
352
  source: SOURCE,
353
- message: `${resourceLabel}: '${fieldPath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
354
- data: { resource: resourceData, filePath, path: fieldPath },
353
+ message: `${resourceLabel}: '${concretePath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
354
+ data: { resource: resourceData, filePath, path: concretePath },
355
355
  });
356
356
  }
357
357
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.10.1",
3
+ "version": "1.1.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -41,14 +41,16 @@
41
41
  "ajv-formats": "^3.0.1",
42
42
  "jsonpath-plus": "^10.3.0",
43
43
  "yaml": "^2.8.3",
44
- "@telorun/sdk": "0.11.1",
45
- "@telorun/templating": "0.2.3"
44
+ "@telorun/templating": "1.0.0"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@types/node": "^20.0.0",
49
48
  "typescript": "^5.0.0",
50
49
  "vitest": "^2.1.8"
51
50
  },
51
+ "peerDependencies": {
52
+ "@telorun/sdk": "1.0.0"
53
+ },
52
54
  "scripts": {
53
55
  "build": "tsc -p tsconfig.lib.json",
54
56
  "test": "vitest run",
@@ -101,6 +101,43 @@ export class AnalysisRegistry {
101
101
  return computeSuggestKind(badKind, this.aliases, this.defs);
102
102
  }
103
103
 
104
+ /** Returns every user-facing (alias-form) kind that satisfies the given
105
+ * `x-telo-ref` constraint string (e.g. `"telo#Invocable"`, `"std/sql#Connection"`).
106
+ * Resolution mirrors `validateReferences.checkKind`: abstract targets expand to
107
+ * the set of definitions extending them; concrete targets yield just themselves.
108
+ * Returns `undefined` when the ref can't be resolved (e.g. unregistered identity),
109
+ * so callers can fall back to the unfiltered kind list. */
110
+ userFacingKindsForRef(xTeloRef: string): string[] | undefined {
111
+ const targetKind = this.defs.resolveRef(xTeloRef);
112
+ if (!targetKind) return undefined;
113
+ const targetDef = this.defs.resolve(targetKind);
114
+ if (!targetDef) return undefined;
115
+
116
+ const canonicalKinds: string[] = [];
117
+ if (targetDef.kind === "Telo.Abstract") {
118
+ for (const def of this.defs.getByExtends(targetKind)) {
119
+ const module = (def.metadata as { module?: string } | undefined)?.module;
120
+ if (module && def.metadata?.name) {
121
+ canonicalKinds.push(`${module}.${def.metadata.name as string}`);
122
+ }
123
+ }
124
+ } else {
125
+ canonicalKinds.push(targetKind);
126
+ }
127
+
128
+ const out = new Set<string>();
129
+ for (const kind of canonicalKinds) {
130
+ const dot = kind.indexOf(".");
131
+ if (dot === -1) continue;
132
+ const moduleName = kind.slice(0, dot);
133
+ const typeName = kind.slice(dot + 1);
134
+ for (const alias of this.aliases.aliasesFor(moduleName)) {
135
+ out.add(`${alias}.${typeName}`);
136
+ }
137
+ }
138
+ return Array.from(out);
139
+ }
140
+
104
141
  /** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
105
142
  _context(): AnalysisContext {
106
143
  return { aliases: this.aliases, definitions: this.defs, aliasesByModule: this.aliasesByModule };