@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +50 -13
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +71 -97
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -0
- package/dist/release/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- package/dist/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- package/dist/schema-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +9 -4
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +48 -16
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- package/dist/validate-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +141 -60
- package/dist/validate-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -0
- package/package.json +3 -2
- package/src/analyzer.ts +64 -11
- package/src/builtins.ts +85 -97
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/index.ts +1 -0
- package/src/nearest-name.ts +47 -0
- package/src/release/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- package/src/schema-error-report.ts +16 -4
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +25 -0
- package/src/validate-cel-context.ts +57 -16
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- package/src/validate-provider-coherence.ts +17 -127
- package/src/validate-references.ts +141 -60
- package/src/validate-template-body.ts +274 -0
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- 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.
|
|
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.
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2805
|
-
// entry —
|
|
2806
|
-
//
|
|
2807
|
-
diagnostics.push(
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
},
|