@telorun/analyzer 0.52.0 → 0.54.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/analysis-registry.d.ts +8 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +21 -3
- package/dist/analyzer.d.ts +3 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +193 -26
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +32 -12
- package/dist/call-graph.d.ts +189 -0
- package/dist/call-graph.d.ts.map +1 -0
- package/dist/call-graph.js +617 -0
- package/dist/dependency-graph.d.ts +17 -7
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +36 -65
- package/dist/flatten-for-analyzer.d.ts +8 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +32 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/manifest-navigation.d.ts +32 -0
- package/dist/manifest-navigation.d.ts.map +1 -0
- package/dist/manifest-navigation.js +91 -0
- package/dist/manifest-visitor.js +1 -1
- package/dist/ref-slot.d.ts +125 -0
- package/dist/ref-slot.d.ts.map +1 -0
- package/dist/ref-slot.js +226 -0
- package/dist/reference-field-map.d.ts +15 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +29 -35
- package/dist/resolve-schema-ref-kinds.d.ts +4 -0
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +31 -8
- package/dist/resolve-zone-requirements.d.ts +110 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -0
- package/dist/resolve-zone-requirements.js +541 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-module-metadata.d.ts +38 -0
- package/dist/validate-module-metadata.d.ts.map +1 -0
- package/dist/validate-module-metadata.js +256 -0
- package/dist/validate-observed-state.d.ts +14 -13
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +21 -88
- package/dist/validate-ref-slots.d.ts +48 -0
- package/dist/validate-ref-slots.d.ts.map +1 -0
- package/dist/validate-ref-slots.js +219 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -1
- package/dist/validate-zone-slots.d.ts +39 -0
- package/dist/validate-zone-slots.d.ts.map +1 -0
- package/dist/validate-zone-slots.js +114 -0
- package/dist/zone-module-documents.d.ts +27 -0
- package/dist/zone-module-documents.d.ts.map +1 -0
- package/dist/zone-module-documents.js +1 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -0
- package/dist/zone-slot.js +91 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +20 -2
- package/src/analyzer.ts +211 -24
- package/src/builtins.ts +32 -12
- package/src/call-graph.ts +827 -0
- package/src/dependency-graph.ts +34 -68
- package/src/flatten-for-analyzer.ts +32 -0
- package/src/index.ts +51 -0
- package/src/manifest-navigation.ts +91 -0
- package/src/manifest-visitor.ts +1 -1
- package/src/ref-slot.ts +273 -0
- package/src/reference-field-map.ts +39 -36
- package/src/resolve-schema-ref-kinds.ts +34 -7
- package/src/resolve-zone-requirements.ts +781 -0
- package/src/types.ts +8 -0
- package/src/validate-module-metadata.ts +335 -0
- package/src/validate-observed-state.ts +26 -92
- package/src/validate-ref-slots.ts +293 -0
- package/src/validate-references.ts +8 -1
- package/src/validate-zone-slots.ts +175 -0
- package/src/zone-module-documents.ts +27 -0
- package/src/zone-slot.ts +116 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { distance } from "./levenshtein.js";
|
|
2
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
3
|
+
const SOURCE = "telo-analyzer";
|
|
4
|
+
/**
|
|
5
|
+
* Static validation of the `metadata:` block on module docs (`Telo.Application` /
|
|
6
|
+
* `Telo.Library`) and of `metadata.deprecated` wherever it appears.
|
|
7
|
+
*
|
|
8
|
+
* These fields are descriptive — nothing in the kernel branches on them — but they
|
|
9
|
+
* are the module's public face: a hub indexes them, and a consumer reads them
|
|
10
|
+
* before deciding to import. That is exactly why they need checking. A field the
|
|
11
|
+
* runtime ignores has no failure mode that would ever surface it, so a mistyped
|
|
12
|
+
* `licence:` or `deprecatd:` is invisible forever, and the module ships claiming
|
|
13
|
+
* nothing while its author believes otherwise.
|
|
14
|
+
*
|
|
15
|
+
* The vocabulary stays **open** — `metadata` accepts any key, because a publisher
|
|
16
|
+
* may carry their own — so an unknown key is only reported when it is a near-miss
|
|
17
|
+
* of a known one. That catches the typo without closing the set.
|
|
18
|
+
*
|
|
19
|
+
* **Everything here is a WARNING, and fatal only at `telo publish`** (see
|
|
20
|
+
* {@link PUBLISH_BLOCKING_CODES}). Refusing to *run* a manifest over a field no
|
|
21
|
+
* runtime reads gets the cost backwards: `version: 1.0` is a YAML float rather
|
|
22
|
+
* than a string, which is a real mistake worth reporting, but stopping the app
|
|
23
|
+
* from starting over it is worse than the mistake. Publication is the moment
|
|
24
|
+
* these fields become consequential — they are projected onto the artifact's
|
|
25
|
+
* annotations and indexed by the hub — so that is where they block.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Codes that must not block running a manifest but MUST block publishing one.
|
|
29
|
+
*
|
|
30
|
+
* Kept as a set rather than a severity because the two audiences differ: a
|
|
31
|
+
* developer running a manifest wants to know, a publisher must be stopped. If a
|
|
32
|
+
* later check earns the same treatment, add its code here rather than inventing
|
|
33
|
+
* a third severity level.
|
|
34
|
+
*/
|
|
35
|
+
export const PUBLISH_BLOCKING_CODES = new Set([
|
|
36
|
+
"METADATA_INVALID_TYPE",
|
|
37
|
+
"METADATA_UNKNOWN_FIELD",
|
|
38
|
+
"INVALID_DEPRECATION",
|
|
39
|
+
"DEPRECATION_REPLACEMENT_UNRESOLVED",
|
|
40
|
+
]);
|
|
41
|
+
/** Conventional module-doc metadata, and the type each carries. Descriptive only;
|
|
42
|
+
* `name` is the sole field anything resolves against. */
|
|
43
|
+
const MODULE_METADATA_TYPES = {
|
|
44
|
+
name: "string",
|
|
45
|
+
module: "string",
|
|
46
|
+
version: "string",
|
|
47
|
+
description: "string",
|
|
48
|
+
repository: "string",
|
|
49
|
+
homepage: "string",
|
|
50
|
+
documentation: "string",
|
|
51
|
+
license: "string",
|
|
52
|
+
namespace: "string",
|
|
53
|
+
categories: "string[]",
|
|
54
|
+
deprecated: "object",
|
|
55
|
+
};
|
|
56
|
+
/** What a kind doc's `metadata:` may carry.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately narrower than a module's: `version`, `license` and the rest
|
|
59
|
+
* belong to the module, and a kind restating them means nothing. `categories`
|
|
60
|
+
* is legal and *replaces* the module's for that kind; `description` is hub
|
|
61
|
+
* search text. Both have exactly the failure mode this file exists for — a
|
|
62
|
+
* `descriptoin:` on a kind doc is read by nothing and reported by nothing, so
|
|
63
|
+
* it ships silently — which is why kind docs are checked rather than exempt. */
|
|
64
|
+
const KIND_METADATA_TYPES = {
|
|
65
|
+
name: "string",
|
|
66
|
+
module: "string",
|
|
67
|
+
description: "string",
|
|
68
|
+
categories: "string[]",
|
|
69
|
+
deprecated: "object",
|
|
70
|
+
};
|
|
71
|
+
/** Alias-qualified kind — `Self.Migrations`, `Cache.Store`, `Telo.JsonSchema`. */
|
|
72
|
+
const ALIAS_KIND_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
|
|
73
|
+
/** The built-in namespace, resolvable without an import — mirrors `validate-extends`. */
|
|
74
|
+
const TELO_BUILTIN_ALIAS = "Telo";
|
|
75
|
+
function typeOf(value) {
|
|
76
|
+
if (typeof value === "string")
|
|
77
|
+
return "string";
|
|
78
|
+
if (Array.isArray(value))
|
|
79
|
+
return value.every((v) => typeof v === "string") ? "string[]" : "other";
|
|
80
|
+
if (value !== null && typeof value === "object")
|
|
81
|
+
return "object";
|
|
82
|
+
return "other";
|
|
83
|
+
}
|
|
84
|
+
export function validateModuleMetadata(manifests, registry, aliases) {
|
|
85
|
+
const out = [];
|
|
86
|
+
// Docs forwarded from imported libraries carry `metadata.module` set to that
|
|
87
|
+
// library's name. Their `replacedBy` aliases — `Self`, or any alias private to
|
|
88
|
+
// that library — belong to the library's OWN scope, which the consumer's
|
|
89
|
+
// resolver knows nothing about, so re-checking them here reports a false
|
|
90
|
+
// DEPRECATION_REPLACEMENT_UNRESOLVED against a manifest the consumer does not
|
|
91
|
+
// own. They are validated when that library is analyzed as a root, which is
|
|
92
|
+
// its author's concern. Same rule, and the same reason, as `validate-extends`.
|
|
93
|
+
const importedModules = new Set();
|
|
94
|
+
for (const m of manifests) {
|
|
95
|
+
if (m.kind !== "Telo.Import")
|
|
96
|
+
continue;
|
|
97
|
+
const resolved = m.metadata
|
|
98
|
+
?.resolvedModuleName;
|
|
99
|
+
if (resolved)
|
|
100
|
+
importedModules.add(resolved);
|
|
101
|
+
}
|
|
102
|
+
for (const manifest of manifests) {
|
|
103
|
+
const isModuleDoc = manifest.kind === "Telo.Application" || manifest.kind === "Telo.Library";
|
|
104
|
+
const isKindDoc = manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract";
|
|
105
|
+
if (!isModuleDoc && !isKindDoc)
|
|
106
|
+
continue;
|
|
107
|
+
const metadata = manifest.metadata;
|
|
108
|
+
if (!metadata)
|
|
109
|
+
continue;
|
|
110
|
+
const ownModule = metadata.module;
|
|
111
|
+
if (ownModule && importedModules.has(ownModule))
|
|
112
|
+
continue;
|
|
113
|
+
const name = typeof metadata.name === "string" ? metadata.name : undefined;
|
|
114
|
+
const filePath = typeof metadata.source === "string" ? metadata.source : undefined;
|
|
115
|
+
const ctx = {
|
|
116
|
+
label: `${manifest.kind}/${name ?? "(unnamed)"}`,
|
|
117
|
+
resource: { kind: manifest.kind, name },
|
|
118
|
+
filePath,
|
|
119
|
+
};
|
|
120
|
+
validateFieldTypes(metadata, isModuleDoc ? MODULE_METADATA_TYPES : KIND_METADATA_TYPES, ctx, out);
|
|
121
|
+
validateDeprecation(metadata, isModuleDoc, ctx, registry, aliases, out);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/** How far a key may be from a known one and still be called a typo.
|
|
126
|
+
*
|
|
127
|
+
* Scaled, not absolute: at a flat 2, `date:` (a perfectly ordinary key an
|
|
128
|
+
* author might carry) is two edits from `name` and gets told it is a
|
|
129
|
+
* misspelling of it. The vocabulary is open, so a false accusation on a short
|
|
130
|
+
* key is worse than missing a typo on one. */
|
|
131
|
+
function typoThreshold(key, known) {
|
|
132
|
+
return Math.max(1, Math.floor(Math.min(key.length, known.length) / 3));
|
|
133
|
+
}
|
|
134
|
+
function validateFieldTypes(metadata, allowed, ctx, out) {
|
|
135
|
+
const known = Object.keys(allowed);
|
|
136
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
137
|
+
// Stamped by the loader, not authored — never a typo to report on.
|
|
138
|
+
if (key === "source")
|
|
139
|
+
continue;
|
|
140
|
+
// Listed as known so a typo still gets suggested against it, but its shape
|
|
141
|
+
// belongs to `validateDeprecation`, which can say what is actually wrong.
|
|
142
|
+
// Type-checking it here too would report one mistake twice.
|
|
143
|
+
if (key === "deprecated")
|
|
144
|
+
continue;
|
|
145
|
+
const expected = allowed[key];
|
|
146
|
+
if (expected === undefined) {
|
|
147
|
+
const near = known.find((k) => distance(key, k) <= typoThreshold(key, k));
|
|
148
|
+
if (near) {
|
|
149
|
+
out.push({
|
|
150
|
+
severity: DiagnosticSeverity.Warning,
|
|
151
|
+
code: "METADATA_UNKNOWN_FIELD",
|
|
152
|
+
source: SOURCE,
|
|
153
|
+
message: `${ctx.label}: 'metadata.${key}' is not a known field — did you mean '${near}'? ` +
|
|
154
|
+
`Nothing reads an unrecognized key, so this declares nothing.`,
|
|
155
|
+
data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (typeOf(value) !== expected) {
|
|
161
|
+
out.push({
|
|
162
|
+
severity: DiagnosticSeverity.Warning,
|
|
163
|
+
code: "METADATA_INVALID_TYPE",
|
|
164
|
+
source: SOURCE,
|
|
165
|
+
message: `${ctx.label}: 'metadata.${key}' must be ${describeType(expected)}.`,
|
|
166
|
+
data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function describeType(t) {
|
|
172
|
+
if (t === "string[]")
|
|
173
|
+
return "an array of strings";
|
|
174
|
+
if (t === "object")
|
|
175
|
+
return "an object";
|
|
176
|
+
return "a string";
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* `metadata.deprecated: { reason, replacedBy? }`.
|
|
180
|
+
*
|
|
181
|
+
* `replacedBy` is deliberately resolvable rather than free text, and its form
|
|
182
|
+
* follows the level: a module doc names another **module ref** (the `imports:`
|
|
183
|
+
* source grammar), a kind doc names an **alias-qualified kind** resolved through
|
|
184
|
+
* this file's own imports — the same grammar `kind:` / `extends:` use, so the
|
|
185
|
+
* replacement is a link a consumer can follow rather than a sentence they have to
|
|
186
|
+
* interpret.
|
|
187
|
+
*
|
|
188
|
+
* A kind whose replacement lives in a module this one does not import cannot be
|
|
189
|
+
* named; that case deprecates at module level with a module ref instead. Accepted
|
|
190
|
+
* over inventing a second grammar for it.
|
|
191
|
+
*/
|
|
192
|
+
function validateDeprecation(metadata, isModuleDoc, ctx, registry, aliases, out) {
|
|
193
|
+
const deprecated = metadata.deprecated;
|
|
194
|
+
if (deprecated === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const at = "metadata.deprecated";
|
|
197
|
+
const push = (code, message, path = at, severity = DiagnosticSeverity.Warning) => {
|
|
198
|
+
out.push({
|
|
199
|
+
severity,
|
|
200
|
+
code,
|
|
201
|
+
source: SOURCE,
|
|
202
|
+
message: `${ctx.label}: ${message}`,
|
|
203
|
+
data: { resource: ctx.resource, filePath: ctx.filePath, path },
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
if (typeOf(deprecated) !== "object") {
|
|
207
|
+
push("INVALID_DEPRECATION", `'${at}' must be an object with a 'reason' (and an optional 'replacedBy'). ` +
|
|
208
|
+
`A bare 'true' says a thing is deprecated without saying what to do instead.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const block = deprecated;
|
|
212
|
+
const allowed = new Set(["reason", "replacedBy"]);
|
|
213
|
+
for (const key of Object.keys(block)) {
|
|
214
|
+
if (!allowed.has(key)) {
|
|
215
|
+
push("INVALID_DEPRECATION", `'${at}.${key}' is not a recognized key (reason, replacedBy).`, `${at}.${key}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (typeof block.reason !== "string" || block.reason.trim() === "") {
|
|
219
|
+
push("INVALID_DEPRECATION", `'${at}.reason' is required and must be a non-empty string — it is what a consumer reads to know what to do instead.`, `${at}.reason`);
|
|
220
|
+
}
|
|
221
|
+
const replacedBy = block.replacedBy;
|
|
222
|
+
if (replacedBy === undefined)
|
|
223
|
+
return;
|
|
224
|
+
const path = `${at}.replacedBy`;
|
|
225
|
+
if (typeof replacedBy !== "string" || replacedBy.trim() === "") {
|
|
226
|
+
push("INVALID_DEPRECATION", `'${path}' must be a non-empty string.`, path);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (isModuleDoc) {
|
|
230
|
+
// A module is replaced by another module, addressed the way an import is.
|
|
231
|
+
// Catching alias form here is worth a dedicated message: it is the natural
|
|
232
|
+
// mistake, and it would otherwise be stored as an unresolvable ref.
|
|
233
|
+
if (ALIAS_KIND_RE.test(replacedBy)) {
|
|
234
|
+
push("INVALID_DEPRECATION", `'${path}: ${replacedBy}' looks like a kind reference, but a module doc's replacement is a ` +
|
|
235
|
+
`module ref (e.g. 'oci://ghcr.io/acme/thing'). Deprecate the kind itself to point at another kind.`, path);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Kind level: resolve through this file's imports, exactly as `extends` does.
|
|
240
|
+
if (!ALIAS_KIND_RE.test(replacedBy)) {
|
|
241
|
+
push("INVALID_DEPRECATION", `'${path}: ${replacedBy}' must be an alias-qualified kind ("<Alias>.<Kind>", ` +
|
|
242
|
+
`e.g. 'Self.Migrations'), resolved via this file's imports.`, path);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const prefix = replacedBy.slice(0, replacedBy.indexOf("."));
|
|
246
|
+
if (prefix !== TELO_BUILTIN_ALIAS && !aliases.hasAlias(prefix)) {
|
|
247
|
+
push("DEPRECATION_REPLACEMENT_UNRESOLVED", `'${path}: ${replacedBy}' — alias '${prefix}' is not an import in this file's scope. ` +
|
|
248
|
+
`Declare the import or correct the alias.`, path);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const canonical = aliases.resolveKind(replacedBy);
|
|
252
|
+
if (!canonical || !registry.resolve(canonical)) {
|
|
253
|
+
push("DEPRECATION_REPLACEMENT_UNRESOLVED", `'${path}: ${replacedBy}' does not resolve to a known kind. A replacement a consumer ` +
|
|
254
|
+
`cannot follow is no better than none.`, path);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { type ModuleScopes } from "./alias-resolver.js";
|
|
3
|
+
import type { CallGraph } from "./call-graph.js";
|
|
3
4
|
/**
|
|
4
5
|
* The `status:` block's own schema — a plain JSON Schema, structurally. The one
|
|
5
6
|
* normative restriction (`required:` is rejected) is enforced by
|
|
@@ -45,21 +46,21 @@ export interface ObservedStateRead {
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function observedStateRead(chain: readonly string[]): ObservedStateRead | undefined;
|
|
47
48
|
/**
|
|
48
|
-
* The names of every resource some slot can start
|
|
49
|
-
* that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
|
|
50
|
-
* `invoke:` target. A resource in none of them can never `run()`, so it can
|
|
51
|
-
* never report observed state.
|
|
49
|
+
* The names of every resource some slot can start.
|
|
52
50
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
51
|
+
* One question, one answer: a resource is run-reachable when a control-
|
|
52
|
+
* transferring edge reaches it in the typed reference graph. `call`, `detached`,
|
|
53
|
+
* `trigger.inbound` and `trigger.consumer` all mean control arrives; `schema` and
|
|
54
|
+
* `dependency` mean it never does.
|
|
55
|
+
*
|
|
56
|
+
* This replaced two independent over-approximations that had to agree by
|
|
57
|
+
* coincidence: a field-map scan keeping slots whose *constraint capability*
|
|
58
|
+
* looked runnable, plus an untyped whole-manifest scan for the declared step
|
|
59
|
+
* invoke key at any depth. Both were guesses at the question `use` now answers —
|
|
60
|
+
* and the first was wrong in the direction that rejects valid manifests, since a
|
|
61
|
+
* slot constrained to `Telo.Invocable` can still be dispatched through `run()`.
|
|
57
62
|
*/
|
|
58
|
-
export declare function collectRunReachableNames(
|
|
59
|
-
resolve(kind: string): ResourceDefinition | undefined;
|
|
60
|
-
}, aliases?: {
|
|
61
|
-
resolveKind(kind: string): string | undefined;
|
|
62
|
-
}): Set<string>;
|
|
63
|
+
export declare function collectRunReachableNames(graph: CallGraph): Set<string>;
|
|
63
64
|
/** What a resource name resolves to for CEL purposes. `status` is present only
|
|
64
65
|
* when the kind declares one; `scoped` marks a resource declared inside an
|
|
65
66
|
* `x-telo-scope` slot, which resolves only within that scope's regions. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-observed-state.d.ts","sourceRoot":"","sources":["../src/validate-observed-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,
|
|
1
|
+
{"version":3,"file":"validate-observed-state.d.ts","sourceRoot":"","sources":["../src/validate-observed-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAgBjD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;CAGjC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB3E;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC;+CAC2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAOzF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAQtE;AAGD;;4EAE4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,EAC/G,MAAM,CAAC,EAAE,YAAY,GACpB,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsC/B;AAyDD;;;gDAGgD;AAChD,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC5C,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAmBN"}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
2
2
|
import { effectiveStatusSchema } from "./extends-resolution.js";
|
|
3
3
|
import { parseExportEntry } from "./flatten-for-analyzer.js";
|
|
4
|
-
import { moduleScopedDefResolver } from "./alias-resolver.js";
|
|
5
|
-
import { buildReferenceFieldMap,
|
|
6
|
-
/** The kernel capabilities whose `run()` the kernel dispatches. A ref slot that
|
|
7
|
-
* accepts one of them is a slot that can start a resource — `targets:` on an
|
|
8
|
-
* Application or a `Run.Sequence`, and a step's `invoke:` (whose schema accepts
|
|
9
|
-
* `Telo.Runnable` alongside `Telo.Invocable`, and which the kernel dispatches
|
|
10
|
-
* through `run()` when the target has no `invoke()`). Keyed on the declared
|
|
11
|
-
* capability, never on a field name or a kind, so any composer that accepts a
|
|
12
|
-
* runnable participates without the analyzer knowing about it. */
|
|
13
|
-
const RUN_DISPATCH_CONTRACTS = new Set(["Telo.Runnable", "Telo.Service"]);
|
|
4
|
+
import { moduleScopedDefResolver, } from "./alias-resolver.js";
|
|
5
|
+
import { buildReferenceFieldMap, isScopeEntry, resolveFieldValues, } from "./reference-field-map.js";
|
|
14
6
|
const SYSTEM_KINDS = new Set([
|
|
15
7
|
"Telo.Definition",
|
|
16
8
|
"Telo.Abstract",
|
|
@@ -77,89 +69,30 @@ export function observedStateRead(chain) {
|
|
|
77
69
|
return undefined;
|
|
78
70
|
}
|
|
79
71
|
/**
|
|
80
|
-
* The names of every resource some slot can start
|
|
81
|
-
* that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
|
|
82
|
-
* `invoke:` target. A resource in none of them can never `run()`, so it can
|
|
83
|
-
* never report observed state.
|
|
72
|
+
* The names of every resource some slot can start.
|
|
84
73
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
74
|
+
* One question, one answer: a resource is run-reachable when a control-
|
|
75
|
+
* transferring edge reaches it in the typed reference graph. `call`, `detached`,
|
|
76
|
+
* `trigger.inbound` and `trigger.consumer` all mean control arrives; `schema` and
|
|
77
|
+
* `dependency` mean it never does.
|
|
78
|
+
*
|
|
79
|
+
* This replaced two independent over-approximations that had to agree by
|
|
80
|
+
* coincidence: a field-map scan keeping slots whose *constraint capability*
|
|
81
|
+
* looked runnable, plus an untyped whole-manifest scan for the declared step
|
|
82
|
+
* invoke key at any depth. Both were guesses at the question `use` now answers —
|
|
83
|
+
* and the first was wrong in the direction that rejects valid manifests, since a
|
|
84
|
+
* slot constrained to `Telo.Invocable` can still be dispatched through `run()`.
|
|
89
85
|
*/
|
|
90
|
-
export function collectRunReachableNames(
|
|
86
|
+
export function collectRunReachableNames(graph) {
|
|
87
|
+
// By NAME, not by resolved node: a `with:`-scoped resource is started by its
|
|
88
|
+
// sequence's `targets:` while never being a top-level node, so resolving first
|
|
89
|
+
// would report it as unstartable. The graph is passed in, never built here —
|
|
90
|
+
// one build per analysis, shared with every other graph consumer.
|
|
91
91
|
const names = new Set();
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const def = resolve(manifest.kind);
|
|
95
|
-
const schema = def?.schema;
|
|
96
|
-
if (!schema)
|
|
97
|
-
continue;
|
|
98
|
-
for (const [path, entry] of buildReferenceFieldMap(schema)) {
|
|
99
|
-
if (!isRefEntry(entry))
|
|
100
|
-
continue;
|
|
101
|
-
if (!entry.refs.some((ref) => RUN_DISPATCH_CONTRACTS.has(ref)))
|
|
102
|
-
continue;
|
|
103
|
-
for (const value of resolveFieldValues(manifest, path))
|
|
104
|
-
collectRefName(value, names);
|
|
105
|
-
}
|
|
106
|
-
// Step arrays nest through `if` / `while` / `switch` / `try`, and the step
|
|
107
|
-
// `invoke:` slot sits behind a local `$ref` the field map does not follow.
|
|
108
|
-
// Match the declared invoke key at any depth instead of re-deriving the
|
|
109
|
-
// nesting rules — over-approximating in the safe direction.
|
|
110
|
-
const invokeKey = stepInvokeKey(schema);
|
|
111
|
-
if (invokeKey)
|
|
112
|
-
collectKeyedRefs(manifest, invokeKey, names);
|
|
113
|
-
}
|
|
92
|
+
for (const edge of graph.controlEdges())
|
|
93
|
+
names.add(edge.toName);
|
|
114
94
|
return names;
|
|
115
95
|
}
|
|
116
|
-
/** The property name a kind's `x-telo-step-context` declares as its dispatch
|
|
117
|
-
* slot (`invoke`), or undefined when the kind has no step array. */
|
|
118
|
-
function stepInvokeKey(schema) {
|
|
119
|
-
for (const fieldSchema of Object.values((schema.properties ?? {}))) {
|
|
120
|
-
const stepCtx = fieldSchema?.["x-telo-step-context"];
|
|
121
|
-
if (stepCtx?.invoke)
|
|
122
|
-
return stepCtx.invoke;
|
|
123
|
-
}
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
/** Collect ref names at every `key` property anywhere in `node`. */
|
|
127
|
-
function collectKeyedRefs(node, key, out) {
|
|
128
|
-
if (Array.isArray(node)) {
|
|
129
|
-
for (const item of node)
|
|
130
|
-
collectKeyedRefs(item, key, out);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (node === null || typeof node !== "object")
|
|
134
|
-
return;
|
|
135
|
-
for (const [k, value] of Object.entries(node)) {
|
|
136
|
-
if (k === key)
|
|
137
|
-
collectRefName(value, out);
|
|
138
|
-
collectKeyedRefs(value, key, out);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/** Record the resource name a slot value points at — a resolved `{kind, name}`
|
|
142
|
-
* ref, an unresolved `!ref` sentinel, or a `{ ref }` / `{ invoke }` wrapper. */
|
|
143
|
-
function collectRefName(value, out) {
|
|
144
|
-
if (value === null || typeof value !== "object")
|
|
145
|
-
return;
|
|
146
|
-
if (Array.isArray(value)) {
|
|
147
|
-
for (const item of value)
|
|
148
|
-
collectRefName(item, out);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const v = value;
|
|
152
|
-
if (typeof v.name === "string")
|
|
153
|
-
out.add(v.name);
|
|
154
|
-
if (typeof v.source === "string") {
|
|
155
|
-
const dot = v.source.lastIndexOf(".");
|
|
156
|
-
out.add(dot >= 0 ? v.source.slice(dot + 1) : v.source);
|
|
157
|
-
}
|
|
158
|
-
for (const wrapper of ["ref", "invoke"]) {
|
|
159
|
-
if (v[wrapper] !== undefined)
|
|
160
|
-
collectRefName(v[wrapper], out);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
96
|
/**
|
|
164
97
|
* Index every resource a CEL `resources.…` read can name: the module's own
|
|
165
98
|
* top-level resources, the ones declared inside `x-telo-scope` slots (a
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static validation of the `x-telo-ref` annotation itself — the strict half of
|
|
3
|
+
* the accessor split. `readRefSlot` is deliberately lenient (it normalizes
|
|
4
|
+
* whatever it can read, because every surface must keep working mid-migration);
|
|
5
|
+
* this pass reads the RAW annotation and reports what leniency would otherwise
|
|
6
|
+
* silently absorb:
|
|
7
|
+
*
|
|
8
|
+
* - an unrecognized `use` token — a typo like `use: cal` would degrade to the
|
|
9
|
+
* legacy no-use reading, indistinguishable from a slot that never answered;
|
|
10
|
+
* - a structured annotation with no `kind` — the editor would recognise the
|
|
11
|
+
* slot but have nothing to pick against;
|
|
12
|
+
* - a structured annotation with no `use` — the structured form is the
|
|
13
|
+
* declaration that answers the question; omitting it is only legal in the
|
|
14
|
+
* legacy bare-string spelling;
|
|
15
|
+
* - `anyOf` branches whose declared uses disagree — a state with no meaning,
|
|
16
|
+
* since `use` is a property of the slot, never of a branch;
|
|
17
|
+
* - a `use` case map whose selector is written in CEL — a call graph known
|
|
18
|
+
* only at runtime is not statically analyzable, which is the property the
|
|
19
|
+
* typed reference graph exists to protect. There is deliberately no
|
|
20
|
+
* fallback: no single value is conservative for every consumer.
|
|
21
|
+
*
|
|
22
|
+
* Scoping follows `X_TELO_REF_UNRESOLVED`: schema issues are reported only for
|
|
23
|
+
* definitions in the entry's own modules, and the dynamic-selector issue only
|
|
24
|
+
* for manifests in them — a published dependency's slot is not the consumer's
|
|
25
|
+
* to fix.
|
|
26
|
+
*
|
|
27
|
+
* Browser-safe: no Node built-ins.
|
|
28
|
+
*/
|
|
29
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
30
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
31
|
+
import { type CallGraph } from "./call-graph.js";
|
|
32
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
33
|
+
export interface RefSlotIssue {
|
|
34
|
+
code: "X_TELO_REF_INVALID_USE" | "X_TELO_REF_MISSING_USE" | "X_TELO_REF_MISSING_KIND" | "X_TELO_REF_USE_CONFLICT" | "X_TELO_REF_DYNAMIC_SELECTOR";
|
|
35
|
+
/** The definition (schema issues) or resource (selector issues) at fault. */
|
|
36
|
+
manifest: ResourceManifest;
|
|
37
|
+
/** Schema path of the slot (schema issues) or concrete value path of the
|
|
38
|
+
* selector's site (dynamic-selector issues). */
|
|
39
|
+
path: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
/** Schema-level checks over one definition/abstract manifest. */
|
|
43
|
+
export declare function validateRefSlotDeclarations(definition: ResourceManifest): RefSlotIssue[];
|
|
44
|
+
/** Manifest-level check: a `use` case map whose selector is written in CEL.
|
|
45
|
+
* Reads the built graph's `unresolvedReason`, so the detection lives once, in
|
|
46
|
+
* `resolveUseAtSite`, and this pass cannot disagree with what consumers saw. */
|
|
47
|
+
export declare function validateDynamicSelectors(allManifests: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>, graph?: CallGraph): RefSlotIssue[];
|
|
48
|
+
//# sourceMappingURL=validate-ref-slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-ref-slots.d.ts","sourceRoot":"","sources":["../src/validate-ref-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EACA,wBAAwB,GACxB,wBAAwB,GACxB,yBAAyB,GACzB,yBAAyB,GACzB,6BAA6B,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;qDACiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAuJD,iEAAiE;AACjE,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA2CxF;AAED;;iFAEiF;AACjF,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,gBAAgB,EAAE,EAChC,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAC5C,KAAK,CAAC,EAAE,SAAS,GAChB,YAAY,EAAE,CA0BhB"}
|