@ontrails/core 0.2.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 (86) hide show
  1. package/CHANGELOG.md +849 -0
  2. package/README.md +190 -0
  3. package/package.json +36 -0
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-derivation.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +90 -0
  9. package/src/branded.ts +135 -0
  10. package/src/collections.ts +99 -0
  11. package/src/compose-batch.ts +69 -0
  12. package/src/compose-schema.ts +36 -0
  13. package/src/context.ts +66 -0
  14. package/src/derive.ts +485 -0
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/entity.ts +346 -0
  19. package/src/error-rendering.ts +87 -0
  20. package/src/errors.ts +483 -0
  21. package/src/execute.ts +1577 -0
  22. package/src/fetch.ts +138 -0
  23. package/src/fire.ts +1172 -0
  24. package/src/glob.ts +81 -0
  25. package/src/guards.ts +37 -0
  26. package/src/index.ts +704 -0
  27. package/src/internal/fork-ctx.ts +69 -0
  28. package/src/layer-field-rendering.ts +193 -0
  29. package/src/layer.ts +81 -0
  30. package/src/observe.ts +361 -0
  31. package/src/path-scope.ts +66 -0
  32. package/src/path-security.ts +98 -0
  33. package/src/patterns/bulk.ts +16 -0
  34. package/src/patterns/change.ts +12 -0
  35. package/src/patterns/date-range.ts +12 -0
  36. package/src/patterns/index.ts +8 -0
  37. package/src/patterns/pagination.ts +22 -0
  38. package/src/patterns/progress.ts +13 -0
  39. package/src/patterns/sorting.ts +14 -0
  40. package/src/patterns/status.ts +11 -0
  41. package/src/patterns/timestamps.ts +12 -0
  42. package/src/permits.ts +12 -0
  43. package/src/queue.ts +163 -0
  44. package/src/redaction/index.ts +3 -0
  45. package/src/redaction/patterns.ts +50 -0
  46. package/src/redaction/redactor.ts +178 -0
  47. package/src/resilience.ts +234 -0
  48. package/src/resource-config.ts +804 -0
  49. package/src/resource.ts +194 -0
  50. package/src/result.ts +212 -0
  51. package/src/run.ts +76 -0
  52. package/src/runtime-builtins.ts +69 -0
  53. package/src/schedule-runtime.ts +689 -0
  54. package/src/schedule.ts +326 -0
  55. package/src/serialization.ts +265 -0
  56. package/src/sha256.ts +136 -0
  57. package/src/signal-diagnostics.ts +633 -0
  58. package/src/signal-ref.ts +111 -0
  59. package/src/signal.ts +104 -0
  60. package/src/store/accessor-protocol.ts +56 -0
  61. package/src/store/index.ts +4 -0
  62. package/src/structured-examples.ts +248 -0
  63. package/src/surface-derivation.ts +91 -0
  64. package/src/surface-filter.ts +101 -0
  65. package/src/surface-overlay.ts +694 -0
  66. package/src/surface-versioning.ts +42 -0
  67. package/src/topo.ts +835 -0
  68. package/src/tracing.ts +346 -0
  69. package/src/trail-id-glob.ts +15 -0
  70. package/src/trail.ts +1351 -0
  71. package/src/trails/derive-trail.ts +835 -0
  72. package/src/trails/index.ts +9 -0
  73. package/src/trails/ingest.ts +152 -0
  74. package/src/trails-db.ts +212 -0
  75. package/src/transport-error-map.ts +163 -0
  76. package/src/type-utils.ts +87 -0
  77. package/src/types.ts +300 -0
  78. package/src/validate-established-topo.ts +73 -0
  79. package/src/validate-topo.ts +725 -0
  80. package/src/validation.ts +330 -0
  81. package/src/version-marker.ts +716 -0
  82. package/src/version-resolution.ts +308 -0
  83. package/src/version-runtime.ts +120 -0
  84. package/src/webhook.ts +461 -0
  85. package/src/workspace.ts +244 -0
  86. package/src/zod-wrappers.ts +72 -0
@@ -0,0 +1,194 @@
1
+ import { InternalError } from './errors.js';
2
+ import type { Result } from './result.js';
3
+ import type { AnySignal } from './signal.js';
4
+ import type { ResourceLookup, TrailContext } from './types.js';
5
+ import type { z } from 'zod';
6
+
7
+ /**
8
+ * Stable process-scoped fields available when constructing a resource.
9
+ *
10
+ * Resources are app-level singletons, so they intentionally do not receive
11
+ * the full per-request TrailContext. When a resource declares a `config` schema,
12
+ * the validated config is passed as `resourceCtx.config`.
13
+ */
14
+ export type ResourceContext<C = unknown> = Pick<
15
+ TrailContext,
16
+ 'cwd' | 'env' | 'workspaceRoot'
17
+ > & {
18
+ readonly config: C;
19
+ };
20
+
21
+ /** Explicit marker for resources that intentionally cannot provide a mock. */
22
+ export interface ResourceUnmockable {
23
+ readonly reason: string;
24
+ }
25
+
26
+ /**
27
+ * Everything needed to describe a resource before a factory is introduced.
28
+ *
29
+ * When `config` is a Zod schema, the `create` callback receives
30
+ * `ResourceContext<C>` with the validated config value.
31
+ */
32
+ export interface ResourceSpec<T, C = unknown> {
33
+ /** Create the resource instance from stable process-scoped context. */
34
+ readonly create: (
35
+ resourceCtx: ResourceContext<C>
36
+ ) => Result<T, Error> | Promise<Result<T, Error>>;
37
+ /** Config schema — when present, config is validated and passed to `create`. */
38
+ readonly config?: z.ZodType<C> | undefined;
39
+ /** Optional cleanup performed when the host application shuts down. */
40
+ readonly dispose?: ((resource: T) => void | Promise<void>) | undefined;
41
+ /** Optional operational readiness probe for introspection tooling. */
42
+ readonly health?:
43
+ | ((
44
+ resource: T
45
+ ) => Result<unknown, Error> | Promise<Result<unknown, Error>>)
46
+ | undefined;
47
+ /** Optional test factory used by higher-level helpers. */
48
+ readonly mock?: (() => T | Promise<T>) | undefined;
49
+ /** Document why this resource intentionally cannot provide a test mock. */
50
+ readonly unmockable?: ResourceUnmockable | undefined;
51
+ /** Human-readable description. */
52
+ readonly description?: string | undefined;
53
+ /** Arbitrary meta for tooling and filtering. */
54
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
55
+ /** Signals derived or owned by this resource. */
56
+ readonly signals?: readonly AnySignal[] | undefined;
57
+ /** Reserved for future resource-specific design; trail versioning is trail-only. */
58
+ readonly version?: never;
59
+ }
60
+
61
+ type ResourceSpecWithConfig<T, S extends z.ZodTypeAny> = Omit<
62
+ ResourceSpec<T, z.infer<S>>,
63
+ 'config'
64
+ > & {
65
+ readonly config: S;
66
+ };
67
+
68
+ /** A typed resource definition. */
69
+ export interface Resource<T, C = unknown> extends ResourceSpec<T, C> {
70
+ readonly kind: 'resource';
71
+ readonly id: string;
72
+ /** Read the resolved resource instance from a trail context. */
73
+ from(ctx: TrailContext): T;
74
+ }
75
+
76
+ /**
77
+ * Existential type for heterogeneous resource collections.
78
+ *
79
+ * `Resource<T>` includes function parameters in `dispose`/`health`, so
80
+ * `unknown` is too narrow for mixed resource arrays. `any` is the correct
81
+ * existential here.
82
+ */
83
+ // oxlint-disable-next-line no-explicit-any -- existential type for heterogeneous resource collections
84
+ export type AnyResource = Resource<any, any>;
85
+
86
+ /** Explicit runtime overrides keyed by resource ID. */
87
+ export type ResourceOverrideMap = Readonly<Record<string, unknown>>;
88
+
89
+ const getResourceId = <T>(
90
+ resourceOrId: string | Pick<Resource<T>, 'id'>
91
+ ): string =>
92
+ typeof resourceOrId === 'string' ? resourceOrId : resourceOrId.id;
93
+
94
+ const getResourceInstance = <T>(
95
+ ctx: Pick<TrailContext, 'extensions'>,
96
+ resourceOrId: string | Pick<Resource<T>, 'id'>
97
+ ): T => {
98
+ const id = getResourceId(resourceOrId);
99
+ return ctx.extensions?.[id] as T;
100
+ };
101
+
102
+ const hasResourceInstance = (
103
+ ctx: Pick<TrailContext, 'extensions'>,
104
+ resourceOrId: string | Pick<AnyResource, 'id'>
105
+ ): boolean => Object.hasOwn(ctx.extensions ?? {}, getResourceId(resourceOrId));
106
+
107
+ /** Create a `ctx.resource(...)` accessor bound to a concrete context snapshot. */
108
+ export const createResourceLookup = (
109
+ getContext: () => Pick<TrailContext, 'extensions'>
110
+ ): ResourceLookup =>
111
+ ((resourceOrId: string | Pick<AnyResource, 'id'>) => {
112
+ const id = getResourceId(resourceOrId);
113
+ const ctx = getContext();
114
+ if (!hasResourceInstance(ctx, id)) {
115
+ throw new InternalError(`Resource "${id}" not provisioned in context`);
116
+ }
117
+ return getResourceInstance(ctx, id);
118
+ }) as ResourceLookup;
119
+
120
+ /**
121
+ * Create a typed resource definition.
122
+ *
123
+ * The resource object is inert until a later execution branch resolves concrete
124
+ * instances into TrailContext extensions.
125
+ */
126
+ export function resource<T, S extends z.ZodTypeAny>(
127
+ id: string,
128
+ spec: ResourceSpecWithConfig<T, S>
129
+ ): Resource<T, z.infer<S>>;
130
+ export function resource<T, C = unknown>(
131
+ id: string,
132
+ spec: ResourceSpec<T, C>
133
+ ): Resource<T, C>;
134
+ export function resource<T, C = unknown>(
135
+ id: string,
136
+ spec: ResourceSpec<T, C>
137
+ ): Resource<T, C> {
138
+ if (id.includes(':')) {
139
+ throw new InternalError(
140
+ `Resource "${id}" is invalid because resource ids may not contain ":"`
141
+ );
142
+ }
143
+ if (spec.mock !== undefined && spec.unmockable !== undefined) {
144
+ throw new InternalError(
145
+ `Resource "${id}" cannot define both mock and unmockable`
146
+ );
147
+ }
148
+ if (
149
+ spec.unmockable !== undefined &&
150
+ spec.unmockable.reason.trim().length === 0
151
+ ) {
152
+ throw new InternalError(
153
+ `Resource "${id}" is invalid because unmockable.reason must not be empty`
154
+ );
155
+ }
156
+
157
+ return Object.freeze({
158
+ ...spec,
159
+ from(ctx: TrailContext): T {
160
+ const lookup = ctx.resource ?? createResourceLookup(() => ctx);
161
+ return lookup(this);
162
+ },
163
+ id,
164
+ kind: 'resource' as const,
165
+ });
166
+ }
167
+
168
+ /** Narrow unknown values to resource definitions during topo discovery. */
169
+ export const isResource = (value: unknown): value is AnyResource => {
170
+ if (typeof value !== 'object' || value === null) {
171
+ return false;
172
+ }
173
+ const v = value as { kind?: unknown; id?: unknown };
174
+ return v.kind === 'resource' && typeof v.id === 'string';
175
+ };
176
+
177
+ /**
178
+ * Return the first duplicate resource ID in a collection, if any.
179
+ *
180
+ * This supports later topo registration without each caller duplicating the
181
+ * same scan logic.
182
+ */
183
+ export const findDuplicateResourceId = (
184
+ resources: readonly Pick<AnyResource, 'id'>[]
185
+ ): string | undefined => {
186
+ const seen = new Set<string>();
187
+ for (const candidate of resources) {
188
+ if (seen.has(candidate.id)) {
189
+ return candidate.id;
190
+ }
191
+ seen.add(candidate.id);
192
+ }
193
+ return undefined;
194
+ };
package/src/result.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * A type-safe Result monad for representing success/failure without exceptions.
3
+ */
4
+
5
+ import { InternalError, ValidationError } from './errors.js';
6
+
7
+ export const resultAccessorNames = [
8
+ 'error',
9
+ 'flatMap',
10
+ 'isErr',
11
+ 'isOk',
12
+ 'map',
13
+ 'mapErr',
14
+ 'match',
15
+ 'unwrap',
16
+ 'unwrapOr',
17
+ 'value',
18
+ ] as const satisfies readonly (
19
+ | keyof Ok<unknown, unknown>
20
+ | keyof Err<unknown>
21
+ )[];
22
+
23
+ export type ResultAccessorName = (typeof resultAccessorNames)[number];
24
+
25
+ class Ok<T, E> {
26
+ readonly value: T;
27
+
28
+ constructor(value: T) {
29
+ this.value = value;
30
+ }
31
+
32
+ // oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
33
+ isOk(): this is Ok<T, E> {
34
+ return true;
35
+ }
36
+
37
+ // oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
38
+ isErr(): this is Err<E> {
39
+ return false;
40
+ }
41
+
42
+ map<U>(fn: (value: T) => U): Result<U, E> {
43
+ return new Ok(fn(this.value));
44
+ }
45
+
46
+ flatMap<U, F = E>(fn: (value: T) => Result<U, F>): Result<U, E | F> {
47
+ return fn(this.value);
48
+ }
49
+
50
+ mapErr<F>(_fn: (error: E) => F): Result<T, F> {
51
+ return new Ok(this.value);
52
+ }
53
+
54
+ match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U {
55
+ return handlers.ok(this.value);
56
+ }
57
+
58
+ unwrap(): T {
59
+ return this.value;
60
+ }
61
+
62
+ unwrapOr(_fallback: T): T {
63
+ return this.value;
64
+ }
65
+ }
66
+
67
+ // oxlint-disable-next-line max-classes-per-file -- Result monad requires paired Ok/Err classes
68
+ class Err<E> {
69
+ readonly error: E;
70
+
71
+ constructor(error: E) {
72
+ this.error = error;
73
+ }
74
+
75
+ // oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
76
+ isOk(): this is Ok<never, E> {
77
+ return false;
78
+ }
79
+
80
+ // oxlint-disable-next-line class-methods-use-this -- type guard for Result discriminated union
81
+ isErr(): this is Err<E> {
82
+ return true;
83
+ }
84
+
85
+ map<U>(_fn: (value: never) => U): Result<U, E> {
86
+ return new Err(this.error);
87
+ }
88
+
89
+ flatMap<U, F = E>(_fn: (value: never) => Result<U, F>): Result<U, E | F> {
90
+ return new Err(this.error);
91
+ }
92
+
93
+ mapErr<F>(fn: (error: E) => F): Result<never, F> {
94
+ return new Err(fn(this.error));
95
+ }
96
+
97
+ match<U>(handlers: { ok: (value: never) => U; err: (error: E) => U }): U {
98
+ return handlers.err(this.error);
99
+ }
100
+
101
+ unwrap(): never {
102
+ throw this.error instanceof Error
103
+ ? this.error
104
+ : new Error(String(this.error));
105
+ }
106
+
107
+ // oxlint-disable-next-line class-methods-use-this -- symmetric API with Ok.unwrapOr
108
+ unwrapOr<T>(fallback: T): T {
109
+ return fallback;
110
+ }
111
+ }
112
+
113
+ export type Result<T, E = Error> = Ok<T, E> | Err<E>;
114
+
115
+ // eslint-disable-next-line @typescript-eslint/no-namespace
116
+ export const Result = {
117
+ combine<T, E>(results: readonly Result<T, E>[]): Result<T[], E> {
118
+ const values: T[] = [];
119
+ for (const result of results) {
120
+ if (result.isErr()) {
121
+ return new Err(result.error);
122
+ }
123
+ values.push(result.value);
124
+ }
125
+ return new Ok(values);
126
+ },
127
+
128
+ err<E>(error: E): Err<E> {
129
+ return new Err(error);
130
+ },
131
+
132
+ /**
133
+ * Wrap a fetch call in a Result, mapping failures to TrailsError subclasses.
134
+ *
135
+ * Network errors become NetworkError. Abort signals become CancelledError.
136
+ * HTTP error status codes map to the appropriate error category.
137
+ */
138
+ async fromFetch(
139
+ input: string | URL | Request,
140
+ init?: RequestInit
141
+ ): Promise<Result<Response, Error>> {
142
+ // Lazy import avoids a circular dependency (fetch.ts imports Result)
143
+ const { fromFetch: fetchImpl } = await import('./fetch.js');
144
+ return fetchImpl(input, init);
145
+ },
146
+
147
+ /**
148
+ * Parse a JSON string, returning a Result instead of throwing.
149
+ */
150
+ fromJson(json: string): Result<unknown, ValidationError> {
151
+ try {
152
+ return new Ok(JSON.parse(json) as unknown);
153
+ } catch (error) {
154
+ return new Err(
155
+ new ValidationError('Invalid JSON', {
156
+ cause: error instanceof Error ? error : new Error(String(error)),
157
+ context: { input: json.slice(0, 200) },
158
+ })
159
+ );
160
+ }
161
+ },
162
+
163
+ ok<T = void>(value?: T): Result<T, never> {
164
+ return new Ok(value as T);
165
+ },
166
+
167
+ /**
168
+ * Stringify a value to JSON, returning a Result. Handles circular references.
169
+ */
170
+ toJson(value: unknown): Result<string, InternalError> {
171
+ try {
172
+ // Track the current ancestor chain, not every object ever visited.
173
+ // This allows shared references in a DAG while still detecting cycles.
174
+ const stack: unknown[] = [];
175
+ const keys: string[] = [];
176
+
177
+ const json = JSON.stringify(value, function json(key, val: unknown) {
178
+ if (stack.length > 0) {
179
+ // `this` is the object that contains `key`. Trim the stack back
180
+ // to `this` so we only track the current ancestor path.
181
+ const thisIndex = stack.lastIndexOf(this as unknown);
182
+ stack.splice(thisIndex + 1);
183
+ keys.splice(thisIndex);
184
+ }
185
+
186
+ if (typeof val === 'object' && val !== null) {
187
+ if (stack.includes(val)) {
188
+ return '[Circular]';
189
+ }
190
+ stack.push(val);
191
+ keys.push(key);
192
+ }
193
+ return val;
194
+ });
195
+
196
+ if (json === undefined) {
197
+ return new Err(
198
+ new InternalError('Value is not JSON-serializable', {
199
+ context: { type: typeof value },
200
+ })
201
+ );
202
+ }
203
+ return new Ok(json);
204
+ } catch (error) {
205
+ return new Err(
206
+ new InternalError('Failed to stringify value', {
207
+ cause: error instanceof Error ? error : new Error(String(error)),
208
+ })
209
+ );
210
+ }
211
+ },
212
+ } as const;
package/src/run.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Headless trail execution without surface registration.
3
+ *
4
+ * Looks up a trail by ID in a topo, then delegates to `executeTrail`.
5
+ * Returns a `Result` and never throws.
6
+ */
7
+
8
+ import type { Topo } from './topo.js';
9
+ import { executeTrail } from './execute.js';
10
+ import type { ExecuteTrailOptions } from './execute.js';
11
+ import { parseTrailIdVersionReference } from './version-resolution.js';
12
+ import { NotFoundError, ValidationError } from './errors.js';
13
+ import { Result } from './result.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Options
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Options forwarded to `executeTrail` from `run`. */
20
+ export type RunOptions = ExecuteTrailOptions;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // run()
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Execute a trail by ID from a topo without registering it on a surface.
28
+ *
29
+ * Resolves the trail from the topo, then runs it through the standard
30
+ * `executeTrail` pipeline with `topo` threaded so `ctx.fire()` is bound
31
+ * to the producer's context inside `executeTrail`. Returns
32
+ * `Result.err(NotFoundError)` if the trail ID is not registered. Never
33
+ * throws — unexpected exceptions are returned as `Result.err(InternalError)`.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const result = await run(myTopo, 'greet', { name: 'Alice' });
38
+ * if (result.isOk()) console.log(result.value);
39
+ * ```
40
+ */
41
+ export const run = (
42
+ topo: Topo,
43
+ id: string,
44
+ input: unknown,
45
+ options?: RunOptions
46
+ ): Promise<Result<unknown, Error>> => {
47
+ const parsed = parseTrailIdVersionReference(id);
48
+ if (parsed.isErr()) {
49
+ return Promise.resolve(Result.err(parsed.error));
50
+ }
51
+ if (parsed.value.version !== undefined && options?.version !== undefined) {
52
+ return Promise.resolve(
53
+ Result.err(
54
+ new ValidationError(
55
+ `Trail "${parsed.value.id}" cannot combine an @version suffix with options.version`
56
+ )
57
+ )
58
+ );
59
+ }
60
+
61
+ const trail = topo.get(parsed.value.id);
62
+ if (trail === undefined) {
63
+ return Promise.resolve(
64
+ Result.err(
65
+ new NotFoundError(`Trail "${id}" not found in topo "${topo.name}"`)
66
+ )
67
+ );
68
+ }
69
+ return executeTrail(trail, input, {
70
+ ...options,
71
+ ...(parsed.value.version === undefined
72
+ ? {}
73
+ : { version: parsed.value.version }),
74
+ topo,
75
+ });
76
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Lazy runtime-builtin loading for the execution-portable core barrel.
3
+ *
4
+ * Core tooling modules that need Bun or Node capabilities (`bun:sqlite`,
5
+ * `node:fs`, `node:os`, `node:path`) must not import them eagerly: the
6
+ * core barrel sits on the execution path of every surface, and edge
7
+ * runtimes such as workerd refuse module graphs that import `bun:`
8
+ * builtins and gate `node:` builtins behind compatibility flags
9
+ * (TRL-1198). Loading through `process.getBuiltinModule` keeps the
10
+ * specifier out of bundler module graphs entirely and defers the
11
+ * capability requirement to first use, so runtimes that never call a
12
+ * tooling helper never pay for it.
13
+ */
14
+
15
+ import type * as BunSqlite from 'bun:sqlite';
16
+ import type * as NodeFs from 'node:fs';
17
+ import type * as NodeOs from 'node:os';
18
+ import type * as NodePath from 'node:path';
19
+
20
+ import { InternalError } from './errors.js';
21
+
22
+ interface BuiltinModules {
23
+ readonly 'bun:sqlite': typeof BunSqlite;
24
+ readonly 'node:fs': typeof NodeFs;
25
+ readonly 'node:os': typeof NodeOs;
26
+ readonly 'node:path': typeof NodePath;
27
+ }
28
+
29
+ const loadedBuiltins = new Map<keyof BuiltinModules, unknown>();
30
+
31
+ /**
32
+ * Load a Bun/Node builtin module at first use.
33
+ *
34
+ * @throws {InternalError} When the runtime does not expose the builtin
35
+ * (for example workerd without `nodejs_compat`). Trail execution never
36
+ * reaches this loader; only tooling helpers (trails-db, workspace
37
+ * discovery) do.
38
+ */
39
+ export const loadRuntimeBuiltin = <TName extends keyof BuiltinModules>(
40
+ name: TName
41
+ ): BuiltinModules[TName] => {
42
+ const cached = loadedBuiltins.get(name);
43
+ if (cached !== undefined) {
44
+ return cached as BuiltinModules[TName];
45
+ }
46
+
47
+ const proc = (
48
+ globalThis as {
49
+ readonly process?: {
50
+ readonly getBuiltinModule?: (id: string) => unknown;
51
+ };
52
+ }
53
+ ).process;
54
+ if (typeof proc?.getBuiltinModule !== 'function') {
55
+ throw new InternalError(
56
+ `Runtime builtin "${name}" is unavailable: this runtime does not expose process.getBuiltinModule. Trails tooling helpers need a Bun or Node runtime; the trail execution path never loads them.`
57
+ );
58
+ }
59
+
60
+ const loaded = proc.getBuiltinModule(name);
61
+ if (loaded === undefined || loaded === null) {
62
+ throw new InternalError(
63
+ `Runtime builtin "${name}" is unavailable on this runtime.`
64
+ );
65
+ }
66
+
67
+ loadedBuiltins.set(name, loaded);
68
+ return loaded as BuiltinModules[TName];
69
+ };