@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.
@@ -0,0 +1,1656 @@
1
+ import { join, resolve } from 'node:path';
2
+
3
+ import { adapterTargetPlacements, checkAdapters } from '@ontrails/adapter-kit';
4
+ import type { AdapterFact } from '@ontrails/adapter-kit';
5
+ import {
6
+ AmbiguousError,
7
+ DerivationError,
8
+ NotFoundError,
9
+ Result,
10
+ ValidationError,
11
+ errorCategories,
12
+ surfaceNames,
13
+ topo,
14
+ trail,
15
+ } from '@ontrails/core';
16
+ import type { TrailsError } from '@ontrails/core';
17
+ import { z } from 'zod';
18
+
19
+ import { deriveTopoGraphDiff } from '../diff.js';
20
+ import type { TopoGraph, TopoGraphEntry } from '../types.js';
21
+ import {
22
+ filterWayfinderEntityRefs,
23
+ wayfinderEntityFilterSchema,
24
+ } from './filters.js';
25
+ import type {
26
+ WayfinderEntityFilterInput,
27
+ WayfinderEntityKind,
28
+ } from './filters.js';
29
+ import { loadWayfinderArtifacts } from './loader.js';
30
+ import type {
31
+ WayfinderArtifactLoad,
32
+ WayfinderArtifactLoaderOptions,
33
+ } from './loader.js';
34
+ import { deriveTrailErrorFacts } from './error-facts.js';
35
+ import {
36
+ resolveWayfinderPopulation,
37
+ resolveWayfinderRelations,
38
+ } from './navigation.js';
39
+ import { wayfinderDriftFromArtifactStatus } from './provenance.js';
40
+ import {
41
+ diffResult,
42
+ impactNodeSchema,
43
+ relationEdgeSchema,
44
+ relationGroupSchema,
45
+ } from './relations.js';
46
+ const artifactSourceSchema = z.object({
47
+ kind: z.enum(['lockManifest', 'topoGraph', 'topoStore']),
48
+ path: z.string().optional(),
49
+ schemaVersion: z.number().optional(),
50
+ });
51
+
52
+ const envelopeSchema = z.object({
53
+ drift: z.object({
54
+ artifacts: z
55
+ .array(z.enum(['lockManifest', 'topoGraph', 'topoStore']))
56
+ .readonly()
57
+ .optional(),
58
+ reasons: z.array(z.record(z.string(), z.unknown())).readonly().optional(),
59
+ status: z.enum(['absent', 'aligned', 'drifted']),
60
+ }),
61
+ source: artifactSourceSchema,
62
+ });
63
+
64
+ const sourceInputSchema = z
65
+ .object({
66
+ dir: z.string().optional().describe('Directory containing trails.lock'),
67
+ rootDir: z.string().optional().describe('Workspace root directory'),
68
+ trailsDbPath: z.string().optional().describe('Path to trails.db'),
69
+ })
70
+ .strict();
71
+
72
+ const filteredInputSchema = sourceInputSchema.extend({
73
+ filters: wayfinderEntityFilterSchema.optional(),
74
+ limit: z.number().int().positive().max(500).default(100),
75
+ });
76
+
77
+ const adapterFactKindSchema = z.enum([
78
+ 'available',
79
+ 'configured',
80
+ 'observed',
81
+ 'used',
82
+ ]);
83
+
84
+ const adapterFactsInputSchema = z
85
+ .object({
86
+ filters: z
87
+ .object({
88
+ kind: adapterFactKindSchema.optional(),
89
+ packageName: z.string().optional(),
90
+ target: z.string().optional(),
91
+ })
92
+ .optional(),
93
+ limit: z.number().int().positive().max(500).default(100),
94
+ rootDir: z.string().optional().describe('Workspace root directory'),
95
+ })
96
+ .strict();
97
+
98
+ const overlayInputSchema = sourceInputSchema.extend({
99
+ namespace: z
100
+ .string()
101
+ .min(1)
102
+ .describe('Overlay namespace to read from the saved graph'),
103
+ });
104
+
105
+ const inspectKindSchema = z.enum([
106
+ 'entity',
107
+ 'trailhead',
108
+ 'resource',
109
+ 'signal',
110
+ 'surface',
111
+ 'trail',
112
+ 'version',
113
+ ]);
114
+
115
+ const inspectInputSchema = sourceInputSchema.extend({
116
+ id: z.string().min(1).describe('Entity ID to inspect'),
117
+ kind: inspectKindSchema.optional().describe('Optional entity kind'),
118
+ });
119
+
120
+ const contractInputSchema = inspectInputSchema.extend({
121
+ version: z.number().int().positive().optional(),
122
+ });
123
+
124
+ const relationInputSchema = inspectInputSchema.extend({
125
+ filters: wayfinderEntityFilterSchema.optional(),
126
+ });
127
+
128
+ const impactDirectionSchema = z
129
+ .enum(['downstream', 'upstream', 'both'])
130
+ .default('downstream');
131
+
132
+ const impactInputSchema = inspectInputSchema.extend({
133
+ direction: impactDirectionSchema,
134
+ filters: wayfinderEntityFilterSchema.optional(),
135
+ limit: z.number().int().positive().max(500).default(100),
136
+ maxDepth: z.number().int().positive().max(10).default(2),
137
+ });
138
+
139
+ type ImpactDirection = z.output<typeof impactDirectionSchema>;
140
+
141
+ const relationModeFromImpactDirection = (direction: ImpactDirection) => {
142
+ if (direction === 'both') {
143
+ return 'related';
144
+ }
145
+ return direction === 'upstream' ? 'deps' : 'impact';
146
+ };
147
+
148
+ const diffInputSchema = sourceInputSchema
149
+ .extend({
150
+ againstDir: z
151
+ .string()
152
+ .optional()
153
+ .describe('Baseline artifact directory containing trails.lock'),
154
+ againstRootDir: z
155
+ .string()
156
+ .optional()
157
+ .describe('Baseline workspace root directory'),
158
+ againstTrailsDbPath: z
159
+ .string()
160
+ .optional()
161
+ .describe('Baseline trails.db path'),
162
+ })
163
+ .strict()
164
+ .refine(
165
+ (input) =>
166
+ input.againstDir !== undefined || input.againstRootDir !== undefined,
167
+ {
168
+ message: 'Provide againstDir or againstRootDir for the baseline graph.',
169
+ path: ['againstDir'],
170
+ }
171
+ )
172
+ .refine(
173
+ (input) =>
174
+ input.againstDir === undefined || input.againstRootDir === undefined,
175
+ {
176
+ message: 'Provide only one of againstDir or againstRootDir.',
177
+ path: ['againstDir'],
178
+ }
179
+ );
180
+
181
+ const refOutputSchema = z.object({
182
+ id: z.string(),
183
+ kind: inspectKindSchema,
184
+ trailId: z.string().optional(),
185
+ versionKey: z.string().optional(),
186
+ });
187
+
188
+ const entrySummarySchema = z.object({
189
+ exampleCount: z.number(),
190
+ id: z.string(),
191
+ surfaces: z.array(z.string()).readonly(),
192
+ });
193
+
194
+ const cliRouteSchema = z.object({
195
+ kind: z.enum(['alias', 'canonical']),
196
+ path: z.array(z.string()).readonly(),
197
+ source: z.enum(['derived', 'surface', 'trail']),
198
+ target: z.string(),
199
+ });
200
+
201
+ const cliDerivedSchema = z
202
+ .object({
203
+ path: z.array(z.string()).readonly(),
204
+ routes: z.array(cliRouteSchema).readonly().optional(),
205
+ })
206
+ .nullable();
207
+
208
+ const trailSummarySchema = entrySummarySchema.extend({
209
+ cli: cliDerivedSchema,
210
+ composes: z.array(z.string()).readonly(),
211
+ intent: z.enum(['destroy', 'read', 'write']),
212
+ kind: z.literal('trail'),
213
+ resources: z.array(z.string()).readonly(),
214
+ signals: z.array(z.string()).readonly(),
215
+ version: z.number().nullable(),
216
+ });
217
+
218
+ const entitySummarySchema = entrySummarySchema.extend({
219
+ identity: z.string().optional(),
220
+ kind: z.literal('entity'),
221
+ references: z.array(z.unknown()).readonly(),
222
+ });
223
+
224
+ const resourceSummarySchema = entrySummarySchema.extend({
225
+ kind: z.literal('resource'),
226
+ usedBy: z.array(z.string()).readonly(),
227
+ });
228
+
229
+ const signalSummarySchema = entrySummarySchema.extend({
230
+ consumers: z.array(z.string()).readonly(),
231
+ kind: z.literal('signal'),
232
+ producers: z.array(z.string()).readonly(),
233
+ });
234
+
235
+ const surfaceSummarySchema = z.object({
236
+ id: z.string(),
237
+ trailheads: z.array(z.string()).readonly(),
238
+ trails: z.array(z.string()).readonly(),
239
+ });
240
+
241
+ const trailheadSummarySchema = z.object({
242
+ description: z.string(),
243
+ id: z.string(),
244
+ memberIds: z.array(z.string()).readonly(),
245
+ surfaces: z.array(z.string()).readonly(),
246
+ visibility: z.enum(['internal', 'public']).optional(),
247
+ });
248
+
249
+ const versionSummarySchema = z.object({
250
+ current: z.boolean(),
251
+ exampleCount: z.number(),
252
+ id: z.string(),
253
+ kind: z.enum(['current', 'fork', 'revision']),
254
+ marker: z.string().optional(),
255
+ resources: z.array(z.string()).readonly(),
256
+ status: z.unknown().optional(),
257
+ trailId: z.string(),
258
+ version: z.number(),
259
+ });
260
+
261
+ const exampleSummarySchema = z.object({
262
+ example: z.unknown(),
263
+ index: z.number(),
264
+ source: z.enum(['entry', 'version']),
265
+ targetId: z.string(),
266
+ });
267
+
268
+ const errorFactsCompletenessSchema = z.discriminatedUnion('status', [
269
+ z.object({
270
+ reason: z.literal('authored-facts-exhausted'),
271
+ status: z.literal('complete'),
272
+ }),
273
+ z.object({
274
+ reason: z.enum(['inferred-facts-supplied', 'observed-facts-supplied']),
275
+ status: z.literal('partial'),
276
+ }),
277
+ z.object({
278
+ reason: z.enum(['no-exhaustive-emitted-error-contract', 'not-evaluated']),
279
+ status: z.literal('unknown'),
280
+ }),
281
+ ]);
282
+
283
+ const errorSurfaceDerivedSchema = z.object({
284
+ category: z.enum(errorCategories),
285
+ code: z.number(),
286
+ name: z.string(),
287
+ retryable: z.boolean(),
288
+ surface: z.enum(surfaceNames),
289
+ });
290
+
291
+ const errorTaxonomyDerivedSchema = z.object({
292
+ category: z.enum(errorCategories).optional(),
293
+ dynamicCategory: z
294
+ .object({
295
+ inheritsCategoryFrom: z.literal('wrapped-error'),
296
+ })
297
+ .optional(),
298
+ known: z.boolean(),
299
+ name: z.string(),
300
+ retryable: z.boolean().optional(),
301
+ surfaces: z.array(errorSurfaceDerivedSchema).readonly(),
302
+ });
303
+
304
+ const errorFactProvenanceSchema = z.object({
305
+ detail: z.string().optional(),
306
+ detourIndex: z.number().optional(),
307
+ exampleName: z.string().optional(),
308
+ source: z.enum([
309
+ 'runtime-observation',
310
+ 'static-inference',
311
+ 'trail.detours',
312
+ 'trail.examples',
313
+ 'trail.versions.detours',
314
+ 'trail.versions.examples',
315
+ ]),
316
+ trailId: z.string(),
317
+ version: z.number().optional(),
318
+ });
319
+
320
+ const errorFactSchema = z.object({
321
+ completeness: errorFactsCompletenessSchema,
322
+ kind: z.enum(['documented', 'handled', 'inferred', 'observed']),
323
+ provenance: errorFactProvenanceSchema,
324
+ taxonomy: errorTaxonomyDerivedSchema,
325
+ });
326
+
327
+ const trailErrorFactsSchema = z.object({
328
+ completeness: z.object({
329
+ documented: errorFactsCompletenessSchema,
330
+ emitted: errorFactsCompletenessSchema,
331
+ handled: errorFactsCompletenessSchema,
332
+ inferred: errorFactsCompletenessSchema,
333
+ observed: errorFactsCompletenessSchema,
334
+ }),
335
+ facts: z.array(errorFactSchema).readonly(),
336
+ trailId: z.string(),
337
+ });
338
+
339
+ const describeOutputSchema = envelopeSchema.extend({
340
+ entity: z.record(z.string(), z.unknown()),
341
+ });
342
+
343
+ const contractOutputSchema = envelopeSchema.extend({
344
+ contract: z.record(z.string(), z.unknown()),
345
+ });
346
+
347
+ const nearbyOutputSchema = envelopeSchema.extend({
348
+ edges: z.array(relationEdgeSchema).readonly(),
349
+ relations: z.array(relationGroupSchema).readonly(),
350
+ target: refOutputSchema,
351
+ });
352
+
353
+ const impactOutputSchema = envelopeSchema.extend({
354
+ direction: z.enum(['downstream', 'upstream', 'both']),
355
+ edges: z.array(relationEdgeSchema).readonly(),
356
+ maxDepth: z.number(),
357
+ nodes: z.array(impactNodeSchema).readonly(),
358
+ target: refOutputSchema,
359
+ });
360
+
361
+ const diffEntryOutputSchema = z.object({
362
+ change: z.enum(['added', 'removed', 'modified']),
363
+ details: z.array(z.string()).readonly(),
364
+ id: z.string(),
365
+ kind: z.enum(['entity', 'trailhead', 'resource', 'signal', 'trail']),
366
+ severity: z.enum(['info', 'warning', 'breaking']),
367
+ });
368
+
369
+ const diffResultOutputSchema = z.object({
370
+ breaking: z.array(diffEntryOutputSchema).readonly(),
371
+ entries: z.array(diffEntryOutputSchema).readonly(),
372
+ hasBreaking: z.boolean(),
373
+ info: z.array(diffEntryOutputSchema).readonly(),
374
+ warnings: z.array(diffEntryOutputSchema).readonly(),
375
+ });
376
+
377
+ const diffOutputSchema = envelopeSchema.extend({
378
+ against: envelopeSchema,
379
+ diff: diffResultOutputSchema,
380
+ });
381
+
382
+ const errorsOutputSchema = envelopeSchema.extend({
383
+ errors: z.array(trailErrorFactsSchema).readonly(),
384
+ });
385
+
386
+ const adapterPlacementSchema = z.enum(adapterTargetPlacements);
387
+
388
+ const adapterFactProvenanceSchema = z.object({
389
+ packageJsonPath: z.string().optional(),
390
+ paths: z.array(z.string()).readonly().optional(),
391
+ source: z.enum([
392
+ 'adapter-package-manifest',
393
+ 'conformance-test',
394
+ 'owner-package-manifest',
395
+ 'runtime-observation',
396
+ ]),
397
+ });
398
+
399
+ const adapterFactSchema = z.object({
400
+ adapterType: z.string().optional(),
401
+ key: z.string(),
402
+ kind: adapterFactKindSchema,
403
+ ownerPackage: z.string().optional(),
404
+ packageName: z.string().optional(),
405
+ placement: adapterPlacementSchema.optional(),
406
+ placements: z.array(adapterPlacementSchema).readonly().optional(),
407
+ provenance: adapterFactProvenanceSchema,
408
+ target: z.string(),
409
+ targetKey: z.string().optional(),
410
+ });
411
+
412
+ const adapterFactCountsSchema = z.object({
413
+ available: z.number(),
414
+ configured: z.number(),
415
+ diagnostics: z.number(),
416
+ observed: z.number(),
417
+ used: z.number(),
418
+ });
419
+
420
+ const adapterDiagnosticSchema = z.object({
421
+ code: z.string(),
422
+ message: z.string(),
423
+ packageJsonPath: z.string(),
424
+ packageName: z.string().optional(),
425
+ placement: adapterPlacementSchema.optional(),
426
+ severity: z.enum(['error', 'warn']),
427
+ target: z.string().optional(),
428
+ });
429
+
430
+ const adaptersOutputSchema = z.object({
431
+ adapters: z.array(adapterFactSchema).readonly(),
432
+ counts: adapterFactCountsSchema,
433
+ diagnostics: z.array(adapterDiagnosticSchema).readonly(),
434
+ rootDir: z.string(),
435
+ });
436
+
437
+ type SourceInput = z.output<typeof sourceInputSchema>;
438
+ type AdapterFactsInput = z.output<typeof adapterFactsInputSchema>;
439
+ type InspectInput = z.output<typeof inspectInputSchema>;
440
+ type ContractInput = z.output<typeof contractInputSchema>;
441
+ type DiffInput = z.output<typeof diffInputSchema>;
442
+
443
+ interface LoadedWayfinderGraph {
444
+ readonly graph: TopoGraph;
445
+ readonly load: WayfinderArtifactLoad;
446
+ readonly source: {
447
+ readonly kind: 'topoGraph';
448
+ readonly path: string;
449
+ readonly schemaVersion: number;
450
+ };
451
+ }
452
+
453
+ const toLoaderOptions = (
454
+ input: SourceInput,
455
+ cwd: string | undefined
456
+ ): WayfinderArtifactLoaderOptions => {
457
+ const rootDir = input.rootDir ?? (input.dir === undefined ? cwd : undefined);
458
+ return {
459
+ ...(input.dir === undefined ? {} : { dir: input.dir }),
460
+ ...(rootDir === undefined ? {} : { rootDir }),
461
+ ...(input.trailsDbPath === undefined ? {} : { path: input.trailsDbPath }),
462
+ };
463
+ };
464
+
465
+ const topoGraphSourcePath = (
466
+ input: SourceInput,
467
+ cwd: string | undefined
468
+ ): string | undefined => {
469
+ if (input.dir !== undefined) {
470
+ return join(input.dir, 'trails.lock');
471
+ }
472
+ const rootDir = input.rootDir ?? cwd;
473
+ return rootDir === undefined ? undefined : join(rootDir, 'trails.lock');
474
+ };
475
+
476
+ const loadGraph = async (
477
+ input: SourceInput,
478
+ cwd: string | undefined
479
+ ): Promise<Result<LoadedWayfinderGraph, TrailsError>> => {
480
+ let load: WayfinderArtifactLoad;
481
+ try {
482
+ load = await loadWayfinderArtifacts(toLoaderOptions(input, cwd));
483
+ } catch (error) {
484
+ const cause = error instanceof Error ? error : new Error(String(error));
485
+ return Result.err(
486
+ new DerivationError('Unable to load Wayfinder artifacts.', {
487
+ cause,
488
+ context: {
489
+ artifact: 'topoGraph',
490
+ path: topoGraphSourcePath(input, cwd) ?? 'trails.lock',
491
+ },
492
+ })
493
+ );
494
+ }
495
+ if (
496
+ load.artifactStatus.status === 'schema-version-drift' &&
497
+ load.artifactStatus.artifact === 'topoGraph'
498
+ ) {
499
+ return Result.err(
500
+ new DerivationError(load.artifactStatus.message, {
501
+ context: {
502
+ artifact: load.artifactStatus.artifact,
503
+ artifactStatus: load.artifactStatus.status,
504
+ },
505
+ })
506
+ );
507
+ }
508
+ if (load.topoGraph === null) {
509
+ return Result.err(
510
+ new NotFoundError(
511
+ 'No wayfinder TopoGraph artifact found. Run `trails compile` first.'
512
+ )
513
+ );
514
+ }
515
+ return Result.ok({
516
+ graph: load.topoGraph,
517
+ load,
518
+ source: {
519
+ kind: 'topoGraph',
520
+ path: topoGraphSourcePath(input, cwd) ?? 'trails.lock',
521
+ schemaVersion: load.topoGraph.topoGraphSchemaVersion,
522
+ },
523
+ });
524
+ };
525
+
526
+ const adapterFactsRootDir = (
527
+ input: AdapterFactsInput,
528
+ cwd: string | undefined
529
+ ): Result<string, ValidationError> => {
530
+ const rootDir = input.rootDir ?? cwd;
531
+ return rootDir === undefined
532
+ ? Result.err(
533
+ new ValidationError(
534
+ 'Provide rootDir or run wayfind.adapters from a workspace directory.'
535
+ )
536
+ )
537
+ : Result.ok(resolve(rootDir));
538
+ };
539
+
540
+ const filteredAdapterFacts = (
541
+ input: AdapterFactsInput,
542
+ cwd: string | undefined
543
+ ): Result<z.output<typeof adaptersOutputSchema>, ValidationError> => {
544
+ const rootDir = adapterFactsRootDir(input, cwd);
545
+ if (rootDir.isErr()) {
546
+ return rootDir;
547
+ }
548
+
549
+ const report = checkAdapters(rootDir.value);
550
+ const facts = report.facts
551
+ .filter(
552
+ (fact: AdapterFact) =>
553
+ (input.filters?.kind === undefined ||
554
+ fact.kind === input.filters.kind) &&
555
+ (input.filters?.target === undefined ||
556
+ fact.target === input.filters.target) &&
557
+ (input.filters?.packageName === undefined ||
558
+ fact.packageName === input.filters.packageName)
559
+ )
560
+ .slice(0, input.limit);
561
+
562
+ return Result.ok({
563
+ adapters: facts,
564
+ counts: {
565
+ available: report.facts.filter(
566
+ (fact: AdapterFact) => fact.kind === 'available'
567
+ ).length,
568
+ configured: report.facts.filter(
569
+ (fact: AdapterFact) => fact.kind === 'configured'
570
+ ).length,
571
+ diagnostics: report.diagnostics.length,
572
+ observed: report.facts.filter(
573
+ (fact: AdapterFact) => fact.kind === 'observed'
574
+ ).length,
575
+ used: report.facts.filter((fact: AdapterFact) => fact.kind === 'used')
576
+ .length,
577
+ },
578
+ diagnostics: [...report.diagnostics],
579
+ rootDir: rootDir.value,
580
+ });
581
+ };
582
+
583
+ const envelope = (
584
+ loaded: LoadedWayfinderGraph
585
+ ): z.output<typeof envelopeSchema> => ({
586
+ drift: wayfinderDriftFromArtifactStatus(loaded.load.artifactStatus),
587
+ source: loaded.source,
588
+ });
589
+
590
+ const entrySignals = (entry: TopoGraphEntry): readonly string[] =>
591
+ [
592
+ ...(entry.fires ?? []),
593
+ ...(entry.on ?? []),
594
+ ...(entry.from ?? []),
595
+ ...(entry.producers ?? []),
596
+ ...(entry.consumers ?? []),
597
+ ].toSorted();
598
+
599
+ const entryExamples = (entry: TopoGraphEntry): readonly unknown[] =>
600
+ entry.examples ?? [];
601
+
602
+ const entryById = (
603
+ graph: TopoGraph,
604
+ id: string,
605
+ kind?: TopoGraphEntry['kind']
606
+ ): TopoGraphEntry | undefined =>
607
+ graph.entries.find(
608
+ (entry) => entry.id === id && (kind === undefined || entry.kind === kind)
609
+ );
610
+
611
+ const usedByResource = (
612
+ graph: TopoGraph,
613
+ resourceId: string
614
+ ): readonly string[] =>
615
+ graph.entries
616
+ .filter(
617
+ (entry) =>
618
+ entry.kind === 'trail' && (entry.resources ?? []).includes(resourceId)
619
+ )
620
+ .map((entry) => entry.id)
621
+ .toSorted();
622
+
623
+ const trailSummaries = (graph: TopoGraph) =>
624
+ graph.entries
625
+ .filter((entry) => entry.kind === 'trail')
626
+ .map((entry) => ({
627
+ cli: entry.cli ?? null,
628
+ composes: entry.composes ?? [],
629
+ exampleCount: entry.exampleCount,
630
+ id: entry.id,
631
+ intent: entry.intent ?? 'write',
632
+ kind: 'trail' as const,
633
+ resources: entry.resources ?? [],
634
+ signals: entrySignals(entry),
635
+ surfaces: entry.surfaces,
636
+ version: entry.version ?? null,
637
+ }));
638
+
639
+ const entitySummaries = (graph: TopoGraph) =>
640
+ graph.entries
641
+ .filter((entry) => entry.kind === 'entity')
642
+ .map((entry) => ({
643
+ exampleCount: entry.exampleCount,
644
+ id: entry.id,
645
+ identity: entry.identity,
646
+ kind: 'entity' as const,
647
+ references: entry.references ?? [],
648
+ surfaces: entry.surfaces,
649
+ }));
650
+
651
+ const resourceSummaries = (graph: TopoGraph) =>
652
+ graph.entries
653
+ .filter((entry) => entry.kind === 'resource')
654
+ .map((entry) => ({
655
+ exampleCount: entry.exampleCount,
656
+ id: entry.id,
657
+ kind: 'resource' as const,
658
+ surfaces: entry.surfaces,
659
+ usedBy: usedByResource(graph, entry.id),
660
+ }));
661
+
662
+ const signalSummaries = (graph: TopoGraph) =>
663
+ graph.entries
664
+ .filter((entry) => entry.kind === 'signal')
665
+ .map((entry) => ({
666
+ consumers: entry.consumers ?? [],
667
+ exampleCount: entry.exampleCount,
668
+ id: entry.id,
669
+ kind: 'signal' as const,
670
+ producers: entry.producers ?? [],
671
+ surfaces: entry.surfaces,
672
+ }));
673
+
674
+ const surfaceSummaries = (graph: TopoGraph) => {
675
+ const surfaceIds = new Set<string>();
676
+ for (const entry of graph.entries) {
677
+ for (const surface of entry.surfaces) {
678
+ surfaceIds.add(surface);
679
+ }
680
+ }
681
+ for (const trailhead of graph.trailheads ?? []) {
682
+ for (const surface of trailhead.surfaces) {
683
+ surfaceIds.add(surface);
684
+ }
685
+ }
686
+ return [...surfaceIds].toSorted().map((surface) => ({
687
+ id: surface,
688
+ trailheads: (graph.trailheads ?? [])
689
+ .filter((trailhead) => trailhead.surfaces.includes(surface))
690
+ .map((trailhead) => trailhead.id)
691
+ .toSorted(),
692
+ trails: filterWayfinderEntityRefs(graph, {
693
+ kind: 'trail',
694
+ surface,
695
+ }).map((ref) => ref.id),
696
+ }));
697
+ };
698
+
699
+ const trailheadSummaries = (graph: TopoGraph) =>
700
+ (graph.trailheads ?? []).map((trailhead) => ({
701
+ description: trailhead.description,
702
+ id: trailhead.id,
703
+ memberIds: trailhead.memberIds,
704
+ surfaces: trailhead.surfaces,
705
+ visibility: trailhead.visibility,
706
+ }));
707
+
708
+ const versionSummaries = (graph: TopoGraph) =>
709
+ graph.entries
710
+ .filter((entry) => entry.kind === 'trail' && entry.version !== undefined)
711
+ .flatMap((entry) => {
712
+ const current = {
713
+ current: true,
714
+ exampleCount: entry.exampleCount,
715
+ id: `${entry.id}@${entry.version as number}`,
716
+ kind: 'current' as const,
717
+ marker: entry.marker,
718
+ resources: entry.resources ?? [],
719
+ trailId: entry.id,
720
+ version: entry.version as number,
721
+ };
722
+ const historical = Object.entries(entry.versions ?? {}).map(
723
+ ([versionKey, version]) => ({
724
+ current: false,
725
+ exampleCount: version.exampleCount,
726
+ id: `${entry.id}@${versionKey}`,
727
+ kind: version.kind,
728
+ marker: version.marker,
729
+ resources: version.resources ?? [],
730
+ status: version.status,
731
+ trailId: entry.id,
732
+ version: Number(versionKey),
733
+ })
734
+ );
735
+ return [current, ...historical].toSorted(
736
+ (a, b) => a.trailId.localeCompare(b.trailId) || a.version - b.version
737
+ );
738
+ });
739
+
740
+ const exampleSummaries = (graph: TopoGraph) =>
741
+ graph.entries.flatMap((entry) => {
742
+ const current = entryExamples(entry).map((example, index) => ({
743
+ example,
744
+ index,
745
+ source: 'entry' as const,
746
+ targetId: entry.id,
747
+ }));
748
+ const versions = Object.entries(entry.versions ?? {}).flatMap(
749
+ ([versionKey, version]) =>
750
+ (version.examples ?? []).map((example, index) => ({
751
+ example,
752
+ index,
753
+ source: 'version' as const,
754
+ targetId: `${entry.id}@${versionKey}`,
755
+ }))
756
+ );
757
+ return [...current, ...versions];
758
+ });
759
+
760
+ const kindFilter = (
761
+ kind: WayfinderEntityKind,
762
+ filters: WayfinderEntityFilterInput | undefined
763
+ ): WayfinderEntityFilterInput => ({ ...filters, kind });
764
+
765
+ const allowsTrailKind = (
766
+ filters: WayfinderEntityFilterInput | undefined
767
+ ): boolean => {
768
+ const kind = filters?.kind;
769
+ return (
770
+ kind === undefined ||
771
+ kind === 'trail' ||
772
+ (Array.isArray(kind) && kind.includes('trail'))
773
+ );
774
+ };
775
+
776
+ const allowsVersionKind = (
777
+ filters: WayfinderEntityFilterInput | undefined
778
+ ): boolean => {
779
+ const kind = filters?.kind;
780
+ return (
781
+ kind === undefined ||
782
+ kind === 'version' ||
783
+ (Array.isArray(kind) && kind.includes('version'))
784
+ );
785
+ };
786
+
787
+ const allowsExampleWidening = (
788
+ filters: WayfinderEntityFilterInput | undefined
789
+ ): boolean => filters?.exampleCoverage !== false;
790
+
791
+ const filteredExampleSummaries = (
792
+ graph: TopoGraph,
793
+ filters: WayfinderEntityFilterInput | undefined,
794
+ limit: number
795
+ ) => {
796
+ if (filters === undefined || Object.keys(filters).length === 0) {
797
+ return exampleSummaries(graph).slice(0, limit);
798
+ }
799
+ const ids = new Set(
800
+ filterWayfinderEntityRefs(graph, filters).map((ref) => ref.id)
801
+ );
802
+ const trailIds = new Set(
803
+ allowsTrailKind(filters)
804
+ ? filterWayfinderEntityRefs(graph, kindFilter('trail', filters)).map(
805
+ (ref) => ref.id
806
+ )
807
+ : []
808
+ );
809
+ const versionIds = new Set(
810
+ allowsVersionKind(filters)
811
+ ? filterWayfinderEntityRefs(graph, kindFilter('version', filters)).map(
812
+ (ref) => ref.id
813
+ )
814
+ : []
815
+ );
816
+ const currentVersionTrailIds = new Set(
817
+ graph.entries
818
+ .filter(
819
+ (entry) =>
820
+ entry.kind === 'trail' &&
821
+ entry.version !== undefined &&
822
+ versionIds.has(`${entry.id}@${entry.version}`)
823
+ )
824
+ .map((entry) => entry.id)
825
+ );
826
+ const historicalVersionTrailIds = new Map(
827
+ graph.entries.flatMap((entry) =>
828
+ entry.kind === 'trail'
829
+ ? Object.keys(entry.versions ?? {}).map((versionKey) => [
830
+ `${entry.id}@${versionKey}`,
831
+ entry.id,
832
+ ])
833
+ : []
834
+ )
835
+ );
836
+ return exampleSummaries(graph)
837
+ .filter(
838
+ (example) =>
839
+ ids.has(example.targetId) ||
840
+ (example.source === 'entry' &&
841
+ currentVersionTrailIds.has(example.targetId)) ||
842
+ (example.source === 'version' &&
843
+ allowsExampleWidening(filters) &&
844
+ trailIds.has(historicalVersionTrailIds.get(example.targetId) ?? ''))
845
+ )
846
+ .slice(0, limit);
847
+ };
848
+
849
+ const filteredVersionSummaries = (
850
+ graph: TopoGraph,
851
+ filters: WayfinderEntityFilterInput | undefined,
852
+ limit: number
853
+ ) => {
854
+ const summaries = versionSummaries(graph);
855
+ if (filters === undefined || Object.keys(filters).length === 0) {
856
+ return summaries.slice(0, limit);
857
+ }
858
+ const ids = new Set(
859
+ filterWayfinderEntityRefs(graph, kindFilter('version', filters)).map(
860
+ (ref) => ref.id
861
+ )
862
+ );
863
+ const trailIds = new Set(
864
+ filterWayfinderEntityRefs(graph, kindFilter('trail', filters)).map(
865
+ (ref) => ref.id
866
+ )
867
+ );
868
+ return summaries
869
+ .filter(
870
+ (summary) =>
871
+ ids.has(summary.id) ||
872
+ (summary.current && trailIds.has(summary.trailId))
873
+ )
874
+ .slice(0, limit);
875
+ };
876
+
877
+ const againstInput = (input: DiffInput): SourceInput => ({
878
+ ...(input.againstDir === undefined ? {} : { dir: input.againstDir }),
879
+ ...(input.againstRootDir === undefined
880
+ ? {}
881
+ : { rootDir: input.againstRootDir }),
882
+ ...(input.againstTrailsDbPath === undefined
883
+ ? {}
884
+ : { trailsDbPath: input.againstTrailsDbPath }),
885
+ });
886
+
887
+ const diffBaselineError = (input: DiffInput): ValidationError | undefined => {
888
+ if (input.againstDir === undefined && input.againstRootDir === undefined) {
889
+ return new ValidationError(
890
+ 'Provide againstDir or againstRootDir for the baseline graph.'
891
+ );
892
+ }
893
+ if (input.againstDir !== undefined && input.againstRootDir !== undefined) {
894
+ return new ValidationError(
895
+ 'Provide only one of againstDir or againstRootDir.'
896
+ );
897
+ }
898
+ return undefined;
899
+ };
900
+
901
+ const describeSurface = (graph: TopoGraph, id: string) => {
902
+ const surface = surfaceSummaries(graph).find(
903
+ (candidate) => candidate.id === id
904
+ );
905
+ return surface === undefined ? undefined : { ...surface, kind: 'surface' };
906
+ };
907
+
908
+ const describeTrailhead = (graph: TopoGraph, id: string) => {
909
+ const trailhead = trailheadSummaries(graph).find(
910
+ (candidate) => candidate.id === id
911
+ );
912
+ return trailhead === undefined
913
+ ? undefined
914
+ : { ...trailhead, kind: 'trailhead' };
915
+ };
916
+
917
+ const describeVersion = (graph: TopoGraph, id: string) => {
918
+ const version = versionSummaries(graph).find(
919
+ (candidate) => candidate.id === id
920
+ );
921
+ return version === undefined ? undefined : { ...version, kind: 'version' };
922
+ };
923
+
924
+ type ResolvedEntity = Readonly<Record<string, unknown>>;
925
+
926
+ interface ResolvedEntityCandidate {
927
+ readonly kind: WayfinderEntityKind;
928
+ readonly value: ResolvedEntity;
929
+ }
930
+
931
+ const ambiguousId = (
932
+ id: string,
933
+ candidates: readonly ResolvedEntityCandidate[]
934
+ ): AmbiguousError =>
935
+ new AmbiguousError(
936
+ `Wayfinder id "${id}" matched multiple entity kinds: ${candidates
937
+ .map((candidate) => candidate.kind)
938
+ .join(', ')}. Pass kind to disambiguate.`
939
+ );
940
+
941
+ const singleCandidate = (
942
+ id: string,
943
+ candidates: readonly ResolvedEntityCandidate[]
944
+ ): Result<ResolvedEntity | undefined, AmbiguousError> => {
945
+ if (candidates.length > 1) {
946
+ return Result.err(ambiguousId(id, candidates));
947
+ }
948
+ return Result.ok(candidates[0]?.value);
949
+ };
950
+
951
+ const describeCandidates = (
952
+ graph: TopoGraph,
953
+ id: string
954
+ ): readonly ResolvedEntityCandidate[] => {
955
+ const candidates: ResolvedEntityCandidate[] = [];
956
+ const entry = entryById(graph, id);
957
+ if (entry !== undefined) {
958
+ candidates.push({
959
+ kind: entry.kind,
960
+ value: entry as unknown as ResolvedEntity,
961
+ });
962
+ }
963
+ const trailhead = describeTrailhead(graph, id);
964
+ if (trailhead !== undefined) {
965
+ candidates.push({ kind: 'trailhead', value: trailhead });
966
+ }
967
+ const surface = describeSurface(graph, id);
968
+ if (surface !== undefined) {
969
+ candidates.push({ kind: 'surface', value: surface });
970
+ }
971
+ const version = describeVersion(graph, id);
972
+ if (version !== undefined) {
973
+ candidates.push({ kind: 'version', value: version });
974
+ }
975
+ return candidates;
976
+ };
977
+
978
+ const describeEntry = (
979
+ graph: TopoGraph,
980
+ input: InspectInput
981
+ ): Result<ResolvedEntity | undefined, AmbiguousError> => {
982
+ if (input.kind === 'surface') {
983
+ return Result.ok(describeSurface(graph, input.id));
984
+ }
985
+ if (input.kind === 'trailhead') {
986
+ return Result.ok(describeTrailhead(graph, input.id));
987
+ }
988
+ if (input.kind === 'version') {
989
+ return Result.ok(describeVersion(graph, input.id));
990
+ }
991
+ if (input.kind === undefined) {
992
+ return singleCandidate(input.id, describeCandidates(graph, input.id));
993
+ }
994
+ const entry = entryById(graph, input.id, input.kind);
995
+ return Result.ok(
996
+ entry === undefined ? undefined : (entry as unknown as ResolvedEntity)
997
+ );
998
+ };
999
+
1000
+ const contractVersionFor = (
1001
+ graph: TopoGraph,
1002
+ input: ContractInput
1003
+ ): Readonly<Record<string, unknown>> | undefined => {
1004
+ const versionId =
1005
+ input.version === undefined ? input.id : `${input.id}@${input.version}`;
1006
+ const described = describeVersion(graph, versionId);
1007
+ if (described === undefined) {
1008
+ return undefined;
1009
+ }
1010
+ const entry = entryById(graph, described.trailId, 'trail');
1011
+ const version = entry?.versions?.[String(described.version)];
1012
+ return {
1013
+ id: described.trailId,
1014
+ input: version?.input ?? entry?.input ?? null,
1015
+ kind: 'version',
1016
+ output: version?.output ?? entry?.output ?? null,
1017
+ resources: version?.resources ?? entry?.resources ?? [],
1018
+ version: described.version,
1019
+ };
1020
+ };
1021
+
1022
+ const contractEntryKind = (
1023
+ kind: ContractInput['kind']
1024
+ ): TopoGraphEntry['kind'] | undefined =>
1025
+ kind === undefined ||
1026
+ kind === 'trailhead' ||
1027
+ kind === 'surface' ||
1028
+ kind === 'version'
1029
+ ? undefined
1030
+ : kind;
1031
+
1032
+ const contractEntry = (
1033
+ entry: TopoGraphEntry
1034
+ ): Readonly<Record<string, unknown>> => ({
1035
+ cli: entry.cli ?? null,
1036
+ id: entry.id,
1037
+ input: entry.input ?? null,
1038
+ kind: entry.kind,
1039
+ output: entry.output ?? null,
1040
+ payload: entry.payload ?? null,
1041
+ resources: entry.resources ?? [],
1042
+ schema: entry.schema ?? null,
1043
+ version: entry.version ?? null,
1044
+ });
1045
+
1046
+ const contractSurfaceOrTrailhead = (
1047
+ graph: TopoGraph,
1048
+ input: ContractInput
1049
+ ): ResolvedEntity | undefined => {
1050
+ if (input.kind === 'trailhead') {
1051
+ return describeTrailhead(graph, input.id);
1052
+ }
1053
+ if (input.kind === 'surface') {
1054
+ return describeSurface(graph, input.id);
1055
+ }
1056
+ return undefined;
1057
+ };
1058
+
1059
+ const contractFor = (
1060
+ graph: TopoGraph,
1061
+ input: ContractInput
1062
+ ): Result<ResolvedEntity | undefined, AmbiguousError> => {
1063
+ if (input.kind === 'version' || input.version !== undefined) {
1064
+ return Result.ok(contractVersionFor(graph, input));
1065
+ }
1066
+ if (input.kind === 'surface' || input.kind === 'trailhead') {
1067
+ return Result.ok(contractSurfaceOrTrailhead(graph, input));
1068
+ }
1069
+ if (input.kind === undefined) {
1070
+ const candidates: ResolvedEntityCandidate[] = [];
1071
+ const entry = entryById(graph, input.id);
1072
+ if (entry !== undefined) {
1073
+ candidates.push({ kind: entry.kind, value: contractEntry(entry) });
1074
+ }
1075
+ const trailhead = describeTrailhead(graph, input.id);
1076
+ if (trailhead !== undefined) {
1077
+ candidates.push({ kind: 'trailhead', value: trailhead });
1078
+ }
1079
+ const surface = describeSurface(graph, input.id);
1080
+ if (surface !== undefined) {
1081
+ candidates.push({ kind: 'surface', value: surface });
1082
+ }
1083
+ const version = contractVersionFor(graph, { ...input, kind: 'version' });
1084
+ if (version !== undefined) {
1085
+ candidates.push({ kind: 'version', value: version });
1086
+ }
1087
+ return singleCandidate(input.id, candidates);
1088
+ }
1089
+
1090
+ const entry = entryById(graph, input.id, contractEntryKind(input.kind));
1091
+ if (entry === undefined) {
1092
+ return Result.ok(contractSurfaceOrTrailhead(graph, input));
1093
+ }
1094
+ return Result.ok(contractEntry(entry));
1095
+ };
1096
+
1097
+ const withGraph = async <TValue>(
1098
+ input: SourceInput,
1099
+ cwd: string | undefined,
1100
+ derive: (loaded: LoadedWayfinderGraph) => TValue
1101
+ ): Promise<Result<TValue, TrailsError>> => {
1102
+ const loaded = await loadGraph(input, cwd);
1103
+ if (loaded.isErr()) {
1104
+ return loaded;
1105
+ }
1106
+ return Result.ok(derive(loaded.value));
1107
+ };
1108
+
1109
+ const notFound = (kind: string, id: string): NotFoundError =>
1110
+ new NotFoundError(`No Wayfinder ${kind} found for "${id}".`);
1111
+
1112
+ const filteredIds = (
1113
+ graph: TopoGraph,
1114
+ kind: WayfinderEntityKind,
1115
+ filters: WayfinderEntityFilterInput | undefined,
1116
+ limit: number
1117
+ ): ReadonlySet<string> =>
1118
+ new Set(
1119
+ resolveWayfinderPopulation(graph, {
1120
+ filters,
1121
+ kind,
1122
+ limit,
1123
+ }).map((ref) => ref.id)
1124
+ );
1125
+
1126
+ const filteredErrorFacts = (
1127
+ graph: TopoGraph,
1128
+ filters: WayfinderEntityFilterInput | undefined,
1129
+ limit: number
1130
+ ) => {
1131
+ const ids = filteredIds(graph, 'trail', filters, limit);
1132
+ return deriveTrailErrorFacts(graph)
1133
+ .filter((entry) => ids.has(entry.trailId))
1134
+ .slice(0, limit);
1135
+ };
1136
+
1137
+ export const wayfindOverviewTrail = trail('wayfind.overview', {
1138
+ description: 'Summarize the saved Wayfinder topo graph',
1139
+ examples: [{ input: {}, name: 'Overview' }],
1140
+ implementation: async (input, ctx) =>
1141
+ withGraph(input, ctx.cwd, (loaded) => {
1142
+ const { graph } = loaded;
1143
+ return {
1144
+ ...envelope(loaded),
1145
+ counts: {
1146
+ entities: entitySummaries(graph).length,
1147
+ examples: exampleSummaries(graph).length,
1148
+ resources: resourceSummaries(graph).length,
1149
+ signals: signalSummaries(graph).length,
1150
+ surfaces: surfaceSummaries(graph).length,
1151
+ trailheads: trailheadSummaries(graph).length,
1152
+ trails: trailSummaries(graph).length,
1153
+ versions: versionSummaries(graph).length,
1154
+ },
1155
+ generatedAt: graph.generatedAt ?? null,
1156
+ workspace:
1157
+ graph.workspace === undefined
1158
+ ? null
1159
+ : {
1160
+ collisionCount: graph.workspace.collisions?.length ?? 0,
1161
+ trailCount: Object.keys(graph.workspace.trails).length,
1162
+ },
1163
+ };
1164
+ }),
1165
+ input: sourceInputSchema,
1166
+ intent: 'read',
1167
+ output: envelopeSchema.extend({
1168
+ counts: z.object({
1169
+ entities: z.number(),
1170
+ examples: z.number(),
1171
+ resources: z.number(),
1172
+ signals: z.number(),
1173
+ surfaces: z.number(),
1174
+ trailheads: z.number(),
1175
+ trails: z.number(),
1176
+ versions: z.number(),
1177
+ }),
1178
+ generatedAt: z.string().nullable(),
1179
+ workspace: z
1180
+ .object({
1181
+ collisionCount: z.number(),
1182
+ trailCount: z.number(),
1183
+ })
1184
+ .nullable(),
1185
+ }),
1186
+ visibility: 'internal',
1187
+ });
1188
+
1189
+ export const wayfindSearchTrail = trail('wayfind.search', {
1190
+ description: 'Find topo graph entities with typed filters',
1191
+ examples: [{ input: { filters: { kind: 'trail' } }, name: 'Find trails' }],
1192
+ implementation: async (input, ctx) =>
1193
+ withGraph(input, ctx.cwd, (loaded) => ({
1194
+ ...envelope(loaded),
1195
+ matches: resolveWayfinderPopulation(loaded.graph, {
1196
+ filters: input.filters,
1197
+ limit: input.limit,
1198
+ }).map((ref) => ({
1199
+ id: ref.id,
1200
+ kind: ref.kind,
1201
+ ...(ref.trailId === undefined ? {} : { trailId: ref.trailId }),
1202
+ ...(ref.versionKey === undefined ? {} : { versionKey: ref.versionKey }),
1203
+ })),
1204
+ })),
1205
+ input: filteredInputSchema,
1206
+ intent: 'read',
1207
+ output: envelopeSchema.extend({
1208
+ matches: z.array(refOutputSchema).readonly(),
1209
+ }),
1210
+ visibility: 'internal',
1211
+ });
1212
+
1213
+ export const wayfindTrailsTrail = trail('wayfind.trails', {
1214
+ description: 'List saved trail contracts',
1215
+ examples: [{ input: {}, name: 'List trails' }],
1216
+ implementation: async (input, ctx) =>
1217
+ withGraph(input, ctx.cwd, (loaded) => {
1218
+ const ids = filteredIds(
1219
+ loaded.graph,
1220
+ 'trail',
1221
+ input.filters,
1222
+ input.limit
1223
+ );
1224
+ return {
1225
+ ...envelope(loaded),
1226
+ trails: trailSummaries(loaded.graph).filter((entry) =>
1227
+ ids.has(entry.id)
1228
+ ),
1229
+ };
1230
+ }),
1231
+ input: filteredInputSchema,
1232
+ intent: 'read',
1233
+ output: envelopeSchema.extend({
1234
+ trails: z.array(trailSummarySchema).readonly(),
1235
+ }),
1236
+ visibility: 'internal',
1237
+ });
1238
+
1239
+ export const wayfindEntitiesTrail = trail('wayfind.entities', {
1240
+ description: 'List saved entity contracts',
1241
+ examples: [{ input: {}, name: 'List entities' }],
1242
+ implementation: async (input, ctx) =>
1243
+ withGraph(input, ctx.cwd, (loaded) => {
1244
+ const ids = filteredIds(
1245
+ loaded.graph,
1246
+ 'entity',
1247
+ input.filters,
1248
+ input.limit
1249
+ );
1250
+ return {
1251
+ ...envelope(loaded),
1252
+ entities: entitySummaries(loaded.graph).filter((entry) =>
1253
+ ids.has(entry.id)
1254
+ ),
1255
+ };
1256
+ }),
1257
+ input: filteredInputSchema,
1258
+ intent: 'read',
1259
+ output: envelopeSchema.extend({
1260
+ entities: z.array(entitySummarySchema).readonly(),
1261
+ }),
1262
+ visibility: 'internal',
1263
+ });
1264
+
1265
+ export const wayfindResourcesTrail = trail('wayfind.resources', {
1266
+ description: 'List saved resource contracts and usage',
1267
+ examples: [{ input: {}, name: 'List resources' }],
1268
+ implementation: async (input, ctx) =>
1269
+ withGraph(input, ctx.cwd, (loaded) => {
1270
+ const ids = filteredIds(
1271
+ loaded.graph,
1272
+ 'resource',
1273
+ input.filters,
1274
+ input.limit
1275
+ );
1276
+ return {
1277
+ ...envelope(loaded),
1278
+ resources: resourceSummaries(loaded.graph).filter((entry) =>
1279
+ ids.has(entry.id)
1280
+ ),
1281
+ };
1282
+ }),
1283
+ input: filteredInputSchema,
1284
+ intent: 'read',
1285
+ output: envelopeSchema.extend({
1286
+ resources: z.array(resourceSummarySchema).readonly(),
1287
+ }),
1288
+ visibility: 'internal',
1289
+ });
1290
+
1291
+ export const wayfindSignalsTrail = trail('wayfind.signals', {
1292
+ description: 'List saved signal contracts and graph usage',
1293
+ examples: [{ input: {}, name: 'List signals' }],
1294
+ implementation: async (input, ctx) =>
1295
+ withGraph(input, ctx.cwd, (loaded) => {
1296
+ const ids = filteredIds(
1297
+ loaded.graph,
1298
+ 'signal',
1299
+ input.filters,
1300
+ input.limit
1301
+ );
1302
+ return {
1303
+ ...envelope(loaded),
1304
+ signals: signalSummaries(loaded.graph).filter((entry) =>
1305
+ ids.has(entry.id)
1306
+ ),
1307
+ };
1308
+ }),
1309
+ input: filteredInputSchema,
1310
+ intent: 'read',
1311
+ output: envelopeSchema.extend({
1312
+ signals: z.array(signalSummarySchema).readonly(),
1313
+ }),
1314
+ visibility: 'internal',
1315
+ });
1316
+
1317
+ export const wayfindSurfacesTrail = trail('wayfind.surfaces', {
1318
+ description: 'List saved direct and trailhead-rendered surfaces',
1319
+ examples: [{ input: {}, name: 'List surfaces' }],
1320
+ implementation: async (input, ctx) =>
1321
+ withGraph(input, ctx.cwd, (loaded) => {
1322
+ const ids = filteredIds(
1323
+ loaded.graph,
1324
+ 'surface',
1325
+ input.filters,
1326
+ input.limit
1327
+ );
1328
+ return {
1329
+ ...envelope(loaded),
1330
+ surfaces: surfaceSummaries(loaded.graph).filter((entry) =>
1331
+ ids.has(entry.id)
1332
+ ),
1333
+ };
1334
+ }),
1335
+ input: filteredInputSchema,
1336
+ intent: 'read',
1337
+ output: envelopeSchema.extend({
1338
+ surfaces: z.array(surfaceSummarySchema).readonly(),
1339
+ }),
1340
+ visibility: 'internal',
1341
+ });
1342
+
1343
+ export const wayfindTrailheadsTrail = trail('wayfind.trailheads', {
1344
+ description: 'List saved trailhead membership',
1345
+ examples: [{ input: {}, name: 'List trailheads' }],
1346
+ implementation: async (input, ctx) =>
1347
+ withGraph(input, ctx.cwd, (loaded) => {
1348
+ const ids = filteredIds(
1349
+ loaded.graph,
1350
+ 'trailhead',
1351
+ input.filters,
1352
+ input.limit
1353
+ );
1354
+ return {
1355
+ ...envelope(loaded),
1356
+ trailheads: trailheadSummaries(loaded.graph).filter((entry) =>
1357
+ ids.has(entry.id)
1358
+ ),
1359
+ };
1360
+ }),
1361
+ input: filteredInputSchema,
1362
+ intent: 'read',
1363
+ output: envelopeSchema.extend({
1364
+ trailheads: z.array(trailheadSummarySchema).readonly(),
1365
+ }),
1366
+ visibility: 'internal',
1367
+ });
1368
+
1369
+ export const wayfindVersionsTrail = trail('wayfind.versions', {
1370
+ description: 'List saved trail version contracts',
1371
+ examples: [{ input: {}, name: 'List versions' }],
1372
+ implementation: async (input, ctx) =>
1373
+ withGraph(input, ctx.cwd, (loaded) => ({
1374
+ ...envelope(loaded),
1375
+ versions: filteredVersionSummaries(
1376
+ loaded.graph,
1377
+ input.filters,
1378
+ input.limit
1379
+ ),
1380
+ })),
1381
+ input: filteredInputSchema,
1382
+ intent: 'read',
1383
+ output: envelopeSchema.extend({
1384
+ versions: z.array(versionSummarySchema).readonly(),
1385
+ }),
1386
+ visibility: 'internal',
1387
+ });
1388
+
1389
+ export const wayfindExamplesTrail = trail('wayfind.examples', {
1390
+ description: 'List saved examples without executing trails',
1391
+ examples: [{ input: {}, name: 'List examples' }],
1392
+ implementation: async (input, ctx) =>
1393
+ withGraph(input, ctx.cwd, (loaded) => ({
1394
+ ...envelope(loaded),
1395
+ examples: filteredExampleSummaries(
1396
+ loaded.graph,
1397
+ input.filters,
1398
+ input.limit
1399
+ ),
1400
+ })),
1401
+ input: filteredInputSchema,
1402
+ intent: 'read',
1403
+ output: envelopeSchema.extend({
1404
+ examples: z.array(exampleSummarySchema).readonly(),
1405
+ }),
1406
+ visibility: 'internal',
1407
+ });
1408
+
1409
+ export const wayfindErrorsTrail = trail('wayfind.errors', {
1410
+ description: 'List saved trail error facts with provenance',
1411
+ examples: [{ input: {}, name: 'List trail error facts' }],
1412
+ implementation: async (input, ctx) =>
1413
+ withGraph(input, ctx.cwd, (loaded) => ({
1414
+ ...envelope(loaded),
1415
+ errors: filteredErrorFacts(loaded.graph, input.filters, input.limit),
1416
+ })),
1417
+ input: filteredInputSchema,
1418
+ intent: 'read',
1419
+ output: errorsOutputSchema,
1420
+ visibility: 'internal',
1421
+ });
1422
+
1423
+ export const wayfindAdaptersTrail = trail('wayfind.adapters', {
1424
+ description: 'List adapter facts with package and conformance provenance',
1425
+ examples: [{ input: {}, name: 'List adapter facts' }],
1426
+ implementation: (input, ctx) => filteredAdapterFacts(input, ctx.cwd),
1427
+ input: adapterFactsInputSchema,
1428
+ intent: 'read',
1429
+ output: adaptersOutputSchema,
1430
+ visibility: 'internal',
1431
+ });
1432
+
1433
+ export const wayfindOverlayTrail = trail('wayfind.overlay', {
1434
+ description: 'Read a namespaced fact overlay from the saved graph',
1435
+ examples: [
1436
+ { input: { namespace: 'cloudflare' }, name: 'Read cloudflare lock facts' },
1437
+ ],
1438
+ implementation: async (input, ctx) => {
1439
+ const loaded = await loadGraph(input, ctx.cwd);
1440
+ if (loaded.isErr()) {
1441
+ return loaded;
1442
+ }
1443
+ const overlays = loaded.value.graph.overlays ?? {};
1444
+ const namespaces = Object.keys(overlays).toSorted();
1445
+ if (!Object.hasOwn(overlays, input.namespace)) {
1446
+ return Result.err(
1447
+ new NotFoundError(
1448
+ `No lock overlay named "${input.namespace}". Available overlays: ${namespaces.length === 0 ? 'none' : namespaces.join(', ')}. Adapters contribute overlays via trailsOverlays; run \`trails compile\` to refresh the lock.`
1449
+ )
1450
+ );
1451
+ }
1452
+ return Result.ok({
1453
+ ...envelope(loaded.value),
1454
+ facts: overlays[input.namespace],
1455
+ namespace: input.namespace,
1456
+ namespaces,
1457
+ });
1458
+ },
1459
+ input: overlayInputSchema,
1460
+ intent: 'read',
1461
+ output: envelopeSchema.extend({
1462
+ facts: z.unknown(),
1463
+ namespace: z.string(),
1464
+ namespaces: z.array(z.string()).readonly(),
1465
+ }),
1466
+ visibility: 'internal',
1467
+ });
1468
+
1469
+ export const wayfindDescribeTrail = trail('wayfind.describe', {
1470
+ args: ['id'],
1471
+ description: 'Inspect one saved topo graph entity',
1472
+ examples: [{ input: { id: 'user.create' }, name: 'Describe entity' }],
1473
+ implementation: async (input, ctx) => {
1474
+ const loaded = await loadGraph(input, ctx.cwd);
1475
+ if (loaded.isErr()) {
1476
+ return loaded;
1477
+ }
1478
+ const entity = describeEntry(loaded.value.graph, input);
1479
+ if (entity.isErr()) {
1480
+ return entity;
1481
+ }
1482
+ if (entity.value === undefined) {
1483
+ return Result.err(notFound(input.kind ?? 'entity', input.id));
1484
+ }
1485
+ return Result.ok({ ...envelope(loaded.value), entity: entity.value });
1486
+ },
1487
+ input: inspectInputSchema,
1488
+ intent: 'read',
1489
+ output: describeOutputSchema,
1490
+ visibility: 'internal',
1491
+ });
1492
+
1493
+ export const wayfindContractTrail = trail('wayfind.contract', {
1494
+ args: ['id'],
1495
+ description: 'Inspect one saved input/output contract',
1496
+ examples: [{ input: { id: 'user.create' }, name: 'Inspect contract' }],
1497
+ implementation: async (input, ctx) => {
1498
+ const loaded = await loadGraph(input, ctx.cwd);
1499
+ if (loaded.isErr()) {
1500
+ return loaded;
1501
+ }
1502
+ const contract = contractFor(loaded.value.graph, input);
1503
+ if (contract.isErr()) {
1504
+ return contract;
1505
+ }
1506
+ if (contract.value === undefined) {
1507
+ return Result.err(notFound(input.kind ?? 'contract', input.id));
1508
+ }
1509
+ return Result.ok({ ...envelope(loaded.value), contract: contract.value });
1510
+ },
1511
+ input: contractInputSchema,
1512
+ intent: 'read',
1513
+ output: contractOutputSchema,
1514
+ visibility: 'internal',
1515
+ });
1516
+
1517
+ export const wayfindNearbyTrail = trail('wayfind.nearby', {
1518
+ args: ['id'],
1519
+ description: 'Inspect direct graph relationships around one topo entity',
1520
+ examples: [{ input: { id: 'user.create' }, name: 'Nearby graph context' }],
1521
+ implementation: async (input, ctx) => {
1522
+ const loaded = await loadGraph(input, ctx.cwd);
1523
+ if (loaded.isErr()) {
1524
+ return loaded;
1525
+ }
1526
+ const resolved = resolveWayfinderRelations(loaded.value.graph, {
1527
+ filters: input.filters,
1528
+ id: input.id,
1529
+ kind: input.kind,
1530
+ limit: 100,
1531
+ maxDepth: 1,
1532
+ mode: 'related',
1533
+ view: 'groups',
1534
+ });
1535
+ if (resolved.isErr()) {
1536
+ return resolved;
1537
+ }
1538
+ if (resolved.value === undefined) {
1539
+ return Result.err(notFound(input.kind ?? 'entity', input.id));
1540
+ }
1541
+ return Result.ok({
1542
+ ...envelope(loaded.value),
1543
+ edges: resolved.value.edges,
1544
+ relations: resolved.value.groups,
1545
+ target: resolved.value.target,
1546
+ });
1547
+ },
1548
+ input: relationInputSchema,
1549
+ intent: 'read',
1550
+ output: nearbyOutputSchema,
1551
+ visibility: 'internal',
1552
+ });
1553
+
1554
+ export const wayfindImpactTrail = trail('wayfind.impact', {
1555
+ args: ['id'],
1556
+ description: 'Traverse multi-hop graph impact from one topo entity',
1557
+ examples: [
1558
+ {
1559
+ input: { direction: 'downstream', id: 'db.main', kind: 'resource' },
1560
+ name: 'Resource impact',
1561
+ },
1562
+ ],
1563
+ implementation: async (input, ctx) => {
1564
+ const impactInput = {
1565
+ ...input,
1566
+ direction: input.direction ?? 'downstream',
1567
+ limit: input.limit ?? 100,
1568
+ maxDepth: input.maxDepth ?? 2,
1569
+ };
1570
+ const loaded = await loadGraph(input, ctx.cwd);
1571
+ if (loaded.isErr()) {
1572
+ return loaded;
1573
+ }
1574
+ const resolved = resolveWayfinderRelations(loaded.value.graph, {
1575
+ filters: input.filters,
1576
+ id: input.id,
1577
+ kind: input.kind,
1578
+ limit: impactInput.limit,
1579
+ maxDepth: impactInput.maxDepth,
1580
+ mode: relationModeFromImpactDirection(impactInput.direction),
1581
+ });
1582
+ if (resolved.isErr()) {
1583
+ return resolved;
1584
+ }
1585
+ if (resolved.value === undefined) {
1586
+ return Result.err(notFound(input.kind ?? 'entity', input.id));
1587
+ }
1588
+ return Result.ok({
1589
+ ...envelope(loaded.value),
1590
+ direction: impactInput.direction,
1591
+ edges: resolved.value.edges,
1592
+ maxDepth: impactInput.maxDepth,
1593
+ nodes: resolved.value.nodes,
1594
+ target: resolved.value.target,
1595
+ });
1596
+ },
1597
+ input: impactInputSchema,
1598
+ intent: 'read',
1599
+ output: impactOutputSchema,
1600
+ visibility: 'internal',
1601
+ });
1602
+
1603
+ export const wayfindDiffTrail = trail('wayfind.diff', {
1604
+ description: 'Diff two saved Wayfinder topo graph artifacts',
1605
+ examples: [
1606
+ {
1607
+ input: { againstDir: '.trails-baseline' },
1608
+ name: 'Diff against saved artifacts',
1609
+ },
1610
+ ],
1611
+ implementation: async (input, ctx) => {
1612
+ const baselineError = diffBaselineError(input);
1613
+ if (baselineError !== undefined) {
1614
+ return Result.err(baselineError);
1615
+ }
1616
+ const current = await loadGraph(input, ctx.cwd);
1617
+ if (current.isErr()) {
1618
+ return current;
1619
+ }
1620
+ const baseline = await loadGraph(againstInput(input), ctx.cwd);
1621
+ if (baseline.isErr()) {
1622
+ return baseline;
1623
+ }
1624
+ const diff = deriveTopoGraphDiff(baseline.value.graph, current.value.graph);
1625
+ return Result.ok({
1626
+ ...envelope(current.value),
1627
+ against: envelope(baseline.value),
1628
+ diff: diffResult(diff),
1629
+ });
1630
+ },
1631
+ input: diffInputSchema,
1632
+ intent: 'read',
1633
+ output: diffOutputSchema,
1634
+ visibility: 'internal',
1635
+ });
1636
+
1637
+ export const wayfinderTopo = topo('wayfinder', {
1638
+ wayfindAdaptersTrail,
1639
+ wayfindContractTrail,
1640
+ wayfindDescribeTrail,
1641
+ wayfindDiffTrail,
1642
+ wayfindEntitiesTrail,
1643
+ wayfindErrorsTrail,
1644
+ wayfindExamplesTrail,
1645
+ wayfindImpactTrail,
1646
+ wayfindNearbyTrail,
1647
+ wayfindOverlayTrail,
1648
+ wayfindOverviewTrail,
1649
+ wayfindResourcesTrail,
1650
+ wayfindSearchTrail,
1651
+ wayfindSignalsTrail,
1652
+ wayfindSurfacesTrail,
1653
+ wayfindTrailheadsTrail,
1654
+ wayfindTrailsTrail,
1655
+ wayfindVersionsTrail,
1656
+ });