@ontrails/trails 1.0.0-beta.23 → 1.0.0-beta.29

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 (48) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +6 -5
  3. package/package.json +16 -14
  4. package/src/app.ts +42 -3
  5. package/src/cli.ts +17 -3
  6. package/src/local-state-io.ts +20 -0
  7. package/src/mcp-app.ts +10 -0
  8. package/src/mcp-options.ts +2 -3
  9. package/src/project-writes.ts +11 -11
  10. package/src/release/check.ts +41 -25
  11. package/src/release/index.ts +40 -0
  12. package/src/release/native-bun-registry.ts +282 -19
  13. package/src/release/pack-coherence.ts +457 -0
  14. package/src/release/policy.ts +1684 -0
  15. package/src/release/semver.ts +104 -0
  16. package/src/release/wayfinder-dogfood-smoke.ts +600 -66
  17. package/src/run-completions-install.ts +2 -2
  18. package/src/run-schema.ts +41 -0
  19. package/src/run-wayfind-outline.ts +166 -0
  20. package/src/trails/adapter-check.ts +1 -1
  21. package/src/trails/add-surface.ts +1 -1
  22. package/src/trails/add-verify.ts +3 -3
  23. package/src/trails/compile.ts +17 -25
  24. package/src/trails/create-scaffold.ts +14 -14
  25. package/src/trails/create.ts +5 -5
  26. package/src/trails/dev-support.ts +44 -24
  27. package/src/trails/doctor.ts +23 -2
  28. package/src/trails/draft-promote.ts +7 -7
  29. package/src/trails/guide.ts +15 -19
  30. package/src/trails/load-app.ts +58 -9
  31. package/src/trails/operator-context.ts +66 -0
  32. package/src/trails/regrade.ts +72 -0
  33. package/src/trails/release-check.ts +1 -0
  34. package/src/trails/run-example.ts +21 -37
  35. package/src/trails/run-examples.ts +16 -32
  36. package/src/trails/run.ts +81 -57
  37. package/src/trails/survey.ts +76 -62
  38. package/src/trails/topo-output-schemas.ts +11 -0
  39. package/src/trails/topo-pin.ts +6 -20
  40. package/src/trails/topo-read-support.ts +91 -41
  41. package/src/trails/topo-reports.ts +4 -2
  42. package/src/trails/topo-store-support.ts +172 -39
  43. package/src/trails/topo-support.ts +1 -2
  44. package/src/trails/topo.ts +5 -19
  45. package/src/trails/validate.ts +9 -20
  46. package/src/trails/version-lifecycle-support.ts +10 -20
  47. package/src/trails/warden.ts +18 -0
  48. package/src/trails/wayfind.ts +1001 -0
@@ -1,6 +1,7 @@
1
- import { mkdtemp, rm } from 'node:fs/promises';
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
- import { join, resolve } from 'node:path';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
4
5
 
5
6
  const repoRoot = resolve(process.cwd());
6
7
  const trailsBin = join(repoRoot, 'apps/trails/bin/trails.ts');
@@ -21,9 +22,9 @@ const assertObject = (value: unknown, label: string): JsonObject => {
21
22
  return value as JsonObject;
22
23
  };
23
24
 
24
- const assertFreshSource = (value: JsonObject, label: string): void => {
25
- const freshness = assertObject(value['freshness'], `${label}.freshness`);
26
- if (freshness['status'] !== 'fresh') {
25
+ const assertAlignedSource = (value: JsonObject, label: string): void => {
26
+ const drift = assertObject(value['drift'], `${label}.drift`);
27
+ if (drift['status'] !== 'aligned') {
27
28
  throw new Error(`${label} did not read fresh artifacts`);
28
29
  }
29
30
  const source = assertObject(value['source'], `${label}.source`);
@@ -40,18 +41,15 @@ const parseJson = (stdout: string, label: string): JsonObject => {
40
41
  }
41
42
  };
42
43
 
43
- const runWayfind = (tempRoot: string, args: readonly string[]): JsonObject => {
44
- const command = [
45
- process.execPath,
46
- trailsBin,
47
- 'wayfind',
48
- ...args,
49
- '--root-dir',
50
- tempRoot,
51
- '--json',
52
- ];
44
+ const runCommandProcess = (
45
+ command: readonly string[]
46
+ ): {
47
+ readonly exitCode: number;
48
+ readonly stderr: string;
49
+ readonly stdout: string;
50
+ } => {
53
51
  const result = Bun.spawnSync({
54
- cmd: command,
52
+ cmd: [...command],
55
53
  cwd: repoRoot,
56
54
  env: { ...process.env, NO_COLOR: '1' } as Record<
57
55
  string,
@@ -60,19 +58,185 @@ const runWayfind = (tempRoot: string, args: readonly string[]): JsonObject => {
60
58
  stderr: 'pipe',
61
59
  stdout: 'pipe',
62
60
  });
63
- const stdout = result.stdout.toString();
64
- const stderr = result.stderr.toString();
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);
65
70
  if (result.exitCode !== 0) {
66
71
  throw new Error(
67
72
  [
68
73
  `Wayfinder dogfood command failed: ${command.join(' ')}`,
69
74
  `exitCode: ${result.exitCode}`,
70
- `stdout: ${stdout}`,
71
- `stderr: ${stderr}`,
75
+ `stdout: ${result.stdout}`,
76
+ `stderr: ${result.stderr}`,
72
77
  ].join('\n')
73
78
  );
74
79
  }
75
- return parseJson(stdout, `trails ${args.join(' ')}`);
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, trailsCliAliases } 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
+ );
76
240
  };
77
241
 
78
242
  const assertSearchFindsWayfinder = (search: JsonObject): void => {
@@ -88,6 +252,61 @@ const assertSearchFindsWayfinder = (search: JsonObject): void => {
88
252
  }
89
253
  };
90
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
+
91
310
  const assertErrorsFindWayfinder = (errorsResult: JsonObject): void => {
92
311
  const { errors } = errorsResult;
93
312
  if (!Array.isArray(errors)) {
@@ -112,28 +331,141 @@ const assertErrorsFindWayfinder = (errorsResult: JsonObject): void => {
112
331
  }
113
332
  };
114
333
 
115
- const assertAdaptersFindHono = (adaptersResult: JsonObject): void => {
116
- const counts = assertObject(
117
- adaptersResult['counts'],
118
- 'wayfind adapters counts'
119
- );
120
- if (counts['configured'] !== 1 || counts['used'] !== 1) {
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) {
121
401
  throw new Error(
122
- 'wayfind adapters did not report configured/used adapter facts'
402
+ 'demo entity.add errors did not include AlreadyExistsError'
123
403
  );
124
404
  }
125
- if (counts['observed'] !== 0) {
126
- throw new Error('wayfind adapters reported unsupported observed facts');
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}`);
127
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');
128
455
  const { adapters } = adaptersResult;
129
456
  if (!Array.isArray(adapters)) {
130
- throw new TypeError('wayfind adapters did not return adapters');
457
+ throw new TypeError('wayfind --include adapters did not return adapters');
131
458
  }
132
- const ids = adapters
133
- .map((adapter) => assertObject(adapter, 'wayfind adapters fact')['key'])
134
- .filter((key): key is string => typeof key === 'string');
135
- if (!ids.includes('@ontrails/hono:http:used')) {
136
- throw new Error('wayfind adapters did not find Hono conformance usage');
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');
137
469
  }
138
470
  };
139
471
 
@@ -144,6 +476,9 @@ const assertContractForSearch = (contractResult: JsonObject): void => {
144
476
  }
145
477
  };
146
478
 
479
+ const unwrapWayfindResult = (value: JsonObject, label: string): JsonObject =>
480
+ assertObject(value['result'], `${label}.result`);
481
+
147
482
  const assertResolvedTarget = (value: JsonObject, label: string): void => {
148
483
  const target = assertObject(value['target'], `${label}.target`);
149
484
  if (target['id'] !== 'wayfind.search') {
@@ -151,24 +486,159 @@ const assertResolvedTarget = (value: JsonObject, label: string): void => {
151
486
  }
152
487
  };
153
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
+
154
535
  export const runWayfinderDogfoodSmoke =
155
536
  async (): Promise<WayfinderDogfoodSmokeResult> => {
156
537
  const tempRoot = await mkdtemp(join(tmpdir(), 'trails-wayfinder-dogfood-'));
157
538
 
158
539
  try {
159
- const [{ trailsMcpApp }, { exportCurrentTopo }] = await Promise.all([
160
- import('../mcp-app.js'),
161
- import('../trails/topo-store-support.js'),
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"]}',
162
549
  ]);
163
- const exported = await exportCurrentTopo(trailsMcpApp, {
164
- rootDir: tempRoot,
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',
165
592
  });
166
- if (exported.isErr()) {
167
- throw exported.error;
168
- }
169
593
 
170
- const overview = runWayfind(tempRoot, ['overview']);
171
- assertFreshSource(overview, 'wayfind overview');
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');
172
642
  const counts = assertObject(
173
643
  overview['counts'],
174
644
  'wayfind overview counts'
@@ -178,37 +648,101 @@ export const runWayfinderDogfoodSmoke =
178
648
  throw new Error('wayfind overview did not report trail counts');
179
649
  }
180
650
 
181
- const search = runWayfind(tempRoot, [
182
- 'search',
183
- '--input-json',
184
- '{"filters":{"kind":"trail","idPrefix":"wayfind."}}',
185
- ]);
186
- assertFreshSource(search, 'wayfind search');
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');
187
665
  assertSearchFindsWayfinder(search);
188
666
 
189
- const errors = runWayfind(tempRoot, [
190
- 'errors',
191
- '--input-json',
192
- '{"filters":{"kind":"trail","idPrefix":"wayfind."}}',
193
- ]);
194
- assertFreshSource(errors, 'wayfind errors');
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');
195
684
  assertErrorsFindWayfinder(errors);
196
685
 
197
- const adapters = runWayfind(repoRoot, ['adapters']);
198
- assertAdaptersFindHono(adapters);
686
+ const adapterFiltered = runWayfind(tempRoot, [
687
+ '--adapter',
688
+ '@ontrails/hono',
689
+ '--include',
690
+ 'adapters',
691
+ ]);
692
+ assertAdapterPredicateWayfind(adapterFiltered);
199
693
 
200
- const contract = runWayfind(tempRoot, ['contract', 'wayfind.search']);
201
- assertFreshSource(contract, 'wayfind contract');
694
+ const contract = unwrapWayfindResult(
695
+ runWayfind(tempRoot, ['wayfind.search', '--contract']),
696
+ 'wayfind contract'
697
+ );
698
+ assertAlignedSource(contract, 'wayfind contract');
202
699
  assertContractForSearch(contract);
203
700
 
204
- const nearby = runWayfind(tempRoot, ['nearby', 'wayfind.search']);
205
- assertFreshSource(nearby, 'wayfind nearby');
701
+ const nearby = unwrapWayfindResult(
702
+ runWayfind(tempRoot, ['wayfind.search']),
703
+ 'wayfind nearby'
704
+ );
705
+ assertAlignedSource(nearby, 'wayfind nearby');
206
706
  assertResolvedTarget(nearby, 'wayfind nearby');
207
707
 
208
- const impact = runWayfind(tempRoot, ['impact', 'wayfind.search']);
209
- assertFreshSource(impact, 'wayfind impact');
708
+ const impact = unwrapWayfindResult(
709
+ runWayfind(tempRoot, ['wayfind.search', '--impact']),
710
+ 'wayfind impact'
711
+ );
712
+ assertAlignedSource(impact, 'wayfind impact');
210
713
  assertResolvedTarget(impact, 'wayfind impact');
211
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
+
212
746
  return {
213
747
  check: 'wayfinder-dogfood',
214
748
  message: `Wayfinder dogfood smoke passed: ${String(trailCount)} trails inspected from saved operator topo artifacts.`,