@ontrails/topography 0.2.0
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 +866 -0
- package/README.md +228 -0
- package/package.json +40 -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 +273 -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-derivation.ts +131 -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 +277 -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-configured-aliases.ts +221 -0
- package/src/workspace-lock-census.ts +306 -0
- package/src/workspace-view-types.ts +104 -0
- package/src/workspace-view.ts +486 -0
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
|
+
deriveTrailCliCommandRendering,
|
|
11
|
+
getEntityReferences,
|
|
12
|
+
deriveActivationSourceFacts,
|
|
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 { deriveTopoGraphLibrary } from './library-derivation.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 { deriveTrailVersions } 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 deriveActivationSource = (
|
|
160
|
+
source: ActivationSource
|
|
161
|
+
): TopoGraphActivationSource =>
|
|
162
|
+
deriveActivationSourceFacts(source) as TopoGraphActivationSource;
|
|
163
|
+
|
|
164
|
+
const deriveActivationEdge = (
|
|
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 derived = deriveActivationSource(activation.source);
|
|
194
|
+
sources.set(derived.key, derived);
|
|
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 = deriveActivationEdge(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: deriveActivationSource(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 derivation = deriveTrailVersions(t, toSortedJsonSchema);
|
|
486
|
+
if (derivation === undefined) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
entry['marker'] = derivation.marker;
|
|
491
|
+
entry['supports'] = derivation.supports;
|
|
492
|
+
entry['version'] = derivation.version;
|
|
493
|
+
if (derivation.versions !== undefined) {
|
|
494
|
+
entry['versions'] = derivation.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: deriveTrailCliCommandRendering(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 = deriveTopoGraphLibrary(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
|
+
};
|