@telorun/kernel 0.66.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/controller-loader.d.ts +7 -5
  2. package/dist/controller-loader.d.ts.map +1 -1
  3. package/dist/controller-loader.js +7 -5
  4. package/dist/controller-loader.js.map +1 -1
  5. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  6. package/dist/controllers/resource-definition/resource-definition-controller.js +29 -8
  7. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  8. package/dist/evaluation-context.d.ts +6 -0
  9. package/dist/evaluation-context.d.ts.map +1 -1
  10. package/dist/evaluation-context.js +20 -8
  11. package/dist/evaluation-context.js.map +1 -1
  12. package/dist/kernel.d.ts.map +1 -1
  13. package/dist/kernel.js +13 -2
  14. package/dist/kernel.js.map +1 -1
  15. package/dist/manifest-schemas.d.ts +1 -1
  16. package/dist/manifest-schemas.d.ts.map +1 -1
  17. package/dist/manifest-schemas.js +6 -0
  18. package/dist/manifest-schemas.js.map +1 -1
  19. package/dist/module-context.d.ts.map +1 -1
  20. package/dist/module-context.js +3 -4
  21. package/dist/module-context.js.map +1 -1
  22. package/dist/resource-context.d.ts +16 -2
  23. package/dist/resource-context.d.ts.map +1 -1
  24. package/dist/resource-context.js +58 -3
  25. package/dist/resource-context.js.map +1 -1
  26. package/dist/resource-handle.d.ts +11 -0
  27. package/dist/resource-handle.d.ts.map +1 -0
  28. package/dist/resource-handle.js +32 -0
  29. package/dist/resource-handle.js.map +1 -0
  30. package/dist/runtime-seam.d.ts.map +1 -1
  31. package/dist/runtime-seam.js +8 -2
  32. package/dist/runtime-seam.js.map +1 -1
  33. package/dist/zone-context.d.ts +94 -0
  34. package/dist/zone-context.d.ts.map +1 -0
  35. package/dist/zone-context.js +272 -0
  36. package/dist/zone-context.js.map +1 -0
  37. package/package.json +4 -4
  38. package/src/controller-loader.ts +7 -5
  39. package/src/controllers/resource-definition/resource-definition-controller.ts +31 -13
  40. package/src/evaluation-context.ts +21 -7
  41. package/src/kernel.ts +17 -1
  42. package/src/manifest-schemas.ts +6 -0
  43. package/src/module-context.ts +3 -4
  44. package/src/resource-context.ts +98 -2
  45. package/src/resource-handle.ts +35 -0
  46. package/src/runtime-seam.ts +9 -1
  47. package/src/zone-context.ts +337 -0
@@ -183,14 +183,16 @@ export class ControllerLoader {
183
183
  * Resolve a controller without importing it: pick the first candidate this
184
184
  * environment can host (same ordering + env-missing fallback as {@link load}),
185
185
  * verify it's present, and return a {@link ResolvedController} whose
186
- * `importInstance` defers the actual import/eval. Used by lazy controller
187
- * loading so a `Telo.Definition` fails fast at boot when its controller can't
188
- * load at all, while the expensive import is paid only on first instantiation.
186
+ * `importInstance` defers the actual import/eval. Lazy controller loading
187
+ * calls this from the kind's first instantiation — never at boot so a
188
+ * definition whose candidate list nothing in this environment can host
189
+ * registers fine and errors only when a resource of it is declared (matching
190
+ * the Rust kernel's deferral).
189
191
  *
190
192
  * Silent by design — no lifecycle events here; the caller emits
191
193
  * ControllerLoading/Loaded around `importInstance` so the events fire when the
192
- * load actually happens. A total resolution failure throws (the boot-time
193
- * fail-fast), mirroring {@link load}'s aggregated error.
194
+ * load actually happens. A total resolution failure throws, mirroring
195
+ * {@link load}'s aggregated error.
194
196
  */
195
197
  async resolve(
196
198
  purlCandidates: string[],
@@ -161,32 +161,39 @@ class ResourceDefinition implements ResourceInstance {
161
161
  cacheRoot: host.getCacheRoot?.(),
162
162
  log: ctx.log,
163
163
  });
164
- // Eager resolve — verify the controller is hostable now (so a broken
165
- // `controllers:` candidate fails fast at boot), but defer the expensive
166
- // import/eval and the controller's `register()` to the kind's first
167
- // instantiation. Definitions whose kind is never instantiated never import.
168
164
  // The artifact of the module that DECLARED this kind — a bundled controller
169
165
  // ships in its own module's payload, not the consumer's. It owns the pinned
170
166
  // ref and the verified layer index, so the loader picks a candidate and asks
171
167
  // it for that selector's directory rather than fetching anything itself.
172
168
  const artifact = host.getModuleArtifact?.(this.resource.metadata.source);
173
- const resolved = await loader.resolve(
174
- this.resource.controllers,
175
- this.resource.metadata.source,
176
- ctx.getControllerPolicy(),
177
- artifact,
178
- );
179
169
  ctx.registerDefinition(this.resource);
180
170
 
181
171
  const moduleName = this.resource.metadata.module;
182
172
  const kindName = this.resource.metadata.name;
183
- // Emitted here (not in the loader) so ControllerLoading / ControllerLoaded /
184
- // ControllerLoadFailed and the import duration — surface when the load
185
- // actually happens (first instantiation), with the resolved PURL + source.
173
+ const controllers = this.resource.controllers;
174
+ const source = this.resource.metadata.source;
175
+ const policy = ctx.getControllerPolicy();
176
+ // Resolution AND import are deferred to the kind's first instantiation,
177
+ // matching the Rust kernel: a definition whose candidate list nothing in
178
+ // this environment can host registers fine and errors — naming the kind —
179
+ // only when a resource of it is declared. That is what lets both kernels
180
+ // load a partially-covered module (e.g. console's stream kinds with no Rust
181
+ // controller) instead of rejecting it over kinds nobody uses.
182
+ // Lifecycle events are emitted here (not in the loader) so ControllerLoading
183
+ // / ControllerLoaded / ControllerLoadFailed — and the import duration —
184
+ // surface when the load actually happens, with the resolved PURL + source.
186
185
  host.registerLazyController(
187
186
  moduleName,
188
187
  kindName,
189
188
  async () => {
189
+ const resolved = await loader
190
+ .resolve(controllers, source, policy, artifact)
191
+ .catch((err) => {
192
+ if (err instanceof RuntimeError) {
193
+ throw new RuntimeError(err.code, `kind '${moduleName}.${kindName}': ${err.message}`);
194
+ }
195
+ throw err;
196
+ });
190
197
  await ctx.emit("ControllerLoading", { purl: resolved.purl });
191
198
  const startedAt = Date.now();
192
199
  const instance = await resolved.importInstance().catch(async (err) => {
@@ -241,6 +248,17 @@ export function register(ctx: ControllerContext): void {
241
248
  }
242
249
 
243
250
  export async function create(resource: any, ctx: ResourceContext): Promise<ResourceDefinition> {
251
+ // Named preflight for the one capability the schema rejects by omission: the
252
+ // AJV oneOf failure it produces never says WHY, and this is the mistake an
253
+ // author migrating a multi-kind slot is most likely to make.
254
+ if (resource?.capability === "Telo.Executable") {
255
+ throw new Error(
256
+ `Invalid ResourceDefinition "${resource.metadata?.name}": capability 'Telo.Executable' ` +
257
+ `is an x-telo-ref slot constraint (the parent Telo.Invocable and Telo.Runnable ` +
258
+ `extend), not a declarable lifecycle role. Declare 'Telo.Invocable' (invoke) or ` +
259
+ `'Telo.Runnable' (run) instead.`,
260
+ );
261
+ }
244
262
  // Validate incoming resource definition against schema
245
263
  if (!validateResourceDefinition(resource)) {
246
264
  throw new Error(
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { formatSpanCounter } from "./logging/span-id.js";
3
3
  import {
4
+ deriveContext,
4
5
  getRefIdentity,
5
6
  isCompiledValue,
6
7
  isInvokeError,
@@ -263,6 +264,15 @@ export function ambientInvokeContext(): InvokeContext | undefined {
263
264
  return cancellationStore.getStore();
264
265
  }
265
266
 
267
+ /**
268
+ * Establish `ctx` as the ambient invocation context for the duration of `fn`.
269
+ * The seam `withZone` uses to make an opened zone ambient without owning the
270
+ * store — the store itself stays kernel-internal, never on the SDK surface.
271
+ */
272
+ export function runWithAmbientContext<T>(ctx: InvokeContext, fn: () => T): T {
273
+ return cancellationStore.run(ctx, fn);
274
+ }
275
+
266
276
  /** Marks a scope built by {@link EvaluationContext.bindScope}, whose properties
267
277
  * are getters. `expandWith` merges such a scope by descriptor rather than by
268
278
  * value — reading the value here is what a lazy binding must not do. */
@@ -1189,11 +1199,13 @@ export class EvaluationContext implements IEvaluationContext {
1189
1199
  outcome,
1190
1200
  phase === "end" && rootScope ? { ...detail, context: rootScope } : detail,
1191
1201
  );
1192
- // When tracing, a fresh context carries the new id down the tree so nested
1202
+ // When tracing, a derived context carries the new id down the tree so nested
1193
1203
  // invokes read it as their parent; it is never `=== ambient`, so the call
1194
- // always (re)establishes the ALS scope.
1204
+ // always (re)establishes the ALS scope. Derived, never a fresh literal: a
1205
+ // literal drops every field it does not restate (`zones`), making
1206
+ // propagation differ between tracing on and off.
1195
1207
  const invokeCtx: InvokeContext = tracing
1196
- ? { cancellation: token, invocationId, parentInvocationId, traceId }
1208
+ ? deriveContext(baseCtx, { invocationId, parentInvocationId, traceId })
1197
1209
  : baseCtx;
1198
1210
 
1199
1211
  // Pre-dispatch gate: a sub-invoke reached after the tree was cancelled is
@@ -1391,12 +1403,14 @@ export class EvaluationContext implements IEvaluationContext {
1391
1403
  );
1392
1404
 
1393
1405
  await this.emit(`${opts.ref.name}.Requesting`, payload("start", undefined));
1394
- const context: InvokeContext = {
1395
- cancellation: ctx.cancellation,
1406
+ // Derived so the span context keeps whatever `base` carries beyond the span
1407
+ // ids — with tracing off this method returns `base` unchanged, and tracing
1408
+ // must not change what propagates.
1409
+ const context: InvokeContext = deriveContext(ctx, {
1396
1410
  invocationId: spanId,
1397
1411
  parentInvocationId: parentSpanId,
1398
1412
  traceId,
1399
- };
1413
+ });
1400
1414
  let settled = false;
1401
1415
  return {
1402
1416
  context,
@@ -1460,7 +1474,7 @@ export class EvaluationContext implements IEvaluationContext {
1460
1474
  phase === "end" && rootScope ? { ...detail, context: rootScope } : detail,
1461
1475
  );
1462
1476
  const invokeCtx: InvokeContext = tracing
1463
- ? { cancellation: token, invocationId, parentInvocationId, traceId }
1477
+ ? deriveContext(baseCtx, { invocationId, parentInvocationId, traceId })
1464
1478
  : baseCtx;
1465
1479
 
1466
1480
  // Refuse a target reached after the boot run was cancelled.
package/src/kernel.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AnalysisRegistry,
3
3
  buildEvalPaths,
4
+ collectZoneModuleDocuments,
4
5
  flattenForAnalyzer,
5
6
  flattenLoadedModule,
6
7
  isModuleKind,
@@ -45,6 +46,7 @@ import { formatSpanCounter } from "./logging/span-id.js";
45
46
  import { ambientInvokeContext } from "./evaluation-context.js";
46
47
  import { ModuleContext } from "./module-context.js";
47
48
  import { ResourceContextImpl } from "./resource-context.js";
49
+ import { mintResourceHandle } from "./resource-handle.js";
48
50
  import { nodeCelHandlers } from "./cel-handlers.js";
49
51
  import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
50
52
  import { stripCompiledValues } from "./schema-compiled-values.js";
@@ -520,7 +522,10 @@ export class Kernel implements IKernel {
520
522
  const skipValidation = stamp?.signature === analysisSignature;
521
523
  const errors = this.analyzer.analyzeErrors(
522
524
  staticManifests,
523
- { skipValidation },
525
+ // Imported libraries' full documents, for the zone stage's per-library
526
+ // export derivation: `flattenForAnalyzer` forwards only each library's
527
+ // export surface, never its internal dispatch chain.
528
+ { skipValidation, moduleDocuments: collectZoneModuleDocuments(analysisGraph) },
524
529
  this.registry,
525
530
  );
526
531
  if (errors.length > 0) {
@@ -1286,6 +1291,17 @@ export class Kernel implements IKernel {
1286
1291
  const instance = await controller.create(processedResource, ctx);
1287
1292
  if (!instance) return null;
1288
1293
 
1294
+ // Mint the instance's identity here, at the single instance-production site,
1295
+ // so an instance is never observable without a handle — the same argument
1296
+ // that put contract binding here. First mint wins: a `base:` child IS the
1297
+ // parent instance returned verbatim and must not be re-identified.
1298
+ const handle = mintResourceHandle(
1299
+ instance,
1300
+ resolvedKind,
1301
+ (processedResource.metadata?.name as string | undefined) ?? "<unnamed>",
1302
+ );
1303
+ (ctx as ResourceContextImpl).bindResourceIdentity(handle, resolvedKind, processedResource);
1304
+
1289
1305
  // Bind the resolved invocation contract to the instance, here at the kernel's
1290
1306
  // single instance-production site — so every consumer holds an already
1291
1307
  // enforcing instance, including the majority that read a Phase-5-injected ref
@@ -96,6 +96,12 @@ const KNOWN_CAPABILITIES = [
96
96
  // path, and dispatch emits trace events, so routing logs through it would
97
97
  // generate telemetry from inside the telemetry path. See kernel/specs/logging.md §10.
98
98
  "Telo.Sink",
99
+ // `Telo.Executable` is deliberately declarable NOWHERE: it is the slot-
100
+ // constraint parent of Invocable and Runnable ("control can be transferred to
101
+ // this"), naming no lifecycle role. Listing it here keeps the open third-party
102
+ // fallback branch below from accepting it — and since no branch above admits
103
+ // it either, `capability: Telo.Executable` fails validation outright.
104
+ "Telo.Executable",
99
105
  ] as const;
100
106
 
101
107
  /** Rule 8: `throws:` is only meaningful on Telo.Invocable or Telo.Runnable.
@@ -1,4 +1,4 @@
1
- import { executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
1
+ import { deriveContext, executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
2
2
  import type { ScopeConfig } from "./logging/scope-config.js";
3
3
  import type {
4
4
  BootTarget,
@@ -585,12 +585,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
585
585
  );
586
586
  const targetCtx: InvokeContext | undefined =
587
587
  tracing && ctx
588
- ? {
589
- cancellation: ctx.cancellation,
588
+ ? deriveContext(ctx, {
590
589
  invocationId: appSpanId,
591
590
  parentInvocationId: undefined,
592
591
  traceId: appTraceId,
593
- }
592
+ })
594
593
  : ctx;
595
594
 
596
595
  const steps: Record<string, unknown> = {};
@@ -1,15 +1,20 @@
1
1
  import {
2
+ InvokeError,
2
3
  NoopValidator,
3
4
  ResourceContext,
4
5
  ResourceInstance,
5
6
  ResourceManifest,
6
7
  RuntimeError,
7
8
  RuntimeResource,
9
+ UNCANCELLABLE_CONTEXT,
8
10
  createCancellationSource,
11
+ deriveContext,
12
+ getRefIdentity,
9
13
  resolveRefInstance,
10
14
  type CancellationSource,
11
15
  type ControllerPolicy,
12
16
  type EvaluationContext as IEvaluationContext,
17
+ type InvokeByNameOptions,
13
18
  type InvokeContext,
14
19
  type LoadOptions,
15
20
  type ModuleContext,
@@ -17,10 +22,14 @@ import {
17
22
  type OpenSpan,
18
23
  type OpenSpanOptions,
19
24
  type ParsedArgs,
25
+ type ResourceDefinition,
26
+ type ResourceHandle,
20
27
  type RuntimeSeam,
21
28
  type TypeRule,
29
+ type ZoneEntry,
22
30
  } from "@telorun/sdk";
23
31
  import { isRefSentinel } from "@telorun/templating";
32
+ import { ZoneContext } from "./zone-context.js";
24
33
  import * as path from "path";
25
34
  import { pathToFileURL } from "url";
26
35
  import type { ModuleArtifact } from "./bundle/module-artifact.js";
@@ -350,6 +359,88 @@ export class ResourceContextImpl implements ResourceContext {
350
359
  return createCancellationSource();
351
360
  }
352
361
 
362
+ // ── Execution zones (kernel/specs/execution-zones.md) ─────────────────────
363
+ //
364
+ // Identity is held here (it is the resource's, not the zone subsystem's);
365
+ // everything else delegates to `ZoneContext`, which owns the annotation
366
+ // resolution, correlation walk and stack matching — and memoizes them, since
367
+ // this sits on the dispatch path.
368
+
369
+ #self: ResourceHandle | undefined;
370
+ #zones: ZoneContext | undefined;
371
+
372
+ /** Kernel-internal: stamped at `create()`, the moment the instance exists. */
373
+ bindResourceIdentity(
374
+ handle: ResourceHandle,
375
+ resolvedKind: string,
376
+ manifest: Record<string, unknown>,
377
+ ): void {
378
+ this.#self = handle;
379
+ this.#zones = new ZoneContext({
380
+ resourceName: (this.metadata?.name as string) ?? "<unnamed>",
381
+ resolvedKind,
382
+ self: handle,
383
+ // The SAME object Phase-5 injection later mutates, so a correlation
384
+ // pointer read at invoke time sees live instances in ref slots.
385
+ manifest,
386
+ resolveDefinition: (kind) => this.kernel.getAnalysisRegistry().resolveDefinition(kind),
387
+ resolveDefinitionIn: (kind, module) =>
388
+ this.kernel.getAnalysisRegistry().resolveDefinitionIn(kind, module),
389
+ resolveLocalInstance: (name) => this.resolveLocalInstance(name),
390
+ resolveLocalManifest: (name) =>
391
+ this.contextForName(name).resourceInstances.get(name)?.resource as
392
+ | Record<string, unknown>
393
+ | undefined,
394
+ });
395
+ }
396
+
397
+ get self(): ResourceHandle {
398
+ if (!this.#self) {
399
+ throw new RuntimeError(
400
+ "ERR_RESOURCE_IDENTITY_UNBOUND",
401
+ `[${this.metadata.name}] ctx.self is unavailable inside create() — the handle is minted when create() returns`,
402
+ );
403
+ }
404
+ return this.#self;
405
+ }
406
+
407
+ /** The zone subsystem, available once `create()` has returned. */
408
+ private zoneContext(): ZoneContext {
409
+ if (!this.#zones) {
410
+ throw new RuntimeError(
411
+ "ERR_RESOURCE_IDENTITY_UNBOUND",
412
+ `[${this.metadata.name}] zones are unavailable inside create() — the handle is minted when create() returns`,
413
+ );
414
+ }
415
+ return this.#zones;
416
+ }
417
+
418
+ withZone<T>(
419
+ slot: string,
420
+ fn: (ctx: InvokeContext, entry: ZoneEntry) => Promise<T>,
421
+ base?: InvokeContext,
422
+ ): Promise<T> {
423
+ return this.zoneContext().withZone(slot, fn, base);
424
+ }
425
+
426
+ requireZone(field: string, ctx?: InvokeContext): ZoneEntry {
427
+ return this.zoneContext().requireZone(field, ctx);
428
+ }
429
+
430
+ findZone(field: string, ctx?: InvokeContext): ZoneEntry | undefined {
431
+ return this.zoneContext().findZone(field, ctx);
432
+ }
433
+
434
+ zonesFor(instance: ResourceInstance, ctx?: InvokeContext): readonly ZoneEntry[] {
435
+ return this.zoneContext().zonesFor(instance, ctx);
436
+ }
437
+
438
+ /** The root context for runtime-driven inbound work — inherits nothing from
439
+ * whatever ambient happens to be live at the registration site. */
440
+ rootContext(opts?: { cancellation?: CancellationSource }): InvokeContext {
441
+ return opts?.cancellation?.context ?? UNCANCELLABLE_CONTEXT;
442
+ }
443
+
353
444
  /** In-flight fire-and-forget tasks this resource spawned. Owned here, not by
354
445
  * the kernel: the resource drains them in its own teardown (see
355
446
  * `drainDetached`), so background work is bounded by the resource's lifetime. */
@@ -401,8 +492,13 @@ export class ResourceContextImpl implements ResourceContext {
401
492
  return this.owningContext.openSpan(base, opts);
402
493
  }
403
494
 
404
- invoke<TInputs>(kind: string, name: string, inputs: TInputs): Promise<any> {
405
- return this.contextForName(name).invoke(kind, name, inputs);
495
+ invoke<TInputs>(
496
+ kind: string,
497
+ name: string,
498
+ inputs: TInputs,
499
+ options?: InvokeByNameOptions,
500
+ ): Promise<any> {
501
+ return this.contextForName(name).invoke(kind, name, inputs, options?.ctx);
406
502
  }
407
503
 
408
504
  invokeResolved<TInputs>(
@@ -0,0 +1,35 @@
1
+ import type { ResourceHandle, ResourceInstanceId } from "@telorun/sdk";
2
+
3
+ /**
4
+ * Instance → handle, minted at `create()` — the kernel's single
5
+ * instance-production site, where the invocation contract already binds — so an
6
+ * instance is never observable without one. The reverse direction deliberately
7
+ * does not exist: nothing turns a handle back into someone else's live
8
+ * instance, which is what keeps the ambient zone stack from leaking instances
9
+ * across module boundaries.
10
+ */
11
+ const handles = new WeakMap<object, ResourceHandle>();
12
+
13
+ let counter = 0;
14
+
15
+ /**
16
+ * Mint (or return) the handle for a live instance. Idempotent, first mint wins
17
+ * — a `base:` child IS its parent's instance returned verbatim, so the nested
18
+ * parent create stamps first and the child create must not re-identify it; one
19
+ * live instance, one id, exactly like `stampRefIdentity`.
20
+ */
21
+ export function mintResourceHandle(instance: object, kind: string, name: string): ResourceHandle {
22
+ const existing = handles.get(instance);
23
+ if (existing) return existing;
24
+ const handle: ResourceHandle = Object.freeze({
25
+ id: `ri-${++counter}` as ResourceInstanceId,
26
+ ref: Object.freeze({ kind, name }),
27
+ });
28
+ handles.set(instance, handle);
29
+ return handle;
30
+ }
31
+
32
+ /** The handle minted for a live instance, if any. */
33
+ export function handleOfInstance(instance: object): ResourceHandle | undefined {
34
+ return handles.get(instance);
35
+ }
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  Loader,
3
3
  StaticAnalyzer,
4
+ collectZoneModuleDocuments,
4
5
  flattenForAnalyzer,
5
6
  type AnalysisDiagnostic,
6
7
  type ManifestSource,
8
+ type ZoneModuleDocuments,
7
9
  } from "@telorun/analyzer";
8
10
  import {
9
11
  Stream,
@@ -211,6 +213,7 @@ export class KernelRuntimeSeam implements RuntimeSeam {
211
213
  // `analyze()`'s — carried out of the try so the checks below can see them.
212
214
  let parseDiagnostics: AnalysisDiagnostic[] = [];
213
215
  let versionDiagnostics: AnalysisDiagnostic[] = [];
216
+ let moduleDocuments: ZoneModuleDocuments[] = [];
214
217
  try {
215
218
  const graph = await loader.loadGraph(source, {
216
219
  desugarImports: options?.desugarImports ?? true,
@@ -219,6 +222,9 @@ export class KernelRuntimeSeam implements RuntimeSeam {
219
222
  parseDiagnostics = graph.parseDiagnostics;
220
223
  versionDiagnostics = graph.versionDiagnostics;
221
224
  manifests = flattenForAnalyzer(graph);
225
+ // The zone stage derives each imported library's export contracts from
226
+ // its own full documents, which the flattened list drops.
227
+ moduleDocuments = collectZoneModuleDocuments(graph);
222
228
  } catch (err) {
223
229
  // A graph that would not load is an answer, not a failure of the call —
224
230
  // "this manifest does not load, and here is the reason" is precisely what
@@ -243,7 +249,9 @@ export class KernelRuntimeSeam implements RuntimeSeam {
243
249
 
244
250
  // `analyze()` never sees version skew, so without merging these a major
245
251
  // mismatch — which `load()` refuses to boot on — would check clean.
246
- const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests);
252
+ const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests, {
253
+ moduleDocuments,
254
+ });
247
255
  return {
248
256
  diagnostics: [...versionDiagnostics, ...diagnostics].map(toCheckDiagnostic),
249
257
  };