@telorun/analyzer 0.72.0 → 0.73.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 (90) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +50 -13
  3. package/dist/builtins.d.ts.map +1 -1
  4. package/dist/builtins.js +71 -97
  5. package/dist/cel-scope-query.d.ts.map +1 -1
  6. package/dist/cel-scope-query.js +32 -11
  7. package/dist/cel-scope.d.ts.map +1 -1
  8. package/dist/cel-scope.js +1 -0
  9. package/dist/eval-paths.d.ts +36 -5
  10. package/dist/eval-paths.d.ts.map +1 -1
  11. package/dist/eval-paths.js +47 -6
  12. package/dist/extends-resolution.d.ts +12 -0
  13. package/dist/extends-resolution.d.ts.map +1 -1
  14. package/dist/extends-resolution.js +23 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/nearest-name.d.ts +22 -0
  19. package/dist/nearest-name.d.ts.map +1 -0
  20. package/dist/nearest-name.js +43 -0
  21. package/dist/release/destinations.d.ts +59 -0
  22. package/dist/release/destinations.d.ts.map +1 -0
  23. package/dist/release/destinations.js +75 -0
  24. package/dist/release/index.d.ts +6 -2
  25. package/dist/release/index.d.ts.map +1 -1
  26. package/dist/release/index.js +3 -1
  27. package/dist/release/ledger.d.ts +20 -11
  28. package/dist/release/ledger.d.ts.map +1 -1
  29. package/dist/release/ledger.js +43 -14
  30. package/dist/release/release-plan.d.ts +3 -2
  31. package/dist/release/release-plan.d.ts.map +1 -1
  32. package/dist/release/release-plan.js +12 -19
  33. package/dist/release/workspace-config.d.ts +114 -34
  34. package/dist/release/workspace-config.d.ts.map +1 -1
  35. package/dist/release/workspace-config.js +322 -38
  36. package/dist/release/workspace-schema.d.ts +41 -0
  37. package/dist/release/workspace-schema.d.ts.map +1 -0
  38. package/dist/release/workspace-schema.js +77 -0
  39. package/dist/schema-error-report.d.ts +7 -0
  40. package/dist/schema-error-report.d.ts.map +1 -1
  41. package/dist/schema-error-report.js +9 -4
  42. package/dist/telo-version.d.ts +1 -1
  43. package/dist/telo-version.js +1 -1
  44. package/dist/template-body.d.ts +10 -0
  45. package/dist/template-body.d.ts.map +1 -1
  46. package/dist/template-body.js +25 -0
  47. package/dist/validate-cel-context.d.ts +23 -8
  48. package/dist/validate-cel-context.d.ts.map +1 -1
  49. package/dist/validate-cel-context.js +48 -16
  50. package/dist/validate-exports.d.ts +28 -0
  51. package/dist/validate-exports.d.ts.map +1 -0
  52. package/dist/validate-exports.js +146 -0
  53. package/dist/validate-extends.d.ts.map +1 -1
  54. package/dist/validate-extends.js +37 -1
  55. package/dist/validate-provider-coherence.d.ts +4 -8
  56. package/dist/validate-provider-coherence.d.ts.map +1 -1
  57. package/dist/validate-provider-coherence.js +12 -123
  58. package/dist/validate-references.d.ts.map +1 -1
  59. package/dist/validate-references.js +141 -60
  60. package/dist/validate-template-body.d.ts +36 -0
  61. package/dist/validate-template-body.d.ts.map +1 -0
  62. package/dist/validate-template-body.js +246 -0
  63. package/package.json +3 -2
  64. package/src/analyzer.ts +64 -11
  65. package/src/builtins.ts +85 -97
  66. package/src/cel-scope-query.ts +28 -9
  67. package/src/cel-scope.ts +1 -0
  68. package/src/eval-paths.ts +53 -6
  69. package/src/extends-resolution.ts +24 -0
  70. package/src/index.ts +1 -0
  71. package/src/nearest-name.ts +47 -0
  72. package/src/release/destinations.ts +105 -0
  73. package/src/release/index.ts +33 -2
  74. package/src/release/ledger.ts +51 -21
  75. package/src/release/release-plan.ts +16 -26
  76. package/src/release/workspace-config.ts +483 -47
  77. package/src/release/workspace-schema.ts +99 -0
  78. package/src/schema-error-report.ts +16 -4
  79. package/src/telo-version.ts +1 -1
  80. package/src/template-body.ts +25 -0
  81. package/src/validate-cel-context.ts +57 -16
  82. package/src/validate-exports.ts +185 -0
  83. package/src/validate-extends.ts +43 -0
  84. package/src/validate-provider-coherence.ts +17 -127
  85. package/src/validate-references.ts +141 -60
  86. package/src/validate-template-body.ts +274 -0
  87. package/dist/validate-template-dispatch.d.ts +0 -27
  88. package/dist/validate-template-dispatch.d.ts.map +0 -1
  89. package/dist/validate-template-dispatch.js +0 -95
  90. package/src/validate-template-dispatch.ts +0 -99
@@ -0,0 +1,246 @@
1
+ import { nearestName } from "./nearest-name.js";
2
+ import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
3
+ import { refSentinelTarget } from "./ref-sentinel-target.js";
4
+ import { isRefEntry, resolveFieldEntries, satisfiesValueBranch } from "./reference-field-map.js";
5
+ import { templateBodies } from "./template-body.js";
6
+ import { DiagnosticSeverity, DiagnosticTag } from "./types.js";
7
+ const SOURCE = "telo-analyzer";
8
+ /** The four slots a `Telo.Definition` names its dispatch target in.
9
+ *
10
+ * WHAT THE TARGET'S CAPABILITY IS IS DELIBERATELY NOT CHECKED. The kernel tests
11
+ * METHOD PRESENCE at dispatch (`entry.instance?.invoke`), so a declared
12
+ * capability says nothing about whether a dispatch works — the same structural
13
+ * -versus-nominal split that keeps `use` uncross-constrained against a slot's
14
+ * target. A check here read `Run.Sequence`'s `capability: Telo.Runnable` and
15
+ * rejected `Ai.Buffered`, a shipping module whose sequence implements `invoke()`
16
+ * over an inputs/outputs contract and dispatches correctly. Trimming the refused
17
+ * set would only move that: any controller may expose a method its capability
18
+ * does not name, and a template body is exactly where that is done on purpose. */
19
+ const DISPATCH_SLOTS = ["invoke", "run", "provide", "mount"];
20
+ /**
21
+ * A TEMPLATE BODY IS WRITTEN LIKE EVERY OTHER MANIFEST, AND CHECKED LIKE ONE.
22
+ *
23
+ * A `Telo.Definition` is in both reference skip sets — its body holds
24
+ * declarations of OTHER kinds, and its dispatch slots carry no `x-telo-ref` — so
25
+ * no reference pass reached it, and each rule about it was added per construct:
26
+ * `provide:` and `mount:` had a target check for the `{ kind, name }` object
27
+ * form, `!ref` had one that switched itself off for any definition with a
28
+ * CEL-named entry, and `invoke:` / `run:` had none. What was tagged was caught
29
+ * and what was a plain name was resolved by the kernel alone. This pass is the
30
+ * one reader of a body's reference surface, and it rests on one spelling:
31
+ *
32
+ * - every `resources:` entry is named by a LITERAL (`TEMPLATE_ENTRY_NAME_DYNAMIC`).
33
+ * Each instance of a template owns its children in a child context of its
34
+ * own, so a per-instance suffix (`self.name + '-query'`) buys nothing — and a
35
+ * `!ref` is looked up verbatim, so a CEL-named sibling is one nothing can name;
36
+ * - a dispatch slot is `!ref <entry>` naming an entry
37
+ * (`TEMPLATE_DISPATCH_UNKNOWN`); the legacy spellings are deprecated rather
38
+ * than refused, since published artifacts carry them. What the target's
39
+ * CAPABILITY is stays unchecked — see `DISPATCH_SLOTS`;
40
+ * - a reference slot INSIDE an entry follows the rule every other slot does —
41
+ * `!ref` or an inline declaration, never `{ kind, name }` or a bare string
42
+ * (`INVALID_REFERENCE_FORM`) — and a bare `!ref` names a sibling or a
43
+ * resource of the defining module (`TEMPLATE_REF_UNKNOWN`). Slots are found
44
+ * through the nested kind's own field map, resolved in the DEFINING module's
45
+ * alias scope, since that is where the body's kinds are written.
46
+ *
47
+ * Entry-module-scoped, like every other declaration check: a published
48
+ * dependency's template body is not the consumer's to fix. Browser-safe.
49
+ */
50
+ export function validateTemplateBody(manifests, registry, aliases, aliasesByModule, rootModules) {
51
+ const out = [];
52
+ const scopes = { aliasesByModule, rootModules };
53
+ // A definition forwarded from an imported library is that library's to fix;
54
+ // the imports name them (the `validate-extends` rule).
55
+ const importedModules = new Set();
56
+ // Every named instance, by declaring module — what a body's bare `!ref` may
57
+ // reach beside its siblings. A manifest with no stamp belongs to the entry.
58
+ const instancesByModule = new Map();
59
+ for (const m of manifests) {
60
+ const name = m.metadata?.name;
61
+ if (typeof name !== "string")
62
+ continue;
63
+ if (m.kind === "Telo.Import") {
64
+ const resolved = m.metadata.resolvedModuleName;
65
+ if (resolved)
66
+ importedModules.add(resolved);
67
+ continue;
68
+ }
69
+ if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract")
70
+ continue;
71
+ if (m.kind === "Telo.Library" || m.kind === "Telo.Application")
72
+ continue;
73
+ const mod = m.metadata?.module;
74
+ let set = instancesByModule.get(mod);
75
+ if (!set)
76
+ instancesByModule.set(mod, (set = new Set()));
77
+ set.add(name);
78
+ }
79
+ for (const m of manifests) {
80
+ if (m.kind !== "Telo.Definition")
81
+ continue;
82
+ const meta = m.metadata;
83
+ const name = meta?.name;
84
+ if (!name)
85
+ continue;
86
+ if (meta?.module && importedModules.has(meta.module))
87
+ continue;
88
+ const bodies = m.resources;
89
+ const resourceRef = { kind: m.kind, name };
90
+ const filePath = meta?.source;
91
+ const label = `${m.kind}/${name}`;
92
+ const report = (code, path, message, fix) => out.push({
93
+ severity: DiagnosticSeverity.Error,
94
+ code,
95
+ source: SOURCE,
96
+ message: `${label}: ${message}`,
97
+ data: {
98
+ resource: resourceRef,
99
+ filePath,
100
+ path,
101
+ ...(fix ? { fix: { replacement: fix } } : {}),
102
+ },
103
+ });
104
+ /** A spelling the kernel still READS. Warning-grade because refusing it
105
+ * would break every app pinning a version that carries it, and tagged so an
106
+ * editor strikes it through rather than merely colouring it — the
107
+ * `DEPRECATED_KIND` shape, for the same reason. */
108
+ const deprecate = (code, path, message) => out.push({
109
+ severity: DiagnosticSeverity.Warning,
110
+ code,
111
+ source: SOURCE,
112
+ message: `${label}: ${message}`,
113
+ tags: [DiagnosticTag.Deprecated],
114
+ data: { resource: resourceRef, filePath, path },
115
+ });
116
+ const entries = Array.isArray(bodies) ? bodies : [];
117
+ const siblings = [];
118
+ let anyDynamic = false;
119
+ entries.forEach((entry, i) => {
120
+ const entryName = entry?.metadata?.name;
121
+ if (typeof entryName === "string" && !entryName.includes("${{")) {
122
+ siblings.push(entryName);
123
+ return;
124
+ }
125
+ if (entryName === undefined)
126
+ return;
127
+ anyDynamic = true;
128
+ deprecate("DEPRECATED_TEMPLATE_ENTRY_NAME", `resources[${i}].metadata.name`, `a 'resources:' entry named by ${describeDynamic(entryName)} is deprecated — name it ` +
129
+ `with a literal and dispatch to it with '!ref'. Each instance of a template owns its ` +
130
+ `children in a child context of its own, so a per-instance suffix buys nothing, and a ` +
131
+ `'!ref' is looked up verbatim, so nothing can name a computed entry. While one is ` +
132
+ `present the target checks below are switched off for this kind.`);
133
+ });
134
+ // Siblings PLUS the declaring module's own resources, because that is what
135
+ // the runtime resolves at a reference slot: a body's `!ref` is stamped
136
+ // `{kind, name}` with an empty kind for a non-sibling, and Phase-5 injection
137
+ // dispatches by NAME and recovers the kind from the instance it finds — so
138
+ // `client: !ref moduleClient` in a body reaches the module-level client and
139
+ // works. (A step's `invoke:` is a different path that does NOT: see the
140
+ // `resources` note in `validate-template-body`'s own tests.)
141
+ const reachable = new Set([
142
+ ...siblings,
143
+ ...(instancesByModule.get(meta?.module) ?? []),
144
+ ]);
145
+ // --- dispatch slots ---
146
+ for (const slot of DISPATCH_SLOTS) {
147
+ const value = m[slot];
148
+ if (value == null)
149
+ continue;
150
+ if (!isRefSentinel(value)) {
151
+ deprecate("DEPRECATED_TEMPLATE_DISPATCH_FORM", slot, `'${slot}:' written as ${describeDispatchValue(value)} is deprecated — ` +
152
+ `write '!ref <entry>', the spelling every other reference uses. ` +
153
+ `Available: ${siblings.join(", ") || "(none)"}.`);
154
+ continue;
155
+ }
156
+ const source = value.source;
157
+ const target = source.startsWith("Self.") ? source.slice("Self.".length) : source;
158
+ if (siblings.includes(target))
159
+ continue;
160
+ // A dynamic sibling has already been reported, and might be the one
161
+ // meant — a second diagnostic about the same line would be noise.
162
+ if (anyDynamic)
163
+ continue;
164
+ const suggestion = nearestName(target, siblings);
165
+ report("TEMPLATE_DISPATCH_UNKNOWN", slot, `'${slot}: !ref ${source}' names no entry in 'resources:'. ` +
166
+ `Available: ${siblings.join(", ") || "(none)"}.` +
167
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""), suggestion);
168
+ }
169
+ // --- reference slots inside each entry ---
170
+ for (const body of templateBodies(m, registry, aliases, scopes)) {
171
+ const kind = body.manifest.kind;
172
+ const fieldMap = registry.expandedFieldMapForResource({ ...body.manifest, metadata: { ...(body.manifest.metadata ?? {}), module: meta?.module } }, aliases, aliasesByModule);
173
+ if (!fieldMap)
174
+ continue;
175
+ for (const [fieldPath, entry] of fieldMap) {
176
+ if (!isRefEntry(entry))
177
+ continue;
178
+ for (const site of resolveFieldEntries(body.manifest, fieldPath)) {
179
+ const value = site.value;
180
+ if (value == null)
181
+ continue;
182
+ const path = `${body.prefix}.${site.path}`;
183
+ if (isTaggedSentinel(value)) {
184
+ const target = refSentinelTarget(value);
185
+ if (!target)
186
+ continue;
187
+ const { alias, name: refName } = target;
188
+ if (alias && alias !== "Self")
189
+ continue;
190
+ if (reachable.has(refName))
191
+ continue;
192
+ const suggestion = nearestName(refName, [...reachable]);
193
+ report("TEMPLATE_REF_UNKNOWN", path, `'${site.path}: !ref ${value.source}' on the ${kind} entry names no sibling ` +
194
+ `'resources:' entry and no resource of this module. ` +
195
+ `Siblings: ${siblings.filter((s) => s !== body.manifest.metadata?.name).join(", ") || "(none)"}.` +
196
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""), suggestion);
197
+ continue;
198
+ }
199
+ const hasValueBranch = (entry.valueBranches?.length ?? 0) > 0;
200
+ if (hasValueBranch && typeof value !== "object")
201
+ continue;
202
+ if (satisfiesValueBranch(value, entry.valueBranches, registry))
203
+ continue;
204
+ if (typeof value === "string") {
205
+ if (value.includes("${{"))
206
+ continue;
207
+ report("INVALID_REFERENCE_FORM", path, `string reference at '${site.path}' on the ${kind} entry → '${value}' is not ` +
208
+ `supported; write it as '!ref ${value}'`);
209
+ continue;
210
+ }
211
+ if (value && typeof value === "object" && !Array.isArray(value)) {
212
+ const obj = value;
213
+ if (typeof obj.kind === "string" && obj.name !== undefined) {
214
+ report("INVALID_REFERENCE_FORM", path, `object reference '{ kind, name }' at '${site.path}' on the ${kind} entry is ` +
215
+ `not supported; write it as '!ref ${describeRefName(obj.name)}'`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+ function describeDynamic(name) {
225
+ if (isTaggedSentinel(name))
226
+ return `the expression '${name.source}'`;
227
+ if (typeof name === "string")
228
+ return `the template '${name}'`;
229
+ return `a ${typeof name}`;
230
+ }
231
+ function describeDispatchValue(value) {
232
+ if (isTaggedSentinel(value))
233
+ return `a '!${value.engine}' expression`;
234
+ if (typeof value === "string")
235
+ return `the string '${value}'`;
236
+ if (value && typeof value === "object" && "name" in value)
237
+ return "the '{ kind, name }' object form";
238
+ return `a ${typeof value}`;
239
+ }
240
+ function describeRefName(name) {
241
+ if (typeof name === "string")
242
+ return name;
243
+ if (isTaggedSentinel(name))
244
+ return "<entry>";
245
+ return String(name);
246
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.72.0",
3
+ "version": "0.73.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -43,12 +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.19.0"
46
+ "@telorun/templating": "0.20.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/glob": "0.3.0",
52
53
  "@telorun/sdk": "0.87.0"
53
54
  },
54
55
  "peerDependencies": {
package/src/analyzer.ts CHANGED
@@ -25,7 +25,13 @@ import {
25
25
  } from "./cel-environment.js";
26
26
  import { DefinitionRegistry } from "./definition-registry.js";
27
27
  import { readDeprecation } from "./deprecation.js";
28
- import { type ContractDirection, effectiveAuthorSchema } from "./extends-resolution.js";
28
+ import {
29
+ type ContractDirection,
30
+ type DefResolver,
31
+ effectiveAuthorSchema,
32
+ inheritedCapability,
33
+ inheritedRequiredFields,
34
+ } from "./extends-resolution.js";
29
35
  import {
30
36
  analyzerContractScope,
31
37
  type ContractScope,
@@ -142,6 +148,7 @@ import {
142
148
  import {
143
149
  celEvalModeAt,
144
150
  celEvalSites,
151
+ implicitEvalSites,
145
152
  mergeCelEvalSites,
146
153
  NO_CEL_EVAL_SITES,
147
154
  type CelEvalSites,
@@ -172,7 +179,8 @@ import { kindSatisfies, validateReferences } from "./validate-references.js";
172
179
  import { validateReferenceForms } from "./validate-reference-forms.js";
173
180
  import { isInjectedDeclaration } from "./resource-input.js";
174
181
  import { validateResourceInputs } from "./validate-resource-inputs.js";
175
- import { validateTemplateDispatch } from "./validate-template-dispatch.js";
182
+ import { validateExports } from "./validate-exports.js";
183
+ import { validateTemplateBody } from "./validate-template-body.js";
176
184
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
177
185
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
178
186
  import { readStepSlot } from "./step-slot.js";
@@ -2029,12 +2037,41 @@ export class StaticAnalyzer {
2029
2037
  allManifests as Record<string, any>[],
2030
2038
  );
2031
2039
  const issues = [...ajvIssues, ...valueSchemaIssues];
2040
+ // WHY A FIELD THIS KIND EXISTS TO SUPPLY IS STILL REQUIRED OF ITS
2041
+ // CONSUMER. A child that `extends` and declares no `base:` is authored
2042
+ // against merge(parent, own), so the parent's `required` stays on the
2043
+ // CHILD's surface — and a kind written to wire that field internally
2044
+ // then demands it from the consumer anyway. The `base:` mapping is what
2045
+ // narrows (with it the surface is the child's own schema alone), and
2046
+ // nothing in "is missing required property" points there, which is what
2047
+ // turns one mistake into a three-step dead end.
2048
+ //
2049
+ // Matched against names DERIVED from the parent, never by parsing the
2050
+ // message: which fields are inherited is a fact about the definition,
2051
+ // and a check that reads a validator's prose breaks when the prose does.
2052
+ const inheritedRequired = inheritedRequiredFields(definition, (k) =>
2053
+ defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
2054
+ );
2055
+ const parentKind = (definition as { extends?: string } | undefined)?.extends;
2032
2056
  for (const issue of issues) {
2057
+ // Keyed on the STRUCTURED failure, never on the sentence: the field
2058
+ // names are derived from the parent, and matching them against the
2059
+ // validator's prose would break when the prose changes and mis-fire on
2060
+ // any other issue quoting the same name (an `additionalProperties`
2061
+ // rejection, an `enum` listing).
2062
+ const inherited =
2063
+ issue.keyword === "required" && issue.missingProperty !== undefined
2064
+ ? inheritedRequired.filter((f) => f === issue.missingProperty)
2065
+ : [];
2066
+ const hint =
2067
+ inherited.length > 0 && parentKind
2068
+ ? ` — ${inherited.map((f) => `'${f}'`).join(", ")} ${inherited.length > 1 ? "come" : "comes"} from '${parentKind}', which this kind extends without a 'base:' mapping, so the parent's required fields stay on this kind's author surface. Add 'base:' to set them internally; that also narrows the surface to this kind's own schema.`
2069
+ : "";
2033
2070
  diagnostics.push({
2034
2071
  severity: DiagnosticSeverity.Error,
2035
2072
  code: "SCHEMA_VIOLATION",
2036
2073
  source: SOURCE,
2037
- message: `${m.kind}/${resource.name}: ${issue.message}`,
2074
+ message: `${m.kind}/${resource.name}: ${issue.message}${hint}`,
2038
2075
  data: { resource, filePath, path: issue.path },
2039
2076
  });
2040
2077
  }
@@ -2472,8 +2509,15 @@ export class StaticAnalyzer {
2472
2509
 
2473
2510
  // The non-eval-field check only applies to runtime resource instances:
2474
2511
  // structural / templating kinds (capability `Telo.Template`, or no
2475
- // definition) carry CEL the kernel evaluates by other rules.
2476
- const capability = e.definition?.capability;
2512
+ // definition) carry CEL the kernel evaluates by other rules. The
2513
+ // capability is INHERITED along `extends` — an inheritance kind
2514
+ // writes none of its own — and reading the declared one left the
2515
+ // rule off for every such kind, and its expressions untyped.
2516
+ const resolveDef: DefResolver = (k) =>
2517
+ defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k);
2518
+ const capability = e.definition
2519
+ ? inheritedCapability(e.definition as unknown as ResourceDefinition, resolveDef)
2520
+ : undefined;
2477
2521
  celRuleApplies =
2478
2522
  !!e.definition?.schema && capability !== undefined && capability !== "Telo.Template";
2479
2523
  if (celRuleApplies) {
@@ -2487,15 +2531,18 @@ export class StaticAnalyzer {
2487
2531
  // disagreeing about what the manifest means.
2488
2532
  const ownSchema = effectiveAuthorSchema(
2489
2533
  e.definition as unknown as ResourceDefinition,
2490
- (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
2534
+ resolveDef,
2491
2535
  ) as Record<string, any>;
2492
2536
  const capabilityDef = capability ? defs.resolve(capability) : undefined;
2493
2537
  // A `Telo.Provider`'s fields are implicitly compile-eval — the
2494
2538
  // capability abstract carries the root annotation — so its reads are
2495
- // covered here without the provider restating anything.
2539
+ // covered here without the provider restating anything. A base-form
2540
+ // child's own fields are too (`implicitEvalSites`): `base:` reads
2541
+ // them once at create().
2496
2542
  celSites = mergeCelEvalSites(
2497
2543
  celEvalSites(ownSchema),
2498
2544
  celEvalSites(capabilityDef?.schema as Record<string, any> | undefined),
2545
+ implicitEvalSites(e.definition as { base?: unknown }),
2499
2546
  );
2500
2547
  } else {
2501
2548
  celSites = NO_CEL_EVAL_SITES;
@@ -2801,10 +2848,16 @@ export class StaticAnalyzer {
2801
2848
  // Warn about declared variables / secrets / ports that no CEL references.
2802
2849
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
2803
2850
 
2804
- // A `!ref` at a definition's dispatch slot must name a sibling `resources:`
2805
- // entry — the slot no reference pass reaches, so the tag would otherwise
2806
- // advertise a resolution nothing performs.
2807
- diagnostics.push(...validateTemplateDispatch(allManifests, rootModules));
2851
+ // A template body's reference surface its entry names, its dispatch slots
2852
+ // and the ref slots inside each entry — which no reference pass reaches,
2853
+ // since a `Telo.Definition` is in both skip sets.
2854
+ diagnostics.push(
2855
+ ...validateTemplateBody(allManifests, defs, aliases, aliasesByModule, rootModules),
2856
+ );
2857
+
2858
+ // A library's export list, resolved against what it declares — otherwise a
2859
+ // listed name that exists nowhere fails in the consumer's file.
2860
+ diagnostics.push(...validateExports(allManifests, defs, aliases, rootModules));
2808
2861
 
2809
2862
  // A library's declared resource inputs, and every import that supplies them.
2810
2863
  diagnostics.push(
package/src/builtins.ts CHANGED
@@ -23,6 +23,62 @@ const observedStateSlot = {
23
23
  "What a resource of this kind reports while running, published at `resources.<name>.status.<field>`.",
24
24
  $ref: manifestFragmentRef("JsonSchema7"),
25
25
  };
26
+
27
+ /** The `self`-only CEL scope a dispatch slot's name template is written in. */
28
+ const dispatchSelfContext = {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ properties: { self: { "x-telo-context-from-root": "schema" } },
32
+ };
33
+
34
+ /**
35
+ * A `Telo.Definition` dispatch slot: which `resources:` entry receives the call.
36
+ *
37
+ * Three shapes, and the union is what keeps the slot CONSTRAINED for manifests
38
+ * no pass walks — `validate-template-body` resolves the target and its
39
+ * capability, but it is entry-scoped, so a dependency's definition would
40
+ * otherwise have nothing checking this slot at all. An array or a number fails
41
+ * every branch here, which is the floor AJV used to provide.
42
+ *
43
+ * `anyOf`, never `oneOf`: a `!ref` sentinel is an object, so it would match both
44
+ * the sentinel branch and any permissive object branch and then fail for
45
+ * matching twice. That is also how the sentinel used to pass — the legacy object
46
+ * branch required nothing, so it accepted one by accident; here it is a branch
47
+ * of its own that says so.
48
+ */
49
+ function dispatchSlot(title: string, description: string): Record<string, unknown> {
50
+ return {
51
+ title,
52
+ description,
53
+ anyOf: [
54
+ // `!ref <entry>` — the spelling to write.
55
+ {
56
+ type: "object",
57
+ required: ["__tagged", "engine", "source"],
58
+ properties: {
59
+ __tagged: { const: true },
60
+ engine: { const: "ref" },
61
+ source: { type: "string", minLength: 1 },
62
+ },
63
+ additionalProperties: false,
64
+ },
65
+ // Legacy: a name template expanded against `self`. Read forever, because
66
+ // published artifacts carry it; `DEPRECATED_TEMPLATE_DISPATCH_FORM` is
67
+ // what moves an author off it.
68
+ { type: "string", "x-telo-context": dispatchSelfContext },
69
+ // Legacy: `{ kind?, name }`, where `name` is that same template.
70
+ {
71
+ type: "object",
72
+ required: ["name"],
73
+ properties: {
74
+ kind: { type: "string" },
75
+ name: { type: "string", "x-telo-context": dispatchSelfContext },
76
+ },
77
+ additionalProperties: true,
78
+ },
79
+ ],
80
+ };
81
+ }
26
82
  import type { ResourceDefinition } from "@telorun/sdk";
27
83
 
28
84
  /** Descriptive provenance a module declares about itself, shared by
@@ -480,99 +536,31 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
480
536
  },
481
537
  },
482
538
  },
483
- invoke: {
484
- oneOf: [
485
- {
486
- type: "string",
487
- "x-telo-context": {
488
- type: "object",
489
- additionalProperties: false,
490
- properties: {
491
- self: { "x-telo-context-from-root": "schema" },
492
- },
493
- },
494
- },
495
- {
496
- type: "object",
497
- additionalProperties: true,
498
- properties: {
499
- kind: { type: "string" },
500
- name: {
501
- type: "string",
502
- "x-telo-context": {
503
- type: "object",
504
- additionalProperties: false,
505
- properties: {
506
- self: { "x-telo-context-from-root": "schema" },
507
- },
508
- },
509
- },
510
- },
511
- },
512
- ],
513
- },
514
- provide: {
515
- type: "object",
516
- additionalProperties: true,
517
- properties: {
518
- kind: { type: "string" },
519
- name: {
520
- type: "string",
521
- "x-telo-context": {
522
- type: "object",
523
- additionalProperties: false,
524
- properties: {
525
- self: { "x-telo-context-from-root": "schema" },
526
- },
527
- },
528
- },
529
- },
530
- },
531
- run: {
532
- type: "string",
533
- "x-telo-context": {
534
- type: "object",
535
- additionalProperties: false,
536
- properties: {
537
- self: { "x-telo-context-from-root": "schema" },
538
- },
539
- },
540
- },
541
- // Mount dispatch: names the `resources:` entry (a Telo.Mount, e.g. an
542
- // Http.Api) whose `register()` this definition delegates to. Same
543
- // string / { kind, name } grammar as `invoke:`. The named child stays
544
- // persistent so the produced mount's routes can `!ref` its siblings.
545
- mount: {
546
- oneOf: [
547
- {
548
- type: "string",
549
- "x-telo-context": {
550
- type: "object",
551
- additionalProperties: false,
552
- properties: {
553
- self: { "x-telo-context-from-root": "schema" },
554
- },
555
- },
556
- },
557
- {
558
- type: "object",
559
- additionalProperties: true,
560
- properties: {
561
- kind: { type: "string" },
562
- name: {
563
- type: "string",
564
- "x-telo-context": {
565
- type: "object",
566
- additionalProperties: false,
567
- properties: {
568
- self: { "x-telo-context-from-root": "schema" },
569
- },
570
- },
571
- },
572
- },
573
- },
574
- ],
575
- },
539
+ // A dispatch slot names the `resources:` entry that receives the call,
540
+ // as `!ref <entry>` — the one spelling every reference in Telo has. The
541
+ // string and `{ kind, name }` forms it used to admit (a CEL name
542
+ // template matched against CEL-named entries) were the removed
543
+ // reference object surviving in the one place nothing resolved it;
544
+ // `validate-template-body` is what resolves this slot, so the schema
545
+ // stays open here rather than reporting the same defect twice.
546
+ invoke: dispatchSlot(
547
+ "Invoke target",
548
+ "The `resources:` entry whose `invoke()` this kind dispatches to, as `!ref <entry>`.",
549
+ ),
550
+ provide: dispatchSlot(
551
+ "Provide target",
552
+ "The `resources:` entry whose `invoke()` produces this provider's value, as `!ref <entry>`.",
553
+ ),
554
+ run: dispatchSlot(
555
+ "Run target",
556
+ "The `resources:` entry whose `run()` this kind dispatches to, as `!ref <entry>`.",
557
+ ),
558
+ // The named child stays persistent so the produced mount's routes can
559
+ // `!ref` its siblings.
560
+ mount: dispatchSlot(
561
+ "Mount target",
562
+ "The `resources:` entry (a Telo.Mount, e.g. an Http.Api) whose `register()` this kind delegates to, as `!ref <entry>`.",
563
+ ),
576
564
  inputs: {
577
565
  type: "object",
578
566
  additionalProperties: true,
@@ -593,11 +581,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
593
581
  additionalProperties: false,
594
582
  properties: {
595
583
  self: { "x-telo-context-from-root": "schema" },
584
+ // Typed from the dispatch target's declared output: the slot
585
+ // holds a `!ref` to a `resources:` entry, resolved to that
586
+ // entry's kind (or its own `outputType`, where it narrows one).
596
587
  result: {
597
- "x-telo-context-from-ref-kind": [
598
- "provide/kind#outputType",
599
- "invoke/kind#outputType",
600
- ],
588
+ "x-telo-context-from-ref-kind": ["provide#outputType", "invoke#outputType"],
601
589
  },
602
590
  },
603
591
  },