@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.30

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 (245) hide show
  1. package/CHANGELOG.md +305 -8
  2. package/README.md +114 -33
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +241 -16
  15. package/src/detours.ts +8 -0
  16. package/src/draft.ts +342 -0
  17. package/src/error-projection.ts +51 -0
  18. package/src/errors.ts +335 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +495 -34
  22. package/src/internal/fork-ctx.ts +69 -0
  23. package/src/layer-projection.ts +193 -0
  24. package/src/layer.ts +43 -6
  25. package/src/observe.ts +361 -0
  26. package/src/path-security.ts +1 -1
  27. package/src/permits.ts +12 -0
  28. package/src/redaction/patterns.ts +6 -3
  29. package/src/resilience.ts +11 -11
  30. package/src/resource-config.ts +792 -0
  31. package/src/resource.ts +194 -0
  32. package/src/result.ts +18 -0
  33. package/src/run.ts +76 -0
  34. package/src/schedule-runtime.ts +689 -0
  35. package/src/schedule.ts +326 -0
  36. package/src/serialization.ts +92 -55
  37. package/src/signal-diagnostics.ts +633 -0
  38. package/src/signal-ref.ts +87 -0
  39. package/src/signal.ts +104 -0
  40. package/src/store/accessor-protocol.ts +56 -0
  41. package/src/store/index.ts +4 -0
  42. package/src/structured-examples.ts +248 -0
  43. package/src/surface-derivation.ts +91 -0
  44. package/src/surface-filter.ts +176 -0
  45. package/src/surface-versioning.ts +42 -0
  46. package/src/topo.ts +769 -67
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1168 -30
  49. package/src/trails/derive-trail.ts +842 -0
  50. package/src/trails/index.ts +9 -0
  51. package/src/trails/ingest.ts +139 -0
  52. package/src/trails-db.ts +206 -0
  53. package/src/transport-error-map.ts +174 -0
  54. package/src/type-utils.ts +87 -0
  55. package/src/types.ts +254 -12
  56. package/src/validate-established-topo.ts +69 -0
  57. package/src/validate-topo.ts +393 -45
  58. package/src/validation.ts +152 -4
  59. package/src/version-marker.ts +716 -0
  60. package/src/version-resolution.ts +308 -0
  61. package/src/version-runtime.ts +120 -0
  62. package/src/webhook.ts +278 -0
  63. package/src/workspace.ts +1 -1
  64. package/src/zod-wrappers.ts +72 -0
  65. package/.turbo/turbo-build.log +0 -1
  66. package/.turbo/turbo-lint.log +0 -3
  67. package/.turbo/turbo-typecheck.log +0 -1
  68. package/dist/adapters.d.ts +0 -39
  69. package/dist/adapters.d.ts.map +0 -1
  70. package/dist/adapters.js +0 -2
  71. package/dist/adapters.js.map +0 -1
  72. package/dist/blob-ref.d.ts +0 -20
  73. package/dist/blob-ref.d.ts.map +0 -1
  74. package/dist/blob-ref.js +0 -22
  75. package/dist/blob-ref.js.map +0 -1
  76. package/dist/branded.d.ts +0 -36
  77. package/dist/branded.d.ts.map +0 -1
  78. package/dist/branded.js +0 -89
  79. package/dist/branded.js.map +0 -1
  80. package/dist/collections.d.ts +0 -31
  81. package/dist/collections.d.ts.map +0 -1
  82. package/dist/collections.js +0 -60
  83. package/dist/collections.js.map +0 -1
  84. package/dist/context.d.ts +0 -10
  85. package/dist/context.d.ts.map +0 -1
  86. package/dist/context.js +0 -15
  87. package/dist/context.js.map +0 -1
  88. package/dist/derive.d.ts +0 -33
  89. package/dist/derive.d.ts.map +0 -1
  90. package/dist/derive.js +0 -125
  91. package/dist/derive.js.map +0 -1
  92. package/dist/errors.d.ts +0 -83
  93. package/dist/errors.d.ts.map +0 -1
  94. package/dist/errors.js +0 -142
  95. package/dist/errors.js.map +0 -1
  96. package/dist/event.d.ts +0 -45
  97. package/dist/event.d.ts.map +0 -1
  98. package/dist/event.js +0 -17
  99. package/dist/event.js.map +0 -1
  100. package/dist/fetch.d.ts +0 -15
  101. package/dist/fetch.d.ts.map +0 -1
  102. package/dist/fetch.js +0 -102
  103. package/dist/fetch.js.map +0 -1
  104. package/dist/guards.d.ts +0 -17
  105. package/dist/guards.d.ts.map +0 -1
  106. package/dist/guards.js +0 -25
  107. package/dist/guards.js.map +0 -1
  108. package/dist/health.d.ts +0 -18
  109. package/dist/health.d.ts.map +0 -1
  110. package/dist/health.js +0 -5
  111. package/dist/health.js.map +0 -1
  112. package/dist/hike.d.ts +0 -36
  113. package/dist/hike.d.ts.map +0 -1
  114. package/dist/hike.js +0 -20
  115. package/dist/hike.js.map +0 -1
  116. package/dist/index.d.ts +0 -36
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -40
  119. package/dist/index.js.map +0 -1
  120. package/dist/job.d.ts +0 -24
  121. package/dist/job.d.ts.map +0 -1
  122. package/dist/job.js +0 -17
  123. package/dist/job.js.map +0 -1
  124. package/dist/layer.d.ts +0 -17
  125. package/dist/layer.d.ts.map +0 -1
  126. package/dist/layer.js +0 -21
  127. package/dist/layer.js.map +0 -1
  128. package/dist/path-security.d.ts +0 -28
  129. package/dist/path-security.d.ts.map +0 -1
  130. package/dist/path-security.js +0 -63
  131. package/dist/path-security.js.map +0 -1
  132. package/dist/patterns/bulk.d.ts +0 -15
  133. package/dist/patterns/bulk.d.ts.map +0 -1
  134. package/dist/patterns/bulk.js +0 -14
  135. package/dist/patterns/bulk.js.map +0 -1
  136. package/dist/patterns/change.d.ts +0 -10
  137. package/dist/patterns/change.d.ts.map +0 -1
  138. package/dist/patterns/change.js +0 -10
  139. package/dist/patterns/change.js.map +0 -1
  140. package/dist/patterns/date-range.d.ts +0 -10
  141. package/dist/patterns/date-range.d.ts.map +0 -1
  142. package/dist/patterns/date-range.js +0 -10
  143. package/dist/patterns/date-range.js.map +0 -1
  144. package/dist/patterns/index.d.ts +0 -9
  145. package/dist/patterns/index.d.ts.map +0 -1
  146. package/dist/patterns/index.js +0 -9
  147. package/dist/patterns/index.js.map +0 -1
  148. package/dist/patterns/pagination.d.ts +0 -18
  149. package/dist/patterns/pagination.d.ts.map +0 -1
  150. package/dist/patterns/pagination.js +0 -18
  151. package/dist/patterns/pagination.js.map +0 -1
  152. package/dist/patterns/progress.d.ts +0 -11
  153. package/dist/patterns/progress.d.ts.map +0 -1
  154. package/dist/patterns/progress.js +0 -11
  155. package/dist/patterns/progress.js.map +0 -1
  156. package/dist/patterns/sorting.d.ts +0 -13
  157. package/dist/patterns/sorting.d.ts.map +0 -1
  158. package/dist/patterns/sorting.js +0 -10
  159. package/dist/patterns/sorting.js.map +0 -1
  160. package/dist/patterns/status.d.ts +0 -15
  161. package/dist/patterns/status.d.ts.map +0 -1
  162. package/dist/patterns/status.js +0 -9
  163. package/dist/patterns/status.js.map +0 -1
  164. package/dist/patterns/timestamps.d.ts +0 -10
  165. package/dist/patterns/timestamps.d.ts.map +0 -1
  166. package/dist/patterns/timestamps.js +0 -10
  167. package/dist/patterns/timestamps.js.map +0 -1
  168. package/dist/redaction/index.d.ts +0 -4
  169. package/dist/redaction/index.d.ts.map +0 -1
  170. package/dist/redaction/index.js +0 -3
  171. package/dist/redaction/index.js.map +0 -1
  172. package/dist/redaction/patterns.d.ts +0 -9
  173. package/dist/redaction/patterns.d.ts.map +0 -1
  174. package/dist/redaction/patterns.js +0 -39
  175. package/dist/redaction/patterns.js.map +0 -1
  176. package/dist/redaction/redactor.d.ts +0 -27
  177. package/dist/redaction/redactor.d.ts.map +0 -1
  178. package/dist/redaction/redactor.js +0 -89
  179. package/dist/redaction/redactor.js.map +0 -1
  180. package/dist/resilience.d.ts +0 -34
  181. package/dist/resilience.d.ts.map +0 -1
  182. package/dist/resilience.js +0 -164
  183. package/dist/resilience.js.map +0 -1
  184. package/dist/result.d.ts +0 -57
  185. package/dist/result.d.ts.map +0 -1
  186. package/dist/result.js +0 -156
  187. package/dist/result.js.map +0 -1
  188. package/dist/serialization.d.ts +0 -27
  189. package/dist/serialization.d.ts.map +0 -1
  190. package/dist/serialization.js +0 -153
  191. package/dist/serialization.js.map +0 -1
  192. package/dist/topo.d.ts +0 -18
  193. package/dist/topo.d.ts.map +0 -1
  194. package/dist/topo.js +0 -80
  195. package/dist/topo.js.map +0 -1
  196. package/dist/trail.d.ts +0 -83
  197. package/dist/trail.d.ts.map +0 -1
  198. package/dist/trail.js +0 -16
  199. package/dist/trail.js.map +0 -1
  200. package/dist/types.d.ts +0 -46
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -2
  203. package/dist/types.js.map +0 -1
  204. package/dist/validate-topo.d.ts +0 -24
  205. package/dist/validate-topo.d.ts.map +0 -1
  206. package/dist/validate-topo.js +0 -156
  207. package/dist/validate-topo.js.map +0 -1
  208. package/dist/validation.d.ts +0 -27
  209. package/dist/validation.d.ts.map +0 -1
  210. package/dist/validation.js +0 -134
  211. package/dist/validation.js.map +0 -1
  212. package/dist/workspace.d.ts +0 -25
  213. package/dist/workspace.d.ts.map +0 -1
  214. package/dist/workspace.js +0 -57
  215. package/dist/workspace.js.map +0 -1
  216. package/src/__tests__/blob-ref.test.ts +0 -103
  217. package/src/__tests__/branded.test.ts +0 -148
  218. package/src/__tests__/collections.test.ts +0 -126
  219. package/src/__tests__/context.test.ts +0 -66
  220. package/src/__tests__/derive.test.ts +0 -203
  221. package/src/__tests__/errors.test.ts +0 -309
  222. package/src/__tests__/event.test.ts +0 -82
  223. package/src/__tests__/fetch.test.ts +0 -217
  224. package/src/__tests__/guards.test.ts +0 -102
  225. package/src/__tests__/hike.test.ts +0 -117
  226. package/src/__tests__/job.test.ts +0 -98
  227. package/src/__tests__/layer.test.ts +0 -224
  228. package/src/__tests__/path-security.test.ts +0 -114
  229. package/src/__tests__/patterns.test.ts +0 -273
  230. package/src/__tests__/redaction.test.ts +0 -244
  231. package/src/__tests__/resilience.test.ts +0 -246
  232. package/src/__tests__/result.test.ts +0 -155
  233. package/src/__tests__/serialization.test.ts +0 -401
  234. package/src/__tests__/topo.test.ts +0 -220
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -280
  237. package/src/__tests__/validation.test.ts +0 -283
  238. package/src/__tests__/workspace.test.ts +0 -183
  239. package/src/adapters.ts +0 -68
  240. package/src/event.ts +0 -77
  241. package/src/health.ts +0 -23
  242. package/src/hike.ts +0 -77
  243. package/src/job.ts +0 -20
  244. package/tsconfig.json +0 -9
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,221 @@
1
+ import type { ActivationSource } from './activation-source.js';
2
+ import { zodToJsonSchema } from './validation.js';
3
+
4
+ type ZodSchemaInput = Parameters<typeof zodToJsonSchema>[0];
5
+
6
+ export type ActivationSourceProjection = Readonly<Record<string, unknown>> & {
7
+ readonly id: string;
8
+ readonly key: string;
9
+ readonly kind: string;
10
+ };
11
+
12
+ export const activationSourceKey = (
13
+ source: Pick<ActivationSource, 'id' | 'kind'>
14
+ ): string => `${source.kind}:${source.id}`;
15
+
16
+ const sortKeys = <T extends Record<string, unknown>>(value: T): T => {
17
+ const sorted: Record<string, unknown> = {};
18
+ for (const key of Object.keys(value).toSorted()) {
19
+ sorted[key] = value[key];
20
+ }
21
+ return sorted as T;
22
+ };
23
+
24
+ const deepSortKeys = (value: unknown): unknown => {
25
+ if (Array.isArray(value)) {
26
+ return value.map(deepSortKeys);
27
+ }
28
+ if (value !== null && typeof value === 'object') {
29
+ const sorted: Record<string, unknown> = {};
30
+ for (const key of Object.keys(value).toSorted()) {
31
+ sorted[key] = deepSortKeys((value as Record<string, unknown>)[key]);
32
+ }
33
+ return sorted;
34
+ }
35
+ return value;
36
+ };
37
+
38
+ const canonicalLeaf = (value: unknown): unknown => {
39
+ switch (typeof value) {
40
+ case 'bigint': {
41
+ return value.toString();
42
+ }
43
+ case 'function': {
44
+ return `[Function:${value.name || 'anonymous'}]`;
45
+ }
46
+ case 'symbol': {
47
+ return `[Symbol:${value.description ?? ''}]`;
48
+ }
49
+ case 'undefined': {
50
+ return '[Undefined]';
51
+ }
52
+ default: {
53
+ return value;
54
+ }
55
+ }
56
+ };
57
+
58
+ const canonicalObject = (
59
+ value: Record<string, unknown>,
60
+ visit: (value: unknown) => unknown
61
+ ): Record<string, unknown> => {
62
+ const sorted: Record<string, unknown> = {};
63
+ for (const key of Object.keys(value).toSorted()) {
64
+ const next = value[key];
65
+ sorted[key] = next === undefined ? '[Undefined]' : visit(next);
66
+ }
67
+ return sorted;
68
+ };
69
+
70
+ const canonicalize = (value: unknown): unknown => {
71
+ if (Array.isArray(value)) {
72
+ return value.map(canonicalize);
73
+ }
74
+ if (value instanceof Date) {
75
+ return value.toISOString();
76
+ }
77
+ if (value instanceof RegExp) {
78
+ return value.toString();
79
+ }
80
+ if (value !== null && typeof value === 'object') {
81
+ return canonicalObject(value as Record<string, unknown>, canonicalize);
82
+ }
83
+ return canonicalLeaf(value);
84
+ };
85
+
86
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
87
+ typeof value === 'object' && value !== null && !Array.isArray(value);
88
+
89
+ const isZodSchema = (value: unknown): value is ZodSchemaInput =>
90
+ isObjectRecord(value) &&
91
+ typeof value['safeParse'] === 'function' &&
92
+ isObjectRecord(value['_zod']);
93
+
94
+ const toSortedJsonSchema = (
95
+ schema: ZodSchemaInput
96
+ ): Readonly<Record<string, unknown>> =>
97
+ deepSortKeys(zodToJsonSchema(schema)) as Readonly<Record<string, unknown>>;
98
+
99
+ const parseOutputSchema = (
100
+ source: ActivationSource
101
+ ): ZodSchemaInput | undefined => {
102
+ if (isZodSchema(source.parse)) {
103
+ return source.parse;
104
+ }
105
+ if (isObjectRecord(source.parse) && isZodSchema(source.parse['output'])) {
106
+ return source.parse['output'];
107
+ }
108
+ return undefined;
109
+ };
110
+
111
+ const normalizeWebhookMethod = (method: string | undefined): string =>
112
+ (method ?? 'POST').trim().toUpperCase();
113
+
114
+ const normalizeWebhookPath = (path: string): string => path.trim();
115
+
116
+ export const projectActivationSourceDeclaration = (
117
+ source: ActivationSource
118
+ ): ActivationSourceProjection => {
119
+ const record: Record<string, unknown> = {
120
+ id: source.id,
121
+ key: activationSourceKey(source),
122
+ kind: source.kind,
123
+ };
124
+
125
+ if (source.cron !== undefined) {
126
+ record['cron'] = source.cron;
127
+ }
128
+ if (Object.hasOwn(source, 'input')) {
129
+ if (isZodSchema(source.input)) {
130
+ record['inputSchema'] = toSortedJsonSchema(source.input);
131
+ } else {
132
+ record['input'] = canonicalize(source.input);
133
+ }
134
+ }
135
+ if (source.meta !== undefined) {
136
+ record['meta'] = canonicalize(source.meta);
137
+ }
138
+ if (source.kind === 'webhook') {
139
+ record['method'] = normalizeWebhookMethod(source.method);
140
+ } else if (source.method !== undefined) {
141
+ record['method'] = source.method;
142
+ }
143
+ if (source.parse !== undefined) {
144
+ record['hasParse'] = true;
145
+ const output = parseOutputSchema(source);
146
+ if (output !== undefined) {
147
+ record['parseOutputSchema'] = toSortedJsonSchema(output);
148
+ }
149
+ }
150
+ if (source.path !== undefined) {
151
+ record['path'] =
152
+ source.kind === 'webhook'
153
+ ? normalizeWebhookPath(source.path)
154
+ : source.path;
155
+ }
156
+ if (source.payload !== undefined) {
157
+ record['hasPayloadSchema'] = true;
158
+ if (isZodSchema(source.payload)) {
159
+ record['payloadSchema'] = toSortedJsonSchema(source.payload);
160
+ }
161
+ }
162
+ if (source.timezone !== undefined) {
163
+ record['timezone'] = source.timezone;
164
+ }
165
+ if (source.verify !== undefined) {
166
+ record['hasVerify'] = true;
167
+ }
168
+
169
+ return sortKeys(record) as ActivationSourceProjection;
170
+ };
171
+
172
+ /**
173
+ * Identifies the verifier function attached to an activation source, when one
174
+ * exists. Two declarations sharing the same id/method/path/parse but different
175
+ * `verify` references must be treated as conflicting source options. The
176
+ * returned token captures the function's reference identity so it changes when
177
+ * the verifier function changes.
178
+ *
179
+ * The token is intentionally kept out of {@link projectActivationSourceDeclaration}
180
+ * so that the persisted topo-store projection remains stable and free of
181
+ * nondeterministic function identity. Use this only for in-memory comparisons
182
+ * (validation, conflict detection).
183
+ */
184
+ const verifierIds = new WeakMap<object, number>();
185
+ let verifierIdCounter = 0;
186
+
187
+ const verifierIdToken = (verify: object): string => {
188
+ const existing = verifierIds.get(verify);
189
+ if (existing !== undefined) {
190
+ return `verify#${existing}`;
191
+ }
192
+ verifierIdCounter += 1;
193
+ verifierIds.set(verify, verifierIdCounter);
194
+ return `verify#${verifierIdCounter}`;
195
+ };
196
+
197
+ const verifierIdentityToken = (
198
+ source: ActivationSource
199
+ ): string | undefined => {
200
+ if (source.kind !== 'webhook') {
201
+ return undefined;
202
+ }
203
+ const { verify } = source as { readonly verify?: unknown };
204
+ if (typeof verify !== 'function') {
205
+ return undefined;
206
+ }
207
+ // Use a per-process WeakMap-backed counter so two different function
208
+ // references produce different tokens, even if they share a name.
209
+ return verifierIdToken(verify);
210
+ };
211
+
212
+ export const activationSourceDeclarationSignature = (
213
+ source: ActivationSource
214
+ ): string => {
215
+ const projection = projectActivationSourceDeclaration(source);
216
+ const verifyToken = verifierIdentityToken(source);
217
+ if (verifyToken === undefined) {
218
+ return JSON.stringify(projection);
219
+ }
220
+ return JSON.stringify({ projection, verify: verifyToken });
221
+ };
@@ -0,0 +1,91 @@
1
+ import type { z } from 'zod';
2
+
3
+ import type { AnySignal } from './signal.js';
4
+
5
+ export const activationSourceKinds = Object.freeze([
6
+ 'signal',
7
+ 'schedule',
8
+ 'webhook',
9
+ ] as const);
10
+
11
+ export type BuiltinActivationSourceKind =
12
+ (typeof activationSourceKinds)[number];
13
+
14
+ export type ActivationSourceKind = string;
15
+
16
+ export type ActivationSourceMeta = Readonly<Record<string, unknown>>;
17
+
18
+ export type ActivationSourceParse<TPayload = unknown> =
19
+ | z.ZodType<TPayload>
20
+ | {
21
+ readonly output?: z.ZodType<TPayload> | undefined;
22
+ };
23
+
24
+ export interface ActivationSource {
25
+ readonly id: string;
26
+ readonly kind: ActivationSourceKind;
27
+ readonly cron?: string | undefined;
28
+ readonly input?: unknown;
29
+ readonly meta?: ActivationSourceMeta | undefined;
30
+ readonly method?: string | undefined;
31
+ readonly parse?: ActivationSourceParse | undefined;
32
+ readonly path?: string | undefined;
33
+ readonly payload?: z.ZodType<unknown> | undefined;
34
+ readonly timezone?: string | undefined;
35
+ readonly verify?: unknown;
36
+ }
37
+
38
+ export interface ActivationWhereExample {
39
+ readonly input?: unknown;
40
+ readonly on: boolean;
41
+ readonly payload?: unknown;
42
+ }
43
+
44
+ /* oxlint-disable no-explicit-any -- contextual predicate authoring needs source-specific payload inference; unknown would make inline predicates unusable until a helper API exists. */
45
+ export type ActivationWherePredicate<TPayload = any> = (
46
+ payload: TPayload
47
+ ) => boolean | Promise<boolean>;
48
+
49
+ export interface ActivationWhere<TPayload = any> {
50
+ readonly examples?: readonly ActivationWhereExample[] | undefined;
51
+ readonly predicate: ActivationWherePredicate<TPayload>;
52
+ }
53
+
54
+ export type ActivationWhereSpec<TPayload = any> =
55
+ | ActivationWhere<TPayload>
56
+ | ActivationWherePredicate<TPayload>;
57
+
58
+ export type ActivationSourceRef = string | AnySignal | ActivationSource;
59
+
60
+ export interface ActivationEntrySpec {
61
+ readonly source: ActivationSourceRef;
62
+ readonly meta?: ActivationSourceMeta | undefined;
63
+ readonly where?: ActivationWhereSpec | undefined;
64
+ }
65
+
66
+ export interface ActivationEntry {
67
+ readonly source: ActivationSource;
68
+ readonly meta?: ActivationSourceMeta | undefined;
69
+ readonly where?: ActivationWhereSpec | undefined;
70
+ }
71
+
72
+ export const getActivationWherePredicate = (
73
+ where: ActivationWhereSpec | undefined
74
+ ): ActivationWherePredicate | undefined =>
75
+ typeof where === 'function' ? where : where?.predicate;
76
+
77
+ export const isKnownActivationSourceKind = (
78
+ kind: string
79
+ ): kind is BuiltinActivationSourceKind =>
80
+ (activationSourceKinds as readonly string[]).includes(kind);
81
+
82
+ export const isActivationEntrySpec = (
83
+ value: unknown
84
+ ): value is ActivationEntrySpec =>
85
+ typeof value === 'object' && value !== null && 'source' in value;
86
+
87
+ export const isActivationSource = (value: unknown): value is ActivationSource =>
88
+ typeof value === 'object' &&
89
+ value !== null &&
90
+ 'id' in value &&
91
+ 'kind' in value;
package/src/blob-ref.ts CHANGED
@@ -2,6 +2,11 @@
2
2
  * BlobRef — a frozen reference to binary data for @ontrails/core.
3
3
  */
4
4
 
5
+ import { z } from 'zod';
6
+
7
+ /** Metadata key used to recognize BlobRef schemas during JSON Schema projection. */
8
+ export const BLOB_REF_SCHEMA_META_KEY = 'ontrails/blob-ref';
9
+
5
10
  /** Immutable reference to a blob of binary data. */
6
11
  export interface BlobRef {
7
12
  readonly name: string;
@@ -10,6 +15,37 @@ export interface BlobRef {
10
15
  readonly data: Uint8Array | ReadableStream<Uint8Array>;
11
16
  }
12
17
 
18
+ /** Schema-projected metadata for a BlobRef value. */
19
+ export interface BlobRefDescriptor {
20
+ readonly kind: 'blob';
21
+ readonly mimeType: string;
22
+ readonly name: string;
23
+ readonly size: number;
24
+ readonly uri: string;
25
+ }
26
+
27
+ /** Public descriptor schema emitted to transport clients instead of raw bytes. */
28
+ export const blobRefDescriptorSchema = z.object({
29
+ kind: z.literal('blob'),
30
+ mimeType: z.string(),
31
+ name: z.string(),
32
+ size: z.number(),
33
+ uri: z.string(),
34
+ });
35
+
36
+ /** Canonical JSON Schema for BlobRef descriptors across derived surfaces. */
37
+ export const blobRefJsonSchema = Object.freeze({
38
+ properties: Object.freeze({
39
+ kind: Object.freeze({ const: 'blob' }),
40
+ mimeType: Object.freeze({ type: 'string' }),
41
+ name: Object.freeze({ type: 'string' }),
42
+ size: Object.freeze({ type: 'number' }),
43
+ uri: Object.freeze({ type: 'string' }),
44
+ }),
45
+ required: Object.freeze(['kind', 'mimeType', 'name', 'size', 'uri']),
46
+ type: 'object',
47
+ });
48
+
13
49
  /** Creates a frozen BlobRef. */
14
50
  export const createBlobRef = (options: {
15
51
  name: string;
@@ -37,3 +73,18 @@ export const isBlobRef = (value: unknown): value is BlobRef => {
37
73
  (obj['data'] instanceof Uint8Array || obj['data'] instanceof ReadableStream)
38
74
  );
39
75
  };
76
+
77
+ /** Zod schema for runtime BlobRef values with metadata for descriptor projection. */
78
+ export const blobRefSchema = z
79
+ .custom<BlobRef>(isBlobRef, { error: 'Expected BlobRef' })
80
+ .meta({ [BLOB_REF_SCHEMA_META_KEY]: true });
81
+
82
+ /** Convert a runtime BlobRef into its schema-aware transport descriptor. */
83
+ export const toBlobRefDescriptor = (blob: BlobRef): BlobRefDescriptor =>
84
+ Object.freeze({
85
+ kind: 'blob',
86
+ mimeType: blob.mimeType,
87
+ name: blob.name,
88
+ size: blob.size,
89
+ uri: `blob://${blob.name}`,
90
+ });
package/src/branded.ts CHANGED
@@ -120,7 +120,7 @@ export const shortId = (length = 8): string => {
120
120
  * Produce a deterministic hex hash from an input string.
121
121
  * Uses a simple FNV-1a 32-bit hash — good enough for non-cryptographic IDs.
122
122
  */
123
- export const hashId = (input: string): string => {
123
+ export const deriveIdHash = (input: string): string => {
124
124
  // FNV offset basis
125
125
  let hash = 2_166_136_261;
126
126
  for (let i = 0; i < input.length; i += 1) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared helpers for `ctx.compose([...])` batch execution.
3
+ *
4
+ * These helpers normalize batch options, produce validation results, and
5
+ * implement the unlimited/limited worker-pool execution strategies used by
6
+ * both the real executor (`packages/core/src/execute.ts`) and the scenario
7
+ * runner in `@ontrails/testing`. Extracting them here keeps the validation
8
+ * rule, error message, and worker-pool semantics authored in one place so
9
+ * the two call sites cannot drift.
10
+ */
11
+
12
+ import { ValidationError } from './errors.js';
13
+ import { Result } from './result.js';
14
+ import type { ComposeBatchOptions } from './types.js';
15
+
16
+ /**
17
+ * Validate the `concurrency` option on a batch `ctx.compose()` call.
18
+ *
19
+ * Returns `Ok(undefined)` when no limit is requested, `Ok(n)` when a
20
+ * positive integer is supplied, and `Err(ValidationError)` for any other
21
+ * value. The error message is load-bearing: callers and tests depend on
22
+ * the exact string.
23
+ */
24
+ export const normalizeComposeBatchConcurrency = (
25
+ options: ComposeBatchOptions | undefined
26
+ ): Result<number | undefined, Error> => {
27
+ const concurrency = options?.concurrency;
28
+ if (concurrency === undefined) {
29
+ return Result.ok();
30
+ }
31
+
32
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
33
+ return Result.err(
34
+ new ValidationError(
35
+ 'ctx.compose() batch concurrency must be a positive integer'
36
+ )
37
+ );
38
+ }
39
+
40
+ return Result.ok(concurrency);
41
+ };
42
+
43
+ /**
44
+ * Produce one validation-error result per call, preserving the original
45
+ * call order. Used when `normalizeComposeBatchConcurrency` fails so the caller
46
+ * can surface a uniform batch shape to the blaze.
47
+ */
48
+ export const createComposeBatchValidationResults = <TCall>(
49
+ calls: readonly TCall[],
50
+ error: Error
51
+ ): Result<unknown, Error>[] => calls.map(() => Result.err(error));
52
+
53
+ /**
54
+ * Claim the next branch index from a shared counter. Safe to call from
55
+ * multiple worker coroutines because JavaScript is single-threaded between
56
+ * awaits — the read/increment pair runs without interleaving.
57
+ */
58
+ export const claimNextComposeBatchIndex = <TCall>(
59
+ nextIndex: { value: number },
60
+ calls: readonly TCall[]
61
+ ): number | undefined => {
62
+ if (nextIndex.value >= calls.length) {
63
+ return undefined;
64
+ }
65
+
66
+ const branchIndex = nextIndex.value;
67
+ nextIndex.value += 1;
68
+ return branchIndex;
69
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Compose-invocation schema merging for trails with `composeInput`.
3
+ *
4
+ * When a trail declares `composeInput`, callers via `ctx.compose()` pass both
5
+ * public input and composition-only fields. The merged schema validates the
6
+ * combined shape so `executeTrail` doesn't reject the extra fields.
7
+ */
8
+
9
+ import { z } from 'zod';
10
+
11
+ import type { AnyTrail } from './trail.js';
12
+
13
+ /**
14
+ * Build the validation schema for a compose-invoked trail.
15
+ *
16
+ * When the target trail declares `composeInput`, returns the intersection of
17
+ * `trail.input` and `trail.composeInput`. Returns `undefined` when no
18
+ * `composeInput` is declared, signaling that normal input validation suffices.
19
+ */
20
+ export const buildComposeValidationSchema = (
21
+ trailDef: AnyTrail
22
+ ): z.ZodType | undefined => {
23
+ if (!trailDef.composeInput) {
24
+ return undefined;
25
+ }
26
+ // Prefer .merge() for ZodObject pairs — produces a proper merged object
27
+ // schema that strips unknown keys and exposes .shape. Fall back to
28
+ // z.intersection for non-object schemas.
29
+ if (
30
+ trailDef.input instanceof z.ZodObject &&
31
+ trailDef.composeInput instanceof z.ZodObject
32
+ ) {
33
+ return trailDef.input.merge(trailDef.composeInput);
34
+ }
35
+ return z.intersection(trailDef.input, trailDef.composeInput);
36
+ };
package/src/context.ts CHANGED
@@ -1,18 +1,52 @@
1
- import type { TrailContext } from './types.js';
1
+ import { createResourceLookup } from './resource.js';
2
+ import type { TrailContext, TrailContextInit, TraceFn } from './types.js';
3
+
4
+ type MutableTrailContext = {
5
+ -readonly [K in keyof TrailContext]: TrailContext[K];
6
+ };
7
+
8
+ /**
9
+ * Default passthrough `trace` used when a context is built outside
10
+ * `executeTrail`. `executeTrail` replaces this with a real sink-writing
11
+ * implementation. The passthrough runs `fn` without recording anything so
12
+ * direct `createTrailContext()` callers (tests, ad-hoc compositions) don't
13
+ * crash when invoking `ctx.trace(...)`.
14
+ *
15
+ * Declared `async` so both synchronous throws and async rejections from `fn`
16
+ * propagate as a rejected promise to the caller — matching the real
17
+ * sink-writing implementation's error semantics.
18
+ */
19
+ export const passthroughTrace: TraceFn = async <T>(
20
+ _label: string,
21
+ fn: () => T | Promise<T>
22
+ ): Promise<T> => await fn();
2
23
 
3
24
  /**
4
25
  * Create a TrailContext with sensible defaults.
5
26
  *
6
27
  * - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID)
7
- * - `signal` defaults to a fresh, non-aborted `AbortSignal`
28
+ * - `abortSignal` defaults to a fresh, non-aborted `AbortSignal`
8
29
  * - All other fields come from `overrides`
9
30
  */
10
31
  export const createTrailContext = (
11
- overrides?: Partial<TrailContext>
12
- ): TrailContext => ({
13
- cwd: process.cwd(),
14
- env: process.env as Record<string, string | undefined>,
15
- requestId: Bun.randomUUIDv7(),
16
- signal: new AbortController().signal,
17
- ...overrides,
18
- });
32
+ overrides?: Partial<TrailContextInit>
33
+ ): TrailContext => {
34
+ const ctx = {
35
+ abortSignal: new AbortController().signal,
36
+ cwd: process.cwd(),
37
+ dryRun: false,
38
+ env: process.env as Record<string, string | undefined>,
39
+ requestId: Bun.randomUUIDv7(),
40
+ trace: passthroughTrace,
41
+ ...overrides,
42
+ } as MutableTrailContext;
43
+ const lookup = overrides?.resource ?? createResourceLookup(() => ctx);
44
+ ctx.resource = lookup;
45
+ if (ctx.trace === undefined) {
46
+ ctx.trace = passthroughTrace;
47
+ }
48
+ if (ctx.dryRun === undefined) {
49
+ ctx.dryRun = false;
50
+ }
51
+ return ctx;
52
+ };