@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15

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 (287) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +19 -14
  4. package/dist/branded.d.ts +1 -1
  5. package/dist/branded.d.ts.map +1 -1
  6. package/dist/branded.js +1 -1
  7. package/dist/branded.js.map +1 -1
  8. package/dist/context.d.ts +13 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +19 -3
  11. package/dist/context.js.map +1 -1
  12. package/dist/contour.d.ts +81 -0
  13. package/dist/contour.d.ts.map +1 -0
  14. package/dist/contour.js +176 -0
  15. package/dist/contour.js.map +1 -0
  16. package/dist/cross-schema.d.ts +18 -0
  17. package/dist/cross-schema.d.ts.map +1 -0
  18. package/dist/cross-schema.js +29 -0
  19. package/dist/cross-schema.js.map +1 -0
  20. package/dist/derive.d.ts +6 -0
  21. package/dist/derive.d.ts.map +1 -1
  22. package/dist/derive.js +29 -5
  23. package/dist/derive.js.map +1 -1
  24. package/dist/draft.d.ts +28 -0
  25. package/dist/draft.d.ts.map +1 -0
  26. package/dist/draft.js +157 -0
  27. package/dist/draft.js.map +1 -0
  28. package/dist/errors.d.ts +62 -4
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +44 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/execute.d.ts +26 -9
  33. package/dist/execute.d.ts.map +1 -1
  34. package/dist/execute.js +441 -20
  35. package/dist/execute.js.map +1 -1
  36. package/dist/fire.d.ts +43 -0
  37. package/dist/fire.d.ts.map +1 -0
  38. package/dist/fire.js +185 -0
  39. package/dist/fire.js.map +1 -0
  40. package/dist/index.d.ts +33 -17
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal/cross-batch.d.ts +44 -0
  45. package/dist/internal/cross-batch.d.ts.map +1 -0
  46. package/dist/internal/cross-batch.js +58 -0
  47. package/dist/internal/cross-batch.js.map +1 -0
  48. package/dist/internal/fork-ctx.d.ts +41 -0
  49. package/dist/internal/fork-ctx.d.ts.map +1 -0
  50. package/dist/internal/fork-ctx.js +47 -0
  51. package/dist/internal/fork-ctx.js.map +1 -0
  52. package/dist/internal/signal-ref.d.ts +15 -0
  53. package/dist/internal/signal-ref.d.ts.map +1 -0
  54. package/dist/internal/signal-ref.js +41 -0
  55. package/dist/internal/signal-ref.js.map +1 -0
  56. package/dist/internal/topo-saves.d.ts +53 -0
  57. package/dist/internal/topo-saves.d.ts.map +1 -0
  58. package/dist/internal/topo-saves.js +449 -0
  59. package/dist/internal/topo-saves.js.map +1 -0
  60. package/dist/internal/topo-snapshots.d.ts +57 -0
  61. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  62. package/dist/internal/topo-snapshots.js +372 -0
  63. package/dist/internal/topo-snapshots.js.map +1 -0
  64. package/dist/internal/topo-store-read.d.ts +70 -0
  65. package/dist/internal/topo-store-read.d.ts.map +1 -0
  66. package/dist/internal/topo-store-read.js +221 -0
  67. package/dist/internal/topo-store-read.js.map +1 -0
  68. package/dist/internal/topo-store.d.ts +26 -0
  69. package/dist/internal/topo-store.d.ts.map +1 -0
  70. package/dist/internal/topo-store.js +621 -0
  71. package/dist/internal/topo-store.js.map +1 -0
  72. package/dist/internal/tracing.d.ts +98 -0
  73. package/dist/internal/tracing.d.ts.map +1 -0
  74. package/dist/internal/tracing.js +102 -0
  75. package/dist/internal/tracing.js.map +1 -0
  76. package/dist/internal/trails-db.d.ts +16 -0
  77. package/dist/internal/trails-db.d.ts.map +1 -0
  78. package/dist/internal/trails-db.js +118 -0
  79. package/dist/internal/trails-db.js.map +1 -0
  80. package/dist/internal/zod-wrappers.d.ts +40 -0
  81. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  82. package/dist/internal/zod-wrappers.js +59 -0
  83. package/dist/internal/zod-wrappers.js.map +1 -0
  84. package/dist/layer.d.ts +22 -5
  85. package/dist/layer.d.ts.map +1 -1
  86. package/dist/layer.js +20 -3
  87. package/dist/layer.js.map +1 -1
  88. package/dist/path-security.d.ts +1 -1
  89. package/dist/path-security.d.ts.map +1 -1
  90. package/dist/path-security.js +1 -1
  91. package/dist/path-security.js.map +1 -1
  92. package/dist/resilience.d.ts +1 -1
  93. package/dist/resilience.d.ts.map +1 -1
  94. package/dist/resilience.js +3 -3
  95. package/dist/resilience.js.map +1 -1
  96. package/dist/resource-config.d.ts +22 -0
  97. package/dist/resource-config.d.ts.map +1 -0
  98. package/dist/resource-config.js +210 -0
  99. package/dist/resource-config.js.map +1 -0
  100. package/dist/resource.d.ts +74 -0
  101. package/dist/resource.d.ts.map +1 -0
  102. package/dist/resource.js +61 -0
  103. package/dist/resource.js.map +1 -0
  104. package/dist/run.d.ts +4 -3
  105. package/dist/run.d.ts.map +1 -1
  106. package/dist/run.js +5 -4
  107. package/dist/run.js.map +1 -1
  108. package/dist/serialization.d.ts.map +1 -1
  109. package/dist/serialization.js +2 -1
  110. package/dist/serialization.js.map +1 -1
  111. package/dist/store/accessor-protocol.d.ts +51 -0
  112. package/dist/store/accessor-protocol.d.ts.map +1 -0
  113. package/dist/store/accessor-protocol.js +2 -0
  114. package/dist/store/accessor-protocol.js.map +1 -0
  115. package/dist/store/index.d.ts +2 -0
  116. package/dist/store/index.d.ts.map +1 -0
  117. package/dist/store/index.js +2 -0
  118. package/dist/store/index.js.map +1 -0
  119. package/dist/surface-filter.d.ts +13 -0
  120. package/dist/surface-filter.d.ts.map +1 -0
  121. package/dist/surface-filter.js +76 -0
  122. package/dist/surface-filter.js.map +1 -0
  123. package/dist/topo-store.d.ts +62 -0
  124. package/dist/topo-store.d.ts.map +1 -0
  125. package/dist/topo-store.js +319 -0
  126. package/dist/topo-store.js.map +1 -0
  127. package/dist/topo.d.ts +23 -9
  128. package/dist/topo.d.ts.map +1 -1
  129. package/dist/topo.js +175 -46
  130. package/dist/topo.js.map +1 -1
  131. package/dist/trail.d.ts +86 -19
  132. package/dist/trail.d.ts.map +1 -1
  133. package/dist/trail.js +59 -4
  134. package/dist/trail.js.map +1 -1
  135. package/dist/trails/derive-trail.d.ts +66 -0
  136. package/dist/trails/derive-trail.d.ts.map +1 -0
  137. package/dist/trails/derive-trail.js +378 -0
  138. package/dist/trails/derive-trail.js.map +1 -0
  139. package/dist/trails/index.d.ts +5 -0
  140. package/dist/trails/index.d.ts.map +1 -0
  141. package/dist/trails/index.js +3 -0
  142. package/dist/trails/index.js.map +1 -0
  143. package/dist/trails/ingest.d.ts +24 -0
  144. package/dist/trails/ingest.d.ts.map +1 -0
  145. package/dist/trails/ingest.js +63 -0
  146. package/dist/trails/ingest.js.map +1 -0
  147. package/dist/transport-error-map.d.ts +106 -0
  148. package/dist/transport-error-map.d.ts.map +1 -0
  149. package/dist/transport-error-map.js +24 -0
  150. package/dist/transport-error-map.js.map +1 -0
  151. package/dist/type-checks.test-d.d.ts +71 -0
  152. package/dist/type-checks.test-d.d.ts.map +1 -0
  153. package/dist/type-checks.test-d.js +12 -0
  154. package/dist/type-checks.test-d.js.map +1 -0
  155. package/dist/type-utils.d.ts +11 -2
  156. package/dist/type-utils.d.ts.map +1 -1
  157. package/dist/type-utils.js.map +1 -1
  158. package/dist/types.d.ts +113 -7
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/validate-established-topo.d.ts +76 -0
  162. package/dist/validate-established-topo.d.ts.map +1 -0
  163. package/dist/validate-established-topo.js +43 -0
  164. package/dist/validate-established-topo.js.map +1 -0
  165. package/dist/validate-topo.d.ts.map +1 -1
  166. package/dist/validate-topo.js +27 -8
  167. package/dist/validate-topo.js.map +1 -1
  168. package/dist/workspace.d.ts +1 -1
  169. package/dist/workspace.d.ts.map +1 -1
  170. package/dist/workspace.js +1 -1
  171. package/dist/workspace.js.map +1 -1
  172. package/package.json +10 -1
  173. package/src/__tests__/branded.test.ts +5 -5
  174. package/src/__tests__/context.test.ts +3 -3
  175. package/src/__tests__/contour.test.ts +263 -0
  176. package/src/__tests__/derive-trail.test.ts +668 -0
  177. package/src/__tests__/derive.test.ts +58 -1
  178. package/src/__tests__/errors.test.ts +118 -0
  179. package/src/__tests__/execute.test.ts +1179 -141
  180. package/src/__tests__/fire.test.ts +1056 -0
  181. package/src/__tests__/fork-ctx.test.ts +83 -0
  182. package/src/__tests__/ingest.test.ts +191 -0
  183. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  184. package/src/__tests__/path-security.test.ts +7 -7
  185. package/src/__tests__/resilience.test.ts +8 -8
  186. package/src/__tests__/resource.test.ts +204 -0
  187. package/src/__tests__/run.test.ts +13 -13
  188. package/src/__tests__/service-config.test.ts +28 -32
  189. package/src/__tests__/surface-filter.test.ts +196 -0
  190. package/src/__tests__/topo-store-read.test.ts +256 -0
  191. package/src/__tests__/topo-store.test.ts +869 -0
  192. package/src/__tests__/topo.test.ts +350 -54
  193. package/src/__tests__/trail.test.ts +189 -24
  194. package/src/__tests__/trails-db.test.ts +198 -0
  195. package/src/__tests__/transport-error-map.test.ts +82 -0
  196. package/src/__tests__/type-utils.test.ts +29 -1
  197. package/src/__tests__/validate-topo.test.ts +305 -16
  198. package/src/__tests__/workspace.test.ts +6 -6
  199. package/src/__tests__/zod-wrappers.test.ts +62 -0
  200. package/src/branded.ts +1 -1
  201. package/src/context.ts +24 -4
  202. package/src/contour.ts +344 -0
  203. package/src/cross-schema.ts +36 -0
  204. package/src/derive.ts +39 -8
  205. package/src/draft.ts +338 -0
  206. package/src/errors.ts +62 -18
  207. package/src/execute.ts +840 -29
  208. package/src/fire.ts +295 -0
  209. package/src/index.ts +142 -26
  210. package/src/internal/cross-batch.ts +77 -0
  211. package/src/internal/fork-ctx.ts +69 -0
  212. package/src/internal/signal-ref.ts +87 -0
  213. package/src/internal/topo-snapshots.ts +517 -0
  214. package/src/internal/topo-store-read.ts +481 -0
  215. package/src/internal/topo-store.ts +1192 -0
  216. package/src/internal/tracing.ts +187 -0
  217. package/src/internal/trails-db.ts +187 -0
  218. package/src/internal/zod-wrappers.ts +78 -0
  219. package/src/layer.ts +50 -0
  220. package/src/path-security.ts +1 -1
  221. package/src/resilience.ts +3 -3
  222. package/src/{provision-config.ts → resource-config.ts} +109 -123
  223. package/src/resource.ts +155 -0
  224. package/src/run.ts +5 -4
  225. package/src/serialization.ts +2 -0
  226. package/src/store/accessor-protocol.ts +56 -0
  227. package/src/store/index.ts +4 -0
  228. package/src/surface-filter.ts +182 -0
  229. package/src/topo-store.ts +510 -0
  230. package/src/topo.ts +328 -57
  231. package/src/trail.ts +199 -31
  232. package/src/trails/derive-trail.ts +842 -0
  233. package/src/trails/index.ts +9 -0
  234. package/src/trails/ingest.ts +146 -0
  235. package/src/transport-error-map.ts +56 -0
  236. package/src/type-checks.test-d.ts +106 -0
  237. package/src/type-utils.ts +17 -2
  238. package/src/types.ts +148 -11
  239. package/src/validate-established-topo.ts +63 -0
  240. package/src/validate-topo.ts +37 -9
  241. package/src/workspace.ts +1 -1
  242. package/tsconfig.tests.json +10 -0
  243. package/tsconfig.tsbuildinfo +1 -1
  244. package/dist/adapters.d.ts +0 -39
  245. package/dist/adapters.d.ts.map +0 -1
  246. package/dist/adapters.js +0 -2
  247. package/dist/adapters.js.map +0 -1
  248. package/dist/dispatch.d.ts +0 -27
  249. package/dist/dispatch.d.ts.map +0 -1
  250. package/dist/dispatch.js +0 -34
  251. package/dist/dispatch.js.map +0 -1
  252. package/dist/event.d.ts +0 -8
  253. package/dist/event.d.ts.map +0 -1
  254. package/dist/event.js +0 -7
  255. package/dist/event.js.map +0 -1
  256. package/dist/gate.d.ts +0 -17
  257. package/dist/gate.d.ts.map +0 -1
  258. package/dist/gate.js +0 -21
  259. package/dist/gate.js.map +0 -1
  260. package/dist/health.d.ts +0 -18
  261. package/dist/health.d.ts.map +0 -1
  262. package/dist/health.js +0 -5
  263. package/dist/health.js.map +0 -1
  264. package/dist/job.d.ts +0 -24
  265. package/dist/job.d.ts.map +0 -1
  266. package/dist/job.js +0 -17
  267. package/dist/job.js.map +0 -1
  268. package/dist/provision-config.d.ts +0 -22
  269. package/dist/provision-config.d.ts.map +0 -1
  270. package/dist/provision-config.js +0 -210
  271. package/dist/provision-config.js.map +0 -1
  272. package/dist/provision.d.ts +0 -71
  273. package/dist/provision.d.ts.map +0 -1
  274. package/dist/provision.js +0 -56
  275. package/dist/provision.js.map +0 -1
  276. package/dist/service-config.d.ts +0 -22
  277. package/dist/service-config.d.ts.map +0 -1
  278. package/dist/service-config.js +0 -210
  279. package/dist/service-config.js.map +0 -1
  280. package/dist/service.d.ts +0 -71
  281. package/dist/service.d.ts.map +0 -1
  282. package/dist/service.js +0 -56
  283. package/dist/service.js.map +0 -1
  284. package/src/__tests__/service.test.ts +0 -197
  285. package/src/event.ts +0 -15
  286. package/src/gate.ts +0 -44
  287. package/src/provision.ts +0 -148
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { forkCtx } from '../internal/fork-ctx';
4
+ import type { CrossFn, FireFn, TrailContext } from '../types';
5
+
6
+ // Sentinel values — the helper never invokes these, so their shapes are
7
+ // irrelevant. What matters is identity: a forked ctx must either clear
8
+ // them or preserve the exact reference.
9
+ const noopCross = {} as unknown as CrossFn;
10
+ const noopFire = {} as unknown as FireFn;
11
+ const noopResource = {} as unknown as TrailContext['resource'];
12
+
13
+ describe('forkCtx', () => {
14
+ test('clears cross, fire, and resource by default', () => {
15
+ const parent = {
16
+ cross: noopCross,
17
+ env: { NODE_ENV: 'test' },
18
+ extensions: { trailhead: 'cli' },
19
+ fire: noopFire,
20
+ logger: undefined,
21
+ resource: noopResource,
22
+ };
23
+
24
+ const forked = forkCtx(parent, {});
25
+
26
+ expect(forked.cross).toBeUndefined();
27
+ expect(forked.fire).toBeUndefined();
28
+ expect(forked.resource).toBeUndefined();
29
+ // Non-reset keys are preserved.
30
+ expect(forked.env).toEqual({ NODE_ENV: 'test' });
31
+ expect(forked.extensions).toEqual({ trailhead: 'cli' });
32
+ });
33
+
34
+ test('applies overrides after reset so overrides win', () => {
35
+ const parent = {
36
+ cross: noopCross,
37
+ env: { NODE_ENV: 'test' },
38
+ extensions: { a: 1 },
39
+ fire: noopFire,
40
+ resource: noopResource,
41
+ };
42
+
43
+ const forked = forkCtx(parent, {
44
+ env: { NODE_ENV: 'prod' },
45
+ extensions: { b: 2 },
46
+ });
47
+
48
+ expect(forked.env).toEqual({ NODE_ENV: 'prod' });
49
+ expect(forked.extensions).toEqual({ b: 2 });
50
+ expect(forked.cross).toBeUndefined();
51
+ expect(forked.fire).toBeUndefined();
52
+ expect(forked.resource).toBeUndefined();
53
+ });
54
+
55
+ test('does not mutate the parent context', () => {
56
+ const parent = {
57
+ cross: noopCross,
58
+ fire: noopFire,
59
+ resource: noopResource,
60
+ };
61
+
62
+ forkCtx(parent, {});
63
+
64
+ expect(parent.cross).toBe(noopCross);
65
+ expect(parent.fire).toBe(noopFire);
66
+ expect(parent.resource).toBe(noopResource);
67
+ });
68
+
69
+ test('honours a caller-supplied reset list', () => {
70
+ const parent = {
71
+ cross: noopCross,
72
+ fire: noopFire,
73
+ resource: noopResource,
74
+ };
75
+
76
+ const forked = forkCtx(parent, {}, ['fire']);
77
+
78
+ expect(forked.fire).toBeUndefined();
79
+ // Only `fire` is reset when caller overrides the list.
80
+ expect(forked.cross).toBe(noopCross);
81
+ expect(forked.resource).toBe(noopResource);
82
+ });
83
+ });
@@ -0,0 +1,191 @@
1
+ /* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { z } from 'zod';
5
+
6
+ import { ValidationError } from '../errors.js';
7
+ import type { Layer } from '../layer.js';
8
+ import { Result } from '../result.js';
9
+ import { run } from '../run.js';
10
+ import { signal } from '../signal.js';
11
+ import { topo } from '../topo.js';
12
+ import { trail } from '../trail.js';
13
+ import { ingest } from '../trails/index.js';
14
+
15
+ const withExamples = <TSchema extends z.ZodType>(
16
+ schema: TSchema,
17
+ examples: readonly z.input<TSchema>[]
18
+ ): TSchema & { readonly examples: readonly z.input<TSchema>[] } =>
19
+ Object.assign(schema, { examples });
20
+
21
+ const paymentCompleted = signal('payment.completed', {
22
+ payload: z.object({
23
+ amount: z.number(),
24
+ paymentId: z.string(),
25
+ }),
26
+ });
27
+
28
+ const rawPaymentEvent = z.object({
29
+ amount: z.number(),
30
+ paymentId: z.string(),
31
+ });
32
+
33
+ const stripeEvent = z.object({
34
+ data: z.object({
35
+ object: z.object({
36
+ amount: z.number(),
37
+ id: z.string(),
38
+ }),
39
+ }),
40
+ });
41
+
42
+ const createConsumer = (
43
+ id: string,
44
+ capture: { payloads: unknown[] },
45
+ source = paymentCompleted
46
+ ) =>
47
+ trail(id, {
48
+ blaze: (input) => {
49
+ capture.payloads.push(input);
50
+ return Result.ok({ ok: true });
51
+ },
52
+ input: source.payload,
53
+ on: [source],
54
+ });
55
+
56
+ describe('ingest()', () => {
57
+ test('derives id, intent, fires, output schema, and examples from schema examples', () => {
58
+ const schema = withExamples(rawPaymentEvent, [
59
+ { amount: 42, paymentId: 'pay_1' },
60
+ { amount: 7, paymentId: 'pay_2' },
61
+ ]);
62
+
63
+ const paymentIngest = ingest({
64
+ schema,
65
+ signal: paymentCompleted,
66
+ });
67
+
68
+ expect(paymentIngest.id).toBe('payment.completed.ingest');
69
+ expect(paymentIngest.intent).toBe('write');
70
+ expect(paymentIngest.pattern).toBe('ingest');
71
+ expect(paymentIngest.fires).toEqual(['payment.completed']);
72
+ expect(paymentIngest.output?.safeParse().success).toBe(true);
73
+ expect(paymentIngest.examples).toEqual([
74
+ {
75
+ input: { amount: 42, paymentId: 'pay_1' },
76
+ name: 'Ingest payment.completed 1',
77
+ },
78
+ {
79
+ input: { amount: 7, paymentId: 'pay_2' },
80
+ name: 'Ingest payment.completed 2',
81
+ },
82
+ ]);
83
+ });
84
+
85
+ test('validates input before attempting to emit the signal', async () => {
86
+ const capture = { payloads: [] as unknown[] };
87
+ const paymentIngest = ingest({
88
+ schema: rawPaymentEvent,
89
+ signal: paymentCompleted,
90
+ });
91
+
92
+ const app = topo('ingest-validation', {
93
+ consumer: createConsumer('payments.capture', capture),
94
+ paymentCompleted,
95
+ paymentIngest,
96
+ });
97
+
98
+ const result = await run(app, paymentIngest.id, {
99
+ amount: '42',
100
+ paymentId: 'pay_1',
101
+ });
102
+
103
+ expect(result.isErr()).toBe(true);
104
+ expect(result.error).toBeInstanceOf(ValidationError);
105
+ expect(capture.payloads).toEqual([]);
106
+ });
107
+
108
+ test('applies the verification layer when provided', async () => {
109
+ const wrapped: string[] = [];
110
+ const verify: Layer = {
111
+ name: 'verify',
112
+ wrap: (trailDef, implementation) => async (input, ctx) => {
113
+ wrapped.push(trailDef.id);
114
+ return await implementation(input, ctx);
115
+ },
116
+ };
117
+ const capture = { payloads: [] as unknown[] };
118
+ const paymentIngest = ingest({
119
+ schema: rawPaymentEvent,
120
+ signal: paymentCompleted,
121
+ verify,
122
+ });
123
+
124
+ const app = topo('ingest-verify', {
125
+ consumer: createConsumer('payments.capture', capture),
126
+ paymentCompleted,
127
+ paymentIngest,
128
+ });
129
+
130
+ const result = await run(app, paymentIngest.id, {
131
+ amount: 42,
132
+ paymentId: 'pay_1',
133
+ });
134
+
135
+ expect(result.isOk()).toBe(true);
136
+ expect(wrapped).toEqual(['payment.completed.ingest']);
137
+ expect(capture.payloads).toEqual([{ amount: 42, paymentId: 'pay_1' }]);
138
+ });
139
+
140
+ test('emits the declared signal after successful ingestion', async () => {
141
+ const capture = { payloads: [] as unknown[] };
142
+ const paymentIngest = ingest({
143
+ schema: rawPaymentEvent,
144
+ signal: paymentCompleted,
145
+ });
146
+
147
+ const app = topo('ingest-fire', {
148
+ consumer: createConsumer('payments.capture', capture),
149
+ paymentCompleted,
150
+ paymentIngest,
151
+ });
152
+
153
+ const result = await run(app, paymentIngest.id, {
154
+ amount: 42,
155
+ paymentId: 'pay_1',
156
+ });
157
+
158
+ expect(result.isOk()).toBe(true);
159
+ expect(capture.payloads).toEqual([{ amount: 42, paymentId: 'pay_1' }]);
160
+ });
161
+
162
+ test('applies the transform before emitting the signal payload', async () => {
163
+ const capture = { payloads: [] as unknown[] };
164
+ const paymentIngest = ingest({
165
+ schema: stripeEvent,
166
+ signal: paymentCompleted,
167
+ transform: (payload) => ({
168
+ amount: payload.data.object.amount,
169
+ paymentId: payload.data.object.id,
170
+ }),
171
+ });
172
+
173
+ const app = topo('ingest-transform', {
174
+ consumer: createConsumer('payments.capture', capture),
175
+ paymentCompleted,
176
+ paymentIngest,
177
+ });
178
+
179
+ const result = await run(app, paymentIngest.id, {
180
+ data: {
181
+ object: {
182
+ amount: 99,
183
+ id: 'pay_99',
184
+ },
185
+ },
186
+ });
187
+
188
+ expect(result.isOk()).toBe(true);
189
+ expect(capture.payloads).toEqual([{ amount: 99, paymentId: 'pay_99' }]);
190
+ });
191
+ });
@@ -1,18 +1,18 @@
1
- /* oxlint-disable require-await -- gate wrappers satisfy async interfaces without awaiting */
1
+ /* oxlint-disable require-await -- layer wrappers satisfy async interfaces without awaiting */
2
2
  import { describe, test, expect } from 'bun:test';
3
3
 
4
4
  import { z } from 'zod';
5
5
 
6
6
  import { createTrailContext } from '../context';
7
- import { composeGates } from '../gate';
8
- import type { Gate } from '../gate';
7
+ import { composeLayers } from '../layer';
8
+ import type { Layer } from '../layer';
9
9
  import { Result } from '../result';
10
10
  import { trail } from '../trail';
11
11
  import type { TrailContext } from '../types';
12
12
 
13
13
  const stubCtx: TrailContext = createTrailContext({
14
14
  abortSignal: AbortSignal.timeout(5000),
15
- requestId: 'test-gate',
15
+ requestId: 'test-layer',
16
16
  });
17
17
 
18
18
  const echoTrail = trail('echo', {
@@ -23,12 +23,12 @@ const echoTrail = trail('echo', {
23
23
  });
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
- // Gate tests
26
+ // Layer tests
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- describe('Gate', () => {
30
- test('single gate wraps implementation', async () => {
31
- const prefixGate: Gate = {
29
+ describe('Layer', () => {
30
+ test('single layer wraps implementation', async () => {
31
+ const prefixGate: Layer = {
32
32
  name: 'prefix',
33
33
  wrap(_trail, impl) {
34
34
  return async (input, ctx) => {
@@ -41,17 +41,17 @@ describe('Gate', () => {
41
41
  },
42
42
  };
43
43
 
44
- const wrapped = composeGates([prefixGate], echoTrail, echoTrail.blaze);
44
+ const wrapped = composeLayers([prefixGate], echoTrail, echoTrail.blaze);
45
45
  const result = await wrapped({ value: 'hello' }, stubCtx);
46
46
 
47
47
  expect(result.isOk()).toBe(true);
48
48
  expect(result.unwrap()).toEqual({ value: 'prefixed:hello' });
49
49
  });
50
50
 
51
- test('multiple gates compose in outermost-first order', async () => {
51
+ test('multiple layers compose in outermost-first order', async () => {
52
52
  const log: string[] = [];
53
53
 
54
- const outer: Gate = {
54
+ const outer: Layer = {
55
55
  name: 'outer',
56
56
  wrap(_trail, impl) {
57
57
  return async (input, ctx) => {
@@ -63,7 +63,7 @@ describe('Gate', () => {
63
63
  },
64
64
  };
65
65
 
66
- const inner: Gate = {
66
+ const inner: Layer = {
67
67
  name: 'inner',
68
68
  wrap(_trail, impl) {
69
69
  return async (input, ctx) => {
@@ -75,7 +75,7 @@ describe('Gate', () => {
75
75
  },
76
76
  };
77
77
 
78
- const wrapped = composeGates([outer, inner], echoTrail, echoTrail.blaze);
78
+ const wrapped = composeLayers([outer, inner], echoTrail, echoTrail.blaze);
79
79
  await wrapped({ value: 'x' }, stubCtx);
80
80
 
81
81
  expect(log).toEqual([
@@ -86,15 +86,15 @@ describe('Gate', () => {
86
86
  ]);
87
87
  });
88
88
 
89
- test('gate can short-circuit without calling inner implementation', async () => {
90
- const shortCircuit: Gate = {
89
+ test('layer can short-circuit without calling inner implementation', async () => {
90
+ const shortCircuit: Layer = {
91
91
  name: 'short-circuit',
92
92
  wrap() {
93
93
  return async () => Result.err(new Error('blocked'));
94
94
  },
95
95
  };
96
96
 
97
- const wrapped = composeGates([shortCircuit], echoTrail, echoTrail.blaze);
97
+ const wrapped = composeLayers([shortCircuit], echoTrail, echoTrail.blaze);
98
98
  const result = await wrapped({ value: 'hello' }, stubCtx);
99
99
 
100
100
  expect(result.isErr()).toBe(true);
@@ -102,10 +102,10 @@ describe('Gate', () => {
102
102
  expect(err.error.message).toBe('blocked');
103
103
  });
104
104
 
105
- test('gate can inspect trail meta', () => {
105
+ test('layer can inspect trail meta', () => {
106
106
  let capturedDomain: unknown;
107
107
 
108
- const inspectGate: Gate = {
108
+ const inspectGate: Layer = {
109
109
  name: 'inspect',
110
110
  wrap(t, impl) {
111
111
  capturedDomain = t.meta?.['domain'];
@@ -113,13 +113,13 @@ describe('Gate', () => {
113
113
  },
114
114
  };
115
115
 
116
- composeGates([inspectGate], echoTrail, echoTrail.blaze);
116
+ composeLayers([inspectGate], echoTrail, echoTrail.blaze);
117
117
 
118
118
  expect(capturedDomain).toBe('test');
119
119
  });
120
120
 
121
- test('empty gates array returns implementation unchanged', () => {
122
- const wrapped = composeGates([], echoTrail, echoTrail.blaze);
121
+ test('empty layers array returns implementation unchanged', () => {
122
+ const wrapped = composeLayers([], echoTrail, echoTrail.blaze);
123
123
  expect(wrapped).toBe(echoTrail.blaze);
124
124
  });
125
125
  });
@@ -2,7 +2,7 @@ import { describe, test, expect } from 'bun:test';
2
2
  import { resolve, join } from 'node:path';
3
3
 
4
4
  import { PermissionError } from '../errors.js';
5
- import { securePath, isPathSafe, resolveSafePath } from '../path-security.js';
5
+ import { securePath, isPathSafe, deriveSafePath } from '../path-security.js';
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // securePath
@@ -81,33 +81,33 @@ describe('isPathSafe', () => {
81
81
  });
82
82
 
83
83
  // ---------------------------------------------------------------------------
84
- // resolveSafePath
84
+ // deriveSafePath
85
85
  // ---------------------------------------------------------------------------
86
86
 
87
- describe('resolveSafePath', () => {
87
+ describe('deriveSafePath', () => {
88
88
  const base = '/project/workspace';
89
89
 
90
90
  test('joins multiple segments safely', () => {
91
- const result = resolveSafePath(base, 'src', 'lib', 'utils.ts');
91
+ const result = deriveSafePath(base, 'src', 'lib', 'utils.ts');
92
92
  expect(result.isOk()).toBe(true);
93
93
  expect(result.unwrap()).toBe(join(resolve(base), 'src', 'lib', 'utils.ts'));
94
94
  });
95
95
 
96
96
  test('rejects when segments escape the base', () => {
97
- const result = resolveSafePath(base, 'src', '../../../etc');
97
+ const result = deriveSafePath(base, 'src', '../../../etc');
98
98
  expect(result.isErr()).toBe(true);
99
99
  const err = result as unknown as { error: PermissionError };
100
100
  expect(err.error).toBeInstanceOf(PermissionError);
101
101
  });
102
102
 
103
103
  test('handles single segment', () => {
104
- const result = resolveSafePath(base, 'file.txt');
104
+ const result = deriveSafePath(base, 'file.txt');
105
105
  expect(result.isOk()).toBe(true);
106
106
  expect(result.unwrap()).toBe(resolve(base, 'file.txt'));
107
107
  });
108
108
 
109
109
  test('handles empty segments', () => {
110
- const result = resolveSafePath(base);
110
+ const result = deriveSafePath(base);
111
111
  expect(result.isOk()).toBe(true);
112
112
  expect(result.unwrap()).toBe(resolve(base));
113
113
  });
@@ -11,7 +11,7 @@ import {
11
11
  retry,
12
12
  withTimeout,
13
13
  shouldRetry,
14
- getBackoffDelay,
14
+ deriveBackoffDelay,
15
15
  } from '../resilience.js';
16
16
  import { Result } from '../result.js';
17
17
 
@@ -37,20 +37,20 @@ describe('shouldRetry', () => {
37
37
  });
38
38
 
39
39
  // ---------------------------------------------------------------------------
40
- // getBackoffDelay
40
+ // deriveBackoffDelay
41
41
  // ---------------------------------------------------------------------------
42
42
 
43
- describe('getBackoffDelay', () => {
43
+ describe('deriveBackoffDelay', () => {
44
44
  test('returns a number >= 0', () => {
45
45
  for (let i = 0; i < 20; i += 1) {
46
- const delay = getBackoffDelay(0);
46
+ const delay = deriveBackoffDelay(0);
47
47
  expect(delay).toBeGreaterThanOrEqual(0);
48
48
  }
49
49
  });
50
50
 
51
51
  test('respects maxDelay cap', () => {
52
52
  for (let i = 0; i < 20; i += 1) {
53
- const delay = getBackoffDelay(10, { maxDelay: 100 });
53
+ const delay = deriveBackoffDelay(10, { maxDelay: 100 });
54
54
  expect(delay).toBeLessThanOrEqual(100);
55
55
  }
56
56
  });
@@ -59,7 +59,7 @@ describe('getBackoffDelay', () => {
59
59
  // With attempt=0, exponential = base * factor^0 = base
60
60
  // Jitter is [0, base], so delay <= base
61
61
  for (let i = 0; i < 20; i += 1) {
62
- const delay = getBackoffDelay(0, { backoffFactor: 3, baseDelay: 500 });
62
+ const delay = deriveBackoffDelay(0, { backoffFactor: 3, baseDelay: 500 });
63
63
  expect(delay).toBeLessThanOrEqual(500);
64
64
  }
65
65
  });
@@ -67,10 +67,10 @@ describe('getBackoffDelay', () => {
67
67
  test('scales with attempt number', () => {
68
68
  // Higher attempts should have higher caps (on average)
69
69
  const samples0 = Array.from({ length: 100 }, () =>
70
- getBackoffDelay(0, { baseDelay: 100 })
70
+ deriveBackoffDelay(0, { baseDelay: 100 })
71
71
  );
72
72
  const samples3 = Array.from({ length: 100 }, () =>
73
- getBackoffDelay(3, { baseDelay: 100 })
73
+ deriveBackoffDelay(3, { baseDelay: 100 })
74
74
  );
75
75
  const avg0 = samples0.reduce((a, b) => a + b, 0) / samples0.length;
76
76
  const avg3 = samples3.reduce((a, b) => a + b, 0) / samples3.length;