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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +197 -2
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +15 -5
  5. package/src/cli.ts +303 -10
  6. package/src/completions.ts +240 -0
  7. package/src/load-app-mirror.ts +160 -0
  8. package/src/local-state-io.ts +153 -0
  9. package/src/project-writes.ts +320 -0
  10. package/src/run-collision.ts +125 -0
  11. package/src/run-completions-install.ts +179 -0
  12. package/src/run-example.ts +149 -0
  13. package/src/run-examples.ts +148 -0
  14. package/src/run-quiet.ts +75 -0
  15. package/src/run-trace.ts +273 -0
  16. package/src/run-warden.ts +39 -0
  17. package/src/run-watch.ts +432 -0
  18. package/src/scaffold-versions.generated.ts +12 -0
  19. package/src/trails/add-surface.ts +45 -23
  20. package/src/trails/add-trail.ts +27 -17
  21. package/src/trails/add-verify.ts +57 -17
  22. package/src/trails/completions-complete.ts +165 -0
  23. package/src/trails/completions.ts +47 -0
  24. package/src/trails/create-scaffold.ts +86 -33
  25. package/src/trails/create.ts +11 -3
  26. package/src/trails/dev-clean.ts +6 -1
  27. package/src/trails/dev-reset.ts +6 -1
  28. package/src/trails/dev-stats.ts +6 -1
  29. package/src/trails/dev-support.ts +29 -17
  30. package/src/trails/draft-promote.ts +289 -80
  31. package/src/trails/guide.ts +54 -34
  32. package/src/trails/load-app.ts +251 -56
  33. package/src/trails/root-dir.ts +21 -0
  34. package/src/trails/run-example.ts +482 -0
  35. package/src/trails/run-examples.ts +141 -0
  36. package/src/trails/run.ts +403 -0
  37. package/src/trails/survey.ts +506 -200
  38. package/src/trails/topo-activation.ts +385 -0
  39. package/src/trails/topo-compile.ts +55 -0
  40. package/src/trails/topo-history.ts +6 -1
  41. package/src/trails/topo-output-schemas.ts +175 -0
  42. package/src/trails/topo-pin.ts +19 -6
  43. package/src/trails/topo-read-support.ts +171 -228
  44. package/src/trails/topo-reports.ts +400 -25
  45. package/src/trails/topo-store-support.ts +43 -19
  46. package/src/trails/topo-support.ts +18 -28
  47. package/src/trails/topo-unpin.ts +6 -1
  48. package/src/trails/topo-verify.ts +18 -5
  49. package/src/trails/topo.ts +60 -23
  50. package/src/trails/warden-guide.ts +121 -0
  51. package/src/trails/warden.ts +137 -56
  52. package/src/versions.ts +3 -18
  53. package/.turbo/turbo-build.log +0 -1
  54. package/.turbo/turbo-lint.log +0 -3
  55. package/.turbo/turbo-typecheck.log +0 -1
  56. package/__tests__/examples.test.ts +0 -45
  57. package/dist/bin/trails.d.ts +0 -3
  58. package/dist/bin/trails.d.ts.map +0 -1
  59. package/dist/bin/trails.js +0 -4
  60. package/dist/bin/trails.js.map +0 -1
  61. package/dist/src/app.d.ts +0 -2
  62. package/dist/src/app.d.ts.map +0 -1
  63. package/dist/src/app.js +0 -22
  64. package/dist/src/app.js.map +0 -1
  65. package/dist/src/clack.d.ts +0 -9
  66. package/dist/src/clack.d.ts.map +0 -1
  67. package/dist/src/clack.js +0 -84
  68. package/dist/src/clack.js.map +0 -1
  69. package/dist/src/cli.d.ts +0 -2
  70. package/dist/src/cli.d.ts.map +0 -1
  71. package/dist/src/cli.js +0 -14
  72. package/dist/src/cli.js.map +0 -1
  73. package/dist/src/trails/add-surface.d.ts +0 -13
  74. package/dist/src/trails/add-surface.d.ts.map +0 -1
  75. package/dist/src/trails/add-surface.js +0 -110
  76. package/dist/src/trails/add-surface.js.map +0 -1
  77. package/dist/src/trails/add-trail.d.ts +0 -12
  78. package/dist/src/trails/add-trail.d.ts.map +0 -1
  79. package/dist/src/trails/add-trail.js +0 -104
  80. package/dist/src/trails/add-trail.js.map +0 -1
  81. package/dist/src/trails/add-trailhead.d.ts +0 -13
  82. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  83. package/dist/src/trails/add-trailhead.js +0 -88
  84. package/dist/src/trails/add-trailhead.js.map +0 -1
  85. package/dist/src/trails/add-verify.d.ts +0 -10
  86. package/dist/src/trails/add-verify.d.ts.map +0 -1
  87. package/dist/src/trails/add-verify.js +0 -68
  88. package/dist/src/trails/add-verify.js.map +0 -1
  89. package/dist/src/trails/create-scaffold.d.ts +0 -15
  90. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  91. package/dist/src/trails/create-scaffold.js +0 -295
  92. package/dist/src/trails/create-scaffold.js.map +0 -1
  93. package/dist/src/trails/create.d.ts +0 -18
  94. package/dist/src/trails/create.d.ts.map +0 -1
  95. package/dist/src/trails/create.js +0 -126
  96. package/dist/src/trails/create.js.map +0 -1
  97. package/dist/src/trails/dev-clean.d.ts +0 -9
  98. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  99. package/dist/src/trails/dev-clean.js +0 -66
  100. package/dist/src/trails/dev-clean.js.map +0 -1
  101. package/dist/src/trails/dev-reset.d.ts +0 -6
  102. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  103. package/dist/src/trails/dev-reset.js +0 -39
  104. package/dist/src/trails/dev-reset.js.map +0 -1
  105. package/dist/src/trails/dev-stats.d.ts +0 -7
  106. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  107. package/dist/src/trails/dev-stats.js +0 -61
  108. package/dist/src/trails/dev-stats.js.map +0 -1
  109. package/dist/src/trails/dev-support.d.ts +0 -64
  110. package/dist/src/trails/dev-support.d.ts.map +0 -1
  111. package/dist/src/trails/dev-support.js +0 -181
  112. package/dist/src/trails/dev-support.js.map +0 -1
  113. package/dist/src/trails/draft-promote.d.ts +0 -18
  114. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  115. package/dist/src/trails/draft-promote.js +0 -400
  116. package/dist/src/trails/draft-promote.js.map +0 -1
  117. package/dist/src/trails/guide.d.ts +0 -21
  118. package/dist/src/trails/guide.d.ts.map +0 -1
  119. package/dist/src/trails/guide.js +0 -61
  120. package/dist/src/trails/guide.js.map +0 -1
  121. package/dist/src/trails/load-app.d.ts +0 -12
  122. package/dist/src/trails/load-app.d.ts.map +0 -1
  123. package/dist/src/trails/load-app.js +0 -415
  124. package/dist/src/trails/load-app.js.map +0 -1
  125. package/dist/src/trails/project.d.ts +0 -8
  126. package/dist/src/trails/project.d.ts.map +0 -1
  127. package/dist/src/trails/project.js +0 -54
  128. package/dist/src/trails/project.js.map +0 -1
  129. package/dist/src/trails/survey.d.ts +0 -18
  130. package/dist/src/trails/survey.d.ts.map +0 -1
  131. package/dist/src/trails/survey.js +0 -234
  132. package/dist/src/trails/survey.js.map +0 -1
  133. package/dist/src/trails/topo-constants.d.ts +0 -3
  134. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  135. package/dist/src/trails/topo-constants.js +0 -3
  136. package/dist/src/trails/topo-constants.js.map +0 -1
  137. package/dist/src/trails/topo-export.d.ts +0 -19
  138. package/dist/src/trails/topo-export.d.ts.map +0 -1
  139. package/dist/src/trails/topo-export.js +0 -31
  140. package/dist/src/trails/topo-export.js.map +0 -1
  141. package/dist/src/trails/topo-history.d.ts +0 -20
  142. package/dist/src/trails/topo-history.d.ts.map +0 -1
  143. package/dist/src/trails/topo-history.js +0 -32
  144. package/dist/src/trails/topo-history.js.map +0 -1
  145. package/dist/src/trails/topo-pin.d.ts +0 -17
  146. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  147. package/dist/src/trails/topo-pin.js +0 -31
  148. package/dist/src/trails/topo-pin.js.map +0 -1
  149. package/dist/src/trails/topo-read-support.d.ts +0 -58
  150. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  151. package/dist/src/trails/topo-read-support.js +0 -167
  152. package/dist/src/trails/topo-read-support.js.map +0 -1
  153. package/dist/src/trails/topo-reports.d.ts +0 -54
  154. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  155. package/dist/src/trails/topo-reports.js +0 -128
  156. package/dist/src/trails/topo-reports.js.map +0 -1
  157. package/dist/src/trails/topo-show.d.ts +0 -23
  158. package/dist/src/trails/topo-show.d.ts.map +0 -1
  159. package/dist/src/trails/topo-show.js +0 -49
  160. package/dist/src/trails/topo-show.js.map +0 -1
  161. package/dist/src/trails/topo-store-support.d.ts +0 -13
  162. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  163. package/dist/src/trails/topo-store-support.js +0 -55
  164. package/dist/src/trails/topo-store-support.js.map +0 -1
  165. package/dist/src/trails/topo-support.d.ts +0 -76
  166. package/dist/src/trails/topo-support.d.ts.map +0 -1
  167. package/dist/src/trails/topo-support.js +0 -132
  168. package/dist/src/trails/topo-support.js.map +0 -1
  169. package/dist/src/trails/topo-unpin.d.ts +0 -20
  170. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  171. package/dist/src/trails/topo-unpin.js +0 -44
  172. package/dist/src/trails/topo-unpin.js.map +0 -1
  173. package/dist/src/trails/topo-verify.d.ts +0 -5
  174. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  175. package/dist/src/trails/topo-verify.js +0 -24
  176. package/dist/src/trails/topo-verify.js.map +0 -1
  177. package/dist/src/trails/topo.d.ts +0 -5
  178. package/dist/src/trails/topo.d.ts.map +0 -1
  179. package/dist/src/trails/topo.js +0 -63
  180. package/dist/src/trails/topo.js.map +0 -1
  181. package/dist/src/trails/warden.d.ts +0 -20
  182. package/dist/src/trails/warden.d.ts.map +0 -1
  183. package/dist/src/trails/warden.js +0 -98
  184. package/dist/src/trails/warden.js.map +0 -1
  185. package/dist/src/versions.d.ts +0 -12
  186. package/dist/src/versions.d.ts.map +0 -1
  187. package/dist/src/versions.js +0 -23
  188. package/dist/src/versions.js.map +0 -1
  189. package/dist/tsconfig.tsbuildinfo +0 -1
  190. package/src/__tests__/add-trail.test.ts +0 -97
  191. package/src/__tests__/create.test.ts +0 -415
  192. package/src/__tests__/draft-promote.test.ts +0 -144
  193. package/src/__tests__/guide.test.ts +0 -96
  194. package/src/__tests__/load-app.test.ts +0 -419
  195. package/src/__tests__/survey.test.ts +0 -377
  196. package/src/__tests__/topo-dev.test.ts +0 -426
  197. package/src/__tests__/warden.test.ts +0 -74
  198. package/src/trails/topo-export.ts +0 -35
  199. package/src/trails/topo-show.ts +0 -54
  200. package/tsconfig.json +0 -9
  201. package/tsconfig.tests.json +0 -10
@@ -1,26 +1,84 @@
1
- import type { Topo, Trail } from '@ontrails/core';
1
+ import { DETOUR_MAX_ATTEMPTS_CAP, zodToJsonSchema } from '@ontrails/core';
2
+ import type { AnyTrail, Signal, Topo } from '@ontrails/core';
3
+ import { deriveTopoGraph } from '@ontrails/topographer';
4
+ import type {
5
+ JsonSchema,
6
+ TopoGraph,
7
+ TopoGraphActivationEdge,
8
+ TopoGraphEntry,
9
+ TopoGraphFieldOverride,
10
+ TopoGraphLayerReference,
11
+ } from '@ontrails/topographer';
12
+ import { z } from 'zod';
2
13
 
14
+ import type {
15
+ ActivationChainReport,
16
+ ActivationEdgeReport,
17
+ ActivationGraphReport,
18
+ ActivationOverviewReport,
19
+ ActivationSourceReport,
20
+ SignalActivationRelations,
21
+ } from './topo-activation.js';
22
+ import {
23
+ deriveActivationGraph,
24
+ deriveDeclaredTrailActivation,
25
+ deriveSignalActivationRelations,
26
+ } from './topo-activation.js';
3
27
  import { REPORT_CONTRACT_VERSION, REPORT_VERSION } from './topo-constants.js';
4
28
 
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;
29
+ export type {
30
+ ActivationChainReport,
31
+ ActivationEdgeReport,
32
+ ActivationGraphReport,
33
+ ActivationOverviewReport,
34
+ ActivationSourceReport,
35
+ SignalActivationRelations,
36
+ TrailActivationReport,
37
+ } from './topo-activation.js';
38
+
39
+ export const briefReportSchema = z.object({
40
+ contractVersion: z.string(),
41
+ features: z.object({
42
+ detours: z.boolean(),
43
+ examples: z.boolean(),
44
+ outputSchemas: z.boolean(),
45
+ resources: z.boolean(),
46
+ signals: z.boolean(),
47
+ }),
48
+ name: z.string(),
49
+ resources: z.number(),
50
+ signals: z.number(),
51
+ trails: z.number(),
52
+ version: z.string(),
53
+ });
54
+
55
+ type BriefReportShape = z.infer<typeof briefReportSchema>;
56
+
57
+ export type BriefReport = Readonly<
58
+ Omit<BriefReportShape, 'features'> & {
59
+ readonly features: Readonly<BriefReportShape['features']>;
60
+ }
61
+ >;
62
+
63
+ export type SurfaceLayerKey = 'cli' | 'http' | 'mcp';
64
+
65
+ export type SurfaceLayerNames = Readonly<
66
+ Record<SurfaceLayerKey, readonly string[]>
67
+ >;
68
+
69
+ type TopoGraphContourEntry = TopoGraphEntry & { readonly kind: 'contour' };
70
+
71
+ export interface TrailDetailOptions {
72
+ readonly surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined;
73
+ readonly topoGraph?: TopoGraph | undefined;
19
74
  }
20
75
 
21
76
  export interface SurveyListReport {
77
+ readonly activation: ActivationOverviewReport;
22
78
  readonly count: number;
23
79
  readonly entries: readonly {
80
+ readonly activatedBy: readonly string[];
81
+ readonly activates: readonly string[];
24
82
  readonly examples: number;
25
83
  readonly id: string;
26
84
  readonly kind: string;
@@ -35,21 +93,93 @@ export interface SurveyListReport {
35
93
  readonly lifetime: 'singleton';
36
94
  readonly usedBy: readonly string[];
37
95
  }[];
96
+ readonly signalCount: number;
97
+ readonly signals: readonly {
98
+ readonly consumers: readonly string[];
99
+ readonly description: string | null;
100
+ readonly examples: number;
101
+ readonly from: readonly string[];
102
+ readonly id: string;
103
+ readonly kind: 'signal';
104
+ readonly payloadSchema: boolean;
105
+ readonly producers: readonly string[];
106
+ }[];
38
107
  }
39
108
 
40
109
  export interface TrailDetailReport {
110
+ readonly activatedBy: readonly string[];
111
+ readonly activates: readonly string[];
112
+ readonly activationChains: readonly ActivationChainReport[];
113
+ readonly activationContext: {
114
+ readonly edgeCount: number;
115
+ readonly sourceCount: number;
116
+ readonly sourceKeys: readonly string[];
117
+ readonly trailIds: readonly string[];
118
+ };
119
+ readonly activationEdges: readonly ActivationEdgeReport[];
120
+ readonly activationSources: readonly ActivationSourceReport[];
121
+ readonly cli: {
122
+ readonly path: readonly string[];
123
+ } | null;
124
+ /**
125
+ * Composed layer names visible at the survey boundary.
126
+ *
127
+ * Reports the names of typed layers that wrap this trail at execution time,
128
+ * in the framework's composition order: `topo → surface → trail`
129
+ * (outermost-first). Surface-scope layers are keyed by surface because
130
+ * each surface owns its own attachment set.
131
+ */
132
+ readonly composedLayers: {
133
+ readonly topo: readonly string[];
134
+ readonly surface: SurfaceLayerNames;
135
+ readonly trail: readonly string[];
136
+ };
137
+ readonly contourDetails: readonly TopoGraphContourEntry[];
138
+ readonly contours: readonly string[];
41
139
  readonly description: string | null;
42
140
  readonly detours:
43
141
  | readonly { readonly on: string; readonly maxAttempts: number }[]
44
142
  | null;
45
143
  readonly examples: readonly unknown[];
144
+ readonly fieldOverrides: readonly TopoGraphFieldOverride[];
46
145
  readonly crosses: readonly string[];
146
+ readonly fires: readonly string[];
147
+ readonly governance: Readonly<Record<string, unknown>> | null;
47
148
  readonly id: string;
149
+ readonly input: JsonSchema | null;
48
150
  readonly intent: 'read' | 'write' | 'destroy';
49
- readonly kind: string;
151
+ readonly kind: 'trail';
152
+ readonly layers: readonly TopoGraphLayerReference[];
153
+ readonly on: readonly string[];
154
+ readonly output: JsonSchema | null;
50
155
  readonly pattern: string | null;
51
156
  readonly safety: string;
52
157
  readonly resources: readonly string[];
158
+ readonly surfaceProjections: readonly {
159
+ readonly derivedName: string;
160
+ readonly method: string | null;
161
+ readonly surface: string;
162
+ readonly trailId: string;
163
+ }[];
164
+ readonly surfaces: readonly string[];
165
+ }
166
+
167
+ export interface SignalDetailReport {
168
+ readonly consumers: readonly string[];
169
+ readonly description: string | null;
170
+ readonly examples: readonly unknown[];
171
+ readonly from: readonly string[];
172
+ readonly id: string;
173
+ readonly kind: 'signal';
174
+ /**
175
+ * The signal's payload schema (JSON Schema object), or `null` when the
176
+ * surface-map entry is missing for this signal. `null` is meaningful:
177
+ * it matches the list view's `payloadSchema: false` flag and lets
178
+ * consumers distinguish "schema not found" from "schema accepts any
179
+ * value" (the latter would be an empty object `{}`).
180
+ */
181
+ readonly payload: Readonly<Record<string, unknown>> | null;
182
+ readonly producers: readonly string[];
53
183
  }
54
184
 
55
185
  const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
@@ -170,9 +300,40 @@ const formatResourceList = (app: Topo): SurveyListReport['resources'] => {
170
300
  .toSorted((a, b) => a.id.localeCompare(b.id));
171
301
  };
172
302
 
303
+ const formatSignalList = (
304
+ app: Topo,
305
+ relations: ReadonlyMap<string, SignalActivationRelations>
306
+ ): SurveyListReport['signals'] =>
307
+ app
308
+ .listSignals()
309
+ .map((signalDef) => {
310
+ const related = relations.get(signalDef.id);
311
+ const consumers = related?.consumers ?? [];
312
+ const producers = related?.producers ?? [];
313
+ return {
314
+ consumers,
315
+ description: signalDef.description ?? null,
316
+ examples: signalDef.examples?.length ?? 0,
317
+ from: signalDef.from?.toSorted() ?? [],
318
+ id: signalDef.id,
319
+ kind: signalDef.kind,
320
+ // Mirror the store path (`mapSignalRow` in `topo-store-read.ts`) which
321
+ // derives this from the surface-map entry. SignalSpec<T> requires
322
+ // `payload` so this is `true` in practice today; the explicit check
323
+ // keeps the in-memory and store reports self-consistent if a future
324
+ // SignalSpec variant ever omits `payload`.
325
+ payloadSchema: signalDef.payload !== undefined,
326
+ producers,
327
+ };
328
+ })
329
+ .toSorted((a, b) => a.id.localeCompare(b.id));
330
+
173
331
  export const deriveSurveyList = (app: Topo): SurveyListReport => {
174
332
  const items = app.list();
333
+ const activation = deriveActivationGraph(app);
175
334
  const entries = items.map((item) => {
335
+ const trailActivation =
336
+ activation.trails.get(item.id) ?? deriveDeclaredTrailActivation(item);
176
337
  const safety = safetyLabel(
177
338
  item as unknown as { intent?: 'read' | 'write' | 'destroy' }
178
339
  );
@@ -183,6 +344,8 @@ export const deriveSurveyList = (app: Topo): SurveyListReport => {
183
344
  : 0;
184
345
 
185
346
  return {
347
+ activatedBy: trailActivation.activatedBy,
348
+ activates: trailActivation.activates,
186
349
  examples,
187
350
  id: item.id,
188
351
  kind: item.kind,
@@ -191,38 +354,250 @@ export const deriveSurveyList = (app: Topo): SurveyListReport => {
191
354
  });
192
355
 
193
356
  const resources = formatResourceList(app);
357
+ const signals = formatSignalList(app, activation.signals);
194
358
 
195
359
  return {
360
+ activation: activation.overview,
196
361
  count: items.length,
197
362
  entries,
198
363
  resourceCount: resources.length,
199
364
  resources,
365
+ signalCount: signals.length,
366
+ signals,
200
367
  };
201
368
  };
202
369
 
370
+ export const deriveSignalDetail = (
371
+ app: Topo,
372
+ signalId: string,
373
+ activationGraph?: ActivationGraphReport | undefined
374
+ ): SignalDetailReport | undefined => {
375
+ const signalDef = app.signals.get(signalId) as Signal<unknown> | undefined;
376
+ if (signalDef === undefined) {
377
+ return undefined;
378
+ }
379
+ const related =
380
+ activationGraph?.signals.get(signalId) ??
381
+ deriveSignalActivationRelations(app, signalId);
382
+
383
+ return {
384
+ consumers: [...related.consumers],
385
+ description: signalDef.description ?? null,
386
+ examples: [...(signalDef.examples ?? [])],
387
+ from: signalDef.from?.toSorted() ?? [],
388
+ id: signalDef.id,
389
+ kind: 'signal',
390
+ payload: zodToJsonSchema(signalDef.payload),
391
+ producers: [...related.producers],
392
+ };
393
+ };
394
+
395
+ const emptySurfaceLayerNames = (): SurfaceLayerNames => ({
396
+ cli: [],
397
+ http: [],
398
+ mcp: [],
399
+ });
400
+
401
+ const normalizeSurfaceLayerNames = (
402
+ names?: Partial<SurfaceLayerNames> | undefined
403
+ ): SurfaceLayerNames => {
404
+ const base = emptySurfaceLayerNames();
405
+ if (names === undefined) {
406
+ return base;
407
+ }
408
+ return {
409
+ cli: names.cli ?? base.cli,
410
+ http: names.http ?? base.http,
411
+ mcp: names.mcp ?? base.mcp,
412
+ };
413
+ };
414
+
415
+ const emptyActivationContext = (): TrailDetailReport['activationContext'] => ({
416
+ edgeCount: 0,
417
+ sourceCount: 0,
418
+ sourceKeys: [],
419
+ trailIds: [],
420
+ });
421
+
422
+ const activationContextFromTopoGraph = (
423
+ topoGraph: TopoGraph | undefined,
424
+ trailId: string,
425
+ fallbackEdges: readonly TopoGraphActivationEdge[]
426
+ ): TrailDetailReport['activationContext'] => {
427
+ const edges =
428
+ topoGraph?.activationGraph.edges.filter(
429
+ (edge) => edge.trailId === trailId
430
+ ) ?? fallbackEdges;
431
+ if (edges.length === 0) {
432
+ return emptyActivationContext();
433
+ }
434
+ return {
435
+ edgeCount: edges.length,
436
+ sourceCount: new Set(edges.map((edge) => edge.sourceKey)).size,
437
+ sourceKeys: [...new Set(edges.map((edge) => edge.sourceKey))].toSorted(),
438
+ trailIds: [...new Set(edges.map((edge) => edge.trailId))].toSorted(),
439
+ };
440
+ };
441
+
442
+ const findTopoEntry = (
443
+ topoGraph: TopoGraph | undefined,
444
+ id: string,
445
+ kind: TopoGraphEntry['kind']
446
+ ): TopoGraphEntry | undefined =>
447
+ topoGraph?.entries.find((entry) => entry.id === id && entry.kind === kind);
448
+
449
+ const trailActivationEdgesFromTopoGraph = (
450
+ topoGraph: TopoGraph | undefined,
451
+ trailId: string,
452
+ fallback: readonly TopoGraphActivationEdge[]
453
+ ): readonly TopoGraphActivationEdge[] =>
454
+ topoGraph?.activationGraph.edges.filter((edge) => edge.trailId === trailId) ??
455
+ fallback;
456
+
457
+ const deriveSurfaceProjections = (
458
+ entry: TopoGraphEntry | undefined
459
+ ): TrailDetailReport['surfaceProjections'] => {
460
+ if (entry === undefined) {
461
+ return [];
462
+ }
463
+
464
+ const cliProjection =
465
+ entry.cli === undefined
466
+ ? []
467
+ : [
468
+ {
469
+ derivedName: entry.cli.path.join(' '),
470
+ method: null,
471
+ surface: 'cli',
472
+ trailId: entry.id,
473
+ },
474
+ ];
475
+ return cliProjection.toSorted((a, b) => a.surface.localeCompare(b.surface));
476
+ };
477
+
478
+ const deriveResolvedTrailGraphDetail = (
479
+ app: Topo | undefined,
480
+ trailId: string,
481
+ fallbackActivationEdges: readonly TopoGraphActivationEdge[],
482
+ topoGraphOverride?: TopoGraph | undefined
483
+ ): Pick<
484
+ TrailDetailReport,
485
+ | 'activationContext'
486
+ | 'activationEdges'
487
+ | 'cli'
488
+ | 'contourDetails'
489
+ | 'contours'
490
+ | 'fieldOverrides'
491
+ | 'governance'
492
+ | 'input'
493
+ | 'layers'
494
+ | 'output'
495
+ | 'surfaceProjections'
496
+ | 'surfaces'
497
+ > => {
498
+ const topoGraph =
499
+ topoGraphOverride ?? (app === undefined ? undefined : deriveTopoGraph(app));
500
+ const topoEntry = findTopoEntry(topoGraph, trailId, 'trail');
501
+ const contours = topoEntry?.contours ?? [];
502
+ const contourDetails = contours
503
+ .map((contourId) => findTopoEntry(topoGraph, contourId, 'contour'))
504
+ .filter(
505
+ (entry): entry is TopoGraphContourEntry =>
506
+ entry !== undefined && entry.kind === 'contour'
507
+ );
508
+
509
+ return {
510
+ activationContext: activationContextFromTopoGraph(
511
+ topoGraph,
512
+ trailId,
513
+ fallbackActivationEdges
514
+ ),
515
+ activationEdges: trailActivationEdgesFromTopoGraph(
516
+ topoGraph,
517
+ trailId,
518
+ fallbackActivationEdges
519
+ ),
520
+ cli: topoEntry?.cli ?? null,
521
+ contourDetails,
522
+ contours,
523
+ fieldOverrides: topoEntry?.fieldOverrides ?? [],
524
+ governance: topoEntry?.governance ?? null,
525
+ input: topoEntry?.input ?? null,
526
+ layers: topoEntry?.layers ?? [],
527
+ output: topoEntry?.output ?? null,
528
+ surfaceProjections: deriveSurfaceProjections(topoEntry),
529
+ surfaces: topoEntry?.surfaces ?? [],
530
+ };
531
+ };
532
+
533
+ const formatTrailDetours = (item: AnyTrail): TrailDetailReport['detours'] =>
534
+ item.detours.length > 0
535
+ ? item.detours.map((d) => ({
536
+ maxAttempts: Math.max(
537
+ 1,
538
+ Math.min(d.maxAttempts ?? 1, DETOUR_MAX_ATTEMPTS_CAP)
539
+ ),
540
+ on: d.on.name,
541
+ }))
542
+ : null;
543
+
203
544
  export const deriveTrailDetail = (
204
- item: Trail<unknown, unknown>
545
+ item: AnyTrail,
546
+ app?: Topo | undefined,
547
+ activationGraph?: ActivationGraphReport | undefined,
548
+ options: TrailDetailOptions = {}
205
549
  ): TrailDetailReport => {
550
+ const activation =
551
+ app === undefined
552
+ ? deriveDeclaredTrailActivation(item)
553
+ : ((activationGraph ?? deriveActivationGraph(app)).trails.get(item.id) ??
554
+ deriveDeclaredTrailActivation(item));
206
555
  const safety = safetyLabel(
207
556
  item as unknown as { intent?: 'read' | 'write' | 'destroy' }
208
557
  );
209
558
 
559
+ const trailLayerNames = item.layers.map((layer) => layer.name);
560
+ const topoLayerNames = (app?.layers ?? []).map((layer) => layer.name);
561
+ const graphDetail = deriveResolvedTrailGraphDetail(
562
+ app,
563
+ item.id,
564
+ activation.edges,
565
+ options.topoGraph
566
+ );
567
+
210
568
  return {
569
+ activatedBy: activation.activatedBy,
570
+ activates: activation.activates,
571
+ activationChains: activation.chains,
572
+ activationContext: graphDetail.activationContext,
573
+ activationEdges: graphDetail.activationEdges,
574
+ activationSources: activation.sources,
575
+ cli: graphDetail.cli,
576
+ composedLayers: {
577
+ surface: normalizeSurfaceLayerNames(options.surfaceLayerNames),
578
+ topo: topoLayerNames,
579
+ trail: trailLayerNames,
580
+ },
581
+ contourDetails: graphDetail.contourDetails,
582
+ contours: graphDetail.contours,
211
583
  crosses: item.crosses.toSorted(),
212
584
  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,
585
+ detours: formatTrailDetours(item),
220
586
  examples: item.examples ?? [],
587
+ fieldOverrides: graphDetail.fieldOverrides,
588
+ fires: activation.fires,
589
+ governance: graphDetail.governance,
221
590
  id: item.id,
591
+ input: graphDetail.input,
222
592
  intent: item.intent,
223
- kind: item.kind,
593
+ kind: 'trail',
594
+ layers: graphDetail.layers,
595
+ on: activation.on,
596
+ output: graphDetail.output,
224
597
  pattern: item.pattern ?? null,
225
598
  resources: item.resources.map((resource) => resource.id).toSorted(),
226
599
  safety,
600
+ surfaceProjections: graphDetail.surfaceProjections,
601
+ surfaces: graphDetail.surfaces,
227
602
  };
228
603
  };
@@ -5,19 +5,26 @@
5
5
  * keeping module boundaries clean.
6
6
  */
7
7
 
8
- import type { Topo, TopoSnapshot } from '@ontrails/core';
9
- import { InternalError, Result } from '@ontrails/core';
10
- import type { StoredTopoExport } from '@ontrails/core/internal/topo-store';
11
- import {
12
- createTopoSnapshot,
13
- getStoredTopoExport,
14
- } from '@ontrails/core/internal/topo-store';
8
+ import { Database } from 'bun:sqlite';
9
+
10
+ import type { Topo } from '@ontrails/core';
15
11
  import {
16
- openWriteTrailsDb,
17
12
  deriveTrailsDir,
18
- } from '@ontrails/core/internal/trails-db';
19
- import type { SurfaceLock, SurfaceMap } from '@ontrails/schema';
20
- import { writeSurfaceLock, writeSurfaceMap } from '@ontrails/schema';
13
+ InternalError,
14
+ openWriteTrailsDb,
15
+ Result,
16
+ } from '@ontrails/core';
17
+ import type {
18
+ LockManifest,
19
+ TopoGraph,
20
+ TopoSnapshot,
21
+ } from '@ontrails/topographer';
22
+ import type { StoredTopoExport } from '@ontrails/topographer/backend-support';
23
+ import { writeLockManifest, writeTopoGraph } from '@ontrails/topographer';
24
+ import {
25
+ createStoredTopoSnapshot,
26
+ getStoredTopoExport,
27
+ } from '@ontrails/topographer/backend-support';
21
28
 
22
29
  import type { TopoExportReport } from './topo-support.js';
23
30
  import {
@@ -34,7 +41,7 @@ const persistAndReadStoredExport = (
34
41
  { snapshot: TopoSnapshot; storedExport: StoredTopoExport },
35
42
  Error
36
43
  > => {
37
- const snapshotResult = createTopoSnapshot(db, app, {
44
+ const snapshotResult = createStoredTopoSnapshot(db, app, {
38
45
  ...readGitState(rootDir),
39
46
  ...deriveTopoCounts(app),
40
47
  });
@@ -59,23 +66,40 @@ const persistAndReadStoredExport = (
59
66
  });
60
67
  };
61
68
 
69
+ export const deriveCurrentTopoExport = (
70
+ app: Topo,
71
+ options?: { readonly rootDir?: string }
72
+ ): Result<StoredTopoExport, Error> => {
73
+ const rootDir = deriveRootDir(options?.rootDir);
74
+ const db = new Database(':memory:');
75
+
76
+ try {
77
+ const projected = persistAndReadStoredExport(app, db, rootDir);
78
+ return projected.isErr()
79
+ ? projected
80
+ : Result.ok(projected.value.storedExport);
81
+ } finally {
82
+ db.close();
83
+ }
84
+ };
85
+
62
86
  const writeStoredExportArtifacts = async (
63
87
  storedExport: StoredTopoExport,
64
88
  trailsDir: string
65
- ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'mapPath'>> => {
66
- const mapPath = await writeSurfaceMap(
67
- JSON.parse(storedExport.surfaceMapJson) as SurfaceMap,
89
+ ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>> => {
90
+ const topoPath = await writeTopoGraph(
91
+ JSON.parse(storedExport.topoGraphJson) as TopoGraph,
68
92
  { dir: trailsDir }
69
93
  );
70
- const lockPath = await writeSurfaceLock(
71
- JSON.parse(storedExport.lockContent) as SurfaceLock,
94
+ const lockPath = await writeLockManifest(
95
+ JSON.parse(storedExport.lockManifestJson) as LockManifest,
72
96
  { dir: trailsDir }
73
97
  );
74
98
 
75
99
  return {
76
- hash: storedExport.surfaceHash,
100
+ hash: storedExport.topoGraphHash,
77
101
  lockPath,
78
- mapPath,
102
+ topoPath,
79
103
  };
80
104
  };
81
105
 
@@ -1,18 +1,23 @@
1
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { existsSync } from 'node:fs';
4
2
  import { fileURLToPath } from 'node:url';
5
3
 
4
+ import { deriveTrailsDbPath } from '@ontrails/core';
5
+ import type { Topo } from '@ontrails/core';
6
6
  import {
7
7
  createTopoSnapshot as persistTopoSnapshot,
8
8
  listTopoSnapshots as readTopoSnapshots,
9
9
  pinTopoSnapshot,
10
10
  unpinTopoSnapshot,
11
- } from '@ontrails/core';
12
- import type { Topo, TopoSnapshot } from '@ontrails/core';
13
- import { deriveTrailsDbPath } from '@ontrails/core/internal/trails-db';
11
+ } from '@ontrails/topographer';
12
+ import type { TopoSnapshot } from '@ontrails/topographer';
14
13
  import { z } from 'zod';
15
14
 
15
+ import {
16
+ createIsolatedExampleRoot,
17
+ writeIsolatedExampleAppModule,
18
+ } from '../local-state-io.js';
19
+
20
+ import { requireTrailRootDir } from './root-dir.js';
16
21
  import type { BriefReport, SurveyListReport } from './topo-reports.js';
17
22
 
18
23
  /** Output schema for a topo snapshot record. Shared across topo trails. */
@@ -27,11 +32,13 @@ export const topoSnapshotOutput = z.object({
27
32
  trailCount: z.number(),
28
33
  });
29
34
 
30
- export const DEFAULT_APP_MODULE = './src/app.ts';
31
35
  export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
32
36
  export const LOCK_PATH = '.trails/trails.lock';
33
37
  const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
34
38
 
39
+ const uniqueExampleRootName = (name: string): string =>
40
+ `${name}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
41
+
35
42
  export interface TopoSummaryReport {
36
43
  readonly app: BriefReport;
37
44
  readonly dbPath: string;
@@ -51,8 +58,8 @@ export interface TopoHistoryReport {
51
58
  export interface TopoExportReport {
52
59
  readonly hash: string;
53
60
  readonly lockPath: string;
54
- readonly mapPath: string;
55
61
  readonly snapshot: TopoSnapshot;
62
+ readonly topoPath: string;
56
63
  }
57
64
 
58
65
  export interface TopoVerifyReport {
@@ -62,7 +69,7 @@ export interface TopoVerifyReport {
62
69
  readonly stale: false;
63
70
  }
64
71
 
65
- export const deriveRootDir = (cwd?: string): string => cwd ?? process.cwd();
72
+ export const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
66
73
 
67
74
  const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
68
75
  const proc = Bun.spawnSync({
@@ -137,30 +144,13 @@ const buildSnapshotInput = (
137
144
  export const createIsolatedExampleInput = (
138
145
  name: string
139
146
  ): { readonly module: string; readonly rootDir: string } => {
140
- const rootDir = join(tmpdir(), 'ontrails-trails-examples', name);
141
- rmSync(rootDir, { force: true, recursive: true });
142
- mkdirSync(rootDir, { recursive: true });
147
+ const rootDir = createIsolatedExampleRoot(uniqueExampleRootName(name));
143
148
  return {
144
- module: EXAMPLE_APP_MODULE,
149
+ module: writeIsolatedExampleAppModule(rootDir, EXAMPLE_APP_MODULE),
145
150
  rootDir,
146
151
  };
147
152
  };
148
153
 
149
- export const createCurrentTopoSnapshot = (
150
- app: Topo,
151
- options?: { readonly rootDir?: string }
152
- ): TopoSnapshot => {
153
- const rootDir = deriveRootDir(options?.rootDir);
154
- const result = persistTopoSnapshot(app, {
155
- rootDir,
156
- ...buildSnapshotInput(app, rootDir),
157
- });
158
- if (result.isErr()) {
159
- throw result.error;
160
- }
161
- return result.value;
162
- };
163
-
164
154
  export const listTopoHistory = (options?: {
165
155
  readonly limit?: number;
166
156
  readonly rootDir?: string;