@ontrails/topography 1.0.0-beta.41

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.
@@ -0,0 +1,1102 @@
1
+ import type { Database, SQLQueryBindings } from 'bun:sqlite';
2
+
3
+ import { ValidationError } from '@ontrails/core';
4
+
5
+ import type {
6
+ ListTopoSnapshotsOptions,
7
+ TopoSnapshot,
8
+ } from './topo-snapshots.js';
9
+ import {
10
+ listTopoSnapshots,
11
+ readPinnedTopoSnapshot,
12
+ readTopoSnapshot,
13
+ } from './topo-snapshots.js';
14
+ import type { StoredTopoExport } from './topo-store.js';
15
+ import { getStoredTopoExport } from './topo-store.js';
16
+ import { topoGraphSchema } from '../types.js';
17
+ import type {
18
+ JsonSchema,
19
+ LockManifest,
20
+ TopoGraph,
21
+ TopoGraphActivationEdge,
22
+ TopoGraphActivationEntry,
23
+ TopoGraphFieldOverride,
24
+ TopoGraphLayerReference,
25
+ TopoGraphEntry,
26
+ } from '../types.js';
27
+
28
+ export interface TopoStoreRef {
29
+ readonly pin?: string;
30
+ readonly snapshotId?: string;
31
+ }
32
+
33
+ export type TopoStoreEntryKind = TopoGraphEntry['kind'];
34
+
35
+ export interface TopoStoreTopoGraphRecord {
36
+ readonly snapshot: TopoSnapshot;
37
+ readonly topoGraph: TopoGraph;
38
+ }
39
+
40
+ export interface TopoStoreTopoGraphEntryRecord extends TopoGraphEntry {
41
+ readonly snapshotId: string;
42
+ }
43
+
44
+ export type TopoStoreEntityRecord = TopoStoreTopoGraphEntryRecord & {
45
+ readonly kind: 'entity';
46
+ };
47
+
48
+ export interface TopoStoreActivationContextRecord {
49
+ readonly edgeCount: number;
50
+ readonly sourceCount: number;
51
+ readonly sourceKeys: readonly string[];
52
+ readonly trailIds: readonly string[];
53
+ }
54
+
55
+ export interface TopoStoreSurfaceProjectionRecord {
56
+ readonly derivedName: string;
57
+ readonly method: string | null;
58
+ readonly surface: string;
59
+ readonly trailId: string;
60
+ }
61
+
62
+ export interface TopoStoreTrailRecord {
63
+ readonly description: string | null;
64
+ readonly exampleCount: number;
65
+ readonly hasExamples: boolean;
66
+ readonly hasOutput: boolean;
67
+ readonly id: string;
68
+ readonly idempotent: boolean;
69
+ readonly intent: 'destroy' | 'read' | 'write';
70
+ readonly kind: 'trail';
71
+ readonly meta: Readonly<Record<string, unknown>> | null;
72
+ readonly pattern: string | null;
73
+ readonly safety: '-' | 'destroy' | 'read' | 'write';
74
+ readonly snapshotId: string;
75
+ }
76
+
77
+ export interface TopoStoreExampleRecord {
78
+ readonly description: string | null;
79
+ readonly error: string | null;
80
+ readonly expected: unknown;
81
+ readonly expectedMatch: unknown;
82
+ readonly input: unknown;
83
+ readonly name: string;
84
+ readonly ordinal: number;
85
+ readonly signals: unknown;
86
+ }
87
+
88
+ export interface TopoStoreTrailDetailRecord extends TopoStoreTrailRecord {
89
+ readonly activationContext: TopoStoreActivationContextRecord;
90
+ readonly activationEdges: readonly TopoGraphActivationEdge[];
91
+ readonly activationSources: readonly TopoGraphActivationEntry[];
92
+ readonly cli: TopoGraphEntry['cli'] | null;
93
+ readonly entityDetails: readonly TopoStoreEntityRecord[];
94
+ readonly entities: readonly string[];
95
+ readonly composes: readonly string[];
96
+ readonly detours:
97
+ | readonly { readonly on: string; readonly maxAttempts: number }[]
98
+ | null;
99
+ readonly examples: readonly TopoStoreExampleRecord[];
100
+ readonly fieldOverrides: readonly TopoGraphFieldOverride[];
101
+ readonly governance: Readonly<Record<string, unknown>> | null;
102
+ readonly input: JsonSchema | null;
103
+ readonly layers: readonly TopoGraphLayerReference[];
104
+ readonly output: JsonSchema | null;
105
+ readonly resources: readonly string[];
106
+ readonly surfaceProjections: readonly TopoStoreSurfaceProjectionRecord[];
107
+ readonly surfaces: readonly string[];
108
+ }
109
+
110
+ export interface TopoStoreResourceRecord {
111
+ readonly description: string | null;
112
+ readonly hasHealth: boolean;
113
+ readonly hasMock: boolean;
114
+ readonly health: 'available' | 'none';
115
+ readonly id: string;
116
+ readonly kind: 'resource';
117
+ readonly lifetime: 'singleton';
118
+ readonly snapshotId: string;
119
+ readonly usedBy: readonly string[];
120
+ }
121
+
122
+ export interface TopoStoreSignalRecord {
123
+ readonly consumers: readonly string[];
124
+ readonly description: string | null;
125
+ readonly exampleCount: number;
126
+ readonly from: readonly string[];
127
+ readonly hasExamples: boolean;
128
+ readonly id: string;
129
+ readonly kind: 'signal';
130
+ readonly payloadSchema: boolean;
131
+ readonly producers: readonly string[];
132
+ readonly snapshotId: string;
133
+ }
134
+
135
+ export interface TopoStoreSignalDetailRecord extends TopoStoreSignalRecord {
136
+ readonly examples: readonly unknown[];
137
+ readonly payload: Readonly<Record<string, unknown>> | null;
138
+ }
139
+
140
+ export interface TopoStoreExportRecord extends StoredTopoExport {
141
+ readonly lockManifest: LockManifest;
142
+ readonly snapshot: TopoSnapshot;
143
+ readonly topoGraph: TopoGraph;
144
+ }
145
+
146
+ interface TopoTrailRow {
147
+ readonly description: string | null;
148
+ readonly example_count: number;
149
+ readonly has_examples: number;
150
+ readonly has_output: number;
151
+ readonly id: string;
152
+ readonly idempotent: number;
153
+ readonly intent: string | null;
154
+ readonly meta: string | null;
155
+ readonly pattern: string | null;
156
+ readonly snapshot_id: string;
157
+ }
158
+
159
+ interface TopoComposingRow {
160
+ readonly target_id: string;
161
+ }
162
+
163
+ interface TopoTrailResourceRow {
164
+ readonly resource_id: string;
165
+ }
166
+
167
+ interface TopoExampleRow {
168
+ readonly description: string | null;
169
+ readonly error: string | null;
170
+ readonly expected: string | null;
171
+ readonly expected_match: string | null;
172
+ readonly input: string;
173
+ readonly name: string;
174
+ readonly ordinal: number;
175
+ readonly signals: string | null;
176
+ }
177
+
178
+ interface TopoSurfaceProjectionRow {
179
+ readonly derived_name: string;
180
+ readonly method: string | null;
181
+ readonly surface: string;
182
+ readonly trail_id: string;
183
+ }
184
+
185
+ interface TopoResourceRow {
186
+ readonly has_health: number;
187
+ readonly has_mock: number;
188
+ readonly id: string;
189
+ readonly snapshot_id: string;
190
+ }
191
+
192
+ interface TopoSignalRow {
193
+ readonly description: string | null;
194
+ readonly id: string;
195
+ readonly snapshot_id: string;
196
+ }
197
+
198
+ interface TopoSignalRelationRow {
199
+ readonly trail_id: string;
200
+ }
201
+
202
+ interface TopoSignalRelationBatchRow extends TopoSignalRelationRow {
203
+ readonly signal_id: string;
204
+ }
205
+
206
+ const ensureSingleRefSelector = (ref?: TopoStoreRef): void => {
207
+ if (ref?.pin !== undefined && ref.snapshotId !== undefined) {
208
+ throw new ValidationError(
209
+ 'Topo store references may use pin or snapshotId, not both'
210
+ );
211
+ }
212
+ };
213
+
214
+ const normalizeIntent = (
215
+ intent: string | null
216
+ ): TopoStoreTrailRecord['intent'] => {
217
+ if (intent === 'destroy' || intent === 'read') {
218
+ return intent;
219
+ }
220
+ return 'write';
221
+ };
222
+
223
+ const safetyForIntent = (
224
+ intent: TopoStoreTrailRecord['intent']
225
+ ): TopoStoreTrailRecord['safety'] => {
226
+ if (intent === 'destroy') {
227
+ return 'destroy';
228
+ }
229
+ if (intent === 'read') {
230
+ return 'read';
231
+ }
232
+ return 'write';
233
+ };
234
+
235
+ const parseMeta = (
236
+ value: string | null
237
+ ): Readonly<Record<string, unknown>> | null =>
238
+ value === null
239
+ ? null
240
+ : (JSON.parse(value) as Readonly<Record<string, unknown>>);
241
+
242
+ const parseJson = (value: string): unknown => JSON.parse(value) as unknown;
243
+
244
+ const parseLockManifest = (stored: StoredTopoExport): LockManifest =>
245
+ JSON.parse(stored.lockManifestJson) as LockManifest;
246
+
247
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
248
+ typeof value === 'object' && value !== null && !Array.isArray(value);
249
+
250
+ const hasRetiredDomainVocabulary = (value: unknown): boolean => {
251
+ if (!isRecord(value) || !Array.isArray(value['entries'])) {
252
+ return false;
253
+ }
254
+
255
+ return value['entries'].some(
256
+ (entry) =>
257
+ isRecord(entry) &&
258
+ (entry['kind'] === 'contour' || Object.hasOwn(entry, 'contours'))
259
+ );
260
+ };
261
+
262
+ const formatTopoGraphIssues = (
263
+ issues: readonly {
264
+ readonly message: string;
265
+ readonly path: readonly PropertyKey[];
266
+ }[]
267
+ ): string =>
268
+ issues
269
+ .slice(0, 3)
270
+ .map((issue) => {
271
+ const path =
272
+ issue.path.length === 0 ? '<root>' : issue.path.map(String).join('.');
273
+ return `${path}: ${issue.message}`;
274
+ })
275
+ .join('; ');
276
+
277
+ const parseTopoGraph = (stored: StoredTopoExport): TopoGraph => {
278
+ const parsed = JSON.parse(stored.topoGraphJson) as unknown;
279
+ if (hasRetiredDomainVocabulary(parsed)) {
280
+ throw new ValidationError(
281
+ 'Stored topo export uses retired "contour" vocabulary. Regenerate the Topography store before reading it.'
282
+ );
283
+ }
284
+
285
+ const result = topoGraphSchema.safeParse(parsed);
286
+ if (!result.success) {
287
+ throw new ValidationError(
288
+ `Stored topo export is not compatible with the current TopoGraph schema (${formatTopoGraphIssues(result.error.issues)}). Regenerate the Topography store before reading it.`
289
+ );
290
+ }
291
+
292
+ return result.data;
293
+ };
294
+
295
+ const readSnapshotRef = (
296
+ db: Database,
297
+ ref?: TopoStoreRef
298
+ ): TopoSnapshot | undefined => {
299
+ ensureSingleRefSelector(ref);
300
+
301
+ if (ref?.snapshotId !== undefined) {
302
+ return readTopoSnapshot(db, ref.snapshotId);
303
+ }
304
+
305
+ if (ref?.pin !== undefined) {
306
+ return readPinnedTopoSnapshot(db, ref.pin);
307
+ }
308
+
309
+ return listTopoSnapshots(db, { limit: 1 })[0];
310
+ };
311
+
312
+ const readStoredTopoGraph = (
313
+ db: Database,
314
+ snapshotId: string
315
+ ): TopoGraph | undefined => {
316
+ const stored = getStoredTopoExport(db, snapshotId);
317
+ return stored === undefined ? undefined : parseTopoGraph(stored);
318
+ };
319
+
320
+ const toTopoGraphEntryRecord = (
321
+ snapshotId: string,
322
+ entry: TopoGraphEntry
323
+ ): TopoStoreTopoGraphEntryRecord => ({
324
+ ...entry,
325
+ snapshotId,
326
+ });
327
+
328
+ const listStoredEntries = (
329
+ db: Database,
330
+ snapshotId: string
331
+ ): readonly TopoStoreTopoGraphEntryRecord[] =>
332
+ readStoredTopoGraph(db, snapshotId)?.entries.map((entry) =>
333
+ toTopoGraphEntryRecord(snapshotId, entry)
334
+ ) ?? [];
335
+
336
+ const readStoredEntry = (
337
+ db: Database,
338
+ snapshotId: string,
339
+ kind: TopoStoreEntryKind,
340
+ id: string
341
+ ): TopoStoreTopoGraphEntryRecord | undefined =>
342
+ listStoredEntries(db, snapshotId).find(
343
+ (entry) => entry.id === id && entry.kind === kind
344
+ );
345
+
346
+ const findTopoGraphEntry = (
347
+ topoGraph: TopoGraph | undefined,
348
+ kind: TopoStoreEntryKind,
349
+ id: string
350
+ ): TopoGraphEntry | undefined =>
351
+ topoGraph?.entries.find((entry) => entry.id === id && entry.kind === kind);
352
+
353
+ const mapTrailRow = (row: TopoTrailRow): TopoStoreTrailRecord => {
354
+ const intent = normalizeIntent(row.intent);
355
+
356
+ return {
357
+ description: row.description,
358
+ exampleCount: row.example_count,
359
+ hasExamples: row.has_examples === 1,
360
+ hasOutput: row.has_output === 1,
361
+ id: row.id,
362
+ idempotent: row.idempotent === 1,
363
+ intent,
364
+ kind: 'trail',
365
+ meta: parseMeta(row.meta),
366
+ pattern: row.pattern,
367
+ safety: safetyForIntent(intent),
368
+ snapshotId: row.snapshot_id,
369
+ };
370
+ };
371
+
372
+ const readTrailComposings = (
373
+ db: Database,
374
+ snapshotId: string,
375
+ trailId: string
376
+ ): readonly string[] =>
377
+ db
378
+ .query<TopoComposingRow, [string, string]>(
379
+ `SELECT target_id
380
+ FROM topo_composings
381
+ WHERE snapshot_id = ? AND source_id = ?
382
+ ORDER BY target_id ASC`
383
+ )
384
+ .all(snapshotId, trailId)
385
+ .map((row) => row.target_id);
386
+
387
+ const readTrailResourceIds = (
388
+ db: Database,
389
+ snapshotId: string,
390
+ trailId: string
391
+ ): readonly string[] =>
392
+ db
393
+ .query<TopoTrailResourceRow, [string, string]>(
394
+ `SELECT resource_id
395
+ FROM topo_trail_resources
396
+ WHERE snapshot_id = ? AND trail_id = ?
397
+ ORDER BY resource_id ASC`
398
+ )
399
+ .all(snapshotId, trailId)
400
+ .map((row) => row.resource_id);
401
+
402
+ const readTrailExamples = (
403
+ db: Database,
404
+ snapshotId: string,
405
+ trailId: string
406
+ ): readonly TopoStoreExampleRecord[] =>
407
+ db
408
+ .query<TopoExampleRow, [string, string]>(
409
+ `SELECT ordinal, name, description, input, expected, expected_match, error, signals
410
+ FROM topo_examples
411
+ WHERE snapshot_id = ? AND trail_id = ?
412
+ ORDER BY ordinal ASC`
413
+ )
414
+ .all(snapshotId, trailId)
415
+ .map((row) => ({
416
+ description: row.description,
417
+ error: row.error,
418
+ expected: row.expected === null ? null : parseJson(row.expected),
419
+ expectedMatch:
420
+ row.expected_match === null ? null : parseJson(row.expected_match),
421
+ input: parseJson(row.input),
422
+ name: row.name,
423
+ ordinal: row.ordinal,
424
+ signals: row.signals === null ? null : parseJson(row.signals),
425
+ }));
426
+
427
+ const readTrailSurfaceProjections = (
428
+ db: Database,
429
+ snapshotId: string,
430
+ trailId: string
431
+ ): readonly TopoStoreSurfaceProjectionRecord[] =>
432
+ db
433
+ .query<TopoSurfaceProjectionRow, [string, string]>(
434
+ `SELECT trail_id, surface, derived_name, method
435
+ FROM topo_surfaces
436
+ WHERE snapshot_id = ? AND trail_id = ?
437
+ ORDER BY surface ASC, derived_name ASC`
438
+ )
439
+ .all(snapshotId, trailId)
440
+ .map((row) => ({
441
+ derivedName: row.derived_name,
442
+ method: row.method,
443
+ surface: row.surface,
444
+ trailId: row.trail_id,
445
+ }));
446
+
447
+ const emptyActivationContext = (): TopoStoreActivationContextRecord => ({
448
+ edgeCount: 0,
449
+ sourceCount: 0,
450
+ sourceKeys: [],
451
+ trailIds: [],
452
+ });
453
+
454
+ const mapActivationContext = (
455
+ topoGraph: TopoGraph | undefined,
456
+ trailId: string
457
+ ): TopoStoreActivationContextRecord => {
458
+ const edges =
459
+ topoGraph?.activationGraph.edges.filter(
460
+ (edge) => edge.trailId === trailId
461
+ ) ?? [];
462
+ if (edges.length === 0) {
463
+ return emptyActivationContext();
464
+ }
465
+ return {
466
+ edgeCount: edges.length,
467
+ sourceCount: new Set(edges.map((edge) => edge.sourceKey)).size,
468
+ sourceKeys: [...new Set(edges.map((edge) => edge.sourceKey))].toSorted(),
469
+ trailIds: [...new Set(edges.map((edge) => edge.trailId))].toSorted(),
470
+ };
471
+ };
472
+
473
+ const readTrailActivationEdges = (
474
+ topoGraph: TopoGraph | undefined,
475
+ trailId: string
476
+ ): readonly TopoGraphActivationEdge[] =>
477
+ topoGraph?.activationGraph.edges.filter((edge) => edge.trailId === trailId) ??
478
+ [];
479
+
480
+ const readTrailEntityDetails = (
481
+ topoGraph: TopoGraph | undefined,
482
+ snapshotId: string,
483
+ entityIds: readonly string[]
484
+ ): readonly TopoStoreEntityRecord[] =>
485
+ entityIds
486
+ .map((entityId) => findTopoGraphEntry(topoGraph, 'entity', entityId))
487
+ .filter((entry): entry is TopoGraphEntry => entry !== undefined)
488
+ .map((entry) => toTopoGraphEntryRecord(snapshotId, entry))
489
+ .map((entry) => entry as TopoStoreEntityRecord);
490
+
491
+ type StoredTrailEntryDetail = Pick<
492
+ TopoStoreTrailDetailRecord,
493
+ | 'activationSources'
494
+ | 'cli'
495
+ | 'entities'
496
+ | 'detours'
497
+ | 'fieldOverrides'
498
+ | 'governance'
499
+ | 'input'
500
+ | 'layers'
501
+ | 'output'
502
+ | 'surfaces'
503
+ >;
504
+
505
+ const emptyStoredTrailEntryDetail = (): StoredTrailEntryDetail => ({
506
+ activationSources: [],
507
+ cli: null,
508
+ detours: null,
509
+ entities: [],
510
+ fieldOverrides: [],
511
+ governance: null,
512
+ input: null,
513
+ layers: [],
514
+ output: null,
515
+ surfaces: [],
516
+ });
517
+
518
+ const mapStoredTrailEntryDetail = (
519
+ storedEntry: TopoGraphEntry | undefined
520
+ ): StoredTrailEntryDetail => {
521
+ if (storedEntry === undefined) {
522
+ return emptyStoredTrailEntryDetail();
523
+ }
524
+
525
+ const {
526
+ activationSources = [],
527
+ cli = null,
528
+ entities = [],
529
+ detours = null,
530
+ fieldOverrides = [],
531
+ governance = null,
532
+ input = null,
533
+ layers = [],
534
+ output = null,
535
+ surfaces = [],
536
+ } = storedEntry;
537
+
538
+ return {
539
+ activationSources,
540
+ cli,
541
+ detours,
542
+ entities,
543
+ fieldOverrides,
544
+ governance,
545
+ input,
546
+ layers,
547
+ output,
548
+ surfaces,
549
+ };
550
+ };
551
+
552
+ const buildTrailGraphDetail = (
553
+ db: Database,
554
+ snapshotId: string,
555
+ trailId: string
556
+ ): Pick<
557
+ TopoStoreTrailDetailRecord,
558
+ | 'activationContext'
559
+ | 'activationEdges'
560
+ | 'activationSources'
561
+ | 'cli'
562
+ | 'entityDetails'
563
+ | 'entities'
564
+ | 'detours'
565
+ | 'fieldOverrides'
566
+ | 'governance'
567
+ | 'input'
568
+ | 'layers'
569
+ | 'output'
570
+ | 'surfaceProjections'
571
+ | 'surfaces'
572
+ > => {
573
+ const storedTopoGraph = readStoredTopoGraph(db, snapshotId);
574
+ const storedEntry = findTopoGraphEntry(storedTopoGraph, 'trail', trailId);
575
+ const entryDetail = mapStoredTrailEntryDetail(storedEntry);
576
+
577
+ return {
578
+ activationContext: mapActivationContext(storedTopoGraph, trailId),
579
+ activationEdges: readTrailActivationEdges(storedTopoGraph, trailId),
580
+ activationSources: entryDetail.activationSources,
581
+ cli: entryDetail.cli,
582
+ detours: entryDetail.detours,
583
+ entities: entryDetail.entities,
584
+ entityDetails: readTrailEntityDetails(
585
+ storedTopoGraph,
586
+ snapshotId,
587
+ entryDetail.entities
588
+ ),
589
+ fieldOverrides: entryDetail.fieldOverrides,
590
+ governance: entryDetail.governance,
591
+ input: entryDetail.input,
592
+ layers: entryDetail.layers,
593
+ output: entryDetail.output,
594
+ surfaceProjections: readTrailSurfaceProjections(db, snapshotId, trailId),
595
+ surfaces: entryDetail.surfaces,
596
+ };
597
+ };
598
+
599
+ const readResourceUsage = (
600
+ db: Database,
601
+ snapshotId: string
602
+ ): ReadonlyMap<string, readonly string[]> => {
603
+ const rows = db
604
+ .query<{ resource_id: string; trail_id: string }, [string]>(
605
+ `SELECT resource_id, trail_id
606
+ FROM topo_trail_resources
607
+ WHERE snapshot_id = ?
608
+ ORDER BY resource_id ASC, trail_id ASC`
609
+ )
610
+ .all(snapshotId);
611
+
612
+ const usage = new Map<string, string[]>();
613
+ for (const row of rows) {
614
+ const trails = usage.get(row.resource_id) ?? [];
615
+ trails.push(row.trail_id);
616
+ usage.set(row.resource_id, trails);
617
+ }
618
+
619
+ return usage as ReadonlyMap<string, readonly string[]>;
620
+ };
621
+
622
+ type SignalRelationTable =
623
+ | 'topo_trail_fires'
624
+ | 'topo_trail_on'
625
+ | 'topo_trail_signals';
626
+
627
+ // Hard-coded query strings keyed by the union variant. The TypeScript union
628
+ // already constrains callers, but holding the literal SQL here removes any
629
+ // runtime path where `${table}` could be substituted from an unconstrained
630
+ // string.
631
+ const SIGNAL_RELATION_QUERIES = {
632
+ topo_trail_fires: `SELECT trail_id
633
+ FROM topo_trail_fires
634
+ WHERE snapshot_id = ? AND signal_id = ?
635
+ ORDER BY trail_id ASC`,
636
+ topo_trail_on: `SELECT trail_id
637
+ FROM topo_trail_on
638
+ WHERE snapshot_id = ? AND signal_id = ?
639
+ ORDER BY trail_id ASC`,
640
+ topo_trail_signals: `SELECT trail_id
641
+ FROM topo_trail_signals
642
+ WHERE snapshot_id = ? AND signal_id = ?
643
+ ORDER BY trail_id ASC`,
644
+ } as const satisfies Record<SignalRelationTable, string>;
645
+
646
+ const SIGNAL_RELATION_BATCH_QUERIES = {
647
+ topo_trail_fires: `SELECT signal_id, trail_id
648
+ FROM topo_trail_fires
649
+ WHERE snapshot_id = ?
650
+ ORDER BY signal_id ASC, trail_id ASC`,
651
+ topo_trail_on: `SELECT signal_id, trail_id
652
+ FROM topo_trail_on
653
+ WHERE snapshot_id = ?
654
+ ORDER BY signal_id ASC, trail_id ASC`,
655
+ topo_trail_signals: `SELECT signal_id, trail_id
656
+ FROM topo_trail_signals
657
+ WHERE snapshot_id = ?
658
+ ORDER BY signal_id ASC, trail_id ASC`,
659
+ } as const satisfies Record<SignalRelationTable, string>;
660
+
661
+ const readSignalTrailIds = (
662
+ db: Database,
663
+ table: SignalRelationTable,
664
+ snapshotId: string,
665
+ signalId: string
666
+ ): readonly string[] =>
667
+ db
668
+ .query<TopoSignalRelationRow, [string, string]>(
669
+ SIGNAL_RELATION_QUERIES[table]
670
+ )
671
+ .all(snapshotId, signalId)
672
+ .map((row) => row.trail_id);
673
+
674
+ const readSignalRelationUsage = (
675
+ db: Database,
676
+ table: SignalRelationTable,
677
+ snapshotId: string
678
+ ): ReadonlyMap<string, readonly string[]> => {
679
+ const rows = db
680
+ .query<TopoSignalRelationBatchRow, [string]>(
681
+ SIGNAL_RELATION_BATCH_QUERIES[table]
682
+ )
683
+ .all(snapshotId);
684
+
685
+ const usage = new Map<string, string[]>();
686
+ for (const row of rows) {
687
+ const trails = usage.get(row.signal_id) ?? [];
688
+ trails.push(row.trail_id);
689
+ usage.set(row.signal_id, trails);
690
+ }
691
+
692
+ return usage as ReadonlyMap<string, readonly string[]>;
693
+ };
694
+
695
+ const signalExamplePayload = (example: unknown): unknown => {
696
+ if (typeof example !== 'object' || example === null) {
697
+ return example;
698
+ }
699
+ const candidate = example as {
700
+ readonly kind?: unknown;
701
+ readonly payload?: unknown;
702
+ };
703
+ return candidate.kind === 'payload' && 'payload' in candidate
704
+ ? candidate.payload
705
+ : example;
706
+ };
707
+
708
+ const signalExamplesFromEntry = (
709
+ storedEntry: TopoGraphEntry | undefined
710
+ ): readonly unknown[] =>
711
+ storedEntry?.examples?.map((example) => signalExamplePayload(example)) ?? [];
712
+
713
+ export const readTopoStoreSnapshot = (
714
+ db: Database,
715
+ ref?: TopoStoreRef
716
+ ): TopoSnapshot | undefined => readSnapshotRef(db, ref);
717
+
718
+ export const listTopoStoreSnapshots = (
719
+ db: Database,
720
+ options?: ListTopoSnapshotsOptions
721
+ ): readonly TopoSnapshot[] => listTopoSnapshots(db, options);
722
+
723
+ export const getTopoStoreExport = (
724
+ db: Database,
725
+ ref?: TopoStoreRef
726
+ ): TopoStoreExportRecord | undefined => {
727
+ const snapshot = readSnapshotRef(db, ref);
728
+ if (snapshot === undefined) {
729
+ return undefined;
730
+ }
731
+
732
+ const stored = getStoredTopoExport(db, snapshot.id);
733
+ if (stored === undefined) {
734
+ return undefined;
735
+ }
736
+
737
+ return {
738
+ ...stored,
739
+ lockManifest: parseLockManifest(stored),
740
+ snapshot,
741
+ topoGraph: parseTopoGraph(stored),
742
+ };
743
+ };
744
+
745
+ export const getTopoStoreTopoGraph = (
746
+ db: Database,
747
+ ref?: TopoStoreRef
748
+ ): TopoStoreTopoGraphRecord | undefined => {
749
+ const exported = getTopoStoreExport(db, ref);
750
+ if (exported === undefined) {
751
+ return undefined;
752
+ }
753
+ return { snapshot: exported.snapshot, topoGraph: exported.topoGraph };
754
+ };
755
+
756
+ export const listTopoStoreEntries = (
757
+ db: Database,
758
+ options?: {
759
+ readonly kind?: TopoStoreEntryKind;
760
+ readonly snapshot?: TopoStoreRef;
761
+ }
762
+ ): readonly TopoStoreTopoGraphEntryRecord[] => {
763
+ const snapshot = readSnapshotRef(db, options?.snapshot);
764
+ if (snapshot === undefined) {
765
+ return [];
766
+ }
767
+
768
+ const entries = listStoredEntries(db, snapshot.id);
769
+ return options?.kind === undefined
770
+ ? entries
771
+ : entries.filter((entry) => entry.kind === options.kind);
772
+ };
773
+
774
+ export const getTopoStoreEntry = (
775
+ db: Database,
776
+ id: string,
777
+ options?: {
778
+ readonly kind?: TopoStoreEntryKind;
779
+ readonly snapshot?: TopoStoreRef;
780
+ }
781
+ ): TopoStoreTopoGraphEntryRecord | undefined => {
782
+ const entries = listTopoStoreEntries(db, options);
783
+ return entries.find((entry) => entry.id === id);
784
+ };
785
+
786
+ export const listTopoStoreEntities = (
787
+ db: Database,
788
+ options?: { readonly snapshot?: TopoStoreRef }
789
+ ): readonly TopoStoreEntityRecord[] =>
790
+ listTopoStoreEntries(db, { ...options, kind: 'entity' }).map(
791
+ (entry) => entry as TopoStoreEntityRecord
792
+ );
793
+
794
+ export const getTopoStoreEntity = (
795
+ db: Database,
796
+ id: string,
797
+ options?: { readonly snapshot?: TopoStoreRef }
798
+ ): TopoStoreEntityRecord | undefined =>
799
+ getTopoStoreEntry(db, id, { ...options, kind: 'entity' }) as
800
+ | TopoStoreEntityRecord
801
+ | undefined;
802
+
803
+ export const listTopoStoreTrails = (
804
+ db: Database,
805
+ options?: {
806
+ readonly intent?: TopoStoreTrailRecord['intent'];
807
+ readonly snapshot?: TopoStoreRef;
808
+ }
809
+ ): readonly TopoStoreTrailRecord[] => {
810
+ const snapshot = readSnapshotRef(db, options?.snapshot);
811
+ if (snapshot === undefined) {
812
+ return [];
813
+ }
814
+
815
+ const baseQuery = `SELECT id, intent, idempotent, has_output, has_examples, example_count, description, pattern, meta, snapshot_id
816
+ FROM topo_trails`;
817
+
818
+ let rows: TopoTrailRow[];
819
+ if (options?.intent === undefined) {
820
+ rows = db
821
+ .query<TopoTrailRow, [string]>(
822
+ `${baseQuery} WHERE snapshot_id = ? ORDER BY id ASC`
823
+ )
824
+ .all(snapshot.id);
825
+ } else if (options.intent === 'write') {
826
+ rows = db
827
+ .query<TopoTrailRow, [string]>(
828
+ `${baseQuery} WHERE snapshot_id = ? AND (intent = 'write' OR intent IS NULL) ORDER BY id ASC`
829
+ )
830
+ .all(snapshot.id);
831
+ } else {
832
+ rows = db
833
+ .query<TopoTrailRow, [string, string]>(
834
+ `${baseQuery} WHERE snapshot_id = ? AND intent = ? ORDER BY id ASC`
835
+ )
836
+ .all(snapshot.id, options.intent);
837
+ }
838
+
839
+ return rows.map(mapTrailRow);
840
+ };
841
+
842
+ export const getTopoStoreTrail = (
843
+ db: Database,
844
+ trailId: string,
845
+ options?: { readonly snapshot?: TopoStoreRef }
846
+ ): TopoStoreTrailDetailRecord | undefined => {
847
+ const snapshot = readSnapshotRef(db, options?.snapshot);
848
+ if (snapshot === undefined) {
849
+ return undefined;
850
+ }
851
+
852
+ const row = db
853
+ .query<TopoTrailRow, [string, string]>(
854
+ `SELECT id, intent, idempotent, has_output, has_examples, example_count, description, pattern, meta, snapshot_id
855
+ FROM topo_trails
856
+ WHERE snapshot_id = ? AND id = ?
857
+ LIMIT 1`
858
+ )
859
+ .get(snapshot.id, trailId);
860
+
861
+ if (row === null || row === undefined) {
862
+ return undefined;
863
+ }
864
+
865
+ const graphDetail = buildTrailGraphDetail(db, snapshot.id, trailId);
866
+
867
+ return {
868
+ ...mapTrailRow(row),
869
+ activationContext: graphDetail.activationContext,
870
+ activationEdges: graphDetail.activationEdges,
871
+ activationSources: graphDetail.activationSources,
872
+ cli: graphDetail.cli,
873
+ composes: readTrailComposings(db, snapshot.id, trailId),
874
+ detours: graphDetail.detours,
875
+ entities: graphDetail.entities,
876
+ entityDetails: graphDetail.entityDetails,
877
+ examples: readTrailExamples(db, snapshot.id, trailId),
878
+ fieldOverrides: graphDetail.fieldOverrides,
879
+ governance: graphDetail.governance,
880
+ input: graphDetail.input,
881
+ layers: graphDetail.layers,
882
+ output: graphDetail.output,
883
+ resources: readTrailResourceIds(db, snapshot.id, trailId),
884
+ surfaceProjections: graphDetail.surfaceProjections,
885
+ surfaces: graphDetail.surfaces,
886
+ };
887
+ };
888
+
889
+ const mapResourceRow = (
890
+ row: TopoResourceRow,
891
+ usedBy: readonly string[],
892
+ storedEntry?: TopoGraphEntry
893
+ ): TopoStoreResourceRecord => ({
894
+ description: storedEntry?.description ?? null,
895
+ hasHealth: row.has_health === 1,
896
+ hasMock: row.has_mock === 1,
897
+ health:
898
+ row.has_health === 1 || storedEntry?.healthcheck === true
899
+ ? 'available'
900
+ : 'none',
901
+ id: row.id,
902
+ kind: 'resource',
903
+ lifetime: 'singleton',
904
+ snapshotId: row.snapshot_id,
905
+ usedBy,
906
+ });
907
+
908
+ export const listTopoStoreResources = (
909
+ db: Database,
910
+ options?: { readonly snapshot?: TopoStoreRef }
911
+ ): readonly TopoStoreResourceRecord[] => {
912
+ const snapshot = readSnapshotRef(db, options?.snapshot);
913
+ if (snapshot === undefined) {
914
+ return [];
915
+ }
916
+
917
+ const usage = readResourceUsage(db, snapshot.id);
918
+ const rows = db
919
+ .query<TopoResourceRow, [string]>(
920
+ `SELECT id, has_mock, has_health, snapshot_id
921
+ FROM topo_resources
922
+ WHERE snapshot_id = ?
923
+ ORDER BY id ASC`
924
+ )
925
+ .all(snapshot.id);
926
+
927
+ const entries = listStoredEntries(db, snapshot.id);
928
+
929
+ return rows.map((row) =>
930
+ mapResourceRow(
931
+ row,
932
+ usage.get(row.id) ?? [],
933
+ entries.find((entry) => entry.id === row.id && entry.kind === 'resource')
934
+ )
935
+ );
936
+ };
937
+
938
+ export const getTopoStoreResource = (
939
+ db: Database,
940
+ resourceId: string,
941
+ options?: { readonly snapshot?: TopoStoreRef }
942
+ ): TopoStoreResourceRecord | undefined => {
943
+ const snapshot = readSnapshotRef(db, options?.snapshot);
944
+ if (snapshot === undefined) {
945
+ return undefined;
946
+ }
947
+
948
+ const row = db
949
+ .query<TopoResourceRow, [string, string]>(
950
+ `SELECT id, has_mock, has_health, snapshot_id
951
+ FROM topo_resources
952
+ WHERE snapshot_id = ? AND id = ?
953
+ LIMIT 1`
954
+ )
955
+ .get(snapshot.id, resourceId);
956
+
957
+ if (row === null || row === undefined) {
958
+ return undefined;
959
+ }
960
+
961
+ const usage = readResourceUsage(db, snapshot.id);
962
+ return mapResourceRow(
963
+ row,
964
+ usage.get(resourceId) ?? [],
965
+ readStoredEntry(db, snapshot.id, 'resource', resourceId)
966
+ );
967
+ };
968
+
969
+ const mapSignalRow = (
970
+ row: TopoSignalRow,
971
+ storedEntry: TopoGraphEntry | undefined,
972
+ relations: {
973
+ readonly consumers: readonly string[];
974
+ readonly from: readonly string[];
975
+ readonly producers: readonly string[];
976
+ }
977
+ ): TopoStoreSignalRecord => {
978
+ const exampleCount =
979
+ storedEntry?.exampleCount ?? storedEntry?.examples?.length ?? 0;
980
+
981
+ return {
982
+ consumers: relations.consumers,
983
+ description: storedEntry?.description ?? row.description,
984
+ exampleCount,
985
+ from: relations.from,
986
+ hasExamples: exampleCount > 0,
987
+ id: row.id,
988
+ kind: 'signal',
989
+ // Derived from the stored TopoGraph entry rather than hard-coded so the
990
+ // list flag stays coherent with the detail record's `payload` field. If the
991
+ // TopoGraph entry is missing for a signal row (e.g. partial import or
992
+ // schema migration), `payload` would be null in the detail; signaling
993
+ // `payloadSchema: true` here would mislead consumers into skipping the
994
+ // detail call.
995
+ payloadSchema:
996
+ storedEntry?.payload !== undefined || storedEntry?.input !== undefined,
997
+ producers: relations.producers,
998
+ snapshotId: row.snapshot_id,
999
+ };
1000
+ };
1001
+
1002
+ const readSignalRelations = (
1003
+ db: Database,
1004
+ snapshotId: string,
1005
+ signalId: string
1006
+ ) => ({
1007
+ consumers: readSignalTrailIds(db, 'topo_trail_on', snapshotId, signalId),
1008
+ from: readSignalTrailIds(db, 'topo_trail_signals', snapshotId, signalId),
1009
+ producers: readSignalTrailIds(db, 'topo_trail_fires', snapshotId, signalId),
1010
+ });
1011
+
1012
+ export const listTopoStoreSignals = (
1013
+ db: Database,
1014
+ options?: { readonly snapshot?: TopoStoreRef }
1015
+ ): readonly TopoStoreSignalRecord[] => {
1016
+ const snapshot = readSnapshotRef(db, options?.snapshot);
1017
+ if (snapshot === undefined) {
1018
+ return [];
1019
+ }
1020
+
1021
+ const rows = db
1022
+ .query<TopoSignalRow, [string]>(
1023
+ `SELECT id, description, snapshot_id
1024
+ FROM topo_signals
1025
+ WHERE snapshot_id = ?
1026
+ ORDER BY id ASC`
1027
+ )
1028
+ .all(snapshot.id);
1029
+
1030
+ const entries = listStoredEntries(db, snapshot.id);
1031
+ const consumersBySignal = readSignalRelationUsage(
1032
+ db,
1033
+ 'topo_trail_on',
1034
+ snapshot.id
1035
+ );
1036
+ const fromBySignal = readSignalRelationUsage(
1037
+ db,
1038
+ 'topo_trail_signals',
1039
+ snapshot.id
1040
+ );
1041
+ const producersBySignal = readSignalRelationUsage(
1042
+ db,
1043
+ 'topo_trail_fires',
1044
+ snapshot.id
1045
+ );
1046
+
1047
+ return rows.map((row) =>
1048
+ mapSignalRow(
1049
+ row,
1050
+ entries.find((entry) => entry.id === row.id && entry.kind === 'signal'),
1051
+ {
1052
+ consumers: consumersBySignal.get(row.id) ?? [],
1053
+ from: fromBySignal.get(row.id) ?? [],
1054
+ producers: producersBySignal.get(row.id) ?? [],
1055
+ }
1056
+ )
1057
+ );
1058
+ };
1059
+
1060
+ export const getTopoStoreSignal = (
1061
+ db: Database,
1062
+ signalId: string,
1063
+ options?: { readonly snapshot?: TopoStoreRef }
1064
+ ): TopoStoreSignalDetailRecord | undefined => {
1065
+ const snapshot = readSnapshotRef(db, options?.snapshot);
1066
+ if (snapshot === undefined) {
1067
+ return undefined;
1068
+ }
1069
+
1070
+ const row = db
1071
+ .query<TopoSignalRow, [string, string]>(
1072
+ `SELECT id, description, snapshot_id
1073
+ FROM topo_signals
1074
+ WHERE snapshot_id = ? AND id = ?
1075
+ LIMIT 1`
1076
+ )
1077
+ .get(snapshot.id, signalId);
1078
+
1079
+ if (row === null || row === undefined) {
1080
+ return undefined;
1081
+ }
1082
+
1083
+ const storedEntry = readStoredEntry(db, snapshot.id, 'signal', signalId);
1084
+ return {
1085
+ ...mapSignalRow(
1086
+ row,
1087
+ storedEntry,
1088
+ readSignalRelations(db, snapshot.id, signalId)
1089
+ ),
1090
+ examples: signalExamplesFromEntry(storedEntry),
1091
+ payload: storedEntry?.payload ?? storedEntry?.input ?? null,
1092
+ };
1093
+ };
1094
+
1095
+ export const queryTopoStore = <TRow extends Record<string, unknown>>(
1096
+ db: Database,
1097
+ sql: string,
1098
+ bindings?: readonly SQLQueryBindings[]
1099
+ ): readonly TRow[] =>
1100
+ db
1101
+ .query<TRow, SQLQueryBindings[]>(sql)
1102
+ .all(...(bindings === undefined ? [] : [...bindings]));