@telorun/analyzer 0.45.0 → 0.47.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/alias-resolver.d.ts +45 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +33 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +106 -5
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +3 -0
- package/dist/extends-resolution.d.ts +19 -2
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +25 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/kernel-globals.d.ts +9 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +24 -1
- package/dist/resolve-ref-sentinels.d.ts +13 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +56 -5
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -1
- package/dist/validate-observed-state.d.ts +98 -0
- package/dist/validate-observed-state.d.ts.map +1 -0
- package/dist/validate-observed-state.js +304 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -2
- package/package.json +2 -2
- package/src/alias-resolver.ts +58 -0
- package/src/analyzer.ts +126 -4
- package/src/builtins.ts +3 -0
- package/src/extends-resolution.ts +37 -3
- package/src/index.ts +13 -0
- package/src/kernel-globals.ts +20 -0
- package/src/resolve-ref-sentinels.ts +68 -4
- package/src/validate-cel-context.ts +20 -1
- package/src/validate-observed-state.ts +354 -0
- package/src/validate-references.ts +8 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { isScopeEntry, resolveFieldEntries, } from "./reference-field-map.js";
|
|
2
3
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
3
4
|
/**
|
|
4
5
|
* Rewrites every `!ref <name>` sentinel in each non-system resource's value tree
|
|
@@ -41,7 +42,11 @@ export function resolveRefSentinels(resources, aliases, aliasesByModule,
|
|
|
41
42
|
// walked as sources). The kernel passes the analyzer-flattened set here so the runtime
|
|
42
43
|
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
43
44
|
// imported libraries' exported instances.
|
|
44
|
-
crossModuleTargets = []
|
|
45
|
+
crossModuleTargets = [],
|
|
46
|
+
/** Supplies each kind's `x-telo-scope` slots. Without it a scoped name cannot be
|
|
47
|
+
* told from a module-level one, and a shadowed `!ref` resolves to the resource
|
|
48
|
+
* it shadows — so both call sites pass it. */
|
|
49
|
+
defs) {
|
|
45
50
|
const moduleOf = (r) => r.metadata?.module;
|
|
46
51
|
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
47
52
|
// cross-module resolution targets only — never walked as local ref sources here.
|
|
@@ -96,11 +101,50 @@ crossModuleTargets = []) {
|
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
103
|
};
|
|
104
|
+
/** Names a resource declares in its own execution scopes, read from the kind's
|
|
105
|
+
* `x-telo-scope` slots — the analyzer's single definition of "scope", shared
|
|
106
|
+
* with `manifest-visitor`. Inferring it structurally instead (any array of
|
|
107
|
+
* named inline manifests) would give scope-local shadowing to the first kind
|
|
108
|
+
* that happens to carry such an array without asking for it, and this pass is
|
|
109
|
+
* shared with the kernel, so the guess would be baked into the runtime tree
|
|
110
|
+
* rather than merely reported. */
|
|
111
|
+
const declaredInScopes = (resource) => {
|
|
112
|
+
const fieldMap = defs?.getFieldMapForKind(resource.kind, aliases);
|
|
113
|
+
if (!fieldMap)
|
|
114
|
+
return undefined;
|
|
115
|
+
let declared;
|
|
116
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
117
|
+
if (!isScopeEntry(entry))
|
|
118
|
+
continue;
|
|
119
|
+
for (const { value } of resolveFieldEntries(resource, fieldPath)) {
|
|
120
|
+
for (const element of Array.isArray(value) ? value : [value]) {
|
|
121
|
+
if (!element || typeof element !== "object" || Array.isArray(element))
|
|
122
|
+
continue;
|
|
123
|
+
const manifest = element;
|
|
124
|
+
const name = manifest.metadata?.name;
|
|
125
|
+
if (typeof manifest.kind === "string" && typeof name === "string") {
|
|
126
|
+
(declared ??= new Map()).set(name, manifest);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return declared;
|
|
132
|
+
};
|
|
99
133
|
// Resolve every `!ref` sentinel in the tree; leave opaque tagged / precompiled
|
|
100
134
|
// nodes (e.g. `!cel`) untouched and don't descend into them.
|
|
101
|
-
|
|
135
|
+
//
|
|
136
|
+
// `scoped` carries the names the enclosing resource declares in its `x-telo-scope`
|
|
137
|
+
// slots, and they SHADOW the module-level ones — the order the runtime resolves
|
|
138
|
+
// in. Baking the module-level kind into a shadowed reference would label traces
|
|
139
|
+
// and `getRefIdentity` with a resource that never runs.
|
|
140
|
+
const walk = (value, scoped) => {
|
|
102
141
|
if (isRefSentinel(value)) {
|
|
103
|
-
|
|
142
|
+
const source = value.source;
|
|
143
|
+
const bare = source.indexOf(".") === -1;
|
|
144
|
+
const shadow = bare ? scoped?.get(source) : undefined;
|
|
145
|
+
if (shadow)
|
|
146
|
+
return { kind: shadow.kind, name: source };
|
|
147
|
+
return resolveTarget(source) ?? value;
|
|
104
148
|
}
|
|
105
149
|
if (value === null || typeof value !== "object")
|
|
106
150
|
return value;
|
|
@@ -110,12 +154,19 @@ crossModuleTargets = []) {
|
|
|
110
154
|
return value;
|
|
111
155
|
if (Array.isArray(value)) {
|
|
112
156
|
for (let i = 0; i < value.length; i++)
|
|
113
|
-
value[i] = walk(value[i]);
|
|
157
|
+
value[i] = walk(value[i], scoped);
|
|
114
158
|
return value;
|
|
115
159
|
}
|
|
116
160
|
const obj = value;
|
|
161
|
+
// A nested inline resource may declare scopes of its own (a `Run.Sequence`
|
|
162
|
+
// inside another sequence's `with:`). Collected before descending, so the
|
|
163
|
+
// declarations are visible to every region of the resource that declares
|
|
164
|
+
// them — a sequence's `with:` names resolve in its `targets:` and `steps:`
|
|
165
|
+
// alike, not only inside `with:` itself.
|
|
166
|
+
const declared = typeof obj.kind === "string" ? declaredInScopes(obj) : undefined;
|
|
167
|
+
const inner = declared ? new Map([...(scoped ?? new Map()), ...declared]) : scoped;
|
|
117
168
|
for (const key of Object.keys(obj))
|
|
118
|
-
obj[key] = walk(obj[key]);
|
|
169
|
+
obj[key] = walk(obj[key], inner);
|
|
119
170
|
return value;
|
|
120
171
|
};
|
|
121
172
|
for (const r of resources) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAmIrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAqBxF"}
|
|
@@ -276,13 +276,29 @@ export function resolveContextAnnotations(schema, manifestItem, opts) {
|
|
|
276
276
|
typeof ref.kind === "string" &&
|
|
277
277
|
typeof ref.name === "string" &&
|
|
278
278
|
subpath) {
|
|
279
|
+
const segments = subpath.split("/");
|
|
279
280
|
const refManifest = allManifests.find((m) => m.kind === ref.kind && m.metadata?.name === ref.name);
|
|
280
281
|
if (refManifest) {
|
|
281
|
-
const resolved = resolveTypeFieldToSchema(navigatePath(refManifest,
|
|
282
|
+
const resolved = resolveTypeFieldToSchema(navigatePath(refManifest, segments), allManifests);
|
|
282
283
|
if (resolved && typeof resolved === "object") {
|
|
283
284
|
return resolved;
|
|
284
285
|
}
|
|
285
286
|
}
|
|
287
|
+
// The instance declares nothing, so fall back to its KIND's declaration —
|
|
288
|
+
// the same layering `buildStepContextSchema` applies to `steps.<name>.result`,
|
|
289
|
+
// so a kind with one fixed output shape (declared once on its Telo.Definition)
|
|
290
|
+
// types the context, while a kind that exposes the field for per-instance
|
|
291
|
+
// narrowing keeps winning above.
|
|
292
|
+
if (defs) {
|
|
293
|
+
const canonical = aliases?.resolveKind(ref.kind) ?? ref.kind;
|
|
294
|
+
const def = defs.resolve(canonical);
|
|
295
|
+
if (def) {
|
|
296
|
+
const resolved = resolveTypeFieldToSchema(navigatePath(def, segments), allManifests);
|
|
297
|
+
if (resolved && typeof resolved === "object") {
|
|
298
|
+
return resolved;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
286
302
|
}
|
|
287
303
|
// Fallback: open schema (no false errors when outputType is not declared)
|
|
288
304
|
return { ...schema, additionalProperties: true };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { type ModuleScopes } from "./alias-resolver.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `status:` block's own schema — a plain JSON Schema, structurally. The one
|
|
5
|
+
* normative restriction (`required:` is rejected) is enforced by
|
|
6
|
+
* {@link validateObservedStateDeclarations} rather than here, so the author gets
|
|
7
|
+
* a message naming the rule and the fix instead of AJV's "must NOT be valid".
|
|
8
|
+
*
|
|
9
|
+
* Exported from the analyzer and re-used by the kernel's manifest schemas, so
|
|
10
|
+
* the rule has one definition rather than two kept in sync by hand.
|
|
11
|
+
*/
|
|
12
|
+
export declare const OBSERVED_STATE_SCHEMA: {
|
|
13
|
+
type: string;
|
|
14
|
+
additionalProperties: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* `required:` inside a `status:` block. Every declared field is mandatory once
|
|
18
|
+
* the resource has run, so the list would be either redundant or a lie; a
|
|
19
|
+
* genuinely sometimes-absent value is declared with a nullable type, which
|
|
20
|
+
* `CEL_NULLABLE_ACCESS` already guards.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateObservedStateDeclarations(manifests: readonly ResourceManifest[]): Array<{
|
|
23
|
+
kind: string;
|
|
24
|
+
name: string;
|
|
25
|
+
filePath?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
/** A CEL access into a resource's observed-state segment. */
|
|
29
|
+
export interface ObservedStateRead {
|
|
30
|
+
/** Import alias, when the read crosses a module boundary
|
|
31
|
+
* (`resources.<Alias>.<name>.status`). */
|
|
32
|
+
alias?: string;
|
|
33
|
+
/** Resource name. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** The field read under `.status`, when the chain names one. */
|
|
36
|
+
field?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Recognise an observed-state read in a member-access chain. Purely syntactic —
|
|
40
|
+
* it inspects the chain, not the topology — so the availability rule it feeds
|
|
41
|
+
* applies to every kind, declared or not.
|
|
42
|
+
*
|
|
43
|
+
* `resources.<name>.status.<field>` and the two-level cross-module form
|
|
44
|
+
* `resources.<Alias>.<name>.status.<field>` are both observed-state reads.
|
|
45
|
+
*/
|
|
46
|
+
export declare function observedStateRead(chain: readonly string[]): ObservedStateRead | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The names of every resource some slot can start: referenced from a ref slot
|
|
49
|
+
* that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
|
|
50
|
+
* `invoke:` target. A resource in none of them can never `run()`, so it can
|
|
51
|
+
* never report observed state.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately an over-approximation — a name reachable through any of these
|
|
54
|
+
* routes counts as runnable — because the cost of a false "can never run" is a
|
|
55
|
+
* valid manifest rejected, while the cost of a miss is only that the reader
|
|
56
|
+
* finds out at runtime instead, with a message that names the same fix.
|
|
57
|
+
*/
|
|
58
|
+
export declare function collectRunReachableNames(manifests: readonly ResourceManifest[], defs: {
|
|
59
|
+
resolve(kind: string): ResourceDefinition | undefined;
|
|
60
|
+
}, aliases?: {
|
|
61
|
+
resolveKind(kind: string): string | undefined;
|
|
62
|
+
}): Set<string>;
|
|
63
|
+
/** What a resource name resolves to for CEL purposes. `status` is present only
|
|
64
|
+
* when the kind declares one; `scoped` marks a resource declared inside an
|
|
65
|
+
* `x-telo-scope` slot, which resolves only within that scope's regions. */
|
|
66
|
+
export interface AnalyzedResource {
|
|
67
|
+
kind: string;
|
|
68
|
+
status?: Record<string, any>;
|
|
69
|
+
scoped?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Index every resource a CEL `resources.…` read can name: the module's own
|
|
73
|
+
* top-level resources, the ones declared inside `x-telo-scope` slots (a
|
|
74
|
+
* `Run.Sequence`'s `with:`), and each import's exported instances — keyed
|
|
75
|
+
* `<Alias>.<name>`, the two-level shape those publish under.
|
|
76
|
+
*
|
|
77
|
+
* Scope slots are found through the declaring kind's schema annotation, not by
|
|
78
|
+
* field name, so any composer with a scope participates.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildObservedStateIndex(manifests: readonly ResourceManifest[], defs: {
|
|
81
|
+
resolve(kind: string): ResourceDefinition | undefined;
|
|
82
|
+
}, aliases?: {
|
|
83
|
+
resolveKind(kind: string): string | undefined;
|
|
84
|
+
moduleForAlias?(alias: string): string | undefined;
|
|
85
|
+
}, scopes?: ModuleScopes): Map<string, AnalyzedResource>;
|
|
86
|
+
/** The `resources` node of a CEL context schema: one entry per resource, each
|
|
87
|
+
* open except for a typed, closed `status` node on kinds that declare one.
|
|
88
|
+
* `open` keeps the map itself permissive, so unknown resource names and every
|
|
89
|
+
* flat field pass exactly as they do today. */
|
|
90
|
+
export declare function buildObservedStateResourcesSchema(index: ReadonlyMap<string, AnalyzedResource>, open: boolean): Record<string, any>;
|
|
91
|
+
/**
|
|
92
|
+
* Write the typed `status` node for one index key into a `resources` property
|
|
93
|
+
* map. A dotted key (`Alias.name`) is an import's exported instance, which
|
|
94
|
+
* publishes two levels deep — the alias node stays open so every other name
|
|
95
|
+
* under it keeps resolving as it does today.
|
|
96
|
+
*/
|
|
97
|
+
export declare function applyObservedStateNode(properties: Record<string, any>, key: string, status: Record<string, any>): void;
|
|
98
|
+
//# sourceMappingURL=validate-observed-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-observed-state.d.ts","sourceRoot":"","sources":["../src/validate-observed-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAyBjF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;CAGjC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB3E;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC;+CAC2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAOzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,GAC1D,GAAG,CAAC,MAAM,CAAC,CAyBb;AAgDD;;4EAE4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,EAC/G,MAAM,CAAC,EAAE,YAAY,GACpB,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsC/B;AAyDD;;;gDAGgD;AAChD,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC5C,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAmBN"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
2
|
+
import { effectiveStatusSchema } from "./extends-resolution.js";
|
|
3
|
+
import { parseExportEntry } from "./flatten-for-analyzer.js";
|
|
4
|
+
import { moduleScopedDefResolver } from "./alias-resolver.js";
|
|
5
|
+
import { buildReferenceFieldMap, isRefEntry, isScopeEntry, resolveFieldValues, } from "./reference-field-map.js";
|
|
6
|
+
/** The kernel capabilities whose `run()` the kernel dispatches. A ref slot that
|
|
7
|
+
* accepts one of them is a slot that can start a resource — `targets:` on an
|
|
8
|
+
* Application or a `Run.Sequence`, and a step's `invoke:` (whose schema accepts
|
|
9
|
+
* `Telo.Runnable` alongside `Telo.Invocable`, and which the kernel dispatches
|
|
10
|
+
* through `run()` when the target has no `invoke()`). Keyed on the declared
|
|
11
|
+
* capability, never on a field name or a kind, so any composer that accepts a
|
|
12
|
+
* runnable participates without the analyzer knowing about it. */
|
|
13
|
+
const RUN_DISPATCH_CONTRACTS = new Set(["Telo.Runnable", "Telo.Service"]);
|
|
14
|
+
const SYSTEM_KINDS = new Set([
|
|
15
|
+
"Telo.Definition",
|
|
16
|
+
"Telo.Abstract",
|
|
17
|
+
"Telo.Import",
|
|
18
|
+
"Telo.Application",
|
|
19
|
+
"Telo.Library",
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* The `status:` block's own schema — a plain JSON Schema, structurally. The one
|
|
23
|
+
* normative restriction (`required:` is rejected) is enforced by
|
|
24
|
+
* {@link validateObservedStateDeclarations} rather than here, so the author gets
|
|
25
|
+
* a message naming the rule and the fix instead of AJV's "must NOT be valid".
|
|
26
|
+
*
|
|
27
|
+
* Exported from the analyzer and re-used by the kernel's manifest schemas, so
|
|
28
|
+
* the rule has one definition rather than two kept in sync by hand.
|
|
29
|
+
*/
|
|
30
|
+
export const OBSERVED_STATE_SCHEMA = {
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: true,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* `required:` inside a `status:` block. Every declared field is mandatory once
|
|
36
|
+
* the resource has run, so the list would be either redundant or a lie; a
|
|
37
|
+
* genuinely sometimes-absent value is declared with a nullable type, which
|
|
38
|
+
* `CEL_NULLABLE_ACCESS` already guards.
|
|
39
|
+
*/
|
|
40
|
+
export function validateObservedStateDeclarations(manifests) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const m of manifests) {
|
|
43
|
+
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract")
|
|
44
|
+
continue;
|
|
45
|
+
const status = m.status;
|
|
46
|
+
if (!status || typeof status !== "object" || !Array.isArray(status.required))
|
|
47
|
+
continue;
|
|
48
|
+
const name = m.metadata?.name ?? "<unnamed>";
|
|
49
|
+
out.push({
|
|
50
|
+
kind: m.kind,
|
|
51
|
+
name,
|
|
52
|
+
filePath: m.metadata?.source,
|
|
53
|
+
message: `${m.kind}/${name}: 'status:' must not declare 'required:' — every field a kind declares ` +
|
|
54
|
+
`it reports is mandatory once the resource has run, so the list is either redundant or a ` +
|
|
55
|
+
`lie. Declare a sometimes-absent field with a nullable type instead ` +
|
|
56
|
+
`(e.g. type: [string, "null"]); CEL_NULLABLE_ACCESS then forces the reader to guard it.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Recognise an observed-state read in a member-access chain. Purely syntactic —
|
|
63
|
+
* it inspects the chain, not the topology — so the availability rule it feeds
|
|
64
|
+
* applies to every kind, declared or not.
|
|
65
|
+
*
|
|
66
|
+
* `resources.<name>.status.<field>` and the two-level cross-module form
|
|
67
|
+
* `resources.<Alias>.<name>.status.<field>` are both observed-state reads.
|
|
68
|
+
*/
|
|
69
|
+
export function observedStateRead(chain) {
|
|
70
|
+
if (chain[0] !== "resources")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (chain[2] === OBSERVED_STATE_KEY)
|
|
73
|
+
return { name: chain[1], field: chain[3] };
|
|
74
|
+
if (chain[3] === OBSERVED_STATE_KEY) {
|
|
75
|
+
return { alias: chain[1], name: chain[2], field: chain[4] };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The names of every resource some slot can start: referenced from a ref slot
|
|
81
|
+
* that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
|
|
82
|
+
* `invoke:` target. A resource in none of them can never `run()`, so it can
|
|
83
|
+
* never report observed state.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately an over-approximation — a name reachable through any of these
|
|
86
|
+
* routes counts as runnable — because the cost of a false "can never run" is a
|
|
87
|
+
* valid manifest rejected, while the cost of a miss is only that the reader
|
|
88
|
+
* finds out at runtime instead, with a message that names the same fix.
|
|
89
|
+
*/
|
|
90
|
+
export function collectRunReachableNames(manifests, defs, aliases) {
|
|
91
|
+
const names = new Set();
|
|
92
|
+
const resolve = (kind) => defs.resolve(aliases?.resolveKind(kind) ?? kind) ?? defs.resolve(kind);
|
|
93
|
+
for (const manifest of manifests) {
|
|
94
|
+
const def = resolve(manifest.kind);
|
|
95
|
+
const schema = def?.schema;
|
|
96
|
+
if (!schema)
|
|
97
|
+
continue;
|
|
98
|
+
for (const [path, entry] of buildReferenceFieldMap(schema)) {
|
|
99
|
+
if (!isRefEntry(entry))
|
|
100
|
+
continue;
|
|
101
|
+
if (!entry.refs.some((ref) => RUN_DISPATCH_CONTRACTS.has(ref)))
|
|
102
|
+
continue;
|
|
103
|
+
for (const value of resolveFieldValues(manifest, path))
|
|
104
|
+
collectRefName(value, names);
|
|
105
|
+
}
|
|
106
|
+
// Step arrays nest through `if` / `while` / `switch` / `try`, and the step
|
|
107
|
+
// `invoke:` slot sits behind a local `$ref` the field map does not follow.
|
|
108
|
+
// Match the declared invoke key at any depth instead of re-deriving the
|
|
109
|
+
// nesting rules — over-approximating in the safe direction.
|
|
110
|
+
const invokeKey = stepInvokeKey(schema);
|
|
111
|
+
if (invokeKey)
|
|
112
|
+
collectKeyedRefs(manifest, invokeKey, names);
|
|
113
|
+
}
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
/** The property name a kind's `x-telo-step-context` declares as its dispatch
|
|
117
|
+
* slot (`invoke`), or undefined when the kind has no step array. */
|
|
118
|
+
function stepInvokeKey(schema) {
|
|
119
|
+
for (const fieldSchema of Object.values((schema.properties ?? {}))) {
|
|
120
|
+
const stepCtx = fieldSchema?.["x-telo-step-context"];
|
|
121
|
+
if (stepCtx?.invoke)
|
|
122
|
+
return stepCtx.invoke;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/** Collect ref names at every `key` property anywhere in `node`. */
|
|
127
|
+
function collectKeyedRefs(node, key, out) {
|
|
128
|
+
if (Array.isArray(node)) {
|
|
129
|
+
for (const item of node)
|
|
130
|
+
collectKeyedRefs(item, key, out);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (node === null || typeof node !== "object")
|
|
134
|
+
return;
|
|
135
|
+
for (const [k, value] of Object.entries(node)) {
|
|
136
|
+
if (k === key)
|
|
137
|
+
collectRefName(value, out);
|
|
138
|
+
collectKeyedRefs(value, key, out);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Record the resource name a slot value points at — a resolved `{kind, name}`
|
|
142
|
+
* ref, an unresolved `!ref` sentinel, or a `{ ref }` / `{ invoke }` wrapper. */
|
|
143
|
+
function collectRefName(value, out) {
|
|
144
|
+
if (value === null || typeof value !== "object")
|
|
145
|
+
return;
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
for (const item of value)
|
|
148
|
+
collectRefName(item, out);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const v = value;
|
|
152
|
+
if (typeof v.name === "string")
|
|
153
|
+
out.add(v.name);
|
|
154
|
+
if (typeof v.source === "string") {
|
|
155
|
+
const dot = v.source.lastIndexOf(".");
|
|
156
|
+
out.add(dot >= 0 ? v.source.slice(dot + 1) : v.source);
|
|
157
|
+
}
|
|
158
|
+
for (const wrapper of ["ref", "invoke"]) {
|
|
159
|
+
if (v[wrapper] !== undefined)
|
|
160
|
+
collectRefName(v[wrapper], out);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Index every resource a CEL `resources.…` read can name: the module's own
|
|
165
|
+
* top-level resources, the ones declared inside `x-telo-scope` slots (a
|
|
166
|
+
* `Run.Sequence`'s `with:`), and each import's exported instances — keyed
|
|
167
|
+
* `<Alias>.<name>`, the two-level shape those publish under.
|
|
168
|
+
*
|
|
169
|
+
* Scope slots are found through the declaring kind's schema annotation, not by
|
|
170
|
+
* field name, so any composer with a scope participates.
|
|
171
|
+
*/
|
|
172
|
+
export function buildObservedStateIndex(manifests, defs, aliases, scopes) {
|
|
173
|
+
const out = new Map();
|
|
174
|
+
const resolve = moduleScopedDefResolver(defs, aliases, scopes);
|
|
175
|
+
/** `module` is the resource's DECLARING module: an exported instance is
|
|
176
|
+
* written with that library's aliases (`kind: Self.Listener`), which the
|
|
177
|
+
* consumer's table cannot resolve. */
|
|
178
|
+
const record = (kind, key, scoped, module) => {
|
|
179
|
+
const status = effectiveStatusSchema(resolve.in(kind, module), resolve);
|
|
180
|
+
out.set(key, { kind, ...(status ? { status } : {}), ...(scoped ? { scoped } : {}) });
|
|
181
|
+
};
|
|
182
|
+
for (const manifest of manifests) {
|
|
183
|
+
const kind = manifest.kind;
|
|
184
|
+
const name = manifest.metadata?.name;
|
|
185
|
+
if (!kind || SYSTEM_KINDS.has(kind))
|
|
186
|
+
continue;
|
|
187
|
+
if (name)
|
|
188
|
+
record(kind, name, false, manifest.metadata?.module);
|
|
189
|
+
const schema = resolve(kind)?.schema;
|
|
190
|
+
if (!schema)
|
|
191
|
+
continue;
|
|
192
|
+
for (const [path, entry] of buildReferenceFieldMap(schema)) {
|
|
193
|
+
if (!isScopeEntry(entry))
|
|
194
|
+
continue;
|
|
195
|
+
for (const value of resolveFieldValues(manifest, path)) {
|
|
196
|
+
for (const scopedEntry of Array.isArray(value) ? value : [value]) {
|
|
197
|
+
const scopedKind = scopedEntry?.kind;
|
|
198
|
+
const scopedName = scopedEntry?.metadata?.name;
|
|
199
|
+
if (typeof scopedKind === "string" && typeof scopedName === "string") {
|
|
200
|
+
record(scopedKind, scopedName, true);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const [alias, name, kind, module] of importedExports(manifests, aliases)) {
|
|
207
|
+
record(kind, `${alias}.${name}`, false, module);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Every `<alias, exported name, kind>` an import makes readable as
|
|
213
|
+
* `resources.<Alias>.<name>`. The importer's `Telo.Import` docs give the
|
|
214
|
+
* aliases; the exported instances are the ones already stamped
|
|
215
|
+
* `metadata.forwardedExport` by `selectModuleManifestsForAnalysis` — the module
|
|
216
|
+
* doc that declared `exports.resources` is dropped for non-root modules, so the
|
|
217
|
+
* stamp, not the declaration, is what survives into the consumer's manifest
|
|
218
|
+
* list. A module doc is still consulted when one IS present (a single-library
|
|
219
|
+
* analysis, the editor's projection).
|
|
220
|
+
*/
|
|
221
|
+
function* importedExports(manifests, aliases) {
|
|
222
|
+
if (!aliases?.moduleForAlias)
|
|
223
|
+
return;
|
|
224
|
+
const declaredByModule = new Map();
|
|
225
|
+
for (const m of manifests) {
|
|
226
|
+
if (m.kind !== "Telo.Library")
|
|
227
|
+
continue;
|
|
228
|
+
const libName = (m.metadata?.name ?? m.metadata?.module);
|
|
229
|
+
const declared = m.exports?.resources;
|
|
230
|
+
if (!libName || !Array.isArray(declared))
|
|
231
|
+
continue;
|
|
232
|
+
declaredByModule.set(libName, new Set(declared
|
|
233
|
+
.filter((e) => typeof e === "string")
|
|
234
|
+
.map((e) => parseExportEntry(e).name)));
|
|
235
|
+
}
|
|
236
|
+
const exportsByModule = new Map();
|
|
237
|
+
for (const m of manifests) {
|
|
238
|
+
const module = m.metadata?.module;
|
|
239
|
+
const name = m.metadata?.name;
|
|
240
|
+
if (!module || !name || SYSTEM_KINDS.has(m.kind))
|
|
241
|
+
continue;
|
|
242
|
+
const forwarded = m.metadata?.forwardedExport;
|
|
243
|
+
if (!forwarded && !declaredByModule.get(module)?.has(name))
|
|
244
|
+
continue;
|
|
245
|
+
let byName = exportsByModule.get(module);
|
|
246
|
+
if (!byName)
|
|
247
|
+
exportsByModule.set(module, (byName = new Map()));
|
|
248
|
+
byName.set(name, m.kind);
|
|
249
|
+
}
|
|
250
|
+
for (const m of manifests) {
|
|
251
|
+
if (m.kind !== "Telo.Import")
|
|
252
|
+
continue;
|
|
253
|
+
const alias = m.metadata?.name;
|
|
254
|
+
if (!alias)
|
|
255
|
+
continue;
|
|
256
|
+
const targetModule = aliases.moduleForAlias(alias);
|
|
257
|
+
const exported = targetModule && exportsByModule.get(targetModule);
|
|
258
|
+
if (!exported)
|
|
259
|
+
continue;
|
|
260
|
+
for (const [name, kind] of exported)
|
|
261
|
+
yield [alias, name, kind, targetModule];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** The `resources` node of a CEL context schema: one entry per resource, each
|
|
265
|
+
* open except for a typed, closed `status` node on kinds that declare one.
|
|
266
|
+
* `open` keeps the map itself permissive, so unknown resource names and every
|
|
267
|
+
* flat field pass exactly as they do today. */
|
|
268
|
+
export function buildObservedStateResourcesSchema(index, open) {
|
|
269
|
+
const properties = {};
|
|
270
|
+
for (const [key, { status }] of index) {
|
|
271
|
+
if (!status)
|
|
272
|
+
continue;
|
|
273
|
+
applyObservedStateNode(properties, key, status);
|
|
274
|
+
}
|
|
275
|
+
return open
|
|
276
|
+
? { type: "object", additionalProperties: true, properties }
|
|
277
|
+
: { type: "object", properties };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Write the typed `status` node for one index key into a `resources` property
|
|
281
|
+
* map. A dotted key (`Alias.name`) is an import's exported instance, which
|
|
282
|
+
* publishes two levels deep — the alias node stays open so every other name
|
|
283
|
+
* under it keeps resolving as it does today.
|
|
284
|
+
*/
|
|
285
|
+
export function applyObservedStateNode(properties, key, status) {
|
|
286
|
+
const dot = key.indexOf(".");
|
|
287
|
+
const leaf = {
|
|
288
|
+
type: "object",
|
|
289
|
+
additionalProperties: true,
|
|
290
|
+
properties: { [OBSERVED_STATE_KEY]: { ...status, additionalProperties: false } },
|
|
291
|
+
};
|
|
292
|
+
if (dot < 0) {
|
|
293
|
+
properties[key] = leaf;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const alias = key.slice(0, dot);
|
|
297
|
+
const name = key.slice(dot + 1);
|
|
298
|
+
const aliasNode = (properties[alias] ??= {
|
|
299
|
+
type: "object",
|
|
300
|
+
additionalProperties: true,
|
|
301
|
+
properties: {},
|
|
302
|
+
});
|
|
303
|
+
(aliasNode.properties ??= {})[name] = leaf;
|
|
304
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAmetB"}
|
|
@@ -254,8 +254,14 @@ export function validateReferences(resources, context) {
|
|
|
254
254
|
}
|
|
255
255
|
// Local reference (bare name or explicit `Self.`-qualified).
|
|
256
256
|
const localName = aliasPrefix === "Self" ? refName.slice(dot + 1) : refName;
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
// Scope-local FIRST, enclosing module as the fallback — the order the
|
|
258
|
+
// runtime uses at every name-resolution site (`ScopeContext.getInstance`,
|
|
259
|
+
// `ResourceContext.resolveRef`, and the CEL `resources` layering). Module-first
|
|
260
|
+
// here would validate a shadowed name against the resource the kernel will
|
|
261
|
+
// never bind: a false pass when the outer kind fits and the scoped one does
|
|
262
|
+
// not, a false REFERENCE_KIND_MISMATCH when it is the other way round.
|
|
263
|
+
const target = visibleScopeManifests.find((m) => m.metadata?.name === localName) ??
|
|
264
|
+
byName.get(localName);
|
|
259
265
|
if (!target) {
|
|
260
266
|
diagnostics.push({
|
|
261
267
|
severity: DiagnosticSeverity.Error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.59.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/alias-resolver.ts
CHANGED
|
@@ -121,3 +121,61 @@ export function scopeResolverForModule(
|
|
|
121
121
|
? aliasesByModule.get(ownModule)
|
|
122
122
|
: undefined;
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/** Per-declaring-module alias tables plus the set of root (consumer-owned)
|
|
126
|
+
* modules. The shape `StaticAnalyzer.analyze` already threads through its
|
|
127
|
+
* passes, and what {@link moduleScopedDefResolver} needs to re-scope. */
|
|
128
|
+
export interface ModuleScopes {
|
|
129
|
+
aliasesByModule: ReadonlyMap<string, { resolveKind(kind: string): string | undefined }>;
|
|
130
|
+
rootModules: ReadonlySet<string>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Minimal view of the definition registry a kind lookup needs. */
|
|
134
|
+
export interface DefinitionLookup {
|
|
135
|
+
resolve(kind: string): unknown;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve a kind to its definition **in the scope that declared it**.
|
|
140
|
+
*
|
|
141
|
+
* An `extends` alias belongs to the file it was written in, not to whoever is
|
|
142
|
+
* reading it: a consumer that imports only a backend (the sanctioned "one import
|
|
143
|
+
* instead of two") has no alias for the abstract's library, so folding an
|
|
144
|
+
* inheritance chain with the consumer's table silently stops at the first hop
|
|
145
|
+
* and yields an un-merged schema. `from` — the definition the kind was read off —
|
|
146
|
+
* carries `metadata.module`, which is the scope to resolve in; a chain crossing
|
|
147
|
+
* several modules re-scopes at every hop.
|
|
148
|
+
*
|
|
149
|
+
* `resolveIn` takes the module explicitly, for the top-level lookup where there
|
|
150
|
+
* is no `from` yet (an exported instance's `kind: Self.X` is written in the
|
|
151
|
+
* exporting library's scope, which the consumer's table cannot resolve either).
|
|
152
|
+
*
|
|
153
|
+
* The runtime counterpart is `resource-definition-controller`, which resolves
|
|
154
|
+
* against the defining module context and stamps the result; keeping both on the
|
|
155
|
+
* same rule is what stops `telo check` and the kernel from disagreeing about
|
|
156
|
+
* which inherited fields a child kind may set.
|
|
157
|
+
*/
|
|
158
|
+
export function moduleScopedDefResolver<T>(
|
|
159
|
+
defs: { resolve(kind: string): T | undefined },
|
|
160
|
+
aliases?: { resolveKind(kind: string): string | undefined },
|
|
161
|
+
scopes?: ModuleScopes,
|
|
162
|
+
): {
|
|
163
|
+
(kind: string, from?: { metadata?: { module?: string } }): T | undefined;
|
|
164
|
+
in(kind: string, module?: string): T | undefined;
|
|
165
|
+
} {
|
|
166
|
+
const resolveIn = (kind: string, module?: string): T | undefined => {
|
|
167
|
+
const scoped =
|
|
168
|
+
module && scopes && !scopes.rootModules.has(module)
|
|
169
|
+
? scopes.aliasesByModule.get(module)
|
|
170
|
+
: undefined;
|
|
171
|
+
return (
|
|
172
|
+
(scoped ? defs.resolve(scoped.resolveKind(kind) ?? kind) : undefined) ??
|
|
173
|
+
defs.resolve(aliases?.resolveKind(kind) ?? kind) ??
|
|
174
|
+
defs.resolve(kind)
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
const resolver = ((kind: string, from?: { metadata?: { module?: string } }) =>
|
|
178
|
+
resolveIn(kind, from?.metadata?.module)) as ReturnType<typeof moduleScopedDefResolver<T>>;
|
|
179
|
+
resolver.in = resolveIn;
|
|
180
|
+
return resolver;
|
|
181
|
+
}
|