@ontrails/trails 1.0.0-beta.14 → 1.0.0-beta.16

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 (197) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +17 -7
  5. package/src/clack.ts +1 -1
  6. package/src/cli.ts +304 -10
  7. package/src/completions.ts +240 -0
  8. package/src/load-app-mirror.ts +160 -0
  9. package/src/local-state-io.ts +153 -0
  10. package/src/project-writes.ts +320 -0
  11. package/src/run-collision.ts +125 -0
  12. package/src/run-completions-install.ts +179 -0
  13. package/src/run-example.ts +149 -0
  14. package/src/run-examples.ts +148 -0
  15. package/src/run-quiet.ts +75 -0
  16. package/src/run-trace.ts +273 -0
  17. package/src/run-warden.ts +39 -0
  18. package/src/run-watch.ts +432 -0
  19. package/src/scaffold-versions.generated.ts +12 -0
  20. package/src/trails/add-surface.ts +172 -0
  21. package/src/trails/add-trail.ts +73 -27
  22. package/src/trails/add-verify.ts +68 -23
  23. package/src/trails/completions-complete.ts +165 -0
  24. package/src/trails/completions.ts +47 -0
  25. package/src/trails/create-scaffold.ts +101 -35
  26. package/src/trails/create.ts +87 -74
  27. package/src/trails/dev-clean.ts +31 -22
  28. package/src/trails/dev-reset.ts +9 -3
  29. package/src/trails/dev-stats.ts +28 -20
  30. package/src/trails/dev-support.ts +109 -95
  31. package/src/trails/draft-promote.ts +351 -107
  32. package/src/trails/guide.ts +55 -38
  33. package/src/trails/load-app.ts +712 -38
  34. package/src/trails/root-dir.ts +21 -0
  35. package/src/trails/run-example.ts +482 -0
  36. package/src/trails/run-examples.ts +141 -0
  37. package/src/trails/run.ts +403 -0
  38. package/src/trails/survey.ts +517 -186
  39. package/src/trails/topo-activation.ts +385 -0
  40. package/src/trails/topo-compile.ts +55 -0
  41. package/src/trails/topo-history.ts +14 -11
  42. package/src/trails/topo-output-schemas.ts +175 -0
  43. package/src/trails/topo-pin.ts +25 -16
  44. package/src/trails/topo-read-support.ts +178 -238
  45. package/src/trails/topo-reports.ts +445 -63
  46. package/src/trails/topo-store-support.ts +67 -35
  47. package/src/trails/topo-support.ts +93 -147
  48. package/src/trails/topo-unpin.ts +17 -7
  49. package/src/trails/topo-verify.ts +19 -10
  50. package/src/trails/topo.ts +64 -31
  51. package/src/trails/warden-guide.ts +121 -0
  52. package/src/trails/warden.ts +137 -47
  53. package/src/versions.ts +28 -0
  54. package/.turbo/turbo-build.log +0 -1
  55. package/.turbo/turbo-lint.log +0 -3
  56. package/.turbo/turbo-typecheck.log +0 -1
  57. package/__tests__/examples.test.ts +0 -20
  58. package/dist/bin/trails.d.ts +0 -3
  59. package/dist/bin/trails.d.ts.map +0 -1
  60. package/dist/bin/trails.js +0 -4
  61. package/dist/bin/trails.js.map +0 -1
  62. package/dist/src/app.d.ts +0 -2
  63. package/dist/src/app.d.ts.map +0 -1
  64. package/dist/src/app.js +0 -22
  65. package/dist/src/app.js.map +0 -1
  66. package/dist/src/clack.d.ts +0 -9
  67. package/dist/src/clack.d.ts.map +0 -1
  68. package/dist/src/clack.js +0 -84
  69. package/dist/src/clack.js.map +0 -1
  70. package/dist/src/cli.d.ts +0 -2
  71. package/dist/src/cli.d.ts.map +0 -1
  72. package/dist/src/cli.js +0 -13
  73. package/dist/src/cli.js.map +0 -1
  74. package/dist/src/trails/add-surface.d.ts +0 -13
  75. package/dist/src/trails/add-surface.d.ts.map +0 -1
  76. package/dist/src/trails/add-surface.js +0 -88
  77. package/dist/src/trails/add-surface.js.map +0 -1
  78. package/dist/src/trails/add-trail.d.ts +0 -10
  79. package/dist/src/trails/add-trail.d.ts.map +0 -1
  80. package/dist/src/trails/add-trail.js +0 -77
  81. package/dist/src/trails/add-trail.js.map +0 -1
  82. package/dist/src/trails/add-trailhead.d.ts +0 -13
  83. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  84. package/dist/src/trails/add-trailhead.js +0 -88
  85. package/dist/src/trails/add-trailhead.js.map +0 -1
  86. package/dist/src/trails/add-verify.d.ts +0 -10
  87. package/dist/src/trails/add-verify.d.ts.map +0 -1
  88. package/dist/src/trails/add-verify.js +0 -67
  89. package/dist/src/trails/add-verify.js.map +0 -1
  90. package/dist/src/trails/create-scaffold.d.ts +0 -15
  91. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  92. package/dist/src/trails/create-scaffold.js +0 -288
  93. package/dist/src/trails/create-scaffold.js.map +0 -1
  94. package/dist/src/trails/create.d.ts +0 -22
  95. package/dist/src/trails/create.d.ts.map +0 -1
  96. package/dist/src/trails/create.js +0 -121
  97. package/dist/src/trails/create.js.map +0 -1
  98. package/dist/src/trails/dev-clean.d.ts +0 -9
  99. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  100. package/dist/src/trails/dev-clean.js +0 -65
  101. package/dist/src/trails/dev-clean.js.map +0 -1
  102. package/dist/src/trails/dev-reset.d.ts +0 -6
  103. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  104. package/dist/src/trails/dev-reset.js +0 -38
  105. package/dist/src/trails/dev-reset.js.map +0 -1
  106. package/dist/src/trails/dev-stats.d.ts +0 -7
  107. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  108. package/dist/src/trails/dev-stats.js +0 -61
  109. package/dist/src/trails/dev-stats.js.map +0 -1
  110. package/dist/src/trails/dev-support.d.ts +0 -64
  111. package/dist/src/trails/dev-support.d.ts.map +0 -1
  112. package/dist/src/trails/dev-support.js +0 -178
  113. package/dist/src/trails/dev-support.js.map +0 -1
  114. package/dist/src/trails/draft-promote.d.ts +0 -18
  115. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  116. package/dist/src/trails/draft-promote.js +0 -386
  117. package/dist/src/trails/draft-promote.js.map +0 -1
  118. package/dist/src/trails/guide.d.ts +0 -21
  119. package/dist/src/trails/guide.d.ts.map +0 -1
  120. package/dist/src/trails/guide.js +0 -64
  121. package/dist/src/trails/guide.js.map +0 -1
  122. package/dist/src/trails/load-app.d.ts +0 -6
  123. package/dist/src/trails/load-app.d.ts.map +0 -1
  124. package/dist/src/trails/load-app.js +0 -67
  125. package/dist/src/trails/load-app.js.map +0 -1
  126. package/dist/src/trails/project.d.ts +0 -8
  127. package/dist/src/trails/project.d.ts.map +0 -1
  128. package/dist/src/trails/project.js +0 -54
  129. package/dist/src/trails/project.js.map +0 -1
  130. package/dist/src/trails/survey.d.ts +0 -18
  131. package/dist/src/trails/survey.d.ts.map +0 -1
  132. package/dist/src/trails/survey.js +0 -212
  133. package/dist/src/trails/survey.js.map +0 -1
  134. package/dist/src/trails/topo-constants.d.ts +0 -3
  135. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  136. package/dist/src/trails/topo-constants.js +0 -3
  137. package/dist/src/trails/topo-constants.js.map +0 -1
  138. package/dist/src/trails/topo-export.d.ts +0 -18
  139. package/dist/src/trails/topo-export.d.ts.map +0 -1
  140. package/dist/src/trails/topo-export.js +0 -34
  141. package/dist/src/trails/topo-export.js.map +0 -1
  142. package/dist/src/trails/topo-history.d.ts +0 -24
  143. package/dist/src/trails/topo-history.d.ts.map +0 -1
  144. package/dist/src/trails/topo-history.js +0 -33
  145. package/dist/src/trails/topo-history.js.map +0 -1
  146. package/dist/src/trails/topo-pin.d.ts +0 -21
  147. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  148. package/dist/src/trails/topo-pin.js +0 -35
  149. package/dist/src/trails/topo-pin.js.map +0 -1
  150. package/dist/src/trails/topo-read-support.d.ts +0 -54
  151. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  152. package/dist/src/trails/topo-read-support.js +0 -178
  153. package/dist/src/trails/topo-read-support.js.map +0 -1
  154. package/dist/src/trails/topo-reports.d.ts +0 -50
  155. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  156. package/dist/src/trails/topo-reports.js +0 -122
  157. package/dist/src/trails/topo-reports.js.map +0 -1
  158. package/dist/src/trails/topo-show.d.ts +0 -23
  159. package/dist/src/trails/topo-show.d.ts.map +0 -1
  160. package/dist/src/trails/topo-show.js +0 -53
  161. package/dist/src/trails/topo-show.js.map +0 -1
  162. package/dist/src/trails/topo-store-support.d.ts +0 -13
  163. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  164. package/dist/src/trails/topo-store-support.js +0 -55
  165. package/dist/src/trails/topo-store-support.js.map +0 -1
  166. package/dist/src/trails/topo-support.d.ts +0 -87
  167. package/dist/src/trails/topo-support.d.ts.map +0 -1
  168. package/dist/src/trails/topo-support.js +0 -165
  169. package/dist/src/trails/topo-support.js.map +0 -1
  170. package/dist/src/trails/topo-unpin.d.ts +0 -15
  171. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  172. package/dist/src/trails/topo-unpin.js +0 -39
  173. package/dist/src/trails/topo-unpin.js.map +0 -1
  174. package/dist/src/trails/topo-verify.d.ts +0 -5
  175. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  176. package/dist/src/trails/topo-verify.js +0 -28
  177. package/dist/src/trails/topo-verify.js.map +0 -1
  178. package/dist/src/trails/topo.d.ts +0 -5
  179. package/dist/src/trails/topo.d.ts.map +0 -1
  180. package/dist/src/trails/topo.js +0 -67
  181. package/dist/src/trails/topo.js.map +0 -1
  182. package/dist/src/trails/warden.d.ts +0 -19
  183. package/dist/src/trails/warden.d.ts.map +0 -1
  184. package/dist/src/trails/warden.js +0 -89
  185. package/dist/src/trails/warden.js.map +0 -1
  186. package/dist/tsconfig.tsbuildinfo +0 -1
  187. package/src/__tests__/create.test.ts +0 -351
  188. package/src/__tests__/draft-promote.test.ts +0 -144
  189. package/src/__tests__/guide.test.ts +0 -91
  190. package/src/__tests__/load-app.test.ts +0 -58
  191. package/src/__tests__/survey.test.ts +0 -301
  192. package/src/__tests__/topo-dev.test.ts +0 -424
  193. package/src/__tests__/warden.test.ts +0 -74
  194. package/src/trails/add-trailhead.ts +0 -121
  195. package/src/trails/topo-export.ts +0 -39
  196. package/src/trails/topo-show.ts +0 -58
  197. package/tsconfig.json +0 -9
@@ -1,296 +1,627 @@
1
1
  /**
2
2
  * `survey` trail -- Full topo introspection.
3
3
  *
4
- * Lists trails, shows detail for individual trails, generates trailhead maps,
5
- * and diffs against previous versions.
4
+ * Lists trails, looks up trails/resources/signals, and diffs against previous
5
+ * versions.
6
6
  */
7
7
 
8
+ import { basename, extname, join } from 'node:path';
9
+
8
10
  import type { Topo } from '@ontrails/core';
9
- import { NotFoundError, Result, trail } from '@ontrails/core';
10
- import type { DiffResult } from '@ontrails/schema';
11
11
  import {
12
- diffTrailheadMaps,
13
- generateOpenApiSpec,
14
- generateTrailheadMap,
15
- readTrailheadMap,
16
- } from '@ontrails/schema';
12
+ deriveSafePath,
13
+ NotFoundError,
14
+ Result,
15
+ trail,
16
+ ValidationError,
17
+ } from '@ontrails/core';
18
+ import type { DiffEntry, DiffResult, TopoGraph } from '@ontrails/topographer';
19
+ import {
20
+ createTopoStore,
21
+ deriveTopoGraphDiff,
22
+ deriveTopoGraph,
23
+ TOPO_GRAPH_SCHEMA_VERSION,
24
+ readTopoGraph,
25
+ } from '@ontrails/topographer';
17
26
  import { z } from 'zod';
18
27
 
19
- import { loadApp } from './load-app.js';
28
+ import { writeIsolatedExampleJsonFile } from '../local-state-io.js';
29
+
30
+ import { tryLoadFreshAppLease } from './load-app.js';
31
+ import { resolveTrailRootDir } from './root-dir.js';
20
32
  import {
21
33
  buildCurrentTopoBrief,
22
- buildCurrentTopoDetail,
23
34
  buildCurrentTopoList,
35
+ buildCurrentTopoMatches,
36
+ buildCurrentTrailDetail,
37
+ buildCurrentResourceDetail,
38
+ buildCurrentSignalDetail,
39
+ readSurfaceLayerNamesFromContext,
24
40
  } from './topo-read-support.js';
25
- import { exportCurrentTopo } from './topo-store-support.js';
41
+ import {
42
+ activationOverviewOutput,
43
+ resourceDetailOutput,
44
+ signalDetailOutput,
45
+ trailDetailOutput,
46
+ } from './topo-output-schemas.js';
47
+ import { createIsolatedExampleInput } from './topo-support.js';
48
+ import { briefReportSchema } from './topo-reports.js';
49
+ import type { SurfaceLayerNames } from './topo-reports.js';
26
50
 
27
51
  export {
28
- formatProvisionDetail,
29
- generateBriefReport,
30
- generateSurveyList,
31
- generateTrailDetail,
52
+ briefReportSchema,
53
+ deriveBriefReport,
54
+ deriveResourceDetail,
55
+ deriveSignalDetail,
56
+ deriveSurveyList,
57
+ deriveTrailDetail,
32
58
  } from './topo-reports.js';
33
59
  export type {
34
60
  BriefReport,
61
+ SignalDetailReport,
62
+ SurfaceLayerNames,
35
63
  SurveyListReport,
36
64
  TrailDetailReport,
37
65
  } from './topo-reports.js';
38
66
 
39
67
  // ---------------------------------------------------------------------------
40
- // Brief report (formerly scout)
68
+ // Survey diff helpers
41
69
  // ---------------------------------------------------------------------------
42
70
 
43
- const formatDiff = (diff: DiffResult): object => ({
71
+ interface SurveyDiffReport {
72
+ readonly against: string;
73
+ readonly breaking: readonly DiffEntry[];
74
+ readonly hasBreaking: boolean;
75
+ readonly info: readonly DiffEntry[];
76
+ readonly mode: 'diff';
77
+ readonly warnings: readonly DiffEntry[];
78
+ }
79
+
80
+ const formatDiff = (diff: DiffResult, against: string): SurveyDiffReport => ({
81
+ against,
44
82
  breaking: diff.breaking,
45
83
  hasBreaking: diff.hasBreaking,
46
84
  info: diff.info,
85
+ mode: 'diff',
47
86
  warnings: diff.warnings,
48
87
  });
49
88
 
89
+ const createDiffExampleInput = (): {
90
+ readonly against: string;
91
+ readonly module: string;
92
+ readonly rootDir: string;
93
+ } => {
94
+ const input = createIsolatedExampleInput('survey-diff');
95
+ writeIsolatedExampleJsonFile(input.rootDir, 'baseline/topo.lock', {
96
+ activationGraph: {
97
+ edgeCount: 0,
98
+ edges: [],
99
+ sourceCount: 0,
100
+ sourceKeys: [],
101
+ trailIds: [],
102
+ },
103
+ activationSources: {},
104
+ entries: [],
105
+ generatedAt: '2026-01-01T00:00:00.000Z',
106
+ topoGraphSchemaVersion: TOPO_GRAPH_SCHEMA_VERSION,
107
+ } satisfies TopoGraph);
108
+ return { ...input, against: 'baseline' };
109
+ };
110
+
111
+ const isNotFound = (error: unknown): boolean =>
112
+ typeof error === 'object' &&
113
+ error !== null &&
114
+ (error as NodeJS.ErrnoException).code === 'ENOENT';
115
+
116
+ const readTopoGraphFile = async (
117
+ filePath: string
118
+ ): Promise<TopoGraph | null> => {
119
+ try {
120
+ return (await Bun.file(filePath).json()) as TopoGraph;
121
+ } catch (error: unknown) {
122
+ if (isNotFound(error)) {
123
+ return null;
124
+ }
125
+ throw error;
126
+ }
127
+ };
128
+
129
+ const readStoredTopoGraph = (
130
+ rootDir: string,
131
+ against: string
132
+ ): TopoGraph | undefined => {
133
+ try {
134
+ const store = createTopoStore({ rootDir });
135
+ const stored =
136
+ store.exports.get({ pin: against }) ??
137
+ store.exports.get({ snapshotId: against });
138
+ return stored === undefined
139
+ ? undefined
140
+ : (JSON.parse(stored.topoGraphJson) as TopoGraph);
141
+ } catch (error: unknown) {
142
+ if (error instanceof NotFoundError) {
143
+ return undefined;
144
+ }
145
+ throw error;
146
+ }
147
+ };
148
+
149
+ const readPathTopoGraph = async (
150
+ rootDir: string,
151
+ against: string
152
+ ): Promise<Result<TopoGraph | null, Error>> => {
153
+ const safePath = deriveSafePath(rootDir, against);
154
+ if (safePath.isErr()) {
155
+ return safePath;
156
+ }
157
+
158
+ return Result.ok(
159
+ basename(safePath.value) === 'topo.lock' ||
160
+ extname(safePath.value) === '.json'
161
+ ? await readTopoGraphFile(safePath.value)
162
+ : await readTopoGraph({ dir: safePath.value })
163
+ );
164
+ };
165
+
166
+ const describeAgainstPathTarget = (against: string): string =>
167
+ basename(against) === 'topo.lock' || extname(against) === '.json'
168
+ ? 'workspace-relative TopoGraph file'
169
+ : 'workspace-relative directory containing topo.lock';
170
+
171
+ const topoGraphNotFound = (against: string): NotFoundError =>
172
+ new NotFoundError(
173
+ `No TopoGraph found for: ${against}. Tried ${describeAgainstPathTarget(
174
+ against
175
+ )}, then topo-store pin and snapshot references.`
176
+ );
177
+
178
+ const readAgainstTopoGraph = async (
179
+ rootDir: string,
180
+ against?: string | undefined
181
+ ): Promise<Result<{ against: string; map: TopoGraph }, Error>> => {
182
+ if (against === undefined || against === 'saved') {
183
+ const map = await readTopoGraph({ dir: join(rootDir, '.trails') });
184
+ return map === null
185
+ ? Result.err(
186
+ new NotFoundError(
187
+ 'No saved TopoGraph found. Run `trails topo compile` first.'
188
+ )
189
+ )
190
+ : Result.ok({ against: 'saved', map });
191
+ }
192
+
193
+ // Treat explicit filesystem targets as the most local user intent; stored
194
+ // pins and snapshot ids are fallback references when no path exists.
195
+ const pathMap = await readPathTopoGraph(rootDir, against);
196
+ if (pathMap.isErr()) {
197
+ return pathMap;
198
+ }
199
+ if (pathMap.value !== null) {
200
+ return Result.ok({ against, map: pathMap.value });
201
+ }
202
+
203
+ const storedMap = readStoredTopoGraph(rootDir, against);
204
+ if (storedMap !== undefined) {
205
+ return Result.ok({ against, map: storedMap });
206
+ }
207
+
208
+ return Result.err(topoGraphNotFound(against));
209
+ };
210
+
50
211
  const buildSurveyDiff = async (
51
212
  app: Topo,
52
- breakingOnly: boolean
53
- ): Promise<Result<object, Error>> => {
54
- const currentMap = generateTrailheadMap(app);
55
- const previousMap = await readTrailheadMap();
56
- if (!previousMap) {
57
- return Result.err(
58
- new NotFoundError(
59
- 'No previous trailhead map found. Run `trails topo export` first.'
60
- )
61
- );
213
+ rootDir: string,
214
+ breakingOnly: boolean,
215
+ against?: string | undefined
216
+ ): Promise<Result<SurveyDiffReport, Error>> => {
217
+ const currentMap = deriveTopoGraph(app);
218
+ const previous = await readAgainstTopoGraph(rootDir, against);
219
+ if (previous.isErr()) {
220
+ return previous;
62
221
  }
63
222
 
64
- const diff = diffTrailheadMaps(previousMap, currentMap);
223
+ const diff = deriveTopoGraphDiff(previous.value.map, currentMap);
65
224
  return Result.ok(
66
225
  breakingOnly
67
- ? formatDiff({
68
- ...diff,
69
- entries: diff.breaking,
70
- info: [],
71
- warnings: [],
72
- })
73
- : formatDiff(diff)
226
+ ? formatDiff({ ...diff, info: [], warnings: [] }, previous.value.against)
227
+ : formatDiff(diff, previous.value.against)
74
228
  );
75
229
  };
76
230
 
77
- const buildSurveyDetail = (
231
+ const buildSurveyLookup = (
232
+ app: Topo,
233
+ entityId: string,
234
+ rootDir: string,
235
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
236
+ ): Result<object, Error> => {
237
+ const matches = buildCurrentTopoMatches(app, entityId, {
238
+ rootDir,
239
+ surfaceLayerNames,
240
+ });
241
+ return Result.ok({ matches });
242
+ };
243
+
244
+ const buildSurveyTrailDetail = (
245
+ app: Topo,
246
+ id: string,
247
+ rootDir: string,
248
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
249
+ ): Result<object, Error> => {
250
+ const detail = buildCurrentTrailDetail(app, id, {
251
+ rootDir,
252
+ surfaceLayerNames,
253
+ });
254
+ return detail === undefined
255
+ ? Result.err(new NotFoundError(`Trail not found: ${id}`))
256
+ : Result.ok(detail);
257
+ };
258
+
259
+ const buildSurveyResourceDetail = (
78
260
  app: Topo,
79
- trailId: string,
261
+ id: string,
80
262
  rootDir: string
81
263
  ): Result<object, Error> => {
82
- const detail = buildCurrentTopoDetail(app, trailId, { rootDir });
83
- if (detail !== undefined) {
84
- return Result.ok(detail);
85
- }
86
- return Result.err(
87
- new NotFoundError(`Trail or provision not found: ${trailId}`)
88
- );
264
+ const detail = buildCurrentResourceDetail(app, id, { rootDir });
265
+ return detail === undefined
266
+ ? Result.err(new NotFoundError(`Resource not found: ${id}`))
267
+ : Result.ok(detail);
89
268
  };
90
269
 
91
- const buildSurveyGenerate = async (
270
+ const buildSurveySignalDetail = (
92
271
  app: Topo,
272
+ id: string,
93
273
  rootDir: string
94
- ): Promise<Result<object, Error>> => {
95
- const exported = await exportCurrentTopo(app, { rootDir });
96
- if (exported.isErr()) {
97
- return exported;
98
- }
99
- return Result.ok({
100
- hash: exported.value.hash,
101
- lockPath: exported.value.lockPath,
102
- mapPath: exported.value.mapPath,
103
- });
274
+ ): Result<object, Error> => {
275
+ const detail = buildCurrentSignalDetail(app, id, { rootDir });
276
+ return detail === undefined
277
+ ? Result.err(new NotFoundError(`Signal not found: ${id}`))
278
+ : Result.ok(detail);
104
279
  };
105
280
 
106
281
  interface SurveyInput {
107
- breakingOnly: boolean;
108
- brief: boolean;
109
- diff?: string | undefined;
110
- generate: boolean;
111
- openapi: boolean;
112
- trailId?: string | undefined;
282
+ id?: string | undefined;
283
+ module?: string | undefined;
284
+ rootDir?: string | undefined;
113
285
  }
114
286
 
115
- type SurveyMode = 'brief' | 'detail' | 'diff' | 'generate' | 'list' | 'openapi';
287
+ type SurveyMode = 'lookup' | 'overview';
116
288
 
117
- /** Ordered mode checks first truthy predicate wins, otherwise 'list'. */
118
- const modeChecks: readonly [(input: SurveyInput) => boolean, SurveyMode][] = [
119
- [(i) => i.brief, 'brief'],
120
- [(i) => Boolean(i.diff), 'diff'],
121
- [(i) => Boolean(i.trailId), 'detail'],
122
- [(i) => i.generate, 'generate'],
123
- [(i) => i.openapi, 'openapi'],
124
- ];
289
+ type SurveyEnvelope = { readonly mode: SurveyMode } & Record<string, unknown>;
125
290
 
126
- /** Determine which survey mode was requested, falling back to 'list'. */
127
- const resolveSurveyMode = (input: SurveyInput): SurveyMode =>
128
- modeChecks.find(([predicate]) => predicate(input))?.[1] ?? 'list';
291
+ /** Determine which survey mode was requested, falling back to 'overview'. */
292
+ const deriveSurveyMode = (input: SurveyInput): SurveyMode =>
293
+ input.id === undefined || input.id === '' ? 'overview' : 'lookup';
129
294
 
130
295
  type SurveyHandler = (
131
296
  app: Topo,
132
297
  input: SurveyInput,
133
- rootDir: string
298
+ rootDir: string,
299
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
134
300
  ) => Result<object, Error> | Promise<Result<object, Error>>;
135
301
 
136
302
  /** Handlers keyed by survey mode. */
137
303
  const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
138
- brief: (app, _input, rootDir) =>
139
- Result.ok(buildCurrentTopoBrief(app, { rootDir })),
140
- detail: (app, input, rootDir) =>
141
- buildSurveyDetail(app, input.trailId ?? '', rootDir),
142
- diff: (app, input) => buildSurveyDiff(app, input.breakingOnly),
143
- generate: (app, _input, rootDir) => buildSurveyGenerate(app, rootDir),
144
- list: (app, _input, rootDir) =>
304
+ lookup: (app, input, rootDir, surfaceLayerNames) =>
305
+ input.id === undefined || input.id === ''
306
+ ? Result.err(new ValidationError('Survey lookup requires an id'))
307
+ : buildSurveyLookup(app, input.id, rootDir, surfaceLayerNames),
308
+ overview: (app, _input, rootDir) =>
145
309
  Result.ok(buildCurrentTopoList(app, { rootDir })),
146
- openapi: (app) => Result.ok(generateOpenApiSpec(app)),
147
310
  };
148
311
 
312
+ const envelopeSurveyValue = (
313
+ mode: SurveyMode,
314
+ value: object
315
+ ): SurveyEnvelope => ({ ...value, mode });
316
+
149
317
  /** Dispatch to the appropriate survey sub-command based on input flags. */
150
- const dispatchSurvey = (
318
+ const dispatchSurvey = async (
151
319
  app: Topo,
152
320
  input: SurveyInput,
153
- rootDir: string
154
- ): Result<object, Error> | Promise<Result<object, Error>> => {
155
- const mode = resolveSurveyMode(input);
321
+ rootDir: string,
322
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
323
+ ): Promise<Result<SurveyEnvelope, Error>> => {
324
+ const mode = deriveSurveyMode(input);
156
325
  const handler = surveyHandlers[mode];
157
- return handler(app, input, rootDir);
326
+ const result = await handler(app, input, rootDir, surfaceLayerNames);
327
+ if (result.isErr()) {
328
+ return result;
329
+ }
330
+ return Result.ok(envelopeSurveyValue(mode, result.value));
331
+ };
332
+
333
+ const detailInputSchema = z.object({
334
+ id: z.string().describe('Trail, resource, or signal ID'),
335
+ module: z.string().optional().describe('Path to the app module'),
336
+ rootDir: z.string().optional().describe('Workspace root directory'),
337
+ });
338
+
339
+ const withFreshSurveyApp = async <T>(
340
+ input: { readonly module?: string | undefined },
341
+ rootDir: string,
342
+ consume: (app: Topo) => Promise<Result<T, Error>> | Result<T, Error>
343
+ ): Promise<Result<T, Error>> => {
344
+ const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
345
+ if (leaseResult.isErr()) {
346
+ return Result.err(leaseResult.error);
347
+ }
348
+ const lease = leaseResult.value;
349
+ try {
350
+ return await consume(lease.app);
351
+ } finally {
352
+ lease.release();
353
+ }
158
354
  };
159
355
 
356
+ const withResolvedSurveyApp = async <T>(
357
+ input: {
358
+ readonly module?: string | undefined;
359
+ readonly rootDir?: string | undefined;
360
+ },
361
+ cwd: string | undefined,
362
+ consume: (
363
+ app: Topo,
364
+ rootDir: string
365
+ ) => Promise<Result<T, Error>> | Result<T, Error>
366
+ ): Promise<Result<T, Error>> => {
367
+ const rootDirResult = resolveTrailRootDir(input.rootDir, cwd);
368
+ if (rootDirResult.isErr()) {
369
+ return Result.err(rootDirResult.error);
370
+ }
371
+ const rootDir = rootDirResult.value;
372
+ return withFreshSurveyApp(input, rootDir, (app) => consume(app, rootDir));
373
+ };
374
+
375
+ const moduleInputSchema = z.object({
376
+ module: z.string().optional().describe('Path to the app module'),
377
+ rootDir: z.string().optional().describe('Workspace root directory'),
378
+ });
379
+
380
+ const diffEntryOutput = z.object({
381
+ change: z.enum(['added', 'removed', 'modified']),
382
+ details: z.array(z.string()).readonly(),
383
+ id: z.string(),
384
+ kind: z.enum(['contour', 'trail', 'signal', 'resource']),
385
+ severity: z.enum(['info', 'warning', 'breaking']),
386
+ });
387
+
388
+ const diffOutput = z.object({
389
+ against: z.string(),
390
+ breaking: z.array(diffEntryOutput),
391
+ hasBreaking: z.boolean(),
392
+ info: z.array(diffEntryOutput),
393
+ mode: z.literal('diff'),
394
+ warnings: z.array(diffEntryOutput),
395
+ });
396
+
397
+ const surveyMatchOutput = z.discriminatedUnion('kind', [
398
+ z.object({
399
+ detail: trailDetailOutput,
400
+ kind: z.literal('trail'),
401
+ }),
402
+ z.object({
403
+ detail: resourceDetailOutput,
404
+ kind: z.literal('resource'),
405
+ }),
406
+ z.object({
407
+ detail: signalDetailOutput,
408
+ kind: z.literal('signal'),
409
+ }),
410
+ ]);
411
+
160
412
  // ---------------------------------------------------------------------------
161
413
  // Trail definition
162
414
  // ---------------------------------------------------------------------------
163
415
 
164
416
  export const surveyTrail = trail('survey', {
165
- blaze: async (input, ctx) => {
166
- const rootDir = ctx.cwd ?? '.';
167
- const app = await loadApp(input.module, rootDir);
168
- return dispatchSurvey(app, input, rootDir);
169
- },
417
+ args: ['id'],
418
+ blaze: async (input, ctx) =>
419
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
420
+ dispatchSurvey(app, input, rootDir, readSurfaceLayerNamesFromContext(ctx))
421
+ ),
170
422
  description: 'Full topo introspection',
171
423
  examples: [
172
424
  {
173
- description: 'Lists all registered trails with safety and trailhead info',
174
- input: { module: './src/app.ts' },
175
- name: 'List all trails',
425
+ description: 'Show all registered trails, resources, and signals',
426
+ input: createIsolatedExampleInput('survey-overview'),
427
+ name: 'Overview',
176
428
  },
177
429
  {
178
- description: 'Quick capability summary with counts and feature flags',
179
- input: { brief: true, module: './src/app.ts' },
180
- name: 'Brief capability report',
181
- },
182
- {
183
- description: 'Generate an OpenAPI 3.1 specification for the topo',
184
- input: { module: './src/app.ts', openapi: true },
185
- name: 'OpenAPI spec',
430
+ description: 'Find every trail, resource, or signal with a matching ID',
431
+ input: { ...createIsolatedExampleInput('survey-lookup'), id: 'survey' },
432
+ name: 'Lookup by ID',
186
433
  },
187
434
  ],
188
435
  input: z.object({
189
- breakingOnly: z
190
- .boolean()
191
- .default(false)
192
- .describe('Only show breaking changes'),
193
- brief: z.boolean().default(false).describe('Quick capability summary'),
194
- diff: z.string().optional().describe('Diff against a git ref'),
195
- generate: z
196
- .boolean()
197
- .default(false)
198
- .describe('Generate trailhead map and lock file'),
199
- module: z
436
+ id: z
200
437
  .string()
201
- .default('./src/app.ts')
202
- .describe('Path to the app module'),
203
- openapi: z.boolean().default(false).describe('Output OpenAPI 3.1 spec'),
204
- trailId: z.string().optional().describe('Trail ID for detail view'),
438
+ .optional()
439
+ .describe('Trail, resource, or signal ID to look up'),
440
+ module: z.string().optional().describe('Path to the app module'),
441
+ rootDir: z.string().optional().describe('Workspace root directory'),
205
442
  }),
206
443
  intent: 'read',
207
- output: z.union([
444
+ output: z.discriminatedUnion('mode', [
208
445
  z.object({
446
+ activation: activationOverviewOutput,
209
447
  count: z.number(),
210
448
  entries: z.array(
211
449
  z.object({
450
+ activatedBy: z.array(z.string()).readonly(),
451
+ activates: z.array(z.string()).readonly(),
212
452
  examples: z.number(),
213
453
  id: z.string(),
214
454
  kind: z.string(),
215
455
  safety: z.string(),
216
456
  })
217
457
  ),
218
- provisionCount: z.number(),
219
- provisions: z.array(
458
+ mode: z.literal('overview'),
459
+ resourceCount: z.number(),
460
+ resources: z.array(
220
461
  z.object({
221
462
  description: z.string().nullable(),
222
463
  health: z.enum(['available', 'none']),
223
464
  id: z.string(),
224
- kind: z.literal('provision'),
465
+ kind: z.literal('resource'),
225
466
  lifetime: z.literal('singleton'),
226
467
  usedBy: z.array(z.string()),
227
468
  })
228
469
  ),
470
+ signalCount: z.number(),
471
+ signals: z.array(
472
+ z.object({
473
+ consumers: z.array(z.string()).readonly(),
474
+ description: z.string().nullable(),
475
+ examples: z.number(),
476
+ from: z.array(z.string()).readonly(),
477
+ id: z.string(),
478
+ kind: z.literal('signal'),
479
+ payloadSchema: z.boolean(),
480
+ producers: z.array(z.string()).readonly(),
481
+ })
482
+ ),
229
483
  }),
230
484
  z.object({
231
- contractVersion: z.string(),
232
- features: z.object({
233
- detours: z.boolean(),
234
- examples: z.boolean(),
235
- outputSchemas: z.boolean(),
236
- provisions: z.boolean(),
237
- signals: z.boolean(),
238
- }),
239
- name: z.string(),
240
- provisions: z.number(),
241
- signals: z.number(),
242
- trails: z.number(),
243
- version: z.string(),
244
- }),
245
- z.object({
246
- breaking: z.array(z.unknown()),
247
- hasBreaking: z.boolean(),
248
- info: z.array(z.unknown()),
249
- warnings: z.array(z.unknown()),
250
- }),
251
- z.object({
252
- crosses: z.array(z.string()),
253
- description: z.unknown().nullable(),
254
- detours: z.unknown().nullable(),
255
- examples: z.array(z.unknown()),
256
- id: z.string(),
257
- intent: z.enum(['read', 'write', 'destroy']),
258
- kind: z.string(),
259
- provisions: z.array(z.string()),
260
- safety: z.string(),
261
- }),
262
- z.object({
263
- description: z.string().nullable(),
264
- health: z.enum(['available', 'none']),
265
- id: z.string(),
266
- kind: z.literal('provision'),
267
- lifetime: z.literal('singleton'),
268
- usedBy: z.array(z.string()),
269
- }),
270
- z.object({
271
- hash: z.string(),
272
- lockPath: z.string(),
273
- mapPath: z.string(),
274
- }),
275
- z.object({
276
- components: z.object({
277
- schemas: z.record(z.string(), z.unknown()),
278
- }),
279
- info: z.object({
280
- description: z.string().optional(),
281
- title: z.string(),
282
- version: z.string(),
283
- }),
284
- openapi: z.literal('3.1.0'),
285
- paths: z.record(z.string(), z.record(z.string(), z.unknown())),
286
- servers: z
287
- .array(
288
- z.object({
289
- description: z.string().optional(),
290
- url: z.string(),
291
- })
292
- )
293
- .optional(),
485
+ matches: z.array(surveyMatchOutput),
486
+ mode: z.literal('lookup'),
294
487
  }),
295
488
  ]),
296
489
  });
490
+
491
+ export const surveyBriefTrail = trail('survey.brief', {
492
+ blaze: async (input, ctx) =>
493
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
494
+ Result.ok(buildCurrentTopoBrief(app, { rootDir }))
495
+ ),
496
+ description: 'Summarize topo capabilities',
497
+ examples: [
498
+ {
499
+ description: 'Show counts and feature flags',
500
+ input: createIsolatedExampleInput('survey-brief'),
501
+ name: 'Brief capability report',
502
+ },
503
+ ],
504
+ input: moduleInputSchema,
505
+ intent: 'read',
506
+ output: briefReportSchema,
507
+ });
508
+
509
+ export const surveyDiffTrail = trail('survey.diff', {
510
+ blaze: async (input, ctx) =>
511
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
512
+ buildSurveyDiff(app, rootDir, input.breakingOnly, input.against)
513
+ ),
514
+ description: 'Diff the current topo against a saved TopoGraph',
515
+ examples: [
516
+ {
517
+ description: 'Compare current topo to a saved TopoGraph directory',
518
+ input: createDiffExampleInput(),
519
+ name: 'Diff against baseline',
520
+ },
521
+ {
522
+ description: 'Reject an empty saved map target',
523
+ error: 'ValidationError',
524
+ input: { against: '' },
525
+ name: 'Reject empty diff target',
526
+ },
527
+ {
528
+ description: 'Reject an empty target before filtering breaking drift',
529
+ error: 'ValidationError',
530
+ input: {
531
+ against: '',
532
+ breakingOnly: true,
533
+ },
534
+ name: 'Reject empty breaking-only target',
535
+ },
536
+ ],
537
+ input: z.object({
538
+ against: z
539
+ .string()
540
+ .min(1)
541
+ .optional()
542
+ .describe(
543
+ 'Saved TopoGraph target: "saved", a workspace path (topo.lock, .json file, or directory with topo.lock), then a pin/snapshot id'
544
+ ),
545
+ breakingOnly: z
546
+ .boolean()
547
+ .default(false)
548
+ .describe('Only show breaking changes'),
549
+ module: z.string().optional().describe('Path to the app module'),
550
+ rootDir: z.string().optional().describe('Workspace root directory'),
551
+ }),
552
+ intent: 'read',
553
+ output: diffOutput,
554
+ });
555
+
556
+ export const surveyTrailDetailTrail = trail('survey.trail', {
557
+ args: ['id'],
558
+ blaze: async (input, ctx) =>
559
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
560
+ buildSurveyTrailDetail(
561
+ app,
562
+ input.id,
563
+ rootDir,
564
+ readSurfaceLayerNamesFromContext(ctx)
565
+ )
566
+ ),
567
+ description: 'Inspect one trail by ID',
568
+ examples: [
569
+ {
570
+ description: 'Show trail contract detail',
571
+ input: {
572
+ ...createIsolatedExampleInput('survey-trail-detail'),
573
+ id: 'survey',
574
+ },
575
+ name: 'Trail detail',
576
+ },
577
+ ],
578
+ input: detailInputSchema,
579
+ intent: 'read',
580
+ output: trailDetailOutput,
581
+ });
582
+
583
+ export const surveyResourceTrail = trail('survey.resource', {
584
+ args: ['id'],
585
+ blaze: async (input, ctx) =>
586
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
587
+ buildSurveyResourceDetail(app, input.id, rootDir)
588
+ ),
589
+ description: 'Inspect one resource by ID',
590
+ examples: [
591
+ {
592
+ description: 'Show resource usage detail',
593
+ error: 'NotFoundError',
594
+ input: {
595
+ ...createIsolatedExampleInput('survey-resource-detail'),
596
+ id: 'db.main',
597
+ },
598
+ name: 'Resource detail',
599
+ },
600
+ ],
601
+ input: detailInputSchema,
602
+ intent: 'read',
603
+ output: resourceDetailOutput,
604
+ });
605
+
606
+ export const surveySignalTrail = trail('survey.signal', {
607
+ args: ['id'],
608
+ blaze: async (input, ctx) =>
609
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
610
+ buildSurveySignalDetail(app, input.id, rootDir)
611
+ ),
612
+ description: 'Inspect one signal by ID',
613
+ examples: [
614
+ {
615
+ description: 'Show signal producer and consumer detail',
616
+ error: 'NotFoundError',
617
+ input: {
618
+ ...createIsolatedExampleInput('survey-signal-detail'),
619
+ id: 'hello.greeted',
620
+ },
621
+ name: 'Signal detail',
622
+ },
623
+ ],
624
+ input: detailInputSchema,
625
+ intent: 'read',
626
+ output: signalDetailOutput,
627
+ });