@telorun/kernel 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.
Files changed (66) hide show
  1. package/dist/application-env.d.ts +25 -0
  2. package/dist/application-env.d.ts.map +1 -1
  3. package/dist/application-env.js +79 -3
  4. package/dist/application-env.js.map +1 -1
  5. package/dist/controller-loader.d.ts +7 -5
  6. package/dist/controller-loader.d.ts.map +1 -1
  7. package/dist/controller-loader.js +7 -5
  8. package/dist/controller-loader.js.map +1 -1
  9. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  10. package/dist/controllers/resource-definition/resource-definition-controller.js +29 -8
  11. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  12. package/dist/evaluation-context.d.ts +6 -0
  13. package/dist/evaluation-context.d.ts.map +1 -1
  14. package/dist/evaluation-context.js +20 -8
  15. package/dist/evaluation-context.js.map +1 -1
  16. package/dist/kernel.d.ts.map +1 -1
  17. package/dist/kernel.js +32 -3
  18. package/dist/kernel.js.map +1 -1
  19. package/dist/manifest-schemas.d.ts +1 -1
  20. package/dist/manifest-schemas.d.ts.map +1 -1
  21. package/dist/manifest-schemas.js +6 -0
  22. package/dist/manifest-schemas.js.map +1 -1
  23. package/dist/module-context.d.ts.map +1 -1
  24. package/dist/module-context.js +3 -4
  25. package/dist/module-context.js.map +1 -1
  26. package/dist/resource-context.d.ts +21 -20
  27. package/dist/resource-context.d.ts.map +1 -1
  28. package/dist/resource-context.js +71 -51
  29. package/dist/resource-context.js.map +1 -1
  30. package/dist/resource-handle.d.ts +11 -0
  31. package/dist/resource-handle.d.ts.map +1 -0
  32. package/dist/resource-handle.js +32 -0
  33. package/dist/resource-handle.js.map +1 -0
  34. package/dist/runtime-seam.d.ts.map +1 -1
  35. package/dist/runtime-seam.js +8 -2
  36. package/dist/runtime-seam.js.map +1 -1
  37. package/dist/schema-compiled-values.d.ts.map +1 -1
  38. package/dist/schema-compiled-values.js +39 -4
  39. package/dist/schema-compiled-values.js.map +1 -1
  40. package/dist/schema-validator.d.ts +21 -1
  41. package/dist/schema-validator.d.ts.map +1 -1
  42. package/dist/schema-validator.js +88 -6
  43. package/dist/schema-validator.js.map +1 -1
  44. package/dist/type-field-schema.d.ts +21 -0
  45. package/dist/type-field-schema.d.ts.map +1 -0
  46. package/dist/type-field-schema.js +54 -0
  47. package/dist/type-field-schema.js.map +1 -0
  48. package/dist/zone-context.d.ts +94 -0
  49. package/dist/zone-context.d.ts.map +1 -0
  50. package/dist/zone-context.js +272 -0
  51. package/dist/zone-context.js.map +1 -0
  52. package/package.json +4 -4
  53. package/src/application-env.ts +93 -4
  54. package/src/controller-loader.ts +7 -5
  55. package/src/controllers/resource-definition/resource-definition-controller.ts +31 -13
  56. package/src/evaluation-context.ts +21 -7
  57. package/src/kernel.ts +38 -1
  58. package/src/manifest-schemas.ts +6 -0
  59. package/src/module-context.ts +3 -4
  60. package/src/resource-context.ts +111 -50
  61. package/src/resource-handle.ts +35 -0
  62. package/src/runtime-seam.ts +9 -1
  63. package/src/schema-compiled-values.ts +36 -4
  64. package/src/schema-validator.ts +87 -5
  65. package/src/type-field-schema.ts +77 -0
  66. package/src/zone-context.ts +337 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Resolve a type field (`inputType` / `outputType`, or any `telo#Type` slot) to
3
+ * the JSON Schema behind it.
4
+ *
5
+ * Extracted from `ResourceContextImpl` so the build-time validator warm
6
+ * (`precompileDefinitionSchemas`) resolves a contract through the SAME code the
7
+ * runtime binding does. It used to compile the raw declaration instead —
8
+ * `{kind: Telo.JsonSchema, schema: {...}}`, which is not a JSON Schema at all,
9
+ * so `SchemaValidator.compile` read it as a property map and baked a validator
10
+ * for `{kind, schema}` that no dispatch would ever ask for. One resolver, one
11
+ * cache key.
12
+ *
13
+ * `getSchema` is the registry lookup — `SchemaValidator.getSchema` at both call
14
+ * sites. At warm time named types are not registered yet, so a bare-name
15
+ * declaration resolves to `undefined` and the caller simply skips it.
16
+ */
17
+ export type SchemaLookup = (name: string) => object | undefined;
18
+
19
+ /** The four declaration forms: a registered type's name, a `{kind, name}` ref
20
+ * object, an inline `{kind, schema}` type resource, and a raw JSON Schema. */
21
+ function readTypeSchema(
22
+ typeRef: unknown,
23
+ getSchema: SchemaLookup,
24
+ ): Record<string, any> | undefined {
25
+ if (!typeRef) return undefined;
26
+ if (typeof typeRef === "string") return getSchema(typeRef) as Record<string, any> | undefined;
27
+ if (typeof typeRef !== "object") return undefined;
28
+ const ref = typeRef as Record<string, any>;
29
+ if (ref.schema && typeof ref.schema === "object") return ref.schema;
30
+ if (typeof ref.name === "string") return getSchema(ref.name) as Record<string, any> | undefined;
31
+ if (ref.type || ref.properties || ref.$ref) return ref;
32
+ return undefined;
33
+ }
34
+
35
+ /**
36
+ * Follow a schema that is nothing but a `$ref` to a registered type, so the
37
+ * schema-level questions (which properties are streams, which paths carry a
38
+ * default) are asked of the real shape rather than of an alias.
39
+ *
40
+ * Only the whole-document alias form is followed, and only to READ it — the
41
+ * schema handed to AJV keeps its `$ref`s intact, because AJV resolves them
42
+ * itself against the registered ids and each type stays its own document with
43
+ * its own `$defs`. Inlining instead would move a `$ref: "#/$defs/X"` out of the
44
+ * document that defines `$defs.X`.
45
+ *
46
+ * `seen` guards a cycle two mutually-referencing types would otherwise spin on.
47
+ * A `$ref` alongside other keywords is left alone: that is a composition, not
48
+ * an alias.
49
+ */
50
+ function followTypeAlias(
51
+ schema: Record<string, any> | undefined,
52
+ getSchema: SchemaLookup,
53
+ ): Record<string, any> | undefined {
54
+ const seen = new Set<string>();
55
+ let current = schema;
56
+ while (
57
+ current &&
58
+ typeof current.$ref === "string" &&
59
+ Object.keys(current).length === 1 &&
60
+ !seen.has(current.$ref)
61
+ ) {
62
+ seen.add(current.$ref);
63
+ const target = getSchema(current.$ref) as Record<string, any> | undefined;
64
+ if (!target) return current;
65
+ current = target;
66
+ }
67
+ return current;
68
+ }
69
+
70
+ /** The JSON Schema a type field names, or `undefined` when it resolves to
71
+ * nothing (an unregistered name, a declaration in none of the four forms). */
72
+ export function resolveTypeFieldSchema(
73
+ typeRef: unknown,
74
+ getSchema: SchemaLookup,
75
+ ): Record<string, any> | undefined {
76
+ return followTypeAlias(readTypeSchema(typeRef, getSchema), getSchema);
77
+ }
@@ -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
+ }