@ontrails/trails 1.0.0-beta.23 → 1.0.0-beta.29

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 (48) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +6 -5
  3. package/package.json +16 -14
  4. package/src/app.ts +42 -3
  5. package/src/cli.ts +17 -3
  6. package/src/local-state-io.ts +20 -0
  7. package/src/mcp-app.ts +10 -0
  8. package/src/mcp-options.ts +2 -3
  9. package/src/project-writes.ts +11 -11
  10. package/src/release/check.ts +41 -25
  11. package/src/release/index.ts +40 -0
  12. package/src/release/native-bun-registry.ts +282 -19
  13. package/src/release/pack-coherence.ts +457 -0
  14. package/src/release/policy.ts +1684 -0
  15. package/src/release/semver.ts +104 -0
  16. package/src/release/wayfinder-dogfood-smoke.ts +600 -66
  17. package/src/run-completions-install.ts +2 -2
  18. package/src/run-schema.ts +41 -0
  19. package/src/run-wayfind-outline.ts +166 -0
  20. package/src/trails/adapter-check.ts +1 -1
  21. package/src/trails/add-surface.ts +1 -1
  22. package/src/trails/add-verify.ts +3 -3
  23. package/src/trails/compile.ts +17 -25
  24. package/src/trails/create-scaffold.ts +14 -14
  25. package/src/trails/create.ts +5 -5
  26. package/src/trails/dev-support.ts +44 -24
  27. package/src/trails/doctor.ts +23 -2
  28. package/src/trails/draft-promote.ts +7 -7
  29. package/src/trails/guide.ts +15 -19
  30. package/src/trails/load-app.ts +58 -9
  31. package/src/trails/operator-context.ts +66 -0
  32. package/src/trails/regrade.ts +72 -0
  33. package/src/trails/release-check.ts +1 -0
  34. package/src/trails/run-example.ts +21 -37
  35. package/src/trails/run-examples.ts +16 -32
  36. package/src/trails/run.ts +81 -57
  37. package/src/trails/survey.ts +76 -62
  38. package/src/trails/topo-output-schemas.ts +11 -0
  39. package/src/trails/topo-pin.ts +6 -20
  40. package/src/trails/topo-read-support.ts +91 -41
  41. package/src/trails/topo-reports.ts +4 -2
  42. package/src/trails/topo-store-support.ts +172 -39
  43. package/src/trails/topo-support.ts +1 -2
  44. package/src/trails/topo.ts +5 -19
  45. package/src/trails/validate.ts +9 -20
  46. package/src/trails/version-lifecycle-support.ts +10 -20
  47. package/src/trails/warden.ts +18 -0
  48. package/src/trails/wayfind.ts +1001 -0
@@ -0,0 +1,1001 @@
1
+ import { Result, ValidationError, trail } from '@ontrails/core';
2
+ import type { Result as TrailResult, TrailContext } from '@ontrails/core';
3
+ import {
4
+ wayfinderIncludeSchema,
5
+ wayfinderResolverSchema,
6
+ wayfinderSourceModeSchema,
7
+ wayfinderViewSchema,
8
+ } from '@ontrails/wayfinder';
9
+ import type {
10
+ WayfinderInclude,
11
+ WayfinderResolver,
12
+ WayfinderView,
13
+ } from '@ontrails/wayfinder';
14
+ import { z } from 'zod';
15
+
16
+ const wayfindInputSchema = z
17
+ .object({
18
+ adapter: z
19
+ .string()
20
+ .optional()
21
+ .describe('Filter graph facts delivered through an adapter package'),
22
+ contours: z.boolean().default(false).describe('Resolve contour facts'),
23
+ contract: z
24
+ .boolean()
25
+ .default(false)
26
+ .describe('Render the contract view for the selected target'),
27
+ deps: z
28
+ .boolean()
29
+ .default(false)
30
+ .describe('Resolve upstream dependencies for the selected target'),
31
+ depth: z
32
+ .number()
33
+ .int()
34
+ .positive()
35
+ .max(10)
36
+ .default(2)
37
+ .describe('Maximum graph traversal depth for relational views'),
38
+ describe: z
39
+ .boolean()
40
+ .default(false)
41
+ .describe('Render the describe view for the selected target'),
42
+ errors: z.boolean().default(false).describe('Resolve trail error facts'),
43
+ facets: z.boolean().default(false).describe('Resolve surface facet facts'),
44
+ impact: z
45
+ .boolean()
46
+ .default(false)
47
+ .describe('Resolve downstream impact for the selected target'),
48
+ include: z
49
+ .array(wayfinderIncludeSchema)
50
+ .default([])
51
+ .describe('Attach bounded fact families to the selected result'),
52
+ intent: z
53
+ .enum(['destroy', 'read', 'write'])
54
+ .optional()
55
+ .describe('Filter trails by intent'),
56
+ limit: z.number().int().positive().max(500).default(100),
57
+ map: z
58
+ .boolean()
59
+ .default(false)
60
+ .describe('Render the map view for the selected target'),
61
+ module: z
62
+ .string()
63
+ .optional()
64
+ .describe('Workspace-relative app module for live source reads'),
65
+ outline: z
66
+ .boolean()
67
+ .default(false)
68
+ .describe('Render the outline view for a source file target'),
69
+ overview: z
70
+ .boolean()
71
+ .default(false)
72
+ .describe('Render the graph overview view'),
73
+ resources: z.boolean().default(false).describe('Resolve resource facts'),
74
+ rootDir: z.string().optional().describe('Workspace root directory'),
75
+ signals: z.boolean().default(false).describe('Resolve signal facts'),
76
+ source: wayfinderSourceModeSchema
77
+ .default('locked')
78
+ .describe('Graph source to read'),
79
+ surfaces: z.boolean().default(false).describe('Resolve surface facts'),
80
+ target: z
81
+ .string()
82
+ .optional()
83
+ .describe('Graph entity ID or source file path to inspect'),
84
+ trails: z.boolean().default(false).describe('Resolve trail facts'),
85
+ view: wayfinderViewSchema
86
+ .default('list')
87
+ .describe('View to render for the resolved target or population'),
88
+ })
89
+ .strict()
90
+ .refine((input) => !(input.deps && input.impact), {
91
+ message: 'Provide only one relation flag: --deps or --impact.',
92
+ path: ['deps'],
93
+ })
94
+ .refine(
95
+ (input) => !(input.deps || input.impact) || input.target !== undefined,
96
+ {
97
+ message: 'Relation flags require a Wayfinder target.',
98
+ path: ['target'],
99
+ }
100
+ )
101
+ .refine(
102
+ (input) =>
103
+ [
104
+ input.contract,
105
+ input.describe,
106
+ input.map,
107
+ input.outline,
108
+ input.overview,
109
+ ].filter(Boolean).length <= 1,
110
+ {
111
+ message: 'Provide only one view shortcut flag.',
112
+ path: ['view'],
113
+ }
114
+ )
115
+ .refine(
116
+ (input) =>
117
+ ![
118
+ input.contract,
119
+ input.describe,
120
+ input.map,
121
+ input.outline,
122
+ input.overview,
123
+ ].some(Boolean) || input.view === 'list',
124
+ {
125
+ message: 'Use either --view or one view shortcut flag, not both.',
126
+ path: ['view'],
127
+ }
128
+ )
129
+ .refine(
130
+ (input) =>
131
+ input.target === undefined ||
132
+ input.deps ||
133
+ input.impact ||
134
+ (input.adapter === undefined &&
135
+ !input.contours &&
136
+ !input.errors &&
137
+ !input.facets &&
138
+ input.intent === undefined &&
139
+ !input.resources &&
140
+ !input.signals &&
141
+ !input.surfaces &&
142
+ !input.trails),
143
+ {
144
+ message:
145
+ 'Target lookup cannot be combined with population selector flags. Use --include for bounded fact attachments.',
146
+ path: ['target'],
147
+ }
148
+ )
149
+ .refine(
150
+ (input) =>
151
+ input.include.length === 0 ||
152
+ (input.source !== 'live' && !input.deps && !input.impact),
153
+ {
154
+ message:
155
+ '--include attaches facts to a target or filtered population from locked artifacts; impact/deps and live-source includes are not supported yet.',
156
+ path: ['include'],
157
+ }
158
+ );
159
+
160
+ const wayfindComposeInputSchema = z
161
+ .object({
162
+ resolver: wayfinderResolverSchema.optional(),
163
+ })
164
+ .strict();
165
+
166
+ const wayfindOutputSchema = z.object({
167
+ includes: z.record(z.string(), z.unknown()).optional(),
168
+ result: z.unknown(),
169
+ source: wayfinderSourceModeSchema,
170
+ target: z.string().optional(),
171
+ view: wayfinderViewSchema,
172
+ });
173
+
174
+ type WayfindInput = z.output<typeof wayfindInputSchema> & {
175
+ readonly resolver?: WayfinderResolver | undefined;
176
+ };
177
+ type TrailContextWithCompose = TrailContext & {
178
+ readonly compose: NonNullable<TrailContext['compose']>;
179
+ };
180
+
181
+ const sourceInput = (
182
+ input: WayfindInput
183
+ ): { readonly rootDir?: string | undefined } =>
184
+ input.rootDir === undefined ? {} : { rootDir: input.rootDir };
185
+
186
+ const liveSourceError = (message: string): ValidationError =>
187
+ new ValidationError(message);
188
+
189
+ const liveModuleInput = (
190
+ input: WayfindInput
191
+ ): {
192
+ readonly module?: string | undefined;
193
+ readonly rootDir?: string | undefined;
194
+ } => ({
195
+ ...(input.module === undefined ? {} : { module: input.module }),
196
+ ...sourceInput(input),
197
+ });
198
+
199
+ const hasLiveTypedFilter = (input: WayfindInput): boolean =>
200
+ input.adapter !== undefined ||
201
+ input.contours ||
202
+ input.errors ||
203
+ input.facets ||
204
+ input.intent !== undefined ||
205
+ input.resources ||
206
+ input.signals ||
207
+ input.surfaces ||
208
+ input.trails;
209
+
210
+ const populationFilters = (
211
+ input: WayfindInput
212
+ ): {
213
+ readonly intent?: WayfindInput['intent'];
214
+ readonly kind?:
215
+ | readonly (
216
+ | 'contour'
217
+ | 'facet'
218
+ | 'resource'
219
+ | 'signal'
220
+ | 'surface'
221
+ | 'trail'
222
+ )[]
223
+ | 'contour'
224
+ | 'facet'
225
+ | 'resource'
226
+ | 'signal'
227
+ | 'surface'
228
+ | 'trail'
229
+ | undefined;
230
+ readonly query?: string | undefined;
231
+ readonly surface?: readonly string[] | string | undefined;
232
+ } => {
233
+ const kinds = [
234
+ ...(input.contours ? ['contour' as const] : []),
235
+ ...(input.facets ? ['facet' as const] : []),
236
+ ...(input.resources ? ['resource' as const] : []),
237
+ ...(input.signals ? ['signal' as const] : []),
238
+ ...(input.surfaces ? ['surface' as const] : []),
239
+ ...(input.trails ? ['trail' as const] : []),
240
+ ];
241
+ return {
242
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
243
+ ...(kinds.length === 0
244
+ ? {}
245
+ : { kind: kinds.length === 1 ? kinds[0] : kinds }),
246
+ ...(input.resolver === 'query' && input.target !== undefined
247
+ ? { query: input.target }
248
+ : {}),
249
+ };
250
+ };
251
+
252
+ const targetLooksLikeFile = (target: string): boolean =>
253
+ target.includes('/') || /\.[cm]?[jt]sx?$/.test(target);
254
+
255
+ const targetLooksLikePattern = (target: string): boolean =>
256
+ target.includes('*') || target.includes('?');
257
+
258
+ const targetFilter = (input: WayfindInput) => {
259
+ if (input.target === undefined) {
260
+ return populationFilters(input);
261
+ }
262
+ if (input.resolver === 'pattern') {
263
+ return { idGlob: input.target };
264
+ }
265
+ if (input.resolver === 'query') {
266
+ return { query: input.target };
267
+ }
268
+ return { id: input.target };
269
+ };
270
+
271
+ const includeResultKey = (include: WayfinderInclude): string => include;
272
+
273
+ const includeInput = (input: WayfindInput) => ({
274
+ filters: targetFilter(input),
275
+ limit: input.limit,
276
+ ...sourceInput(input),
277
+ });
278
+
279
+ const composeInclude = (
280
+ include: WayfinderInclude,
281
+ input: WayfindInput,
282
+ ctx: TrailContextWithCompose
283
+ ) => {
284
+ switch (include) {
285
+ case 'adapters': {
286
+ return ctx.compose('wayfind.adapters', {
287
+ ...(input.adapter === undefined
288
+ ? {}
289
+ : { filters: { packageName: input.adapter } }),
290
+ limit: input.limit,
291
+ ...sourceInput(input),
292
+ });
293
+ }
294
+ case 'errors': {
295
+ return ctx.compose('wayfind.errors', includeInput(input));
296
+ }
297
+ case 'examples': {
298
+ return ctx.compose('wayfind.examples', includeInput(input));
299
+ }
300
+ case 'surfaces': {
301
+ return ctx.compose('wayfind.surfaces', includeInput(input));
302
+ }
303
+ case 'versions': {
304
+ return ctx.compose('wayfind.versions', includeInput(input));
305
+ }
306
+ default: {
307
+ return Result.err(
308
+ new ValidationError(`Unsupported Wayfinder include: ${include}`)
309
+ );
310
+ }
311
+ }
312
+ };
313
+
314
+ const resolveIncludes = async (
315
+ input: WayfindInput,
316
+ ctx: TrailContextWithCompose
317
+ ) => {
318
+ if (input.include.length === 0) {
319
+ return Result.ok();
320
+ }
321
+ const includes: Record<string, unknown> = {};
322
+ for (const include of input.include) {
323
+ const result = await composeInclude(include, input, ctx);
324
+ if (result.isErr()) {
325
+ return Result.err(result.error);
326
+ }
327
+ includes[includeResultKey(include)] = result.value;
328
+ }
329
+ return Result.ok(includes);
330
+ };
331
+
332
+ const envelopeFor = async (
333
+ result: Awaited<ReturnType<TrailContextWithCompose['compose']>>,
334
+ input: WayfindInput,
335
+ ctx: TrailContextWithCompose,
336
+ view: WayfinderView
337
+ ): Promise<TrailResult<z.output<typeof wayfindOutputSchema>, Error>> => {
338
+ if (result.isErr()) {
339
+ return Result.err(result.error);
340
+ }
341
+ const includes = await resolveIncludes(input, ctx);
342
+ if (includes.isErr()) {
343
+ return Result.err(includes.error);
344
+ }
345
+ return Result.ok({
346
+ ...(includes.value === undefined ? {} : { includes: includes.value }),
347
+ result: result.value,
348
+ source: input.source,
349
+ ...(input.target === undefined ? {} : { target: input.target }),
350
+ view,
351
+ });
352
+ };
353
+
354
+ const viewFor = (input: WayfindInput): WayfinderView => {
355
+ if (input.contract) {
356
+ return 'contract';
357
+ }
358
+ if (input.describe) {
359
+ return 'describe';
360
+ }
361
+ if (input.map) {
362
+ return 'map';
363
+ }
364
+ if (input.outline) {
365
+ return 'outline';
366
+ }
367
+ if (input.overview) {
368
+ return 'overview';
369
+ }
370
+ return input.view;
371
+ };
372
+
373
+ const viewLiveSource = async (
374
+ input: WayfindInput,
375
+ ctx: TrailContextWithCompose
376
+ ) => {
377
+ const view = viewFor(input);
378
+ if (input.deps || input.impact) {
379
+ return {
380
+ result: Result.err(
381
+ liveSourceError(
382
+ '`trails wayfind --source live` supports overview and ID lookup; use locked artifacts for relational graph views.'
383
+ )
384
+ ),
385
+ view,
386
+ };
387
+ }
388
+ if (hasLiveTypedFilter(input)) {
389
+ return {
390
+ result: Result.err(
391
+ liveSourceError(
392
+ '`trails wayfind --source live` supports overview and ID lookup; use locked artifacts for typed filters.'
393
+ )
394
+ ),
395
+ view,
396
+ };
397
+ }
398
+ if (input.target !== undefined && targetLooksLikeFile(input.target)) {
399
+ return {
400
+ result: Result.err(
401
+ liveSourceError(
402
+ '`trails wayfind --source live` does not support source file targets; use locked artifacts for file outlines.'
403
+ )
404
+ ),
405
+ view,
406
+ };
407
+ }
408
+ if (view === 'contract' || view === 'map' || view === 'outline') {
409
+ return {
410
+ result: Result.err(
411
+ liveSourceError(
412
+ '`trails wayfind --source live` supports overview and ID lookup; use locked artifacts for this view.'
413
+ )
414
+ ),
415
+ view,
416
+ };
417
+ }
418
+ return {
419
+ result: ctx.compose('survey', {
420
+ ...(input.target === undefined ? {} : { id: input.target }),
421
+ ...liveModuleInput(input),
422
+ }),
423
+ view:
424
+ input.target === undefined
425
+ ? ('overview' as const)
426
+ : ('describe' as const),
427
+ };
428
+ };
429
+
430
+ const viewRelation = (input: WayfindInput, ctx: TrailContextWithCompose) => {
431
+ if (input.target === undefined || !(input.deps || input.impact)) {
432
+ return Promise.resolve();
433
+ }
434
+ if (
435
+ targetLooksLikeFile(input.target) ||
436
+ targetLooksLikePattern(input.target)
437
+ ) {
438
+ return {
439
+ result: Result.err(
440
+ new ValidationError('Relation flags require a graph entity target.')
441
+ ),
442
+ view: 'map' as const,
443
+ };
444
+ }
445
+ return {
446
+ result: ctx.compose('wayfind.impact', {
447
+ direction: input.deps ? 'upstream' : 'downstream',
448
+ filters: populationFilters(input),
449
+ id: input.target,
450
+ limit: input.limit,
451
+ maxDepth: input.depth,
452
+ ...sourceInput(input),
453
+ }),
454
+ view: 'map' as const,
455
+ };
456
+ };
457
+
458
+ const explicitGlobError = (view: WayfinderView) => ({
459
+ result: Result.err(
460
+ new ValidationError(
461
+ 'Glob patterns require `trails wayfind pattern <glob>` so the selector is explicit.'
462
+ )
463
+ ),
464
+ view,
465
+ });
466
+
467
+ const viewSelectorTarget = (
468
+ input: WayfindInput,
469
+ ctx: TrailContextWithCompose,
470
+ resolver: WayfinderResolver,
471
+ view: WayfinderView
472
+ ) => {
473
+ const { target } = input;
474
+ if (target === undefined) {
475
+ return null;
476
+ }
477
+ if (
478
+ resolver !== 'file' &&
479
+ resolver !== 'pattern' &&
480
+ targetLooksLikePattern(target)
481
+ ) {
482
+ return explicitGlobError(view);
483
+ }
484
+ if (resolver === 'pattern') {
485
+ return {
486
+ result: ctx.compose('wayfind.search', {
487
+ filters: { idGlob: target, ...populationFilters(input) },
488
+ limit: input.limit,
489
+ ...sourceInput(input),
490
+ }),
491
+ view: 'list' as const,
492
+ };
493
+ }
494
+ if (resolver === 'query') {
495
+ return {
496
+ result: ctx.compose('wayfind.search', {
497
+ filters: { query: target, ...populationFilters(input) },
498
+ limit: input.limit,
499
+ ...sourceInput(input),
500
+ }),
501
+ view: 'list' as const,
502
+ };
503
+ }
504
+ if (resolver === 'file' && !targetLooksLikeFile(target)) {
505
+ return {
506
+ result: Result.err(
507
+ new ValidationError(
508
+ '`trails wayfind file` requires a source file path target.'
509
+ )
510
+ ),
511
+ view: 'outline' as const,
512
+ };
513
+ }
514
+ if (resolver === 'file') {
515
+ return {
516
+ result: ctx.compose('wayfind.outline', {
517
+ all: false,
518
+ contracts: false,
519
+ file: target,
520
+ review: false,
521
+ source: false,
522
+ ...sourceInput(input),
523
+ surfaces: false,
524
+ }),
525
+ view: 'outline' as const,
526
+ };
527
+ }
528
+ return null;
529
+ };
530
+
531
+ const viewTarget = async (
532
+ input: WayfindInput,
533
+ ctx: TrailContextWithCompose
534
+ ) => {
535
+ const { target } = input;
536
+ if (target === undefined) {
537
+ return;
538
+ }
539
+ const view = viewFor(input);
540
+ if (input.resolver === undefined && targetLooksLikePattern(target)) {
541
+ return explicitGlobError(view);
542
+ }
543
+ const resolver =
544
+ input.resolver ?? (targetLooksLikeFile(target) ? 'file' : 'id');
545
+ const selectorView = viewSelectorTarget(input, ctx, resolver, view);
546
+ if (selectorView !== null) {
547
+ return selectorView;
548
+ }
549
+ if (resolver !== 'file' && view === 'outline') {
550
+ return {
551
+ result: Result.err(
552
+ new ValidationError(
553
+ 'The outline view requires a source file path target. Use `trails wayfind file <file> --view outline` or pass a file-like target.'
554
+ )
555
+ ),
556
+ view: 'outline' as const,
557
+ };
558
+ }
559
+ if (view === 'overview') {
560
+ return {
561
+ result: Result.err(
562
+ new ValidationError('The overview view does not accept a target.')
563
+ ),
564
+ view,
565
+ };
566
+ }
567
+ if (view === 'contract') {
568
+ return {
569
+ result: ctx.compose('wayfind.contract', {
570
+ id: target,
571
+ ...sourceInput(input),
572
+ }),
573
+ view: 'contract' as const,
574
+ };
575
+ }
576
+ if (view === 'describe' || view === 'summary') {
577
+ return {
578
+ result: ctx.compose('wayfind.describe', {
579
+ id: target,
580
+ ...sourceInput(input),
581
+ }),
582
+ view,
583
+ };
584
+ }
585
+ if (view === 'map') {
586
+ return {
587
+ result: ctx.compose('wayfind.impact', {
588
+ direction: 'both',
589
+ filters: populationFilters(input),
590
+ id: target,
591
+ limit: input.limit,
592
+ maxDepth: 1,
593
+ ...sourceInput(input),
594
+ }),
595
+ view: 'map' as const,
596
+ };
597
+ }
598
+ return {
599
+ result: ctx.compose('wayfind.nearby', {
600
+ id: target,
601
+ ...sourceInput(input),
602
+ }),
603
+ view: 'summary' as const,
604
+ };
605
+ };
606
+
607
+ const adapterSurfaceTargets = async (
608
+ input: WayfindInput,
609
+ ctx: TrailContextWithCompose
610
+ ): Promise<TrailResult<readonly string[], Error>> => {
611
+ if (input.adapter === undefined) {
612
+ return Result.ok([]);
613
+ }
614
+ const facts = await ctx.compose('wayfind.adapters', {
615
+ filters: { packageName: input.adapter },
616
+ limit: input.limit,
617
+ ...sourceInput(input),
618
+ });
619
+ if (facts.isErr()) {
620
+ return facts;
621
+ }
622
+ const { adapters } = facts.value as { adapters?: unknown };
623
+ if (!Array.isArray(adapters)) {
624
+ return Result.ok([]);
625
+ }
626
+ return Result.ok(
627
+ [
628
+ ...new Set(
629
+ adapters
630
+ .map((fact) =>
631
+ typeof fact === 'object' && fact !== null
632
+ ? (fact as { target?: unknown }).target
633
+ : undefined
634
+ )
635
+ .filter((target): target is string => typeof target === 'string')
636
+ ),
637
+ ].toSorted()
638
+ );
639
+ };
640
+
641
+ const populationFiltersWithAdapter = async (
642
+ input: WayfindInput,
643
+ ctx: TrailContextWithCompose
644
+ ): Promise<TrailResult<ReturnType<typeof populationFilters>, Error>> => {
645
+ const filters = populationFilters(input);
646
+ const adapterTargets = await adapterSurfaceTargets(input, ctx);
647
+ if (adapterTargets.isErr()) {
648
+ return adapterTargets;
649
+ }
650
+ if (adapterTargets.value.length === 0) {
651
+ return Result.ok(
652
+ input.adapter === undefined
653
+ ? filters
654
+ : { ...filters, surface: '__no_adapter_target__' }
655
+ );
656
+ }
657
+ return Result.ok({ ...filters, surface: adapterTargets.value });
658
+ };
659
+
660
+ const viewPopulation = async (
661
+ input: WayfindInput,
662
+ ctx: TrailContextWithCompose
663
+ ) => {
664
+ const filtersResult = await populationFiltersWithAdapter(input, ctx);
665
+ if (filtersResult.isErr()) {
666
+ return { result: filtersResult, view: 'list' as const };
667
+ }
668
+ const filters = filtersResult.value;
669
+ const view = viewFor(input);
670
+ if (view === 'overview') {
671
+ return {
672
+ result: ctx.compose('wayfind.overview', sourceInput(input)),
673
+ view: 'overview' as const,
674
+ };
675
+ }
676
+ if (input.resources) {
677
+ return {
678
+ result: ctx.compose('wayfind.resources', {
679
+ filters,
680
+ limit: input.limit,
681
+ ...sourceInput(input),
682
+ }),
683
+ view: 'list' as const,
684
+ };
685
+ }
686
+ if (input.contours) {
687
+ return {
688
+ result: ctx.compose('wayfind.contours', {
689
+ filters,
690
+ limit: input.limit,
691
+ ...sourceInput(input),
692
+ }),
693
+ view: 'list' as const,
694
+ };
695
+ }
696
+ if (input.signals) {
697
+ return {
698
+ result: ctx.compose('wayfind.signals', {
699
+ filters,
700
+ limit: input.limit,
701
+ ...sourceInput(input),
702
+ }),
703
+ view: 'list' as const,
704
+ };
705
+ }
706
+ if (input.surfaces) {
707
+ return {
708
+ result: ctx.compose('wayfind.surfaces', {
709
+ filters,
710
+ limit: input.limit,
711
+ ...sourceInput(input),
712
+ }),
713
+ view: 'list' as const,
714
+ };
715
+ }
716
+ if (input.facets) {
717
+ return {
718
+ result: ctx.compose('wayfind.facets', {
719
+ filters,
720
+ limit: input.limit,
721
+ ...sourceInput(input),
722
+ }),
723
+ view: 'list' as const,
724
+ };
725
+ }
726
+ if (input.errors) {
727
+ return {
728
+ result: ctx.compose('wayfind.errors', {
729
+ filters,
730
+ limit: input.limit,
731
+ ...sourceInput(input),
732
+ }),
733
+ view: 'list' as const,
734
+ };
735
+ }
736
+ if (input.trails || input.intent !== undefined) {
737
+ return {
738
+ result: ctx.compose('wayfind.trails', {
739
+ filters,
740
+ limit: input.limit,
741
+ ...sourceInput(input),
742
+ }),
743
+ view: 'list' as const,
744
+ };
745
+ }
746
+ return {
747
+ result: ctx.compose('wayfind.search', {
748
+ filters,
749
+ limit: input.limit,
750
+ ...sourceInput(input),
751
+ }),
752
+ view: 'list' as const,
753
+ };
754
+ };
755
+
756
+ export const wayfindTrail = trail('wayfind.navigate', {
757
+ args: ['target'],
758
+ blaze: async (input, ctx) => {
759
+ if (input.source === 'live') {
760
+ const dispatched = await viewLiveSource(input, ctx);
761
+ if (dispatched !== undefined) {
762
+ return envelopeFor(
763
+ await dispatched.result,
764
+ input,
765
+ ctx,
766
+ dispatched.view
767
+ );
768
+ }
769
+ }
770
+ const dispatched =
771
+ (await viewRelation(input, ctx)) ??
772
+ (await (input.target === undefined
773
+ ? viewPopulation(input, ctx)
774
+ : viewTarget(input, ctx)));
775
+ if (dispatched === undefined) {
776
+ return Result.err(
777
+ new ValidationError('Provide a Wayfinder target or population filter.')
778
+ );
779
+ }
780
+ return envelopeFor(await dispatched.result, input, ctx, dispatched.view);
781
+ },
782
+ cli: {
783
+ path: 'wayfind',
784
+ },
785
+ composeInput: wayfindComposeInputSchema,
786
+ composes: [
787
+ 'survey',
788
+ 'wayfind.adapters',
789
+ 'wayfind.contract',
790
+ 'wayfind.contours',
791
+ 'wayfind.describe',
792
+ 'wayfind.errors',
793
+ 'wayfind.examples',
794
+ 'wayfind.facets',
795
+ 'wayfind.impact',
796
+ 'wayfind.nearby',
797
+ 'wayfind.outline',
798
+ 'wayfind.overview',
799
+ 'wayfind.resources',
800
+ 'wayfind.search',
801
+ 'wayfind.signals',
802
+ 'wayfind.surfaces',
803
+ 'wayfind.trails',
804
+ 'wayfind.versions',
805
+ ],
806
+ description: 'Navigate Trails graph facts by target, filters, and view',
807
+ examples: [
808
+ {
809
+ input: {},
810
+ name: 'List graph entries',
811
+ },
812
+ {
813
+ input: { contract: true, target: 'wayfind.search' },
814
+ name: 'Inspect a trail contract',
815
+ },
816
+ {
817
+ input: { target: 'wayfind.search' },
818
+ name: 'Inspect nearby graph context',
819
+ },
820
+ {
821
+ input: { map: true, target: 'wayfind.search' },
822
+ name: 'Map nearby graph context',
823
+ },
824
+ {
825
+ input: { target: 'apps/trails/src/trails/wayfind.ts' },
826
+ name: 'Outline a source file',
827
+ },
828
+ {
829
+ input: { resources: true },
830
+ name: 'List resource facts',
831
+ },
832
+ {
833
+ input: { contours: true },
834
+ name: 'List contour facts',
835
+ },
836
+ {
837
+ input: { signals: true },
838
+ name: 'List signal facts',
839
+ },
840
+ {
841
+ input: { surfaces: true },
842
+ name: 'List surface facts',
843
+ },
844
+ {
845
+ input: { facets: true },
846
+ name: 'List facet facts',
847
+ },
848
+ {
849
+ input: { overview: true },
850
+ name: 'Show graph overview',
851
+ },
852
+ {
853
+ input: { include: ['examples'], target: 'wayfind.search' },
854
+ name: 'Attach examples for a target',
855
+ },
856
+ {
857
+ input: { include: ['versions'], target: 'wayfind.search' },
858
+ name: 'Attach version facts for a target',
859
+ },
860
+ {
861
+ input: { source: 'live', target: 'wayfind.search' },
862
+ name: 'Inspect a live app entity',
863
+ },
864
+ {
865
+ input: { impact: true, target: 'db.main' },
866
+ name: 'Trace downstream graph impact',
867
+ },
868
+ {
869
+ input: { deps: true, target: 'wayfind.search' },
870
+ name: 'Inspect upstream dependencies',
871
+ },
872
+ {
873
+ input: { intent: 'read', trails: true },
874
+ name: 'List read trails',
875
+ },
876
+ ],
877
+ input: wayfindInputSchema,
878
+ intent: 'read',
879
+ output: wayfindOutputSchema,
880
+ visibility: 'public',
881
+ });
882
+
883
+ const wayfindSelectorBaseInputSchema = (selectorDescription: string) =>
884
+ z
885
+ .object({
886
+ limit: z.number().int().positive().max(500).default(100),
887
+ rootDir: z.string().optional().describe('Workspace root directory'),
888
+ selector: z.string().min(1).describe(selectorDescription),
889
+ })
890
+ .strict();
891
+
892
+ const wayfindPatternInputSchema = wayfindSelectorBaseInputSchema(
893
+ 'Wayfinder ID glob pattern'
894
+ );
895
+ const wayfindQueryInputSchema = wayfindSelectorBaseInputSchema(
896
+ 'Wayfinder indexed text query'
897
+ );
898
+ const wayfindFileInputSchema = wayfindSelectorBaseInputSchema(
899
+ 'Wayfinder source file path'
900
+ ).extend({
901
+ outline: z
902
+ .boolean()
903
+ .default(false)
904
+ .describe('Render the outline view for a source file target'),
905
+ });
906
+
907
+ const selectorSourceInput = (
908
+ input: Readonly<{ rootDir?: string | undefined }>
909
+ ): { readonly rootDir?: string | undefined } =>
910
+ input.rootDir === undefined ? {} : { rootDir: input.rootDir };
911
+
912
+ export const wayfindPatternTrail = trail('wayfind.pattern', {
913
+ args: ['selector'],
914
+ blaze: (input, ctx) =>
915
+ ctx.compose('wayfind.navigate', {
916
+ limit: input.limit,
917
+ resolver: 'pattern',
918
+ target: input.selector,
919
+ view: 'list',
920
+ ...selectorSourceInput(input),
921
+ }),
922
+ cli: {
923
+ path: 'wayfind pattern',
924
+ },
925
+ composes: ['wayfind.navigate'],
926
+ description: 'Find Wayfinder graph facts by explicit ID glob pattern',
927
+ examples: [
928
+ {
929
+ input: { selector: 'wayfind.*' },
930
+ name: 'Find Wayfinder trails',
931
+ },
932
+ ],
933
+ input: wayfindPatternInputSchema,
934
+ intent: 'read',
935
+ meta: {
936
+ internal: true,
937
+ },
938
+ output: wayfindOutputSchema,
939
+ visibility: 'internal',
940
+ });
941
+
942
+ export const wayfindQueryTrail = trail('wayfind.query', {
943
+ args: ['selector'],
944
+ blaze: (input, ctx) =>
945
+ ctx.compose('wayfind.navigate', {
946
+ limit: input.limit,
947
+ resolver: 'query',
948
+ target: input.selector,
949
+ view: 'list',
950
+ ...selectorSourceInput(input),
951
+ }),
952
+ cli: {
953
+ path: 'wayfind query',
954
+ },
955
+ composes: ['wayfind.navigate'],
956
+ description: 'Find Wayfinder graph facts by explicit text query',
957
+ examples: [
958
+ {
959
+ input: { selector: 'release drift' },
960
+ name: 'Find release drift facts',
961
+ },
962
+ ],
963
+ input: wayfindQueryInputSchema,
964
+ intent: 'read',
965
+ meta: {
966
+ internal: true,
967
+ },
968
+ output: wayfindOutputSchema,
969
+ visibility: 'internal',
970
+ });
971
+
972
+ export const wayfindFileTrail = trail('wayfind.file', {
973
+ args: ['selector'],
974
+ blaze: (input, ctx) =>
975
+ ctx.compose('wayfind.navigate', {
976
+ limit: input.limit,
977
+ outline: input.outline,
978
+ resolver: 'file',
979
+ target: input.selector,
980
+ view: 'outline',
981
+ ...selectorSourceInput(input),
982
+ }),
983
+ cli: {
984
+ path: 'wayfind file',
985
+ },
986
+ composes: ['wayfind.navigate'],
987
+ description: 'Outline one source file through Wayfinder',
988
+ examples: [
989
+ {
990
+ input: { selector: 'apps/trails/src/app.ts' },
991
+ name: 'Outline the Trails app module',
992
+ },
993
+ ],
994
+ input: wayfindFileInputSchema,
995
+ intent: 'read',
996
+ meta: {
997
+ internal: true,
998
+ },
999
+ output: wayfindOutputSchema,
1000
+ visibility: 'internal',
1001
+ });