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

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