@telorun/analyzer 0.63.0 → 0.65.0

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 (64) hide show
  1. package/dist/analysis-registry.d.ts +24 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +35 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +76 -470
  7. package/dist/cel-scope-query.d.ts +109 -0
  8. package/dist/cel-scope-query.d.ts.map +1 -0
  9. package/dist/cel-scope-query.js +270 -0
  10. package/dist/cel-scope.d.ts +166 -0
  11. package/dist/cel-scope.d.ts.map +1 -0
  12. package/dist/cel-scope.js +377 -0
  13. package/dist/definition-registry.d.ts +38 -6
  14. package/dist/definition-registry.d.ts.map +1 -1
  15. package/dist/definition-registry.js +66 -22
  16. package/dist/find-manifest.d.ts +10 -0
  17. package/dist/find-manifest.d.ts.map +1 -0
  18. package/dist/find-manifest.js +12 -0
  19. package/dist/index.d.ts +11 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -1
  22. package/dist/invocation-contract.d.ts +11 -0
  23. package/dist/invocation-contract.d.ts.map +1 -1
  24. package/dist/invocation-contract.js +15 -0
  25. package/dist/manifest-analysis.d.ts +73 -0
  26. package/dist/manifest-analysis.d.ts.map +1 -0
  27. package/dist/manifest-analysis.js +78 -0
  28. package/dist/manifest-path.d.ts +18 -0
  29. package/dist/manifest-path.d.ts.map +1 -0
  30. package/dist/manifest-path.js +37 -0
  31. package/dist/schema-compat.d.ts +59 -22
  32. package/dist/schema-compat.d.ts.map +1 -1
  33. package/dist/schema-compat.js +60 -75
  34. package/dist/schema-error-report.d.ts +68 -0
  35. package/dist/schema-error-report.d.ts.map +1 -0
  36. package/dist/schema-error-report.js +356 -0
  37. package/dist/schema-walk.d.ts +25 -0
  38. package/dist/schema-walk.d.ts.map +1 -0
  39. package/dist/schema-walk.js +126 -0
  40. package/dist/telo-version.d.ts +1 -1
  41. package/dist/telo-version.js +1 -1
  42. package/dist/validate-nested-inline.d.ts +22 -1
  43. package/dist/validate-nested-inline.d.ts.map +1 -1
  44. package/dist/validate-nested-inline.js +17 -9
  45. package/dist/validate-step-inputs.d.ts +17 -0
  46. package/dist/validate-step-inputs.d.ts.map +1 -1
  47. package/dist/validate-step-inputs.js +108 -9
  48. package/package.json +2 -2
  49. package/src/analysis-registry.ts +37 -0
  50. package/src/analyzer.ts +83 -587
  51. package/src/cel-scope-query.ts +337 -0
  52. package/src/cel-scope.ts +570 -0
  53. package/src/definition-registry.ts +79 -24
  54. package/src/find-manifest.ts +19 -0
  55. package/src/index.ts +23 -2
  56. package/src/invocation-contract.ts +22 -0
  57. package/src/manifest-analysis.ts +132 -0
  58. package/src/manifest-path.ts +34 -0
  59. package/src/schema-compat.ts +92 -79
  60. package/src/schema-error-report.ts +417 -0
  61. package/src/schema-walk.ts +144 -0
  62. package/src/telo-version.ts +1 -1
  63. package/src/validate-nested-inline.ts +35 -14
  64. package/src/validate-step-inputs.ts +153 -11
@@ -0,0 +1,377 @@
1
+ import { bindingContextProperties, bindingPathChain, BINDINGS_ANNOTATION, schemaAtChain, } from "./cel-bindings.js";
2
+ import { buildImportInputCelEnvironment, buildTypedCelEnvironment } from "./cel-environment.js";
3
+ import { effectiveAuthorSchema } from "./extends-resolution.js";
4
+ import { analyzerContractScope, PERMISSIVE_CONTRACT, resolveContract, } from "./invocation-contract.js";
5
+ import { mergeKernelGlobalsIntoContext, } from "./kernel-globals.js";
6
+ import { resolveLocalRef, walkStepArray } from "./schema-walk.js";
7
+ import { readStepSlot } from "./step-slot.js";
8
+ import { getManifestItem, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
9
+ /** Build a closed JSON Schema for the `self` CEL variable available inside a
10
+ * `Telo.Definition` template body. Mirrors the runtime template controller's
11
+ * `const self = { ...resource, name: resource.metadata.name };` — every
12
+ * property the user declared in `schema:` plus synthetic `name` / `kind` and
13
+ * the metadata sub-object (kept open since metadata legitimately carries
14
+ * arbitrary user-added fields). */
15
+ function buildSelfSchema(definition, defs, aliases) {
16
+ // The author-facing schema resolves inheritance: with `base:` the child's own
17
+ // schema (the parent's config is internal); without it, `merge(parent, own)`.
18
+ const userSchema = (defs
19
+ ? effectiveAuthorSchema(definition, (k) => defs.resolve(aliases?.resolveKind(k) ?? k) ?? defs.resolve(k))
20
+ : (definition.schema ?? {}));
21
+ const userProps = (userSchema.properties ?? {});
22
+ const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
23
+ return {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ properties: {
27
+ ...userProps,
28
+ name: { type: "string" },
29
+ kind: { type: "string" },
30
+ metadata: {
31
+ type: "object",
32
+ additionalProperties: true,
33
+ properties: { name: { type: "string" } },
34
+ },
35
+ },
36
+ required: [...userRequired, "name", "kind"],
37
+ };
38
+ }
39
+ /** Build the JSON Schema for the `inputs` CEL variable available inside an
40
+ * invocable template body — the shared contract resolver applied to the
41
+ * definition itself, so a body is typed against the exact signature callers are
42
+ * checked against and dispatch enforces. Walks the whole `extends` chain rather
43
+ * than one hop, so a definition two levels below the declaration still gets
44
+ * typed inputs. Undefined when nothing in the chain declares a contract —
45
+ * the caller signals opaque `map<string, dyn>` upstream. */
46
+ function lookupTemplateInputsSchema(definition, defs, aliases, allManifests, scopes) {
47
+ return resolveContract("inputType", undefined, definition, analyzerContractScope(defs, aliases, scopes, allManifests))?.schema;
48
+ }
49
+ /** Returns a "resolver-facing" view of the manifest where the fields used as
50
+ * navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
51
+ * have been pre-augmented:
52
+ * - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
53
+ * - `inputType` → resolved through the shared contract resolver, so
54
+ * `x-telo-context-from-root: inputType` substitutes the
55
+ * real signature. Without it the annotation would replace
56
+ * the node verbatim with the inline `{kind, schema}` wrapper
57
+ * the standard library writes everywhere, typing `inputs` as
58
+ * `{kind, schema}` instead of the declared properties.
59
+ *
60
+ * For non-definition manifests the original object is returned. */
61
+ export function manifestRootForResolver(m, defs, aliases, allManifests, scopes) {
62
+ if (m.kind !== "Telo.Definition")
63
+ return m;
64
+ const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
65
+ return {
66
+ ...m,
67
+ schema: buildSelfSchema(m, defs, aliases),
68
+ ...(inputs ? { inputType: inputs } : {}),
69
+ };
70
+ }
71
+ /**
72
+ * Build a `steps` context schema for a kind's step body.
73
+ * Walks each step in the manifest array, resolves the invoked resource's output
74
+ * contract, and builds `steps.<name>.result` context entries.
75
+ *
76
+ * Resolution is the shared {@link resolveContract} — the invoked resource
77
+ * manifest's own declaration, then the kind's, resolved to the nearest
78
+ * declaration along `extends`, then permissive. Sharing it with the kernel is
79
+ * what stops `telo check` from typing `steps.X.result` against one contract
80
+ * while dispatch validates against another.
81
+ *
82
+ * The kind layer is what makes `x-telo-stream` properties on definitions
83
+ * actually govern step-result chain validation — without it, the validator falls
84
+ * back to permissive and the stream-opacity rule never fires.
85
+ *
86
+ * Recursion into nested step arrays is annotation-driven via
87
+ * `x-telo-topology-role`. The analyzer recognises three role values:
88
+ * - `branch` — value is an array of steps (e.g. then / else / do / catch).
89
+ * - `branch-list`— value is an array of objects each carrying further roled
90
+ * sub-properties (e.g. elseif: [{ if, then }]).
91
+ * - `case-map` — value is an object whose values are step arrays (e.g. cases).
92
+ * No specific Run.Sequence field name is hardcoded; any kind that uses
93
+ * a step body and tags its branch fields with these roles works.
94
+ */
95
+ export function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases, scopes) {
96
+ const props = defSchema.properties;
97
+ if (!props)
98
+ return undefined;
99
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
100
+ const readingModule = manifest.metadata?.module;
101
+ for (const [fieldName, fieldSchema] of Object.entries(props)) {
102
+ const stepCtx = readStepSlot(fieldSchema);
103
+ if (!stepCtx)
104
+ continue;
105
+ const invokeField = stepCtx.invoke;
106
+ const outputTypeField = stepCtx.outputType;
107
+ // Optional: the field a step uses to produce a result without dispatching.
108
+ // Only a kind that declares one has pure steps at all.
109
+ const valueField = stepCtx.value;
110
+ if (!invokeField || !outputTypeField)
111
+ continue;
112
+ const steps = manifest[fieldName];
113
+ if (!Array.isArray(steps))
114
+ continue;
115
+ const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
116
+ // The instance's own input contract, for typing a pure step that just
117
+ // forwards one of its values.
118
+ const ownInputs = resolveTypeFieldToSchema(manifest.inputType, allManifests);
119
+ const stepProperties = {};
120
+ walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
121
+ const name = s.name;
122
+ const invoke = s[invokeField];
123
+ // Only invoke steps register a `steps.<name>.result` entry — control-flow
124
+ // wrappers (try/if/while/switch/throw) don't produce a result and must
125
+ // not shadow real entries with a permissive `additionalProperties: true`,
126
+ // or unknown step references slip through chain validation.
127
+ if (typeof name !== "string")
128
+ return;
129
+ if (!invoke || typeof invoke !== "object") {
130
+ // A pure step dispatches nothing, so there is no contract to resolve.
131
+ // Where its expression is a plain chain into something already typed —
132
+ // an earlier step's result, or the kind's own inputs — that type carries
133
+ // through; anything else (arithmetic, a call, a comprehension) stays
134
+ // permissive rather than guessed. Same rule as a named binding's.
135
+ if (valueField && valueField in s) {
136
+ const scopeRoot = {
137
+ properties: {
138
+ steps: { type: "object", properties: { ...stepProperties } },
139
+ ...(ownInputs ? { inputs: ownInputs } : {}),
140
+ },
141
+ };
142
+ const chained = schemaAtChain(bindingPathChain(s[valueField]), scopeRoot);
143
+ stepProperties[name] = {
144
+ type: "object",
145
+ properties: { result: chained ?? PERMISSIVE_CONTRACT },
146
+ };
147
+ }
148
+ return;
149
+ }
150
+ const invokedKind = invoke.kind;
151
+ const invokedName = invoke.name;
152
+ // A named `!ref` carries the target's own manifest (which may narrow the
153
+ // contract for this one instance); an inline `{ kind, ... }` step IS the
154
+ // manifest. Either way the kind layer resolves through `extends`.
155
+ const invokedManifest = invokedName
156
+ ? allManifests.find((m) => m.metadata?.name === invokedName && (!invokedKind || m.kind === invokedKind))
157
+ : invoke;
158
+ const invokedDef = invokedKind
159
+ ? contractScope.resolveIn(invokedKind, readingModule)
160
+ : undefined;
161
+ const outputSchema = resolveContract(outputTypeField, invokedManifest, invokedDef, contractScope)?.schema;
162
+ stepProperties[name] = {
163
+ type: "object",
164
+ properties: {
165
+ result: outputSchema ?? PERMISSIVE_CONTRACT,
166
+ },
167
+ };
168
+ });
169
+ if (Object.keys(stepProperties).length > 0) {
170
+ return {
171
+ type: "object",
172
+ properties: stepProperties,
173
+ };
174
+ }
175
+ }
176
+ return undefined;
177
+ }
178
+ export function collectErrorContextScopes(defSchema) {
179
+ const out = new Map();
180
+ if (!defSchema || typeof defSchema !== "object")
181
+ return out;
182
+ const seen = new Set();
183
+ const walk = (schema) => {
184
+ if (!schema || typeof schema !== "object" || seen.has(schema))
185
+ return;
186
+ seen.add(schema);
187
+ const props = schema.properties;
188
+ if (props) {
189
+ for (const [fieldName, fieldSchema] of Object.entries(props)) {
190
+ if (fieldSchema && typeof fieldSchema === "object") {
191
+ const errCtx = fieldSchema["x-telo-error-context"];
192
+ if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
193
+ out.set(fieldName, errCtx);
194
+ }
195
+ }
196
+ walk(resolveLocalRef(fieldSchema, defSchema));
197
+ }
198
+ }
199
+ if (schema.items)
200
+ walk(resolveLocalRef(schema.items, defSchema));
201
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
202
+ const arr = schema[key];
203
+ if (Array.isArray(arr))
204
+ for (const sub of arr)
205
+ walk(resolveLocalRef(sub, defSchema));
206
+ }
207
+ if (schema.$defs && typeof schema.$defs === "object") {
208
+ for (const sub of Object.values(schema.$defs)) {
209
+ walk(sub);
210
+ }
211
+ }
212
+ };
213
+ walk(defSchema);
214
+ return out;
215
+ }
216
+ /**
217
+ * Return the error-context schema for a CEL `path` when the path lies within
218
+ * (any depth under) one of the error-bearing fields, else undefined. A path is
219
+ * "within" field `f` when it contains a segment `f[<index>]`. When multiple
220
+ * error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
221
+ * deepest — the one whose segment appears latest in the path — wins, so the
222
+ * innermost branch's schema governs.
223
+ */
224
+ export function errorContextForPath(path, scopes) {
225
+ let best;
226
+ for (const [fieldName, schema] of scopes) {
227
+ const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
228
+ for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
229
+ if (best === undefined || match.index > best.index) {
230
+ best = { index: match.index, schema };
231
+ }
232
+ }
233
+ }
234
+ return best?.schema;
235
+ }
236
+ /** Add a kind's named bindings to a resolved context, when the context declares
237
+ * a bindings region. They go UNDER the context's own properties: a scope
238
+ * variable wins over a same-named binding at runtime, so static typing has to
239
+ * agree (the collision itself is `BINDING_NAME_RESERVED`). */
240
+ export function withBindingNames(contextSchema, resource) {
241
+ const field = contextSchema[BINDINGS_ANNOTATION];
242
+ if (typeof field !== "string")
243
+ return contextSchema;
244
+ const bindings = resource[field];
245
+ if (bindings === null || typeof bindings !== "object" || Array.isArray(bindings)) {
246
+ return contextSchema;
247
+ }
248
+ return {
249
+ ...contextSchema,
250
+ properties: {
251
+ ...bindingContextProperties(bindings, contextSchema),
252
+ ...(contextSchema.properties ?? {}),
253
+ },
254
+ };
255
+ }
256
+ /**
257
+ * The CEL scope rule, applied per resource and then per expression.
258
+ *
259
+ * Stateful across a resource because the per-resource half — the step context,
260
+ * the error-bearing regions, the invocation context — is derived from the kind's
261
+ * schema once and read by every expression in that resource. `enterResource`
262
+ * establishes it; `scopeFor` answers for one path.
263
+ */
264
+ export class CelScopeResolver {
265
+ inputs;
266
+ typedEnvByManifest = new Map();
267
+ /** Per-resource state, replaced at each `enterResource`. */
268
+ stepContext;
269
+ invocationContext;
270
+ errorScopes = new Map();
271
+ constructor(inputs) {
272
+ this.inputs = inputs;
273
+ }
274
+ /** The `steps` context schema for the current resource, or undefined when its
275
+ * kind declares no step body. Exposed because the step-inputs check needs the
276
+ * same schema this resource's expressions are typed against — recomputing it
277
+ * there is how the two would come to disagree. */
278
+ get stepContextSchema() {
279
+ return this.stepContext;
280
+ }
281
+ /** The error-bearing regions the current resource's kind declares. */
282
+ get errorContextScopes() {
283
+ return this.errorScopes;
284
+ }
285
+ /** The resource-wide invocation context, when this resource is an inline
286
+ * declaration that carries one. Read by the "CEL in a non-eval field" check:
287
+ * such a resource's CEL is evaluated by the enclosing kind, not by an
288
+ * `x-telo-eval` annotation of its own. */
289
+ get invocationContextSchema() {
290
+ return this.invocationContext;
291
+ }
292
+ /** Establish the per-resource half of the scope. */
293
+ enterResource(m, definition) {
294
+ const { allManifests, defs, aliases, scopes } = this.inputs;
295
+ this.invocationContext = m.metadata?.xTeloInvocationContext;
296
+ this.stepContext = definition?.schema
297
+ ? buildStepContextSchema(m, definition.schema, allManifests, defs, aliases, scopes)
298
+ : undefined;
299
+ this.errorScopes = collectErrorContextScopes(definition?.schema);
300
+ }
301
+ /**
302
+ * What the expression at `site` is typed against.
303
+ *
304
+ * The environment is cached per manifest when no context applied, which is
305
+ * most expressions: it then depends only on the manifest, so rebuilding it per
306
+ * expression is pure waste — a clone plus a re-registration of every variable,
307
+ * on every keystroke in the IDE. A matched context makes it path-specific (its
308
+ * schema is resolved against the enclosing array item), so those build fresh
309
+ * rather than risk one item's types leaking into another's.
310
+ */
311
+ scopeFor(site) {
312
+ const m = site.source;
313
+ const contextSchema = this.resolveContextFor(site);
314
+ const { celEnv, allManifests, moduleManifest, } = this.inputs;
315
+ const cached = contextSchema === null ? this.typedEnvByManifest.get(m) : undefined;
316
+ const env = cached ??
317
+ // A `Telo.Import`'s variables/secrets are a config-only contract evaluated
318
+ // in the IMPORTING module's scope, so they type from the owning module doc
319
+ // and drop `resources`/`env`, making a reference to either an error.
320
+ (m.kind === "Telo.Import"
321
+ ? buildImportInputCelEnvironment(celEnv, allManifests.find((mm) => (mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
322
+ mm.metadata?.name ===
323
+ m.metadata?.module))
324
+ : buildTypedCelEnvironment(celEnv, m, contextSchema ?? undefined, moduleManifest));
325
+ if (contextSchema === null && !cached)
326
+ this.typedEnvByManifest.set(m, env);
327
+ return { env, contextSchema };
328
+ }
329
+ /** The context schema in force at `site`: the matched `x-telo-context` (or the
330
+ * resource-wide invocation context), plus the step and error regions this
331
+ * path falls in, resolved and merged with the kernel globals. */
332
+ resolveContextFor(site) {
333
+ const { path } = site;
334
+ const m = site.source;
335
+ let matched = site.contextSchema ?? this.invocationContext;
336
+ if (this.stepContext) {
337
+ const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
338
+ matched = {
339
+ ...base,
340
+ properties: { ...(base.properties ?? {}), steps: this.stepContext },
341
+ };
342
+ }
343
+ // `error` is only in scope inside an error-bearing branch (e.g. a
344
+ // `catch:` / `finally:`), so it's merged per-path, not resource-wide.
345
+ const errorSchema = this.errorScopes.size > 0 ? errorContextForPath(path, this.errorScopes) : undefined;
346
+ if (errorSchema) {
347
+ const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
348
+ matched = {
349
+ ...base,
350
+ properties: { ...(base.properties ?? {}), error: errorSchema },
351
+ };
352
+ }
353
+ if (!matched) {
354
+ // No `x-telo-context` matched, so nothing was chain-validated here before.
355
+ // Validate the observed-state segment alone rather than merging the kernel
356
+ // globals, whose closed `variables` / `ports` nodes would newly reject
357
+ // reads that pass today.
358
+ return this.inputs.observedStateContext;
359
+ }
360
+ const { defs, aliases, scopes, allManifests, kernelGlobals } = this.inputs;
361
+ const manifestItem = site.matchedScope
362
+ ? getManifestItem(path, site.matchedScope, m)
363
+ : m;
364
+ const rootForResolver = manifestRootForResolver(m, defs, aliases, allManifests, scopes);
365
+ const resolved = resolveContextAnnotations(matched, manifestItem, {
366
+ manifestRoot: rootForResolver,
367
+ defs,
368
+ aliases,
369
+ allManifests: allManifests,
370
+ });
371
+ return mergeKernelGlobalsIntoContext(withBindingNames(resolved, m),
372
+ // Typed in the module that DECLARED this resource — for a manifest
373
+ // forwarded from an imported library, that is its `moduleGlobals` stamp,
374
+ // not the consuming application's block.
375
+ kernelGlobals.forResource(m));
376
+ }
377
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
2
  import type { AliasResolver } from "./alias-resolver.js";
3
3
  import { type ReferenceFieldMap } from "./reference-field-map.js";
4
- /** Pure kind ResourceDefinition map. No controller loading, no lifecycle. */
4
+ import { type SchemaIssue } from "./schema-error-report.js";
5
5
  export declare class DefinitionRegistry {
6
6
  constructor();
7
7
  /** Per-instance AJV for cross-module $ref resolution. Isolated so each registry
@@ -9,6 +9,7 @@ export declare class DefinitionRegistry {
9
9
  * across analyze() calls and no unbounded growth across the process lifetime. */
10
10
  private readonly ajv;
11
11
  private readonly registeredSchemaIds;
12
+ private readonly compiledValidators;
12
13
  /** The subset of `registeredSchemaIds` claimed by a kind's schema. Kinds and
13
14
  * named `Telo.Type`s share one `telo://<module>/<Name>` id space, so this is
14
15
  * what lets a colliding type name be reported instead of silently dropped. */
@@ -77,12 +78,28 @@ export declare class DefinitionRegistry {
77
78
  * sanctioned way to reuse one, so a comparator that cannot follow the
78
79
  * reference judges two opaque nodes and learns nothing. */
79
80
  schemaForId(id: string): Record<string, any> | undefined;
80
- /** Validates data against a schema using this registry's AJV instance, which has all
81
- * registered definition schemas loaded enabling cross-module $ref resolution.
82
- * A compile failure returns `[]` here; it is surfaced loudly (once, on the
83
- * owning definition) by `schemaCompileError` via the analyzer's
84
- * definition-schema compile check, so resources are never silently skipped. */
81
+ /**
82
+ * Validates a resource's configuration against its kind's schema, with the
83
+ * offending field's path — what a `SCHEMA_VIOLATION` diagnostic is built from.
84
+ *
85
+ * On THIS registry's AJV, which is the point: it holds every registered
86
+ * definition schema and every named `Telo.Type`, so a kind whose schema
87
+ * references a shape declared elsewhere is checked rather than skipped. The
88
+ * module-level instance this used to run on had none of them registered, so
89
+ * such a schema failed to compile and the failure was swallowed — a resource
90
+ * could be arbitrarily wrong and `telo check` reported nothing, while the
91
+ * kernel (whose validator does resolve the reference) rejected it at boot.
92
+ * Two AJVs answering one question is what made that possible; there is now
93
+ * one, and it is the same one `schemaCompileError` reports through.
94
+ */
85
95
  validateWithRefs(data: unknown, schema: Record<string, any>): string[];
96
+ /** {@link validateWithRefs}, with the path each issue is anchored at. */
97
+ validateResourceConfig(data: unknown, schema: Record<string, any>): SchemaIssue[];
98
+ /** Memoized per schema OBJECT — the analyzer validates every resource of a
99
+ * kind against the same one, and this runs at keystroke time in an editor.
100
+ * A schema AJV refuses compiles to `undefined`; that is reported once,
101
+ * anchored on the owning definition, by `schemaCompileError`. */
102
+ private compiledFor;
86
103
  /** Returns the AJV compile error for `schema`, or `undefined` when it compiles.
87
104
  * Compiles on this registry's instance, which has every loaded module schema
88
105
  * plus the manifest root registered, so local `#/$defs`, `telo://manifest`,
@@ -133,6 +150,21 @@ export declare class DefinitionRegistry {
133
150
  * anchors depend on a sibling property at runtime and stay unexpanded; the
134
151
  * analyzer's reference validation phase already flags the cases that matter. */
135
152
  expandedFieldMapForResource(resource: ResourceManifest, aliases: AliasResolver, aliasesByModule: Map<string, AliasResolver>): ReferenceFieldMap | undefined;
153
+ /**
154
+ * The schema an `x-telo-schema-from` slot derives its shape from.
155
+ *
156
+ * Only the STATIC form resolves: an anchor that is a dotted alias-qualified
157
+ * kind (`HttpDispatch.Request/$defs/Matcher`). The polymorphic forms — a
158
+ * relative anchor, or a single-segment absolute one — name a sibling property
159
+ * whose value is known per resource, so a definition-level lookup would be
160
+ * guessing at one instance's shape.
161
+ *
162
+ * Its own method because a schema-from slot is otherwise INVISIBLE to anything
163
+ * reading `properties`: the field map needs the nested ref slots, and an IDE
164
+ * needs the very same node to offer a key or describe one. Two resolutions of
165
+ * one annotation would eventually disagree about which anchors are static.
166
+ */
167
+ resolveSchemaFromNode(schemaFrom: string, ownerScope: AliasResolver): Record<string, any> | undefined;
136
168
  private resolveSchemaFromSubMap;
137
169
  /** Returns all definitions that transitively extend the given abstract kind.
138
170
  * Follows the capability chain to any depth (equivalent to instanceof in OOP).
@@ -1 +1 @@
1
- {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAIlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD;;mFAE+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;;;;wEAIoE;IACpE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAoC9C,OAAO,CAAC,aAAa;IASrB;;;;;;;;;;;;;2FAauF;IACvF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAK1E;;;;;;;;;;gFAU4E;IAC5E,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO;IAQzE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,YAAY;IASpB;uFACmF;IACnF,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIhC;;;gEAG4D;IAC5D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS;IAMxD;;;;oFAIgF;IAChF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE;;;;;;iCAM6B;IAC7B,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS;IASnE;;;;4EAIwE;IACxE,OAAO,CAAC,iBAAiB;IAqBzB;;;;;;;;;;;;;;4BAcwB;IACxB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAShD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD;;;uCAGmC;IACnC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAWxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;;;;;;;qFAQiF;IACjF,2BAA2B,CACzB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,iBAAiB,GAAG,SAAS;IAkChC,OAAO,CAAC,uBAAuB;IA+B/B;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
1
+ {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { canonicalTypeSchemaId } from "@telorun/sdk";
2
2
  import { KERNEL_BUILTINS } from "./builtins.js";
3
3
  import { buildFieldMapAtPath, buildReferenceFieldMap, isSchemaFromEntry, } from "./reference-field-map.js";
4
- import { createAjv, formatSingleError, navigateJsonPointer } from "./schema-compat.js";
4
+ import { createAjv, navigateJsonPointer } from "./schema-compat.js";
5
+ import { formatSingleError, reduceSchemaErrors, schemaIssues, } from "./schema-error-report.js";
5
6
  import { effectiveAuthorSchema } from "./extends-resolution.js";
6
- /** Pure kind → ResourceDefinition map. No controller loading, no lifecycle. */
7
7
  export class DefinitionRegistry {
8
8
  constructor() {
9
9
  for (const def of KERNEL_BUILTINS)
@@ -14,6 +14,7 @@ export class DefinitionRegistry {
14
14
  * across analyze() calls and no unbounded growth across the process lifetime. */
15
15
  ajv = createAjv();
16
16
  registeredSchemaIds = new Set();
17
+ compiledValidators = new WeakMap();
17
18
  /** The subset of `registeredSchemaIds` claimed by a kind's schema. Kinds and
18
19
  * named `Telo.Type`s share one `telo://<module>/<Name>` id space, so this is
19
20
  * what lets a colliding type name be reported instead of silently dropped. */
@@ -152,22 +153,49 @@ export class DefinitionRegistry {
152
153
  const schema = compiled?.schema;
153
154
  return schema && typeof schema === "object" ? schema : undefined;
154
155
  }
155
- /** Validates data against a schema using this registry's AJV instance, which has all
156
- * registered definition schemas loaded enabling cross-module $ref resolution.
157
- * A compile failure returns `[]` here; it is surfaced loudly (once, on the
158
- * owning definition) by `schemaCompileError` via the analyzer's
159
- * definition-schema compile check, so resources are never silently skipped. */
156
+ /**
157
+ * Validates a resource's configuration against its kind's schema, with the
158
+ * offending field's path — what a `SCHEMA_VIOLATION` diagnostic is built from.
159
+ *
160
+ * On THIS registry's AJV, which is the point: it holds every registered
161
+ * definition schema and every named `Telo.Type`, so a kind whose schema
162
+ * references a shape declared elsewhere is checked rather than skipped. The
163
+ * module-level instance this used to run on had none of them registered, so
164
+ * such a schema failed to compile and the failure was swallowed — a resource
165
+ * could be arbitrarily wrong and `telo check` reported nothing, while the
166
+ * kernel (whose validator does resolve the reference) rejected it at boot.
167
+ * Two AJVs answering one question is what made that possible; there is now
168
+ * one, and it is the same one `schemaCompileError` reports through.
169
+ */
160
170
  validateWithRefs(data, schema) {
161
- let validate;
171
+ const validate = this.compiledFor(schema);
172
+ if (!validate || validate(data))
173
+ return [];
174
+ return reduceSchemaErrors(validate.errors).map(formatSingleError);
175
+ }
176
+ /** {@link validateWithRefs}, with the path each issue is anchored at. */
177
+ validateResourceConfig(data, schema) {
178
+ const validate = this.compiledFor(schema);
179
+ if (!validate || validate(data))
180
+ return [];
181
+ return schemaIssues(validate.errors);
182
+ }
183
+ /** Memoized per schema OBJECT — the analyzer validates every resource of a
184
+ * kind against the same one, and this runs at keystroke time in an editor.
185
+ * A schema AJV refuses compiles to `undefined`; that is reported once,
186
+ * anchored on the owning definition, by `schemaCompileError`. */
187
+ compiledFor(schema) {
188
+ const cached = this.compiledValidators.get(schema);
189
+ if (cached)
190
+ return cached;
162
191
  try {
163
- validate = this.ajv.compile(schema);
192
+ const validate = this.ajv.compile(schema);
193
+ this.compiledValidators.set(schema, validate);
194
+ return validate;
164
195
  }
165
196
  catch {
166
- return [];
197
+ return undefined;
167
198
  }
168
- if (validate(data))
169
- return [];
170
- return (validate.errors ?? []).map(formatSingleError);
171
199
  }
172
200
  /** Returns the AJV compile error for `schema`, or `undefined` when it compiles.
173
201
  * Compiles on this registry's instance, which has every loaded module schema
@@ -299,28 +327,44 @@ export class DefinitionRegistry {
299
327
  }
300
328
  return expanded;
301
329
  }
302
- resolveSchemaFromSubMap(schemaFrom, fieldPath, ownerScope) {
330
+ /**
331
+ * The schema an `x-telo-schema-from` slot derives its shape from.
332
+ *
333
+ * Only the STATIC form resolves: an anchor that is a dotted alias-qualified
334
+ * kind (`HttpDispatch.Request/$defs/Matcher`). The polymorphic forms — a
335
+ * relative anchor, or a single-segment absolute one — name a sibling property
336
+ * whose value is known per resource, so a definition-level lookup would be
337
+ * guessing at one instance's shape.
338
+ *
339
+ * Its own method because a schema-from slot is otherwise INVISIBLE to anything
340
+ * reading `properties`: the field map needs the nested ref slots, and an IDE
341
+ * needs the very same node to offer a key or describe one. Two resolutions of
342
+ * one annotation would eventually disagree about which anchors are static.
343
+ */
344
+ resolveSchemaFromNode(schemaFrom, ownerScope) {
303
345
  const isAbsolute = schemaFrom.startsWith("/");
304
346
  const expr = isAbsolute ? schemaFrom.slice(1) : schemaFrom;
305
347
  const slashIdx = expr.indexOf("/");
306
348
  if (slashIdx === -1)
307
- return null;
349
+ return undefined;
308
350
  const anchorName = expr.slice(0, slashIdx);
309
351
  const jsonPointer = "/" + expr.slice(slashIdx + 1);
310
- // Static form: absolute path whose anchor is a dotted alias (e.g.
311
- // "HttpDispatch.Outcomes/$defs/Returns"). Polymorphic forms — relative
312
- // anchors or single-segment absolute anchors — only resolve once we know a
313
- // sibling property's value, which is per-resource.
314
352
  if (!anchorName.includes("."))
315
- return null;
353
+ return undefined;
316
354
  const targetKind = ownerScope.resolveKind(anchorName);
317
355
  if (!targetKind)
318
- return null;
356
+ return undefined;
319
357
  const targetDef = this.resolve(targetKind);
320
358
  if (!targetDef?.schema)
321
- return null;
359
+ return undefined;
322
360
  const subSchema = navigateJsonPointer(targetDef.schema, jsonPointer);
323
361
  if (!subSchema || typeof subSchema !== "object")
362
+ return undefined;
363
+ return subSchema;
364
+ }
365
+ resolveSchemaFromSubMap(schemaFrom, fieldPath, ownerScope) {
366
+ const subSchema = this.resolveSchemaFromNode(schemaFrom, ownerScope);
367
+ if (!subSchema)
324
368
  return null;
325
369
  return buildFieldMapAtPath(subSchema, fieldPath);
326
370
  }
@@ -0,0 +1,10 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ /**
3
+ * The manifest a `(kind, name)` pair addresses.
4
+ *
5
+ * One implementation, because two consumers needed it and a manifest set is
6
+ * exactly the thing `ManifestAnalysis` exists to have one answer over. Undefined
7
+ * when the set holds no such resource — a document the author is still writing.
8
+ */
9
+ export declare function findManifest(manifests: readonly ResourceManifest[], kind: string | undefined, name: string | undefined): ResourceManifest | undefined;
10
+ //# sourceMappingURL=find-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-manifest.d.ts","sourceRoot":"","sources":["../src/find-manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,gBAAgB,GAAG,SAAS,CAK9B"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The manifest a `(kind, name)` pair addresses.
3
+ *
4
+ * One implementation, because two consumers needed it and a manifest set is
5
+ * exactly the thing `ManifestAnalysis` exists to have one answer over. Undefined
6
+ * when the set holds no such resource — a document the author is still writing.
7
+ */
8
+ export function findManifest(manifests, kind, name) {
9
+ if (!kind || !name)
10
+ return undefined;
11
+ return manifests.find((m) => m.kind === kind && m.metadata?.name === name);
12
+ }
package/dist/index.d.ts CHANGED
@@ -60,8 +60,10 @@ export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validat
60
60
  export type { RefSlotIssue } from "./validate-ref-slots.js";
61
61
  export { validateValueTypeSlots } from "./validate-value-type-slots.js";
62
62
  export type { ValueTypeSlotIssue } from "./validate-value-type-slots.js";
63
- export { checkSchemaCompatibility, selectUnionBranch } from "./schema-compat.js";
64
- export type { CompatibilityResult } from "./schema-compat.js";
63
+ export { checkSchemaCompatibility, resolveRefIn, selectUnionBranch } from "./schema-compat.js";
64
+ export type { CompatibilityResult, ExternalSchemaResolver } from "./schema-compat.js";
65
+ export { ajvErrorToPath, formatAjvErrors, formatSingleError, reduceSchemaErrors, schemaIssues, } from "./schema-error-report.js";
66
+ export type { AjvErrorLike, SchemaIssue } from "./schema-error-report.js";
65
67
  export { visitManifest } from "./manifest-visitor.js";
66
68
  export type { CelSiteEvent, ManifestVisitor, RefSiteEvent, ResourceEnterEvent, ResourceExitEvent, ScopeBoundaryEvent, SchemaFromSiteEvent, VisitOptions, } from "./manifest-visitor.js";
67
69
  export { Loader } from "./manifest-loader.js";
@@ -108,6 +110,13 @@ export { documentToAst, parseToAst } from "./yaml-ast.js";
108
110
  export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
109
111
  export { CelParseError, buildCelSegments, wrapCelAst } from "./cel-ast.js";
110
112
  export type { CelNode, CelSegment } from "./cel-ast.js";
113
+ export { CelScopeResolver } from "./cel-scope.js";
114
+ export type { CelScope, CelScopeInputs, CelSiteRef } from "./cel-scope.js";
115
+ export { CelScopeQuery } from "./cel-scope-query.js";
116
+ export type { CelScopeQueryContext, ContextDeclarationSite } from "./cel-scope-query.js";
117
+ export { navigateConcretePath } from "./manifest-path.js";
118
+ export { ManifestAnalysis } from "./manifest-analysis.js";
119
+ export type { ManifestRef } from "./manifest-analysis.js";
111
120
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity, diagnosticFix } from "./types.js";
112
121
  export type { AnalysisDiagnostic, AnalysisOptions, DiagnosticData, DiagnosticFix, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.js";
113
122
  export * from "./manifest-schemas.js";