@telorun/analyzer 0.63.0 → 0.64.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.
- package/dist/analysis-registry.d.ts +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +35 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +57 -467
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +15 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +25 -9
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/invocation-contract.d.ts +11 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +15 -0
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-step-inputs.d.ts +17 -0
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +97 -6
- package/package.json +2 -2
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +61 -585
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +31 -13
- package/src/find-manifest.ts +19 -0
- package/src/index.ts +13 -0
- package/src/invocation-contract.ts +22 -0
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-path.ts +34 -0
- package/src/schema-walk.ts +144 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-step-inputs.ts +143 -7
package/src/analyzer.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
26
26
|
import { type ContractDirection, effectiveAuthorSchema } from "./extends-resolution.js";
|
|
27
27
|
import {
|
|
28
|
+
analyzerContractScope,
|
|
28
29
|
type ContractScope,
|
|
29
30
|
PERMISSIVE_CONTRACT,
|
|
30
31
|
resolveContract,
|
|
@@ -53,7 +54,16 @@ import { resolveSchemaRefKinds, type RefConstraintIssue } from "./resolve-schema
|
|
|
53
54
|
import { runZoneAnalysis, type ZoneExportCache } from "./resolve-zone-requirements.js";
|
|
54
55
|
import { validateDurableRegions } from "./validate-durable-regions.js";
|
|
55
56
|
import { validateZoneViolations } from "./validate-zone-violations.js";
|
|
56
|
-
import {
|
|
57
|
+
import { ManifestRootSchema } from "./manifest-schemas.js";
|
|
58
|
+
import { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
|
|
59
|
+
import { buildStepContextSchema, CelScopeResolver, manifestRootForResolver } from "./cel-scope.js";
|
|
60
|
+
|
|
61
|
+
// The structural walks and the CEL scope rule moved out of this file — the
|
|
62
|
+
// first so both halves can reach them, the second so the IDE can ask what a
|
|
63
|
+
// cursor sees without pulling the analysis pass in behind it. Re-exported here
|
|
64
|
+
// because they were part of this module's surface before the split.
|
|
65
|
+
export { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
|
|
66
|
+
export { analyzerContractScope } from "./invocation-contract.js";
|
|
57
67
|
import { validateZoneSlotDeclarations, type ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
58
68
|
import {
|
|
59
69
|
validateSchemaProjection,
|
|
@@ -135,7 +145,7 @@ import { validateModuleMetadata } from "./validate-module-metadata.js";
|
|
|
135
145
|
import { validateRequires } from "./validate-requires.js";
|
|
136
146
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
137
147
|
import { validateInvocationContract } from "./validate-invocation-contract.js";
|
|
138
|
-
import { collectStepInputIssues } from "./validate-step-inputs.js";
|
|
148
|
+
import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
|
|
139
149
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
140
150
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
141
151
|
import { validateReferences } from "./validate-references.js";
|
|
@@ -194,117 +204,8 @@ function resolveSelfOrAlias(
|
|
|
194
204
|
return scopeResolver.resolveKind(value);
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
/** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
|
|
198
|
-
* resolve in the module that declared the definition they were read off (so an
|
|
199
|
-
* `extends` chain crossing module boundaries re-scopes at every hop), and named
|
|
200
|
-
* `telo#Type` references resolve against the flattened manifest list. `resolveIn`
|
|
201
|
-
* is the top-level entry point, where the kind was written by the READING
|
|
202
|
-
* module and there is no declaring definition yet. */
|
|
203
|
-
export function analyzerContractScope(
|
|
204
|
-
defs: DefinitionRegistry,
|
|
205
|
-
aliases: AliasResolver,
|
|
206
|
-
scopes: ModuleScopes,
|
|
207
|
-
allManifests: Record<string, any>[],
|
|
208
|
-
): ContractScope & { resolveIn(kind: string, module?: string): ResourceDefinition | undefined } {
|
|
209
|
-
const resolve = moduleScopedDefResolver<ResourceDefinition>(defs, aliases, scopes);
|
|
210
|
-
return {
|
|
211
|
-
resolveDefinition: resolve,
|
|
212
|
-
resolveIn: resolve.in,
|
|
213
|
-
typeManifestsFor: () => allManifests,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
207
|
const SOURCE = "telo-analyzer";
|
|
218
208
|
|
|
219
|
-
/** Build a closed JSON Schema for the `self` CEL variable available inside a
|
|
220
|
-
* `Telo.Definition` template body. Mirrors the runtime template controller's
|
|
221
|
-
* `const self = { ...resource, name: resource.metadata.name };` — every
|
|
222
|
-
* property the user declared in `schema:` plus synthetic `name` / `kind` and
|
|
223
|
-
* the metadata sub-object (kept open since metadata legitimately carries
|
|
224
|
-
* arbitrary user-added fields). */
|
|
225
|
-
function buildSelfSchema(
|
|
226
|
-
definition: Record<string, any>,
|
|
227
|
-
defs?: DefinitionRegistry,
|
|
228
|
-
aliases?: AliasResolver,
|
|
229
|
-
): Record<string, any> {
|
|
230
|
-
// The author-facing schema resolves inheritance: with `base:` the child's own
|
|
231
|
-
// schema (the parent's config is internal); without it, `merge(parent, own)`.
|
|
232
|
-
const userSchema = (
|
|
233
|
-
defs
|
|
234
|
-
? effectiveAuthorSchema(definition as unknown as ResourceDefinition, (k) =>
|
|
235
|
-
defs.resolve(aliases?.resolveKind(k) ?? k) ?? defs.resolve(k),
|
|
236
|
-
)
|
|
237
|
-
: (definition.schema ?? {})
|
|
238
|
-
) as Record<string, any>;
|
|
239
|
-
const userProps = (userSchema.properties ?? {}) as Record<string, any>;
|
|
240
|
-
const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
|
|
241
|
-
return {
|
|
242
|
-
type: "object",
|
|
243
|
-
additionalProperties: false,
|
|
244
|
-
properties: {
|
|
245
|
-
...userProps,
|
|
246
|
-
name: { type: "string" },
|
|
247
|
-
kind: { type: "string" },
|
|
248
|
-
metadata: {
|
|
249
|
-
type: "object",
|
|
250
|
-
additionalProperties: true,
|
|
251
|
-
properties: { name: { type: "string" } },
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
required: [...userRequired, "name", "kind"],
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
259
|
-
* invocable template body — the shared contract resolver applied to the
|
|
260
|
-
* definition itself, so a body is typed against the exact signature callers are
|
|
261
|
-
* checked against and dispatch enforces. Walks the whole `extends` chain rather
|
|
262
|
-
* than one hop, so a definition two levels below the declaration still gets
|
|
263
|
-
* typed inputs. Undefined when nothing in the chain declares a contract —
|
|
264
|
-
* the caller signals opaque `map<string, dyn>` upstream. */
|
|
265
|
-
function lookupTemplateInputsSchema(
|
|
266
|
-
definition: Record<string, any>,
|
|
267
|
-
defs: DefinitionRegistry,
|
|
268
|
-
aliases: AliasResolver,
|
|
269
|
-
allManifests: Record<string, any>[],
|
|
270
|
-
scopes: ModuleScopes,
|
|
271
|
-
): Record<string, any> | undefined {
|
|
272
|
-
return resolveContract(
|
|
273
|
-
"inputType",
|
|
274
|
-
undefined,
|
|
275
|
-
definition as unknown as ResourceDefinition,
|
|
276
|
-
analyzerContractScope(defs, aliases, scopes, allManifests),
|
|
277
|
-
)?.schema;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
281
|
-
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
282
|
-
* have been pre-augmented:
|
|
283
|
-
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
284
|
-
* - `inputType` → resolved through the shared contract resolver, so
|
|
285
|
-
* `x-telo-context-from-root: inputType` substitutes the
|
|
286
|
-
* real signature. Without it the annotation would replace
|
|
287
|
-
* the node verbatim with the inline `{kind, schema}` wrapper
|
|
288
|
-
* the standard library writes everywhere, typing `inputs` as
|
|
289
|
-
* `{kind, schema}` instead of the declared properties.
|
|
290
|
-
*
|
|
291
|
-
* For non-definition manifests the original object is returned. */
|
|
292
|
-
function manifestRootForResolver(
|
|
293
|
-
m: Record<string, any>,
|
|
294
|
-
defs: DefinitionRegistry,
|
|
295
|
-
aliases: AliasResolver,
|
|
296
|
-
allManifests: Record<string, any>[],
|
|
297
|
-
scopes: ModuleScopes,
|
|
298
|
-
): Record<string, any> {
|
|
299
|
-
if (m.kind !== "Telo.Definition") return m;
|
|
300
|
-
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
|
|
301
|
-
return {
|
|
302
|
-
...m,
|
|
303
|
-
schema: buildSelfSchema(m, defs, aliases),
|
|
304
|
-
...(inputs ? { inputType: inputs } : {}),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
209
|
/** True when an issue reports a property that is absent — its path points at a
|
|
309
210
|
* node the manifest does not contain. */
|
|
310
211
|
export const missingRequired = (issue: { message: string }): boolean =>
|
|
@@ -338,273 +239,6 @@ function contractOwnerLabel(
|
|
|
338
239
|
return canonical;
|
|
339
240
|
}
|
|
340
241
|
|
|
341
|
-
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
342
|
-
* Non-refs and unresolved refs pass through unchanged. */
|
|
343
|
-
export function resolveLocalRef(
|
|
344
|
-
schema: Record<string, any> | undefined,
|
|
345
|
-
root: Record<string, any>,
|
|
346
|
-
): Record<string, any> | undefined {
|
|
347
|
-
if (!schema) return undefined;
|
|
348
|
-
const ref = schema.$ref;
|
|
349
|
-
if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
|
|
350
|
-
const defName = ref.slice("#/$defs/".length);
|
|
351
|
-
const resolved = root.$defs?.[defName];
|
|
352
|
-
if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
|
|
353
|
-
}
|
|
354
|
-
// A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
|
|
355
|
-
// Resolved HERE rather than by each walker: this is the one chokepoint every
|
|
356
|
-
// structural walk already goes through — the step-array walks, the call graph,
|
|
357
|
-
// the zone projection, the eval-path collector — so a composer that points at a
|
|
358
|
-
// shared shape stays legible to all of them at once. Nothing is inlined into
|
|
359
|
-
// the stored schema, which keeps validator-cache identity stable and matches
|
|
360
|
-
// what `resolveSchemaTypeRefs` does for a named user type.
|
|
361
|
-
if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
|
|
362
|
-
const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
|
|
363
|
-
const resolved = (ManifestRootSchema.$defs as Record<string, unknown>)[defName];
|
|
364
|
-
if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
|
|
365
|
-
}
|
|
366
|
-
return schema;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
|
|
370
|
-
|
|
371
|
-
/** Gather property schemas from a (possibly variant-bearing) object schema:
|
|
372
|
-
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
|
|
373
|
-
*
|
|
374
|
-
* Each branch is resolved through {@link resolveLocalRef} first, so a branch
|
|
375
|
-
* that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
|
|
376
|
-
* site — contributes its properties like an inline one. Without that, pointing a
|
|
377
|
-
* composer at a shared shape would silently empty every role-driven lookup that
|
|
378
|
-
* reads this (the inputs slot, the retry policy, the eval paths), which is a
|
|
379
|
-
* failure with no diagnostic attached to it. */
|
|
380
|
-
export function gatherPropertySchemas(
|
|
381
|
-
schema: Record<string, any>,
|
|
382
|
-
root?: Record<string, any>,
|
|
383
|
-
): Array<[string, Record<string, any>]> {
|
|
384
|
-
const out: Array<[string, Record<string, any>]> = [];
|
|
385
|
-
const base = resolveLocalRef(schema, root ?? schema) ?? schema;
|
|
386
|
-
if (base.properties && typeof base.properties === "object") {
|
|
387
|
-
for (const [k, v] of Object.entries(base.properties as Record<string, any>)) {
|
|
388
|
-
out.push([k, v as Record<string, any>]);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
|
|
392
|
-
const arr = base[variantKey];
|
|
393
|
-
if (!Array.isArray(arr)) continue;
|
|
394
|
-
for (const raw of arr) {
|
|
395
|
-
if (!raw || typeof raw !== "object") continue;
|
|
396
|
-
const variant = resolveLocalRef(raw as Record<string, any>, root ?? schema) ?? raw;
|
|
397
|
-
if (variant.properties) {
|
|
398
|
-
for (const [k, v] of Object.entries(variant.properties as Record<string, any>)) {
|
|
399
|
-
out.push([k, v as Record<string, any>]);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return out;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Generic, role-driven walk over a step array. Calls
|
|
409
|
-
* `visit(step, stepPath)` for every step — top-level and nested through the
|
|
410
|
-
* `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
|
|
411
|
-
* the single definition of how steps nest, shared by `buildStepContextSchema`
|
|
412
|
-
* (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
|
|
413
|
-
* checks invoke refs), so the topology contract lives in one place — adding a
|
|
414
|
-
* role or nesting form updates both consumers at once. No resource kind is
|
|
415
|
-
* hardcoded; recursion is driven entirely by the schema annotations.
|
|
416
|
-
*/
|
|
417
|
-
export function walkStepArray(
|
|
418
|
-
steps: unknown[],
|
|
419
|
-
stepItemSchema: Record<string, any> | undefined,
|
|
420
|
-
rootSchema: Record<string, any>,
|
|
421
|
-
basePath: string,
|
|
422
|
-
visit: (step: Record<string, any>, stepPath: string) => void,
|
|
423
|
-
): void {
|
|
424
|
-
const dispatchRole = (
|
|
425
|
-
data: unknown,
|
|
426
|
-
role: string,
|
|
427
|
-
itemsSchema: Record<string, any> | undefined,
|
|
428
|
-
path: string,
|
|
429
|
-
): void => {
|
|
430
|
-
if (role === "branch" && Array.isArray(data)) {
|
|
431
|
-
walkStepArray(data, stepItemSchema, rootSchema, path, visit);
|
|
432
|
-
} else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
|
|
433
|
-
for (const [caseKey, arr] of Object.entries(data as Record<string, unknown>)) {
|
|
434
|
-
if (Array.isArray(arr)) walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
|
|
435
|
-
}
|
|
436
|
-
} else if (role === "branch-list" && Array.isArray(data)) {
|
|
437
|
-
const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
|
|
438
|
-
if (!entrySchema) return;
|
|
439
|
-
data.forEach((entry, i) => {
|
|
440
|
-
if (!entry || typeof entry !== "object") return;
|
|
441
|
-
for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
|
|
442
|
-
const subRole = subSchema["x-telo-topology-role"];
|
|
443
|
-
if (typeof subRole !== "string") continue;
|
|
444
|
-
dispatchRole(
|
|
445
|
-
(entry as Record<string, any>)[subKey],
|
|
446
|
-
subRole,
|
|
447
|
-
subSchema.items as Record<string, any> | undefined,
|
|
448
|
-
`${path}[${i}].${subKey}`,
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
steps.forEach((step, i) => {
|
|
456
|
-
if (!step || typeof step !== "object") return;
|
|
457
|
-
const s = step as Record<string, any>;
|
|
458
|
-
const stepPath = `${basePath}[${i}]`;
|
|
459
|
-
visit(s, stepPath);
|
|
460
|
-
if (!stepItemSchema) return;
|
|
461
|
-
for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
|
|
462
|
-
const role = propSchema["x-telo-topology-role"];
|
|
463
|
-
if (typeof role !== "string") continue;
|
|
464
|
-
dispatchRole(
|
|
465
|
-
s[propKey],
|
|
466
|
-
role,
|
|
467
|
-
propSchema.items as Record<string, any> | undefined,
|
|
468
|
-
`${stepPath}.${propKey}`,
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Build a `steps` context schema for a kind's step body.
|
|
476
|
-
* Walks each step in the manifest array, resolves the invoked resource's output
|
|
477
|
-
* contract, and builds `steps.<name>.result` context entries.
|
|
478
|
-
*
|
|
479
|
-
* Resolution is the shared {@link resolveContract} — the invoked resource
|
|
480
|
-
* manifest's own declaration, then the kind's, resolved to the nearest
|
|
481
|
-
* declaration along `extends`, then permissive. Sharing it with the kernel is
|
|
482
|
-
* what stops `telo check` from typing `steps.X.result` against one contract
|
|
483
|
-
* while dispatch validates against another.
|
|
484
|
-
*
|
|
485
|
-
* The kind layer is what makes `x-telo-stream` properties on definitions
|
|
486
|
-
* actually govern step-result chain validation — without it, the validator falls
|
|
487
|
-
* back to permissive and the stream-opacity rule never fires.
|
|
488
|
-
*
|
|
489
|
-
* Recursion into nested step arrays is annotation-driven via
|
|
490
|
-
* `x-telo-topology-role`. The analyzer recognises three role values:
|
|
491
|
-
* - `branch` — value is an array of steps (e.g. then / else / do / catch).
|
|
492
|
-
* - `branch-list`— value is an array of objects each carrying further roled
|
|
493
|
-
* sub-properties (e.g. elseif: [{ if, then }]).
|
|
494
|
-
* - `case-map` — value is an object whose values are step arrays (e.g. cases).
|
|
495
|
-
* No specific Run.Sequence field name is hardcoded; any kind that uses
|
|
496
|
-
* a step body and tags its branch fields with these roles works.
|
|
497
|
-
*/
|
|
498
|
-
function buildStepContextSchema(
|
|
499
|
-
manifest: Record<string, any>,
|
|
500
|
-
defSchema: Record<string, any>,
|
|
501
|
-
allManifests: Record<string, any>[],
|
|
502
|
-
defs: DefinitionRegistry,
|
|
503
|
-
aliases: AliasResolver,
|
|
504
|
-
scopes: ModuleScopes,
|
|
505
|
-
): Record<string, any> | undefined {
|
|
506
|
-
const props = defSchema.properties as Record<string, any> | undefined;
|
|
507
|
-
if (!props) return undefined;
|
|
508
|
-
|
|
509
|
-
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
510
|
-
const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
511
|
-
|
|
512
|
-
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
513
|
-
const stepCtx = readStepSlot(fieldSchema);
|
|
514
|
-
if (!stepCtx) continue;
|
|
515
|
-
|
|
516
|
-
const invokeField = stepCtx.invoke;
|
|
517
|
-
const outputTypeField = stepCtx.outputType;
|
|
518
|
-
// Optional: the field a step uses to produce a result without dispatching.
|
|
519
|
-
// Only a kind that declares one has pure steps at all.
|
|
520
|
-
const valueField = stepCtx.value;
|
|
521
|
-
if (!invokeField || !outputTypeField) continue;
|
|
522
|
-
|
|
523
|
-
const steps = manifest[fieldName];
|
|
524
|
-
if (!Array.isArray(steps)) continue;
|
|
525
|
-
|
|
526
|
-
const stepItemSchema = resolveLocalRef(
|
|
527
|
-
fieldSchema.items as Record<string, any> | undefined,
|
|
528
|
-
defSchema,
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
// The instance's own input contract, for typing a pure step that just
|
|
532
|
-
// forwards one of its values.
|
|
533
|
-
const ownInputs = resolveTypeFieldToSchema(
|
|
534
|
-
(manifest as Record<string, any>).inputType,
|
|
535
|
-
allManifests,
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const stepProperties: Record<string, any> = {};
|
|
539
|
-
|
|
540
|
-
walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
|
|
541
|
-
const name = s.name;
|
|
542
|
-
const invoke = s[invokeField] as Record<string, any> | undefined;
|
|
543
|
-
// Only invoke steps register a `steps.<name>.result` entry — control-flow
|
|
544
|
-
// wrappers (try/if/while/switch/throw) don't produce a result and must
|
|
545
|
-
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
546
|
-
// or unknown step references slip through chain validation.
|
|
547
|
-
if (typeof name !== "string") return;
|
|
548
|
-
if (!invoke || typeof invoke !== "object") {
|
|
549
|
-
// A pure step dispatches nothing, so there is no contract to resolve.
|
|
550
|
-
// Where its expression is a plain chain into something already typed —
|
|
551
|
-
// an earlier step's result, or the kind's own inputs — that type carries
|
|
552
|
-
// through; anything else (arithmetic, a call, a comprehension) stays
|
|
553
|
-
// permissive rather than guessed. Same rule as a named binding's.
|
|
554
|
-
if (valueField && valueField in s) {
|
|
555
|
-
const scopeRoot = {
|
|
556
|
-
properties: {
|
|
557
|
-
steps: { type: "object", properties: { ...stepProperties } },
|
|
558
|
-
...(ownInputs ? { inputs: ownInputs } : {}),
|
|
559
|
-
},
|
|
560
|
-
};
|
|
561
|
-
const chained = schemaAtChain(bindingPathChain(s[valueField]), scopeRoot);
|
|
562
|
-
stepProperties[name] = {
|
|
563
|
-
type: "object",
|
|
564
|
-
properties: { result: chained ?? PERMISSIVE_CONTRACT },
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const invokedKind = invoke.kind as string | undefined;
|
|
570
|
-
const invokedName = invoke.name as string | undefined;
|
|
571
|
-
// A named `!ref` carries the target's own manifest (which may narrow the
|
|
572
|
-
// contract for this one instance); an inline `{ kind, ... }` step IS the
|
|
573
|
-
// manifest. Either way the kind layer resolves through `extends`.
|
|
574
|
-
const invokedManifest = invokedName
|
|
575
|
-
? (allManifests.find(
|
|
576
|
-
(m) =>
|
|
577
|
-
(m.metadata as any)?.name === invokedName && (!invokedKind || m.kind === invokedKind),
|
|
578
|
-
) as Record<string, any> | undefined)
|
|
579
|
-
: (invoke as Record<string, any>);
|
|
580
|
-
const invokedDef = invokedKind
|
|
581
|
-
? contractScope.resolveIn(invokedKind, readingModule)
|
|
582
|
-
: undefined;
|
|
583
|
-
const outputSchema = resolveContract(
|
|
584
|
-
outputTypeField as ContractDirection,
|
|
585
|
-
invokedManifest,
|
|
586
|
-
invokedDef,
|
|
587
|
-
contractScope,
|
|
588
|
-
)?.schema;
|
|
589
|
-
stepProperties[name] = {
|
|
590
|
-
type: "object",
|
|
591
|
-
properties: {
|
|
592
|
-
result: outputSchema ?? PERMISSIVE_CONTRACT,
|
|
593
|
-
},
|
|
594
|
-
};
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
if (Object.keys(stepProperties).length > 0) {
|
|
598
|
-
return {
|
|
599
|
-
type: "object",
|
|
600
|
-
properties: stepProperties,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return undefined;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
242
|
/** The built-in namespace: globally resolvable, crossing no import boundary. */
|
|
609
243
|
const TELO_BUILTIN_MODULE = "Telo";
|
|
610
244
|
|
|
@@ -882,92 +516,6 @@ function pathCrossesNestedResource(root: unknown, path: string): boolean {
|
|
|
882
516
|
return false;
|
|
883
517
|
}
|
|
884
518
|
|
|
885
|
-
function collectErrorContextScopes(
|
|
886
|
-
defSchema: Record<string, any> | undefined,
|
|
887
|
-
): Map<string, Record<string, any>> {
|
|
888
|
-
const out = new Map<string, Record<string, any>>();
|
|
889
|
-
if (!defSchema || typeof defSchema !== "object") return out;
|
|
890
|
-
const seen = new Set<Record<string, any>>();
|
|
891
|
-
|
|
892
|
-
const walk = (schema: Record<string, any> | undefined): void => {
|
|
893
|
-
if (!schema || typeof schema !== "object" || seen.has(schema)) return;
|
|
894
|
-
seen.add(schema);
|
|
895
|
-
|
|
896
|
-
const props = schema.properties as Record<string, any> | undefined;
|
|
897
|
-
if (props) {
|
|
898
|
-
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
899
|
-
if (fieldSchema && typeof fieldSchema === "object") {
|
|
900
|
-
const errCtx = (fieldSchema as Record<string, any>)["x-telo-error-context"];
|
|
901
|
-
if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
|
|
902
|
-
out.set(fieldName, errCtx as Record<string, any>);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
walk(resolveLocalRef(fieldSchema as Record<string, any>, defSchema));
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
if (schema.items) walk(resolveLocalRef(schema.items as Record<string, any>, defSchema));
|
|
909
|
-
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
910
|
-
const arr = schema[key];
|
|
911
|
-
if (Array.isArray(arr)) for (const sub of arr) walk(resolveLocalRef(sub, defSchema));
|
|
912
|
-
}
|
|
913
|
-
if (schema.$defs && typeof schema.$defs === "object") {
|
|
914
|
-
for (const sub of Object.values(schema.$defs as Record<string, any>)) {
|
|
915
|
-
walk(sub as Record<string, any>);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
walk(defSchema);
|
|
921
|
-
return out;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Return the error-context schema for a CEL `path` when the path lies within
|
|
926
|
-
* (any depth under) one of the error-bearing fields, else undefined. A path is
|
|
927
|
-
* "within" field `f` when it contains a segment `f[<index>]`. When multiple
|
|
928
|
-
* error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
|
|
929
|
-
* deepest — the one whose segment appears latest in the path — wins, so the
|
|
930
|
-
* innermost branch's schema governs.
|
|
931
|
-
*/
|
|
932
|
-
function errorContextForPath(
|
|
933
|
-
path: string,
|
|
934
|
-
scopes: Map<string, Record<string, any>>,
|
|
935
|
-
): Record<string, any> | undefined {
|
|
936
|
-
let best: { index: number; schema: Record<string, any> } | undefined;
|
|
937
|
-
for (const [fieldName, schema] of scopes) {
|
|
938
|
-
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
939
|
-
for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
|
|
940
|
-
if (best === undefined || match.index > best.index) {
|
|
941
|
-
best = { index: match.index, schema };
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return best?.schema;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** Add a kind's named bindings to a resolved context, when the context declares
|
|
949
|
-
* a bindings region. They go UNDER the context's own properties: a scope
|
|
950
|
-
* variable wins over a same-named binding at runtime, so static typing has to
|
|
951
|
-
* agree (the collision itself is `BINDING_NAME_RESERVED`). */
|
|
952
|
-
function withBindingNames(
|
|
953
|
-
contextSchema: Record<string, any>,
|
|
954
|
-
resource: Record<string, any>,
|
|
955
|
-
): Record<string, any> {
|
|
956
|
-
const field = contextSchema[BINDINGS_ANNOTATION];
|
|
957
|
-
if (typeof field !== "string") return contextSchema;
|
|
958
|
-
const bindings = resource[field];
|
|
959
|
-
if (bindings === null || typeof bindings !== "object" || Array.isArray(bindings)) {
|
|
960
|
-
return contextSchema;
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
...contextSchema,
|
|
964
|
-
properties: {
|
|
965
|
-
...bindingContextProperties(bindings as Record<string, unknown>, contextSchema),
|
|
966
|
-
...(contextSchema.properties ?? {}),
|
|
967
|
-
},
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
519
|
/** Member-access chains in a CEL expression, or none when it doesn't parse.
|
|
972
520
|
* Best-effort: a syntax error is reported by the engine pass, not here. */
|
|
973
521
|
function celAccessChains(env: Environment, expr: string): string[][] {
|
|
@@ -2077,9 +1625,20 @@ export class StaticAnalyzer {
|
|
|
2077
1625
|
// fit the slot at all", the schema answers "do their type arguments agree",
|
|
2078
1626
|
// which cel-js cannot express because it types by constructor identity.
|
|
2079
1627
|
const celSourceSchemaByPath = new Map<ResourceManifest, Map<string, Record<string, any>>>();
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
|
|
1628
|
+
// What every CEL expression in this set is typed against. The rule lives in
|
|
1629
|
+
// `cel-scope.ts` so the IDE asks the same question the pass does — a
|
|
1630
|
+
// completion list is a claim that the name it offers will pass this check,
|
|
1631
|
+
// and two implementations of it could not be held in agreement.
|
|
1632
|
+
const celScope = new CelScopeResolver({
|
|
1633
|
+
celEnv: this.celEnv,
|
|
1634
|
+
defs,
|
|
1635
|
+
aliases,
|
|
1636
|
+
scopes: { aliasesByModule, rootModules },
|
|
1637
|
+
allManifests,
|
|
1638
|
+
kernelGlobals,
|
|
1639
|
+
moduleManifest,
|
|
1640
|
+
observedStateContext,
|
|
1641
|
+
});
|
|
2083
1642
|
|
|
2084
1643
|
// Validate each non-definition, non-system resource
|
|
2085
1644
|
for (const m of allManifests) {
|
|
@@ -2481,11 +2040,11 @@ export class StaticAnalyzer {
|
|
|
2481
2040
|
// state (definitions, aliases, the typed CEL env).
|
|
2482
2041
|
// Per-resource state computed at enter and read by that resource's CEL
|
|
2483
2042
|
// sites. The manifest / resource / filePath come straight off each CelSite's
|
|
2484
|
-
// `source` (no need to capture them);
|
|
2485
|
-
// context
|
|
2043
|
+
// `source` (no need to capture them); the derived step / invocation / error
|
|
2044
|
+
// context is the scope resolver's, read back from it where a CHECK needs the
|
|
2045
|
+
// same schema an expression is typed against.
|
|
2486
2046
|
let celStepContextSchema: Record<string, any> | undefined;
|
|
2487
|
-
let
|
|
2488
|
-
let celErrorScopes: Map<string, Record<string, any>> = new Map();
|
|
2047
|
+
let celErrorScopes: ReadonlyMap<string, Record<string, any>> = new Map();
|
|
2489
2048
|
// Region coverage for the "CEL in a non-eval field" check: the union of
|
|
2490
2049
|
// `x-telo-eval` paths (own + capability) and `x-telo-context` /
|
|
2491
2050
|
// `x-telo-step-context` / `x-telo-error-context` scopes. A `!cel` outside
|
|
@@ -2506,23 +2065,29 @@ export class StaticAnalyzer {
|
|
|
2506
2065
|
{
|
|
2507
2066
|
onResourceEnter: (e) => {
|
|
2508
2067
|
const m = e.source;
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2068
|
+
celScope.enterResource(m, e.definition);
|
|
2069
|
+
// Read back rather than recomputed: the step-inputs check has to see
|
|
2070
|
+
// the same `steps` schema this resource's expressions are typed
|
|
2071
|
+
// against, and a second computation is how the two come to disagree.
|
|
2072
|
+
celStepContextSchema = celScope.stepContextSchema;
|
|
2073
|
+
celErrorScopes = celScope.errorContextScopes;
|
|
2074
|
+
if (e.definition?.schema) {
|
|
2075
|
+
const stepName = (m.metadata as any)?.name as string | undefined;
|
|
2076
|
+
const stepFile = (m.metadata as { source?: string } | undefined)?.source;
|
|
2077
|
+
// Both drivers of the SAME check: a step's `inputs:` found through
|
|
2078
|
+
// the step grammar, and a reference slot's found through the
|
|
2079
|
+
// `x-telo-ref` `inputs:` pointer. A call site the editor can
|
|
2080
|
+
// complete is a call site `telo check` validates.
|
|
2081
|
+
const inputIssues = [
|
|
2082
|
+
...collectRefInputIssues(
|
|
2514
2083
|
m as Record<string, any>,
|
|
2515
|
-
|
|
2084
|
+
defs.expandedFieldMapForResource(m, aliases, aliasesByModule),
|
|
2516
2085
|
allManifests as Record<string, any>[],
|
|
2517
2086
|
defs,
|
|
2518
2087
|
aliases,
|
|
2519
2088
|
{ aliasesByModule, rootModules },
|
|
2520
|
-
)
|
|
2521
|
-
|
|
2522
|
-
if (e.definition?.schema) {
|
|
2523
|
-
const stepName = (m.metadata as any)?.name as string | undefined;
|
|
2524
|
-
const stepFile = (m.metadata as { source?: string } | undefined)?.source;
|
|
2525
|
-
for (const issue of collectStepInputIssues(
|
|
2089
|
+
),
|
|
2090
|
+
...collectStepInputIssues(
|
|
2526
2091
|
m as Record<string, any>,
|
|
2527
2092
|
e.definition.schema as Record<string, any>,
|
|
2528
2093
|
allManifests as Record<string, any>[],
|
|
@@ -2530,7 +2095,9 @@ export class StaticAnalyzer {
|
|
|
2530
2095
|
aliases,
|
|
2531
2096
|
{ aliasesByModule, rootModules },
|
|
2532
2097
|
celStepContextSchema,
|
|
2533
|
-
|
|
2098
|
+
),
|
|
2099
|
+
];
|
|
2100
|
+
for (const issue of inputIssues) {
|
|
2534
2101
|
diagnostics.push({
|
|
2535
2102
|
severity: DiagnosticSeverity.Error,
|
|
2536
2103
|
code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
|
|
@@ -2549,10 +2116,6 @@ export class StaticAnalyzer {
|
|
|
2549
2116
|
});
|
|
2550
2117
|
}
|
|
2551
2118
|
}
|
|
2552
|
-
celErrorScopes = collectErrorContextScopes(
|
|
2553
|
-
e.definition?.schema as Record<string, any> | undefined,
|
|
2554
|
-
);
|
|
2555
|
-
|
|
2556
2119
|
celBindingSites = findBindingSites(e.definition?.schema as Record<string, any>);
|
|
2557
2120
|
if (celBindingSites) {
|
|
2558
2121
|
const declared = (m as Record<string, any>)[celBindingSites.field];
|
|
@@ -2680,7 +2243,7 @@ export class StaticAnalyzer {
|
|
|
2680
2243
|
if (
|
|
2681
2244
|
celRuleApplies &&
|
|
2682
2245
|
engineName === "cel" &&
|
|
2683
|
-
|
|
2246
|
+
celScope.invocationContextSchema === undefined &&
|
|
2684
2247
|
!evalPathsCover(celEvalPaths, path) &&
|
|
2685
2248
|
!celRegionScopes.some((scope) => pathMatchesScope(path, scope)) &&
|
|
2686
2249
|
!pathCrossesNestedResource(m, path)
|
|
@@ -2733,70 +2296,6 @@ export class StaticAnalyzer {
|
|
|
2733
2296
|
}
|
|
2734
2297
|
}
|
|
2735
2298
|
|
|
2736
|
-
let matchedContext: Record<string, any> | undefined =
|
|
2737
|
-
e.contextSchema ?? celInvocationContext;
|
|
2738
|
-
|
|
2739
|
-
if (celStepContextSchema) {
|
|
2740
|
-
const base =
|
|
2741
|
-
matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
2742
|
-
matchedContext = {
|
|
2743
|
-
...base,
|
|
2744
|
-
properties: {
|
|
2745
|
-
...(base.properties ?? {}),
|
|
2746
|
-
steps: celStepContextSchema,
|
|
2747
|
-
},
|
|
2748
|
-
};
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
2752
|
-
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
2753
|
-
const errorSchema =
|
|
2754
|
-
celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
|
|
2755
|
-
if (errorSchema) {
|
|
2756
|
-
const base =
|
|
2757
|
-
matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
2758
|
-
matchedContext = {
|
|
2759
|
-
...base,
|
|
2760
|
-
properties: {
|
|
2761
|
-
...(base.properties ?? {}),
|
|
2762
|
-
error: errorSchema,
|
|
2763
|
-
},
|
|
2764
|
-
};
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
let effectiveContext: Record<string, any> | null = null;
|
|
2768
|
-
if (matchedContext) {
|
|
2769
|
-
const manifestItem = matchedScope
|
|
2770
|
-
? getManifestItem(path, matchedScope, m as Record<string, any>)
|
|
2771
|
-
: (m as Record<string, any>);
|
|
2772
|
-
const rootForResolver = manifestRootForResolver(
|
|
2773
|
-
m as Record<string, any>,
|
|
2774
|
-
defs,
|
|
2775
|
-
aliases,
|
|
2776
|
-
allManifests as Record<string, any>[],
|
|
2777
|
-
{ aliasesByModule, rootModules },
|
|
2778
|
-
);
|
|
2779
|
-
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
2780
|
-
manifestRoot: rootForResolver,
|
|
2781
|
-
defs,
|
|
2782
|
-
aliases,
|
|
2783
|
-
allManifests: allManifests as Record<string, any>[],
|
|
2784
|
-
});
|
|
2785
|
-
effectiveContext = mergeKernelGlobalsIntoContext(
|
|
2786
|
-
withBindingNames(resolvedContext, m as Record<string, any>),
|
|
2787
|
-
// Typed in the module that DECLARED this resource — for a manifest
|
|
2788
|
-
// forwarded from an imported library, that is its `moduleGlobals`
|
|
2789
|
-
// stamp, not the consuming application's block.
|
|
2790
|
-
kernelGlobals.forResource(m),
|
|
2791
|
-
);
|
|
2792
|
-
} else if (observedStateContext) {
|
|
2793
|
-
// No `x-telo-context` matched, so nothing was chain-validated here
|
|
2794
|
-
// before. Validate the observed-state segment alone rather than
|
|
2795
|
-
// merging the kernel globals, whose closed `variables` / `ports`
|
|
2796
|
-
// nodes would newly reject reads that pass today.
|
|
2797
|
-
effectiveContext = observedStateContext;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
2299
|
const engine = defaultRegistry().get(engineName);
|
|
2801
2300
|
if (!engine) {
|
|
2802
2301
|
// No registered engine owns this tag — the expression would go
|
|
@@ -2811,38 +2310,15 @@ export class StaticAnalyzer {
|
|
|
2811
2310
|
return;
|
|
2812
2311
|
}
|
|
2813
2312
|
// The engine type-checks, so it gets the environment typed for THIS
|
|
2814
|
-
// path — not the bare base one.
|
|
2815
|
-
//
|
|
2816
|
-
//
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
// A matched context makes the environment path-specific (its schema is
|
|
2824
|
-
// resolved against the enclosing array item), so those still build
|
|
2825
|
-
// fresh rather than risk one item's types leaking into another's.
|
|
2826
|
-
const cached = effectiveContext === null ? typedEnvByManifest.get(m) : undefined;
|
|
2827
|
-
const typedEnv =
|
|
2828
|
-
cached ??
|
|
2829
|
-
(m.kind === "Telo.Import"
|
|
2830
|
-
? buildImportInputCelEnvironment(
|
|
2831
|
-
this.celEnv,
|
|
2832
|
-
allManifests.find(
|
|
2833
|
-
(mm) =>
|
|
2834
|
-
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
2835
|
-
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
2836
|
-
(m.metadata as { module?: string } | undefined)?.module,
|
|
2837
|
-
),
|
|
2838
|
-
)
|
|
2839
|
-
: buildTypedCelEnvironment(
|
|
2840
|
-
this.celEnv,
|
|
2841
|
-
m,
|
|
2842
|
-
effectiveContext ?? undefined,
|
|
2843
|
-
moduleManifest,
|
|
2844
|
-
));
|
|
2845
|
-
if (effectiveContext === null && !cached) typedEnvByManifest.set(m, typedEnv);
|
|
2313
|
+
// path — not the bare base one. Both halves come from the one scope
|
|
2314
|
+
// rule, which is what makes the IDE's answer and this check the same
|
|
2315
|
+
// answer rather than two that agree today.
|
|
2316
|
+
const { env: typedEnv, contextSchema: effectiveContext } = celScope.scopeFor({
|
|
2317
|
+
source: m,
|
|
2318
|
+
path,
|
|
2319
|
+
contextSchema: e.contextSchema,
|
|
2320
|
+
matchedScope,
|
|
2321
|
+
});
|
|
2846
2322
|
|
|
2847
2323
|
const result = engine.analyze(expr, { celEnv: typedEnv, contextSchema: effectiveContext });
|
|
2848
2324
|
|