@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.
Files changed (87) hide show
  1. package/dist/bundle/module-artifact.d.ts +2 -1
  2. package/dist/bundle/module-artifact.d.ts.map +1 -1
  3. package/dist/bundle/module-artifact.js +14 -10
  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 +71 -63
  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 +2 -1
  14. package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
  15. package/dist/controller-loaders/napi-loader.js +4 -1
  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 +13 -7
  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 +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -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 +60 -3
  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/module-context.d.ts +5 -1
  58. package/dist/module-context.d.ts.map +1 -1
  59. package/dist/module-context.js +13 -1
  60. package/dist/module-context.js.map +1 -1
  61. package/dist/resource-context.d.ts +12 -1
  62. package/dist/resource-context.d.ts.map +1 -1
  63. package/dist/resource-context.js +15 -0
  64. package/dist/resource-context.js.map +1 -1
  65. package/dist/zone-context.d.ts +16 -1
  66. package/dist/zone-context.d.ts.map +1 -1
  67. package/dist/zone-context.js +61 -1
  68. package/dist/zone-context.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/bundle/module-artifact.ts +25 -8
  71. package/src/controller-loader.ts +114 -80
  72. package/src/controller-loaders/bundle-loader.ts +10 -2
  73. package/src/controller-loaders/napi-loader.ts +6 -0
  74. package/src/controller-loaders/npm-loader.ts +14 -4
  75. package/src/controller-loaders/source-bundle-builder.ts +6 -0
  76. package/src/controllers/resource-definition/resource-definition-controller.ts +7 -3
  77. package/src/controllers/resource-definition/resource-template-controller.ts +12 -5
  78. package/src/dependency-injection.ts +7 -7
  79. package/src/evaluation-context.ts +195 -15
  80. package/src/index.ts +6 -0
  81. package/src/instance-declaration.ts +36 -0
  82. package/src/invocation-contract-binding.ts +49 -1
  83. package/src/kernel.ts +77 -2
  84. package/src/manifest-schemas.ts +1 -0
  85. package/src/module-context.ts +24 -3
  86. package/src/resource-context.ts +18 -0
  87. 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
- 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
@@ -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
- 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
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
- const input = resolveBoundContract("inputType", resource, definition, resolveDef, factory);
1495
- const output = resolveBoundContract("outputType", resource, definition, resolveDef, factory);
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
+ }
@@ -22,6 +22,7 @@ export {
22
22
  manifestFragmentRef,
23
23
  ResourceRefSchema,
24
24
  RetryPolicySchema,
25
+ StepSchema,
25
26
  withSchemaFragments,
26
27
  } from "@telorun/analyzer";
27
28
 
@@ -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
- ) => (() => { kind: string; instance: ResourceInstance } | undefined) | undefined;
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
- ) => (() => { kind: string; instance: ResourceInstance } | undefined) | undefined,
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
- return { kind, instance: inst.instance };
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. */
@@ -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,