@ontrails/core 1.0.0-beta.13 → 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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +2 -1
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +156 -0
- package/dist/draft.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/topo-saves.d.ts +47 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +310 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +67 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +222 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +12 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +571 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/topo-store.d.ts +48 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +175 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +5 -3
- package/dist/validate-topo.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/topo-store-read.test.ts +251 -0
- package/src/__tests__/topo-store.test.ts +469 -0
- package/src/__tests__/trails-db.test.ts +191 -0
- package/src/__tests__/validate-topo.test.ts +167 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +334 -0
- package/src/index.ts +30 -1
- package/src/internal/topo-saves.ts +429 -0
- package/src/internal/topo-store-read.ts +473 -0
- package/src/internal/topo-store.ts +1087 -0
- package/src/internal/trails-db.ts +189 -0
- package/src/topo-store.ts +301 -0
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +7 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,13 +2,45 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
import { deriveFields } from '../derive.js';
|
|
5
|
+
import { deriveCliPath, deriveFields } from '../derive.js';
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Tests
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
describe('derive', () => {
|
|
12
|
+
describe('cli paths', () => {
|
|
13
|
+
test('top-level trail IDs derive to a one-segment CLI path', () => {
|
|
14
|
+
expect(deriveCliPath('search')).toEqual(['search']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('multi-dot trail IDs derive to the full ordered CLI path', () => {
|
|
18
|
+
expect(deriveCliPath('topo.pin.remove')).toEqual([
|
|
19
|
+
'topo',
|
|
20
|
+
'pin',
|
|
21
|
+
'remove',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('trail IDs with consecutive dots throw a ValidationError', () => {
|
|
26
|
+
expect(() => deriveCliPath('topo..pin')).toThrow(
|
|
27
|
+
'Trail ID "topo..pin" contains an empty segment'
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('trail IDs with a leading dot throw a ValidationError', () => {
|
|
32
|
+
expect(() => deriveCliPath('.pin')).toThrow(
|
|
33
|
+
'Trail ID ".pin" contains an empty segment'
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('trail IDs with a trailing dot throw a ValidationError', () => {
|
|
38
|
+
expect(() => deriveCliPath('pin.')).toThrow(
|
|
39
|
+
'Trail ID "pin." contains an empty segment'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
12
44
|
describe('primitive types', () => {
|
|
13
45
|
test('z.string() derives string field', () => {
|
|
14
46
|
const schema = z.object({ name: z.string() });
|
|
@@ -119,6 +151,31 @@ describe('derive', () => {
|
|
|
119
151
|
expect(fields[0]?.type).toBe('string[]');
|
|
120
152
|
expect(fields[0]?.default).toEqual(['a', 'b']);
|
|
121
153
|
});
|
|
154
|
+
|
|
155
|
+
test('z.array(z.object(...)) is omitted when it cannot be represented faithfully', () => {
|
|
156
|
+
const schema = z.object({
|
|
157
|
+
files: z.array(
|
|
158
|
+
z.object({
|
|
159
|
+
content: z.string(),
|
|
160
|
+
filename: z.string(),
|
|
161
|
+
})
|
|
162
|
+
),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(deriveFields(schema)).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('unsupported shapes', () => {
|
|
170
|
+
test('nested objects are omitted when they cannot be represented faithfully', () => {
|
|
171
|
+
const schema = z.object({
|
|
172
|
+
filter: z.object({
|
|
173
|
+
query: z.string(),
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(deriveFields(schema)).toEqual([]);
|
|
178
|
+
});
|
|
122
179
|
});
|
|
123
180
|
|
|
124
181
|
describe('modifiers', () => {
|
|
@@ -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
|
+
});
|