@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.
- package/README.md +233 -0
- package/dist/adapters/http-adapter.d.ts +1 -1
- package/dist/adapters/http-adapter.d.ts.map +1 -1
- package/dist/adapters/http-adapter.js +2 -1
- package/dist/adapters/node-adapter.d.ts +3 -1
- package/dist/adapters/node-adapter.d.ts.map +1 -1
- package/dist/adapters/node-adapter.js +43 -4
- package/dist/adapters/registry-adapter.d.ts +1 -1
- package/dist/adapters/registry-adapter.d.ts.map +1 -1
- package/dist/adapters/registry-adapter.js +2 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +6 -1
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +4 -0
- package/dist/cel-environment.d.ts +5 -2
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +5 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/kernel-globals.d.ts +34 -0
- package/dist/kernel-globals.d.ts.map +1 -0
- package/dist/kernel-globals.js +94 -0
- package/dist/manifest-loader.d.ts +10 -2
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +164 -0
- package/dist/schema-compat.d.ts +1 -1
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +43 -14
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +13 -1
- package/package.json +22 -3
- package/src/adapters/http-adapter.ts +2 -2
- package/src/adapters/registry-adapter.ts +2 -2
- package/src/analyzer.ts +7 -1
- package/src/builtins.ts +4 -0
- package/src/cel-environment.ts +5 -3
- package/src/index.ts +1 -2
- package/src/kernel-globals.ts +110 -0
- package/src/manifest-loader.ts +204 -7
- package/src/schema-compat.ts +47 -13
- package/src/types.ts +13 -0
- package/src/validate-references.ts +13 -1
- 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,
|
|
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"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -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");
|
package/dist/schema-compat.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/schema-compat.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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(
|
|
270
|
+
return celPlaceholderForSchema(resolved);
|
|
242
271
|
}
|
|
243
272
|
if (Array.isArray(data)) {
|
|
244
|
-
const itemSchema = (
|
|
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 = (
|
|
249
|
-
const addlProps =
|
|
250
|
-
?
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -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,
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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}
|
|
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
|
|
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)}
|
|
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
|
|
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
package/src/cel-environment.ts
CHANGED
|
@@ -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`, `
|
|
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
|
+
}
|