@ontrails/trails 1.0.0-beta.3 → 1.0.0-beta.32

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 (158) hide show
  1. package/CHANGELOG.md +975 -0
  2. package/README.md +27 -0
  3. package/package.json +30 -7
  4. package/src/app.ts +125 -2
  5. package/src/cli.ts +345 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +40 -0
  11. package/src/mcp-options.ts +77 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/release/bindings.ts +39 -0
  16. package/src/release/check.ts +844 -0
  17. package/src/release/config.ts +63 -0
  18. package/src/release/contract-facts.ts +425 -0
  19. package/src/release/index.ts +125 -0
  20. package/src/release/native-bun-publish.ts +651 -0
  21. package/src/release/native-bun-registry.ts +754 -0
  22. package/src/release/pack-coherence.ts +455 -0
  23. package/src/release/packed-artifacts-smoke.ts +236 -0
  24. package/src/release/policy.ts +1684 -0
  25. package/src/release/semver.ts +104 -0
  26. package/src/release/smoke.ts +46 -0
  27. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  28. package/src/retired-topo-command.ts +36 -0
  29. package/src/run-adapter-check.ts +76 -0
  30. package/src/run-collision.ts +126 -0
  31. package/src/run-completions-install.ts +179 -0
  32. package/src/run-example.ts +149 -0
  33. package/src/run-examples.ts +148 -0
  34. package/src/run-quiet.ts +75 -0
  35. package/src/run-release-check.ts +74 -0
  36. package/src/run-schema.ts +41 -0
  37. package/src/run-trace.ts +273 -0
  38. package/src/run-warden.ts +39 -0
  39. package/src/run-watch.ts +432 -0
  40. package/src/run-wayfind-outline.ts +166 -0
  41. package/src/scaffold-version-sync.ts +183 -0
  42. package/src/scaffold-versions.generated.ts +12 -0
  43. package/src/trails/adapter-check.ts +244 -0
  44. package/src/trails/add-surface.ts +94 -40
  45. package/src/trails/add-trail.ts +79 -41
  46. package/src/trails/add-verify.ts +95 -25
  47. package/src/trails/compile.ts +59 -0
  48. package/src/trails/completions-complete.ts +165 -0
  49. package/src/trails/completions.ts +47 -0
  50. package/src/trails/create-adapter.ts +1024 -0
  51. package/src/trails/create-scaffold.ts +399 -104
  52. package/src/trails/create-versions.ts +62 -0
  53. package/src/trails/create.ts +186 -72
  54. package/src/trails/deprecate.ts +59 -0
  55. package/src/trails/dev-clean.ts +82 -0
  56. package/src/trails/dev-reset.ts +50 -0
  57. package/src/trails/dev-stats.ts +72 -0
  58. package/src/trails/dev-support.ts +360 -0
  59. package/src/trails/doctor.ts +77 -0
  60. package/src/trails/draft-promote.ts +949 -0
  61. package/src/trails/guide.ts +71 -69
  62. package/src/trails/load-app.ts +1142 -16
  63. package/src/trails/operator-context.ts +66 -0
  64. package/src/trails/project.ts +17 -3
  65. package/src/trails/regrade.ts +292 -0
  66. package/src/trails/release-check.ts +105 -0
  67. package/src/trails/release-smoke.ts +48 -0
  68. package/src/trails/revise.ts +53 -0
  69. package/src/trails/root-dir.ts +21 -0
  70. package/src/trails/run-example.ts +475 -0
  71. package/src/trails/run-examples.ts +129 -0
  72. package/src/trails/run.ts +434 -0
  73. package/src/trails/scaffold-json.ts +58 -0
  74. package/src/trails/survey.ts +896 -227
  75. package/src/trails/topo-activation.ts +14 -0
  76. package/src/trails/topo-constants.ts +2 -0
  77. package/src/trails/topo-history.ts +47 -0
  78. package/src/trails/topo-output-schemas.ts +259 -0
  79. package/src/trails/topo-pin.ts +38 -0
  80. package/src/trails/topo-read-support.ts +363 -0
  81. package/src/trails/topo-reports.ts +809 -0
  82. package/src/trails/topo-store-support.ts +307 -0
  83. package/src/trails/topo-support.ts +219 -0
  84. package/src/trails/topo-unpin.ts +61 -0
  85. package/src/trails/topo.ts +92 -0
  86. package/src/trails/validate.ts +27 -0
  87. package/src/trails/version-lifecycle-support.ts +935 -0
  88. package/src/trails/warden-guide.ts +134 -0
  89. package/src/trails/warden.ts +198 -58
  90. package/src/trails/wayfind.ts +1001 -0
  91. package/src/versions.ts +31 -0
  92. package/.turbo/turbo-build.log +0 -1
  93. package/.turbo/turbo-lint.log +0 -3
  94. package/.turbo/turbo-typecheck.log +0 -1
  95. package/__tests__/examples.test.ts +0 -6
  96. package/dist/bin/trails.d.ts +0 -3
  97. package/dist/bin/trails.d.ts.map +0 -1
  98. package/dist/bin/trails.js +0 -4
  99. package/dist/bin/trails.js.map +0 -1
  100. package/dist/src/app.d.ts +0 -2
  101. package/dist/src/app.d.ts.map +0 -1
  102. package/dist/src/app.js +0 -11
  103. package/dist/src/app.js.map +0 -1
  104. package/dist/src/clack.d.ts +0 -9
  105. package/dist/src/clack.d.ts.map +0 -1
  106. package/dist/src/clack.js +0 -84
  107. package/dist/src/clack.js.map +0 -1
  108. package/dist/src/cli.d.ts +0 -2
  109. package/dist/src/cli.d.ts.map +0 -1
  110. package/dist/src/cli.js +0 -13
  111. package/dist/src/cli.js.map +0 -1
  112. package/dist/src/trails/add-surface.d.ts +0 -13
  113. package/dist/src/trails/add-surface.d.ts.map +0 -1
  114. package/dist/src/trails/add-surface.js +0 -88
  115. package/dist/src/trails/add-surface.js.map +0 -1
  116. package/dist/src/trails/add-trail.d.ts +0 -11
  117. package/dist/src/trails/add-trail.d.ts.map +0 -1
  118. package/dist/src/trails/add-trail.js +0 -85
  119. package/dist/src/trails/add-trail.js.map +0 -1
  120. package/dist/src/trails/add-verify.d.ts +0 -10
  121. package/dist/src/trails/add-verify.d.ts.map +0 -1
  122. package/dist/src/trails/add-verify.js +0 -67
  123. package/dist/src/trails/add-verify.js.map +0 -1
  124. package/dist/src/trails/create-scaffold.d.ts +0 -15
  125. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  126. package/dist/src/trails/create-scaffold.js +0 -288
  127. package/dist/src/trails/create-scaffold.js.map +0 -1
  128. package/dist/src/trails/create.d.ts +0 -22
  129. package/dist/src/trails/create.d.ts.map +0 -1
  130. package/dist/src/trails/create.js +0 -121
  131. package/dist/src/trails/create.js.map +0 -1
  132. package/dist/src/trails/guide.d.ts +0 -11
  133. package/dist/src/trails/guide.d.ts.map +0 -1
  134. package/dist/src/trails/guide.js +0 -80
  135. package/dist/src/trails/guide.js.map +0 -1
  136. package/dist/src/trails/load-app.d.ts +0 -4
  137. package/dist/src/trails/load-app.d.ts.map +0 -1
  138. package/dist/src/trails/load-app.js +0 -24
  139. package/dist/src/trails/load-app.js.map +0 -1
  140. package/dist/src/trails/project.d.ts +0 -8
  141. package/dist/src/trails/project.d.ts.map +0 -1
  142. package/dist/src/trails/project.js +0 -43
  143. package/dist/src/trails/project.js.map +0 -1
  144. package/dist/src/trails/survey.d.ts +0 -33
  145. package/dist/src/trails/survey.d.ts.map +0 -1
  146. package/dist/src/trails/survey.js +0 -225
  147. package/dist/src/trails/survey.js.map +0 -1
  148. package/dist/src/trails/warden.d.ts +0 -19
  149. package/dist/src/trails/warden.d.ts.map +0 -1
  150. package/dist/src/trails/warden.js +0 -88
  151. package/dist/src/trails/warden.js.map +0 -1
  152. package/dist/tsconfig.tsbuildinfo +0 -1
  153. package/src/__tests__/create.test.ts +0 -349
  154. package/src/__tests__/guide.test.ts +0 -91
  155. package/src/__tests__/load-app.test.ts +0 -15
  156. package/src/__tests__/survey.test.ts +0 -161
  157. package/src/__tests__/warden.test.ts +0 -74
  158. package/tsconfig.json +0 -9
@@ -1,307 +1,976 @@
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 type { Topo, Trail } from '@ontrails/core';
9
- import { Result, trail } from '@ontrails/core';
10
- import type { DiffResult } from '@ontrails/schema';
8
+ import { basename, extname, join } from 'node:path';
9
+
10
+ import type { CliCommandAliasInput, Topo } from '@ontrails/core';
11
+ import {
12
+ deriveSafePath,
13
+ NotFoundError,
14
+ Result,
15
+ trail,
16
+ ValidationError,
17
+ } from '@ontrails/core';
18
+ import type { DiffEntry, DiffResult, TopoGraph } from '@ontrails/topographer';
11
19
  import {
12
- diffSurfaceMaps,
13
- generateSurfaceMap,
14
- hashSurfaceMap,
15
- readSurfaceMap,
16
- writeSurfaceLock,
17
- writeSurfaceMap,
18
- } from '@ontrails/schema';
20
+ createTopoStore,
21
+ deriveTopoGraphDiff,
22
+ deriveTopoGraph,
23
+ resolveTopoGraphVersionReference,
24
+ TOPO_GRAPH_SCHEMA_VERSION,
25
+ readTopoGraph,
26
+ } from '@ontrails/topographer';
19
27
  import { z } from 'zod';
20
28
 
21
- import { loadApp } from './load-app.js';
29
+ import { writeIsolatedExampleJsonFile } from '../local-state-io.js';
30
+
31
+ import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
32
+ import {
33
+ deriveCurrentTopoBrief,
34
+ deriveCurrentTopoList,
35
+ deriveCurrentTopoMatches,
36
+ deriveCurrentTrailDetail,
37
+ deriveCurrentResourceDetail,
38
+ deriveCurrentSignalDetail,
39
+ readSurfaceLayerNamesFromContext,
40
+ } from './topo-read-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';
54
+
55
+ export {
56
+ briefReportSchema,
57
+ deriveBriefReport,
58
+ deriveResourceDetail,
59
+ deriveShippedSurfaceProjectionInventory,
60
+ deriveSignalDetail,
61
+ deriveSurveyList,
62
+ deriveTrailDetail,
63
+ } from './topo-reports.js';
64
+ export type {
65
+ BriefReport,
66
+ ShippedSurfaceInventoryReport,
67
+ ShippedSurfaceProjection,
68
+ SignalDetailReport,
69
+ SurfaceLayerNames,
70
+ SurveyListReport,
71
+ TrailDetailReport,
72
+ } from './topo-reports.js';
22
73
 
23
74
  // ---------------------------------------------------------------------------
24
- // Brief report (formerly scout)
75
+ // Survey diff helpers
25
76
  // ---------------------------------------------------------------------------
26
77
 
27
- export interface BriefReport {
28
- readonly name: string;
29
- readonly version: string;
30
- readonly contractVersion: string;
31
- readonly features: {
32
- readonly outputSchemas: boolean;
33
- readonly examples: boolean;
34
- readonly detours: boolean;
35
- readonly hikes: boolean;
36
- readonly events: boolean;
37
- };
38
- readonly trails: number;
39
- readonly hikes: number;
40
- readonly events: number;
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[];
41
85
  }
42
86
 
43
- /** Check if a trail has a specific feature. */
44
- const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
45
- if (key === 'examples') {
46
- return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
47
- }
48
- return Boolean(raw[key]);
49
- };
87
+ interface DiffInput {
88
+ readonly against?: string | undefined;
89
+ readonly breakingOnly?: boolean | undefined;
90
+ readonly breaks?: boolean | undefined;
91
+ readonly forces?: boolean | undefined;
92
+ readonly module?: string | undefined;
93
+ readonly rootDir?: string | undefined;
94
+ readonly target?: string | undefined;
95
+ }
96
+
97
+ interface ParsedDiffTarget {
98
+ readonly id: string;
99
+ readonly versions?: ReadonlySet<number> | undefined;
100
+ }
101
+
102
+ const formatDiff = (diff: DiffResult, against: string): SurveyDiffReport => ({
103
+ against,
104
+ breaking: diff.breaking,
105
+ hasBreaking: diff.hasBreaking,
106
+ info: diff.info,
107
+ mode: 'diff',
108
+ warnings: diff.warnings,
109
+ });
50
110
 
51
- /** Detect which features are used across trails. */
52
- const detectFeatures = (
53
- app: Topo
54
- ): { hasDetours: boolean; hasExamples: boolean; hasOutputSchemas: boolean } => {
55
- const trails = [...app.trails.values()].map(
56
- (item) => item as unknown as Record<string, unknown>
111
+ const partitionDiffEntries = (entries: readonly DiffEntry[]): DiffResult => {
112
+ const sorted = [...entries].toSorted((left, right) =>
113
+ left.id.localeCompare(right.id)
57
114
  );
115
+ const breaking = sorted.filter((entry) => entry.severity === 'breaking');
116
+ const warnings = sorted.filter((entry) => entry.severity === 'warning');
117
+ const info = sorted.filter((entry) => entry.severity === 'info');
118
+
58
119
  return {
59
- hasDetours: trails.some((r) => trailHas(r, 'detours')),
60
- hasExamples: trails.some((r) => trailHas(r, 'examples')),
61
- hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
120
+ breaking,
121
+ entries: sorted,
122
+ hasBreaking: breaking.length > 0,
123
+ info,
124
+ warnings,
62
125
  };
63
126
  };
64
127
 
65
- /** Generate a compact capability report for the given topo. */
66
- export const generateBriefReport = (app: Topo): BriefReport => {
67
- const { hasDetours, hasExamples, hasOutputSchemas } = detectFeatures(app);
128
+ const parseVersionRange = (
129
+ reference: string
130
+ ): ReadonlySet<number> | undefined => {
131
+ const match = /^(\d+)\.\.(\d+)$/.exec(reference);
132
+ if (match === null) {
133
+ return undefined;
134
+ }
135
+ const start = Number(match[1]);
136
+ const end = Number(match[2]);
137
+ if (start < 1 || end < start) {
138
+ throw new ValidationError(
139
+ `Diff version range must use ascending positive versions: ${reference}`
140
+ );
141
+ }
68
142
 
69
- return {
70
- contractVersion: '2026-03',
71
- events: app.events.size,
72
- features: {
73
- detours: hasDetours,
74
- events: app.events.size > 0,
75
- examples: hasExamples,
76
- hikes: app.hikes.size > 0,
77
- outputSchemas: hasOutputSchemas,
78
- },
79
- hikes: app.hikes.size,
80
- name: app.name,
81
- trails: app.trails.size,
82
- version: '0.1.0',
83
- };
143
+ return new Set(
144
+ Array.from({ length: end - start + 1 }, (_value, index) => start + index)
145
+ );
84
146
  };
85
147
 
86
- // ---------------------------------------------------------------------------
87
- // Formatting helpers
88
- // ---------------------------------------------------------------------------
148
+ const findDiffTargetEntry = (
149
+ previous: TopoGraph,
150
+ current: TopoGraph,
151
+ id: string
152
+ ) =>
153
+ current.entries.find((entry) => entry.id === id) ??
154
+ previous.entries.find((entry) => entry.id === id);
89
155
 
90
- const safetyLabel = (entry: {
91
- readOnly?: boolean;
92
- destructive?: boolean;
93
- }): string => {
94
- if (entry.destructive) {
95
- return 'destructive';
156
+ const parseDiffTarget = (
157
+ previous: TopoGraph,
158
+ current: TopoGraph,
159
+ target: string | undefined
160
+ ): Result<ParsedDiffTarget | undefined, Error> => {
161
+ if (target === undefined || target.length === 0) {
162
+ return Result.ok();
96
163
  }
97
- if (entry.readOnly) {
98
- return 'readOnly';
164
+
165
+ const separator = target.lastIndexOf('@');
166
+ const id = separator === -1 ? target : target.slice(0, separator);
167
+ const reference =
168
+ separator === -1 ? undefined : target.slice(separator + 1).trim();
169
+ if (id.length === 0 || reference === '') {
170
+ return Result.err(
171
+ new ValidationError('Diff target must use trail.id or trail.id@version')
172
+ );
99
173
  }
100
- return '-';
101
- };
102
174
 
103
- const formatTrailList = (app: Topo): object => {
104
- const items = app.list();
105
- const entries = items.map((item) => {
106
- const safety = safetyLabel(
107
- item as unknown as { readOnly?: boolean; destructive?: boolean }
175
+ const entry = findDiffTargetEntry(previous, current, id);
176
+ if (entry === undefined) {
177
+ return Result.err(new NotFoundError(`Trail not found for diff: ${id}`));
178
+ }
179
+
180
+ if (reference === undefined) {
181
+ return Result.ok({ id });
182
+ }
183
+
184
+ try {
185
+ const range = parseVersionRange(reference);
186
+ if (range !== undefined) {
187
+ return Result.ok({ id, versions: range });
188
+ }
189
+
190
+ return Result.ok({
191
+ id,
192
+ versions: new Set([
193
+ resolveTopoGraphVersionReference(entry, reference).version,
194
+ ]),
195
+ });
196
+ } catch (error: unknown) {
197
+ return Result.err(
198
+ error instanceof Error ? error : new Error(String(error))
108
199
  );
109
- const examples = Array.isArray(
110
- (item as unknown as { examples?: unknown[] }).examples
200
+ }
201
+ };
202
+
203
+ const detailVersions = (detail: string): readonly number[] => {
204
+ const match = /^(?:Live version|Version) (\d+)\b/.exec(detail);
205
+ if (match !== null) {
206
+ return [Number(match[1])];
207
+ }
208
+
209
+ const supportMatch = /^Supported versions (?:added|removed): (.+)$/.exec(
210
+ detail
211
+ );
212
+ if (supportMatch === null) {
213
+ return [];
214
+ }
215
+
216
+ return (supportMatch[1] ?? '')
217
+ .split(',')
218
+ .map((part) => Number(part.trim()))
219
+ .filter((version) => Number.isInteger(version) && version > 0);
220
+ };
221
+
222
+ type DiffSeverity = DiffEntry['severity'];
223
+
224
+ const severityRank: Record<DiffSeverity, number> = {
225
+ breaking: 2,
226
+ info: 0,
227
+ warning: 1,
228
+ };
229
+
230
+ const higherSeverity = (
231
+ left: DiffSeverity,
232
+ right: DiffSeverity
233
+ ): DiffSeverity => (severityRank[right] > severityRank[left] ? right : left);
234
+
235
+ const versionStatus = (detail: string): string | undefined =>
236
+ /^Version \d+ (?:added|removed) \(([^)]+)\)$/.exec(detail)?.[1];
237
+
238
+ const visibleDetailSeverity = (detail: string): DiffSeverity => {
239
+ if (detail.startsWith('Force event ')) {
240
+ return 'warning';
241
+ }
242
+ if (detail.startsWith('Supported versions removed: ')) {
243
+ return 'breaking';
244
+ }
245
+ if (detail.startsWith('Supported versions added: ')) {
246
+ return 'info';
247
+ }
248
+ if (
249
+ /^Live version \d+ (?:added without examples|example coverage removed)$/.test(
250
+ detail
111
251
  )
112
- ? (item as unknown as { examples: unknown[] }).examples.length
113
- : 0;
114
-
115
- return {
116
- examples,
117
- id: item.id,
118
- kind: item.kind,
119
- safety,
120
- };
252
+ ) {
253
+ return 'warning';
254
+ }
255
+ if (detail.startsWith('Live version ') && detail.includes(' examples: ')) {
256
+ return 'info';
257
+ }
258
+ if (/^Version \d+ status changed: .+ -> archived$/.test(detail)) {
259
+ return 'warning';
260
+ }
261
+ if (detail.startsWith('Version ') && detail.includes(' status changed: ')) {
262
+ return 'info';
263
+ }
264
+
265
+ const status = versionStatus(detail);
266
+ if (detail.startsWith('Version ') && detail.includes(' removed (')) {
267
+ return status === 'archived' ? 'warning' : 'breaking';
268
+ }
269
+ if (detail.startsWith('Version ') && detail.includes(' added (')) {
270
+ return status === 'archived' ? 'info' : 'warning';
271
+ }
272
+ if (
273
+ /^Version \d+ (?:kind changed:|Required (?:input|contour) field ".+" added|(?:Input|Output|Contour) field ".+" (?:removed|type changed:|changed from optional to required))/.test(
274
+ detail
275
+ )
276
+ ) {
277
+ return 'breaking';
278
+ }
279
+ if (
280
+ /^Version \d+ (?:marker changed:|Optional (?:input|contour) field ".+" added|Output field ".+" added)/.test(
281
+ detail
282
+ )
283
+ ) {
284
+ return 'info';
285
+ }
286
+
287
+ return 'info';
288
+ };
289
+
290
+ const visibleDetailsSeverity = (details: readonly string[]): DiffSeverity => {
291
+ let severity: DiffSeverity = 'info';
292
+ for (const detail of details) {
293
+ severity = higherSeverity(severity, visibleDetailSeverity(detail));
294
+ }
295
+ return severity;
296
+ };
297
+
298
+ const detailsChanged = (
299
+ previous: readonly string[],
300
+ next: readonly string[]
301
+ ): boolean =>
302
+ previous.length !== next.length ||
303
+ previous.some((detail, index) => detail !== next[index]);
304
+
305
+ const filterDetails = (
306
+ details: readonly string[],
307
+ target: ParsedDiffTarget | undefined,
308
+ forcesOnly: boolean
309
+ ): readonly string[] => {
310
+ const visible = forcesOnly
311
+ ? details.filter((detail) => detail.startsWith('Force event '))
312
+ : [...details];
313
+ if (target?.versions === undefined || forcesOnly) {
314
+ return visible;
315
+ }
316
+
317
+ return visible.filter((detail) => {
318
+ const versions = detailVersions(detail);
319
+ return versions.some((version) => target.versions?.has(version));
121
320
  });
321
+ };
122
322
 
123
- return { count: items.length, entries };
323
+ const filterDiff = (
324
+ diff: DiffResult,
325
+ target: ParsedDiffTarget | undefined,
326
+ options: Pick<DiffInput, 'breakingOnly' | 'breaks' | 'forces'>
327
+ ): DiffResult => {
328
+ const entries = diff.entries.flatMap((entry): DiffEntry[] => {
329
+ if (target !== undefined && entry.id !== target.id) {
330
+ return [];
331
+ }
332
+ const details = filterDetails(
333
+ entry.details,
334
+ target,
335
+ options.forces === true
336
+ );
337
+ if (details.length === 0) {
338
+ return [];
339
+ }
340
+ return [
341
+ {
342
+ ...entry,
343
+ details,
344
+ severity: detailsChanged(entry.details, details)
345
+ ? visibleDetailsSeverity(details)
346
+ : entry.severity,
347
+ },
348
+ ];
349
+ });
350
+
351
+ const partitioned = partitionDiffEntries(entries);
352
+ return options.breakingOnly === true || options.breaks === true
353
+ ? partitionDiffEntries(partitioned.breaking)
354
+ : partitioned;
124
355
  };
125
356
 
126
- /**
127
- * Build a human-readable detail view for a single trail.
128
- *
129
- * Overlaps with `trailToEntry` in `@ontrails/schema` which builds the
130
- * surface-map entry. The two serve different audiences (human display vs
131
- * machine-diffable surface map) so they are kept separate.
132
- */
133
- const formatTrailDetail = (item: Trail<unknown, unknown>): object => {
134
- const safety = safetyLabel(
135
- item as unknown as { readOnly?: boolean; destructive?: boolean }
136
- );
357
+ const createDiffExampleInput = (): {
358
+ readonly against: string;
359
+ readonly module: string;
360
+ readonly rootDir: string;
361
+ } => {
362
+ const input = createIsolatedExampleInput('survey-diff');
363
+ writeIsolatedExampleJsonFile(input.rootDir, 'baseline/topo.lock', {
364
+ activationGraph: {
365
+ edgeCount: 0,
366
+ edges: [],
367
+ sourceCount: 0,
368
+ sourceKeys: [],
369
+ trailIds: [],
370
+ },
371
+ activationSources: {},
372
+ entries: [],
373
+ generatedAt: '2026-01-01T00:00:00.000Z',
374
+ topoGraphSchemaVersion: TOPO_GRAPH_SCHEMA_VERSION,
375
+ } satisfies TopoGraph);
376
+ return { ...input, against: 'baseline' };
377
+ };
137
378
 
138
- return {
139
- description: item.description ?? null,
140
- detours: item.detours ?? null,
141
- examples: item.examples ?? [],
142
- id: item.id,
143
- kind: item.kind,
144
- safety,
145
- };
379
+ const isNotFound = (error: unknown): boolean =>
380
+ typeof error === 'object' &&
381
+ error !== null &&
382
+ (error as NodeJS.ErrnoException).code === 'ENOENT';
383
+
384
+ const readTopoGraphFile = async (
385
+ filePath: string
386
+ ): Promise<TopoGraph | null> => {
387
+ try {
388
+ return (await Bun.file(filePath).json()) as TopoGraph;
389
+ } catch (error: unknown) {
390
+ if (isNotFound(error)) {
391
+ return null;
392
+ }
393
+ throw error;
394
+ }
146
395
  };
147
396
 
148
- const formatDiff = (diff: DiffResult): object => ({
149
- breaking: diff.breaking,
150
- hasBreaking: diff.hasBreaking,
151
- info: diff.info,
152
- warnings: diff.warnings,
153
- });
397
+ const readStoredTopoGraph = (
398
+ rootDir: string,
399
+ against: string
400
+ ): TopoGraph | undefined => {
401
+ try {
402
+ const store = createTopoStore({ rootDir });
403
+ const stored =
404
+ store.exports.get({ pin: against }) ??
405
+ store.exports.get({ snapshotId: against });
406
+ return stored === undefined
407
+ ? undefined
408
+ : (JSON.parse(stored.topoGraphJson) as TopoGraph);
409
+ } catch (error: unknown) {
410
+ if (error instanceof NotFoundError) {
411
+ return undefined;
412
+ }
413
+ throw error;
414
+ }
415
+ };
416
+
417
+ const readPathTopoGraph = async (
418
+ rootDir: string,
419
+ against: string
420
+ ): Promise<Result<TopoGraph | null, Error>> => {
421
+ const safePath = deriveSafePath(rootDir, against);
422
+ if (safePath.isErr()) {
423
+ return safePath;
424
+ }
425
+
426
+ return Result.ok(
427
+ basename(safePath.value) === 'topo.lock' ||
428
+ extname(safePath.value) === '.json'
429
+ ? await readTopoGraphFile(safePath.value)
430
+ : await readTopoGraph({ dir: safePath.value })
431
+ );
432
+ };
433
+
434
+ const describeAgainstPathTarget = (against: string): string =>
435
+ basename(against) === 'topo.lock' || extname(against) === '.json'
436
+ ? 'workspace-relative TopoGraph file'
437
+ : 'workspace-relative directory containing trails.lock or topo.lock';
438
+
439
+ const topoGraphNotFound = (against: string): NotFoundError =>
440
+ new NotFoundError(
441
+ `No TopoGraph found for: ${against}. Tried ${describeAgainstPathTarget(
442
+ against
443
+ )}, then topo-store pin and snapshot references.`
444
+ );
445
+
446
+ const readAgainstTopoGraph = async (
447
+ rootDir: string,
448
+ against?: string | undefined
449
+ ): Promise<Result<{ against: string; map: TopoGraph }, Error>> => {
450
+ if (against === undefined || against === 'saved') {
451
+ const map =
452
+ (await readTopoGraph({ dir: rootDir })) ??
453
+ (await readTopoGraph({ dir: join(rootDir, '.trails') }));
454
+ return map === null
455
+ ? Result.err(
456
+ new NotFoundError(
457
+ 'No saved TopoGraph found. Run `trails compile` first.'
458
+ )
459
+ )
460
+ : Result.ok({ against: 'saved', map });
461
+ }
462
+
463
+ // Treat explicit filesystem targets as the most local user intent; stored
464
+ // pins and snapshot ids are fallback references when no path exists.
465
+ const pathMap = await readPathTopoGraph(rootDir, against);
466
+ if (pathMap.isErr()) {
467
+ return pathMap;
468
+ }
469
+ if (pathMap.value !== null) {
470
+ return Result.ok({ against, map: pathMap.value });
471
+ }
472
+
473
+ const storedMap = readStoredTopoGraph(rootDir, against);
474
+ if (storedMap !== undefined) {
475
+ return Result.ok({ against, map: storedMap });
476
+ }
477
+
478
+ return Result.err(topoGraphNotFound(against));
479
+ };
154
480
 
155
481
  const buildSurveyDiff = async (
156
482
  app: Topo,
157
- breakingOnly: boolean
158
- ): Promise<Result<object, Error>> => {
159
- const currentMap = generateSurfaceMap(app);
160
- const previousMap = await readSurfaceMap();
161
- if (!previousMap) {
162
- return Result.err(
163
- new Error(
164
- 'No previous surface map found. Run `trails survey generate` first.'
165
- )
166
- );
483
+ rootDir: string,
484
+ input: DiffInput
485
+ ): Promise<Result<SurveyDiffReport, Error>> => {
486
+ const currentMap = deriveTopoGraph(app);
487
+ const previous = await readAgainstTopoGraph(rootDir, input.against);
488
+ if (previous.isErr()) {
489
+ return previous;
167
490
  }
168
491
 
169
- const diff = diffSurfaceMaps(previousMap, currentMap);
170
- return Result.ok(
171
- breakingOnly
172
- ? formatDiff({
173
- ...diff,
174
- entries: diff.breaking,
175
- info: [],
176
- warnings: [],
177
- })
178
- : formatDiff(diff)
492
+ const target = parseDiffTarget(previous.value.map, currentMap, input.target);
493
+ if (target.isErr()) {
494
+ return target;
495
+ }
496
+
497
+ const diff = filterDiff(
498
+ deriveTopoGraphDiff(previous.value.map, currentMap),
499
+ target.value,
500
+ input
179
501
  );
502
+ return Result.ok(formatDiff(diff, previous.value.against));
180
503
  };
181
504
 
182
- const buildSurveyDetail = (
505
+ const buildSurveyLookup = (
183
506
  app: Topo,
184
- trailId: string
507
+ entityId: string,
508
+ rootDir: string,
509
+ cliAliases:
510
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
511
+ | undefined,
512
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
185
513
  ): Result<object, Error> => {
186
- const item = app.get(trailId);
187
- if (!item) {
188
- return Result.err(new Error(`Trail not found: ${trailId}`));
189
- }
190
- return Result.ok(formatTrailDetail(item as Trail<unknown, unknown>));
514
+ const matches = deriveCurrentTopoMatches(app, entityId, {
515
+ cliAliases,
516
+ rootDir,
517
+ surfaceLayerNames,
518
+ });
519
+ return Result.ok({ matches });
520
+ };
521
+
522
+ const buildSurveyTrailDetail = (
523
+ app: Topo,
524
+ id: string,
525
+ rootDir: string,
526
+ cliAliases:
527
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
528
+ | undefined,
529
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
530
+ ): Result<object, Error> => {
531
+ const detail = deriveCurrentTrailDetail(app, id, {
532
+ cliAliases,
533
+ rootDir,
534
+ surfaceLayerNames,
535
+ });
536
+ return detail === undefined
537
+ ? Result.err(new NotFoundError(`Trail not found: ${id}`))
538
+ : Result.ok(detail);
539
+ };
540
+
541
+ const buildSurveyResourceDetail = (
542
+ app: Topo,
543
+ id: string,
544
+ rootDir: string
545
+ ): Result<object, Error> => {
546
+ const detail = deriveCurrentResourceDetail(app, id, { rootDir });
547
+ return detail === undefined
548
+ ? Result.err(new NotFoundError(`Resource not found: ${id}`))
549
+ : Result.ok(detail);
550
+ };
551
+
552
+ const buildSurveySignalDetail = (
553
+ app: Topo,
554
+ id: string,
555
+ rootDir: string
556
+ ): Result<object, Error> => {
557
+ const detail = deriveCurrentSignalDetail(app, id, { rootDir });
558
+ return detail === undefined
559
+ ? Result.err(new NotFoundError(`Signal not found: ${id}`))
560
+ : Result.ok(detail);
561
+ };
562
+
563
+ const buildSurveySurfaceInventory = (app: Topo): Result<object, Error> =>
564
+ Result.ok(deriveShippedSurfaceProjectionInventory(app));
565
+
566
+ interface SurveyInput {
567
+ id?: string | undefined;
568
+ module?: string | undefined;
569
+ rootDir?: string | undefined;
570
+ }
571
+
572
+ type SurveyMode = 'lookup' | 'overview';
573
+
574
+ type SurveyEnvelope = { readonly mode: SurveyMode } & Record<string, unknown>;
575
+
576
+ /** Determine which survey mode was requested, falling back to 'overview'. */
577
+ const deriveSurveyMode = (input: SurveyInput): SurveyMode =>
578
+ input.id === undefined || input.id === '' ? 'overview' : 'lookup';
579
+
580
+ type SurveyHandler = (
581
+ app: Topo,
582
+ input: SurveyInput,
583
+ rootDir: string,
584
+ cliAliases:
585
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
586
+ | undefined,
587
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
588
+ ) => Result<object, Error> | Promise<Result<object, Error>>;
589
+
590
+ /** Handlers keyed by survey mode. */
591
+ const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
592
+ lookup: (app, input, rootDir, cliAliases, surfaceLayerNames) =>
593
+ input.id === undefined || input.id === ''
594
+ ? Result.err(new ValidationError('Survey lookup requires an id'))
595
+ : buildSurveyLookup(
596
+ app,
597
+ input.id,
598
+ rootDir,
599
+ cliAliases,
600
+ surfaceLayerNames
601
+ ),
602
+ overview: (app, _input, rootDir) =>
603
+ Result.ok(deriveCurrentTopoList(app, { rootDir })),
191
604
  };
192
605
 
193
- const buildSurveyGenerate = async (
194
- app: Topo
195
- ): Promise<Result<object, Error>> => {
196
- const surfaceMap = generateSurfaceMap(app);
197
- const mapPath = await writeSurfaceMap(surfaceMap);
198
- const hash = hashSurfaceMap(surfaceMap);
199
- const lockPath = await writeSurfaceLock(hash);
200
- return Result.ok({ hash, lockPath, mapPath });
606
+ const envelopeSurveyValue = (
607
+ mode: SurveyMode,
608
+ value: object
609
+ ): SurveyEnvelope => ({ ...value, mode });
610
+
611
+ /** Dispatch to the appropriate survey sub-command based on input flags. */
612
+ const dispatchSurvey = async (
613
+ app: Topo,
614
+ input: SurveyInput,
615
+ rootDir: string,
616
+ cliAliases:
617
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
618
+ | undefined,
619
+ surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
620
+ ): Promise<Result<SurveyEnvelope, Error>> => {
621
+ const mode = deriveSurveyMode(input);
622
+ const handler = surveyHandlers[mode];
623
+ const result = await handler(
624
+ app,
625
+ input,
626
+ rootDir,
627
+ cliAliases,
628
+ surfaceLayerNames
629
+ );
630
+ if (result.isErr()) {
631
+ return result;
632
+ }
633
+ return Result.ok(envelopeSurveyValue(mode, result.value));
201
634
  };
202
635
 
636
+ const detailInputSchema = z.object({
637
+ id: z.string().describe('Trail, resource, or signal ID'),
638
+ module: z.string().optional().describe('Path to the app module'),
639
+ rootDir: z.string().optional().describe('Workspace root directory'),
640
+ });
641
+
642
+ const withFreshSurveyApp = async <T>(
643
+ input: { readonly module?: string | undefined },
644
+ rootDir: string,
645
+ consume: (
646
+ app: Topo,
647
+ cliAliases:
648
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
649
+ | undefined
650
+ ) => Promise<Result<T, Error>> | Result<T, Error>
651
+ ): Promise<Result<T, Error>> =>
652
+ withFreshAppLease(input.module, rootDir, (lease) =>
653
+ consume(lease.app, lease.cliAliases)
654
+ );
655
+
656
+ const withResolvedSurveyApp = async <T>(
657
+ input: {
658
+ readonly module?: string | undefined;
659
+ readonly rootDir?: string | undefined;
660
+ },
661
+ cwd: string | undefined,
662
+ consume: (
663
+ app: Topo,
664
+ rootDir: string,
665
+ cliAliases:
666
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
667
+ | undefined
668
+ ) => Promise<Result<T, Error>> | Result<T, Error>
669
+ ): Promise<Result<T, Error>> =>
670
+ withOperatorRootDir(input, { cwd }, (rootDir) =>
671
+ withFreshSurveyApp(input, rootDir, (app, cliAliases) =>
672
+ consume(app, rootDir, cliAliases)
673
+ )
674
+ );
675
+
676
+ const moduleInputSchema = z.object({
677
+ module: z.string().optional().describe('Path to the app module'),
678
+ rootDir: z.string().optional().describe('Workspace root directory'),
679
+ });
680
+
681
+ const diffEntryOutput = z.object({
682
+ change: z.enum(['added', 'removed', 'modified']),
683
+ details: z.array(z.string()).readonly(),
684
+ id: z.string(),
685
+ kind: z.enum(['contour', 'trail', 'signal', 'resource']),
686
+ severity: z.enum(['info', 'warning', 'breaking']),
687
+ });
688
+
689
+ const diffOutput = z.object({
690
+ against: z.string(),
691
+ breaking: z.array(diffEntryOutput),
692
+ hasBreaking: z.boolean(),
693
+ info: z.array(diffEntryOutput),
694
+ mode: z.literal('diff'),
695
+ warnings: z.array(diffEntryOutput),
696
+ });
697
+
698
+ const diffInputSchema = z.object({
699
+ against: z
700
+ .string()
701
+ .min(1)
702
+ .optional()
703
+ .describe(
704
+ 'Saved TopoGraph target: "saved", a workspace path (topo.lock, .json file, or directory with topo.lock), then a pin/snapshot id'
705
+ ),
706
+ breakingOnly: z
707
+ .boolean()
708
+ .default(false)
709
+ .describe('Legacy alias for --breaks; only show breaking changes'),
710
+ breaks: z.boolean().default(false).describe('Only show breaking changes'),
711
+ forces: z
712
+ .boolean()
713
+ .default(false)
714
+ .describe('Only show graph force audit events'),
715
+ module: z.string().optional().describe('Path to the app module'),
716
+ rootDir: z.string().optional().describe('Workspace root directory'),
717
+ target: z
718
+ .string()
719
+ .min(1)
720
+ .optional()
721
+ .describe('Trail or trail version target, such as user.create@1..2'),
722
+ });
723
+
724
+ const surveyMatchOutput = z.discriminatedUnion('kind', [
725
+ z.object({
726
+ detail: trailDetailOutput,
727
+ kind: z.literal('trail'),
728
+ }),
729
+ z.object({
730
+ detail: resourceDetailOutput,
731
+ kind: z.literal('resource'),
732
+ }),
733
+ z.object({
734
+ detail: signalDetailOutput,
735
+ kind: z.literal('signal'),
736
+ }),
737
+ ]);
738
+
203
739
  // ---------------------------------------------------------------------------
204
740
  // Trail definition
205
741
  // ---------------------------------------------------------------------------
206
742
 
207
743
  export const surveyTrail = trail('survey', {
744
+ args: ['id'],
745
+ blaze: async (input, ctx) =>
746
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, cliAliases) =>
747
+ dispatchSurvey(
748
+ app,
749
+ input,
750
+ rootDir,
751
+ cliAliases,
752
+ readSurfaceLayerNamesFromContext(ctx)
753
+ )
754
+ ),
208
755
  description: 'Full topo introspection',
209
756
  examples: [
210
757
  {
211
- description: 'Lists all registered trails with safety and surface info',
212
- input: {},
213
- name: 'List all trails',
758
+ description: 'Show all registered trails, resources, and signals',
759
+ input: createIsolatedExampleInput('survey-overview'),
760
+ name: 'Overview',
214
761
  },
215
762
  {
216
- description: 'Quick capability summary with counts and feature flags',
217
- input: { brief: true },
218
- name: 'Brief capability report',
763
+ description: 'Find every trail, resource, or signal with a matching ID',
764
+ input: { ...createIsolatedExampleInput('survey-lookup'), id: 'survey' },
765
+ name: 'Lookup by ID',
219
766
  },
220
767
  ],
221
- implementation: async (input, ctx) => {
222
- const app = await loadApp(input.module, ctx.cwd ?? '.');
223
-
224
- if (input.brief) {
225
- return Result.ok(generateBriefReport(app));
226
- }
227
-
228
- if (input.diff) {
229
- return await buildSurveyDiff(app, input.breakingOnly);
230
- }
231
-
232
- if (input.trailId) {
233
- return buildSurveyDetail(app, input.trailId);
234
- }
235
-
236
- if (input.generate) {
237
- return await buildSurveyGenerate(app);
238
- }
239
-
240
- return Result.ok(formatTrailList(app));
241
- },
242
768
  input: z.object({
243
- breakingOnly: z
244
- .boolean()
245
- .default(false)
246
- .describe('Only show breaking changes'),
247
- brief: z.boolean().default(false).describe('Quick capability summary'),
248
- diff: z.string().optional().describe('Diff against a git ref'),
249
- generate: z
250
- .boolean()
251
- .default(false)
252
- .describe('Generate surface map and lock file'),
253
- module: z
769
+ id: z
254
770
  .string()
255
- .default('./src/app.ts')
256
- .describe('Path to the app module'),
257
- trailId: z.string().optional().describe('Trail ID for detail view'),
771
+ .optional()
772
+ .describe('Trail, resource, or signal ID to look up'),
773
+ module: z.string().optional().describe('Path to the app module'),
774
+ rootDir: z.string().optional().describe('Workspace root directory'),
258
775
  }),
259
- output: z.union([
776
+ intent: 'read',
777
+ output: z.discriminatedUnion('mode', [
260
778
  z.object({
779
+ activation: activationOverviewOutput,
261
780
  count: z.number(),
262
781
  entries: z.array(
263
782
  z.object({
783
+ activatedBy: z.array(z.string()).readonly(),
784
+ activates: z.array(z.string()).readonly(),
264
785
  examples: z.number(),
265
786
  id: z.string(),
266
787
  kind: z.string(),
267
788
  safety: z.string(),
268
789
  })
269
790
  ),
791
+ mode: z.literal('overview'),
792
+ resourceCount: z.number(),
793
+ resources: z.array(
794
+ z.object({
795
+ description: z.string().nullable(),
796
+ health: z.enum(['available', 'none']),
797
+ id: z.string(),
798
+ kind: z.literal('resource'),
799
+ lifetime: z.literal('singleton'),
800
+ usedBy: z.array(z.string()),
801
+ })
802
+ ),
803
+ signalCount: z.number(),
804
+ signals: z.array(
805
+ z.object({
806
+ consumers: z.array(z.string()).readonly(),
807
+ description: z.string().nullable(),
808
+ examples: z.number(),
809
+ from: z.array(z.string()).readonly(),
810
+ id: z.string(),
811
+ kind: z.literal('signal'),
812
+ payloadSchema: z.boolean(),
813
+ producers: z.array(z.string()).readonly(),
814
+ })
815
+ ),
270
816
  }),
271
817
  z.object({
272
- contractVersion: z.string(),
273
- events: z.number(),
274
- features: z.object({
275
- detours: z.boolean(),
276
- events: z.boolean(),
277
- examples: z.boolean(),
278
- hikes: z.boolean(),
279
- outputSchemas: z.boolean(),
280
- }),
281
- hikes: z.number(),
282
- name: z.string(),
283
- trails: z.number(),
284
- version: z.string(),
285
- }),
286
- z.object({
287
- breaking: z.array(z.unknown()),
288
- hasBreaking: z.boolean(),
289
- info: z.array(z.unknown()),
290
- warnings: z.array(z.unknown()),
291
- }),
292
- z.object({
293
- description: z.unknown().nullable(),
294
- detours: z.unknown().nullable(),
295
- examples: z.array(z.unknown()),
296
- id: z.string(),
297
- kind: z.string(),
298
- safety: z.string(),
299
- }),
300
- z.object({
301
- hash: z.string(),
302
- lockPath: z.string(),
303
- mapPath: z.string(),
818
+ matches: z.array(surveyMatchOutput),
819
+ mode: z.literal('lookup'),
304
820
  }),
305
821
  ]),
306
- readOnly: true,
822
+ });
823
+
824
+ export const surveyBriefTrail = trail('survey.brief', {
825
+ blaze: async (input, ctx) =>
826
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
827
+ Result.ok(deriveCurrentTopoBrief(app, { rootDir }))
828
+ ),
829
+ description: 'Summarize topo capabilities',
830
+ examples: [
831
+ {
832
+ description: 'Show counts and feature flags',
833
+ input: createIsolatedExampleInput('survey-brief'),
834
+ name: 'Brief capability report',
835
+ },
836
+ ],
837
+ input: moduleInputSchema,
838
+ intent: 'read',
839
+ output: briefReportSchema,
840
+ });
841
+
842
+ export const surveySurfacesTrail = trail('survey.surfaces', {
843
+ blaze: async (input, ctx) =>
844
+ withResolvedSurveyApp(input, ctx.cwd, (app) =>
845
+ buildSurveySurfaceInventory(app)
846
+ ),
847
+ description: 'Inventory shipped surface projections',
848
+ examples: [
849
+ {
850
+ description: 'Show CLI, MCP, and HTTP projections for public trails',
851
+ input: createIsolatedExampleInput('survey-surfaces'),
852
+ name: 'Shipped surface inventory',
853
+ },
854
+ ],
855
+ input: moduleInputSchema,
856
+ intent: 'read',
857
+ output: shippedSurfaceInventoryOutput,
858
+ });
859
+
860
+ export const surveyDiffTrail = trail('survey.diff', {
861
+ args: ['target'],
862
+ blaze: async (input, ctx) =>
863
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
864
+ buildSurveyDiff(app, rootDir, input)
865
+ ),
866
+ description: 'Diff the current topo against a saved TopoGraph',
867
+ examples: [
868
+ {
869
+ description: 'Compare current topo to a saved TopoGraph directory',
870
+ input: createDiffExampleInput(),
871
+ name: 'Diff against baseline',
872
+ },
873
+ {
874
+ description: 'Show only breaking contract drift',
875
+ input: { ...createDiffExampleInput(), breaks: true },
876
+ name: 'Breaking changes',
877
+ },
878
+ {
879
+ description: 'Show graph-only force audit events',
880
+ input: { ...createDiffExampleInput(), forces: true },
881
+ name: 'Force audit events',
882
+ },
883
+ {
884
+ description: 'Reject an empty saved map target',
885
+ error: 'ValidationError',
886
+ input: { against: '' },
887
+ name: 'Reject empty diff target',
888
+ },
889
+ {
890
+ description: 'Reject an empty target before filtering breaking drift',
891
+ error: 'ValidationError',
892
+ input: {
893
+ against: '',
894
+ breakingOnly: true,
895
+ },
896
+ name: 'Reject empty breaking-only target',
897
+ },
898
+ ],
899
+ input: diffInputSchema,
900
+ intent: 'read',
901
+ output: diffOutput,
902
+ });
903
+
904
+ export const surveyTrailDetailTrail = trail('survey.trail', {
905
+ args: ['id'],
906
+ blaze: async (input, ctx) =>
907
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, cliAliases) =>
908
+ buildSurveyTrailDetail(
909
+ app,
910
+ input.id,
911
+ rootDir,
912
+ cliAliases,
913
+ readSurfaceLayerNamesFromContext(ctx)
914
+ )
915
+ ),
916
+ description: 'Inspect one trail by ID',
917
+ examples: [
918
+ {
919
+ description: 'Show trail contract detail',
920
+ input: {
921
+ ...createIsolatedExampleInput('survey-trail-detail'),
922
+ id: 'survey',
923
+ },
924
+ name: 'Trail detail',
925
+ },
926
+ ],
927
+ input: detailInputSchema,
928
+ intent: 'read',
929
+ output: trailDetailOutput,
930
+ });
931
+
932
+ export const surveyResourceTrail = trail('survey.resource', {
933
+ args: ['id'],
934
+ blaze: async (input, ctx) =>
935
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
936
+ buildSurveyResourceDetail(app, input.id, rootDir)
937
+ ),
938
+ description: 'Inspect one resource by ID',
939
+ examples: [
940
+ {
941
+ description: 'Show resource usage detail',
942
+ error: 'NotFoundError',
943
+ input: {
944
+ ...createIsolatedExampleInput('survey-resource-detail'),
945
+ id: 'db.main',
946
+ },
947
+ name: 'Resource detail',
948
+ },
949
+ ],
950
+ input: detailInputSchema,
951
+ intent: 'read',
952
+ output: resourceDetailOutput,
953
+ });
954
+
955
+ export const surveySignalTrail = trail('survey.signal', {
956
+ args: ['id'],
957
+ blaze: async (input, ctx) =>
958
+ withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
959
+ buildSurveySignalDetail(app, input.id, rootDir)
960
+ ),
961
+ description: 'Inspect one signal by ID',
962
+ examples: [
963
+ {
964
+ description: 'Show signal producer and consumer detail',
965
+ error: 'NotFoundError',
966
+ input: {
967
+ ...createIsolatedExampleInput('survey-signal-detail'),
968
+ id: 'hello.greeted',
969
+ },
970
+ name: 'Signal detail',
971
+ },
972
+ ],
973
+ input: detailInputSchema,
974
+ intent: 'read',
975
+ output: signalDetailOutput,
307
976
  });