@telorun/analyzer 0.1.1
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/LICENSE +17 -0
- package/dist/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +17 -0
- package/dist/adapters/node-adapter.d.ts +15 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +32 -0
- package/dist/adapters/registry-adapter.d.ts +11 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +33 -0
- package/dist/alias-resolver.d.ts +12 -0
- package/dist/alias-resolver.d.ts.map +1 -0
- package/dist/alias-resolver.js +36 -0
- package/dist/analysis-registry.d.ts +29 -0
- package/dist/analysis-registry.d.ts.map +1 -0
- package/dist/analysis-registry.js +55 -0
- package/dist/analyzer.d.ts +14 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +314 -0
- package/dist/builtins.d.ts +3 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/builtins.js +109 -0
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -0
- package/dist/cel-environment.js +59 -0
- package/dist/definition-registry.d.ts +58 -0
- package/dist/definition-registry.d.ts.map +1 -0
- package/dist/definition-registry.js +155 -0
- package/dist/dependency-graph.d.ts +38 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +155 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/manifest-loader.d.ts +11 -0
- package/dist/manifest-loader.d.ts.map +1 -0
- package/dist/manifest-loader.js +194 -0
- package/dist/normalize-inline-resources.d.ts +22 -0
- package/dist/normalize-inline-resources.d.ts.map +1 -0
- package/dist/normalize-inline-resources.js +136 -0
- package/dist/precompile.d.ts +9 -0
- package/dist/precompile.d.ts.map +1 -0
- package/dist/precompile.js +51 -0
- package/dist/reference-field-map.d.ts +53 -0
- package/dist/reference-field-map.d.ts.map +1 -0
- package/dist/reference-field-map.js +107 -0
- package/dist/schema-compat.d.ts +42 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +234 -0
- package/dist/scope-resolver.d.ts +5 -0
- package/dist/scope-resolver.d.ts.map +1 -0
- package/dist/scope-resolver.js +13 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/validate-cel-context.d.ts +24 -0
- package/dist/validate-cel-context.d.ts.map +1 -0
- package/dist/validate-cel-context.js +136 -0
- package/dist/validate-references.d.ts +19 -0
- package/dist/validate-references.d.ts.map +1 -0
- package/dist/validate-references.js +275 -0
- package/package.json +34 -0
- package/src/adapters/http-adapter.ts +23 -0
- package/src/adapters/node-adapter.ts +38 -0
- package/src/adapters/registry-adapter.ts +43 -0
- package/src/alias-resolver.ts +37 -0
- package/src/analysis-registry.ts +68 -0
- package/src/analyzer.ts +399 -0
- package/src/builtins.ts +111 -0
- package/src/cel-environment.ts +70 -0
- package/src/definition-registry.ts +170 -0
- package/src/dependency-graph.ts +187 -0
- package/src/index.ts +17 -0
- package/src/manifest-loader.ts +203 -0
- package/src/normalize-inline-resources.ts +170 -0
- package/src/precompile.ts +54 -0
- package/src/reference-field-map.ts +147 -0
- package/src/schema-compat.ts +264 -0
- package/src/scope-resolver.ts +13 -0
- package/src/types.ts +68 -0
- package/src/validate-cel-context.ts +142 -0
- package/src/validate-references.ts +311 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldValues } from "./reference-field-map.js";
|
|
2
|
+
import { navigateJsonPointer } from "./schema-compat.js";
|
|
3
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
4
|
+
const SOURCE = "telo-analyzer";
|
|
5
|
+
const SYSTEM_KINDS = new Set(["Kernel.Definition", "Kernel.Abstract"]);
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether `kind` satisfies the ref constraint in `entry`.
|
|
8
|
+
* Returns an empty array when valid, or mismatch error strings when not.
|
|
9
|
+
* Returns an empty array immediately when the ref identity is not registered
|
|
10
|
+
* (partial context — skip check rather than false-positive).
|
|
11
|
+
*/
|
|
12
|
+
function checkKind(kind, entry, registry, aliases) {
|
|
13
|
+
const resolved = aliases.resolveKind(kind) ?? kind;
|
|
14
|
+
const errors = [];
|
|
15
|
+
for (const refStr of entry.refs) {
|
|
16
|
+
const targetKind = registry.resolveRef(refStr);
|
|
17
|
+
if (!targetKind)
|
|
18
|
+
return [];
|
|
19
|
+
const targetDef = registry.resolve(targetKind);
|
|
20
|
+
if (!targetDef)
|
|
21
|
+
return [];
|
|
22
|
+
if (targetDef.kind === "Kernel.Abstract") {
|
|
23
|
+
const implementing = registry.getByExtends(targetKind);
|
|
24
|
+
if (implementing.length === 0)
|
|
25
|
+
return []; // partial context — no implementations loaded yet
|
|
26
|
+
const implementingKinds = new Set(implementing.map((d) => `${d.metadata.module}.${d.metadata.name}`));
|
|
27
|
+
if (implementingKinds.has(resolved))
|
|
28
|
+
return [];
|
|
29
|
+
const options = [...implementingKinds].join(", ");
|
|
30
|
+
errors.push(`'${kind}' does not implement '${targetKind}' (known implementations: ${options})`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (resolved === targetKind)
|
|
34
|
+
return [];
|
|
35
|
+
errors.push(`'${kind}' (resolved: '${resolved}') does not match required '${targetKind}'`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Phase 3 — Reference validation.
|
|
42
|
+
*
|
|
43
|
+
* For each x-telo-ref slot in every non-system resource, validates:
|
|
44
|
+
* 1. Structural — the value has string `kind` and `name` fields.
|
|
45
|
+
* 2. Kind — the alias-resolved kind satisfies the x-telo-ref constraint
|
|
46
|
+
* (abstract: must extend the target; concrete: must equal it exactly).
|
|
47
|
+
* 3. Resolution — a resource with that name exists in the visible manifest set
|
|
48
|
+
* (outer manifests + scope manifests for in-scope ref paths).
|
|
49
|
+
*
|
|
50
|
+
* Ref values with keys beyond kind/name/metadata are treated as inline resources
|
|
51
|
+
* pending Phase 2 normalization and are skipped without error.
|
|
52
|
+
*
|
|
53
|
+
* Returns an empty array when `context.aliases` or `context.definitions` is absent.
|
|
54
|
+
*/
|
|
55
|
+
export function validateReferences(resources, context) {
|
|
56
|
+
const diagnostics = [];
|
|
57
|
+
const aliases = context.aliases;
|
|
58
|
+
const registry = context.definitions;
|
|
59
|
+
if (!aliases || !registry)
|
|
60
|
+
return diagnostics;
|
|
61
|
+
// Build outer resource lookup by name for resolution check.
|
|
62
|
+
// Exclude system kinds (Kernel.Definition) — they are type blueprints, not instances,
|
|
63
|
+
// and their names (e.g. "Server", "Job") would shadow user-defined resource instances.
|
|
64
|
+
const byName = new Map();
|
|
65
|
+
for (const r of resources) {
|
|
66
|
+
if (r.metadata?.name && !SYSTEM_KINDS.has(r.kind))
|
|
67
|
+
byName.set(r.metadata.name, r);
|
|
68
|
+
}
|
|
69
|
+
for (const r of resources) {
|
|
70
|
+
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
71
|
+
continue;
|
|
72
|
+
const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
|
|
73
|
+
if (!fieldMap)
|
|
74
|
+
continue;
|
|
75
|
+
const resourceLabel = `${r.kind}/${r.metadata.name}`;
|
|
76
|
+
const resourceData = { kind: r.kind, name: r.metadata.name };
|
|
77
|
+
// Collect scope visibility prefixes (JSON Pointer → dot prefix) and their manifests.
|
|
78
|
+
// scope field path → flat array of ResourceManifest declared in that scope.
|
|
79
|
+
const scopeManifestsByPointer = new Map();
|
|
80
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
81
|
+
if (!isScopeEntry(entry))
|
|
82
|
+
continue;
|
|
83
|
+
const raw = resolveFieldValues(r, fieldPath)
|
|
84
|
+
.flatMap((v) => (Array.isArray(v) ? v : [v]))
|
|
85
|
+
.filter((v) => !!v && typeof v === "object");
|
|
86
|
+
const pointers = Array.isArray(entry.scope) ? entry.scope : [entry.scope];
|
|
87
|
+
for (const pointer of pointers) {
|
|
88
|
+
scopeManifestsByPointer.set(pointer, raw);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const scopePrefixes = Array.from(scopeManifestsByPointer.keys()).map((p) => p.replace(/^\//, "").replace(/\//g, "."));
|
|
92
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
93
|
+
if (!isRefEntry(entry))
|
|
94
|
+
continue;
|
|
95
|
+
const inScope = scopePrefixes.some((prefix) => fieldPath === prefix ||
|
|
96
|
+
fieldPath.startsWith(prefix + ".") ||
|
|
97
|
+
fieldPath.startsWith(prefix + "["));
|
|
98
|
+
// Scope manifests visible to this ref path.
|
|
99
|
+
const visibleScopeManifests = [];
|
|
100
|
+
if (inScope) {
|
|
101
|
+
for (const [pointer, manifests] of scopeManifestsByPointer) {
|
|
102
|
+
const prefix = pointer.replace(/^\//, "").replace(/\//g, ".");
|
|
103
|
+
if (fieldPath === prefix ||
|
|
104
|
+
fieldPath.startsWith(prefix + ".") ||
|
|
105
|
+
fieldPath.startsWith(prefix + "[")) {
|
|
106
|
+
visibleScopeManifests.push(...manifests);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const val of resolveFieldValues(r, fieldPath)) {
|
|
111
|
+
if (!val)
|
|
112
|
+
continue;
|
|
113
|
+
// Name-only reference (plain string) — look up by name to validate.
|
|
114
|
+
if (typeof val === "string") {
|
|
115
|
+
const target = byName.get(val) ?? visibleScopeManifests.find((m) => m.metadata?.name === val);
|
|
116
|
+
if (!target) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
severity: DiagnosticSeverity.Error,
|
|
119
|
+
code: "UNRESOLVED_REFERENCE",
|
|
120
|
+
source: SOURCE,
|
|
121
|
+
message: `${resourceLabel}: reference at '${fieldPath}' → resource '${val}' not found`,
|
|
122
|
+
data: { resource: resourceData, path: fieldPath },
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const kindErrors = checkKind(target.kind, entry, registry, aliases);
|
|
127
|
+
if (kindErrors.length > 0) {
|
|
128
|
+
diagnostics.push({
|
|
129
|
+
severity: DiagnosticSeverity.Error,
|
|
130
|
+
code: "REFERENCE_KIND_MISMATCH",
|
|
131
|
+
source: SOURCE,
|
|
132
|
+
message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
|
|
133
|
+
data: { resource: resourceData, path: fieldPath },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (typeof val !== "object")
|
|
139
|
+
continue;
|
|
140
|
+
const refVal = val;
|
|
141
|
+
// Skip inline resources — Phase 2 normalization hasn't run yet.
|
|
142
|
+
if (isInlineResource(refVal))
|
|
143
|
+
continue;
|
|
144
|
+
// 1. Structural check
|
|
145
|
+
if (typeof refVal.kind !== "string" || typeof refVal.name !== "string") {
|
|
146
|
+
diagnostics.push({
|
|
147
|
+
severity: DiagnosticSeverity.Error,
|
|
148
|
+
code: "INVALID_REFERENCE",
|
|
149
|
+
source: SOURCE,
|
|
150
|
+
message: `${resourceLabel}: reference at '${fieldPath}' must have string 'kind' and 'name' fields`,
|
|
151
|
+
data: { resource: resourceData, path: fieldPath },
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// 2. Kind check
|
|
156
|
+
const kindErrors = checkKind(refVal.kind, entry, registry, aliases);
|
|
157
|
+
if (kindErrors.length > 0) {
|
|
158
|
+
diagnostics.push({
|
|
159
|
+
severity: DiagnosticSeverity.Error,
|
|
160
|
+
code: "REFERENCE_KIND_MISMATCH",
|
|
161
|
+
source: SOURCE,
|
|
162
|
+
message: `${resourceLabel}: reference at '${fieldPath}' → ${kindErrors.join("; ")}`,
|
|
163
|
+
data: { resource: resourceData, path: fieldPath },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// 3. Resolution check — resource with this name must exist.
|
|
167
|
+
const exists = byName.has(refVal.name) ||
|
|
168
|
+
visibleScopeManifests.some((m) => m.metadata?.name === refVal.name);
|
|
169
|
+
if (!exists) {
|
|
170
|
+
diagnostics.push({
|
|
171
|
+
severity: DiagnosticSeverity.Error,
|
|
172
|
+
code: "UNRESOLVED_REFERENCE",
|
|
173
|
+
source: SOURCE,
|
|
174
|
+
message: `${resourceLabel}: reference at '${fieldPath}' → resource '${refVal.name}' not found`,
|
|
175
|
+
data: { resource: resourceData, path: fieldPath },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Phase 3b — x-telo-schema-from validation.
|
|
182
|
+
// For each field with a schemaFrom path expression, resolve the anchor ref to get the
|
|
183
|
+
// concrete kind, navigate the JSON Pointer into that kind's definition schema, and
|
|
184
|
+
// validate the field value against the resulting sub-schema.
|
|
185
|
+
for (const r of resources) {
|
|
186
|
+
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
187
|
+
continue;
|
|
188
|
+
const fieldMap = registry.getFieldMapForKind(r.kind, aliases);
|
|
189
|
+
if (!fieldMap)
|
|
190
|
+
continue;
|
|
191
|
+
const resourceLabel = `${r.kind}/${r.metadata.name}`;
|
|
192
|
+
const resourceData = { kind: r.kind, name: r.metadata.name };
|
|
193
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
194
|
+
if (!isSchemaFromEntry(entry))
|
|
195
|
+
continue;
|
|
196
|
+
const { schemaFrom } = entry;
|
|
197
|
+
const isAbsolute = schemaFrom.startsWith("/");
|
|
198
|
+
const expr = isAbsolute ? schemaFrom.slice(1) : schemaFrom;
|
|
199
|
+
const slashIdx = expr.indexOf("/");
|
|
200
|
+
if (slashIdx === -1) {
|
|
201
|
+
diagnostics.push({
|
|
202
|
+
severity: DiagnosticSeverity.Error,
|
|
203
|
+
code: "INVALID_SCHEMA_FROM",
|
|
204
|
+
source: SOURCE,
|
|
205
|
+
message: `${resourceLabel}: x-telo-schema-from "${schemaFrom}" must contain at least one "/" to separate anchor from JSON Pointer`,
|
|
206
|
+
data: { resource: resourceData, path: fieldPath },
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const anchorName = expr.slice(0, slashIdx);
|
|
211
|
+
const jsonPointer = "/" + expr.slice(slashIdx + 1);
|
|
212
|
+
// Derive the anchor path in the resource config.
|
|
213
|
+
let anchorPath;
|
|
214
|
+
if (isAbsolute) {
|
|
215
|
+
anchorPath = anchorName;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Relative: replace the last dot-segment of fieldPath with anchorName.
|
|
219
|
+
// e.g. "nodes[].options" → "nodes[].backend"
|
|
220
|
+
const lastDot = fieldPath.lastIndexOf(".");
|
|
221
|
+
anchorPath = lastDot === -1 ? anchorName : fieldPath.slice(0, lastDot + 1) + anchorName;
|
|
222
|
+
}
|
|
223
|
+
const anchorValues = resolveFieldValues(r, anchorPath);
|
|
224
|
+
if (anchorValues.length === 0)
|
|
225
|
+
continue; // anchor field not set — nothing to validate
|
|
226
|
+
const fieldValues = resolveFieldValues(r, fieldPath);
|
|
227
|
+
for (let i = 0; i < fieldValues.length; i++) {
|
|
228
|
+
const fieldValue = fieldValues[i];
|
|
229
|
+
if (fieldValue == null)
|
|
230
|
+
continue;
|
|
231
|
+
// For absolute paths, the single anchor applies to all field values.
|
|
232
|
+
const anchorVal = isAbsolute ? anchorValues[0] : anchorValues[i];
|
|
233
|
+
if (!anchorVal || typeof anchorVal !== "object")
|
|
234
|
+
continue;
|
|
235
|
+
const refVal = anchorVal;
|
|
236
|
+
if (typeof refVal.kind !== "string")
|
|
237
|
+
continue;
|
|
238
|
+
const refResolvedKind = aliases.resolveKind(refVal.kind) ?? refVal.kind;
|
|
239
|
+
const refDef = registry.resolve(refVal.kind) ?? registry.resolve(refResolvedKind);
|
|
240
|
+
if (!refDef?.schema) {
|
|
241
|
+
diagnostics.push({
|
|
242
|
+
severity: DiagnosticSeverity.Error,
|
|
243
|
+
code: "SCHEMA_FROM_MISSING_PATH",
|
|
244
|
+
source: SOURCE,
|
|
245
|
+
message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema`,
|
|
246
|
+
data: { resource: resourceData, path: fieldPath },
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const subSchema = navigateJsonPointer(refDef.schema, jsonPointer);
|
|
251
|
+
if (subSchema === undefined) {
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
severity: DiagnosticSeverity.Error,
|
|
254
|
+
code: "SCHEMA_FROM_MISSING_PATH",
|
|
255
|
+
source: SOURCE,
|
|
256
|
+
message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
|
|
257
|
+
data: { resource: resourceData, path: fieldPath },
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const issues = registry.validateWithRefs(fieldValue, subSchema);
|
|
262
|
+
for (const issue of issues) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
severity: DiagnosticSeverity.Error,
|
|
265
|
+
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
266
|
+
source: SOURCE,
|
|
267
|
+
message: `${resourceLabel}: '${fieldPath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
|
|
268
|
+
data: { resource: resourceData, path: fieldPath },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return diagnostics;
|
|
275
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telorun/analyzer",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bun": "./src/index.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/**",
|
|
17
|
+
"src/**"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@marcbachmann/cel-js": "^7.5.3",
|
|
21
|
+
"ajv": "^8.17.1",
|
|
22
|
+
"ajv-formats": "^3.0.1",
|
|
23
|
+
"yaml": "^2.8.3",
|
|
24
|
+
"jsonpath-plus": "^10.3.0",
|
|
25
|
+
"@telorun/sdk": "0.2.6"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.lib.json"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ManifestAdapter } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export class HttpAdapter implements ManifestAdapter {
|
|
4
|
+
supports(url: string): boolean {
|
|
5
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async read(url: string): Promise<{ text: string; source: string }> {
|
|
9
|
+
const fetchUrl = url.includes(".yaml") ? url : `${url}/module.yaml`;
|
|
10
|
+
const response = await fetch(fetchUrl);
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Failed to fetch manifest from ${fetchUrl}: ${response.status} ${response.statusText}`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return { text: await response.text(), source: fetchUrl };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resolveRelative(base: string, relative: string): string {
|
|
20
|
+
const baseWithSlash = base.endsWith("/") ? base : `${base}/`;
|
|
21
|
+
return new URL(relative, baseWithSlash).href;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type { ManifestAdapter } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
6
|
+
export class NodeAdapter implements ManifestAdapter {
|
|
7
|
+
constructor(private readonly cwd: string = process.cwd()) {}
|
|
8
|
+
|
|
9
|
+
supports(url: string): boolean {
|
|
10
|
+
return (
|
|
11
|
+
url.startsWith("file://") ||
|
|
12
|
+
url.startsWith("/") ||
|
|
13
|
+
url.startsWith("./") ||
|
|
14
|
+
url.startsWith("../") ||
|
|
15
|
+
(!url.includes("://") && !url.includes("@"))
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async read(url: string): Promise<{ text: string; source: string }> {
|
|
20
|
+
const filePath = url.startsWith("file://") ? new URL(url).pathname : url;
|
|
21
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
22
|
+
const resolvedPath =
|
|
23
|
+
stat?.isDirectory() ? path.join(filePath, "module.yaml") : filePath;
|
|
24
|
+
const text = await fs.readFile(resolvedPath, "utf8");
|
|
25
|
+
return { text, source: resolvedPath };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolveRelative(base: string, relative: string): string {
|
|
29
|
+
const basePath = base.startsWith("file://") ? new URL(base).pathname : base;
|
|
30
|
+
const baseDir = path.dirname(path.resolve(this.cwd, basePath));
|
|
31
|
+
return path.resolve(baseDir, relative);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
36
|
+
export function createNodeAdapter(cwd: string = process.cwd()): ManifestAdapter {
|
|
37
|
+
return new NodeAdapter(cwd);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ManifestAdapter } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const REGISTRY_BASE = "https://registry.telo.run";
|
|
4
|
+
|
|
5
|
+
export class RegistryAdapter implements ManifestAdapter {
|
|
6
|
+
supports(url: string): boolean {
|
|
7
|
+
return (
|
|
8
|
+
!url.startsWith("http://") &&
|
|
9
|
+
!url.startsWith("https://") &&
|
|
10
|
+
!url.startsWith("/") &&
|
|
11
|
+
!url.startsWith(".") &&
|
|
12
|
+
url.includes("@") &&
|
|
13
|
+
url.includes("/")
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async read(moduleRef: string): Promise<{ text: string; source: string }> {
|
|
18
|
+
const fetchUrl = this.toRegistryUrl(moduleRef);
|
|
19
|
+
const response = await fetch(fetchUrl);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return { text: await response.text(), source: fetchUrl };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolveRelative(base: string, relative: string): string {
|
|
29
|
+
const baseUrl = this.supports(base)
|
|
30
|
+
? this.toRegistryUrl(base).replace("/module.yaml", "")
|
|
31
|
+
: base;
|
|
32
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
33
|
+
return new URL(relative, baseWithSlash).href;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private toRegistryUrl(moduleRef: string): string {
|
|
37
|
+
const atIdx = moduleRef.lastIndexOf("@");
|
|
38
|
+
const modulePath = moduleRef.slice(0, atIdx);
|
|
39
|
+
const version = moduleRef.slice(atIdx + 1);
|
|
40
|
+
const versionSegment = version.startsWith("v") ? version.substring(1) : version;
|
|
41
|
+
return `${REGISTRY_BASE}/${modulePath}/${versionSegment}/module.yaml`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pure alias → real module name resolver.
|
|
2
|
+
* Ported from ModuleContext.resolveKind() without any lifecycle dependency. */
|
|
3
|
+
export class AliasResolver {
|
|
4
|
+
private readonly importAliases = new Map<string, string>();
|
|
5
|
+
private readonly importedKinds = new Map<string, Set<string>>();
|
|
6
|
+
|
|
7
|
+
registerImport(alias: string, targetModule: string, exportedKinds: string[]): void {
|
|
8
|
+
this.importAliases.set(alias, targetModule);
|
|
9
|
+
if (exportedKinds.length > 0) {
|
|
10
|
+
this.importedKinds.set(alias, new Set(exportedKinds));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolves "Http.Api" → "http-server.Api". Returns undefined if alias is unknown. */
|
|
15
|
+
resolveKind(kind: string): string | undefined {
|
|
16
|
+
if (!kind) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const dot = kind.indexOf(".");
|
|
20
|
+
if (dot === -1) return undefined;
|
|
21
|
+
const prefix = kind.slice(0, dot);
|
|
22
|
+
const suffix = kind.slice(dot + 1);
|
|
23
|
+
const realModule = this.importAliases.get(prefix);
|
|
24
|
+
if (!realModule) return undefined;
|
|
25
|
+
const allowed = this.importedKinds.get(prefix);
|
|
26
|
+
if (allowed !== undefined && !allowed.has(suffix)) return undefined;
|
|
27
|
+
return `${realModule}.${suffix}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
hasAlias(alias: string): boolean {
|
|
31
|
+
return this.importAliases.has(alias);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
knownAliases(): string[] {
|
|
35
|
+
return Array.from(this.importAliases.keys());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
4
|
+
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
6
|
+
import type { AnalysisContext } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Accumulates type and alias knowledge for a running kernel or analysis session.
|
|
10
|
+
* Wraps AliasResolver and DefinitionRegistry into a single domain-level interface
|
|
11
|
+
* so callers never touch the raw registries directly.
|
|
12
|
+
*/
|
|
13
|
+
export class AnalysisRegistry {
|
|
14
|
+
private readonly defs = new DefinitionRegistry();
|
|
15
|
+
private readonly aliases = new AliasResolver();
|
|
16
|
+
|
|
17
|
+
registerDefinition(def: ResourceDefinition): void {
|
|
18
|
+
this.defs.register(def);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
registerModuleIdentity(namespace: string | null, name: string): void {
|
|
22
|
+
this.defs.registerModuleIdentity(namespace, name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
registerImport(alias: string, target: string, kinds: string[]): void {
|
|
26
|
+
this.aliases.registerImport(alias, target, kinds);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolveKind(kind: string): string | undefined {
|
|
30
|
+
return this.aliases.resolveKind(kind);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Iterates a resource's reference and scope fields as declared by its definition.
|
|
35
|
+
* Calls onRef for each plain reference field and onScope for each scope field.
|
|
36
|
+
*/
|
|
37
|
+
iterateFieldEntries(
|
|
38
|
+
resource: ResourceManifest,
|
|
39
|
+
onRef: (fieldPath: string) => void,
|
|
40
|
+
onScope: (fieldPath: string) => void,
|
|
41
|
+
): void {
|
|
42
|
+
const fieldMap = this.defs.getFieldMapForKind(resource.kind, this.aliases);
|
|
43
|
+
if (!fieldMap) return;
|
|
44
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
45
|
+
if (isScopeEntry(entry)) {
|
|
46
|
+
onScope(fieldPath);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isRefEntry(entry)) {
|
|
50
|
+
onRef(fieldPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the built-in kernel definitions. The underlying DefinitionRegistry already
|
|
57
|
+
* seeds these on construction; this method exposes them so callers (e.g. the kernel's
|
|
58
|
+
* controller registry) can iterate them without importing KERNEL_BUILTINS directly.
|
|
59
|
+
*/
|
|
60
|
+
builtinDefinitions(): ResourceDefinition[] {
|
|
61
|
+
return KERNEL_BUILTINS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
65
|
+
_context(): AnalysisContext {
|
|
66
|
+
return { aliases: this.aliases, definitions: this.defs };
|
|
67
|
+
}
|
|
68
|
+
}
|