@telorun/analyzer 0.8.1 → 0.10.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 +5 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +7 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +5 -5
- package/dist/definition-registry.d.ts +13 -1
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +58 -2
- package/dist/dependency-graph.d.ts +1 -1
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +8 -2
- package/dist/flatten-for-analyzer.d.ts +30 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -0
- package/dist/flatten-for-analyzer.js +119 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/loaded-types.d.ts +81 -0
- package/dist/loaded-types.d.ts.map +1 -0
- package/dist/loaded-types.js +1 -0
- package/dist/manifest-loader.d.ts +30 -9
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +197 -417
- package/dist/normalize-inline-resources.d.ts +1 -1
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +7 -2
- package/dist/parse-loaded-file.d.ts +12 -0
- package/dist/parse-loaded-file.d.ts.map +1 -0
- package/dist/parse-loaded-file.js +50 -0
- package/dist/position-metadata.d.ts +27 -0
- package/dist/position-metadata.d.ts.map +1 -0
- package/dist/position-metadata.js +88 -0
- package/dist/reference-field-map.d.ts +5 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +9 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +68 -1
- package/package.json +3 -3
- package/src/analysis-registry.ts +11 -2
- package/src/analyzer.ts +17 -5
- package/src/definition-registry.ts +76 -3
- package/src/dependency-graph.ts +9 -1
- package/src/flatten-for-analyzer.ts +134 -0
- package/src/index.ts +18 -0
- package/src/loaded-types.ts +86 -0
- package/src/manifest-loader.ts +230 -459
- package/src/normalize-inline-resources.ts +8 -1
- package/src/parse-loaded-file.ts +70 -0
- package/src/position-metadata.ts +106 -0
- package/src/reference-field-map.ts +13 -0
- package/src/types.ts +6 -0
- package/src/validate-references.ts +74 -1
package/dist/manifest-loader.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { isCompiledValue } from "@telorun/sdk";
|
|
2
|
-
import { defaultCustomTags } from "@telorun/templating";
|
|
3
|
-
import { isMap, isPair, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
4
1
|
import { HttpSource } from "./sources/http-source.js";
|
|
5
2
|
import { RegistrySource } from "./sources/registry-source.js";
|
|
6
3
|
import { buildCelEnvironment } from "./cel-environment.js";
|
|
7
4
|
import { isModuleKind } from "./module-kinds.js";
|
|
8
|
-
import {
|
|
5
|
+
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
9
6
|
import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
|
|
10
7
|
const SYSTEM_KINDS = new Set([
|
|
11
8
|
"Telo.Application",
|
|
@@ -14,7 +11,11 @@ const SYSTEM_KINDS = new Set([
|
|
|
14
11
|
"Telo.Definition",
|
|
15
12
|
]);
|
|
16
13
|
export class Loader {
|
|
17
|
-
|
|
14
|
+
/** LoadedFile cache keyed by `${compile ? "compiled" : "raw"}:${source}`.
|
|
15
|
+
* Same dual-keying as the legacy ResourceManifest[] cache: a compile-mode
|
|
16
|
+
* caller (kernel) and a raw-mode caller (analyzer/editor) on the same file
|
|
17
|
+
* get distinct entries, so neither sees the wrong manifest tree. */
|
|
18
|
+
fileCache = new Map();
|
|
18
19
|
sources;
|
|
19
20
|
celEnv;
|
|
20
21
|
constructor(extraSourcesOrOptions = []) {
|
|
@@ -47,98 +48,197 @@ export class Loader {
|
|
|
47
48
|
const { source } = await this.pick(url).read(url);
|
|
48
49
|
return source;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
// --- New API: returns LoadedFile / LoadedModule / LoadedGraph ----------
|
|
52
|
+
/** Read one file via the source chain and parse it into a LoadedFile.
|
|
53
|
+
* The result is shared with `Loader.fileCache`. Callers that want a
|
|
54
|
+
* private mutable copy must call `parseLoadedFile` directly with the
|
|
55
|
+
* LoadedFile's `text`. */
|
|
56
|
+
async loadFile(url, options) {
|
|
51
57
|
const { text, source } = await this.pick(url).read(url);
|
|
52
58
|
const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
|
|
53
|
-
const cached = this.
|
|
54
|
-
if (cached && cached.text === text)
|
|
55
|
-
return
|
|
59
|
+
const cached = this.fileCache.get(cacheKey);
|
|
60
|
+
if (cached && cached.text === text)
|
|
61
|
+
return cached;
|
|
62
|
+
const loaded = parseLoadedFile(source, url, text, {
|
|
63
|
+
compile: options?.compile,
|
|
64
|
+
celEnv: this.celEnv,
|
|
65
|
+
});
|
|
66
|
+
this.fileCache.set(cacheKey, loaded);
|
|
67
|
+
return loaded;
|
|
68
|
+
}
|
|
69
|
+
/** Load an owner file plus every partial reachable through its `include:`
|
|
70
|
+
* list. Globs are expanded via the owning source's `expandGlob`. The
|
|
71
|
+
* partials list is empty when the owner declares no `include:`. */
|
|
72
|
+
async loadModule(url, options) {
|
|
73
|
+
const owner = await this.loadFile(url, options);
|
|
74
|
+
this.assertSingleModuleDeclaration(owner);
|
|
75
|
+
this.assertNoSystemKindsInPartialContext(owner, /*isPartial*/ false);
|
|
76
|
+
const moduleManifest = owner.manifests.find((m) => m && isModuleKind(m.kind));
|
|
77
|
+
const includePatterns = moduleManifest?.include;
|
|
78
|
+
if (!includePatterns?.length)
|
|
79
|
+
return { owner, partials: [] };
|
|
80
|
+
const picked = this.pick(owner.source);
|
|
81
|
+
const includedFiles = await this.resolveIncludes(owner.source, includePatterns, picked);
|
|
82
|
+
const partials = [];
|
|
83
|
+
for (const includedUrl of includedFiles) {
|
|
84
|
+
const partial = await this.loadFile(includedUrl, options);
|
|
85
|
+
this.assertNoSystemKindsInPartialContext(partial, /*isPartial*/ true);
|
|
86
|
+
partials.push(partial);
|
|
56
87
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
return { owner, partials };
|
|
89
|
+
}
|
|
90
|
+
/** Load a module and every transitively-imported library. Returns the full
|
|
91
|
+
* LoadedGraph: `entry`, `modules` keyed by canonical source, and
|
|
92
|
+
* `importEdges` mapping each importing file's PascalCase aliases to their
|
|
93
|
+
* target's canonical source. */
|
|
94
|
+
async loadGraph(entryUrl, options) {
|
|
95
|
+
const entry = await this.loadModule(entryUrl, options);
|
|
96
|
+
const rootSource = entry.owner.source;
|
|
97
|
+
const modules = new Map();
|
|
98
|
+
modules.set(rootSource, entry);
|
|
99
|
+
const importEdges = new Map();
|
|
100
|
+
const errors = [];
|
|
101
|
+
const queue = [entry];
|
|
102
|
+
const visited = new Set([rootSource]);
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const mod = queue.shift();
|
|
105
|
+
for (const file of [mod.owner, ...mod.partials]) {
|
|
106
|
+
const aliases = importEdges.get(file.source) ?? new Map();
|
|
107
|
+
for (let i = 0; i < file.manifests.length; i++) {
|
|
108
|
+
const m = file.manifests[i];
|
|
109
|
+
if (!m || m.kind !== "Telo.Import")
|
|
110
|
+
continue;
|
|
111
|
+
const importSource = m.source;
|
|
112
|
+
if (!importSource)
|
|
113
|
+
continue;
|
|
114
|
+
const alias = m.metadata?.name;
|
|
115
|
+
if (!alias)
|
|
116
|
+
continue;
|
|
117
|
+
// Source line of this Telo.Import doc — read from the LoadedFile's
|
|
118
|
+
// position table since `parseLoadedFile` doesn't stamp `sourceLine`
|
|
119
|
+
// onto manifest metadata. Used to pin import-resolution diagnostics
|
|
120
|
+
// to the line where the import was declared.
|
|
121
|
+
const sourceLine = file.positions[i]?.sourceLine ?? 0;
|
|
122
|
+
let resolvedTarget;
|
|
123
|
+
try {
|
|
124
|
+
resolvedTarget = this.resolveImportUrl(file.source, importSource);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
errors.push({
|
|
128
|
+
url: importSource,
|
|
129
|
+
fromSource: file.source,
|
|
130
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// Resolve the file we'll fetch through the source chain to get the
|
|
135
|
+
// canonical `source` URL — same identity used as the modules-map key.
|
|
136
|
+
let targetCanonical;
|
|
137
|
+
let targetModule;
|
|
138
|
+
if (modules.has(resolvedTarget)) {
|
|
139
|
+
targetCanonical = resolvedTarget;
|
|
140
|
+
targetModule = modules.get(resolvedTarget);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
const loaded = await this.loadModule(resolvedTarget, options);
|
|
145
|
+
targetCanonical = loaded.owner.source;
|
|
146
|
+
if (!modules.has(targetCanonical)) {
|
|
147
|
+
modules.set(targetCanonical, loaded);
|
|
148
|
+
targetModule = loaded;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
targetModule = modules.get(targetCanonical);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
156
|
+
e.sourceLine = sourceLine;
|
|
157
|
+
errors.push({ url: resolvedTarget, fromSource: file.source, error: e });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Resolve target identity from its Telo.Library doc and stamp it
|
|
162
|
+
// on the edge — flattenForAnalyzer reads from the edge directly,
|
|
163
|
+
// never re-deriving from manifest.metadata.
|
|
164
|
+
let targetModuleName = null;
|
|
165
|
+
let targetNamespace = null;
|
|
166
|
+
if (targetModule) {
|
|
167
|
+
const lib = targetModule.owner.manifests.find((d) => d?.kind === "Telo.Library");
|
|
168
|
+
const libName = lib?.metadata?.name;
|
|
169
|
+
if (typeof libName === "string")
|
|
170
|
+
targetModuleName = libName;
|
|
171
|
+
const libNs = lib?.metadata
|
|
172
|
+
?.namespace;
|
|
173
|
+
if (typeof libNs === "string")
|
|
174
|
+
targetNamespace = libNs;
|
|
175
|
+
}
|
|
176
|
+
aliases.set(alias, {
|
|
177
|
+
targetSource: targetCanonical,
|
|
178
|
+
targetModuleName,
|
|
179
|
+
targetNamespace,
|
|
180
|
+
});
|
|
181
|
+
if (targetModule && !visited.has(targetCanonical)) {
|
|
182
|
+
visited.add(targetCanonical);
|
|
183
|
+
this.assertImportTargetIsLibrary(targetModule, importSource, sourceLine);
|
|
184
|
+
queue.push(targetModule);
|
|
185
|
+
}
|
|
77
186
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
compiledDocs = [rawDoc];
|
|
81
|
-
}
|
|
82
|
-
for (const doc of compiledDocs) {
|
|
83
|
-
if (doc === null || doc === undefined)
|
|
84
|
-
continue;
|
|
85
|
-
const manifest = doc;
|
|
86
|
-
const metadata = { ...manifest.metadata, source, sourceLine };
|
|
87
|
-
// positionIndex is non-enumerable so it is invisible to spread, JSON.stringify,
|
|
88
|
-
// and schema validation — but still accessible via (m.metadata as any).positionIndex.
|
|
89
|
-
Object.defineProperty(metadata, "positionIndex", {
|
|
90
|
-
value: positionIndex,
|
|
91
|
-
enumerable: false,
|
|
92
|
-
writable: true,
|
|
93
|
-
configurable: true,
|
|
94
|
-
});
|
|
95
|
-
resolved.push({ ...manifest, metadata });
|
|
187
|
+
if (aliases.size > 0)
|
|
188
|
+
importEdges.set(file.source, aliases);
|
|
96
189
|
}
|
|
97
190
|
}
|
|
98
|
-
|
|
191
|
+
return { rootSource, entry, modules, importEdges, errors };
|
|
192
|
+
}
|
|
193
|
+
resolveImportUrl(fromSource, importSource) {
|
|
194
|
+
if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
195
|
+
return this.pick(fromSource).resolveRelative(fromSource, importSource);
|
|
196
|
+
}
|
|
197
|
+
return importSource;
|
|
198
|
+
}
|
|
199
|
+
assertSingleModuleDeclaration(file) {
|
|
200
|
+
const moduleManifests = file.manifests.filter((m) => !!m && isModuleKind(m.kind));
|
|
99
201
|
if (moduleManifests.length > 1) {
|
|
100
202
|
const kinds = moduleManifests.map((m) => m.kind).join(", ");
|
|
101
|
-
throw new Error(`File '${source}' contains ${moduleManifests.length} module declarations (${kinds}). ` +
|
|
203
|
+
throw new Error(`File '${file.source}' contains ${moduleManifests.length} module declarations (${kinds}). ` +
|
|
102
204
|
`A file may declare at most one Telo.Application or Telo.Library.`);
|
|
103
205
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
writable: true,
|
|
116
|
-
configurable: true,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
206
|
+
}
|
|
207
|
+
assertNoSystemKindsInPartialContext(file, isPartial) {
|
|
208
|
+
if (!isPartial)
|
|
209
|
+
return;
|
|
210
|
+
for (const m of file.manifests) {
|
|
211
|
+
if (!m)
|
|
212
|
+
continue;
|
|
213
|
+
const kind = m.kind;
|
|
214
|
+
if (typeof kind === "string" && SYSTEM_KINDS.has(kind)) {
|
|
215
|
+
throw new Error(`Included file '${file.source}' contains '${kind}' which is not allowed in partial files. ` +
|
|
216
|
+
`Only the owner telo.yaml may declare ${kind} resources.`);
|
|
120
217
|
}
|
|
121
218
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const includedFiles = await this.resolveIncludes(source, includePatterns, picked);
|
|
132
|
-
for (const includedUrl of includedFiles) {
|
|
133
|
-
const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
|
|
134
|
-
resolved.push(...partialManifests);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
219
|
+
}
|
|
220
|
+
assertImportTargetIsLibrary(target, importSource, sourceLine) {
|
|
221
|
+
const importedLibrary = target.owner.manifests.find((m) => m?.kind === "Telo.Library");
|
|
222
|
+
const importedApplication = target.owner.manifests.find((m) => m?.kind === "Telo.Application");
|
|
223
|
+
if (importedApplication) {
|
|
224
|
+
const e = new Error(`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
225
|
+
`Only Telo.Library modules may be imported. Applications are run directly, not imported.`);
|
|
226
|
+
e.sourceLine = sourceLine;
|
|
227
|
+
throw e;
|
|
137
228
|
}
|
|
138
|
-
if (!
|
|
139
|
-
|
|
229
|
+
if (!importedLibrary) {
|
|
230
|
+
const kinds = target.owner.manifests
|
|
231
|
+
.map((m) => m?.kind)
|
|
232
|
+
.filter((k) => typeof k === "string");
|
|
233
|
+
const detail = kinds.length
|
|
234
|
+
? `Fetched ${target.owner.manifests.length} document(s) with kinds [${kinds.join(", ")}].`
|
|
235
|
+
: `Fetched manifest contained no recognizable Telo documents — check that the source ` +
|
|
236
|
+
`serves a Telo.Library manifest and not an upstream error page.`;
|
|
237
|
+
const e = new Error(`Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
|
|
238
|
+
`Fetched from: ${target.owner.source}. ${detail}`);
|
|
239
|
+
e.sourceLine = sourceLine;
|
|
240
|
+
throw e;
|
|
140
241
|
}
|
|
141
|
-
return cloneManifestArray(resolved);
|
|
142
242
|
}
|
|
143
243
|
async resolveIncludes(ownerSource, patterns, source) {
|
|
144
244
|
const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
|
|
@@ -150,355 +250,35 @@ export class Loader {
|
|
|
150
250
|
}
|
|
151
251
|
return source.expandGlob(ownerSource, patterns);
|
|
152
252
|
}
|
|
153
|
-
// Literal relative paths — deduplicate in case the same file appears under multiple patterns.
|
|
154
253
|
return [...new Set(patterns.map((p) => source.resolveRelative(ownerSource, p)))];
|
|
155
254
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const lineOffsets = buildLineOffsets(text);
|
|
162
|
-
const resolved = [];
|
|
163
|
-
let docIdx = 0;
|
|
164
|
-
for (const rawDoc of rawDocs) {
|
|
165
|
-
const currentDocIdx = docIdx++;
|
|
166
|
-
const sourceLine = offsets[currentDocIdx] ?? 0;
|
|
167
|
-
const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
|
|
168
|
-
if (rawDoc === null || rawDoc === undefined)
|
|
169
|
-
continue;
|
|
170
|
-
const kind = rawDoc.kind;
|
|
171
|
-
if (kind && SYSTEM_KINDS.has(kind)) {
|
|
172
|
-
throw new Error(`Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
|
|
173
|
-
`Only the owner telo.yaml may declare ${kind} resources.`);
|
|
174
|
-
}
|
|
175
|
-
let compiledDocs;
|
|
176
|
-
if (options?.compile) {
|
|
177
|
-
try {
|
|
178
|
-
const result = precompileDoc(rawDoc, this.celEnv);
|
|
179
|
-
compiledDocs = Array.isArray(result) ? result : [result];
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
throw new Error(`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
compiledDocs = [rawDoc];
|
|
187
|
-
}
|
|
188
|
-
for (const doc of compiledDocs) {
|
|
189
|
-
if (doc === null || doc === undefined)
|
|
190
|
-
continue;
|
|
191
|
-
const manifest = doc;
|
|
192
|
-
const metadata = {
|
|
193
|
-
...manifest.metadata,
|
|
194
|
-
source,
|
|
195
|
-
sourceLine,
|
|
196
|
-
...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
|
|
197
|
-
};
|
|
198
|
-
Object.defineProperty(metadata, "positionIndex", {
|
|
199
|
-
value: positionIndex,
|
|
200
|
-
enumerable: false,
|
|
201
|
-
writable: true,
|
|
202
|
-
configurable: true,
|
|
203
|
-
});
|
|
204
|
-
resolved.push({ ...manifest, metadata });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return resolved;
|
|
208
|
-
}
|
|
209
|
-
async loadModuleForFile(fileUrl) {
|
|
210
|
-
// Try loading as a regular module first (it might be a telo.yaml itself).
|
|
211
|
-
// Use loadManifests (not loadModule) so imported definitions are included —
|
|
212
|
-
// otherwise the analyzer won't know about kinds from Telo.Import sources.
|
|
255
|
+
/** Find the owning telo.yaml for `fileUrl` (or use it directly if it's an
|
|
256
|
+
* owner) and return the `LoadedGraph` rooted at that owner. Returns
|
|
257
|
+
* `null` only when `fileUrl` is neither an owner nor reachable from one
|
|
258
|
+
* via parent-directory traversal. */
|
|
259
|
+
async loadGraphForFile(fileUrl) {
|
|
213
260
|
try {
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
|
|
261
|
+
const owner = await this.loadFile(fileUrl);
|
|
262
|
+
const isOwner = owner.manifests.some((m) => m && isModuleKind(m.kind));
|
|
263
|
+
if (isOwner) {
|
|
264
|
+
const graph = await this.loadGraph(fileUrl);
|
|
265
|
+
return { graph, ownerUrl: graph.rootSource };
|
|
220
266
|
}
|
|
221
267
|
}
|
|
222
268
|
catch (err) {
|
|
223
|
-
// If the file looks like an owner manifest (named telo.yaml), rethrow —
|
|
224
|
-
// a broken owner shouldn't silently fall through to parent lookup.
|
|
225
269
|
const normalized = fileUrl.replace(/\\/g, "/");
|
|
226
|
-
if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) ||
|
|
270
|
+
if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) ||
|
|
271
|
+
normalized === DEFAULT_MANIFEST_FILENAME) {
|
|
227
272
|
throw err;
|
|
228
273
|
}
|
|
229
|
-
// Otherwise fall through to owner lookup — this is likely a partial file
|
|
230
274
|
}
|
|
231
|
-
// Find the owning telo.yaml via parent-directory traversal
|
|
232
275
|
const source = this.pick(fileUrl);
|
|
233
276
|
if (!source.resolveOwnerOf)
|
|
234
277
|
return null;
|
|
235
278
|
const ownerUrl = await source.resolveOwnerOf(fileUrl);
|
|
236
279
|
if (!ownerUrl)
|
|
237
280
|
return null;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
|
|
241
|
-
}
|
|
242
|
-
async loadModuleGraph(entryUrl, onError) {
|
|
243
|
-
const visited = new Set([entryUrl]);
|
|
244
|
-
const result = new Map();
|
|
245
|
-
const entry = await this.loadModule(entryUrl);
|
|
246
|
-
result.set(entryUrl, entry);
|
|
247
|
-
const queue = [...entry];
|
|
248
|
-
while (queue.length > 0) {
|
|
249
|
-
const m = queue.shift();
|
|
250
|
-
if (m.kind !== "Telo.Import")
|
|
251
|
-
continue;
|
|
252
|
-
const importSource = m.source;
|
|
253
|
-
if (!importSource)
|
|
254
|
-
continue;
|
|
255
|
-
const base = m.metadata?.source ?? entryUrl;
|
|
256
|
-
const importUrl = importSource.startsWith(".") || importSource.startsWith("/")
|
|
257
|
-
? this.pick(base).resolveRelative(base, importSource)
|
|
258
|
-
: importSource;
|
|
259
|
-
if (visited.has(importUrl))
|
|
260
|
-
continue;
|
|
261
|
-
visited.add(importUrl);
|
|
262
|
-
let imported;
|
|
263
|
-
try {
|
|
264
|
-
imported = await this.loadModule(importUrl);
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
268
|
-
onError?.(importUrl, error);
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
result.set(importUrl, imported);
|
|
272
|
-
for (const im of imported) {
|
|
273
|
-
if (im.kind === "Telo.Import")
|
|
274
|
-
queue.push(im);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
|
-
async loadManifests(entryUrl) {
|
|
280
|
-
const visited = new Set([entryUrl]);
|
|
281
|
-
// Cache resolved library identity per import URL so a Telo.Import re-encountered
|
|
282
|
-
// through a different chain still gets `resolvedModuleName` / `resolvedNamespace`
|
|
283
|
-
// stamped — without re-loading the target. The early `visited` short-circuit used
|
|
284
|
-
// to silently leave duplicate Telo.Imports unstamped, which broke alias resolution
|
|
285
|
-
// when the same library was imported by two different files in the same analysis set.
|
|
286
|
-
const libraryIdentityByUrl = new Map();
|
|
287
|
-
const entry = await this.loadModule(entryUrl);
|
|
288
|
-
// Forward Telo.Definition, Telo.Abstract, AND Telo.Import docs from imported
|
|
289
|
-
// libraries to the analyzer so its downstream passes can see them:
|
|
290
|
-
// - Definitions / Abstracts feed cross-package `x-telo-ref` resolution and
|
|
291
|
-
// `extends` target validation.
|
|
292
|
-
// - Imports feed the per-library alias resolver — alias-form `extends` inside
|
|
293
|
-
// a library (e.g. ai-openai's `extends: Ai.Model`) resolves against THAT
|
|
294
|
-
// library's own `Telo.Import` declarations, not the root manifest's. Without
|
|
295
|
-
// forwarding the imports, importing such a library would surface a spurious
|
|
296
|
-
// EXTENDS_MALFORMED for an alias the library legitimately uses internally.
|
|
297
|
-
// Alias resolution itself stays in the analyzer; the loader's only semantic
|
|
298
|
-
// action is stamping `resolvedModuleName` / `resolvedNamespace` — recording the
|
|
299
|
-
// result of loading. Identity is cached per URL (see libraryIdentityByUrl above)
|
|
300
|
-
// because the same library can be reached through multiple chains, and every
|
|
301
|
-
// Telo.Import doc — including the duplicates short-circuited by `visited` —
|
|
302
|
-
// must end up stamped, otherwise per-scope alias resolution falls back to a
|
|
303
|
-
// path-derived string (e.g. "abstract-lib.yaml") and produces wrong canonical
|
|
304
|
-
// kinds.
|
|
305
|
-
const importedDefs = [];
|
|
306
|
-
const queue = [...entry];
|
|
307
|
-
while (queue.length > 0) {
|
|
308
|
-
const m = queue.shift();
|
|
309
|
-
if (m.kind !== "Telo.Import")
|
|
310
|
-
continue;
|
|
311
|
-
const importSource = m.source;
|
|
312
|
-
if (!importSource)
|
|
313
|
-
continue;
|
|
314
|
-
const base = m.metadata?.source ?? entryUrl;
|
|
315
|
-
const importUrl = importSource.startsWith(".") || importSource.startsWith("/")
|
|
316
|
-
? this.pick(base).resolveRelative(base, importSource)
|
|
317
|
-
: importSource;
|
|
318
|
-
if (!visited.has(importUrl)) {
|
|
319
|
-
visited.add(importUrl);
|
|
320
|
-
let imported;
|
|
321
|
-
try {
|
|
322
|
-
imported = await this.loadModule(importUrl);
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
326
|
-
e.sourceLine = m.metadata?.sourceLine ?? 0;
|
|
327
|
-
throw e;
|
|
328
|
-
}
|
|
329
|
-
// Import target must be a Telo.Library. Check the Library branch
|
|
330
|
-
// explicitly rather than "anything that's a module kind" so that a
|
|
331
|
-
// future third kind can't silently slip past as a valid import target.
|
|
332
|
-
const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
|
|
333
|
-
const importedApplication = imported.find((im) => im.kind === "Telo.Application");
|
|
334
|
-
if (importedApplication) {
|
|
335
|
-
const e = new Error(`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
336
|
-
`Only Telo.Library modules may be imported. Applications are run directly, not imported.`);
|
|
337
|
-
e.sourceLine = m.metadata?.sourceLine ?? 0;
|
|
338
|
-
throw e;
|
|
339
|
-
}
|
|
340
|
-
if (!importedLibrary) {
|
|
341
|
-
const kinds = imported
|
|
342
|
-
.map((im) => im.kind)
|
|
343
|
-
.filter((k) => typeof k === "string");
|
|
344
|
-
// Prefer the URL the source layer actually fetched (stamped onto every
|
|
345
|
-
// loaded manifest's metadata.source) over the raw input — for registry
|
|
346
|
-
// refs the input is e.g. "std/foo@1.0.0", not a URL.
|
|
347
|
-
const fetchedFrom = (imported[0]?.metadata?.source) ?? importUrl;
|
|
348
|
-
const detail = kinds.length
|
|
349
|
-
? `Fetched ${imported.length} document(s) with kinds [${kinds.join(", ")}].`
|
|
350
|
-
: `Fetched manifest contained no recognizable Telo documents — check that the source ` +
|
|
351
|
-
`serves a Telo.Library manifest and not an upstream error page.`;
|
|
352
|
-
const e = new Error(`Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
|
|
353
|
-
`Fetched from: ${fetchedFrom}. ${detail}`);
|
|
354
|
-
e.sourceLine = m.metadata?.sourceLine ?? 0;
|
|
355
|
-
throw e;
|
|
356
|
-
}
|
|
357
|
-
if (importedLibrary?.metadata?.name) {
|
|
358
|
-
libraryIdentityByUrl.set(importUrl, {
|
|
359
|
-
name: importedLibrary.metadata.name,
|
|
360
|
-
namespace: importedLibrary.metadata.namespace ?? null,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
for (const im of imported) {
|
|
364
|
-
if (im.kind === "Telo.Definition" ||
|
|
365
|
-
im.kind === "Telo.Abstract" ||
|
|
366
|
-
im.kind === "Telo.Import") {
|
|
367
|
-
importedDefs.push(im);
|
|
368
|
-
}
|
|
369
|
-
if (im.kind === "Telo.Import")
|
|
370
|
-
queue.push(im);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
// Stamp m with cached identity (works for both fresh and duplicate visits).
|
|
374
|
-
const identity = libraryIdentityByUrl.get(importUrl);
|
|
375
|
-
if (identity) {
|
|
376
|
-
const pi = m.metadata?.positionIndex;
|
|
377
|
-
m.metadata = {
|
|
378
|
-
...m.metadata,
|
|
379
|
-
resolvedModuleName: identity.name,
|
|
380
|
-
resolvedNamespace: identity.namespace,
|
|
381
|
-
};
|
|
382
|
-
if (pi) {
|
|
383
|
-
Object.defineProperty(m.metadata, "positionIndex", {
|
|
384
|
-
value: pi,
|
|
385
|
-
enumerable: false,
|
|
386
|
-
writable: true,
|
|
387
|
-
configurable: true,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return [...entry, ...importedDefs];
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function cloneManifestArray(manifests) {
|
|
396
|
-
return manifests.map((manifest) => cloneManifestValue(manifest));
|
|
397
|
-
}
|
|
398
|
-
function cloneManifestValue(value) {
|
|
399
|
-
if (Array.isArray(value)) {
|
|
400
|
-
return value.map((entry) => cloneManifestValue(entry));
|
|
401
|
-
}
|
|
402
|
-
if (isCompiledValue(value)) {
|
|
403
|
-
return value;
|
|
404
|
-
}
|
|
405
|
-
if (value !== null && typeof value === "object") {
|
|
406
|
-
const source = value;
|
|
407
|
-
const clone = {};
|
|
408
|
-
for (const [key, entry] of Object.entries(source)) {
|
|
409
|
-
clone[key] = cloneManifestValue(entry);
|
|
410
|
-
}
|
|
411
|
-
const positionIndex = Object.getOwnPropertyDescriptor(source, "positionIndex");
|
|
412
|
-
if (positionIndex) {
|
|
413
|
-
Object.defineProperty(clone, "positionIndex", positionIndex);
|
|
414
|
-
}
|
|
415
|
-
return clone;
|
|
416
|
-
}
|
|
417
|
-
return value;
|
|
418
|
-
}
|
|
419
|
-
function groupBySource(manifests) {
|
|
420
|
-
const map = new Map();
|
|
421
|
-
for (const m of manifests) {
|
|
422
|
-
const src = m.metadata?.source ?? "unknown";
|
|
423
|
-
let list = map.get(src);
|
|
424
|
-
if (!list) {
|
|
425
|
-
list = [];
|
|
426
|
-
map.set(src, list);
|
|
427
|
-
}
|
|
428
|
-
list.push(m);
|
|
429
|
-
}
|
|
430
|
-
return map;
|
|
431
|
-
}
|
|
432
|
-
function documentLineOffsets(text) {
|
|
433
|
-
const offsets = [0];
|
|
434
|
-
const lines = text.split("\n");
|
|
435
|
-
for (let i = 0; i < lines.length; i++) {
|
|
436
|
-
const t = lines[i].trimEnd();
|
|
437
|
-
if (t === "---" || t.startsWith("--- "))
|
|
438
|
-
offsets.push(i + 1);
|
|
439
|
-
}
|
|
440
|
-
return offsets;
|
|
441
|
-
}
|
|
442
|
-
/** Builds a byte-offset-to-line/character lookup table from raw text. */
|
|
443
|
-
function buildLineOffsets(text) {
|
|
444
|
-
const offsets = [0];
|
|
445
|
-
for (let i = 0; i < text.length; i++) {
|
|
446
|
-
if (text[i] === "\n")
|
|
447
|
-
offsets.push(i + 1);
|
|
448
|
-
}
|
|
449
|
-
return offsets;
|
|
450
|
-
}
|
|
451
|
-
function offsetToPosition(offset, lineOffsets) {
|
|
452
|
-
let lo = 0;
|
|
453
|
-
let hi = lineOffsets.length - 1;
|
|
454
|
-
while (lo < hi) {
|
|
455
|
-
const mid = (lo + hi + 1) >> 1;
|
|
456
|
-
if (lineOffsets[mid] <= offset)
|
|
457
|
-
lo = mid;
|
|
458
|
-
else
|
|
459
|
-
hi = mid - 1;
|
|
460
|
-
}
|
|
461
|
-
return { line: lo, character: offset - lineOffsets[lo] };
|
|
462
|
-
}
|
|
463
|
-
/** Walks the YAML AST and records source ranges for every field value, keyed by
|
|
464
|
-
* dotted path (e.g. "kind", "config.handler", "config.routes[0].path"). */
|
|
465
|
-
function buildPositionIndex(doc, lineOffsets) {
|
|
466
|
-
const index = new Map();
|
|
467
|
-
function recordNode(node, path) {
|
|
468
|
-
if (!node || !node.range)
|
|
469
|
-
return;
|
|
470
|
-
const [start, , end] = node.range;
|
|
471
|
-
index.set(path, {
|
|
472
|
-
start: offsetToPosition(start, lineOffsets),
|
|
473
|
-
end: offsetToPosition(end, lineOffsets),
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
function walk(node, path) {
|
|
477
|
-
if (isMap(node)) {
|
|
478
|
-
for (const pair of node.items) {
|
|
479
|
-
if (!isPair(pair))
|
|
480
|
-
continue;
|
|
481
|
-
const key = isScalar(pair.key) ? String(pair.key.value) : null;
|
|
482
|
-
if (key == null)
|
|
483
|
-
continue;
|
|
484
|
-
const childPath = path ? `${path}.${key}` : key;
|
|
485
|
-
if (pair.value != null) {
|
|
486
|
-
recordNode(pair.value, childPath);
|
|
487
|
-
walk(pair.value, childPath);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
else if (isSeq(node)) {
|
|
492
|
-
for (let i = 0; i < node.items.length; i++) {
|
|
493
|
-
const item = node.items[i];
|
|
494
|
-
const childPath = `${path}[${i}]`;
|
|
495
|
-
recordNode(item, childPath);
|
|
496
|
-
walk(item, childPath);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (doc.contents) {
|
|
501
|
-
walk(doc.contents, "");
|
|
281
|
+
const graph = await this.loadGraph(ownerUrl);
|
|
282
|
+
return { graph, ownerUrl: graph.rootSource };
|
|
502
283
|
}
|
|
503
|
-
return index;
|
|
504
284
|
}
|