@telorun/analyzer 0.1.2 → 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.
Files changed (47) hide show
  1. package/README.md +233 -0
  2. package/dist/adapters/http-adapter.d.ts +1 -1
  3. package/dist/adapters/http-adapter.d.ts.map +1 -1
  4. package/dist/adapters/http-adapter.js +2 -1
  5. package/dist/adapters/node-adapter.d.ts +3 -1
  6. package/dist/adapters/node-adapter.d.ts.map +1 -1
  7. package/dist/adapters/node-adapter.js +43 -4
  8. package/dist/adapters/registry-adapter.d.ts +1 -1
  9. package/dist/adapters/registry-adapter.d.ts.map +1 -1
  10. package/dist/adapters/registry-adapter.js +2 -1
  11. package/dist/analyzer.d.ts.map +1 -1
  12. package/dist/analyzer.js +6 -1
  13. package/dist/builtins.d.ts.map +1 -1
  14. package/dist/builtins.js +4 -0
  15. package/dist/cel-environment.d.ts +5 -2
  16. package/dist/cel-environment.d.ts.map +1 -1
  17. package/dist/cel-environment.js +5 -3
  18. package/dist/index.d.ts +1 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -2
  21. package/dist/kernel-globals.d.ts +34 -0
  22. package/dist/kernel-globals.d.ts.map +1 -0
  23. package/dist/kernel-globals.js +94 -0
  24. package/dist/manifest-loader.d.ts +10 -2
  25. package/dist/manifest-loader.d.ts.map +1 -1
  26. package/dist/manifest-loader.js +164 -0
  27. package/dist/schema-compat.d.ts +1 -1
  28. package/dist/schema-compat.d.ts.map +1 -1
  29. package/dist/schema-compat.js +43 -14
  30. package/dist/types.d.ts +10 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js +2 -0
  33. package/dist/validate-references.d.ts.map +1 -1
  34. package/dist/validate-references.js +13 -1
  35. package/package.json +22 -3
  36. package/src/adapters/http-adapter.ts +2 -2
  37. package/src/adapters/registry-adapter.ts +2 -2
  38. package/src/analyzer.ts +7 -1
  39. package/src/builtins.ts +4 -0
  40. package/src/cel-environment.ts +5 -3
  41. package/src/index.ts +1 -2
  42. package/src/kernel-globals.ts +110 -0
  43. package/src/manifest-loader.ts +204 -7
  44. package/src/schema-compat.ts +47 -13
  45. package/src/types.ts +13 -0
  46. package/src/validate-references.ts +13 -1
  47. package/src/adapters/node-adapter.ts +0 -38
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKrD,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,eAAe,EAGhB,MAAM,YAAY,CAAC;AAEpB,qBAAa,MAAM;IACjB,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;IAyE3E,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"}
@@ -1,8 +1,12 @@
1
+ import { isCompiledValue } from "@telorun/sdk";
1
2
  import { isMap, isPair, isScalar, isSeq, parseAllDocuments } from "yaml";
2
3
  import { HttpAdapter } from "./adapters/http-adapter.js";
3
4
  import { RegistryAdapter } from "./adapters/registry-adapter.js";
4
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"]);
5
8
  export class Loader {
9
+ static moduleCache = new Map();
6
10
  adapters;
7
11
  constructor(extraAdaptersOrOptions = []) {
8
12
  const options = Array.isArray(extraAdaptersOrOptions)
@@ -35,6 +39,11 @@ export class Loader {
35
39
  }
36
40
  async loadModule(url, options) {
37
41
  const { text, source } = await this.pick(url).read(url);
42
+ const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
43
+ const cached = Loader.moduleCache.get(cacheKey);
44
+ if (cached && cached.text === text) {
45
+ return cloneManifestArray(cached.manifests);
46
+ }
38
47
  const parsedDocuments = parseAllDocuments(text);
39
48
  const rawDocs = parsedDocuments.map((d) => d.toJSON());
40
49
  const offsets = documentLineOffsets(text);
@@ -98,8 +107,126 @@ export class Loader {
98
107
  }
99
108
  }
100
109
  }
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
+ }
129
+ return cloneManifestArray(resolved);
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
+ }
101
195
  return resolved;
102
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
+ }
103
230
  async loadModuleGraph(entryUrl, onError) {
104
231
  const visited = new Set([entryUrl]);
105
232
  const result = new Map();
@@ -192,6 +319,43 @@ export class Loader {
192
319
  return [...entry, ...importedDefs];
193
320
  }
194
321
  }
322
+ function cloneManifestArray(manifests) {
323
+ return manifests.map((manifest) => cloneManifestValue(manifest));
324
+ }
325
+ function cloneManifestValue(value) {
326
+ if (Array.isArray(value)) {
327
+ return value.map((entry) => cloneManifestValue(entry));
328
+ }
329
+ if (isCompiledValue(value)) {
330
+ return value;
331
+ }
332
+ if (value !== null && typeof value === "object") {
333
+ const source = value;
334
+ const clone = {};
335
+ for (const [key, entry] of Object.entries(source)) {
336
+ clone[key] = cloneManifestValue(entry);
337
+ }
338
+ const positionIndex = Object.getOwnPropertyDescriptor(source, "positionIndex");
339
+ if (positionIndex) {
340
+ Object.defineProperty(clone, "positionIndex", positionIndex);
341
+ }
342
+ return clone;
343
+ }
344
+ return value;
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
+ }
195
359
  function documentLineOffsets(text) {
196
360
  const offsets = [0];
197
361
  const lines = text.split("\n");
@@ -37,6 +37,6 @@ export declare function celTypeSatisfiesJsonSchema(celType: string, schema: Reco
37
37
  export declare function celPlaceholderForSchema(schema: Record<string, any>): unknown;
38
38
  /** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
39
39
  * schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
40
- export declare function substituteCelFields(data: unknown, schema: Record<string, any>): unknown;
40
+ export declare function substituteCelFields(data: unknown, schema: Record<string, any>, rootSchema?: Record<string, any>): unknown;
41
41
  export {};
42
42
  //# sourceMappingURL=schema-compat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;0FAC0F;AAC1F,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAMpD;AAID,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;oEAEoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,mBAAmB,CAIrB;AAiDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAelD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGxE;AAuBD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CAY/F;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ7E;AAED;;;;6DAI6D;AAC7D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAsBjC;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAuBnF;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAqBhG;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAiB5E;AAID;iGACiG;AACjG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAqBvF"}
1
+ {"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;0FAC0F;AAC1F,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAMpD;AAKD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;oEAEoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,mBAAmB,CAIrB;AAiDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAelD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGxE;AAuBD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CAe/F;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ7E;AAED;;;;6DAI6D;AAC7D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAsBjC;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAuBnF;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAqBhG;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAiB5E;AA2BD;iGACiG;AACjG,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC/B,OAAO,CAwBT"}
@@ -11,6 +11,7 @@ export function createAjv() {
11
11
  return instance;
12
12
  }
13
13
  const ajv = createAjv();
14
+ const compiledSchemaValidators = new WeakMap();
14
15
  /** Conservative structural JSON Schema compatibility check.
15
16
  * Only flags definite mismatches: missing required fields and primitive type conflicts.
16
17
  * Ambiguous cases (anyOf/oneOf/etc.) are treated as compatible. */
@@ -93,12 +94,15 @@ function ajvErrorToPath(err) {
93
94
  }
94
95
  /** Validate actual data against a JSON Schema. Returns issues with path info, or empty array if valid. */
95
96
  export function validateAgainstSchema(data, schema) {
96
- let validate;
97
- try {
98
- validate = ajv.compile(schema);
99
- }
100
- catch {
101
- return [];
97
+ let validate = compiledSchemaValidators.get(schema);
98
+ if (!validate) {
99
+ try {
100
+ validate = ajv.compile(schema);
101
+ compiledSchemaValidators.set(schema, validate);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
102
106
  }
103
107
  if (validate(data))
104
108
  return [];
@@ -234,24 +238,49 @@ export function celPlaceholderForSchema(schema) {
234
238
  }
235
239
  }
236
240
  const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
241
+ /** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
242
+ function resolveRef(schema, root) {
243
+ if (schema.$ref && typeof schema.$ref === "string" && schema.$ref.startsWith("#/$defs/")) {
244
+ const defName = schema.$ref.slice("#/$defs/".length);
245
+ const resolved = root.$defs?.[defName];
246
+ if (resolved)
247
+ return resolved;
248
+ }
249
+ return schema;
250
+ }
251
+ /** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
252
+ function collectProperties(schema) {
253
+ const props = { ...(schema.properties ?? {}) };
254
+ for (const sub of schema.oneOf ?? schema.anyOf ?? []) {
255
+ if (sub && typeof sub === "object" && sub.properties) {
256
+ for (const [k, v] of Object.entries(sub.properties)) {
257
+ if (!(k in props))
258
+ props[k] = v;
259
+ }
260
+ }
261
+ }
262
+ return props;
263
+ }
237
264
  /** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
238
265
  * schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
239
- export function substituteCelFields(data, schema) {
266
+ export function substituteCelFields(data, schema, rootSchema) {
267
+ const root = rootSchema ?? schema;
268
+ const resolved = resolveRef(schema, root);
240
269
  if (typeof data === "string" && CEL_PURE_RE.test(data)) {
241
- return celPlaceholderForSchema(schema);
270
+ return celPlaceholderForSchema(resolved);
242
271
  }
243
272
  if (Array.isArray(data)) {
244
- const itemSchema = (schema.items ?? {});
245
- return data.map((item) => substituteCelFields(item, itemSchema));
273
+ const itemSchema = resolveRef((resolved.items ?? {}), root);
274
+ return data.map((item) => substituteCelFields(item, itemSchema, root));
246
275
  }
247
276
  if (data !== null && typeof data === "object") {
248
- const props = (schema.properties ?? {});
249
- const addlProps = schema.additionalProperties && typeof schema.additionalProperties === "object"
250
- ? schema.additionalProperties
277
+ const props = collectProperties(resolved);
278
+ const addlProps = resolved.additionalProperties && typeof resolved.additionalProperties === "object"
279
+ ? resolved.additionalProperties
251
280
  : undefined;
252
281
  const result = {};
253
282
  for (const [k, v] of Object.entries(data)) {
254
- result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}));
283
+ result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}), root);
255
284
  }
256
285
  return result;
257
286
  }
package/dist/types.d.ts CHANGED
@@ -7,6 +7,8 @@ export declare const DiagnosticSeverity: {
7
7
  readonly Hint: 4;
8
8
  };
9
9
  export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
10
+ /** Default entry-point filename when a directory is given instead of a file. */
11
+ export declare const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
10
12
  export interface Position {
11
13
  /** 0-based line number */
12
14
  line: number;
@@ -40,6 +42,14 @@ export interface ManifestAdapter {
40
42
  source: string;
41
43
  }>;
42
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>;
43
53
  }
44
54
  export interface LoadOptions {
45
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,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/dist/types.js CHANGED
@@ -6,3 +6,5 @@ export const DiagnosticSeverity = {
6
6
  Information: 3,
7
7
  Hint: 4,
8
8
  };
9
+ /** Default entry-point filename when a directory is given instead of a file. */
10
+ export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
@@ -1 +1 @@
1
- {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA6C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAoPtB"}
1
+ {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA6C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAgQtB"}
@@ -111,9 +111,21 @@ export function validateReferences(resources, context) {
111
111
  if (!val)
112
112
  continue;
113
113
  // Name-only reference (plain string) — look up by name to validate.
114
+ // Qualified references use "Kind.Name" format (e.g. "Http.Api.PaymentApi");
115
+ // extract the resource name from the last dot segment.
114
116
  if (typeof val === "string") {
115
- const target = byName.get(val) ?? visibleScopeManifests.find((m) => m.metadata?.name === val);
117
+ const lastDot = val.lastIndexOf(".");
118
+ const refName = lastDot > 0 ? val.slice(lastDot + 1) : val;
119
+ const refKindPrefix = lastDot > 0 ? val.slice(0, lastDot) : undefined;
120
+ const target = byName.get(refName) ?? visibleScopeManifests.find((m) => m.metadata?.name === refName);
116
121
  if (!target) {
122
+ // Cross-module reference: "Alias.ResourceName" (single dot, bare alias prefix).
123
+ // The resource lives in the imported module's scope and can't be validated here.
124
+ // Multi-dot prefixes like "Alias.Kind.Name" are local resources with qualified
125
+ // kinds — those must be validated.
126
+ if (refKindPrefix && !refKindPrefix.includes(".") && aliases.hasAlias(refKindPrefix)) {
127
+ continue;
128
+ }
117
129
  diagnostics.push({
118
130
  severity: DiagnosticSeverity.Error,
119
131
  code: "UNRESOLVED_REFERENCE",
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
+ "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
+ "keywords": [
6
+ "telo",
7
+ "analyzer",
8
+ "validator",
9
+ "manifest",
10
+ "yaml"
11
+ ],
12
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
13
+ "license": "SEE LICENSE IN LICENSE",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/telorun/telo.git",
17
+ "directory": "analyzer/nodejs"
18
+ },
19
+ "homepage": "https://github.com/telorun/telo#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/telorun/telo/issues"
22
+ },
4
23
  "type": "module",
5
24
  "main": "./dist/index.js",
6
25
  "exports": {
@@ -20,9 +39,9 @@
20
39
  "@marcbachmann/cel-js": "^7.5.3",
21
40
  "ajv": "^8.17.1",
22
41
  "ajv-formats": "^3.0.1",
23
- "yaml": "^2.8.3",
24
42
  "jsonpath-plus": "^10.3.0",
25
- "@telorun/sdk": "0.2.7"
43
+ "yaml": "^2.8.3",
44
+ "@telorun/sdk": "0.2.8"
26
45
  },
27
46
  "devDependencies": {
28
47
  "@types/node": "^20.0.0",
@@ -1,4 +1,4 @@
1
- import type { ManifestAdapter } from "../types.js";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
2
2
 
3
3
  export class HttpAdapter implements ManifestAdapter {
4
4
  supports(url: string): boolean {
@@ -6,7 +6,7 @@ export class HttpAdapter implements ManifestAdapter {
6
6
  }
7
7
 
8
8
  async read(url: string): Promise<{ text: string; source: string }> {
9
- const fetchUrl = url.includes(".yaml") ? url : `${url}/module.yaml`;
9
+ const fetchUrl = url.includes(".yaml") ? url : `${url}/${DEFAULT_MANIFEST_FILENAME}`;
10
10
  const response = await fetch(fetchUrl);
11
11
  if (!response.ok) {
12
12
  throw new Error(
@@ -1,4 +1,4 @@
1
- import type { ManifestAdapter } from "../types.js";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
2
2
 
3
3
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
4
4
 
@@ -40,7 +40,7 @@ export class RegistryAdapter implements ManifestAdapter {
40
40
  }
41
41
 
42
42
  private toRegistryUrl(moduleRef: string): string {
43
- return `${this.toRegistryModuleBase(moduleRef)}/module.yaml`;
43
+ return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
44
44
  }
45
45
 
46
46
  private parseModuleRef(moduleRef: string): { modulePath: string; version: string } {
package/src/analyzer.ts CHANGED
@@ -4,6 +4,7 @@ import { AnalysisRegistry } from "./analysis-registry.js";
4
4
  import { buildTypedCelEnvironment, celEnvironment } from "./cel-environment.js";
5
5
  import { DefinitionRegistry } from "./definition-registry.js";
6
6
  import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
7
+ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
7
8
  import { normalizeInlineResources } from "./normalize-inline-resources.js";
8
9
  import {
9
10
  celTypeSatisfiesJsonSchema,
@@ -317,6 +318,10 @@ export class StaticAnalyzer {
317
318
  }
318
319
  }
319
320
 
321
+ // Build typed kernel globals schema so x-telo-context chain validation
322
+ // recognises variables, secrets, resources, env automatically
323
+ const kernelGlobals = buildKernelGlobalsSchema(allManifests);
324
+
320
325
  // Validate each non-definition, non-system resource
321
326
  for (const m of allManifests) {
322
327
  if (!m.kind || !m.metadata?.name) {
@@ -463,11 +468,12 @@ export class StaticAnalyzer {
463
468
  const manifestItem = matchedScope
464
469
  ? getManifestItem(path, matchedScope, m as Record<string, any>)
465
470
  : (m as Record<string, any>);
466
- const effectiveContext = resolveContextAnnotations(
471
+ const resolvedContext = resolveContextAnnotations(
467
472
  matchedContext,
468
473
  manifestItem,
469
474
  allManifests as Record<string, any>[],
470
475
  );
476
+ const effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
471
477
 
472
478
  for (const chain of accessChains) {
473
479
  const err = validateChainAgainstSchema(chain, effectiveContext);
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: {
@@ -20,8 +20,11 @@ export const celEnvironment = new Environment({ unlistedVariablesAreDyn: true })
20
20
  *
21
21
  * - `variables`: typed from the manifest's `variables` field if it is a schema map
22
22
  * (only `Kernel.Module` resources carry this); otherwise registered as `map` (dyn).
23
- * - `secrets`, `resources`, `imports`, `env`: always `map` (dyn — output schemas unknown).
24
- * - `extraContextSchema`: additional variables from an `x-telo-context` annotation. */
23
+ * - `secrets`, `resources`, `env`: always `map` (dyn — output schemas unknown).
24
+ * - `extraContextSchema`: additional variables from an `x-telo-context` annotation.
25
+ *
26
+ * NOTE: The set of kernel globals registered here must match `KERNEL_GLOBAL_NAMES`
27
+ * in kernel-globals.ts, which is used for chain-access validation. */
25
28
  export function buildTypedCelEnvironment(
26
29
  manifest: ResourceManifest,
27
30
  extraContextSchema?: Record<string, any> | null,
@@ -50,7 +53,6 @@ export function buildTypedCelEnvironment(
50
53
 
51
54
  env.registerVariable("secrets", "map");
52
55
  env.registerVariable("resources", "map");
53
- env.registerVariable("imports", "map");
54
56
  env.registerVariable("env", "map");
55
57
 
56
58
  if (extraContextSchema?.properties) {
package/src/index.ts CHANGED
@@ -1,10 +1,9 @@
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";
6
5
  export { Loader } from "./manifest-loader.js";
7
- export { DiagnosticSeverity } from "./types.js";
6
+ export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
8
7
  export type {
9
8
  AnalysisDiagnostic,
10
9
  AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestAdapter,
@@ -0,0 +1,110 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+
3
+ /**
4
+ * Kernel global names available in every CEL evaluation context at runtime.
5
+ * Both `buildKernelGlobalsSchema` (chain-access validation) and
6
+ * `buildTypedCelEnvironment` in cel-environment.ts (CEL type-checking)
7
+ * must stay in sync with this list.
8
+ *
9
+ * Note: `env` is only available in the root module context. Child modules
10
+ * loaded via Kernel.Import do not receive host environment variables.
11
+ * There is no `imports` namespace at runtime — import snapshots are stored
12
+ * under `resources.<alias>`.
13
+ */
14
+ export const KERNEL_GLOBAL_NAMES = ["variables", "secrets", "resources", "env"] as const;
15
+
16
+ const SYSTEM_KINDS = new Set([
17
+ "Kernel.Definition",
18
+ "Kernel.Module",
19
+ "Kernel.Abstract",
20
+ ]);
21
+
22
+ /**
23
+ * Build a typed JSON Schema describing the kernel globals available in the
24
+ * given manifest set. Used to merge into `x-telo-context` schemas so that
25
+ * chain-access validation recognises kernel globals without module authors
26
+ * having to re-declare them.
27
+ *
28
+ * - `variables` / `secrets`: typed from the `Kernel.Module` declaration
29
+ * - `resources`: enumerates all non-system resource names
30
+ * - `env`: dynamic (runtime env vars, root module only)
31
+ */
32
+ export function buildKernelGlobalsSchema(
33
+ manifests: ResourceManifest[],
34
+ ): Record<string, any> {
35
+ const moduleManifest = manifests.find((m) => m.kind === "Kernel.Module") as
36
+ | Record<string, any>
37
+ | undefined;
38
+
39
+ const resourceProps: Record<string, any> = {};
40
+ for (const m of manifests) {
41
+ const name = m.metadata?.name as string | undefined;
42
+ if (!name || !m.kind) continue;
43
+ // Kernel.Import snapshots are stored under resources.<alias> at runtime,
44
+ // so they appear here alongside regular resources.
45
+ if (!SYSTEM_KINDS.has(m.kind)) {
46
+ resourceProps[name] = { type: "object", additionalProperties: true };
47
+ }
48
+ }
49
+
50
+ return {
51
+ type: "object",
52
+ properties: {
53
+ variables: buildSchemaMapSchema(moduleManifest?.variables),
54
+ secrets: buildSchemaMapSchema(moduleManifest?.secrets),
55
+ resources: {
56
+ type: "object",
57
+ properties: resourceProps,
58
+ additionalProperties: false,
59
+ },
60
+ env: { type: "object", additionalProperties: true },
61
+ },
62
+ };
63
+ }
64
+
65
+ /** Wrap a JSON Schema property map (like `Kernel.Module.variables`) into a
66
+ * closed object schema suitable for chain-access validation. Falls back to
67
+ * an open map when the module declares no variables/secrets. */
68
+ function buildSchemaMapSchema(
69
+ schemaMap: Record<string, any> | null | undefined,
70
+ ): Record<string, any> {
71
+ if (!schemaMap || typeof schemaMap !== "object" || Array.isArray(schemaMap)) {
72
+ return { type: "object", additionalProperties: true };
73
+ }
74
+ const props: Record<string, any> = {};
75
+ for (const [key, value] of Object.entries(schemaMap)) {
76
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
77
+ props[key] = value;
78
+ }
79
+ }
80
+ if (Object.keys(props).length === 0) {
81
+ return { type: "object", additionalProperties: true };
82
+ }
83
+ return {
84
+ type: "object",
85
+ properties: props,
86
+ additionalProperties: false,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Merge kernel globals into an `x-telo-context` schema so chain-access
92
+ * validation recognises `variables`, `secrets`, `resources`, `env`
93
+ * without module authors having to re-declare them.
94
+ *
95
+ * Context-specific properties take precedence over globals (spread order).
96
+ * The original `additionalProperties` setting is preserved.
97
+ */
98
+ export function mergeKernelGlobalsIntoContext(
99
+ contextSchema: Record<string, any>,
100
+ globalsSchema: Record<string, any>,
101
+ ): Record<string, any> {
102
+ return {
103
+ ...contextSchema,
104
+ properties: {
105
+ ...globalsSchema.properties,
106
+ ...(contextSchema.properties ?? {}),
107
+ },
108
+ additionalProperties: contextSchema.additionalProperties ?? false,
109
+ };
110
+ }