@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,204 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ Result,
6
+ createTrailContext,
7
+ findDuplicateResourceId,
8
+ isResource,
9
+ resource,
10
+ } from '../index.js';
11
+ import type {
12
+ Resource,
13
+ ResourceContext,
14
+ ResourceSpec,
15
+ TrailContext,
16
+ } from '../index.js';
17
+
18
+ const resourceCtx: ResourceContext = {
19
+ config: {},
20
+ cwd: '/tmp/trails',
21
+ env: { DATABASE_URL: 'file::memory:' },
22
+ workspaceRoot: '/tmp',
23
+ };
24
+
25
+ let disposedValue: number | undefined;
26
+
27
+ const counterResourceSpec: ResourceSpec<number> = {
28
+ create: () => Result.ok(3),
29
+ description: 'Counter resource',
30
+ dispose: (value) => {
31
+ disposedValue = value;
32
+ },
33
+ health: (value) => Result.ok({ healthy: value > 0 }),
34
+ meta: { domain: 'data' },
35
+ mock: () => 1,
36
+ };
37
+
38
+ const resolvedResourceCtx = (id: string, instance: unknown): TrailContext =>
39
+ createTrailContext({
40
+ abortSignal: new AbortController().signal,
41
+ extensions: { [id]: instance },
42
+ requestId: `${id}-request`,
43
+ });
44
+
45
+ describe('resource types', () => {
46
+ test('ResourceContext exposes the stable process-scoped fields', () => {
47
+ expect(resourceCtx.cwd).toBe('/tmp/trails');
48
+ expect(resourceCtx.env?.DATABASE_URL).toBe('file::memory:');
49
+ expect(resourceCtx.workspaceRoot).toBe('/tmp');
50
+ });
51
+
52
+ test('ResourceSpec stores description and meta', () => {
53
+ expect(counterResourceSpec.description).toBe('Counter resource');
54
+ expect(counterResourceSpec.meta).toEqual({ domain: 'data' });
55
+ });
56
+
57
+ test('ResourceSpec can reserve a config schema for future composition', () => {
58
+ const config = z.object({ url: z.string().url() });
59
+ const spec: ResourceSpec<number> = {
60
+ config,
61
+ create: () => Result.ok(1),
62
+ };
63
+
64
+ expect(spec.config).toBe(config);
65
+ });
66
+
67
+ test('ResourceSpec create and health callbacks are callable', async () => {
68
+ const result = await counterResourceSpec.create(resourceCtx);
69
+ expect(result.isOk()).toBe(true);
70
+ expect(result.unwrap()).toBe(3);
71
+
72
+ const health = await counterResourceSpec.health?.(result.unwrap());
73
+ expect(health?.isOk()).toBe(true);
74
+ expect(health?.unwrap()).toEqual({ healthy: true });
75
+ });
76
+
77
+ test('ResourceSpec mock and dispose callbacks are callable', async () => {
78
+ disposedValue = undefined;
79
+ const result = await counterResourceSpec.create(resourceCtx);
80
+ expect(result.isOk()).toBe(true);
81
+
82
+ const instance = result.unwrap();
83
+ expect(await counterResourceSpec.mock?.()).toBe(1);
84
+
85
+ await counterResourceSpec.dispose?.(instance);
86
+ expect(disposedValue).toBe(3);
87
+ });
88
+
89
+ test('ResourceSpec create can be async', async () => {
90
+ const spec: ResourceSpec<number> = {
91
+ create: async () => {
92
+ await Bun.sleep(0);
93
+ return Result.ok(7);
94
+ },
95
+ };
96
+
97
+ const result = await spec.create(resourceCtx);
98
+ expect(result.isOk()).toBe(true);
99
+ expect(result.unwrap()).toBe(7);
100
+ });
101
+
102
+ test('Resource carries identity alongside the shared spec fields', async () => {
103
+ const counterResource: Resource<number> = {
104
+ from(ctx) {
105
+ return ctx.resource(this);
106
+ },
107
+ id: 'counter.main',
108
+ kind: 'resource',
109
+ ...counterResourceSpec,
110
+ };
111
+
112
+ expect(counterResource.kind).toBe('resource');
113
+ expect(counterResource.id).toBe('counter.main');
114
+
115
+ const created = await counterResource.create(resourceCtx);
116
+ expect(created.isOk()).toBe(true);
117
+ expect(created.unwrap()).toBe(3);
118
+ expect(await counterResource.mock?.()).toBe(1);
119
+ });
120
+ });
121
+
122
+ describe('resource()', () => {
123
+ test('returns a frozen resource object with kind and id', () => {
124
+ const counter = resource('counter.main', counterResourceSpec);
125
+
126
+ expect(counter.kind).toBe('resource');
127
+ expect(counter.id).toBe('counter.main');
128
+ expect(Object.isFrozen(counter)).toBe(true);
129
+ });
130
+
131
+ test('rejects resource ids containing a scope separator', () => {
132
+ expect(() => resource('billing:primary', counterResourceSpec)).toThrow(
133
+ 'Resource "billing:primary" is invalid because resource ids may not contain ":"'
134
+ );
135
+ });
136
+
137
+ test('infers the resource type through from(ctx)', () => {
138
+ const db = resource('db.main', {
139
+ create: () =>
140
+ Result.ok({
141
+ query(sql: string) {
142
+ return sql.length;
143
+ },
144
+ }),
145
+ description: 'Typed database resource',
146
+ });
147
+
148
+ const store = {
149
+ query(sql: string) {
150
+ return sql.length;
151
+ },
152
+ };
153
+ const ctx = resolvedResourceCtx('db.main', store);
154
+
155
+ const resolved = db.from(ctx);
156
+ expect(resolved.query('select 1')).toBe(8);
157
+ });
158
+
159
+ test('from(ctx) throws when the resource is missing', () => {
160
+ const counter = resource('counter.main', counterResourceSpec);
161
+ const ctx = createTrailContext({
162
+ abortSignal: new AbortController().signal,
163
+ requestId: 'missing-resource',
164
+ });
165
+
166
+ expect(() => counter.from(ctx)).toThrow(
167
+ 'Resource "counter.main" not provisioned in context'
168
+ );
169
+ });
170
+
171
+ test('from(ctx) returns undefined when the resource key exists with an undefined value', () => {
172
+ const optional = resource<undefined>('optional.main', {
173
+ create: () => Result.ok<undefined>(),
174
+ });
175
+ const ctx = createTrailContext({
176
+ abortSignal: new AbortController().signal,
177
+ extensions: { [optional.id]: undefined },
178
+ requestId: 'undefined-resource',
179
+ });
180
+
181
+ expect(optional.from(ctx)).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ describe('resource helpers', () => {
186
+ test('isResource identifies resource definitions', () => {
187
+ const counter = resource('counter.main', counterResourceSpec);
188
+
189
+ expect(isResource(counter)).toBe(true);
190
+ expect(isResource({ id: 'counter.main', kind: 'trail' })).toBe(false);
191
+ expect(isResource(null)).toBe(false);
192
+ });
193
+
194
+ test('findDuplicateResourceId returns the first repeated ID', () => {
195
+ const first = resource('counter.main', counterResourceSpec);
196
+ const duplicate = resource('counter.main', counterResourceSpec);
197
+ const other = resource('counter.secondary', counterResourceSpec);
198
+
199
+ expect(findDuplicateResourceId([first, other])).toBeUndefined();
200
+ expect(findDuplicateResourceId([first, other, duplicate])).toBe(
201
+ 'counter.main'
202
+ );
203
+ });
204
+ });
@@ -5,9 +5,9 @@ import { z } from 'zod';
5
5
 
6
6
  import { run } from '../run';
7
7
  import { InternalError, NotFoundError, ValidationError } from '../errors';
8
- import type { Gate } from '../gate';
8
+ import type { Layer } from '../layer';
9
9
  import { Result } from '../result';
10
- import { provision } from '../provision';
10
+ import { resource } from '../resource';
11
11
  import { topo } from '../topo';
12
12
  import { trail } from '../trail';
13
13
  import type { TrailContext, TrailContextInit } from '../types';
@@ -44,25 +44,25 @@ describe('run', () => {
44
44
  expect(result.unwrap()).toEqual({ value: 'hello' });
45
45
  });
46
46
 
47
- test('passes provision overrides through to executeTrail', async () => {
48
- const id = `run.provision.${Bun.randomUUIDv7()}`;
49
- const db = provision(id, {
47
+ test('passes resource overrides through to executeTrail', async () => {
48
+ const id = `run.resource.${Bun.randomUUIDv7()}`;
49
+ const db = resource(id, {
50
50
  create: () => Result.ok({ source: 'factory' }),
51
51
  });
52
52
  const searchTrail = trail('search', {
53
53
  blaze: (_input, ctx) => Result.ok({ source: db.from(ctx).source }),
54
54
  input: z.object({}),
55
55
  output: z.object({ source: z.string() }),
56
- provisions: [db],
56
+ resources: [db],
57
57
  });
58
- const searchTopo = topo('provision-test', { searchTrail });
58
+ const searchTopo = topo('resource-test', { searchTrail });
59
59
 
60
60
  const result = await run(
61
61
  searchTopo,
62
62
  'search',
63
63
  {},
64
64
  {
65
- provisions: { [id]: { source: 'override' } },
65
+ resources: { [id]: { source: 'override' } },
66
66
  }
67
67
  );
68
68
 
@@ -91,11 +91,11 @@ describe('run', () => {
91
91
  });
92
92
  });
93
93
 
94
- describe('gates', () => {
95
- test('gate composition works through run', async () => {
94
+ describe('layers', () => {
95
+ test('layer composition works through run', async () => {
96
96
  const log: string[] = [];
97
- const gate: Gate = {
98
- name: 'test-gate',
97
+ const layer: Layer = {
98
+ name: 'test-layer',
99
99
  wrap(_trail, impl) {
100
100
  return async (input, ctx) => {
101
101
  log.push('before');
@@ -110,7 +110,7 @@ describe('run', () => {
110
110
  testTopo,
111
111
  'echo',
112
112
  { value: 'x' },
113
- { gates: [gate] }
113
+ { layers: [layer] }
114
114
  );
115
115
 
116
116
  expect(result.isOk()).toBe(true);
@@ -5,9 +5,9 @@ import { z } from 'zod';
5
5
 
6
6
  import { executeTrail } from '../execute.js';
7
7
  import { Result } from '../result.js';
8
- import { provision } from '../provision.js';
8
+ import { resource } from '../resource.js';
9
9
  import { trail } from '../trail.js';
10
- import type { ProvisionContext } from '../provision.js';
10
+ import type { ResourceContext } from '../resource.js';
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Helpers
@@ -18,9 +18,9 @@ const nextId = (name: string): string =>
18
18
 
19
19
  const createSingletonConfigTrail = (id: string) => {
20
20
  const captures = { createCalls: 0 };
21
- const svc = provision(id, {
21
+ const svc = resource(id, {
22
22
  config: z.object({ key: z.string() }),
23
- create: (ctx: ProvisionContext<{ key: string }>) => {
23
+ create: (ctx: ResourceContext<{ key: string }>) => {
24
24
  captures.createCalls += 1;
25
25
  return Result.ok({
26
26
  createCall: captures.createCalls,
@@ -39,7 +39,7 @@ const createSingletonConfigTrail = (id: string) => {
39
39
  }),
40
40
  input: z.object({}),
41
41
  output: z.object({ createCall: z.number(), key: z.string() }),
42
- provisions: [svc],
42
+ resources: [svc],
43
43
  }),
44
44
  };
45
45
  };
@@ -48,14 +48,14 @@ const createSingletonConfigTrail = (id: string) => {
48
48
  // Tests
49
49
  // ---------------------------------------------------------------------------
50
50
 
51
- describe('ProvisionContext.config', () => {
52
- test('provision with config schema receives validated config in svc.config', async () => {
51
+ describe('ResourceContext.config', () => {
52
+ test('resource with config schema receives validated config in svc.config', async () => {
53
53
  const id = nextId('typed-config');
54
54
  let capturedConfig: unknown;
55
55
 
56
- const db = provision(id, {
56
+ const db = resource(id, {
57
57
  config: z.object({ poolSize: z.number(), url: z.string().url() }),
58
- create: (svc: ProvisionContext<{ url: string; poolSize: number }>) => {
58
+ create: (svc: ResourceContext<{ url: string; poolSize: number }>) => {
59
59
  capturedConfig = svc.config;
60
60
  return Result.ok({ connected: true });
61
61
  },
@@ -65,7 +65,7 @@ describe('ProvisionContext.config', () => {
65
65
  blaze: (_input, ctx) => Result.ok({ connected: db.from(ctx).connected }),
66
66
  input: z.object({}),
67
67
  output: z.object({ connected: z.boolean() }),
68
- provisions: [db],
68
+ resources: [db],
69
69
  });
70
70
 
71
71
  const result = await executeTrail(
@@ -82,11 +82,11 @@ describe('ProvisionContext.config', () => {
82
82
  expect(capturedConfig).toEqual({ poolSize: 5, url: 'https://example.com' });
83
83
  });
84
84
 
85
- test('provision without config still works — svc.config is undefined', async () => {
85
+ test('resource without config still works — svc.config is undefined', async () => {
86
86
  const id = nextId('no-config');
87
87
  let capturedConfig: unknown = 'sentinel';
88
88
 
89
- const counter = provision(id, {
89
+ const counter = resource(id, {
90
90
  create: (svc) => {
91
91
  capturedConfig = svc.config;
92
92
  return Result.ok(42);
@@ -96,7 +96,7 @@ describe('ProvisionContext.config', () => {
96
96
  const counterTrail = trail('svc-config.no-config', {
97
97
  blaze: (_input, ctx) => Result.ok({ value: counter.from(ctx) }),
98
98
  input: z.object({}),
99
- provisions: [counter],
99
+ resources: [counter],
100
100
  });
101
101
 
102
102
  const result = await executeTrail(counterTrail, {});
@@ -105,10 +105,10 @@ describe('ProvisionContext.config', () => {
105
105
  expect(capturedConfig).toBeUndefined();
106
106
  });
107
107
 
108
- test('config validation failure returns Result.err at provision creation time', async () => {
108
+ test('config validation failure returns Result.err at resource creation time', async () => {
109
109
  const id = nextId('invalid-config');
110
110
 
111
- const db = provision(id, {
111
+ const db = resource(id, {
112
112
  config: z.object({ url: z.string().url() }),
113
113
  create: () => Result.ok({ connected: true }),
114
114
  });
@@ -116,7 +116,7 @@ describe('ProvisionContext.config', () => {
116
116
  const dbTrail = trail('svc-config.invalid', {
117
117
  blaze: () => Result.ok(null),
118
118
  input: z.object({}),
119
- provisions: [db],
119
+ resources: [db],
120
120
  });
121
121
 
122
122
  const result = await executeTrail(
@@ -133,10 +133,10 @@ describe('ProvisionContext.config', () => {
133
133
  expect(result.error.message).toContain(id);
134
134
  });
135
135
 
136
- test('missing configValues for a provision with config schema returns Result.err', async () => {
136
+ test('missing configValues for a resource with config schema returns Result.err', async () => {
137
137
  const id = nextId('missing-config');
138
138
 
139
- const db = provision(id, {
139
+ const db = resource(id, {
140
140
  config: z.object({ url: z.string().url() }),
141
141
  create: () => Result.ok({ connected: true }),
142
142
  });
@@ -144,7 +144,7 @@ describe('ProvisionContext.config', () => {
144
144
  const dbTrail = trail('svc-config.missing', {
145
145
  blaze: () => Result.ok(null),
146
146
  input: z.object({}),
147
- provisions: [db],
147
+ resources: [db],
148
148
  });
149
149
 
150
150
  const result = await executeTrail(dbTrail, {});
@@ -153,10 +153,10 @@ describe('ProvisionContext.config', () => {
153
153
  expect(result.error.message).toContain(id);
154
154
  });
155
155
 
156
- test('provision override bypasses config validation', async () => {
156
+ test('resource override bypasses config validation', async () => {
157
157
  const id = nextId('override-bypass');
158
158
 
159
- const db = provision(id, {
159
+ const db = resource(id, {
160
160
  config: z.object({ url: z.string().url() }),
161
161
  create: () => Result.ok({ connected: true }),
162
162
  });
@@ -165,15 +165,11 @@ describe('ProvisionContext.config', () => {
165
165
  blaze: (_input, ctx) => Result.ok({ value: db.from(ctx) as number }),
166
166
  input: z.object({}),
167
167
  output: z.object({ value: z.number() }),
168
- provisions: [db],
168
+ resources: [db],
169
169
  });
170
170
 
171
- // Provide the provision via overrides without any configValues — should NOT fail
172
- const result = await executeTrail(
173
- dbTrail,
174
- {},
175
- { provisions: { [id]: 42 } }
176
- );
171
+ // Provide the resource via overrides without any configValues — should NOT fail
172
+ const result = await executeTrail(dbTrail, {}, { resources: { [id]: 42 } });
177
173
 
178
174
  expect(result.isOk()).toBe(true);
179
175
  expect(result.unwrap()).toEqual({ value: 42 });
@@ -182,9 +178,9 @@ describe('ProvisionContext.config', () => {
182
178
  const id = nextId('options-config');
183
179
  const captures: unknown[] = [];
184
180
 
185
- const svc = provision(id, {
181
+ const svc = resource(id, {
186
182
  config: z.object({ key: z.string() }),
187
- create: (ctx: ProvisionContext<{ key: string }>) => {
183
+ create: (ctx: ResourceContext<{ key: string }>) => {
188
184
  captures.push(ctx.config);
189
185
  return Result.ok({ key: ctx.config.key });
190
186
  },
@@ -194,7 +190,7 @@ describe('ProvisionContext.config', () => {
194
190
  blaze: (_input, ctx) => Result.ok({ key: svc.from(ctx).key }),
195
191
  input: z.object({}),
196
192
  output: z.object({ key: z.string() }),
197
- provisions: [svc],
193
+ resources: [svc],
198
194
  });
199
195
 
200
196
  const result = await executeTrail(
@@ -210,7 +206,7 @@ describe('ProvisionContext.config', () => {
210
206
  expect(captures).toEqual([{ key: 'hello' }]);
211
207
  });
212
208
 
213
- test('config-aware singleton provisions reuse the cached instance', async () => {
209
+ test('config-aware singleton resources reuse the cached instance', async () => {
214
210
  const id = nextId('singleton-config');
215
211
  const { captures, trail: singletonTrail } = createSingletonConfigTrail(id);
216
212
  const options = {
@@ -0,0 +1,196 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { Result } from '../result.js';
6
+ import { filterSurfaceTrails, matchesTrailPattern } from '../surface-filter.js';
7
+ import { signal } from '../signal.js';
8
+ import { trail } from '../trail.js';
9
+
10
+ const orderPlaced = signal('order.placed', {
11
+ payload: z.object({ orderId: z.string() }),
12
+ });
13
+
14
+ const publicTrail = trail('entity.show', {
15
+ blaze: () => Result.ok({ ok: true }),
16
+ input: z.object({}),
17
+ intent: 'read',
18
+ });
19
+
20
+ const nestedTrail = trail('entity.admin.audit', {
21
+ blaze: () => Result.ok({ ok: true }),
22
+ input: z.object({}),
23
+ intent: 'destroy',
24
+ });
25
+
26
+ const internalTrail = trail('entity.secret.rotate', {
27
+ blaze: () => Result.ok({ ok: true }),
28
+ input: z.object({}),
29
+ visibility: 'internal',
30
+ });
31
+
32
+ const legacyInternalTrail = trail('entity.legacy.rotate', {
33
+ blaze: () => Result.ok({ ok: true }),
34
+ input: z.object({}),
35
+ meta: { internal: true },
36
+ });
37
+
38
+ const bareEntityTrail = trail('entity', {
39
+ blaze: () => Result.ok({ ok: true }),
40
+ input: z.object({}),
41
+ });
42
+
43
+ const otherTrail = trail('other', {
44
+ blaze: () => Result.ok({ ok: true }),
45
+ input: z.object({}),
46
+ });
47
+
48
+ const consumerTrail = trail('notify.email', {
49
+ blaze: () => Result.ok({ ok: true }),
50
+ input: z.object({}),
51
+ on: [orderPlaced],
52
+ });
53
+
54
+ describe('matchesTrailPattern', () => {
55
+ test('exact trail IDs match directly', () => {
56
+ expect(matchesTrailPattern('entity.show', 'entity.show')).toBe(true);
57
+ });
58
+
59
+ test('* matches a single dotted segment', () => {
60
+ expect(matchesTrailPattern('entity.show', 'entity.*')).toBe(true);
61
+ expect(matchesTrailPattern('entity.admin.show', 'entity.*')).toBe(false);
62
+ });
63
+
64
+ test('** matches any remaining dotted depth', () => {
65
+ expect(matchesTrailPattern('entity.admin.show', 'entity.**')).toBe(true);
66
+ expect(matchesTrailPattern('entity.show', 'entity.**')).toBe(true);
67
+ });
68
+
69
+ test('** terminal segment matches entity and all descendants', () => {
70
+ // Zero-remaining-segments case: bare parent matches entity.**.
71
+ expect(matchesTrailPattern('entity', 'entity.**')).toBe(true);
72
+ // One-remaining-segment case for completeness.
73
+ expect(matchesTrailPattern('entity.read', 'entity.**')).toBe(true);
74
+ // Sibling namespace must not match entity.**.
75
+ expect(matchesTrailPattern('other', 'entity.**')).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('filterSurfaceTrails', () => {
80
+ describe('visibility defaults', () => {
81
+ test('includes public trails by default', () => {
82
+ expect(filterSurfaceTrails([publicTrail])).toEqual([publicTrail]);
83
+ });
84
+
85
+ test('excludes internal trails by default', () => {
86
+ expect(filterSurfaceTrails([internalTrail])).toEqual([]);
87
+ });
88
+
89
+ test('does not expose internal trails for wildcard includes', () => {
90
+ expect(
91
+ filterSurfaceTrails([internalTrail], { include: ['entity.**'] })
92
+ ).toEqual([]);
93
+ });
94
+
95
+ test('allows exact include to expose an internal trail', () => {
96
+ expect(
97
+ filterSurfaceTrails([internalTrail], {
98
+ include: ['entity.secret.rotate'],
99
+ })
100
+ ).toEqual([internalTrail]);
101
+ });
102
+
103
+ test('legacy meta.internal is treated as internal visibility', () => {
104
+ // Default filter: legacy internal trails must not leak out.
105
+ expect(filterSurfaceTrails([legacyInternalTrail])).toEqual([]);
106
+
107
+ // Wildcard include must also refuse to expose the legacy internal trail.
108
+ expect(
109
+ filterSurfaceTrails([legacyInternalTrail], { include: ['entity.**'] })
110
+ ).toEqual([]);
111
+
112
+ // Explicit exact-id include is the documented escape hatch.
113
+ expect(
114
+ filterSurfaceTrails([legacyInternalTrail], {
115
+ include: ['entity.legacy.rotate'],
116
+ })
117
+ ).toEqual([legacyInternalTrail]);
118
+ });
119
+ });
120
+
121
+ describe('include and exclude patterns', () => {
122
+ test('exclude patterns remove matches before include narrowing', () => {
123
+ expect(
124
+ filterSurfaceTrails([publicTrail], {
125
+ exclude: ['entity.*'],
126
+ include: ['entity.show'],
127
+ })
128
+ ).toEqual([]);
129
+ });
130
+
131
+ test('include patterns narrow the visible trail set', () => {
132
+ expect(
133
+ filterSurfaceTrails([publicTrail, nestedTrail], {
134
+ include: ['entity.**'],
135
+ })
136
+ ).toEqual([publicTrail, nestedTrail]);
137
+ expect(
138
+ filterSurfaceTrails([publicTrail, nestedTrail], {
139
+ include: ['entity.*'],
140
+ })
141
+ ).toEqual([publicTrail]);
142
+ });
143
+
144
+ test('entity.** include matches bare entity and descendants', () => {
145
+ // Regression guard for the ** terminal-segment zero-depth case.
146
+ expect(
147
+ filterSurfaceTrails([bareEntityTrail, publicTrail, otherTrail], {
148
+ include: ['entity.**'],
149
+ })
150
+ ).toEqual([bareEntityTrail, publicTrail]);
151
+ });
152
+ });
153
+
154
+ describe('intent filtering', () => {
155
+ test('intent filtering narrows visible trails by behavior class', () => {
156
+ expect(
157
+ filterSurfaceTrails([publicTrail, nestedTrail], {
158
+ intent: ['read'],
159
+ })
160
+ ).toEqual([publicTrail]);
161
+
162
+ expect(
163
+ filterSurfaceTrails([publicTrail, nestedTrail], {
164
+ intent: ['destroy'],
165
+ })
166
+ ).toEqual([nestedTrail]);
167
+ });
168
+
169
+ test('intent filtering composes with glob include patterns using AND logic', () => {
170
+ expect(
171
+ filterSurfaceTrails([publicTrail, nestedTrail], {
172
+ include: ['entity.**'],
173
+ intent: ['read'],
174
+ })
175
+ ).toEqual([publicTrail]);
176
+ });
177
+
178
+ test('empty intent filters behave like no filter', () => {
179
+ expect(
180
+ filterSurfaceTrails([publicTrail, nestedTrail], {
181
+ intent: [],
182
+ })
183
+ ).toEqual([publicTrail, nestedTrail]);
184
+ });
185
+ });
186
+
187
+ describe('consumer trails', () => {
188
+ test('consumer trails are never exposed on trailheads', () => {
189
+ expect(
190
+ filterSurfaceTrails([consumerTrail], {
191
+ include: ['notify.email'],
192
+ })
193
+ ).toEqual([]);
194
+ });
195
+ });
196
+ });