@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,37 +2,66 @@ import { describe, test, expect } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
+ import { contour } from '../contour.js';
5
6
  import { ValidationError } from '../errors.js';
6
- import { provision } from '../provision.js';
7
+ import {
8
+ attachLateBoundSignalRef,
9
+ cloneSignalWithId,
10
+ } from '../internal/signal-ref.js';
11
+ import { resource } from '../resource.js';
7
12
  import { Result } from '../result.js';
13
+ import { signal } from '../signal.js';
14
+ import { trail } from '../trail.js';
8
15
  import { topo } from '../topo.js';
9
16
 
10
17
  // ---------------------------------------------------------------------------
11
18
  // Mock factories
12
19
  // ---------------------------------------------------------------------------
13
20
 
14
- // oxlint-disable-next-line require-await -- satisfies async interface without needing await
15
- const noop = async () => Result.ok();
21
+ const mockContour = (name: string) =>
22
+ contour(
23
+ name,
24
+ {
25
+ id: z.string().uuid(),
26
+ value: z.string(),
27
+ },
28
+ { identity: 'id' }
29
+ );
30
+
31
+ const mockTrail = (
32
+ id: string,
33
+ crosses?: readonly string[],
34
+ contours?: readonly ReturnType<typeof mockContour>[]
35
+ ) =>
36
+ trail(id, {
37
+ blaze: () => Result.ok({ y: 0 }),
38
+ contours,
39
+ crosses,
40
+ input: z.object({ x: z.number() }),
41
+ output: z.object({ y: z.number() }),
42
+ });
16
43
 
17
- const mockTrail = (id: string, crosses?: readonly string[]) => ({
18
- blaze: noop,
19
- crosses: Object.freeze([...(crosses ?? [])]),
20
- id,
21
- input: z.object({ x: z.number() }),
22
- kind: 'trail' as const,
23
- output: z.object({ y: z.number() }),
24
- });
44
+ const mockEvent = (id: string) =>
45
+ signal(id, {
46
+ payload: z.object({ payload: z.string() }),
47
+ });
25
48
 
26
- const mockEvent = (id: string) => ({
27
- id,
28
- kind: 'signal' as const,
29
- payload: z.object({ payload: z.string() }),
30
- });
49
+ const mockSignalConsumer = (on: readonly ReturnType<typeof mockEvent>[]) =>
50
+ trail('notify.users', {
51
+ blaze: () => Result.ok({ ok: true }),
52
+ input: z.object({ payload: z.string() }),
53
+ on,
54
+ output: z.object({ ok: z.boolean() }),
55
+ });
31
56
 
32
- const mockProvision = (id: string) =>
33
- provision(id, {
57
+ const mockResource = (
58
+ id: string,
59
+ signals?: readonly ReturnType<typeof mockEvent>[]
60
+ ) =>
61
+ resource(id, {
34
62
  create: () => Result.ok({ id }),
35
- description: `${id} provision`,
63
+ description: `${id} resource`,
64
+ signals,
36
65
  });
37
66
 
38
67
  // ---------------------------------------------------------------------------
@@ -40,7 +69,21 @@ const mockProvision = (id: string) =>
40
69
  // ---------------------------------------------------------------------------
41
70
 
42
71
  describe('topo', () => {
43
- describe('collection', () => {
72
+ describe('identity', () => {
73
+ test('preserves object identity metadata', () => {
74
+ const t = topo({
75
+ description: 'Demo topo',
76
+ name: 'my-app',
77
+ version: '1.2.3',
78
+ });
79
+
80
+ expect(t.name).toBe('my-app');
81
+ expect(t.version).toBe('1.2.3');
82
+ expect(t.description).toBe('Demo topo');
83
+ });
84
+ });
85
+
86
+ describe('trail collection', () => {
44
87
  test('returns Topo with name', () => {
45
88
  const t = topo('my-app');
46
89
  expect(t.name).toBe('my-app');
@@ -57,7 +100,7 @@ describe('topo', () => {
57
100
  test('auto-scans exports by kind discriminant', () => {
58
101
  const mod = {
59
102
  event1: mockEvent('e1'),
60
- provision1: mockProvision('s1'),
103
+ resource1: mockResource('s1'),
61
104
  trail1: mockTrail('t1'),
62
105
  trail2: mockTrail('t2', ['t1']),
63
106
  };
@@ -65,7 +108,7 @@ describe('topo', () => {
65
108
 
66
109
  expect(t.trails.size).toBe(2);
67
110
  expect(t.signals.size).toBe(1);
68
- expect(t.provisions.size).toBe(1);
111
+ expect(t.resources.size).toBe(1);
69
112
  });
70
113
 
71
114
  test('collects from multiple modules', () => {
@@ -90,7 +133,7 @@ describe('topo', () => {
90
133
 
91
134
  expect(t.trails.size).toBe(1);
92
135
  expect(t.signals.size).toBe(0);
93
- expect(t.provisions.size).toBe(0);
136
+ expect(t.resources.size).toBe(0);
94
137
  });
95
138
 
96
139
  test('trail with crossings registers correctly', () => {
@@ -101,13 +144,212 @@ describe('topo', () => {
101
144
  const registered = t.trails.get('trail-1');
102
145
  expect(registered?.crosses).toEqual(['trail-2']);
103
146
  });
147
+ });
104
148
 
105
- test('collects provisions from modules', () => {
106
- const mod = { db: mockProvision('db.main') };
149
+ describe('resource and signal collection', () => {
150
+ test('collects resources from modules', () => {
151
+ const mod = { db: mockResource('db.main') };
107
152
  const t = topo('app', mod);
108
153
 
109
- expect(t.provisions.size).toBe(1);
110
- expect(t.provisions.get('db.main')).toBe(mod.db);
154
+ expect(t.resources.size).toBe(1);
155
+ expect(t.resources.get('db.main')).toBe(mod.db);
156
+ });
157
+
158
+ test('registers signals declared on resources into the topo graph', () => {
159
+ const usersCreated = mockEvent('users.created');
160
+ const t = topo('app', {
161
+ db: mockResource('db.main', [usersCreated]),
162
+ });
163
+
164
+ expect(t.signals.size).toBe(1);
165
+ expect(t.listSignals()).toContain(usersCreated);
166
+ });
167
+
168
+ test('resolves late-bound store signal refs from resource-scoped signals', () => {
169
+ const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
170
+ kind: 'store-derived',
171
+ token: 'users-created',
172
+ });
173
+ const consumer = mockSignalConsumer([authored]);
174
+ const scoped = cloneSignalWithId(authored, 'identity:users.created');
175
+ const t = topo('app', {
176
+ consumer,
177
+ identity: mockResource('identity', [scoped]),
178
+ });
179
+
180
+ expect(t.get('notify.users')?.on).toEqual(['identity:users.created']);
181
+ expect(t.listSignals().map((s) => s.id)).toContain(
182
+ 'identity:users.created'
183
+ );
184
+ });
185
+
186
+ test('preserves canonical scoped signal ids across multi-binding stores', () => {
187
+ // Regression test: the same store definition bound under two resources
188
+ // (identity + billing). A trail that registers under one resource and
189
+ // another that registers under the other must resolve to the distinct
190
+ // canonical scoped ids, not collide on a shared late-bound token.
191
+ const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
192
+ kind: 'store-derived',
193
+ token: 'users-created-multi',
194
+ });
195
+ const identityScoped = cloneSignalWithId(
196
+ authored,
197
+ 'identity:users.created'
198
+ );
199
+ const billingScoped = cloneSignalWithId(
200
+ authored,
201
+ 'billing:users.created'
202
+ );
203
+
204
+ const identityConsumer = trail('notify.identity-users', {
205
+ blaze: () => Result.ok({ ok: true }),
206
+ input: z.object({ payload: z.string() }),
207
+ on: [identityScoped],
208
+ output: z.object({ ok: z.boolean() }),
209
+ });
210
+ const billingConsumer = trail('notify.billing-users', {
211
+ blaze: () => Result.ok({ ok: true }),
212
+ input: z.object({ payload: z.string() }),
213
+ on: [billingScoped],
214
+ output: z.object({ ok: z.boolean() }),
215
+ });
216
+
217
+ const t = topo('app', {
218
+ billing: mockResource('billing', [billingScoped]),
219
+ billingConsumer,
220
+ identity: mockResource('identity', [identityScoped]),
221
+ identityConsumer,
222
+ });
223
+
224
+ expect(t.get('notify.identity-users')?.on).toEqual([
225
+ 'identity:users.created',
226
+ ]);
227
+ expect(t.get('notify.billing-users')?.on).toEqual([
228
+ 'billing:users.created',
229
+ ]);
230
+ });
231
+
232
+ test('preserves canonical scoped signal ids when resource ids contain dots', () => {
233
+ // Regression test: the canonical-scope predicate previously disallowed
234
+ // dots inside the scope segment, which collapsed dotted resource ids
235
+ // like `demo.store` and `other.store` back to their shared late-bound
236
+ // token. With `:` prohibited inside resource ids, the scope is
237
+ // unambiguously everything before the first `:`, so dotted scopes must
238
+ // resolve to distinct canonical ids.
239
+ const authored = attachLateBoundSignalRef(mockEvent('gists.created'), {
240
+ kind: 'store-derived',
241
+ token: 'gists-created-dotted',
242
+ });
243
+ const demoScoped = cloneSignalWithId(
244
+ authored,
245
+ 'demo.store:gists.created'
246
+ );
247
+ const otherScoped = cloneSignalWithId(
248
+ authored,
249
+ 'other.store:gists.created'
250
+ );
251
+
252
+ const demoConsumer = trail('notify.demo-gists', {
253
+ blaze: () => Result.ok({ ok: true }),
254
+ input: z.object({ payload: z.string() }),
255
+ on: [demoScoped],
256
+ output: z.object({ ok: z.boolean() }),
257
+ });
258
+ const otherConsumer = trail('notify.other-gists', {
259
+ blaze: () => Result.ok({ ok: true }),
260
+ input: z.object({ payload: z.string() }),
261
+ on: [otherScoped],
262
+ output: z.object({ ok: z.boolean() }),
263
+ });
264
+
265
+ const t = topo('app', {
266
+ 'demo.store': mockResource('demo.store', [demoScoped]),
267
+ demoConsumer,
268
+ 'other.store': mockResource('other.store', [otherScoped]),
269
+ otherConsumer,
270
+ });
271
+
272
+ expect(t.get('notify.demo-gists')?.on).toEqual([
273
+ 'demo.store:gists.created',
274
+ ]);
275
+ expect(t.get('notify.other-gists')?.on).toEqual([
276
+ 'other.store:gists.created',
277
+ ]);
278
+ });
279
+
280
+ test('markerizes late-bound ids containing `:` but not in canonical scoped form', () => {
281
+ // Guard regression: `normalizeSignalRef` previously passed any id
282
+ // containing `:` through unchanged, which would let a non-canonical
283
+ // late-bound id like `foo:bar` slip past markerization and then fail
284
+ // to resolve during topo finalization. The strict predicate should
285
+ // still rewrite such ids onto the late-bound marker path, while
286
+ // leaving canonical `<scope>:<table>.<event>` ids alone.
287
+ const nonCanonical = attachLateBoundSignalRef(mockEvent('foo:bar'), {
288
+ kind: 'store-derived',
289
+ token: 'non-canonical',
290
+ });
291
+ const nonCanonicalConsumer = trail('notify.non-canonical', {
292
+ blaze: () => Result.ok({ ok: true }),
293
+ input: z.object({ payload: z.string() }),
294
+ on: [nonCanonical],
295
+ output: z.object({ ok: z.boolean() }),
296
+ });
297
+ expect(nonCanonicalConsumer.on).not.toEqual(['foo:bar']);
298
+
299
+ const canonical = attachLateBoundSignalRef(mockEvent('users.created'), {
300
+ kind: 'store-derived',
301
+ token: 'canonical',
302
+ });
303
+ const scoped = cloneSignalWithId(canonical, 'identity:users.created');
304
+ const canonicalConsumer = trail('notify.canonical', {
305
+ blaze: () => Result.ok({ ok: true }),
306
+ input: z.object({ payload: z.string() }),
307
+ on: [scoped],
308
+ output: z.object({ ok: z.boolean() }),
309
+ });
310
+ expect(canonicalConsumer.on).toEqual(['identity:users.created']);
311
+ });
312
+
313
+ test('rejects ambiguous late-bound store signal refs', () => {
314
+ const authored = attachLateBoundSignalRef(mockEvent('users.created'), {
315
+ kind: 'store-derived',
316
+ token: 'users-created',
317
+ });
318
+ const consumer = mockSignalConsumer([authored]);
319
+
320
+ expect(() =>
321
+ topo('app', {
322
+ billing: mockResource('billing', [
323
+ cloneSignalWithId(authored, 'billing:users.created'),
324
+ ]),
325
+ consumer,
326
+ identity: mockResource('identity', [
327
+ cloneSignalWithId(authored, 'identity:users.created'),
328
+ ]),
329
+ })
330
+ ).toThrow(
331
+ 'Trail "notify.users" references late-bound signal "users.created" but it resolves to multiple bound resource signals'
332
+ );
333
+ });
334
+ });
335
+
336
+ describe('contour collection', () => {
337
+ test('collects contours exported directly from modules', () => {
338
+ const user = mockContour('user');
339
+ const t = topo('app', { user });
340
+
341
+ expect(t.contours.size).toBe(1);
342
+ expect(t.getContour('user')).toBe(user);
343
+ });
344
+
345
+ test('registers contours declared on trails into the topo graph', () => {
346
+ const user = mockContour('user');
347
+ const t = topo('app', {
348
+ createUser: mockTrail('user.create', [], [user]),
349
+ });
350
+
351
+ expect(t.contours.size).toBe(1);
352
+ expect(t.getContour('user')).toBe(user);
111
353
  });
112
354
  });
113
355
 
@@ -132,13 +374,23 @@ describe('topo', () => {
132
374
  );
133
375
  });
134
376
 
135
- test('rejects duplicate provision IDs', () => {
136
- const mod1 = { a: mockProvision('dup') };
137
- const mod2 = { b: mockProvision('dup') };
377
+ test('rejects duplicate resource IDs', () => {
378
+ const mod1 = { a: mockResource('dup') };
379
+ const mod2 = { b: mockResource('dup') };
380
+
381
+ expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
382
+ expect(() => topo('app', mod1, mod2)).toThrow(
383
+ 'Duplicate resource ID: "dup"'
384
+ );
385
+ });
386
+
387
+ test('rejects duplicate contour names', () => {
388
+ const mod1 = { a: mockContour('user') };
389
+ const mod2 = { b: mockContour('user') };
138
390
 
139
391
  expect(() => topo('app', mod1, mod2)).toThrow(ValidationError);
140
392
  expect(() => topo('app', mod1, mod2)).toThrow(
141
- 'Duplicate provision ID: "dup"'
393
+ 'Duplicate contour name: "user"'
142
394
  );
143
395
  });
144
396
  });
@@ -162,26 +414,42 @@ describe('topo accessors', () => {
162
414
  expect(app.count).toBe(1);
163
415
  });
164
416
 
165
- test('provisionCount returns number of provisions', () => {
166
- const db = mockProvision('db.main');
167
- const cache = mockProvision('cache.main');
417
+ test('resourceCount returns number of resources', () => {
418
+ const db = mockResource('db.main');
419
+ const cache = mockResource('cache.main');
168
420
  const app = topo('test', { cache, db });
169
- expect(app.provisionCount).toBe(2);
421
+ expect(app.resourceCount).toBe(2);
422
+ });
423
+
424
+ test('contourCount returns number of contours', () => {
425
+ const gist = mockContour('gist');
426
+ const user = mockContour('user');
427
+ const app = topo('test', { gist, user });
428
+ expect(app.contourCount).toBe(2);
170
429
  });
171
430
 
172
431
  test('empty topo has zero count and empty ids', () => {
173
432
  const app = topo('empty');
174
433
  expect(app.count).toBe(0);
434
+ expect(app.contourCount).toBe(0);
435
+ expect(app.contourIds()).toEqual([]);
175
436
  expect(app.ids()).toEqual([]);
176
- expect(app.provisionCount).toBe(0);
177
- expect(app.provisionIds()).toEqual([]);
437
+ expect(app.resourceCount).toBe(0);
438
+ expect(app.resourceIds()).toEqual([]);
178
439
  });
179
440
 
180
- test('provisionIds() returns all provision IDs', () => {
181
- const db = mockProvision('db.main');
182
- const cache = mockProvision('cache.main');
441
+ test('resourceIds() returns all resource IDs', () => {
442
+ const db = mockResource('db.main');
443
+ const cache = mockResource('cache.main');
183
444
  const app = topo('test', { cache, db });
184
- expect(app.provisionIds().toSorted()).toEqual(['cache.main', 'db.main']);
445
+ expect(app.resourceIds().toSorted()).toEqual(['cache.main', 'db.main']);
446
+ });
447
+
448
+ test('contourIds() returns all contour names', () => {
449
+ const gist = mockContour('gist');
450
+ const user = mockContour('user');
451
+ const app = topo('test', { gist, user });
452
+ expect(app.contourIds().toSorted()).toEqual(['gist', 'user']);
185
453
  });
186
454
  });
187
455
 
@@ -192,7 +460,7 @@ describe('topo accessors', () => {
192
460
  describe('Topo', () => {
193
461
  const mod = {
194
462
  e1: mockEvent('event-1'),
195
- p1: mockProvision('provision-1'),
463
+ p1: mockResource('resource-1'),
196
464
  t1: mockTrail('trail-1'),
197
465
  t2: mockTrail('trail-2'),
198
466
  t3: mockTrail('trail-3', ['trail-1']),
@@ -202,6 +470,13 @@ describe('Topo', () => {
202
470
  const app = topo('app', mod);
203
471
 
204
472
  describe('get()', () => {
473
+ test('retrieves contour by name', () => {
474
+ const user = mockContour('user');
475
+ const contourApp = topo('app', { user });
476
+
477
+ expect(contourApp.getContour('user')).toBe(user);
478
+ });
479
+
205
480
  test('retrieves trail by ID', () => {
206
481
  expect(app.get('trail-1')).toBe(mod.t1);
207
482
  });
@@ -216,6 +491,13 @@ describe('Topo', () => {
216
491
  });
217
492
 
218
493
  describe('has()', () => {
494
+ test('returns true for known contour', () => {
495
+ const user = mockContour('user');
496
+ const contourApp = topo('app', { user });
497
+
498
+ expect(contourApp.hasContour('user')).toBe(true);
499
+ });
500
+
219
501
  test('returns true for known trail', () => {
220
502
  expect(app.has('trail-1')).toBe(true);
221
503
  });
@@ -228,28 +510,34 @@ describe('Topo', () => {
228
510
  expect(app.has('nope')).toBe(false);
229
511
  });
230
512
 
513
+ test('returns false for unknown contour', () => {
514
+ const contourApp = topo('app');
515
+
516
+ expect(contourApp.hasContour('missing')).toBe(false);
517
+ });
518
+
231
519
  test('returns false for event ID (signals are not trails)', () => {
232
520
  expect(app.has('event-1')).toBe(false);
233
521
  });
234
522
  });
235
523
 
236
- describe('getProvision()', () => {
237
- test('retrieves provision by ID', () => {
238
- expect(app.getProvision('provision-1')).toBe(mod.p1);
524
+ describe('getResource()', () => {
525
+ test('retrieves resource by ID', () => {
526
+ expect(app.getResource('resource-1')).toBe(mod.p1);
239
527
  });
240
528
 
241
- test('returns undefined for unknown provision ID', () => {
242
- expect(app.getProvision('missing-provision')).toBeUndefined();
529
+ test('returns undefined for unknown resource ID', () => {
530
+ expect(app.getResource('missing-resource')).toBeUndefined();
243
531
  });
244
532
  });
245
533
 
246
- describe('hasProvision()', () => {
247
- test('returns true for known provision', () => {
248
- expect(app.hasProvision('provision-1')).toBe(true);
534
+ describe('hasResource()', () => {
535
+ test('returns true for known resource', () => {
536
+ expect(app.hasResource('resource-1')).toBe(true);
249
537
  });
250
538
 
251
- test('returns false for unknown provision', () => {
252
- expect(app.hasProvision('missing-provision')).toBe(false);
539
+ test('returns false for unknown resource', () => {
540
+ expect(app.hasResource('missing-resource')).toBe(false);
253
541
  });
254
542
  });
255
543
 
@@ -268,10 +556,18 @@ describe('Topo', () => {
268
556
  expect(items).toContain(mod.e1);
269
557
  });
270
558
 
271
- test('listProvisions() returns all provisions', () => {
272
- const items = app.listProvisions();
559
+ test('listResources() returns all resources', () => {
560
+ const items = app.listResources();
273
561
  expect(items).toHaveLength(1);
274
562
  expect(items).toContain(mod.p1);
275
563
  });
564
+
565
+ test('listContours() returns all contours', () => {
566
+ const gist = mockContour('gist');
567
+ const user = mockContour('user');
568
+ const contourApp = topo('app', { gist, user });
569
+
570
+ expect(contourApp.listContours()).toEqual([gist, user]);
571
+ });
276
572
  });
277
573
  });