@telorun/analyzer 0.1.1
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/LICENSE +17 -0
- package/dist/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +17 -0
- package/dist/adapters/node-adapter.d.ts +15 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +32 -0
- package/dist/adapters/registry-adapter.d.ts +11 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +33 -0
- package/dist/alias-resolver.d.ts +12 -0
- package/dist/alias-resolver.d.ts.map +1 -0
- package/dist/alias-resolver.js +36 -0
- package/dist/analysis-registry.d.ts +29 -0
- package/dist/analysis-registry.d.ts.map +1 -0
- package/dist/analysis-registry.js +55 -0
- package/dist/analyzer.d.ts +14 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +314 -0
- package/dist/builtins.d.ts +3 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/builtins.js +109 -0
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -0
- package/dist/cel-environment.js +59 -0
- package/dist/definition-registry.d.ts +58 -0
- package/dist/definition-registry.d.ts.map +1 -0
- package/dist/definition-registry.js +155 -0
- package/dist/dependency-graph.d.ts +38 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +155 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/manifest-loader.d.ts +11 -0
- package/dist/manifest-loader.d.ts.map +1 -0
- package/dist/manifest-loader.js +194 -0
- package/dist/normalize-inline-resources.d.ts +22 -0
- package/dist/normalize-inline-resources.d.ts.map +1 -0
- package/dist/normalize-inline-resources.js +136 -0
- package/dist/precompile.d.ts +9 -0
- package/dist/precompile.d.ts.map +1 -0
- package/dist/precompile.js +51 -0
- package/dist/reference-field-map.d.ts +53 -0
- package/dist/reference-field-map.d.ts.map +1 -0
- package/dist/reference-field-map.js +107 -0
- package/dist/schema-compat.d.ts +42 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +234 -0
- package/dist/scope-resolver.d.ts +5 -0
- package/dist/scope-resolver.d.ts.map +1 -0
- package/dist/scope-resolver.js +13 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/validate-cel-context.d.ts +24 -0
- package/dist/validate-cel-context.d.ts.map +1 -0
- package/dist/validate-cel-context.js +136 -0
- package/dist/validate-references.d.ts +19 -0
- package/dist/validate-references.d.ts.map +1 -0
- package/dist/validate-references.js +275 -0
- package/package.json +34 -0
- package/src/adapters/http-adapter.ts +23 -0
- package/src/adapters/node-adapter.ts +38 -0
- package/src/adapters/registry-adapter.ts +43 -0
- package/src/alias-resolver.ts +37 -0
- package/src/analysis-registry.ts +68 -0
- package/src/analyzer.ts +399 -0
- package/src/builtins.ts +111 -0
- package/src/cel-environment.ts +70 -0
- package/src/definition-registry.ts +170 -0
- package/src/dependency-graph.ts +187 -0
- package/src/index.ts +17 -0
- package/src/manifest-loader.ts +203 -0
- package/src/normalize-inline-resources.ts +170 -0
- package/src/precompile.ts +54 -0
- package/src/reference-field-map.ts +147 -0
- package/src/schema-compat.ts +264 -0
- package/src/scope-resolver.ts +13 -0
- package/src/types.ts +68 -0
- package/src/validate-cel-context.ts +142 -0
- package/src/validate-references.ts +311 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import AjvModule from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
const Ajv = AjvModule.default ?? AjvModule;
|
|
4
|
+
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
5
|
+
* Called once for the module-level instance and once per DefinitionRegistry instance. */
|
|
6
|
+
export function createAjv() {
|
|
7
|
+
const instance = new Ajv({ allErrors: true, strict: false });
|
|
8
|
+
addFormats.default
|
|
9
|
+
? addFormats.default(instance)
|
|
10
|
+
: addFormats(instance);
|
|
11
|
+
return instance;
|
|
12
|
+
}
|
|
13
|
+
const ajv = createAjv();
|
|
14
|
+
/** Conservative structural JSON Schema compatibility check.
|
|
15
|
+
* Only flags definite mismatches: missing required fields and primitive type conflicts.
|
|
16
|
+
* Ambiguous cases (anyOf/oneOf/etc.) are treated as compatible. */
|
|
17
|
+
export function checkSchemaCompatibility(source, target) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
checkObject(source, target, "", issues);
|
|
20
|
+
return { compatible: issues.length === 0, issues };
|
|
21
|
+
}
|
|
22
|
+
function checkObject(source, target, path, issues) {
|
|
23
|
+
const targetRequired = target.required ?? [];
|
|
24
|
+
const sourceProps = source.properties ?? {};
|
|
25
|
+
const targetProps = target.properties ?? {};
|
|
26
|
+
for (const field of targetRequired) {
|
|
27
|
+
if (!(field in sourceProps)) {
|
|
28
|
+
issues.push(`${path}/${field}: required by target but missing from source`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const srcProp = sourceProps[field];
|
|
32
|
+
const tgtProp = targetProps[field];
|
|
33
|
+
if (tgtProp && srcProp) {
|
|
34
|
+
checkProperty(srcProp, tgtProp, `${path}/${field}`, issues);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function checkProperty(source, target, path, issues) {
|
|
39
|
+
// Only flag definite primitive type clashes; skip anyOf/oneOf/allOf
|
|
40
|
+
if (source.type &&
|
|
41
|
+
target.type &&
|
|
42
|
+
typeof source.type === "string" &&
|
|
43
|
+
typeof target.type === "string" &&
|
|
44
|
+
source.type !== target.type) {
|
|
45
|
+
issues.push(`${path}: type mismatch — source is '${source.type}', target expects '${target.type}'`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (target.type === "object" && source.type === "object") {
|
|
49
|
+
checkObject(source, target, path, issues);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function formatSingleError(err) {
|
|
53
|
+
const p = err.instancePath || "/";
|
|
54
|
+
return `${p} ${err.message ?? "is invalid"}`;
|
|
55
|
+
}
|
|
56
|
+
export function formatAjvErrors(errors) {
|
|
57
|
+
if (!errors || errors.length === 0)
|
|
58
|
+
return "Unknown schema error";
|
|
59
|
+
return errors.map(formatSingleError).join("; ");
|
|
60
|
+
}
|
|
61
|
+
/** Converts an AJV error object to a dotted path string compatible with PositionIndex keys.
|
|
62
|
+
* e.g. instancePath "/config/routes/0/handler" → "config.routes[0].handler"
|
|
63
|
+
* For "required" keyword errors, appends the missing property to the parent path. */
|
|
64
|
+
function ajvErrorToPath(err) {
|
|
65
|
+
const instancePath = (err.instancePath ?? "");
|
|
66
|
+
const parts = instancePath.split("/").filter((p) => p !== "");
|
|
67
|
+
let result = "";
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
if (/^\d+$/.test(part)) {
|
|
70
|
+
result += `[${part}]`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result += result ? `.${part}` : part;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
77
|
+
const missing = err.params.missingProperty;
|
|
78
|
+
result += result ? `.${missing}` : missing;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
/** Validate actual data against a JSON Schema. Returns issues with path info, or empty array if valid. */
|
|
83
|
+
export function validateAgainstSchema(data, schema) {
|
|
84
|
+
let validate;
|
|
85
|
+
try {
|
|
86
|
+
validate = ajv.compile(schema);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
if (validate(data))
|
|
92
|
+
return [];
|
|
93
|
+
return (validate.errors ?? []).map((err) => ({
|
|
94
|
+
message: formatSingleError(err),
|
|
95
|
+
path: ajvErrorToPath(err),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/** Resolves a JSON Pointer (RFC 6901, must start with "/") into a schema object.
|
|
99
|
+
* Returns undefined when any segment along the path is missing or not an object. */
|
|
100
|
+
export function navigateJsonPointer(schema, pointer) {
|
|
101
|
+
const segments = pointer.split("/").slice(1); // drop leading empty string from "/"
|
|
102
|
+
let current = schema;
|
|
103
|
+
for (const seg of segments) {
|
|
104
|
+
if (current === null || typeof current !== "object")
|
|
105
|
+
return undefined;
|
|
106
|
+
current = current[seg];
|
|
107
|
+
}
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
/** Navigate a JSON Schema following a `walkCelExpressions`-style path
|
|
111
|
+
* (e.g. `port`, `routes[0].handler.when`).
|
|
112
|
+
* Dot-separated segments navigate `properties`; `[N]` indices navigate `items`.
|
|
113
|
+
* Stops and returns the current node when a union type (`anyOf`/`oneOf`) is reached.
|
|
114
|
+
* Returns `undefined` if any segment cannot be resolved. */
|
|
115
|
+
export function navigateSchemaToExprPath(schema, path) {
|
|
116
|
+
if (!path)
|
|
117
|
+
return schema;
|
|
118
|
+
let current = schema;
|
|
119
|
+
for (const part of path.split(".")) {
|
|
120
|
+
if (!current || typeof current !== "object")
|
|
121
|
+
return undefined;
|
|
122
|
+
if (current.anyOf || current.oneOf)
|
|
123
|
+
return current;
|
|
124
|
+
const m = part.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\[\d+\])*)$/);
|
|
125
|
+
if (!m)
|
|
126
|
+
return undefined;
|
|
127
|
+
const [, ident, indices] = m;
|
|
128
|
+
const props = current.properties;
|
|
129
|
+
if (!props || !(ident in props))
|
|
130
|
+
return undefined;
|
|
131
|
+
current = props[ident];
|
|
132
|
+
if (!current)
|
|
133
|
+
return undefined;
|
|
134
|
+
const indexCount = (indices.match(/\[/g) ?? []).length;
|
|
135
|
+
for (let i = 0; i < indexCount; i++) {
|
|
136
|
+
if (!current || typeof current !== "object")
|
|
137
|
+
return undefined;
|
|
138
|
+
if (current.anyOf || current.oneOf)
|
|
139
|
+
return current;
|
|
140
|
+
if (!current.items)
|
|
141
|
+
return undefined;
|
|
142
|
+
current = current.items;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return current;
|
|
146
|
+
}
|
|
147
|
+
/** Map a JSON Schema type annotation to a CEL type string. */
|
|
148
|
+
export function jsonSchemaToCelType(schema) {
|
|
149
|
+
if (!schema || typeof schema !== "object")
|
|
150
|
+
return "dyn";
|
|
151
|
+
if (schema.anyOf || schema.oneOf || schema.allOf)
|
|
152
|
+
return "dyn";
|
|
153
|
+
if (Array.isArray(schema.type))
|
|
154
|
+
return "dyn";
|
|
155
|
+
switch (schema.type) {
|
|
156
|
+
case "integer": return "int";
|
|
157
|
+
case "number": return "double";
|
|
158
|
+
case "string": return "string";
|
|
159
|
+
case "boolean": return "bool";
|
|
160
|
+
case "array": return "list";
|
|
161
|
+
case "object": return "map";
|
|
162
|
+
case "null": return "null_type";
|
|
163
|
+
}
|
|
164
|
+
if (schema.properties)
|
|
165
|
+
return "map";
|
|
166
|
+
if (schema.items)
|
|
167
|
+
return "list";
|
|
168
|
+
return "dyn";
|
|
169
|
+
}
|
|
170
|
+
/** Check whether a CEL return type is compatible with a JSON Schema type constraint. */
|
|
171
|
+
export function celTypeSatisfiesJsonSchema(celType, schema) {
|
|
172
|
+
if (celType === "dyn")
|
|
173
|
+
return true;
|
|
174
|
+
if (!schema.type && !schema.anyOf && !schema.oneOf && !schema.allOf)
|
|
175
|
+
return true;
|
|
176
|
+
if (schema.anyOf || schema.oneOf || schema.allOf)
|
|
177
|
+
return true;
|
|
178
|
+
const schemaTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
179
|
+
const accepted = {
|
|
180
|
+
int: ["integer", "number"],
|
|
181
|
+
uint: ["integer", "number"],
|
|
182
|
+
double: ["number"],
|
|
183
|
+
string: ["string"],
|
|
184
|
+
bool: ["boolean"],
|
|
185
|
+
list: ["array"],
|
|
186
|
+
map: ["object"],
|
|
187
|
+
null_type: ["null"],
|
|
188
|
+
timestamp: ["string"],
|
|
189
|
+
duration: ["string"],
|
|
190
|
+
bytes: ["string"],
|
|
191
|
+
};
|
|
192
|
+
const compatibleWith = accepted[celType];
|
|
193
|
+
if (!compatibleWith)
|
|
194
|
+
return true; // unknown CEL type — don't flag
|
|
195
|
+
return compatibleWith.some((t) => schemaTypes.includes(t));
|
|
196
|
+
}
|
|
197
|
+
/** Return a literal placeholder value of the correct schema type for AJV. */
|
|
198
|
+
export function celPlaceholderForSchema(schema) {
|
|
199
|
+
if (schema.default !== undefined)
|
|
200
|
+
return schema.default;
|
|
201
|
+
switch (schema.type) {
|
|
202
|
+
case "integer":
|
|
203
|
+
case "number": return schema.minimum ?? 0;
|
|
204
|
+
case "string": return "";
|
|
205
|
+
case "boolean": return false;
|
|
206
|
+
case "array": return [];
|
|
207
|
+
case "object": return {};
|
|
208
|
+
default: return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
212
|
+
/** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
|
|
213
|
+
* schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
|
|
214
|
+
export function substituteCelFields(data, schema) {
|
|
215
|
+
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
216
|
+
return celPlaceholderForSchema(schema);
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(data)) {
|
|
219
|
+
const itemSchema = (schema.items ?? {});
|
|
220
|
+
return data.map((item) => substituteCelFields(item, itemSchema));
|
|
221
|
+
}
|
|
222
|
+
if (data !== null && typeof data === "object") {
|
|
223
|
+
const props = (schema.properties ?? {});
|
|
224
|
+
const addlProps = schema.additionalProperties && typeof schema.additionalProperties === "object"
|
|
225
|
+
? schema.additionalProperties
|
|
226
|
+
: undefined;
|
|
227
|
+
const result = {};
|
|
228
|
+
for (const [k, v] of Object.entries(data)) {
|
|
229
|
+
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}));
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
return data;
|
|
234
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Evaluate a JSON Path (RFC 9535) expression against a resource config and return the
|
|
2
|
+
* string values found. These are the referenced resource names at that call site.
|
|
3
|
+
* Returns [] when the path matches nothing or yields non-string values. Never throws. */
|
|
4
|
+
export declare function resolveScope(config: Record<string, any>, scope: string): string[];
|
|
5
|
+
//# sourceMappingURL=scope-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-resolver.d.ts","sourceRoot":"","sources":["../src/scope-resolver.ts"],"names":[],"mappings":"AAEA;;0FAE0F;AAC1F,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAOjF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JSONPath } from "jsonpath-plus";
|
|
2
|
+
/** Evaluate a JSON Path (RFC 9535) expression against a resource config and return the
|
|
3
|
+
* string values found. These are the referenced resource names at that call site.
|
|
4
|
+
* Returns [] when the path matches nothing or yields non-string values. Never throws. */
|
|
5
|
+
export function resolveScope(config, scope) {
|
|
6
|
+
try {
|
|
7
|
+
const results = JSONPath({ path: scope, json: config, resultType: "value" });
|
|
8
|
+
return results.filter((v) => typeof v === "string");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Matches LSP DiagnosticSeverity values exactly.
|
|
2
|
+
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity */
|
|
3
|
+
export declare const DiagnosticSeverity: {
|
|
4
|
+
readonly Error: 1;
|
|
5
|
+
readonly Warning: 2;
|
|
6
|
+
readonly Information: 3;
|
|
7
|
+
readonly Hint: 4;
|
|
8
|
+
};
|
|
9
|
+
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
10
|
+
export interface Position {
|
|
11
|
+
/** 0-based line number */
|
|
12
|
+
line: number;
|
|
13
|
+
/** 0-based character offset */
|
|
14
|
+
character: number;
|
|
15
|
+
}
|
|
16
|
+
export interface Range {
|
|
17
|
+
start: Position;
|
|
18
|
+
end: Position;
|
|
19
|
+
}
|
|
20
|
+
/** Maps a dotted field path (e.g. "config.handler", "kind") to its source Range.
|
|
21
|
+
* Built from the YAML AST before conversion to plain objects, so positions reflect
|
|
22
|
+
* the actual text locations in the source file. */
|
|
23
|
+
export type PositionIndex = Map<string, Range>;
|
|
24
|
+
/** LSP-compatible Diagnostic shape. range is optional because parsed YAML may not carry
|
|
25
|
+
* position info when only the parsed object (not raw text) is available. */
|
|
26
|
+
export interface AnalysisDiagnostic {
|
|
27
|
+
range?: Range;
|
|
28
|
+
severity?: DiagnosticSeverity;
|
|
29
|
+
code?: string | number;
|
|
30
|
+
/** e.g. "telo-analyzer" */
|
|
31
|
+
source?: string;
|
|
32
|
+
message: string;
|
|
33
|
+
/** Telo-specific extras such as { resource: { kind, name }, path } */
|
|
34
|
+
data?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface ManifestAdapter {
|
|
37
|
+
supports(url: string): boolean;
|
|
38
|
+
read(url: string): Promise<{
|
|
39
|
+
text: string;
|
|
40
|
+
source: string;
|
|
41
|
+
}>;
|
|
42
|
+
resolveRelative(base: string, relative: string): string;
|
|
43
|
+
}
|
|
44
|
+
export interface LoadOptions {
|
|
45
|
+
/** When true, each YAML document is passed through the CEL precompiler before being
|
|
46
|
+
* returned. All `${{ expr }}` template strings are replaced with `CompiledValue` wrappers
|
|
47
|
+
* so the kernel can evaluate them at runtime. Leave unset (false) for static analysis —
|
|
48
|
+
* the analyzer works on raw strings and does not need compiled values. */
|
|
49
|
+
compile?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface AnalysisOptions {
|
|
52
|
+
strictContexts?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Pre-seeded state for incremental analysis. Passed to StaticAnalyzer.analyze() so it does
|
|
55
|
+
* not rebuild from scratch on every call. The provided instances are mutated — new definitions
|
|
56
|
+
* and aliases found in the analysed manifests are registered into them. A single context can
|
|
57
|
+
* be reused across successive analyze() calls and accumulates state over time, which is the
|
|
58
|
+
* intended pattern for browser editors (persistent state across edits) and the kernel (live
|
|
59
|
+
* registry updated as resources are registered at runtime). */
|
|
60
|
+
export interface AnalysisContext {
|
|
61
|
+
aliases?: import("./alias-resolver.js").AliasResolver;
|
|
62
|
+
definitions?: import("./definition-registry.js").DefinitionRegistry;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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,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
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Matches LSP DiagnosticSeverity values exactly.
|
|
2
|
+
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity */
|
|
3
|
+
export const DiagnosticSeverity = {
|
|
4
|
+
Error: 1,
|
|
5
|
+
Warning: 2,
|
|
6
|
+
Information: 3,
|
|
7
|
+
Hint: 4,
|
|
8
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ASTNode } from "@marcbachmann/cel-js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract all member-access chains from a CEL AST.
|
|
4
|
+
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
5
|
+
* Chains that start with a call or non-identifier root are ignored.
|
|
6
|
+
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractAccessChains(node: ASTNode): string[][];
|
|
9
|
+
/**
|
|
10
|
+
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
11
|
+
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
12
|
+
* properties without `additionalProperties: true`.
|
|
13
|
+
* Returns null when the chain is valid or the schema is too open to judge.
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].handler.inputs.name")
|
|
18
|
+
* falls within the container region of a context scope (e.g. "$.routes[*].handler").
|
|
19
|
+
*
|
|
20
|
+
* The container is derived by stripping the last dot-separated segment from the scope, so that
|
|
21
|
+
* sibling fields within the same parent (e.g. routes[*].response) also match.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pathMatchesScope(exprPath: string, scope: string): boolean;
|
|
24
|
+
//# sourceMappingURL=validate-cel-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AAwED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAiBf;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAsBzE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract all member-access chains from a CEL AST.
|
|
3
|
+
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
4
|
+
* Chains that start with a call or non-identifier root are ignored.
|
|
5
|
+
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
6
|
+
*/
|
|
7
|
+
export function extractAccessChains(node) {
|
|
8
|
+
const chains = [];
|
|
9
|
+
visitNode(node, chains, new Set());
|
|
10
|
+
return chains;
|
|
11
|
+
}
|
|
12
|
+
// CEL comprehension macros that bind a variable: list.filter(x, ...), list.map(x, ...), etc.
|
|
13
|
+
const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
|
|
14
|
+
function visitNode(node, chains, boundVars) {
|
|
15
|
+
const chain = extractChain(node, boundVars);
|
|
16
|
+
if (chain !== null) {
|
|
17
|
+
chains.push(chain);
|
|
18
|
+
return; // don't recurse into parts of an already-collected chain
|
|
19
|
+
}
|
|
20
|
+
// Comprehension macros bind a variable in their body — handle them specially
|
|
21
|
+
// AST shape: { op: "rcall", args: [methodName, receiver, [boundVarId, body, ...]] }
|
|
22
|
+
if (node.op === "rcall" &&
|
|
23
|
+
Array.isArray(node.args) &&
|
|
24
|
+
typeof node.args[0] === "string" &&
|
|
25
|
+
COMPREHENSION_METHODS.has(node.args[0])) {
|
|
26
|
+
const receiver = node.args[1];
|
|
27
|
+
const comprehensionArgs = node.args[2];
|
|
28
|
+
if (isASTNode(receiver))
|
|
29
|
+
visitNode(receiver, chains, boundVars);
|
|
30
|
+
if (Array.isArray(comprehensionArgs) &&
|
|
31
|
+
comprehensionArgs.length >= 2 &&
|
|
32
|
+
isASTNode(comprehensionArgs[0]) &&
|
|
33
|
+
comprehensionArgs[0].op === "id") {
|
|
34
|
+
const newBoundVars = new Set(boundVars);
|
|
35
|
+
newBoundVars.add(comprehensionArgs[0].args);
|
|
36
|
+
for (let i = 1; i < comprehensionArgs.length; i++) {
|
|
37
|
+
const arg = comprehensionArgs[i];
|
|
38
|
+
if (isASTNode(arg))
|
|
39
|
+
visitNode(arg, chains, newBoundVars);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const args = node.args;
|
|
45
|
+
if (Array.isArray(args)) {
|
|
46
|
+
for (const arg of args) {
|
|
47
|
+
if (isASTNode(arg)) {
|
|
48
|
+
visitNode(arg, chains, boundVars);
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(arg)) {
|
|
51
|
+
for (const item of arg) {
|
|
52
|
+
if (isASTNode(item))
|
|
53
|
+
visitNode(item, chains, boundVars);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isASTNode(v) {
|
|
60
|
+
return v !== null && typeof v === "object" && "op" in v;
|
|
61
|
+
}
|
|
62
|
+
/** Returns the member-access chain for a node if it is purely an id or "." chain; else null. */
|
|
63
|
+
function extractChain(node, boundVars) {
|
|
64
|
+
if (node.op === "id") {
|
|
65
|
+
const name = node.args;
|
|
66
|
+
if (boundVars.has(name))
|
|
67
|
+
return null; // bound by a comprehension macro, not a free access
|
|
68
|
+
return [name];
|
|
69
|
+
}
|
|
70
|
+
if (node.op === ".") {
|
|
71
|
+
const [obj, field] = node.args;
|
|
72
|
+
const parent = extractChain(obj, boundVars);
|
|
73
|
+
if (parent !== null)
|
|
74
|
+
return [...parent, field];
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
80
|
+
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
81
|
+
* properties without `additionalProperties: true`.
|
|
82
|
+
* Returns null when the chain is valid or the schema is too open to judge.
|
|
83
|
+
*/
|
|
84
|
+
export function validateChainAgainstSchema(chain, schema) {
|
|
85
|
+
let current = schema;
|
|
86
|
+
for (let i = 0; i < chain.length; i++) {
|
|
87
|
+
const key = chain[i];
|
|
88
|
+
if (!current || typeof current !== "object")
|
|
89
|
+
return null;
|
|
90
|
+
// Open schema: no properties declared or explicitly allows additional properties
|
|
91
|
+
const props = current.properties;
|
|
92
|
+
if (!props)
|
|
93
|
+
return null;
|
|
94
|
+
if (current.additionalProperties === true)
|
|
95
|
+
return null;
|
|
96
|
+
if (!(key in props)) {
|
|
97
|
+
const path = chain.slice(0, i + 1).join(".");
|
|
98
|
+
const available = Object.keys(props).join(", ");
|
|
99
|
+
return `'${path}' is not defined (available: ${available})`;
|
|
100
|
+
}
|
|
101
|
+
current = props[key];
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].handler.inputs.name")
|
|
107
|
+
* falls within the container region of a context scope (e.g. "$.routes[*].handler").
|
|
108
|
+
*
|
|
109
|
+
* The container is derived by stripping the last dot-separated segment from the scope, so that
|
|
110
|
+
* sibling fields within the same parent (e.g. routes[*].response) also match.
|
|
111
|
+
*/
|
|
112
|
+
export function pathMatchesScope(exprPath, scope) {
|
|
113
|
+
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
114
|
+
const lastDot = stripped.lastIndexOf(".");
|
|
115
|
+
if (lastDot <= 0)
|
|
116
|
+
return false;
|
|
117
|
+
const container = stripped.slice(0, lastDot); // e.g. "routes[*]"
|
|
118
|
+
// Split on wildcard array segments; each [*] must match a concrete [N] in exprPath
|
|
119
|
+
const parts = container.split("[*]");
|
|
120
|
+
let remaining = exprPath;
|
|
121
|
+
for (let i = 0; i < parts.length; i++) {
|
|
122
|
+
const part = parts[i];
|
|
123
|
+
if (!remaining.startsWith(part))
|
|
124
|
+
return false;
|
|
125
|
+
remaining = remaining.slice(part.length);
|
|
126
|
+
if (i < parts.length - 1) {
|
|
127
|
+
// Expect a concrete array index like [0], [12], ...
|
|
128
|
+
const m = remaining.match(/^\[\d+\]/);
|
|
129
|
+
if (!m)
|
|
130
|
+
return false;
|
|
131
|
+
remaining = remaining.slice(m[0].length);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Expression must end here or continue into a child path
|
|
135
|
+
return remaining === "" || remaining[0] === "." || remaining[0] === "[";
|
|
136
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { type AnalysisDiagnostic, type AnalysisContext } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Phase 3 — Reference validation.
|
|
5
|
+
*
|
|
6
|
+
* For each x-telo-ref slot in every non-system resource, validates:
|
|
7
|
+
* 1. Structural — the value has string `kind` and `name` fields.
|
|
8
|
+
* 2. Kind — the alias-resolved kind satisfies the x-telo-ref constraint
|
|
9
|
+
* (abstract: must extend the target; concrete: must equal it exactly).
|
|
10
|
+
* 3. Resolution — a resource with that name exists in the visible manifest set
|
|
11
|
+
* (outer manifests + scope manifests for in-scope ref paths).
|
|
12
|
+
*
|
|
13
|
+
* Ref values with keys beyond kind/name/metadata are treated as inline resources
|
|
14
|
+
* pending Phase 2 normalization and are skipped without error.
|
|
15
|
+
*
|
|
16
|
+
* Returns an empty array when `context.aliases` or `context.definitions` is absent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateReferences(resources: ResourceManifest[], context: AnalysisContext): AnalysisDiagnostic[];
|
|
19
|
+
//# sourceMappingURL=validate-references.d.ts.map
|
|
@@ -0,0 +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"}
|