@opum-ai/lore 0.1.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/bin/lore.cjs +109 -0
  4. package/package.json +67 -0
  5. package/src/adapters/backlog.ts +1084 -0
  6. package/src/adapters/git.ts +221 -0
  7. package/src/cli.ts +667 -0
  8. package/src/commands/agent.ts +301 -0
  9. package/src/commands/agents.ts +302 -0
  10. package/src/commands/args.ts +209 -0
  11. package/src/commands/changed.ts +70 -0
  12. package/src/commands/check.ts +1031 -0
  13. package/src/commands/codex-bridge.ts +49 -0
  14. package/src/commands/concurrency.ts +48 -0
  15. package/src/commands/context.ts +292 -0
  16. package/src/commands/discover.ts +89 -0
  17. package/src/commands/explorer.ts +253 -0
  18. package/src/commands/export.ts +93 -0
  19. package/src/commands/fswrite.ts +928 -0
  20. package/src/commands/graph.ts +291 -0
  21. package/src/commands/help.ts +151 -0
  22. package/src/commands/impact.ts +59 -0
  23. package/src/commands/init.ts +583 -0
  24. package/src/commands/instructions.ts +91 -0
  25. package/src/commands/link.ts +929 -0
  26. package/src/commands/new.ts +476 -0
  27. package/src/commands/orphans.ts +457 -0
  28. package/src/commands/path.ts +67 -0
  29. package/src/commands/provenance.ts +68 -0
  30. package/src/commands/query.ts +312 -0
  31. package/src/commands/reconcile-shared.ts +280 -0
  32. package/src/commands/rename.ts +585 -0
  33. package/src/commands/replace.ts +320 -0
  34. package/src/commands/scaffold.ts +346 -0
  35. package/src/commands/schema.ts +293 -0
  36. package/src/commands/snapshot.ts +130 -0
  37. package/src/commands/supersede.ts +400 -0
  38. package/src/commands/sync.ts +371 -0
  39. package/src/commands/tasks.ts +271 -0
  40. package/src/commands/traversal.ts +151 -0
  41. package/src/commands/validate.ts +226 -0
  42. package/src/config.ts +598 -0
  43. package/src/core/agent-bridge.ts +287 -0
  44. package/src/core/agent-context.ts +498 -0
  45. package/src/core/agent-profile.ts +447 -0
  46. package/src/core/bundle.ts +893 -0
  47. package/src/core/check.ts +853 -0
  48. package/src/core/codex-bridge.ts +100 -0
  49. package/src/core/concept.ts +597 -0
  50. package/src/core/consumer-scaffold.ts +433 -0
  51. package/src/core/context.ts +271 -0
  52. package/src/core/explorer-contract.ts +441 -0
  53. package/src/core/explorer-qualification.ts +58 -0
  54. package/src/core/explorer.ts +518 -0
  55. package/src/core/finding.ts +31 -0
  56. package/src/core/graph.ts +201 -0
  57. package/src/core/indexes.ts +436 -0
  58. package/src/core/instructions.ts +209 -0
  59. package/src/core/ladybug-driver.ts +1795 -0
  60. package/src/core/ladybug-lifecycle.ts +1178 -0
  61. package/src/core/ladybug-native.ts +95 -0
  62. package/src/core/ladybug-source.ts +667 -0
  63. package/src/core/links.ts +681 -0
  64. package/src/core/log.ts +253 -0
  65. package/src/core/managed-block.ts +540 -0
  66. package/src/core/manifest.ts +718 -0
  67. package/src/core/order.ts +13 -0
  68. package/src/core/profile.ts +1007 -0
  69. package/src/core/projection.ts +195 -0
  70. package/src/core/query.ts +542 -0
  71. package/src/core/reconcile.ts +236 -0
  72. package/src/core/replace.ts +419 -0
  73. package/src/core/retrieval.ts +213 -0
  74. package/src/core/rewrite.ts +940 -0
  75. package/src/core/scaffold.ts +255 -0
  76. package/src/core/schema.ts +366 -0
  77. package/src/core/snapshot-runtime.ts +52 -0
  78. package/src/core/snapshot-store.ts +287 -0
  79. package/src/core/snapshot.ts +711 -0
  80. package/src/core/template.ts +429 -0
  81. package/src/core/traversal.ts +487 -0
  82. package/src/core/validate.ts +517 -0
  83. package/src/core/workspace-contract.ts +473 -0
  84. package/src/core/workspace-projection.ts +365 -0
  85. package/src/core/workspace-retrieval.ts +196 -0
  86. package/src/core/workspace-source.ts +174 -0
  87. package/src/errors.ts +697 -0
  88. package/src/meta.ts +7 -0
  89. package/src/output.ts +589 -0
  90. package/src/scripts/upstream-backlog-watch.ts +288 -0
  91. package/src/state.ts +390 -0
@@ -0,0 +1,487 @@
1
+ /** Storage-neutral bounded path and impact traversal over authored projection facts. */
2
+
3
+ import { LoreError } from "../errors";
4
+ import type {
5
+ LadybugProjectionSource,
6
+ ProjectionConceptRecord,
7
+ ProjectionEdgeRecord,
8
+ ProjectionTaskRecord,
9
+ } from "./ladybug-source";
10
+ import { compareCodeUnits } from "./order";
11
+ import type { WorkspaceRecordProvenance, WorkspaceResultScope } from "./workspace-contract";
12
+
13
+ export type TraversalEndpointKind = "concept" | "task";
14
+ export type TraversalDirection = "outbound" | "inbound" | "either";
15
+
16
+ export const DEFAULT_TRAVERSAL_MAX_DEPTH = 4;
17
+ export const MAX_TRAVERSAL_DEPTH = 16;
18
+ export const DEFAULT_TRAVERSAL_LIMIT = 20;
19
+ export const MAX_TRAVERSAL_LIMIT = 100;
20
+ export const MAX_TRAVERSAL_EDGE_VISITS = 10_000;
21
+
22
+ export interface RepositoryRecordProvenance {
23
+ readonly repositoryScopeKey: string;
24
+ readonly bundleId: string;
25
+ readonly gitCommit: string | null;
26
+ readonly exportDigest: string;
27
+ readonly recordKind: TraversalEndpointKind;
28
+ readonly recordKey: string;
29
+ readonly sourceRecordKey: string;
30
+ readonly sourcePath: string | null;
31
+ readonly sourceId: string;
32
+ }
33
+
34
+ export type TraversalRecordProvenance = RepositoryRecordProvenance | WorkspaceRecordProvenance;
35
+
36
+ export interface TraversalEndpointRef {
37
+ readonly kind: TraversalEndpointKind;
38
+ readonly id: string;
39
+ }
40
+
41
+ export interface TraversalEndpoint extends TraversalEndpointRef {
42
+ readonly recordKey: string;
43
+ readonly provenance: TraversalRecordProvenance;
44
+ }
45
+
46
+ export interface TraversalEdge {
47
+ readonly recordKey: string;
48
+ readonly from: TraversalEndpointRef;
49
+ readonly to: TraversalEndpointRef | null;
50
+ /** Public authored kind. Workspace manifest kinds replace the storage-level link/task kind. */
51
+ readonly kind: string;
52
+ readonly sourceKind: string;
53
+ readonly target: string;
54
+ readonly ordinal: number;
55
+ readonly dangling: boolean;
56
+ readonly provenance: TraversalEdgeProvenance;
57
+ }
58
+
59
+ export interface TraversalEdgeProvenance {
60
+ readonly recordKind: "authored-edge";
61
+ readonly recordKey: string;
62
+ readonly sourceRecordKey: string;
63
+ readonly from: TraversalRecordProvenance;
64
+ readonly to: TraversalRecordProvenance | null;
65
+ }
66
+
67
+ export interface TraversalStep {
68
+ readonly from: TraversalEndpoint;
69
+ readonly to: TraversalEndpoint;
70
+ readonly edge: TraversalEdge;
71
+ /** How this step traversed the authored edge, whose stored orientation remains in edge.from/edge.to. */
72
+ readonly direction: "outbound" | "inbound";
73
+ }
74
+
75
+ export interface TraversalSnapshot {
76
+ readonly endpoints: ReadonlyMap<string, TraversalEndpoint>;
77
+ readonly edges: readonly TraversalEdge[];
78
+ readonly workspace?: WorkspaceResultScope;
79
+ }
80
+
81
+ export interface TraversalSourceRecords {
82
+ readonly concepts: readonly ProjectionConceptRecord[];
83
+ readonly tasks: readonly ProjectionTaskRecord[];
84
+ readonly authoredEdges: readonly ProjectionEdgeRecord[];
85
+ }
86
+
87
+ export interface BuildTraversalSnapshotOptions {
88
+ readonly workspace?: WorkspaceResultScope;
89
+ readonly conceptProvenanceById?: ReadonlyMap<string, WorkspaceRecordProvenance>;
90
+ readonly taskProvenanceById?: ReadonlyMap<string, WorkspaceRecordProvenance>;
91
+ /** Restrict a full workspace source to the already validated selected repository subset. */
92
+ readonly allowedIds?: ReadonlySet<string>;
93
+ }
94
+
95
+ export interface TraversalLimits {
96
+ readonly maxDepth: number;
97
+ readonly limit: number;
98
+ readonly maxEdgeVisits: number;
99
+ }
100
+
101
+ export interface TraversalAccounting {
102
+ readonly edgeVisits: number;
103
+ readonly depthBoundReached: boolean;
104
+ readonly truncated: boolean;
105
+ readonly complete: boolean;
106
+ }
107
+
108
+ export interface PathOptions {
109
+ readonly from: TraversalEndpointRef;
110
+ readonly to: TraversalEndpointRef;
111
+ readonly direction: TraversalDirection;
112
+ readonly edgeKinds?: readonly string[];
113
+ readonly maxDepth?: number;
114
+ readonly limit?: number;
115
+ }
116
+
117
+ export interface TraversalPath {
118
+ readonly depth: number;
119
+ readonly endpoints: readonly TraversalEndpoint[];
120
+ readonly edges: readonly TraversalStep[];
121
+ }
122
+
123
+ export interface PathResult extends TraversalAccounting {
124
+ readonly schemaVersion: "lore-path-result/1";
125
+ readonly from: TraversalEndpoint;
126
+ readonly to: TraversalEndpoint;
127
+ readonly direction: TraversalDirection;
128
+ readonly edgeKinds: readonly string[];
129
+ readonly limits: TraversalLimits;
130
+ readonly paths: readonly TraversalPath[];
131
+ readonly shown: number;
132
+ readonly workspace?: WorkspaceResultScope;
133
+ }
134
+
135
+ export interface ImpactOptions {
136
+ readonly root: TraversalEndpointRef;
137
+ readonly direction: TraversalDirection;
138
+ readonly edgeKinds?: readonly string[];
139
+ readonly maxDepth?: number;
140
+ readonly limit?: number;
141
+ }
142
+
143
+ export interface ImpactEntry {
144
+ readonly endpoint: TraversalEndpoint;
145
+ readonly depth: number;
146
+ readonly relationship: "direct" | "transitive";
147
+ readonly evidence: readonly TraversalStep[];
148
+ }
149
+
150
+ export interface ImpactResult extends TraversalAccounting {
151
+ readonly schemaVersion: "lore-impact-result/1";
152
+ readonly root: TraversalEndpoint;
153
+ readonly direction: TraversalDirection;
154
+ readonly edgeKinds: readonly string[];
155
+ readonly limits: TraversalLimits;
156
+ readonly impacts: readonly ImpactEntry[];
157
+ readonly shown: number;
158
+ readonly workspace?: WorkspaceResultScope;
159
+ }
160
+
161
+ interface WalkState {
162
+ readonly endpointKey: string;
163
+ readonly endpoints: readonly TraversalEndpoint[];
164
+ readonly steps: readonly TraversalStep[];
165
+ readonly visited: ReadonlySet<string>;
166
+ }
167
+
168
+ interface AdjacentStep {
169
+ readonly endpointKey: string;
170
+ readonly step: TraversalStep;
171
+ }
172
+
173
+ /** Build exact typed traversal facts from the already validated projection source. */
174
+ export function buildTraversalSnapshot(
175
+ source: LadybugProjectionSource,
176
+ records: TraversalSourceRecords = source,
177
+ options: BuildTraversalSnapshotOptions = {},
178
+ ): TraversalSnapshot {
179
+ const endpoints = new Map<string, TraversalEndpoint>();
180
+ const byRecordKey = new Map<string, TraversalEndpoint>();
181
+ for (const record of records.concepts) addEndpoint(record, "concept", source, options, endpoints, byRecordKey);
182
+ for (const record of records.tasks) addEndpoint(record, "task", source, options, endpoints, byRecordKey);
183
+
184
+ const edges: TraversalEdge[] = [];
185
+ for (const record of records.authoredEdges) {
186
+ const from = byRecordKey.get(record.from);
187
+ const to = record.to === null ? null : byRecordKey.get(record.to);
188
+ if (from === undefined || (record.to !== null && to === undefined)) {
189
+ // A selected workspace subset deliberately excludes cross-boundary facts.
190
+ if (options.allowedIds !== undefined) continue;
191
+ throw invalidTraversal(`edge ${record.key} names an unknown endpoint`);
192
+ }
193
+ edges.push({
194
+ recordKey: record.key,
195
+ from: endpointRef(from),
196
+ to: to === null || to === undefined ? null : endpointRef(to),
197
+ kind: record.workspaceLinkKind ?? record.kind,
198
+ sourceKind: record.kind,
199
+ target: record.target,
200
+ ordinal: record.ordinal,
201
+ dangling: record.dangling || to === null,
202
+ provenance: {
203
+ recordKind: "authored-edge",
204
+ recordKey: record.key,
205
+ sourceRecordKey: record.workspaceSourceRecordKey ?? record.key,
206
+ from: from.provenance,
207
+ to: to?.provenance ?? null,
208
+ },
209
+ });
210
+ }
211
+ edges.sort(compareEdges);
212
+ return {
213
+ endpoints,
214
+ edges,
215
+ ...(options.workspace !== undefined ? { workspace: options.workspace } : {}),
216
+ };
217
+ }
218
+
219
+ /** Return up to the requested number of shortest deterministic simple paths. */
220
+ export function findPaths(snapshot: TraversalSnapshot, options: PathOptions): PathResult {
221
+ const limits = normalizedLimits(options.maxDepth, options.limit);
222
+ const from = requireEndpoint(snapshot, options.from);
223
+ const to = requireEndpoint(snapshot, options.to);
224
+ const allowedKinds = normalizedKinds(options.edgeKinds, snapshot.edges);
225
+ const adjacency = buildDirectedAdjacency(snapshot, options.direction, new Set(allowedKinds));
226
+ const queue: WalkState[] = [stateFor(from)];
227
+ const matches: TraversalPath[] = [];
228
+ let cursor = 0;
229
+ let edgeVisits = 0;
230
+ let visitTruncated = false;
231
+ let depthBoundReached = false;
232
+
233
+ while (cursor < queue.length && matches.length <= limits.limit && !visitTruncated) {
234
+ const state = queue[cursor++] as WalkState;
235
+ if (state.endpointKey === endpointKey(to)) {
236
+ matches.push({
237
+ depth: state.steps.length,
238
+ endpoints: state.endpoints,
239
+ edges: state.steps,
240
+ });
241
+ continue;
242
+ }
243
+ if (state.steps.length >= limits.maxDepth) {
244
+ if ((adjacency.get(state.endpointKey) ?? []).some((adjacent) => !state.visited.has(adjacent.endpointKey))) {
245
+ depthBoundReached = true;
246
+ }
247
+ continue;
248
+ }
249
+ for (const adjacent of adjacency.get(state.endpointKey) ?? []) {
250
+ edgeVisits += 1;
251
+ if (edgeVisits > limits.maxEdgeVisits) {
252
+ visitTruncated = true;
253
+ break;
254
+ }
255
+ if (state.visited.has(adjacent.endpointKey)) continue;
256
+ const endpoint = snapshot.endpoints.get(adjacent.endpointKey);
257
+ if (endpoint === undefined) throw invalidTraversal("adjacency names an unknown endpoint");
258
+ queue.push({
259
+ endpointKey: adjacent.endpointKey,
260
+ endpoints: [...state.endpoints, endpoint],
261
+ steps: [...state.steps, adjacent.step],
262
+ visited: new Set([...state.visited, adjacent.endpointKey]),
263
+ });
264
+ }
265
+ }
266
+
267
+ const resultTruncated = matches.length > limits.limit;
268
+ const paths = matches.slice(0, limits.limit);
269
+ const truncated = resultTruncated || visitTruncated;
270
+ return {
271
+ schemaVersion: "lore-path-result/1",
272
+ from,
273
+ to,
274
+ direction: options.direction,
275
+ edgeKinds: allowedKinds,
276
+ limits,
277
+ paths,
278
+ shown: paths.length,
279
+ edgeVisits: Math.min(edgeVisits, limits.maxEdgeVisits),
280
+ depthBoundReached,
281
+ truncated,
282
+ complete: !truncated,
283
+ ...(snapshot.workspace !== undefined ? { workspace: snapshot.workspace } : {}),
284
+ };
285
+ }
286
+
287
+ /** Expand a bounded impact frontier and retain one canonical shortest evidence chain per endpoint. */
288
+ export function findImpact(snapshot: TraversalSnapshot, options: ImpactOptions): ImpactResult {
289
+ const limits = normalizedLimits(options.maxDepth, options.limit);
290
+ const root = requireEndpoint(snapshot, options.root);
291
+ const allowedKinds = normalizedKinds(options.edgeKinds, snapshot.edges);
292
+ const adjacency = buildDirectedAdjacency(snapshot, options.direction, new Set(allowedKinds));
293
+ const queue: WalkState[] = [stateFor(root)];
294
+ const visited = new Set<string>([endpointKey(root)]);
295
+ const impacts: ImpactEntry[] = [];
296
+ let cursor = 0;
297
+ let edgeVisits = 0;
298
+ let visitTruncated = false;
299
+ let depthBoundReached = false;
300
+
301
+ while (cursor < queue.length && impacts.length <= limits.limit && !visitTruncated) {
302
+ const state = queue[cursor++] as WalkState;
303
+ if (state.steps.length >= limits.maxDepth) {
304
+ if ((adjacency.get(state.endpointKey) ?? []).some((adjacent) => !visited.has(adjacent.endpointKey))) {
305
+ depthBoundReached = true;
306
+ }
307
+ continue;
308
+ }
309
+ for (const adjacent of adjacency.get(state.endpointKey) ?? []) {
310
+ edgeVisits += 1;
311
+ if (edgeVisits > limits.maxEdgeVisits) {
312
+ visitTruncated = true;
313
+ break;
314
+ }
315
+ if (visited.has(adjacent.endpointKey)) continue;
316
+ const endpoint = snapshot.endpoints.get(adjacent.endpointKey);
317
+ if (endpoint === undefined) throw invalidTraversal("adjacency names an unknown endpoint");
318
+ visited.add(adjacent.endpointKey);
319
+ const steps = [...state.steps, adjacent.step];
320
+ impacts.push({
321
+ endpoint,
322
+ depth: steps.length,
323
+ relationship: steps.length === 1 ? "direct" : "transitive",
324
+ evidence: steps,
325
+ });
326
+ queue.push({
327
+ endpointKey: adjacent.endpointKey,
328
+ endpoints: [...state.endpoints, endpoint],
329
+ steps,
330
+ visited,
331
+ });
332
+ if (impacts.length > limits.limit) break;
333
+ }
334
+ }
335
+
336
+ const resultTruncated = impacts.length > limits.limit;
337
+ const shown = impacts.slice(0, limits.limit);
338
+ const truncated = resultTruncated || visitTruncated;
339
+ return {
340
+ schemaVersion: "lore-impact-result/1",
341
+ root,
342
+ direction: options.direction,
343
+ edgeKinds: allowedKinds,
344
+ limits,
345
+ impacts: shown,
346
+ shown: shown.length,
347
+ edgeVisits: Math.min(edgeVisits, limits.maxEdgeVisits),
348
+ depthBoundReached,
349
+ truncated,
350
+ complete: !truncated,
351
+ ...(snapshot.workspace !== undefined ? { workspace: snapshot.workspace } : {}),
352
+ };
353
+ }
354
+
355
+ function addEndpoint(
356
+ record: ProjectionConceptRecord | ProjectionTaskRecord,
357
+ kind: TraversalEndpointKind,
358
+ source: LadybugProjectionSource,
359
+ options: BuildTraversalSnapshotOptions,
360
+ endpoints: Map<string, TraversalEndpoint>,
361
+ byRecordKey: Map<string, TraversalEndpoint>,
362
+ ): void {
363
+ if (options.allowedIds !== undefined && !options.allowedIds.has(record.id)) return;
364
+ const workspaceProvenance =
365
+ kind === "concept" ? options.conceptProvenanceById?.get(record.id) : options.taskProvenanceById?.get(record.id);
366
+ const provenance: TraversalRecordProvenance = workspaceProvenance ?? repositoryProvenance(record, kind, source);
367
+ const endpoint: TraversalEndpoint = {
368
+ kind,
369
+ id: record.id,
370
+ recordKey: record.key,
371
+ provenance,
372
+ };
373
+ const key = endpointKey(endpoint);
374
+ if (endpoints.has(key) || byRecordKey.has(record.key))
375
+ throw invalidTraversal(`duplicate ${kind} endpoint ${record.id}`);
376
+ endpoints.set(key, endpoint);
377
+ byRecordKey.set(record.key, endpoint);
378
+ }
379
+
380
+ function repositoryProvenance(
381
+ record: ProjectionConceptRecord | ProjectionTaskRecord,
382
+ kind: TraversalEndpointKind,
383
+ source: LadybugProjectionSource,
384
+ ): RepositoryRecordProvenance {
385
+ return {
386
+ repositoryScopeKey: source.repositoryScopeKey,
387
+ bundleId: source.manifest.bundle.id,
388
+ gitCommit: source.manifest.bundle.gitCommit,
389
+ exportDigest: source.exportDigest,
390
+ recordKind: kind,
391
+ recordKey: record.key,
392
+ sourceRecordKey: record.key,
393
+ sourcePath: kind === "concept" ? (record as ProjectionConceptRecord).path : null,
394
+ sourceId: record.id,
395
+ };
396
+ }
397
+
398
+ function normalizedLimits(maxDepth = DEFAULT_TRAVERSAL_MAX_DEPTH, limit = DEFAULT_TRAVERSAL_LIMIT): TraversalLimits {
399
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 0 || maxDepth > MAX_TRAVERSAL_DEPTH) {
400
+ throw invalidTraversal(`maximum depth must be between 0 and ${MAX_TRAVERSAL_DEPTH}`);
401
+ }
402
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TRAVERSAL_LIMIT) {
403
+ throw invalidTraversal(`result limit must be between 1 and ${MAX_TRAVERSAL_LIMIT}`);
404
+ }
405
+ return { maxDepth, limit, maxEdgeVisits: MAX_TRAVERSAL_EDGE_VISITS };
406
+ }
407
+
408
+ function normalizedKinds(requested: readonly string[] | undefined, edges: readonly TraversalEdge[]): string[] {
409
+ const values = requested === undefined || requested.length === 0 ? edges.map((edge) => edge.kind) : requested;
410
+ return [...new Set(values)].sort(compareCodeUnits);
411
+ }
412
+
413
+ function requireEndpoint(snapshot: TraversalSnapshot, ref: TraversalEndpointRef): TraversalEndpoint {
414
+ const endpoint = snapshot.endpoints.get(endpointKey(ref));
415
+ if (endpoint !== undefined) return endpoint;
416
+ throw new LoreError(
417
+ "not_found",
418
+ `${ref.kind} "${ref.id}" is not in the selected traversal scope`,
419
+ "check the typed endpoint and explicit repository selection",
420
+ );
421
+ }
422
+
423
+ function stateFor(endpoint: TraversalEndpoint): WalkState {
424
+ const key = endpointKey(endpoint);
425
+ return {
426
+ endpointKey: key,
427
+ endpoints: [endpoint],
428
+ steps: [],
429
+ visited: new Set([key]),
430
+ };
431
+ }
432
+
433
+ function buildDirectedAdjacency(
434
+ snapshot: TraversalSnapshot,
435
+ direction: TraversalDirection,
436
+ allowedKinds: ReadonlySet<string>,
437
+ ): Map<string, AdjacentStep[]> {
438
+ const adjacency = new Map<string, AdjacentStep[]>();
439
+ const add = (
440
+ from: TraversalEndpoint,
441
+ to: TraversalEndpoint,
442
+ edge: TraversalEdge,
443
+ stepDirection: "outbound" | "inbound",
444
+ ) => {
445
+ const key = endpointKey(from);
446
+ const values = adjacency.get(key) ?? [];
447
+ values.push({
448
+ endpointKey: endpointKey(to),
449
+ step: { from, to, edge, direction: stepDirection },
450
+ });
451
+ adjacency.set(key, values);
452
+ };
453
+ for (const edge of snapshot.edges) {
454
+ if (edge.to === null || !allowedKinds.has(edge.kind)) continue;
455
+ const from = snapshot.endpoints.get(endpointKey(edge.from));
456
+ const to = snapshot.endpoints.get(endpointKey(edge.to));
457
+ if (from === undefined || to === undefined)
458
+ throw invalidTraversal("edge endpoint is missing from traversal snapshot");
459
+ if (direction === "outbound" || direction === "either") add(from, to, edge, "outbound");
460
+ if (direction === "inbound" || direction === "either") add(to, from, edge, "inbound");
461
+ }
462
+ return adjacency;
463
+ }
464
+
465
+ function endpointRef(endpoint: TraversalEndpoint): TraversalEndpointRef {
466
+ return { kind: endpoint.kind, id: endpoint.id };
467
+ }
468
+
469
+ function endpointKey(endpoint: TraversalEndpointRef): string {
470
+ return `${endpoint.kind}\0${endpoint.id}`;
471
+ }
472
+
473
+ function compareEdges(a: TraversalEdge, b: TraversalEdge): number {
474
+ return (
475
+ compareCodeUnits(a.from.kind, b.from.kind) ||
476
+ compareCodeUnits(a.from.id, b.from.id) ||
477
+ compareCodeUnits(a.to?.kind ?? "", b.to?.kind ?? "") ||
478
+ compareCodeUnits(a.to?.id ?? "", b.to?.id ?? "") ||
479
+ compareCodeUnits(a.kind, b.kind) ||
480
+ a.ordinal - b.ordinal ||
481
+ compareCodeUnits(a.recordKey, b.recordKey)
482
+ );
483
+ }
484
+
485
+ function invalidTraversal(message: string): LoreError {
486
+ return new LoreError("validation", `traversal projection is invalid: ${message}`);
487
+ }