@telorun/analyzer 0.66.0 → 0.67.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 (76) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +37 -2
  3. package/dist/builtins.d.ts.map +1 -1
  4. package/dist/builtins.js +67 -16
  5. package/dist/cel-scope.d.ts +8 -0
  6. package/dist/cel-scope.d.ts.map +1 -1
  7. package/dist/cel-scope.js +66 -8
  8. package/dist/definition-registry.d.ts +17 -0
  9. package/dist/definition-registry.d.ts.map +1 -1
  10. package/dist/definition-registry.js +35 -0
  11. package/dist/dependency-graph.d.ts.map +1 -1
  12. package/dist/dependency-graph.js +65 -0
  13. package/dist/flatten-for-analyzer.d.ts +36 -0
  14. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  15. package/dist/flatten-for-analyzer.js +103 -4
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -2
  19. package/dist/inline-imports.d.ts.map +1 -1
  20. package/dist/inline-imports.js +1 -0
  21. package/dist/manifest-visitor.d.ts +4 -0
  22. package/dist/manifest-visitor.d.ts.map +1 -1
  23. package/dist/manifest-visitor.js +28 -0
  24. package/dist/precompile.d.ts.map +1 -1
  25. package/dist/precompile.js +8 -0
  26. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  27. package/dist/resolve-ref-sentinels.js +14 -1
  28. package/dist/resource-input.d.ts +75 -0
  29. package/dist/resource-input.d.ts.map +1 -0
  30. package/dist/resource-input.js +90 -0
  31. package/dist/schema-projection.d.ts +13 -0
  32. package/dist/schema-projection.d.ts.map +1 -1
  33. package/dist/schema-projection.js +7 -0
  34. package/dist/system-kinds.d.ts +7 -2
  35. package/dist/system-kinds.d.ts.map +1 -1
  36. package/dist/system-kinds.js +7 -2
  37. package/dist/telo-version.d.ts +1 -1
  38. package/dist/telo-version.js +1 -1
  39. package/dist/template-body.d.ts +50 -0
  40. package/dist/template-body.d.ts.map +1 -0
  41. package/dist/template-body.js +58 -0
  42. package/dist/validate-cel-context.d.ts.map +1 -1
  43. package/dist/validate-cel-context.js +68 -8
  44. package/dist/validate-identifier-names.d.ts.map +1 -1
  45. package/dist/validate-identifier-names.js +17 -2
  46. package/dist/validate-references.d.ts +17 -0
  47. package/dist/validate-references.d.ts.map +1 -1
  48. package/dist/validate-references.js +68 -16
  49. package/dist/validate-resource-inputs.d.ts +35 -0
  50. package/dist/validate-resource-inputs.d.ts.map +1 -0
  51. package/dist/validate-resource-inputs.js +319 -0
  52. package/dist/validate-template-dispatch.d.ts +27 -0
  53. package/dist/validate-template-dispatch.d.ts.map +1 -0
  54. package/dist/validate-template-dispatch.js +95 -0
  55. package/package.json +3 -3
  56. package/src/analyzer.ts +45 -2
  57. package/src/builtins.ts +69 -16
  58. package/src/cel-scope.ts +90 -14
  59. package/src/definition-registry.ts +36 -0
  60. package/src/dependency-graph.ts +66 -0
  61. package/src/flatten-for-analyzer.ts +116 -3
  62. package/src/index.ts +12 -0
  63. package/src/inline-imports.ts +1 -0
  64. package/src/manifest-visitor.ts +33 -0
  65. package/src/precompile.ts +8 -0
  66. package/src/resolve-ref-sentinels.ts +12 -1
  67. package/src/resource-input.ts +132 -0
  68. package/src/schema-projection.ts +19 -0
  69. package/src/system-kinds.ts +7 -2
  70. package/src/telo-version.ts +1 -1
  71. package/src/template-body.ts +104 -0
  72. package/src/validate-cel-context.ts +67 -7
  73. package/src/validate-identifier-names.ts +18 -3
  74. package/src/validate-references.ts +70 -14
  75. package/src/validate-resource-inputs.ts +367 -0
  76. package/src/validate-template-dispatch.ts +99 -0
@@ -0,0 +1,319 @@
1
+ import { isRefSentinel } from "@telorun/templating";
2
+ import { findDynamicLeaf } from "./resource-rule.js";
3
+ import { readResourceInputs, readSuppliedResources } from "./resource-input.js";
4
+ import { DiagnosticSeverity } from "./types.js";
5
+ const SOURCE = "telo-analyzer";
6
+ /**
7
+ * The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
8
+ * split applied to the resource-input boundary, and not optional for the same
9
+ * reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
10
+ * leaves the library referencing a name nothing stands behind; an unchecked
11
+ * INJECTION hands a library an instance of a kind it never asked for, which
12
+ * surfaces as a method-missing failure inside someone else's module.
13
+ *
14
+ * Two surfaces, scoped the same way every other declaration check is — to the
15
+ * entry's own modules, since a published dependency's block is not the
16
+ * consumer's to fix:
17
+ *
18
+ * - **The library's own declaration.** Each entry's alias-qualified `kind:` must
19
+ * resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
20
+ * A constraint that resolves to nothing accepts anything, which is the same
21
+ * hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
22
+ * - **The injection site.** Every declared entry must be supplied
23
+ * (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
24
+ * variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
25
+ * must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
26
+ * kind must satisfy the declared constraint transitively
27
+ * (`RESOURCE_INPUT_KIND_MISMATCH`).
28
+ *
29
+ * Browser-safe.
30
+ */
31
+ export function validateResourceInputs(manifests, registry, aliases, rootModules,
32
+ /** Kind acceptance, transitively — the `checkKind` rule `validate-references`
33
+ * applies at an ordinary ref slot, passed in rather than re-derived so the
34
+ * two cannot disagree about what satisfies a constraint. */
35
+ acceptsKind) {
36
+ const out = [];
37
+ const isOwn = (module) => !module || rootModules.has(module);
38
+ // --- the library's own declaration -------------------------------------
39
+ for (const m of manifests) {
40
+ if (m.kind !== "Telo.Library")
41
+ continue;
42
+ const moduleName = m.metadata?.name;
43
+ // Only the entry's own modules, so the declaring scope is always the root
44
+ // alias table — a library analyzed here IS a root module.
45
+ if (!isOwn(moduleName))
46
+ continue;
47
+ const scope = aliases;
48
+ const exported = new Set((m.exports?.resources ?? []).filter((e) => typeof e === "string"));
49
+ for (const input of readResourceInputs(m)) {
50
+ // An input is the IMPORTER's instance, borrowed. Exporting it back out
51
+ // would forward a kind-only stand-in into the consumer's flattened set as
52
+ // a resource this library declares — a phantom the consumer can `!ref`
53
+ // and whose kind constraint is all there is behind it. Handing an
54
+ // instance straight back to whoever supplied it is also a relation
55
+ // nothing needs; if it ever does, it should be a decision rather than a
56
+ // name collision nobody noticed.
57
+ if (exported.has(input.name)) {
58
+ out.push({
59
+ severity: DiagnosticSeverity.Error,
60
+ code: "RESOURCE_INPUT_EXPORTED",
61
+ source: SOURCE,
62
+ message: `Resource input '${input.name}' is also listed in 'exports.resources'. An input is ` +
63
+ `an instance the importer supplies, not one this library declares, so there is ` +
64
+ `nothing to export — remove it from 'exports.resources', or rename the input.`,
65
+ data: {
66
+ resource: { kind: m.kind, name: moduleName },
67
+ filePath: m.metadata?.source,
68
+ path: `resources.${input.name}`,
69
+ },
70
+ });
71
+ }
72
+ if (registry.resolve(input.kind) ?? registry.resolve(scope.resolveKind(input.kind) ?? "")) {
73
+ continue;
74
+ }
75
+ out.push({
76
+ severity: DiagnosticSeverity.Error,
77
+ code: "RESOURCE_INPUT_KIND_UNRESOLVED",
78
+ source: SOURCE,
79
+ message: `Resource input '${input.name}' is constrained to kind '${input.kind}', which does not ` +
80
+ `resolve in this library's scope. Write it alias-qualified — '<Alias>.<Kind>' for an ` +
81
+ `import declared in this file, 'Self.<Kind>' for a kind this library owns, or ` +
82
+ `'Telo.<Kind>' for a built-in. An unresolvable constraint accepts anything.`,
83
+ data: {
84
+ resource: { kind: m.kind, name: moduleName },
85
+ filePath: m.metadata?.source,
86
+ path: `resources.${input.name}.kind`,
87
+ },
88
+ });
89
+ }
90
+ }
91
+ // --- the injection site --------------------------------------------------
92
+ for (const m of manifests) {
93
+ if (m.kind !== "Telo.Import")
94
+ continue;
95
+ const meta = m.metadata;
96
+ const alias = meta?.name;
97
+ if (!alias || !isOwn(meta?.module))
98
+ continue;
99
+ // An import whose target identity was never established registers nothing —
100
+ // reporting a missing input against it would blame the author for a
101
+ // dependency the loader has already said it could not obtain.
102
+ if (!meta?.resolvedModuleName)
103
+ continue;
104
+ // A singleton has no room for a per-import override: `logging:` scopes a
105
+ // subtree that is no longer this import's subtree, and `runtime:` selects a
106
+ // controller backend for a library that is instantiated once. Whichever
107
+ // import happened to be created first would decide, so the field is
108
+ // rejected rather than silently applied to everyone or silently dropped.
109
+ if (meta.sharedLibrary === true) {
110
+ for (const field of ["logging", "runtime"]) {
111
+ if (m[field] === undefined)
112
+ continue;
113
+ out.push({
114
+ severity: DiagnosticSeverity.Error,
115
+ code: "SHARED_LIBRARY_OVERRIDE",
116
+ source: SOURCE,
117
+ message: `Import '${alias}' declares '${field}:', but module ` +
118
+ `'${meta.resolvedModuleName}' is 'lifecycle: shared' — one instantiation for the ` +
119
+ `whole application, so a per-import override cannot apply to it. Remove it, or ` +
120
+ `make the library 'lifecycle: isolated'.`,
121
+ data: {
122
+ resource: { kind: m.kind, name: alias },
123
+ filePath: meta.source,
124
+ path: field,
125
+ },
126
+ });
127
+ }
128
+ }
129
+ const required = Object.entries(meta.requiredResources ?? {});
130
+ const supplied = readSuppliedResources(m);
131
+ const declared = new Map(required);
132
+ const resource = { kind: m.kind, name: alias };
133
+ const filePath = meta.source;
134
+ for (const [entryName, entryKind] of required) {
135
+ if (entryName in supplied)
136
+ continue;
137
+ out.push({
138
+ severity: DiagnosticSeverity.Error,
139
+ code: "RESOURCE_INPUT_MISSING",
140
+ source: SOURCE,
141
+ message: `Import '${alias}' does not supply the resource input '${entryName}', which module ` +
142
+ `'${meta.resolvedModuleName}' requires (kind '${entryKind}'). Add ` +
143
+ `\`resources: { ${entryName}: !ref <name> }\` to the import.`,
144
+ data: { resource, filePath, path: "resources" },
145
+ });
146
+ }
147
+ for (const [name, value] of Object.entries(supplied)) {
148
+ const path = `resources.${name}`;
149
+ const entry = declared.get(name);
150
+ if (!entry) {
151
+ const known = required.map(([n]) => n).join(", ") || "(none)";
152
+ out.push({
153
+ severity: DiagnosticSeverity.Error,
154
+ code: "RESOURCE_INPUT_UNKNOWN",
155
+ source: SOURCE,
156
+ message: `Import '${alias}' supplies a resource input '${name}', which module ` +
157
+ `'${meta.resolvedModuleName}' does not declare. Declared inputs: ${known}.`,
158
+ data: { resource, filePath, path },
159
+ });
160
+ continue;
161
+ }
162
+ // Phase 2.5 rewrote a resolvable `!ref` to `{kind, name}`; a sentinel that
163
+ // survived names nothing the importer can reach.
164
+ if (isRefSentinel(value)) {
165
+ out.push({
166
+ severity: DiagnosticSeverity.Error,
167
+ code: "RESOURCE_INPUT_UNRESOLVED",
168
+ source: SOURCE,
169
+ message: `Import '${alias}': resource input '${name}' → resource '${value.source}' not found`,
170
+ data: { resource, filePath, path },
171
+ });
172
+ continue;
173
+ }
174
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
175
+ out.push({
176
+ severity: DiagnosticSeverity.Error,
177
+ code: "RESOURCE_INPUT_UNRESOLVED",
178
+ source: SOURCE,
179
+ message: `Import '${alias}': resource input '${name}' must be a '!ref' to a resource this ` +
180
+ `module declares.`,
181
+ data: { resource, filePath, path },
182
+ });
183
+ continue;
184
+ }
185
+ const suppliedKind = value.kind;
186
+ if (typeof suppliedKind !== "string")
187
+ continue;
188
+ const canonical = aliases.resolveKind(suppliedKind) ?? suppliedKind;
189
+ if (acceptsKind(canonical, entry))
190
+ continue;
191
+ out.push({
192
+ severity: DiagnosticSeverity.Error,
193
+ code: "RESOURCE_INPUT_KIND_MISMATCH",
194
+ source: SOURCE,
195
+ message: `Import '${alias}': resource input '${name}' is '${suppliedKind}' (resolved: ` +
196
+ `'${canonical}'), which does not satisfy the declared constraint '${entry}'.`,
197
+ data: { resource, filePath, path },
198
+ });
199
+ }
200
+ }
201
+ out.push(...sharedLibraryConflicts(manifests, isOwn));
202
+ return out;
203
+ }
204
+ const SUPPLIED_BLOCKS = ["variables", "secrets", "resources"];
205
+ /**
206
+ * Two imports of ONE `lifecycle: shared` library supplying different values.
207
+ *
208
+ * The runtime half (`ERR_SHARED_LIBRARY_CONFLICT`) is authoritative — it holds
209
+ * resolved values and live instances, and a library reached through a
210
+ * programmatic load never passed `telo check`. But the overwhelmingly common
211
+ * shape is two literal scalars in one flattened manifest set, which is decidable
212
+ * here, and a conflict is a hard BOOT failure: finding it at `telo check` is the
213
+ * difference between a squiggle and a deployment that will not start.
214
+ *
215
+ * Only what is DECIDABLE is compared, and the two ways it is not are skipped
216
+ * rather than guessed:
217
+ *
218
+ * - A value holding a `!cel` (at any depth) is known only at load. Two
219
+ * different expressions may evaluate equal, and identical text in two
220
+ * different modules may not.
221
+ * - A `resources:` entry names a resource by name, which means the same
222
+ * instance only when both imports were DECLARED in the same module — so a
223
+ * cross-module pair is left to the runtime.
224
+ *
225
+ * Grouped by the target's RESOLVED SOURCE, the identity the kernel keys its
226
+ * singleton registry on; `resolvedModuleName` would collapse two versions of one
227
+ * module, which are two libraries.
228
+ */
229
+ function sharedLibraryConflicts(manifests, isOwn) {
230
+ const out = [];
231
+ const byTarget = new Map();
232
+ for (const m of manifests) {
233
+ if (m.kind !== "Telo.Import")
234
+ continue;
235
+ const meta = m.metadata;
236
+ if (meta?.sharedLibrary !== true || !meta.name || !meta.resolvedSource)
237
+ continue;
238
+ const blocks = new Map();
239
+ for (const block of SUPPLIED_BLOCKS) {
240
+ const value = m[block];
241
+ blocks.set(block, value && typeof value === "object" && !Array.isArray(value)
242
+ ? value
243
+ : {});
244
+ }
245
+ const bucket = byTarget.get(meta.resolvedSource);
246
+ const entry = {
247
+ alias: meta.name,
248
+ module: meta.module,
249
+ filePath: meta.source,
250
+ blocks,
251
+ };
252
+ if (bucket)
253
+ bucket.push(entry);
254
+ else
255
+ byTarget.set(meta.resolvedSource, [entry]);
256
+ }
257
+ for (const group of byTarget.values()) {
258
+ if (group.length < 2)
259
+ continue;
260
+ const [first, ...rest] = group;
261
+ for (const later of rest) {
262
+ for (const block of SUPPLIED_BLOCKS) {
263
+ // A reference means the same instance only within one module's scope.
264
+ if (block === "resources" && first.module !== later.module)
265
+ continue;
266
+ const a = first.blocks.get(block);
267
+ const b = later.blocks.get(block);
268
+ for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
269
+ if (findDynamicLeaf(a[key]) || findDynamicLeaf(b[key]))
270
+ continue;
271
+ if (sameSuppliedValue(a[key], b[key]))
272
+ continue;
273
+ out.push({
274
+ severity: DiagnosticSeverity.Error,
275
+ code: "SHARED_LIBRARY_CONFLICT",
276
+ source: SOURCE,
277
+ message: `Import '${later.alias}' and import '${first.alias}' both reach a ` +
278
+ `'lifecycle: shared' library — one instantiation for the whole application — but ` +
279
+ `they supply different values for ${block}.${key}. Make the two imports agree, or ` +
280
+ `make the library 'lifecycle: isolated'.`,
281
+ data: isOwn(later.module)
282
+ ? {
283
+ resource: { kind: "Telo.Import", name: later.alias },
284
+ filePath: later.filePath,
285
+ path: `${block}.${key}`,
286
+ }
287
+ : {
288
+ resource: { kind: "Telo.Import", name: first.alias },
289
+ filePath: first.filePath,
290
+ path: `${block}.${key}`,
291
+ },
292
+ });
293
+ }
294
+ }
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ /** Structural equality, key-order insensitive — the analyzer's half of the rule
300
+ * the kernel applies to resolved values. A secret's VALUE is compared but never
301
+ * printed: the key is what the author has to look at. */
302
+ function sameSuppliedValue(a, b) {
303
+ if (a === b)
304
+ return true;
305
+ if (a === undefined || b === undefined || a === null || b === null)
306
+ return false;
307
+ if (Array.isArray(a) || Array.isArray(b)) {
308
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
309
+ return false;
310
+ return a.every((item, i) => sameSuppliedValue(item, b[i]));
311
+ }
312
+ if (typeof a === "object" && typeof b === "object") {
313
+ const left = a;
314
+ const right = b;
315
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
316
+ return [...keys].every((key) => sameSuppliedValue(left[key], right[key]));
317
+ }
318
+ return false;
319
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import { type AnalysisDiagnostic } from "./types.js";
3
+ /**
4
+ * A `!ref` at a definition's dispatch slot must name a sibling `resources:`
5
+ * entry.
6
+ *
7
+ * `!ref` is the one spelling an author — and the editor's rename, completion and
8
+ * go-to-definition — expects to be RESOLVED. But a `Telo.Definition` is in both
9
+ * `REF_VALIDATION_SKIP_KINDS` and `REF_RESOLUTION_SKIP_KINDS`, and the dispatch
10
+ * slots carry no `x-telo-ref` (the accepted targets are the definition's own
11
+ * template-internal entries, not resources of any module), so no reference pass
12
+ * reaches them. Introducing the tag at a slot nothing resolves would be worse
13
+ * than the string form it replaces: a typo that used to be an obvious runtime
14
+ * miss becomes a typo in a construct that advertises static resolution.
15
+ *
16
+ * Decidable only when every sibling name is LITERAL. A template routinely names
17
+ * its entries with CEL (`name: !cel "self.name + '-query'"`), and an expression
18
+ * could expand to the referenced name — so one dynamic sibling switches the
19
+ * check off for that definition rather than inventing a miss.
20
+ *
21
+ * Entry-module-scoped, like every other declaration check: a published
22
+ * dependency's template body is not the consumer's to fix.
23
+ *
24
+ * Browser-safe.
25
+ */
26
+ export declare function validateTemplateDispatch(manifests: ResourceManifest[], rootModules: ReadonlySet<string>): AnalysisDiagnostic[];
27
+ //# sourceMappingURL=validate-template-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-template-dispatch.d.ts","sourceRoot":"","sources":["../src/validate-template-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AASzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,kBAAkB,EAAE,CAgDtB"}
@@ -0,0 +1,95 @@
1
+ import { isRefSentinel } from "@telorun/templating";
2
+ import { distance } from "./levenshtein.js";
3
+ import { DiagnosticSeverity } from "./types.js";
4
+ const SOURCE = "telo-analyzer";
5
+ /** The four slots a `Telo.Definition` names its dispatch target in. Each takes
6
+ * the same grammar; which one is legal for a given capability is
7
+ * `validate-provider-coherence`'s question, not this one's. */
8
+ const DISPATCH_FIELDS = ["invoke", "run", "provide", "mount"];
9
+ /**
10
+ * A `!ref` at a definition's dispatch slot must name a sibling `resources:`
11
+ * entry.
12
+ *
13
+ * `!ref` is the one spelling an author — and the editor's rename, completion and
14
+ * go-to-definition — expects to be RESOLVED. But a `Telo.Definition` is in both
15
+ * `REF_VALIDATION_SKIP_KINDS` and `REF_RESOLUTION_SKIP_KINDS`, and the dispatch
16
+ * slots carry no `x-telo-ref` (the accepted targets are the definition's own
17
+ * template-internal entries, not resources of any module), so no reference pass
18
+ * reaches them. Introducing the tag at a slot nothing resolves would be worse
19
+ * than the string form it replaces: a typo that used to be an obvious runtime
20
+ * miss becomes a typo in a construct that advertises static resolution.
21
+ *
22
+ * Decidable only when every sibling name is LITERAL. A template routinely names
23
+ * its entries with CEL (`name: !cel "self.name + '-query'"`), and an expression
24
+ * could expand to the referenced name — so one dynamic sibling switches the
25
+ * check off for that definition rather than inventing a miss.
26
+ *
27
+ * Entry-module-scoped, like every other declaration check: a published
28
+ * dependency's template body is not the consumer's to fix.
29
+ *
30
+ * Browser-safe.
31
+ */
32
+ export function validateTemplateDispatch(manifests, rootModules) {
33
+ const out = [];
34
+ for (const m of manifests) {
35
+ if (m.kind !== "Telo.Definition")
36
+ continue;
37
+ const meta = m.metadata;
38
+ const name = meta?.name;
39
+ if (!name)
40
+ continue;
41
+ if (meta?.module && !rootModules.has(meta.module))
42
+ continue;
43
+ const bodies = m.resources;
44
+ if (!Array.isArray(bodies))
45
+ continue;
46
+ const siblings = [];
47
+ let anyDynamic = false;
48
+ for (const entry of bodies) {
49
+ const entryName = entry?.metadata?.name;
50
+ if (typeof entryName === "string" && !entryName.includes("${{"))
51
+ siblings.push(entryName);
52
+ else if (entryName !== undefined)
53
+ anyDynamic = true;
54
+ }
55
+ if (anyDynamic)
56
+ continue;
57
+ for (const field of DISPATCH_FIELDS) {
58
+ const value = m[field];
59
+ if (!isRefSentinel(value))
60
+ continue;
61
+ const source = value.source;
62
+ const target = source.startsWith("Self.") ? source.slice("Self.".length) : source;
63
+ if (siblings.includes(target))
64
+ continue;
65
+ const suggestion = nearest(target, siblings);
66
+ out.push({
67
+ severity: DiagnosticSeverity.Error,
68
+ code: "TEMPLATE_DISPATCH_UNKNOWN",
69
+ source: SOURCE,
70
+ message: `${m.kind}/${name}: '${field}: !ref ${source}' names no entry in 'resources:'. ` +
71
+ `Available: ${siblings.join(", ") || "(none)"}.` +
72
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""),
73
+ data: {
74
+ resource: { kind: m.kind, name },
75
+ filePath: meta?.source,
76
+ path: field,
77
+ ...(suggestion ? { fix: { replacement: suggestion } } : {}),
78
+ },
79
+ });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ /** The closest sibling name, when one is close enough to be a typo rather than a
85
+ * different name. */
86
+ function nearest(target, candidates) {
87
+ let best;
88
+ for (const candidate of candidates) {
89
+ const d = distance(target, candidate);
90
+ if (!best || d < best.d)
91
+ best = { name: candidate, d };
92
+ }
93
+ const limit = Math.max(1, Math.floor(target.length / 3));
94
+ return best && best.d <= limit ? best.name : undefined;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.66.0",
3
+ "version": "0.67.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -43,13 +43,13 @@
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "packageurl-js": "^2.0.1",
45
45
  "yaml": "^2.8.3",
46
- "@telorun/templating": "0.17.0"
46
+ "@telorun/templating": "0.18.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.0.0",
50
50
  "typescript": "^5.0.0",
51
51
  "vitest": "^2.1.8",
52
- "@telorun/sdk": "0.82.1"
52
+ "@telorun/sdk": "0.83.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -157,8 +157,11 @@ import { validateInvocationContract } from "./validate-invocation-contract.js";
157
157
  import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
158
158
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
159
159
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
160
- import { validateReferences } from "./validate-references.js";
160
+ import { kindSatisfies, validateReferences } from "./validate-references.js";
161
161
  import { validateReferenceForms } from "./validate-reference-forms.js";
162
+ import { isInjectedDeclaration } from "./resource-input.js";
163
+ import { validateResourceInputs } from "./validate-resource-inputs.js";
164
+ import { validateTemplateDispatch } from "./validate-template-dispatch.js";
162
165
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
163
166
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
164
167
  import { readStepSlot } from "./step-slot.js";
@@ -1627,6 +1630,16 @@ export class StaticAnalyzer {
1627
1630
  const runReachable = reportsObservedState
1628
1631
  ? collectRunReachableNames(getCallGraph())
1629
1632
  : new Set<string>();
1633
+ // "Nothing starts this" is DECLARATION-derived, and a library does not
1634
+ // declare its injected inputs — whether the application starts the instance
1635
+ // it hands down is answerable only where that instance is declared. Inside
1636
+ // the library the question has no answer, so the name is treated as
1637
+ // reachable rather than reported on a `targets:` list the author cannot
1638
+ // write. The check still runs at the injection site, against the real
1639
+ // declaration.
1640
+ for (const m of allManifests) {
1641
+ if (isInjectedDeclaration(m)) runReachable.add(m.metadata?.name as string);
1642
+ }
1630
1643
 
1631
1644
  // Build typed kernel globals schema so x-telo-context chain validation
1632
1645
  // recognises variables, secrets, resources, env automatically
@@ -1762,6 +1775,15 @@ export class StaticAnalyzer {
1762
1775
  continue;
1763
1776
  }
1764
1777
 
1778
+ // A kind-only stand-in for a `resources:` entry is a DECLARATION, not an
1779
+ // instantiation: its kind is routinely an abstract and its configuration
1780
+ // is the importer's to supply, so validating it here would report a
1781
+ // non-instantiable kind and a page of missing required fields against a
1782
+ // block that is correct. Its kind is checked where it was written, by
1783
+ // `validate-resource-inputs`; it participates here only as a resolution
1784
+ // target and as a CEL type.
1785
+ if (isInjectedDeclaration(m)) continue;
1786
+
1765
1787
  const resource = { kind: m.kind, name: m.metadata?.name as string };
1766
1788
 
1767
1789
  // Resolve kind through alias if needed; direct lookup takes priority so that
@@ -1861,6 +1883,11 @@ export class StaticAnalyzer {
1861
1883
  ) as Record<string, any>;
1862
1884
  if (!ownModule || rootModules.has(ownModule)) {
1863
1885
  for (const failure of projectionFailures) {
1886
+ // A projection through a library's own resource INPUT is
1887
+ // unanswerable here — the entries belong to the declaration the
1888
+ // importer supplies — so it is not a defect in the block that named
1889
+ // it. The check runs at the injection site instead.
1890
+ if (failure.reason === "injected") continue;
1864
1891
  diagnostics.push({
1865
1892
  severity: DiagnosticSeverity.Error,
1866
1893
  code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
@@ -2556,7 +2583,7 @@ export class StaticAnalyzer {
2556
2583
  }
2557
2584
  },
2558
2585
  },
2559
- { aliases },
2586
+ { aliases, aliasesByModule, rootModules },
2560
2587
  );
2561
2588
 
2562
2589
  // The two halves of "does this expression fit the slot it flows into" meet
@@ -2645,6 +2672,22 @@ export class StaticAnalyzer {
2645
2672
  // Warn about declared variables / secrets / ports that no CEL references.
2646
2673
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
2647
2674
 
2675
+ // A `!ref` at a definition's dispatch slot must name a sibling `resources:`
2676
+ // entry — the slot no reference pass reaches, so the tag would otherwise
2677
+ // advertise a resolution nothing performs.
2678
+ diagnostics.push(...validateTemplateDispatch(allManifests, rootModules));
2679
+
2680
+ // A library's declared resource inputs, and every import that supplies them.
2681
+ diagnostics.push(
2682
+ ...validateResourceInputs(
2683
+ allManifests,
2684
+ defs,
2685
+ aliases,
2686
+ rootModules,
2687
+ (supplied, required) => kindSatisfies(supplied, required, defs),
2688
+ ),
2689
+ );
2690
+
2648
2691
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
2649
2692
  // the chain root so position-index lookups land on the parent doc.
2650
2693
  return rewriteSyntheticOrigins(