@telorun/kernel 0.23.0 → 0.24.1

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.
@@ -47,6 +47,32 @@ function collectSecretValues(secrets: Record<string, unknown>): Set<string> {
47
47
  return values;
48
48
  }
49
49
 
50
+ /** A boot target whose ref slot Phase 5 injection already replaced with the
51
+ * live instance (the documented "pre-resolved instance once Phase 5 ran"
52
+ * shape from `BootTarget`). Distinguished from a structural `{kind, name}` ref
53
+ * by carrying a `run()` method. */
54
+ function isRunnableInstance(value: unknown): value is ResourceInstance {
55
+ return (
56
+ !!value &&
57
+ typeof value === "object" &&
58
+ typeof (value as { run?: unknown }).run === "function"
59
+ );
60
+ }
61
+
62
+ /** Cycle-safe serialization for diagnostics. A boot target can be a live
63
+ * instance whose object graph (e.g. a Kysely/Ajv schema) is cyclic, which
64
+ * would make a plain JSON.stringify throw and mask the real error. */
65
+ function safeStringify(value: unknown): string {
66
+ const seen = new WeakSet<object>();
67
+ return JSON.stringify(value, (_key, val) => {
68
+ if (val && typeof val === "object") {
69
+ if (seen.has(val)) return "[Circular]";
70
+ seen.add(val);
71
+ }
72
+ return val;
73
+ });
74
+ }
75
+
50
76
  /**
51
77
  * Persistent, module-scoped context. Reserved CEL namespaces:
52
78
  * variables, secrets, resources, ports (Application-only).
@@ -319,6 +345,18 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
319
345
  this.invokeResolved(kind, name, instance, inputs, ctx),
320
346
  resolveImportedInstance: (alias, name) => this.resolveImportedInstance(alias, name),
321
347
  };
348
+ // Mirror the local-run gate: refuse a target reached after the boot run was
349
+ // cancelled, then run the pre-resolved instance directly.
350
+ const runResolvedInstance = async (inst: ResourceInstance, label: string) => {
351
+ const token = ctx?.cancellation;
352
+ if (token?.isCancelled) {
353
+ throw new RuntimeError(
354
+ "ERR_INVOKE_CANCELLED",
355
+ `Run ${label} was cancelled${token.reason ? `: ${token.reason}` : ""}`,
356
+ );
357
+ }
358
+ await inst.run!(ctx);
359
+ };
322
360
  for (let i = 0; i < this.targets.length; i++) {
323
361
  const target = this.targets[i]!;
324
362
  if (typeof target === "string") {
@@ -341,36 +379,54 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
341
379
  if ("ref" in target && target.ref != null) {
342
380
  if (target.when === undefined || this.expandWith(target.when, { steps })) {
343
381
  const ref = target.ref as unknown;
344
- const r =
345
- ref && typeof ref === "object" ? (ref as { name: string; alias?: string }) : undefined;
346
- if (r && typeof r.alias === "string" && r.alias !== "Self") {
347
- const inst = this.resolveImportedInstance(r.alias, r.name);
348
- if (!inst || typeof inst.run !== "function") {
349
- throw new Error(
350
- `Boot target '${r.alias}.${r.name}' is not a runnable exported instance`,
351
- );
352
- }
353
- // Mirror the local-run gate: refuse a cross-module target reached
354
- // after the boot run was cancelled.
355
- const token = ctx?.cancellation;
356
- if (token?.isCancelled) {
357
- throw new RuntimeError(
358
- "ERR_INVOKE_CANCELLED",
359
- `Run ${r.alias}.${r.name} was cancelled${token.reason ? `: ${token.reason}` : ""}`,
360
- );
361
- }
362
- await inst.run(ctx);
382
+ // Phase 5 injection may have replaced the ref slot with the live
383
+ // instance; run it directly.
384
+ if (isRunnableInstance(ref)) {
385
+ await runResolvedInstance(ref, `target[${i}]`);
363
386
  } else {
364
- await this.run(typeof ref === "string" ? ref : r!.name, ctx);
387
+ const r =
388
+ ref && typeof ref === "object"
389
+ ? (ref as { name: string; alias?: string })
390
+ : undefined;
391
+ if (r && typeof r.alias === "string" && r.alias !== "Self") {
392
+ const inst = this.resolveImportedInstance(r.alias, r.name);
393
+ if (!inst || typeof inst.run !== "function") {
394
+ throw new Error(
395
+ `Boot target '${r.alias}.${r.name}' is not a runnable exported instance`,
396
+ );
397
+ }
398
+ await runResolvedInstance(inst, `${r.alias}.${r.name}`);
399
+ } else {
400
+ await this.run(typeof ref === "string" ? ref : r!.name, ctx);
401
+ }
365
402
  }
366
403
  }
367
404
  continue;
368
405
  }
369
- if ("name" in target && typeof target.name === "string") {
370
- await this.run(target.name, ctx);
406
+ // Bare run target. Phase 5 injection resolves a `!ref name` /
407
+ // cross-module `!ref Alias.name` slot into the live instance before boot,
408
+ // so the common runtime shape here is a pre-resolved ResourceInstance;
409
+ // fall back to the structural ref forms when injection left them in place.
410
+ if (isRunnableInstance(target)) {
411
+ await runResolvedInstance(target, `target[${i}]`);
412
+ continue;
413
+ }
414
+ const bare = target as { name?: unknown; alias?: unknown };
415
+ if (typeof bare.alias === "string" && bare.alias !== "Self" && typeof bare.name === "string") {
416
+ const inst = this.resolveImportedInstance(bare.alias, bare.name);
417
+ if (!inst || typeof inst.run !== "function") {
418
+ throw new Error(
419
+ `Boot target '${bare.alias}.${bare.name}' is not a runnable exported instance`,
420
+ );
421
+ }
422
+ await runResolvedInstance(inst, `${bare.alias}.${bare.name}`);
423
+ continue;
424
+ }
425
+ if (typeof bare.name === "string") {
426
+ await this.run(bare.name, ctx);
371
427
  continue;
372
428
  }
373
- throw new Error(`Unrecognized target shape at index ${i}: ${JSON.stringify(target)}`);
429
+ throw new Error(`Unrecognized target shape at index ${i}: ${safeStringify(target)}`);
374
430
  }
375
431
  }
376
432
  }
@@ -251,14 +251,19 @@ export class ResourceContextImpl implements ResourceContext {
251
251
  return { kind: ref.kind, name: ref.name, alias };
252
252
  }
253
253
  const refName = alias === "Self" ? source.slice(dot + 1) : source;
254
- const entry = this.moduleContext.resourceInstances.get(refName);
255
- if (!entry) {
256
- throw new RuntimeError(
257
- "ERR_RESOURCE_NOT_FOUND",
258
- `[${this.metadata.name}] !ref '${refName}' did not resolve to a registered resource.`,
259
- );
260
- }
261
- return { kind: entry.resource.kind as string, name: refName };
254
+ // Mirror the legacy `{kind, name}` path: return a `{kind, name}` ref and let
255
+ // the downstream resolution (module-global `invoke`, or scope-local
256
+ // `executeInvokeStep → ScopeContext.getInstance`) find it by name at invoke
257
+ // time. The kind is carried for invocation-event naming. Prefer an
258
+ // already-initialized instance's kind, else the authored kind from the
259
+ // static manifest (init-order-independent — the target may not be
260
+ // initialized yet when this resolves, and scope-local resources never enter
261
+ // `resourceInstances`).
262
+ const kind =
263
+ (this.moduleContext.resourceInstances.get(refName)?.resource.kind as string | undefined) ??
264
+ this.kernel.resourceKindByName(refName) ??
265
+ "";
266
+ return { kind, name: refName };
262
267
  }
263
268
 
264
269
  if (!resource.kind) {
@@ -274,6 +279,12 @@ export class ResourceContextImpl implements ResourceContext {
274
279
  resource.metadata?.name ??
275
280
  resourceName ??
276
281
  `Unnamed${Math.random().toString(16).slice(2, 8)}`;
282
+ // `alias` marks a resolved cross-module reference (`{kind, name, alias}`)
283
+ // produced from a `!ref Alias.name`. It is always a reference into an
284
+ // imported library's exported instance — never an inline definition — so it
285
+ // is a reference key alongside kind/name/metadata (not inline config) and is
286
+ // carried through so downstream scope resolution routes into the import.
287
+ const alias = typeof resource.alias === "string" ? resource.alias : undefined;
277
288
 
278
289
  // Register an inline manifest when:
279
290
  // - the ref carries definition properties (clearly an inline definition), or
@@ -287,7 +298,7 @@ export class ResourceContextImpl implements ResourceContext {
287
298
  // the contract here; previous silent-skip-on-duplicate hid real bugs
288
299
  // (e.g. inline auto-names colliding across sibling Run.Sequence steps).
289
300
  const hasInlineProperties = Object.keys(resource).some(
290
- (k) => k !== "kind" && k !== "name" && k !== "metadata",
301
+ (k) => k !== "kind" && k !== "name" && k !== "metadata" && k !== "alias",
291
302
  );
292
303
  const hasExplicitName = resource.name !== undefined || resource.metadata?.name !== undefined;
293
304
  const shouldRegister =
@@ -304,7 +315,7 @@ export class ResourceContextImpl implements ResourceContext {
304
315
  });
305
316
  }
306
317
 
307
- return { kind, name };
318
+ return alias ? { kind, name, alias } : { kind, name };
308
319
  }
309
320
 
310
321
  getResourcesByName(_kind: string, name: string): RuntimeResource | null {
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as path from "node:path";
10
10
  import { formatAjvErrors } from "./manifest-schemas.js";
11
- import { ManifestRootSchema } from "@telorun/templating";
11
+ import { ManifestRootSchema, normalizeRefSlots } from "@telorun/templating";
12
12
 
13
13
  const Ajv = AjvModule.default ?? AjvModule;
14
14
  // AJV's standalone subpath is CJS — the default export shows up as either
@@ -174,7 +174,7 @@ export class SchemaValidator {
174
174
  required: Object.keys(schema),
175
175
  additionalProperties: false,
176
176
  };
177
- const injected =
177
+ const withImplicit =
178
178
  normalized.additionalProperties === false
179
179
  ? {
180
180
  ...normalized,
@@ -186,6 +186,13 @@ export class SchemaValidator {
186
186
  }
187
187
  : normalized;
188
188
 
189
+ // Drop the legacy scalar `type` an older published module may still pin on
190
+ // its `x-telo-ref` slots. Schema validation runs in create() before Phase 5
191
+ // injection, so a ref slot holds the resolved `{kind, name, alias?}` object
192
+ // (or an unresolved sentinel) — both objects the stale `type: "string"`
193
+ // would otherwise reject.
194
+ const injected = normalizeRefSlots(withImplicit) as typeof withImplicit;
195
+
189
196
  const hash = createHash("sha256")
190
197
  .update(JSON.stringify({ runtime: VALIDATOR_RUNTIME_TAG, schema: injected }))
191
198
  .digest("hex")