@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,430 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { matchesAnyTrailIdGlob } from '@ontrails/core';
|
|
4
|
+
import type {
|
|
5
|
+
TopoGraph,
|
|
6
|
+
TopoGraphEntry,
|
|
7
|
+
TopoGraphTrailheadEntry,
|
|
8
|
+
TopoGraphVersionEntry,
|
|
9
|
+
} from '../types.js';
|
|
10
|
+
|
|
11
|
+
export const wayfinderEntityKindSchema = z.enum([
|
|
12
|
+
'entity',
|
|
13
|
+
'trailhead',
|
|
14
|
+
'resource',
|
|
15
|
+
'signal',
|
|
16
|
+
'surface',
|
|
17
|
+
'trail',
|
|
18
|
+
'version',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export type WayfinderEntityKind = z.infer<typeof wayfinderEntityKindSchema>;
|
|
22
|
+
|
|
23
|
+
export const wayfinderIntentSchema = z.enum(['destroy', 'read', 'write']);
|
|
24
|
+
|
|
25
|
+
export type WayfinderIntent = z.infer<typeof wayfinderIntentSchema>;
|
|
26
|
+
|
|
27
|
+
const stringListSchema = z.union([z.string(), z.array(z.string()).readonly()]);
|
|
28
|
+
|
|
29
|
+
const entityKindListSchema = z.union([
|
|
30
|
+
wayfinderEntityKindSchema,
|
|
31
|
+
z.array(wayfinderEntityKindSchema).readonly(),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const intentListSchema = z.union([
|
|
35
|
+
wayfinderIntentSchema,
|
|
36
|
+
z.array(wayfinderIntentSchema).readonly(),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const wayfinderEntityFilterSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
exampleCoverage: z.boolean().optional(),
|
|
42
|
+
id: stringListSchema.optional(),
|
|
43
|
+
idGlob: stringListSchema.optional(),
|
|
44
|
+
idPrefix: stringListSchema.optional(),
|
|
45
|
+
intent: intentListSchema.optional(),
|
|
46
|
+
kind: entityKindListSchema.optional(),
|
|
47
|
+
namespace: stringListSchema.optional(),
|
|
48
|
+
query: z.string().optional(),
|
|
49
|
+
surface: stringListSchema.optional(),
|
|
50
|
+
trailhead: stringListSchema.optional(),
|
|
51
|
+
usesResource: stringListSchema.optional(),
|
|
52
|
+
usesSignal: stringListSchema.optional(),
|
|
53
|
+
versioned: z.boolean().optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
|
|
57
|
+
export type WayfinderEntityFilterInput = z.input<
|
|
58
|
+
typeof wayfinderEntityFilterSchema
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
export type WayfinderEntityFilters = z.output<
|
|
62
|
+
typeof wayfinderEntityFilterSchema
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
export interface WayfinderEntityRef {
|
|
66
|
+
readonly entry?: TopoGraphEntry | undefined;
|
|
67
|
+
readonly trailhead?: TopoGraphTrailheadEntry | undefined;
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly kind: WayfinderEntityKind;
|
|
70
|
+
readonly trailId?: string | undefined;
|
|
71
|
+
readonly version?: TopoGraphVersionEntry | undefined;
|
|
72
|
+
readonly versionKey?: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WayfinderFilterContext {
|
|
76
|
+
readonly trailheadsById: ReadonlyMap<string, TopoGraphTrailheadEntry>;
|
|
77
|
+
readonly trailheadIdsByTrailId: ReadonlyMap<string, readonly string[]>;
|
|
78
|
+
readonly trailheadSurfacesByTrailId: ReadonlyMap<string, readonly string[]>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toArray = <TValue>(
|
|
82
|
+
value: TValue | readonly TValue[] | undefined
|
|
83
|
+
): readonly TValue[] => {
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
return value as readonly TValue[];
|
|
89
|
+
}
|
|
90
|
+
return [value as TValue];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const includesAny = (
|
|
94
|
+
values: readonly string[] | undefined,
|
|
95
|
+
expected: readonly string[]
|
|
96
|
+
): boolean =>
|
|
97
|
+
expected.length === 0 ||
|
|
98
|
+
(values ?? []).some((value) => expected.includes(value));
|
|
99
|
+
|
|
100
|
+
const namespaceMatches = (id: string, namespace: string): boolean =>
|
|
101
|
+
id === namespace || id.startsWith(`${namespace}.`);
|
|
102
|
+
|
|
103
|
+
const entryHasVersioning = (entry: TopoGraphEntry): boolean =>
|
|
104
|
+
entry.version !== undefined ||
|
|
105
|
+
entry.versions !== undefined ||
|
|
106
|
+
(entry.supports?.length ?? 0) > 0;
|
|
107
|
+
|
|
108
|
+
const entryHasExamples = (entry: TopoGraphEntry): boolean =>
|
|
109
|
+
entry.exampleCount > 0 || (entry.examples?.length ?? 0) > 0;
|
|
110
|
+
|
|
111
|
+
const versionHasExamples = (version: TopoGraphVersionEntry): boolean =>
|
|
112
|
+
version.exampleCount > 0 || (version.examples?.length ?? 0) > 0;
|
|
113
|
+
|
|
114
|
+
const entrySignalIds = (entry: TopoGraphEntry): readonly string[] => [
|
|
115
|
+
...(entry.fires ?? []),
|
|
116
|
+
...(entry.on ?? []),
|
|
117
|
+
...(entry.from ?? []),
|
|
118
|
+
...(entry.producers ?? []),
|
|
119
|
+
...(entry.consumers ?? []),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const entrySurfaces = (
|
|
123
|
+
ref: WayfinderEntityRef,
|
|
124
|
+
context: WayfinderFilterContext
|
|
125
|
+
): readonly string[] => {
|
|
126
|
+
if (ref.kind === 'trailhead') {
|
|
127
|
+
return ref.trailhead?.surfaces ?? [];
|
|
128
|
+
}
|
|
129
|
+
if (ref.kind === 'surface') {
|
|
130
|
+
return [ref.id];
|
|
131
|
+
}
|
|
132
|
+
return [
|
|
133
|
+
...(ref.entry?.surfaces ??
|
|
134
|
+
context.trailheadsById.get(ref.id)?.surfaces ??
|
|
135
|
+
[]),
|
|
136
|
+
...(context.trailheadSurfacesByTrailId.get(ref.trailId ?? ref.id) ?? []),
|
|
137
|
+
];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const refHasExamples = (ref: WayfinderEntityRef): boolean => {
|
|
141
|
+
if (ref.version !== undefined) {
|
|
142
|
+
return versionHasExamples(ref.version);
|
|
143
|
+
}
|
|
144
|
+
return ref.entry === undefined ? false : entryHasExamples(ref.entry);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const refHasVersioning = (ref: WayfinderEntityRef): boolean => {
|
|
148
|
+
if (ref.kind === 'version') {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return ref.entry === undefined ? false : entryHasVersioning(ref.entry);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const refResourceIds = (ref: WayfinderEntityRef): readonly string[] =>
|
|
155
|
+
[
|
|
156
|
+
...(ref.entry?.resources ?? []),
|
|
157
|
+
...(ref.version?.resources ?? []),
|
|
158
|
+
...(ref.kind === 'resource' ? [ref.id] : []),
|
|
159
|
+
].toSorted();
|
|
160
|
+
|
|
161
|
+
const refSignalIds = (ref: WayfinderEntityRef): readonly string[] => {
|
|
162
|
+
if (ref.entry !== undefined) {
|
|
163
|
+
return entrySignalIds(ref.entry);
|
|
164
|
+
}
|
|
165
|
+
return ref.kind === 'signal' ? [ref.id] : [];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const refTrailheadIds = (
|
|
169
|
+
ref: WayfinderEntityRef,
|
|
170
|
+
context: WayfinderFilterContext
|
|
171
|
+
): readonly string[] => {
|
|
172
|
+
if (ref.kind === 'trailhead') {
|
|
173
|
+
return [ref.id];
|
|
174
|
+
}
|
|
175
|
+
return context.trailheadIdsByTrailId.get(ref.trailId ?? ref.id) ?? [];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const createWayfinderFilterContext = (
|
|
179
|
+
topoGraph: TopoGraph
|
|
180
|
+
): WayfinderFilterContext => {
|
|
181
|
+
const trailheadsById = new Map<string, TopoGraphTrailheadEntry>();
|
|
182
|
+
const trailheadIdsByTrailId = new Map<string, string[]>();
|
|
183
|
+
const trailheadSurfacesByTrailId = new Map<string, string[]>();
|
|
184
|
+
|
|
185
|
+
for (const trailhead of topoGraph.trailheads ?? []) {
|
|
186
|
+
trailheadsById.set(trailhead.id, trailhead);
|
|
187
|
+
for (const memberId of trailhead.memberIds) {
|
|
188
|
+
const current = trailheadIdsByTrailId.get(memberId) ?? [];
|
|
189
|
+
current.push(trailhead.id);
|
|
190
|
+
trailheadIdsByTrailId.set(memberId, current);
|
|
191
|
+
|
|
192
|
+
const currentSurfaces = trailheadSurfacesByTrailId.get(memberId) ?? [];
|
|
193
|
+
currentSurfaces.push(...trailhead.surfaces);
|
|
194
|
+
trailheadSurfacesByTrailId.set(
|
|
195
|
+
memberId,
|
|
196
|
+
[...new Set(currentSurfaces)].toSorted()
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
trailheadIdsByTrailId,
|
|
203
|
+
trailheadSurfacesByTrailId,
|
|
204
|
+
trailheadsById,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const listWayfinderEntityRefs = (
|
|
209
|
+
topoGraph: TopoGraph
|
|
210
|
+
): readonly WayfinderEntityRef[] => {
|
|
211
|
+
const refs: WayfinderEntityRef[] = topoGraph.entries.map((entry) => ({
|
|
212
|
+
entry,
|
|
213
|
+
id: entry.id,
|
|
214
|
+
kind: entry.kind,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
for (const trailhead of topoGraph.trailheads ?? []) {
|
|
218
|
+
refs.push({ id: trailhead.id, kind: 'trailhead', trailhead });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const surfaceIds = new Set<string>();
|
|
222
|
+
for (const entry of topoGraph.entries) {
|
|
223
|
+
for (const surface of entry.surfaces) {
|
|
224
|
+
surfaceIds.add(surface);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const trailhead of topoGraph.trailheads ?? []) {
|
|
228
|
+
for (const surface of trailhead.surfaces) {
|
|
229
|
+
surfaceIds.add(surface);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const surface of [...surfaceIds].toSorted()) {
|
|
233
|
+
refs.push({ id: surface, kind: 'surface' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const entry of topoGraph.entries) {
|
|
237
|
+
if (entry.kind !== 'trail') {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const versionRefs: WayfinderEntityRef[] =
|
|
241
|
+
entry.version === undefined
|
|
242
|
+
? []
|
|
243
|
+
: [
|
|
244
|
+
{
|
|
245
|
+
entry,
|
|
246
|
+
id: `${entry.id}@${entry.version}`,
|
|
247
|
+
kind: 'version',
|
|
248
|
+
trailId: entry.id,
|
|
249
|
+
versionKey: String(entry.version),
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
for (const [versionKey, version] of Object.entries(entry.versions ?? {})) {
|
|
253
|
+
versionRefs.push({
|
|
254
|
+
entry,
|
|
255
|
+
id: `${entry.id}@${versionKey}`,
|
|
256
|
+
kind: 'version',
|
|
257
|
+
trailId: entry.id,
|
|
258
|
+
version,
|
|
259
|
+
versionKey,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
refs.push(...versionRefs.toSorted((a, b) => a.id.localeCompare(b.id)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return refs;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const matchesIdentityFilters = (
|
|
269
|
+
ref: WayfinderEntityRef,
|
|
270
|
+
filters: {
|
|
271
|
+
readonly idPrefixes: readonly string[];
|
|
272
|
+
readonly idGlobs: readonly string[];
|
|
273
|
+
readonly ids: readonly string[];
|
|
274
|
+
readonly namespaces: readonly string[];
|
|
275
|
+
}
|
|
276
|
+
): boolean => {
|
|
277
|
+
if (filters.ids.length > 0 && !filters.ids.includes(ref.id)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
filters.idPrefixes.length > 0 &&
|
|
282
|
+
!filters.idPrefixes.some((prefix) => ref.id.startsWith(prefix))
|
|
283
|
+
) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
filters.idGlobs.length > 0 &&
|
|
288
|
+
!matchesAnyTrailIdGlob(ref.id, filters.idGlobs)
|
|
289
|
+
) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return (
|
|
293
|
+
filters.namespaces.length === 0 ||
|
|
294
|
+
filters.namespaces.some((namespace) => namespaceMatches(ref.id, namespace))
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const matchesTrailFilters = (
|
|
299
|
+
ref: WayfinderEntityRef,
|
|
300
|
+
filters: {
|
|
301
|
+
readonly exampleCoverage?: boolean | undefined;
|
|
302
|
+
readonly intents: readonly WayfinderIntent[];
|
|
303
|
+
readonly versioned?: boolean | undefined;
|
|
304
|
+
}
|
|
305
|
+
): boolean => {
|
|
306
|
+
if (
|
|
307
|
+
filters.intents.length > 0 &&
|
|
308
|
+
(ref.entry?.kind !== 'trail' ||
|
|
309
|
+
!filters.intents.includes(ref.entry.intent ?? 'write'))
|
|
310
|
+
) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
filters.versioned !== undefined &&
|
|
315
|
+
refHasVersioning(ref) !== filters.versioned
|
|
316
|
+
) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return (
|
|
320
|
+
filters.exampleCoverage === undefined ||
|
|
321
|
+
refHasExamples(ref) === filters.exampleCoverage
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const matchesRelationshipFilters = (
|
|
326
|
+
ref: WayfinderEntityRef,
|
|
327
|
+
context: WayfinderFilterContext,
|
|
328
|
+
filters: {
|
|
329
|
+
readonly trailheads: readonly string[];
|
|
330
|
+
readonly resources: readonly string[];
|
|
331
|
+
readonly signals: readonly string[];
|
|
332
|
+
readonly surfaces: readonly string[];
|
|
333
|
+
}
|
|
334
|
+
): boolean =>
|
|
335
|
+
includesAny(entrySurfaces(ref, context), filters.surfaces) &&
|
|
336
|
+
includesAny(refTrailheadIds(ref, context), filters.trailheads) &&
|
|
337
|
+
includesAny(refResourceIds(ref), filters.resources) &&
|
|
338
|
+
includesAny(refSignalIds(ref), filters.signals);
|
|
339
|
+
|
|
340
|
+
const matchesQueryFilter = (
|
|
341
|
+
ref: WayfinderEntityRef,
|
|
342
|
+
context: WayfinderFilterContext,
|
|
343
|
+
query: string | undefined
|
|
344
|
+
): boolean => {
|
|
345
|
+
const normalized = query?.trim().toLowerCase();
|
|
346
|
+
if (!normalized) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
const haystack = [
|
|
350
|
+
ref.id,
|
|
351
|
+
ref.kind,
|
|
352
|
+
ref.trailId,
|
|
353
|
+
ref.entry?.intent,
|
|
354
|
+
...entrySurfaces(ref, context),
|
|
355
|
+
...refTrailheadIds(ref, context),
|
|
356
|
+
...refResourceIds(ref),
|
|
357
|
+
...refSignalIds(ref),
|
|
358
|
+
]
|
|
359
|
+
.filter((value): value is string => typeof value === 'string')
|
|
360
|
+
.join(' ')
|
|
361
|
+
.toLowerCase();
|
|
362
|
+
return haystack.includes(normalized);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const createWayfinderEntityPredicate = (
|
|
366
|
+
context: WayfinderFilterContext,
|
|
367
|
+
filters: WayfinderEntityFilterInput = {}
|
|
368
|
+
): ((ref: WayfinderEntityRef) => boolean) => {
|
|
369
|
+
const parsed = wayfinderEntityFilterSchema.parse(filters);
|
|
370
|
+
const kinds = toArray(parsed.kind);
|
|
371
|
+
const ids = toArray(parsed.id);
|
|
372
|
+
const idGlobs = toArray(parsed.idGlob);
|
|
373
|
+
const idPrefixes = toArray(parsed.idPrefix);
|
|
374
|
+
const namespaces = toArray(parsed.namespace);
|
|
375
|
+
const intents = toArray(parsed.intent);
|
|
376
|
+
const surfaces = toArray(parsed.surface);
|
|
377
|
+
const trailheads = toArray(parsed.trailhead);
|
|
378
|
+
const resources = toArray(parsed.usesResource);
|
|
379
|
+
const signals = toArray(parsed.usesSignal);
|
|
380
|
+
return (ref) => {
|
|
381
|
+
if (kinds.length > 0 && !kinds.includes(ref.kind)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (
|
|
385
|
+
!matchesIdentityFilters(ref, { idGlobs, idPrefixes, ids, namespaces })
|
|
386
|
+
) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
!matchesTrailFilters(ref, {
|
|
391
|
+
exampleCoverage: parsed.exampleCoverage,
|
|
392
|
+
intents,
|
|
393
|
+
versioned: parsed.versioned,
|
|
394
|
+
})
|
|
395
|
+
) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
!matchesRelationshipFilters(ref, context, {
|
|
400
|
+
resources,
|
|
401
|
+
signals,
|
|
402
|
+
surfaces,
|
|
403
|
+
trailheads,
|
|
404
|
+
})
|
|
405
|
+
) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
if (!matchesQueryFilter(ref, context, parsed.query)) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export const createWayfinderGraphEntityPredicate = (
|
|
416
|
+
topoGraph: TopoGraph,
|
|
417
|
+
filters: WayfinderEntityFilterInput = {}
|
|
418
|
+
): ((ref: WayfinderEntityRef) => boolean) =>
|
|
419
|
+
createWayfinderEntityPredicate(
|
|
420
|
+
createWayfinderFilterContext(topoGraph),
|
|
421
|
+
filters
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
export const filterWayfinderEntityRefs = (
|
|
425
|
+
topoGraph: TopoGraph,
|
|
426
|
+
filters: WayfinderEntityFilterInput = {}
|
|
427
|
+
): readonly WayfinderEntityRef[] =>
|
|
428
|
+
listWayfinderEntityRefs(topoGraph).filter(
|
|
429
|
+
createWayfinderGraphEntityPredicate(topoGraph, filters)
|
|
430
|
+
);
|