@ontrails/core 1.0.0-beta.12 → 1.0.0-beta.14

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 (139) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +65 -35
  3. package/README.md +21 -20
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +5 -4
  7. package/dist/context.js.map +1 -1
  8. package/dist/derive.d.ts +14 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +36 -12
  11. package/dist/derive.js.map +1 -1
  12. package/dist/draft.d.ts +28 -0
  13. package/dist/draft.d.ts.map +1 -0
  14. package/dist/draft.js +156 -0
  15. package/dist/draft.js.map +1 -0
  16. package/dist/event.d.ts +4 -41
  17. package/dist/event.d.ts.map +1 -1
  18. package/dist/event.js +4 -14
  19. package/dist/event.js.map +1 -1
  20. package/dist/execute.d.ts +10 -10
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js +35 -28
  23. package/dist/execute.js.map +1 -1
  24. package/dist/gate.d.ts +17 -0
  25. package/dist/gate.d.ts.map +1 -0
  26. package/dist/gate.js +21 -0
  27. package/dist/gate.js.map +1 -0
  28. package/dist/index.d.ts +16 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/topo-saves.d.ts +47 -0
  33. package/dist/internal/topo-saves.d.ts.map +1 -0
  34. package/dist/internal/topo-saves.js +310 -0
  35. package/dist/internal/topo-saves.js.map +1 -0
  36. package/dist/internal/topo-store-read.d.ts +67 -0
  37. package/dist/internal/topo-store-read.d.ts.map +1 -0
  38. package/dist/internal/topo-store-read.js +222 -0
  39. package/dist/internal/topo-store-read.js.map +1 -0
  40. package/dist/internal/topo-store.d.ts +12 -0
  41. package/dist/internal/topo-store.d.ts.map +1 -0
  42. package/dist/internal/topo-store.js +571 -0
  43. package/dist/internal/topo-store.js.map +1 -0
  44. package/dist/internal/trails-db.d.ts +16 -0
  45. package/dist/internal/trails-db.d.ts.map +1 -0
  46. package/dist/internal/trails-db.js +118 -0
  47. package/dist/internal/trails-db.js.map +1 -0
  48. package/dist/layer.d.ts +5 -16
  49. package/dist/layer.d.ts.map +1 -1
  50. package/dist/layer.js +3 -20
  51. package/dist/layer.js.map +1 -1
  52. package/dist/provision-config.d.ts +22 -0
  53. package/dist/provision-config.d.ts.map +1 -0
  54. package/dist/provision-config.js +210 -0
  55. package/dist/provision-config.js.map +1 -0
  56. package/dist/provision.d.ts +71 -0
  57. package/dist/provision.d.ts.map +1 -0
  58. package/dist/provision.js +56 -0
  59. package/dist/provision.js.map +1 -0
  60. package/dist/run.d.ts +27 -0
  61. package/dist/run.d.ts.map +1 -0
  62. package/dist/run.js +34 -0
  63. package/dist/run.js.map +1 -0
  64. package/dist/service-config.d.ts +7 -7
  65. package/dist/service-config.d.ts.map +1 -1
  66. package/dist/service-config.js +75 -73
  67. package/dist/service-config.js.map +1 -1
  68. package/dist/service.d.ts +32 -36
  69. package/dist/service.d.ts.map +1 -1
  70. package/dist/service.js +21 -21
  71. package/dist/service.js.map +1 -1
  72. package/dist/signal.d.ts +33 -0
  73. package/dist/signal.d.ts.map +1 -0
  74. package/dist/signal.js +17 -0
  75. package/dist/signal.js.map +1 -0
  76. package/dist/topo-store.d.ts +48 -0
  77. package/dist/topo-store.d.ts.map +1 -0
  78. package/dist/topo-store.js +175 -0
  79. package/dist/topo-store.js.map +1 -0
  80. package/dist/topo.d.ts +10 -10
  81. package/dist/topo.d.ts.map +1 -1
  82. package/dist/topo.js +48 -35
  83. package/dist/topo.js.map +1 -1
  84. package/dist/trail.d.ts +16 -16
  85. package/dist/trail.d.ts.map +1 -1
  86. package/dist/trail.js +5 -4
  87. package/dist/trail.js.map +1 -1
  88. package/dist/types.d.ts +10 -10
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/validate-established-topo.d.ts +76 -0
  93. package/dist/validate-established-topo.d.ts.map +1 -0
  94. package/dist/validate-established-topo.js +43 -0
  95. package/dist/validate-established-topo.js.map +1 -0
  96. package/dist/validate-topo.d.ts +2 -2
  97. package/dist/validate-topo.d.ts.map +1 -1
  98. package/dist/validate-topo.js +34 -32
  99. package/dist/validate-topo.js.map +1 -1
  100. package/package.json +4 -1
  101. package/src/__tests__/context.test.ts +8 -8
  102. package/src/__tests__/derive.test.ts +58 -1
  103. package/src/__tests__/execute.test.ts +124 -122
  104. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  105. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  106. package/src/__tests__/service-config.test.ts +41 -37
  107. package/src/__tests__/service.test.ts +71 -71
  108. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  109. package/src/__tests__/topo-store-read.test.ts +251 -0
  110. package/src/__tests__/topo-store.test.ts +469 -0
  111. package/src/__tests__/topo.test.ts +54 -54
  112. package/src/__tests__/trail-permit.test.ts +4 -4
  113. package/src/__tests__/trail.test.ts +58 -58
  114. package/src/__tests__/trails-db.test.ts +191 -0
  115. package/src/__tests__/type-utils.test.ts +3 -3
  116. package/src/__tests__/validate-topo.test.ts +205 -38
  117. package/src/context.ts +5 -4
  118. package/src/derive.ts +47 -16
  119. package/src/draft.ts +334 -0
  120. package/src/event.ts +11 -73
  121. package/src/execute.ts +56 -40
  122. package/src/{layer.ts → gate.ts} +13 -13
  123. package/src/index.ts +54 -23
  124. package/src/internal/topo-saves.ts +429 -0
  125. package/src/internal/topo-store-read.ts +473 -0
  126. package/src/internal/topo-store.ts +1087 -0
  127. package/src/internal/trails-db.ts +189 -0
  128. package/src/{service-config.ts → provision-config.ts} +124 -105
  129. package/src/provision.ts +148 -0
  130. package/src/{dispatch.ts → run.ts} +8 -8
  131. package/src/signal.ts +65 -0
  132. package/src/topo-store.ts +301 -0
  133. package/src/topo.ts +74 -52
  134. package/src/trail.ts +23 -22
  135. package/src/types.ts +11 -11
  136. package/src/validate-established-topo.ts +63 -0
  137. package/src/validate-topo.ts +38 -34
  138. package/tsconfig.tsbuildinfo +1 -1
  139. package/src/service.ts +0 -145
@@ -1,24 +1,24 @@
1
- import { describe, test, expect } from 'bun:test';
1
+ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import { event } from '../event';
5
+ import { signal } from '../signal.js';
6
6
 
7
7
  const payloadSchema = z.object({
8
8
  action: z.string(),
9
9
  userId: z.string(),
10
10
  });
11
11
 
12
- const userAction = event('user.action', {
12
+ const userAction = signal('user.action', {
13
13
  description: 'A user performed an action',
14
14
  from: ['auth.login', 'auth.signup'],
15
- metadata: { domain: 'auth', priority: 1 },
15
+ meta: { domain: 'auth', priority: 1 },
16
16
  payload: payloadSchema,
17
17
  });
18
18
 
19
- describe('event() basics', () => {
20
- test("returns kind 'event'", () => {
21
- expect(userAction.kind).toBe('event');
19
+ describe('signal() basics', () => {
20
+ test("returns kind 'signal'", () => {
21
+ expect(userAction.kind).toBe('signal');
22
22
  });
23
23
 
24
24
  test('returns correct id', () => {
@@ -45,7 +45,7 @@ describe('event() basics', () => {
45
45
  });
46
46
  });
47
47
 
48
- describe('event() from and metadata', () => {
48
+ describe('signal() from and meta', () => {
49
49
  test('preserves from', () => {
50
50
  expect(userAction.from).toEqual(['auth.login', 'auth.signup']);
51
51
  });
@@ -54,29 +54,29 @@ describe('event() from and metadata', () => {
54
54
  expect(Object.isFrozen(userAction.from)).toBe(true);
55
55
  });
56
56
 
57
- test('preserves metadata', () => {
58
- expect(userAction.metadata).toEqual({ domain: 'auth', priority: 1 });
57
+ test('preserves meta', () => {
58
+ expect(userAction.meta).toEqual({ domain: 'auth', priority: 1 });
59
59
  });
60
60
 
61
61
  test('optional fields default to undefined', () => {
62
- const minimal = event('minimal', {
62
+ const minimal = signal('minimal', {
63
63
  payload: z.string(),
64
64
  });
65
65
  expect(minimal.description).toBeUndefined();
66
66
  expect(minimal.from).toBeUndefined();
67
- expect(minimal.metadata).toBeUndefined();
67
+ expect(minimal.meta).toBeUndefined();
68
68
  });
69
69
  });
70
70
 
71
- describe('event() single-object overload', () => {
71
+ describe('signal() single-object overload', () => {
72
72
  test('accepts spec with id property', () => {
73
- const e = event({
73
+ const e = signal({
74
74
  from: ['entity.add', 'entity.delete'],
75
75
  id: 'entity.updated',
76
76
  payload: z.object({ entityId: z.string() }),
77
77
  });
78
78
  expect(e.id).toBe('entity.updated');
79
- expect(e.kind).toBe('event');
79
+ expect(e.kind).toBe('signal');
80
80
  expect(e.from).toEqual(['entity.add', 'entity.delete']);
81
81
  });
82
82
  });
@@ -0,0 +1,251 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { z } from 'zod';
7
+
8
+ import {
9
+ createMockTopoStore,
10
+ createTopoStore,
11
+ provision,
12
+ Result,
13
+ signal,
14
+ topo,
15
+ topoStore,
16
+ trail,
17
+ } from '../index.js';
18
+ import { pinTopoSave } from '../internal/topo-saves.js';
19
+ import type { TopoSaveRecord } from '../internal/topo-saves.js';
20
+ import { persistEstablishedTopoSave } from '../internal/topo-store.js';
21
+ import { openWriteTrailsDb } from '../internal/trails-db.js';
22
+
23
+ const noop = () => Result.ok({ ok: true });
24
+
25
+ const expectOk = async <T>(
26
+ result: Promise<Result<T, Error>> | Result<T, Error>
27
+ ): Promise<T> => {
28
+ const resolved = await result;
29
+ if (resolved.isErr()) {
30
+ throw resolved.error;
31
+ }
32
+ return resolved.value;
33
+ };
34
+
35
+ const requireValue = <T>(value: T | undefined, message: string): T => {
36
+ if (value === undefined) {
37
+ throw new Error(message);
38
+ }
39
+ return value;
40
+ };
41
+
42
+ const exampleApp = () => {
43
+ const dbMain = provision('db.main', {
44
+ create: () => Result.ok({ source: 'factory' }),
45
+ description: 'Primary database',
46
+ health: () => Result.ok({ ok: true }),
47
+ mock: () => ({ source: 'mock' }),
48
+ });
49
+
50
+ const entityAdded = signal('entity.added', {
51
+ description: 'An entity was added',
52
+ from: ['entity.add'],
53
+ payload: z.object({ id: z.string() }),
54
+ });
55
+
56
+ const entityAdd = trail('entity.add', {
57
+ blaze: (input: { readonly name: string }) =>
58
+ Result.ok({ id: input.name.toLowerCase(), ok: true }),
59
+ description: 'Add a new entity',
60
+ examples: [
61
+ {
62
+ expected: { id: 'ada', ok: true },
63
+ input: { name: 'Ada' },
64
+ name: 'Add Ada',
65
+ },
66
+ ],
67
+ input: z.object({ name: z.string() }),
68
+ output: z.object({ id: z.string(), ok: z.boolean() }),
69
+ provisions: [dbMain],
70
+ });
71
+
72
+ const entityList = trail('entity.list', {
73
+ blaze: noop,
74
+ crosses: ['entity.add'],
75
+ description: 'List entities',
76
+ detours: {
77
+ ConflictError: ['entity.add'],
78
+ },
79
+ idempotent: true,
80
+ input: z.object({}),
81
+ intent: 'read',
82
+ output: z.object({ ok: z.boolean() }),
83
+ provisions: [dbMain],
84
+ });
85
+
86
+ return topo('projection-app', {
87
+ dbMain,
88
+ entityAdd,
89
+ entityAdded,
90
+ entityList,
91
+ });
92
+ };
93
+
94
+ describe('read-only topo store', () => {
95
+ let tmpRoot: string | undefined;
96
+
97
+ afterEach(() => {
98
+ if (tmpRoot) {
99
+ rmSync(tmpRoot, { force: true, recursive: true });
100
+ tmpRoot = undefined;
101
+ }
102
+ });
103
+
104
+ const makeRoot = (): string => {
105
+ tmpRoot = mkdtempSync(join(tmpdir(), 'topo-store-read-'));
106
+ return tmpRoot;
107
+ };
108
+
109
+ const seedStore = (): {
110
+ readonly rootDir: string;
111
+ readonly save: TopoSaveRecord;
112
+ } => {
113
+ const rootDir = makeRoot();
114
+ const db = openWriteTrailsDb({ rootDir });
115
+
116
+ try {
117
+ const result = persistEstablishedTopoSave(db, exampleApp(), {
118
+ createdAt: '2026-04-03T14:00:00.000Z',
119
+ gitSha: 'abc123',
120
+ });
121
+ if (result.isErr()) {
122
+ throw result.error;
123
+ }
124
+ const save = result.value;
125
+ pinTopoSave(db, { name: 'baseline', saveId: save.id });
126
+ return { rootDir, save };
127
+ } finally {
128
+ db.close();
129
+ }
130
+ };
131
+
132
+ test('lists save-scoped trails and provisions through typed accessors', () => {
133
+ const { rootDir, save } = seedStore();
134
+ const store = createTopoStore({ rootDir });
135
+
136
+ expect(store.saves.latest()?.id).toBe(save.id);
137
+ expect(store.pins.get('baseline')?.saveId).toBe(save.id);
138
+
139
+ expect(store.trails.list({ save: { saveId: save.id } })).toEqual([
140
+ expect.objectContaining({
141
+ description: 'Add a new entity',
142
+ exampleCount: 1,
143
+ hasOutput: true,
144
+ id: 'entity.add',
145
+ intent: 'write',
146
+ }),
147
+ expect.objectContaining({
148
+ description: 'List entities',
149
+ exampleCount: 0,
150
+ id: 'entity.list',
151
+ intent: 'read',
152
+ safety: 'read',
153
+ }),
154
+ ]);
155
+
156
+ expect(store.provisions.list({ save: { saveId: save.id } })).toEqual([
157
+ expect.objectContaining({
158
+ description: 'Primary database',
159
+ health: 'available',
160
+ id: 'db.main',
161
+ usedBy: ['entity.add', 'entity.list'],
162
+ }),
163
+ ]);
164
+ });
165
+
166
+ test('filters trails by intent', () => {
167
+ const { rootDir, save } = seedStore();
168
+ const store = createTopoStore({ rootDir });
169
+
170
+ const writeTrails = store.trails.list({
171
+ intent: 'write',
172
+ save: { saveId: save.id },
173
+ });
174
+ expect(writeTrails).toHaveLength(1);
175
+ expect(writeTrails[0]?.id).toBe('entity.add');
176
+
177
+ const readTrails = store.trails.list({
178
+ intent: 'read',
179
+ save: { saveId: save.id },
180
+ });
181
+ expect(readTrails).toHaveLength(1);
182
+ expect(readTrails[0]?.id).toBe('entity.list');
183
+ });
184
+
185
+ test('returns detailed trail and export views, plus a query escape hatch', () => {
186
+ const { rootDir, save } = seedStore();
187
+ const store = createTopoStore({ rootDir });
188
+
189
+ const detail = store.trails.get('entity.list', {
190
+ save: { saveId: save.id },
191
+ });
192
+ expect(detail).toEqual(
193
+ expect.objectContaining({
194
+ crosses: ['entity.add'],
195
+ detours: { ConflictError: ['entity.add'] },
196
+ id: 'entity.list',
197
+ provisions: ['db.main'],
198
+ })
199
+ );
200
+ expect(detail?.examples).toEqual([]);
201
+
202
+ const exported = store.exports.get({ pin: 'baseline' });
203
+ expect(exported?.save.id).toBe(save.id);
204
+ expect(exported?.trailheadHash).toHaveLength(64);
205
+
206
+ const rows = store.query<{ id: string }>(
207
+ 'SELECT id FROM topo_trails WHERE save_id = ? ORDER BY id ASC',
208
+ [save.id]
209
+ );
210
+ expect(rows).toEqual([{ id: 'entity.add' }, { id: 'entity.list' }]);
211
+ });
212
+
213
+ test('fails loudly when no saved topo state exists', () => {
214
+ const rootDir = makeRoot();
215
+ const store = createTopoStore({ rootDir });
216
+
217
+ expect(() => store.saves.latest()).toThrow('No saved topo state found');
218
+ expect(() => store.trails.list()).toThrow('No saved topo state found');
219
+ expect(() => store.exports.get()).toThrow('No saved topo state found');
220
+ });
221
+
222
+ test('exposes a provision factory and mock for topoStore', async () => {
223
+ const { rootDir } = seedStore();
224
+
225
+ const created = await expectOk(
226
+ topoStore.create({
227
+ config: undefined,
228
+ cwd: rootDir,
229
+ env: {},
230
+ workspaceRoot: rootDir,
231
+ })
232
+ );
233
+
234
+ expect(created.saves.latest()).toBeDefined();
235
+ expect(
236
+ created.query<{ count: number }>(
237
+ 'SELECT COUNT(*) as count FROM topo_saves'
238
+ )[0]?.count
239
+ ).toBe(1);
240
+
241
+ expect(topoStore.mock).toBeDefined();
242
+ const mock = await requireValue(
243
+ topoStore.mock,
244
+ 'Expected topoStore to define a mock factory'
245
+ )();
246
+ expect(mock).toBeDefined();
247
+ expect(() =>
248
+ createMockTopoStore().query('SELECT id FROM topo_trails')
249
+ ).toThrow('Mock topoStore.query() is unsupported');
250
+ });
251
+ });