@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
@@ -2,7 +2,10 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import { provision } from '../provision.js';
5
+ import { deriveDraftReport, isDraftId } from '../draft.js';
6
+ import { contour } from '../contour.js';
7
+ import { validateEstablishedTopo } from '../validate-established-topo.js';
8
+ import { resource } from '../resource.js';
6
9
  import { Result } from '../result.js';
7
10
  import { topo } from '../topo.js';
8
11
  import type { TopoIssue } from '../validate-topo.js';
@@ -19,6 +22,7 @@ const mockTrail = (
19
22
  id: string,
20
23
  overrides?: {
21
24
  crosses?: readonly string[];
25
+ contours?: readonly ReturnType<typeof contour>[];
22
26
  examples?: readonly {
23
27
  name: string;
24
28
  input: unknown;
@@ -26,20 +30,21 @@ const mockTrail = (
26
30
  error?: string;
27
31
  }[];
28
32
  output?: z.ZodType;
29
- provisions?: readonly ReturnType<typeof provision>[];
33
+ resources?: readonly ReturnType<typeof resource>[];
30
34
  }
31
35
  ) => ({
32
36
  blaze: noop,
37
+ contours: Object.freeze([...(overrides?.contours ?? [])]),
33
38
  crosses: Object.freeze([...(overrides?.crosses ?? [])]),
34
39
  id,
35
40
  input: z.object({ name: z.string() }),
36
41
  kind: 'trail' as const,
37
- provisions: Object.freeze([...(overrides?.provisions ?? [])]),
42
+ resources: Object.freeze([...(overrides?.resources ?? [])]),
38
43
  ...overrides,
39
44
  });
40
45
 
41
- const mockProvision = (id: string) =>
42
- provision(id, {
46
+ const mockResource = (id: string) =>
47
+ resource(id, {
43
48
  create: () => Result.ok({ id }),
44
49
  });
45
50
 
@@ -78,6 +83,17 @@ describe('validateTopo', () => {
78
83
  });
79
84
 
80
85
  describe('trail crossing', () => {
86
+ test('draft crossings are allowed in the authored graph', () => {
87
+ const app = topo('app', {
88
+ exportTrail: mockTrail('entity.export', {
89
+ crosses: ['_draft.entity.prepare'],
90
+ }),
91
+ });
92
+
93
+ const result = validateTopo(app);
94
+ expect(result.isOk()).toBe(true);
95
+ });
96
+
81
97
  test('trail crossing a non-existent trail fails', () => {
82
98
  const app = topo('app', {
83
99
  onboard: mockTrail('entity.onboard', {
@@ -149,13 +165,25 @@ describe('validateTopo', () => {
149
165
  });
150
166
  });
151
167
 
152
- describe('provision declarations', () => {
153
- test('trail declaring a registered provision passes', () => {
154
- const db = mockProvision('db.main');
168
+ describe('resource declarations', () => {
169
+ test('draft resource references are allowed in the authored graph', () => {
170
+ const db = mockResource('_draft.db.main');
171
+ const app = topo('app', {
172
+ show: mockTrail('entity.show', {
173
+ resources: [db],
174
+ }),
175
+ });
176
+
177
+ const result = validateTopo(app);
178
+ expect(result.isOk()).toBe(true);
179
+ });
180
+
181
+ test('trail declaring a registered resource passes', () => {
182
+ const db = mockResource('db.main');
155
183
  const app = topo('app', {
156
184
  db,
157
185
  show: mockTrail('entity.show', {
158
- provisions: [db],
186
+ resources: [db],
159
187
  }),
160
188
  });
161
189
 
@@ -163,11 +191,11 @@ describe('validateTopo', () => {
163
191
  expect(result.isOk()).toBe(true);
164
192
  });
165
193
 
166
- test('trail declaring a missing provision fails', () => {
167
- const db = mockProvision('db.main');
194
+ test('trail declaring a missing resource fails', () => {
195
+ const db = mockResource('db.main');
168
196
  const app = topo('app', {
169
197
  show: mockTrail('entity.show', {
170
- provisions: [db],
198
+ resources: [db],
171
199
  }),
172
200
  });
173
201
 
@@ -176,7 +204,7 @@ describe('validateTopo', () => {
176
204
 
177
205
  const issues = extractIssues(result);
178
206
  expect(issues).toHaveLength(1);
179
- expect(issues[0]?.rule).toBe('provision-exists');
207
+ expect(issues[0]?.rule).toBe('resource-exists');
180
208
  expect(issues[0]?.message).toContain('db.main');
181
209
  });
182
210
  });
@@ -275,6 +303,68 @@ describe('validateTopo', () => {
275
303
  });
276
304
  });
277
305
 
306
+ describe('contour references', () => {
307
+ test('contour referencing a registered contour passes', () => {
308
+ const user = contour(
309
+ 'user',
310
+ { id: z.string().uuid(), name: z.string() },
311
+ { identity: 'id' }
312
+ );
313
+ const post = contour(
314
+ 'post',
315
+ { authorId: user.id(), id: z.string().uuid() },
316
+ { identity: 'id' }
317
+ );
318
+
319
+ const app = topo('app', { post, user });
320
+
321
+ const result = validateTopo(app);
322
+ expect(result.isOk()).toBe(true);
323
+ });
324
+
325
+ test('contour referencing a missing contour fails', () => {
326
+ const user = contour(
327
+ 'user',
328
+ { id: z.string().uuid(), name: z.string() },
329
+ { identity: 'id' }
330
+ );
331
+ const post = contour(
332
+ 'post',
333
+ { authorId: user.id(), id: z.string().uuid() },
334
+ { identity: 'id' }
335
+ );
336
+
337
+ // Only register post, not user — the reference is dangling
338
+ const app = topo('app', { post });
339
+
340
+ const result = validateTopo(app);
341
+ expect(result.isErr()).toBe(true);
342
+
343
+ const issues = extractIssues(result);
344
+ expect(issues).toHaveLength(1);
345
+ expect(issues[0]?.rule).toBe('contour-reference-exists');
346
+ expect(issues[0]?.message).toContain('user');
347
+ });
348
+
349
+ test('draft contour references are allowed', () => {
350
+ const draftUser = contour(
351
+ '_draft.user',
352
+ { id: z.string().uuid() },
353
+ { identity: 'id' }
354
+ );
355
+ const post = contour(
356
+ 'post',
357
+ { authorId: draftUser.id(), id: z.string().uuid() },
358
+ { identity: 'id' }
359
+ );
360
+
361
+ const app = topo('app', { post });
362
+
363
+ const result = validateTopo(app);
364
+ expect(result.isOk()).toBe(true);
365
+ });
366
+ });
367
+
278
368
  describe('event origins', () => {
279
369
  test('event with non-existent origin fails', () => {
280
370
  const app = topo('app', {
@@ -301,11 +391,11 @@ describe('validateTopo', () => {
301
391
  });
302
392
 
303
393
  test('collects multiple issues', () => {
304
- const db = mockProvision('db.main');
394
+ const db = mockResource('db.main');
305
395
  const app = topo('app', {
306
396
  broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
307
- missingService: mockTrail('entity.missing-service', {
308
- provisions: [db],
397
+ missingResource: mockTrail('entity.missing-resource', {
398
+ resources: [db],
309
399
  }),
310
400
  show: mockTrail('entity.show', {
311
401
  examples: [{ input: { name: 123 }, name: 'Bad' }],
@@ -319,4 +409,203 @@ describe('validateTopo', () => {
319
409
  const issues = extractIssues(result);
320
410
  expect(issues).toHaveLength(4);
321
411
  });
412
+
413
+ describe('signal origins', () => {
414
+ test('draft signal origins are allowed in the authored graph', () => {
415
+ const app = topo('app', {
416
+ updated: mockEvent('entity.updated', ['_draft.entity.prepare']),
417
+ });
418
+
419
+ const result = validateTopo(app);
420
+ expect(result.isOk()).toBe(true);
421
+ });
422
+ });
423
+ });
424
+
425
+ describe('draft state analysis', () => {
426
+ test('detects declared draft ids', () => {
427
+ const app = topo('app', {
428
+ draftTrail: mockTrail('_draft.entity.prepare'),
429
+ });
430
+
431
+ const analysis = deriveDraftReport(app);
432
+
433
+ expect(analysis.declaredDraftIds.has('_draft.entity.prepare')).toBe(true);
434
+ expect(analysis.contaminatedIds.has('_draft.entity.prepare')).toBe(true);
435
+ expect(analysis.findings.map((finding) => finding.rule)).toContain(
436
+ 'draft-id'
437
+ );
438
+ });
439
+
440
+ test('propagates contamination through dependencies', () => {
441
+ const app = topo('app', {
442
+ exportTrail: mockTrail('entity.export', {
443
+ crosses: ['entity.prepare'],
444
+ }),
445
+ prepareTrail: mockTrail('entity.prepare', {
446
+ crosses: ['_draft.entity.store'],
447
+ }),
448
+ });
449
+
450
+ const analysis = deriveDraftReport(app);
451
+ const exportFinding = analysis.findings.find(
452
+ (finding) => finding.id === 'entity.export'
453
+ );
454
+
455
+ expect(analysis.contaminatedIds.has('entity.prepare')).toBe(true);
456
+ expect(analysis.contaminatedIds.has('entity.export')).toBe(true);
457
+ expect(exportFinding).toBeDefined();
458
+ expect(exportFinding?.rule).toBe('draft-contamination');
459
+ });
460
+
461
+ test('detects draft contour declarations and schema-reference contamination', () => {
462
+ const draftUser = contour(
463
+ '_draft.user',
464
+ {
465
+ id: z.string().uuid(),
466
+ },
467
+ { identity: 'id' }
468
+ );
469
+ const gist = contour(
470
+ 'gist',
471
+ {
472
+ id: z.string().uuid(),
473
+ ownerId: draftUser.id(),
474
+ },
475
+ { identity: 'id' }
476
+ );
477
+
478
+ const app = topo('app', {
479
+ draftUser,
480
+ gist,
481
+ });
482
+
483
+ const analysis = deriveDraftReport(app);
484
+ expect(analysis.declaredDraftIds.has('_draft.user')).toBe(true);
485
+ expect(analysis.contaminatedIds.has('gist')).toBe(true);
486
+ expect(analysis.dependencies).toContainEqual({
487
+ fromId: 'gist',
488
+ kind: 'schema-reference',
489
+ toId: '_draft.user',
490
+ });
491
+ });
492
+
493
+ test('propagates contamination through trail contour dependencies', () => {
494
+ const draftUser = contour(
495
+ '_draft.user',
496
+ {
497
+ id: z.string().uuid(),
498
+ },
499
+ { identity: 'id' }
500
+ );
501
+
502
+ const app = topo('app', {
503
+ createGist: mockTrail('gist.create', {
504
+ contours: [draftUser],
505
+ }),
506
+ draftUser,
507
+ });
508
+
509
+ const analysis = deriveDraftReport(app);
510
+ const finding = analysis.findings.find(
511
+ (entry) => entry.id === 'gist.create'
512
+ );
513
+
514
+ expect(analysis.contaminatedIds.has('gist.create')).toBe(true);
515
+ expect(finding?.via).toBe('contour');
516
+ });
517
+ });
518
+
519
+ describe('validateEstablishedTopo', () => {
520
+ test('passes for an established topo', () => {
521
+ const app = topo('app', {
522
+ show: mockTrail('entity.show'),
523
+ });
524
+
525
+ const result = validateEstablishedTopo(app);
526
+ expect(result.isOk()).toBe(true);
527
+ });
528
+
529
+ test('fails when draft declarations remain', () => {
530
+ const app = topo('app', {
531
+ draftTrail: mockTrail('_draft.entity.prepare'),
532
+ });
533
+
534
+ const result = validateEstablishedTopo(app);
535
+ expect(result.isErr()).toBe(true);
536
+
537
+ const issues = extractIssues(result);
538
+ expect(issues).toHaveLength(1);
539
+ expect(issues[0]?.rule).toBe('draft-id');
540
+ });
541
+
542
+ test('fails when established trails depend on draft state', () => {
543
+ const app = topo('app', {
544
+ exportTrail: mockTrail('entity.export', {
545
+ crosses: ['_draft.entity.prepare'],
546
+ }),
547
+ });
548
+
549
+ const result = validateEstablishedTopo(app);
550
+ expect(result.isErr()).toBe(true);
551
+
552
+ const issues = extractIssues(result);
553
+ expect(issues).toHaveLength(1);
554
+ expect(issues[0]?.rule).toBe('draft-contamination');
555
+ expect(issues[0]?.message).toContain('entity.export');
556
+ });
557
+
558
+ test('fails when authored structural validation still fails', () => {
559
+ const app = topo('app', {
560
+ exportTrail: mockTrail('entity.export', {
561
+ crosses: ['entity.missing'],
562
+ }),
563
+ });
564
+
565
+ const result = validateEstablishedTopo(app);
566
+ expect(result.isErr()).toBe(true);
567
+
568
+ const issues = extractIssues(result);
569
+ expect(issues).toHaveLength(1);
570
+ expect(issues[0]?.rule).toBe('cross-exists');
571
+ });
572
+
573
+ test('fails when resource declarations are not established in the topo', () => {
574
+ const app = topo('app', {
575
+ show: mockTrail('entity.show', {
576
+ resources: [mockResource('db.main')],
577
+ }),
578
+ });
579
+
580
+ const result = validateEstablishedTopo(app);
581
+ expect(result.isErr()).toBe(true);
582
+
583
+ const issues = extractIssues(result);
584
+ expect(issues).toHaveLength(1);
585
+ expect(issues[0]?.rule).toBe('resource-exists');
586
+ });
587
+
588
+ test('allows authoring-only example issues outside projection checks', () => {
589
+ const app = topo('app', {
590
+ show: mockTrail('entity.show', {
591
+ examples: [
592
+ {
593
+ expected: { ok: true },
594
+ input: { name: 'test' },
595
+ name: 'Has expected',
596
+ },
597
+ ],
598
+ }),
599
+ });
600
+
601
+ const result = validateEstablishedTopo(app);
602
+ expect(result.isOk()).toBe(true);
603
+ });
604
+ });
605
+
606
+ describe('isDraftId', () => {
607
+ test('recognizes the reserved draft prefix', () => {
608
+ expect(isDraftId('_draft.entity.show')).toBe(true);
609
+ expect(isDraftId('entity.show')).toBe(false);
610
+ });
322
611
  });
@@ -7,7 +7,7 @@ import { NotFoundError } from '../errors.js';
7
7
  import {
8
8
  findWorkspaceRoot,
9
9
  isInsideWorkspace,
10
- getRelativePath,
10
+ deriveRelativePath,
11
11
  } from '../workspace.js';
12
12
 
13
13
  // ---------------------------------------------------------------------------
@@ -162,22 +162,22 @@ describe('isInsideWorkspace', () => {
162
162
  });
163
163
 
164
164
  // ---------------------------------------------------------------------------
165
- // getRelativePath
165
+ // deriveRelativePath
166
166
  // ---------------------------------------------------------------------------
167
167
 
168
- describe('getRelativePath', () => {
168
+ describe('deriveRelativePath', () => {
169
169
  test('returns relative path from workspace root', () => {
170
170
  expect(
171
- getRelativePath('/project/packages/core/src/index.ts', '/project')
171
+ deriveRelativePath('/project/packages/core/src/index.ts', '/project')
172
172
  ).toBe('packages/core/src/index.ts');
173
173
  });
174
174
 
175
175
  test('returns .. segments for paths outside workspace', () => {
176
- const rel = getRelativePath('/other/file.ts', '/project');
176
+ const rel = deriveRelativePath('/other/file.ts', '/project');
177
177
  expect(rel.startsWith('..')).toBe(true);
178
178
  });
179
179
 
180
180
  test('returns empty string for the root itself', () => {
181
- expect(getRelativePath('/project', '/project')).toBe('');
181
+ expect(deriveRelativePath('/project', '/project')).toBe('');
182
182
  });
183
183
  });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ stripDefaultsFromShape,
6
+ stripDefaultWrappers,
7
+ } from '../internal/zod-wrappers.js';
8
+
9
+ describe('stripDefaultWrappers', () => {
10
+ test('drops plain default wrappers', () => {
11
+ const withDefault = z.string().default('x');
12
+ const stripped = stripDefaultWrappers(withDefault);
13
+
14
+ expect(stripped.def.type).toBe('string');
15
+ // Feeding an undefined through the stripped schema no longer
16
+ // materializes the default.
17
+ expect(stripped.safeParse().success).toBe(false);
18
+ expect(stripped.safeParse('value').success).toBe(true);
19
+ });
20
+
21
+ test('drops optional + default layers together', () => {
22
+ const schema = z.string().optional().default('x');
23
+ const stripped = stripDefaultWrappers(schema);
24
+
25
+ // Every wrapper around the underlying string is gone — optional is
26
+ // dropped on purpose because downstream `.partial()` calls re-add it.
27
+ expect(stripped.def.type).toBe('string');
28
+ expect(stripped.safeParse().success).toBe(false);
29
+ expect(stripped.safeParse('value').success).toBe(true);
30
+ });
31
+
32
+ test('preserves nullable constraints through the strip', () => {
33
+ const schema = z.string().nullable().default('x');
34
+ const stripped = stripDefaultWrappers(schema);
35
+
36
+ // Default is gone; null is still a legal input.
37
+ expect(stripped.def.type).toBe('nullable');
38
+ expect(stripped.safeParse(null).success).toBe(true);
39
+ expect(stripped.safeParse('value').success).toBe(true);
40
+ });
41
+
42
+ test('is load-bearing for .partial() on both call sites', () => {
43
+ const shape = {
44
+ author: z.string().optional().default('anon'),
45
+ title: z.string().default('untitled'),
46
+ };
47
+ const schema = z.object(shape);
48
+
49
+ // Both the store `deriveUpdateSchema` helper and the core derive-trail
50
+ // `toPartialSchema` helper compose the same pattern: extend the schema
51
+ // with the stripped shape, then call `.partial()`. Both call sites
52
+ // must produce a schema that accepts an empty object without
53
+ // re-materializing defaults.
54
+ const stripped = stripDefaultsFromShape(schema);
55
+ const partial = schema.extend(stripped).partial();
56
+
57
+ const parsed = partial.parse({});
58
+ expect(parsed).toEqual({});
59
+ expect('author' in parsed).toBe(false);
60
+ expect('title' in parsed).toBe(false);
61
+ });
62
+ });
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
  *
@@ -20,9 +36,13 @@ export const createTrailContext = (
20
36
  cwd: process.cwd(),
21
37
  env: process.env as Record<string, string | undefined>,
22
38
  requestId: Bun.randomUUIDv7(),
39
+ trace: passthroughTrace,
23
40
  ...overrides,
24
41
  } as MutableTrailContext;
25
- const lookup = overrides?.provision ?? createProvisionLookup(() => ctx);
26
- ctx.provision = lookup;
42
+ const lookup = overrides?.resource ?? createResourceLookup(() => ctx);
43
+ ctx.resource = lookup;
44
+ if (ctx.trace === undefined) {
45
+ ctx.trace = passthroughTrace;
46
+ }
27
47
  return ctx;
28
48
  };