@ontrails/core 1.0.0-beta.39 → 1.0.0-beta.41

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.
package/src/trail.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  isActivationEntrySpec,
12
12
  isActivationSource,
13
13
  } from './activation-source.js';
14
- import type { AnyContour } from './contour.js';
14
+ import type { AnyEntity } from './entity.js';
15
15
  import type {
16
16
  FieldOverride,
17
17
  CliCommandPathInput,
@@ -24,6 +24,7 @@ import type { AnySignal } from './signal.js';
24
24
  import {
25
25
  createLateBoundSignalMarker,
26
26
  getLateBoundSignalRef,
27
+ isBoundLateBoundSignal,
27
28
  } from './signal-ref.js';
28
29
  import type { TrailsError } from './errors.js';
29
30
  import type {
@@ -75,18 +76,18 @@ export interface TrailExample<I, O> {
75
76
  }
76
77
 
77
78
  // ---------------------------------------------------------------------------
78
- // Blaze input — merges composeInput when declared
79
+ // Implementation input — merges composeInput when declared
79
80
  // ---------------------------------------------------------------------------
80
81
 
81
82
  /**
82
- * The input type received by a trail's blaze function.
83
+ * The input type received by a trail's implementation function.
83
84
  *
84
85
  * When a trail declares `composeInput`, the runtime merges those fields into
85
- * the input object before calling blaze. This type makes the compiler aware
86
+ * the input object before calling implementation. This type makes the compiler aware
86
87
  * of the merged shape so developers can access composeInput fields without a
87
88
  * cast. Falls back to plain `I` when `CI` is `never` (the default).
88
89
  */
89
- export type BlazeInput<I, CI> = [CI] extends [never] ? I : I & CI;
90
+ export type ImplementationInput<I, CI> = [CI] extends [never] ? I : I & CI;
90
91
 
91
92
  type TrailRef = string | { readonly id: string };
92
93
 
@@ -105,13 +106,18 @@ type SchemaOwnedTrailSpec<
105
106
  ComposeSchemaOutput<TComposeInputSchema>,
106
107
  C
107
108
  >,
108
- 'blaze' | 'composeInput' | 'detours' | 'examples' | 'input' | 'versions'
109
+ | 'implementation'
110
+ | 'composeInput'
111
+ | 'detours'
112
+ | 'examples'
113
+ | 'input'
114
+ | 'versions'
109
115
  > & {
110
- /** Zod schema for validating caller input and materializing blaze input. */
116
+ /** Zod schema for validating caller input and materializing implementation input. */
111
117
  readonly input: TInputSchema;
112
118
  /** The pure function receives schema-materialized input after validation/defaults. */
113
- readonly blaze: Implementation<
114
- BlazeInput<
119
+ readonly implementation: Implementation<
120
+ ImplementationInput<
115
121
  z.output<TInputSchema>,
116
122
  ComposeSchemaOutput<TComposeInputSchema>
117
123
  >,
@@ -122,7 +128,7 @@ type SchemaOwnedTrailSpec<
122
128
  readonly examples?:
123
129
  | readonly TrailExample<z.input<TInputSchema>, O>[]
124
130
  | undefined;
125
- /** Recovery paths see the same materialized input as the blaze. */
131
+ /** Recovery paths see the same materialized input as the implementation. */
126
132
  readonly detours?:
127
133
  | readonly Detour<z.output<TInputSchema>, O, TrailsError>[]
128
134
  | undefined;
@@ -167,9 +173,9 @@ type LegacyOutputTrailSpec<
167
173
  O,
168
174
  CI,
169
175
  C extends readonly TrailRef[] | undefined,
170
- > = Omit<TrailSpec<I, O, CI, C>, 'blaze' | 'output'> & {
171
- readonly blaze: Implementation<
172
- BlazeInput<I, CI>,
176
+ > = Omit<TrailSpec<I, O, CI, C>, 'implementation' | 'output'> & {
177
+ readonly implementation: Implementation<
178
+ ImplementationInput<I, CI>,
173
179
  NoInfer<O>,
174
180
  ComposeContextFor<C>
175
181
  >;
@@ -181,8 +187,12 @@ type LegacyOutputlessTrailSpec<
181
187
  O,
182
188
  CI,
183
189
  C extends readonly TrailRef[] | undefined,
184
- > = Omit<TrailSpec<I, O, CI, C>, 'blaze' | 'output'> & {
185
- readonly blaze: Implementation<BlazeInput<I, CI>, O, ComposeContextFor<C>>;
190
+ > = Omit<TrailSpec<I, O, CI, C>, 'implementation' | 'output'> & {
191
+ readonly implementation: Implementation<
192
+ ImplementationInput<I, CI>,
193
+ O,
194
+ ComposeContextFor<C>
195
+ >;
186
196
  readonly output?: undefined;
187
197
  };
188
198
 
@@ -253,7 +263,7 @@ export interface TrailVersionRevisionEntry<
253
263
  CurrentInput = unknown,
254
264
  CurrentOutput = unknown,
255
265
  > extends VersionEntry<VersionContract<VersionInput, VersionOutput>> {
256
- readonly blaze?: never;
266
+ readonly implementation?: never;
257
267
  readonly composeInput?: never;
258
268
  readonly composes?: never;
259
269
  readonly detours?: never;
@@ -274,8 +284,8 @@ export interface TrailVersionForkEntry<
274
284
  VersionOutput = unknown,
275
285
  ComposeInput = never,
276
286
  > extends VersionEntry<VersionContract<VersionInput, VersionOutput>> {
277
- readonly blaze: Implementation<
278
- BlazeInput<VersionInput, ComposeInput>,
287
+ readonly implementation: Implementation<
288
+ ImplementationInput<VersionInput, ComposeInput>,
279
289
  VersionOutput
280
290
  >;
281
291
  readonly composes?: readonly (string | AnyTrail)[] | undefined;
@@ -316,7 +326,7 @@ export type TrailVersions<
316
326
  >;
317
327
 
318
328
  /**
319
- * Spec for {@link forkVersion}: a fork entry whose blaze signature is owned
329
+ * Spec for {@link forkVersion}: a fork entry whose implementation signature is owned
320
330
  * by the entry's own schemas instead of falling back to `unknown`.
321
331
  */
322
332
  export interface TrailVersionForkSpec<
@@ -324,9 +334,9 @@ export interface TrailVersionForkSpec<
324
334
  TOutputSchema extends z.ZodType,
325
335
  TComposeInputSchema extends z.ZodType | undefined = undefined,
326
336
  > {
327
- /** The historical blaze, typed by this entry's schemas. */
328
- readonly blaze: Implementation<
329
- BlazeInput<
337
+ /** The historical implementation, typed by this entry's schemas. */
338
+ readonly implementation: Implementation<
339
+ ImplementationInput<
330
340
  z.output<TInputSchema>,
331
341
  ComposeSchemaOutput<TComposeInputSchema>
332
342
  >,
@@ -351,15 +361,15 @@ export interface TrailVersionForkSpec<
351
361
  }
352
362
 
353
363
  /**
354
- * Author a fork version entry with a blaze typed by the entry's own schemas.
364
+ * Author a fork version entry with a implementation typed by the entry's own schemas.
355
365
  *
356
- * `TrailVersions` fixes every entry's generics to `unknown`, so a fork blaze
366
+ * `TrailVersions` fixes every entry's generics to `unknown`, so a fork implementation
357
367
  * written inline receives `unknown` input and authors end up re-parsing the
358
368
  * already-validated value just to narrow it. This helper threads the entry's
359
- * `input`/`output` schemas into the blaze signature and erases the generics
369
+ * `input`/`output` schemas into the implementation signature and erases the generics
360
370
  * on the way out. The erasure is sound because the fork pipeline validates
361
371
  * raw input against this entry's own `input` schema before dispatching to
362
- * the entry blaze (see `createForkTrailVersion` in execute.ts).
372
+ * the entry implementation (see `createForkTrailVersion` in execute.ts).
363
373
  *
364
374
  * @example
365
375
  * ```ts
@@ -371,7 +381,7 @@ export interface TrailVersionForkSpec<
371
381
  * version: 2,
372
382
  * versions: {
373
383
  * 1: forkVersion({
374
- * blaze: (input) =>
384
+ * implementation: (input) =>
375
385
  * // input is { name: string; weightOz: number } — no re-parse
376
386
  * Result.ok({ id: input.name, weightOz: input.weightOz }),
377
387
  * input: gearV1Input,
@@ -390,14 +400,14 @@ export const forkVersion = <
390
400
  ): TrailVersionForkEntry =>
391
401
  // Erasing the per-entry generics narrows function parameters, which TS
392
402
  // cannot express without a conversion. Safe: the fork pipeline re-validates
393
- // input against `spec.input` before the blaze runs.
403
+ // input against `spec.input` before the implementation runs.
394
404
  spec as unknown as TrailVersionForkEntry;
395
405
 
396
406
  export const getTrailVersionEntryKind = (
397
407
  entry: TrailVersionEntry
398
408
  ): TrailVersionEntryKind => {
399
409
  const raw = entry as unknown as Record<string, unknown>;
400
- return typeof raw['blaze'] === 'function' ? 'fork' : 'revision';
410
+ return typeof raw['implementation'] === 'function' ? 'fork' : 'revision';
401
411
  };
402
412
 
403
413
  export const isArchivedTrailVersionEntry = (
@@ -455,7 +465,11 @@ export interface TrailSpec<
455
465
  /** Zod schema for validating output (optional — some trails are fire-and-forget) */
456
466
  readonly output?: z.ZodType<O> | undefined;
457
467
  /** The pure function that does the work (sync or async authoring) */
458
- readonly blaze: Implementation<BlazeInput<I, CI>, O, ComposeContextFor<C>>;
468
+ readonly implementation: Implementation<
469
+ ImplementationInput<I, CI>,
470
+ O,
471
+ ComposeContextFor<C>
472
+ >;
459
473
  /** Human-readable description */
460
474
  readonly description?: string | undefined;
461
475
  /** Declared operational shape for governance, derivation, and agent guidance. */
@@ -478,16 +492,16 @@ export interface TrailSpec<
478
492
  readonly visibility?: TrailVisibility | undefined;
479
493
  /** Arbitrary meta for tooling and filtering */
480
494
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
481
- /** Recovery paths activated when blaze fails with a matching error class. */
495
+ /** Recovery paths activated when implementation fails with a matching error class. */
482
496
  readonly detours?: readonly Detour<I, O, TrailsError>[] | undefined;
483
497
  /**
484
498
  * Typed layers attached at trail scope.
485
499
  *
486
500
  * Layers declared here wrap this trail's implementation on every execution,
487
501
  * regardless of which surface invokes it. The execution pipeline composes
488
- * trail-scope layers innermost — closer to the blaze than surface-scope
502
+ * trail-scope layers innermost — closer to the implementation than surface-scope
489
503
  * or topo-scope layers — so the final order is
490
- * `topo → surface → trail → blaze` (outermost-first).
504
+ * `topo → surface → trail → implementation` (outermost-first).
491
505
  *
492
506
  * Layers are typed and inspectable. Omit `input` for surface-invisible
493
507
  * wrappers that do not project any fields.
@@ -497,15 +511,15 @@ export interface TrailSpec<
497
511
  readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
498
512
  /** CLI projection metadata for canonical command path overrides and aliases. */
499
513
  readonly cli?: CliCommandPathInput | TrailCliProjection | undefined;
500
- /** Contours this trail operates on. */
501
- readonly contours?: readonly AnyContour[] | undefined;
514
+ /** Entities this trail operates on. */
515
+ readonly entities?: readonly AnyEntity[] | undefined;
502
516
  /** IDs or trail objects of downstream trails this trail may invoke via ctx.compose() */
503
517
  readonly composes?: C;
504
518
  /**
505
519
  * Composition-only input schema — merged with `input` for `ctx.compose()` calls,
506
520
  * invisible to public surfaces (CLI, MCP, HTTP).
507
521
  *
508
- * Fields here are available in the blaze but are not derived into CLI flags,
522
+ * Fields here are available in the implementation but are not derived into CLI flags,
509
523
  * MCP tool parameters, or HTTP request bodies. Use for data that only makes
510
524
  * sense when one trail composes another (e.g. `forkedFrom`).
511
525
  */
@@ -564,8 +578,8 @@ export type TrailVisibility = 'public' | 'internal';
564
578
  export interface Trail<I, O, CI = never> extends Omit<
565
579
  TrailSpec<I, O, CI, readonly TrailRef[] | undefined>,
566
580
  | 'args'
567
- | 'blaze'
568
- | 'contours'
581
+ | 'implementation'
582
+ | 'entities'
569
583
  | 'composes'
570
584
  | 'composeInput'
571
585
  | 'detours'
@@ -577,20 +591,20 @@ export interface Trail<I, O, CI = never> extends Omit<
577
591
  > {
578
592
  readonly kind: 'trail';
579
593
  readonly id: string;
580
- readonly blaze: Implementation<BlazeInput<I, CI>, O>;
581
- /** Contours this trail operates on (always present, default []). */
582
- readonly contours: readonly AnyContour[];
594
+ readonly implementation: Implementation<ImplementationInput<I, CI>, O>;
595
+ /** Entities this trail operates on (always present, default []). */
596
+ readonly entities: readonly AnyEntity[];
583
597
  /** IDs of downstream trails this trail may invoke via ctx.compose() (always present, default []) */
584
598
  readonly composes: readonly string[];
585
599
  /** Composition-only input schema, merged with `input` for ctx.compose() calls (optional) */
586
600
  readonly composeInput?: z.ZodType<CI> | undefined;
587
- /** Recovery paths activated when blaze fails with a matching error (always present, default []). */
601
+ /** Recovery paths activated when implementation fails with a matching error (always present, default []). */
588
602
  readonly detours: readonly Detour<I, O, TrailsError>[];
589
603
  /**
590
604
  * Typed layers attached at trail scope (always present, default []).
591
605
  *
592
- * Composed innermost in the layer chain — closest to the blaze. The final
593
- * composition order is `topo → surface → trail → blaze` (outermost-first).
606
+ * Composed innermost in the layer chain — closest to the implementation. The final
607
+ * composition order is `topo → surface → trail → implementation` (outermost-first).
594
608
  */
595
609
  readonly layers: readonly Layer[];
596
610
  /** Resources this trail may access via resource.from(ctx) (always present, default []) */
@@ -616,18 +630,6 @@ export interface Trail<I, O, CI = never> extends Omit<
616
630
  // Factory
617
631
  // ---------------------------------------------------------------------------
618
632
 
619
- /**
620
- * Canonical scoped-signal id shape: `<scope>:<table>.<event>`.
621
- *
622
- * Matches exactly one `:` separating a non-empty scope from a non-empty
623
- * dotted tail of at least two segments (e.g. `identity:users.created`).
624
- * The scope forbids only `:` and whitespace so resource ids may contain
625
- * dots for namespacing (e.g. `demo.store:gists.created`). Tail segments
626
- * forbid both `:` and `.` so strings like `foo:bar` (no dot) or
627
- * `a:b.c.d` stay unambiguous.
628
- */
629
- const SCOPED_SIGNAL_ID = /^[^:\s]+:[^:.\s]+(?:\.[^:.\s]+)+$/;
630
-
631
633
  const normalizeSignalRef = (entry: string | AnySignal): string => {
632
634
  if (typeof entry === 'string') {
633
635
  return entry;
@@ -638,19 +640,10 @@ const normalizeSignalRef = (entry: string | AnySignal): string => {
638
640
  return entry.id;
639
641
  }
640
642
 
641
- // Already-scoped canonical ids (e.g. "identity:users.created") must pass
642
- // through unchanged. Rewriting them to a bare marker token collapses
643
- // multi-binding cases where the same store definition is bound under two
644
- // separate resources: both bindings share the late-bound token, so the
645
- // caller's explicit choice of scope would be lost and topo resolution
646
- // would throw an ambiguity error.
647
- //
648
- // Use a strict predicate that matches the canonical scoped shape
649
- // `<scope>:<table>.<event>` (exactly one `:` separating a non-empty scope
650
- // from a non-empty dotted tail of at least two segments). A looser
651
- // `includes(':')` check would let unscoped ids that happen to contain `:`
652
- // slip past markerization and then fail to resolve at topo finalization.
653
- if (SCOPED_SIGNAL_ID.test(entry.id)) {
643
+ // Bound refs preserve an explicit resource choice. Authored refs always
644
+ // become markers, even when a table name contains the scope separator.
645
+ // Ownership is a contract fact; do not infer it from signal ID grammar.
646
+ if (isBoundLateBoundSignal(entry)) {
654
647
  return entry.id;
655
648
  }
656
649
 
@@ -992,11 +985,11 @@ const normalizeVersionEntry = <CurrentInput, CurrentOutput>(
992
985
  );
993
986
  }
994
987
 
995
- const hasBlaze = typeof raw['blaze'] === 'function';
988
+ const hasImplementation = typeof raw['implementation'] === 'function';
996
989
  const hasTranspose = raw['transpose'] !== undefined;
997
- if (hasBlaze && hasTranspose) {
990
+ if (hasImplementation && hasTranspose) {
998
991
  throw new ValidationError(
999
- `Trail "${trailId}" version ${version} cannot declare both blaze and transpose`
992
+ `Trail "${trailId}" version ${version} cannot declare both implementation and transpose`
1000
993
  );
1001
994
  }
1002
995
 
@@ -1013,11 +1006,14 @@ const normalizeVersionEntry = <CurrentInput, CurrentOutput>(
1013
1006
  : { status: normalizeVersionStatus(trailId, version, raw['status']) }),
1014
1007
  };
1015
1008
 
1016
- if (hasBlaze) {
1009
+ if (hasImplementation) {
1017
1010
  return Object.freeze({
1018
1011
  ...base,
1019
- blaze: async (input: unknown, ctx: TrailContext) =>
1020
- await (raw['blaze'] as Implementation<unknown, unknown>)(input, ctx),
1012
+ implementation: async (input: unknown, ctx: TrailContext) =>
1013
+ await (raw['implementation'] as Implementation<unknown, unknown>)(
1014
+ input,
1015
+ ctx
1016
+ ),
1021
1017
  ...(raw['composeInput'] === undefined
1022
1018
  ? {}
1023
1019
  : { composeInput: raw['composeInput'] }),
@@ -1153,7 +1149,7 @@ const normalizeCollections = <
1153
1149
  ): {
1154
1150
  readonly args: readonly string[] | false | undefined;
1155
1151
  readonly activationSources: readonly ActivationEntry[];
1156
- readonly contours: readonly AnyContour[];
1152
+ readonly entities: readonly AnyEntity[];
1157
1153
  readonly detours: readonly Detour<I, O, TrailsError>[];
1158
1154
  readonly fires: readonly string[];
1159
1155
  readonly layers: readonly Layer[];
@@ -1164,8 +1160,8 @@ const normalizeCollections = <
1164
1160
  return {
1165
1161
  activationSources,
1166
1162
  args: Array.isArray(spec.args) ? Object.freeze([...spec.args]) : spec.args,
1167
- contours: Object.freeze([...(spec.contours ?? [])]),
1168
1163
  detours: Object.freeze([...(spec.detours ?? [])]),
1164
+ entities: Object.freeze([...(spec.entities ?? [])]),
1169
1165
  fires: Object.freeze((spec.fires ?? []).map(normalizeSignalRef)),
1170
1166
  layers: Object.freeze([...(spec.layers ?? [])]),
1171
1167
  on: extractSignalActivationIds(activationSources),
@@ -1184,14 +1180,14 @@ const normalizeCollections = <
1184
1180
  * // ID as first argument (recommended for human authoring)
1185
1181
  * const show = trail("entity.show", {
1186
1182
  * input: z.object({ name: z.string() }),
1187
- * blaze: (input) => Result.ok(entity),
1183
+ * implementation: (input) => Result.ok(entity),
1188
1184
  * });
1189
1185
  *
1190
1186
  * // Full spec object (for programmatic generation)
1191
1187
  * const show = trail({
1192
1188
  * id: "entity.show",
1193
1189
  * input: z.object({ name: z.string() }),
1194
- * blaze: (input) => Result.ok(entity),
1190
+ * implementation: (input) => Result.ok(entity),
1195
1191
  * });
1196
1192
  * ```
1197
1193
  */
@@ -1281,21 +1277,28 @@ export function trail<
1281
1277
  if (!resolved.spec) {
1282
1278
  throw new TypeError('trail() requires a spec when an id is provided');
1283
1279
  }
1284
- if (hasOwn(resolved.spec as unknown as Record<string, unknown>, 'marker')) {
1280
+ const rawSpec = resolved.spec as unknown as Record<string, unknown>;
1281
+ if (hasOwn(rawSpec, 'contours')) {
1282
+ throw new ValidationError(
1283
+ `Trail "${resolved.id}" uses retired "contours"; use "entities" instead`
1284
+ );
1285
+ }
1286
+
1287
+ if (hasOwn(rawSpec, 'marker')) {
1285
1288
  throw new ValidationError(
1286
1289
  `Trail "${resolved.id}" must not author marker; it is projected`
1287
1290
  );
1288
1291
  }
1289
1292
 
1290
1293
  const {
1291
- blaze,
1294
+ implementation,
1292
1295
  composeInput,
1293
1296
  composes: rawComposes,
1294
1297
  intent: rawIntent,
1295
1298
  visibility: rawVisibility,
1296
1299
  // Destructure away fields handled by normalizeCollections
1297
1300
  args: _a,
1298
- contours: _c,
1301
+ entities: _c,
1299
1302
  detours: _d,
1300
1303
  fires: _f,
1301
1304
  layers: _l,
@@ -1317,11 +1320,13 @@ export function trail<
1317
1320
  return Object.freeze({
1318
1321
  ...spec,
1319
1322
  ...collections,
1320
- blaze: async (input: BlazeInput<I, CI>, ctx: TrailContext) =>
1321
- await blaze(input, ctx as ComposeContextFor<C>),
1322
1323
  composeInput,
1323
1324
  composes: Object.freeze((rawComposes ?? []).map(normalizeComposeRef)),
1324
1325
  id: resolved.id,
1326
+ implementation: async (
1327
+ input: ImplementationInput<I, CI>,
1328
+ ctx: TrailContext
1329
+ ) => await implementation(input, ctx as ComposeContextFor<C>),
1325
1330
  intent: rawIntent ?? 'write',
1326
1331
  kind: 'trail' as const,
1327
1332
  ...(rawVersion === undefined ? {} : { version: rawVersion }),
@@ -1331,14 +1336,14 @@ export function trail<
1331
1336
  }
1332
1337
 
1333
1338
  // Re-export types that callers of trail() will need
1334
- // The Omit+override avoids a TypeScript limitation where BlazeInput's conditional type
1339
+ // The Omit+override avoids a TypeScript limitation where ImplementationInput's conditional type
1335
1340
  // makes Trail<any, any, any> structurally incompatible with Trail<I, O, never>.
1336
1341
  /* oxlint-disable no-explicit-any -- existential type for heterogeneous collections */
1337
1342
  export type AnyTrail = Omit<
1338
1343
  Trail<any, any, never>,
1339
- 'blaze' | 'composeInput'
1344
+ 'implementation' | 'composeInput'
1340
1345
  > & {
1341
- readonly blaze: Implementation<any, any>;
1346
+ readonly implementation: Implementation<any, any>;
1342
1347
  readonly composeInput?: z.ZodType<any> | undefined;
1343
1348
  };
1344
1349
  /* oxlint-enable no-explicit-any */