@ontrails/trails 1.0.0-beta.4 → 1.0.0-beta.42

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