@telorun/analyzer 0.1.3 → 0.1.4

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.
@@ -9,6 +9,8 @@ export declare class NodeAdapter implements ManifestAdapter {
9
9
  source: string;
10
10
  }>;
11
11
  resolveRelative(base: string, relative: string): string;
12
+ expandGlob(base: string, patterns: string[]): Promise<string[]>;
13
+ resolveOwnerOf(fileUrl: string): Promise<string | null>;
12
14
  }
13
15
  /** @deprecated Use `new NodeAdapter(cwd)` instead */
14
16
  export declare function createNodeAdapter(cwd?: string): ManifestAdapter;
@@ -1 +1 @@
1
- {"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/node-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9E,gFAAgF;AAChF,qBAAa,WAAY,YAAW,eAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,MAAsB;IAExD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IASlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAKxD;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,eAAe,CAE9E"}
1
+ {"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/node-adapter.ts"],"names":[],"mappings":"AAIA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9E,gFAAgF;AAChF,qBAAa,WAAY,YAAW,eAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,MAAsB;IAExD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IASlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAc/D,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAoB9D;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,eAAe,CAE9E"}
@@ -1,6 +1,11 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { minimatch } from "minimatch";
3
5
  import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
6
+ function toFilePath(url) {
7
+ return url.startsWith("file://") ? fileURLToPath(url) : url;
8
+ }
4
9
  /** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
5
10
  export class NodeAdapter {
6
11
  cwd;
@@ -15,17 +20,50 @@ export class NodeAdapter {
15
20
  (!url.includes("://") && !url.includes("@")));
16
21
  }
17
22
  async read(url) {
18
- const filePath = url.startsWith("file://") ? new URL(url).pathname : url;
23
+ const filePath = toFilePath(url);
19
24
  const stat = await fs.stat(filePath).catch(() => null);
20
25
  const resolvedPath = stat?.isDirectory() ? path.join(filePath, DEFAULT_MANIFEST_FILENAME) : filePath;
21
26
  const text = await fs.readFile(resolvedPath, "utf8");
22
27
  return { text, source: resolvedPath };
23
28
  }
24
29
  resolveRelative(base, relative) {
25
- const basePath = base.startsWith("file://") ? new URL(base).pathname : base;
26
- const baseDir = path.dirname(path.resolve(this.cwd, basePath));
30
+ const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
27
31
  return path.resolve(baseDir, relative);
28
32
  }
33
+ async expandGlob(base, patterns) {
34
+ const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
35
+ const entries = await fs.readdir(baseDir, { recursive: true, encoding: "utf8" });
36
+ const normalizedPatterns = patterns.map((p) => p.replace(/\\/g, "/").replace(/^\.\//, ""));
37
+ const matched = [];
38
+ for (const entry of entries) {
39
+ const normalized = entry.replace(/\\/g, "/");
40
+ if (normalizedPatterns.some((p) => minimatch(normalized, p))) {
41
+ matched.push(path.resolve(baseDir, entry));
42
+ }
43
+ }
44
+ return matched.sort();
45
+ }
46
+ async resolveOwnerOf(fileUrl) {
47
+ const resolved = path.resolve(this.cwd, toFilePath(fileUrl));
48
+ let dir = path.dirname(resolved);
49
+ while (true) {
50
+ const candidate = path.join(dir, DEFAULT_MANIFEST_FILENAME);
51
+ if (candidate !== resolved) {
52
+ try {
53
+ await fs.access(candidate);
54
+ return candidate;
55
+ }
56
+ catch {
57
+ // telo.yaml not found at this level
58
+ }
59
+ }
60
+ const parent = path.dirname(dir);
61
+ if (parent === dir)
62
+ break;
63
+ dir = parent;
64
+ }
65
+ return null;
66
+ }
29
67
  }
30
68
  /** @deprecated Use `new NodeAdapter(cwd)` instead */
31
69
  export function createNodeAdapter(cwd = process.cwd()) {
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EA4G/C,CAAC"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAgH/C,CAAC"}
package/dist/builtins.js CHANGED
@@ -94,6 +94,10 @@ export const KERNEL_BUILTINS = [
94
94
  ],
95
95
  },
96
96
  },
97
+ include: {
98
+ type: "array",
99
+ items: { type: "string" },
100
+ },
97
101
  exports: {
98
102
  type: "object",
99
103
  properties: {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { HttpAdapter } from "./adapters/http-adapter.js";
2
- export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
3
2
  export { RegistryAdapter } from "./adapters/registry-adapter.js";
4
3
  export { AnalysisRegistry } from "./analysis-registry.js";
5
4
  export { StaticAnalyzer } from "./analyzer.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAChE,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAChE,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { HttpAdapter } from "./adapters/http-adapter.js";
2
- export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
3
2
  export { RegistryAdapter } from "./adapters/registry-adapter.js";
4
3
  export { AnalysisRegistry } from "./analysis-registry.js";
5
4
  export { StaticAnalyzer } from "./analyzer.js";
@@ -1,5 +1,5 @@
1
1
  import { type ResourceManifest } from "@telorun/sdk";
2
- import type { LoadOptions, LoaderInitOptions, ManifestAdapter } from "./types.js";
2
+ import { type LoadOptions, type LoaderInitOptions, type ManifestAdapter } from "./types.js";
3
3
  export declare class Loader {
4
4
  private static readonly moduleCache;
5
5
  protected adapters: ManifestAdapter[];
@@ -8,6 +8,13 @@ export declare class Loader {
8
8
  private pick;
9
9
  resolveEntryPoint(url: string): Promise<string>;
10
10
  loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]>;
11
+ private resolveIncludes;
12
+ private loadPartialFile;
13
+ loadModuleForFile(fileUrl: string): Promise<{
14
+ ownerUrl: string;
15
+ manifests: ResourceManifest[];
16
+ sourceManifests: Map<string, ResourceManifest[]>;
17
+ } | null>;
11
18
  loadModuleGraph(entryUrl: string, onError?: (url: string, error: Error) => void): Promise<Map<string, ResourceManifest[]>>;
12
19
  loadManifests(entryUrl: string): Promise<ResourceManifest[]>;
13
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKtE,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,eAAe,EAGhB,MAAM,YAAY,CAAC;AAEpB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAG/B;IAEJ,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;gBAE1B,sBAAsB,GAAE,eAAe,EAAE,GAAG,iBAAsB;IAiB9E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAKxC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAgF3E,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAoDnE"}
1
+ {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGrB,MAAM,YAAY,CAAC;AAIpB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAG/B;IAEJ,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;gBAE1B,sBAAsB,GAAE,eAAe,EAAE,GAAG,iBAAsB;IAiB9E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAKxC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAmGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAoDnE"}
@@ -3,6 +3,8 @@ import { isMap, isPair, isScalar, isSeq, parseAllDocuments } from "yaml";
3
3
  import { HttpAdapter } from "./adapters/http-adapter.js";
4
4
  import { RegistryAdapter } from "./adapters/registry-adapter.js";
5
5
  import { precompileDoc } from "./precompile.js";
6
+ import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
7
+ const SYSTEM_KINDS = new Set(["Kernel.Module", "Kernel.Import", "Kernel.Definition"]);
6
8
  export class Loader {
7
9
  static moduleCache = new Map();
8
10
  adapters;
@@ -105,9 +107,126 @@ export class Loader {
105
107
  }
106
108
  }
107
109
  }
108
- Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
110
+ // Expand include directives load partial files into the same module scope.
111
+ // Results with includes are NOT cached because partial file content is not
112
+ // tracked in the cache key — the cache would serve stale data if a partial changes.
113
+ let hasIncludes = false;
114
+ if (moduleManifest) {
115
+ const includePatterns = moduleManifest.include;
116
+ if (includePatterns?.length) {
117
+ hasIncludes = true;
118
+ const adapter = this.pick(source);
119
+ const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
120
+ for (const includedUrl of includedFiles) {
121
+ const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
122
+ resolved.push(...partialManifests);
123
+ }
124
+ }
125
+ }
126
+ if (!hasIncludes) {
127
+ Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
128
+ }
109
129
  return cloneManifestArray(resolved);
110
130
  }
131
+ async resolveIncludes(ownerSource, patterns, adapter) {
132
+ const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
133
+ if (hasGlobs) {
134
+ if (!adapter.expandGlob) {
135
+ throw new Error(`Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
136
+ `does not support glob expansion. Use explicit file paths instead of patterns like: ` +
137
+ patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "));
138
+ }
139
+ return adapter.expandGlob(ownerSource, patterns);
140
+ }
141
+ // Literal relative paths — deduplicate in case the same file appears under multiple patterns.
142
+ return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
143
+ }
144
+ async loadPartialFile(url, ownerModuleName, options) {
145
+ const { text, source } = await this.pick(url).read(url);
146
+ const parsedDocuments = parseAllDocuments(text);
147
+ const rawDocs = parsedDocuments.map((d) => d.toJSON());
148
+ const offsets = documentLineOffsets(text);
149
+ const lineOffsets = buildLineOffsets(text);
150
+ const resolved = [];
151
+ let docIdx = 0;
152
+ for (const rawDoc of rawDocs) {
153
+ const currentDocIdx = docIdx++;
154
+ const sourceLine = offsets[currentDocIdx] ?? 0;
155
+ const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
156
+ if (rawDoc === null || rawDoc === undefined)
157
+ continue;
158
+ const kind = rawDoc.kind;
159
+ if (kind && SYSTEM_KINDS.has(kind)) {
160
+ throw new Error(`Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
161
+ `Only the owner telo.yaml may declare ${kind} resources.`);
162
+ }
163
+ let compiledDocs;
164
+ if (options?.compile) {
165
+ try {
166
+ const result = precompileDoc(rawDoc);
167
+ compiledDocs = Array.isArray(result) ? result : [result];
168
+ }
169
+ catch (error) {
170
+ throw new Error(`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`);
171
+ }
172
+ }
173
+ else {
174
+ compiledDocs = [rawDoc];
175
+ }
176
+ for (const doc of compiledDocs) {
177
+ if (doc === null || doc === undefined)
178
+ continue;
179
+ const manifest = doc;
180
+ const metadata = {
181
+ ...manifest.metadata,
182
+ source,
183
+ sourceLine,
184
+ ...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
185
+ };
186
+ Object.defineProperty(metadata, "positionIndex", {
187
+ value: positionIndex,
188
+ enumerable: false,
189
+ writable: true,
190
+ configurable: true,
191
+ });
192
+ resolved.push({ ...manifest, metadata });
193
+ }
194
+ }
195
+ return resolved;
196
+ }
197
+ async loadModuleForFile(fileUrl) {
198
+ // Try loading as a regular module first (it might be a telo.yaml itself).
199
+ // Use loadManifests (not loadModule) so imported definitions are included —
200
+ // otherwise the analyzer won't know about kinds from Kernel.Import sources.
201
+ try {
202
+ const docs = await this.loadModule(fileUrl);
203
+ const hasModule = docs.some((d) => d.kind === "Kernel.Module");
204
+ if (hasModule) {
205
+ const { source } = await this.pick(fileUrl).read(fileUrl);
206
+ const manifests = await this.loadManifests(fileUrl);
207
+ return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
208
+ }
209
+ }
210
+ catch (err) {
211
+ // If the file looks like an owner manifest (named telo.yaml), rethrow —
212
+ // a broken owner shouldn't silently fall through to parent lookup.
213
+ const normalized = fileUrl.replace(/\\/g, "/");
214
+ if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
215
+ throw err;
216
+ }
217
+ // Otherwise fall through to owner lookup — this is likely a partial file
218
+ }
219
+ // Find the owning telo.yaml via parent-directory traversal
220
+ const adapter = this.pick(fileUrl);
221
+ if (!adapter.resolveOwnerOf)
222
+ return null;
223
+ const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
224
+ if (!ownerUrl)
225
+ return null;
226
+ // Load the owner module (which will load included files via include expansion)
227
+ const manifests = await this.loadManifests(ownerUrl);
228
+ return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
229
+ }
111
230
  async loadModuleGraph(entryUrl, onError) {
112
231
  const visited = new Set([entryUrl]);
113
232
  const result = new Map();
@@ -224,6 +343,19 @@ function cloneManifestValue(value) {
224
343
  }
225
344
  return value;
226
345
  }
346
+ function groupBySource(manifests) {
347
+ const map = new Map();
348
+ for (const m of manifests) {
349
+ const src = m.metadata?.source ?? "unknown";
350
+ let list = map.get(src);
351
+ if (!list) {
352
+ list = [];
353
+ map.set(src, list);
354
+ }
355
+ list.push(m);
356
+ }
357
+ return map;
358
+ }
227
359
  function documentLineOffsets(text) {
228
360
  const offsets = [0];
229
361
  const lines = text.split("\n");
package/dist/types.d.ts CHANGED
@@ -42,6 +42,14 @@ export interface ManifestAdapter {
42
42
  source: string;
43
43
  }>;
44
44
  resolveRelative(base: string, relative: string): string;
45
+ /** Expand glob patterns relative to a base source. Returns sources in the same
46
+ * format as read().source — suitable to pass back into read() / resolveRelative().
47
+ * Optional — only filesystem-capable adapters implement this. */
48
+ expandGlob?(base: string, patterns: string[]): Promise<string[]>;
49
+ /** Walk parent directories from fileUrl looking for the nearest telo.yaml.
50
+ * Returns the source in the same format as read().source, or null if none found.
51
+ * Optional — only filesystem-capable adapters implement this. */
52
+ resolveOwnerOf?(fileUrl: string): Promise<string | null>;
45
53
  }
46
54
  export interface LoadOptions {
47
55
  /** When true, each YAML document is passed through the CEL precompiler before being
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CACzD;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;CACrE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;sEAEkE;IAClE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;sEAEkE;IAClE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;CACrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -39,8 +39,8 @@
39
39
  "@marcbachmann/cel-js": "^7.5.3",
40
40
  "ajv": "^8.17.1",
41
41
  "ajv-formats": "^3.0.1",
42
- "yaml": "^2.8.3",
43
42
  "jsonpath-plus": "^10.3.0",
43
+ "yaml": "^2.8.3",
44
44
  "@telorun/sdk": "0.2.8"
45
45
  },
46
46
  "devDependencies": {
package/src/builtins.ts CHANGED
@@ -96,6 +96,10 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
96
96
  ],
97
97
  },
98
98
  },
99
+ include: {
100
+ type: "array",
101
+ items: { type: "string" },
102
+ },
99
103
  exports: {
100
104
  type: "object",
101
105
  properties: {
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { HttpAdapter } from "./adapters/http-adapter.js";
2
- export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
3
2
  export { RegistryAdapter } from "./adapters/registry-adapter.js";
4
3
  export { AnalysisRegistry } from "./analysis-registry.js";
5
4
  export { StaticAnalyzer } from "./analyzer.js";
@@ -3,14 +3,17 @@ import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from
3
3
  import { HttpAdapter } from "./adapters/http-adapter.js";
4
4
  import { RegistryAdapter } from "./adapters/registry-adapter.js";
5
5
  import { precompileDoc } from "./precompile.js";
6
- import type {
7
- LoadOptions,
8
- LoaderInitOptions,
9
- ManifestAdapter,
10
- Position,
11
- PositionIndex,
6
+ import {
7
+ DEFAULT_MANIFEST_FILENAME,
8
+ type LoadOptions,
9
+ type LoaderInitOptions,
10
+ type ManifestAdapter,
11
+ type Position,
12
+ type PositionIndex,
12
13
  } from "./types.js";
13
14
 
15
+ const SYSTEM_KINDS = new Set(["Kernel.Module", "Kernel.Import", "Kernel.Definition"]);
16
+
14
17
  export class Loader {
15
18
  private static readonly moduleCache = new Map<
16
19
  string,
@@ -128,10 +131,152 @@ export class Loader {
128
131
  }
129
132
  }
130
133
 
131
- Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
134
+ // Expand include directives load partial files into the same module scope.
135
+ // Results with includes are NOT cached because partial file content is not
136
+ // tracked in the cache key — the cache would serve stale data if a partial changes.
137
+ let hasIncludes = false;
138
+ if (moduleManifest) {
139
+ const includePatterns = (moduleManifest as any).include as string[] | undefined;
140
+ if (includePatterns?.length) {
141
+ hasIncludes = true;
142
+ const adapter = this.pick(source);
143
+ const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
144
+ for (const includedUrl of includedFiles) {
145
+ const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
146
+ resolved.push(...partialManifests);
147
+ }
148
+ }
149
+ }
150
+
151
+ if (!hasIncludes) {
152
+ Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
153
+ }
132
154
  return cloneManifestArray(resolved);
133
155
  }
134
156
 
157
+ private async resolveIncludes(
158
+ ownerSource: string,
159
+ patterns: string[],
160
+ adapter: ManifestAdapter,
161
+ ): Promise<string[]> {
162
+ const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
163
+ if (hasGlobs) {
164
+ if (!adapter.expandGlob) {
165
+ throw new Error(
166
+ `Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
167
+ `does not support glob expansion. Use explicit file paths instead of patterns like: ` +
168
+ patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "),
169
+ );
170
+ }
171
+ return adapter.expandGlob(ownerSource, patterns);
172
+ }
173
+ // Literal relative paths — deduplicate in case the same file appears under multiple patterns.
174
+ return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
175
+ }
176
+
177
+ private async loadPartialFile(
178
+ url: string,
179
+ ownerModuleName: string | undefined,
180
+ options?: LoadOptions,
181
+ ): Promise<ResourceManifest[]> {
182
+ const { text, source } = await this.pick(url).read(url);
183
+
184
+ const parsedDocuments = parseAllDocuments(text);
185
+ const rawDocs = parsedDocuments.map((d) => d.toJSON());
186
+ const offsets = documentLineOffsets(text);
187
+ const lineOffsets = buildLineOffsets(text);
188
+ const resolved: ResourceManifest[] = [];
189
+ let docIdx = 0;
190
+
191
+ for (const rawDoc of rawDocs) {
192
+ const currentDocIdx = docIdx++;
193
+ const sourceLine = offsets[currentDocIdx] ?? 0;
194
+ const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
195
+ if (rawDoc === null || rawDoc === undefined) continue;
196
+
197
+ const kind = rawDoc.kind as string | undefined;
198
+ if (kind && SYSTEM_KINDS.has(kind)) {
199
+ throw new Error(
200
+ `Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
201
+ `Only the owner telo.yaml may declare ${kind} resources.`,
202
+ );
203
+ }
204
+
205
+ let compiledDocs: unknown[];
206
+ if (options?.compile) {
207
+ try {
208
+ const result = precompileDoc(rawDoc);
209
+ compiledDocs = Array.isArray(result) ? result : [result];
210
+ } catch (error) {
211
+ throw new Error(
212
+ `Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`,
213
+ );
214
+ }
215
+ } else {
216
+ compiledDocs = [rawDoc];
217
+ }
218
+
219
+ for (const doc of compiledDocs) {
220
+ if (doc === null || doc === undefined) continue;
221
+ const manifest = doc as ResourceManifest;
222
+ const metadata = {
223
+ ...manifest.metadata,
224
+ source,
225
+ sourceLine,
226
+ ...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
227
+ };
228
+ Object.defineProperty(metadata, "positionIndex", {
229
+ value: positionIndex,
230
+ enumerable: false,
231
+ writable: true,
232
+ configurable: true,
233
+ });
234
+ resolved.push({ ...manifest, metadata });
235
+ }
236
+ }
237
+
238
+ return resolved;
239
+ }
240
+
241
+ async loadModuleForFile(
242
+ fileUrl: string,
243
+ ): Promise<{
244
+ ownerUrl: string;
245
+ manifests: ResourceManifest[];
246
+ sourceManifests: Map<string, ResourceManifest[]>;
247
+ } | null> {
248
+ // Try loading as a regular module first (it might be a telo.yaml itself).
249
+ // Use loadManifests (not loadModule) so imported definitions are included —
250
+ // otherwise the analyzer won't know about kinds from Kernel.Import sources.
251
+ try {
252
+ const docs = await this.loadModule(fileUrl);
253
+ const hasModule = docs.some((d) => d.kind === "Kernel.Module");
254
+ if (hasModule) {
255
+ const { source } = await this.pick(fileUrl).read(fileUrl);
256
+ const manifests = await this.loadManifests(fileUrl);
257
+ return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
258
+ }
259
+ } catch (err) {
260
+ // If the file looks like an owner manifest (named telo.yaml), rethrow —
261
+ // a broken owner shouldn't silently fall through to parent lookup.
262
+ const normalized = fileUrl.replace(/\\/g, "/");
263
+ if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
264
+ throw err;
265
+ }
266
+ // Otherwise fall through to owner lookup — this is likely a partial file
267
+ }
268
+
269
+ // Find the owning telo.yaml via parent-directory traversal
270
+ const adapter = this.pick(fileUrl);
271
+ if (!adapter.resolveOwnerOf) return null;
272
+ const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
273
+ if (!ownerUrl) return null;
274
+
275
+ // Load the owner module (which will load included files via include expansion)
276
+ const manifests = await this.loadManifests(ownerUrl);
277
+ return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
278
+ }
279
+
135
280
  async loadModuleGraph(
136
281
  entryUrl: string,
137
282
  onError?: (url: string, error: Error) => void,
@@ -253,6 +398,20 @@ function cloneManifestValue<T>(value: T): T {
253
398
  return value;
254
399
  }
255
400
 
401
+ function groupBySource(manifests: ResourceManifest[]): Map<string, ResourceManifest[]> {
402
+ const map = new Map<string, ResourceManifest[]>();
403
+ for (const m of manifests) {
404
+ const src = (m.metadata?.source as string) ?? "unknown";
405
+ let list = map.get(src);
406
+ if (!list) {
407
+ list = [];
408
+ map.set(src, list);
409
+ }
410
+ list.push(m);
411
+ }
412
+ return map;
413
+ }
414
+
256
415
  function documentLineOffsets(text: string): number[] {
257
416
  const offsets = [0];
258
417
  const lines = text.split("\n");
package/src/types.ts CHANGED
@@ -45,6 +45,16 @@ export interface ManifestAdapter {
45
45
  supports(url: string): boolean;
46
46
  read(url: string): Promise<{ text: string; source: string }>;
47
47
  resolveRelative(base: string, relative: string): string;
48
+
49
+ /** Expand glob patterns relative to a base source. Returns sources in the same
50
+ * format as read().source — suitable to pass back into read() / resolveRelative().
51
+ * Optional — only filesystem-capable adapters implement this. */
52
+ expandGlob?(base: string, patterns: string[]): Promise<string[]>;
53
+
54
+ /** Walk parent directories from fileUrl looking for the nearest telo.yaml.
55
+ * Returns the source in the same format as read().source, or null if none found.
56
+ * Optional — only filesystem-capable adapters implement this. */
57
+ resolveOwnerOf?(fileUrl: string): Promise<string | null>;
48
58
  }
49
59
 
50
60
  export interface LoadOptions {
@@ -1,38 +0,0 @@
1
- import * as fs from "fs/promises";
2
- import * as path from "path";
3
- import { DEFAULT_MANIFEST_FILENAME, 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, DEFAULT_MANIFEST_FILENAME) : 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
- }