@telorun/analyzer 0.46.0 → 0.48.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 +8 -2
- package/dist/artifact-layer-index.d.ts +55 -0
- package/dist/artifact-layer-index.d.ts.map +1 -0
- package/dist/artifact-layer-index.js +116 -0
- package/dist/artifact-selector.d.ts +81 -0
- package/dist/artifact-selector.d.ts.map +1 -0
- package/dist/artifact-selector.js +122 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +58 -19
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/resolve-ref-sentinels.d.ts +13 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +56 -5
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -1
- package/dist/validate-module-artifact.d.ts +27 -0
- package/dist/validate-module-artifact.d.ts.map +1 -0
- package/dist/validate-module-artifact.js +131 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -2
- package/package.json +2 -2
- package/src/analyzer.ts +14 -2
- package/src/artifact-layer-index.ts +162 -0
- package/src/artifact-selector.ts +171 -0
- package/src/builtins.ts +61 -19
- package/src/index.ts +25 -0
- package/src/resolve-ref-sentinels.ts +68 -4
- package/src/validate-cel-context.ts +20 -1
- package/src/validate-module-artifact.ts +141 -0
- package/src/validate-references.ts +8 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { isScopeEntry, resolveFieldEntries, } from "./reference-field-map.js";
|
|
2
3
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
3
4
|
/**
|
|
4
5
|
* Rewrites every `!ref <name>` sentinel in each non-system resource's value tree
|
|
@@ -41,7 +42,11 @@ export function resolveRefSentinels(resources, aliases, aliasesByModule,
|
|
|
41
42
|
// walked as sources). The kernel passes the analyzer-flattened set here so the runtime
|
|
42
43
|
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
43
44
|
// imported libraries' exported instances.
|
|
44
|
-
crossModuleTargets = []
|
|
45
|
+
crossModuleTargets = [],
|
|
46
|
+
/** Supplies each kind's `x-telo-scope` slots. Without it a scoped name cannot be
|
|
47
|
+
* told from a module-level one, and a shadowed `!ref` resolves to the resource
|
|
48
|
+
* it shadows — so both call sites pass it. */
|
|
49
|
+
defs) {
|
|
45
50
|
const moduleOf = (r) => r.metadata?.module;
|
|
46
51
|
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
47
52
|
// cross-module resolution targets only — never walked as local ref sources here.
|
|
@@ -96,11 +101,50 @@ crossModuleTargets = []) {
|
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
103
|
};
|
|
104
|
+
/** Names a resource declares in its own execution scopes, read from the kind's
|
|
105
|
+
* `x-telo-scope` slots — the analyzer's single definition of "scope", shared
|
|
106
|
+
* with `manifest-visitor`. Inferring it structurally instead (any array of
|
|
107
|
+
* named inline manifests) would give scope-local shadowing to the first kind
|
|
108
|
+
* that happens to carry such an array without asking for it, and this pass is
|
|
109
|
+
* shared with the kernel, so the guess would be baked into the runtime tree
|
|
110
|
+
* rather than merely reported. */
|
|
111
|
+
const declaredInScopes = (resource) => {
|
|
112
|
+
const fieldMap = defs?.getFieldMapForKind(resource.kind, aliases);
|
|
113
|
+
if (!fieldMap)
|
|
114
|
+
return undefined;
|
|
115
|
+
let declared;
|
|
116
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
117
|
+
if (!isScopeEntry(entry))
|
|
118
|
+
continue;
|
|
119
|
+
for (const { value } of resolveFieldEntries(resource, fieldPath)) {
|
|
120
|
+
for (const element of Array.isArray(value) ? value : [value]) {
|
|
121
|
+
if (!element || typeof element !== "object" || Array.isArray(element))
|
|
122
|
+
continue;
|
|
123
|
+
const manifest = element;
|
|
124
|
+
const name = manifest.metadata?.name;
|
|
125
|
+
if (typeof manifest.kind === "string" && typeof name === "string") {
|
|
126
|
+
(declared ??= new Map()).set(name, manifest);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return declared;
|
|
132
|
+
};
|
|
99
133
|
// Resolve every `!ref` sentinel in the tree; leave opaque tagged / precompiled
|
|
100
134
|
// nodes (e.g. `!cel`) untouched and don't descend into them.
|
|
101
|
-
|
|
135
|
+
//
|
|
136
|
+
// `scoped` carries the names the enclosing resource declares in its `x-telo-scope`
|
|
137
|
+
// slots, and they SHADOW the module-level ones — the order the runtime resolves
|
|
138
|
+
// in. Baking the module-level kind into a shadowed reference would label traces
|
|
139
|
+
// and `getRefIdentity` with a resource that never runs.
|
|
140
|
+
const walk = (value, scoped) => {
|
|
102
141
|
if (isRefSentinel(value)) {
|
|
103
|
-
|
|
142
|
+
const source = value.source;
|
|
143
|
+
const bare = source.indexOf(".") === -1;
|
|
144
|
+
const shadow = bare ? scoped?.get(source) : undefined;
|
|
145
|
+
if (shadow)
|
|
146
|
+
return { kind: shadow.kind, name: source };
|
|
147
|
+
return resolveTarget(source) ?? value;
|
|
104
148
|
}
|
|
105
149
|
if (value === null || typeof value !== "object")
|
|
106
150
|
return value;
|
|
@@ -110,12 +154,19 @@ crossModuleTargets = []) {
|
|
|
110
154
|
return value;
|
|
111
155
|
if (Array.isArray(value)) {
|
|
112
156
|
for (let i = 0; i < value.length; i++)
|
|
113
|
-
value[i] = walk(value[i]);
|
|
157
|
+
value[i] = walk(value[i], scoped);
|
|
114
158
|
return value;
|
|
115
159
|
}
|
|
116
160
|
const obj = value;
|
|
161
|
+
// A nested inline resource may declare scopes of its own (a `Run.Sequence`
|
|
162
|
+
// inside another sequence's `with:`). Collected before descending, so the
|
|
163
|
+
// declarations are visible to every region of the resource that declares
|
|
164
|
+
// them — a sequence's `with:` names resolve in its `targets:` and `steps:`
|
|
165
|
+
// alike, not only inside `with:` itself.
|
|
166
|
+
const declared = typeof obj.kind === "string" ? declaredInScopes(obj) : undefined;
|
|
167
|
+
const inner = declared ? new Map([...(scoped ?? new Map()), ...declared]) : scoped;
|
|
117
168
|
for (const key of Object.keys(obj))
|
|
118
|
-
obj[key] = walk(obj[key]);
|
|
169
|
+
obj[key] = walk(obj[key], inner);
|
|
119
170
|
return value;
|
|
120
171
|
};
|
|
121
172
|
for (const r of resources) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAmIrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAqBxF"}
|
|
@@ -276,13 +276,29 @@ export function resolveContextAnnotations(schema, manifestItem, opts) {
|
|
|
276
276
|
typeof ref.kind === "string" &&
|
|
277
277
|
typeof ref.name === "string" &&
|
|
278
278
|
subpath) {
|
|
279
|
+
const segments = subpath.split("/");
|
|
279
280
|
const refManifest = allManifests.find((m) => m.kind === ref.kind && m.metadata?.name === ref.name);
|
|
280
281
|
if (refManifest) {
|
|
281
|
-
const resolved = resolveTypeFieldToSchema(navigatePath(refManifest,
|
|
282
|
+
const resolved = resolveTypeFieldToSchema(navigatePath(refManifest, segments), allManifests);
|
|
282
283
|
if (resolved && typeof resolved === "object") {
|
|
283
284
|
return resolved;
|
|
284
285
|
}
|
|
285
286
|
}
|
|
287
|
+
// The instance declares nothing, so fall back to its KIND's declaration —
|
|
288
|
+
// the same layering `buildStepContextSchema` applies to `steps.<name>.result`,
|
|
289
|
+
// so a kind with one fixed output shape (declared once on its Telo.Definition)
|
|
290
|
+
// types the context, while a kind that exposes the field for per-instance
|
|
291
|
+
// narrowing keeps winning above.
|
|
292
|
+
if (defs) {
|
|
293
|
+
const canonical = aliases?.resolveKind(ref.kind) ?? ref.kind;
|
|
294
|
+
const def = defs.resolve(canonical);
|
|
295
|
+
if (def) {
|
|
296
|
+
const resolved = resolveTypeFieldToSchema(navigatePath(def, segments), allManifests);
|
|
297
|
+
if (resolved && typeof resolved === "object") {
|
|
298
|
+
return resolved;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
286
302
|
}
|
|
287
303
|
// Fallback: open schema (no false errors when outputType is not declared)
|
|
288
304
|
return { ...schema, additionalProperties: true };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Static validation of the module-artifact surface — `kernel/specs/module-artifact.md`.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is decidable from the manifest text alone, and every case would
|
|
7
|
+
* otherwise surface on a *consumer's* machine at controller-resolve time (or, worse,
|
|
8
|
+
* not at all). That is the whole argument: an author who mistypes a platform axis
|
|
9
|
+
* gets a platform-neutral candidate, publish emits one layer, and every host
|
|
10
|
+
* happily loads a binary built for one architecture — silently, forever.
|
|
11
|
+
*
|
|
12
|
+
* Two checks. Note that several candidates *sharing* one selector is not among
|
|
13
|
+
* them: a controller layer holds the entry points of every candidate with that
|
|
14
|
+
* selector (spec §1), which is what every module with two `js` controllers relies
|
|
15
|
+
* on.
|
|
16
|
+
*
|
|
17
|
+
* 1. **Controller selector qualifiers.** `os` / `arch` / `libc` / `siblings` are
|
|
18
|
+
* authored surface. An unknown qualifier is reported rather than ignored, since
|
|
19
|
+
* ignoring is what makes a typo invisible; an invalid value is reported here
|
|
20
|
+
* instead of throwing from the loader later.
|
|
21
|
+
* 2. **The published layer index.** The owner doc's JSON Schema covers shape; the
|
|
22
|
+
* semantic rules — controller-requires-selector, singletons carry none, no
|
|
23
|
+
* duplicate selector, the token grammar (`os: Linux` passes the schema and
|
|
24
|
+
* throws at runtime) — live in the parser, so run it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateModuleArtifact(manifests: ResourceManifest[]): AnalysisDiagnostic[];
|
|
27
|
+
//# sourceMappingURL=validate-module-artifact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-module-artifact.d.ts","sourceRoot":"","sources":["../src/validate-module-artifact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,kBAAkB,EAAE,CAO1F"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseLayerIndex, LayerIndexError } from "./artifact-layer-index.js";
|
|
2
|
+
import { ArtifactSelectorError, PLATFORM_AXES, selectorFromQualifiers, } from "./artifact-selector.js";
|
|
3
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
4
|
+
const SOURCE = "telo-analyzer";
|
|
5
|
+
/**
|
|
6
|
+
* Static validation of the module-artifact surface — `kernel/specs/module-artifact.md`.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is decidable from the manifest text alone, and every case would
|
|
9
|
+
* otherwise surface on a *consumer's* machine at controller-resolve time (or, worse,
|
|
10
|
+
* not at all). That is the whole argument: an author who mistypes a platform axis
|
|
11
|
+
* gets a platform-neutral candidate, publish emits one layer, and every host
|
|
12
|
+
* happily loads a binary built for one architecture — silently, forever.
|
|
13
|
+
*
|
|
14
|
+
* Two checks. Note that several candidates *sharing* one selector is not among
|
|
15
|
+
* them: a controller layer holds the entry points of every candidate with that
|
|
16
|
+
* selector (spec §1), which is what every module with two `js` controllers relies
|
|
17
|
+
* on.
|
|
18
|
+
*
|
|
19
|
+
* 1. **Controller selector qualifiers.** `os` / `arch` / `libc` / `siblings` are
|
|
20
|
+
* authored surface. An unknown qualifier is reported rather than ignored, since
|
|
21
|
+
* ignoring is what makes a typo invisible; an invalid value is reported here
|
|
22
|
+
* instead of throwing from the loader later.
|
|
23
|
+
* 2. **The published layer index.** The owner doc's JSON Schema covers shape; the
|
|
24
|
+
* semantic rules — controller-requires-selector, singletons carry none, no
|
|
25
|
+
* duplicate selector, the token grammar (`os: Linux` passes the schema and
|
|
26
|
+
* throws at runtime) — live in the parser, so run it.
|
|
27
|
+
*/
|
|
28
|
+
export function validateModuleArtifact(manifests) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const manifest of manifests) {
|
|
31
|
+
validateLayerIndex(manifest, out);
|
|
32
|
+
validateControllerSelectors(manifest, out);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const KNOWN_QUALIFIERS = new Set(["path", "siblings", ...PLATFORM_AXES]);
|
|
37
|
+
/** `pkg:telo/local/<format>?…` — the bundled-controller delivery mode. Parsed by
|
|
38
|
+
* hand rather than with a PURL library: the analyzer must stay browser-safe and
|
|
39
|
+
* dependency-light, and the only thing needed here is the qualifier map. */
|
|
40
|
+
function parseBundledPurl(purl) {
|
|
41
|
+
if (!purl.startsWith("pkg:telo/local/"))
|
|
42
|
+
return null;
|
|
43
|
+
const withoutFragment = purl.split("#")[0];
|
|
44
|
+
const [head, query = ""] = withoutFragment.split("?");
|
|
45
|
+
const format = head.slice("pkg:telo/local/".length);
|
|
46
|
+
if (format === "")
|
|
47
|
+
return null;
|
|
48
|
+
const qualifiers = {};
|
|
49
|
+
for (const pair of query.split("&")) {
|
|
50
|
+
if (pair === "")
|
|
51
|
+
continue;
|
|
52
|
+
const eq = pair.indexOf("=");
|
|
53
|
+
if (eq < 0)
|
|
54
|
+
continue;
|
|
55
|
+
qualifiers[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1));
|
|
56
|
+
}
|
|
57
|
+
return { format, qualifiers };
|
|
58
|
+
}
|
|
59
|
+
function validateControllerSelectors(manifest, out) {
|
|
60
|
+
const controllers = manifest.controllers;
|
|
61
|
+
if (!Array.isArray(controllers))
|
|
62
|
+
return;
|
|
63
|
+
const metadata = manifest.metadata;
|
|
64
|
+
const name = metadata?.name;
|
|
65
|
+
const filePath = metadata?.source;
|
|
66
|
+
const resource = { kind: manifest.kind, name };
|
|
67
|
+
controllers.forEach((candidate, index) => {
|
|
68
|
+
if (typeof candidate !== "string")
|
|
69
|
+
return;
|
|
70
|
+
const parsed = parseBundledPurl(candidate);
|
|
71
|
+
if (!parsed)
|
|
72
|
+
return;
|
|
73
|
+
const at = `controllers[${index}]`;
|
|
74
|
+
const unknown = Object.keys(parsed.qualifiers).filter((k) => !KNOWN_QUALIFIERS.has(k));
|
|
75
|
+
for (const key of unknown) {
|
|
76
|
+
out.push({
|
|
77
|
+
severity: DiagnosticSeverity.Error,
|
|
78
|
+
code: "CONTROLLER_UNKNOWN_QUALIFIER",
|
|
79
|
+
source: SOURCE,
|
|
80
|
+
message: `${manifest.kind}/${name ?? "(unnamed)"}: bundled controller qualifier '${key}' is not ` +
|
|
81
|
+
`recognized. Known qualifiers: ${[...KNOWN_QUALIFIERS].sort().join(", ")}. An ` +
|
|
82
|
+
`unrecognized platform axis is ignored, which would make this candidate ` +
|
|
83
|
+
`platform-neutral and offer a single-platform binary to every host.`,
|
|
84
|
+
data: { resource, filePath, path: `${at}?${key}` },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Validate the selector; the value is not otherwise needed here, since
|
|
88
|
+
// candidates sharing a selector legitimately share a layer.
|
|
89
|
+
try {
|
|
90
|
+
selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!(err instanceof ArtifactSelectorError))
|
|
94
|
+
throw err;
|
|
95
|
+
out.push({
|
|
96
|
+
severity: DiagnosticSeverity.Error,
|
|
97
|
+
code: "CONTROLLER_INVALID_SELECTOR",
|
|
98
|
+
source: SOURCE,
|
|
99
|
+
message: `${manifest.kind}/${name ?? "(unnamed)"}: ${err.message}`,
|
|
100
|
+
data: { resource, filePath, path: at },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function validateLayerIndex(manifest, out) {
|
|
106
|
+
if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library")
|
|
107
|
+
return;
|
|
108
|
+
const layers = manifest.layers;
|
|
109
|
+
if (layers === undefined)
|
|
110
|
+
return;
|
|
111
|
+
const metadata = manifest.metadata;
|
|
112
|
+
const name = metadata?.name;
|
|
113
|
+
try {
|
|
114
|
+
parseLayerIndex(layers);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (!(err instanceof LayerIndexError) && !(err instanceof ArtifactSelectorError))
|
|
118
|
+
throw err;
|
|
119
|
+
out.push({
|
|
120
|
+
severity: DiagnosticSeverity.Error,
|
|
121
|
+
code: "INVALID_LAYER_INDEX",
|
|
122
|
+
source: SOURCE,
|
|
123
|
+
message: `${manifest.kind}/${name ?? "(unnamed)"}: ${err.message}`,
|
|
124
|
+
data: {
|
|
125
|
+
resource: { kind: manifest.kind, name },
|
|
126
|
+
filePath: metadata?.source,
|
|
127
|
+
path: "layers",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAmetB"}
|
|
@@ -254,8 +254,14 @@ export function validateReferences(resources, context) {
|
|
|
254
254
|
}
|
|
255
255
|
// Local reference (bare name or explicit `Self.`-qualified).
|
|
256
256
|
const localName = aliasPrefix === "Self" ? refName.slice(dot + 1) : refName;
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
// Scope-local FIRST, enclosing module as the fallback — the order the
|
|
258
|
+
// runtime uses at every name-resolution site (`ScopeContext.getInstance`,
|
|
259
|
+
// `ResourceContext.resolveRef`, and the CEL `resources` layering). Module-first
|
|
260
|
+
// here would validate a shadowed name against the resource the kernel will
|
|
261
|
+
// never bind: a false pass when the outer kind fits and the scoped one does
|
|
262
|
+
// not, a false REFERENCE_KIND_MISMATCH when it is the other way round.
|
|
263
|
+
const target = visibleScopeManifests.find((m) => m.metadata?.name === localName) ??
|
|
264
|
+
byName.get(localName);
|
|
259
265
|
if (!target) {
|
|
260
266
|
diagnostics.push({
|
|
261
267
|
severity: DiagnosticSeverity.Error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.60.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/analyzer.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
|
|
52
52
|
import { validateExtends } from "./validate-extends.js";
|
|
53
53
|
import { validateLogging } from "./validate-logging.js";
|
|
54
|
+
import { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
54
55
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
55
56
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
56
57
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
@@ -1153,7 +1154,7 @@ export class StaticAnalyzer {
|
|
|
1153
1154
|
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
1154
1155
|
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
1155
1156
|
// original and inline-extracted manifests have their sentinels resolved.
|
|
1156
|
-
resolveRefSentinels(allManifests, aliases, aliasesByModule);
|
|
1157
|
+
resolveRefSentinels(allManifests, aliases, aliasesByModule, [], defs);
|
|
1157
1158
|
|
|
1158
1159
|
// Phase 2.6: register each named `Telo.Type` resource's schema under its
|
|
1159
1160
|
// canonical module-scoped id (`telo://<module>/<name>`), validate
|
|
@@ -1202,6 +1203,11 @@ export class StaticAnalyzer {
|
|
|
1202
1203
|
// §14.1 / §10.3: redaction paths and `on_full: block` are statically
|
|
1203
1204
|
// detectable, so they fail `telo check` rather than only at boot.
|
|
1204
1205
|
diagnostics.push(...validateLogging(allManifests, defs, aliases, aliasesByModule));
|
|
1206
|
+
// Module-artifact surface: bundled-controller selector qualifiers and the
|
|
1207
|
+
// published `layers:` index. Every case is decidable from the manifest and
|
|
1208
|
+
// would otherwise fail on a consumer's machine — or, for a mistyped platform
|
|
1209
|
+
// axis, silently offer one platform's binary to every host.
|
|
1210
|
+
diagnostics.push(...validateModuleArtifact(allManifests));
|
|
1205
1211
|
}
|
|
1206
1212
|
resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
|
|
1207
1213
|
|
|
@@ -1957,7 +1963,13 @@ export class StaticAnalyzer {
|
|
|
1957
1963
|
// Resolve !ref sentinels after normalize so both the original and
|
|
1958
1964
|
// inline-extracted manifests get their refs canonicalized to
|
|
1959
1965
|
// {kind, name} for the kernel that consumes this output.
|
|
1960
|
-
resolveRefSentinels(
|
|
1966
|
+
resolveRefSentinels(
|
|
1967
|
+
normalized,
|
|
1968
|
+
ctx.aliases,
|
|
1969
|
+
ctx.aliasesByModule,
|
|
1970
|
+
crossModuleTargets ?? [],
|
|
1971
|
+
ctx.definitions!,
|
|
1972
|
+
);
|
|
1961
1973
|
// Canonicalize import-scoped schema `$ref`s (`telo://Self|Alias/Type`) so the
|
|
1962
1974
|
// kernel that executes this output compiles inputs/outputs against the same
|
|
1963
1975
|
// ids the type controllers register their schemas under.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **layer index** of `kernel/specs/module-artifact.md` — the `layers:` block
|
|
3
|
+
* a published `telo.yaml` carries, listing every layer of the module artifact
|
|
4
|
+
* except the manifest layer itself.
|
|
5
|
+
*
|
|
6
|
+
* Why it lives in `telo.yaml` rather than in the OCI manifest, which has layers
|
|
7
|
+
* natively: a Telo import is pinned to a hash of `telo.yaml` and nothing else.
|
|
8
|
+
* The OCI manifest sits one level up, is fetched by a reference that is usually
|
|
9
|
+
* a mutable tag, and is never hashed by Telo — so digests held only there would
|
|
10
|
+
* leave the pin proving nothing about the payload. Pinning the OCI manifest
|
|
11
|
+
* instead is circular: `telo.yaml` is one of its layers.
|
|
12
|
+
*
|
|
13
|
+
* The manifest layer therefore has no entry — a hash of `telo.yaml` cannot sit
|
|
14
|
+
* inside `telo.yaml`. It is pinned by the importer's `#sha256-...` instead, so
|
|
15
|
+
* the chain reads `import pin -> telo.yaml -> blob digest -> layer contents`.
|
|
16
|
+
*
|
|
17
|
+
* Each entry carries two digests, answering different questions:
|
|
18
|
+
* - `blob` — the OCI blob digest over the pushed bytes. It *addresses* the
|
|
19
|
+
* layer, so a client pulls by digest and never reads the OCI layer list, and
|
|
20
|
+
* it verifies the transfer. Publish pushes payload blobs first and injects
|
|
21
|
+
* their digests here, then pushes the manifest blob, so nothing is circular.
|
|
22
|
+
* - `integrity` — the content digest (`computeFilesIntegrity`) over that
|
|
23
|
+
* layer's own files, independent of tar/gzip framing. It verifies what is
|
|
24
|
+
* already extracted on disk and can be re-derived from it without re-tarring,
|
|
25
|
+
* which is what makes a per-layer cache marker checkable.
|
|
26
|
+
*
|
|
27
|
+
* Browser-safe: `telo check`, the editor and the hub validate an index through
|
|
28
|
+
* this module; only the kernel fetches and extracts.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
isLayerRole,
|
|
33
|
+
normalizeSelector,
|
|
34
|
+
selectorKey,
|
|
35
|
+
selectorMatches,
|
|
36
|
+
type ArtifactSelector,
|
|
37
|
+
type LayerRole,
|
|
38
|
+
type PlatformTarget,
|
|
39
|
+
} from "./artifact-selector.js";
|
|
40
|
+
|
|
41
|
+
/** OCI content digest: `sha256:` + 64 lowercase hex. */
|
|
42
|
+
const BLOB_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
43
|
+
|
|
44
|
+
/** Telo content digest: `sha256-` + unpadded base64url of 32 bytes. */
|
|
45
|
+
const CONTENT_DIGEST = /^sha256-[A-Za-z0-9_-]{43}$/;
|
|
46
|
+
|
|
47
|
+
export interface ArtifactLayer {
|
|
48
|
+
role: LayerRole;
|
|
49
|
+
/** Present on `controller` layers only. */
|
|
50
|
+
selector?: ArtifactSelector;
|
|
51
|
+
/** OCI blob digest — addresses the layer and verifies the transfer. */
|
|
52
|
+
blob: string;
|
|
53
|
+
/** Content digest over the layer's files — verifies what is on disk. */
|
|
54
|
+
integrity: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class LayerIndexError extends Error {
|
|
58
|
+
readonly code = "INVALID_LAYER_INDEX";
|
|
59
|
+
|
|
60
|
+
constructor(detail: string) {
|
|
61
|
+
super(detail);
|
|
62
|
+
this.name = "LayerIndexError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function digest(field: "blob" | "integrity", raw: unknown, describe: string): string {
|
|
67
|
+
if (typeof raw !== "string" || raw === "") {
|
|
68
|
+
throw new LayerIndexError(`${describe}: ${field} is required and must be a string.`);
|
|
69
|
+
}
|
|
70
|
+
const pattern = field === "blob" ? BLOB_DIGEST : CONTENT_DIGEST;
|
|
71
|
+
if (!pattern.test(raw)) {
|
|
72
|
+
throw new LayerIndexError(
|
|
73
|
+
field === "blob"
|
|
74
|
+
? `${describe}: blob '${raw}' is not an OCI digest (expected 'sha256:' + 64 hex characters).`
|
|
75
|
+
: `${describe}: integrity '${raw}' is not a content digest (expected 'sha256-' + 43 base64url characters).`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse and validate a `layers:` value off an owner document. Order is
|
|
83
|
+
* preserved — when several controller layers match a target, precedence is
|
|
84
|
+
* declaration order, so the author controls it.
|
|
85
|
+
*/
|
|
86
|
+
export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLayer[] {
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
throw new LayerIndexError(`${describe}: expected an array of layer entries.`);
|
|
89
|
+
}
|
|
90
|
+
const layers: ArtifactLayer[] = [];
|
|
91
|
+
const seenSelectors = new Set<string>();
|
|
92
|
+
const seenSingletons = new Set<LayerRole>();
|
|
93
|
+
|
|
94
|
+
value.forEach((raw, index) => {
|
|
95
|
+
const where = `${describe}[${index}]`;
|
|
96
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
97
|
+
throw new LayerIndexError(`${where}: expected an object.`);
|
|
98
|
+
}
|
|
99
|
+
const entry = raw as Record<string, unknown>;
|
|
100
|
+
if (!isLayerRole(entry.role)) {
|
|
101
|
+
throw new LayerIndexError(
|
|
102
|
+
`${where}: role must be one of 'controller', 'assets', 'common'; got ` +
|
|
103
|
+
`${entry.role === undefined ? "nothing" : `'${String(entry.role)}'`}.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const role = entry.role;
|
|
107
|
+
|
|
108
|
+
let selector: ArtifactSelector | undefined;
|
|
109
|
+
if (role === "controller") {
|
|
110
|
+
if (entry.selector === undefined) {
|
|
111
|
+
throw new LayerIndexError(`${where}: a controller layer must declare a selector.`);
|
|
112
|
+
}
|
|
113
|
+
selector = normalizeSelector(entry.selector, where);
|
|
114
|
+
const key = selectorKey(selector);
|
|
115
|
+
if (seenSelectors.has(key)) {
|
|
116
|
+
throw new LayerIndexError(
|
|
117
|
+
`${where}: a second controller layer claims the selector ${key}. ` +
|
|
118
|
+
`Each selector addresses exactly one layer.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
seenSelectors.add(key);
|
|
122
|
+
} else {
|
|
123
|
+
if (entry.selector !== undefined) {
|
|
124
|
+
throw new LayerIndexError(
|
|
125
|
+
`${where}: a '${role}' layer must not declare a selector — it is a singleton.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (seenSingletons.has(role)) {
|
|
129
|
+
throw new LayerIndexError(`${where}: a second '${role}' layer is declared.`);
|
|
130
|
+
}
|
|
131
|
+
seenSingletons.add(role);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
layers.push({
|
|
135
|
+
role,
|
|
136
|
+
...(selector ? { selector } : {}),
|
|
137
|
+
blob: digest("blob", entry.blob, where),
|
|
138
|
+
integrity: digest("integrity", entry.integrity, where),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return layers;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The singleton layer for a role, or undefined when the artifact has none. */
|
|
146
|
+
export function singletonLayer(
|
|
147
|
+
layers: readonly ArtifactLayer[],
|
|
148
|
+
role: Exclude<LayerRole, "controller">,
|
|
149
|
+
): ArtifactLayer | undefined {
|
|
150
|
+
return layers.find((l) => l.role === role);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Every controller layer matching `target`, in declaration order. Used by
|
|
154
|
+
* `telo install` to warm a cache for one platform. */
|
|
155
|
+
export function matchControllerLayers(
|
|
156
|
+
layers: readonly ArtifactLayer[],
|
|
157
|
+
target: PlatformTarget,
|
|
158
|
+
): ArtifactLayer[] {
|
|
159
|
+
return layers.filter(
|
|
160
|
+
(l) => l.role === "controller" && l.selector !== undefined && selectorMatches(l.selector, target),
|
|
161
|
+
);
|
|
162
|
+
}
|