@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.
Files changed (59) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +2 -1
  4. package/dist/derive.d.ts +6 -0
  5. package/dist/derive.d.ts.map +1 -1
  6. package/dist/derive.js +29 -5
  7. package/dist/derive.js.map +1 -1
  8. package/dist/draft.d.ts +28 -0
  9. package/dist/draft.d.ts.map +1 -0
  10. package/dist/draft.js +156 -0
  11. package/dist/draft.js.map +1 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/internal/topo-saves.d.ts +47 -0
  17. package/dist/internal/topo-saves.d.ts.map +1 -0
  18. package/dist/internal/topo-saves.js +310 -0
  19. package/dist/internal/topo-saves.js.map +1 -0
  20. package/dist/internal/topo-store-read.d.ts +67 -0
  21. package/dist/internal/topo-store-read.d.ts.map +1 -0
  22. package/dist/internal/topo-store-read.js +222 -0
  23. package/dist/internal/topo-store-read.js.map +1 -0
  24. package/dist/internal/topo-store.d.ts +12 -0
  25. package/dist/internal/topo-store.d.ts.map +1 -0
  26. package/dist/internal/topo-store.js +571 -0
  27. package/dist/internal/topo-store.js.map +1 -0
  28. package/dist/internal/trails-db.d.ts +16 -0
  29. package/dist/internal/trails-db.d.ts.map +1 -0
  30. package/dist/internal/trails-db.js +118 -0
  31. package/dist/internal/trails-db.js.map +1 -0
  32. package/dist/topo-store.d.ts +48 -0
  33. package/dist/topo-store.d.ts.map +1 -0
  34. package/dist/topo-store.js +175 -0
  35. package/dist/topo-store.js.map +1 -0
  36. package/dist/validate-established-topo.d.ts +76 -0
  37. package/dist/validate-established-topo.d.ts.map +1 -0
  38. package/dist/validate-established-topo.js +43 -0
  39. package/dist/validate-established-topo.js.map +1 -0
  40. package/dist/validate-topo.d.ts.map +1 -1
  41. package/dist/validate-topo.js +5 -3
  42. package/dist/validate-topo.js.map +1 -1
  43. package/package.json +4 -1
  44. package/src/__tests__/derive.test.ts +58 -1
  45. package/src/__tests__/topo-store-read.test.ts +251 -0
  46. package/src/__tests__/topo-store.test.ts +469 -0
  47. package/src/__tests__/trails-db.test.ts +191 -0
  48. package/src/__tests__/validate-topo.test.ts +167 -0
  49. package/src/derive.ts +39 -8
  50. package/src/draft.ts +334 -0
  51. package/src/index.ts +30 -1
  52. package/src/internal/topo-saves.ts +429 -0
  53. package/src/internal/topo-store-read.ts +473 -0
  54. package/src/internal/topo-store.ts +1087 -0
  55. package/src/internal/trails-db.ts +189 -0
  56. package/src/topo-store.ts +301 -0
  57. package/src/validate-established-topo.ts +63 -0
  58. package/src/validate-topo.ts +7 -3
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,473 @@
1
+ import type { Database, SQLQueryBindings } from 'bun:sqlite';
2
+
3
+ import { ValidationError } from '../errors.js';
4
+ import type { TopoPinRecord, TopoSaveRecord } from './topo-saves.js';
5
+ import {
6
+ getTopoPin,
7
+ getTopoSave,
8
+ listTopoPins,
9
+ listTopoSaves,
10
+ } from './topo-saves.js';
11
+ import type { StoredTopoExport } from './topo-store.js';
12
+ import { getStoredTopoExport } from './topo-store.js';
13
+
14
+ export interface TopoStoreRef {
15
+ readonly pin?: string;
16
+ readonly saveId?: string;
17
+ }
18
+
19
+ export interface TopoStoreTrailRecord {
20
+ readonly description: string | null;
21
+ readonly exampleCount: number;
22
+ readonly hasExamples: boolean;
23
+ readonly hasOutput: boolean;
24
+ readonly id: string;
25
+ readonly idempotent: boolean;
26
+ readonly intent: 'destroy' | 'read' | 'write';
27
+ readonly kind: 'trail';
28
+ readonly meta: Readonly<Record<string, unknown>> | null;
29
+ readonly safety: '-' | 'destroy' | 'read' | 'write';
30
+ readonly saveId: string;
31
+ }
32
+
33
+ export interface TopoStoreExampleRecord {
34
+ readonly description: string | null;
35
+ readonly error: string | null;
36
+ readonly expected: unknown;
37
+ readonly input: unknown;
38
+ readonly name: string;
39
+ readonly ordinal: number;
40
+ }
41
+
42
+ export interface TopoStoreTrailDetailRecord extends TopoStoreTrailRecord {
43
+ readonly crosses: readonly string[];
44
+ readonly detours: Readonly<Record<string, readonly string[]>> | null;
45
+ readonly examples: readonly TopoStoreExampleRecord[];
46
+ readonly provisions: readonly string[];
47
+ }
48
+
49
+ export interface TopoStoreProvisionRecord {
50
+ readonly description: string | null;
51
+ readonly hasHealth: boolean;
52
+ readonly hasMock: boolean;
53
+ readonly health: 'available' | 'none';
54
+ readonly id: string;
55
+ readonly kind: 'provision';
56
+ readonly lifetime: 'singleton';
57
+ readonly saveId: string;
58
+ readonly usedBy: readonly string[];
59
+ }
60
+
61
+ export interface TopoStoreExportRecord extends StoredTopoExport {
62
+ readonly save: TopoSaveRecord;
63
+ }
64
+
65
+ interface TopoTrailRow {
66
+ readonly description: string | null;
67
+ readonly example_count: number;
68
+ readonly has_examples: number;
69
+ readonly has_output: number;
70
+ readonly id: string;
71
+ readonly idempotent: number;
72
+ readonly intent: string | null;
73
+ readonly meta: string | null;
74
+ readonly save_id: string;
75
+ }
76
+
77
+ interface TopoCrossingRow {
78
+ readonly target_id: string;
79
+ }
80
+
81
+ interface TopoTrailProvisionRow {
82
+ readonly provision_id: string;
83
+ }
84
+
85
+ interface TopoExampleRow {
86
+ readonly description: string | null;
87
+ readonly error: string | null;
88
+ readonly expected: string | null;
89
+ readonly input: string;
90
+ readonly name: string;
91
+ readonly ordinal: number;
92
+ }
93
+
94
+ interface TopoProvisionRow {
95
+ readonly has_health: number;
96
+ readonly has_mock: number;
97
+ readonly id: string;
98
+ readonly save_id: string;
99
+ }
100
+
101
+ interface StoredTrailheadMapEntry {
102
+ readonly description?: string;
103
+ readonly detours?: Readonly<Record<string, readonly string[]>>;
104
+ readonly healthcheck?: boolean;
105
+ readonly id: string;
106
+ readonly kind: 'provision' | 'signal' | 'trail';
107
+ }
108
+
109
+ interface StoredTrailheadMap {
110
+ readonly entries: readonly StoredTrailheadMapEntry[];
111
+ }
112
+
113
+ const ensureSingleRefSelector = (ref?: TopoStoreRef): void => {
114
+ if (ref?.pin !== undefined && ref.saveId !== undefined) {
115
+ throw new ValidationError(
116
+ 'Topo store references may use pin or saveId, not both'
117
+ );
118
+ }
119
+ };
120
+
121
+ const normalizeIntent = (
122
+ intent: string | null
123
+ ): TopoStoreTrailRecord['intent'] => {
124
+ if (intent === 'destroy' || intent === 'read') {
125
+ return intent;
126
+ }
127
+ return 'write';
128
+ };
129
+
130
+ const safetyForIntent = (
131
+ intent: TopoStoreTrailRecord['intent']
132
+ ): TopoStoreTrailRecord['safety'] => {
133
+ if (intent === 'destroy') {
134
+ return 'destroy';
135
+ }
136
+ if (intent === 'read') {
137
+ return 'read';
138
+ }
139
+ return 'write';
140
+ };
141
+
142
+ const parseMeta = (
143
+ value: string | null
144
+ ): Readonly<Record<string, unknown>> | null =>
145
+ value === null
146
+ ? null
147
+ : (JSON.parse(value) as Readonly<Record<string, unknown>>);
148
+
149
+ const parseJson = (value: string): unknown => JSON.parse(value) as unknown;
150
+
151
+ const resolveRefSave = (
152
+ db: Database,
153
+ ref?: TopoStoreRef
154
+ ): TopoSaveRecord | undefined => {
155
+ ensureSingleRefSelector(ref);
156
+
157
+ if (ref?.saveId !== undefined) {
158
+ return getTopoSave(db, ref.saveId);
159
+ }
160
+
161
+ if (ref?.pin !== undefined) {
162
+ const pin = getTopoPin(db, ref.pin);
163
+ return pin === undefined ? undefined : getTopoSave(db, pin.saveId);
164
+ }
165
+
166
+ return listTopoSaves(db)[0];
167
+ };
168
+
169
+ const readStoredEntry = (
170
+ db: Database,
171
+ saveId: string,
172
+ kind: StoredTrailheadMapEntry['kind'],
173
+ id: string
174
+ ): StoredTrailheadMapEntry | undefined => {
175
+ const stored = getStoredTopoExport(db, saveId);
176
+ if (stored === undefined) {
177
+ return undefined;
178
+ }
179
+
180
+ const map = JSON.parse(stored.trailheadMapJson) as StoredTrailheadMap;
181
+ return map.entries.find((entry) => entry.kind === kind && entry.id === id);
182
+ };
183
+
184
+ const mapTrailRow = (row: TopoTrailRow): TopoStoreTrailRecord => {
185
+ const intent = normalizeIntent(row.intent);
186
+
187
+ return {
188
+ description: row.description,
189
+ exampleCount: row.example_count,
190
+ hasExamples: row.has_examples === 1,
191
+ hasOutput: row.has_output === 1,
192
+ id: row.id,
193
+ idempotent: row.idempotent === 1,
194
+ intent,
195
+ kind: 'trail',
196
+ meta: parseMeta(row.meta),
197
+ safety: safetyForIntent(intent),
198
+ saveId: row.save_id,
199
+ };
200
+ };
201
+
202
+ const readTrailCrossings = (
203
+ db: Database,
204
+ saveId: string,
205
+ trailId: string
206
+ ): readonly string[] =>
207
+ db
208
+ .query<TopoCrossingRow, [string, string]>(
209
+ `SELECT target_id
210
+ FROM topo_crossings
211
+ WHERE save_id = ? AND source_id = ?
212
+ ORDER BY target_id ASC`
213
+ )
214
+ .all(saveId, trailId)
215
+ .map((row) => row.target_id);
216
+
217
+ const readTrailProvisionIds = (
218
+ db: Database,
219
+ saveId: string,
220
+ trailId: string
221
+ ): readonly string[] =>
222
+ db
223
+ .query<TopoTrailProvisionRow, [string, string]>(
224
+ `SELECT provision_id
225
+ FROM topo_trail_provisions
226
+ WHERE save_id = ? AND trail_id = ?
227
+ ORDER BY provision_id ASC`
228
+ )
229
+ .all(saveId, trailId)
230
+ .map((row) => row.provision_id);
231
+
232
+ const readTrailExamples = (
233
+ db: Database,
234
+ saveId: string,
235
+ trailId: string
236
+ ): readonly TopoStoreExampleRecord[] =>
237
+ db
238
+ .query<TopoExampleRow, [string, string]>(
239
+ `SELECT ordinal, name, description, input, expected, error
240
+ FROM topo_examples
241
+ WHERE save_id = ? AND trail_id = ?
242
+ ORDER BY ordinal ASC`
243
+ )
244
+ .all(saveId, trailId)
245
+ .map((row) => ({
246
+ description: row.description,
247
+ error: row.error,
248
+ expected: row.expected === null ? null : parseJson(row.expected),
249
+ input: parseJson(row.input),
250
+ name: row.name,
251
+ ordinal: row.ordinal,
252
+ }));
253
+
254
+ const readProvisionUsage = (
255
+ db: Database,
256
+ saveId: string
257
+ ): ReadonlyMap<string, readonly string[]> => {
258
+ const rows = db
259
+ .query<{ provision_id: string; trail_id: string }, [string]>(
260
+ `SELECT provision_id, trail_id
261
+ FROM topo_trail_provisions
262
+ WHERE save_id = ?
263
+ ORDER BY provision_id ASC, trail_id ASC`
264
+ )
265
+ .all(saveId);
266
+
267
+ const usage = new Map<string, string[]>();
268
+ for (const row of rows) {
269
+ const trails = usage.get(row.provision_id) ?? [];
270
+ trails.push(row.trail_id);
271
+ usage.set(row.provision_id, trails);
272
+ }
273
+
274
+ return new Map(
275
+ [...usage.entries()].map(([id, trails]) => [id, trails] as const)
276
+ );
277
+ };
278
+
279
+ export const resolveTopoStoreSave = (
280
+ db: Database,
281
+ ref?: TopoStoreRef
282
+ ): TopoSaveRecord | undefined => resolveRefSave(db, ref);
283
+
284
+ export const listTopoStorePins = (db: Database): readonly TopoPinRecord[] =>
285
+ listTopoPins(db);
286
+
287
+ export const listTopoStoreSaves = (db: Database): readonly TopoSaveRecord[] =>
288
+ listTopoSaves(db);
289
+
290
+ export const getTopoStoreExport = (
291
+ db: Database,
292
+ ref?: TopoStoreRef
293
+ ): TopoStoreExportRecord | undefined => {
294
+ const save = resolveRefSave(db, ref);
295
+ if (save === undefined) {
296
+ return undefined;
297
+ }
298
+
299
+ const stored = getStoredTopoExport(db, save.id);
300
+ if (stored === undefined) {
301
+ return undefined;
302
+ }
303
+
304
+ return {
305
+ ...stored,
306
+ save,
307
+ };
308
+ };
309
+
310
+ export const listTopoStoreTrails = (
311
+ db: Database,
312
+ options?: {
313
+ readonly intent?: TopoStoreTrailRecord['intent'];
314
+ readonly save?: TopoStoreRef;
315
+ }
316
+ ): readonly TopoStoreTrailRecord[] => {
317
+ const save = resolveRefSave(db, options?.save);
318
+ if (save === undefined) {
319
+ return [];
320
+ }
321
+
322
+ const baseQuery = `SELECT id, intent, idempotent, has_output, has_examples, example_count, description, meta, save_id
323
+ FROM topo_trails`;
324
+
325
+ let rows: TopoTrailRow[];
326
+ if (options?.intent === undefined) {
327
+ rows = db
328
+ .query<TopoTrailRow, [string]>(
329
+ `${baseQuery} WHERE save_id = ? ORDER BY id ASC`
330
+ )
331
+ .all(save.id);
332
+ } else if (options.intent === 'write') {
333
+ rows = db
334
+ .query<TopoTrailRow, [string]>(
335
+ `${baseQuery} WHERE save_id = ? AND intent = 'write' ORDER BY id ASC`
336
+ )
337
+ .all(save.id);
338
+ } else {
339
+ rows = db
340
+ .query<TopoTrailRow, [string, string]>(
341
+ `${baseQuery} WHERE save_id = ? AND intent = ? ORDER BY id ASC`
342
+ )
343
+ .all(save.id, options.intent);
344
+ }
345
+
346
+ return rows.map(mapTrailRow);
347
+ };
348
+
349
+ export const getTopoStoreTrail = (
350
+ db: Database,
351
+ trailId: string,
352
+ options?: { readonly save?: TopoStoreRef }
353
+ ): TopoStoreTrailDetailRecord | undefined => {
354
+ const save = resolveRefSave(db, options?.save);
355
+ if (save === undefined) {
356
+ return undefined;
357
+ }
358
+
359
+ const row = db
360
+ .query<TopoTrailRow, [string, string]>(
361
+ `SELECT id, intent, idempotent, has_output, has_examples, example_count, description, meta, save_id
362
+ FROM topo_trails
363
+ WHERE save_id = ? AND id = ?
364
+ LIMIT 1`
365
+ )
366
+ .get(save.id, trailId);
367
+
368
+ if (row === null || row === undefined) {
369
+ return undefined;
370
+ }
371
+
372
+ const storedEntry = readStoredEntry(db, save.id, 'trail', trailId);
373
+
374
+ return {
375
+ ...mapTrailRow(row),
376
+ crosses: readTrailCrossings(db, save.id, trailId),
377
+ detours: storedEntry?.detours ?? null,
378
+ examples: readTrailExamples(db, save.id, trailId),
379
+ provisions: readTrailProvisionIds(db, save.id, trailId),
380
+ };
381
+ };
382
+
383
+ const mapProvisionRow = (
384
+ row: TopoProvisionRow,
385
+ usedBy: readonly string[],
386
+ storedEntry?: StoredTrailheadMapEntry
387
+ ): TopoStoreProvisionRecord => ({
388
+ description: storedEntry?.description ?? null,
389
+ hasHealth: row.has_health === 1,
390
+ hasMock: row.has_mock === 1,
391
+ health:
392
+ row.has_health === 1 || storedEntry?.healthcheck === true
393
+ ? 'available'
394
+ : 'none',
395
+ id: row.id,
396
+ kind: 'provision',
397
+ lifetime: 'singleton',
398
+ saveId: row.save_id,
399
+ usedBy,
400
+ });
401
+
402
+ export const listTopoStoreProvisions = (
403
+ db: Database,
404
+ options?: { readonly save?: TopoStoreRef }
405
+ ): readonly TopoStoreProvisionRecord[] => {
406
+ const save = resolveRefSave(db, options?.save);
407
+ if (save === undefined) {
408
+ return [];
409
+ }
410
+
411
+ const usage = readProvisionUsage(db, save.id);
412
+ const rows = db
413
+ .query<TopoProvisionRow, [string]>(
414
+ `SELECT id, has_mock, has_health, save_id
415
+ FROM topo_provisions
416
+ WHERE save_id = ?
417
+ ORDER BY id ASC`
418
+ )
419
+ .all(save.id);
420
+
421
+ const stored = getStoredTopoExport(db, save.id);
422
+ const entries = stored
423
+ ? (JSON.parse(stored.trailheadMapJson) as StoredTrailheadMap).entries
424
+ : [];
425
+
426
+ return rows.map((row) =>
427
+ mapProvisionRow(
428
+ row,
429
+ usage.get(row.id) ?? [],
430
+ entries.find((e) => e.kind === 'provision' && e.id === row.id)
431
+ )
432
+ );
433
+ };
434
+
435
+ export const getTopoStoreProvision = (
436
+ db: Database,
437
+ provisionId: string,
438
+ options?: { readonly save?: TopoStoreRef }
439
+ ): TopoStoreProvisionRecord | undefined => {
440
+ const save = resolveRefSave(db, options?.save);
441
+ if (save === undefined) {
442
+ return undefined;
443
+ }
444
+
445
+ const row = db
446
+ .query<TopoProvisionRow, [string, string]>(
447
+ `SELECT id, has_mock, has_health, save_id
448
+ FROM topo_provisions
449
+ WHERE save_id = ? AND id = ?
450
+ LIMIT 1`
451
+ )
452
+ .get(save.id, provisionId);
453
+
454
+ if (row === null || row === undefined) {
455
+ return undefined;
456
+ }
457
+
458
+ const usage = readProvisionUsage(db, save.id);
459
+ return mapProvisionRow(
460
+ row,
461
+ usage.get(provisionId) ?? [],
462
+ readStoredEntry(db, save.id, 'provision', provisionId)
463
+ );
464
+ };
465
+
466
+ export const queryTopoStore = <TRow extends Record<string, unknown>>(
467
+ db: Database,
468
+ sql: string,
469
+ bindings?: readonly SQLQueryBindings[]
470
+ ): readonly TRow[] =>
471
+ db
472
+ .query<TRow, SQLQueryBindings[]>(sql)
473
+ .all(...(bindings === undefined ? [] : [...bindings]));