@ontrails/trails 1.0.0-beta.14 → 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 (197) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +17 -7
  5. package/src/clack.ts +1 -1
  6. package/src/cli.ts +304 -10
  7. package/src/completions.ts +240 -0
  8. package/src/load-app-mirror.ts +160 -0
  9. package/src/local-state-io.ts +153 -0
  10. package/src/project-writes.ts +320 -0
  11. package/src/run-collision.ts +125 -0
  12. package/src/run-completions-install.ts +179 -0
  13. package/src/run-example.ts +149 -0
  14. package/src/run-examples.ts +148 -0
  15. package/src/run-quiet.ts +75 -0
  16. package/src/run-trace.ts +273 -0
  17. package/src/run-warden.ts +39 -0
  18. package/src/run-watch.ts +432 -0
  19. package/src/scaffold-versions.generated.ts +12 -0
  20. package/src/trails/add-surface.ts +172 -0
  21. package/src/trails/add-trail.ts +73 -27
  22. package/src/trails/add-verify.ts +68 -23
  23. package/src/trails/completions-complete.ts +165 -0
  24. package/src/trails/completions.ts +47 -0
  25. package/src/trails/create-scaffold.ts +101 -35
  26. package/src/trails/create.ts +87 -74
  27. package/src/trails/dev-clean.ts +31 -22
  28. package/src/trails/dev-reset.ts +9 -3
  29. package/src/trails/dev-stats.ts +28 -20
  30. package/src/trails/dev-support.ts +109 -95
  31. package/src/trails/draft-promote.ts +351 -107
  32. package/src/trails/guide.ts +55 -38
  33. package/src/trails/load-app.ts +712 -38
  34. package/src/trails/root-dir.ts +21 -0
  35. package/src/trails/run-example.ts +482 -0
  36. package/src/trails/run-examples.ts +141 -0
  37. package/src/trails/run.ts +403 -0
  38. package/src/trails/survey.ts +517 -186
  39. package/src/trails/topo-activation.ts +385 -0
  40. package/src/trails/topo-compile.ts +55 -0
  41. package/src/trails/topo-history.ts +14 -11
  42. package/src/trails/topo-output-schemas.ts +175 -0
  43. package/src/trails/topo-pin.ts +25 -16
  44. package/src/trails/topo-read-support.ts +178 -238
  45. package/src/trails/topo-reports.ts +445 -63
  46. package/src/trails/topo-store-support.ts +67 -35
  47. package/src/trails/topo-support.ts +93 -147
  48. package/src/trails/topo-unpin.ts +17 -7
  49. package/src/trails/topo-verify.ts +19 -10
  50. package/src/trails/topo.ts +64 -31
  51. package/src/trails/warden-guide.ts +121 -0
  52. package/src/trails/warden.ts +137 -47
  53. package/src/versions.ts +28 -0
  54. package/.turbo/turbo-build.log +0 -1
  55. package/.turbo/turbo-lint.log +0 -3
  56. package/.turbo/turbo-typecheck.log +0 -1
  57. package/__tests__/examples.test.ts +0 -20
  58. package/dist/bin/trails.d.ts +0 -3
  59. package/dist/bin/trails.d.ts.map +0 -1
  60. package/dist/bin/trails.js +0 -4
  61. package/dist/bin/trails.js.map +0 -1
  62. package/dist/src/app.d.ts +0 -2
  63. package/dist/src/app.d.ts.map +0 -1
  64. package/dist/src/app.js +0 -22
  65. package/dist/src/app.js.map +0 -1
  66. package/dist/src/clack.d.ts +0 -9
  67. package/dist/src/clack.d.ts.map +0 -1
  68. package/dist/src/clack.js +0 -84
  69. package/dist/src/clack.js.map +0 -1
  70. package/dist/src/cli.d.ts +0 -2
  71. package/dist/src/cli.d.ts.map +0 -1
  72. package/dist/src/cli.js +0 -13
  73. package/dist/src/cli.js.map +0 -1
  74. package/dist/src/trails/add-surface.d.ts +0 -13
  75. package/dist/src/trails/add-surface.d.ts.map +0 -1
  76. package/dist/src/trails/add-surface.js +0 -88
  77. package/dist/src/trails/add-surface.js.map +0 -1
  78. package/dist/src/trails/add-trail.d.ts +0 -10
  79. package/dist/src/trails/add-trail.d.ts.map +0 -1
  80. package/dist/src/trails/add-trail.js +0 -77
  81. package/dist/src/trails/add-trail.js.map +0 -1
  82. package/dist/src/trails/add-trailhead.d.ts +0 -13
  83. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  84. package/dist/src/trails/add-trailhead.js +0 -88
  85. package/dist/src/trails/add-trailhead.js.map +0 -1
  86. package/dist/src/trails/add-verify.d.ts +0 -10
  87. package/dist/src/trails/add-verify.d.ts.map +0 -1
  88. package/dist/src/trails/add-verify.js +0 -67
  89. package/dist/src/trails/add-verify.js.map +0 -1
  90. package/dist/src/trails/create-scaffold.d.ts +0 -15
  91. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  92. package/dist/src/trails/create-scaffold.js +0 -288
  93. package/dist/src/trails/create-scaffold.js.map +0 -1
  94. package/dist/src/trails/create.d.ts +0 -22
  95. package/dist/src/trails/create.d.ts.map +0 -1
  96. package/dist/src/trails/create.js +0 -121
  97. package/dist/src/trails/create.js.map +0 -1
  98. package/dist/src/trails/dev-clean.d.ts +0 -9
  99. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  100. package/dist/src/trails/dev-clean.js +0 -65
  101. package/dist/src/trails/dev-clean.js.map +0 -1
  102. package/dist/src/trails/dev-reset.d.ts +0 -6
  103. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  104. package/dist/src/trails/dev-reset.js +0 -38
  105. package/dist/src/trails/dev-reset.js.map +0 -1
  106. package/dist/src/trails/dev-stats.d.ts +0 -7
  107. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  108. package/dist/src/trails/dev-stats.js +0 -61
  109. package/dist/src/trails/dev-stats.js.map +0 -1
  110. package/dist/src/trails/dev-support.d.ts +0 -64
  111. package/dist/src/trails/dev-support.d.ts.map +0 -1
  112. package/dist/src/trails/dev-support.js +0 -178
  113. package/dist/src/trails/dev-support.js.map +0 -1
  114. package/dist/src/trails/draft-promote.d.ts +0 -18
  115. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  116. package/dist/src/trails/draft-promote.js +0 -386
  117. package/dist/src/trails/draft-promote.js.map +0 -1
  118. package/dist/src/trails/guide.d.ts +0 -21
  119. package/dist/src/trails/guide.d.ts.map +0 -1
  120. package/dist/src/trails/guide.js +0 -64
  121. package/dist/src/trails/guide.js.map +0 -1
  122. package/dist/src/trails/load-app.d.ts +0 -6
  123. package/dist/src/trails/load-app.d.ts.map +0 -1
  124. package/dist/src/trails/load-app.js +0 -67
  125. package/dist/src/trails/load-app.js.map +0 -1
  126. package/dist/src/trails/project.d.ts +0 -8
  127. package/dist/src/trails/project.d.ts.map +0 -1
  128. package/dist/src/trails/project.js +0 -54
  129. package/dist/src/trails/project.js.map +0 -1
  130. package/dist/src/trails/survey.d.ts +0 -18
  131. package/dist/src/trails/survey.d.ts.map +0 -1
  132. package/dist/src/trails/survey.js +0 -212
  133. package/dist/src/trails/survey.js.map +0 -1
  134. package/dist/src/trails/topo-constants.d.ts +0 -3
  135. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  136. package/dist/src/trails/topo-constants.js +0 -3
  137. package/dist/src/trails/topo-constants.js.map +0 -1
  138. package/dist/src/trails/topo-export.d.ts +0 -18
  139. package/dist/src/trails/topo-export.d.ts.map +0 -1
  140. package/dist/src/trails/topo-export.js +0 -34
  141. package/dist/src/trails/topo-export.js.map +0 -1
  142. package/dist/src/trails/topo-history.d.ts +0 -24
  143. package/dist/src/trails/topo-history.d.ts.map +0 -1
  144. package/dist/src/trails/topo-history.js +0 -33
  145. package/dist/src/trails/topo-history.js.map +0 -1
  146. package/dist/src/trails/topo-pin.d.ts +0 -21
  147. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  148. package/dist/src/trails/topo-pin.js +0 -35
  149. package/dist/src/trails/topo-pin.js.map +0 -1
  150. package/dist/src/trails/topo-read-support.d.ts +0 -54
  151. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  152. package/dist/src/trails/topo-read-support.js +0 -178
  153. package/dist/src/trails/topo-read-support.js.map +0 -1
  154. package/dist/src/trails/topo-reports.d.ts +0 -50
  155. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  156. package/dist/src/trails/topo-reports.js +0 -122
  157. package/dist/src/trails/topo-reports.js.map +0 -1
  158. package/dist/src/trails/topo-show.d.ts +0 -23
  159. package/dist/src/trails/topo-show.d.ts.map +0 -1
  160. package/dist/src/trails/topo-show.js +0 -53
  161. package/dist/src/trails/topo-show.js.map +0 -1
  162. package/dist/src/trails/topo-store-support.d.ts +0 -13
  163. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  164. package/dist/src/trails/topo-store-support.js +0 -55
  165. package/dist/src/trails/topo-store-support.js.map +0 -1
  166. package/dist/src/trails/topo-support.d.ts +0 -87
  167. package/dist/src/trails/topo-support.d.ts.map +0 -1
  168. package/dist/src/trails/topo-support.js +0 -165
  169. package/dist/src/trails/topo-support.js.map +0 -1
  170. package/dist/src/trails/topo-unpin.d.ts +0 -15
  171. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  172. package/dist/src/trails/topo-unpin.js +0 -39
  173. package/dist/src/trails/topo-unpin.js.map +0 -1
  174. package/dist/src/trails/topo-verify.d.ts +0 -5
  175. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  176. package/dist/src/trails/topo-verify.js +0 -28
  177. package/dist/src/trails/topo-verify.js.map +0 -1
  178. package/dist/src/trails/topo.d.ts +0 -5
  179. package/dist/src/trails/topo.d.ts.map +0 -1
  180. package/dist/src/trails/topo.js +0 -67
  181. package/dist/src/trails/topo.js.map +0 -1
  182. package/dist/src/trails/warden.d.ts +0 -19
  183. package/dist/src/trails/warden.d.ts.map +0 -1
  184. package/dist/src/trails/warden.js +0 -89
  185. package/dist/src/trails/warden.js.map +0 -1
  186. package/dist/tsconfig.tsbuildinfo +0 -1
  187. package/src/__tests__/create.test.ts +0 -351
  188. package/src/__tests__/draft-promote.test.ts +0 -144
  189. package/src/__tests__/guide.test.ts +0 -91
  190. package/src/__tests__/load-app.test.ts +0 -58
  191. package/src/__tests__/survey.test.ts +0 -301
  192. package/src/__tests__/topo-dev.test.ts +0 -424
  193. package/src/__tests__/warden.test.ts +0 -74
  194. package/src/trails/add-trailhead.ts +0 -121
  195. package/src/trails/topo-export.ts +0 -39
  196. package/src/trails/topo-show.ts +0 -58
  197. package/tsconfig.json +0 -9
@@ -0,0 +1,21 @@
1
+ import { Result, ValidationError } from '@ontrails/core';
2
+
3
+ const ROOT_DIR_MESSAGE =
4
+ 'Trail execution requires rootDir input or ctx.cwd from the runtime context.';
5
+
6
+ export const resolveTrailRootDir = (
7
+ rootDir: string | undefined,
8
+ cwd: string | undefined
9
+ ): Result<string, ValidationError> => {
10
+ const resolved = rootDir ?? cwd;
11
+ return resolved === undefined
12
+ ? Result.err(new ValidationError(ROOT_DIR_MESSAGE))
13
+ : Result.ok(resolved);
14
+ };
15
+
16
+ export const requireTrailRootDir = (rootDir: string | undefined): string => {
17
+ if (rootDir === undefined) {
18
+ throw new ValidationError(ROOT_DIR_MESSAGE);
19
+ }
20
+ return rootDir;
21
+ };
@@ -0,0 +1,482 @@
1
+ /**
2
+ * `run.example` trail -- run one named example and compare the result.
3
+ */
4
+
5
+ import {
6
+ NotFoundError,
7
+ Result,
8
+ TrailsError,
9
+ deriveStructuredTrailExamples,
10
+ run,
11
+ trail,
12
+ } from '@ontrails/core';
13
+ import type { StructuredTrailExample, Topo } from '@ontrails/core';
14
+ import { z } from 'zod';
15
+
16
+ import { tryLoadFreshAppLease } from './load-app.js';
17
+ import { resolveRunModulePath } from './run.js';
18
+ import { resolveTrailRootDir } from './root-dir.js';
19
+ import { createIsolatedExampleInput } from './topo-support.js';
20
+
21
+ export const RUN_EXAMPLE_COMPARISON_KIND = 'example-comparison' as const;
22
+
23
+ export const runExampleComparisonSchema = z.object({
24
+ actual: z.unknown(),
25
+ diff: z.array(z.string()).readonly().optional(),
26
+ exampleName: z.string(),
27
+ expected: z.unknown(),
28
+ input: z.unknown(),
29
+ kind: z.literal(RUN_EXAMPLE_COMPARISON_KIND),
30
+ match: z.boolean(),
31
+ mode: z.union([
32
+ z.literal('expected'),
33
+ z.literal('expectedMatch'),
34
+ z.literal('error'),
35
+ z.literal('none'),
36
+ ]),
37
+ trailId: z.string(),
38
+ });
39
+
40
+ export type RunExampleComparison = z.infer<typeof runExampleComparisonSchema>;
41
+ export type RunExampleComparisonMode = RunExampleComparison['mode'];
42
+
43
+ interface ActualOutcomeOk {
44
+ readonly outcome: 'ok';
45
+ readonly value: unknown;
46
+ }
47
+
48
+ interface ActualOutcomeErr {
49
+ readonly errorCategory?: string;
50
+ readonly errorClassName: string;
51
+ readonly errorMessage: string;
52
+ readonly outcome: 'err';
53
+ }
54
+
55
+ type ActualOutcome = ActualOutcomeOk | ActualOutcomeErr;
56
+
57
+ const buildHappyExampleInput = (): {
58
+ readonly exampleName: string;
59
+ readonly id: string;
60
+ readonly module: string;
61
+ readonly rootDir: string;
62
+ } => ({
63
+ ...createIsolatedExampleInput('run-example-happy'),
64
+ exampleName: 'Brief capability report',
65
+ id: 'survey.brief',
66
+ });
67
+
68
+ const projectActual = (result: Result<unknown, Error>): ActualOutcome => {
69
+ if (result.isOk()) {
70
+ return { outcome: 'ok', value: result.value };
71
+ }
72
+ const { error } = result;
73
+ return {
74
+ errorClassName: error.constructor.name,
75
+ errorMessage: error.message,
76
+ outcome: 'err',
77
+ ...(error instanceof TrailsError ? { errorCategory: error.category } : {}),
78
+ };
79
+ };
80
+
81
+ const formatLeaf = (value: unknown): string => {
82
+ try {
83
+ const encoded = JSON.stringify(value);
84
+ return encoded === undefined ? String(value) : encoded;
85
+ } catch {
86
+ return String(value);
87
+ }
88
+ };
89
+
90
+ const formatPath = (segments: readonly string[]): string =>
91
+ segments.length === 0 ? 'value' : `value.${segments.join('.')}`;
92
+
93
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
94
+ typeof value === 'object' && value !== null && !Array.isArray(value);
95
+
96
+ const deepEqualWithDiff = (
97
+ actual: unknown,
98
+ expected: unknown,
99
+ path: readonly string[],
100
+ diffs: string[]
101
+ ): boolean => {
102
+ if (Array.isArray(expected)) {
103
+ if (!Array.isArray(actual)) {
104
+ diffs.push(`${formatPath(path)}: expected array, got ${typeof actual}`);
105
+ return false;
106
+ }
107
+ if (actual.length !== expected.length) {
108
+ diffs.push(
109
+ `${formatPath(path)}: array length ${actual.length} != ${expected.length}`
110
+ );
111
+ return false;
112
+ }
113
+ let ok = true;
114
+ for (let i = 0; i < expected.length; i += 1) {
115
+ if (
116
+ !deepEqualWithDiff(actual[i], expected[i], [...path, `[${i}]`], diffs)
117
+ ) {
118
+ ok = false;
119
+ }
120
+ }
121
+ return ok;
122
+ }
123
+
124
+ if (isPlainObject(expected)) {
125
+ if (!isPlainObject(actual)) {
126
+ diffs.push(`${formatPath(path)}: expected object, got ${typeof actual}`);
127
+ return false;
128
+ }
129
+ let ok = true;
130
+ for (const key of Object.keys(expected)) {
131
+ if (!(key in actual)) {
132
+ diffs.push(`${formatPath([...path, key])}: missing in actual`);
133
+ ok = false;
134
+ continue;
135
+ }
136
+ if (
137
+ !deepEqualWithDiff(actual[key], expected[key], [...path, key], diffs)
138
+ ) {
139
+ ok = false;
140
+ }
141
+ }
142
+ for (const key of Object.keys(actual)) {
143
+ if (!(key in expected)) {
144
+ diffs.push(`${formatPath([...path, key])}: unexpected key in actual`);
145
+ ok = false;
146
+ }
147
+ }
148
+ return ok;
149
+ }
150
+
151
+ if (actual !== expected) {
152
+ if (
153
+ typeof actual === 'number' &&
154
+ typeof expected === 'number' &&
155
+ Number.isNaN(actual) &&
156
+ Number.isNaN(expected)
157
+ ) {
158
+ return true;
159
+ }
160
+ diffs.push(
161
+ `${formatPath(path)}: ${formatLeaf(actual)} != ${formatLeaf(expected)}`
162
+ );
163
+ return false;
164
+ }
165
+ return true;
166
+ };
167
+
168
+ const partialMatchWithDiff = (
169
+ actual: unknown,
170
+ expected: unknown,
171
+ path: readonly string[],
172
+ diffs: string[]
173
+ ): boolean => {
174
+ if (Array.isArray(expected)) {
175
+ if (!Array.isArray(actual)) {
176
+ diffs.push(`${formatPath(path)}: expected array, got ${typeof actual}`);
177
+ return false;
178
+ }
179
+ let ok = true;
180
+ const consumed = new Set<number>();
181
+ for (const [index, expectedEntry] of expected.entries()) {
182
+ const matchIndex = actual.findIndex((candidate, candidateIndex) => {
183
+ if (consumed.has(candidateIndex)) {
184
+ return false;
185
+ }
186
+ const probe: string[] = [];
187
+ return partialMatchWithDiff(candidate, expectedEntry, [], probe);
188
+ });
189
+ if (matchIndex === -1) {
190
+ diffs.push(
191
+ `${formatPath([...path, `[${index}]`])}: expected array to contain ${formatLeaf(expectedEntry)}`
192
+ );
193
+ ok = false;
194
+ continue;
195
+ }
196
+ consumed.add(matchIndex);
197
+ }
198
+ return ok;
199
+ }
200
+
201
+ if (isPlainObject(expected)) {
202
+ if (!isPlainObject(actual)) {
203
+ diffs.push(`${formatPath(path)}: expected object, got ${typeof actual}`);
204
+ return false;
205
+ }
206
+ let ok = true;
207
+ for (const key of Object.keys(expected)) {
208
+ if (!(key in actual)) {
209
+ diffs.push(`${formatPath([...path, key])}: missing in actual`);
210
+ ok = false;
211
+ continue;
212
+ }
213
+ if (
214
+ !partialMatchWithDiff(actual[key], expected[key], [...path, key], diffs)
215
+ ) {
216
+ ok = false;
217
+ }
218
+ }
219
+ return ok;
220
+ }
221
+
222
+ if (actual !== expected) {
223
+ diffs.push(
224
+ `${formatPath(path)}: ${formatLeaf(actual)} != ${formatLeaf(expected)}`
225
+ );
226
+ return false;
227
+ }
228
+ return true;
229
+ };
230
+
231
+ const compareExpected = (
232
+ result: Result<unknown, Error>,
233
+ expected: unknown
234
+ ): {
235
+ readonly diff?: readonly string[] | undefined;
236
+ readonly match: boolean;
237
+ } => {
238
+ if (result.isErr()) {
239
+ return {
240
+ diff: [
241
+ `value: expected Result.ok(...), got Result.err(${result.error.constructor.name}: ${result.error.message})`,
242
+ ],
243
+ match: false,
244
+ };
245
+ }
246
+ const diffs: string[] = [];
247
+ const match = deepEqualWithDiff(result.value, expected, [], diffs);
248
+ return { diff: match ? undefined : diffs, match };
249
+ };
250
+
251
+ const compareExpectedMatch = (
252
+ result: Result<unknown, Error>,
253
+ expectedMatch: unknown
254
+ ): {
255
+ readonly diff?: readonly string[] | undefined;
256
+ readonly match: boolean;
257
+ } => {
258
+ if (result.isErr()) {
259
+ return {
260
+ diff: [
261
+ `value: expected Result.ok(...), got Result.err(${result.error.constructor.name}: ${result.error.message})`,
262
+ ],
263
+ match: false,
264
+ };
265
+ }
266
+ const diffs: string[] = [];
267
+ const match = partialMatchWithDiff(result.value, expectedMatch, [], diffs);
268
+ return { diff: match ? undefined : diffs, match };
269
+ };
270
+
271
+ const compareError = (
272
+ result: Result<unknown, Error>,
273
+ expectedErrorName: string
274
+ ): {
275
+ readonly diff?: readonly string[] | undefined;
276
+ readonly match: boolean;
277
+ } => {
278
+ if (result.isOk()) {
279
+ return {
280
+ diff: [
281
+ `value: expected Result.err(${expectedErrorName}), got Result.ok(${formatLeaf(result.value)})`,
282
+ ],
283
+ match: false,
284
+ };
285
+ }
286
+ const className = result.error.constructor.name;
287
+ if (className === expectedErrorName) {
288
+ return { diff: undefined, match: true };
289
+ }
290
+ return {
291
+ diff: [
292
+ `value: expected Result.err(${expectedErrorName}), got Result.err(${className}: ${result.error.message})`,
293
+ ],
294
+ match: false,
295
+ };
296
+ };
297
+
298
+ const findExample = (
299
+ app: Topo,
300
+ trailId: string,
301
+ exampleName: string
302
+ ): Result<StructuredTrailExample, Error> => {
303
+ const target = app.get(trailId);
304
+ if (target === undefined) {
305
+ return Result.err(
306
+ new NotFoundError(
307
+ `Trail '${trailId}' was not found in the resolved app.`,
308
+ { context: { trailId } }
309
+ )
310
+ );
311
+ }
312
+
313
+ const structured = deriveStructuredTrailExamples(target.examples) ?? [];
314
+ const match = structured.find((entry) => entry.name === exampleName);
315
+ if (match !== undefined) {
316
+ return Result.ok(match);
317
+ }
318
+
319
+ const available = structured.map((entry) => entry.name);
320
+ const listing = available.length === 0 ? '<none>' : available.join(', ');
321
+ return Result.err(
322
+ new NotFoundError(
323
+ `Example '${exampleName}' not found on trail '${trailId}'. Available: ${listing}.`,
324
+ {
325
+ context: {
326
+ available,
327
+ exampleName,
328
+ trailId,
329
+ },
330
+ }
331
+ )
332
+ );
333
+ };
334
+
335
+ const determineMode = (
336
+ example: StructuredTrailExample
337
+ ): RunExampleComparisonMode => {
338
+ if (example.error !== undefined) {
339
+ return 'error';
340
+ }
341
+ if (example.expectedMatch !== undefined) {
342
+ return 'expectedMatch';
343
+ }
344
+ if (example.expected !== undefined) {
345
+ return 'expected';
346
+ }
347
+ return 'none';
348
+ };
349
+
350
+ const buildComparisonEnvelope = async (
351
+ app: Topo,
352
+ trailId: string,
353
+ exampleName: string
354
+ ): Promise<Result<RunExampleComparison, Error>> => {
355
+ const exampleResult = findExample(app, trailId, exampleName);
356
+ if (exampleResult.isErr()) {
357
+ return Result.err(exampleResult.error);
358
+ }
359
+ const example = exampleResult.value;
360
+ const mode = determineMode(example);
361
+ const executed = await run(app, trailId, example.input);
362
+ const actual = projectActual(executed);
363
+
364
+ if (mode === 'error') {
365
+ const expectedName = example.error ?? '';
366
+ const { diff, match } = compareError(executed, expectedName);
367
+ return Result.ok({
368
+ actual,
369
+ diff,
370
+ exampleName,
371
+ expected: { errorClassName: expectedName },
372
+ input: example.input,
373
+ kind: RUN_EXAMPLE_COMPARISON_KIND,
374
+ match,
375
+ mode,
376
+ trailId,
377
+ });
378
+ }
379
+ if (mode === 'expectedMatch') {
380
+ const { diff, match } = compareExpectedMatch(
381
+ executed,
382
+ example.expectedMatch
383
+ );
384
+ return Result.ok({
385
+ actual,
386
+ diff,
387
+ exampleName,
388
+ expected: example.expectedMatch,
389
+ input: example.input,
390
+ kind: RUN_EXAMPLE_COMPARISON_KIND,
391
+ match,
392
+ mode,
393
+ trailId,
394
+ });
395
+ }
396
+ if (mode === 'none') {
397
+ return Result.ok({
398
+ actual,
399
+ exampleName,
400
+ expected: undefined,
401
+ input: example.input,
402
+ kind: RUN_EXAMPLE_COMPARISON_KIND,
403
+ match: true,
404
+ mode,
405
+ trailId,
406
+ });
407
+ }
408
+
409
+ const { diff, match } = compareExpected(executed, example.expected);
410
+ return Result.ok({
411
+ actual,
412
+ diff,
413
+ exampleName,
414
+ expected: example.expected,
415
+ input: example.input,
416
+ kind: RUN_EXAMPLE_COMPARISON_KIND,
417
+ match,
418
+ mode,
419
+ trailId,
420
+ });
421
+ };
422
+
423
+ export const runExampleTrail = trail('run.example', {
424
+ args: ['id', 'exampleName'],
425
+ blaze: async (input, ctx) => {
426
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
427
+ if (rootDirResult.isErr()) {
428
+ return Result.err(rootDirResult.error);
429
+ }
430
+ const rootDir = rootDirResult.value;
431
+ const moduleResolution = await resolveRunModulePath(
432
+ rootDir,
433
+ input.module,
434
+ input.id,
435
+ input.app
436
+ );
437
+ if (moduleResolution.isErr()) {
438
+ return Result.err(moduleResolution.error);
439
+ }
440
+
441
+ const leaseResult = await tryLoadFreshAppLease(
442
+ moduleResolution.value,
443
+ rootDir
444
+ );
445
+ if (leaseResult.isErr()) {
446
+ return Result.err(leaseResult.error);
447
+ }
448
+ const lease = leaseResult.value;
449
+
450
+ try {
451
+ return await buildComparisonEnvelope(
452
+ lease.app,
453
+ input.id,
454
+ input.exampleName
455
+ );
456
+ } finally {
457
+ lease.release();
458
+ }
459
+ },
460
+ description: 'Run a named example on a trail and compare actual vs expected',
461
+ examples: [
462
+ {
463
+ description: 'Run a named example on a target trail',
464
+ input: buildHappyExampleInput(),
465
+ name: 'Run named example',
466
+ },
467
+ ],
468
+ input: z.object({
469
+ app: z
470
+ .string()
471
+ .optional()
472
+ .describe(
473
+ 'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
474
+ ),
475
+ exampleName: z.string().describe('Name of the example to run'),
476
+ id: z.string().describe('Trail ID whose example should run'),
477
+ module: z.string().optional().describe('Path to the app module'),
478
+ rootDir: z.string().optional().describe('Workspace root directory'),
479
+ }),
480
+ intent: 'write',
481
+ output: runExampleComparisonSchema,
482
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `run.examples` trail -- list examples for a target trail.
3
+ */
4
+
5
+ import {
6
+ NotFoundError,
7
+ Result,
8
+ deriveStructuredTrailExamples,
9
+ trail,
10
+ } from '@ontrails/core';
11
+ import type { StructuredTrailExample, Topo } from '@ontrails/core';
12
+ import { z } from 'zod';
13
+
14
+ import { tryLoadFreshAppLease } from './load-app.js';
15
+ import { resolveRunModulePath } from './run.js';
16
+ import { resolveTrailRootDir } from './root-dir.js';
17
+ import { createIsolatedExampleInput } from './topo-support.js';
18
+
19
+ export const RUN_EXAMPLES_LISTING_KIND = 'examples-listing' as const;
20
+
21
+ export const structuredTrailExampleSchema = z
22
+ .object({
23
+ description: z.string().optional(),
24
+ error: z.string().optional(),
25
+ expected: z.unknown().optional(),
26
+ expectedMatch: z.unknown().optional(),
27
+ input: z.unknown(),
28
+ kind: z.union([z.literal('success'), z.literal('error')]),
29
+ name: z.string(),
30
+ provenance: z.object({ source: z.literal('trail.examples') }),
31
+ signals: z
32
+ .array(
33
+ z.object({
34
+ payload: z.unknown().optional(),
35
+ payloadMatch: z.unknown().optional(),
36
+ signalId: z.string(),
37
+ times: z.number().optional(),
38
+ })
39
+ )
40
+ .readonly()
41
+ .optional(),
42
+ })
43
+ .passthrough();
44
+
45
+ export const runExamplesListingSchema = z.object({
46
+ examples: z.array(structuredTrailExampleSchema).readonly(),
47
+ kind: z.literal(RUN_EXAMPLES_LISTING_KIND),
48
+ trailId: z.string(),
49
+ });
50
+
51
+ export type RunExamplesListing = z.infer<typeof runExamplesListingSchema>;
52
+
53
+ const buildHappyExampleInput = (): {
54
+ readonly id: string;
55
+ readonly module: string;
56
+ readonly rootDir: string;
57
+ } => ({
58
+ ...createIsolatedExampleInput('run-examples-happy'),
59
+ id: 'survey.brief',
60
+ });
61
+
62
+ const buildExamplesListing = (
63
+ app: Topo,
64
+ trailId: string
65
+ ): Result<RunExamplesListing, Error> => {
66
+ const target = app.get(trailId);
67
+ if (target === undefined) {
68
+ return Result.err(
69
+ new NotFoundError(
70
+ `Trail '${trailId}' was not found in the resolved app.`,
71
+ { context: { trailId } }
72
+ )
73
+ );
74
+ }
75
+
76
+ const structured =
77
+ (deriveStructuredTrailExamples(target.examples) as
78
+ | readonly StructuredTrailExample[]
79
+ | undefined) ?? [];
80
+ return Result.ok({
81
+ examples: structured as unknown as RunExamplesListing['examples'],
82
+ kind: RUN_EXAMPLES_LISTING_KIND,
83
+ trailId,
84
+ });
85
+ };
86
+
87
+ export const runExamplesTrail = trail('run.examples', {
88
+ args: ['id'],
89
+ blaze: async (input, ctx) => {
90
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
91
+ if (rootDirResult.isErr()) {
92
+ return Result.err(rootDirResult.error);
93
+ }
94
+ const rootDir = rootDirResult.value;
95
+ const moduleResolution = await resolveRunModulePath(
96
+ rootDir,
97
+ input.module,
98
+ input.id,
99
+ input.app
100
+ );
101
+ if (moduleResolution.isErr()) {
102
+ return Result.err(moduleResolution.error);
103
+ }
104
+
105
+ const leaseResult = await tryLoadFreshAppLease(
106
+ moduleResolution.value,
107
+ rootDir
108
+ );
109
+ if (leaseResult.isErr()) {
110
+ return Result.err(leaseResult.error);
111
+ }
112
+ const lease = leaseResult.value;
113
+
114
+ try {
115
+ return buildExamplesListing(lease.app, input.id);
116
+ } finally {
117
+ lease.release();
118
+ }
119
+ },
120
+ description: "List a trail's examples without executing it",
121
+ examples: [
122
+ {
123
+ description: 'List examples authored on a target trail',
124
+ input: buildHappyExampleInput(),
125
+ name: 'List trail examples',
126
+ },
127
+ ],
128
+ input: z.object({
129
+ app: z
130
+ .string()
131
+ .optional()
132
+ .describe(
133
+ 'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
134
+ ),
135
+ id: z.string().describe('Trail ID whose examples should be listed'),
136
+ module: z.string().optional().describe('Path to the app module'),
137
+ rootDir: z.string().optional().describe('Workspace root directory'),
138
+ }),
139
+ intent: 'read',
140
+ output: runExamplesListingSchema,
141
+ });