@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/src/derive.ts ADDED
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Derive a deterministic topo graph from a Topo.
3
+ */
4
+
5
+ import {
6
+ DETOUR_MAX_ATTEMPTS_CAP,
7
+ activationSourceKey,
8
+ deriveStructuredSignalExamples,
9
+ deriveStructuredTrailExamples,
10
+ deriveTrailCliCommandProjection,
11
+ getEntityReferences,
12
+ projectActivationSourceDeclaration,
13
+ validateEstablishedTopo,
14
+ signalDiagnosticDefinitions,
15
+ zodToJsonSchema,
16
+ } from '@ontrails/core';
17
+ import type {
18
+ ActivationEntry,
19
+ ActivationSource,
20
+ AnyEntity,
21
+ AnyResource,
22
+ CliSurfaceBindingAliases,
23
+ FieldOverride,
24
+ Layer,
25
+ Signal,
26
+ Topo,
27
+ Trail,
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 { TOPO_GRAPH_SCHEMA_VERSION } from './types.js';
38
+ import { projectTrailVersions } from './versioning.js';
39
+ import type {
40
+ DeriveTopoGraphOptions,
41
+ JsonSchema,
42
+ TopoGraph,
43
+ TopoGraphActivationEdge,
44
+ TopoGraphActivationGraph,
45
+ TopoGraphActivationSource,
46
+ TopoGraphEntry,
47
+ TopoGraphFieldOverride,
48
+ TopoGraphFieldOverrideKey,
49
+ } from './types.js';
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** Sort object keys lexicographically (shallow). */
56
+ const sortKeys = <T extends Record<string, unknown>>(obj: T): T => {
57
+ const sorted: Record<string, unknown> = {};
58
+ for (const key of Object.keys(obj).toSorted()) {
59
+ sorted[key] = obj[key];
60
+ }
61
+ return sorted as T;
62
+ };
63
+
64
+ /** Sort object keys recursively for deterministic JSON Schema output. */
65
+ const deepSortKeys = (value: unknown): unknown => {
66
+ if (Array.isArray(value)) {
67
+ return value.map(deepSortKeys);
68
+ }
69
+ if (value !== null && typeof value === 'object') {
70
+ const sorted: Record<string, unknown> = {};
71
+ for (const key of Object.keys(value).toSorted()) {
72
+ sorted[key] = deepSortKeys((value as Record<string, unknown>)[key]);
73
+ }
74
+ return sorted;
75
+ }
76
+ return value;
77
+ };
78
+
79
+ const canonicalLeaf = (value: unknown): unknown => {
80
+ switch (typeof value) {
81
+ case 'bigint': {
82
+ return value.toString();
83
+ }
84
+ case 'function': {
85
+ return `[Function:${value.name || 'anonymous'}]`;
86
+ }
87
+ case 'symbol': {
88
+ return `[Symbol:${value.description ?? ''}]`;
89
+ }
90
+ case 'undefined': {
91
+ return '[Undefined]';
92
+ }
93
+ default: {
94
+ return value;
95
+ }
96
+ }
97
+ };
98
+
99
+ const canonicalObject = (
100
+ value: Record<string, unknown>,
101
+ visit: (value: unknown) => unknown
102
+ ): Record<string, unknown> => {
103
+ const sorted: Record<string, unknown> = {};
104
+ for (const key of Object.keys(value).toSorted()) {
105
+ const next = value[key];
106
+ sorted[key] = next === undefined ? '[Undefined]' : visit(next);
107
+ }
108
+ return sorted;
109
+ };
110
+
111
+ const canonicalize = (value: unknown): unknown => {
112
+ if (Array.isArray(value)) {
113
+ return value.map(canonicalize);
114
+ }
115
+ if (value instanceof Date) {
116
+ return value.toISOString();
117
+ }
118
+ if (value instanceof RegExp) {
119
+ return value.toString();
120
+ }
121
+ if (value !== null && typeof value === 'object') {
122
+ return canonicalObject(value as Record<string, unknown>, canonicalize);
123
+ }
124
+ return canonicalLeaf(value);
125
+ };
126
+
127
+ /** Convert a Zod schema to a deterministically-keyed JSON Schema. */
128
+ const toSortedJsonSchema = (schema: unknown): JsonSchema => {
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ const raw = zodToJsonSchema(schema as any);
131
+ return deepSortKeys(raw) as JsonSchema;
132
+ };
133
+
134
+ interface SignalGraphRelations {
135
+ readonly consumers: readonly string[];
136
+ readonly producers: readonly string[];
137
+ }
138
+
139
+ const EMPTY_SIGNAL_RELATIONS: SignalGraphRelations = {
140
+ consumers: [],
141
+ producers: [],
142
+ };
143
+
144
+ const SIGNAL_DIAGNOSTIC_CODES = Object.keys(
145
+ signalDiagnosticDefinitions
146
+ ).toSorted();
147
+
148
+ const SIGNAL_DIAGNOSTIC_HOOKS = {
149
+ codes: SIGNAL_DIAGNOSTIC_CODES,
150
+ strictMode: true,
151
+ } as const;
152
+
153
+ const SIGNAL_GOVERNANCE_HOOKS = {
154
+ consumers: 'trail.on',
155
+ payload: 'signal.payload',
156
+ producers: 'trail.fires',
157
+ } as const;
158
+
159
+ const projectActivationSource = (
160
+ source: ActivationSource
161
+ ): TopoGraphActivationSource =>
162
+ projectActivationSourceDeclaration(source) as TopoGraphActivationSource;
163
+
164
+ const projectActivationEdge = (
165
+ trailId: string,
166
+ activation: ActivationEntry
167
+ ): TopoGraphActivationEdge => {
168
+ const sourceKey = activationSourceKey(activation.source);
169
+ const edge: Record<string, unknown> = {
170
+ hasWhere: activation.where !== undefined,
171
+ sourceId: activation.source.id,
172
+ sourceKey,
173
+ sourceKind: activation.source.kind,
174
+ trailId,
175
+ };
176
+
177
+ if (activation.meta !== undefined) {
178
+ edge['meta'] = canonicalize(activation.meta);
179
+ }
180
+ if (activation.where !== undefined) {
181
+ edge['where'] = { predicate: true };
182
+ }
183
+
184
+ return sortKeys(edge) as TopoGraphActivationEdge;
185
+ };
186
+
187
+ const collectActivationSourceCatalog = (
188
+ topo: Topo
189
+ ): readonly TopoGraphActivationSource[] => {
190
+ const sources = new Map<string, TopoGraphActivationSource>();
191
+ for (const trail of topo.trails.values()) {
192
+ for (const activation of trail.activationSources) {
193
+ const projected = projectActivationSource(activation.source);
194
+ sources.set(projected.key, projected);
195
+ }
196
+ }
197
+ return [...sources.values()].toSorted((a, b) => a.key.localeCompare(b.key));
198
+ };
199
+
200
+ const collectActivationGraphEdges = (
201
+ topo: Topo
202
+ ): readonly TopoGraphActivationEdge[] => {
203
+ const edges = new Map<string, TopoGraphActivationEdge>();
204
+ for (const trail of topo.trails.values()) {
205
+ for (const activation of trail.activationSources) {
206
+ const edge = projectActivationEdge(trail.id, activation);
207
+ const key = `${edge.sourceKey}\0${edge.trailId}`;
208
+ const previous = edges.get(key);
209
+ edges.set(
210
+ key,
211
+ previous === undefined || (!previous.hasWhere && edge.hasWhere)
212
+ ? edge
213
+ : previous
214
+ );
215
+ }
216
+ }
217
+
218
+ return [...edges.values()].toSorted(
219
+ (a, b) =>
220
+ a.sourceKey.localeCompare(b.sourceKey) ||
221
+ a.trailId.localeCompare(b.trailId)
222
+ );
223
+ };
224
+
225
+ const buildActivationGraph = (
226
+ edges: readonly TopoGraphActivationEdge[],
227
+ sources: readonly TopoGraphActivationSource[]
228
+ ): TopoGraphActivationGraph =>
229
+ sortKeys({
230
+ edgeCount: edges.length,
231
+ edges,
232
+ sourceCount: sources.length,
233
+ sourceKeys: sources.map((source) => source.key),
234
+ trailIds: [...new Set(edges.map((edge) => edge.trailId))].toSorted(),
235
+ }) as TopoGraphActivationGraph;
236
+
237
+ const collectSignalGraphRelations = (
238
+ topo: Topo
239
+ ): ReadonlyMap<string, SignalGraphRelations> => {
240
+ const producers = new Map<string, Set<string>>();
241
+ const consumers = new Map<string, Set<string>>();
242
+
243
+ for (const trail of topo.trails.values()) {
244
+ for (const signalId of trail.fires) {
245
+ const current = producers.get(signalId) ?? new Set<string>();
246
+ current.add(trail.id);
247
+ producers.set(signalId, current);
248
+ }
249
+ for (const signalId of trail.on) {
250
+ const current = consumers.get(signalId) ?? new Set<string>();
251
+ current.add(trail.id);
252
+ consumers.set(signalId, current);
253
+ }
254
+ }
255
+
256
+ return new Map(
257
+ [...topo.signals.keys()].map((signalId) => [
258
+ signalId,
259
+ {
260
+ consumers: [...(consumers.get(signalId) ?? [])].toSorted(),
261
+ producers: [...(producers.get(signalId) ?? [])].toSorted(),
262
+ },
263
+ ])
264
+ );
265
+ };
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Entry builders
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /** Extract surfaces from a raw object. */
272
+ const extractSurfaces = (raw: Record<string, unknown>): string[] =>
273
+ Array.isArray(raw['surfaces'])
274
+ ? (raw['surfaces'] as string[]).toSorted()
275
+ : [];
276
+
277
+ /** Add optional schemas to an entry. */
278
+ const addSchemas = (
279
+ entry: Record<string, unknown>,
280
+ t: Trail<unknown, unknown, unknown>
281
+ ): void => {
282
+ if (t.input) {
283
+ entry['input'] = toSortedJsonSchema(t.input);
284
+ }
285
+ if (t.output) {
286
+ entry['output'] = toSortedJsonSchema(t.output);
287
+ }
288
+ };
289
+
290
+ const addEntitySchema = (
291
+ entry: Record<string, unknown>,
292
+ entity: AnyEntity
293
+ ): void => {
294
+ entry['schema'] = toSortedJsonSchema(entity);
295
+ };
296
+
297
+ /** Add safety markers to an entry. */
298
+ const addSafetyMarkers = (
299
+ entry: Record<string, unknown>,
300
+ t: Trail<unknown, unknown, unknown>
301
+ ): void => {
302
+ if (t.intent !== 'write') {
303
+ entry['intent'] = t.intent;
304
+ }
305
+ if (t.idempotent === true) {
306
+ entry['idempotent'] = true;
307
+ }
308
+ if (t.dryRun === true) {
309
+ entry['dryRunCapable'] = true;
310
+ }
311
+ };
312
+
313
+ /** Add deprecation and detours to an entry. */
314
+ const addExtendedMetadata = (
315
+ entry: Record<string, unknown>,
316
+ t: Trail<unknown, unknown, unknown>,
317
+ raw: Record<string, unknown>
318
+ ): void => {
319
+ if (raw['deprecated'] === true) {
320
+ entry['deprecated'] = true;
321
+ }
322
+ if (typeof raw['replacedBy'] === 'string') {
323
+ entry['replacedBy'] = raw['replacedBy'];
324
+ }
325
+ if (t.detours.length > 0) {
326
+ entry['detours'] = t.detours.map((d) => ({
327
+ maxAttempts: Math.max(
328
+ 1,
329
+ Math.min(d.maxAttempts ?? 1, DETOUR_MAX_ATTEMPTS_CAP)
330
+ ),
331
+ on: d.on.name,
332
+ }));
333
+ }
334
+ };
335
+
336
+ /** Add optional meta fields to an entry. */
337
+ const addMetadata = (
338
+ entry: Record<string, unknown>,
339
+ t: Trail<unknown, unknown, unknown>,
340
+ raw: Record<string, unknown>
341
+ ): void => {
342
+ if (t.description !== undefined) {
343
+ entry['description'] = t.description;
344
+ }
345
+ if (t.pattern !== undefined) {
346
+ entry['pattern'] = t.pattern;
347
+ }
348
+ addSafetyMarkers(entry, t);
349
+ addExtendedMetadata(entry, t, raw);
350
+ };
351
+
352
+ const addTrailRelations = (
353
+ entry: Record<string, unknown>,
354
+ t: Trail<unknown, unknown, unknown>
355
+ ): void => {
356
+ if (t.activationSources.length > 0) {
357
+ entry['activationSources'] = t.activationSources.map((activation) =>
358
+ sortKeys({
359
+ ...(activation.meta === undefined
360
+ ? {}
361
+ : { meta: canonicalize(activation.meta) }),
362
+ source: projectActivationSource(activation.source),
363
+ ...(activation.where === undefined
364
+ ? {}
365
+ : { where: sortKeys({ predicate: true }) }),
366
+ })
367
+ );
368
+ }
369
+ if (t.composes.length > 0) {
370
+ entry['composes'] = t.composes.toSorted();
371
+ }
372
+ if (t.fires.length > 0) {
373
+ entry['fires'] = t.fires.toSorted();
374
+ }
375
+ if (t.on.length > 0) {
376
+ entry['on'] = t.on.toSorted();
377
+ }
378
+ if (t.entities.length > 0) {
379
+ entry['entities'] = t.entities.map((entity) => entity.name).toSorted();
380
+ }
381
+ if (t.resources.length > 0) {
382
+ entry['resources'] = t.resources.map((resource) => resource.id).toSorted();
383
+ }
384
+ };
385
+
386
+ const layerToReference = (
387
+ scope: 'topo' | 'trail',
388
+ layer: Layer
389
+ ): Record<string, unknown> => {
390
+ const reference: Record<string, unknown> = {
391
+ name: layer.name,
392
+ scope,
393
+ };
394
+ if (layer.input !== undefined) {
395
+ reference['input'] = toSortedJsonSchema(layer.input);
396
+ }
397
+ return sortKeys(reference);
398
+ };
399
+
400
+ const addLayerAttachments = (
401
+ entry: Record<string, unknown>,
402
+ topoLayers: readonly Layer[],
403
+ t: Trail<unknown, unknown, unknown>
404
+ ): void => {
405
+ const layers = [
406
+ ...topoLayers.map((layer) => layerToReference('topo', layer)),
407
+ ...t.layers.map((layer) => layerToReference('trail', layer)),
408
+ ].toSorted((left, right) => {
409
+ const scope = String(left['scope']).localeCompare(String(right['scope']));
410
+ return scope === 0
411
+ ? String(left['name']).localeCompare(String(right['name']))
412
+ : scope;
413
+ });
414
+
415
+ if (layers.length > 0) {
416
+ entry['layers'] = layers;
417
+ }
418
+ };
419
+
420
+ const FIELD_OVERRIDE_KEYS: readonly TopoGraphFieldOverrideKey[] = [
421
+ 'hint',
422
+ 'label',
423
+ 'message',
424
+ 'options',
425
+ ];
426
+
427
+ const collectFieldOverrideKeys = (
428
+ override: FieldOverride
429
+ ): readonly TopoGraphFieldOverrideKey[] =>
430
+ FIELD_OVERRIDE_KEYS.filter((key) => override[key] !== undefined);
431
+
432
+ const deriveFieldOverrides = (
433
+ fields: Readonly<Record<string, FieldOverride>> | undefined
434
+ ): readonly TopoGraphFieldOverride[] | undefined => {
435
+ if (fields === undefined) {
436
+ return undefined;
437
+ }
438
+
439
+ const overrides = Object.entries(fields)
440
+ .flatMap(([field, override]) => {
441
+ const overrideKeys = collectFieldOverrideKeys(override);
442
+ if (overrideKeys.length === 0) {
443
+ return [];
444
+ }
445
+ return [
446
+ {
447
+ field,
448
+ overrides: overrideKeys,
449
+ provenance: { source: 'trail.fields' as const },
450
+ },
451
+ ];
452
+ })
453
+ .toSorted((a, b) => a.field.localeCompare(b.field));
454
+
455
+ return overrides.length > 0 ? overrides : undefined;
456
+ };
457
+
458
+ const addFieldOverrides = (
459
+ entry: Record<string, unknown>,
460
+ t: Trail<unknown, unknown, unknown>
461
+ ): void => {
462
+ const fieldOverrides = deriveFieldOverrides(t.fields);
463
+ if (fieldOverrides !== undefined) {
464
+ entry['fieldOverrides'] = fieldOverrides;
465
+ }
466
+ };
467
+
468
+ const addExamples = (
469
+ entry: Record<string, unknown>,
470
+ t: Trail<unknown, unknown, unknown>
471
+ ): void => {
472
+ const examples = deriveStructuredTrailExamples(t.examples);
473
+ if (examples !== undefined) {
474
+ entry['examples'] = examples;
475
+ }
476
+ };
477
+
478
+ const addVersioning = (
479
+ entry: Record<string, unknown>,
480
+ t: Trail<unknown, unknown, unknown>
481
+ ): void => {
482
+ if (t.version === undefined) {
483
+ return;
484
+ }
485
+ const projection = projectTrailVersions(t, toSortedJsonSchema);
486
+ if (projection === undefined) {
487
+ return;
488
+ }
489
+
490
+ entry['marker'] = projection.marker;
491
+ entry['supports'] = projection.supports;
492
+ entry['version'] = projection.version;
493
+ if (projection.versions !== undefined) {
494
+ entry['versions'] = projection.versions;
495
+ }
496
+ };
497
+
498
+ const trailToEntry = (
499
+ t: Trail<unknown, unknown, unknown>,
500
+ topoLayers: readonly Layer[],
501
+ surfaceAliases: CliSurfaceBindingAliases | undefined
502
+ ): TopoGraphEntry => {
503
+ const raw = t as unknown as Record<string, unknown>;
504
+ const surfaces = extractSurfaces(raw);
505
+ const entry: Record<string, unknown> = {
506
+ cli: deriveTrailCliCommandProjection(t, {
507
+ aliasSource: 'surface',
508
+ aliases: surfaceAliases?.[t.id],
509
+ }),
510
+ exampleCount: Array.isArray(t.examples) ? t.examples.length : 0,
511
+ id: t.id,
512
+ kind: t.kind,
513
+ surfaces,
514
+ };
515
+
516
+ addSchemas(entry, t);
517
+ addMetadata(entry, t, raw);
518
+ addPermitRequirement(entry, t);
519
+ addTrailRelations(entry, t);
520
+ addLayerAttachments(entry, topoLayers, t);
521
+ addFieldOverrides(entry, t);
522
+ addExamples(entry, t);
523
+ addVersioning(entry, t);
524
+
525
+ return sortKeys(entry) as unknown as TopoGraphEntry;
526
+ };
527
+
528
+ /** Add optional signal-specific fields. */
529
+ const addSignalFields = (
530
+ entry: Record<string, unknown>,
531
+ e: Signal<unknown>,
532
+ raw: Record<string, unknown>,
533
+ relations: SignalGraphRelations
534
+ ): void => {
535
+ if (e.payload) {
536
+ const payload = toSortedJsonSchema(e.payload);
537
+ entry['input'] = payload;
538
+ entry['payload'] = payload;
539
+ }
540
+ if (e.description !== undefined) {
541
+ entry['description'] = e.description;
542
+ }
543
+ if (e.meta !== undefined) {
544
+ entry['meta'] = e.meta;
545
+ }
546
+ if (e.from !== undefined && e.from.length > 0) {
547
+ entry['from'] = e.from.toSorted();
548
+ }
549
+ entry['consumers'] = relations.consumers;
550
+ entry['diagnostics'] = SIGNAL_DIAGNOSTIC_HOOKS;
551
+ entry['governance'] = SIGNAL_GOVERNANCE_HOOKS;
552
+ entry['producers'] = relations.producers;
553
+ const examples = deriveStructuredSignalExamples(e.examples);
554
+ if (examples !== undefined) {
555
+ entry['examples'] = examples;
556
+ }
557
+ if (raw['deprecated'] === true) {
558
+ entry['deprecated'] = true;
559
+ }
560
+ if (typeof raw['replacedBy'] === 'string') {
561
+ entry['replacedBy'] = raw['replacedBy'];
562
+ }
563
+ };
564
+
565
+ const signalToEntry = (
566
+ e: Signal<unknown>,
567
+ relations: SignalGraphRelations
568
+ ): TopoGraphEntry => {
569
+ const raw = e as unknown as Record<string, unknown>;
570
+ const surfaces = extractSurfaces(raw);
571
+ const entry: Record<string, unknown> = {
572
+ exampleCount: e.examples?.length ?? 0,
573
+ id: e.id,
574
+ kind: 'signal',
575
+ surfaces,
576
+ };
577
+ addSignalFields(entry, e, raw, relations);
578
+ return sortKeys(entry) as unknown as TopoGraphEntry;
579
+ };
580
+
581
+ const resourceToEntry = (resource: AnyResource): TopoGraphEntry => {
582
+ const entry: Record<string, unknown> = {
583
+ exampleCount: 0,
584
+ id: resource.id,
585
+ kind: 'resource',
586
+ surfaces: [],
587
+ };
588
+
589
+ if (resource.description !== undefined) {
590
+ entry['description'] = resource.description;
591
+ }
592
+ if (resource.health !== undefined) {
593
+ entry['healthcheck'] = true;
594
+ }
595
+
596
+ return sortKeys(entry) as unknown as TopoGraphEntry;
597
+ };
598
+
599
+ const entityToEntry = (entity: AnyEntity): TopoGraphEntry => {
600
+ const entry: Record<string, unknown> = {
601
+ exampleCount: entity.examples?.length ?? 0,
602
+ id: entity.name,
603
+ identity: entity.identity,
604
+ kind: 'entity',
605
+ surfaces: [],
606
+ };
607
+
608
+ addEntitySchema(entry, entity);
609
+
610
+ const references = getEntityReferences(entity);
611
+ if (references.length > 0) {
612
+ entry['references'] = references;
613
+ }
614
+
615
+ return sortKeys(entry) as unknown as TopoGraphEntry;
616
+ };
617
+
618
+ const assertEstablishedTopo = (topo: Topo): void => {
619
+ const validated = validateEstablishedTopo(topo);
620
+ if (validated.isErr()) {
621
+ throw validated.error;
622
+ }
623
+ };
624
+
625
+ const collectEntries = (
626
+ topo: Topo,
627
+ surfaceAliases: CliSurfaceBindingAliases | undefined
628
+ ): TopoGraphEntry[] => {
629
+ const signalRelations = collectSignalGraphRelations(topo);
630
+ return [
631
+ ...[...topo.entities.values()].map((entity) => entityToEntry(entity)),
632
+ ...[...topo.trails.values()].map((trail) =>
633
+ trailToEntry(
634
+ trail as Trail<unknown, unknown, unknown>,
635
+ topo.layers,
636
+ surfaceAliases
637
+ )
638
+ ),
639
+ ...[...topo.signals.values()].map((signal) =>
640
+ signalToEntry(
641
+ signal as Signal<unknown>,
642
+ signalRelations.get(signal.id) ?? EMPTY_SIGNAL_RELATIONS
643
+ )
644
+ ),
645
+ ...[...topo.resources.values()].map((resource) =>
646
+ resourceToEntry(resource)
647
+ ),
648
+ ];
649
+ };
650
+
651
+ /**
652
+ * Derive a deterministic topo graph from a Topo.
653
+ *
654
+ * Entries are sorted alphabetically by id. Object keys within each entry
655
+ * are sorted lexicographically for stable serialization.
656
+ */
657
+ export const deriveTopoGraph = (
658
+ topo: Topo,
659
+ options?: DeriveTopoGraphOptions
660
+ ): TopoGraph => {
661
+ assertEstablishedTopo(topo);
662
+ const surfaceAliases = resolveCliAliasInputsFromOverlays(
663
+ topo,
664
+ options?.overlays
665
+ );
666
+ const sorted = collectEntries(topo, surfaceAliases).toSorted((a, b) =>
667
+ a.id.localeCompare(b.id)
668
+ );
669
+ const activationSources = collectActivationSourceCatalog(topo);
670
+ const activationEdges = collectActivationGraphEdges(topo);
671
+ const trailheads = resolveTrailheadEntriesFromOverlays(
672
+ topo,
673
+ options?.overlays
674
+ );
675
+ const overlays = collectTopoGraphOverlays(topo, options?.overlays);
676
+ const library = collectLibraryProjection(topo);
677
+
678
+ return {
679
+ activationGraph: buildActivationGraph(activationEdges, activationSources),
680
+ activationSources: Object.fromEntries(
681
+ activationSources.map((source) => [source.key, source])
682
+ ),
683
+ entries: sorted,
684
+ ...(trailheads === undefined ? {} : { trailheads }),
685
+ ...(overlays === undefined ? {} : { overlays }),
686
+ generatedAt: new Date().toISOString(),
687
+ library,
688
+ topoGraphSchemaVersion: TOPO_GRAPH_SCHEMA_VERSION,
689
+ };
690
+ };