@telorun/kernel 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/controller-loader.d.ts +7 -5
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +7 -5
- package/dist/controller-loader.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +29 -8
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +6 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +20 -8
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +13 -2
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +6 -0
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +3 -4
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +16 -2
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +58 -3
- package/dist/resource-context.js.map +1 -1
- package/dist/resource-handle.d.ts +11 -0
- package/dist/resource-handle.d.ts.map +1 -0
- package/dist/resource-handle.js +32 -0
- package/dist/resource-handle.js.map +1 -0
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +29 -3
- package/dist/runtime-seam.js.map +1 -1
- package/dist/zone-context.d.ts +94 -0
- package/dist/zone-context.d.ts.map +1 -0
- package/dist/zone-context.js +272 -0
- package/dist/zone-context.js.map +1 -0
- package/package.json +4 -4
- package/src/controller-loader.ts +7 -5
- package/src/controllers/resource-definition/resource-definition-controller.ts +31 -13
- package/src/evaluation-context.ts +21 -7
- package/src/kernel.ts +17 -1
- package/src/manifest-schemas.ts +6 -0
- package/src/module-context.ts +3 -4
- package/src/resource-context.ts +98 -2
- package/src/resource-handle.ts +35 -0
- package/src/runtime-seam.ts +31 -2
- package/src/zone-context.ts +337 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kernel half of execution zones — `kernel/specs/execution-zones.md`.
|
|
3
|
+
*
|
|
4
|
+
* Everything a `ResourceContext` needs to open a zone declared by one of its
|
|
5
|
+
* own slots, and to assert it is inside one: annotation resolution in the
|
|
6
|
+
* kind's *declaring* scope, correlation-pointer resolution over the resource's
|
|
7
|
+
* own (Phase-5-injected) manifest, `extends`-aware kind matching, and the
|
|
8
|
+
* ambient-stack search.
|
|
9
|
+
*
|
|
10
|
+
* It lives beside `resource-handle.ts` rather than inside `resource-context.ts`
|
|
11
|
+
* because it is a subsystem with its own state, not a few accessors: the
|
|
12
|
+
* context delegates and keeps its own file about the resource lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* **Resolution is memoized per annotated field.** Everything a match needs —
|
|
15
|
+
* the resolved zone kind, the accepted-kind set, and the correlation handle —
|
|
16
|
+
* is a function of `(kind, field)` and the injected manifest, all fixed once
|
|
17
|
+
* `create()` has returned. A statement resolves them on *every* dispatch
|
|
18
|
+
* otherwise, and this sits on the SQL hot path.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
InvokeError,
|
|
22
|
+
RuntimeError,
|
|
23
|
+
UNCANCELLABLE_CONTEXT,
|
|
24
|
+
deriveContext,
|
|
25
|
+
getRefIdentity,
|
|
26
|
+
sameResource,
|
|
27
|
+
type InvokeContext,
|
|
28
|
+
type ResourceDefinition,
|
|
29
|
+
type ResourceHandle,
|
|
30
|
+
type ResourceInstance,
|
|
31
|
+
type ZoneEntry,
|
|
32
|
+
} from "@telorun/sdk";
|
|
33
|
+
import { readProvidesZone, readRequiresZone, type RequiresZoneSlot } from "@telorun/analyzer";
|
|
34
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
35
|
+
import { ambientInvokeContext, runWithAmbientContext } from "./evaluation-context.js";
|
|
36
|
+
import { handleOfInstance } from "./resource-handle.js";
|
|
37
|
+
|
|
38
|
+
/** What the owning context supplies. Structural, so `ResourceContext` stays the
|
|
39
|
+
* only thing that knows how any of it is reached. */
|
|
40
|
+
export interface ZoneHost {
|
|
41
|
+
/** This resource's `metadata.name`, for diagnostics. */
|
|
42
|
+
readonly resourceName: string;
|
|
43
|
+
/** Canonical `<module>.<Kind>` of this resource's kind. */
|
|
44
|
+
readonly resolvedKind: string;
|
|
45
|
+
/** This resource's own handle — a zone's `provider`. */
|
|
46
|
+
readonly self: ResourceHandle;
|
|
47
|
+
/** The resource's manifest — the SAME object Phase-5 injection mutates, so a
|
|
48
|
+
* correlation pointer read at invoke time sees live instances in ref slots. */
|
|
49
|
+
readonly manifest: Record<string, unknown>;
|
|
50
|
+
/** The definition registered for a kind, resolved globally. */
|
|
51
|
+
resolveDefinition(kind: string): ResourceDefinition | undefined;
|
|
52
|
+
/** The definition for a kind resolved in `module`'s alias scope. */
|
|
53
|
+
resolveDefinitionIn(kind: string, module?: string): ResourceDefinition | undefined;
|
|
54
|
+
/** A sibling instance by name, scope-local first. */
|
|
55
|
+
resolveLocalInstance(name: string): ResourceInstance | undefined;
|
|
56
|
+
/** A sibling's manifest by name, scope-local first — for a key pointer that
|
|
57
|
+
* traverses a `!ref` into the referenced resource's own field. */
|
|
58
|
+
resolveLocalManifest(name: string): Record<string, unknown> | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Everything a requirement match needs, resolved once per annotated field. */
|
|
62
|
+
interface ResolvedRequirement {
|
|
63
|
+
requirement: RequiresZoneSlot;
|
|
64
|
+
/** Canonical kind the requirement names. */
|
|
65
|
+
requiredKind: string;
|
|
66
|
+
/** Does an open zone's kind satisfy this requirement — is it the required
|
|
67
|
+
* kind, or does it transitively `extends` it (Liskov acceptance)? Memoized
|
|
68
|
+
* per entry kind, so the walk up the chain happens once. */
|
|
69
|
+
accepts: (entryKind: string) => boolean;
|
|
70
|
+
/** The instance the key pointers resolved to; absent = uncorrelated. */
|
|
71
|
+
keyHandle?: ResourceHandle;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ZoneContext {
|
|
75
|
+
readonly #host: ZoneHost;
|
|
76
|
+
/** Field → resolved requirement. See the file docstring. */
|
|
77
|
+
readonly #requirements = new Map<string, ResolvedRequirement>();
|
|
78
|
+
/** Slot → resolved correlation handle for a providing slot. */
|
|
79
|
+
readonly #providers = new Map<string, { key?: ResourceHandle }>();
|
|
80
|
+
|
|
81
|
+
constructor(host: ZoneHost) {
|
|
82
|
+
this.#host = host;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async withZone<T>(
|
|
86
|
+
slot: string,
|
|
87
|
+
fn: (ctx: InvokeContext, entry: ZoneEntry) => Promise<T>,
|
|
88
|
+
base?: InvokeContext,
|
|
89
|
+
): Promise<T> {
|
|
90
|
+
const provider = this.resolveProvider(slot);
|
|
91
|
+
// Every field derived: kind = the declaring kind, provider = this resource,
|
|
92
|
+
// key = the annotation's pointer over this resource's own manifest — which
|
|
93
|
+
// is the runtime half of why a schema cannot claim another module's zone.
|
|
94
|
+
const entry: ZoneEntry = Object.freeze({
|
|
95
|
+
kind: this.#host.resolvedKind,
|
|
96
|
+
provider: this.#host.self,
|
|
97
|
+
...(provider.key ? { key: provider.key } : {}),
|
|
98
|
+
});
|
|
99
|
+
const from = base ?? ambientInvokeContext() ?? UNCANCELLABLE_CONTEXT;
|
|
100
|
+
const derived = deriveContext(from, { zones: [...(from.zones ?? []), entry] });
|
|
101
|
+
return runWithAmbientContext(derived, () => fn(derived, entry));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
requireZone(field: string, ctx?: InvokeContext): ZoneEntry {
|
|
105
|
+
const resolved = this.resolveRequirement(field);
|
|
106
|
+
const entry = this.match(resolved, ctx);
|
|
107
|
+
if (entry) return entry;
|
|
108
|
+
const where = resolved.keyHandle
|
|
109
|
+
? ` on ${resolved.keyHandle.ref.kind} '${resolved.keyHandle.ref.name}'`
|
|
110
|
+
: "";
|
|
111
|
+
const reason = resolved.requirement.reason ? `: ${resolved.requirement.reason}` : "";
|
|
112
|
+
throw new InvokeError(
|
|
113
|
+
"ERR_ZONE_REQUIRED",
|
|
114
|
+
`${this.#host.resolvedKind} '${this.#host.resourceName}': no ${resolved.requiredKind} zone open${where}${reason}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
findZone(field: string, ctx?: InvokeContext): ZoneEntry | undefined {
|
|
119
|
+
return this.match(this.resolveRequirement(field), ctx);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
zonesFor(instance: ResourceInstance, ctx?: InvokeContext): readonly ZoneEntry[] {
|
|
123
|
+
const zones = (ctx ?? ambientInvokeContext())?.zones;
|
|
124
|
+
if (!zones || zones.length === 0) return [];
|
|
125
|
+
const handle = handleOfInstance(instance as object);
|
|
126
|
+
if (!handle) return [];
|
|
127
|
+
const out: ZoneEntry[] = [];
|
|
128
|
+
for (let i = zones.length - 1; i >= 0; i--) {
|
|
129
|
+
const entry = zones[i]!;
|
|
130
|
+
if (entry.key && sameResource(entry.key, handle)) out.push(entry);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── resolution ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
private resolveProvider(slot: string): { key?: ResourceHandle } {
|
|
138
|
+
const cached = this.#providers.get(slot);
|
|
139
|
+
if (cached) return cached;
|
|
140
|
+
const provides = readProvidesZone(this.slotSchema(slot));
|
|
141
|
+
if (!provides) {
|
|
142
|
+
throw new RuntimeError(
|
|
143
|
+
"ERR_ZONE_ANNOTATION_MISSING",
|
|
144
|
+
`[${this.#host.resourceName}] withZone('${slot}'): schema.properties.${slot} of ` +
|
|
145
|
+
`${this.#host.resolvedKind} carries no x-telo-provides-zone — the controller and its ` +
|
|
146
|
+
`schema disagree`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const key = provides.key ? this.resolveCorrelationHandle([provides.key]) : undefined;
|
|
150
|
+
const resolved = key ? { key } : {};
|
|
151
|
+
this.#providers.set(slot, resolved);
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private resolveRequirement(field: string): ResolvedRequirement {
|
|
156
|
+
const cached = this.#requirements.get(field);
|
|
157
|
+
if (cached) return cached;
|
|
158
|
+
const requirement = readRequiresZone(this.slotSchema(field));
|
|
159
|
+
if (!requirement) {
|
|
160
|
+
throw new RuntimeError(
|
|
161
|
+
"ERR_ZONE_ANNOTATION_MISSING",
|
|
162
|
+
`[${this.#host.resourceName}] requireZone('${field}'): schema.properties.${field} of ` +
|
|
163
|
+
`${this.#host.resolvedKind} carries no x-telo-requires-zone — the controller and its ` +
|
|
164
|
+
`schema disagree`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const requiredKind = this.resolveZoneKind(requirement.zone);
|
|
168
|
+
const resolved: ResolvedRequirement = {
|
|
169
|
+
requirement,
|
|
170
|
+
requiredKind,
|
|
171
|
+
accepts: this.acceptancePredicate(requiredKind),
|
|
172
|
+
// When no pointer resolves, the requirement discharges uncorrelated — any
|
|
173
|
+
// zone of the right type. Inventing a correlation the manifest does not
|
|
174
|
+
// state would manufacture failures from a guess.
|
|
175
|
+
keyHandle:
|
|
176
|
+
requirement.key.length > 0 ? this.resolveCorrelationHandle(requirement.key) : undefined,
|
|
177
|
+
};
|
|
178
|
+
this.#requirements.set(field, resolved);
|
|
179
|
+
return resolved;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Innermost first — the order a nested zone must win in. */
|
|
183
|
+
private match(resolved: ResolvedRequirement, ctx?: InvokeContext): ZoneEntry | undefined {
|
|
184
|
+
const zones = (ctx ?? ambientInvokeContext())?.zones ?? [];
|
|
185
|
+
for (let i = zones.length - 1; i >= 0; i--) {
|
|
186
|
+
const entry = zones[i]!;
|
|
187
|
+
if (!resolved.accepts(entry.kind)) continue;
|
|
188
|
+
if (resolved.keyHandle && !(entry.key && sameResource(entry.key, resolved.keyHandle))) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
return entry;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The schema node for one of this kind's own top-level fields — the site the
|
|
197
|
+
* zone annotations live on. */
|
|
198
|
+
private slotSchema(field: string): Record<string, any> | undefined {
|
|
199
|
+
const def = this.ownDefinition();
|
|
200
|
+
return (def.schema as { properties?: Record<string, Record<string, any>> } | undefined)
|
|
201
|
+
?.properties?.[field];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private ownDefinition(): ResourceDefinition {
|
|
205
|
+
const def = this.#host.resolveDefinition(this.#host.resolvedKind);
|
|
206
|
+
if (!def) {
|
|
207
|
+
throw new RuntimeError(
|
|
208
|
+
"ERR_ZONE_ANNOTATION_MISSING",
|
|
209
|
+
`[${this.#host.resourceName}] no registered definition for kind '${this.#host.resolvedKind}'`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return def;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Resolve the annotation's zone kind to canonical `<module>.<Kind>`.
|
|
216
|
+
* `resolveSchemaRefKinds` already rewrote it in the DECLARING module's scope
|
|
217
|
+
* during analysis; the scoped lookup is the fallback for an un-analyzed load. */
|
|
218
|
+
private resolveZoneKind(zone: string): string {
|
|
219
|
+
const def = this.#host.resolveDefinitionIn(zone, this.ownDefinition().metadata.module);
|
|
220
|
+
if (!def) {
|
|
221
|
+
throw new RuntimeError(
|
|
222
|
+
"ERR_ZONE_UNRESOLVED",
|
|
223
|
+
`[${this.#host.resourceName}] x-telo-requires-zone names '${zone}', which resolves to no ` +
|
|
224
|
+
`registered kind`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return `${def.metadata.module}.${def.metadata.name}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Acceptance for one requirement: the required kind, or anything that
|
|
232
|
+
* transitively `extends` it.
|
|
233
|
+
*
|
|
234
|
+
* A predicate rather than a precomputed set because the kernel's registry has
|
|
235
|
+
* no `extendedBy` index — only the upward `extends` link — so the set of
|
|
236
|
+
* acceptable kinds is not enumerable from the requirement alone. Each
|
|
237
|
+
* candidate walks UP instead, and only ever for a kind actually seen on the
|
|
238
|
+
* stack. The verdict is cached per entry kind, so a repeated dispatch through
|
|
239
|
+
* the same shape costs one map lookup.
|
|
240
|
+
*/
|
|
241
|
+
private acceptancePredicate(requiredKind: string): (entryKind: string) => boolean {
|
|
242
|
+
const verdicts = new Map<string, boolean>([[requiredKind, true]]);
|
|
243
|
+
return (entryKind: string): boolean => {
|
|
244
|
+
const known = verdicts.get(entryKind);
|
|
245
|
+
if (known !== undefined) return known;
|
|
246
|
+
const verdict = this.extendsChainReaches(entryKind, requiredKind);
|
|
247
|
+
verdicts.set(entryKind, verdict);
|
|
248
|
+
return verdict;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private extendsChainReaches(entryKind: string, requiredKind: string): boolean {
|
|
253
|
+
let current: string | undefined = entryKind;
|
|
254
|
+
const seen = new Set<string>();
|
|
255
|
+
while (current !== undefined && !seen.has(current)) {
|
|
256
|
+
if (current === requiredKind) return true;
|
|
257
|
+
seen.add(current);
|
|
258
|
+
const def = this.#host.resolveDefinition(current);
|
|
259
|
+
if (typeof def?.extends !== "string") return false;
|
|
260
|
+
const parent = this.#host.resolveDefinitionIn(def.extends, def.metadata.module);
|
|
261
|
+
current = parent ? `${parent.metadata.module}.${parent.metadata.name}` : undefined;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** First pointer that resolves to a live instance wins — the manifest-level
|
|
267
|
+
* transcription of a controller's own `a ?? b` derivation. */
|
|
268
|
+
private resolveCorrelationHandle(pointers: readonly string[]): ResourceHandle | undefined {
|
|
269
|
+
for (const pointer of pointers) {
|
|
270
|
+
const handle = this.resolveKeyPointer(pointer);
|
|
271
|
+
if (handle) return handle;
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private resolveKeyPointer(pointer: string): ResourceHandle | undefined {
|
|
277
|
+
const segments = pointer.split("/").filter((s) => s.length > 0);
|
|
278
|
+
if (segments.length === 0) return undefined;
|
|
279
|
+
let manifest: Record<string, unknown> | undefined = this.#host.manifest;
|
|
280
|
+
let value: unknown;
|
|
281
|
+
for (let i = 0; i < segments.length; i++) {
|
|
282
|
+
if (i > 0) {
|
|
283
|
+
// A pointer may traverse a `!ref` into the referenced resource's own
|
|
284
|
+
// manifest: read field → resolve reference → read field. Purely
|
|
285
|
+
// mechanical — no kind is named here.
|
|
286
|
+
const name = this.referencedName(value);
|
|
287
|
+
manifest = name ? this.#host.resolveLocalManifest(name) : undefined;
|
|
288
|
+
if (!manifest) return undefined;
|
|
289
|
+
}
|
|
290
|
+
value = manifest[segments[i]!];
|
|
291
|
+
if (value === undefined || value === null) return undefined;
|
|
292
|
+
}
|
|
293
|
+
return this.handleOfValue(value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The LOCAL name a reference value points at, in any of its three shapes.
|
|
298
|
+
*
|
|
299
|
+
* A cross-module `!ref Alias.name` yields undefined: it names an instance in
|
|
300
|
+
* another module's scope, and this resolver is scope-local, so taking the
|
|
301
|
+
* bare name would bind to whatever local resource happened to share it.
|
|
302
|
+
* Leaving it uncorrelated is the under-approximating direction. The analyzer's
|
|
303
|
+
* `refName` holds the identical rule, so the two halves agree about what a
|
|
304
|
+
* traversing pointer means.
|
|
305
|
+
*/
|
|
306
|
+
private referencedName(value: unknown): string | undefined {
|
|
307
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
308
|
+
const id = getRefIdentity(value as object) ?? handleOfInstance(value as object)?.ref;
|
|
309
|
+
if (id) return id.name;
|
|
310
|
+
if (isRefSentinel(value)) {
|
|
311
|
+
const source = value.source;
|
|
312
|
+
const dot = source.indexOf(".");
|
|
313
|
+
if (dot <= 0) return source;
|
|
314
|
+
return source.slice(0, dot) === "Self" ? source.slice(dot + 1) : undefined;
|
|
315
|
+
}
|
|
316
|
+
const v = value as Record<string, unknown>;
|
|
317
|
+
const pure =
|
|
318
|
+
typeof v.kind === "string" &&
|
|
319
|
+
typeof v.name === "string" &&
|
|
320
|
+
Object.keys(v).every((k) => k === "kind" || k === "name" || k === "alias");
|
|
321
|
+
if (!pure) return undefined;
|
|
322
|
+
if (typeof v.alias === "string" && v.alias !== "Self") return undefined;
|
|
323
|
+
return v.name as string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The handle a pointer's terminal value identifies: a live instance's own
|
|
327
|
+
* handle, or the handle of the instance a reference resolves to. */
|
|
328
|
+
private handleOfValue(value: unknown): ResourceHandle | undefined {
|
|
329
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
330
|
+
const direct = handleOfInstance(value as object);
|
|
331
|
+
if (direct) return direct;
|
|
332
|
+
const name = this.referencedName(value);
|
|
333
|
+
if (!name) return undefined;
|
|
334
|
+
const instance = this.#host.resolveLocalInstance(name);
|
|
335
|
+
return instance ? handleOfInstance(instance as object) : undefined;
|
|
336
|
+
}
|
|
337
|
+
}
|