@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16

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 (314) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +73 -24
  3. package/package.json +11 -4
  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/context.ts +28 -4
  11. package/src/contour.ts +344 -0
  12. package/src/cross-batch.ts +69 -0
  13. package/src/cross-schema.ts +36 -0
  14. package/src/detours.ts +8 -0
  15. package/src/draft.ts +60 -52
  16. package/src/error-projection.ts +51 -0
  17. package/src/errors.ts +245 -48
  18. package/src/execute.ts +1250 -40
  19. package/src/fire.ts +1169 -0
  20. package/src/index.ts +383 -49
  21. package/src/internal/fork-ctx.ts +69 -0
  22. package/src/layer-projection.ts +192 -0
  23. package/src/layer.ts +81 -0
  24. package/src/observe.ts +361 -0
  25. package/src/path-security.ts +1 -1
  26. package/src/permits.ts +12 -0
  27. package/src/redaction/patterns.ts +6 -3
  28. package/src/resilience.ts +11 -11
  29. package/src/resource-config.ts +792 -0
  30. package/src/resource.ts +175 -0
  31. package/src/result.ts +18 -0
  32. package/src/run.ts +7 -6
  33. package/src/schedule-runtime.ts +689 -0
  34. package/src/schedule.ts +324 -0
  35. package/src/serialization.ts +90 -55
  36. package/src/signal-diagnostics.ts +633 -0
  37. package/src/signal-ref.ts +87 -0
  38. package/src/signal.ts +37 -0
  39. package/src/store/accessor-protocol.ts +56 -0
  40. package/src/store/index.ts +4 -0
  41. package/src/structured-examples.ts +245 -0
  42. package/src/surface-derivation.ts +91 -0
  43. package/src/surface-filter.ts +176 -0
  44. package/src/topo.ts +710 -59
  45. package/src/tracing.ts +345 -0
  46. package/src/trail.ts +347 -33
  47. package/src/trails/derive-trail.ts +842 -0
  48. package/src/trails/index.ts +9 -0
  49. package/src/trails/ingest.ts +138 -0
  50. package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
  51. package/src/transport-error-map.ts +174 -0
  52. package/src/type-utils.ts +17 -2
  53. package/src/types.ts +208 -17
  54. package/src/validate-established-topo.ts +8 -2
  55. package/src/validate-topo.ts +244 -13
  56. package/src/validation.ts +58 -1
  57. package/src/webhook.ts +276 -0
  58. package/src/workspace.ts +1 -1
  59. package/src/zod-wrappers.ts +72 -0
  60. package/.turbo/turbo-build.log +0 -1
  61. package/.turbo/turbo-lint.log +0 -3
  62. package/.turbo/turbo-typecheck.log +0 -1
  63. package/dist/adapters.d.ts +0 -39
  64. package/dist/adapters.d.ts.map +0 -1
  65. package/dist/adapters.js +0 -2
  66. package/dist/adapters.js.map +0 -1
  67. package/dist/blob-ref.d.ts +0 -20
  68. package/dist/blob-ref.d.ts.map +0 -1
  69. package/dist/blob-ref.js +0 -22
  70. package/dist/blob-ref.js.map +0 -1
  71. package/dist/branded.d.ts +0 -36
  72. package/dist/branded.d.ts.map +0 -1
  73. package/dist/branded.js +0 -89
  74. package/dist/branded.js.map +0 -1
  75. package/dist/collections.d.ts +0 -31
  76. package/dist/collections.d.ts.map +0 -1
  77. package/dist/collections.js +0 -60
  78. package/dist/collections.js.map +0 -1
  79. package/dist/context.d.ts +0 -10
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -21
  82. package/dist/context.js.map +0 -1
  83. package/dist/derive.d.ts +0 -44
  84. package/dist/derive.d.ts.map +0 -1
  85. package/dist/derive.js +0 -149
  86. package/dist/derive.js.map +0 -1
  87. package/dist/dispatch.d.ts +0 -27
  88. package/dist/dispatch.d.ts.map +0 -1
  89. package/dist/dispatch.js +0 -34
  90. package/dist/dispatch.js.map +0 -1
  91. package/dist/draft.d.ts +0 -28
  92. package/dist/draft.d.ts.map +0 -1
  93. package/dist/draft.js +0 -156
  94. package/dist/draft.js.map +0 -1
  95. package/dist/errors.d.ts +0 -83
  96. package/dist/errors.d.ts.map +0 -1
  97. package/dist/errors.js +0 -142
  98. package/dist/errors.js.map +0 -1
  99. package/dist/event.d.ts +0 -8
  100. package/dist/event.d.ts.map +0 -1
  101. package/dist/event.js +0 -7
  102. package/dist/event.js.map +0 -1
  103. package/dist/execute.d.ts +0 -35
  104. package/dist/execute.d.ts.map +0 -1
  105. package/dist/execute.js +0 -90
  106. package/dist/execute.js.map +0 -1
  107. package/dist/fetch.d.ts +0 -15
  108. package/dist/fetch.d.ts.map +0 -1
  109. package/dist/fetch.js +0 -102
  110. package/dist/fetch.js.map +0 -1
  111. package/dist/gate.d.ts +0 -17
  112. package/dist/gate.d.ts.map +0 -1
  113. package/dist/gate.js +0 -21
  114. package/dist/gate.js.map +0 -1
  115. package/dist/guards.d.ts +0 -17
  116. package/dist/guards.d.ts.map +0 -1
  117. package/dist/guards.js +0 -25
  118. package/dist/guards.js.map +0 -1
  119. package/dist/health.d.ts +0 -18
  120. package/dist/health.d.ts.map +0 -1
  121. package/dist/health.js +0 -5
  122. package/dist/health.js.map +0 -1
  123. package/dist/index.d.ts +0 -48
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js +0 -51
  126. package/dist/index.js.map +0 -1
  127. package/dist/internal/topo-saves.d.ts +0 -47
  128. package/dist/internal/topo-saves.d.ts.map +0 -1
  129. package/dist/internal/topo-saves.js +0 -310
  130. package/dist/internal/topo-saves.js.map +0 -1
  131. package/dist/internal/topo-store-read.d.ts +0 -67
  132. package/dist/internal/topo-store-read.d.ts.map +0 -1
  133. package/dist/internal/topo-store-read.js +0 -222
  134. package/dist/internal/topo-store-read.js.map +0 -1
  135. package/dist/internal/topo-store.d.ts +0 -12
  136. package/dist/internal/topo-store.d.ts.map +0 -1
  137. package/dist/internal/topo-store.js +0 -571
  138. package/dist/internal/topo-store.js.map +0 -1
  139. package/dist/internal/trails-db.d.ts +0 -16
  140. package/dist/internal/trails-db.d.ts.map +0 -1
  141. package/dist/internal/trails-db.js +0 -118
  142. package/dist/internal/trails-db.js.map +0 -1
  143. package/dist/job.d.ts +0 -24
  144. package/dist/job.d.ts.map +0 -1
  145. package/dist/job.js +0 -17
  146. package/dist/job.js.map +0 -1
  147. package/dist/layer.d.ts +0 -6
  148. package/dist/layer.d.ts.map +0 -1
  149. package/dist/layer.js +0 -4
  150. package/dist/layer.js.map +0 -1
  151. package/dist/path-security.d.ts +0 -28
  152. package/dist/path-security.d.ts.map +0 -1
  153. package/dist/path-security.js +0 -63
  154. package/dist/path-security.js.map +0 -1
  155. package/dist/patterns/bulk.d.ts +0 -15
  156. package/dist/patterns/bulk.d.ts.map +0 -1
  157. package/dist/patterns/bulk.js +0 -14
  158. package/dist/patterns/bulk.js.map +0 -1
  159. package/dist/patterns/change.d.ts +0 -10
  160. package/dist/patterns/change.d.ts.map +0 -1
  161. package/dist/patterns/change.js +0 -10
  162. package/dist/patterns/change.js.map +0 -1
  163. package/dist/patterns/date-range.d.ts +0 -10
  164. package/dist/patterns/date-range.d.ts.map +0 -1
  165. package/dist/patterns/date-range.js +0 -10
  166. package/dist/patterns/date-range.js.map +0 -1
  167. package/dist/patterns/index.d.ts +0 -9
  168. package/dist/patterns/index.d.ts.map +0 -1
  169. package/dist/patterns/index.js +0 -9
  170. package/dist/patterns/index.js.map +0 -1
  171. package/dist/patterns/pagination.d.ts +0 -18
  172. package/dist/patterns/pagination.d.ts.map +0 -1
  173. package/dist/patterns/pagination.js +0 -18
  174. package/dist/patterns/pagination.js.map +0 -1
  175. package/dist/patterns/progress.d.ts +0 -11
  176. package/dist/patterns/progress.d.ts.map +0 -1
  177. package/dist/patterns/progress.js +0 -11
  178. package/dist/patterns/progress.js.map +0 -1
  179. package/dist/patterns/sorting.d.ts +0 -13
  180. package/dist/patterns/sorting.d.ts.map +0 -1
  181. package/dist/patterns/sorting.js +0 -10
  182. package/dist/patterns/sorting.js.map +0 -1
  183. package/dist/patterns/status.d.ts +0 -15
  184. package/dist/patterns/status.d.ts.map +0 -1
  185. package/dist/patterns/status.js +0 -9
  186. package/dist/patterns/status.js.map +0 -1
  187. package/dist/patterns/timestamps.d.ts +0 -10
  188. package/dist/patterns/timestamps.d.ts.map +0 -1
  189. package/dist/patterns/timestamps.js +0 -10
  190. package/dist/patterns/timestamps.js.map +0 -1
  191. package/dist/provision-config.d.ts +0 -22
  192. package/dist/provision-config.d.ts.map +0 -1
  193. package/dist/provision-config.js +0 -210
  194. package/dist/provision-config.js.map +0 -1
  195. package/dist/provision.d.ts +0 -71
  196. package/dist/provision.d.ts.map +0 -1
  197. package/dist/provision.js +0 -56
  198. package/dist/provision.js.map +0 -1
  199. package/dist/redaction/index.d.ts +0 -4
  200. package/dist/redaction/index.d.ts.map +0 -1
  201. package/dist/redaction/index.js +0 -3
  202. package/dist/redaction/index.js.map +0 -1
  203. package/dist/redaction/patterns.d.ts +0 -9
  204. package/dist/redaction/patterns.d.ts.map +0 -1
  205. package/dist/redaction/patterns.js +0 -39
  206. package/dist/redaction/patterns.js.map +0 -1
  207. package/dist/redaction/redactor.d.ts +0 -27
  208. package/dist/redaction/redactor.d.ts.map +0 -1
  209. package/dist/redaction/redactor.js +0 -89
  210. package/dist/redaction/redactor.js.map +0 -1
  211. package/dist/resilience.d.ts +0 -34
  212. package/dist/resilience.d.ts.map +0 -1
  213. package/dist/resilience.js +0 -164
  214. package/dist/resilience.js.map +0 -1
  215. package/dist/result.d.ts +0 -57
  216. package/dist/result.d.ts.map +0 -1
  217. package/dist/result.js +0 -156
  218. package/dist/result.js.map +0 -1
  219. package/dist/run.d.ts +0 -27
  220. package/dist/run.d.ts.map +0 -1
  221. package/dist/run.js +0 -34
  222. package/dist/run.js.map +0 -1
  223. package/dist/serialization.d.ts +0 -27
  224. package/dist/serialization.d.ts.map +0 -1
  225. package/dist/serialization.js +0 -153
  226. package/dist/serialization.js.map +0 -1
  227. package/dist/service-config.d.ts +0 -22
  228. package/dist/service-config.d.ts.map +0 -1
  229. package/dist/service-config.js +0 -210
  230. package/dist/service-config.js.map +0 -1
  231. package/dist/service.d.ts +0 -71
  232. package/dist/service.d.ts.map +0 -1
  233. package/dist/service.js +0 -56
  234. package/dist/service.js.map +0 -1
  235. package/dist/signal.d.ts +0 -33
  236. package/dist/signal.d.ts.map +0 -1
  237. package/dist/signal.js +0 -17
  238. package/dist/signal.js.map +0 -1
  239. package/dist/topo-store.d.ts +0 -48
  240. package/dist/topo-store.d.ts.map +0 -1
  241. package/dist/topo-store.js +0 -175
  242. package/dist/topo-store.js.map +0 -1
  243. package/dist/topo.d.ts +0 -25
  244. package/dist/topo.d.ts.map +0 -1
  245. package/dist/topo.js +0 -112
  246. package/dist/topo.js.map +0 -1
  247. package/dist/trail.d.ts +0 -96
  248. package/dist/trail.d.ts.map +0 -1
  249. package/dist/trail.js +0 -20
  250. package/dist/trail.js.map +0 -1
  251. package/dist/type-utils.d.ts +0 -24
  252. package/dist/type-utils.d.ts.map +0 -1
  253. package/dist/type-utils.js +0 -12
  254. package/dist/type-utils.js.map +0 -1
  255. package/dist/types.d.ts +0 -71
  256. package/dist/types.d.ts.map +0 -1
  257. package/dist/types.js +0 -3
  258. package/dist/types.js.map +0 -1
  259. package/dist/validate-established-topo.d.ts +0 -76
  260. package/dist/validate-established-topo.d.ts.map +0 -1
  261. package/dist/validate-established-topo.js +0 -43
  262. package/dist/validate-established-topo.js.map +0 -1
  263. package/dist/validate-topo.d.ts +0 -24
  264. package/dist/validate-topo.d.ts.map +0 -1
  265. package/dist/validate-topo.js +0 -176
  266. package/dist/validate-topo.js.map +0 -1
  267. package/dist/validation.d.ts +0 -27
  268. package/dist/validation.d.ts.map +0 -1
  269. package/dist/validation.js +0 -165
  270. package/dist/validation.js.map +0 -1
  271. package/dist/workspace.d.ts +0 -25
  272. package/dist/workspace.d.ts.map +0 -1
  273. package/dist/workspace.js +0 -57
  274. package/dist/workspace.js.map +0 -1
  275. package/src/__tests__/blob-ref.test.ts +0 -103
  276. package/src/__tests__/branded.test.ts +0 -148
  277. package/src/__tests__/collections.test.ts +0 -126
  278. package/src/__tests__/context.test.ts +0 -77
  279. package/src/__tests__/derive.test.ts +0 -260
  280. package/src/__tests__/errors.test.ts +0 -309
  281. package/src/__tests__/execute.test.ts +0 -525
  282. package/src/__tests__/fetch.test.ts +0 -217
  283. package/src/__tests__/gate.test.ts +0 -125
  284. package/src/__tests__/guards.test.ts +0 -102
  285. package/src/__tests__/path-security.test.ts +0 -114
  286. package/src/__tests__/patterns.test.ts +0 -273
  287. package/src/__tests__/redaction.test.ts +0 -244
  288. package/src/__tests__/resilience.test.ts +0 -246
  289. package/src/__tests__/result.test.ts +0 -155
  290. package/src/__tests__/run.test.ts +0 -181
  291. package/src/__tests__/serialization.test.ts +0 -401
  292. package/src/__tests__/service-config.test.ts +0 -228
  293. package/src/__tests__/service.test.ts +0 -197
  294. package/src/__tests__/signal.test.ts +0 -82
  295. package/src/__tests__/topo-store-read.test.ts +0 -251
  296. package/src/__tests__/topo-store.test.ts +0 -469
  297. package/src/__tests__/topo.test.ts +0 -277
  298. package/src/__tests__/trail-permit.test.ts +0 -60
  299. package/src/__tests__/trail.test.ts +0 -261
  300. package/src/__tests__/trails-db.test.ts +0 -191
  301. package/src/__tests__/type-utils.test.ts +0 -90
  302. package/src/__tests__/validate-topo.test.ts +0 -489
  303. package/src/__tests__/validation.test.ts +0 -336
  304. package/src/__tests__/workspace.test.ts +0 -183
  305. package/src/event.ts +0 -15
  306. package/src/gate.ts +0 -44
  307. package/src/internal/topo-saves.ts +0 -429
  308. package/src/internal/topo-store-read.ts +0 -473
  309. package/src/internal/topo-store.ts +0 -1087
  310. package/src/provision-config.ts +0 -373
  311. package/src/provision.ts +0 -148
  312. package/src/topo-store.ts +0 -301
  313. package/tsconfig.json +0 -9
  314. package/tsconfig.tsbuildinfo +0 -1
@@ -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) {
package/src/context.ts CHANGED
@@ -1,10 +1,26 @@
1
- import { createProvisionLookup } from './provision.js';
2
- import type { TrailContext, TrailContextInit } from './types.js';
1
+ import { createResourceLookup } from './resource.js';
2
+ import type { TrailContext, TrailContextInit, TraceFn } from './types.js';
3
3
 
4
4
  type MutableTrailContext = {
5
5
  -readonly [K in keyof TrailContext]: TrailContext[K];
6
6
  };
7
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();
23
+
8
24
  /**
9
25
  * Create a TrailContext with sensible defaults.
10
26
  *
@@ -18,11 +34,19 @@ export const createTrailContext = (
18
34
  const ctx = {
19
35
  abortSignal: new AbortController().signal,
20
36
  cwd: process.cwd(),
37
+ dryRun: false,
21
38
  env: process.env as Record<string, string | undefined>,
22
39
  requestId: Bun.randomUUIDv7(),
40
+ trace: passthroughTrace,
23
41
  ...overrides,
24
42
  } as MutableTrailContext;
25
- const lookup = overrides?.provision ?? createProvisionLookup(() => ctx);
26
- ctx.provision = lookup;
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
+ }
27
51
  return ctx;
28
52
  };
package/src/contour.ts ADDED
@@ -0,0 +1,344 @@
1
+ import { z } from 'zod';
2
+
3
+ import type { Branded } from './branded.js';
4
+
5
+ /**
6
+ * Runtime options for a contour declaration.
7
+ */
8
+ export interface ContourOptions<
9
+ TShape extends z.ZodRawShape,
10
+ TIdentity extends keyof TShape & string,
11
+ > {
12
+ /** Field name that acts as the contour's primary identity. */
13
+ readonly identity: TIdentity;
14
+ /** Example instances validated against the contour schema at declaration time. */
15
+ readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
16
+ }
17
+
18
+ /** Type-level brand name applied to a contour's identity schema. */
19
+ export type ContourIdBrand<TName extends string> = `${Capitalize<TName>}Id`;
20
+
21
+ type BrandedSchema<
22
+ TSchema extends z.core.$ZodType,
23
+ TBrand extends string,
24
+ > = TSchema & z.ZodType<Branded<z.output<TSchema>, TBrand>>;
25
+
26
+ type BrandableSchema<TSchema extends z.core.$ZodType> = TSchema & {
27
+ brand<TBrand extends string>(): BrandedSchema<TSchema, TBrand>;
28
+ };
29
+
30
+ /** Output value of a branded contour identity schema. */
31
+ export type ContourIdValue<
32
+ TSchema extends z.core.$ZodType,
33
+ TName extends string,
34
+ > = Branded<z.output<TSchema>, ContourIdBrand<TName>>;
35
+
36
+ /** Runtime metadata attached to schemas returned from `contour.id()`. */
37
+ export interface ContourIdMetadata<
38
+ TName extends string = string,
39
+ TIdentity extends string = string,
40
+ > {
41
+ readonly contour: TName;
42
+ readonly identity: TIdentity;
43
+ }
44
+
45
+ /** A structural contour reference declared by another contour field schema. */
46
+ export interface ContourReference<
47
+ TName extends string = string,
48
+ TIdentity extends string = string,
49
+ > extends ContourIdMetadata<TName, TIdentity> {
50
+ readonly field: string;
51
+ }
52
+
53
+ /** Symbol used to tag branded contour reference schemas at runtime. */
54
+ export const CONTOUR_ID_METADATA = Symbol.for('@ontrails/core/contour-id');
55
+
56
+ /**
57
+ * Module-level WeakMap storing contour identity metadata keyed by schema object.
58
+ *
59
+ * First-write-wins: when multiple contours share the same underlying schema
60
+ * (e.g. `contour('admin', { id: user.shape.id }, ...)`), the first contour to
61
+ * brand the schema claims it. Subsequent calls skip the write to prevent
62
+ * silent metadata corruption.
63
+ */
64
+ const contourIdMetadata = new WeakMap<object, ContourIdMetadata>();
65
+
66
+ /**
67
+ * A contour identity schema branded for one contour and tagged with runtime
68
+ * metadata so the topo layer can recognize declared references later on.
69
+ */
70
+ export type ContourIdSchema<
71
+ TSchema extends z.core.$ZodType = z.core.$ZodType,
72
+ TName extends string = string,
73
+ TIdentity extends string = string,
74
+ > = BrandedSchema<TSchema, ContourIdBrand<TName>> & {
75
+ /** @deprecated Use `getContourIdMetadata()` — metadata lives in a WeakMap, not on the schema. */
76
+ readonly [CONTOUR_ID_METADATA]?: ContourIdMetadata<TName, TIdentity>;
77
+ };
78
+
79
+ /**
80
+ * A first-class domain object with schema, identity metadata, and examples.
81
+ *
82
+ * A contour behaves like the `ZodObject` it wraps, so standard Zod composition
83
+ * helpers such as `.pick()`, `.extend()`, and `.array()` continue to work.
84
+ */
85
+ export type Contour<
86
+ TName extends string = string,
87
+ TShape extends z.ZodRawShape = z.ZodRawShape,
88
+ TIdentity extends keyof TShape & string = keyof TShape & string,
89
+ > = z.ZodObject<TShape> & {
90
+ readonly kind: 'contour';
91
+ readonly name: TName;
92
+ readonly identity: TIdentity;
93
+ readonly identitySchema: TShape[TIdentity];
94
+ readonly id: () => ContourIdSchema<TShape[TIdentity], TName, TIdentity>;
95
+ readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
96
+ };
97
+
98
+ const formatExampleIssues = (issues: readonly z.core.$ZodIssue[]): string =>
99
+ issues
100
+ .map((issue) => {
101
+ const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
102
+ return `${path}: ${issue.message}`;
103
+ })
104
+ .join('; ');
105
+
106
+ const assertIdentityField = <
107
+ TShape extends z.ZodRawShape,
108
+ TIdentity extends keyof TShape & string,
109
+ >(
110
+ name: string,
111
+ shape: TShape,
112
+ identity: TIdentity
113
+ ): void => {
114
+ if (!Object.hasOwn(shape, identity)) {
115
+ throw new TypeError(
116
+ `contour("${name}") identity "${identity}" must match a declared field`
117
+ );
118
+ }
119
+ };
120
+
121
+ const assertExamples = <TShape extends z.ZodRawShape>(
122
+ name: string,
123
+ schema: z.ZodObject<TShape>,
124
+ examples: readonly z.output<z.ZodObject<TShape>>[]
125
+ ): void => {
126
+ for (const [index, example] of examples.entries()) {
127
+ const parsed = schema.safeParse(example);
128
+ if (!parsed.success) {
129
+ throw new TypeError(
130
+ `contour("${name}") example ${index} is invalid: ${formatExampleIssues(parsed.error.issues)}`
131
+ );
132
+ }
133
+ }
134
+ };
135
+
136
+ const validateExamples = <TShape extends z.ZodRawShape>(
137
+ name: string,
138
+ schema: z.ZodObject<TShape>,
139
+ examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined
140
+ ): void => {
141
+ if (examples) {
142
+ assertExamples(name, schema, examples);
143
+ }
144
+ };
145
+
146
+ const brandIdentitySchema = <
147
+ TSchema extends z.core.$ZodType,
148
+ TName extends string,
149
+ TIdentity extends string,
150
+ >(
151
+ contour: TName,
152
+ identity: TIdentity,
153
+ schema: TSchema
154
+ ): ContourIdSchema<TSchema, TName, TIdentity> => {
155
+ const branded = (schema as BrandableSchema<TSchema>).brand<
156
+ ContourIdBrand<TName>
157
+ >();
158
+
159
+ // First-write-wins: if another contour already claimed this schema object
160
+ // (possible when Zod v4 brand() returns `this`), preserve the original
161
+ // metadata rather than silently overwriting it.
162
+ if (!contourIdMetadata.has(branded)) {
163
+ contourIdMetadata.set(branded, {
164
+ contour,
165
+ identity,
166
+ } satisfies ContourIdMetadata<TName, TIdentity>);
167
+ }
168
+
169
+ return branded as ContourIdSchema<TSchema, TName, TIdentity>;
170
+ };
171
+
172
+ const attachContourMetadata = <
173
+ TName extends string,
174
+ TShape extends z.ZodRawShape,
175
+ TIdentity extends keyof TShape & string,
176
+ >(
177
+ schema: z.ZodObject<TShape>,
178
+ metadata: {
179
+ readonly examples?: readonly z.output<z.ZodObject<TShape>>[] | undefined;
180
+ readonly idSchema: ContourIdSchema<TShape[TIdentity], TName, TIdentity>;
181
+ readonly identity: TIdentity;
182
+ readonly identitySchema: TShape[TIdentity];
183
+ readonly name: TName;
184
+ }
185
+ ): void => {
186
+ Object.defineProperties(schema, {
187
+ examples: {
188
+ enumerable: true,
189
+ value: metadata.examples,
190
+ writable: false,
191
+ },
192
+ id: {
193
+ enumerable: true,
194
+ value: () => metadata.idSchema,
195
+ writable: false,
196
+ },
197
+ identity: {
198
+ enumerable: true,
199
+ value: metadata.identity,
200
+ writable: false,
201
+ },
202
+ identitySchema: {
203
+ enumerable: true,
204
+ value: metadata.identitySchema,
205
+ writable: false,
206
+ },
207
+ kind: {
208
+ enumerable: true,
209
+ value: 'contour',
210
+ writable: false,
211
+ },
212
+ name: {
213
+ enumerable: true,
214
+ value: metadata.name,
215
+ writable: false,
216
+ },
217
+ });
218
+ };
219
+
220
+ /** Read contour identity metadata from the module-level WeakMap, if present. */
221
+ const readMetadata = (schema: unknown): ContourIdMetadata | undefined =>
222
+ typeof schema === 'object' && schema !== null
223
+ ? contourIdMetadata.get(schema)
224
+ : undefined;
225
+
226
+ /** Resolve the inner schema from a Zod wrapper (ZodOptional, ZodNullable, etc.). */
227
+ const unwrapInner = (schema: unknown): unknown => {
228
+ const def = (schema as { _def?: Record<string, unknown> })._def;
229
+ return (def?.['innerType'] ?? def?.['schema']) as unknown;
230
+ };
231
+
232
+ /**
233
+ * Walk through Zod wrapper layers searching for `CONTOUR_ID_METADATA`.
234
+ *
235
+ * `.nullish()` produces `ZodOptional<ZodNullable<T>>` — two wrapper levels —
236
+ * so a single-step unwrap is insufficient. This iterates until it finds the
237
+ * metadata or exhausts all wrapper layers.
238
+ */
239
+ const unwrapToMetadata = (schema: unknown): ContourIdMetadata | undefined => {
240
+ let current: unknown = schema;
241
+ while (typeof current === 'object' && current !== null) {
242
+ const inner = unwrapInner(current);
243
+ if (typeof inner !== 'object' || inner === null) {
244
+ return undefined;
245
+ }
246
+ const metadata = readMetadata(inner);
247
+ if (metadata !== undefined) {
248
+ return metadata;
249
+ }
250
+ current = inner;
251
+ }
252
+ return undefined;
253
+ };
254
+
255
+ /**
256
+ * Read contour-reference metadata from a schema returned by `contour.id()`.
257
+ *
258
+ * When the schema is wrapped by Zod combinators (`.optional()`, `.nullable()`,
259
+ * `.default()`, `.nullish()`, etc.) the `CONTOUR_ID_METADATA` symbol lives on
260
+ * the inner schema, not on the wrapper. The unwrap handles arbitrarily nested
261
+ * wrapper levels.
262
+ */
263
+ export const getContourIdMetadata = (
264
+ schema: unknown
265
+ ): ContourIdMetadata | undefined =>
266
+ readMetadata(schema) ?? unwrapToMetadata(schema);
267
+
268
+ /** Inspect a contour schema for fields that reference other contours via `.id()`. */
269
+ export const getContourReferences = (
270
+ contour: AnyContour
271
+ ): readonly ContourReference[] =>
272
+ Object.entries(contour.shape)
273
+ .flatMap(([field, schema]) => {
274
+ if (field === contour.identity) {
275
+ return [];
276
+ }
277
+ const metadata = getContourIdMetadata(schema);
278
+ if (metadata === undefined) {
279
+ return [];
280
+ }
281
+
282
+ return [{ field, ...metadata }];
283
+ })
284
+ .toSorted((left, right) =>
285
+ left.field === right.field
286
+ ? left.contour.localeCompare(right.contour)
287
+ : left.field.localeCompare(right.field)
288
+ );
289
+
290
+ /**
291
+ * Create a contour definition from a raw Zod object shape.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const user = contour(
296
+ * 'user',
297
+ * {
298
+ * id: z.string().uuid(),
299
+ * email: z.string().email(),
300
+ * name: z.string(),
301
+ * },
302
+ * { identity: 'id' }
303
+ * );
304
+ * ```
305
+ */
306
+ export const contour = <
307
+ TName extends string,
308
+ TShape extends z.ZodRawShape,
309
+ TIdentity extends keyof TShape & string,
310
+ >(
311
+ name: TName,
312
+ shape: TShape,
313
+ options: ContourOptions<TShape, TIdentity>
314
+ ): Contour<TName, TShape, TIdentity> => {
315
+ assertIdentityField(name, shape, options.identity);
316
+
317
+ const schema = z.object(shape);
318
+ validateExamples(name, schema, options.examples);
319
+
320
+ const identitySchema = shape[options.identity];
321
+ if (!identitySchema) {
322
+ throw new TypeError(
323
+ `contour("${name}") identity "${options.identity}" must resolve to a schema`
324
+ );
325
+ }
326
+
327
+ const idSchema = brandIdentitySchema(name, options.identity, identitySchema);
328
+ const examples = options.examples
329
+ ? Object.freeze([...options.examples])
330
+ : undefined;
331
+
332
+ attachContourMetadata(schema, {
333
+ examples,
334
+ idSchema,
335
+ identity: options.identity,
336
+ identitySchema,
337
+ name,
338
+ });
339
+
340
+ return schema as Contour<TName, TShape, TIdentity>;
341
+ };
342
+
343
+ /** Existential type for heterogeneous contour collections. */
344
+ export type AnyContour = Contour<string, z.ZodRawShape, string>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared helpers for `ctx.cross([...])` 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 { CrossBatchOptions } from './types.js';
15
+
16
+ /**
17
+ * Validate the `concurrency` option on a batch `ctx.cross()` 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 normalizeCrossBatchConcurrency = (
25
+ options: CrossBatchOptions | 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.cross() 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 `normalizeCrossBatchConcurrency` fails so the caller
46
+ * can surface a uniform batch shape to the trail implementation.
47
+ */
48
+ export const createCrossBatchValidationResults = <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 claimNextCrossBatchIndex = <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
+ * Cross-invocation schema merging for trails with `crossInput`.
3
+ *
4
+ * When a trail declares `crossInput`, callers via `ctx.cross()` 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 cross-invoked trail.
15
+ *
16
+ * When the target trail declares `crossInput`, returns the intersection of
17
+ * `trail.input` and `trail.crossInput`. Returns `undefined` when no
18
+ * `crossInput` is declared, signaling that normal input validation suffices.
19
+ */
20
+ export const buildCrossValidationSchema = (
21
+ trailDef: AnyTrail
22
+ ): z.ZodType | undefined => {
23
+ if (!trailDef.crossInput) {
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.crossInput instanceof z.ZodObject
32
+ ) {
33
+ return trailDef.input.merge(trailDef.crossInput);
34
+ }
35
+ return z.intersection(trailDef.input, trailDef.crossInput);
36
+ };
package/src/detours.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hard upper bound for detour recovery attempts.
3
+ *
4
+ * Execution and derived surface/topo projections both clamp declared detour
5
+ * attempts to this value so runtime behavior and inspectable contracts stay in
6
+ * lockstep.
7
+ */
8
+ export const DETOUR_MAX_ATTEMPTS_CAP = 5;