@ontrails/trails 1.0.0-beta.15 → 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 (201) hide show
  1. package/CHANGELOG.md +197 -2
  2. package/README.md +27 -0
  3. package/package.json +19 -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 +506 -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 +175 -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 +400 -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,461 @@
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
+ 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';
28
50
 
29
51
  export {
52
+ briefReportSchema,
30
53
  deriveBriefReport,
31
54
  deriveResourceDetail,
55
+ deriveSignalDetail,
32
56
  deriveSurveyList,
33
57
  deriveTrailDetail,
34
58
  } from './topo-reports.js';
35
59
  export type {
36
60
  BriefReport,
61
+ SignalDetailReport,
62
+ SurfaceLayerNames,
37
63
  SurveyListReport,
38
64
  TrailDetailReport,
39
65
  } from './topo-reports.js';
40
66
 
41
67
  // ---------------------------------------------------------------------------
42
- // Brief report (formerly scout)
68
+ // Survey diff helpers
43
69
  // ---------------------------------------------------------------------------
44
70
 
45
- 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,
46
82
  breaking: diff.breaking,
47
83
  hasBreaking: diff.hasBreaking,
48
84
  info: diff.info,
85
+ mode: 'diff',
49
86
  warnings: diff.warnings,
50
87
  });
51
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
+
52
211
  const buildSurveyDiff = async (
53
212
  app: Topo,
54
213
  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
- );
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;
65
221
  }
66
222
 
67
- const diff = deriveSurfaceMapDiff(previousMap, currentMap);
223
+ const diff = deriveTopoGraphDiff(previous.value.map, currentMap);
68
224
  return Result.ok(
69
225
  breakingOnly
70
- ? formatDiff({
71
- ...diff,
72
- entries: diff.breaking,
73
- info: [],
74
- warnings: [],
75
- })
76
- : formatDiff(diff)
226
+ ? formatDiff({ ...diff, info: [], warnings: [] }, previous.value.against)
227
+ : formatDiff(diff, previous.value.against)
77
228
  );
78
229
  };
79
230
 
80
- const buildSurveyDetail = (
231
+ const buildSurveyLookup = (
81
232
  app: Topo,
82
- trailId: string,
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 = (
260
+ app: Topo,
261
+ id: string,
83
262
  rootDir: string
84
263
  ): 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
- );
264
+ const detail = buildCurrentResourceDetail(app, id, { rootDir });
265
+ return detail === undefined
266
+ ? Result.err(new NotFoundError(`Resource not found: ${id}`))
267
+ : Result.ok(detail);
92
268
  };
93
269
 
94
- const buildSurveyGenerate = async (
270
+ const buildSurveySignalDetail = (
95
271
  app: Topo,
272
+ id: string,
96
273
  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
- });
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);
107
279
  };
108
280
 
109
281
  interface SurveyInput {
110
- breakingOnly: boolean;
111
- brief: boolean;
112
- diffSaved: boolean;
113
- generate: boolean;
114
- openapi: boolean;
115
- trailId?: string | undefined;
282
+ id?: string | undefined;
283
+ module?: string | undefined;
284
+ rootDir?: string | undefined;
116
285
  }
117
286
 
118
- type SurveyMode = 'brief' | 'detail' | 'diff' | 'generate' | 'list' | 'openapi';
287
+ type SurveyMode = 'lookup' | 'overview';
119
288
 
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
- ];
289
+ type SurveyEnvelope = { readonly mode: SurveyMode } & Record<string, unknown>;
128
290
 
129
- /** Determine which survey mode was requested, falling back to 'list'. */
291
+ /** Determine which survey mode was requested, falling back to 'overview'. */
130
292
  const deriveSurveyMode = (input: SurveyInput): SurveyMode =>
131
- modeChecks.find(([predicate]) => predicate(input))?.[1] ?? 'list';
293
+ input.id === undefined || input.id === '' ? 'overview' : 'lookup';
132
294
 
133
295
  type SurveyHandler = (
134
296
  app: Topo,
135
297
  input: SurveyInput,
136
- rootDir: string
298
+ rootDir: string,
299
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
137
300
  ) => Result<object, Error> | Promise<Result<object, Error>>;
138
301
 
139
302
  /** Handlers keyed by survey mode. */
140
303
  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) =>
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) =>
149
309
  Result.ok(buildCurrentTopoList(app, { rootDir })),
150
- openapi: (app) => Result.ok(deriveOpenApiSpec(app)),
151
310
  };
152
311
 
312
+ const envelopeSurveyValue = (
313
+ mode: SurveyMode,
314
+ value: object
315
+ ): SurveyEnvelope => ({ ...value, mode });
316
+
153
317
  /** Dispatch to the appropriate survey sub-command based on input flags. */
154
- const dispatchSurvey = (
318
+ const dispatchSurvey = async (
155
319
  app: Topo,
156
320
  input: SurveyInput,
157
- rootDir: string
158
- ): Result<object, Error> | Promise<Result<object, Error>> => {
321
+ rootDir: string,
322
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
323
+ ): Promise<Result<SurveyEnvelope, Error>> => {
159
324
  const mode = deriveSurveyMode(input);
160
325
  const handler = surveyHandlers[mode];
161
- 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
+ }
354
+ };
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));
162
373
  };
163
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
+
164
412
  // ---------------------------------------------------------------------------
165
413
  // Trail definition
166
414
  // ---------------------------------------------------------------------------
167
415
 
168
416
  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
- },
417
+ args: ['id'],
418
+ blaze: async (input, ctx) =>
419
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
420
+ dispatchSurvey(app, input, rootDir, readSurfaceLayerNamesFromContext(ctx))
421
+ ),
195
422
  description: 'Full topo introspection',
196
423
  examples: [
197
424
  {
198
- description: 'Lists all registered trails with safety and surface info',
199
- input: { module: './src/app.ts' },
200
- name: 'List all trails',
425
+ description: 'Show all registered trails, resources, and signals',
426
+ input: createIsolatedExampleInput('survey-overview'),
427
+ name: 'Overview',
201
428
  },
202
429
  {
203
- description: 'Quick capability summary with counts and feature flags',
204
- input: { brief: true, module: './src/app.ts' },
205
- name: 'Brief capability report',
206
- },
207
- {
208
- description: 'Generate an OpenAPI 3.1 specification for the topo',
209
- input: { module: './src/app.ts', openapi: true },
210
- 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',
211
433
  },
212
434
  ],
213
435
  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'),
436
+ id: z
437
+ .string()
438
+ .optional()
439
+ .describe('Trail, resource, or signal ID to look up'),
227
440
  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'),
441
+ rootDir: z.string().optional().describe('Workspace root directory'),
230
442
  }),
231
443
  intent: 'read',
232
- output: z.union([
444
+ output: z.discriminatedUnion('mode', [
233
445
  z.object({
446
+ activation: activationOverviewOutput,
234
447
  count: z.number(),
235
448
  entries: z.array(
236
449
  z.object({
450
+ activatedBy: z.array(z.string()).readonly(),
451
+ activates: z.array(z.string()).readonly(),
237
452
  examples: z.number(),
238
453
  id: z.string(),
239
454
  kind: z.string(),
240
455
  safety: z.string(),
241
456
  })
242
457
  ),
458
+ mode: z.literal('overview'),
243
459
  resourceCount: z.number(),
244
460
  resources: z.array(
245
461
  z.object({
@@ -251,71 +467,161 @@ export const surveyTrail = trail('survey', {
251
467
  usedBy: z.array(z.string()),
252
468
  })
253
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
+ ),
254
483
  }),
255
484
  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(),
485
+ matches: z.array(surveyMatchOutput),
486
+ mode: z.literal('lookup'),
319
487
  }),
320
488
  ]),
321
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
+ });