@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.
- package/CHANGELOG.md +543 -0
- package/README.md +196 -0
- package/package.json +33 -0
- package/src/activation-report.ts +385 -0
- package/src/backend-support.ts +11 -0
- package/src/derive.ts +690 -0
- package/src/diff.ts +1088 -0
- package/src/forces.ts +125 -0
- package/src/hash.ts +55 -0
- package/src/index.ts +272 -0
- package/src/internal/topo-snapshots.ts +682 -0
- package/src/internal/topo-store-read.ts +1102 -0
- package/src/internal/topo-store.ts +1651 -0
- package/src/io.ts +280 -0
- package/src/library-projection.ts +133 -0
- package/src/overlays.ts +155 -0
- package/src/permit.ts +19 -0
- package/src/source-fingerprint.ts +103 -0
- package/src/surface-bindings.ts +101 -0
- package/src/topo-store.ts +716 -0
- package/src/types.ts +749 -0
- package/src/versioning.ts +280 -0
- package/src/wayfind/error-facts.ts +363 -0
- package/src/wayfind/filters.ts +430 -0
- package/src/wayfind/loader.ts +443 -0
- package/src/wayfind/navigation.ts +265 -0
- package/src/wayfind/provenance.ts +152 -0
- package/src/wayfind/queries.ts +1656 -0
- package/src/wayfind/relations.ts +344 -0
- package/src/workspace-topos.ts +483 -0
|
@@ -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]));
|