@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21
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/CHANGELOG.md +283 -5
- package/README.md +113 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +59 -18
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +309 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +477 -32
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +36 -4
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +119 -39
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -57
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1161 -29
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +208 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +442 -35
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +278 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -122
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -34
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -145
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -115
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -74
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -108
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -159
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -236
- package/src/__tests__/topo.test.ts +0 -184
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -201
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/trail.ts
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
import { ValidationError } from './errors.js';
|
|
4
|
+
import type {
|
|
5
|
+
ActivationEntry,
|
|
6
|
+
ActivationEntrySpec,
|
|
7
|
+
ActivationSource,
|
|
8
|
+
ActivationSourceRef,
|
|
9
|
+
} from './activation-source.js';
|
|
10
|
+
import {
|
|
11
|
+
isActivationEntrySpec,
|
|
12
|
+
isActivationSource,
|
|
13
|
+
} from './activation-source.js';
|
|
14
|
+
import type { AnyContour } from './contour.js';
|
|
3
15
|
import type { FieldOverride } from './derive.js';
|
|
16
|
+
import type { Layer } from './layer.js';
|
|
4
17
|
import type { Result } from './result.js';
|
|
5
|
-
import type {
|
|
18
|
+
import type { AnyResource } from './resource.js';
|
|
19
|
+
import type { AnySignal } from './signal.js';
|
|
20
|
+
import {
|
|
21
|
+
createLateBoundSignalMarker,
|
|
22
|
+
getLateBoundSignalRef,
|
|
23
|
+
} from './signal-ref.js';
|
|
24
|
+
import type { TrailsError } from './errors.js';
|
|
25
|
+
import type {
|
|
26
|
+
ComposeTrailContext,
|
|
27
|
+
Detour,
|
|
28
|
+
Implementation,
|
|
29
|
+
PermitRequirement,
|
|
30
|
+
TrailContext,
|
|
31
|
+
} from './types.js';
|
|
32
|
+
import { zodToJsonSchema } from './validation.js';
|
|
6
33
|
|
|
7
34
|
// ---------------------------------------------------------------------------
|
|
8
35
|
// Trail example
|
|
9
36
|
// ---------------------------------------------------------------------------
|
|
10
37
|
|
|
38
|
+
export interface TrailExampleSignalAssertion {
|
|
39
|
+
/** Signal contract object or stable signal ID expected during the example. */
|
|
40
|
+
readonly signal: AnySignal | string;
|
|
41
|
+
/** Exact payload assertion for the fired signal. */
|
|
42
|
+
readonly payload?: unknown | undefined;
|
|
43
|
+
/** Partial payload assertion; declared fields must match, extras ignored. */
|
|
44
|
+
readonly payloadMatch?: unknown | undefined;
|
|
45
|
+
/** Number of matching fired signals expected. Defaults to one. */
|
|
46
|
+
readonly times?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
/**
|
|
12
50
|
* A named example for documentation and testing.
|
|
13
51
|
*
|
|
@@ -22,57 +60,1035 @@ export interface TrailExample<I, O> {
|
|
|
22
60
|
readonly description?: string | undefined;
|
|
23
61
|
/** The input value — fields with schema defaults may be omitted */
|
|
24
62
|
readonly input: Partial<I>;
|
|
25
|
-
/** Expected output for success-path examples */
|
|
63
|
+
/** Expected output for success-path examples (deep equality) */
|
|
26
64
|
readonly expected?: O | undefined;
|
|
65
|
+
/** Partial output assertion — declared fields must match, others ignored */
|
|
66
|
+
readonly expectedMatch?: Partial<O> | undefined;
|
|
27
67
|
/** Error class name for error-path examples */
|
|
28
68
|
readonly error?: string | undefined;
|
|
69
|
+
/** Signal fires expected while executing this example. */
|
|
70
|
+
readonly signals?: readonly TrailExampleSignalAssertion[] | undefined;
|
|
29
71
|
}
|
|
30
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Blaze input — merges composeInput when declared
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The input type received by a trail's blaze function.
|
|
79
|
+
*
|
|
80
|
+
* When a trail declares `composeInput`, the runtime merges those fields into
|
|
81
|
+
* the input object before calling blaze. This type makes the compiler aware
|
|
82
|
+
* of the merged shape so developers can access composeInput fields without a
|
|
83
|
+
* cast. Falls back to plain `I` when `CI` is `never` (the default).
|
|
84
|
+
*/
|
|
85
|
+
export type BlazeInput<I, CI> = [CI] extends [never] ? I : I & CI;
|
|
86
|
+
|
|
87
|
+
type TrailRef = string | { readonly id: string };
|
|
88
|
+
|
|
89
|
+
type ComposeSchemaOutput<TSchema extends z.ZodType | undefined> =
|
|
90
|
+
TSchema extends z.ZodType ? z.output<TSchema> : never;
|
|
91
|
+
|
|
92
|
+
type SchemaOwnedTrailSpec<
|
|
93
|
+
TInputSchema extends z.ZodType,
|
|
94
|
+
O,
|
|
95
|
+
TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
96
|
+
C extends readonly TrailRef[] | undefined = undefined,
|
|
97
|
+
> = Omit<
|
|
98
|
+
TrailSpec<
|
|
99
|
+
z.output<TInputSchema>,
|
|
100
|
+
O,
|
|
101
|
+
ComposeSchemaOutput<TComposeInputSchema>,
|
|
102
|
+
C
|
|
103
|
+
>,
|
|
104
|
+
'blaze' | 'composeInput' | 'detours' | 'examples' | 'input' | 'versions'
|
|
105
|
+
> & {
|
|
106
|
+
/** Zod schema for validating caller input and materializing blaze input. */
|
|
107
|
+
readonly input: TInputSchema;
|
|
108
|
+
/** The pure function receives schema-materialized input after validation/defaults. */
|
|
109
|
+
readonly blaze: Implementation<
|
|
110
|
+
BlazeInput<
|
|
111
|
+
z.output<TInputSchema>,
|
|
112
|
+
ComposeSchemaOutput<TComposeInputSchema>
|
|
113
|
+
>,
|
|
114
|
+
O,
|
|
115
|
+
ComposeContextFor<C>
|
|
116
|
+
>;
|
|
117
|
+
/** Named examples use caller-side input; schema defaults may be omitted. */
|
|
118
|
+
readonly examples?:
|
|
119
|
+
| readonly TrailExample<z.input<TInputSchema>, O>[]
|
|
120
|
+
| undefined;
|
|
121
|
+
/** Recovery paths see the same materialized input as the blaze. */
|
|
122
|
+
readonly detours?:
|
|
123
|
+
| readonly Detour<z.output<TInputSchema>, O, TrailsError>[]
|
|
124
|
+
| undefined;
|
|
125
|
+
/** Composition-only schema, merged internally for ctx.compose() calls. */
|
|
126
|
+
readonly composeInput?: TComposeInputSchema | undefined;
|
|
127
|
+
/** Explicit historical trail versions. Current stays top-level. */
|
|
128
|
+
readonly versions?: TrailVersions<z.output<TInputSchema>, O> | undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type SchemaOwnedOutputTrailSpec<
|
|
132
|
+
TInputSchema extends z.ZodType,
|
|
133
|
+
TOutputSchema extends z.ZodType,
|
|
134
|
+
TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
135
|
+
C extends readonly TrailRef[] | undefined = undefined,
|
|
136
|
+
> = Omit<
|
|
137
|
+
SchemaOwnedTrailSpec<
|
|
138
|
+
TInputSchema,
|
|
139
|
+
z.output<TOutputSchema>,
|
|
140
|
+
TComposeInputSchema,
|
|
141
|
+
C
|
|
142
|
+
>,
|
|
143
|
+
'output'
|
|
144
|
+
> & {
|
|
145
|
+
readonly output: TOutputSchema;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type SchemaOwnedTrail<
|
|
149
|
+
TInputSchema extends z.ZodType,
|
|
150
|
+
O,
|
|
151
|
+
TComposeInputSchema extends z.ZodType | undefined,
|
|
152
|
+
> = Trail<
|
|
153
|
+
z.output<TInputSchema>,
|
|
154
|
+
O,
|
|
155
|
+
ComposeSchemaOutput<TComposeInputSchema>
|
|
156
|
+
> & {
|
|
157
|
+
readonly input: TInputSchema;
|
|
158
|
+
readonly composeInput?: TComposeInputSchema | undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
type LegacyOutputTrailSpec<
|
|
162
|
+
I,
|
|
163
|
+
O,
|
|
164
|
+
CI,
|
|
165
|
+
C extends readonly TrailRef[] | undefined,
|
|
166
|
+
> = Omit<TrailSpec<I, O, CI, C>, 'blaze' | 'output'> & {
|
|
167
|
+
readonly blaze: Implementation<
|
|
168
|
+
BlazeInput<I, CI>,
|
|
169
|
+
NoInfer<O>,
|
|
170
|
+
ComposeContextFor<C>
|
|
171
|
+
>;
|
|
172
|
+
readonly output: z.ZodType<O>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
type LegacyOutputlessTrailSpec<
|
|
176
|
+
I,
|
|
177
|
+
O,
|
|
178
|
+
CI,
|
|
179
|
+
C extends readonly TrailRef[] | undefined,
|
|
180
|
+
> = Omit<TrailSpec<I, O, CI, C>, 'blaze' | 'output'> & {
|
|
181
|
+
readonly blaze: Implementation<BlazeInput<I, CI>, O, ComposeContextFor<C>>;
|
|
182
|
+
readonly output?: undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Trail versioning
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
/** Contract pair represented by a version entry. */
|
|
190
|
+
export interface VersionContract<I = unknown, O = unknown> {
|
|
191
|
+
readonly input: I;
|
|
192
|
+
readonly output: O;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface TrailVersionDeprecatedStatus {
|
|
196
|
+
readonly migration?: readonly string[] | undefined;
|
|
197
|
+
readonly note?: string | undefined;
|
|
198
|
+
readonly state: 'deprecated';
|
|
199
|
+
readonly successor?: number | undefined;
|
|
200
|
+
readonly [key: string]: unknown;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface TrailVersionArchivedStatus {
|
|
204
|
+
readonly reason?: string | undefined;
|
|
205
|
+
readonly state: 'archived';
|
|
206
|
+
readonly [key: string]: unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Shared lifecycle metadata for historical version entries. */
|
|
210
|
+
export type TrailVersionStatus =
|
|
211
|
+
| TrailVersionArchivedStatus
|
|
212
|
+
| TrailVersionDeprecatedStatus;
|
|
213
|
+
|
|
214
|
+
/** Shared base for version entries. Historical entries never inherit schemas. */
|
|
215
|
+
export interface VersionEntry<
|
|
216
|
+
TContract extends VersionContract = VersionContract,
|
|
217
|
+
> {
|
|
218
|
+
readonly examples?:
|
|
219
|
+
| readonly TrailExample<TContract['input'], TContract['output']>[]
|
|
220
|
+
| undefined;
|
|
221
|
+
readonly input: z.ZodType<TContract['input']>;
|
|
222
|
+
readonly marker?: never;
|
|
223
|
+
readonly output: z.ZodType<TContract['output']>;
|
|
224
|
+
readonly status?: TrailVersionStatus | undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type TrailVersionTransposeInput<VersionInput, CurrentInput> = (value: {
|
|
228
|
+
readonly input: VersionInput;
|
|
229
|
+
}) => CurrentInput | Promise<CurrentInput>;
|
|
230
|
+
|
|
231
|
+
export type TrailVersionTransposeOutput<CurrentOutput, VersionOutput> =
|
|
232
|
+
(value: {
|
|
233
|
+
readonly output: CurrentOutput;
|
|
234
|
+
}) => VersionOutput | Promise<VersionOutput>;
|
|
235
|
+
|
|
236
|
+
export interface TrailVersionTranspose<
|
|
237
|
+
VersionInput,
|
|
238
|
+
VersionOutput,
|
|
239
|
+
CurrentInput,
|
|
240
|
+
CurrentOutput,
|
|
241
|
+
> {
|
|
242
|
+
readonly input: TrailVersionTransposeInput<VersionInput, CurrentInput>;
|
|
243
|
+
readonly output: TrailVersionTransposeOutput<CurrentOutput, VersionOutput>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface TrailVersionRevisionEntry<
|
|
247
|
+
VersionInput = unknown,
|
|
248
|
+
VersionOutput = unknown,
|
|
249
|
+
CurrentInput = unknown,
|
|
250
|
+
CurrentOutput = unknown,
|
|
251
|
+
> extends VersionEntry<VersionContract<VersionInput, VersionOutput>> {
|
|
252
|
+
readonly blaze?: never;
|
|
253
|
+
readonly composeInput?: never;
|
|
254
|
+
readonly composes?: never;
|
|
255
|
+
readonly detours?: never;
|
|
256
|
+
readonly kind?: never;
|
|
257
|
+
readonly resources?: never;
|
|
258
|
+
readonly transpose?:
|
|
259
|
+
| TrailVersionTranspose<
|
|
260
|
+
VersionInput,
|
|
261
|
+
VersionOutput,
|
|
262
|
+
CurrentInput,
|
|
263
|
+
CurrentOutput
|
|
264
|
+
>
|
|
265
|
+
| undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface TrailVersionForkEntry<
|
|
269
|
+
VersionInput = unknown,
|
|
270
|
+
VersionOutput = unknown,
|
|
271
|
+
ComposeInput = never,
|
|
272
|
+
> extends VersionEntry<VersionContract<VersionInput, VersionOutput>> {
|
|
273
|
+
readonly blaze: Implementation<
|
|
274
|
+
BlazeInput<VersionInput, ComposeInput>,
|
|
275
|
+
VersionOutput
|
|
276
|
+
>;
|
|
277
|
+
readonly composes?: readonly (string | AnyTrail)[] | undefined;
|
|
278
|
+
readonly composeInput?: z.ZodType<ComposeInput> | undefined;
|
|
279
|
+
readonly detours?:
|
|
280
|
+
| readonly Detour<VersionInput, VersionOutput, TrailsError>[]
|
|
281
|
+
| undefined;
|
|
282
|
+
readonly kind?: never;
|
|
283
|
+
readonly resources?: readonly AnyResource[] | undefined;
|
|
284
|
+
readonly transpose?: never;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type TrailVersionEntry<
|
|
288
|
+
VersionInput = unknown,
|
|
289
|
+
VersionOutput = unknown,
|
|
290
|
+
CurrentInput = unknown,
|
|
291
|
+
CurrentOutput = unknown,
|
|
292
|
+
ComposeInput = never,
|
|
293
|
+
> =
|
|
294
|
+
| TrailVersionRevisionEntry<
|
|
295
|
+
VersionInput,
|
|
296
|
+
VersionOutput,
|
|
297
|
+
CurrentInput,
|
|
298
|
+
CurrentOutput
|
|
299
|
+
>
|
|
300
|
+
| TrailVersionForkEntry<VersionInput, VersionOutput, ComposeInput>;
|
|
301
|
+
|
|
302
|
+
export type TrailVersionEntryKind = 'revision' | 'fork';
|
|
303
|
+
|
|
304
|
+
export type TrailVersions<
|
|
305
|
+
CurrentInput = unknown,
|
|
306
|
+
CurrentOutput = unknown,
|
|
307
|
+
> = Readonly<
|
|
308
|
+
Record<
|
|
309
|
+
number,
|
|
310
|
+
TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput>
|
|
311
|
+
>
|
|
312
|
+
>;
|
|
313
|
+
|
|
314
|
+
export const getTrailVersionEntryKind = (
|
|
315
|
+
entry: TrailVersionEntry
|
|
316
|
+
): TrailVersionEntryKind => {
|
|
317
|
+
const raw = entry as unknown as Record<string, unknown>;
|
|
318
|
+
return typeof raw['blaze'] === 'function' ? 'fork' : 'revision';
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export const isArchivedTrailVersionEntry = (
|
|
322
|
+
entry: Pick<TrailVersionEntry, 'status'>
|
|
323
|
+
): boolean => entry.status?.state === 'archived';
|
|
324
|
+
|
|
325
|
+
export const isDeprecatedTrailVersionEntry = (
|
|
326
|
+
entry: Pick<TrailVersionEntry, 'status'>
|
|
327
|
+
): boolean => entry.status?.state === 'deprecated';
|
|
328
|
+
|
|
329
|
+
export const isLiveTrailVersionEntry = (
|
|
330
|
+
entry: Pick<TrailVersionEntry, 'status'>
|
|
331
|
+
): boolean => entry.status === undefined || entry.status.state === 'deprecated';
|
|
332
|
+
|
|
333
|
+
export const hasDeprecatedTrailVersionGuidance = (
|
|
334
|
+
status: Pick<TrailVersionDeprecatedStatus, 'migration' | 'note' | 'successor'>
|
|
335
|
+
): boolean =>
|
|
336
|
+
status.successor !== undefined ||
|
|
337
|
+
(Array.isArray(status.migration) && status.migration.length > 0) ||
|
|
338
|
+
(typeof status.note === 'string' && status.note.trim().length > 0);
|
|
339
|
+
|
|
340
|
+
export const deriveSupportedTrailVersions = (
|
|
341
|
+
trail: Pick<AnyTrail, 'version' | 'versions'>
|
|
342
|
+
): readonly number[] => {
|
|
343
|
+
if (trail.version === undefined) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const supported = new Set<number>([trail.version]);
|
|
348
|
+
for (const [rawVersion, entry] of Object.entries(trail.versions ?? {})) {
|
|
349
|
+
if (isLiveTrailVersionEntry(entry)) {
|
|
350
|
+
supported.add(Number(rawVersion));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return Object.freeze([...supported].toSorted((a, b) => a - b));
|
|
355
|
+
};
|
|
356
|
+
|
|
31
357
|
// ---------------------------------------------------------------------------
|
|
32
358
|
// Trail spec
|
|
33
359
|
// ---------------------------------------------------------------------------
|
|
34
360
|
|
|
35
361
|
/** Everything needed to define a trail (minus the id) */
|
|
36
|
-
|
|
362
|
+
type ComposeContextFor<C extends readonly TrailRef[] | undefined> =
|
|
363
|
+
C extends undefined ? TrailContext : ComposeTrailContext;
|
|
364
|
+
|
|
365
|
+
export interface TrailSpec<
|
|
366
|
+
I,
|
|
367
|
+
O,
|
|
368
|
+
CI = never,
|
|
369
|
+
C extends readonly TrailRef[] | undefined = undefined,
|
|
370
|
+
> {
|
|
37
371
|
/** Zod schema for validating input */
|
|
38
372
|
readonly input: z.ZodType<I>;
|
|
39
373
|
/** Zod schema for validating output (optional — some trails are fire-and-forget) */
|
|
40
374
|
readonly output?: z.ZodType<O> | undefined;
|
|
41
375
|
/** The pure function that does the work (sync or async authoring) */
|
|
42
|
-
readonly
|
|
376
|
+
readonly blaze: Implementation<BlazeInput<I, CI>, O, ComposeContextFor<C>>;
|
|
43
377
|
/** Human-readable description */
|
|
44
378
|
readonly description?: string | undefined;
|
|
379
|
+
/** Declared operational shape for governance, derivation, and agent guidance. */
|
|
380
|
+
readonly pattern?: string | undefined;
|
|
45
381
|
/** Named examples for docs and testing */
|
|
46
382
|
readonly examples?: readonly TrailExample<I, O>[] | undefined;
|
|
47
|
-
/**
|
|
48
|
-
readonly
|
|
49
|
-
/** Trail is destructive (deletes or overwrites data) */
|
|
50
|
-
readonly destructive?: boolean | undefined;
|
|
383
|
+
/** What this trail does to the world: read, write (default), or destroy */
|
|
384
|
+
readonly intent?: 'read' | 'write' | 'destroy' | undefined;
|
|
51
385
|
/** Trail is idempotent (safe to retry) */
|
|
52
386
|
readonly idempotent?: boolean | undefined;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Trail explicitly supports dry-run execution semantics.
|
|
389
|
+
*
|
|
390
|
+
* This is a declaration for governance, derivation, and surface tooling. It
|
|
391
|
+
* does not change runtime behavior by itself; the active invocation signal is
|
|
392
|
+
* `TrailContext.dryRun`.
|
|
393
|
+
*/
|
|
394
|
+
readonly dryRun?: boolean | undefined;
|
|
395
|
+
/** Whether surfaces expose this trail by default. */
|
|
396
|
+
readonly visibility?: TrailVisibility | undefined;
|
|
397
|
+
/** Arbitrary meta for tooling and filtering */
|
|
398
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
399
|
+
/** Recovery paths activated when blaze fails with a matching error class. */
|
|
400
|
+
readonly detours?: readonly Detour<I, O, TrailsError>[] | undefined;
|
|
401
|
+
/**
|
|
402
|
+
* Typed layers attached at trail scope.
|
|
403
|
+
*
|
|
404
|
+
* Layers declared here wrap this trail's implementation on every execution,
|
|
405
|
+
* regardless of which surface invokes it. The execution pipeline composes
|
|
406
|
+
* trail-scope layers innermost — closer to the blaze than surface-scope
|
|
407
|
+
* or topo-scope layers — so the final order is
|
|
408
|
+
* `topo → surface → trail → blaze` (outermost-first).
|
|
409
|
+
*
|
|
410
|
+
* Layers are typed and inspectable. Omit `input` for surface-invisible
|
|
411
|
+
* wrappers that do not project any fields.
|
|
412
|
+
*/
|
|
413
|
+
readonly layers?: readonly Layer[] | undefined;
|
|
57
414
|
/** Per-field overrides for deriveFields() (labels, hints, options) */
|
|
58
415
|
readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
|
|
416
|
+
/** Contours this trail operates on. */
|
|
417
|
+
readonly contours?: readonly AnyContour[] | undefined;
|
|
418
|
+
/** IDs or trail objects of downstream trails this trail may invoke via ctx.compose() */
|
|
419
|
+
readonly composes?: C;
|
|
420
|
+
/**
|
|
421
|
+
* Composition-only input schema — merged with `input` for `ctx.compose()` calls,
|
|
422
|
+
* invisible to public surfaces (CLI, MCP, HTTP).
|
|
423
|
+
*
|
|
424
|
+
* Fields here are available in the blaze but are not derived into CLI flags,
|
|
425
|
+
* MCP tool parameters, or HTTP request bodies. Use for data that only makes
|
|
426
|
+
* sense when one trail composes another (e.g. `forkedFrom`).
|
|
427
|
+
*/
|
|
428
|
+
readonly composeInput?: z.ZodType<CI> | undefined;
|
|
429
|
+
/** Resources this trail may access via resource.from(ctx) */
|
|
430
|
+
readonly resources?: readonly AnyResource[] | undefined;
|
|
431
|
+
/**
|
|
432
|
+
* Signals this trail fires via `ctx.fire()`.
|
|
433
|
+
*
|
|
434
|
+
* Accepts either a string id or a `Signal` value. Both forms are
|
|
435
|
+
* normalized to the signal's id at trail definition time, so
|
|
436
|
+
* `trail.fires` is always `readonly string[]`.
|
|
437
|
+
*
|
|
438
|
+
* Note: `composes` also accepts trail objects (normalized to IDs),
|
|
439
|
+
* following the same pattern as signal references here.
|
|
440
|
+
*/
|
|
441
|
+
readonly fires?: readonly (string | AnySignal)[] | undefined;
|
|
442
|
+
/**
|
|
443
|
+
* Activation sources that can invoke this trail.
|
|
444
|
+
*
|
|
445
|
+
* Bare strings and `Signal` values are signal-source shorthand. Object form
|
|
446
|
+
* preserves the source kind and per-source metadata for the activation graph.
|
|
447
|
+
*/
|
|
448
|
+
readonly on?:
|
|
449
|
+
| readonly (ActivationEntrySpec | ActivationSourceRef)[]
|
|
450
|
+
| undefined;
|
|
451
|
+
/** Auth requirement: scopes object, 'public', or omitted (undeclared) */
|
|
452
|
+
readonly permit?: PermitRequirement | undefined;
|
|
453
|
+
/** Primary input fields and their order. CLI projects as positional args. */
|
|
454
|
+
readonly args?: readonly string[] | false | undefined;
|
|
455
|
+
/** Current trail version number. Omit for current-only unversioned trails. */
|
|
456
|
+
readonly version?: number | undefined;
|
|
457
|
+
/** Explicit historical trail versions. Current stays top-level. */
|
|
458
|
+
readonly versions?: TrailVersions<I, O> | undefined;
|
|
459
|
+
/** Version markers are projected into the resolved graph, not authored. */
|
|
460
|
+
readonly marker?: never;
|
|
59
461
|
}
|
|
60
462
|
|
|
61
463
|
// ---------------------------------------------------------------------------
|
|
62
464
|
// Trail (the frozen runtime object)
|
|
63
465
|
// ---------------------------------------------------------------------------
|
|
64
466
|
|
|
467
|
+
/** Intent describes what a trail does to the world. */
|
|
468
|
+
export const intentValues = Object.freeze([
|
|
469
|
+
'read',
|
|
470
|
+
'write',
|
|
471
|
+
'destroy',
|
|
472
|
+
] as const);
|
|
473
|
+
|
|
474
|
+
export type Intent = (typeof intentValues)[number];
|
|
475
|
+
|
|
476
|
+
/** Whether surfaces expose a trail by default. */
|
|
477
|
+
export type TrailVisibility = 'public' | 'internal';
|
|
478
|
+
|
|
65
479
|
/** A fully-defined trail — the unit of work in the Trails system */
|
|
66
|
-
export interface Trail<I, O> extends Omit<
|
|
480
|
+
export interface Trail<I, O, CI = never> extends Omit<
|
|
481
|
+
TrailSpec<I, O, CI, readonly TrailRef[] | undefined>,
|
|
482
|
+
| 'args'
|
|
483
|
+
| 'blaze'
|
|
484
|
+
| 'contours'
|
|
485
|
+
| 'composes'
|
|
486
|
+
| 'composeInput'
|
|
487
|
+
| 'detours'
|
|
488
|
+
| 'fires'
|
|
489
|
+
| 'intent'
|
|
490
|
+
| 'layers'
|
|
491
|
+
| 'on'
|
|
492
|
+
| 'resources'
|
|
493
|
+
> {
|
|
67
494
|
readonly kind: 'trail';
|
|
68
495
|
readonly id: string;
|
|
69
|
-
readonly
|
|
496
|
+
readonly blaze: Implementation<BlazeInput<I, CI>, O>;
|
|
497
|
+
/** Contours this trail operates on (always present, default []). */
|
|
498
|
+
readonly contours: readonly AnyContour[];
|
|
499
|
+
/** IDs of downstream trails this trail may invoke via ctx.compose() (always present, default []) */
|
|
500
|
+
readonly composes: readonly string[];
|
|
501
|
+
/** Composition-only input schema, merged with `input` for ctx.compose() calls (optional) */
|
|
502
|
+
readonly composeInput?: z.ZodType<CI> | undefined;
|
|
503
|
+
/** Recovery paths activated when blaze fails with a matching error (always present, default []). */
|
|
504
|
+
readonly detours: readonly Detour<I, O, TrailsError>[];
|
|
505
|
+
/**
|
|
506
|
+
* Typed layers attached at trail scope (always present, default []).
|
|
507
|
+
*
|
|
508
|
+
* Composed innermost in the layer chain — closest to the blaze. The final
|
|
509
|
+
* composition order is `topo → surface → trail → blaze` (outermost-first).
|
|
510
|
+
*/
|
|
511
|
+
readonly layers: readonly Layer[];
|
|
512
|
+
/** Resources this trail may access via resource.from(ctx) (always present, default []) */
|
|
513
|
+
readonly resources: readonly AnyResource[];
|
|
514
|
+
/** IDs of signals this trail fires via ctx.fire() (always present, default []) */
|
|
515
|
+
readonly fires: readonly string[];
|
|
516
|
+
/**
|
|
517
|
+
* IDs of signal sources that activate this trail (always present, default []).
|
|
518
|
+
* Non-signal activation sources live in `activationSources`.
|
|
519
|
+
*/
|
|
520
|
+
readonly on: readonly string[];
|
|
521
|
+
/** Normalized activation source entries declared through `on` (always present, default []). */
|
|
522
|
+
readonly activationSources: readonly ActivationEntry[];
|
|
523
|
+
/** What this trail does to the world (always present, default 'write') */
|
|
524
|
+
readonly intent: Intent;
|
|
525
|
+
/** Whether surfaces expose this trail by default (always present, default 'public'). */
|
|
526
|
+
readonly visibility: TrailVisibility;
|
|
527
|
+
/** Primary input fields and their order (always present, default undefined) */
|
|
528
|
+
readonly args?: readonly string[] | false | undefined;
|
|
70
529
|
}
|
|
71
530
|
|
|
72
531
|
// ---------------------------------------------------------------------------
|
|
73
532
|
// Factory
|
|
74
533
|
// ---------------------------------------------------------------------------
|
|
75
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Canonical scoped-signal id shape: `<scope>:<table>.<event>`.
|
|
537
|
+
*
|
|
538
|
+
* Matches exactly one `:` separating a non-empty scope from a non-empty
|
|
539
|
+
* dotted tail of at least two segments (e.g. `identity:users.created`).
|
|
540
|
+
* The scope forbids only `:` and whitespace so resource ids may contain
|
|
541
|
+
* dots for namespacing (e.g. `demo.store:gists.created`). Tail segments
|
|
542
|
+
* forbid both `:` and `.` so strings like `foo:bar` (no dot) or
|
|
543
|
+
* `a:b.c.d` stay unambiguous.
|
|
544
|
+
*/
|
|
545
|
+
const SCOPED_SIGNAL_ID = /^[^:\s]+:[^:.\s]+(?:\.[^:.\s]+)+$/;
|
|
546
|
+
|
|
547
|
+
const normalizeSignalRef = (entry: string | AnySignal): string => {
|
|
548
|
+
if (typeof entry === 'string') {
|
|
549
|
+
return entry;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const ref = getLateBoundSignalRef(entry);
|
|
553
|
+
if (!ref) {
|
|
554
|
+
return entry.id;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Already-scoped canonical ids (e.g. "identity:users.created") must pass
|
|
558
|
+
// through unchanged. Rewriting them to a bare marker token collapses
|
|
559
|
+
// multi-binding cases where the same store definition is bound under two
|
|
560
|
+
// separate resources: both bindings share the late-bound token, so the
|
|
561
|
+
// caller's explicit choice of scope would be lost and topo resolution
|
|
562
|
+
// would throw an ambiguity error.
|
|
563
|
+
//
|
|
564
|
+
// Use a strict predicate that matches the canonical scoped shape
|
|
565
|
+
// `<scope>:<table>.<event>` (exactly one `:` separating a non-empty scope
|
|
566
|
+
// from a non-empty dotted tail of at least two segments). A looser
|
|
567
|
+
// `includes(':')` check would let unscoped ids that happen to contain `:`
|
|
568
|
+
// slip past markerization and then fail to resolve at topo finalization.
|
|
569
|
+
if (SCOPED_SIGNAL_ID.test(entry.id)) {
|
|
570
|
+
return entry.id;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return createLateBoundSignalMarker(ref, entry.id);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const freezeActivationSource = (source: ActivationSource): ActivationSource =>
|
|
577
|
+
Object.freeze({
|
|
578
|
+
...source,
|
|
579
|
+
...(source.meta === undefined
|
|
580
|
+
? {}
|
|
581
|
+
: { meta: Object.freeze({ ...source.meta }) }),
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const shouldPreserveSignalSource = (source: ActivationSource): boolean =>
|
|
585
|
+
source.kind === 'signal' &&
|
|
586
|
+
(!('payload' in source) ||
|
|
587
|
+
'input' in source ||
|
|
588
|
+
'parse' in source ||
|
|
589
|
+
'cron' in source ||
|
|
590
|
+
'timezone' in source);
|
|
591
|
+
|
|
592
|
+
const normalizeActivationSource = (
|
|
593
|
+
source: ActivationSourceRef
|
|
594
|
+
): ActivationSource => {
|
|
595
|
+
if (typeof source === 'string') {
|
|
596
|
+
return freezeActivationSource({ id: source, kind: 'signal' });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (isActivationSource(source) && shouldPreserveSignalSource(source)) {
|
|
600
|
+
return freezeActivationSource({
|
|
601
|
+
...source,
|
|
602
|
+
id: normalizeSignalRef(source.id),
|
|
603
|
+
kind: 'signal',
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (isActivationSource(source) && source.kind !== 'signal') {
|
|
608
|
+
return freezeActivationSource(source);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return freezeActivationSource({
|
|
612
|
+
id: normalizeSignalRef(source as string | AnySignal),
|
|
613
|
+
kind: 'signal',
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const normalizeActivationEntry = (
|
|
618
|
+
entry: ActivationEntrySpec | ActivationSourceRef
|
|
619
|
+
): ActivationEntry => {
|
|
620
|
+
const source = isActivationEntrySpec(entry) ? entry.source : entry;
|
|
621
|
+
const normalized: ActivationEntry = {
|
|
622
|
+
source: normalizeActivationSource(source),
|
|
623
|
+
...(isActivationEntrySpec(entry) && entry.meta !== undefined
|
|
624
|
+
? { meta: Object.freeze({ ...entry.meta }) }
|
|
625
|
+
: {}),
|
|
626
|
+
...(isActivationEntrySpec(entry) && entry.where !== undefined
|
|
627
|
+
? { where: entry.where }
|
|
628
|
+
: {}),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
return Object.freeze(normalized);
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const normalizeActivationSources = (
|
|
635
|
+
entries: readonly (ActivationEntrySpec | ActivationSourceRef)[]
|
|
636
|
+
): readonly ActivationEntry[] =>
|
|
637
|
+
Object.freeze(entries.map((entry) => normalizeActivationEntry(entry)));
|
|
638
|
+
|
|
639
|
+
const extractSignalActivationIds = (
|
|
640
|
+
activations: readonly ActivationEntry[]
|
|
641
|
+
): readonly string[] =>
|
|
642
|
+
Object.freeze(
|
|
643
|
+
activations
|
|
644
|
+
.filter((entry) => entry.source.kind === 'signal')
|
|
645
|
+
.map((entry) => entry.source.id)
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
/** Normalize a composes entry — trail objects are reduced to their id. */
|
|
649
|
+
const normalizeComposeRef = (entry: TrailRef): string =>
|
|
650
|
+
typeof entry === 'string' ? entry : entry.id;
|
|
651
|
+
|
|
652
|
+
const assertVersionNumber = (
|
|
653
|
+
trailId: string,
|
|
654
|
+
label: string,
|
|
655
|
+
version: number
|
|
656
|
+
): void => {
|
|
657
|
+
if (!Number.isSafeInteger(version) || version <= 0) {
|
|
658
|
+
throw new ValidationError(
|
|
659
|
+
`Trail "${trailId}" ${label} must be a positive integer`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const hasOwn = (value: Record<string, unknown>, key: string): boolean =>
|
|
665
|
+
Object.hasOwn(value, key);
|
|
666
|
+
|
|
667
|
+
const ORDER_INSENSITIVE_SCHEMA_ARRAY_KEYS = new Set([
|
|
668
|
+
'allOf',
|
|
669
|
+
'anyOf',
|
|
670
|
+
'enum',
|
|
671
|
+
'oneOf',
|
|
672
|
+
'required',
|
|
673
|
+
'type',
|
|
674
|
+
]);
|
|
675
|
+
|
|
676
|
+
const canonicalizeVersionSchema = (
|
|
677
|
+
value: unknown,
|
|
678
|
+
parentKey?: string
|
|
679
|
+
): unknown => {
|
|
680
|
+
if (Array.isArray(value)) {
|
|
681
|
+
const items = value.map((item) => canonicalizeVersionSchema(item));
|
|
682
|
+
return parentKey !== undefined &&
|
|
683
|
+
ORDER_INSENSITIVE_SCHEMA_ARRAY_KEYS.has(parentKey)
|
|
684
|
+
? items.toSorted((left, right) =>
|
|
685
|
+
JSON.stringify(left).localeCompare(JSON.stringify(right))
|
|
686
|
+
)
|
|
687
|
+
: items;
|
|
688
|
+
}
|
|
689
|
+
if (value !== null && typeof value === 'object') {
|
|
690
|
+
const sorted: Record<string, unknown> = {};
|
|
691
|
+
for (const key of Object.keys(value).toSorted()) {
|
|
692
|
+
sorted[key] = canonicalizeVersionSchema(
|
|
693
|
+
(value as Record<string, unknown>)[key],
|
|
694
|
+
key
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return sorted;
|
|
698
|
+
}
|
|
699
|
+
return value;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const schemasMatch = (left: z.ZodType, right: z.ZodType): boolean =>
|
|
703
|
+
JSON.stringify(canonicalizeVersionSchema(zodToJsonSchema(left))) ===
|
|
704
|
+
JSON.stringify(canonicalizeVersionSchema(zodToJsonSchema(right)));
|
|
705
|
+
|
|
706
|
+
const assertZodSchema = (
|
|
707
|
+
trailId: string,
|
|
708
|
+
version: number,
|
|
709
|
+
entry: Record<string, unknown>,
|
|
710
|
+
field: 'input' | 'output'
|
|
711
|
+
): void => {
|
|
712
|
+
if (!hasOwn(entry, field) || entry[field] === undefined) {
|
|
713
|
+
throw new ValidationError(
|
|
714
|
+
`Trail "${trailId}" version ${version} must declare explicit ${field}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const normalizeVersionStatusMigration = (
|
|
720
|
+
trailId: string,
|
|
721
|
+
version: number,
|
|
722
|
+
migration: unknown
|
|
723
|
+
): readonly string[] | undefined => {
|
|
724
|
+
if (migration === undefined) {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
if (!Array.isArray(migration)) {
|
|
728
|
+
throw new ValidationError(
|
|
729
|
+
`Trail "${trailId}" version ${version} status.migration must be an array`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return Object.freeze(
|
|
734
|
+
migration.map((step, index) => {
|
|
735
|
+
if (typeof step !== 'string' || step.trim().length === 0) {
|
|
736
|
+
throw new ValidationError(
|
|
737
|
+
`Trail "${trailId}" version ${version} status.migration[${index}] must be a non-empty string`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
return step;
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const normalizeVersionStatus = (
|
|
746
|
+
trailId: string,
|
|
747
|
+
version: number,
|
|
748
|
+
status: unknown
|
|
749
|
+
): TrailVersionStatus | undefined => {
|
|
750
|
+
if (status === undefined) {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
if (typeof status !== 'object' || status === null || Array.isArray(status)) {
|
|
754
|
+
throw new ValidationError(
|
|
755
|
+
`Trail "${trailId}" version ${version} status must be an object`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const raw = status as Record<string, unknown>;
|
|
760
|
+
if (raw['state'] !== 'deprecated' && raw['state'] !== 'archived') {
|
|
761
|
+
throw new ValidationError(
|
|
762
|
+
`Trail "${trailId}" version ${version} status.state must be "deprecated" or "archived"`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (raw['state'] === 'deprecated') {
|
|
766
|
+
if (raw['successor'] !== undefined) {
|
|
767
|
+
assertVersionNumber(
|
|
768
|
+
trailId,
|
|
769
|
+
`version ${version} status.successor`,
|
|
770
|
+
raw['successor'] as number
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (raw['note'] !== undefined) {
|
|
774
|
+
if (typeof raw['note'] !== 'string') {
|
|
775
|
+
throw new ValidationError(
|
|
776
|
+
`Trail "${trailId}" version ${version} status.note must be a string`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
if (raw['note'].trim().length === 0) {
|
|
780
|
+
throw new ValidationError(
|
|
781
|
+
`Trail "${trailId}" version ${version} status.note must be a non-empty string`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const migration = normalizeVersionStatusMigration(
|
|
786
|
+
trailId,
|
|
787
|
+
version,
|
|
788
|
+
raw['migration']
|
|
789
|
+
);
|
|
790
|
+
const normalized = Object.freeze({
|
|
791
|
+
...raw,
|
|
792
|
+
...(migration === undefined ? {} : { migration }),
|
|
793
|
+
state: 'deprecated',
|
|
794
|
+
}) as TrailVersionDeprecatedStatus;
|
|
795
|
+
if (!hasDeprecatedTrailVersionGuidance(normalized)) {
|
|
796
|
+
throw new ValidationError(
|
|
797
|
+
`Trail "${trailId}" version ${version} deprecated status must declare successor, migration, or note guidance`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return normalized;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (
|
|
804
|
+
raw['reason'] !== undefined &&
|
|
805
|
+
(typeof raw['reason'] !== 'string' || raw['reason'].trim().length === 0)
|
|
806
|
+
) {
|
|
807
|
+
throw new ValidationError(
|
|
808
|
+
`Trail "${trailId}" version ${version} status.reason must be a non-empty string`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return Object.freeze({ ...raw, state: 'archived' }) as TrailVersionStatus;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const normalizeVersionExamples = (
|
|
816
|
+
trailId: string,
|
|
817
|
+
version: number,
|
|
818
|
+
examples: unknown
|
|
819
|
+
): readonly TrailExample<unknown, unknown>[] | undefined => {
|
|
820
|
+
if (examples === undefined) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
if (!Array.isArray(examples)) {
|
|
824
|
+
throw new ValidationError(
|
|
825
|
+
`Trail "${trailId}" version ${version} examples must be an array`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return Object.freeze([...examples]) as readonly TrailExample<
|
|
830
|
+
unknown,
|
|
831
|
+
unknown
|
|
832
|
+
>[];
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const normalizeTranspose = (
|
|
836
|
+
trailId: string,
|
|
837
|
+
version: number,
|
|
838
|
+
transpose: unknown
|
|
839
|
+
): TrailVersionTranspose<unknown, unknown, unknown, unknown> | undefined => {
|
|
840
|
+
if (transpose === undefined) {
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
if (
|
|
844
|
+
typeof transpose !== 'object' ||
|
|
845
|
+
transpose === null ||
|
|
846
|
+
Array.isArray(transpose)
|
|
847
|
+
) {
|
|
848
|
+
throw new ValidationError(
|
|
849
|
+
`Trail "${trailId}" version ${version} transpose must be an object`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const raw = transpose as Record<string, unknown>;
|
|
854
|
+
if (
|
|
855
|
+
typeof raw['input'] !== 'function' ||
|
|
856
|
+
typeof raw['output'] !== 'function'
|
|
857
|
+
) {
|
|
858
|
+
throw new ValidationError(
|
|
859
|
+
`Trail "${trailId}" version ${version} transpose must define input and output functions`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return Object.freeze({
|
|
864
|
+
input: raw['input'],
|
|
865
|
+
output: raw['output'],
|
|
866
|
+
}) as TrailVersionTranspose<unknown, unknown, unknown, unknown>;
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const assertRevisionOwnsNoRuntimeFields = (
|
|
870
|
+
trailId: string,
|
|
871
|
+
version: number,
|
|
872
|
+
entry: Record<string, unknown>
|
|
873
|
+
): void => {
|
|
874
|
+
const forbidden = ['composeInput', 'composes', 'resources', 'detours'];
|
|
875
|
+
const declared = forbidden.filter((field) => hasOwn(entry, field));
|
|
876
|
+
if (declared.length > 0) {
|
|
877
|
+
throw new ValidationError(
|
|
878
|
+
`Trail "${trailId}" version ${version} is a revision and cannot declare ${declared.join(', ')}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const normalizeVersionEntry = <CurrentInput, CurrentOutput>(
|
|
884
|
+
trailId: string,
|
|
885
|
+
version: number,
|
|
886
|
+
currentInput: z.ZodType<CurrentInput>,
|
|
887
|
+
currentOutput: z.ZodType<CurrentOutput> | undefined,
|
|
888
|
+
entry: TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput>
|
|
889
|
+
): TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput> => {
|
|
890
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
891
|
+
throw new ValidationError(
|
|
892
|
+
`Trail "${trailId}" version ${version} must be an object`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const raw = entry as unknown as Record<string, unknown>;
|
|
897
|
+
assertZodSchema(trailId, version, raw, 'input');
|
|
898
|
+
assertZodSchema(trailId, version, raw, 'output');
|
|
899
|
+
|
|
900
|
+
if (hasOwn(raw, 'kind')) {
|
|
901
|
+
throw new ValidationError(
|
|
902
|
+
`Trail "${trailId}" version ${version} must not author kind; it is projected`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (hasOwn(raw, 'marker')) {
|
|
906
|
+
throw new ValidationError(
|
|
907
|
+
`Trail "${trailId}" version ${version} must not author marker; it is projected`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const hasBlaze = typeof raw['blaze'] === 'function';
|
|
912
|
+
const hasTranspose = raw['transpose'] !== undefined;
|
|
913
|
+
if (hasBlaze && hasTranspose) {
|
|
914
|
+
throw new ValidationError(
|
|
915
|
+
`Trail "${trailId}" version ${version} cannot declare both blaze and transpose`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const base = {
|
|
920
|
+
...(raw['examples'] === undefined
|
|
921
|
+
? {}
|
|
922
|
+
: {
|
|
923
|
+
examples: normalizeVersionExamples(trailId, version, raw['examples']),
|
|
924
|
+
}),
|
|
925
|
+
input: raw['input'],
|
|
926
|
+
output: raw['output'],
|
|
927
|
+
...(raw['status'] === undefined
|
|
928
|
+
? {}
|
|
929
|
+
: { status: normalizeVersionStatus(trailId, version, raw['status']) }),
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
if (hasBlaze) {
|
|
933
|
+
return Object.freeze({
|
|
934
|
+
...base,
|
|
935
|
+
blaze: async (input: unknown, ctx: TrailContext) =>
|
|
936
|
+
await (raw['blaze'] as Implementation<unknown, unknown>)(input, ctx),
|
|
937
|
+
...(raw['composeInput'] === undefined
|
|
938
|
+
? {}
|
|
939
|
+
: { composeInput: raw['composeInput'] }),
|
|
940
|
+
composes: Object.freeze(
|
|
941
|
+
(
|
|
942
|
+
(raw['composes'] as readonly (string | AnyTrail)[] | undefined) ?? []
|
|
943
|
+
).map(normalizeComposeRef)
|
|
944
|
+
),
|
|
945
|
+
detours: Object.freeze([
|
|
946
|
+
...(((raw['detours'] as readonly Detour<
|
|
947
|
+
unknown,
|
|
948
|
+
unknown,
|
|
949
|
+
TrailsError
|
|
950
|
+
>[]) ?? []) as readonly Detour<unknown, unknown, TrailsError>[]),
|
|
951
|
+
]),
|
|
952
|
+
resources: Object.freeze([
|
|
953
|
+
...(((raw['resources'] as readonly AnyResource[]) ??
|
|
954
|
+
[]) as readonly AnyResource[]),
|
|
955
|
+
]),
|
|
956
|
+
}) as TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput>;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
assertRevisionOwnsNoRuntimeFields(trailId, version, raw);
|
|
960
|
+
const inputMatchesCurrent = schemasMatch(
|
|
961
|
+
raw['input'] as z.ZodType,
|
|
962
|
+
currentInput
|
|
963
|
+
);
|
|
964
|
+
const outputMatchesCurrent =
|
|
965
|
+
currentOutput === undefined ||
|
|
966
|
+
schemasMatch(raw['output'] as z.ZodType, currentOutput);
|
|
967
|
+
if (!hasTranspose && (!inputMatchesCurrent || !outputMatchesCurrent)) {
|
|
968
|
+
throw new ValidationError(
|
|
969
|
+
`Trail "${trailId}" version ${version} changes schema and must declare transpose`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return Object.freeze({
|
|
974
|
+
...base,
|
|
975
|
+
...(hasTranspose
|
|
976
|
+
? { transpose: normalizeTranspose(trailId, version, raw['transpose']) }
|
|
977
|
+
: {}),
|
|
978
|
+
}) as TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput>;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const normalizeTrailVersions = <CurrentInput, CurrentOutput>(
|
|
982
|
+
trailId: string,
|
|
983
|
+
currentInput: z.ZodType<CurrentInput>,
|
|
984
|
+
currentOutput: z.ZodType<CurrentOutput> | undefined,
|
|
985
|
+
currentVersion: number | undefined,
|
|
986
|
+
versions: TrailVersions<CurrentInput, CurrentOutput> | undefined
|
|
987
|
+
): TrailVersions<CurrentInput, CurrentOutput> | undefined => {
|
|
988
|
+
if (currentVersion === undefined) {
|
|
989
|
+
if (versions !== undefined) {
|
|
990
|
+
throw new ValidationError(
|
|
991
|
+
`Trail "${trailId}" declares versions without a current version`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
assertVersionNumber(trailId, 'version', currentVersion);
|
|
998
|
+
|
|
999
|
+
if (versions === undefined) {
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
1002
|
+
if (
|
|
1003
|
+
typeof versions !== 'object' ||
|
|
1004
|
+
versions === null ||
|
|
1005
|
+
Array.isArray(versions)
|
|
1006
|
+
) {
|
|
1007
|
+
throw new ValidationError(`Trail "${trailId}" versions must be an object`);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const normalized: Record<
|
|
1011
|
+
number,
|
|
1012
|
+
TrailVersionEntry<unknown, unknown, CurrentInput, CurrentOutput>
|
|
1013
|
+
> = {};
|
|
1014
|
+
for (const [rawVersion, entry] of Object.entries(versions)) {
|
|
1015
|
+
const historicalVersion = Number(rawVersion);
|
|
1016
|
+
if (`${historicalVersion}` !== rawVersion) {
|
|
1017
|
+
throw new ValidationError(
|
|
1018
|
+
`Trail "${trailId}" versions key "${rawVersion}" must be a positive integer`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
assertVersionNumber(trailId, `versions.${rawVersion}`, historicalVersion);
|
|
1022
|
+
if (historicalVersion === currentVersion) {
|
|
1023
|
+
throw new ValidationError(
|
|
1024
|
+
`Trail "${trailId}" version ${historicalVersion} is current and must stay top-level`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
if (historicalVersion > currentVersion) {
|
|
1028
|
+
throw new ValidationError(
|
|
1029
|
+
`Trail "${trailId}" version ${historicalVersion} must be less than the current version (${currentVersion})`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
normalized[historicalVersion] = normalizeVersionEntry(
|
|
1033
|
+
trailId,
|
|
1034
|
+
historicalVersion,
|
|
1035
|
+
currentInput,
|
|
1036
|
+
currentOutput,
|
|
1037
|
+
entry
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const knownVersions = new Set([
|
|
1042
|
+
currentVersion,
|
|
1043
|
+
...Object.keys(normalized).map(Number),
|
|
1044
|
+
]);
|
|
1045
|
+
for (const [rawVersion, entry] of Object.entries(normalized)) {
|
|
1046
|
+
if (
|
|
1047
|
+
entry.status?.state === 'deprecated' &&
|
|
1048
|
+
entry.status.successor !== undefined &&
|
|
1049
|
+
(!knownVersions.has(entry.status.successor) ||
|
|
1050
|
+
entry.status.successor === Number(rawVersion))
|
|
1051
|
+
) {
|
|
1052
|
+
throw new ValidationError(
|
|
1053
|
+
`Trail "${trailId}" version ${rawVersion} status.successor must reference the current version or another known historical version`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return Object.freeze(normalized);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
/** Freeze and normalize all collection fields from a trail spec. */
|
|
1062
|
+
const normalizeCollections = <
|
|
1063
|
+
I,
|
|
1064
|
+
O,
|
|
1065
|
+
CI,
|
|
1066
|
+
C extends readonly TrailRef[] | undefined,
|
|
1067
|
+
>(
|
|
1068
|
+
spec: TrailSpec<I, O, CI, C>
|
|
1069
|
+
): {
|
|
1070
|
+
readonly args: readonly string[] | false | undefined;
|
|
1071
|
+
readonly activationSources: readonly ActivationEntry[];
|
|
1072
|
+
readonly contours: readonly AnyContour[];
|
|
1073
|
+
readonly detours: readonly Detour<I, O, TrailsError>[];
|
|
1074
|
+
readonly fires: readonly string[];
|
|
1075
|
+
readonly layers: readonly Layer[];
|
|
1076
|
+
readonly on: readonly string[];
|
|
1077
|
+
readonly resources: readonly AnyResource[];
|
|
1078
|
+
} => {
|
|
1079
|
+
const activationSources = normalizeActivationSources(spec.on ?? []);
|
|
1080
|
+
return {
|
|
1081
|
+
activationSources,
|
|
1082
|
+
args: Array.isArray(spec.args) ? Object.freeze([...spec.args]) : spec.args,
|
|
1083
|
+
contours: Object.freeze([...(spec.contours ?? [])]),
|
|
1084
|
+
detours: Object.freeze([...(spec.detours ?? [])]),
|
|
1085
|
+
fires: Object.freeze((spec.fires ?? []).map(normalizeSignalRef)),
|
|
1086
|
+
layers: Object.freeze([...(spec.layers ?? [])]),
|
|
1087
|
+
on: extractSignalActivationIds(activationSources),
|
|
1088
|
+
resources: Object.freeze([...(spec.resources ?? [])]),
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
|
|
76
1092
|
/**
|
|
77
1093
|
* Create a trail definition.
|
|
78
1094
|
*
|
|
@@ -84,25 +1100,95 @@ export interface Trail<I, O> extends Omit<TrailSpec<I, O>, 'implementation'> {
|
|
|
84
1100
|
* // ID as first argument (recommended for human authoring)
|
|
85
1101
|
* const show = trail("entity.show", {
|
|
86
1102
|
* input: z.object({ name: z.string() }),
|
|
87
|
-
*
|
|
1103
|
+
* blaze: (input) => Result.ok(entity),
|
|
88
1104
|
* });
|
|
89
1105
|
*
|
|
90
1106
|
* // Full spec object (for programmatic generation)
|
|
91
1107
|
* const show = trail({
|
|
92
1108
|
* id: "entity.show",
|
|
93
1109
|
* input: z.object({ name: z.string() }),
|
|
94
|
-
*
|
|
1110
|
+
* blaze: (input) => Result.ok(entity),
|
|
95
1111
|
* });
|
|
96
1112
|
* ```
|
|
97
1113
|
*/
|
|
98
|
-
export function trail<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1114
|
+
export function trail<
|
|
1115
|
+
const TInputSchema extends z.ZodType,
|
|
1116
|
+
const TOutputSchema extends z.ZodType,
|
|
1117
|
+
const TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
1118
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1119
|
+
>(
|
|
1120
|
+
id: string,
|
|
1121
|
+
spec: SchemaOwnedOutputTrailSpec<
|
|
1122
|
+
TInputSchema,
|
|
1123
|
+
TOutputSchema,
|
|
1124
|
+
TComposeInputSchema,
|
|
1125
|
+
C
|
|
1126
|
+
>
|
|
1127
|
+
): SchemaOwnedTrail<TInputSchema, z.output<TOutputSchema>, TComposeInputSchema>;
|
|
1128
|
+
export function trail<
|
|
1129
|
+
const TInputSchema extends z.ZodType,
|
|
1130
|
+
const TOutputSchema extends z.ZodType,
|
|
1131
|
+
const TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
1132
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1133
|
+
>(
|
|
1134
|
+
spec: SchemaOwnedOutputTrailSpec<
|
|
1135
|
+
TInputSchema,
|
|
1136
|
+
TOutputSchema,
|
|
1137
|
+
TComposeInputSchema,
|
|
1138
|
+
C
|
|
1139
|
+
> & {
|
|
1140
|
+
readonly id: string;
|
|
1141
|
+
}
|
|
1142
|
+
): SchemaOwnedTrail<TInputSchema, z.output<TOutputSchema>, TComposeInputSchema>;
|
|
1143
|
+
export function trail<
|
|
1144
|
+
const TInputSchema extends z.ZodType,
|
|
1145
|
+
O,
|
|
1146
|
+
const TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
1147
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1148
|
+
>(
|
|
1149
|
+
id: string,
|
|
1150
|
+
spec: SchemaOwnedTrailSpec<TInputSchema, O, TComposeInputSchema, C>
|
|
1151
|
+
): SchemaOwnedTrail<TInputSchema, O, TComposeInputSchema>;
|
|
1152
|
+
export function trail<
|
|
1153
|
+
const TInputSchema extends z.ZodType,
|
|
1154
|
+
O,
|
|
1155
|
+
const TComposeInputSchema extends z.ZodType | undefined = undefined,
|
|
1156
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1157
|
+
>(
|
|
1158
|
+
spec: SchemaOwnedTrailSpec<TInputSchema, O, TComposeInputSchema, C> & {
|
|
1159
|
+
readonly id: string;
|
|
1160
|
+
}
|
|
1161
|
+
): SchemaOwnedTrail<TInputSchema, O, TComposeInputSchema>;
|
|
1162
|
+
export function trail<
|
|
1163
|
+
I,
|
|
1164
|
+
O,
|
|
1165
|
+
CI = never,
|
|
1166
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1167
|
+
>(
|
|
1168
|
+
id: string,
|
|
1169
|
+
spec:
|
|
1170
|
+
| LegacyOutputTrailSpec<I, O, CI, C>
|
|
1171
|
+
| LegacyOutputlessTrailSpec<I, O, CI, C>
|
|
1172
|
+
): Trail<I, O, CI>;
|
|
1173
|
+
export function trail<
|
|
1174
|
+
I,
|
|
1175
|
+
O,
|
|
1176
|
+
CI = never,
|
|
1177
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1178
|
+
>(
|
|
1179
|
+
spec:
|
|
1180
|
+
| (LegacyOutputTrailSpec<I, O, CI, C> & { readonly id: string })
|
|
1181
|
+
| (LegacyOutputlessTrailSpec<I, O, CI, C> & { readonly id: string })
|
|
1182
|
+
): Trail<I, O, CI>;
|
|
1183
|
+
export function trail<
|
|
1184
|
+
I,
|
|
1185
|
+
O,
|
|
1186
|
+
CI = never,
|
|
1187
|
+
const C extends readonly TrailRef[] | undefined = undefined,
|
|
1188
|
+
>(
|
|
1189
|
+
idOrSpec: string | (TrailSpec<I, O, CI, C> & { readonly id: string }),
|
|
1190
|
+
maybeSpec?: TrailSpec<I, O, CI, C>
|
|
1191
|
+
): Trail<I, O, CI> {
|
|
106
1192
|
const resolved =
|
|
107
1193
|
typeof idOrSpec === 'string'
|
|
108
1194
|
? { id: idOrSpec, spec: maybeSpec }
|
|
@@ -111,20 +1197,66 @@ export function trail<I, O>(
|
|
|
111
1197
|
if (!resolved.spec) {
|
|
112
1198
|
throw new TypeError('trail() requires a spec when an id is provided');
|
|
113
1199
|
}
|
|
1200
|
+
if (hasOwn(resolved.spec as unknown as Record<string, unknown>, 'marker')) {
|
|
1201
|
+
throw new ValidationError(
|
|
1202
|
+
`Trail "${resolved.id}" must not author marker; it is projected`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
114
1205
|
|
|
115
|
-
const {
|
|
1206
|
+
const {
|
|
1207
|
+
blaze,
|
|
1208
|
+
composeInput,
|
|
1209
|
+
composes: rawComposes,
|
|
1210
|
+
intent: rawIntent,
|
|
1211
|
+
visibility: rawVisibility,
|
|
1212
|
+
// Destructure away fields handled by normalizeCollections
|
|
1213
|
+
args: _a,
|
|
1214
|
+
contours: _c,
|
|
1215
|
+
detours: _d,
|
|
1216
|
+
fires: _f,
|
|
1217
|
+
layers: _l,
|
|
1218
|
+
on: _o,
|
|
1219
|
+
resources: _r,
|
|
1220
|
+
version: rawVersion,
|
|
1221
|
+
versions: rawVersions,
|
|
1222
|
+
...spec
|
|
1223
|
+
} = resolved.spec;
|
|
1224
|
+
const collections = normalizeCollections(resolved.spec);
|
|
1225
|
+
const versions = normalizeTrailVersions<I, O>(
|
|
1226
|
+
resolved.id,
|
|
1227
|
+
resolved.spec.input,
|
|
1228
|
+
resolved.spec.output,
|
|
1229
|
+
rawVersion,
|
|
1230
|
+
rawVersions
|
|
1231
|
+
);
|
|
116
1232
|
|
|
117
1233
|
return Object.freeze({
|
|
118
1234
|
...spec,
|
|
1235
|
+
...collections,
|
|
1236
|
+
blaze: async (input: BlazeInput<I, CI>, ctx: TrailContext) =>
|
|
1237
|
+
await blaze(input, ctx as ComposeContextFor<C>),
|
|
1238
|
+
composeInput,
|
|
1239
|
+
composes: Object.freeze((rawComposes ?? []).map(normalizeComposeRef)),
|
|
119
1240
|
id: resolved.id,
|
|
120
|
-
|
|
121
|
-
await implementation(input, ctx),
|
|
1241
|
+
intent: rawIntent ?? 'write',
|
|
122
1242
|
kind: 'trail' as const,
|
|
1243
|
+
...(rawVersion === undefined ? {} : { version: rawVersion }),
|
|
1244
|
+
...(versions === undefined ? {} : { versions }),
|
|
1245
|
+
visibility: rawVisibility ?? 'public',
|
|
123
1246
|
});
|
|
124
1247
|
}
|
|
125
1248
|
|
|
126
1249
|
// Re-export types that callers of trail() will need
|
|
127
|
-
//
|
|
128
|
-
|
|
1250
|
+
// The Omit+override avoids a TypeScript limitation where BlazeInput's conditional type
|
|
1251
|
+
// makes Trail<any, any, any> structurally incompatible with Trail<I, O, never>.
|
|
1252
|
+
/* oxlint-disable no-explicit-any -- existential type for heterogeneous collections */
|
|
1253
|
+
export type AnyTrail = Omit<
|
|
1254
|
+
Trail<any, any, never>,
|
|
1255
|
+
'blaze' | 'composeInput'
|
|
1256
|
+
> & {
|
|
1257
|
+
readonly blaze: Implementation<any, any>;
|
|
1258
|
+
readonly composeInput?: z.ZodType<any> | undefined;
|
|
1259
|
+
};
|
|
1260
|
+
/* oxlint-enable no-explicit-any */
|
|
129
1261
|
|
|
130
1262
|
export type { Implementation, TrailContext, Result };
|