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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +1104 -4
  2. package/README.md +27 -0
  3. package/package.json +31 -7
  4. package/src/app.ts +147 -3
  5. package/src/cli.ts +345 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +42 -0
  11. package/src/mcp-options.ts +91 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/regrade/history.ts +430 -0
  16. package/src/regrade/live-api-preserve.ts +8 -0
  17. package/src/regrade/plan-artifact.ts +285 -0
  18. package/src/release/bindings.ts +39 -0
  19. package/src/release/check.ts +844 -0
  20. package/src/release/config.ts +63 -0
  21. package/src/release/contract-facts.ts +425 -0
  22. package/src/release/index.ts +142 -0
  23. package/src/release/lock-roundtrip-smoke.ts +230 -0
  24. package/src/release/native-bun-publish.ts +651 -0
  25. package/src/release/native-bun-registry.ts +765 -0
  26. package/src/release/notes-cli.ts +171 -0
  27. package/src/release/notes.ts +390 -0
  28. package/src/release/pack-coherence.ts +455 -0
  29. package/src/release/packed-artifacts-smoke.ts +236 -0
  30. package/src/release/policy.ts +1687 -0
  31. package/src/release/semver.ts +104 -0
  32. package/src/release/smoke.ts +56 -0
  33. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  34. package/src/retired-topo-command.ts +36 -0
  35. package/src/run-adapter-check.ts +76 -0
  36. package/src/run-collision.ts +126 -0
  37. package/src/run-completions-install.ts +179 -0
  38. package/src/run-example.ts +149 -0
  39. package/src/run-examples.ts +148 -0
  40. package/src/run-quiet.ts +75 -0
  41. package/src/run-release-check.ts +74 -0
  42. package/src/run-schema.ts +74 -0
  43. package/src/run-trace.ts +273 -0
  44. package/src/run-warden.ts +39 -0
  45. package/src/run-watch.ts +432 -0
  46. package/src/run-wayfind-outline.ts +170 -0
  47. package/src/scaffold-version-sync.ts +183 -0
  48. package/src/scaffold-versions.generated.ts +12 -0
  49. package/src/trails/adapter-check.ts +244 -0
  50. package/src/trails/add-surface.ts +99 -45
  51. package/src/trails/add-trail.ts +84 -37
  52. package/src/trails/add-verify.ts +100 -30
  53. package/src/trails/compile.ts +58 -0
  54. package/src/trails/completions-complete.ts +165 -0
  55. package/src/trails/completions.ts +47 -0
  56. package/src/trails/create-adapter.ts +785 -0
  57. package/src/trails/create-scaffold.ts +401 -106
  58. package/src/trails/create-versions.ts +62 -0
  59. package/src/trails/create.ts +186 -72
  60. package/src/trails/deprecate.ts +59 -0
  61. package/src/trails/dev-clean.ts +82 -0
  62. package/src/trails/dev-reset.ts +50 -0
  63. package/src/trails/dev-stats.ts +72 -0
  64. package/src/trails/dev-support.ts +360 -0
  65. package/src/trails/doctor.ts +77 -0
  66. package/src/trails/draft-promote.ts +949 -0
  67. package/src/trails/guide.ts +70 -68
  68. package/src/trails/load-app.ts +1123 -15
  69. package/src/trails/operator-context.ts +66 -0
  70. package/src/trails/project.ts +17 -3
  71. package/src/trails/regrade.ts +3091 -0
  72. package/src/trails/release-check.ts +105 -0
  73. package/src/trails/release-smoke.ts +49 -0
  74. package/src/trails/revise.ts +53 -0
  75. package/src/trails/root-dir.ts +21 -0
  76. package/src/trails/run-example.ts +475 -0
  77. package/src/trails/run-examples.ts +129 -0
  78. package/src/trails/run.ts +434 -0
  79. package/src/trails/scaffold-json.ts +58 -0
  80. package/src/trails/survey.ts +877 -214
  81. package/src/trails/topo-activation.ts +14 -0
  82. package/src/trails/topo-constants.ts +2 -0
  83. package/src/trails/topo-history.ts +47 -0
  84. package/src/trails/topo-output-schemas.ts +259 -0
  85. package/src/trails/topo-pin.ts +38 -0
  86. package/src/trails/topo-read-support.ts +368 -0
  87. package/src/trails/topo-reports.ts +809 -0
  88. package/src/trails/topo-store-support.ts +325 -0
  89. package/src/trails/topo-support.ts +220 -0
  90. package/src/trails/topo-unpin.ts +61 -0
  91. package/src/trails/topo.ts +92 -0
  92. package/src/trails/validate.ts +27 -0
  93. package/src/trails/version-lifecycle-support.ts +936 -0
  94. package/src/trails/warden-guide.ts +134 -0
  95. package/src/trails/warden.ts +198 -58
  96. package/src/trails/wayfind-outline.ts +876 -0
  97. package/src/trails/wayfind.ts +1053 -0
  98. package/src/versions.ts +31 -0
  99. package/.turbo/turbo-build.log +0 -1
  100. package/.turbo/turbo-lint.log +0 -3
  101. package/.turbo/turbo-typecheck.log +0 -1
  102. package/__tests__/examples.test.ts +0 -6
  103. package/dist/bin/trails.d.ts +0 -3
  104. package/dist/bin/trails.d.ts.map +0 -1
  105. package/dist/bin/trails.js +0 -4
  106. package/dist/bin/trails.js.map +0 -1
  107. package/dist/src/app.d.ts +0 -2
  108. package/dist/src/app.d.ts.map +0 -1
  109. package/dist/src/app.js +0 -11
  110. package/dist/src/app.js.map +0 -1
  111. package/dist/src/clack.d.ts +0 -9
  112. package/dist/src/clack.d.ts.map +0 -1
  113. package/dist/src/clack.js +0 -84
  114. package/dist/src/clack.js.map +0 -1
  115. package/dist/src/cli.d.ts +0 -2
  116. package/dist/src/cli.d.ts.map +0 -1
  117. package/dist/src/cli.js +0 -13
  118. package/dist/src/cli.js.map +0 -1
  119. package/dist/src/trails/add-surface.d.ts +0 -13
  120. package/dist/src/trails/add-surface.d.ts.map +0 -1
  121. package/dist/src/trails/add-surface.js +0 -88
  122. package/dist/src/trails/add-surface.js.map +0 -1
  123. package/dist/src/trails/add-trail.d.ts +0 -10
  124. package/dist/src/trails/add-trail.d.ts.map +0 -1
  125. package/dist/src/trails/add-trail.js +0 -77
  126. package/dist/src/trails/add-trail.js.map +0 -1
  127. package/dist/src/trails/add-verify.d.ts +0 -10
  128. package/dist/src/trails/add-verify.d.ts.map +0 -1
  129. package/dist/src/trails/add-verify.js +0 -67
  130. package/dist/src/trails/add-verify.js.map +0 -1
  131. package/dist/src/trails/create-scaffold.d.ts +0 -15
  132. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  133. package/dist/src/trails/create-scaffold.js +0 -288
  134. package/dist/src/trails/create-scaffold.js.map +0 -1
  135. package/dist/src/trails/create.d.ts +0 -22
  136. package/dist/src/trails/create.d.ts.map +0 -1
  137. package/dist/src/trails/create.js +0 -121
  138. package/dist/src/trails/create.js.map +0 -1
  139. package/dist/src/trails/guide.d.ts +0 -11
  140. package/dist/src/trails/guide.d.ts.map +0 -1
  141. package/dist/src/trails/guide.js +0 -80
  142. package/dist/src/trails/guide.js.map +0 -1
  143. package/dist/src/trails/load-app.d.ts +0 -4
  144. package/dist/src/trails/load-app.d.ts.map +0 -1
  145. package/dist/src/trails/load-app.js +0 -24
  146. package/dist/src/trails/load-app.js.map +0 -1
  147. package/dist/src/trails/project.d.ts +0 -8
  148. package/dist/src/trails/project.d.ts.map +0 -1
  149. package/dist/src/trails/project.js +0 -43
  150. package/dist/src/trails/project.js.map +0 -1
  151. package/dist/src/trails/survey.d.ts +0 -31
  152. package/dist/src/trails/survey.d.ts.map +0 -1
  153. package/dist/src/trails/survey.js +0 -221
  154. package/dist/src/trails/survey.js.map +0 -1
  155. package/dist/src/trails/warden.d.ts +0 -19
  156. package/dist/src/trails/warden.d.ts.map +0 -1
  157. package/dist/src/trails/warden.js +0 -88
  158. package/dist/src/trails/warden.js.map +0 -1
  159. package/dist/tsconfig.tsbuildinfo +0 -1
  160. package/src/__tests__/create.test.ts +0 -349
  161. package/src/__tests__/guide.test.ts +0 -91
  162. package/src/__tests__/load-app.test.ts +0 -15
  163. package/src/__tests__/survey.test.ts +0 -159
  164. package/src/__tests__/warden.test.ts +0 -74
  165. package/tsconfig.json +0 -9
@@ -0,0 +1,760 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ const repoRoot = resolve(process.cwd());
7
+ const trailsBin = join(repoRoot, 'apps/trails/bin/trails.ts');
8
+
9
+ type JsonObject = Record<string, unknown>;
10
+
11
+ export interface WayfinderDogfoodSmokeResult {
12
+ readonly check: 'wayfinder-dogfood';
13
+ readonly message: string;
14
+ readonly passed: true;
15
+ readonly trailCount: number;
16
+ }
17
+
18
+ const assertObject = (value: unknown, label: string): JsonObject => {
19
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
20
+ throw new Error(`${label} did not return a JSON object`);
21
+ }
22
+ return value as JsonObject;
23
+ };
24
+
25
+ const assertAlignedSource = (value: JsonObject, label: string): void => {
26
+ const drift = assertObject(value['drift'], `${label}.drift`);
27
+ if (drift['status'] !== 'aligned') {
28
+ throw new Error(`${label} did not read fresh artifacts`);
29
+ }
30
+ const source = assertObject(value['source'], `${label}.source`);
31
+ if (source['kind'] !== 'topoGraph') {
32
+ throw new Error(`${label} did not read the TopoGraph source`);
33
+ }
34
+ };
35
+
36
+ const parseJson = (stdout: string, label: string): JsonObject => {
37
+ try {
38
+ return assertObject(JSON.parse(stdout) as unknown, label);
39
+ } catch (error) {
40
+ throw new Error(`${label} did not produce valid JSON`, { cause: error });
41
+ }
42
+ };
43
+
44
+ const runCommandProcess = (
45
+ command: readonly string[]
46
+ ): {
47
+ readonly exitCode: number;
48
+ readonly stderr: string;
49
+ readonly stdout: string;
50
+ } => {
51
+ const result = Bun.spawnSync({
52
+ cmd: [...command],
53
+ cwd: repoRoot,
54
+ env: { ...process.env, NO_COLOR: '1' } as Record<
55
+ string,
56
+ string | undefined
57
+ >,
58
+ stderr: 'pipe',
59
+ stdout: 'pipe',
60
+ });
61
+ return {
62
+ exitCode: result.exitCode,
63
+ stderr: result.stderr.toString(),
64
+ stdout: result.stdout.toString(),
65
+ };
66
+ };
67
+
68
+ const runCommand = (command: readonly string[], label: string): JsonObject => {
69
+ const result = runCommandProcess(command);
70
+ if (result.exitCode !== 0) {
71
+ throw new Error(
72
+ [
73
+ `Wayfinder dogfood command failed: ${command.join(' ')}`,
74
+ `exitCode: ${result.exitCode}`,
75
+ `stdout: ${result.stdout}`,
76
+ `stderr: ${result.stderr}`,
77
+ ].join('\n')
78
+ );
79
+ }
80
+ return parseJson(result.stdout, label);
81
+ };
82
+
83
+ const runFailingCommand = (
84
+ command: readonly string[],
85
+ label: string
86
+ ): JsonObject => {
87
+ const result = runCommandProcess(command);
88
+ if (result.exitCode === 0) {
89
+ throw new Error(
90
+ [
91
+ `Wayfinder dogfood command unexpectedly succeeded: ${command.join(' ')}`,
92
+ `stdout: ${result.stdout}`,
93
+ ].join('\n')
94
+ );
95
+ }
96
+ return parseJson(
97
+ result.stdout.length > 0 ? result.stdout : result.stderr,
98
+ label
99
+ );
100
+ };
101
+
102
+ const runTrails = (tempRoot: string, args: readonly string[]): JsonObject => {
103
+ const command = [
104
+ process.execPath,
105
+ trailsBin,
106
+ ...args,
107
+ '--root-dir',
108
+ tempRoot,
109
+ '--json',
110
+ ];
111
+ return runCommand(command, `trails ${args.join(' ')}`);
112
+ };
113
+
114
+ const runFailingTrails = (
115
+ tempRoot: string,
116
+ args: readonly string[]
117
+ ): JsonObject => {
118
+ const command = [
119
+ process.execPath,
120
+ trailsBin,
121
+ ...args,
122
+ '--root-dir',
123
+ tempRoot,
124
+ '--json',
125
+ ];
126
+ return runFailingCommand(command, `trails ${args.join(' ')}`);
127
+ };
128
+
129
+ const runWayfind = (tempRoot: string, args: readonly string[]): JsonObject =>
130
+ runTrails(tempRoot, ['wayfind', ...args]);
131
+
132
+ const runSchema = (args: readonly string[]): JsonObject =>
133
+ runCommand([process.execPath, trailsBin, 'schema', ...args], 'trails schema');
134
+
135
+ const writeOperatorAppWrapper = async (tempRoot: string): Promise<void> => {
136
+ const srcDir = join(tempRoot, 'src');
137
+ await mkdir(srcDir, { recursive: true });
138
+ const appModuleUrl = pathToFileURL(join(repoRoot, 'apps/trails/src/app.ts'));
139
+ await writeFile(
140
+ join(srcDir, 'app.ts'),
141
+ `export { app, trailsOverlays } from ${JSON.stringify(appModuleUrl.href)};\n`
142
+ );
143
+ };
144
+
145
+ const writeDemoAppWrapper = async (tempRoot: string): Promise<void> => {
146
+ const srcDir = join(tempRoot, 'src');
147
+ await mkdir(srcDir, { recursive: true });
148
+ const appModuleUrl = pathToFileURL(
149
+ join(repoRoot, 'apps/trails-demo/src/app.ts')
150
+ );
151
+ await writeFile(
152
+ join(srcDir, 'demo-app.ts'),
153
+ `export { graph as app } from ${JSON.stringify(appModuleUrl.href)};\n`
154
+ );
155
+ };
156
+
157
+ const writeJson = async (
158
+ path: string,
159
+ value: Readonly<Record<string, unknown>>
160
+ ): Promise<void> => {
161
+ await mkdir(dirname(path), { recursive: true });
162
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
163
+ };
164
+
165
+ const writeAdapterWorkspace = async (tempRoot: string): Promise<void> => {
166
+ await writeJson(join(tempRoot, 'package.json'), {
167
+ name: 'wayfinder-dogfood-root',
168
+ workspaces: ['packages/*', 'adapters/*'],
169
+ });
170
+ await writeJson(join(tempRoot, 'packages/http/package.json'), {
171
+ exports: {
172
+ '.': './src/index.ts',
173
+ './package.json': './package.json',
174
+ './testing': './src/testing.ts',
175
+ },
176
+ name: '@ontrails/http',
177
+ trails: {
178
+ adapterTargets: {
179
+ http: {
180
+ conformance: {
181
+ adapterType: 'HttpAdapterConformanceAdapter',
182
+ casesFactory: 'createHttpAdapterConformanceCases',
183
+ runner: 'runConformance',
184
+ },
185
+ placements: ['extracted'],
186
+ testingImport: '@ontrails/http/testing',
187
+ },
188
+ },
189
+ },
190
+ });
191
+ await mkdir(join(tempRoot, 'packages/http/src'), { recursive: true });
192
+ await writeFile(
193
+ join(tempRoot, 'packages/http/src/index.ts'),
194
+ 'export const http = {};\n'
195
+ );
196
+ await writeFile(
197
+ join(tempRoot, 'packages/http/src/testing.ts'),
198
+ [
199
+ 'export interface HttpAdapterConformanceAdapter {}',
200
+ 'export const createHttpAdapterConformanceCases = () => [];',
201
+ 'export const runConformance = () => undefined;',
202
+ '',
203
+ ].join('\n')
204
+ );
205
+ await writeJson(join(tempRoot, 'adapters/hono/package.json'), {
206
+ dependencies: {
207
+ '@ontrails/core': 'workspace:^',
208
+ hono: '^4.7.0',
209
+ },
210
+ exports: {
211
+ '.': './src/index.ts',
212
+ './package.json': './package.json',
213
+ },
214
+ name: '@ontrails/hono',
215
+ peerDependencies: {
216
+ '@ontrails/http': 'workspace:^',
217
+ },
218
+ trails: {
219
+ adapter: {
220
+ target: 'http',
221
+ },
222
+ },
223
+ });
224
+ await mkdir(join(tempRoot, 'adapters/hono/src/__tests__'), {
225
+ recursive: true,
226
+ });
227
+ await writeFile(
228
+ join(tempRoot, 'adapters/hono/src/index.ts'),
229
+ 'export const honoAdapter = {};\n'
230
+ );
231
+ await writeFile(
232
+ join(tempRoot, 'adapters/hono/src/__tests__/conformance.test.ts'),
233
+ [
234
+ "import { createHttpAdapterConformanceCases, runConformance } from '@ontrails/http/testing';",
235
+ '',
236
+ "runConformance({ name: '@ontrails/hono' }, createHttpAdapterConformanceCases());",
237
+ '',
238
+ ].join('\n')
239
+ );
240
+ };
241
+
242
+ const assertSearchFindsWayfinder = (search: JsonObject): void => {
243
+ const { matches } = search;
244
+ if (!Array.isArray(matches)) {
245
+ throw new TypeError('wayfind search did not return matches');
246
+ }
247
+ const ids = matches
248
+ .map((match) => assertObject(match, 'wayfind search match')['id'])
249
+ .filter((id): id is string => typeof id === 'string');
250
+ if (!ids.includes('wayfind.search')) {
251
+ throw new Error('wayfind search did not find wayfind.search');
252
+ }
253
+ };
254
+
255
+ const assertDiffIsEmpty = (diffResult: JsonObject): void => {
256
+ const diff = assertObject(diffResult['diff'], 'wayfind diff');
257
+ if (diff['hasBreaking'] !== false) {
258
+ throw new Error(
259
+ 'wayfind diff reported breaking changes for same-root diff'
260
+ );
261
+ }
262
+ };
263
+
264
+ const assertTrailsFindsWayfinder = (trailsResult: JsonObject): void => {
265
+ const { trails } = trailsResult;
266
+ if (!Array.isArray(trails)) {
267
+ throw new TypeError('wayfind --trails did not return trails');
268
+ }
269
+ const ids = trails
270
+ .map((entry) => assertObject(entry, 'wayfind trail entry')['id'])
271
+ .filter((id): id is string => typeof id === 'string');
272
+ if (!ids.includes('wayfind.search')) {
273
+ throw new Error('wayfind --trails did not find wayfind.search');
274
+ }
275
+ };
276
+
277
+ const assertRoutePaths = (
278
+ routes: unknown,
279
+ label: string,
280
+ expected: readonly string[]
281
+ ): void => {
282
+ if (!Array.isArray(routes)) {
283
+ throw new TypeError(`${label} did not return command routes`);
284
+ }
285
+ const routePaths = new Set(
286
+ routes
287
+ .map((route) => assertObject(route, `${label} route`)['path'])
288
+ .filter(
289
+ (path): path is string[] =>
290
+ Array.isArray(path) &&
291
+ path.every((segment) => typeof segment === 'string')
292
+ )
293
+ .map((path) => path.join(' '))
294
+ );
295
+ for (const expectedPath of expected) {
296
+ if (!routePaths.has(expectedPath)) {
297
+ throw new Error(`${label} did not include ${expectedPath}`);
298
+ }
299
+ }
300
+ };
301
+
302
+ const assertSchemaForWayfind = (schema: JsonObject): void => {
303
+ const command = assertObject(schema['command'], 'schema command');
304
+ if (command['trailId'] !== 'wayfind.navigate') {
305
+ throw new Error('schema did not inspect wayfind.navigate');
306
+ }
307
+ assertRoutePaths(command['routes'], 'schema', ['wayfind']);
308
+ };
309
+
310
+ const assertErrorsFindWayfinder = (errorsResult: JsonObject): void => {
311
+ const { errors } = errorsResult;
312
+ if (!Array.isArray(errors)) {
313
+ throw new TypeError('wayfind errors did not return errors');
314
+ }
315
+ const searchEntry = errors
316
+ .map((entry) => assertObject(entry, 'wayfind errors entry'))
317
+ .find((entry) => entry['trailId'] === 'wayfind.search');
318
+ if (searchEntry === undefined) {
319
+ throw new Error('wayfind errors did not include wayfind.search');
320
+ }
321
+ const completeness = assertObject(
322
+ searchEntry['completeness'],
323
+ 'wayfind errors completeness'
324
+ );
325
+ const emitted = assertObject(
326
+ completeness['emitted'],
327
+ 'wayfind errors emitted completeness'
328
+ );
329
+ if (emitted['status'] !== 'unknown') {
330
+ throw new Error('wayfind errors overclaimed emitted-error completeness');
331
+ }
332
+ };
333
+
334
+ const assertDemoResources = (resourcesResult: JsonObject): void => {
335
+ const { resources } = resourcesResult;
336
+ if (!Array.isArray(resources)) {
337
+ throw new TypeError('demo wayfind --resources did not return resources');
338
+ }
339
+ const entityStore = resources
340
+ .map((entry) => assertObject(entry, 'demo resource entry'))
341
+ .find((entry) => entry['id'] === 'demo.entity-store');
342
+ if (entityStore === undefined) {
343
+ throw new Error('demo wayfind --resources did not find demo.entity-store');
344
+ }
345
+ const { usedBy } = entityStore;
346
+ if (!Array.isArray(usedBy) || !usedBy.includes('entity.add')) {
347
+ throw new Error('demo resource usage did not include entity.add');
348
+ }
349
+ };
350
+
351
+ const assertDemoSignals = (signalsResult: JsonObject): void => {
352
+ const { signals } = signalsResult;
353
+ if (!Array.isArray(signals)) {
354
+ throw new TypeError('demo wayfind --signals did not return signals');
355
+ }
356
+ const entityUpdated = signals
357
+ .map((entry) => assertObject(entry, 'demo signal entry'))
358
+ .find((entry) => entry['id'] === 'entity.updated');
359
+ if (entityUpdated === undefined) {
360
+ throw new Error('demo wayfind --signals did not find entity.updated');
361
+ }
362
+ const { consumers, producers } = entityUpdated;
363
+ if (!Array.isArray(producers) || !producers.includes('entity.add')) {
364
+ throw new Error('entity.updated producers did not include entity.add');
365
+ }
366
+ if (
367
+ !Array.isArray(consumers) ||
368
+ !consumers.includes('entity.notify-updated')
369
+ ) {
370
+ throw new Error(
371
+ 'entity.updated consumers did not include entity.notify-updated'
372
+ );
373
+ }
374
+ };
375
+
376
+ const assertDemoErrors = (errorsResult: JsonObject): void => {
377
+ const { errors } = errorsResult;
378
+ if (!Array.isArray(errors)) {
379
+ throw new TypeError('demo wayfind --errors did not return errors');
380
+ }
381
+ const entityAdd = errors
382
+ .map((entry) => assertObject(entry, 'demo errors entry'))
383
+ .find((entry) => entry['trailId'] === 'entity.add');
384
+ if (entityAdd === undefined) {
385
+ throw new Error('demo wayfind --errors did not include entity.add');
386
+ }
387
+ const { facts } = entityAdd;
388
+ if (!Array.isArray(facts)) {
389
+ throw new TypeError('demo entity.add errors did not return facts');
390
+ }
391
+ const hasAlreadyExists = facts
392
+ .map((fact) => assertObject(fact, 'demo entity.add error fact'))
393
+ .some((fact) => {
394
+ const taxonomy = assertObject(
395
+ fact['taxonomy'],
396
+ 'demo entity.add error taxonomy'
397
+ );
398
+ return taxonomy['name'] === 'AlreadyExistsError';
399
+ });
400
+ if (!hasAlreadyExists) {
401
+ throw new Error(
402
+ 'demo entity.add errors did not include AlreadyExistsError'
403
+ );
404
+ }
405
+ };
406
+
407
+ const assertRelationIncludes = (
408
+ relationResult: JsonObject,
409
+ label: string,
410
+ expected: {
411
+ readonly from?: string;
412
+ readonly relation: string;
413
+ readonly to?: string;
414
+ }
415
+ ): void => {
416
+ const { edges } = relationResult;
417
+ if (!Array.isArray(edges)) {
418
+ throw new TypeError(`${label} did not return relation edges`);
419
+ }
420
+ const found = edges
421
+ .map((edge) => assertObject(edge, `${label} edge`))
422
+ .some((edge) => {
423
+ const from = assertObject(edge['from'], `${label} edge from`);
424
+ const to = assertObject(edge['to'], `${label} edge to`);
425
+ return (
426
+ edge['relation'] === expected.relation &&
427
+ (expected.from === undefined || from['id'] === expected.from) &&
428
+ (expected.to === undefined || to['id'] === expected.to)
429
+ );
430
+ });
431
+ if (!found) {
432
+ throw new Error(`${label} did not include expected relation edge`);
433
+ }
434
+ };
435
+
436
+ const assertValidationMessage = (
437
+ failure: JsonObject,
438
+ expected: string
439
+ ): void => {
440
+ const error = assertObject(failure['error'], 'expected validation error');
441
+ const { message } = error;
442
+ if (typeof message !== 'string' || !message.includes(expected)) {
443
+ throw new Error(`Validation error did not include: ${expected}`);
444
+ }
445
+ };
446
+
447
+ const assertAdapterPredicateWayfind = (wayfindResult: JsonObject): void => {
448
+ const result = assertObject(wayfindResult['result'], 'wayfind --adapter');
449
+ const { matches } = result;
450
+ if (!Array.isArray(matches)) {
451
+ throw new TypeError('wayfind --adapter did not return matches');
452
+ }
453
+ const includes = assertObject(wayfindResult['includes'], 'adapter includes');
454
+ const adaptersResult = assertObject(includes['adapters'], 'include adapters');
455
+ const { adapters } = adaptersResult;
456
+ if (!Array.isArray(adapters)) {
457
+ throw new TypeError('wayfind --include adapters did not return adapters');
458
+ }
459
+ const configuredHono = adapters
460
+ .map((adapter) => assertObject(adapter, 'wayfind adapter fact'))
461
+ .some(
462
+ (adapter) =>
463
+ adapter['kind'] === 'configured' &&
464
+ adapter['packageName'] === '@ontrails/hono' &&
465
+ adapter['target'] === 'http'
466
+ );
467
+ if (!configuredHono) {
468
+ throw new Error('wayfind --adapter did not include Hono adapter facts');
469
+ }
470
+ };
471
+
472
+ const assertContractForSearch = (contractResult: JsonObject): void => {
473
+ const contract = assertObject(contractResult['contract'], 'contract');
474
+ if (contract['id'] !== 'wayfind.search') {
475
+ throw new Error('wayfind contract did not inspect wayfind.search');
476
+ }
477
+ };
478
+
479
+ const unwrapWayfindResult = (value: JsonObject, label: string): JsonObject =>
480
+ assertObject(value['result'], `${label}.result`);
481
+
482
+ const assertResolvedTarget = (value: JsonObject, label: string): void => {
483
+ const target = assertObject(value['target'], `${label}.target`);
484
+ if (target['id'] !== 'wayfind.search') {
485
+ throw new Error(`${label} did not resolve wayfind.search`);
486
+ }
487
+ };
488
+
489
+ const assertLiveSourceFindsWayfinder = (value: JsonObject): void => {
490
+ const { matches } = value;
491
+ if (!Array.isArray(matches)) {
492
+ throw new TypeError('wayfind --source live did not return matches');
493
+ }
494
+ const found = matches
495
+ .map((match) => assertObject(match, 'wayfind --source live match'))
496
+ .some((match) => {
497
+ const detail = assertObject(
498
+ match['detail'],
499
+ 'wayfind --source live match detail'
500
+ );
501
+ return detail['id'] === 'wayfind.search';
502
+ });
503
+ if (!found) {
504
+ throw new Error('wayfind --source live did not resolve wayfind.search');
505
+ }
506
+ };
507
+
508
+ const assertOutlineFindsOperatorApp = (outline: JsonObject): void => {
509
+ const features = assertObject(
510
+ outline['features'],
511
+ 'wayfind outline features'
512
+ );
513
+ if (features['view'] !== 'default') {
514
+ throw new Error('wayfind outline did not echo the default feature view');
515
+ }
516
+ const { apps } = outline;
517
+ if (!Array.isArray(apps)) {
518
+ throw new TypeError('wayfind outline did not return app facts');
519
+ }
520
+ const appNames = apps
521
+ .map((entry) => assertObject(entry, 'wayfind outline app')['name'])
522
+ .filter((name): name is string => typeof name === 'string');
523
+ if (!appNames.includes('operatorApp')) {
524
+ throw new Error('wayfind outline did not find the operator app export');
525
+ }
526
+ if (outline['file'] !== 'apps/trails/src/app.ts') {
527
+ throw new Error('wayfind outline did not preserve the source file path');
528
+ }
529
+ const counts = assertObject(outline['counts'], 'wayfind outline counts');
530
+ if (counts['apps'] !== apps.length) {
531
+ throw new Error('wayfind outline counts diverged from app facts');
532
+ }
533
+ };
534
+
535
+ export const runWayfinderDogfoodSmoke =
536
+ async (): Promise<WayfinderDogfoodSmokeResult> => {
537
+ const tempRoot = await mkdtemp(join(tmpdir(), 'trails-wayfinder-dogfood-'));
538
+
539
+ try {
540
+ await writeOperatorAppWrapper(tempRoot);
541
+ await writeDemoAppWrapper(tempRoot);
542
+ await writeAdapterWorkspace(tempRoot);
543
+ runTrails(tempRoot, [
544
+ 'compile',
545
+ '--module',
546
+ './src/demo-app.ts',
547
+ '--permit',
548
+ '{"id":"wayfinder-dogfood","scopes":["topo:write"]}',
549
+ ]);
550
+
551
+ const demoResources = unwrapWayfindResult(
552
+ runWayfind(tempRoot, ['--resources']),
553
+ 'demo wayfind --resources'
554
+ );
555
+ assertAlignedSource(demoResources, 'demo wayfind --resources');
556
+ assertDemoResources(demoResources);
557
+
558
+ const demoSignals = unwrapWayfindResult(
559
+ runWayfind(tempRoot, ['--signals']),
560
+ 'demo wayfind --signals'
561
+ );
562
+ assertAlignedSource(demoSignals, 'demo wayfind --signals');
563
+ assertDemoSignals(demoSignals);
564
+
565
+ const demoErrors = unwrapWayfindResult(
566
+ runWayfind(tempRoot, ['--errors']),
567
+ 'demo wayfind --errors'
568
+ );
569
+ assertAlignedSource(demoErrors, 'demo wayfind --errors');
570
+ assertDemoErrors(demoErrors);
571
+
572
+ const signalImpact = unwrapWayfindResult(
573
+ runWayfind(tempRoot, ['entity.updated', '--impact']),
574
+ 'demo wayfind signal impact'
575
+ );
576
+ assertAlignedSource(signalImpact, 'demo wayfind signal impact');
577
+ assertRelationIncludes(signalImpact, 'demo signal impact', {
578
+ from: 'entity.updated',
579
+ relation: 'consumed-by',
580
+ to: 'entity.notify-updated',
581
+ });
582
+
583
+ const trailDeps = unwrapWayfindResult(
584
+ runWayfind(tempRoot, ['entity.add', '--deps']),
585
+ 'demo wayfind trail deps'
586
+ );
587
+ assertAlignedSource(trailDeps, 'demo wayfind trail deps');
588
+ assertRelationIncludes(trailDeps, 'demo trail deps', {
589
+ from: 'demo.entity-store',
590
+ relation: 'used-by',
591
+ to: 'entity.add',
592
+ });
593
+
594
+ assertValidationMessage(
595
+ runFailingTrails(tempRoot, ['wayfind', '*.ts']),
596
+ 'wayfind pattern <glob>'
597
+ );
598
+ assertValidationMessage(
599
+ runFailingTrails(tempRoot, [
600
+ 'wayfind',
601
+ 'entity.add',
602
+ '--impact',
603
+ '--deps',
604
+ ]),
605
+ 'Provide only one relation flag'
606
+ );
607
+
608
+ assertValidationMessage(
609
+ runFailingTrails(tempRoot, [
610
+ 'compile',
611
+ '--module',
612
+ './src/app.ts',
613
+ '--permit',
614
+ '{"id":"wayfinder-dogfood","scopes":["topo:write"]}',
615
+ ]),
616
+ 'breaking change'
617
+ );
618
+ const resourcesAfterRejectedCompile = unwrapWayfindResult(
619
+ runWayfind(tempRoot, ['--resources']),
620
+ 'demo wayfind after rejected compile'
621
+ );
622
+ assertAlignedSource(
623
+ resourcesAfterRejectedCompile,
624
+ 'demo wayfind after rejected compile'
625
+ );
626
+ assertDemoResources(resourcesAfterRejectedCompile);
627
+
628
+ runTrails(tempRoot, [
629
+ 'compile',
630
+ '--module',
631
+ './src/app.ts',
632
+ '--force',
633
+ '--permit',
634
+ '{"id":"wayfinder-dogfood","scopes":["topo:write"]}',
635
+ ]);
636
+
637
+ const overview = unwrapWayfindResult(
638
+ runWayfind(tempRoot, ['--overview']),
639
+ 'wayfind overview'
640
+ );
641
+ assertAlignedSource(overview, 'wayfind overview');
642
+ const counts = assertObject(
643
+ overview['counts'],
644
+ 'wayfind overview counts'
645
+ );
646
+ const trailCount = counts['trails'];
647
+ if (typeof trailCount !== 'number' || trailCount < 1) {
648
+ throw new Error('wayfind overview did not report trail counts');
649
+ }
650
+
651
+ const trails = unwrapWayfindResult(
652
+ runWayfind(tempRoot, ['--trails', '--intent', 'read']),
653
+ 'wayfind --trails'
654
+ );
655
+ assertAlignedSource(trails, 'wayfind --trails');
656
+ assertTrailsFindsWayfinder(trails);
657
+
658
+ assertSchemaForWayfind(runSchema(['wayfind']));
659
+
660
+ const search = unwrapWayfindResult(
661
+ runWayfind(tempRoot, ['pattern', 'wayfind.*']),
662
+ 'wayfind pattern'
663
+ );
664
+ assertAlignedSource(search, 'wayfind pattern');
665
+ assertSearchFindsWayfinder(search);
666
+
667
+ const live = unwrapWayfindResult(
668
+ runWayfind(tempRoot, ['wayfind.search', '--source', 'live']),
669
+ 'wayfind --source live'
670
+ );
671
+ assertLiveSourceFindsWayfinder(live);
672
+
673
+ const resources = unwrapWayfindResult(
674
+ runWayfind(tempRoot, ['--resources']),
675
+ 'wayfind --resources'
676
+ );
677
+ assertAlignedSource(resources, 'wayfind --resources');
678
+
679
+ const errors = unwrapWayfindResult(
680
+ runWayfind(tempRoot, ['--errors']),
681
+ 'wayfind --errors'
682
+ );
683
+ assertAlignedSource(errors, 'wayfind --errors');
684
+ assertErrorsFindWayfinder(errors);
685
+
686
+ const adapterFiltered = runWayfind(tempRoot, [
687
+ '--adapter',
688
+ '@ontrails/hono',
689
+ '--include',
690
+ 'adapters',
691
+ ]);
692
+ assertAdapterPredicateWayfind(adapterFiltered);
693
+
694
+ const contract = unwrapWayfindResult(
695
+ runWayfind(tempRoot, ['wayfind.search', '--contract']),
696
+ 'wayfind contract'
697
+ );
698
+ assertAlignedSource(contract, 'wayfind contract');
699
+ assertContractForSearch(contract);
700
+
701
+ const nearby = unwrapWayfindResult(
702
+ runWayfind(tempRoot, ['wayfind.search']),
703
+ 'wayfind nearby'
704
+ );
705
+ assertAlignedSource(nearby, 'wayfind nearby');
706
+ assertResolvedTarget(nearby, 'wayfind nearby');
707
+
708
+ const impact = unwrapWayfindResult(
709
+ runWayfind(tempRoot, ['wayfind.search', '--impact']),
710
+ 'wayfind impact'
711
+ );
712
+ assertAlignedSource(impact, 'wayfind impact');
713
+ assertResolvedTarget(impact, 'wayfind impact');
714
+
715
+ const deps = unwrapWayfindResult(
716
+ runWayfind(tempRoot, ['wayfind.search', '--deps']),
717
+ 'wayfind deps'
718
+ );
719
+ assertAlignedSource(deps, 'wayfind deps');
720
+ assertResolvedTarget(deps, 'wayfind deps');
721
+
722
+ const diff = runWayfind(tempRoot, [
723
+ 'diff',
724
+ '--against-root-dir',
725
+ tempRoot,
726
+ ]);
727
+ assertAlignedSource(diff, 'wayfind diff');
728
+ assertDiffIsEmpty(diff);
729
+
730
+ const outline = runCommand(
731
+ [
732
+ process.execPath,
733
+ trailsBin,
734
+ 'wayfind',
735
+ 'file',
736
+ 'apps/trails/src/app.ts',
737
+ '--root-dir',
738
+ repoRoot,
739
+ '--outline',
740
+ '--json',
741
+ ],
742
+ 'trails wayfind file'
743
+ );
744
+ assertOutlineFindsOperatorApp(unwrapWayfindResult(outline, 'outline'));
745
+
746
+ return {
747
+ check: 'wayfinder-dogfood',
748
+ message: `Wayfinder dogfood smoke passed: ${String(trailCount)} trails inspected from saved operator topo artifacts.`,
749
+ passed: true,
750
+ trailCount,
751
+ };
752
+ } finally {
753
+ await rm(tempRoot, { force: true, recursive: true });
754
+ }
755
+ };
756
+
757
+ if (import.meta.main) {
758
+ const result = await runWayfinderDogfoodSmoke();
759
+ console.log(result.message);
760
+ }