@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.
- package/README.md +3 -3
- package/dist/analysis-registry.d.ts +7 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +38 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +198 -6
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +158 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +9 -11
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +26 -14
- package/dist/position-metadata.d.ts +5 -1
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +8 -1
- package/dist/reference-field-map.d.ts +21 -4
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +35 -19
- package/dist/residual-schema.d.ts +23 -0
- package/dist/residual-schema.d.ts.map +1 -0
- package/dist/residual-schema.js +45 -0
- package/dist/rewrite-synthetic-origins.d.ts +10 -0
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
- package/dist/rewrite-synthetic-origins.js +55 -0
- package/dist/validate-cel-context.d.ts +61 -7
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +90 -8
- package/dist/validate-provider-coherence.d.ts +23 -0
- package/dist/validate-provider-coherence.d.ts.map +1 -0
- package/dist/validate-provider-coherence.js +148 -0
- package/dist/validate-references.js +24 -24
- package/package.json +5 -3
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +240 -8
- package/src/builtins.ts +158 -1
- package/src/index.ts +1 -0
- package/src/kernel-globals.ts +9 -11
- package/src/normalize-inline-resources.ts +48 -13
- package/src/position-metadata.ts +8 -1
- package/src/reference-field-map.ts +46 -18
- package/src/residual-schema.ts +49 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/validate-cel-context.ts +111 -8
- package/src/validate-provider-coherence.ts +166 -0
- 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
|
|
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 '${
|
|
170
|
-
data: { resource: resourceData, filePath, path:
|
|
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 '${
|
|
181
|
-
data: { resource: resourceData, filePath, path:
|
|
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 '${
|
|
200
|
-
data: { resource: resourceData, filePath, path:
|
|
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 '${
|
|
213
|
-
data: { resource: resourceData, filePath, path:
|
|
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 '${
|
|
227
|
-
data: { resource: resourceData, filePath, path:
|
|
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
|
|
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}: '${
|
|
329
|
-
data: { resource: resourceData, filePath, path:
|
|
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
|
|
350
|
+
const fieldEntries = resolveFieldEntries(r, fieldPath);
|
|
351
351
|
|
|
352
|
-
for (let i = 0; i <
|
|
353
|
-
const fieldValue =
|
|
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 '${
|
|
371
|
-
data: { resource: resourceData, filePath, path:
|
|
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 '${
|
|
383
|
-
data: { resource: resourceData, filePath, path:
|
|
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}: '${
|
|
395
|
-
data: { resource: resourceData, filePath, path:
|
|
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
|
}
|