@telorun/analyzer 0.1.2 → 0.1.3
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 +1 -1
- package/dist/adapters/node-adapter.d.ts.map +1 -1
- package/dist/adapters/node-adapter.js +2 -1
- 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/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 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +2 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -1
- 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 +2 -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 +21 -2
- package/src/adapters/http-adapter.ts +2 -2
- package/src/adapters/node-adapter.ts +2 -2
- package/src/adapters/registry-adapter.ts +2 -2
- package/src/analyzer.ts +7 -1
- package/src/cel-environment.ts +5 -3
- package/src/index.ts +1 -1
- package/src/kernel-globals.ts +110 -0
- package/src/manifest-loader.ts +40 -2
- package/src/schema-compat.ts +47 -13
- package/src/types.ts +3 -0
- package/src/validate-references.ts +13 -1
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;
|
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;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;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"}
|
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.3",
|
|
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": {
|
|
@@ -22,7 +41,7 @@
|
|
|
22
41
|
"ajv-formats": "^3.0.1",
|
|
23
42
|
"yaml": "^2.8.3",
|
|
24
43
|
"jsonpath-plus": "^10.3.0",
|
|
25
|
-
"@telorun/sdk": "0.2.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import type
|
|
3
|
+
import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
|
|
4
4
|
|
|
5
5
|
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
6
6
|
export class NodeAdapter implements ManifestAdapter {
|
|
@@ -20,7 +20,7 @@ export class NodeAdapter implements ManifestAdapter {
|
|
|
20
20
|
const filePath = url.startsWith("file://") ? new URL(url).pathname : url;
|
|
21
21
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
22
22
|
const resolvedPath =
|
|
23
|
-
stat?.isDirectory() ? path.join(filePath,
|
|
23
|
+
stat?.isDirectory() ? path.join(filePath, DEFAULT_MANIFEST_FILENAME) : filePath;
|
|
24
24
|
const text = await fs.readFile(resolvedPath, "utf8");
|
|
25
25
|
return { text, source: resolvedPath };
|
|
26
26
|
}
|
|
@@ -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/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
|
@@ -4,7 +4,7 @@ export { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
|
4
4
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
5
5
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
6
6
|
export { Loader } from "./manifest-loader.js";
|
|
7
|
-
export { DiagnosticSeverity } from "./types.js";
|
|
7
|
+
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
8
8
|
export type {
|
|
9
9
|
AnalysisDiagnostic,
|
|
10
10
|
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
|
+
}
|
package/src/manifest-loader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
|
|
3
3
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
4
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
@@ -12,6 +12,11 @@ import type {
|
|
|
12
12
|
} from "./types.js";
|
|
13
13
|
|
|
14
14
|
export class Loader {
|
|
15
|
+
private static readonly moduleCache = new Map<
|
|
16
|
+
string,
|
|
17
|
+
{ text: string; manifests: ResourceManifest[] }
|
|
18
|
+
>();
|
|
19
|
+
|
|
15
20
|
protected adapters: ManifestAdapter[];
|
|
16
21
|
|
|
17
22
|
constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
|
|
@@ -49,6 +54,12 @@ export class Loader {
|
|
|
49
54
|
|
|
50
55
|
async loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
|
|
51
56
|
const { text, source } = await this.pick(url).read(url);
|
|
57
|
+
const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
|
|
58
|
+
const cached = Loader.moduleCache.get(cacheKey);
|
|
59
|
+
if (cached && cached.text === text) {
|
|
60
|
+
return cloneManifestArray(cached.manifests);
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
const parsedDocuments = parseAllDocuments(text);
|
|
53
64
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
54
65
|
const offsets = documentLineOffsets(text);
|
|
@@ -117,7 +128,8 @@ export class Loader {
|
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
|
|
132
|
+
return cloneManifestArray(resolved);
|
|
121
133
|
}
|
|
122
134
|
|
|
123
135
|
async loadModuleGraph(
|
|
@@ -215,6 +227,32 @@ export class Loader {
|
|
|
215
227
|
}
|
|
216
228
|
}
|
|
217
229
|
|
|
230
|
+
function cloneManifestArray(manifests: ResourceManifest[]): ResourceManifest[] {
|
|
231
|
+
return manifests.map((manifest) => cloneManifestValue(manifest));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function cloneManifestValue<T>(value: T): T {
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return value.map((entry) => cloneManifestValue(entry)) as T;
|
|
237
|
+
}
|
|
238
|
+
if (isCompiledValue(value)) {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
if (value !== null && typeof value === "object") {
|
|
242
|
+
const source = value as Record<string, unknown>;
|
|
243
|
+
const clone: Record<string, unknown> = {};
|
|
244
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
245
|
+
clone[key] = cloneManifestValue(entry);
|
|
246
|
+
}
|
|
247
|
+
const positionIndex = Object.getOwnPropertyDescriptor(source, "positionIndex");
|
|
248
|
+
if (positionIndex) {
|
|
249
|
+
Object.defineProperty(clone, "positionIndex", positionIndex);
|
|
250
|
+
}
|
|
251
|
+
return clone as T;
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
218
256
|
function documentLineOffsets(text: string): number[] {
|
|
219
257
|
const offsets = [0];
|
|
220
258
|
const lines = text.split("\n");
|
package/src/schema-compat.ts
CHANGED
|
@@ -14,6 +14,7 @@ export function createAjv(): InstanceType<typeof Ajv> {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const ajv = createAjv();
|
|
17
|
+
const compiledSchemaValidators = new WeakMap<Record<string, any>, ReturnType<typeof ajv.compile>>();
|
|
17
18
|
|
|
18
19
|
export interface CompatibilityResult {
|
|
19
20
|
compatible: boolean;
|
|
@@ -131,11 +132,14 @@ export interface SchemaIssue {
|
|
|
131
132
|
|
|
132
133
|
/** Validate actual data against a JSON Schema. Returns issues with path info, or empty array if valid. */
|
|
133
134
|
export function validateAgainstSchema(data: unknown, schema: Record<string, any>): SchemaIssue[] {
|
|
134
|
-
let validate
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
let validate = compiledSchemaValidators.get(schema);
|
|
136
|
+
if (!validate) {
|
|
137
|
+
try {
|
|
138
|
+
validate = ajv.compile(schema);
|
|
139
|
+
compiledSchemaValidators.set(schema, validate);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
139
143
|
}
|
|
140
144
|
if (validate(data)) return [];
|
|
141
145
|
return (validate.errors ?? []).map((err: any) => ({
|
|
@@ -260,25 +264,55 @@ export function celPlaceholderForSchema(schema: Record<string, any>): unknown {
|
|
|
260
264
|
|
|
261
265
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
262
266
|
|
|
267
|
+
/** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
|
|
268
|
+
function resolveRef(schema: Record<string, any>, root: Record<string, any>): Record<string, any> {
|
|
269
|
+
if (schema.$ref && typeof schema.$ref === "string" && schema.$ref.startsWith("#/$defs/")) {
|
|
270
|
+
const defName = schema.$ref.slice("#/$defs/".length);
|
|
271
|
+
const resolved = root.$defs?.[defName];
|
|
272
|
+
if (resolved) return resolved;
|
|
273
|
+
}
|
|
274
|
+
return schema;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
|
|
278
|
+
function collectProperties(schema: Record<string, any>): Record<string, any> {
|
|
279
|
+
const props: Record<string, any> = { ...(schema.properties ?? {}) };
|
|
280
|
+
for (const sub of schema.oneOf ?? schema.anyOf ?? []) {
|
|
281
|
+
if (sub && typeof sub === "object" && sub.properties) {
|
|
282
|
+
for (const [k, v] of Object.entries(sub.properties as Record<string, any>)) {
|
|
283
|
+
if (!(k in props)) props[k] = v;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return props;
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
/** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
|
|
264
291
|
* schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
|
|
265
|
-
export function substituteCelFields(
|
|
292
|
+
export function substituteCelFields(
|
|
293
|
+
data: unknown,
|
|
294
|
+
schema: Record<string, any>,
|
|
295
|
+
rootSchema?: Record<string, any>,
|
|
296
|
+
): unknown {
|
|
297
|
+
const root = rootSchema ?? schema;
|
|
298
|
+
const resolved = resolveRef(schema, root);
|
|
299
|
+
|
|
266
300
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
267
|
-
return celPlaceholderForSchema(
|
|
301
|
+
return celPlaceholderForSchema(resolved);
|
|
268
302
|
}
|
|
269
303
|
if (Array.isArray(data)) {
|
|
270
|
-
const itemSchema = (
|
|
271
|
-
return data.map((item) => substituteCelFields(item, itemSchema));
|
|
304
|
+
const itemSchema = resolveRef((resolved.items ?? {}) as Record<string, any>, root);
|
|
305
|
+
return data.map((item) => substituteCelFields(item, itemSchema, root));
|
|
272
306
|
}
|
|
273
307
|
if (data !== null && typeof data === "object") {
|
|
274
|
-
const props = (
|
|
308
|
+
const props = collectProperties(resolved);
|
|
275
309
|
const addlProps =
|
|
276
|
-
|
|
277
|
-
? (
|
|
310
|
+
resolved.additionalProperties && typeof resolved.additionalProperties === "object"
|
|
311
|
+
? (resolved.additionalProperties as Record<string, any>)
|
|
278
312
|
: undefined;
|
|
279
313
|
const result: Record<string, unknown> = {};
|
|
280
314
|
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
281
|
-
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}) as Record<string, any
|
|
315
|
+
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}) as Record<string, any>, root);
|
|
282
316
|
}
|
|
283
317
|
return result;
|
|
284
318
|
}
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,9 @@ export const DiagnosticSeverity = {
|
|
|
8
8
|
} as const;
|
|
9
9
|
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
10
10
|
|
|
11
|
+
/** Default entry-point filename when a directory is given instead of a file. */
|
|
12
|
+
export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
13
|
+
|
|
11
14
|
export interface Position {
|
|
12
15
|
/** 0-based line number */
|
|
13
16
|
line: number;
|
|
@@ -134,10 +134,22 @@ export function validateReferences(
|
|
|
134
134
|
if (!val) continue;
|
|
135
135
|
|
|
136
136
|
// Name-only reference (plain string) — look up by name to validate.
|
|
137
|
+
// Qualified references use "Kind.Name" format (e.g. "Http.Api.PaymentApi");
|
|
138
|
+
// extract the resource name from the last dot segment.
|
|
137
139
|
if (typeof val === "string") {
|
|
140
|
+
const lastDot = val.lastIndexOf(".");
|
|
141
|
+
const refName = lastDot > 0 ? val.slice(lastDot + 1) : val;
|
|
142
|
+
const refKindPrefix = lastDot > 0 ? val.slice(0, lastDot) : undefined;
|
|
138
143
|
const target =
|
|
139
|
-
byName.get(
|
|
144
|
+
byName.get(refName) ?? visibleScopeManifests.find((m) => m.metadata?.name === refName);
|
|
140
145
|
if (!target) {
|
|
146
|
+
// Cross-module reference: "Alias.ResourceName" (single dot, bare alias prefix).
|
|
147
|
+
// The resource lives in the imported module's scope and can't be validated here.
|
|
148
|
+
// Multi-dot prefixes like "Alias.Kind.Name" are local resources with qualified
|
|
149
|
+
// kinds — those must be validated.
|
|
150
|
+
if (refKindPrefix && !refKindPrefix.includes(".") && aliases.hasAlias(refKindPrefix)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
141
153
|
diagnostics.push({
|
|
142
154
|
severity: DiagnosticSeverity.Error,
|
|
143
155
|
code: "UNRESOLVED_REFERENCE",
|