@telorun/kernel 0.69.0 → 0.72.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 (69) hide show
  1. package/dist/bigint-json.d.ts +45 -0
  2. package/dist/bigint-json.d.ts.map +1 -0
  3. package/dist/bigint-json.js +65 -0
  4. package/dist/bigint-json.js.map +1 -0
  5. package/dist/bigint-schema-view.d.ts +34 -0
  6. package/dist/bigint-schema-view.d.ts.map +1 -0
  7. package/dist/bigint-schema-view.js +85 -0
  8. package/dist/bigint-schema-view.js.map +1 -0
  9. package/dist/cel-handlers.d.ts.map +1 -1
  10. package/dist/cel-handlers.js +6 -8
  11. package/dist/cel-handlers.js.map +1 -1
  12. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  13. package/dist/controllers/module/import-controller.js +15 -1
  14. package/dist/controllers/module/import-controller.js.map +1 -1
  15. package/dist/evaluation-context.d.ts.map +1 -1
  16. package/dist/evaluation-context.js +8 -1
  17. package/dist/evaluation-context.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/invocation-contract-binding.d.ts +0 -6
  23. package/dist/invocation-contract-binding.d.ts.map +1 -1
  24. package/dist/invocation-contract-binding.js +9 -64
  25. package/dist/invocation-contract-binding.js.map +1 -1
  26. package/dist/kernel.d.ts +4 -0
  27. package/dist/kernel.d.ts.map +1 -1
  28. package/dist/kernel.js +22 -3
  29. package/dist/kernel.js.map +1 -1
  30. package/dist/logging/encode-json.d.ts.map +1 -1
  31. package/dist/logging/encode-json.js +13 -9
  32. package/dist/logging/encode-json.js.map +1 -1
  33. package/dist/logging/encode-pretty.d.ts.map +1 -1
  34. package/dist/logging/encode-pretty.js +13 -4
  35. package/dist/logging/encode-pretty.js.map +1 -1
  36. package/dist/module-file-resolution.d.ts +22 -0
  37. package/dist/module-file-resolution.d.ts.map +1 -0
  38. package/dist/module-file-resolution.js +51 -0
  39. package/dist/module-file-resolution.js.map +1 -0
  40. package/dist/resolve-include-sentinels.d.ts +37 -0
  41. package/dist/resolve-include-sentinels.d.ts.map +1 -0
  42. package/dist/resolve-include-sentinels.js +175 -0
  43. package/dist/resolve-include-sentinels.js.map +1 -0
  44. package/dist/resource-context.d.ts +10 -1
  45. package/dist/resource-context.d.ts.map +1 -1
  46. package/dist/resource-context.js +42 -45
  47. package/dist/resource-context.js.map +1 -1
  48. package/dist/runtime-seam.d.ts.map +1 -1
  49. package/dist/runtime-seam.js +10 -1
  50. package/dist/runtime-seam.js.map +1 -1
  51. package/dist/schema-validator.d.ts.map +1 -1
  52. package/dist/schema-validator.js +23 -12
  53. package/dist/schema-validator.js.map +1 -1
  54. package/package.json +4 -4
  55. package/src/bigint-json.ts +69 -0
  56. package/src/bigint-schema-view.ts +76 -0
  57. package/src/cel-handlers.ts +6 -9
  58. package/src/controllers/module/import-controller.ts +14 -1
  59. package/src/evaluation-context.ts +8 -1
  60. package/src/index.ts +1 -0
  61. package/src/invocation-contract-binding.ts +9 -55
  62. package/src/kernel.ts +29 -0
  63. package/src/logging/encode-json.ts +14 -9
  64. package/src/logging/encode-pretty.ts +12 -3
  65. package/src/module-file-resolution.ts +64 -0
  66. package/src/resolve-include-sentinels.ts +219 -0
  67. package/src/resource-context.ts +46 -45
  68. package/src/runtime-seam.ts +11 -0
  69. package/src/schema-validator.ts +23 -12
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The BigInt-normalized VIEW a JSON Schema validator can check, and merging its
3
+ * default-fills back onto the real value.
4
+ *
5
+ * CEL evaluates an integer to a BigInt, which AJV does not recognise as `integer`
6
+ * or `number` — `typeof data == "number"` is the whole of its type check. So a
7
+ * computed integer reaching a declared integer slot is rejected for a reason the
8
+ * author cannot act on, and cannot fix without casting the value to a float. The
9
+ * value is not wrong; the validator cannot see it.
10
+ *
11
+ * Validating a normalized view rather than coercing in place is what keeps that a
12
+ * validator concern: the dispatched value keeps its BigInts, since a controller may
13
+ * need the full 64-bit range, and serialization emits them exactly (see
14
+ * `enableBigIntJson` in `@telorun/sdk`). A value beyond the safe-integer range loses
15
+ * precision in the VIEW, which can only affect a bound check at the extremes.
16
+ */
17
+
18
+ /** A structural copy of `value` with every BigInt rendered as a Number. Returns
19
+ * the SAME reference when there was nothing to change, which is what lets a
20
+ * caller skip the merge-back entirely on the common path. Non-plain objects (a
21
+ * live `Stream`, a resource instance) pass through by reference — they are not
22
+ * data to be walked. */
23
+ export function withBigIntsAsNumbers(value: unknown): unknown {
24
+ if (typeof value === "bigint") return Number(value);
25
+ if (Array.isArray(value)) {
26
+ let changed = false;
27
+ const items = value.map((item) => {
28
+ const next = withBigIntsAsNumbers(item);
29
+ if (next !== item) changed = true;
30
+ return next;
31
+ });
32
+ return changed ? items : value;
33
+ }
34
+ if (!value || typeof value !== "object") return value;
35
+ if (Object.getPrototypeOf(value) !== Object.prototype) return value;
36
+ let changed = false;
37
+ const out: Record<string, unknown> = {};
38
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
39
+ const next = withBigIntsAsNumbers(item);
40
+ if (next !== item) changed = true;
41
+ out[key] = next;
42
+ }
43
+ return changed ? out : value;
44
+ }
45
+
46
+ /** Copy keys an AJV `useDefaults` fill added to `view` back onto `target`.
47
+ * Only ADDITIONS are taken: a key already present came from the caller and its
48
+ * original (possibly BigInt) value is the one to keep.
49
+ *
50
+ * Arrays are walked index-wise, not treated as leaves: AJV writes a default at
51
+ * every level it finds one, including inside `items`, and `Collection.Sort`'s
52
+ * `orderBy[].descending` is exactly that shape. Bailing at the array boundary
53
+ * would drop those fills silently — a worse failure than the throw it replaced,
54
+ * because the call then succeeds with the default missing. This mirrors the
55
+ * copy side, where `copyForDefaults` already fans out through an `[]` segment. */
56
+ export function mergeFilledDefaults(target: unknown, view: unknown): unknown {
57
+ if (target === view) return target;
58
+ if (!target || typeof target !== "object") return target;
59
+ if (!view || typeof view !== "object") return target;
60
+
61
+ if (Array.isArray(target)) {
62
+ if (!Array.isArray(view)) return target;
63
+ for (let i = 0; i < target.length && i < view.length; i++) {
64
+ target[i] = mergeFilledDefaults(target[i], view[i]);
65
+ }
66
+ return target;
67
+ }
68
+ if (Array.isArray(view)) return target;
69
+
70
+ const out = target as Record<string, unknown>;
71
+ for (const [key, filled] of Object.entries(view as Record<string, unknown>)) {
72
+ if (!(key in out)) out[key] = filled;
73
+ else out[key] = mergeFilledDefaults(out[key], filled);
74
+ }
75
+ return out;
76
+ }
@@ -12,13 +12,10 @@ export const nodeCelHandlers = {
12
12
  createHmac(algorithm, key).update(message).digest("hex"),
13
13
  base64Encode: (s: string) => Buffer.from(s, "utf8").toString("base64"),
14
14
  base64Decode: (s: string) => Buffer.from(s, "base64").toString("utf8"),
15
- // cel-js represents int / uint as BigInt JSON.stringify throws on BigInts,
16
- // so coerce them down to Number unconditionally. CEL int is i64 and JS Number
17
- // is f64, so values outside ±2^53 lose precision; that's accepted behaviour
18
- // for Telo manifests, which never carry > 2^53 integer values in practice.
19
- // JSON.stringify returns undefined for top-level undefined / function / symbol
20
- // the CEL signature is `json(dyn): string`, so coerce that to "null" rather
21
- // than break the contract. (CEL `null` already serializes to "null".)
22
- json: (value: unknown) =>
23
- JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? Number(v) : v)) ?? "null",
15
+ // An int / uint is a BigInt here and serializes as its exact digits — see
16
+ // `enableBigIntJson` in `@telorun/sdk`, installed at boot. JSON.stringify
17
+ // returns undefined for top-level undefined / function / symbol the CEL
18
+ // signature is `json(dyn): string`, so coerce that to "null" rather than break
19
+ // the contract. (CEL `null` already serializes to "null".)
20
+ json: (value: unknown) => JSON.stringify(value) ?? "null",
24
21
  };
@@ -144,7 +144,20 @@ export async function create(
144
144
  moduleManifest.secrets ?? {},
145
145
  );
146
146
  const childCtx = new ModuleContext(
147
- ctx.moduleContext.source,
147
+ // The LIBRARY's own manifest URL, not the importer's. `source` is what every
148
+ // module-relative file reference is measured from — `ctx.resolveModuleFile`
149
+ // for a controller, an `!include-*` embed for a manifest value — so carrying
150
+ // the parent's here made a library read the CONSUMER's directory. That is
151
+ // worse than a hard error: a consumer who happens to have a file at the same
152
+ // relative path gets theirs silently. It also contradicted packaging, which
153
+ // is per-module and had already put the library's file in the library's own
154
+ // artifact.
155
+ //
156
+ // The MANIFEST URL, stamped by the loader — not `resolvedUrl`, which is the
157
+ // import source as written (`./lib`, a directory). Resolving `assets/x` against
158
+ // a directory URL with no trailing slash drops its last segment, which is how
159
+ // the consumer's directory got read in the first place.
160
+ ((moduleManifest.metadata as { source?: string } | undefined)?.source ?? resolvedUrl),
148
161
  importVariables,
149
162
  importSecrets,
150
163
  {},
@@ -317,7 +317,14 @@ function compileWalker(value: unknown): Walker {
317
317
  return out;
318
318
  };
319
319
  }
320
- if (value !== null && typeof value === "object") {
320
+ // Only PLAIN objects are rebuilt. Anything else a `Uint8Array` embedded by
321
+ // `!include-bytes`, a class instance — is opaque and passes through by
322
+ // reference, the same rule `precompileDoc` follows. Rebuilding from
323
+ // `Object.entries` would turn a byte buffer into `{"0":137,…}` silently, with
324
+ // no error anywhere: the bytes would simply arrive at the controller as the
325
+ // wrong shape.
326
+ const proto = value !== null && typeof value === "object" ? Object.getPrototypeOf(value) : false;
327
+ if (proto === Object.prototype || proto === null) {
321
328
  const entries = Object.entries(value as Record<string, unknown>).map(
322
329
  ([k, v]) => [k, compileWalker(v)] as const,
323
330
  );
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ export type {
44
44
  } from "./transports/transport.js";
45
45
  export { ExecutionContext } from "./execution-context.js";
46
46
  export { Kernel, type KernelOptions } from "./kernel.js";
47
+ export { enableBigIntJson } from "./bigint-json.js";
47
48
  export { nodeCelHandlers } from "./cel-handlers.js";
48
49
  export { ModuleContext } from "./module-context.js";
49
50
  export { ManifestRegistry as Registry } from "./registry.js";
@@ -305,27 +305,23 @@ export function bindContract(instance: ResourceInstance, binding: ContractBindin
305
305
  instance.invoke = async (inputs: any, ...rest: unknown[]) => {
306
306
  let effective = inputs;
307
307
  if (input) {
308
+ // Copied only so the validator's default-fill cannot mutate what the
309
+ // caller still holds. The BigInt normalization a JSON Schema validator
310
+ // needs (CEL evaluates an integer to one, which AJV does not recognise
311
+ // as `integer`) belongs to the validator and is applied there — see
312
+ // `bigint-schema-view.ts`. Doing it here as well would walk every input
313
+ // tree twice per dispatch, and split one concern across two layers.
308
314
  effective = copyForDefaults(inputs, input.defaultPaths());
309
- // Validate a BIGINT-NORMALIZED view, not the values themselves. CEL
310
- // evaluates an integer literal to a BigInt, which a JSON Schema
311
- // validator does not recognise as `integer` — so every computed integer
312
- // reaching a declared integer input would be rejected for a reason the
313
- // author cannot act on. The dispatched values keep their BigInts, since
314
- // a controller may need the full 64-bit range.
315
- const view = withBigIntsAsNumbers(effective);
316
315
  try {
317
- input.validate(view);
316
+ input.validate(effective);
318
317
  } catch (error) {
319
318
  throw contractViolation("inputType", describeTarget, error);
320
319
  }
321
- // Defaults are additive, so anything the validator filled into the view
322
- // is a key the caller omitted — copy exactly those back.
323
- effective = mergeFilledDefaults(effective, view);
324
320
  }
325
321
  const result = await original(effective, ...rest);
326
322
  if (output) {
327
323
  try {
328
- output.validate(withBigIntsAsNumbers(result));
324
+ output.validate(result);
329
325
  } catch (error) {
330
326
  throw contractViolation("outputType", describeTarget, error);
331
327
  }
@@ -339,7 +335,7 @@ export function bindContract(instance: ResourceInstance, binding: ContractBindin
339
335
  instance.provide = async (...args: unknown[]) => {
340
336
  const result = await original(...args);
341
337
  try {
342
- output.validate(withBigIntsAsNumbers(result));
338
+ output.validate(result);
343
339
  } catch (error) {
344
340
  throw contractViolation("outputType", describeTarget, error);
345
341
  }
@@ -348,45 +344,3 @@ export function bindContract(instance: ResourceInstance, binding: ContractBindin
348
344
  }
349
345
  }
350
346
 
351
- /** A structural copy with every BigInt rendered as a Number, for validation
352
- * only. A value beyond the safe-integer range loses precision in the VIEW,
353
- * which can only affect a bound check at the extremes; the dispatched value is
354
- * untouched. Non-plain objects (a live `Stream`, a resource instance) pass
355
- * through by reference — they are not data to be walked. */
356
- export function withBigIntsAsNumbers(value: unknown): unknown {
357
- if (typeof value === "bigint") return Number(value);
358
- if (Array.isArray(value)) {
359
- let changed = false;
360
- const items = value.map((item) => {
361
- const next = withBigIntsAsNumbers(item);
362
- if (next !== item) changed = true;
363
- return next;
364
- });
365
- return changed ? items : value;
366
- }
367
- if (!value || typeof value !== "object") return value;
368
- if (Object.getPrototypeOf(value) !== Object.prototype) return value;
369
- let changed = false;
370
- const out: Record<string, unknown> = {};
371
- for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
372
- const next = withBigIntsAsNumbers(item);
373
- if (next !== item) changed = true;
374
- out[key] = next;
375
- }
376
- return changed ? out : value;
377
- }
378
-
379
- /** Copy keys the validator's default-fill added to `view` back onto `target`.
380
- * Only ADDITIONS are taken: a key already present came from the caller and its
381
- * original (possibly BigInt) value is the one to dispatch. */
382
- function mergeFilledDefaults(target: unknown, view: unknown): unknown {
383
- if (target === view) return target;
384
- if (!target || typeof target !== "object" || Array.isArray(target)) return target;
385
- if (!view || typeof view !== "object" || Array.isArray(view)) return target;
386
- const out = target as Record<string, unknown>;
387
- for (const [key, filled] of Object.entries(view as Record<string, unknown>)) {
388
- if (!(key in out)) out[key] = filled;
389
- else out[key] = mergeFilledDefaults(out[key], filled);
390
- }
391
- return out;
392
- }
package/src/kernel.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  import { parseArgs } from "util";
39
39
  import { ControllerRegistry } from "./controller-registry.js";
40
40
  import { EventBus } from "./events.js";
41
+ import { enableBigIntJson } from "./bigint-json.js";
41
42
  import { hostEnv, lockControllerEnv } from "./host-env.js";
42
43
  import { KernelTracer } from "./tracing.js";
43
44
  import { KernelLogging, type LoggingManifestBlock } from "./logging/kernel-logging.js";
@@ -51,6 +52,7 @@ import { nodeCelHandlers } from "./cel-handlers.js";
51
52
  import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
52
53
  import { stripCompiledValues } from "./schema-compiled-values.js";
53
54
  import { injectAtPath } from "./dependency-injection.js";
55
+ import { resolveIncludeSentinels, type IncludeCache } from "./resolve-include-sentinels.js";
54
56
  import {
55
57
  computeAnalysisSignature,
56
58
  readAnalysisStamp,
@@ -136,6 +138,10 @@ export class Kernel implements IKernel {
136
138
  private idleResolvers: Array<() => void> = [];
137
139
  private _exitCode = 0;
138
140
  private readonly sharedSchemaValidator = new SchemaValidator();
141
+ /** `!include-*` reads, keyed by resolved URI, so two resources embedding the
142
+ * same asset read it once. Kernel-scoped: the values are retained by the
143
+ * resources holding them anyway, so this deduplicates rather than retains. */
144
+ private readonly includeCache: IncludeCache = new Map();
139
145
  private rootContext!: ModuleContext;
140
146
  private staticManifests: ResourceManifest[] = [];
141
147
  private _entryUrl?: string;
@@ -761,6 +767,13 @@ export class Kernel implements IKernel {
761
767
  }
762
768
  this._bootCalled = true;
763
769
 
770
+ // Make a CEL integer JSON-serializable before any controller runs. CEL
771
+ // models `int` as int64 (a JS BigInt here), and `JSON.stringify` throws on
772
+ // one — so every JSON boundary a manifest can reach, in this kernel and in
773
+ // any module, needs the answer installed before the first value crosses it.
774
+ // Process-global and idempotent, like the env guardrail below.
775
+ enableBigIntJson();
776
+
764
777
  // Lock the ambient host environment before any controller runs: a key the
765
778
  // manifest binds via `variables`/`secrets`/`ports` must be read through
766
779
  // `ctx.env` or the declared binding, never the raw `process.env` var. Every
@@ -1145,6 +1158,7 @@ export class Kernel implements IKernel {
1145
1158
  args?: ParsedArgs,
1146
1159
  ownerPrefix = "",
1147
1160
  owningContext?: IEvaluationContext,
1161
+ resolvedKind?: string,
1148
1162
  ): ResourceContext {
1149
1163
  return new ResourceContextImpl(
1150
1164
  this,
@@ -1158,6 +1172,7 @@ export class Kernel implements IKernel {
1158
1172
  args,
1159
1173
  ownerPrefix,
1160
1174
  owningContext ?? moduleContext,
1175
+ resolvedKind,
1161
1176
  );
1162
1177
  }
1163
1178
 
@@ -1275,6 +1290,19 @@ export class Kernel implements IKernel {
1275
1290
  const compile = [...parentEval.compile, ...ownEval.compile];
1276
1291
  const runtime = [...parentEval.runtime, ...ownEval.runtime];
1277
1292
 
1293
+ // Embedded files are read here, at the single instance-production site, and
1294
+ // not at manifest load: `telo.yaml` is its own artifact layer so that reading
1295
+ // a manifest cannot pull the payload, and an app loads every imported
1296
+ // library's manifest. Before schema validation, so the schema sees the
1297
+ // resolved value — bytes arrive at an `x-telo-binary` slot as the
1298
+ // `Uint8Array` that annotation demands.
1299
+ await resolveIncludeSentinels(
1300
+ resource,
1301
+ this.findModuleContext(evalContext).source,
1302
+ this,
1303
+ this.includeCache,
1304
+ );
1305
+
1278
1306
  // Schema validation runs before CEL evaluation so it sees the original manifest
1279
1307
  // shape. CompiledValue wrappers (from load-time precompilation) are stripped,
1280
1308
  // restoring the pre-CEL string view that the schema expects.
@@ -1308,6 +1336,7 @@ export class Kernel implements IKernel {
1308
1336
  // Snapshot publication targets the context that OWNS the instance — for a
1309
1337
  // `with:`-scoped resource that is the per-run scope child, not the module.
1310
1338
  evalContext,
1339
+ resolvedKind,
1311
1340
  );
1312
1341
  const instance = await controller.create(processedResource, ctx);
1313
1342
  if (!instance) return null;
@@ -1,4 +1,4 @@
1
- import { formatUnixNano, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
1
+ import { bigIntAt, formatUnixNano, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
2
2
 
3
3
  /**
4
4
  * The `json` encoding — `kernel/specs/logging.md` §11.1. One JSON object per
@@ -83,16 +83,21 @@ function sortKeysDeep(value: AnyValue): AnyValue {
83
83
  }
84
84
 
85
85
  function makeReplacer(encodeBytes: BytesEncoder) {
86
- return function replacer(this: unknown, _key: string, value: unknown): unknown {
87
- if (typeof value === "bigint") {
88
- // Values beyond 2^53 lose precision in a JS receiver, so they degrade to a
89
- // decimal string rather than to a wrong number — the same reasoning OTLP
90
- // gives for quoting its 64-bit fields.
91
- return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)
92
- ? Number(value)
93
- : value.toString();
86
+ return function replacer(this: unknown, key: string, value: unknown): unknown {
87
+ // Read the BigInt off the HOLDER, not off `value`: `BigInt.prototype.toJSON`
88
+ // has already rewritten it to the exact-digits form every other JSON boundary
89
+ // wants (see `enableBigIntJson`). A log record is the one destination where
90
+ // that is wrong a value beyond 2^53 loses precision in a JS receiver, so it
91
+ // degrades to a decimal string rather than to a wrong number, the same
92
+ // reasoning OTLP gives for quoting its 64-bit fields.
93
+ const source = bigIntAt(this, key);
94
+ if (source !== undefined) {
95
+ return source >= MIN_SAFE && source <= MAX_SAFE ? Number(source) : source.toString();
94
96
  }
95
97
  if (value instanceof Uint8Array) return encodeBytes(value);
96
98
  return value;
97
99
  };
98
100
  }
101
+
102
+ const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
103
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
@@ -1,4 +1,4 @@
1
- import { severityFloor, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
1
+ import { bigIntAt, severityFloor, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
2
2
 
3
3
  /**
4
4
  * The `pretty` encoding — `kernel/specs/logging.md` §11.2. For humans on a
@@ -90,8 +90,17 @@ function quoteIfNeeded(text: string): string {
90
90
  return /[\s"=]/.test(text) ? JSON.stringify(text) : text;
91
91
  }
92
92
 
93
- function jsonSafe(_key: string, value: unknown): unknown {
94
- if (typeof value === "bigint") return value.toString();
93
+ function jsonSafe(this: unknown, key: string, value: unknown): unknown {
94
+ // Read the integer off the HOLDER, not off `value`: `BigInt.prototype.toJSON`
95
+ // (see `enableBigIntJson`) runs BEFORE a replacer, so by the time this is
96
+ // called a wide integer is already the exact-digits form every wire encoding
97
+ // wants. This console encoding renders it as a decimal string instead, and
98
+ // matching on `typeof value === "bigint"` would silently stop doing so —
99
+ // a changed encoding, not an error. Reading the holder keeps the rendering
100
+ // identical whether or not the patch is installed, which matters because a
101
+ // test can call this encoder outside a booted kernel.
102
+ const source = bigIntAt(this, key);
103
+ if (source !== undefined) return source.toString();
95
104
  if (value instanceof Uint8Array) return `<${value.byteLength} bytes>`;
96
105
  return value;
97
106
  }
@@ -0,0 +1,64 @@
1
+ import * as path from "path";
2
+ import { pathToFileURL } from "url";
3
+ import { RuntimeError } from "@telorun/sdk";
4
+ import type { ModuleArtifact } from "./bundle/module-artifact.js";
5
+
6
+ /** The slice of the kernel this module needs: a module's artifact, when it has
7
+ * one. A module already on disk has none — that is normal, not an error. */
8
+ export interface ModuleArtifactLookup {
9
+ getModuleArtifact(source: string | undefined): ModuleArtifact | undefined;
10
+ }
11
+
12
+ /**
13
+ * Resolve a module-relative reference against the declaring module's own
14
+ * directory, materializing the layers that could carry it on first use.
15
+ *
16
+ * A URI, not a filesystem path: the SDK is cross-runtime, and a path is only
17
+ * what *this* kernel happens to return for a module whose files are local. An
18
+ * already-absolute URI (one with a scheme) passes through untouched; a bare
19
+ * absolute filesystem path is returned as a `file://` URI rather than being
20
+ * rebased onto the module directory.
21
+ *
22
+ * Shared by `ctx.resolveModuleFile` and by `!include-*` resolution, so a file
23
+ * reached by a controller and a file embedded by a tag are located by one rule
24
+ * — including which layers get materialized on the way.
25
+ */
26
+ export async function resolveModuleFileUri(
27
+ relative: string,
28
+ source: string,
29
+ lookup: ModuleArtifactLookup,
30
+ ): Promise<string> {
31
+ // An absolute URI names its own location; a bare absolute path is already
32
+ // resolved and must not be rebased onto the module directory.
33
+ if (/^[a-z][a-z0-9+.-]*:/i.test(relative)) return relative;
34
+ if (path.isAbsolute(relative)) return pathToFileURL(relative).href;
35
+
36
+ const artifact = lookup.getModuleArtifact(source);
37
+ if (artifact) {
38
+ // Both the `assets` layer and `common` — the sink rule puts a file the
39
+ // author did not claim via `assets:` into `common`, and a module that ships
40
+ // static files with no bundled controller has no other route to its payload.
41
+ // Fetching only assets would leave such a module resolving into an empty
42
+ // directory.
43
+ await artifact.materializeModuleFiles();
44
+ return new URL(relative, pathToFileURL(path.join(artifact.directory, "/")).href).href;
45
+ }
46
+ // No artifact means no payload to fetch. That is normal for a module already
47
+ // on disk (development) or one that ships no files — but for a module reached
48
+ // over a non-local scheme it means the artifact carries no layer index, i.e. it
49
+ // predates layers. Raise the actionable error here rather than leaving each
50
+ // caller to invent its own message from a URI it cannot open.
51
+ if (!source.startsWith("file://") && !path.isAbsolute(source)) {
52
+ throw new RuntimeError(
53
+ "ERR_MODULE_FILES_UNAVAILABLE",
54
+ `Cannot resolve '${relative}' against module '${source}': the module's artifact ` +
55
+ `carries no layer index, so its files cannot be located. It was published by an ` +
56
+ `older Telo that wrote a single-blob artifact — republish the module, or import it ` +
57
+ `from a local path during development.`,
58
+ );
59
+ }
60
+ // Local module: resolve against the manifest URL, the same rule `include:`
61
+ // and sibling imports follow.
62
+ const base = source.startsWith("file://") ? source : pathToFileURL(source).href;
63
+ return new URL(relative, base).href;
64
+ }