@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,1651 @@
|
|
|
1
|
+
import type { Database, SQLQueryBindings } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DETOUR_MAX_ATTEMPTS_CAP,
|
|
5
|
+
Result,
|
|
6
|
+
activationSourceKey,
|
|
7
|
+
deriveCliPath,
|
|
8
|
+
deriveStructuredSignalExamples,
|
|
9
|
+
deriveStructuredTrailExamples,
|
|
10
|
+
deriveTrailCliCommandProjection,
|
|
11
|
+
getEntityReferences,
|
|
12
|
+
projectActivationSourceDeclaration,
|
|
13
|
+
signalDiagnosticDefinitions,
|
|
14
|
+
validateEstablishedTopo,
|
|
15
|
+
zodToJsonSchema,
|
|
16
|
+
} from '@ontrails/core';
|
|
17
|
+
import type {
|
|
18
|
+
ActivationEntry,
|
|
19
|
+
ActivationSource,
|
|
20
|
+
AnyEntity,
|
|
21
|
+
AnyResource,
|
|
22
|
+
AnySignal,
|
|
23
|
+
AnyTrail,
|
|
24
|
+
CliSurfaceBindingAliases,
|
|
25
|
+
FieldOverride,
|
|
26
|
+
Layer,
|
|
27
|
+
Topo,
|
|
28
|
+
} from '@ontrails/core';
|
|
29
|
+
|
|
30
|
+
import { addPermitRequirement } from '../permit.js';
|
|
31
|
+
import { collectLibraryProjection } from '../library-projection.js';
|
|
32
|
+
import { collectTopoGraphOverlays } from '../overlays.js';
|
|
33
|
+
import {
|
|
34
|
+
resolveCliAliasInputsFromOverlays,
|
|
35
|
+
resolveTrailheadEntriesFromOverlays,
|
|
36
|
+
} from '../surface-bindings.js';
|
|
37
|
+
import { deriveStableHash } from '../hash.js';
|
|
38
|
+
import {
|
|
39
|
+
LOCK_MANIFEST_SCHEMA_VERSION,
|
|
40
|
+
TOPO_GRAPH_SCHEMA_VERSION,
|
|
41
|
+
} from '../types.js';
|
|
42
|
+
import { projectTrailVersions } from '../versioning.js';
|
|
43
|
+
import type {
|
|
44
|
+
LockManifest,
|
|
45
|
+
TopoGraphFieldOverride,
|
|
46
|
+
TopoGraphFieldOverrideKey,
|
|
47
|
+
TopoGraphLibraryProjection,
|
|
48
|
+
TopoGraphOverlays,
|
|
49
|
+
TopoGraphTrailheadEntry,
|
|
50
|
+
} from '../types.js';
|
|
51
|
+
import type {
|
|
52
|
+
CreateTopoSnapshotInput,
|
|
53
|
+
TopoSnapshot,
|
|
54
|
+
} from './topo-snapshots.js';
|
|
55
|
+
import {
|
|
56
|
+
ensureTopoSnapshotSchema,
|
|
57
|
+
insertTopoSnapshotRecord,
|
|
58
|
+
} from './topo-snapshots.js';
|
|
59
|
+
|
|
60
|
+
type TopoGraphEntryRecord = Readonly<Record<string, unknown>> & {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly kind: 'entity' | 'resource' | 'signal' | 'trail';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type TopoGraphRecord = Readonly<{
|
|
66
|
+
readonly activationGraph: ActivationGraphRecord;
|
|
67
|
+
readonly activationSources: Readonly<
|
|
68
|
+
Record<string, ActivationSourceCatalogRecord>
|
|
69
|
+
>;
|
|
70
|
+
readonly entries: readonly TopoGraphEntryRecord[];
|
|
71
|
+
readonly generatedAt: string;
|
|
72
|
+
readonly library: TopoGraphLibraryProjection;
|
|
73
|
+
readonly overlays?: TopoGraphOverlays | undefined;
|
|
74
|
+
readonly topoGraphSchemaVersion: typeof TOPO_GRAPH_SCHEMA_VERSION;
|
|
75
|
+
readonly trailheads?: readonly TopoGraphTrailheadEntry[] | undefined;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
79
|
+
type ZodSchemaInput = Parameters<typeof zodToJsonSchema>[0];
|
|
80
|
+
|
|
81
|
+
interface TopoTrailRow {
|
|
82
|
+
readonly description: string | null;
|
|
83
|
+
readonly exampleCount: number;
|
|
84
|
+
readonly hasExamples: number;
|
|
85
|
+
readonly hasOutput: number;
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly idempotent: number;
|
|
88
|
+
readonly intent: string;
|
|
89
|
+
readonly meta: string | null;
|
|
90
|
+
readonly pattern: string | null;
|
|
91
|
+
readonly snapshotId: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface TopoComposingRow {
|
|
95
|
+
readonly snapshotId: string;
|
|
96
|
+
readonly sourceId: string;
|
|
97
|
+
readonly targetId: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface TopoFiresRow {
|
|
101
|
+
readonly snapshotId: string;
|
|
102
|
+
readonly signalId: string;
|
|
103
|
+
readonly trailId: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface TopoOnRow {
|
|
107
|
+
readonly snapshotId: string;
|
|
108
|
+
readonly signalId: string;
|
|
109
|
+
readonly trailId: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface TopoActivationSourceRow {
|
|
113
|
+
readonly snapshotId: string;
|
|
114
|
+
readonly source: string;
|
|
115
|
+
readonly sourceId: string;
|
|
116
|
+
readonly sourceKey: string;
|
|
117
|
+
readonly sourceKind: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface TopoActivationEdgeRow {
|
|
121
|
+
readonly edge: string;
|
|
122
|
+
readonly hasWhere: number;
|
|
123
|
+
readonly snapshotId: string;
|
|
124
|
+
readonly sourceId: string;
|
|
125
|
+
readonly sourceKey: string;
|
|
126
|
+
readonly sourceKind: string;
|
|
127
|
+
readonly trailId: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface TopoTrailResourceRow {
|
|
131
|
+
readonly resourceId: string;
|
|
132
|
+
readonly snapshotId: string;
|
|
133
|
+
readonly trailId: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface TopoResourceRow {
|
|
137
|
+
readonly hasHealth: number;
|
|
138
|
+
readonly hasMock: number;
|
|
139
|
+
readonly id: string;
|
|
140
|
+
readonly snapshotId: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface TopoSignalRow {
|
|
144
|
+
readonly description: string | null;
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly snapshotId: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface TopoTrailSignalRow {
|
|
150
|
+
readonly snapshotId: string;
|
|
151
|
+
readonly signalId: string;
|
|
152
|
+
readonly trailId: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface TopoSurfaceRow {
|
|
156
|
+
readonly derivedName: string;
|
|
157
|
+
readonly method: string | null;
|
|
158
|
+
readonly snapshotId: string;
|
|
159
|
+
readonly trailId: string;
|
|
160
|
+
readonly surface: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface TopoExampleRow {
|
|
164
|
+
readonly description: string | null;
|
|
165
|
+
readonly error: string | null;
|
|
166
|
+
readonly expected: string | null;
|
|
167
|
+
readonly expectedMatch: string | null;
|
|
168
|
+
readonly id: string;
|
|
169
|
+
readonly input: string;
|
|
170
|
+
readonly name: string;
|
|
171
|
+
readonly ordinal: number;
|
|
172
|
+
readonly signals: string | null;
|
|
173
|
+
readonly snapshotId: string;
|
|
174
|
+
readonly trailId: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface TopoSchemaRow {
|
|
178
|
+
readonly jsonSchema: string;
|
|
179
|
+
readonly ownerId: string;
|
|
180
|
+
readonly ownerKind: 'signal' | 'trail';
|
|
181
|
+
readonly snapshotId: string;
|
|
182
|
+
readonly schemaKind: 'input' | 'output' | 'payload';
|
|
183
|
+
readonly zodHash: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface StoredTopoExportRow {
|
|
187
|
+
readonly snapshotId: string;
|
|
188
|
+
readonly lockManifest: string;
|
|
189
|
+
readonly topoGraphHash: string;
|
|
190
|
+
readonly topoGraph: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface StoredTopoExportDbRow {
|
|
194
|
+
readonly lock_manifest: string;
|
|
195
|
+
readonly topo_graph: string;
|
|
196
|
+
readonly topo_graph_hash: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface StoredTopoExport {
|
|
200
|
+
readonly lockManifestJson: string;
|
|
201
|
+
readonly topoGraphHash: string;
|
|
202
|
+
readonly topoGraphJson: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface MaterializedSchemas {
|
|
206
|
+
readonly rows: readonly TopoSchemaRow[];
|
|
207
|
+
readonly signalPayloads: ReadonlyMap<string, JsonRecord>;
|
|
208
|
+
readonly trailSchemas: ReadonlyMap<
|
|
209
|
+
string,
|
|
210
|
+
Readonly<{
|
|
211
|
+
readonly input: JsonRecord;
|
|
212
|
+
readonly output?: JsonRecord;
|
|
213
|
+
}>
|
|
214
|
+
>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface MaterializedTopoArtifacts {
|
|
218
|
+
readonly exportRow: StoredTopoExportRow;
|
|
219
|
+
readonly schemaRows: readonly TopoSchemaRow[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface SignalGraphRelations {
|
|
223
|
+
readonly consumers: readonly string[];
|
|
224
|
+
readonly producers: readonly string[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface ActivationSourceCatalogRecord extends JsonRecord {
|
|
228
|
+
readonly hasParse?: true;
|
|
229
|
+
readonly hasPayloadSchema?: true;
|
|
230
|
+
readonly id: string;
|
|
231
|
+
readonly kind: string;
|
|
232
|
+
readonly key: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface ActivationGraphEdgeRecord extends JsonRecord {
|
|
236
|
+
readonly hasWhere: boolean;
|
|
237
|
+
readonly sourceId: string;
|
|
238
|
+
readonly sourceKey: string;
|
|
239
|
+
readonly sourceKind: string;
|
|
240
|
+
readonly trailId: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface ActivationGraphRecord extends JsonRecord {
|
|
244
|
+
readonly edgeCount: number;
|
|
245
|
+
readonly edges: readonly ActivationGraphEdgeRecord[];
|
|
246
|
+
readonly sourceCount: number;
|
|
247
|
+
readonly sourceKeys: readonly string[];
|
|
248
|
+
readonly trailIds: readonly string[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const EMPTY_SIGNAL_RELATIONS: SignalGraphRelations = {
|
|
252
|
+
consumers: [],
|
|
253
|
+
producers: [],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const SIGNAL_DIAGNOSTIC_CODES = Object.keys(
|
|
257
|
+
signalDiagnosticDefinitions
|
|
258
|
+
).toSorted();
|
|
259
|
+
|
|
260
|
+
const SIGNAL_DIAGNOSTIC_HOOKS = {
|
|
261
|
+
codes: SIGNAL_DIAGNOSTIC_CODES,
|
|
262
|
+
strictMode: true,
|
|
263
|
+
} as const;
|
|
264
|
+
|
|
265
|
+
const SIGNAL_GOVERNANCE_HOOKS = {
|
|
266
|
+
consumers: 'trail.on',
|
|
267
|
+
payload: 'signal.payload',
|
|
268
|
+
producers: 'trail.fires',
|
|
269
|
+
} as const;
|
|
270
|
+
|
|
271
|
+
interface NormalizedTopoProjection {
|
|
272
|
+
readonly activationEdges: readonly TopoActivationEdgeRow[];
|
|
273
|
+
readonly activationSources: readonly TopoActivationSourceRow[];
|
|
274
|
+
readonly composings: readonly TopoComposingRow[];
|
|
275
|
+
readonly examples: readonly TopoExampleRow[];
|
|
276
|
+
readonly fires: readonly TopoFiresRow[];
|
|
277
|
+
readonly on: readonly TopoOnRow[];
|
|
278
|
+
readonly resources: readonly TopoResourceRow[];
|
|
279
|
+
readonly signals: readonly TopoSignalRow[];
|
|
280
|
+
readonly surfaces: readonly TopoSurfaceRow[];
|
|
281
|
+
readonly trailResources: readonly TopoTrailResourceRow[];
|
|
282
|
+
readonly trailSignals: readonly TopoTrailSignalRow[];
|
|
283
|
+
readonly trails: readonly TopoTrailRow[];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const canonicalLeaf = (value: unknown): unknown => {
|
|
287
|
+
switch (typeof value) {
|
|
288
|
+
case 'bigint': {
|
|
289
|
+
return value.toString();
|
|
290
|
+
}
|
|
291
|
+
case 'function': {
|
|
292
|
+
return `[Function:${value.name || 'anonymous'}]`;
|
|
293
|
+
}
|
|
294
|
+
case 'symbol': {
|
|
295
|
+
return `[Symbol:${value.description ?? ''}]`;
|
|
296
|
+
}
|
|
297
|
+
case 'undefined': {
|
|
298
|
+
return '[Undefined]';
|
|
299
|
+
}
|
|
300
|
+
default: {
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const canonicalObject = (
|
|
307
|
+
value: Record<string, unknown>,
|
|
308
|
+
visit: (value: unknown) => unknown
|
|
309
|
+
): Record<string, unknown> => {
|
|
310
|
+
const sorted: Record<string, unknown> = {};
|
|
311
|
+
for (const key of Object.keys(value).toSorted()) {
|
|
312
|
+
const next = value[key];
|
|
313
|
+
sorted[key] = next === undefined ? '[Undefined]' : visit(next);
|
|
314
|
+
}
|
|
315
|
+
return sorted;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const canonicalize = (value: unknown): unknown => {
|
|
319
|
+
if (Array.isArray(value)) {
|
|
320
|
+
return value.map(canonicalize);
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Date) {
|
|
323
|
+
return value.toISOString();
|
|
324
|
+
}
|
|
325
|
+
if (value instanceof RegExp) {
|
|
326
|
+
return value.toString();
|
|
327
|
+
}
|
|
328
|
+
if (value !== null && typeof value === 'object') {
|
|
329
|
+
return canonicalObject(value as Record<string, unknown>, canonicalize);
|
|
330
|
+
}
|
|
331
|
+
return canonicalLeaf(value);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Canonicalize a value for schema definition hashing.
|
|
336
|
+
*
|
|
337
|
+
* Matches the canonicalization logic used in `@ontrails/topography` so that
|
|
338
|
+
* schema hashes are consistent regardless of which code path computes them.
|
|
339
|
+
* Unlike the general `canonicalize`, this does not convert `Date`, `RegExp`,
|
|
340
|
+
* or `undefined` to sentinel strings — Zod `_zod.def` objects are plain
|
|
341
|
+
* JSON-serializable structures and both paths must agree on the same encoding.
|
|
342
|
+
*/
|
|
343
|
+
const schemaCanonical = (value: unknown): unknown => {
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return value.map(schemaCanonical);
|
|
346
|
+
}
|
|
347
|
+
if (value !== null && typeof value === 'object') {
|
|
348
|
+
const sorted: Record<string, unknown> = {};
|
|
349
|
+
for (const key of Object.keys(value).toSorted()) {
|
|
350
|
+
sorted[key] = schemaCanonical((value as Record<string, unknown>)[key]);
|
|
351
|
+
}
|
|
352
|
+
return sorted;
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const stableJson = (value: unknown): string =>
|
|
358
|
+
JSON.stringify(canonicalize(value));
|
|
359
|
+
|
|
360
|
+
const hashText = (text: string): string => {
|
|
361
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
362
|
+
hasher.update(text);
|
|
363
|
+
return hasher.digest('hex');
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const parseJsonRecord = (value: string): JsonRecord =>
|
|
367
|
+
JSON.parse(value) as JsonRecord;
|
|
368
|
+
|
|
369
|
+
const schemaDefinitionHash = (schema: unknown): string => {
|
|
370
|
+
const def =
|
|
371
|
+
typeof schema === 'object' &&
|
|
372
|
+
schema !== null &&
|
|
373
|
+
'_zod' in schema &&
|
|
374
|
+
typeof (schema as { readonly _zod: unknown })._zod === 'object' &&
|
|
375
|
+
(schema as { readonly _zod: Record<string, unknown> })._zod !== null &&
|
|
376
|
+
'def' in (schema as { readonly _zod: Record<string, unknown> })._zod
|
|
377
|
+
? (schema as { readonly _zod: { readonly def: unknown } })._zod.def
|
|
378
|
+
: schema;
|
|
379
|
+
return hashText(JSON.stringify(schemaCanonical(def)));
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const sortedJsonSchema = (
|
|
383
|
+
schema: ZodSchemaInput
|
|
384
|
+
): { readonly json: string; readonly value: JsonRecord } => {
|
|
385
|
+
const json = stableJson(zodToJsonSchema(schema));
|
|
386
|
+
return {
|
|
387
|
+
json,
|
|
388
|
+
value: parseJsonRecord(json),
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const sortKeys = <T extends Record<string, unknown>>(value: T): T => {
|
|
393
|
+
const sorted: Record<string, unknown> = {};
|
|
394
|
+
for (const key of Object.keys(value).toSorted()) {
|
|
395
|
+
sorted[key] = value[key];
|
|
396
|
+
}
|
|
397
|
+
return sorted as T;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const normalizeTrailRows = (
|
|
401
|
+
trails: readonly AnyTrail[],
|
|
402
|
+
snapshotId: string
|
|
403
|
+
): readonly TopoTrailRow[] =>
|
|
404
|
+
trails.map((trail) => ({
|
|
405
|
+
description: trail.description ?? null,
|
|
406
|
+
exampleCount: trail.examples?.length ?? 0,
|
|
407
|
+
hasExamples: (trail.examples?.length ?? 0) > 0 ? 1 : 0,
|
|
408
|
+
hasOutput: trail.output === undefined ? 0 : 1,
|
|
409
|
+
id: trail.id,
|
|
410
|
+
idempotent: trail.idempotent === true ? 1 : 0,
|
|
411
|
+
intent: trail.intent,
|
|
412
|
+
meta: trail.meta === undefined ? null : stableJson(trail.meta),
|
|
413
|
+
pattern: trail.pattern ?? null,
|
|
414
|
+
snapshotId,
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
const normalizeComposingRows = (
|
|
418
|
+
trails: readonly AnyTrail[],
|
|
419
|
+
snapshotId: string
|
|
420
|
+
): readonly TopoComposingRow[] =>
|
|
421
|
+
trails.flatMap((trail) =>
|
|
422
|
+
[...new Set(trail.composes)].toSorted().map((targetId) => ({
|
|
423
|
+
snapshotId,
|
|
424
|
+
sourceId: trail.id,
|
|
425
|
+
targetId,
|
|
426
|
+
}))
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
export const normalizeFiresRows = (
|
|
430
|
+
trails: readonly AnyTrail[],
|
|
431
|
+
snapshotId: string
|
|
432
|
+
): readonly TopoFiresRow[] =>
|
|
433
|
+
trails.flatMap((trail) =>
|
|
434
|
+
[...new Set(trail.fires)].toSorted().map((signalId) => ({
|
|
435
|
+
signalId,
|
|
436
|
+
snapshotId,
|
|
437
|
+
trailId: trail.id,
|
|
438
|
+
}))
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
export const normalizeOnRows = (
|
|
442
|
+
trails: readonly AnyTrail[],
|
|
443
|
+
snapshotId: string
|
|
444
|
+
): readonly TopoOnRow[] =>
|
|
445
|
+
trails.flatMap((trail) =>
|
|
446
|
+
[...new Set(trail.on)].toSorted().map((signalId) => ({
|
|
447
|
+
signalId,
|
|
448
|
+
snapshotId,
|
|
449
|
+
trailId: trail.id,
|
|
450
|
+
}))
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const projectActivationSource = (
|
|
454
|
+
source: ActivationSource
|
|
455
|
+
): ActivationSourceCatalogRecord =>
|
|
456
|
+
projectActivationSourceDeclaration(source) as ActivationSourceCatalogRecord;
|
|
457
|
+
|
|
458
|
+
const projectActivationEdge = (
|
|
459
|
+
trailId: string,
|
|
460
|
+
activation: ActivationEntry
|
|
461
|
+
): ActivationGraphEdgeRecord => {
|
|
462
|
+
const sourceKey = activationSourceKey(activation.source);
|
|
463
|
+
const edge: Record<string, unknown> = {
|
|
464
|
+
hasWhere: activation.where !== undefined,
|
|
465
|
+
sourceId: activation.source.id,
|
|
466
|
+
sourceKey,
|
|
467
|
+
sourceKind: activation.source.kind,
|
|
468
|
+
trailId,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
if (activation.meta !== undefined) {
|
|
472
|
+
edge['meta'] = canonicalize(activation.meta);
|
|
473
|
+
}
|
|
474
|
+
if (activation.where !== undefined) {
|
|
475
|
+
edge['where'] = { predicate: true };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return sortKeys(edge) as ActivationGraphEdgeRecord;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const collectActivationSourceCatalog = (
|
|
482
|
+
trails: readonly AnyTrail[]
|
|
483
|
+
): readonly ActivationSourceCatalogRecord[] => {
|
|
484
|
+
const sources = new Map<string, ActivationSourceCatalogRecord>();
|
|
485
|
+
for (const trail of trails) {
|
|
486
|
+
for (const activation of trail.activationSources) {
|
|
487
|
+
const projected = projectActivationSource(activation.source);
|
|
488
|
+
sources.set(projected.key, projected);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return [...sources.values()].toSorted((a, b) => a.key.localeCompare(b.key));
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const collectActivationGraphEdges = (
|
|
495
|
+
trails: readonly AnyTrail[]
|
|
496
|
+
): readonly ActivationGraphEdgeRecord[] => {
|
|
497
|
+
const edges = new Map<string, ActivationGraphEdgeRecord>();
|
|
498
|
+
for (const trail of trails) {
|
|
499
|
+
for (const activation of trail.activationSources) {
|
|
500
|
+
const edge = projectActivationEdge(trail.id, activation);
|
|
501
|
+
const key = `${edge.sourceKey}\0${edge.trailId}`;
|
|
502
|
+
const previous = edges.get(key);
|
|
503
|
+
edges.set(
|
|
504
|
+
key,
|
|
505
|
+
previous === undefined || (!previous.hasWhere && edge.hasWhere)
|
|
506
|
+
? edge
|
|
507
|
+
: previous
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return [...edges.values()].toSorted(
|
|
513
|
+
(a, b) =>
|
|
514
|
+
a.sourceKey.localeCompare(b.sourceKey) ||
|
|
515
|
+
a.trailId.localeCompare(b.trailId)
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const buildActivationGraphRecord = (
|
|
520
|
+
edges: readonly ActivationGraphEdgeRecord[],
|
|
521
|
+
sources: readonly ActivationSourceCatalogRecord[]
|
|
522
|
+
): ActivationGraphRecord =>
|
|
523
|
+
sortKeys({
|
|
524
|
+
edgeCount: edges.length,
|
|
525
|
+
edges,
|
|
526
|
+
sourceCount: sources.length,
|
|
527
|
+
sourceKeys: sources.map((source) => source.key),
|
|
528
|
+
trailIds: [...new Set(edges.map((edge) => edge.trailId))].toSorted(),
|
|
529
|
+
}) as ActivationGraphRecord;
|
|
530
|
+
|
|
531
|
+
export const normalizeActivationSourceRows = (
|
|
532
|
+
trails: readonly AnyTrail[],
|
|
533
|
+
snapshotId: string
|
|
534
|
+
): readonly TopoActivationSourceRow[] =>
|
|
535
|
+
collectActivationSourceCatalog(trails).map((source) => ({
|
|
536
|
+
snapshotId,
|
|
537
|
+
source: stableJson(source),
|
|
538
|
+
sourceId: source.id,
|
|
539
|
+
sourceKey: source.key,
|
|
540
|
+
sourceKind: source.kind,
|
|
541
|
+
}));
|
|
542
|
+
|
|
543
|
+
export const normalizeActivationEdgeRows = (
|
|
544
|
+
trails: readonly AnyTrail[],
|
|
545
|
+
snapshotId: string
|
|
546
|
+
): readonly TopoActivationEdgeRow[] =>
|
|
547
|
+
collectActivationGraphEdges(trails).map((edge) => ({
|
|
548
|
+
edge: stableJson(edge),
|
|
549
|
+
hasWhere: edge.hasWhere ? 1 : 0,
|
|
550
|
+
snapshotId,
|
|
551
|
+
sourceId: edge.sourceId,
|
|
552
|
+
sourceKey: edge.sourceKey,
|
|
553
|
+
sourceKind: edge.sourceKind,
|
|
554
|
+
trailId: edge.trailId,
|
|
555
|
+
}));
|
|
556
|
+
|
|
557
|
+
const normalizeTrailResourceRows = (
|
|
558
|
+
trails: readonly AnyTrail[],
|
|
559
|
+
snapshotId: string
|
|
560
|
+
): readonly TopoTrailResourceRow[] =>
|
|
561
|
+
trails.flatMap((trail) =>
|
|
562
|
+
[...new Set(trail.resources.map((resource) => resource.id))]
|
|
563
|
+
.toSorted()
|
|
564
|
+
.map((resourceId) => ({
|
|
565
|
+
resourceId,
|
|
566
|
+
snapshotId,
|
|
567
|
+
trailId: trail.id,
|
|
568
|
+
}))
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const normalizeResourceRows = (
|
|
572
|
+
resources: readonly AnyResource[],
|
|
573
|
+
snapshotId: string
|
|
574
|
+
): readonly TopoResourceRow[] =>
|
|
575
|
+
resources.map((resource) => ({
|
|
576
|
+
hasHealth: resource.health === undefined ? 0 : 1,
|
|
577
|
+
hasMock: resource.mock === undefined ? 0 : 1,
|
|
578
|
+
id: resource.id,
|
|
579
|
+
snapshotId,
|
|
580
|
+
}));
|
|
581
|
+
|
|
582
|
+
const normalizeSignalRows = (
|
|
583
|
+
signals: readonly AnySignal[],
|
|
584
|
+
snapshotId: string
|
|
585
|
+
): readonly TopoSignalRow[] =>
|
|
586
|
+
signals.map((signal) => ({
|
|
587
|
+
description: signal.description ?? null,
|
|
588
|
+
id: signal.id,
|
|
589
|
+
snapshotId,
|
|
590
|
+
}));
|
|
591
|
+
|
|
592
|
+
const normalizeTrailSignalRows = (
|
|
593
|
+
signals: readonly AnySignal[],
|
|
594
|
+
snapshotId: string
|
|
595
|
+
): readonly TopoTrailSignalRow[] =>
|
|
596
|
+
signals.flatMap((signal) => {
|
|
597
|
+
if (signal.from === undefined) {
|
|
598
|
+
return [];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return [...new Set(signal.from)].toSorted().map((trailId) => ({
|
|
602
|
+
signalId: signal.id,
|
|
603
|
+
snapshotId,
|
|
604
|
+
trailId,
|
|
605
|
+
}));
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Project surface rows for stored topo.
|
|
610
|
+
*
|
|
611
|
+
* Currently records only CLI-derived rows. MCP, HTTP, and other surface
|
|
612
|
+
* projections are intentionally deferred until the topo-store schema supports
|
|
613
|
+
* multi-surface representation. The JSON export (`topo_graph` in
|
|
614
|
+
* `topo_exports`) is more faithful for now. See ADR-0015 for the target shape.
|
|
615
|
+
*/
|
|
616
|
+
const normalizeSurfaceRows = (
|
|
617
|
+
trails: readonly AnyTrail[],
|
|
618
|
+
snapshotId: string
|
|
619
|
+
): readonly TopoSurfaceRow[] =>
|
|
620
|
+
trails.map((trail) => ({
|
|
621
|
+
derivedName: deriveCliPath(trail.id).join(' '),
|
|
622
|
+
method: null,
|
|
623
|
+
snapshotId,
|
|
624
|
+
surface: 'cli',
|
|
625
|
+
trailId: trail.id,
|
|
626
|
+
}));
|
|
627
|
+
|
|
628
|
+
const buildExampleId = (
|
|
629
|
+
snapshotId: string,
|
|
630
|
+
trailId: string,
|
|
631
|
+
ordinal: number
|
|
632
|
+
): string => `${snapshotId}:${trailId}:${ordinal.toString().padStart(4, '0')}`;
|
|
633
|
+
|
|
634
|
+
const normalizeExampleRows = (
|
|
635
|
+
trails: readonly AnyTrail[],
|
|
636
|
+
snapshotId: string
|
|
637
|
+
): readonly TopoExampleRow[] =>
|
|
638
|
+
trails.flatMap((trail) => {
|
|
639
|
+
const examples = deriveStructuredTrailExamples(trail.examples) ?? [];
|
|
640
|
+
return examples.map((example, index) => ({
|
|
641
|
+
description: example.description ?? null,
|
|
642
|
+
error: example.error ?? null,
|
|
643
|
+
expected:
|
|
644
|
+
example.expected === undefined ? null : stableJson(example.expected),
|
|
645
|
+
expectedMatch:
|
|
646
|
+
example.expectedMatch === undefined
|
|
647
|
+
? null
|
|
648
|
+
: stableJson(example.expectedMatch),
|
|
649
|
+
id: buildExampleId(snapshotId, trail.id, index),
|
|
650
|
+
input: stableJson(example.input),
|
|
651
|
+
name: example.name,
|
|
652
|
+
ordinal: index,
|
|
653
|
+
signals:
|
|
654
|
+
example.signals === undefined ? null : stableJson(example.signals),
|
|
655
|
+
snapshotId,
|
|
656
|
+
trailId: trail.id,
|
|
657
|
+
}));
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const normalizeTopoProjection = (
|
|
661
|
+
topo: Topo,
|
|
662
|
+
snapshotId: string
|
|
663
|
+
): NormalizedTopoProjection => {
|
|
664
|
+
const trails = topo.list().toSorted((a, b) => a.id.localeCompare(b.id));
|
|
665
|
+
const resources = topo
|
|
666
|
+
.listResources()
|
|
667
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
668
|
+
const signals = topo
|
|
669
|
+
.listSignals()
|
|
670
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
activationEdges: normalizeActivationEdgeRows(trails, snapshotId),
|
|
674
|
+
activationSources: normalizeActivationSourceRows(trails, snapshotId),
|
|
675
|
+
composings: normalizeComposingRows(trails, snapshotId),
|
|
676
|
+
examples: normalizeExampleRows(trails, snapshotId),
|
|
677
|
+
fires: normalizeFiresRows(trails, snapshotId),
|
|
678
|
+
on: normalizeOnRows(trails, snapshotId),
|
|
679
|
+
resources: normalizeResourceRows(resources, snapshotId),
|
|
680
|
+
signals: normalizeSignalRows(signals, snapshotId),
|
|
681
|
+
surfaces: normalizeSurfaceRows(trails, snapshotId),
|
|
682
|
+
trailResources: normalizeTrailResourceRows(trails, snapshotId),
|
|
683
|
+
trailSignals: normalizeTrailSignalRows(signals, snapshotId),
|
|
684
|
+
trails: normalizeTrailRows(trails, snapshotId),
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Materialize the JSON Schema row for an owner, always generating fresh
|
|
690
|
+
* output from the live Zod schema.
|
|
691
|
+
*
|
|
692
|
+
* Earlier revisions reused a previously stored `json_schema` when the
|
|
693
|
+
* `zod_hash` matched, across all snapshots in the store. That hash is
|
|
694
|
+
* computed from `JSON.stringify` over the Zod definition, which cannot see
|
|
695
|
+
* `.describe()` metadata or object field order, so schema edits that change
|
|
696
|
+
* the generated JSON Schema could keep serving pre-edit bytes and make a
|
|
697
|
+
* freshly compiled lock diverge from a fresh derivation (TRL-1191). The
|
|
698
|
+
* `zod_hash` column remains as per-snapshot provenance only — it is never
|
|
699
|
+
* a substitute for generation.
|
|
700
|
+
*/
|
|
701
|
+
const resolveSchemaRow = (
|
|
702
|
+
_db: Database,
|
|
703
|
+
ownerId: string,
|
|
704
|
+
ownerKind: TopoSchemaRow['ownerKind'],
|
|
705
|
+
snapshotId: string,
|
|
706
|
+
schemaKind: TopoSchemaRow['schemaKind'],
|
|
707
|
+
schema: ZodSchemaInput
|
|
708
|
+
): {
|
|
709
|
+
readonly row: TopoSchemaRow;
|
|
710
|
+
readonly value: JsonRecord;
|
|
711
|
+
} => {
|
|
712
|
+
const zodHash = schemaDefinitionHash(schema);
|
|
713
|
+
const generated = sortedJsonSchema(schema);
|
|
714
|
+
return {
|
|
715
|
+
row: {
|
|
716
|
+
jsonSchema: generated.json,
|
|
717
|
+
ownerId,
|
|
718
|
+
ownerKind,
|
|
719
|
+
schemaKind,
|
|
720
|
+
snapshotId,
|
|
721
|
+
zodHash,
|
|
722
|
+
},
|
|
723
|
+
value: generated.value,
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const materializeTrailSchema = (
|
|
728
|
+
db: Database,
|
|
729
|
+
snapshotId: string,
|
|
730
|
+
trail: AnyTrail
|
|
731
|
+
): {
|
|
732
|
+
readonly rows: readonly TopoSchemaRow[];
|
|
733
|
+
readonly value: Readonly<{
|
|
734
|
+
readonly input: JsonRecord;
|
|
735
|
+
readonly output?: JsonRecord;
|
|
736
|
+
}>;
|
|
737
|
+
} => {
|
|
738
|
+
const inputSchema = resolveSchemaRow(
|
|
739
|
+
db,
|
|
740
|
+
trail.id,
|
|
741
|
+
'trail',
|
|
742
|
+
snapshotId,
|
|
743
|
+
'input',
|
|
744
|
+
trail.input as ZodSchemaInput
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
if (trail.output === undefined) {
|
|
748
|
+
return {
|
|
749
|
+
rows: [inputSchema.row],
|
|
750
|
+
value: {
|
|
751
|
+
input: inputSchema.value,
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const outputSchema = resolveSchemaRow(
|
|
757
|
+
db,
|
|
758
|
+
trail.id,
|
|
759
|
+
'trail',
|
|
760
|
+
snapshotId,
|
|
761
|
+
'output',
|
|
762
|
+
trail.output as ZodSchemaInput
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
rows: [inputSchema.row, outputSchema.row],
|
|
767
|
+
value: {
|
|
768
|
+
input: inputSchema.value,
|
|
769
|
+
output: outputSchema.value,
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const materializeTrailSchemas = (
|
|
775
|
+
db: Database,
|
|
776
|
+
snapshotId: string,
|
|
777
|
+
trails: readonly AnyTrail[]
|
|
778
|
+
): Pick<MaterializedSchemas, 'rows' | 'trailSchemas'> => {
|
|
779
|
+
const rows: TopoSchemaRow[] = [];
|
|
780
|
+
const trailSchemas = new Map<
|
|
781
|
+
string,
|
|
782
|
+
Readonly<{
|
|
783
|
+
readonly input: JsonRecord;
|
|
784
|
+
readonly output?: JsonRecord;
|
|
785
|
+
}>
|
|
786
|
+
>();
|
|
787
|
+
|
|
788
|
+
for (const trail of trails) {
|
|
789
|
+
const materialized = materializeTrailSchema(db, snapshotId, trail);
|
|
790
|
+
rows.push(...materialized.rows);
|
|
791
|
+
trailSchemas.set(trail.id, materialized.value);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
rows,
|
|
796
|
+
trailSchemas,
|
|
797
|
+
};
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const materializeSignalSchemas = (
|
|
801
|
+
db: Database,
|
|
802
|
+
snapshotId: string,
|
|
803
|
+
signals: readonly AnySignal[]
|
|
804
|
+
): Pick<MaterializedSchemas, 'rows' | 'signalPayloads'> => {
|
|
805
|
+
const rows: TopoSchemaRow[] = [];
|
|
806
|
+
const signalPayloads = new Map<string, JsonRecord>();
|
|
807
|
+
|
|
808
|
+
for (const signal of signals) {
|
|
809
|
+
const payloadSchema = resolveSchemaRow(
|
|
810
|
+
db,
|
|
811
|
+
signal.id,
|
|
812
|
+
'signal',
|
|
813
|
+
snapshotId,
|
|
814
|
+
'payload',
|
|
815
|
+
signal.payload as ZodSchemaInput
|
|
816
|
+
);
|
|
817
|
+
rows.push(payloadSchema.row);
|
|
818
|
+
signalPayloads.set(signal.id, payloadSchema.value);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
rows,
|
|
823
|
+
signalPayloads,
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const materializeSchemas = (
|
|
828
|
+
db: Database,
|
|
829
|
+
snapshotId: string,
|
|
830
|
+
signals: readonly AnySignal[],
|
|
831
|
+
trails: readonly AnyTrail[]
|
|
832
|
+
): MaterializedSchemas => {
|
|
833
|
+
const trailMaterial = materializeTrailSchemas(db, snapshotId, trails);
|
|
834
|
+
const signalMaterial = materializeSignalSchemas(db, snapshotId, signals);
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
rows: [...trailMaterial.rows, ...signalMaterial.rows],
|
|
838
|
+
signalPayloads: signalMaterial.signalPayloads,
|
|
839
|
+
trailSchemas: trailMaterial.trailSchemas,
|
|
840
|
+
};
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const extractSurfaces = (raw: Record<string, unknown>): string[] =>
|
|
844
|
+
Array.isArray(raw['surfaces'])
|
|
845
|
+
? (raw['surfaces'] as string[]).toSorted()
|
|
846
|
+
: [];
|
|
847
|
+
|
|
848
|
+
const addSafetyMarkers = (
|
|
849
|
+
entry: Record<string, unknown>,
|
|
850
|
+
trail: AnyTrail
|
|
851
|
+
): void => {
|
|
852
|
+
if (trail.intent !== 'write') {
|
|
853
|
+
entry['intent'] = trail.intent;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (trail.idempotent === true) {
|
|
857
|
+
entry['idempotent'] = true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (trail.dryRun === true) {
|
|
861
|
+
entry['dryRunCapable'] = true;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const addExtendedMetadata = (
|
|
866
|
+
entry: Record<string, unknown>,
|
|
867
|
+
raw: Record<string, unknown>,
|
|
868
|
+
trail: AnyTrail
|
|
869
|
+
): void => {
|
|
870
|
+
if (raw['deprecated'] === true) {
|
|
871
|
+
entry['deprecated'] = true;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (typeof raw['replacedBy'] === 'string') {
|
|
875
|
+
entry['replacedBy'] = raw['replacedBy'];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (trail.detours.length > 0) {
|
|
879
|
+
entry['detours'] = trail.detours.map((d) => ({
|
|
880
|
+
maxAttempts: Math.max(
|
|
881
|
+
1,
|
|
882
|
+
Math.min(d.maxAttempts ?? 1, DETOUR_MAX_ATTEMPTS_CAP)
|
|
883
|
+
),
|
|
884
|
+
on: d.on.name,
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const addTrailRelations = (
|
|
890
|
+
entry: Record<string, unknown>,
|
|
891
|
+
trail: AnyTrail
|
|
892
|
+
): void => {
|
|
893
|
+
if (trail.activationSources.length > 0) {
|
|
894
|
+
entry['activationSources'] = trail.activationSources.map((activation) =>
|
|
895
|
+
sortKeys({
|
|
896
|
+
...(activation.meta === undefined
|
|
897
|
+
? {}
|
|
898
|
+
: { meta: canonicalize(activation.meta) }),
|
|
899
|
+
source: projectActivationSource(activation.source),
|
|
900
|
+
...(activation.where === undefined
|
|
901
|
+
? {}
|
|
902
|
+
: { where: sortKeys({ predicate: true }) }),
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (trail.composes.length > 0) {
|
|
908
|
+
entry['composes'] = trail.composes.toSorted();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (trail.fires.length > 0) {
|
|
912
|
+
entry['fires'] = trail.fires.toSorted();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (trail.on.length > 0) {
|
|
916
|
+
entry['on'] = trail.on.toSorted();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (trail.entities.length > 0) {
|
|
920
|
+
entry['entities'] = trail.entities.map((entity) => entity.name).toSorted();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (trail.resources.length > 0) {
|
|
924
|
+
entry['resources'] = trail.resources
|
|
925
|
+
.map((resource) => resource.id)
|
|
926
|
+
.toSorted();
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const layerToReferenceRecord = (
|
|
931
|
+
scope: 'topo' | 'trail',
|
|
932
|
+
layer: Layer
|
|
933
|
+
): Record<string, unknown> => {
|
|
934
|
+
const reference: Record<string, unknown> = {
|
|
935
|
+
name: layer.name,
|
|
936
|
+
scope,
|
|
937
|
+
};
|
|
938
|
+
if (layer.input !== undefined) {
|
|
939
|
+
reference['input'] = parseJsonRecord(
|
|
940
|
+
stableJson(zodToJsonSchema(layer.input as ZodSchemaInput))
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return sortKeys(reference);
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const addLayerAttachments = (
|
|
947
|
+
entry: Record<string, unknown>,
|
|
948
|
+
topoLayers: readonly Layer[],
|
|
949
|
+
trail: AnyTrail
|
|
950
|
+
): void => {
|
|
951
|
+
const layers = [
|
|
952
|
+
...topoLayers.map((layer) => layerToReferenceRecord('topo', layer)),
|
|
953
|
+
...trail.layers.map((layer) => layerToReferenceRecord('trail', layer)),
|
|
954
|
+
].toSorted((left, right) => {
|
|
955
|
+
const scope = String(left['scope']).localeCompare(String(right['scope']));
|
|
956
|
+
return scope === 0
|
|
957
|
+
? String(left['name']).localeCompare(String(right['name']))
|
|
958
|
+
: scope;
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
if (layers.length > 0) {
|
|
962
|
+
entry['layers'] = layers;
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const FIELD_OVERRIDE_KEYS: readonly TopoGraphFieldOverrideKey[] = [
|
|
967
|
+
'hint',
|
|
968
|
+
'label',
|
|
969
|
+
'message',
|
|
970
|
+
'options',
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
const collectFieldOverrideKeys = (
|
|
974
|
+
override: FieldOverride
|
|
975
|
+
): readonly TopoGraphFieldOverrideKey[] =>
|
|
976
|
+
FIELD_OVERRIDE_KEYS.filter((key) => override[key] !== undefined);
|
|
977
|
+
|
|
978
|
+
const deriveFieldOverrides = (
|
|
979
|
+
fields: Readonly<Record<string, FieldOverride>> | undefined
|
|
980
|
+
): readonly TopoGraphFieldOverride[] | undefined => {
|
|
981
|
+
if (fields === undefined) {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const overrides = Object.entries(fields)
|
|
986
|
+
.flatMap(([field, override]) => {
|
|
987
|
+
const overrideKeys = collectFieldOverrideKeys(override);
|
|
988
|
+
if (overrideKeys.length === 0) {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
return [
|
|
992
|
+
{
|
|
993
|
+
field,
|
|
994
|
+
overrides: overrideKeys,
|
|
995
|
+
provenance: { source: 'trail.fields' as const },
|
|
996
|
+
},
|
|
997
|
+
];
|
|
998
|
+
})
|
|
999
|
+
.toSorted((a, b) => a.field.localeCompare(b.field));
|
|
1000
|
+
|
|
1001
|
+
return overrides.length > 0 ? overrides : undefined;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
const addFieldOverrides = (
|
|
1005
|
+
entry: Record<string, unknown>,
|
|
1006
|
+
trail: AnyTrail
|
|
1007
|
+
): void => {
|
|
1008
|
+
const fieldOverrides = deriveFieldOverrides(trail.fields);
|
|
1009
|
+
if (fieldOverrides !== undefined) {
|
|
1010
|
+
entry['fieldOverrides'] = fieldOverrides;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const buildTrailEntryBase = (
|
|
1015
|
+
trail: AnyTrail,
|
|
1016
|
+
trailSchema: Readonly<{
|
|
1017
|
+
readonly input: JsonRecord;
|
|
1018
|
+
readonly output?: JsonRecord;
|
|
1019
|
+
}>,
|
|
1020
|
+
surfaceAliases: CliSurfaceBindingAliases | undefined
|
|
1021
|
+
): {
|
|
1022
|
+
readonly entry: Record<string, unknown>;
|
|
1023
|
+
readonly raw: Record<string, unknown>;
|
|
1024
|
+
} => {
|
|
1025
|
+
const raw = trail as unknown as Record<string, unknown>;
|
|
1026
|
+
const entry: Record<string, unknown> = {
|
|
1027
|
+
cli: deriveTrailCliCommandProjection(trail, {
|
|
1028
|
+
aliasSource: 'surface',
|
|
1029
|
+
aliases: surfaceAliases?.[trail.id],
|
|
1030
|
+
}),
|
|
1031
|
+
exampleCount: trail.examples?.length ?? 0,
|
|
1032
|
+
id: trail.id,
|
|
1033
|
+
input: trailSchema.input,
|
|
1034
|
+
kind: trail.kind,
|
|
1035
|
+
surfaces: extractSurfaces(raw),
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
if (trailSchema.output !== undefined) {
|
|
1039
|
+
entry['output'] = trailSchema.output;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const examples = deriveStructuredTrailExamples(trail.examples);
|
|
1043
|
+
if (examples !== undefined) {
|
|
1044
|
+
entry['examples'] = examples;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (trail.description !== undefined) {
|
|
1048
|
+
entry['description'] = trail.description;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (trail.pattern !== undefined) {
|
|
1052
|
+
entry['pattern'] = trail.pattern;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
entry,
|
|
1057
|
+
raw,
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const addVersioning = (
|
|
1062
|
+
entry: Record<string, unknown>,
|
|
1063
|
+
trail: AnyTrail
|
|
1064
|
+
): void => {
|
|
1065
|
+
if (trail.version === undefined) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const projection = projectTrailVersions(
|
|
1069
|
+
trail,
|
|
1070
|
+
(schema) => sortedJsonSchema(schema as ZodSchemaInput).value
|
|
1071
|
+
);
|
|
1072
|
+
if (projection === undefined) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
entry['marker'] = projection.marker;
|
|
1077
|
+
entry['supports'] = projection.supports;
|
|
1078
|
+
entry['version'] = projection.version;
|
|
1079
|
+
if (projection.versions !== undefined) {
|
|
1080
|
+
entry['versions'] = projection.versions;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const trailToEntryRecord = (
|
|
1085
|
+
trail: AnyTrail,
|
|
1086
|
+
topoLayers: readonly Layer[],
|
|
1087
|
+
trailSchema: Readonly<{
|
|
1088
|
+
readonly input: JsonRecord;
|
|
1089
|
+
readonly output?: JsonRecord;
|
|
1090
|
+
}>,
|
|
1091
|
+
surfaceAliases: CliSurfaceBindingAliases | undefined
|
|
1092
|
+
): TopoGraphEntryRecord => {
|
|
1093
|
+
const { entry, raw } = buildTrailEntryBase(
|
|
1094
|
+
trail,
|
|
1095
|
+
trailSchema,
|
|
1096
|
+
surfaceAliases
|
|
1097
|
+
);
|
|
1098
|
+
addSafetyMarkers(entry, trail);
|
|
1099
|
+
addPermitRequirement(entry, trail);
|
|
1100
|
+
addExtendedMetadata(entry, raw, trail);
|
|
1101
|
+
addTrailRelations(entry, trail);
|
|
1102
|
+
addLayerAttachments(entry, topoLayers, trail);
|
|
1103
|
+
addFieldOverrides(entry, trail);
|
|
1104
|
+
addVersioning(entry, trail);
|
|
1105
|
+
return sortKeys(entry) as TopoGraphEntryRecord;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const signalToEntryRecord = (
|
|
1109
|
+
signal: AnySignal,
|
|
1110
|
+
payloadSchema: JsonRecord,
|
|
1111
|
+
relations: SignalGraphRelations
|
|
1112
|
+
): TopoGraphEntryRecord => {
|
|
1113
|
+
const raw = signal as unknown as Record<string, unknown>;
|
|
1114
|
+
const examples = deriveStructuredSignalExamples(signal.examples);
|
|
1115
|
+
const entry: Record<string, unknown> = {
|
|
1116
|
+
consumers: relations.consumers,
|
|
1117
|
+
diagnostics: SIGNAL_DIAGNOSTIC_HOOKS,
|
|
1118
|
+
exampleCount: signal.examples?.length ?? 0,
|
|
1119
|
+
governance: SIGNAL_GOVERNANCE_HOOKS,
|
|
1120
|
+
id: signal.id,
|
|
1121
|
+
input: payloadSchema,
|
|
1122
|
+
kind: 'signal',
|
|
1123
|
+
payload: payloadSchema,
|
|
1124
|
+
producers: relations.producers,
|
|
1125
|
+
surfaces: extractSurfaces(raw),
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
if (signal.description !== undefined) {
|
|
1129
|
+
entry['description'] = signal.description;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (signal.meta !== undefined) {
|
|
1133
|
+
entry['meta'] = signal.meta;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (examples !== undefined && examples.length > 0) {
|
|
1137
|
+
entry['examples'] = examples;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (signal.from !== undefined && signal.from.length > 0) {
|
|
1141
|
+
entry['from'] = signal.from.toSorted();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (raw['deprecated'] === true) {
|
|
1145
|
+
entry['deprecated'] = true;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (typeof raw['replacedBy'] === 'string') {
|
|
1149
|
+
entry['replacedBy'] = raw['replacedBy'];
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return sortKeys(entry) as TopoGraphEntryRecord;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
const resourceToEntryRecord = (resource: AnyResource): TopoGraphEntryRecord => {
|
|
1156
|
+
const entry: Record<string, unknown> = {
|
|
1157
|
+
exampleCount: 0,
|
|
1158
|
+
id: resource.id,
|
|
1159
|
+
kind: 'resource',
|
|
1160
|
+
surfaces: [],
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
if (resource.description !== undefined) {
|
|
1164
|
+
entry['description'] = resource.description;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (resource.health !== undefined) {
|
|
1168
|
+
entry['healthcheck'] = true;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return sortKeys(entry) as TopoGraphEntryRecord;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const entityToEntryRecord = (entity: AnyEntity): TopoGraphEntryRecord => {
|
|
1175
|
+
const schema = sortedJsonSchema(entity);
|
|
1176
|
+
const entry: Record<string, unknown> = {
|
|
1177
|
+
exampleCount: entity.examples?.length ?? 0,
|
|
1178
|
+
id: entity.name,
|
|
1179
|
+
identity: entity.identity,
|
|
1180
|
+
kind: 'entity',
|
|
1181
|
+
schema: schema.value,
|
|
1182
|
+
surfaces: [],
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const references = getEntityReferences(entity);
|
|
1186
|
+
if (references.length > 0) {
|
|
1187
|
+
entry['references'] = references;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return sortKeys(entry) as TopoGraphEntryRecord;
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const requireTrailSchema = (
|
|
1194
|
+
trailSchemas: MaterializedSchemas['trailSchemas'],
|
|
1195
|
+
trailId: string
|
|
1196
|
+
): Readonly<{
|
|
1197
|
+
readonly input: JsonRecord;
|
|
1198
|
+
readonly output?: JsonRecord;
|
|
1199
|
+
}> => {
|
|
1200
|
+
const schema = trailSchemas.get(trailId);
|
|
1201
|
+
if (schema === undefined) {
|
|
1202
|
+
throw new Error(`Missing cached trail schema for "${trailId}"`);
|
|
1203
|
+
}
|
|
1204
|
+
return schema;
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const requireSignalPayload = (
|
|
1208
|
+
signalPayloads: MaterializedSchemas['signalPayloads'],
|
|
1209
|
+
signalId: string
|
|
1210
|
+
): JsonRecord => {
|
|
1211
|
+
const payload = signalPayloads.get(signalId);
|
|
1212
|
+
if (payload === undefined) {
|
|
1213
|
+
throw new Error(`Missing cached signal schema for "${signalId}"`);
|
|
1214
|
+
}
|
|
1215
|
+
return payload;
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
const collectSignalGraphRelations = (
|
|
1219
|
+
signals: readonly AnySignal[],
|
|
1220
|
+
trails: readonly AnyTrail[]
|
|
1221
|
+
): ReadonlyMap<string, SignalGraphRelations> => {
|
|
1222
|
+
const producers = new Map<string, Set<string>>();
|
|
1223
|
+
const consumers = new Map<string, Set<string>>();
|
|
1224
|
+
|
|
1225
|
+
for (const trail of trails) {
|
|
1226
|
+
for (const signalId of trail.fires) {
|
|
1227
|
+
const current = producers.get(signalId) ?? new Set<string>();
|
|
1228
|
+
current.add(trail.id);
|
|
1229
|
+
producers.set(signalId, current);
|
|
1230
|
+
}
|
|
1231
|
+
for (const signalId of trail.on) {
|
|
1232
|
+
const current = consumers.get(signalId) ?? new Set<string>();
|
|
1233
|
+
current.add(trail.id);
|
|
1234
|
+
consumers.set(signalId, current);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return new Map(
|
|
1239
|
+
signals.map((signal) => [
|
|
1240
|
+
signal.id,
|
|
1241
|
+
{
|
|
1242
|
+
consumers: [...(consumers.get(signal.id) ?? [])].toSorted(),
|
|
1243
|
+
producers: [...(producers.get(signal.id) ?? [])].toSorted(),
|
|
1244
|
+
},
|
|
1245
|
+
])
|
|
1246
|
+
);
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
const buildTopoGraph = (
|
|
1250
|
+
entities: readonly AnyEntity[],
|
|
1251
|
+
generatedAt: string,
|
|
1252
|
+
resources: readonly AnyResource[],
|
|
1253
|
+
signalPayloads: ReadonlyMap<string, JsonRecord>,
|
|
1254
|
+
signals: readonly AnySignal[],
|
|
1255
|
+
topo: Topo,
|
|
1256
|
+
topoLayers: readonly Layer[],
|
|
1257
|
+
trailSchemas: ReadonlyMap<
|
|
1258
|
+
string,
|
|
1259
|
+
Readonly<{
|
|
1260
|
+
readonly input: JsonRecord;
|
|
1261
|
+
readonly output?: JsonRecord;
|
|
1262
|
+
}>
|
|
1263
|
+
>,
|
|
1264
|
+
trails: readonly AnyTrail[],
|
|
1265
|
+
options?: Pick<CreateTopoSnapshotInput, 'overlays'> | undefined
|
|
1266
|
+
): TopoGraphRecord => {
|
|
1267
|
+
const overlays = collectTopoGraphOverlays(topo, options?.overlays);
|
|
1268
|
+
const surfaceAliases = resolveCliAliasInputsFromOverlays(
|
|
1269
|
+
topo,
|
|
1270
|
+
options?.overlays
|
|
1271
|
+
);
|
|
1272
|
+
const trailheads = resolveTrailheadEntriesFromOverlays(
|
|
1273
|
+
topo,
|
|
1274
|
+
options?.overlays
|
|
1275
|
+
);
|
|
1276
|
+
const signalRelations = collectSignalGraphRelations(signals, trails);
|
|
1277
|
+
const activationSources = collectActivationSourceCatalog(trails);
|
|
1278
|
+
const activationEdges = collectActivationGraphEdges(trails);
|
|
1279
|
+
const entries = [
|
|
1280
|
+
...entities.map((entity) => entityToEntryRecord(entity)),
|
|
1281
|
+
...trails.map((trail) =>
|
|
1282
|
+
trailToEntryRecord(
|
|
1283
|
+
trail,
|
|
1284
|
+
topoLayers,
|
|
1285
|
+
requireTrailSchema(trailSchemas, trail.id),
|
|
1286
|
+
surfaceAliases
|
|
1287
|
+
)
|
|
1288
|
+
),
|
|
1289
|
+
...signals.map((signal) =>
|
|
1290
|
+
signalToEntryRecord(
|
|
1291
|
+
signal,
|
|
1292
|
+
requireSignalPayload(signalPayloads, signal.id),
|
|
1293
|
+
signalRelations.get(signal.id) ?? EMPTY_SIGNAL_RELATIONS
|
|
1294
|
+
)
|
|
1295
|
+
),
|
|
1296
|
+
...resources.map((resource) => resourceToEntryRecord(resource)),
|
|
1297
|
+
].toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1298
|
+
|
|
1299
|
+
return {
|
|
1300
|
+
activationGraph: buildActivationGraphRecord(
|
|
1301
|
+
activationEdges,
|
|
1302
|
+
activationSources
|
|
1303
|
+
),
|
|
1304
|
+
activationSources: Object.fromEntries(
|
|
1305
|
+
activationSources.map((source) => [source.key, source])
|
|
1306
|
+
),
|
|
1307
|
+
entries,
|
|
1308
|
+
generatedAt,
|
|
1309
|
+
library: collectLibraryProjection(topo),
|
|
1310
|
+
...(overlays === undefined ? {} : { overlays }),
|
|
1311
|
+
topoGraphSchemaVersion: TOPO_GRAPH_SCHEMA_VERSION,
|
|
1312
|
+
...(trailheads === undefined ? {} : { trailheads }),
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const hashTopoGraphRecord = (topoGraph: TopoGraphRecord): string => {
|
|
1317
|
+
// Same canonical hash path as `deriveTopoGraphHash`: strip `generatedAt`,
|
|
1318
|
+
// then key-sorted SHA-256 via the shared `deriveStableHash`.
|
|
1319
|
+
const { generatedAt: _unused, ...rest } = topoGraph;
|
|
1320
|
+
return deriveStableHash(rest);
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const countEntriesForKind = (
|
|
1324
|
+
entries: readonly TopoGraphEntryRecord[],
|
|
1325
|
+
kind: TopoGraphEntryRecord['kind']
|
|
1326
|
+
): number => entries.filter((entry) => entry.kind === kind).length;
|
|
1327
|
+
|
|
1328
|
+
const buildLockManifest = (
|
|
1329
|
+
hash: string,
|
|
1330
|
+
topo: Topo,
|
|
1331
|
+
topoGraph: TopoGraphRecord
|
|
1332
|
+
): LockManifest =>
|
|
1333
|
+
sortKeys({
|
|
1334
|
+
artifacts: [{ path: 'topo.lock', role: 'topo', sha256: hash }],
|
|
1335
|
+
scope: { app: topo.name },
|
|
1336
|
+
summary: {
|
|
1337
|
+
entities: countEntriesForKind(topoGraph.entries, 'entity'),
|
|
1338
|
+
resources: countEntriesForKind(topoGraph.entries, 'resource'),
|
|
1339
|
+
signals: countEntriesForKind(topoGraph.entries, 'signal'),
|
|
1340
|
+
trails: countEntriesForKind(topoGraph.entries, 'trail'),
|
|
1341
|
+
},
|
|
1342
|
+
version: LOCK_MANIFEST_SCHEMA_VERSION,
|
|
1343
|
+
}) as LockManifest;
|
|
1344
|
+
|
|
1345
|
+
const buildStoredTopoExport = (
|
|
1346
|
+
db: Database,
|
|
1347
|
+
snapshot: TopoSnapshot,
|
|
1348
|
+
topo: Topo,
|
|
1349
|
+
options?: Pick<CreateTopoSnapshotInput, 'overlays'> | undefined
|
|
1350
|
+
): MaterializedTopoArtifacts => {
|
|
1351
|
+
const entities = topo
|
|
1352
|
+
.listEntities()
|
|
1353
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
1354
|
+
const trails = topo.list().toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1355
|
+
const resources = topo
|
|
1356
|
+
.listResources()
|
|
1357
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1358
|
+
const signals = topo
|
|
1359
|
+
.listSignals()
|
|
1360
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
1361
|
+
const schemas = materializeSchemas(db, snapshot.id, signals, trails);
|
|
1362
|
+
const topoGraph = buildTopoGraph(
|
|
1363
|
+
entities,
|
|
1364
|
+
snapshot.createdAt,
|
|
1365
|
+
resources,
|
|
1366
|
+
schemas.signalPayloads,
|
|
1367
|
+
signals,
|
|
1368
|
+
topo,
|
|
1369
|
+
topo.layers,
|
|
1370
|
+
schemas.trailSchemas,
|
|
1371
|
+
trails,
|
|
1372
|
+
options
|
|
1373
|
+
);
|
|
1374
|
+
const topoGraphHash = hashTopoGraphRecord(topoGraph);
|
|
1375
|
+
const lockManifest = `${JSON.stringify(
|
|
1376
|
+
buildLockManifest(topoGraphHash, topo, topoGraph),
|
|
1377
|
+
null,
|
|
1378
|
+
2
|
|
1379
|
+
)}\n`;
|
|
1380
|
+
|
|
1381
|
+
return {
|
|
1382
|
+
exportRow: {
|
|
1383
|
+
lockManifest,
|
|
1384
|
+
snapshotId: snapshot.id,
|
|
1385
|
+
topoGraph: `${JSON.stringify(topoGraph, null, 2)}\n`,
|
|
1386
|
+
topoGraphHash,
|
|
1387
|
+
},
|
|
1388
|
+
schemaRows: schemas.rows,
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
const insertRows = <TRow>(
|
|
1393
|
+
db: Database,
|
|
1394
|
+
rows: readonly TRow[],
|
|
1395
|
+
statement: string,
|
|
1396
|
+
toParams: (row: TRow) => SQLQueryBindings[]
|
|
1397
|
+
): void => {
|
|
1398
|
+
for (const row of rows) {
|
|
1399
|
+
db.run(statement, toParams(row));
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
const insertProjectedRows = (
|
|
1404
|
+
db: Database,
|
|
1405
|
+
projection: NormalizedTopoProjection
|
|
1406
|
+
): void => {
|
|
1407
|
+
insertRows(
|
|
1408
|
+
db,
|
|
1409
|
+
projection.trails,
|
|
1410
|
+
`INSERT INTO topo_trails (
|
|
1411
|
+
id, intent, idempotent, has_output, has_examples, example_count, description, pattern, meta, snapshot_id
|
|
1412
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1413
|
+
(row) => [
|
|
1414
|
+
row.id,
|
|
1415
|
+
row.intent,
|
|
1416
|
+
row.idempotent,
|
|
1417
|
+
row.hasOutput,
|
|
1418
|
+
row.hasExamples,
|
|
1419
|
+
row.exampleCount,
|
|
1420
|
+
row.description,
|
|
1421
|
+
row.pattern,
|
|
1422
|
+
row.meta,
|
|
1423
|
+
row.snapshotId,
|
|
1424
|
+
]
|
|
1425
|
+
);
|
|
1426
|
+
insertRows(
|
|
1427
|
+
db,
|
|
1428
|
+
projection.composings,
|
|
1429
|
+
'INSERT INTO topo_composings (source_id, target_id, snapshot_id) VALUES (?, ?, ?)',
|
|
1430
|
+
(row) => [row.sourceId, row.targetId, row.snapshotId]
|
|
1431
|
+
);
|
|
1432
|
+
insertRows(
|
|
1433
|
+
db,
|
|
1434
|
+
projection.trailResources,
|
|
1435
|
+
`INSERT INTO topo_trail_resources (trail_id, resource_id, snapshot_id)
|
|
1436
|
+
VALUES (?, ?, ?)`,
|
|
1437
|
+
(row) => [row.trailId, row.resourceId, row.snapshotId]
|
|
1438
|
+
);
|
|
1439
|
+
insertRows(
|
|
1440
|
+
db,
|
|
1441
|
+
projection.resources,
|
|
1442
|
+
`INSERT INTO topo_resources (id, has_mock, has_health, snapshot_id)
|
|
1443
|
+
VALUES (?, ?, ?, ?)`,
|
|
1444
|
+
(row) => [row.id, row.hasMock, row.hasHealth, row.snapshotId]
|
|
1445
|
+
);
|
|
1446
|
+
insertRows(
|
|
1447
|
+
db,
|
|
1448
|
+
projection.signals,
|
|
1449
|
+
'INSERT INTO topo_signals (id, description, snapshot_id) VALUES (?, ?, ?)',
|
|
1450
|
+
(row) => [row.id, row.description, row.snapshotId]
|
|
1451
|
+
);
|
|
1452
|
+
insertRows(
|
|
1453
|
+
db,
|
|
1454
|
+
projection.trailSignals,
|
|
1455
|
+
`INSERT INTO topo_trail_signals (trail_id, signal_id, snapshot_id)
|
|
1456
|
+
VALUES (?, ?, ?)`,
|
|
1457
|
+
(row) => [row.trailId, row.signalId, row.snapshotId]
|
|
1458
|
+
);
|
|
1459
|
+
insertRows(
|
|
1460
|
+
db,
|
|
1461
|
+
projection.fires,
|
|
1462
|
+
`INSERT INTO topo_trail_fires (trail_id, signal_id, snapshot_id)
|
|
1463
|
+
VALUES (?, ?, ?)`,
|
|
1464
|
+
(row) => [row.trailId, row.signalId, row.snapshotId]
|
|
1465
|
+
);
|
|
1466
|
+
insertRows(
|
|
1467
|
+
db,
|
|
1468
|
+
projection.on,
|
|
1469
|
+
`INSERT INTO topo_trail_on (trail_id, signal_id, snapshot_id)
|
|
1470
|
+
VALUES (?, ?, ?)`,
|
|
1471
|
+
(row) => [row.trailId, row.signalId, row.snapshotId]
|
|
1472
|
+
);
|
|
1473
|
+
insertRows(
|
|
1474
|
+
db,
|
|
1475
|
+
projection.activationSources,
|
|
1476
|
+
`INSERT INTO topo_activation_sources (
|
|
1477
|
+
source_key, source_id, source_kind, source, snapshot_id
|
|
1478
|
+
) VALUES (?, ?, ?, ?, ?)`,
|
|
1479
|
+
(row) => [
|
|
1480
|
+
row.sourceKey,
|
|
1481
|
+
row.sourceId,
|
|
1482
|
+
row.sourceKind,
|
|
1483
|
+
row.source,
|
|
1484
|
+
row.snapshotId,
|
|
1485
|
+
]
|
|
1486
|
+
);
|
|
1487
|
+
insertRows(
|
|
1488
|
+
db,
|
|
1489
|
+
projection.activationEdges,
|
|
1490
|
+
`INSERT INTO topo_activation_edges (
|
|
1491
|
+
source_key, source_id, source_kind, trail_id, has_where, edge, snapshot_id
|
|
1492
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1493
|
+
(row) => [
|
|
1494
|
+
row.sourceKey,
|
|
1495
|
+
row.sourceId,
|
|
1496
|
+
row.sourceKind,
|
|
1497
|
+
row.trailId,
|
|
1498
|
+
row.hasWhere,
|
|
1499
|
+
row.edge,
|
|
1500
|
+
row.snapshotId,
|
|
1501
|
+
]
|
|
1502
|
+
);
|
|
1503
|
+
insertRows(
|
|
1504
|
+
db,
|
|
1505
|
+
projection.surfaces,
|
|
1506
|
+
`INSERT INTO topo_surfaces (trail_id, surface, derived_name, method, snapshot_id)
|
|
1507
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1508
|
+
(row) => [
|
|
1509
|
+
row.trailId,
|
|
1510
|
+
row.surface,
|
|
1511
|
+
row.derivedName,
|
|
1512
|
+
row.method,
|
|
1513
|
+
row.snapshotId,
|
|
1514
|
+
]
|
|
1515
|
+
);
|
|
1516
|
+
insertRows(
|
|
1517
|
+
db,
|
|
1518
|
+
projection.examples,
|
|
1519
|
+
`INSERT INTO topo_examples (
|
|
1520
|
+
id, trail_id, ordinal, name, description, input, expected, expected_match, error, signals, snapshot_id
|
|
1521
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1522
|
+
(row) => [
|
|
1523
|
+
row.id,
|
|
1524
|
+
row.trailId,
|
|
1525
|
+
row.ordinal,
|
|
1526
|
+
row.name,
|
|
1527
|
+
row.description,
|
|
1528
|
+
row.input,
|
|
1529
|
+
row.expected,
|
|
1530
|
+
row.expectedMatch,
|
|
1531
|
+
row.error,
|
|
1532
|
+
row.signals,
|
|
1533
|
+
row.snapshotId,
|
|
1534
|
+
]
|
|
1535
|
+
);
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const insertSchemaRows = (
|
|
1539
|
+
db: Database,
|
|
1540
|
+
rows: readonly TopoSchemaRow[]
|
|
1541
|
+
): void => {
|
|
1542
|
+
insertRows(
|
|
1543
|
+
db,
|
|
1544
|
+
rows,
|
|
1545
|
+
`INSERT INTO topo_schemas (
|
|
1546
|
+
owner_id, owner_kind, schema_kind, zod_hash, json_schema, snapshot_id
|
|
1547
|
+
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1548
|
+
(row) => [
|
|
1549
|
+
row.ownerId,
|
|
1550
|
+
row.ownerKind,
|
|
1551
|
+
row.schemaKind,
|
|
1552
|
+
row.zodHash,
|
|
1553
|
+
row.jsonSchema,
|
|
1554
|
+
row.snapshotId,
|
|
1555
|
+
]
|
|
1556
|
+
);
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
const insertStoredExport = (
|
|
1560
|
+
db: Database,
|
|
1561
|
+
exportRow: StoredTopoExportRow
|
|
1562
|
+
): void => {
|
|
1563
|
+
db.run(
|
|
1564
|
+
`INSERT INTO topo_exports (
|
|
1565
|
+
snapshot_id, topo_graph, topo_graph_hash, lock_manifest
|
|
1566
|
+
) VALUES (?, ?, ?, ?)`,
|
|
1567
|
+
[
|
|
1568
|
+
exportRow.snapshotId,
|
|
1569
|
+
exportRow.topoGraph,
|
|
1570
|
+
exportRow.topoGraphHash,
|
|
1571
|
+
exportRow.lockManifest,
|
|
1572
|
+
]
|
|
1573
|
+
);
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
export const getStoredTopoExport = (
|
|
1577
|
+
db: Database,
|
|
1578
|
+
snapshotId: string
|
|
1579
|
+
): StoredTopoExport | undefined => {
|
|
1580
|
+
const row = db
|
|
1581
|
+
.query<StoredTopoExportDbRow, [string]>(
|
|
1582
|
+
`SELECT topo_graph, topo_graph_hash, lock_manifest
|
|
1583
|
+
FROM topo_exports
|
|
1584
|
+
WHERE snapshot_id = ?`
|
|
1585
|
+
)
|
|
1586
|
+
.get(snapshotId);
|
|
1587
|
+
|
|
1588
|
+
if (row === undefined || row === null) {
|
|
1589
|
+
return undefined;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
return {
|
|
1593
|
+
lockManifestJson: row.lock_manifest,
|
|
1594
|
+
topoGraphHash: row.topo_graph_hash,
|
|
1595
|
+
topoGraphJson: row.topo_graph,
|
|
1596
|
+
};
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Fail-fast boundary validation for the app-authored `surfaces` overlay's
|
|
1601
|
+
* `cli` bindings, run before the snapshot transaction opens so an invalid
|
|
1602
|
+
* binding surfaces as a Result instead of aborting mid-write.
|
|
1603
|
+
*/
|
|
1604
|
+
const validateSurfaceCliBindings = (
|
|
1605
|
+
topo: Topo,
|
|
1606
|
+
input?: Pick<CreateTopoSnapshotInput, 'overlays'> | undefined
|
|
1607
|
+
): Result<void, Error> => {
|
|
1608
|
+
try {
|
|
1609
|
+
resolveCliAliasInputsFromOverlays(topo, input?.overlays);
|
|
1610
|
+
return Result.ok();
|
|
1611
|
+
} catch (error: unknown) {
|
|
1612
|
+
return Result.err(
|
|
1613
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
export const createTopoSnapshot = (
|
|
1619
|
+
db: Database,
|
|
1620
|
+
topo: Topo,
|
|
1621
|
+
input?: CreateTopoSnapshotInput
|
|
1622
|
+
): Result<TopoSnapshot, Error> => {
|
|
1623
|
+
const validated = validateEstablishedTopo(topo);
|
|
1624
|
+
if (validated.isErr()) {
|
|
1625
|
+
return validated;
|
|
1626
|
+
}
|
|
1627
|
+
const cliBindings = validateSurfaceCliBindings(topo, input);
|
|
1628
|
+
if (cliBindings.isErr()) {
|
|
1629
|
+
return cliBindings;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
ensureTopoSnapshotSchema(db);
|
|
1633
|
+
|
|
1634
|
+
const snapshotInput: CreateTopoSnapshotInput = {
|
|
1635
|
+
...input,
|
|
1636
|
+
resourceCount: input?.resourceCount ?? topo.resources.size,
|
|
1637
|
+
signalCount: input?.signalCount ?? topo.signals.size,
|
|
1638
|
+
trailCount: input?.trailCount ?? topo.trails.size,
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
const snapshot = db.transaction(() => {
|
|
1642
|
+
const record = insertTopoSnapshotRecord(db, snapshotInput);
|
|
1643
|
+
const artifacts = buildStoredTopoExport(db, record, topo, snapshotInput);
|
|
1644
|
+
insertProjectedRows(db, normalizeTopoProjection(topo, record.id));
|
|
1645
|
+
insertSchemaRows(db, artifacts.schemaRows);
|
|
1646
|
+
insertStoredExport(db, artifacts.exportRow);
|
|
1647
|
+
return record;
|
|
1648
|
+
})();
|
|
1649
|
+
|
|
1650
|
+
return Result.ok(snapshot);
|
|
1651
|
+
};
|