@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
@@ -100,7 +100,7 @@ export const runCompletionsInstall = async (
100
100
  ): Promise<Result<CompletionsInstallResult, Error>> => {
101
101
  const shellResult = resolveTargetShell(options);
102
102
  if (shellResult.isErr()) {
103
- return Result.err(shellResult.error);
103
+ return shellResult;
104
104
  }
105
105
 
106
106
  const shell = shellResult.value;
@@ -111,7 +111,7 @@ export const runCompletionsInstall = async (
111
111
  options.binName ?? COMPLETIONS_BIN_NAME
112
112
  );
113
113
  if (scriptResult.isErr()) {
114
- return Result.err(scriptResult.error);
114
+ return scriptResult;
115
115
  }
116
116
 
117
117
  let existed: boolean;
@@ -0,0 +1,41 @@
1
+ import type { CliCommand } from '@ontrails/cli';
2
+ import { deriveCliSchema, findCliSchemaCommand } from '@ontrails/cli';
3
+ import type { Command } from 'commander';
4
+
5
+ const normalizeCommandPath = (path: readonly string[] | undefined): string[] =>
6
+ (path ?? []).flatMap((segment) =>
7
+ segment
8
+ .trim()
9
+ .split(/\s+/)
10
+ .filter((part) => part.length > 0)
11
+ );
12
+
13
+ const writeJson = (value: unknown): void => {
14
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
15
+ };
16
+
17
+ export const attachSchemaCommand = (
18
+ program: Command,
19
+ commands: readonly CliCommand[]
20
+ ): void => {
21
+ const schema = deriveCliSchema(commands);
22
+ program
23
+ .command('schema [command...]')
24
+ .description('Inspect accepted CLI command contracts as JSON')
25
+ .action((commandPath: string[] | undefined) => {
26
+ const path = normalizeCommandPath(commandPath);
27
+ if (path.length === 0) {
28
+ writeJson(schema);
29
+ return;
30
+ }
31
+
32
+ const command = findCliSchemaCommand(schema, path);
33
+ if (command === undefined) {
34
+ process.stderr.write(`Unknown CLI command: ${path.join(' ')}\n`);
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+
39
+ writeJson({ command });
40
+ });
41
+ };
@@ -0,0 +1,166 @@
1
+ import { deriveOutputMode, output } from '@ontrails/cli';
2
+ import type { ActionResultContext } from '@ontrails/cli';
3
+ import { outlineOutputSchema } from '@ontrails/wayfinder';
4
+ import type { OutlineFeature, OutlineOutput } from '@ontrails/wayfinder';
5
+
6
+ const includesFeature = (
7
+ outline: OutlineOutput,
8
+ feature: OutlineFeature
9
+ ): boolean => outline.features.included.includes(feature);
10
+
11
+ const formatLine = (
12
+ line: number,
13
+ kind: string,
14
+ name: string,
15
+ suffix = ''
16
+ ): string => `${line.toString().padStart(4, ' ')}: ${kind} ${name}${suffix}`;
17
+
18
+ const appendGraphCount = (lines: string[], outline: OutlineOutput): void => {
19
+ if (
20
+ includesFeature(outline, 'graph') &&
21
+ outline.counts.graphMatches !== undefined
22
+ ) {
23
+ lines.push(` graph matches: ${outline.counts.graphMatches.toString()}`);
24
+ }
25
+ };
26
+
27
+ const appendDiagnosticCount = (
28
+ lines: string[],
29
+ outline: OutlineOutput
30
+ ): void => {
31
+ if (
32
+ includesFeature(outline, 'diagnostics') &&
33
+ outline.counts.diagnostics > 0
34
+ ) {
35
+ lines.push(` diagnostics: ${outline.counts.diagnostics.toString()}`);
36
+ }
37
+ };
38
+
39
+ const appendSourceDeclarations = (
40
+ lines: string[],
41
+ outline: OutlineOutput
42
+ ): void => {
43
+ if (!includesFeature(outline, 'source')) {
44
+ return;
45
+ }
46
+ const declarations = outline.source?.declarations ?? [];
47
+ if (declarations.length === 0) {
48
+ return;
49
+ }
50
+ lines.push('');
51
+ for (const declaration of declarations.slice(0, 40)) {
52
+ lines.push(
53
+ formatLine(declaration.line, declaration.kind, declaration.name)
54
+ );
55
+ }
56
+ };
57
+
58
+ type TrailOutline = NonNullable<OutlineOutput['trails']>[number];
59
+
60
+ const contractFactLabel = (
61
+ contracts: TrailOutline['contracts']
62
+ ): string | undefined => {
63
+ if (contracts === undefined) {
64
+ return undefined;
65
+ }
66
+ if (contracts.input && contracts.output) {
67
+ return 'input+output';
68
+ }
69
+ if (contracts.input) {
70
+ return 'input';
71
+ }
72
+ if (contracts.output) {
73
+ return 'output';
74
+ }
75
+ return 'no schemas';
76
+ };
77
+
78
+ const exampleCountLabel = (count: number): string =>
79
+ `${count.toString()} ${count === 1 ? 'example' : 'examples'}`;
80
+
81
+ const trailFactSuffix = (trail: TrailOutline): string => {
82
+ const facts = [
83
+ trail.graph?.intent,
84
+ contractFactLabel(trail.contracts),
85
+ trail.graph === undefined
86
+ ? undefined
87
+ : exampleCountLabel(trail.graph.exampleCount),
88
+ ].filter((fact): fact is string => fact !== undefined);
89
+
90
+ return facts.length === 0 ? '' : ` (${facts.join(', ')})`;
91
+ };
92
+
93
+ const appendTrails = (lines: string[], outline: OutlineOutput): void => {
94
+ if (!includesFeature(outline, 'trails')) {
95
+ return;
96
+ }
97
+ const trails = outline.trails ?? [];
98
+ if (trails.length === 0) {
99
+ return;
100
+ }
101
+ lines.push('');
102
+ for (const trail of trails) {
103
+ lines.push(
104
+ formatLine(trail.line, 'trail', trail.id, trailFactSuffix(trail))
105
+ );
106
+ }
107
+ };
108
+
109
+ const appendApps = (lines: string[], outline: OutlineOutput): void => {
110
+ if (!includesFeature(outline, 'apps')) {
111
+ return;
112
+ }
113
+ const apps = outline.apps ?? [];
114
+ if (apps.length === 0) {
115
+ return;
116
+ }
117
+ lines.push('');
118
+ for (const app of apps) {
119
+ lines.push(formatLine(app.line, 'app', app.name, ` (${app.callee})`));
120
+ }
121
+ };
122
+
123
+ const appendDiagnostics = (lines: string[], outline: OutlineOutput): void => {
124
+ if (!includesFeature(outline, 'diagnostics')) {
125
+ return;
126
+ }
127
+ for (const diagnostic of outline.diagnostics ?? []) {
128
+ lines.push(
129
+ ` ${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`
130
+ );
131
+ }
132
+ };
133
+
134
+ export const formatWayfindOutlineText = (outline: OutlineOutput): string => {
135
+ const lines = [
136
+ outline.file,
137
+ ` trails: ${outline.counts.trails.toString()}`,
138
+ ` apps: ${outline.counts.apps.toString()}`,
139
+ ` declarations: ${outline.counts.declarations.toString()}`,
140
+ ];
141
+
142
+ appendGraphCount(lines, outline);
143
+ appendDiagnosticCount(lines, outline);
144
+ appendSourceDeclarations(lines, outline);
145
+ appendTrails(lines, outline);
146
+ appendApps(lines, outline);
147
+ appendDiagnostics(lines, outline);
148
+
149
+ return lines.join('\n');
150
+ };
151
+
152
+ export const tryWayfindOutlineOutput = (ctx: ActionResultContext): boolean => {
153
+ if (ctx.trail.id !== 'wayfind.outline' || ctx.result.isErr()) {
154
+ return false;
155
+ }
156
+ const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
157
+ if (mode !== 'text') {
158
+ return false;
159
+ }
160
+ const parsed = outlineOutputSchema.safeParse(ctx.result.value);
161
+ if (!parsed.success) {
162
+ return false;
163
+ }
164
+ output(formatWayfindOutlineText(parsed.data), mode);
165
+ return true;
166
+ };
@@ -145,7 +145,7 @@ const validateAdapterCheckRoot = (
145
145
 
146
146
  const manifest = readWorkspaceManifest(packageJsonPath);
147
147
  if (manifest.isErr()) {
148
- return Result.err(manifest.error);
148
+ return manifest;
149
149
  }
150
150
 
151
151
  if (workspacePatternsFromManifest(manifest.value).length === 0) {
@@ -96,7 +96,7 @@ const updatePkgJsonForSurface = async (
96
96
  ): Promise<Result<string, Error>> => {
97
97
  const pkgPathResult = resolveProjectPath(cwd, 'package.json');
98
98
  if (pkgPathResult.isErr()) {
99
- return Result.err(pkgPathResult.error);
99
+ return pkgPathResult;
100
100
  }
101
101
 
102
102
  const pkgPath = pkgPathResult.value;
@@ -70,7 +70,7 @@ const updatePackageJsonForVerify = async (
70
70
  ): Promise<Result<void, Error>> => {
71
71
  const pkgPathResult = resolveProjectPath(projectDir, 'package.json');
72
72
  if (pkgPathResult.isErr()) {
73
- return Result.err(pkgPathResult.error);
73
+ return pkgPathResult;
74
74
  }
75
75
 
76
76
  const pkgPath = pkgPathResult.value;
@@ -107,7 +107,7 @@ export const addVerify = trail('add.verify', {
107
107
  ): Promise<Result<void, Error>> => {
108
108
  const exists = projectPathExists(projectDir, relativePath);
109
109
  if (exists.isErr()) {
110
- return Result.err(exists.error);
110
+ return exists;
111
111
  }
112
112
  if (exists.value) {
113
113
  return Result.ok();
@@ -115,7 +115,7 @@ export const addVerify = trail('add.verify', {
115
115
 
116
116
  const written = await writeProjectFile(projectDir, relativePath, content);
117
117
  if (written.isErr()) {
118
- return Result.err(written.error);
118
+ return written;
119
119
  }
120
120
  files.push(written.value);
121
121
  return Result.ok();
@@ -1,9 +1,8 @@
1
1
  import { trail } from '@ontrails/core';
2
- import type { Result, Topo } from '@ontrails/core';
2
+ import type { CliCommandAliasInput, Result, Topo } from '@ontrails/core';
3
3
  import { z } from 'zod';
4
4
 
5
- import { tryLoadFreshAppLease } from './load-app.js';
6
- import { resolveTrailRootDir } from './root-dir.js';
5
+ import { withFreshOperatorApp } from './operator-context.js';
7
6
  import { exportCurrentTopo } from './topo-store-support.js';
8
7
  import type { TopoExportReport } from './topo-support.js';
9
8
  import {
@@ -13,7 +12,13 @@ import {
13
12
 
14
13
  export const compileCurrentTopo = async (
15
14
  app: Topo,
16
- options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
15
+ options?: {
16
+ readonly cliAliases?:
17
+ | Readonly<Record<string, readonly CliCommandAliasInput[]>>
18
+ | undefined;
19
+ readonly force?: boolean | undefined;
20
+ readonly rootDir?: string;
21
+ }
17
22
  ): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
18
23
 
19
24
  const compileTrailInputSchema = z.object({
@@ -28,31 +33,19 @@ const compileTrailInputSchema = z.object({
28
33
  type CompileTrailInput = z.output<typeof compileTrailInputSchema>;
29
34
 
30
35
  export const compileTrail = trail('compile', {
31
- blaze: async (input: CompileTrailInput, ctx) => {
32
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
33
- if (rootDirResult.isErr()) {
34
- return rootDirResult;
35
- }
36
- const rootDir = rootDirResult.value;
37
- const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
38
- if (leaseResult.isErr()) {
39
- return leaseResult;
40
- }
41
- const lease = leaseResult.value;
42
- try {
43
- return await compileCurrentTopo(lease.app, {
36
+ blaze: async (input: CompileTrailInput, ctx) =>
37
+ withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
38
+ compileCurrentTopo(lease.app, {
39
+ cliAliases: lease.cliAliases,
44
40
  force: input.force,
45
41
  rootDir,
46
- });
47
- } finally {
48
- lease.release();
49
- }
50
- },
51
- description: 'Compile the current topo to .trails artifacts',
42
+ })
43
+ ),
44
+ description: 'Compile the current topo to trails.lock',
52
45
  examples: [
53
46
  {
54
47
  input: createIsolatedExampleInput('compile'),
55
- name: 'Compile the current topo artifacts',
48
+ name: 'Compile the current topo to trails.lock',
56
49
  },
57
50
  ],
58
51
  input: compileTrailInputSchema,
@@ -61,7 +54,6 @@ export const compileTrail = trail('compile', {
61
54
  hash: z.string(),
62
55
  lockPath: z.string(),
63
56
  snapshot: topoSnapshotOutput,
64
- topoPath: z.string(),
65
57
  }),
66
58
  permit: { scopes: ['topo:write'] },
67
59
  });
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * `create.scaffold` trail -- Creates base project structure.
3
3
  *
4
- * Generates package.json, tsconfig, app.ts, starter trails, and .trails/ directory.
4
+ * Generates package.json, tsconfig, app.ts, starter trails, and scaffold provenance.
5
5
  */
6
6
 
7
7
  import { resolve } from 'node:path';
8
8
 
9
- import { Result, trail, WORKSPACE_GITIGNORE_CONTENT } from '@ontrails/core';
9
+ import { Result, trail } from '@ontrails/core';
10
+ import type { Result as TrailsResult } from '@ontrails/core';
10
11
  import { z } from 'zod';
11
12
 
12
13
  import {
@@ -168,7 +169,7 @@ bun run guide
168
169
  - \`compose\`, not follow
169
170
  - \`surface\`, not transport
170
171
  - \`resource\`, not service or dependency
171
- - \`layer\`, for compose-cutting trail wrapping
172
+ - \`layer\`, for cross-cutting trail wrapping
172
173
 
173
174
  ## Trail Rules
174
175
 
@@ -196,10 +197,7 @@ Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap
196
197
  const GITIGNORE_CONTENT = `node_modules/
197
198
  dist/
198
199
  *.tsbuildinfo
199
- .trails/cache/
200
- .trails/state/
201
- .trails/config.local.js
202
- .trails/config.local.ts
200
+ trails.config.local.*
203
201
  `;
204
202
 
205
203
  const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
@@ -552,7 +550,6 @@ const collectScaffoldFiles = (
552
550
  ['.gitignore', GITIGNORE_CONTENT],
553
551
  ['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
554
552
  ['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
555
- ['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
556
553
  ['.trails/scaffold.json', generateScaffoldProvenance(starter)],
557
554
  ['src/app.ts', generateAppTs(name, starter)],
558
555
  ...starterFileGenerators[starter](),
@@ -583,13 +580,16 @@ export const createScaffold = trail('create.scaffold', {
583
580
  const dryRun = input.dryRun === true;
584
581
  const fileMap = collectScaffoldFiles(input.name, starter);
585
582
  const operations = collectScaffoldOperations(fileMap);
586
- const plannedOperations = dryRun
587
- ? planProjectOperations(projectDir, operations, { existing: 'preserve' })
588
- : await applyProjectOperations(projectDir, operations, {
589
- existing: 'preserve',
590
- });
583
+ const plannedOperations: TrailsResult<PlannedProjectOperation[], Error> =
584
+ dryRun
585
+ ? planProjectOperations(projectDir, operations, {
586
+ existing: 'preserve',
587
+ })
588
+ : await applyProjectOperations(projectDir, operations, {
589
+ existing: 'preserve',
590
+ });
591
591
  if (plannedOperations.isErr()) {
592
- return Result.err(plannedOperations.error);
592
+ return plannedOperations;
593
593
  }
594
594
 
595
595
  const created = dryRun
@@ -86,7 +86,7 @@ const collectSurfaceFiles = async (
86
86
  for (const surface of surfaces) {
87
87
  const result = await addSurface(surface);
88
88
  if (result.isErr()) {
89
- return Result.err(result.error);
89
+ return result;
90
90
  }
91
91
  if (result.value.created !== null) {
92
92
  created.push(result.value.created);
@@ -181,7 +181,7 @@ const writeReadme = async (
181
181
  ): Promise<Result<string | null, Error>> => {
182
182
  const exists = projectPathExists(dir, 'README.md');
183
183
  if (exists.isErr()) {
184
- return Result.err(exists.error);
184
+ return exists;
185
185
  }
186
186
  if (exists.value) {
187
187
  return Result.ok(null);
@@ -225,7 +225,7 @@ export const createTrail = trail('create', {
225
225
  )
226
226
  );
227
227
  if (surfaceFiles.isErr()) {
228
- return Result.err(surfaceFiles.error);
228
+ return surfaceFiles;
229
229
  }
230
230
 
231
231
  const verifyFiles = await collectVerifyFiles(input.verify, () =>
@@ -235,12 +235,12 @@ export const createTrail = trail('create', {
235
235
  )
236
236
  );
237
237
  if (verifyFiles.isErr()) {
238
- return Result.err(verifyFiles.error);
238
+ return verifyFiles;
239
239
  }
240
240
 
241
241
  const readmeFile = await writeReadme(input, scaffolded.value.dir);
242
242
  if (readmeFile.isErr()) {
243
- return Result.err(readmeFile.error);
243
+ return readmeFile;
244
244
  }
245
245
 
246
246
  return Result.ok({
@@ -1,11 +1,10 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { isAbsolute, join } from 'node:path';
3
3
 
4
4
  import {
5
5
  openReadTrailsDb,
6
6
  openWriteTrailsDb,
7
7
  deriveTrailsDbPath,
8
- deriveTrailsDir,
9
8
  } from '@ontrails/core';
10
9
  import {
11
10
  countPinnedSnapshots,
@@ -21,7 +20,10 @@ import {
21
20
  previewTraceCleanup,
22
21
  } from '@ontrails/tracing';
23
22
 
24
- import { removeRootRelativeFileIfPresent } from '../local-state-io.js';
23
+ import {
24
+ removeFileIfPresent,
25
+ removeRootRelativeFileIfPresent,
26
+ } from '../local-state-io.js';
25
27
 
26
28
  import { requireTrailRootDir } from './root-dir.js';
27
29
 
@@ -31,9 +33,17 @@ const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
31
33
 
32
34
  const removeResetFileIfPresent = (
33
35
  rootDir: string,
34
- relativePath: string
36
+ resetPath: string
35
37
  ): boolean => {
36
- const removed = removeRootRelativeFileIfPresent(rootDir, relativePath);
38
+ if (isAbsolute(resetPath)) {
39
+ const removed = removeFileIfPresent(resetPath);
40
+ if (removed.isErr()) {
41
+ throw removed.error;
42
+ }
43
+ return removed.value;
44
+ }
45
+
46
+ const removed = removeRootRelativeFileIfPresent(rootDir, resetPath);
37
47
  if (removed.isErr()) {
38
48
  throw removed.error;
39
49
  }
@@ -139,7 +149,7 @@ const buildDbStats = (
139
149
  ): DevStatsReport['db'] => ({
140
150
  exists,
141
151
  fileSizeBytes: exists ? statSync(dbPath).size : 0,
142
- path: '.trails/state/trails.db',
152
+ path: dbPath,
143
153
  });
144
154
 
145
155
  const emptyDevStats = (
@@ -184,8 +194,7 @@ const liveDevStats = (
184
194
  const deriveDevStatsContext = (options?: DevRetentionOptions) => {
185
195
  const rootDir = deriveRootDir(options?.rootDir);
186
196
  const dbPath = deriveTrailsDbPath({ rootDir });
187
- const trailsDir = deriveTrailsDir({ rootDir });
188
- const lockPath = join(trailsDir, 'trails.lock');
197
+ const lockPath = join(rootDir, 'trails.lock');
189
198
  return {
190
199
  dbExists: existsSync(dbPath),
191
200
  dbPath,
@@ -256,24 +265,35 @@ const buildCleanReport = (
256
265
  };
257
266
  };
258
267
 
259
- const RESET_FILES = [
260
- '.trails/state/trails.db',
261
- '.trails/state/trails.db-shm',
262
- '.trails/state/trails.db-wal',
263
- // Legacy paths (pre-state migration) — cleaned for one cycle so upgrading
264
- // workspaces do not leave stale DB sidecars at old locations.
265
- '.trails/trails.db',
266
- '.trails/trails.db-shm',
267
- '.trails/trails.db-wal',
268
- '.trails/dev/tracing.db',
269
- '.trails/dev/tracing.db-shm',
270
- '.trails/dev/tracing.db-wal',
268
+ const dbSidecarPaths = (basePath: string): readonly string[] => [
269
+ basePath,
270
+ `${basePath}-shm`,
271
+ `${basePath}-wal`,
272
+ ];
273
+
274
+ const legacyRepoPath = (...segments: readonly string[]) => segments.join('/');
275
+ const legacyDbName = (...segments: readonly string[]) => segments.join('.');
276
+
277
+ const legacyResetFiles = [
278
+ // Legacy paths (pre-state migration) cleaned for one cycle so upgrading
279
+ // workspaces do not leave stale DB sidecars at old repo-local locations.
280
+ ...dbSidecarPaths(legacyRepoPath('.trails', 'state', 'trails.db')),
281
+ ...dbSidecarPaths(legacyRepoPath('.trails', legacyDbName('trails', 'db'))),
282
+ ...dbSidecarPaths(legacyRepoPath('.trails', 'dev', 'tracing.db')),
271
283
  ] as const;
272
284
 
273
- const presentResetFiles = (
274
- rootDir: string
275
- ): readonly (typeof RESET_FILES)[number][] =>
276
- RESET_FILES.filter((relativePath) => existsSync(join(rootDir, relativePath)));
285
+ const currentResetFiles = (rootDir: string): readonly string[] => {
286
+ const dbPath = deriveTrailsDbPath({ rootDir });
287
+ return dbSidecarPaths(dbPath);
288
+ };
289
+
290
+ const resetFileExists = (rootDir: string, resetPath: string): boolean =>
291
+ existsSync(isAbsolute(resetPath) ? resetPath : join(rootDir, resetPath));
292
+
293
+ const presentResetFiles = (rootDir: string): readonly string[] =>
294
+ [...currentResetFiles(rootDir), ...legacyResetFiles].filter((resetPath) =>
295
+ resetFileExists(rootDir, resetPath)
296
+ );
277
297
 
278
298
  export const buildDevStats = (
279
299
  options?: DevRetentionOptions
@@ -1,4 +1,10 @@
1
- import { deriveTrailsDir, Result, trail } from '@ontrails/core';
1
+ import {
2
+ deriveTrailsDir,
3
+ InternalError,
4
+ isTrailsError,
5
+ Result,
6
+ trail,
7
+ } from '@ontrails/core';
2
8
  import { readTopoGraph } from '@ontrails/topographer';
3
9
  import { z } from 'zod';
4
10
 
@@ -18,11 +24,26 @@ const readDoctorForceGraph = async (
18
24
  }
19
25
  };
20
26
 
27
+ const toDoctorSummaryError = (error: unknown): InternalError =>
28
+ error instanceof Error
29
+ ? new InternalError('Unable to derive doctor summary', { cause: error })
30
+ : new InternalError(`Unable to derive doctor summary: ${String(error)}`);
31
+
21
32
  export const doctorTrail = trail('doctor', {
22
33
  blaze: async (input, ctx) =>
23
34
  withLifecycleApp(input, ctx.cwd, async (app, rootDir) => {
24
35
  const forceGraph = await readDoctorForceGraph(rootDir);
25
- return Result.ok(deriveDoctorSummary(app, { forceGraph }));
36
+ try {
37
+ return Result.ok(deriveDoctorSummary(app, { forceGraph }));
38
+ } catch (error) {
39
+ // deriveTopoGraph throws ValidationError on invalid topos; doctor must
40
+ // report that diagnosis instead of letting the pipeline redact it to a
41
+ // generic internal error.
42
+ if (isTrailsError(error)) {
43
+ return Result.err(error);
44
+ }
45
+ return Result.err(toDoctorSummaryError(error));
46
+ }
26
47
  }),
27
48
  description: 'Diagnose trail versioning lifecycle state',
28
49
  input: z.object({
@@ -571,7 +571,7 @@ const planRelativeImports = (
571
571
  operations
572
572
  );
573
573
  if (changed.isErr()) {
574
- return Result.err(changed.error);
574
+ return changed;
575
575
  }
576
576
  if (changed.value) {
577
577
  updatedSourceFiles.add(filePath);
@@ -593,7 +593,7 @@ const rewritePromotionState = async (
593
593
  const initialFiles = collectTsFiles(rootDir);
594
594
  const sourcesResult = await readSourceFiles(initialFiles);
595
595
  if (sourcesResult.isErr()) {
596
- return Result.err(sourcesResult.error);
596
+ return sourcesResult;
597
597
  }
598
598
  const sources = sourcesResult.value;
599
599
  const operations: ProjectWriteOperation[] = [];
@@ -608,14 +608,14 @@ const rewritePromotionState = async (
608
608
  operations
609
609
  );
610
610
  if (rewritten.isErr()) {
611
- return Result.err(rewritten.error);
611
+ return rewritten;
612
612
  }
613
613
 
614
614
  const renamesResult = input.renameFiles
615
615
  ? collectFileRenames(initialFiles, sources)
616
616
  : Result.ok([] as FileRename[]);
617
617
  if (renamesResult.isErr()) {
618
- return Result.err(renamesResult.error);
618
+ return renamesResult;
619
619
  }
620
620
 
621
621
  const valid = validateRenameTargets(renamesResult.value);
@@ -637,14 +637,14 @@ const rewritePromotionState = async (
637
637
  operations
638
638
  );
639
639
  if (importUpdates.isErr()) {
640
- return Result.err(importUpdates.error);
640
+ return importUpdates;
641
641
  }
642
642
 
643
643
  const plannedOperations = input.dryRun
644
644
  ? planProjectOperations(rootDir, operations)
645
645
  : await applyProjectOperations(rootDir, operations);
646
646
  if (plannedOperations.isErr()) {
647
- return Result.err(plannedOperations.error);
647
+ return plannedOperations;
648
648
  }
649
649
  return Result.ok({
650
650
  plannedOperations: plannedOperations.value,
@@ -837,7 +837,7 @@ const promoteDraftState = async (
837
837
 
838
838
  const rewriteResult = await rewritePromotionState(rootDirResult.value, input);
839
839
  if (rewriteResult.isErr()) {
840
- return Result.err(rewriteResult.error);
840
+ return rewriteResult;
841
841
  }
842
842
 
843
843
  const { renames, updatedSourceFiles } = rewriteResult.value;