@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.
- package/CHANGELOG.md +209 -0
- package/README.md +6 -5
- package/package.json +16 -14
- package/src/app.ts +42 -3
- package/src/cli.ts +17 -3
- package/src/local-state-io.ts +20 -0
- package/src/mcp-app.ts +10 -0
- package/src/mcp-options.ts +2 -3
- package/src/project-writes.ts +11 -11
- package/src/release/check.ts +41 -25
- package/src/release/index.ts +40 -0
- package/src/release/native-bun-registry.ts +282 -19
- package/src/release/pack-coherence.ts +457 -0
- package/src/release/policy.ts +1684 -0
- package/src/release/semver.ts +104 -0
- package/src/release/wayfinder-dogfood-smoke.ts +600 -66
- package/src/run-completions-install.ts +2 -2
- package/src/run-schema.ts +41 -0
- package/src/run-wayfind-outline.ts +166 -0
- package/src/trails/adapter-check.ts +1 -1
- package/src/trails/add-surface.ts +1 -1
- package/src/trails/add-verify.ts +3 -3
- package/src/trails/compile.ts +17 -25
- package/src/trails/create-scaffold.ts +14 -14
- package/src/trails/create.ts +5 -5
- package/src/trails/dev-support.ts +44 -24
- package/src/trails/doctor.ts +23 -2
- package/src/trails/draft-promote.ts +7 -7
- package/src/trails/guide.ts +15 -19
- package/src/trails/load-app.ts +58 -9
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/regrade.ts +72 -0
- package/src/trails/release-check.ts +1 -0
- package/src/trails/run-example.ts +21 -37
- package/src/trails/run-examples.ts +16 -32
- package/src/trails/run.ts +81 -57
- package/src/trails/survey.ts +76 -62
- package/src/trails/topo-output-schemas.ts +11 -0
- package/src/trails/topo-pin.ts +6 -20
- package/src/trails/topo-read-support.ts +91 -41
- package/src/trails/topo-reports.ts +4 -2
- package/src/trails/topo-store-support.ts +172 -39
- package/src/trails/topo-support.ts +1 -2
- package/src/trails/topo.ts +5 -19
- package/src/trails/validate.ts +9 -20
- package/src/trails/version-lifecycle-support.ts +10 -20
- package/src/trails/warden.ts +18 -0
- 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
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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,
|
|
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
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
'wayfind
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
'
|
|
402
|
+
'demo entity.add errors did not include AlreadyExistsError'
|
|
123
403
|
);
|
|
124
404
|
}
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
133
|
-
.map((adapter) => assertObject(adapter, 'wayfind
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
|
182
|
-
'
|
|
183
|
-
'--
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
190
|
-
'
|
|
191
|
-
'--
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
198
|
-
|
|
686
|
+
const adapterFiltered = runWayfind(tempRoot, [
|
|
687
|
+
'--adapter',
|
|
688
|
+
'@ontrails/hono',
|
|
689
|
+
'--include',
|
|
690
|
+
'adapters',
|
|
691
|
+
]);
|
|
692
|
+
assertAdapterPredicateWayfind(adapterFiltered);
|
|
199
693
|
|
|
200
|
-
const contract =
|
|
201
|
-
|
|
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 =
|
|
205
|
-
|
|
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 =
|
|
209
|
-
|
|
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.`,
|