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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +215 -2
  2. package/README.md +27 -0
  3. package/package.json +21 -8
  4. package/src/app.ts +15 -5
  5. package/src/cli.ts +303 -10
  6. package/src/completions.ts +240 -0
  7. package/src/load-app-mirror.ts +160 -0
  8. package/src/local-state-io.ts +153 -0
  9. package/src/project-writes.ts +320 -0
  10. package/src/run-collision.ts +125 -0
  11. package/src/run-completions-install.ts +179 -0
  12. package/src/run-example.ts +149 -0
  13. package/src/run-examples.ts +148 -0
  14. package/src/run-quiet.ts +75 -0
  15. package/src/run-trace.ts +273 -0
  16. package/src/run-warden.ts +39 -0
  17. package/src/run-watch.ts +432 -0
  18. package/src/scaffold-versions.generated.ts +12 -0
  19. package/src/trails/add-surface.ts +45 -23
  20. package/src/trails/add-trail.ts +27 -17
  21. package/src/trails/add-verify.ts +57 -17
  22. package/src/trails/completions-complete.ts +165 -0
  23. package/src/trails/completions.ts +47 -0
  24. package/src/trails/create-scaffold.ts +86 -33
  25. package/src/trails/create.ts +11 -3
  26. package/src/trails/dev-clean.ts +6 -1
  27. package/src/trails/dev-reset.ts +6 -1
  28. package/src/trails/dev-stats.ts +6 -1
  29. package/src/trails/dev-support.ts +29 -17
  30. package/src/trails/draft-promote.ts +289 -80
  31. package/src/trails/guide.ts +54 -34
  32. package/src/trails/load-app.ts +251 -56
  33. package/src/trails/root-dir.ts +21 -0
  34. package/src/trails/run-example.ts +482 -0
  35. package/src/trails/run-examples.ts +141 -0
  36. package/src/trails/run.ts +403 -0
  37. package/src/trails/survey.ts +534 -200
  38. package/src/trails/topo-activation.ts +385 -0
  39. package/src/trails/topo-compile.ts +55 -0
  40. package/src/trails/topo-history.ts +6 -1
  41. package/src/trails/topo-output-schemas.ts +219 -0
  42. package/src/trails/topo-pin.ts +19 -6
  43. package/src/trails/topo-read-support.ts +171 -228
  44. package/src/trails/topo-reports.ts +587 -25
  45. package/src/trails/topo-store-support.ts +43 -19
  46. package/src/trails/topo-support.ts +18 -28
  47. package/src/trails/topo-unpin.ts +6 -1
  48. package/src/trails/topo-verify.ts +18 -5
  49. package/src/trails/topo.ts +60 -23
  50. package/src/trails/warden-guide.ts +121 -0
  51. package/src/trails/warden.ts +137 -56
  52. package/src/versions.ts +3 -18
  53. package/.turbo/turbo-build.log +0 -1
  54. package/.turbo/turbo-lint.log +0 -3
  55. package/.turbo/turbo-typecheck.log +0 -1
  56. package/__tests__/examples.test.ts +0 -45
  57. package/dist/bin/trails.d.ts +0 -3
  58. package/dist/bin/trails.d.ts.map +0 -1
  59. package/dist/bin/trails.js +0 -4
  60. package/dist/bin/trails.js.map +0 -1
  61. package/dist/src/app.d.ts +0 -2
  62. package/dist/src/app.d.ts.map +0 -1
  63. package/dist/src/app.js +0 -22
  64. package/dist/src/app.js.map +0 -1
  65. package/dist/src/clack.d.ts +0 -9
  66. package/dist/src/clack.d.ts.map +0 -1
  67. package/dist/src/clack.js +0 -84
  68. package/dist/src/clack.js.map +0 -1
  69. package/dist/src/cli.d.ts +0 -2
  70. package/dist/src/cli.d.ts.map +0 -1
  71. package/dist/src/cli.js +0 -14
  72. package/dist/src/cli.js.map +0 -1
  73. package/dist/src/trails/add-surface.d.ts +0 -13
  74. package/dist/src/trails/add-surface.d.ts.map +0 -1
  75. package/dist/src/trails/add-surface.js +0 -110
  76. package/dist/src/trails/add-surface.js.map +0 -1
  77. package/dist/src/trails/add-trail.d.ts +0 -12
  78. package/dist/src/trails/add-trail.d.ts.map +0 -1
  79. package/dist/src/trails/add-trail.js +0 -104
  80. package/dist/src/trails/add-trail.js.map +0 -1
  81. package/dist/src/trails/add-trailhead.d.ts +0 -13
  82. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  83. package/dist/src/trails/add-trailhead.js +0 -88
  84. package/dist/src/trails/add-trailhead.js.map +0 -1
  85. package/dist/src/trails/add-verify.d.ts +0 -10
  86. package/dist/src/trails/add-verify.d.ts.map +0 -1
  87. package/dist/src/trails/add-verify.js +0 -68
  88. package/dist/src/trails/add-verify.js.map +0 -1
  89. package/dist/src/trails/create-scaffold.d.ts +0 -15
  90. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  91. package/dist/src/trails/create-scaffold.js +0 -295
  92. package/dist/src/trails/create-scaffold.js.map +0 -1
  93. package/dist/src/trails/create.d.ts +0 -18
  94. package/dist/src/trails/create.d.ts.map +0 -1
  95. package/dist/src/trails/create.js +0 -126
  96. package/dist/src/trails/create.js.map +0 -1
  97. package/dist/src/trails/dev-clean.d.ts +0 -9
  98. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  99. package/dist/src/trails/dev-clean.js +0 -66
  100. package/dist/src/trails/dev-clean.js.map +0 -1
  101. package/dist/src/trails/dev-reset.d.ts +0 -6
  102. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  103. package/dist/src/trails/dev-reset.js +0 -39
  104. package/dist/src/trails/dev-reset.js.map +0 -1
  105. package/dist/src/trails/dev-stats.d.ts +0 -7
  106. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  107. package/dist/src/trails/dev-stats.js +0 -61
  108. package/dist/src/trails/dev-stats.js.map +0 -1
  109. package/dist/src/trails/dev-support.d.ts +0 -64
  110. package/dist/src/trails/dev-support.d.ts.map +0 -1
  111. package/dist/src/trails/dev-support.js +0 -181
  112. package/dist/src/trails/dev-support.js.map +0 -1
  113. package/dist/src/trails/draft-promote.d.ts +0 -18
  114. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  115. package/dist/src/trails/draft-promote.js +0 -400
  116. package/dist/src/trails/draft-promote.js.map +0 -1
  117. package/dist/src/trails/guide.d.ts +0 -21
  118. package/dist/src/trails/guide.d.ts.map +0 -1
  119. package/dist/src/trails/guide.js +0 -61
  120. package/dist/src/trails/guide.js.map +0 -1
  121. package/dist/src/trails/load-app.d.ts +0 -12
  122. package/dist/src/trails/load-app.d.ts.map +0 -1
  123. package/dist/src/trails/load-app.js +0 -415
  124. package/dist/src/trails/load-app.js.map +0 -1
  125. package/dist/src/trails/project.d.ts +0 -8
  126. package/dist/src/trails/project.d.ts.map +0 -1
  127. package/dist/src/trails/project.js +0 -54
  128. package/dist/src/trails/project.js.map +0 -1
  129. package/dist/src/trails/survey.d.ts +0 -18
  130. package/dist/src/trails/survey.d.ts.map +0 -1
  131. package/dist/src/trails/survey.js +0 -234
  132. package/dist/src/trails/survey.js.map +0 -1
  133. package/dist/src/trails/topo-constants.d.ts +0 -3
  134. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  135. package/dist/src/trails/topo-constants.js +0 -3
  136. package/dist/src/trails/topo-constants.js.map +0 -1
  137. package/dist/src/trails/topo-export.d.ts +0 -19
  138. package/dist/src/trails/topo-export.d.ts.map +0 -1
  139. package/dist/src/trails/topo-export.js +0 -31
  140. package/dist/src/trails/topo-export.js.map +0 -1
  141. package/dist/src/trails/topo-history.d.ts +0 -20
  142. package/dist/src/trails/topo-history.d.ts.map +0 -1
  143. package/dist/src/trails/topo-history.js +0 -32
  144. package/dist/src/trails/topo-history.js.map +0 -1
  145. package/dist/src/trails/topo-pin.d.ts +0 -17
  146. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  147. package/dist/src/trails/topo-pin.js +0 -31
  148. package/dist/src/trails/topo-pin.js.map +0 -1
  149. package/dist/src/trails/topo-read-support.d.ts +0 -58
  150. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  151. package/dist/src/trails/topo-read-support.js +0 -167
  152. package/dist/src/trails/topo-read-support.js.map +0 -1
  153. package/dist/src/trails/topo-reports.d.ts +0 -54
  154. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  155. package/dist/src/trails/topo-reports.js +0 -128
  156. package/dist/src/trails/topo-reports.js.map +0 -1
  157. package/dist/src/trails/topo-show.d.ts +0 -23
  158. package/dist/src/trails/topo-show.d.ts.map +0 -1
  159. package/dist/src/trails/topo-show.js +0 -49
  160. package/dist/src/trails/topo-show.js.map +0 -1
  161. package/dist/src/trails/topo-store-support.d.ts +0 -13
  162. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  163. package/dist/src/trails/topo-store-support.js +0 -55
  164. package/dist/src/trails/topo-store-support.js.map +0 -1
  165. package/dist/src/trails/topo-support.d.ts +0 -76
  166. package/dist/src/trails/topo-support.d.ts.map +0 -1
  167. package/dist/src/trails/topo-support.js +0 -132
  168. package/dist/src/trails/topo-support.js.map +0 -1
  169. package/dist/src/trails/topo-unpin.d.ts +0 -20
  170. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  171. package/dist/src/trails/topo-unpin.js +0 -44
  172. package/dist/src/trails/topo-unpin.js.map +0 -1
  173. package/dist/src/trails/topo-verify.d.ts +0 -5
  174. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  175. package/dist/src/trails/topo-verify.js +0 -24
  176. package/dist/src/trails/topo-verify.js.map +0 -1
  177. package/dist/src/trails/topo.d.ts +0 -5
  178. package/dist/src/trails/topo.d.ts.map +0 -1
  179. package/dist/src/trails/topo.js +0 -63
  180. package/dist/src/trails/topo.js.map +0 -1
  181. package/dist/src/trails/warden.d.ts +0 -20
  182. package/dist/src/trails/warden.d.ts.map +0 -1
  183. package/dist/src/trails/warden.js +0 -98
  184. package/dist/src/trails/warden.js.map +0 -1
  185. package/dist/src/versions.d.ts +0 -12
  186. package/dist/src/versions.d.ts.map +0 -1
  187. package/dist/src/versions.js +0 -23
  188. package/dist/src/versions.js.map +0 -1
  189. package/dist/tsconfig.tsbuildinfo +0 -1
  190. package/src/__tests__/add-trail.test.ts +0 -97
  191. package/src/__tests__/create.test.ts +0 -415
  192. package/src/__tests__/draft-promote.test.ts +0 -144
  193. package/src/__tests__/guide.test.ts +0 -96
  194. package/src/__tests__/load-app.test.ts +0 -419
  195. package/src/__tests__/survey.test.ts +0 -377
  196. package/src/__tests__/topo-dev.test.ts +0 -426
  197. package/src/__tests__/warden.test.ts +0 -74
  198. package/src/trails/topo-export.ts +0 -35
  199. package/src/trails/topo-show.ts +0 -54
  200. package/tsconfig.json +0 -9
  201. package/tsconfig.tests.json +0 -10
@@ -1,26 +1,138 @@
1
- import type { Topo, Trail } from '@ontrails/core';
1
+ import {
2
+ DETOUR_MAX_ATTEMPTS_CAP,
3
+ deriveCliPath,
4
+ filterSurfaceTrails,
5
+ zodToJsonSchema,
6
+ } from '@ontrails/core';
7
+ import type { AnyTrail, Signal, Topo } from '@ontrails/core';
8
+ import { deriveHttpMethod } from '@ontrails/http';
9
+ import type { HttpMethod } from '@ontrails/http';
10
+ import { deriveToolName } from '@ontrails/mcp';
11
+ import { deriveTopoGraph } from '@ontrails/topographer';
12
+ import type {
13
+ JsonSchema,
14
+ TopoGraph,
15
+ TopoGraphActivationEdge,
16
+ TopoGraphEntry,
17
+ TopoGraphFieldOverride,
18
+ TopoGraphLayerReference,
19
+ } from '@ontrails/topographer';
20
+ import { z } from 'zod';
2
21
 
22
+ import type {
23
+ ActivationChainReport,
24
+ ActivationEdgeReport,
25
+ ActivationGraphReport,
26
+ ActivationOverviewReport,
27
+ ActivationSourceReport,
28
+ SignalActivationRelations,
29
+ } from './topo-activation.js';
30
+ import {
31
+ deriveActivationGraph,
32
+ deriveDeclaredTrailActivation,
33
+ deriveSignalActivationRelations,
34
+ } from './topo-activation.js';
3
35
  import { REPORT_CONTRACT_VERSION, REPORT_VERSION } from './topo-constants.js';
4
36
 
5
- export interface BriefReport {
6
- readonly name: string;
7
- readonly version: string;
8
- readonly contractVersion: string;
9
- readonly features: {
10
- readonly resources: boolean;
11
- readonly outputSchemas: boolean;
12
- readonly examples: boolean;
13
- readonly detours: boolean;
14
- readonly signals: boolean;
15
- };
16
- readonly trails: number;
17
- readonly signals: number;
18
- readonly resources: number;
37
+ export type {
38
+ ActivationChainReport,
39
+ ActivationEdgeReport,
40
+ ActivationGraphReport,
41
+ ActivationOverviewReport,
42
+ ActivationSourceReport,
43
+ SignalActivationRelations,
44
+ TrailActivationReport,
45
+ } from './topo-activation.js';
46
+
47
+ export const briefReportSchema = z.object({
48
+ contractVersion: z.string(),
49
+ features: z.object({
50
+ detours: z.boolean(),
51
+ examples: z.boolean(),
52
+ outputSchemas: z.boolean(),
53
+ resources: z.boolean(),
54
+ signals: z.boolean(),
55
+ }),
56
+ name: z.string(),
57
+ resources: z.number(),
58
+ signals: z.number(),
59
+ trails: z.number(),
60
+ version: z.string(),
61
+ });
62
+
63
+ type BriefReportShape = z.infer<typeof briefReportSchema>;
64
+
65
+ export type BriefReport = Readonly<
66
+ Omit<BriefReportShape, 'features'> & {
67
+ readonly features: Readonly<BriefReportShape['features']>;
68
+ }
69
+ >;
70
+
71
+ export type SurfaceLayerKey = 'cli' | 'http' | 'mcp';
72
+
73
+ export type SurfaceLayerNames = Readonly<
74
+ Record<SurfaceLayerKey, readonly string[]>
75
+ >;
76
+
77
+ export type ShippedSurfaceKey = 'cli' | 'mcp' | 'http';
78
+
79
+ export type SurfaceProjectionSource = 'authored' | 'default-derived';
80
+
81
+ export type ShippedSurfaceProjection =
82
+ | {
83
+ readonly commandPath: readonly string[];
84
+ readonly derivedName: string;
85
+ readonly method: null;
86
+ readonly source: SurfaceProjectionSource;
87
+ readonly surface: 'cli';
88
+ readonly trailId: string;
89
+ }
90
+ | {
91
+ readonly derivedName: string;
92
+ readonly method: null;
93
+ readonly source: SurfaceProjectionSource;
94
+ readonly surface: 'mcp';
95
+ readonly toolName: string;
96
+ readonly trailId: string;
97
+ }
98
+ | {
99
+ readonly derivedName: string;
100
+ readonly method: HttpMethod;
101
+ readonly path: string;
102
+ readonly source: SurfaceProjectionSource;
103
+ readonly surface: 'http';
104
+ readonly trailId: string;
105
+ };
106
+
107
+ export interface ShippedSurfaceInventoryReport {
108
+ readonly count: number;
109
+ readonly excludedSurfaces: readonly {
110
+ readonly reason: string;
111
+ readonly status: 'planned';
112
+ readonly surface: 'websocket';
113
+ }[];
114
+ readonly projections: readonly ShippedSurfaceProjection[];
115
+ readonly shippedSurfaces: readonly ShippedSurfaceKey[];
116
+ readonly trails: readonly {
117
+ readonly explicitSurfaces: readonly string[];
118
+ readonly projections: readonly ShippedSurfaceProjection[];
119
+ readonly trailId: string;
120
+ }[];
121
+ }
122
+
123
+ type TopoGraphContourEntry = TopoGraphEntry & { readonly kind: 'contour' };
124
+
125
+ export interface TrailDetailOptions {
126
+ readonly surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined;
127
+ readonly topoGraph?: TopoGraph | undefined;
19
128
  }
20
129
 
21
130
  export interface SurveyListReport {
131
+ readonly activation: ActivationOverviewReport;
22
132
  readonly count: number;
23
133
  readonly entries: readonly {
134
+ readonly activatedBy: readonly string[];
135
+ readonly activates: readonly string[];
24
136
  readonly examples: number;
25
137
  readonly id: string;
26
138
  readonly kind: string;
@@ -35,21 +147,88 @@ export interface SurveyListReport {
35
147
  readonly lifetime: 'singleton';
36
148
  readonly usedBy: readonly string[];
37
149
  }[];
150
+ readonly signalCount: number;
151
+ readonly signals: readonly {
152
+ readonly consumers: readonly string[];
153
+ readonly description: string | null;
154
+ readonly examples: number;
155
+ readonly from: readonly string[];
156
+ readonly id: string;
157
+ readonly kind: 'signal';
158
+ readonly payloadSchema: boolean;
159
+ readonly producers: readonly string[];
160
+ }[];
38
161
  }
39
162
 
40
163
  export interface TrailDetailReport {
164
+ readonly activatedBy: readonly string[];
165
+ readonly activates: readonly string[];
166
+ readonly activationChains: readonly ActivationChainReport[];
167
+ readonly activationContext: {
168
+ readonly edgeCount: number;
169
+ readonly sourceCount: number;
170
+ readonly sourceKeys: readonly string[];
171
+ readonly trailIds: readonly string[];
172
+ };
173
+ readonly activationEdges: readonly ActivationEdgeReport[];
174
+ readonly activationSources: readonly ActivationSourceReport[];
175
+ readonly cli: {
176
+ readonly path: readonly string[];
177
+ } | null;
178
+ /**
179
+ * Composed layer names visible at the survey boundary.
180
+ *
181
+ * Reports the names of typed layers that wrap this trail at execution time,
182
+ * in the framework's composition order: `topo → surface → trail`
183
+ * (outermost-first). Surface-scope layers are keyed by surface because
184
+ * each surface owns its own attachment set.
185
+ */
186
+ readonly composedLayers: {
187
+ readonly topo: readonly string[];
188
+ readonly surface: SurfaceLayerNames;
189
+ readonly trail: readonly string[];
190
+ };
191
+ readonly contourDetails: readonly TopoGraphContourEntry[];
192
+ readonly contours: readonly string[];
41
193
  readonly description: string | null;
42
194
  readonly detours:
43
195
  | readonly { readonly on: string; readonly maxAttempts: number }[]
44
196
  | null;
45
197
  readonly examples: readonly unknown[];
198
+ readonly fieldOverrides: readonly TopoGraphFieldOverride[];
46
199
  readonly crosses: readonly string[];
200
+ readonly fires: readonly string[];
201
+ readonly governance: Readonly<Record<string, unknown>> | null;
47
202
  readonly id: string;
203
+ readonly input: JsonSchema | null;
48
204
  readonly intent: 'read' | 'write' | 'destroy';
49
- readonly kind: string;
205
+ readonly kind: 'trail';
206
+ readonly layers: readonly TopoGraphLayerReference[];
207
+ readonly on: readonly string[];
208
+ readonly output: JsonSchema | null;
50
209
  readonly pattern: string | null;
51
210
  readonly safety: string;
52
211
  readonly resources: readonly string[];
212
+ readonly surfaceProjections: readonly ShippedSurfaceProjection[];
213
+ readonly surfaces: readonly string[];
214
+ }
215
+
216
+ export interface SignalDetailReport {
217
+ readonly consumers: readonly string[];
218
+ readonly description: string | null;
219
+ readonly examples: readonly unknown[];
220
+ readonly from: readonly string[];
221
+ readonly id: string;
222
+ readonly kind: 'signal';
223
+ /**
224
+ * The signal's payload schema (JSON Schema object), or `null` when the
225
+ * surface-map entry is missing for this signal. `null` is meaningful:
226
+ * it matches the list view's `payloadSchema: false` flag and lets
227
+ * consumers distinguish "schema not found" from "schema accepts any
228
+ * value" (the latter would be an empty object `{}`).
229
+ */
230
+ readonly payload: Readonly<Record<string, unknown>> | null;
231
+ readonly producers: readonly string[];
53
232
  }
54
233
 
55
234
  const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
@@ -170,9 +349,40 @@ const formatResourceList = (app: Topo): SurveyListReport['resources'] => {
170
349
  .toSorted((a, b) => a.id.localeCompare(b.id));
171
350
  };
172
351
 
352
+ const formatSignalList = (
353
+ app: Topo,
354
+ relations: ReadonlyMap<string, SignalActivationRelations>
355
+ ): SurveyListReport['signals'] =>
356
+ app
357
+ .listSignals()
358
+ .map((signalDef) => {
359
+ const related = relations.get(signalDef.id);
360
+ const consumers = related?.consumers ?? [];
361
+ const producers = related?.producers ?? [];
362
+ return {
363
+ consumers,
364
+ description: signalDef.description ?? null,
365
+ examples: signalDef.examples?.length ?? 0,
366
+ from: signalDef.from?.toSorted() ?? [],
367
+ id: signalDef.id,
368
+ kind: signalDef.kind,
369
+ // Mirror the store path (`mapSignalRow` in `topo-store-read.ts`) which
370
+ // derives this from the surface-map entry. SignalSpec<T> requires
371
+ // `payload` so this is `true` in practice today; the explicit check
372
+ // keeps the in-memory and store reports self-consistent if a future
373
+ // SignalSpec variant ever omits `payload`.
374
+ payloadSchema: signalDef.payload !== undefined,
375
+ producers,
376
+ };
377
+ })
378
+ .toSorted((a, b) => a.id.localeCompare(b.id));
379
+
173
380
  export const deriveSurveyList = (app: Topo): SurveyListReport => {
174
381
  const items = app.list();
382
+ const activation = deriveActivationGraph(app);
175
383
  const entries = items.map((item) => {
384
+ const trailActivation =
385
+ activation.trails.get(item.id) ?? deriveDeclaredTrailActivation(item);
176
386
  const safety = safetyLabel(
177
387
  item as unknown as { intent?: 'read' | 'write' | 'destroy' }
178
388
  );
@@ -183,6 +393,8 @@ export const deriveSurveyList = (app: Topo): SurveyListReport => {
183
393
  : 0;
184
394
 
185
395
  return {
396
+ activatedBy: trailActivation.activatedBy,
397
+ activates: trailActivation.activates,
186
398
  examples,
187
399
  id: item.id,
188
400
  kind: item.kind,
@@ -191,38 +403,388 @@ export const deriveSurveyList = (app: Topo): SurveyListReport => {
191
403
  });
192
404
 
193
405
  const resources = formatResourceList(app);
406
+ const signals = formatSignalList(app, activation.signals);
194
407
 
195
408
  return {
409
+ activation: activation.overview,
196
410
  count: items.length,
197
411
  entries,
198
412
  resourceCount: resources.length,
199
413
  resources,
414
+ signalCount: signals.length,
415
+ signals,
416
+ };
417
+ };
418
+
419
+ export const deriveSignalDetail = (
420
+ app: Topo,
421
+ signalId: string,
422
+ activationGraph?: ActivationGraphReport | undefined
423
+ ): SignalDetailReport | undefined => {
424
+ const signalDef = app.signals.get(signalId) as Signal<unknown> | undefined;
425
+ if (signalDef === undefined) {
426
+ return undefined;
427
+ }
428
+ const related =
429
+ activationGraph?.signals.get(signalId) ??
430
+ deriveSignalActivationRelations(app, signalId);
431
+
432
+ return {
433
+ consumers: [...related.consumers],
434
+ description: signalDef.description ?? null,
435
+ examples: [...(signalDef.examples ?? [])],
436
+ from: signalDef.from?.toSorted() ?? [],
437
+ id: signalDef.id,
438
+ kind: 'signal',
439
+ payload: zodToJsonSchema(signalDef.payload),
440
+ producers: [...related.producers],
441
+ };
442
+ };
443
+
444
+ const emptySurfaceLayerNames = (): SurfaceLayerNames => ({
445
+ cli: [],
446
+ http: [],
447
+ mcp: [],
448
+ });
449
+
450
+ const normalizeSurfaceLayerNames = (
451
+ names?: Partial<SurfaceLayerNames> | undefined
452
+ ): SurfaceLayerNames => {
453
+ const base = emptySurfaceLayerNames();
454
+ if (names === undefined) {
455
+ return base;
456
+ }
457
+ return {
458
+ cli: names.cli ?? base.cli,
459
+ http: names.http ?? base.http,
460
+ mcp: names.mcp ?? base.mcp,
200
461
  };
201
462
  };
202
463
 
464
+ const emptyActivationContext = (): TrailDetailReport['activationContext'] => ({
465
+ edgeCount: 0,
466
+ sourceCount: 0,
467
+ sourceKeys: [],
468
+ trailIds: [],
469
+ });
470
+
471
+ const activationContextFromTopoGraph = (
472
+ topoGraph: TopoGraph | undefined,
473
+ trailId: string,
474
+ fallbackEdges: readonly TopoGraphActivationEdge[]
475
+ ): TrailDetailReport['activationContext'] => {
476
+ const edges =
477
+ topoGraph?.activationGraph.edges.filter(
478
+ (edge) => edge.trailId === trailId
479
+ ) ?? fallbackEdges;
480
+ if (edges.length === 0) {
481
+ return emptyActivationContext();
482
+ }
483
+ return {
484
+ edgeCount: edges.length,
485
+ sourceCount: new Set(edges.map((edge) => edge.sourceKey)).size,
486
+ sourceKeys: [...new Set(edges.map((edge) => edge.sourceKey))].toSorted(),
487
+ trailIds: [...new Set(edges.map((edge) => edge.trailId))].toSorted(),
488
+ };
489
+ };
490
+
491
+ const findTopoEntry = (
492
+ topoGraph: TopoGraph | undefined,
493
+ id: string,
494
+ kind: TopoGraphEntry['kind']
495
+ ): TopoGraphEntry | undefined =>
496
+ topoGraph?.entries.find((entry) => entry.id === id && entry.kind === kind);
497
+
498
+ const trailActivationEdgesFromTopoGraph = (
499
+ topoGraph: TopoGraph | undefined,
500
+ trailId: string,
501
+ fallback: readonly TopoGraphActivationEdge[]
502
+ ): readonly TopoGraphActivationEdge[] =>
503
+ topoGraph?.activationGraph.edges.filter((edge) => edge.trailId === trailId) ??
504
+ fallback;
505
+
506
+ const SHIPPED_SURFACES = ['cli', 'mcp', 'http'] as const;
507
+
508
+ const PLANNED_SURFACE_EXCLUSIONS = [
509
+ {
510
+ reason: 'WebSocket is planned, but no public package or API ships yet.',
511
+ status: 'planned' as const,
512
+ surface: 'websocket' as const,
513
+ },
514
+ ] as const;
515
+
516
+ const surfaceOrder = (surface: ShippedSurfaceKey): number =>
517
+ SHIPPED_SURFACES.indexOf(surface);
518
+
519
+ const sortSurfaceProjections = (
520
+ projections: readonly ShippedSurfaceProjection[]
521
+ ): readonly ShippedSurfaceProjection[] =>
522
+ projections.toSorted(
523
+ (a, b) =>
524
+ a.trailId.localeCompare(b.trailId) ||
525
+ surfaceOrder(a.surface) - surfaceOrder(b.surface)
526
+ );
527
+
528
+ const explicitSurfacesForEntry = (
529
+ entry: TopoGraphEntry | undefined
530
+ ): readonly string[] => entry?.surfaces ?? [];
531
+
532
+ const projectionSource = (
533
+ entry: TopoGraphEntry | undefined,
534
+ surface: ShippedSurfaceKey
535
+ ): SurfaceProjectionSource =>
536
+ explicitSurfacesForEntry(entry).includes(surface)
537
+ ? 'authored'
538
+ : 'default-derived';
539
+
540
+ const deriveHttpPath = (trailId: string): string =>
541
+ `/${trailId.replaceAll('.', '/')}`;
542
+
543
+ const isSurfaceEligibleTrail = (app: Topo, trail: AnyTrail): boolean =>
544
+ filterSurfaceTrails([trail]).length > 0 &&
545
+ app.trails.get(trail.id) !== undefined;
546
+
547
+ export const deriveShippedSurfaceProjectionsForTrail = (
548
+ app: Topo,
549
+ trail: AnyTrail,
550
+ topoGraph?: TopoGraph | undefined
551
+ ): readonly ShippedSurfaceProjection[] => {
552
+ if (!isSurfaceEligibleTrail(app, trail)) {
553
+ return [];
554
+ }
555
+
556
+ const entry = findTopoEntry(
557
+ topoGraph ?? deriveTopoGraph(app),
558
+ trail.id,
559
+ 'trail'
560
+ );
561
+ const commandPath = deriveCliPath(trail.id);
562
+ const httpMethod = deriveHttpMethod(trail.intent);
563
+ const httpPath = deriveHttpPath(trail.id);
564
+ const mcpToolName = deriveToolName(app.name, trail.id);
565
+
566
+ return sortSurfaceProjections([
567
+ {
568
+ commandPath,
569
+ derivedName: commandPath.join(' '),
570
+ method: null,
571
+ source: projectionSource(entry, 'cli'),
572
+ surface: 'cli',
573
+ trailId: trail.id,
574
+ },
575
+ {
576
+ derivedName: mcpToolName,
577
+ method: null,
578
+ source: projectionSource(entry, 'mcp'),
579
+ surface: 'mcp',
580
+ toolName: mcpToolName,
581
+ trailId: trail.id,
582
+ },
583
+ {
584
+ derivedName: httpPath,
585
+ method: httpMethod,
586
+ path: httpPath,
587
+ source: projectionSource(entry, 'http'),
588
+ surface: 'http',
589
+ trailId: trail.id,
590
+ },
591
+ ]);
592
+ };
593
+
594
+ const deriveFallbackSurfaceProjections = (
595
+ entry: TopoGraphEntry | undefined
596
+ ): readonly ShippedSurfaceProjection[] => {
597
+ if (entry?.cli === undefined) {
598
+ return [];
599
+ }
600
+
601
+ return [
602
+ {
603
+ commandPath: entry.cli.path,
604
+ derivedName: entry.cli.path.join(' '),
605
+ method: null,
606
+ source: projectionSource(entry, 'cli'),
607
+ surface: 'cli',
608
+ trailId: entry.id,
609
+ },
610
+ ];
611
+ };
612
+
613
+ export const deriveShippedSurfaceProjectionInventory = (
614
+ app: Topo
615
+ ): ShippedSurfaceInventoryReport => {
616
+ const topoGraph = deriveTopoGraph(app);
617
+ const trails = filterSurfaceTrails(app.list()).map((trail) => {
618
+ const entry = findTopoEntry(topoGraph, trail.id, 'trail');
619
+ const projections = deriveShippedSurfaceProjectionsForTrail(
620
+ app,
621
+ trail,
622
+ topoGraph
623
+ );
624
+
625
+ return {
626
+ explicitSurfaces: explicitSurfacesForEntry(entry),
627
+ projections,
628
+ trailId: trail.id,
629
+ };
630
+ });
631
+ const projections = sortSurfaceProjections(
632
+ trails.flatMap((trail) => trail.projections)
633
+ );
634
+
635
+ return {
636
+ count: trails.length,
637
+ excludedSurfaces: PLANNED_SURFACE_EXCLUSIONS,
638
+ projections,
639
+ shippedSurfaces: SHIPPED_SURFACES,
640
+ trails: trails.toSorted((a, b) => a.trailId.localeCompare(b.trailId)),
641
+ };
642
+ };
643
+
644
+ const deriveResolvedSurfaceProjections = (
645
+ app: Topo | undefined,
646
+ trailId: string,
647
+ topoEntry: TopoGraphEntry | undefined,
648
+ topoGraph: TopoGraph | undefined
649
+ ): readonly ShippedSurfaceProjection[] => {
650
+ if (app === undefined) {
651
+ return deriveFallbackSurfaceProjections(topoEntry);
652
+ }
653
+
654
+ const trail = app.trails.get(trailId);
655
+ return trail === undefined
656
+ ? deriveFallbackSurfaceProjections(topoEntry)
657
+ : deriveShippedSurfaceProjectionsForTrail(app, trail, topoGraph);
658
+ };
659
+
660
+ const deriveResolvedTrailGraphDetail = (
661
+ app: Topo | undefined,
662
+ trailId: string,
663
+ fallbackActivationEdges: readonly TopoGraphActivationEdge[],
664
+ topoGraphOverride?: TopoGraph | undefined
665
+ ): Pick<
666
+ TrailDetailReport,
667
+ | 'activationContext'
668
+ | 'activationEdges'
669
+ | 'cli'
670
+ | 'contourDetails'
671
+ | 'contours'
672
+ | 'fieldOverrides'
673
+ | 'governance'
674
+ | 'input'
675
+ | 'layers'
676
+ | 'output'
677
+ | 'surfaceProjections'
678
+ | 'surfaces'
679
+ > => {
680
+ const topoGraph =
681
+ topoGraphOverride ?? (app === undefined ? undefined : deriveTopoGraph(app));
682
+ const topoEntry = findTopoEntry(topoGraph, trailId, 'trail');
683
+ const contours = topoEntry?.contours ?? [];
684
+ const contourDetails = contours
685
+ .map((contourId) => findTopoEntry(topoGraph, contourId, 'contour'))
686
+ .filter(
687
+ (entry): entry is TopoGraphContourEntry =>
688
+ entry !== undefined && entry.kind === 'contour'
689
+ );
690
+
691
+ return {
692
+ activationContext: activationContextFromTopoGraph(
693
+ topoGraph,
694
+ trailId,
695
+ fallbackActivationEdges
696
+ ),
697
+ activationEdges: trailActivationEdgesFromTopoGraph(
698
+ topoGraph,
699
+ trailId,
700
+ fallbackActivationEdges
701
+ ),
702
+ cli: topoEntry?.cli ?? null,
703
+ contourDetails,
704
+ contours,
705
+ fieldOverrides: topoEntry?.fieldOverrides ?? [],
706
+ governance: topoEntry?.governance ?? null,
707
+ input: topoEntry?.input ?? null,
708
+ layers: topoEntry?.layers ?? [],
709
+ output: topoEntry?.output ?? null,
710
+ surfaceProjections: deriveResolvedSurfaceProjections(
711
+ app,
712
+ trailId,
713
+ topoEntry,
714
+ topoGraph
715
+ ),
716
+ surfaces: topoEntry?.surfaces ?? [],
717
+ };
718
+ };
719
+
720
+ const formatTrailDetours = (item: AnyTrail): TrailDetailReport['detours'] =>
721
+ item.detours.length > 0
722
+ ? item.detours.map((d) => ({
723
+ maxAttempts: Math.max(
724
+ 1,
725
+ Math.min(d.maxAttempts ?? 1, DETOUR_MAX_ATTEMPTS_CAP)
726
+ ),
727
+ on: d.on.name,
728
+ }))
729
+ : null;
730
+
203
731
  export const deriveTrailDetail = (
204
- item: Trail<unknown, unknown>
732
+ item: AnyTrail,
733
+ app?: Topo | undefined,
734
+ activationGraph?: ActivationGraphReport | undefined,
735
+ options: TrailDetailOptions = {}
205
736
  ): TrailDetailReport => {
737
+ const activation =
738
+ app === undefined
739
+ ? deriveDeclaredTrailActivation(item)
740
+ : ((activationGraph ?? deriveActivationGraph(app)).trails.get(item.id) ??
741
+ deriveDeclaredTrailActivation(item));
206
742
  const safety = safetyLabel(
207
743
  item as unknown as { intent?: 'read' | 'write' | 'destroy' }
208
744
  );
209
745
 
746
+ const trailLayerNames = item.layers.map((layer) => layer.name);
747
+ const topoLayerNames = (app?.layers ?? []).map((layer) => layer.name);
748
+ const graphDetail = deriveResolvedTrailGraphDetail(
749
+ app,
750
+ item.id,
751
+ activation.edges,
752
+ options.topoGraph
753
+ );
754
+
210
755
  return {
756
+ activatedBy: activation.activatedBy,
757
+ activates: activation.activates,
758
+ activationChains: activation.chains,
759
+ activationContext: graphDetail.activationContext,
760
+ activationEdges: graphDetail.activationEdges,
761
+ activationSources: activation.sources,
762
+ cli: graphDetail.cli,
763
+ composedLayers: {
764
+ surface: normalizeSurfaceLayerNames(options.surfaceLayerNames),
765
+ topo: topoLayerNames,
766
+ trail: trailLayerNames,
767
+ },
768
+ contourDetails: graphDetail.contourDetails,
769
+ contours: graphDetail.contours,
211
770
  crosses: item.crosses.toSorted(),
212
771
  description: item.description ?? null,
213
- detours:
214
- item.detours.length > 0
215
- ? item.detours.map((d) => ({
216
- maxAttempts: d.maxAttempts ?? 1,
217
- on: d.on.name,
218
- }))
219
- : null,
772
+ detours: formatTrailDetours(item),
220
773
  examples: item.examples ?? [],
774
+ fieldOverrides: graphDetail.fieldOverrides,
775
+ fires: activation.fires,
776
+ governance: graphDetail.governance,
221
777
  id: item.id,
778
+ input: graphDetail.input,
222
779
  intent: item.intent,
223
- kind: item.kind,
780
+ kind: 'trail',
781
+ layers: graphDetail.layers,
782
+ on: activation.on,
783
+ output: graphDetail.output,
224
784
  pattern: item.pattern ?? null,
225
785
  resources: item.resources.map((resource) => resource.id).toSorted(),
226
786
  safety,
787
+ surfaceProjections: graphDetail.surfaceProjections,
788
+ surfaces: graphDetail.surfaces,
227
789
  };
228
790
  };