@telorun/analyzer 0.65.0 → 0.67.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/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +194 -65
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +7 -1
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -19
- package/dist/cel-scope.d.ts +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +89 -11
- package/dist/definition-registry.d.ts +41 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +76 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- package/dist/eval-paths.d.ts +68 -0
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +138 -0
- package/dist/flatten-for-analyzer.d.ts +37 -1
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +104 -5
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +36 -1
- package/dist/migrations/entries/index.d.ts.map +1 -1
- package/dist/migrations/entries/index.js +2 -0
- package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
- package/dist/peer-binding.d.ts +232 -0
- package/dist/peer-binding.d.ts.map +1 -0
- package/dist/peer-binding.js +418 -0
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -0
- package/dist/ref-slot.d.ts +13 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +26 -0
- package/dist/reference-field-map.d.ts +28 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +25 -0
- package/dist/referrer-rule.d.ts +17 -0
- package/dist/referrer-rule.d.ts.map +1 -1
- package/dist/referrer-rule.js +4 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +14 -1
- package/dist/resource-input.d.ts +75 -0
- package/dist/resource-input.d.ts.map +1 -0
- package/dist/resource-input.js +90 -0
- package/dist/resource-rule.d.ts +40 -27
- package/dist/resource-rule.d.ts.map +1 -1
- package/dist/resource-rule.js +61 -7
- package/dist/rule-condition.d.ts +11 -0
- package/dist/rule-condition.d.ts.map +1 -1
- package/dist/rule-condition.js +13 -0
- package/dist/schema-projection.d.ts +119 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +213 -18
- package/dist/system-kinds.d.ts +7 -2
- package/dist/system-kinds.d.ts.map +1 -1
- package/dist/system-kinds.js +7 -2
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +50 -0
- package/dist/template-body.d.ts.map +1 -0
- package/dist/template-body.js +58 -0
- package/dist/validate-cel-context.d.ts +1 -17
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +73 -78
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- package/dist/validate-reference-forms.d.ts +10 -0
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +23 -0
- package/dist/validate-references.d.ts +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +77 -18
- package/dist/validate-referrer-rules.d.ts +38 -4
- package/dist/validate-referrer-rules.d.ts.map +1 -1
- package/dist/validate-referrer-rules.js +139 -31
- package/dist/validate-resource-inputs.d.ts +35 -0
- package/dist/validate-resource-inputs.d.ts.map +1 -0
- package/dist/validate-resource-inputs.js +319 -0
- package/dist/validate-resource-rules.d.ts +11 -2
- package/dist/validate-resource-rules.d.ts.map +1 -1
- package/dist/validate-resource-rules.js +18 -12
- package/dist/validate-schema-projection.d.ts.map +1 -1
- package/dist/validate-schema-projection.js +33 -2
- package/dist/validate-template-dispatch.d.ts +27 -0
- package/dist/validate-template-dispatch.d.ts.map +1 -0
- package/dist/validate-template-dispatch.js +95 -0
- package/dist/with-synthetic-positions.d.ts +1 -1
- package/dist/with-synthetic-positions.js +1 -1
- package/package.json +3 -3
- package/src/analyzer.ts +236 -70
- package/src/builtins.ts +69 -16
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +113 -19
- package/src/definition-registry.ts +78 -0
- package/src/dependency-graph.ts +66 -0
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +117 -4
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +57 -4
- package/src/inline-imports.ts +1 -0
- package/src/manifest-visitor.ts +41 -1
- package/src/migrations/entries/index.ts +2 -0
- package/src/migrations/entries/schema-prepare-bucket.json +27 -0
- package/src/peer-binding.ts +641 -0
- package/src/precompile.ts +8 -0
- package/src/ref-slot.ts +36 -0
- package/src/reference-field-map.ts +42 -0
- package/src/referrer-rule.ts +20 -1
- package/src/release/index.ts +1 -1
- package/src/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-projection.ts +302 -20
- package/src/system-kinds.ts +7 -2
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +104 -0
- package/src/validate-cel-context.ts +73 -78
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +85 -16
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-template-dispatch.ts +99 -0
- package/src/with-synthetic-positions.ts +1 -1
package/src/call-graph.ts
CHANGED
|
@@ -284,7 +284,13 @@ function resolveUseAtSite(
|
|
|
284
284
|
const resolved = entry.useCases.cases[String(selector)];
|
|
285
285
|
if (resolved) return { use: resolved };
|
|
286
286
|
}
|
|
287
|
-
const slot = {
|
|
287
|
+
const slot = {
|
|
288
|
+
kinds: entry.refs,
|
|
289
|
+
uses: entry.uses,
|
|
290
|
+
useCases: entry.useCases,
|
|
291
|
+
inline: false,
|
|
292
|
+
valueBranches: [],
|
|
293
|
+
};
|
|
288
294
|
const unresolvedReason = isTaggedSentinel(selector)
|
|
289
295
|
? "dynamic"
|
|
290
296
|
: selector === undefined
|
package/src/cel-environment.ts
CHANGED
|
@@ -47,24 +47,34 @@ export function buildTypedCelEnvironment(
|
|
|
47
47
|
(env as any).registerType(brand, { fields: {} });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
// `variables` / `secrets`: the DECLARING module's blocks, which is the
|
|
51
|
+
// contract the resource's CEL is evaluated against at runtime. Read the
|
|
52
|
+
// same three ways `ports` and `module` are, and for the same reason — a
|
|
53
|
+
// resource doc does not carry them, so typing from `manifest` alone left
|
|
54
|
+
// every ordinary resource with an open `variables` and no check at all,
|
|
55
|
+
// while `ports.<typo>` one line away was an error.
|
|
56
|
+
//
|
|
57
|
+
// Order matters: a module-identity doc analyzing itself carries its own
|
|
58
|
+
// block; a resource forwarded from an imported library carries its
|
|
59
|
+
// library's as `metadata.moduleGlobals`, which must win over the consuming
|
|
60
|
+
// application's; everything else is the entry module's own doc.
|
|
61
|
+
const moduleGlobals = (manifest.metadata as Record<string, any> | undefined)?.moduleGlobals as
|
|
62
|
+
| Record<string, unknown>
|
|
63
|
+
| undefined;
|
|
64
|
+
// A KIND document is the exception, and it is not a detail: the CEL inside
|
|
65
|
+
// a `Telo.Definition`'s `schema:` — an `examples:` entry, a `description`
|
|
66
|
+
// showing `${{ secrets.API_KEY }}` — illustrates what a CONSUMER writes, in
|
|
67
|
+
// the consumer's scope. Closing those over the declaring module's blocks
|
|
68
|
+
// reported an error against a name the module never meant to declare, and
|
|
69
|
+
// one nobody could fix without deleting the example.
|
|
70
|
+
const root = (
|
|
71
|
+
isKindDocument(manifest) ? undefined : (rootModuleManifest as Record<string, unknown>)
|
|
72
|
+
) as Record<string, unknown> | undefined;
|
|
73
|
+
registerConfigNamespace(
|
|
74
|
+
env,
|
|
75
|
+
(manifest as Record<string, unknown>).variables ?? moduleGlobals?.variables ?? root?.variables,
|
|
76
|
+
"variables",
|
|
77
|
+
);
|
|
68
78
|
|
|
69
79
|
// `ports` namespace: each entry types as the brand its `protocol` selects
|
|
70
80
|
// (tcp → TcpPort, udp → UdpPort), so `${{ ports.http }}` carries a nominal
|
|
@@ -87,7 +97,11 @@ export function buildTypedCelEnvironment(
|
|
|
87
97
|
env.registerVariable("ports", "map");
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
registerConfigNamespace(
|
|
101
|
+
env,
|
|
102
|
+
(manifest as Record<string, unknown>).secrets ?? moduleGlobals?.secrets ?? root?.secrets,
|
|
103
|
+
"secrets",
|
|
104
|
+
);
|
|
91
105
|
env.registerVariable("resources", "map");
|
|
92
106
|
|
|
93
107
|
// `module` — the declaring module's own `metadata`, so a manifest reads its
|
|
@@ -132,6 +146,21 @@ export function buildTypedCelEnvironment(
|
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
149
|
+
/**
|
|
150
|
+
* A kind document — whose CEL is written for whoever instantiates the kind, not
|
|
151
|
+
* evaluated in the declaring module's own scope.
|
|
152
|
+
*
|
|
153
|
+
* Its `examples:` show a consumer's route reading `request` and `result`, its
|
|
154
|
+
* `description`s show `${{ secrets.API_KEY }}`, and a rule condition reads the
|
|
155
|
+
* `self` / `referrer` its own evaluator binds. None of those names are in scope
|
|
156
|
+
* where they are WRITTEN, and all of them are correct where they are READ — so
|
|
157
|
+
* every check that asks "is this name in scope here" has to stand down on these
|
|
158
|
+
* documents, or it reports errors nobody can fix without deleting the example.
|
|
159
|
+
*/
|
|
160
|
+
export function isKindDocument(manifest: ResourceManifest): boolean {
|
|
161
|
+
return manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract";
|
|
162
|
+
}
|
|
163
|
+
|
|
135
164
|
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
136
165
|
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
137
166
|
function registerConfigNamespace(
|
package/src/cel-scope.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "./kernel-globals.js";
|
|
43
43
|
import { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
|
|
44
44
|
import { readStepSlot } from "./step-slot.js";
|
|
45
|
+
import { bodyForPath, templateBodies } from "./template-body.js";
|
|
45
46
|
import {
|
|
46
47
|
getManifestItem,
|
|
47
48
|
resolveContextAnnotations,
|
|
@@ -415,6 +416,20 @@ export class CelScopeResolver {
|
|
|
415
416
|
private stepContext: Record<string, any> | undefined;
|
|
416
417
|
private invocationContext: Record<string, any> | undefined;
|
|
417
418
|
private errorScopes: Map<string, Record<string, any>> = new Map();
|
|
419
|
+
/** The same two facts per `resources:` entry of a `Telo.Definition` — a
|
|
420
|
+
* template body is a declaration of ANOTHER kind, so its step body and its
|
|
421
|
+
* error branches are that kind's, not the enclosing definition's. */
|
|
422
|
+
private bodyScopes: Array<{
|
|
423
|
+
prefix: string;
|
|
424
|
+
scopePrefix: string;
|
|
425
|
+
manifest: Record<string, any>;
|
|
426
|
+
stepContext: Record<string, any> | undefined;
|
|
427
|
+
errorScopes: Map<string, Record<string, any>>;
|
|
428
|
+
}> = [];
|
|
429
|
+
/** The enclosing definition's `self` schema, resolved once per resource. A
|
|
430
|
+
* template body's contexts resolve against the BODY, so `self` — the one
|
|
431
|
+
* binding anchored on the definition — is substituted before they do. */
|
|
432
|
+
private selfSchema: Record<string, any> | undefined;
|
|
418
433
|
|
|
419
434
|
constructor(private readonly inputs: CelScopeInputs) {}
|
|
420
435
|
|
|
@@ -445,19 +460,44 @@ export class CelScopeResolver {
|
|
|
445
460
|
this.invocationContext = (m.metadata as any)?.xTeloInvocationContext as
|
|
446
461
|
| Record<string, any>
|
|
447
462
|
| undefined;
|
|
448
|
-
|
|
463
|
+
// The INHERITANCE-RESOLVED schema at both sites, for the reason the CEL
|
|
464
|
+
// context regions use it: an `extends` child is authored against
|
|
465
|
+
// merge(parent, own), so a step body or a `catch:` region the parent
|
|
466
|
+
// declares is in force on the child. All three consumers share one memo so
|
|
467
|
+
// they cannot come to disagree about which schema a kind means.
|
|
468
|
+
const authorSchema = defs.effectiveSchemaOf(definition) as Record<string, any> | undefined;
|
|
469
|
+
this.stepContext = authorSchema
|
|
449
470
|
? buildStepContextSchema(
|
|
450
471
|
m as Record<string, any>,
|
|
451
|
-
|
|
472
|
+
authorSchema,
|
|
452
473
|
allManifests as Record<string, any>[],
|
|
453
474
|
defs,
|
|
454
475
|
aliases,
|
|
455
476
|
scopes,
|
|
456
477
|
)
|
|
457
478
|
: undefined;
|
|
458
|
-
this.errorScopes = collectErrorContextScopes(
|
|
459
|
-
|
|
460
|
-
|
|
479
|
+
this.errorScopes = collectErrorContextScopes(authorSchema);
|
|
480
|
+
this.selfSchema =
|
|
481
|
+
m.kind === "Telo.Definition" ? buildSelfSchema(m as Record<string, any>, defs, aliases) : undefined;
|
|
482
|
+
this.bodyScopes = templateBodies(m, defs, aliases, scopes).map((body) => {
|
|
483
|
+
const bodySchema = defs.effectiveSchemaOf(body.definition) as Record<string, any> | undefined;
|
|
484
|
+
return {
|
|
485
|
+
prefix: body.prefix,
|
|
486
|
+
scopePrefix: body.scopePrefix,
|
|
487
|
+
manifest: body.manifest as Record<string, any>,
|
|
488
|
+
stepContext: bodySchema
|
|
489
|
+
? buildStepContextSchema(
|
|
490
|
+
body.manifest as Record<string, any>,
|
|
491
|
+
bodySchema,
|
|
492
|
+
allManifests as Record<string, any>[],
|
|
493
|
+
defs,
|
|
494
|
+
aliases,
|
|
495
|
+
scopes,
|
|
496
|
+
)
|
|
497
|
+
: undefined,
|
|
498
|
+
errorScopes: collectErrorContextScopes(bodySchema),
|
|
499
|
+
};
|
|
500
|
+
});
|
|
461
501
|
}
|
|
462
502
|
|
|
463
503
|
/**
|
|
@@ -514,18 +554,39 @@ export class CelScopeResolver {
|
|
|
514
554
|
const m = site.source;
|
|
515
555
|
let matched: Record<string, any> | undefined = site.contextSchema ?? this.invocationContext;
|
|
516
556
|
|
|
517
|
-
|
|
557
|
+
// Inside a template body the step and error regions are the NESTED kind's.
|
|
558
|
+
// Its `steps` accumulator and its `catch:` branches are declared there, and
|
|
559
|
+
// the enclosing definition's (there are none) would say nothing about them.
|
|
560
|
+
const inBody = this.bodyScopes.length > 0 ? bodyForPath(this.bodyScopes, path) : undefined;
|
|
561
|
+
const stepContext = inBody ? inBody.stepContext : this.stepContext;
|
|
562
|
+
const errorScopes = inBody ? inBody.errorScopes : this.errorScopes;
|
|
563
|
+
|
|
564
|
+
if (stepContext) {
|
|
518
565
|
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
519
566
|
matched = {
|
|
520
567
|
...base,
|
|
521
|
-
properties: {
|
|
568
|
+
properties: {
|
|
569
|
+
// `inputs` is in scope beside `steps` wherever a step body runs — the
|
|
570
|
+
// step engine evaluates a step against the enclosing kind's own
|
|
571
|
+
// arguments, which is what `steps[0].inputs.x` reads. Modelled here
|
|
572
|
+
// because it was modelled NOWHERE: the step context is open, so a
|
|
573
|
+
// chain through it was never checked and the name's absence went
|
|
574
|
+
// unnoticed until something asked whether the root existed.
|
|
575
|
+
//
|
|
576
|
+
// OPEN, not typed from the kind's `inputType`: closing it would newly
|
|
577
|
+
// reject every read of an argument the contract does not spell out,
|
|
578
|
+
// which is a separate decision from knowing the name is legal.
|
|
579
|
+
inputs: { type: "object", additionalProperties: true },
|
|
580
|
+
...(base.properties ?? {}),
|
|
581
|
+
steps: stepContext,
|
|
582
|
+
},
|
|
522
583
|
};
|
|
523
584
|
}
|
|
524
585
|
|
|
525
586
|
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
526
587
|
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
527
588
|
const errorSchema =
|
|
528
|
-
|
|
589
|
+
errorScopes.size > 0 ? errorContextForPath(path, errorScopes) : undefined;
|
|
529
590
|
if (errorSchema) {
|
|
530
591
|
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
531
592
|
matched = {
|
|
@@ -543,16 +604,49 @@ export class CelScopeResolver {
|
|
|
543
604
|
}
|
|
544
605
|
|
|
545
606
|
const { defs, aliases, scopes, allManifests, kernelGlobals } = this.inputs;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
607
|
+
|
|
608
|
+
// A template body's context annotations are the NESTED kind's, and every one
|
|
609
|
+
// that anchors at a root — `x-telo-context-element-from`,
|
|
610
|
+
// `-collection-from`, `-from-root`, `x-telo-bindings-from` — means the root
|
|
611
|
+
// of the DECLARATION they were written for. Resolving them against the
|
|
612
|
+
// enclosing `Telo.Definition` looked `collection:` up on a document that has
|
|
613
|
+
// no such field, so `item` typed open and a typo below it went unreported —
|
|
614
|
+
// the one place a nested declaration did not answer as the same declaration
|
|
615
|
+
// written at the top level.
|
|
616
|
+
//
|
|
617
|
+
// So the path and the scope are rebased into the body and the body becomes
|
|
618
|
+
// the resolution root. `self` is the single binding that genuinely belongs to
|
|
619
|
+
// the enclosing definition, and it is substituted below rather than left as
|
|
620
|
+
// an annotation the rebased root would misread.
|
|
621
|
+
const body = inBody;
|
|
622
|
+
const localPath = body ? path.slice(body.prefix.length + 1) : path;
|
|
623
|
+
const localScope =
|
|
624
|
+
body && site.matchedScope?.startsWith(`${body.scopePrefix}.`)
|
|
625
|
+
? `$.${site.matchedScope.slice(body.scopePrefix.length + 1)}`
|
|
626
|
+
: body
|
|
627
|
+
? undefined
|
|
628
|
+
: site.matchedScope;
|
|
629
|
+
const rootManifest = body ? body.manifest : (m as Record<string, any>);
|
|
630
|
+
|
|
631
|
+
if (body && this.selfSchema && matched.properties?.self) {
|
|
632
|
+
matched = {
|
|
633
|
+
...matched,
|
|
634
|
+
properties: { ...matched.properties, self: this.selfSchema },
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const manifestItem = localScope
|
|
639
|
+
? getManifestItem(localPath, localScope, rootManifest)
|
|
640
|
+
: rootManifest;
|
|
641
|
+
const rootForResolver = body
|
|
642
|
+
? rootManifest
|
|
643
|
+
: manifestRootForResolver(
|
|
644
|
+
m as Record<string, any>,
|
|
645
|
+
defs,
|
|
646
|
+
aliases,
|
|
647
|
+
allManifests as Record<string, any>[],
|
|
648
|
+
scopes,
|
|
649
|
+
);
|
|
556
650
|
const resolved = resolveContextAnnotations(matched, manifestItem, {
|
|
557
651
|
manifestRoot: rootForResolver,
|
|
558
652
|
defs,
|
|
@@ -560,7 +654,7 @@ export class CelScopeResolver {
|
|
|
560
654
|
allManifests: allManifests as Record<string, any>[],
|
|
561
655
|
});
|
|
562
656
|
return mergeKernelGlobalsIntoContext(
|
|
563
|
-
withBindingNames(resolved,
|
|
657
|
+
withBindingNames(resolved, rootManifest),
|
|
564
658
|
// Typed in the module that DECLARED this resource — for a manifest
|
|
565
659
|
// forwarded from an imported library, that is its `moduleGlobals` stamp,
|
|
566
660
|
// not the consuming application's block.
|
|
@@ -41,6 +41,9 @@ export class DefinitionRegistry {
|
|
|
41
41
|
private readonly fieldMaps = new Map<string, ReferenceFieldMap>();
|
|
42
42
|
/** Reverse inheritance index: parent kind → direct child kinds. */
|
|
43
43
|
private readonly extendedBy = new Map<string, string[]>();
|
|
44
|
+
/** Memoized inheritance-resolved schemas. `null` records "resolved to
|
|
45
|
+
* nothing", so a kind with no schema is not re-walked on every resource. */
|
|
46
|
+
private readonly effectiveSchemas = new Map<string, Record<string, unknown> | null>();
|
|
44
47
|
/** DEPRECATED module identity table: identity string → canonical module name
|
|
45
48
|
* ("std/pipeline" → "pipeline"). Serves only the legacy
|
|
46
49
|
* `<namespace>/<module>#<Kind>` form of `x-telo-ref`, kept resolvable for
|
|
@@ -57,6 +60,7 @@ export class DefinitionRegistry {
|
|
|
57
60
|
// so any already-computed map recomputes against the now-larger def set; the
|
|
58
61
|
// maps rebuild lazily on first `getFieldMap` (after all defs are registered).
|
|
59
62
|
this.fieldMaps.clear();
|
|
63
|
+
this.effectiveSchemas.clear();
|
|
60
64
|
// `capability` populates extendedBy for backward-compat with the legacy pattern where
|
|
61
65
|
// a concrete definition overloaded `capability: <AbstractKind>` to mean "implements
|
|
62
66
|
// this abstract." The canonical pattern is `extends` (below). Both populate the index,
|
|
@@ -285,6 +289,44 @@ export class DefinitionRegistry {
|
|
|
285
289
|
return this.defs.get(kind);
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
/**
|
|
293
|
+
* The kind's AUTHOR-FACING schema — its own merged with everything it
|
|
294
|
+
* inherits along `extends` — memoized per kind.
|
|
295
|
+
*
|
|
296
|
+
* This is what every consumer reading a kind's schema to interpret an
|
|
297
|
+
* INSTANCE must use, because that is the schema the instance was authored
|
|
298
|
+
* against and the one the kernel stamps at definition registration. Reading
|
|
299
|
+
* `resolve(kind).schema` instead sees only what the leaf declared, so an
|
|
300
|
+
* annotation on a parent — a CEL context region, a step body, an error
|
|
301
|
+
* context — is invisible on every child, silently and per consumer. Sharing
|
|
302
|
+
* one memo is what keeps those consumers from drifting apart.
|
|
303
|
+
*
|
|
304
|
+
* Lazy for the same reason `getFieldMap` is: a child registered before its
|
|
305
|
+
* parent still sees the inherited half once both are present.
|
|
306
|
+
*/
|
|
307
|
+
effectiveSchema(kind: string): Record<string, unknown> | undefined {
|
|
308
|
+
const cached = this.effectiveSchemas.get(kind);
|
|
309
|
+
if (cached !== undefined) return cached ?? undefined;
|
|
310
|
+
const def = this.defs.get(kind);
|
|
311
|
+
if (!def) return undefined;
|
|
312
|
+
const schema = effectiveAuthorSchema(def, (k) => this.resolve(k)) as
|
|
313
|
+
| Record<string, unknown>
|
|
314
|
+
| undefined;
|
|
315
|
+
this.effectiveSchemas.set(kind, schema ?? null);
|
|
316
|
+
return schema;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The effective schema for a definition, by the kind it is registered under.
|
|
320
|
+
* Falls back to the definition's own schema when it is not in this registry
|
|
321
|
+
* (a synthesized or not-yet-registered kind), which is the pre-inheritance
|
|
322
|
+
* behaviour and the safe direction: it types less, never wrongly. */
|
|
323
|
+
effectiveSchemaOf(definition: ResourceDefinition | undefined): Record<string, unknown> | undefined {
|
|
324
|
+
if (!definition) return undefined;
|
|
325
|
+
const { name, module: mod } = definition.metadata;
|
|
326
|
+
const resolved = this.effectiveSchema(mod ? `${mod}.${name}` : name);
|
|
327
|
+
return resolved ?? (definition.schema as Record<string, unknown> | undefined);
|
|
328
|
+
}
|
|
329
|
+
|
|
288
330
|
/** Returns the reference field map for the given kind, computed lazily from the
|
|
289
331
|
* kind's AUTHOR-FACING (inheritance-resolved) schema and memoized. Lazy so a
|
|
290
332
|
* child registered before its parent still sees the parent's inherited ref
|
|
@@ -407,6 +449,42 @@ export class DefinitionRegistry {
|
|
|
407
449
|
return buildFieldMapAtPath(subSchema, fieldPath);
|
|
408
450
|
}
|
|
409
451
|
|
|
452
|
+
/** The kinds a definition descends from DIRECTLY — the same two edges
|
|
453
|
+
* `register` feeds into `extendedBy`, read the other way.
|
|
454
|
+
*
|
|
455
|
+
* Both spellings, because either can carry the edge that satisfies a slot:
|
|
456
|
+
* `capability:` is the legacy implements-this form, `extends:` the canonical
|
|
457
|
+
* one. A consumer walking these by hand would be a second reading of what an
|
|
458
|
+
* inheritance edge IS, and the downward index and the upward walk have to
|
|
459
|
+
* agree about that forever. */
|
|
460
|
+
parentsOf(kind: string): string[] {
|
|
461
|
+
const def = this.defs.get(kind);
|
|
462
|
+
if (!def) return [];
|
|
463
|
+
return [def.capability, def.extends].filter(
|
|
464
|
+
(parent): parent is string => typeof parent === "string" && parent.length > 0,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Whether every kind this one descends from is registered.
|
|
469
|
+
*
|
|
470
|
+
* When it is, what the kind implements is FULLY KNOWN: a target it does not
|
|
471
|
+
* reach is one it genuinely does not implement, so a mismatch is a verdict.
|
|
472
|
+
* When a hop is missing — an unimported abstract, an alias the declaring file
|
|
473
|
+
* could not resolve — a mismatch cannot be told from a missing dependency,
|
|
474
|
+
* which is the partial context every reference check stays lenient in. */
|
|
475
|
+
ancestryResolved(kind: string): boolean {
|
|
476
|
+
const queue = [kind];
|
|
477
|
+
const seen = new Set<string>();
|
|
478
|
+
while (queue.length > 0) {
|
|
479
|
+
const current = queue.shift()!;
|
|
480
|
+
if (seen.has(current)) continue;
|
|
481
|
+
seen.add(current);
|
|
482
|
+
if (!this.defs.has(current)) return false;
|
|
483
|
+
queue.push(...this.parentsOf(current));
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
410
488
|
/** Returns all definitions that transitively extend the given abstract kind.
|
|
411
489
|
* Follows the capability chain to any depth (equivalent to instanceof in OOP).
|
|
412
490
|
* Definitions are included regardless of registration order. */
|
package/src/dependency-graph.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import { buildCallGraph, projectToPairs, type ResourceGraphNode } from "./call-graph.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { readSuppliedResources } from "./resource-input.js";
|
|
5
6
|
|
|
6
7
|
export interface ResourceNode {
|
|
7
8
|
kind: string;
|
|
@@ -67,6 +68,8 @@ export function buildDependencyGraph(
|
|
|
67
68
|
}
|
|
68
69
|
for (const key of nodes.keys()) if (!deps.has(key)) deps.set(key, new Set());
|
|
69
70
|
|
|
71
|
+
addResourceInputEdges(resources, nodes, deps);
|
|
72
|
+
|
|
70
73
|
// --- Kahn's topological sort ---
|
|
71
74
|
// in-degree[X] = number of X's dependencies (size of deps[X])
|
|
72
75
|
// reverse[dep] = set of nodes that depend on dep (for degree decrement)
|
|
@@ -164,3 +167,66 @@ function findCycle(
|
|
|
164
167
|
|
|
165
168
|
return [];
|
|
166
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Boot-order edges for the one thing a `Telo.Import` does hold: the instances it
|
|
173
|
+
* hands DOWN to its target library's declared `resources:` inputs.
|
|
174
|
+
*
|
|
175
|
+
* An import is otherwise module wiring rather than a runtime node — it is in
|
|
176
|
+
* `DEPENDENCY_GRAPH_SKIP_KINDS` and the call graph gives it no node at all — but
|
|
177
|
+
* a borrowed instance must exist before the import initializes, and a cycle
|
|
178
|
+
* through one is a cycle like any other. The edges are added here rather than
|
|
179
|
+
* read off the reference field map because the accepted KIND at this slot is
|
|
180
|
+
* declared by the TARGET library, not by the `Telo.Import` schema, so there is
|
|
181
|
+
* no `x-telo-ref` for the map to read; the constraint itself is checked by
|
|
182
|
+
* `validate-resource-inputs`.
|
|
183
|
+
*/
|
|
184
|
+
function addResourceInputEdges(
|
|
185
|
+
resources: ResourceManifest[],
|
|
186
|
+
nodes: Map<string, ResourceNode>,
|
|
187
|
+
deps: Map<string, Set<string>>,
|
|
188
|
+
): void {
|
|
189
|
+
// Every import that supplies inputs becomes a node FIRST, so a cross-module
|
|
190
|
+
// reference below can resolve to the import that exports its target.
|
|
191
|
+
const imports: Array<{ key: string; supplied: Record<string, unknown> }> = [];
|
|
192
|
+
for (const m of resources) {
|
|
193
|
+
if (m.kind !== "Telo.Import") continue;
|
|
194
|
+
const alias = m.metadata?.name as string | undefined;
|
|
195
|
+
const supplied = readSuppliedResources(m);
|
|
196
|
+
if (!alias || Object.keys(supplied).length === 0) continue;
|
|
197
|
+
const key = nodeKey(m.kind, alias);
|
|
198
|
+
nodes.set(key, { kind: m.kind, name: alias });
|
|
199
|
+
if (!deps.has(key)) deps.set(key, new Set<string>());
|
|
200
|
+
imports.push({ key, supplied });
|
|
201
|
+
}
|
|
202
|
+
if (imports.length === 0) return;
|
|
203
|
+
|
|
204
|
+
const byName = new Map<string, string>();
|
|
205
|
+
for (const [key, node] of nodes) byName.set(node.name, key);
|
|
206
|
+
// An import is keyed by its alias, and that is also how a cross-module
|
|
207
|
+
// reference names it — index those so one resolves.
|
|
208
|
+
for (const m of resources) {
|
|
209
|
+
if (m.kind !== "Telo.Import") continue;
|
|
210
|
+
const alias = m.metadata?.name as string | undefined;
|
|
211
|
+
const key = alias ? nodeKey(m.kind, alias) : undefined;
|
|
212
|
+
if (alias && key && nodes.has(key)) byName.set(alias, key);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const { key, supplied } of imports) {
|
|
216
|
+
const set = deps.get(key)!;
|
|
217
|
+
for (const value of Object.values(supplied)) {
|
|
218
|
+
const ref = value as { name?: unknown; alias?: unknown } | undefined;
|
|
219
|
+
// A CROSS-MODULE reference (`!ref Other.db`) names an instance exported by
|
|
220
|
+
// another import, never a local resource. Looking it up in the local name
|
|
221
|
+
// map would find an unrelated resource of the same name — a wrong edge,
|
|
222
|
+
// and possibly a phantom cycle — or nothing at all. What it depends on is
|
|
223
|
+
// the IMPORT that exports it, which is the projection
|
|
224
|
+
// `localDependencyNames` already makes at runtime.
|
|
225
|
+
const alias = typeof ref?.alias === "string" ? ref.alias : undefined;
|
|
226
|
+
const targetName =
|
|
227
|
+
alias && alias !== "Self" ? alias : typeof ref?.name === "string" ? ref.name : undefined;
|
|
228
|
+
const target = targetName ? byName.get(targetName) : undefined;
|
|
229
|
+
if (target && target !== key) set.add(target);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/eval-paths.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE IS CEL EVALUATED — the one reader, and the one decision built on it.
|
|
3
|
+
*
|
|
4
|
+
* Two independent ways a value is evaluated, and every consumer needs both:
|
|
5
|
+
* `x-telo-eval` names a field directly, while a REGION annotation
|
|
6
|
+
* (`x-telo-context` / `x-telo-step-context` / `x-telo-error-context`, or a step
|
|
7
|
+
* body) covers everything beneath it — which is how `Run.Choice`'s rows and an
|
|
8
|
+
* `Http.Api` route's `returns:` entries hold expressions while declaring no
|
|
9
|
+
* annotation of their own.
|
|
10
|
+
*
|
|
11
|
+
* The consumers are on both sides of the runtime: `telo check` decides whether a
|
|
12
|
+
* `!cel` here is live (`CEL_IN_NON_EVAL_FIELD`) or resolved at startup
|
|
13
|
+
* (`OBSERVED_STATE_IN_STARTUP_FIELD`), the kernel decides what to expand, and
|
|
14
|
+
* the editor decides whether to offer an expression at all. The editor's answer
|
|
15
|
+
* is a CLAIM that `telo check` will accept what it writes, so a second
|
|
16
|
+
* implementation there is a promise nothing keeps — it read `x-telo-eval` alone
|
|
17
|
+
* and left every predicate inside a region as a bare checkbox, with no way to
|
|
18
|
+
* write the expression the field exists to hold.
|
|
19
|
+
*
|
|
20
|
+
* Browser-safe: no Node built-ins.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { isStepSlot } from "./step-slot.js";
|
|
24
|
+
|
|
1
25
|
/**
|
|
2
26
|
* The single containment rule for `x-telo-eval` paths, shared by every matcher so
|
|
3
27
|
* the analyzer's coverage decision and the kernel's expansion/exclusion can't
|
|
@@ -66,3 +90,131 @@ function collectEvalPathsNode(
|
|
|
66
90
|
}
|
|
67
91
|
}
|
|
68
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Schema keys that declare a CEL-bearing region: a field carrying any of these
|
|
95
|
+
* is evaluated at runtime, so a `!cel` inside it (or a descendant) is live. A
|
|
96
|
+
* STEP BODY is one too, and says so through the grammar its items point at
|
|
97
|
+
* rather than through a key — {@link isStepSlot} reads either spelling. */
|
|
98
|
+
const CEL_REGION_KEYS = [
|
|
99
|
+
"x-telo-context",
|
|
100
|
+
"x-telo-step-context",
|
|
101
|
+
"x-telo-error-context",
|
|
102
|
+
] as const;
|
|
103
|
+
|
|
104
|
+
/** True when this schema node declares a CEL-bearing region. Node-level, so a
|
|
105
|
+
* consumer holding one schema (an editor rendering one field) asks it directly
|
|
106
|
+
* rather than deriving scopes it would then have to match against. */
|
|
107
|
+
export function declaresCelRegion(schema: unknown): boolean {
|
|
108
|
+
if (!schema || typeof schema !== "object") return false;
|
|
109
|
+
const node = schema as Record<string, unknown>;
|
|
110
|
+
return CEL_REGION_KEYS.some((key) => node[key] !== undefined) || isStepSlot(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Walk a JSON Schema tree and collect the JSONPath scopes of every field that
|
|
115
|
+
* declares a CEL-bearing region (`x-telo-context` / `x-telo-step-context` /
|
|
116
|
+
* `x-telo-error-context`). Used — alongside `x-telo-eval` paths — to decide
|
|
117
|
+
* whether a `!cel` expression sits in a slot the runtime actually evaluates.
|
|
118
|
+
* Scopes use the same `$.a.b[*]` form as `extractContextsFromSchema`, matched
|
|
119
|
+
* against expression paths with `pathMatchesScope`.
|
|
120
|
+
*/
|
|
121
|
+
export function extractCelRegionScopes(schema: Record<string, any>, path = "$"): string[] {
|
|
122
|
+
if (!schema || typeof schema !== "object") return [];
|
|
123
|
+
const out: string[] = [];
|
|
124
|
+
|
|
125
|
+
if (declaresCelRegion(schema)) out.push(path);
|
|
126
|
+
|
|
127
|
+
if (schema.properties) {
|
|
128
|
+
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
129
|
+
out.push(...extractCelRegionScopes(value, `${path}.${key}`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (schema.items && typeof schema.items === "object") {
|
|
133
|
+
out.push(...extractCelRegionScopes(schema.items, `${path}[*]`));
|
|
134
|
+
}
|
|
135
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
136
|
+
if (Array.isArray(schema[key])) {
|
|
137
|
+
for (const subschema of schema[key]) out.push(...extractCelRegionScopes(subschema, path));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
|
|
146
|
+
* falls within the scope of a context (e.g. "$.routes[*].inputs").
|
|
147
|
+
*
|
|
148
|
+
* The scope is matched directly (no sibling sharing): a context at "$.routes[*].inputs" only
|
|
149
|
+
* applies to expressions whose path starts with "routes[N].inputs", not to other sibling fields.
|
|
150
|
+
*/
|
|
151
|
+
export function pathMatchesScope(exprPath: string, scope: string): boolean {
|
|
152
|
+
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
153
|
+
if (!stripped) return false;
|
|
154
|
+
|
|
155
|
+
// Split on wildcard array segments; each [*] must match a concrete [N] in exprPath
|
|
156
|
+
const parts = stripped.split("[*]");
|
|
157
|
+
let remaining = exprPath;
|
|
158
|
+
for (let i = 0; i < parts.length; i++) {
|
|
159
|
+
const part = parts[i]!;
|
|
160
|
+
if (!remaining.startsWith(part)) return false;
|
|
161
|
+
remaining = remaining.slice(part.length);
|
|
162
|
+
if (i < parts.length - 1) {
|
|
163
|
+
// Expect a concrete array index like [0], [12], ...
|
|
164
|
+
const m = remaining.match(/^\[\d+\]/);
|
|
165
|
+
if (!m) return false;
|
|
166
|
+
remaining = remaining.slice(m[0].length);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Expression must end here or continue into a child path
|
|
170
|
+
return remaining === "" || remaining[0] === "." || remaining[0] === "[";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Every place a schema says its values are evaluated: the `x-telo-eval` paths,
|
|
174
|
+
* by mode, plus the scopes of the regions that cover their contents. */
|
|
175
|
+
export interface CelEvalSites {
|
|
176
|
+
compile: readonly string[];
|
|
177
|
+
runtime: readonly string[];
|
|
178
|
+
regions: readonly string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const NO_CEL_EVAL_SITES: CelEvalSites = { compile: [], runtime: [], regions: [] };
|
|
182
|
+
|
|
183
|
+
export function celEvalSites(schema: Record<string, any> | undefined): CelEvalSites {
|
|
184
|
+
if (!schema) return NO_CEL_EVAL_SITES;
|
|
185
|
+
const { compile, runtime } = buildEvalPaths(schema);
|
|
186
|
+
return { compile, runtime, regions: extractCelRegionScopes(schema) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The union of several schemas' sites — a kind's own and its capability
|
|
190
|
+
* abstract's, which is how a `Telo.Provider`'s implicit compile-eval reaches
|
|
191
|
+
* fields the provider never annotated. */
|
|
192
|
+
export function mergeCelEvalSites(...sites: CelEvalSites[]): CelEvalSites {
|
|
193
|
+
return {
|
|
194
|
+
compile: sites.flatMap((s) => s.compile),
|
|
195
|
+
runtime: sites.flatMap((s) => s.runtime),
|
|
196
|
+
regions: sites.flatMap((s) => s.regions),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether the value at `path` is evaluated, and when — null for a field whose
|
|
202
|
+
* value is read as a literal.
|
|
203
|
+
*
|
|
204
|
+
* `compile` wins over `runtime`, and both win over a region: a field's own
|
|
205
|
+
* annotation is more specific than the region it sits in, which is the same
|
|
206
|
+
* precedence a nested annotation has over an enclosing one. A region resolves to
|
|
207
|
+
* `runtime` because that is what a region IS — a per-invocation scope naming
|
|
208
|
+
* what its expressions can read.
|
|
209
|
+
*
|
|
210
|
+
* `path` is the `walkCelExpressions` spelling (`routes[0].returns[1].when`).
|
|
211
|
+
*/
|
|
212
|
+
export function celEvalModeAt(
|
|
213
|
+
sites: CelEvalSites,
|
|
214
|
+
path: string,
|
|
215
|
+
): "compile" | "runtime" | null {
|
|
216
|
+
if (evalPathsCover(sites.compile, path)) return "compile";
|
|
217
|
+
if (evalPathsCover(sites.runtime, path)) return "runtime";
|
|
218
|
+
if (sites.regions.some((scope) => pathMatchesScope(path, scope))) return "runtime";
|
|
219
|
+
return null;
|
|
220
|
+
}
|