@ontrails/core 1.0.0-beta.14 → 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 (280) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +17 -13
  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/draft.d.ts +5 -5
  21. package/dist/draft.d.ts.map +1 -1
  22. package/dist/draft.js +28 -27
  23. package/dist/draft.js.map +1 -1
  24. package/dist/errors.d.ts +62 -4
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +44 -1
  27. package/dist/errors.js.map +1 -1
  28. package/dist/execute.d.ts +26 -9
  29. package/dist/execute.d.ts.map +1 -1
  30. package/dist/execute.js +441 -20
  31. package/dist/execute.js.map +1 -1
  32. package/dist/fire.d.ts +43 -0
  33. package/dist/fire.d.ts.map +1 -0
  34. package/dist/fire.js +185 -0
  35. package/dist/fire.js.map +1 -0
  36. package/dist/index.d.ts +31 -20
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +23 -13
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/cross-batch.d.ts +44 -0
  41. package/dist/internal/cross-batch.d.ts.map +1 -0
  42. package/dist/internal/cross-batch.js +58 -0
  43. package/dist/internal/cross-batch.js.map +1 -0
  44. package/dist/internal/fork-ctx.d.ts +41 -0
  45. package/dist/internal/fork-ctx.d.ts.map +1 -0
  46. package/dist/internal/fork-ctx.js +47 -0
  47. package/dist/internal/fork-ctx.js.map +1 -0
  48. package/dist/internal/signal-ref.d.ts +15 -0
  49. package/dist/internal/signal-ref.d.ts.map +1 -0
  50. package/dist/internal/signal-ref.js +41 -0
  51. package/dist/internal/signal-ref.js.map +1 -0
  52. package/dist/internal/topo-saves.d.ts +8 -2
  53. package/dist/internal/topo-saves.d.ts.map +1 -1
  54. package/dist/internal/topo-saves.js +162 -23
  55. package/dist/internal/topo-saves.js.map +1 -1
  56. package/dist/internal/topo-snapshots.d.ts +57 -0
  57. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  58. package/dist/internal/topo-snapshots.js +372 -0
  59. package/dist/internal/topo-snapshots.js.map +1 -0
  60. package/dist/internal/topo-store-read.d.ts +23 -20
  61. package/dist/internal/topo-store-read.d.ts.map +1 -1
  62. package/dist/internal/topo-store-read.js +82 -83
  63. package/dist/internal/topo-store-read.js.map +1 -1
  64. package/dist/internal/topo-store.d.ts +19 -5
  65. package/dist/internal/topo-store.d.ts.map +1 -1
  66. package/dist/internal/topo-store.js +174 -124
  67. package/dist/internal/topo-store.js.map +1 -1
  68. package/dist/internal/tracing.d.ts +98 -0
  69. package/dist/internal/tracing.d.ts.map +1 -0
  70. package/dist/internal/tracing.js +102 -0
  71. package/dist/internal/tracing.js.map +1 -0
  72. package/dist/internal/trails-db.d.ts +2 -2
  73. package/dist/internal/trails-db.d.ts.map +1 -1
  74. package/dist/internal/trails-db.js +8 -8
  75. package/dist/internal/trails-db.js.map +1 -1
  76. package/dist/internal/zod-wrappers.d.ts +40 -0
  77. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  78. package/dist/internal/zod-wrappers.js +59 -0
  79. package/dist/internal/zod-wrappers.js.map +1 -0
  80. package/dist/layer.d.ts +22 -5
  81. package/dist/layer.d.ts.map +1 -1
  82. package/dist/layer.js +20 -3
  83. package/dist/layer.js.map +1 -1
  84. package/dist/path-security.d.ts +1 -1
  85. package/dist/path-security.d.ts.map +1 -1
  86. package/dist/path-security.js +1 -1
  87. package/dist/path-security.js.map +1 -1
  88. package/dist/resilience.d.ts +1 -1
  89. package/dist/resilience.d.ts.map +1 -1
  90. package/dist/resilience.js +3 -3
  91. package/dist/resilience.js.map +1 -1
  92. package/dist/resource-config.d.ts +22 -0
  93. package/dist/resource-config.d.ts.map +1 -0
  94. package/dist/resource-config.js +210 -0
  95. package/dist/resource-config.js.map +1 -0
  96. package/dist/resource.d.ts +74 -0
  97. package/dist/resource.d.ts.map +1 -0
  98. package/dist/resource.js +61 -0
  99. package/dist/resource.js.map +1 -0
  100. package/dist/run.d.ts +4 -3
  101. package/dist/run.d.ts.map +1 -1
  102. package/dist/run.js +5 -4
  103. package/dist/run.js.map +1 -1
  104. package/dist/serialization.d.ts.map +1 -1
  105. package/dist/serialization.js +2 -1
  106. package/dist/serialization.js.map +1 -1
  107. package/dist/store/accessor-protocol.d.ts +51 -0
  108. package/dist/store/accessor-protocol.d.ts.map +1 -0
  109. package/dist/store/accessor-protocol.js +2 -0
  110. package/dist/store/accessor-protocol.js.map +1 -0
  111. package/dist/store/index.d.ts +2 -0
  112. package/dist/store/index.d.ts.map +1 -0
  113. package/dist/store/index.js +2 -0
  114. package/dist/store/index.js.map +1 -0
  115. package/dist/surface-filter.d.ts +13 -0
  116. package/dist/surface-filter.d.ts.map +1 -0
  117. package/dist/surface-filter.js +76 -0
  118. package/dist/surface-filter.js.map +1 -0
  119. package/dist/topo-store.d.ts +36 -22
  120. package/dist/topo-store.d.ts.map +1 -1
  121. package/dist/topo-store.js +215 -71
  122. package/dist/topo-store.js.map +1 -1
  123. package/dist/topo.d.ts +23 -9
  124. package/dist/topo.d.ts.map +1 -1
  125. package/dist/topo.js +175 -46
  126. package/dist/topo.js.map +1 -1
  127. package/dist/trail.d.ts +86 -19
  128. package/dist/trail.d.ts.map +1 -1
  129. package/dist/trail.js +59 -4
  130. package/dist/trail.js.map +1 -1
  131. package/dist/trails/derive-trail.d.ts +66 -0
  132. package/dist/trails/derive-trail.d.ts.map +1 -0
  133. package/dist/trails/derive-trail.js +378 -0
  134. package/dist/trails/derive-trail.js.map +1 -0
  135. package/dist/trails/index.d.ts +5 -0
  136. package/dist/trails/index.d.ts.map +1 -0
  137. package/dist/trails/index.js +3 -0
  138. package/dist/trails/index.js.map +1 -0
  139. package/dist/trails/ingest.d.ts +24 -0
  140. package/dist/trails/ingest.d.ts.map +1 -0
  141. package/dist/trails/ingest.js +63 -0
  142. package/dist/trails/ingest.js.map +1 -0
  143. package/dist/transport-error-map.d.ts +106 -0
  144. package/dist/transport-error-map.d.ts.map +1 -0
  145. package/dist/transport-error-map.js +24 -0
  146. package/dist/transport-error-map.js.map +1 -0
  147. package/dist/type-checks.test-d.d.ts +71 -0
  148. package/dist/type-checks.test-d.d.ts.map +1 -0
  149. package/dist/type-checks.test-d.js +12 -0
  150. package/dist/type-checks.test-d.js.map +1 -0
  151. package/dist/type-utils.d.ts +11 -2
  152. package/dist/type-utils.d.ts.map +1 -1
  153. package/dist/type-utils.js.map +1 -1
  154. package/dist/types.d.ts +113 -7
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/validate-established-topo.js +2 -2
  158. package/dist/validate-established-topo.js.map +1 -1
  159. package/dist/validate-topo.d.ts.map +1 -1
  160. package/dist/validate-topo.js +24 -7
  161. package/dist/validate-topo.js.map +1 -1
  162. package/dist/workspace.d.ts +1 -1
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +1 -1
  165. package/dist/workspace.js.map +1 -1
  166. package/package.json +8 -2
  167. package/src/__tests__/branded.test.ts +5 -5
  168. package/src/__tests__/context.test.ts +3 -3
  169. package/src/__tests__/contour.test.ts +263 -0
  170. package/src/__tests__/derive-trail.test.ts +668 -0
  171. package/src/__tests__/errors.test.ts +118 -0
  172. package/src/__tests__/execute.test.ts +1179 -141
  173. package/src/__tests__/fire.test.ts +1056 -0
  174. package/src/__tests__/fork-ctx.test.ts +83 -0
  175. package/src/__tests__/ingest.test.ts +191 -0
  176. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  177. package/src/__tests__/path-security.test.ts +7 -7
  178. package/src/__tests__/resilience.test.ts +8 -8
  179. package/src/__tests__/resource.test.ts +204 -0
  180. package/src/__tests__/run.test.ts +13 -13
  181. package/src/__tests__/service-config.test.ts +28 -32
  182. package/src/__tests__/surface-filter.test.ts +196 -0
  183. package/src/__tests__/topo-store-read.test.ts +41 -36
  184. package/src/__tests__/topo-store.test.ts +493 -93
  185. package/src/__tests__/topo.test.ts +350 -54
  186. package/src/__tests__/trail.test.ts +189 -24
  187. package/src/__tests__/trails-db.test.ts +32 -25
  188. package/src/__tests__/transport-error-map.test.ts +82 -0
  189. package/src/__tests__/type-utils.test.ts +29 -1
  190. package/src/__tests__/validate-topo.test.ts +147 -25
  191. package/src/__tests__/workspace.test.ts +6 -6
  192. package/src/__tests__/zod-wrappers.test.ts +62 -0
  193. package/src/branded.ts +1 -1
  194. package/src/context.ts +24 -4
  195. package/src/contour.ts +344 -0
  196. package/src/cross-schema.ts +36 -0
  197. package/src/draft.ts +56 -52
  198. package/src/errors.ts +62 -18
  199. package/src/execute.ts +840 -29
  200. package/src/fire.ts +295 -0
  201. package/src/index.ts +116 -29
  202. package/src/internal/cross-batch.ts +77 -0
  203. package/src/internal/fork-ctx.ts +69 -0
  204. package/src/internal/signal-ref.ts +87 -0
  205. package/src/internal/topo-snapshots.ts +517 -0
  206. package/src/internal/topo-store-read.ts +140 -132
  207. package/src/internal/topo-store.ts +291 -186
  208. package/src/internal/tracing.ts +187 -0
  209. package/src/internal/trails-db.ts +9 -11
  210. package/src/internal/zod-wrappers.ts +78 -0
  211. package/src/layer.ts +50 -0
  212. package/src/path-security.ts +1 -1
  213. package/src/resilience.ts +3 -3
  214. package/src/{provision-config.ts → resource-config.ts} +109 -123
  215. package/src/resource.ts +155 -0
  216. package/src/run.ts +5 -4
  217. package/src/serialization.ts +2 -0
  218. package/src/store/accessor-protocol.ts +56 -0
  219. package/src/store/index.ts +4 -0
  220. package/src/surface-filter.ts +182 -0
  221. package/src/topo-store.ts +313 -104
  222. package/src/topo.ts +328 -57
  223. package/src/trail.ts +199 -31
  224. package/src/trails/derive-trail.ts +842 -0
  225. package/src/trails/index.ts +9 -0
  226. package/src/trails/ingest.ts +146 -0
  227. package/src/transport-error-map.ts +56 -0
  228. package/src/type-checks.test-d.ts +106 -0
  229. package/src/type-utils.ts +17 -2
  230. package/src/types.ts +148 -11
  231. package/src/validate-established-topo.ts +2 -2
  232. package/src/validate-topo.ts +32 -8
  233. package/src/workspace.ts +1 -1
  234. package/tsconfig.tests.json +10 -0
  235. package/tsconfig.tsbuildinfo +1 -1
  236. package/dist/adapters.d.ts +0 -39
  237. package/dist/adapters.d.ts.map +0 -1
  238. package/dist/adapters.js +0 -2
  239. package/dist/adapters.js.map +0 -1
  240. package/dist/dispatch.d.ts +0 -27
  241. package/dist/dispatch.d.ts.map +0 -1
  242. package/dist/dispatch.js +0 -34
  243. package/dist/dispatch.js.map +0 -1
  244. package/dist/event.d.ts +0 -8
  245. package/dist/event.d.ts.map +0 -1
  246. package/dist/event.js +0 -7
  247. package/dist/event.js.map +0 -1
  248. package/dist/gate.d.ts +0 -17
  249. package/dist/gate.d.ts.map +0 -1
  250. package/dist/gate.js +0 -21
  251. package/dist/gate.js.map +0 -1
  252. package/dist/health.d.ts +0 -18
  253. package/dist/health.d.ts.map +0 -1
  254. package/dist/health.js +0 -5
  255. package/dist/health.js.map +0 -1
  256. package/dist/job.d.ts +0 -24
  257. package/dist/job.d.ts.map +0 -1
  258. package/dist/job.js +0 -17
  259. package/dist/job.js.map +0 -1
  260. package/dist/provision-config.d.ts +0 -22
  261. package/dist/provision-config.d.ts.map +0 -1
  262. package/dist/provision-config.js +0 -210
  263. package/dist/provision-config.js.map +0 -1
  264. package/dist/provision.d.ts +0 -71
  265. package/dist/provision.d.ts.map +0 -1
  266. package/dist/provision.js +0 -56
  267. package/dist/provision.js.map +0 -1
  268. package/dist/service-config.d.ts +0 -22
  269. package/dist/service-config.d.ts.map +0 -1
  270. package/dist/service-config.js +0 -210
  271. package/dist/service-config.js.map +0 -1
  272. package/dist/service.d.ts +0 -71
  273. package/dist/service.d.ts.map +0 -1
  274. package/dist/service.js +0 -56
  275. package/dist/service.js.map +0 -1
  276. package/src/__tests__/service.test.ts +0 -197
  277. package/src/event.ts +0 -15
  278. package/src/gate.ts +0 -44
  279. package/src/internal/topo-saves.ts +0 -429
  280. package/src/provision.ts +0 -148
@@ -2,9 +2,10 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import { analyzeDraftState, isDraftId } from '../draft.js';
5
+ import { deriveDraftReport, isDraftId } from '../draft.js';
6
+ import { contour } from '../contour.js';
6
7
  import { validateEstablishedTopo } from '../validate-established-topo.js';
7
- import { provision } from '../provision.js';
8
+ import { resource } from '../resource.js';
8
9
  import { Result } from '../result.js';
9
10
  import { topo } from '../topo.js';
10
11
  import type { TopoIssue } from '../validate-topo.js';
@@ -21,6 +22,7 @@ const mockTrail = (
21
22
  id: string,
22
23
  overrides?: {
23
24
  crosses?: readonly string[];
25
+ contours?: readonly ReturnType<typeof contour>[];
24
26
  examples?: readonly {
25
27
  name: string;
26
28
  input: unknown;
@@ -28,20 +30,21 @@ const mockTrail = (
28
30
  error?: string;
29
31
  }[];
30
32
  output?: z.ZodType;
31
- provisions?: readonly ReturnType<typeof provision>[];
33
+ resources?: readonly ReturnType<typeof resource>[];
32
34
  }
33
35
  ) => ({
34
36
  blaze: noop,
37
+ contours: Object.freeze([...(overrides?.contours ?? [])]),
35
38
  crosses: Object.freeze([...(overrides?.crosses ?? [])]),
36
39
  id,
37
40
  input: z.object({ name: z.string() }),
38
41
  kind: 'trail' as const,
39
- provisions: Object.freeze([...(overrides?.provisions ?? [])]),
42
+ resources: Object.freeze([...(overrides?.resources ?? [])]),
40
43
  ...overrides,
41
44
  });
42
45
 
43
- const mockProvision = (id: string) =>
44
- provision(id, {
46
+ const mockResource = (id: string) =>
47
+ resource(id, {
45
48
  create: () => Result.ok({ id }),
46
49
  });
47
50
 
@@ -162,12 +165,12 @@ describe('validateTopo', () => {
162
165
  });
163
166
  });
164
167
 
165
- describe('provision declarations', () => {
166
- test('draft provision references are allowed in the authored graph', () => {
167
- const db = mockProvision('_draft.db.main');
168
+ describe('resource declarations', () => {
169
+ test('draft resource references are allowed in the authored graph', () => {
170
+ const db = mockResource('_draft.db.main');
168
171
  const app = topo('app', {
169
172
  show: mockTrail('entity.show', {
170
- provisions: [db],
173
+ resources: [db],
171
174
  }),
172
175
  });
173
176
 
@@ -175,12 +178,12 @@ describe('validateTopo', () => {
175
178
  expect(result.isOk()).toBe(true);
176
179
  });
177
180
 
178
- test('trail declaring a registered provision passes', () => {
179
- const db = mockProvision('db.main');
181
+ test('trail declaring a registered resource passes', () => {
182
+ const db = mockResource('db.main');
180
183
  const app = topo('app', {
181
184
  db,
182
185
  show: mockTrail('entity.show', {
183
- provisions: [db],
186
+ resources: [db],
184
187
  }),
185
188
  });
186
189
 
@@ -188,11 +191,11 @@ describe('validateTopo', () => {
188
191
  expect(result.isOk()).toBe(true);
189
192
  });
190
193
 
191
- test('trail declaring a missing provision fails', () => {
192
- const db = mockProvision('db.main');
194
+ test('trail declaring a missing resource fails', () => {
195
+ const db = mockResource('db.main');
193
196
  const app = topo('app', {
194
197
  show: mockTrail('entity.show', {
195
- provisions: [db],
198
+ resources: [db],
196
199
  }),
197
200
  });
198
201
 
@@ -201,7 +204,7 @@ describe('validateTopo', () => {
201
204
 
202
205
  const issues = extractIssues(result);
203
206
  expect(issues).toHaveLength(1);
204
- expect(issues[0]?.rule).toBe('provision-exists');
207
+ expect(issues[0]?.rule).toBe('resource-exists');
205
208
  expect(issues[0]?.message).toContain('db.main');
206
209
  });
207
210
  });
@@ -300,6 +303,68 @@ describe('validateTopo', () => {
300
303
  });
301
304
  });
302
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
+
303
368
  describe('event origins', () => {
304
369
  test('event with non-existent origin fails', () => {
305
370
  const app = topo('app', {
@@ -326,11 +391,11 @@ describe('validateTopo', () => {
326
391
  });
327
392
 
328
393
  test('collects multiple issues', () => {
329
- const db = mockProvision('db.main');
394
+ const db = mockResource('db.main');
330
395
  const app = topo('app', {
331
396
  broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
332
- missingService: mockTrail('entity.missing-service', {
333
- provisions: [db],
397
+ missingResource: mockTrail('entity.missing-resource', {
398
+ resources: [db],
334
399
  }),
335
400
  show: mockTrail('entity.show', {
336
401
  examples: [{ input: { name: 123 }, name: 'Bad' }],
@@ -363,7 +428,7 @@ describe('draft state analysis', () => {
363
428
  draftTrail: mockTrail('_draft.entity.prepare'),
364
429
  });
365
430
 
366
- const analysis = analyzeDraftState(app);
431
+ const analysis = deriveDraftReport(app);
367
432
 
368
433
  expect(analysis.declaredDraftIds.has('_draft.entity.prepare')).toBe(true);
369
434
  expect(analysis.contaminatedIds.has('_draft.entity.prepare')).toBe(true);
@@ -382,7 +447,7 @@ describe('draft state analysis', () => {
382
447
  }),
383
448
  });
384
449
 
385
- const analysis = analyzeDraftState(app);
450
+ const analysis = deriveDraftReport(app);
386
451
  const exportFinding = analysis.findings.find(
387
452
  (finding) => finding.id === 'entity.export'
388
453
  );
@@ -392,6 +457,63 @@ describe('draft state analysis', () => {
392
457
  expect(exportFinding).toBeDefined();
393
458
  expect(exportFinding?.rule).toBe('draft-contamination');
394
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
+ });
395
517
  });
396
518
 
397
519
  describe('validateEstablishedTopo', () => {
@@ -448,10 +570,10 @@ describe('validateEstablishedTopo', () => {
448
570
  expect(issues[0]?.rule).toBe('cross-exists');
449
571
  });
450
572
 
451
- test('fails when provision declarations are not established in the topo', () => {
573
+ test('fails when resource declarations are not established in the topo', () => {
452
574
  const app = topo('app', {
453
575
  show: mockTrail('entity.show', {
454
- provisions: [mockProvision('db.main')],
576
+ resources: [mockResource('db.main')],
455
577
  }),
456
578
  });
457
579
 
@@ -460,7 +582,7 @@ describe('validateEstablishedTopo', () => {
460
582
 
461
583
  const issues = extractIssues(result);
462
584
  expect(issues).toHaveLength(1);
463
- expect(issues[0]?.rule).toBe('provision-exists');
585
+ expect(issues[0]?.rule).toBe('resource-exists');
464
586
  });
465
587
 
466
588
  test('allows authoring-only example issues outside projection checks', () => {
@@ -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
  };