@telorun/analyzer 0.47.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 +6 -0
- 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/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/package.json +2 -2
- package/src/analyzer.ts +6 -0
- 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/validate-module-artifact.ts +141 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
import { parseLayerIndex, LayerIndexError } from "./artifact-layer-index.js";
|
|
4
|
+
import {
|
|
5
|
+
ArtifactSelectorError,
|
|
6
|
+
PLATFORM_AXES,
|
|
7
|
+
selectorFromQualifiers,
|
|
8
|
+
} from "./artifact-selector.js";
|
|
9
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const SOURCE = "telo-analyzer";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Static validation of the module-artifact surface — `kernel/specs/module-artifact.md`.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is decidable from the manifest text alone, and every case would
|
|
17
|
+
* otherwise surface on a *consumer's* machine at controller-resolve time (or, worse,
|
|
18
|
+
* not at all). That is the whole argument: an author who mistypes a platform axis
|
|
19
|
+
* gets a platform-neutral candidate, publish emits one layer, and every host
|
|
20
|
+
* happily loads a binary built for one architecture — silently, forever.
|
|
21
|
+
*
|
|
22
|
+
* Two checks. Note that several candidates *sharing* one selector is not among
|
|
23
|
+
* them: a controller layer holds the entry points of every candidate with that
|
|
24
|
+
* selector (spec §1), which is what every module with two `js` controllers relies
|
|
25
|
+
* on.
|
|
26
|
+
*
|
|
27
|
+
* 1. **Controller selector qualifiers.** `os` / `arch` / `libc` / `siblings` are
|
|
28
|
+
* authored surface. An unknown qualifier is reported rather than ignored, since
|
|
29
|
+
* ignoring is what makes a typo invisible; an invalid value is reported here
|
|
30
|
+
* instead of throwing from the loader later.
|
|
31
|
+
* 2. **The published layer index.** The owner doc's JSON Schema covers shape; the
|
|
32
|
+
* semantic rules — controller-requires-selector, singletons carry none, no
|
|
33
|
+
* duplicate selector, the token grammar (`os: Linux` passes the schema and
|
|
34
|
+
* throws at runtime) — live in the parser, so run it.
|
|
35
|
+
*/
|
|
36
|
+
export function validateModuleArtifact(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
|
|
37
|
+
const out: AnalysisDiagnostic[] = [];
|
|
38
|
+
for (const manifest of manifests) {
|
|
39
|
+
validateLayerIndex(manifest, out);
|
|
40
|
+
validateControllerSelectors(manifest, out);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const KNOWN_QUALIFIERS = new Set<string>(["path", "siblings", ...PLATFORM_AXES]);
|
|
46
|
+
|
|
47
|
+
/** `pkg:telo/local/<format>?…` — the bundled-controller delivery mode. Parsed by
|
|
48
|
+
* hand rather than with a PURL library: the analyzer must stay browser-safe and
|
|
49
|
+
* dependency-light, and the only thing needed here is the qualifier map. */
|
|
50
|
+
function parseBundledPurl(
|
|
51
|
+
purl: string,
|
|
52
|
+
): { format: string; qualifiers: Record<string, string> } | null {
|
|
53
|
+
if (!purl.startsWith("pkg:telo/local/")) return null;
|
|
54
|
+
const withoutFragment = purl.split("#")[0];
|
|
55
|
+
const [head, query = ""] = withoutFragment.split("?");
|
|
56
|
+
const format = head.slice("pkg:telo/local/".length);
|
|
57
|
+
if (format === "") return null;
|
|
58
|
+
const qualifiers: Record<string, string> = {};
|
|
59
|
+
for (const pair of query.split("&")) {
|
|
60
|
+
if (pair === "") continue;
|
|
61
|
+
const eq = pair.indexOf("=");
|
|
62
|
+
if (eq < 0) continue;
|
|
63
|
+
qualifiers[decodeURIComponent(pair.slice(0, eq))] = decodeURIComponent(pair.slice(eq + 1));
|
|
64
|
+
}
|
|
65
|
+
return { format, qualifiers };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateControllerSelectors(
|
|
69
|
+
manifest: ResourceManifest,
|
|
70
|
+
out: AnalysisDiagnostic[],
|
|
71
|
+
): void {
|
|
72
|
+
const controllers = (manifest as { controllers?: unknown }).controllers;
|
|
73
|
+
if (!Array.isArray(controllers)) return;
|
|
74
|
+
const metadata = manifest.metadata as
|
|
75
|
+
| { name?: string; module?: string; source?: string }
|
|
76
|
+
| undefined;
|
|
77
|
+
const name = metadata?.name;
|
|
78
|
+
const filePath = metadata?.source;
|
|
79
|
+
const resource = { kind: manifest.kind, name };
|
|
80
|
+
|
|
81
|
+
controllers.forEach((candidate, index) => {
|
|
82
|
+
if (typeof candidate !== "string") return;
|
|
83
|
+
const parsed = parseBundledPurl(candidate);
|
|
84
|
+
if (!parsed) return;
|
|
85
|
+
const at = `controllers[${index}]`;
|
|
86
|
+
|
|
87
|
+
const unknown = Object.keys(parsed.qualifiers).filter((k) => !KNOWN_QUALIFIERS.has(k));
|
|
88
|
+
for (const key of unknown) {
|
|
89
|
+
out.push({
|
|
90
|
+
severity: DiagnosticSeverity.Error,
|
|
91
|
+
code: "CONTROLLER_UNKNOWN_QUALIFIER",
|
|
92
|
+
source: SOURCE,
|
|
93
|
+
message:
|
|
94
|
+
`${manifest.kind}/${name ?? "(unnamed)"}: bundled controller qualifier '${key}' is not ` +
|
|
95
|
+
`recognized. Known qualifiers: ${[...KNOWN_QUALIFIERS].sort().join(", ")}. An ` +
|
|
96
|
+
`unrecognized platform axis is ignored, which would make this candidate ` +
|
|
97
|
+
`platform-neutral and offer a single-platform binary to every host.`,
|
|
98
|
+
data: { resource, filePath, path: `${at}?${key}` },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate the selector; the value is not otherwise needed here, since
|
|
103
|
+
// candidates sharing a selector legitimately share a layer.
|
|
104
|
+
try {
|
|
105
|
+
selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (!(err instanceof ArtifactSelectorError)) throw err;
|
|
108
|
+
out.push({
|
|
109
|
+
severity: DiagnosticSeverity.Error,
|
|
110
|
+
code: "CONTROLLER_INVALID_SELECTOR",
|
|
111
|
+
source: SOURCE,
|
|
112
|
+
message: `${manifest.kind}/${name ?? "(unnamed)"}: ${err.message}`,
|
|
113
|
+
data: { resource, filePath, path: at },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateLayerIndex(manifest: ResourceManifest, out: AnalysisDiagnostic[]): void {
|
|
120
|
+
if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library") return;
|
|
121
|
+
const layers = (manifest as { layers?: unknown }).layers;
|
|
122
|
+
if (layers === undefined) return;
|
|
123
|
+
const metadata = manifest.metadata as { name?: string; source?: string } | undefined;
|
|
124
|
+
const name = metadata?.name;
|
|
125
|
+
try {
|
|
126
|
+
parseLayerIndex(layers);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!(err instanceof LayerIndexError) && !(err instanceof ArtifactSelectorError)) throw err;
|
|
129
|
+
out.push({
|
|
130
|
+
severity: DiagnosticSeverity.Error,
|
|
131
|
+
code: "INVALID_LAYER_INDEX",
|
|
132
|
+
source: SOURCE,
|
|
133
|
+
message: `${manifest.kind}/${name ?? "(unnamed)"}: ${err.message}`,
|
|
134
|
+
data: {
|
|
135
|
+
resource: { kind: manifest.kind, name },
|
|
136
|
+
filePath: metadata?.source,
|
|
137
|
+
path: "layers",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|