@telorun/analyzer 0.66.0 → 0.68.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.map +1 -1
- package/dist/analyzer.js +60 -2
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- package/dist/cel-scope.d.ts +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +66 -8
- package/dist/definition-registry.d.ts +17 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +35 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- package/dist/extends-resolution.d.ts +20 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +29 -0
- package/dist/flatten-for-analyzer.d.ts +36 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +103 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/invocation-contract.d.ts +20 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +74 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +28 -0
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -0
- 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/schema-keywords.d.ts +16 -1
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +13 -0
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +7 -0
- 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.map +1 -1
- package/dist/validate-cel-context.js +68 -8
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +42 -1
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- package/dist/validate-references.d.ts +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +68 -16
- 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-sensitive-slots.d.ts +28 -0
- package/dist/validate-sensitive-slots.d.ts.map +1 -0
- package/dist/validate-sensitive-slots.js +75 -0
- 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/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +1 -0
- package/package.json +3 -3
- package/src/analyzer.ts +71 -2
- package/src/builtins.ts +69 -16
- package/src/cel-scope.ts +90 -14
- package/src/definition-registry.ts +36 -0
- package/src/dependency-graph.ts +66 -0
- package/src/extends-resolution.ts +33 -0
- package/src/flatten-for-analyzer.ts +116 -3
- package/src/index.ts +16 -0
- package/src/inline-imports.ts +1 -0
- package/src/invocation-contract.ts +71 -0
- package/src/manifest-visitor.ts +33 -0
- package/src/precompile.ts +8 -0
- package/src/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/schema-keywords.ts +33 -1
- package/src/schema-projection.ts +19 -0
- 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 +67 -7
- package/src/validate-extends.ts +51 -1
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-references.ts +70 -14
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-sensitive-slots.ts +109 -0
- package/src/validate-template-dispatch.ts +99 -0
- package/src/value-type-keyword.ts +1 -0
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
|
|
|
@@ -462,6 +477,27 @@ export class CelScopeResolver {
|
|
|
462
477
|
)
|
|
463
478
|
: undefined;
|
|
464
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
|
+
});
|
|
465
501
|
}
|
|
466
502
|
|
|
467
503
|
/**
|
|
@@ -518,7 +554,14 @@ export class CelScopeResolver {
|
|
|
518
554
|
const m = site.source;
|
|
519
555
|
let matched: Record<string, any> | undefined = site.contextSchema ?? this.invocationContext;
|
|
520
556
|
|
|
521
|
-
|
|
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) {
|
|
522
565
|
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
523
566
|
matched = {
|
|
524
567
|
...base,
|
|
@@ -535,7 +578,7 @@ export class CelScopeResolver {
|
|
|
535
578
|
// which is a separate decision from knowing the name is legal.
|
|
536
579
|
inputs: { type: "object", additionalProperties: true },
|
|
537
580
|
...(base.properties ?? {}),
|
|
538
|
-
steps:
|
|
581
|
+
steps: stepContext,
|
|
539
582
|
},
|
|
540
583
|
};
|
|
541
584
|
}
|
|
@@ -543,7 +586,7 @@ export class CelScopeResolver {
|
|
|
543
586
|
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
544
587
|
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
545
588
|
const errorSchema =
|
|
546
|
-
|
|
589
|
+
errorScopes.size > 0 ? errorContextForPath(path, errorScopes) : undefined;
|
|
547
590
|
if (errorSchema) {
|
|
548
591
|
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
549
592
|
matched = {
|
|
@@ -561,16 +604,49 @@ export class CelScopeResolver {
|
|
|
561
604
|
}
|
|
562
605
|
|
|
563
606
|
const { defs, aliases, scopes, allManifests, kernelGlobals } = this.inputs;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
);
|
|
574
650
|
const resolved = resolveContextAnnotations(matched, manifestItem, {
|
|
575
651
|
manifestRoot: rootForResolver,
|
|
576
652
|
defs,
|
|
@@ -578,7 +654,7 @@ export class CelScopeResolver {
|
|
|
578
654
|
allManifests: allManifests as Record<string, any>[],
|
|
579
655
|
});
|
|
580
656
|
return mergeKernelGlobalsIntoContext(
|
|
581
|
-
withBindingNames(resolved,
|
|
657
|
+
withBindingNames(resolved, rootManifest),
|
|
582
658
|
// Typed in the module that DECLARED this resource — for a manifest
|
|
583
659
|
// forwarded from an imported library, that is its `moduleGlobals` stamp,
|
|
584
660
|
// not the consuming application's block.
|
|
@@ -449,6 +449,42 @@ export class DefinitionRegistry {
|
|
|
449
449
|
return buildFieldMapAtPath(subSchema, fieldPath);
|
|
450
450
|
}
|
|
451
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
|
+
|
|
452
488
|
/** Returns all definitions that transitively extend the given abstract kind.
|
|
453
489
|
* Follows the capability chain to any depth (equivalent to instanceof in OOP).
|
|
454
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
|
+
}
|
|
@@ -137,6 +137,39 @@ export function effectiveAuthorSchema(
|
|
|
137
137
|
return mergeTypeSchemas([parentSchema, own]) as Record<string, any>;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The fields a merge-form inheriting child publishes over its parent's reading.
|
|
142
|
+
*
|
|
143
|
+
* A child that inherits its controller by delegation and declares no `base:` IS
|
|
144
|
+
* the parent instance, so `resources.<child>` is the parent's `snapshot()` — and
|
|
145
|
+
* a field the parent has never heard of would be readable from nowhere. These
|
|
146
|
+
* are exactly those fields: the ones the effective (merged) schema declares that
|
|
147
|
+
* the controller-bearing ancestor's does not.
|
|
148
|
+
*
|
|
149
|
+
* A REDECLARED name is deliberately excluded. Narrowing an inherited field (a
|
|
150
|
+
* description, a pattern, a widget hint) is ordinary in an additive extension and
|
|
151
|
+
* says nothing about publication, while the parent's `snapshot()` is the sole
|
|
152
|
+
* authority on what a parent instance publishes — its normalizations, its
|
|
153
|
+
* deliberate omissions, and its redactions. Republishing such a field from raw
|
|
154
|
+
* config would silently undo a provider's decision to keep it out.
|
|
155
|
+
*
|
|
156
|
+
* Empty for the `base:` form, whose fields are construction inputs consumed by
|
|
157
|
+
* the mapping, and for a child with its own controller, which publishes itself.
|
|
158
|
+
*/
|
|
159
|
+
export function publishedOwnFields(
|
|
160
|
+
def: ResourceDefinition | undefined,
|
|
161
|
+
resolve: DefResolver,
|
|
162
|
+
): string[] {
|
|
163
|
+
if (body(def).base || !isInheritedDelegation(def, resolve)) return [];
|
|
164
|
+
const ancestor = controllerBearingAncestor(def, resolve);
|
|
165
|
+
if (!ancestor) return [];
|
|
166
|
+
const inherited = new Set(
|
|
167
|
+
Object.keys((effectiveAuthorSchema(ancestor, resolve).properties ?? {}) as object),
|
|
168
|
+
);
|
|
169
|
+
const own = (effectiveAuthorSchema(def, resolve).properties ?? {}) as Record<string, unknown>;
|
|
170
|
+
return Object.keys(own).filter((name) => !inherited.has(name));
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
/** The two directions of a kind's invocation contract. `inputType` is what a
|
|
141
174
|
* caller sends to `invoke()`; `outputType` is what `invoke()` / `provide()`
|
|
142
175
|
* returns. */
|
|
@@ -2,6 +2,12 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
|
|
3
3
|
import type { LoadedFile } from "./loaded-types.js";
|
|
4
4
|
import { isModuleKind } from "./module-kinds.js";
|
|
5
|
+
import {
|
|
6
|
+
injectedDeclarations,
|
|
7
|
+
readLibraryLifecycle,
|
|
8
|
+
readResourceInputs,
|
|
9
|
+
type ResourceInput,
|
|
10
|
+
} from "./resource-input.js";
|
|
5
11
|
import type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
6
12
|
|
|
7
13
|
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
@@ -414,6 +420,12 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
|
|
|
414
420
|
// the kernel parses them. A module that declares none is absent, leaving importers ungated
|
|
415
421
|
// (see stampExportedKinds).
|
|
416
422
|
const declaredKinds = new Map<string, readonly string[]>();
|
|
423
|
+
/** Each library's declared `resources:` block, by module name — the inward
|
|
424
|
+
* half, stamped onto its importers below. */
|
|
425
|
+
const requiredResources = new Map<string, readonly ResourceInput[]>();
|
|
426
|
+
/** Libraries declaring `lifecycle: shared` — a singleton every import
|
|
427
|
+
* resolves to, so a per-import override of it is a contradiction. */
|
|
428
|
+
const sharedModules = new Set<string>();
|
|
417
429
|
for (const [source, mod] of graph.modules) {
|
|
418
430
|
if (source === graph.rootSource) continue; // root is an Application — no exports
|
|
419
431
|
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
|
|
@@ -424,6 +436,9 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
|
|
|
424
436
|
ownerSourceOf.set(moduleName, mod.owner.source);
|
|
425
437
|
specs.push(...reExportSpecsFromExports(moduleName, libDoc.exports?.resources));
|
|
426
438
|
kindModules.push({ module: moduleName, exportsKinds: libDoc.exports?.kinds });
|
|
439
|
+
const inputs = readResourceInputs(libDoc);
|
|
440
|
+
if (inputs.length > 0) requiredResources.set(moduleName, inputs);
|
|
441
|
+
if (readLibraryLifecycle(libDoc) === "shared") sharedModules.add(moduleName);
|
|
427
442
|
if (libDoc.exports?.kinds !== undefined) {
|
|
428
443
|
declaredKinds.set(moduleName, libDoc.exports.kinds.map((e) => parseExportEntry(e).name));
|
|
429
444
|
}
|
|
@@ -440,15 +455,100 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
|
|
|
440
455
|
// Telo.Import manifests so the analyzer can register the re-export mappings.
|
|
441
456
|
const exportedKinds = resolveExportedKinds(kindModules, aliasToModule);
|
|
442
457
|
const imports: Array<{ manifest: ResourceManifest; targetModule: string }> = [];
|
|
458
|
+
const sources: Array<{ manifest: ResourceManifest; targetSource: string }> = [];
|
|
443
459
|
for (const m of result) {
|
|
444
460
|
if (m.kind !== "Telo.Import") continue;
|
|
445
461
|
const owner = (m.metadata as { source?: string } | undefined)?.source;
|
|
446
462
|
const alias = m.metadata?.name as string | undefined;
|
|
447
|
-
const
|
|
448
|
-
if (
|
|
463
|
+
const edge = owner && alias ? graph.importEdges.get(owner)?.get(alias) : undefined;
|
|
464
|
+
if (edge?.targetModuleName) imports.push({ manifest: m, targetModule: edge.targetModuleName });
|
|
465
|
+
if (edge?.targetSource) sources.push({ manifest: m, targetSource: edge.targetSource });
|
|
449
466
|
}
|
|
467
|
+
stampResolvedSource(sources);
|
|
450
468
|
stampReExportedKinds(imports, exportedKinds);
|
|
451
469
|
stampExportedKinds(imports, declaredKinds);
|
|
470
|
+
stampRequiredResources(imports, requiredResources, aliasToModule);
|
|
471
|
+
stampSharedLifecycle(imports, sharedModules);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Stamp `metadata.resolvedSource` — the canonical resolved URL of an import's
|
|
475
|
+
* target — onto every `Telo.Import`. It is the identity the kernel keys a
|
|
476
|
+
* singleton's registry on, so a check that has to decide whether two imports
|
|
477
|
+
* reach the SAME library has to key on it too: `resolvedModuleName` is the
|
|
478
|
+
* module's own name, which two versions of it share. */
|
|
479
|
+
export function stampResolvedSource(
|
|
480
|
+
imports: ReadonlyArray<{ manifest: ResourceManifest; targetSource: string }>,
|
|
481
|
+
): void {
|
|
482
|
+
for (const { manifest, targetSource } of imports) {
|
|
483
|
+
(manifest.metadata as Record<string, unknown>).resolvedSource = targetSource;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Stamp `metadata.sharedLibrary` onto every `Telo.Import` whose target declares
|
|
488
|
+
* `lifecycle: shared`, so the consumer's pass can reject the per-import
|
|
489
|
+
* overrides a singleton has no room for. Stamped on `metadata` for the same
|
|
490
|
+
* reason `exportedKinds` is — the `Telo.Import` schema forbids extra top-level
|
|
491
|
+
* fields. */
|
|
492
|
+
export function stampSharedLifecycle(
|
|
493
|
+
imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
|
|
494
|
+
shared: ReadonlySet<string>,
|
|
495
|
+
): void {
|
|
496
|
+
for (const { manifest, targetModule } of imports) {
|
|
497
|
+
if (!shared.has(targetModule)) continue;
|
|
498
|
+
(manifest.metadata as Record<string, unknown>).sharedLibrary = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Stamp `metadata.requiredResources` — the target library's declared
|
|
503
|
+
* `resources:` block, with each kind constraint CANONICALIZED in the library's
|
|
504
|
+
* own alias scope — onto every `Telo.Import` whose target declares one.
|
|
505
|
+
*
|
|
506
|
+
* Canonicalized here because this is the only point where the target's own
|
|
507
|
+
* import edges are in hand: the consumer's pass sees the flattened list, from
|
|
508
|
+
* which the library doc and its alias scope are both gone, so an entry written
|
|
509
|
+
* `Sql.Connection` would otherwise be read against whatever the CONSUMER
|
|
510
|
+
* happens to alias `Sql` to. Stamped on `metadata` for the same reason
|
|
511
|
+
* `exportedKinds` is — the `Telo.Import` schema forbids extra top-level
|
|
512
|
+
* fields. An entry whose alias resolves to nothing is stamped with its kind
|
|
513
|
+
* unchanged; `validate-resource-inputs.ts` reports that against the library
|
|
514
|
+
* that wrote it. */
|
|
515
|
+
export function stampRequiredResources(
|
|
516
|
+
imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
|
|
517
|
+
declared: ReadonlyMap<string, readonly ResourceInput[]>,
|
|
518
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
519
|
+
): void {
|
|
520
|
+
for (const { manifest, targetModule } of imports) {
|
|
521
|
+
const inputs = declared.get(targetModule);
|
|
522
|
+
if (!inputs?.length) continue;
|
|
523
|
+
// A plain name → canonical-kind MAP, deliberately not a list of
|
|
524
|
+
// `{name, kind}` objects: that shape is character-identical to a resolved
|
|
525
|
+
// `!ref`, and every reference walk in the analyzer recognises a reference by
|
|
526
|
+
// exactly those two keys — so the stamp would be read as a malformed
|
|
527
|
+
// reference sitting in `metadata`.
|
|
528
|
+
const table: Record<string, string> = {};
|
|
529
|
+
for (const input of inputs) {
|
|
530
|
+
table[input.name] = canonicalizeInputKind(input.kind, targetModule, aliasToModule);
|
|
531
|
+
}
|
|
532
|
+
(manifest.metadata as Record<string, unknown>).requiredResources = table;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** `Alias.Kind` → `<owning module>.Kind` in the DECLARING library's scope.
|
|
537
|
+
* `Self.` names the library itself and `Telo.` the kernel built-ins, both of
|
|
538
|
+
* which cross no import edge. An unresolvable prefix is returned unchanged. */
|
|
539
|
+
function canonicalizeInputKind(
|
|
540
|
+
kind: string,
|
|
541
|
+
ownModule: string,
|
|
542
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
543
|
+
): string {
|
|
544
|
+
const dot = kind.indexOf(".");
|
|
545
|
+
if (dot <= 0) return kind;
|
|
546
|
+
const alias = kind.slice(0, dot);
|
|
547
|
+
const suffix = kind.slice(dot + 1);
|
|
548
|
+
if (alias === "Self") return `${ownModule}.${suffix}`;
|
|
549
|
+
if (alias === "Telo") return kind;
|
|
550
|
+
const module = aliasToModule(ownModule, alias);
|
|
551
|
+
return module ? `${module}.${suffix}` : kind;
|
|
452
552
|
}
|
|
453
553
|
|
|
454
554
|
/** Collect every imported library's FULL document set for the zone stage's
|
|
@@ -498,7 +598,20 @@ function collectModuleManifests(mod: LoadedModule): ResourceManifest[] {
|
|
|
498
598
|
for (const p of mod.partials) {
|
|
499
599
|
partials.push(...stampFile(p, ownerModuleName(mod.owner)));
|
|
500
600
|
}
|
|
501
|
-
|
|
601
|
+
const all = [...owner, ...partials];
|
|
602
|
+
// A library's `resources:` block declares the instances it requires from
|
|
603
|
+
// whoever imports it. Standing a kind-only declaration behind each entry is
|
|
604
|
+
// what makes `!ref connection` resolve and `resources.connection.<field>` type
|
|
605
|
+
// in the library's OWN pass — the only pass that sees its internals, since a
|
|
606
|
+
// consumer's flattened analysis drops the library doc. `selectModuleManifests-
|
|
607
|
+
// ForAnalysis` drops them again for a non-root module (they are not exported),
|
|
608
|
+
// and the kernel's import controller filters them out in favour of the
|
|
609
|
+
// borrowed instance itself.
|
|
610
|
+
const moduleDoc = all.find((m) => isModuleKind(m.kind));
|
|
611
|
+
if (moduleDoc) {
|
|
612
|
+
all.push(...injectedDeclarations(moduleDoc, moduleDoc.metadata?.name as string | undefined));
|
|
613
|
+
}
|
|
614
|
+
return all;
|
|
502
615
|
}
|
|
503
616
|
|
|
504
617
|
function ownerModuleName(file: LoadedFile): string | undefined {
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,9 @@ export {
|
|
|
21
21
|
selectModuleManifestsForAnalysis,
|
|
22
22
|
stampExportedKinds,
|
|
23
23
|
stampReExportedKinds,
|
|
24
|
+
stampRequiredResources,
|
|
25
|
+
stampResolvedSource,
|
|
26
|
+
stampSharedLifecycle,
|
|
24
27
|
type ParsedExportEntry,
|
|
25
28
|
type ReExportSpec,
|
|
26
29
|
} from "./flatten-for-analyzer.js";
|
|
@@ -31,9 +34,18 @@ export {
|
|
|
31
34
|
declaresCelRegion,
|
|
32
35
|
evalPathCovers,
|
|
33
36
|
mergeCelEvalSites,
|
|
37
|
+
pathMatchesScope,
|
|
34
38
|
NO_CEL_EVAL_SITES,
|
|
35
39
|
} from "./eval-paths.js";
|
|
36
40
|
export type { CelEvalSites } from "./eval-paths.js";
|
|
41
|
+
export {
|
|
42
|
+
injectedDeclarations,
|
|
43
|
+
isInjectedDeclaration,
|
|
44
|
+
readLibraryLifecycle,
|
|
45
|
+
readResourceInputs,
|
|
46
|
+
readSuppliedResources,
|
|
47
|
+
} from "./resource-input.js";
|
|
48
|
+
export type { LibraryLifecycle, ResourceInput } from "./resource-input.js";
|
|
37
49
|
export {
|
|
38
50
|
BINDINGS_ANNOTATION,
|
|
39
51
|
bindingContextProperties,
|
|
@@ -72,15 +84,19 @@ export {
|
|
|
72
84
|
isInheritedDelegation,
|
|
73
85
|
mappingFieldFor,
|
|
74
86
|
needsContractMapping,
|
|
87
|
+
publishedOwnFields,
|
|
75
88
|
resolveParent,
|
|
76
89
|
} from "./extends-resolution.js";
|
|
77
90
|
export type { ContractDirection, DefResolver } from "./extends-resolution.js";
|
|
91
|
+
export { validateSensitiveSlots } from "./validate-sensitive-slots.js";
|
|
92
|
+
export type { SensitiveSlotIssue } from "./validate-sensitive-slots.js";
|
|
78
93
|
export {
|
|
79
94
|
defaultBearingPaths,
|
|
80
95
|
declaredScalarPaths,
|
|
81
96
|
PERMISSIVE_CONTRACT,
|
|
82
97
|
resolveContract,
|
|
83
98
|
resolveContractSchema,
|
|
99
|
+
sensitivePaths,
|
|
84
100
|
withLiveValuesSkipped,
|
|
85
101
|
} from "./invocation-contract.js";
|
|
86
102
|
export type {
|
package/src/inline-imports.ts
CHANGED
|
@@ -54,6 +54,7 @@ export function inlineImportManifests(
|
|
|
54
54
|
source,
|
|
55
55
|
...(entry.variables !== undefined ? { variables: entry.variables } : {}),
|
|
56
56
|
...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
|
|
57
|
+
...(entry.resources !== undefined ? { resources: entry.resources } : {}),
|
|
57
58
|
...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
|
|
58
59
|
...(entry.logging !== undefined ? { logging: entry.logging } : {}),
|
|
59
60
|
} as unknown as ResourceManifest;
|
|
@@ -334,6 +334,77 @@ export function defaultBearingPaths(
|
|
|
334
334
|
return out;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/** Every path in `schema` the author marked `x-telo-sensitive: true` — the
|
|
338
|
+
* values a trace payload must carry as `[redacted]` rather than verbatim.
|
|
339
|
+
*
|
|
340
|
+
* It exists because making a credential a dispatched `Telo.Invocable` turns its
|
|
341
|
+
* material into an invoke OUTPUT, and invoke inputs and outputs ride the debug
|
|
342
|
+
* wire on every call under `--inspect` — which is every watch session. The
|
|
343
|
+
* kernel's substring scrubbing does not reach them: it has one call site, the
|
|
344
|
+
* resource-Created event's properties.
|
|
345
|
+
*
|
|
346
|
+
* Declared by the kind that OWNS the contract, so the kernel names no kind and
|
|
347
|
+
* any module opts in — the `x-telo-eval` shape. Exempting "an Http.Credential
|
|
348
|
+
* result" directly would be kind-knowledge in the kernel, and would stop at
|
|
349
|
+
* that one kind while the same token surfaces wherever else a contract carries
|
|
350
|
+
* it.
|
|
351
|
+
*
|
|
352
|
+
* Bounded by the schema's declarations exactly as {@link defaultBearingPaths}
|
|
353
|
+
* is: a contract marking nothing walks nothing at dispatch. The key is kept and
|
|
354
|
+
* only the value replaced, per the logging spec §14 — a payload that silently
|
|
355
|
+
* loses a key reads as a value that was never produced. */
|
|
356
|
+
export function sensitivePaths(
|
|
357
|
+
schema: Record<string, any>,
|
|
358
|
+
resolveRef?: (ref: string) => Record<string, any> | undefined,
|
|
359
|
+
): string[][] {
|
|
360
|
+
const out: string[][] = [];
|
|
361
|
+
|
|
362
|
+
const walk = (node: unknown, path: string[], chain: readonly object[]): void => {
|
|
363
|
+
if (!node || typeof node !== "object") return;
|
|
364
|
+
let s = node as Record<string, any>;
|
|
365
|
+
if (chain.includes(s)) return;
|
|
366
|
+
if (resolveRef && typeof s.$ref === "string") {
|
|
367
|
+
const target = resolveRef(s.$ref);
|
|
368
|
+
if (!target || chain.includes(target)) return;
|
|
369
|
+
s = { ...target, ...s, $ref: undefined };
|
|
370
|
+
}
|
|
371
|
+
const here = [...chain, node as object];
|
|
372
|
+
|
|
373
|
+
// A marked node is redacted WHOLE, so there is nothing below it to mark:
|
|
374
|
+
// descending would emit paths into a value that is already gone. The EMPTY
|
|
375
|
+
// path is legal and means the whole value — a contract whose entire output
|
|
376
|
+
// is the secret is the simplest shape there is, and refusing it silently
|
|
377
|
+
// (which `path.length > 0` did) left exactly that case unredacted.
|
|
378
|
+
if (s["x-telo-sensitive"] === true) {
|
|
379
|
+
out.push(path);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const properties = s.properties as Record<string, any> | undefined;
|
|
384
|
+
if (properties) {
|
|
385
|
+
for (const [key, child] of Object.entries(properties)) walk(child, [...path, key], here);
|
|
386
|
+
}
|
|
387
|
+
// A map whose VALUES are secrets — `additionalProperties: {x-telo-sensitive}`
|
|
388
|
+
// — is the shape a headers or query bag takes, and it carries no property
|
|
389
|
+
// names to walk. `{}` is the wildcard segment, the `[]` precedent one axis
|
|
390
|
+
// over.
|
|
391
|
+
for (const key of ["additionalProperties", "patternProperties"] as const) {
|
|
392
|
+
const node = s[key];
|
|
393
|
+
if (!node || typeof node !== "object") continue;
|
|
394
|
+
if (key === "additionalProperties") walk(node, [...path, "{}"], here);
|
|
395
|
+
else for (const child of Object.values(node)) walk(child, [...path, "{}"], here);
|
|
396
|
+
}
|
|
397
|
+
for (const branch of ["allOf", "anyOf", "oneOf"] as const) {
|
|
398
|
+
const list = s[branch];
|
|
399
|
+
if (Array.isArray(list)) for (const child of list) walk(child, path, here);
|
|
400
|
+
}
|
|
401
|
+
if (s.items) walk(s.items, [...path, "[]"], here);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
walk(schema, [], []);
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
337
408
|
/** The scalar form a declared node's value takes at runtime. `int64` and
|
|
338
409
|
* `double` are the two JSON scalars whose runtime representation is NOT decided
|
|
339
410
|
* by the value that arrives: a CEL integer is a BigInt and a CEL double a JS
|