@telorun/kernel 0.78.0 → 0.79.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/bundle/module-artifact.d.ts +2 -1
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +14 -10
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +62 -9
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +71 -63
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts +2 -2
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +9 -3
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts +2 -1
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
- package/dist/controller-loaders/napi-loader.js +4 -1
- package/dist/controller-loaders/napi-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +2 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +13 -7
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts +2 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.js +4 -1
- package/dist/controller-loaders/source-bundle-builder.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 +7 -3
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +12 -5
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/dependency-injection.d.ts.map +1 -1
- package/dist/dependency-injection.js +7 -7
- package/dist/dependency-injection.js.map +1 -1
- package/dist/evaluation-context.d.ts +33 -13
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +145 -4
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-declaration.d.ts +8 -0
- package/dist/instance-declaration.d.ts.map +1 -0
- package/dist/instance-declaration.js +34 -0
- package/dist/instance-declaration.js.map +1 -0
- package/dist/invocation-contract-binding.d.ts +15 -2
- package/dist/invocation-contract-binding.d.ts.map +1 -1
- package/dist/invocation-contract-binding.js +39 -4
- package/dist/invocation-contract-binding.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +60 -3
- 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 +1 -1
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts +5 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +13 -1
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +12 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +15 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/zone-context.d.ts +16 -1
- package/dist/zone-context.d.ts.map +1 -1
- package/dist/zone-context.js +61 -1
- package/dist/zone-context.js.map +1 -1
- package/package.json +3 -3
- package/src/bundle/module-artifact.ts +25 -8
- package/src/controller-loader.ts +114 -80
- package/src/controller-loaders/bundle-loader.ts +10 -2
- package/src/controller-loaders/napi-loader.ts +6 -0
- package/src/controller-loaders/npm-loader.ts +14 -4
- package/src/controller-loaders/source-bundle-builder.ts +6 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +7 -3
- package/src/controllers/resource-definition/resource-template-controller.ts +12 -5
- package/src/dependency-injection.ts +7 -7
- package/src/evaluation-context.ts +195 -15
- package/src/index.ts +6 -0
- package/src/instance-declaration.ts +36 -0
- package/src/invocation-contract-binding.ts +49 -1
- package/src/kernel.ts +77 -2
- package/src/manifest-schemas.ts +1 -0
- package/src/module-context.ts +24 -3
- package/src/resource-context.ts +18 -0
- package/src/zone-context.ts +70 -1
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
isCompiledValue,
|
|
7
7
|
isInvokeError,
|
|
8
8
|
isCancellationError,
|
|
9
|
+
isSuspension,
|
|
9
10
|
resourceKey,
|
|
10
11
|
UNCANCELLABLE_CONTEXT,
|
|
11
12
|
type EvaluationContext as IEvaluationContext,
|
|
13
|
+
type SpanOutcome,
|
|
12
14
|
type EmitEvent,
|
|
13
15
|
type InstanceFactory,
|
|
14
16
|
type InvokeContext,
|
|
@@ -230,6 +232,121 @@ const completedInstances = new WeakSet<object>();
|
|
|
230
232
|
* `kind` is already canonical (`<module>.<Kind>`) on that path, so the resolver
|
|
231
233
|
* needs no alias table.
|
|
232
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* The last published reading for an instance, by identity.
|
|
237
|
+
*
|
|
238
|
+
* CEL reads a resource through its PUBLISHED STATE — that is what
|
|
239
|
+
* `resources.<name>.<field>` has always meant — and a resource reached through a
|
|
240
|
+
* ref slot is no different: `self.table.table` and `resources.users.table` name
|
|
241
|
+
* the same fact and must answer the same way. A ref slot holds the live
|
|
242
|
+
* instance, which CEL cannot read a member off at all, so this is the table that
|
|
243
|
+
* makes the two agree.
|
|
244
|
+
*
|
|
245
|
+
* Keyed by instance rather than by name because a ref slot carries no name, and
|
|
246
|
+
* a `with:`-scoped resource has one published reading per scope run. Weak, so
|
|
247
|
+
* nothing here extends an instance's lifetime, and deliberately one-directional
|
|
248
|
+
* — an entry is a reading OF an instance, never a way to obtain one.
|
|
249
|
+
*/
|
|
250
|
+
const publishedByInstance = new WeakMap<object, Record<string, unknown>>();
|
|
251
|
+
|
|
252
|
+
/** The published reading for an instance, or undefined if it has never
|
|
253
|
+
* published — a resource that has not been created yet, or a foreign object. */
|
|
254
|
+
export function publishedPropsForInstance(
|
|
255
|
+
instance: unknown,
|
|
256
|
+
): Record<string, unknown> | undefined {
|
|
257
|
+
return instance && typeof instance === "object"
|
|
258
|
+
? publishedByInstance.get(instance as object)
|
|
259
|
+
: undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The CEL-facing view of a template's `self`.
|
|
264
|
+
*
|
|
265
|
+
* Ref slots hold live instances after Phase-5 injection; each is replaced by its
|
|
266
|
+
* published reading so a template body can read a scalar off a resource it
|
|
267
|
+
* references. A slot whose instance has never published is left as it is — a
|
|
268
|
+
* value CEL will reject on member access, which is the honest outcome rather
|
|
269
|
+
* than an invented empty object that would make a typo evaluate to null.
|
|
270
|
+
*/
|
|
271
|
+
/**
|
|
272
|
+
* A memoized `self` view, beside the exact readings it substituted.
|
|
273
|
+
*
|
|
274
|
+
* Invalidation is per view, checked against the reading each substituted
|
|
275
|
+
* instance currently has — `publishedPropsOf` publishes a NEW props object every
|
|
276
|
+
* time, so identity IS the version. A process-global counter was wrong twice
|
|
277
|
+
* over: every `invoke()` republishes, so under any concurrency the memo was
|
|
278
|
+
* defeated and the walk was back on the per-request path it was added for; and
|
|
279
|
+
* the counter is shared across in-process kernels, so one app's traffic
|
|
280
|
+
* invalidated another app's views.
|
|
281
|
+
*
|
|
282
|
+
* The check is O(substituted slots) — the refs, not the fields — and a view that
|
|
283
|
+
* substituted nothing has an empty source list and is therefore always valid,
|
|
284
|
+
* which is the common case.
|
|
285
|
+
*/
|
|
286
|
+
interface SelfView {
|
|
287
|
+
readonly view: Record<string, unknown>;
|
|
288
|
+
readonly sources: readonly [object, Record<string, unknown>][];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const selfViews = new WeakMap<object, SelfView>();
|
|
292
|
+
|
|
293
|
+
function viewIsCurrent(cached: SelfView): boolean {
|
|
294
|
+
for (const [instance, props] of cached.sources) {
|
|
295
|
+
if (publishedByInstance.get(instance) !== props) return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function celSelfView(self: Record<string, unknown>): Record<string, unknown> {
|
|
301
|
+
// A template's `invoke` expands its body on EVERY dispatch, so this sits on
|
|
302
|
+
// the per-request path: without the memo a repository rebuilt the whole view
|
|
303
|
+
// per call.
|
|
304
|
+
const cached = selfViews.get(self);
|
|
305
|
+
if (cached && viewIsCurrent(cached)) return cached.view;
|
|
306
|
+
|
|
307
|
+
const sources: [object, Record<string, unknown>][] = [];
|
|
308
|
+
let view: Record<string, unknown> | undefined;
|
|
309
|
+
for (const [key, value] of Object.entries(self)) {
|
|
310
|
+
const substituted = celValueView(value, sources);
|
|
311
|
+
if (substituted === value) continue;
|
|
312
|
+
view ??= { ...self };
|
|
313
|
+
view[key] = substituted;
|
|
314
|
+
}
|
|
315
|
+
const resolved = view ?? self;
|
|
316
|
+
selfViews.set(self, { view: resolved, sources });
|
|
317
|
+
return resolved;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* A ref slot holds one instance or a LIST of them (`tables: [!ref a, !ref b]`),
|
|
322
|
+
* and both are ref slots — so converting only the first would make
|
|
323
|
+
* `self.tables[0].name` mean something different from `self.table.name`, which
|
|
324
|
+
* is the inconsistency this rule exists to remove.
|
|
325
|
+
*
|
|
326
|
+
* Arrays only: the walk stops at any other object, because a nested plain object
|
|
327
|
+
* in a manifest is the author's own data and a resource's published reading is
|
|
328
|
+
* not something to graft into the middle of it.
|
|
329
|
+
*/
|
|
330
|
+
function celValueView(
|
|
331
|
+
value: unknown,
|
|
332
|
+
sources: [object, Record<string, unknown>][],
|
|
333
|
+
): unknown {
|
|
334
|
+
const published = publishedPropsForInstance(value);
|
|
335
|
+
if (published) {
|
|
336
|
+
sources.push([value as object, published]);
|
|
337
|
+
return published;
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(value)) return value;
|
|
340
|
+
let copy: unknown[] | undefined;
|
|
341
|
+
for (let i = 0; i < value.length; i++) {
|
|
342
|
+
const substituted = celValueView(value[i], sources);
|
|
343
|
+
if (substituted === value[i]) continue;
|
|
344
|
+
copy ??= [...value];
|
|
345
|
+
copy[i] = substituted;
|
|
346
|
+
}
|
|
347
|
+
return copy ?? value;
|
|
348
|
+
}
|
|
349
|
+
|
|
233
350
|
export async function publishedPropsOf(
|
|
234
351
|
kind: string,
|
|
235
352
|
name: string,
|
|
@@ -240,7 +357,7 @@ export async function publishedPropsOf(
|
|
|
240
357
|
typeof instance.snapshot === "function"
|
|
241
358
|
? ((await Promise.resolve(instance.snapshot())) as Record<string, unknown> | undefined)
|
|
242
359
|
: undefined;
|
|
243
|
-
|
|
360
|
+
const props = buildPublishedProps(snap, {
|
|
244
361
|
kind,
|
|
245
362
|
name,
|
|
246
363
|
module: kind.split(".")[0],
|
|
@@ -249,6 +366,8 @@ export async function publishedPropsOf(
|
|
|
249
366
|
started: startedInstances.has(instance),
|
|
250
367
|
completed: completedInstances.has(instance),
|
|
251
368
|
});
|
|
369
|
+
publishedByInstance.set(instance, props);
|
|
370
|
+
return props;
|
|
252
371
|
}
|
|
253
372
|
|
|
254
373
|
/**
|
|
@@ -381,6 +500,19 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
381
500
|
/** Resources queued for initialization on this context node. */
|
|
382
501
|
private pendingResources: ResourceManifest[] = [];
|
|
383
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Every manifest ever registered on this node, by name — kept after the
|
|
505
|
+
* resource has been created, unlike `pendingResources`, which drains.
|
|
506
|
+
*
|
|
507
|
+
* A DECLARATION-derived contract (`x-telo-schema-projection-from`) is resolved
|
|
508
|
+
* when the kernel binds a contract, which happens during create — so the
|
|
509
|
+
* pending queue has already given the target up by then. The declaration is
|
|
510
|
+
* what the projection reads (a table's `columns:`), not the instance, so this
|
|
511
|
+
* is the record it needs. Manifests only, one direction: nothing here yields
|
|
512
|
+
* an instance, and nothing here extends one's lifetime.
|
|
513
|
+
*/
|
|
514
|
+
protected readonly declaredManifests = new Map<string, ResourceManifest>();
|
|
515
|
+
|
|
384
516
|
/** Per-resource dependency names, captured at create() time — BEFORE Phase-5
|
|
385
517
|
* injection swaps refs for live instances, so the walk sees plain objects and
|
|
386
518
|
* cannot wander into a controller's (possibly cyclic) object graph. Read only
|
|
@@ -523,18 +655,17 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
523
655
|
| Record<string, unknown>
|
|
524
656
|
| undefined;
|
|
525
657
|
const kind = entry.resource.kind as string;
|
|
526
|
-
|
|
658
|
+
const props = buildPublishedProps(snap, {
|
|
659
|
+
kind,
|
|
527
660
|
name,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}),
|
|
537
|
-
);
|
|
661
|
+
module: entry.resource.metadata?.module as string | undefined,
|
|
662
|
+
statusSchema: this.statusSchemaOf(kind),
|
|
663
|
+
status: reportedStatus.get(entry.instance),
|
|
664
|
+
started: startedInstances.has(entry.instance),
|
|
665
|
+
completed: completedInstances.has(entry.instance),
|
|
666
|
+
});
|
|
667
|
+
publishedByInstance.set(entry.instance, props);
|
|
668
|
+
this.onResourceSnapshotted(name, props);
|
|
538
669
|
}
|
|
539
670
|
|
|
540
671
|
get context(): Record<string, unknown> {
|
|
@@ -580,6 +711,35 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
580
711
|
throw new RuntimeError("ERR_DUPLICATE_RESOURCE", `Resource '${name}' is already registered`);
|
|
581
712
|
}
|
|
582
713
|
this.pendingResources.push(resource);
|
|
714
|
+
this.declaredManifests.set(name, resource);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* The manifest a name was DECLARED with, resolved scope-local first and then
|
|
719
|
+
* up the enclosing chain — the order `getInstance` and the CEL `resources`
|
|
720
|
+
* layering already use, so a declaration lookup and an instance lookup agree
|
|
721
|
+
* about what a name means inside a scope.
|
|
722
|
+
*
|
|
723
|
+
* `alias` routes into an import's exported instances, the one shape a bare
|
|
724
|
+
* name cannot reach. Returns undefined rather than throwing: a caller here is
|
|
725
|
+
* typing a contract, and a name that resolves to nothing is reported by the
|
|
726
|
+
* check that owns that failure, not by a lookup.
|
|
727
|
+
*/
|
|
728
|
+
resolveDeclaredManifest(name: string, alias?: string): ResourceManifest | undefined {
|
|
729
|
+
if (alias && alias !== "Self") return this.resolveImportedManifest(alias, name);
|
|
730
|
+
let node: EvaluationContext | undefined = this;
|
|
731
|
+
while (node) {
|
|
732
|
+
const found = node.declaredManifests.get(name);
|
|
733
|
+
if (found) return found;
|
|
734
|
+
node = node.parent as EvaluationContext | undefined;
|
|
735
|
+
}
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The manifest half of {@link resolveImportedInstance}. Overridden by
|
|
740
|
+
* `ModuleContext`, which is the only node that has imports. */
|
|
741
|
+
resolveImportedManifest(alias: string, name: string): ResourceManifest | undefined {
|
|
742
|
+
return undefined;
|
|
583
743
|
}
|
|
584
744
|
|
|
585
745
|
/**
|
|
@@ -625,6 +785,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
625
785
|
this.emit,
|
|
626
786
|
);
|
|
627
787
|
child.resolveImportedInstance = (alias, name) => this.resolveImportedInstance(alias, name);
|
|
788
|
+
child.resolveImportedManifest = (alias, name) => this.resolveImportedManifest(alias, name);
|
|
628
789
|
child.kindResolver = (kind) => this.resolveKindSafe(kind);
|
|
629
790
|
return this.spawnChild(child);
|
|
630
791
|
}
|
|
@@ -1138,7 +1299,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1138
1299
|
traceId: string | undefined,
|
|
1139
1300
|
capability: "invoke" | "run" | "provide" | "request",
|
|
1140
1301
|
phase: "start" | "end",
|
|
1141
|
-
outcome:
|
|
1302
|
+
outcome: SpanOutcome | undefined,
|
|
1142
1303
|
detail: Record<string, unknown>,
|
|
1143
1304
|
): Record<string, unknown> {
|
|
1144
1305
|
return {
|
|
@@ -1192,7 +1353,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1192
1353
|
const rootScope = tracing && parentInvocationId === undefined ? this.traceRootScope() : undefined;
|
|
1193
1354
|
const span = (
|
|
1194
1355
|
phase: "start" | "end",
|
|
1195
|
-
outcome:
|
|
1356
|
+
outcome: SpanOutcome | undefined,
|
|
1196
1357
|
detail: Record<string, unknown>,
|
|
1197
1358
|
) =>
|
|
1198
1359
|
this.tracePayload(
|
|
@@ -1259,6 +1420,25 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1259
1420
|
await this.emit(`${name}.InvokeCancelled`, span("end", "cancelled", { inputs, reason }));
|
|
1260
1421
|
throw err;
|
|
1261
1422
|
}
|
|
1423
|
+
// A SUSPENSION passes through untouched, exactly as a cancellation does,
|
|
1424
|
+
// and for a sharper reason. It is not a failure — it is the run leaving —
|
|
1425
|
+
// and the chokepoint is the one swallower that cannot be worked around:
|
|
1426
|
+
// EVERY dispatch between a parking kind and the workflow that owns the run
|
|
1427
|
+
// goes through here, so wrapping it once converts a park into an
|
|
1428
|
+
// `ERR_EXECUTION_FAILED` at every hop. The latch would then catch the
|
|
1429
|
+
// corruption at the boundary, which is a loud failure where nothing was
|
|
1430
|
+
// wrong.
|
|
1431
|
+
//
|
|
1432
|
+
// Reported as its own outcome rather than as a rejection: a parked
|
|
1433
|
+
// invocation neither succeeded nor failed, and a trace that recorded it
|
|
1434
|
+
// as failed would say the run broke every time it waited.
|
|
1435
|
+
if (isSuspension(err)) {
|
|
1436
|
+
await this.emit(
|
|
1437
|
+
`${name}.InvokeParked`,
|
|
1438
|
+
span("end", "parked", { inputs, path: err.path, resource: err.resource }),
|
|
1439
|
+
);
|
|
1440
|
+
throw err;
|
|
1441
|
+
}
|
|
1262
1442
|
if (isInvokeError(err)) {
|
|
1263
1443
|
const detail = { inputs, code: err.code, message: err.message, data: err.data };
|
|
1264
1444
|
await this.emit(`${name}.InvokeRejected`, span("end", "rejected", detail));
|
|
@@ -1394,7 +1574,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1394
1574
|
};
|
|
1395
1575
|
const payload = (
|
|
1396
1576
|
phase: "start" | "end",
|
|
1397
|
-
outcome:
|
|
1577
|
+
outcome: SpanOutcome | undefined,
|
|
1398
1578
|
extra: Record<string, unknown> = {},
|
|
1399
1579
|
) =>
|
|
1400
1580
|
this.tracePayload(
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,12 @@ export {
|
|
|
35
35
|
type ResolvedControllerLayer,
|
|
36
36
|
} from "./bundle/module-artifact.js";
|
|
37
37
|
export { readOwnerManifest, type OwnerManifest } from "./bundle/module-manifest.js";
|
|
38
|
+
export {
|
|
39
|
+
buildSiblingLibraries,
|
|
40
|
+
NO_SIBLING_LIBRARIES,
|
|
41
|
+
type SiblingLibraryMap,
|
|
42
|
+
type ResolvedSiblingLibrary,
|
|
43
|
+
} from "./controller-loaders/sibling-libraries.js";
|
|
38
44
|
export {
|
|
39
45
|
buildControllerBundle,
|
|
40
46
|
canBuildFromSource,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance → the manifest it was DECLARED with, recorded at `create()` — the
|
|
5
|
+
* kernel's single instance-production site, beside where the handle is minted
|
|
6
|
+
* and the contract is bound.
|
|
7
|
+
*
|
|
8
|
+
* A contract typed from a referenced declaration
|
|
9
|
+
* (`x-telo-schema-projection-from`) is bound AFTER Phase-5 injection has already
|
|
10
|
+
* replaced the reference in the slot with the live instance, so the reference is
|
|
11
|
+
* gone by the time the contract needs it. This is what recovers the declaration
|
|
12
|
+
* behind it — and recovering it is what lets the kernel enforce exactly the
|
|
13
|
+
* schema the analyzer checked, rather than a reopened one.
|
|
14
|
+
*
|
|
15
|
+
* Its own module rather than a second table in `resource-handle.ts`, which
|
|
16
|
+
* guards an exact export list precisely so a new direction gets looked at: the
|
|
17
|
+
* rule there is that a HANDLE must never become someone else's live instance,
|
|
18
|
+
* and this is the unrelated instance→declaration direction. Weak and one-way —
|
|
19
|
+
* a manifest is obtainable FROM an instance, never an instance from a manifest —
|
|
20
|
+
* so nothing here extends a lifetime or hands out a reference to live state.
|
|
21
|
+
*/
|
|
22
|
+
const declarations = new WeakMap<object, ResourceManifest>();
|
|
23
|
+
|
|
24
|
+
/** Record what a live instance was declared with. First record wins, matching
|
|
25
|
+
* the handle rule: a `base:` child IS its parent instance, and the parent's
|
|
26
|
+
* declaration is the one that produced it. */
|
|
27
|
+
export function recordInstanceDeclaration(instance: object, manifest: ResourceManifest): void {
|
|
28
|
+
if (!declarations.has(instance)) declarations.set(instance, manifest);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The manifest a live instance was declared with, if it is one of ours. */
|
|
32
|
+
export function declarationOfInstance(instance: unknown): ResourceManifest | undefined {
|
|
33
|
+
return instance && typeof instance === "object"
|
|
34
|
+
? declarations.get(instance as object)
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ContractDirection,
|
|
3
|
+
type ProjectionScope,
|
|
3
4
|
declaredScalarPaths,
|
|
4
5
|
type DeclaredScalarForm,
|
|
5
6
|
type DeclaredScalarPath,
|
|
6
7
|
defaultBearingPaths,
|
|
7
8
|
effectiveContractField,
|
|
9
|
+
describeProjectionFailure,
|
|
10
|
+
resolveSchemaProjections,
|
|
11
|
+
type ProjectionFailure,
|
|
8
12
|
type DefResolver,
|
|
9
13
|
withLiveValuesSkipped,
|
|
10
14
|
} from "@telorun/analyzer";
|
|
@@ -13,6 +17,7 @@ import {
|
|
|
13
17
|
ERR_CONTRACT_UNRESOLVABLE,
|
|
14
18
|
ERR_INPUT_INVALID,
|
|
15
19
|
ERR_OUTPUT_INVALID,
|
|
20
|
+
ERR_SCHEMA_PROJECTION_UNRESOLVED,
|
|
16
21
|
InvokeError,
|
|
17
22
|
} from "@telorun/sdk";
|
|
18
23
|
|
|
@@ -120,6 +125,19 @@ export function resolveBoundContract(
|
|
|
120
125
|
definition: ResourceDefinition | undefined,
|
|
121
126
|
resolveDef: DefResolver,
|
|
122
127
|
factory: ContractValidatorFactory,
|
|
128
|
+
/**
|
|
129
|
+
* Resolves a DECLARATION-derived slot (`x-telo-schema-projection-from`) to the
|
|
130
|
+
* shape the referenced declaration projects to.
|
|
131
|
+
*
|
|
132
|
+
* The kernel resolves it for the same reason the analyzer does, and it must be
|
|
133
|
+
* the SAME resolution: a projected contract enforced statically and not at
|
|
134
|
+
* dispatch is a contract with a hole exactly where a value is computed rather
|
|
135
|
+
* than written — `telo check` rejects a misspelled column in a literal and the
|
|
136
|
+
* identical key arriving from a CEL expression reaches the database. Omitted
|
|
137
|
+
* only by callers with no scope to resolve against, where the annotated node
|
|
138
|
+
* stays as it was.
|
|
139
|
+
*/
|
|
140
|
+
projections?: ProjectionScope,
|
|
123
141
|
): BoundContract | undefined {
|
|
124
142
|
const own = (manifest as unknown as Record<string, unknown>)[direction];
|
|
125
143
|
const declared =
|
|
@@ -146,13 +164,43 @@ export function resolveBoundContract(
|
|
|
146
164
|
`The type is not registered, so the contract cannot be enforced.`,
|
|
147
165
|
);
|
|
148
166
|
}
|
|
149
|
-
|
|
167
|
+
let projected = schema;
|
|
168
|
+
if (projections) {
|
|
169
|
+
const failures: ProjectionFailure[] = [];
|
|
170
|
+
projected = resolveSchemaProjections(
|
|
171
|
+
schema,
|
|
172
|
+
manifest as unknown as Record<string, any>,
|
|
173
|
+
projections,
|
|
174
|
+
failures,
|
|
175
|
+
) as Record<string, any>;
|
|
176
|
+
if (failures.length > 0) {
|
|
177
|
+
// A slot that opted into a projection and got none is a DEFECT, not a
|
|
178
|
+
// default: leaving it means the contract silently reopens to the slot's
|
|
179
|
+
// own schema, which for a projected slot constrains nothing — the exact
|
|
180
|
+
// reopening this mechanism exists to prevent. The analyzer reports the
|
|
181
|
+
// same set, but only for the entry's own modules, so a dependency's
|
|
182
|
+
// consumer slot would otherwise be unreported at BOTH ends. Same rule as
|
|
183
|
+
// `ERR_CONTRACT_UNRESOLVABLE` above, for the same reason.
|
|
184
|
+
throw new InvokeError(
|
|
185
|
+
ERR_SCHEMA_PROJECTION_UNRESOLVED,
|
|
186
|
+
`declared \`${direction}\` could not be projected: ` +
|
|
187
|
+
failures.map(describeProjectionFailure).join(" ") +
|
|
188
|
+
` The slot declares 'x-telo-schema-projection-from', so leaving it unresolved ` +
|
|
189
|
+
`would enforce nothing where it promises a declared shape.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const stripped = withLiveValuesSkipped(projected, factory.resolveRef);
|
|
150
194
|
paths = defaultBearingPaths(stripped, factory.resolveRef);
|
|
151
195
|
scalars = declaredScalarPaths(stripped, factory.resolveRef);
|
|
152
196
|
// Compile by NAME whenever the declaration is one, so the type's CEL
|
|
153
197
|
// `rules:` are composed in — including when a stream had to be stripped, in
|
|
154
198
|
// which case the stream-bearing properties are dropped from the schema the
|
|
155
199
|
// named validator sees rather than the reference being abandoned.
|
|
200
|
+
// A named type compiles BY NAME so its CEL `rules:` compose in — but only
|
|
201
|
+
// while the schema is untouched. Stripping a stream or resolving a
|
|
202
|
+
// projection both produce a different schema, so both take the
|
|
203
|
+
// rules-preserving path rather than the bare name.
|
|
156
204
|
compiled = !isNamedTypeReference(declared)
|
|
157
205
|
? factory(stripped)
|
|
158
206
|
: stripped === schema
|
package/src/kernel.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ContractValidatorFactory,
|
|
18
18
|
resolveBoundContract,
|
|
19
19
|
} from "./invocation-contract-binding.js";
|
|
20
|
+
import { readProjectionRef, type ProjectionScope } from "@telorun/analyzer";
|
|
20
21
|
import {
|
|
21
22
|
ControllerContext,
|
|
22
23
|
ControllerPolicy,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
ResourceDefinition,
|
|
27
28
|
ResourceInstance,
|
|
28
29
|
ResourceManifest,
|
|
30
|
+
stampRefIdentity,
|
|
29
31
|
RuntimeError,
|
|
30
32
|
RuntimeEvent,
|
|
31
33
|
type BootTarget,
|
|
@@ -49,6 +51,7 @@ import { ambientInvokeContext } from "./evaluation-context.js";
|
|
|
49
51
|
import { ModuleContext } from "./module-context.js";
|
|
50
52
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
51
53
|
import { mintResourceHandle } from "./resource-handle.js";
|
|
54
|
+
import { declarationOfInstance, recordInstanceDeclaration } from "./instance-declaration.js";
|
|
52
55
|
import { nodeHostVersions } from "./host-versions.js";
|
|
53
56
|
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
54
57
|
import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
|
|
@@ -1417,6 +1420,18 @@ export class Kernel implements IKernel {
|
|
|
1417
1420
|
resolvedKind,
|
|
1418
1421
|
(processedResource.metadata?.name as string | undefined) ?? "<unnamed>",
|
|
1419
1422
|
);
|
|
1423
|
+
recordInstanceDeclaration(instance, processedResource);
|
|
1424
|
+
// Stamp the DECLARATION SITE here rather than at Phase-5 injection, because
|
|
1425
|
+
// this is the only point where the instance and the context that declared it
|
|
1426
|
+
// are both in hand: injection sees the consumer's context, not the target's,
|
|
1427
|
+
// so a module stamped there would be whoever referenced it. Without it a
|
|
1428
|
+
// durable step could not name its target anywhere the instance does not
|
|
1429
|
+
// exist — the whole reason `step()` takes an identity at all.
|
|
1430
|
+
const originPointer = originPointerOf(processedResource);
|
|
1431
|
+
stampRefIdentity(instance, resolvedKind, processedResource.metadata?.name ?? "<unnamed>", {
|
|
1432
|
+
...(moduleCtx?.source ? { module: moduleCtx.source } : {}),
|
|
1433
|
+
...(originPointer === undefined ? {} : { pointer: originPointer }),
|
|
1434
|
+
});
|
|
1420
1435
|
(ctx as ResourceContextImpl).bindResourceIdentity(handle, resolvedKind, processedResource);
|
|
1421
1436
|
|
|
1422
1437
|
// Bind the resolved invocation contract to the instance, here at the kernel's
|
|
@@ -1491,8 +1506,43 @@ export class Kernel implements IKernel {
|
|
|
1491
1506
|
const resolveDef: DefResolver = (kind, from) =>
|
|
1492
1507
|
this.registry.resolveDefinitionIn(kind, from?.metadata?.module);
|
|
1493
1508
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1509
|
+
// A DECLARATION-derived slot is resolved against the context that OWNS this
|
|
1510
|
+
// resource, so a bare name means the same thing here as it does to `!ref`
|
|
1511
|
+
// and to CEL — scope-local first, enclosing module as the fallback — and an
|
|
1512
|
+
// alias routes into that import's exported instances.
|
|
1513
|
+
const projections: ProjectionScope = {
|
|
1514
|
+
resolveDefinition: (kind) =>
|
|
1515
|
+
this.controllers.getDefinition(kind) as unknown as Record<string, any> | undefined,
|
|
1516
|
+
// The slot holds the LIVE INSTANCE by now — Phase-5 injection runs before
|
|
1517
|
+
// create — so the declaration is recovered by instance identity. The ref
|
|
1518
|
+
// shape is still accepted, because a ref slot on a `with:`-scoped resource
|
|
1519
|
+
// is not an injection site and reaches the controller unresolved.
|
|
1520
|
+
resolveManifest: (value) => {
|
|
1521
|
+
const injected = declarationOfInstance(value);
|
|
1522
|
+
if (injected) return { manifest: injected as unknown as Record<string, any> };
|
|
1523
|
+
const ref = readProjectionRef(value);
|
|
1524
|
+
if (!ref) return undefined;
|
|
1525
|
+
const found = impl.resolveDeclaredManifest(ref.name, ref.alias);
|
|
1526
|
+
return found ? { manifest: found as unknown as Record<string, any> } : undefined;
|
|
1527
|
+
},
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
const input = resolveBoundContract(
|
|
1531
|
+
"inputType",
|
|
1532
|
+
resource,
|
|
1533
|
+
definition,
|
|
1534
|
+
resolveDef,
|
|
1535
|
+
factory,
|
|
1536
|
+
projections,
|
|
1537
|
+
);
|
|
1538
|
+
const output = resolveBoundContract(
|
|
1539
|
+
"outputType",
|
|
1540
|
+
resource,
|
|
1541
|
+
definition,
|
|
1542
|
+
resolveDef,
|
|
1543
|
+
factory,
|
|
1544
|
+
projections,
|
|
1545
|
+
);
|
|
1496
1546
|
if (!input && !output) return;
|
|
1497
1547
|
|
|
1498
1548
|
bindContract(instance, {
|
|
@@ -1576,3 +1626,28 @@ export class Kernel implements IKernel {
|
|
|
1576
1626
|
);
|
|
1577
1627
|
}
|
|
1578
1628
|
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* The JSON pointer to an inline declaration, from the origin the loader stamped.
|
|
1632
|
+
*
|
|
1633
|
+
* `xTeloOrigin.pathFromParent` is a dotted path with `[N]` indices — the loader's
|
|
1634
|
+
* own spelling, which addresses the manifest tree and nothing else. A pointer is
|
|
1635
|
+
* what crosses a boundary, so the conversion happens here rather than being left
|
|
1636
|
+
* to each recipient to reinvent: segments are identifiers and indices, so the
|
|
1637
|
+
* rewrite is mechanical and total.
|
|
1638
|
+
*/
|
|
1639
|
+
function originPointerOf(resource: ResourceManifest): string | undefined {
|
|
1640
|
+
const origin = resource.metadata?.xTeloOrigin as
|
|
1641
|
+
| { parentName?: unknown; pathFromParent?: unknown }
|
|
1642
|
+
| undefined;
|
|
1643
|
+
if (typeof origin?.parentName !== "string" || typeof origin.pathFromParent !== "string") {
|
|
1644
|
+
return undefined;
|
|
1645
|
+
}
|
|
1646
|
+
const pointer = origin.pathFromParent
|
|
1647
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
1648
|
+
.split(".")
|
|
1649
|
+
.filter((segment) => segment.length > 0)
|
|
1650
|
+
.map((segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1"))
|
|
1651
|
+
.join("/");
|
|
1652
|
+
return `${origin.parentName}#/${pointer}`;
|
|
1653
|
+
}
|
package/src/manifest-schemas.ts
CHANGED
package/src/module-context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
InvokeStepContext,
|
|
16
16
|
ModuleContext as IModuleContext,
|
|
17
17
|
ResourceInstance,
|
|
18
|
+
ResourceManifest,
|
|
18
19
|
} from "@telorun/sdk";
|
|
19
20
|
import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
|
|
20
21
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
@@ -106,7 +107,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
106
107
|
names: Set<string>;
|
|
107
108
|
terminal: (
|
|
108
109
|
name: string,
|
|
109
|
-
) =>
|
|
110
|
+
) =>
|
|
111
|
+
| (() =>
|
|
112
|
+
| { kind: string; instance: ResourceInstance; manifest?: ResourceManifest }
|
|
113
|
+
| undefined)
|
|
114
|
+
| undefined;
|
|
110
115
|
}
|
|
111
116
|
>();
|
|
112
117
|
|
|
@@ -292,7 +297,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
292
297
|
names: string[],
|
|
293
298
|
terminal: (
|
|
294
299
|
name: string,
|
|
295
|
-
) =>
|
|
300
|
+
) =>
|
|
301
|
+
| (() =>
|
|
302
|
+
| { kind: string; instance: ResourceInstance; manifest?: ResourceManifest }
|
|
303
|
+
| undefined)
|
|
304
|
+
| undefined,
|
|
296
305
|
): void {
|
|
297
306
|
this.importedScopes.set(alias, { names: new Set(names), terminal });
|
|
298
307
|
}
|
|
@@ -369,7 +378,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
369
378
|
const kind = rawKind.startsWith("Self.")
|
|
370
379
|
? `${moduleName}.${rawKind.slice("Self.".length)}`
|
|
371
380
|
: rawKind;
|
|
372
|
-
|
|
381
|
+
// The manifest rides along because a DECLARATION-derived contract
|
|
382
|
+
// (`x-telo-schema-projection-from`) reads the target's own declaration,
|
|
383
|
+
// and a re-export forwards this getter verbatim — so carrying it here
|
|
384
|
+
// makes it reachable at any depth with no second table.
|
|
385
|
+
return { kind, instance: inst.instance, manifest: inst.resource };
|
|
373
386
|
});
|
|
374
387
|
}
|
|
375
388
|
for (const k of kindEntries) {
|
|
@@ -407,6 +420,14 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
407
420
|
return scope.terminal(name)?.()?.instance;
|
|
408
421
|
}
|
|
409
422
|
|
|
423
|
+
/** The manifest half of `resolveImportedInstance` — what a name was DECLARED
|
|
424
|
+
* with, for a contract typed from a referenced declaration. */
|
|
425
|
+
override resolveImportedManifest(alias: string, name: string): ResourceManifest | undefined {
|
|
426
|
+
const scope = this.importedScopes.get(alias);
|
|
427
|
+
if (!scope || !scope.names.has(name)) return undefined;
|
|
428
|
+
return scope.terminal(name)?.()?.manifest;
|
|
429
|
+
}
|
|
430
|
+
|
|
410
431
|
/** Like `resolveImportedInstance`, but returns the `{kind, name}` ref (canonical kind)
|
|
411
432
|
* for controllers that resolve step/handler invokes to refs rather than live instances
|
|
412
433
|
* (e.g. Run.Sequence via `ensureKindRef`). The alias is reattached by the caller. */
|
package/src/resource-context.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ResourceHandle,
|
|
27
27
|
type RuntimeSeam,
|
|
28
28
|
type TypeRule,
|
|
29
|
+
type OpenZoneAttributes,
|
|
29
30
|
type ZoneEntry,
|
|
30
31
|
} from "@telorun/sdk";
|
|
31
32
|
import { registerTeloKeywords } from "@telorun/analyzer";
|
|
@@ -426,6 +427,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
426
427
|
return this.zoneContext().zonesFor(instance, ctx);
|
|
427
428
|
}
|
|
428
429
|
|
|
430
|
+
zoneAttributes(ctx?: InvokeContext): readonly OpenZoneAttributes[] {
|
|
431
|
+
return this.zoneContext().zoneAttributes(ctx);
|
|
432
|
+
}
|
|
433
|
+
|
|
429
434
|
/** The root context for runtime-driven inbound work — inherits nothing from
|
|
430
435
|
* whatever ambient happens to be live at the registration site. */
|
|
431
436
|
rootContext(opts?: { cancellation?: CancellationSource }): InvokeContext {
|
|
@@ -514,6 +519,19 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
514
519
|
return this.moduleContext.resolveImportedInstance(alias, name);
|
|
515
520
|
}
|
|
516
521
|
|
|
522
|
+
/**
|
|
523
|
+
* The manifest a name was DECLARED with, resolved in the context that OWNS
|
|
524
|
+
* this resource — scope-local first, enclosing module as the fallback, an
|
|
525
|
+
* alias routing into that import's exports. The same order `resolveRef` and
|
|
526
|
+
* the CEL `resources` layering use, so a declaration lookup cannot disagree
|
|
527
|
+
* with them about what a name means.
|
|
528
|
+
*
|
|
529
|
+
* Used to resolve a DECLARATION-derived contract slot at bind time.
|
|
530
|
+
*/
|
|
531
|
+
resolveDeclaredManifest(name: string, alias?: string): ResourceManifest | undefined {
|
|
532
|
+
return this.owningContext.resolveDeclaredManifest?.(name, alias);
|
|
533
|
+
}
|
|
534
|
+
|
|
517
535
|
resolveRef<T>(
|
|
518
536
|
value: unknown,
|
|
519
537
|
guard: (candidate: unknown) => candidate is T,
|