@telorun/analyzer 0.1.1 → 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.
Files changed (52) hide show
  1. package/README.md +233 -0
  2. package/dist/adapters/http-adapter.d.ts +1 -1
  3. package/dist/adapters/http-adapter.d.ts.map +1 -1
  4. package/dist/adapters/http-adapter.js +4 -3
  5. package/dist/adapters/node-adapter.d.ts +1 -1
  6. package/dist/adapters/node-adapter.d.ts.map +1 -1
  7. package/dist/adapters/node-adapter.js +2 -1
  8. package/dist/adapters/registry-adapter.d.ts +5 -1
  9. package/dist/adapters/registry-adapter.d.ts.map +1 -1
  10. package/dist/adapters/registry-adapter.js +27 -7
  11. package/dist/analysis-registry.d.ts +2 -0
  12. package/dist/analysis-registry.d.ts.map +1 -1
  13. package/dist/analysis-registry.js +8 -0
  14. package/dist/analyzer.d.ts.map +1 -1
  15. package/dist/analyzer.js +107 -4
  16. package/dist/cel-environment.d.ts +5 -2
  17. package/dist/cel-environment.d.ts.map +1 -1
  18. package/dist/cel-environment.js +5 -3
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/kernel-globals.d.ts +34 -0
  23. package/dist/kernel-globals.d.ts.map +1 -0
  24. package/dist/kernel-globals.js +94 -0
  25. package/dist/manifest-loader.d.ts +6 -3
  26. package/dist/manifest-loader.d.ts.map +1 -1
  27. package/dist/manifest-loader.js +110 -5
  28. package/dist/schema-compat.d.ts +1 -1
  29. package/dist/schema-compat.d.ts.map +1 -1
  30. package/dist/schema-compat.js +82 -28
  31. package/dist/types.d.ts +12 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js +2 -0
  34. package/dist/validate-cel-context.d.ts +26 -4
  35. package/dist/validate-cel-context.d.ts.map +1 -1
  36. package/dist/validate-cel-context.js +123 -15
  37. package/dist/validate-references.d.ts.map +1 -1
  38. package/dist/validate-references.js +13 -1
  39. package/package.json +21 -2
  40. package/src/adapters/http-adapter.ts +4 -4
  41. package/src/adapters/node-adapter.ts +2 -2
  42. package/src/adapters/registry-adapter.ts +30 -8
  43. package/src/analysis-registry.ts +10 -0
  44. package/src/analyzer.ts +139 -5
  45. package/src/cel-environment.ts +5 -3
  46. package/src/index.ts +2 -4
  47. package/src/kernel-globals.ts +110 -0
  48. package/src/manifest-loader.ts +131 -7
  49. package/src/schema-compat.ts +87 -31
  50. package/src/types.ts +14 -0
  51. package/src/validate-cel-context.ts +150 -15
  52. package/src/validate-references.ts +13 -1
@@ -1,5 +1,45 @@
1
1
  import type { ASTNode } from "@marcbachmann/cel-js";
2
2
 
3
+ /**
4
+ * Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
5
+ * - String: look up the named type in allManifests (Type.JsonSchema resources)
6
+ * - Object with `kind` + `schema`: inline type definition → return the `schema`
7
+ * - Object with `type` or `properties`: raw JSON Schema, return as-is
8
+ */
9
+ export function resolveTypeFieldToSchema(
10
+ value: unknown,
11
+ allManifests: Record<string, any>[],
12
+ ): Record<string, any> | undefined {
13
+ if (!value) return undefined;
14
+
15
+ if (typeof value === "string") {
16
+ // Named type reference — find a Kernel.Type resource by name
17
+ const typeManifest = allManifests.find(
18
+ (m) =>
19
+ (m.metadata as any)?.name === value &&
20
+ typeof m.kind === "string" &&
21
+ /\bType\b/.test(m.kind) &&
22
+ typeof m.schema === "object" &&
23
+ m.schema !== null,
24
+ );
25
+ return typeManifest?.schema as Record<string, any> | undefined;
26
+ }
27
+
28
+ if (typeof value === "object" && value !== null) {
29
+ const obj = value as Record<string, any>;
30
+ // Inline type resource: { kind: "Type.JsonSchema", schema: {...} }
31
+ if (obj.schema && typeof obj.schema === "object") {
32
+ return obj.schema as Record<string, any>;
33
+ }
34
+ // Raw JSON Schema (has type or properties)
35
+ if (obj.type || obj.properties) {
36
+ return obj;
37
+ }
38
+ }
39
+
40
+ return undefined;
41
+ }
42
+
3
43
  /**
4
44
  * Extract all member-access chains from a CEL AST.
5
45
  * Returns arrays like ["request", "query", "name"] for `request.query.name`.
@@ -96,35 +136,35 @@ export function validateChainAgainstSchema(
96
136
  for (let i = 0; i < chain.length; i++) {
97
137
  const key = chain[i]!;
98
138
  if (!current || typeof current !== "object") return null;
99
- // Open schema: no properties declared or explicitly allows additional properties
100
139
  const props: Record<string, any> | undefined = current.properties;
101
140
  if (!props) return null;
102
- if (current.additionalProperties === true) return null;
103
- if (!(key in props)) {
104
- const path = chain.slice(0, i + 1).join(".");
105
- const available = Object.keys(props).join(", ");
106
- return `'${path}' is not defined (available: ${available})`;
141
+ if (key in props) {
142
+ // Known property — drill into it even if additionalProperties is true
143
+ current = props[key];
144
+ continue;
107
145
  }
108
- current = props[key];
146
+ // Unknown property — only flag if schema is closed
147
+ if (current.additionalProperties === true) return null;
148
+ const path = chain.slice(0, i + 1).join(".");
149
+ const available = Object.keys(props).join(", ");
150
+ return `'${path}' is not defined (available: ${available})`;
109
151
  }
110
152
  return null;
111
153
  }
112
154
 
113
155
  /**
114
- * Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].handler.inputs.name")
115
- * falls within the container region of a context scope (e.g. "$.routes[*].handler").
156
+ * Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
157
+ * falls within the scope of a context (e.g. "$.routes[*].inputs").
116
158
  *
117
- * The container is derived by stripping the last dot-separated segment from the scope, so that
118
- * sibling fields within the same parent (e.g. routes[*].response) also match.
159
+ * The scope is matched directly (no sibling sharing): a context at "$.routes[*].inputs" only
160
+ * applies to expressions whose path starts with "routes[N].inputs", not to other sibling fields.
119
161
  */
120
162
  export function pathMatchesScope(exprPath: string, scope: string): boolean {
121
163
  const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
122
- const lastDot = stripped.lastIndexOf(".");
123
- if (lastDot <= 0) return false;
124
- const container = stripped.slice(0, lastDot); // e.g. "routes[*]"
164
+ if (!stripped) return false;
125
165
 
126
166
  // Split on wildcard array segments; each [*] must match a concrete [N] in exprPath
127
- const parts = container.split("[*]");
167
+ const parts = stripped.split("[*]");
128
168
  let remaining = exprPath;
129
169
  for (let i = 0; i < parts.length; i++) {
130
170
  const part = parts[i]!;
@@ -140,3 +180,98 @@ export function pathMatchesScope(exprPath: string, scope: string): boolean {
140
180
  // Expression must end here or continue into a child path
141
181
  return remaining === "" || remaining[0] === "." || remaining[0] === "[";
142
182
  }
183
+
184
+ /**
185
+ * Resolves `x-telo-context-from` annotations in a context schema using the concrete
186
+ * manifest item. Navigates the manifest item at the given slash-separated path and merges
187
+ * the result as named properties into the annotated node (locking additionalProperties: false).
188
+ *
189
+ * Example: `x-telo-context-from: "request/schema"` on the `request` context node replaces
190
+ * the open `request` schema with a closed schema whose properties are the keys of
191
+ * `manifestItem.request.schema` (e.g. `query`, `body`, `params`, `headers`).
192
+ */
193
+ export function resolveContextAnnotations(
194
+ schema: Record<string, any>,
195
+ manifestItem: Record<string, any>,
196
+ allManifests?: Record<string, any>[],
197
+ ): Record<string, any> {
198
+ if (!schema || typeof schema !== "object") return schema;
199
+
200
+ const from = schema["x-telo-context-from"] as string | undefined;
201
+ if (from) {
202
+ const resolved = navigatePath(manifestItem, from.split("/")) as Record<string, any> | undefined;
203
+ // `resolved` is a map of property names → sub-schemas (e.g. { query: {...}, body: {...} })
204
+ return {
205
+ ...schema,
206
+ properties: { ...(schema.properties ?? {}), ...(resolved ?? {}) },
207
+ additionalProperties: false,
208
+ };
209
+ }
210
+
211
+ const refFrom = schema["x-telo-context-ref-from"] as string | undefined;
212
+ if (refFrom && allManifests) {
213
+ const slashIdx = refFrom.indexOf("/");
214
+ const refProp = slashIdx === -1 ? refFrom : refFrom.slice(0, slashIdx);
215
+ const subpath = slashIdx === -1 ? undefined : refFrom.slice(slashIdx + 1);
216
+ const ref = manifestItem[refProp] as Record<string, any> | undefined;
217
+ if (
218
+ ref &&
219
+ typeof ref === "object" &&
220
+ typeof ref.kind === "string" &&
221
+ typeof ref.name === "string" &&
222
+ subpath
223
+ ) {
224
+ const refManifest = allManifests.find(
225
+ (m) => m.kind === ref.kind && (m.metadata as any)?.name === ref.name,
226
+ ) as Record<string, any> | undefined;
227
+ if (refManifest) {
228
+ const resolved = resolveTypeFieldToSchema(
229
+ navigatePath(refManifest, subpath.split("/")) as unknown,
230
+ allManifests,
231
+ );
232
+ if (resolved && typeof resolved === "object") {
233
+ return resolved;
234
+ }
235
+ }
236
+ }
237
+ // Fallback: open schema (no false errors when outputType is not declared)
238
+ return { ...schema, additionalProperties: true };
239
+ }
240
+
241
+ if (schema.properties) {
242
+ const props: Record<string, any> = {};
243
+ for (const [k, v] of Object.entries(schema.properties)) {
244
+ props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem, allManifests);
245
+ }
246
+ return { ...schema, properties: props };
247
+ }
248
+
249
+ return schema;
250
+ }
251
+
252
+ /**
253
+ * Extracts the concrete manifest array item for a given expression path + scope.
254
+ * e.g. exprPath="routes[0].inputs.q", scope="$.routes[*].inputs" → manifest.routes[0]
255
+ */
256
+ export function getManifestItem(
257
+ exprPath: string,
258
+ scope: string,
259
+ manifest: Record<string, any>,
260
+ ): Record<string, any> {
261
+ const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
262
+ const wildcardIdx = stripped.indexOf("[*]");
263
+ if (wildcardIdx === -1) return manifest;
264
+ const arrayProp = stripped.slice(0, wildcardIdx); // e.g. "routes"
265
+ const m = exprPath.match(new RegExp(`^${arrayProp}\\[(\\d+)\\]`));
266
+ if (!m) return manifest;
267
+ return (manifest as any)[arrayProp]?.[Number(m[1])] ?? manifest;
268
+ }
269
+
270
+ function navigatePath(obj: unknown, segments: string[]): unknown {
271
+ let cur = obj;
272
+ for (const seg of segments) {
273
+ if (cur === null || typeof cur !== "object") return undefined;
274
+ cur = (cur as Record<string, unknown>)[seg];
275
+ }
276
+ return cur;
277
+ }
@@ -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(val) ?? visibleScopeManifests.find((m) => m.metadata?.name === val);
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",