@telorun/kernel 0.78.0 → 0.80.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 (106) hide show
  1. package/dist/bundle/module-artifact.d.ts +15 -2
  2. package/dist/bundle/module-artifact.d.ts.map +1 -1
  3. package/dist/bundle/module-artifact.js +36 -13
  4. package/dist/bundle/module-artifact.js.map +1 -1
  5. package/dist/controller-loader.d.ts +62 -9
  6. package/dist/controller-loader.d.ts.map +1 -1
  7. package/dist/controller-loader.js +72 -64
  8. package/dist/controller-loader.js.map +1 -1
  9. package/dist/controller-loaders/bundle-loader.d.ts +2 -2
  10. package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
  11. package/dist/controller-loaders/bundle-loader.js +9 -3
  12. package/dist/controller-loaders/bundle-loader.js.map +1 -1
  13. package/dist/controller-loaders/napi-loader.d.ts +22 -1
  14. package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
  15. package/dist/controller-loaders/napi-loader.js +35 -5
  16. package/dist/controller-loaders/napi-loader.js.map +1 -1
  17. package/dist/controller-loaders/npm-loader.d.ts +2 -1
  18. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  19. package/dist/controller-loaders/npm-loader.js +81 -11
  20. package/dist/controller-loaders/npm-loader.js.map +1 -1
  21. package/dist/controller-loaders/source-bundle-builder.d.ts +2 -1
  22. package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -1
  23. package/dist/controller-loaders/source-bundle-builder.js +4 -1
  24. package/dist/controller-loaders/source-bundle-builder.js.map +1 -1
  25. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  26. package/dist/controllers/resource-definition/resource-definition-controller.js +7 -3
  27. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  28. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
  29. package/dist/controllers/resource-definition/resource-template-controller.js +12 -5
  30. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
  31. package/dist/dependency-injection.d.ts.map +1 -1
  32. package/dist/dependency-injection.js +7 -7
  33. package/dist/dependency-injection.js.map +1 -1
  34. package/dist/evaluation-context.d.ts +33 -13
  35. package/dist/evaluation-context.d.ts.map +1 -1
  36. package/dist/evaluation-context.js +145 -4
  37. package/dist/evaluation-context.js.map +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/instance-declaration.d.ts +8 -0
  43. package/dist/instance-declaration.d.ts.map +1 -0
  44. package/dist/instance-declaration.js +34 -0
  45. package/dist/instance-declaration.js.map +1 -0
  46. package/dist/invocation-contract-binding.d.ts +15 -2
  47. package/dist/invocation-contract-binding.d.ts.map +1 -1
  48. package/dist/invocation-contract-binding.js +39 -4
  49. package/dist/invocation-contract-binding.js.map +1 -1
  50. package/dist/kernel.d.ts.map +1 -1
  51. package/dist/kernel.js +78 -14
  52. package/dist/kernel.js.map +1 -1
  53. package/dist/manifest-schemas.d.ts +1 -1
  54. package/dist/manifest-schemas.d.ts.map +1 -1
  55. package/dist/manifest-schemas.js +1 -1
  56. package/dist/manifest-schemas.js.map +1 -1
  57. package/dist/manifest-sources/analysis-stamp.d.ts +12 -8
  58. package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -1
  59. package/dist/manifest-sources/analysis-stamp.js +50 -23
  60. package/dist/manifest-sources/analysis-stamp.js.map +1 -1
  61. package/dist/manifest-sources/local-manifest-cache-source.d.ts +45 -9
  62. package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
  63. package/dist/manifest-sources/local-manifest-cache-source.js +65 -12
  64. package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
  65. package/dist/module-context.d.ts +5 -1
  66. package/dist/module-context.d.ts.map +1 -1
  67. package/dist/module-context.js +13 -1
  68. package/dist/module-context.js.map +1 -1
  69. package/dist/resource-context.d.ts +12 -1
  70. package/dist/resource-context.d.ts.map +1 -1
  71. package/dist/resource-context.js +15 -0
  72. package/dist/resource-context.js.map +1 -1
  73. package/dist/runtime-seam.d.ts.map +1 -1
  74. package/dist/runtime-seam.js +9 -1
  75. package/dist/runtime-seam.js.map +1 -1
  76. package/dist/workspace-marker.d.ts +37 -0
  77. package/dist/workspace-marker.d.ts.map +1 -0
  78. package/dist/workspace-marker.js +68 -0
  79. package/dist/workspace-marker.js.map +1 -0
  80. package/dist/zone-context.d.ts +16 -1
  81. package/dist/zone-context.d.ts.map +1 -1
  82. package/dist/zone-context.js +61 -1
  83. package/dist/zone-context.js.map +1 -1
  84. package/package.json +3 -3
  85. package/src/bundle/module-artifact.ts +47 -15
  86. package/src/controller-loader.ts +115 -81
  87. package/src/controller-loaders/bundle-loader.ts +10 -2
  88. package/src/controller-loaders/napi-loader.ts +42 -3
  89. package/src/controller-loaders/npm-loader.ts +86 -8
  90. package/src/controller-loaders/source-bundle-builder.ts +6 -0
  91. package/src/controllers/resource-definition/resource-definition-controller.ts +7 -3
  92. package/src/controllers/resource-definition/resource-template-controller.ts +12 -5
  93. package/src/dependency-injection.ts +7 -7
  94. package/src/evaluation-context.ts +195 -15
  95. package/src/index.ts +7 -0
  96. package/src/instance-declaration.ts +36 -0
  97. package/src/invocation-contract-binding.ts +49 -1
  98. package/src/kernel.ts +95 -14
  99. package/src/manifest-schemas.ts +1 -0
  100. package/src/manifest-sources/analysis-stamp.ts +53 -25
  101. package/src/manifest-sources/local-manifest-cache-source.ts +68 -13
  102. package/src/module-context.ts +24 -3
  103. package/src/resource-context.ts +18 -0
  104. package/src/runtime-seam.ts +9 -1
  105. package/src/workspace-marker.ts +68 -0
  106. package/src/zone-context.ts +70 -1
@@ -7,6 +7,7 @@ import type {
7
7
  } from "@telorun/sdk";
8
8
  import { isCompiledValue } from "@telorun/sdk";
9
9
  import { isRefSentinel } from "@telorun/templating";
10
+ import { celSelfView } from "../../evaluation-context.js";
10
11
 
11
12
  /** CEL variables that are only bound at call time (request handling, step
12
13
  * chaining, error branches) — never at a template's init(). A persistent
@@ -168,7 +169,13 @@ export function createTemplateController(definition: {
168
169
  for (const key of path[1].split(".").slice(1)) cur = cur?.[key];
169
170
  return cur;
170
171
  }
171
- return definingContext.expandWith(value, { self: getSelf() });
172
+ // CEL cannot read a member off a live instance, and a ref slot holds
173
+ // one after Phase-5 injection. `celSelfView` replaces each with its
174
+ // published reading, so `self.<ref>.<field>` answers exactly as
175
+ // `resources.<name>.<field>` does. The pure-`self.<path>` form above
176
+ // is navigated directly and still yields the instance itself, which is
177
+ // what a ref slot passed straight through (`connection:`) needs.
178
+ return definingContext.expandWith(value, { self: celSelfView(getSelf()) });
172
179
  }
173
180
  if (isRefSentinel(value)) {
174
181
  const source = value.source;
@@ -215,10 +222,10 @@ export function createTemplateController(definition: {
215
222
  throw capabilityError(entry, invokeTarget, "invoke", "Telo.Invocable");
216
223
  }
217
224
  const invokeInputs =
218
- definition.inputs != null ? expand(definition.inputs, { self: getSelf(), inputs }) : inputs;
225
+ definition.inputs != null ? expand(definition.inputs, { self: celSelfView(getSelf()), inputs }) : inputs;
219
226
  const raw = await entry.instance.invoke(invokeInputs);
220
227
  if (definition.result == null) return raw;
221
- return expand(definition.result, { self: getSelf(), result: raw });
228
+ return expand(definition.result, { self: celSelfView(getSelf()), result: raw });
222
229
  },
223
230
  }),
224
231
 
@@ -239,10 +246,10 @@ export function createTemplateController(definition: {
239
246
  throw capabilityError(entry, provideTarget, "provide", "Telo.Invocable");
240
247
  }
241
248
  const provideInputs: any =
242
- definition.inputs != null ? expand(definition.inputs, { self: getSelf() }) : {};
249
+ definition.inputs != null ? expand(definition.inputs, { self: celSelfView(getSelf()) }) : {};
243
250
  const raw = await entry.instance.invoke(provideInputs);
244
251
  if (definition.result == null) return raw;
245
- return expand(definition.result, { self: getSelf(), result: raw });
252
+ return expand(definition.result, { self: celSelfView(getSelf()), result: raw });
246
253
  },
247
254
  }),
248
255
 
@@ -1,4 +1,4 @@
1
- import { ResourceInstance, ResourceManifest, RuntimeError, stampRefIdentity } from "@telorun/sdk";
1
+ import { ResourceInstance, ResourceManifest, RuntimeError } from "@telorun/sdk";
2
2
 
3
3
  /**
4
4
  * Walks `resource` following `fieldPath` (dot notation, `[]` = array traversal,
@@ -41,12 +41,12 @@ export function injectAtPath(
41
41
  `Local reference '${String(ref.name)}' is registered but not initialized yet (deferring to a later init pass)`,
42
42
  );
43
43
  }
44
- // Tag the instance with the kind+name it resolved from, so a consumer that
45
- // holds only the bare instance (an invoke-step target) can dispatch it
46
- // through the traced chokepoint rather than calling `.invoke()` directly.
47
- if (instance && typeof ref.kind === "string" && typeof ref.name === "string") {
48
- stampRefIdentity(instance, ref.kind, ref.name);
49
- }
44
+ // The identity is NOT stamped here. It is stamped at `create()`, the single
45
+ // instance-production site, which is also the only point where an instance
46
+ // and the context that DECLARED it are both in hand — here the context is
47
+ // the CONSUMER's, so a declaration site derived at this point would name
48
+ // whoever referenced the resource. A second write-once stamp competing for
49
+ // the same property would silently decide that by init order.
50
50
  return instance;
51
51
  }
52
52
 
@@ -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
- return buildPublishedProps(snap, {
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
- this.onResourceSnapshotted(
658
+ const props = buildPublishedProps(snap, {
659
+ kind,
527
660
  name,
528
- buildPublishedProps(snap, {
529
- kind,
530
- name,
531
- module: entry.resource.metadata?.module as string | undefined,
532
- statusSchema: this.statusSchemaOf(kind),
533
- status: reportedStatus.get(entry.instance),
534
- started: startedInstances.has(entry.instance),
535
- completed: completedInstances.has(entry.instance),
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: "ok" | "failed" | "rejected" | "cancelled" | undefined,
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: "ok" | "failed" | "rejected" | "cancelled" | undefined,
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: "ok" | "failed" | "rejected" | "cancelled" | undefined,
1577
+ outcome: SpanOutcome | undefined,
1398
1578
  extra: Record<string, unknown> = {},
1399
1579
  ) =>
1400
1580
  this.tracePayload(
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  writeManifestCache,
11
11
  } from "./manifest-sources/local-manifest-cache-source.js";
12
12
  export { MemorySource } from "./manifest-sources/memory-source.js";
13
+ export { WORKSPACE_FILENAME, findWorkspaceRoot, realPath } from "./workspace-marker.js";
13
14
  export type { Transport } from "./transports/transport.js";
14
15
  export { RegistryTransport } from "./transports/registry-transport.js";
15
16
  export { OciTransport } from "./transports/oci/oci-transport.js";
@@ -35,6 +36,12 @@ export {
35
36
  type ResolvedControllerLayer,
36
37
  } from "./bundle/module-artifact.js";
37
38
  export { readOwnerManifest, type OwnerManifest } from "./bundle/module-manifest.js";
39
+ export {
40
+ buildSiblingLibraries,
41
+ NO_SIBLING_LIBRARIES,
42
+ type SiblingLibraryMap,
43
+ type ResolvedSiblingLibrary,
44
+ } from "./controller-loaders/sibling-libraries.js";
38
45
  export {
39
46
  buildControllerBundle,
40
47
  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
- const stripped = withLiveValuesSkipped(schema, factory.resolveRef);
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