@ontrails/trails 1.0.0-beta.15 → 1.0.0-beta.17

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