@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,344 @@
|
|
|
1
|
+
import { AmbiguousError, Result } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import type { DiffResult, TopoGraph, TopoGraphEntry } from '../types.js';
|
|
5
|
+
import { listWayfinderEntityRefs } from './filters.js';
|
|
6
|
+
import type { WayfinderEntityKind, WayfinderEntityRef } from './filters.js';
|
|
7
|
+
|
|
8
|
+
export const relationKindSchema = z.enum([
|
|
9
|
+
'composed-by',
|
|
10
|
+
'consumed-by',
|
|
11
|
+
'entity-referenced-by',
|
|
12
|
+
'trailhead-groups',
|
|
13
|
+
'fired-by',
|
|
14
|
+
'has-version',
|
|
15
|
+
'surface-projects',
|
|
16
|
+
'used-by',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const relationRefSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
kind: z.enum([
|
|
22
|
+
'entity',
|
|
23
|
+
'trailhead',
|
|
24
|
+
'resource',
|
|
25
|
+
'signal',
|
|
26
|
+
'surface',
|
|
27
|
+
'trail',
|
|
28
|
+
'version',
|
|
29
|
+
]),
|
|
30
|
+
trailId: z.string().optional(),
|
|
31
|
+
versionKey: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const relationEdgeSchema = z.object({
|
|
35
|
+
from: relationRefSchema,
|
|
36
|
+
relation: relationKindSchema,
|
|
37
|
+
to: relationRefSchema,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const relationGroupSchema = z.object({
|
|
41
|
+
direction: z.enum(['incoming', 'outgoing']),
|
|
42
|
+
refs: z.array(relationRefSchema).readonly(),
|
|
43
|
+
relation: relationKindSchema,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const impactNodeSchema = relationRefSchema.extend({
|
|
47
|
+
depth: z.number(),
|
|
48
|
+
from: relationRefSchema.optional(),
|
|
49
|
+
via: relationKindSchema.optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type RelationKind = z.output<typeof relationKindSchema>;
|
|
53
|
+
export type RelationRef = z.output<typeof relationRefSchema>;
|
|
54
|
+
export type RelationEdge = z.output<typeof relationEdgeSchema>;
|
|
55
|
+
export type ImpactDirection = 'downstream' | 'upstream' | 'both';
|
|
56
|
+
|
|
57
|
+
export interface ImpactOptions {
|
|
58
|
+
readonly direction: ImpactDirection;
|
|
59
|
+
readonly limit: number;
|
|
60
|
+
readonly maxDepth: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entryRef = (entry: TopoGraphEntry): WayfinderEntityRef => ({
|
|
64
|
+
entry,
|
|
65
|
+
id: entry.id,
|
|
66
|
+
kind: entry.kind,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const refFor = (
|
|
70
|
+
graph: TopoGraph,
|
|
71
|
+
id: string,
|
|
72
|
+
kind: WayfinderEntityKind
|
|
73
|
+
): WayfinderEntityRef =>
|
|
74
|
+
listWayfinderEntityRefs(graph).find(
|
|
75
|
+
(ref) => ref.id === id && ref.kind === kind
|
|
76
|
+
) ?? { id, kind };
|
|
77
|
+
|
|
78
|
+
export const refSummary = (ref: WayfinderEntityRef): RelationRef => ({
|
|
79
|
+
id: ref.id,
|
|
80
|
+
kind: ref.kind,
|
|
81
|
+
...(ref.trailId === undefined ? {} : { trailId: ref.trailId }),
|
|
82
|
+
...(ref.versionKey === undefined ? {} : { versionKey: ref.versionKey }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const signalUses = (
|
|
86
|
+
entry: TopoGraphEntry
|
|
87
|
+
): readonly {
|
|
88
|
+
readonly relation: Extract<RelationKind, 'consumed-by' | 'fired-by'>;
|
|
89
|
+
readonly signalId: string;
|
|
90
|
+
}[] => [
|
|
91
|
+
...(entry.fires ?? []).map((signalId) => ({
|
|
92
|
+
relation: 'fired-by' as const,
|
|
93
|
+
signalId,
|
|
94
|
+
})),
|
|
95
|
+
...[
|
|
96
|
+
...(entry.on ?? []),
|
|
97
|
+
...(entry.from ?? []),
|
|
98
|
+
...(entry.consumers ?? []),
|
|
99
|
+
].map((signalId) => ({
|
|
100
|
+
relation: 'consumed-by' as const,
|
|
101
|
+
signalId,
|
|
102
|
+
})),
|
|
103
|
+
...(entry.producers ?? []).map((signalId) => ({
|
|
104
|
+
relation: 'fired-by' as const,
|
|
105
|
+
signalId,
|
|
106
|
+
})),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const relationEdge = (
|
|
110
|
+
from: WayfinderEntityRef,
|
|
111
|
+
relation: RelationKind,
|
|
112
|
+
to: WayfinderEntityRef
|
|
113
|
+
): RelationEdge => ({
|
|
114
|
+
from: refSummary(from),
|
|
115
|
+
relation,
|
|
116
|
+
to: refSummary(to),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const relationKey = (edge: RelationEdge): string =>
|
|
120
|
+
`${edge.from.kind}:${edge.from.id}:${edge.relation}:${edge.to.kind}:${edge.to.id}`;
|
|
121
|
+
|
|
122
|
+
export const relationEdges = (graph: TopoGraph): readonly RelationEdge[] => {
|
|
123
|
+
const edges: RelationEdge[] = [];
|
|
124
|
+
const add = (
|
|
125
|
+
from: WayfinderEntityRef,
|
|
126
|
+
relation: RelationKind,
|
|
127
|
+
to: WayfinderEntityRef
|
|
128
|
+
) => edges.push(relationEdge(from, relation, to));
|
|
129
|
+
|
|
130
|
+
for (const entry of graph.entries) {
|
|
131
|
+
const target = entryRef(entry);
|
|
132
|
+
for (const composedId of entry.composes ?? []) {
|
|
133
|
+
add(refFor(graph, composedId, 'trail'), 'composed-by', target);
|
|
134
|
+
}
|
|
135
|
+
for (const entityId of entry.entities ?? []) {
|
|
136
|
+
add(refFor(graph, entityId, 'entity'), 'entity-referenced-by', target);
|
|
137
|
+
}
|
|
138
|
+
for (const resourceId of entry.resources ?? []) {
|
|
139
|
+
add(refFor(graph, resourceId, 'resource'), 'used-by', target);
|
|
140
|
+
}
|
|
141
|
+
for (const { relation, signalId } of signalUses(entry)) {
|
|
142
|
+
add(refFor(graph, signalId, 'signal'), relation, target);
|
|
143
|
+
}
|
|
144
|
+
for (const surfaceId of entry.surfaces) {
|
|
145
|
+
add(refFor(graph, surfaceId, 'surface'), 'surface-projects', target);
|
|
146
|
+
}
|
|
147
|
+
if (entry.kind === 'trail' && entry.version !== undefined) {
|
|
148
|
+
add(target, 'has-version', {
|
|
149
|
+
entry,
|
|
150
|
+
id: `${entry.id}@${entry.version}`,
|
|
151
|
+
kind: 'version',
|
|
152
|
+
trailId: entry.id,
|
|
153
|
+
versionKey: String(entry.version),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
for (const [versionKey, version] of Object.entries(entry.versions ?? {})) {
|
|
157
|
+
add(target, 'has-version', {
|
|
158
|
+
entry,
|
|
159
|
+
id: `${entry.id}@${versionKey}`,
|
|
160
|
+
kind: 'version',
|
|
161
|
+
trailId: entry.id,
|
|
162
|
+
version,
|
|
163
|
+
versionKey,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const trailhead of graph.trailheads ?? []) {
|
|
169
|
+
const trailheadRef = refFor(graph, trailhead.id, 'trailhead');
|
|
170
|
+
const surfaceRefs = trailhead.surfaces.map((surfaceId) =>
|
|
171
|
+
refFor(graph, surfaceId, 'surface')
|
|
172
|
+
);
|
|
173
|
+
for (const memberId of trailhead.memberIds) {
|
|
174
|
+
const memberRef = refFor(graph, memberId, 'trail');
|
|
175
|
+
add(trailheadRef, 'trailhead-groups', memberRef);
|
|
176
|
+
for (const surfaceRef of surfaceRefs) {
|
|
177
|
+
add(surfaceRef, 'surface-projects', memberRef);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
...new Map(edges.map((edge) => [relationKey(edge), edge])).values(),
|
|
184
|
+
].toSorted((left, right) =>
|
|
185
|
+
relationKey(left).localeCompare(relationKey(right))
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const refMatches = (ref: RelationRef, target: RelationRef): boolean =>
|
|
190
|
+
ref.id === target.id && ref.kind === target.kind;
|
|
191
|
+
|
|
192
|
+
export const groupNearbyEdges = (
|
|
193
|
+
edges: readonly RelationEdge[],
|
|
194
|
+
target: RelationRef
|
|
195
|
+
) => {
|
|
196
|
+
const groups = new Map<string, z.output<typeof relationGroupSchema>>();
|
|
197
|
+
for (const edge of edges) {
|
|
198
|
+
const direction = refMatches(edge.from, target) ? 'outgoing' : 'incoming';
|
|
199
|
+
const ref = direction === 'outgoing' ? edge.to : edge.from;
|
|
200
|
+
const key = `${direction}:${edge.relation}`;
|
|
201
|
+
const group = groups.get(key) ?? {
|
|
202
|
+
direction,
|
|
203
|
+
refs: [],
|
|
204
|
+
relation: edge.relation,
|
|
205
|
+
};
|
|
206
|
+
groups.set(key, {
|
|
207
|
+
...group,
|
|
208
|
+
refs: [...group.refs, ref].toSorted((left, right) =>
|
|
209
|
+
`${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)
|
|
210
|
+
),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return [...groups.values()].toSorted((left, right) =>
|
|
214
|
+
`${left.direction}:${left.relation}`.localeCompare(
|
|
215
|
+
`${right.direction}:${right.relation}`
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const edgeTouches = (edge: RelationEdge, target: RelationRef): boolean =>
|
|
221
|
+
refMatches(edge.from, target) || refMatches(edge.to, target);
|
|
222
|
+
|
|
223
|
+
const ambiguousRefId = (
|
|
224
|
+
id: string,
|
|
225
|
+
refs: readonly WayfinderEntityRef[]
|
|
226
|
+
): AmbiguousError =>
|
|
227
|
+
new AmbiguousError(
|
|
228
|
+
`Wayfinder id "${id}" matched multiple entity kinds: ${refs
|
|
229
|
+
.map((ref) => ref.kind)
|
|
230
|
+
.join(', ')}. Pass kind to disambiguate.`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
export const resolveEntityRef = (
|
|
234
|
+
graph: TopoGraph,
|
|
235
|
+
input: {
|
|
236
|
+
readonly id: string;
|
|
237
|
+
readonly kind?: WayfinderEntityKind | undefined;
|
|
238
|
+
}
|
|
239
|
+
): Result<WayfinderEntityRef | undefined, AmbiguousError> => {
|
|
240
|
+
const refs = listWayfinderEntityRefs(graph).filter(
|
|
241
|
+
(ref) =>
|
|
242
|
+
ref.id === input.id &&
|
|
243
|
+
(input.kind === undefined || ref.kind === input.kind)
|
|
244
|
+
);
|
|
245
|
+
if (input.kind === undefined) {
|
|
246
|
+
const uniqueKinds = new Set(refs.map((ref) => ref.kind));
|
|
247
|
+
if (uniqueKinds.size > 1) {
|
|
248
|
+
return Result.err(ambiguousRefId(input.id, refs));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return Result.ok(refs[0]);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const edgesForDirection = (
|
|
255
|
+
edges: readonly RelationEdge[],
|
|
256
|
+
current: RelationRef,
|
|
257
|
+
direction: ImpactDirection
|
|
258
|
+
): readonly {
|
|
259
|
+
readonly edge: RelationEdge;
|
|
260
|
+
readonly node: RelationRef;
|
|
261
|
+
}[] => {
|
|
262
|
+
const outgoing =
|
|
263
|
+
direction === 'downstream' || direction === 'both'
|
|
264
|
+
? edges
|
|
265
|
+
.filter((edge) => refMatches(edge.from, current))
|
|
266
|
+
.map((edge) => ({ edge, node: edge.to }))
|
|
267
|
+
: [];
|
|
268
|
+
const incoming =
|
|
269
|
+
direction === 'upstream' || direction === 'both'
|
|
270
|
+
? edges
|
|
271
|
+
.filter((edge) => refMatches(edge.to, current))
|
|
272
|
+
.map((edge) => ({ edge, node: edge.from }))
|
|
273
|
+
: [];
|
|
274
|
+
return [...outgoing, ...incoming].toSorted((left, right) =>
|
|
275
|
+
relationKey(left.edge).localeCompare(relationKey(right.edge))
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export const impactFor = (
|
|
280
|
+
graph: TopoGraph,
|
|
281
|
+
input: ImpactOptions,
|
|
282
|
+
target: RelationRef
|
|
283
|
+
) => {
|
|
284
|
+
const edges = relationEdges(graph);
|
|
285
|
+
const seen = new Set([`${target.kind}:${target.id}`]);
|
|
286
|
+
const queue: {
|
|
287
|
+
readonly depth: number;
|
|
288
|
+
readonly ref: RelationRef;
|
|
289
|
+
}[] = [{ depth: 0, ref: target }];
|
|
290
|
+
const nodes: z.output<typeof impactNodeSchema>[] = [];
|
|
291
|
+
const includedEdges = new Map<string, RelationEdge>();
|
|
292
|
+
|
|
293
|
+
for (
|
|
294
|
+
let index = 0;
|
|
295
|
+
index < queue.length && nodes.length < input.limit;
|
|
296
|
+
index += 1
|
|
297
|
+
) {
|
|
298
|
+
const current = queue[index];
|
|
299
|
+
if (current === undefined) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (current.depth >= input.maxDepth) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
for (const { edge, node } of edgesForDirection(
|
|
306
|
+
edges,
|
|
307
|
+
current.ref,
|
|
308
|
+
input.direction
|
|
309
|
+
)) {
|
|
310
|
+
includedEdges.set(relationKey(edge), edge);
|
|
311
|
+
const key = `${node.kind}:${node.id}`;
|
|
312
|
+
if (seen.has(key)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
seen.add(key);
|
|
316
|
+
const reached = {
|
|
317
|
+
...node,
|
|
318
|
+
depth: current.depth + 1,
|
|
319
|
+
from: current.ref,
|
|
320
|
+
via: edge.relation,
|
|
321
|
+
};
|
|
322
|
+
nodes.push(reached);
|
|
323
|
+
queue.push({ depth: reached.depth, ref: node });
|
|
324
|
+
if (nodes.length >= input.limit) {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
edges: [...includedEdges.values()].toSorted((left, right) =>
|
|
332
|
+
relationKey(left).localeCompare(relationKey(right))
|
|
333
|
+
),
|
|
334
|
+
nodes,
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export const diffResult = (diff: DiffResult): DiffResult => ({
|
|
339
|
+
breaking: diff.breaking,
|
|
340
|
+
entries: diff.entries,
|
|
341
|
+
hasBreaking: diff.hasBreaking,
|
|
342
|
+
info: diff.info,
|
|
343
|
+
warnings: diff.warnings,
|
|
344
|
+
});
|