@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.22

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