@ontrails/trails 1.0.0-beta.24 → 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 +189 -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 +31 -44
  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
@@ -7,18 +7,20 @@
7
7
 
8
8
  import { Database } from 'bun:sqlite';
9
9
 
10
- import type { Topo } from '@ontrails/core';
10
+ import type { CliCommandAliasInput, Topo } from '@ontrails/core';
11
11
  import {
12
12
  ConflictError,
13
13
  deriveTrailsDir,
14
14
  InternalError,
15
15
  openWriteTrailsDb,
16
16
  Result,
17
+ TimeoutError,
17
18
  } from '@ontrails/core';
18
19
  import type {
19
20
  LockManifest,
20
21
  TopoGraph,
21
22
  TopoSnapshot,
23
+ TrailsLock,
22
24
  } from '@ontrails/topographer';
23
25
  import type { StoredTopoExport } from '@ontrails/topographer/backend-support';
24
26
  import {
@@ -27,14 +29,15 @@ import {
27
29
  deriveTopoGraphDiff,
28
30
  deriveTopoGraphHash,
29
31
  readTopoGraph,
30
- writeLockManifest,
31
- writeTopoGraph,
32
+ writeTrailsLock,
32
33
  } from '@ontrails/topographer';
33
34
  import {
34
35
  createStoredTopoSnapshot,
35
36
  getStoredTopoExport,
36
37
  } from '@ontrails/topographer/backend-support';
37
38
 
39
+ import { removeRootRelativeFileIfPresent } from '../local-state-io.js';
40
+
38
41
  import type { TopoExportReport } from './topo-support.js';
39
42
  import {
40
43
  deriveRootDir,
@@ -42,15 +45,21 @@ import {
42
45
  readGitState,
43
46
  } from './topo-support.js';
44
47
 
48
+ type CliAliasesOption = Readonly<
49
+ Record<string, readonly CliCommandAliasInput[]>
50
+ >;
51
+
45
52
  const persistAndReadStoredExport = (
46
53
  app: Topo,
47
54
  db: ReturnType<typeof openWriteTrailsDb>,
48
- rootDir: string
55
+ rootDir: string,
56
+ options?: { readonly cliAliases?: CliAliasesOption | undefined } | undefined
49
57
  ): Result<
50
58
  { snapshot: TopoSnapshot; storedExport: StoredTopoExport },
51
59
  Error
52
60
  > => {
53
61
  const snapshotResult = createStoredTopoSnapshot(db, app, {
62
+ cliAliases: options?.cliAliases,
54
63
  ...readGitState(rootDir),
55
64
  ...deriveTopoCounts(app),
56
65
  });
@@ -75,15 +84,58 @@ const persistAndReadStoredExport = (
75
84
  });
76
85
  };
77
86
 
87
+ const asError = (error: unknown): Error =>
88
+ error instanceof Error ? error : new Error(String(error));
89
+
90
+ const readErrorCode = (error: unknown): string | undefined => {
91
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
92
+ return undefined;
93
+ }
94
+ const { code } = error as { readonly code?: unknown };
95
+ return typeof code === 'string' ? code : undefined;
96
+ };
97
+
98
+ const isSqliteLockError = (error: unknown): boolean => {
99
+ const code = readErrorCode(error);
100
+ if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED') {
101
+ return true;
102
+ }
103
+ return asError(error).message.match(/database is (locked|busy)/i) !== null;
104
+ };
105
+
106
+ export const mapTopoExportError = (error: unknown): Error => {
107
+ if (!isSqliteLockError(error)) {
108
+ return error instanceof Error
109
+ ? error
110
+ : new InternalError('Unable to write topo artifacts');
111
+ }
112
+ return new TimeoutError(
113
+ 'Timed out waiting for the Trails topo store lock while compiling artifacts. Another topo write may be running; retry after it finishes.',
114
+ {
115
+ cause: asError(error),
116
+ context: {
117
+ operation: 'compile',
118
+ reason: 'sqlite-lock-contention',
119
+ resource: 'trails.db',
120
+ },
121
+ }
122
+ );
123
+ };
124
+
78
125
  export const deriveCurrentTopoExport = (
79
126
  app: Topo,
80
- options?: { readonly rootDir?: string }
127
+ options?: {
128
+ readonly cliAliases?: CliAliasesOption | undefined;
129
+ readonly rootDir?: string;
130
+ }
81
131
  ): Result<StoredTopoExport, Error> => {
82
132
  const rootDir = deriveRootDir(options?.rootDir);
83
133
  const db = new Database(':memory:');
84
134
 
85
135
  try {
86
- const projected = persistAndReadStoredExport(app, db, rootDir);
136
+ const projected = persistAndReadStoredExport(app, db, rootDir, {
137
+ cliAliases: options?.cliAliases,
138
+ });
87
139
  return projected.isErr()
88
140
  ? projected
89
141
  : Result.ok(projected.value.storedExport);
@@ -92,12 +144,22 @@ export const deriveCurrentTopoExport = (
92
144
  }
93
145
  };
94
146
 
95
- const writeStoredExportArtifacts = async (
147
+ const readPreviousCommittedTopo = async (
148
+ rootDir: string
149
+ ): Promise<TopoGraph | null> => {
150
+ const rootTopo = await readTopoGraph({ dir: rootDir });
151
+ return rootTopo ?? readTopoGraph({ dir: deriveTrailsDir({ rootDir }) });
152
+ };
153
+
154
+ const prepareStoredExportArtifacts = async (
96
155
  storedExport: StoredTopoExport,
97
- trailsDir: string,
156
+ rootDir: string,
98
157
  options?: { readonly force?: boolean | undefined }
99
- ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>> => {
100
- const previousTopo = await readTopoGraph({ dir: trailsDir });
158
+ ): Promise<{
159
+ readonly hash: string;
160
+ readonly topoGraph: TopoGraph;
161
+ }> => {
162
+ const previousTopo = await readPreviousCommittedTopo(rootDir);
101
163
  const nextTopo = JSON.parse(storedExport.topoGraphJson) as TopoGraph;
102
164
  const diff =
103
165
  previousTopo === null
@@ -118,57 +180,128 @@ const writeStoredExportArtifacts = async (
118
180
  ? topoGraphBase
119
181
  : annotateTopoGraphForces(topoGraphBase, diff.breaking);
120
182
  const hash = deriveTopoGraphHash(topoGraph);
121
- const lockManifest = {
122
- ...(JSON.parse(storedExport.lockManifestJson) as LockManifest),
123
- artifacts: [
124
- {
125
- path: 'topo.lock',
126
- role: 'topo',
127
- sha256: hash,
128
- },
129
- ],
130
- } satisfies LockManifest;
131
-
132
- const topoPath = await writeTopoGraph(topoGraph, { dir: trailsDir });
133
- const lockPath = await writeLockManifest(lockManifest, { dir: trailsDir });
134
183
 
135
184
  return {
136
185
  hash,
186
+ topoGraph,
187
+ };
188
+ };
189
+
190
+ const LEGACY_COMMITTED_ARTIFACTS = [
191
+ '.trails/topo.lock',
192
+ '.trails/trails.lock',
193
+ ] as const;
194
+
195
+ const removeLegacyCommittedArtifacts = (
196
+ rootDir: string
197
+ ): Result<void, Error> => {
198
+ for (const relativePath of LEGACY_COMMITTED_ARTIFACTS) {
199
+ const removed = removeRootRelativeFileIfPresent(rootDir, relativePath);
200
+ if (removed.isErr()) {
201
+ return removed;
202
+ }
203
+ }
204
+
205
+ return Result.ok();
206
+ };
207
+
208
+ const writeStoredExportArtifacts = async (
209
+ storedExport: StoredTopoExport,
210
+ rootDir: string,
211
+ options?: { readonly force?: boolean | undefined }
212
+ ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath'>> => {
213
+ const prepared = await prepareStoredExportArtifacts(
214
+ storedExport,
215
+ rootDir,
216
+ options
217
+ );
218
+
219
+ const lockManifest = JSON.parse(
220
+ storedExport.lockManifestJson
221
+ ) as LockManifest;
222
+ const lockPath = await writeTrailsLock(
223
+ {
224
+ scope: lockManifest.scope,
225
+ summary: {
226
+ contours: prepared.topoGraph.entries.filter(
227
+ (entry) => entry.kind === 'contour'
228
+ ).length,
229
+ resources: prepared.topoGraph.entries.filter(
230
+ (entry) => entry.kind === 'resource'
231
+ ).length,
232
+ signals: prepared.topoGraph.entries.filter(
233
+ (entry) => entry.kind === 'signal'
234
+ ).length,
235
+ trails: prepared.topoGraph.entries.filter(
236
+ (entry) => entry.kind === 'trail'
237
+ ).length,
238
+ },
239
+ topoGraph: prepared.topoGraph,
240
+ topoGraphHash: prepared.hash,
241
+ version: 4,
242
+ } as TrailsLock,
243
+ { dir: rootDir }
244
+ );
245
+ const removedLegacyArtifacts = removeLegacyCommittedArtifacts(rootDir);
246
+ if (removedLegacyArtifacts.isErr()) {
247
+ throw removedLegacyArtifacts.error;
248
+ }
249
+
250
+ return {
251
+ hash: prepared.hash,
137
252
  lockPath,
138
- topoPath,
139
253
  };
140
254
  };
141
255
 
142
256
  export const exportCurrentTopo = async (
143
257
  app: Topo,
144
- options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
258
+ options?: {
259
+ readonly cliAliases?: CliAliasesOption | undefined;
260
+ readonly force?: boolean | undefined;
261
+ readonly rootDir?: string;
262
+ }
145
263
  ): Promise<Result<TopoExportReport, Error>> => {
146
264
  const rootDir = deriveRootDir(options?.rootDir);
147
- const db = openWriteTrailsDb({ rootDir });
265
+ let db: ReturnType<typeof openWriteTrailsDb> | undefined;
148
266
 
149
267
  try {
150
- const persisted = persistAndReadStoredExport(app, db, rootDir);
268
+ const candidate = deriveCurrentTopoExport(app, {
269
+ cliAliases: options?.cliAliases,
270
+ rootDir,
271
+ });
272
+ if (candidate.isErr()) {
273
+ return candidate;
274
+ }
275
+
276
+ try {
277
+ await prepareStoredExportArtifacts(candidate.value, rootDir, {
278
+ force: options?.force,
279
+ });
280
+ } catch (error: unknown) {
281
+ return Result.err(mapTopoExportError(error));
282
+ }
283
+
284
+ db = openWriteTrailsDb({ rootDir });
285
+ const persisted = persistAndReadStoredExport(app, db, rootDir, {
286
+ cliAliases: options?.cliAliases,
287
+ });
151
288
  if (persisted.isErr()) {
152
289
  return persisted;
153
290
  }
154
291
 
155
292
  const { snapshot, storedExport } = persisted.value;
156
- let artifacts: Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>;
293
+ let artifacts: Pick<TopoExportReport, 'hash' | 'lockPath'>;
157
294
  try {
158
- artifacts = await writeStoredExportArtifacts(
159
- storedExport,
160
- deriveTrailsDir({ rootDir }),
161
- { force: options?.force }
162
- );
295
+ artifacts = await writeStoredExportArtifacts(storedExport, rootDir, {
296
+ force: options?.force,
297
+ });
163
298
  } catch (error: unknown) {
164
- return Result.err(
165
- error instanceof Error
166
- ? error
167
- : new InternalError('Unable to write topo artifacts')
168
- );
299
+ return Result.err(mapTopoExportError(error));
169
300
  }
170
301
  return Result.ok({ ...artifacts, snapshot });
302
+ } catch (error: unknown) {
303
+ return Result.err(mapTopoExportError(error));
171
304
  } finally {
172
- db.close();
305
+ db?.close();
173
306
  }
174
307
  };
@@ -33,7 +33,7 @@ export const topoSnapshotOutput = z.object({
33
33
  });
34
34
 
35
35
  export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
36
- export const LOCK_PATH = '.trails/trails.lock';
36
+ export const LOCK_PATH = 'trails.lock';
37
37
  const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
38
38
 
39
39
  const uniqueExampleRootName = (name: string): string =>
@@ -59,7 +59,6 @@ export interface TopoExportReport {
59
59
  readonly hash: string;
60
60
  readonly lockPath: string;
61
61
  readonly snapshot: TopoSnapshot;
62
- readonly topoPath: string;
63
62
  }
64
63
 
65
64
  export interface TopoValidateReport {
@@ -1,8 +1,7 @@
1
1
  import { Result, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
- import { tryLoadFreshAppLease } from './load-app.js';
5
- import { resolveTrailRootDir } from './root-dir.js';
4
+ import { withFreshOperatorApp } from './operator-context.js';
6
5
  import { activationOverviewOutput } from './topo-output-schemas.js';
7
6
  import { buildTopoSummary } from './topo-read-support.js';
8
7
  import { createIsolatedExampleInput } from './topo-support.js';
@@ -73,23 +72,10 @@ const summaryOutput = z.object({
73
72
  });
74
73
 
75
74
  export const topoTrail = trail('topo', {
76
- blaze: async (input, ctx) => {
77
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
78
- if (rootDirResult.isErr()) {
79
- return rootDirResult;
80
- }
81
- const rootDir = rootDirResult.value;
82
- const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
83
- if (leaseResult.isErr()) {
84
- return leaseResult;
85
- }
86
- const lease = leaseResult.value;
87
- try {
88
- return Result.ok(buildTopoSummary(lease.app, { rootDir }));
89
- } finally {
90
- lease.release();
91
- }
92
- },
75
+ blaze: async (input, ctx) =>
76
+ withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
77
+ Result.ok(buildTopoSummary(lease.app, { rootDir }))
78
+ ),
93
79
  description: 'Show the current topo summary and entry list',
94
80
  examples: [
95
81
  {
@@ -1,29 +1,18 @@
1
1
  import { trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
- import { tryLoadFreshAppLease } from './load-app.js';
5
- import { resolveTrailRootDir } from './root-dir.js';
4
+ import { withFreshOperatorApp } from './operator-context.js';
6
5
  import { validateCurrentTopo } from './topo-read-support.js';
7
6
 
8
7
  export const validateTrail = trail('validate', {
9
- blaze: async (input, ctx) => {
10
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
11
- if (rootDirResult.isErr()) {
12
- return rootDirResult;
13
- }
14
- const rootDir = rootDirResult.value;
15
- const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
16
- if (leaseResult.isErr()) {
17
- return leaseResult;
18
- }
19
- const lease = leaseResult.value;
20
- try {
21
- return await validateCurrentTopo(lease.app, { rootDir });
22
- } finally {
23
- lease.release();
24
- }
25
- },
26
- description: 'Validate that committed topo artifacts match the current topo',
8
+ blaze: async (input, ctx) =>
9
+ withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
10
+ validateCurrentTopo(lease.app, {
11
+ cliAliases: lease.cliAliases,
12
+ rootDir,
13
+ })
14
+ ),
15
+ description: 'Validate that root trails.lock matches the current topo',
27
16
  input: z.object({
28
17
  module: z.string().optional().describe('Path to the app module'),
29
18
  rootDir: z.string().optional().describe('Workspace root directory'),
@@ -7,12 +7,11 @@ import { deriveTopoGraph } from '@ontrails/topographer';
7
7
  import type { TopoGraph, TopoGraphForceEntry } from '@ontrails/topographer';
8
8
  import { findTrailDefinitions, parse } from '@ontrails/warden/ast';
9
9
 
10
- import { tryLoadFreshAppLease } from './load-app.js';
11
10
  import {
12
11
  readLifecycleSourceFile,
13
12
  writeLifecycleSourceFile,
14
13
  } from '../lifecycle-source-io.js';
15
- import { resolveTrailRootDir } from './root-dir.js';
14
+ import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
16
15
 
17
16
  export type LifecycleEntryKind = 'revision' | 'fork';
18
17
  export type LifecycleStatusKind = 'deprecated' | 'archived';
@@ -569,7 +568,7 @@ export const reviseTrailSource = (
569
568
  );
570
569
  const written = writeLifecycleSourceFile(match.value.filePath, nextSource);
571
570
  if (written.isErr()) {
572
- return Result.err(written.error);
571
+ return written;
573
572
  }
574
573
 
575
574
  return Result.ok({
@@ -732,7 +731,7 @@ export const setVersionStatusSource = (
732
731
  }
733
732
  const written = writeLifecycleSourceFile(match.value.filePath, nextSource);
734
733
  if (written.isErr()) {
735
- return Result.err(written.error);
734
+ return written;
736
735
  }
737
736
 
738
737
  return Result.ok({
@@ -776,7 +775,7 @@ export const forkVersionEntrySource = (
776
775
  }
777
776
  const written = writeLifecycleSourceFile(match.value.filePath, nextSource);
778
777
  if (written.isErr()) {
779
- return Result.err(written.error);
778
+ return written;
780
779
  }
781
780
 
782
781
  return Result.ok({
@@ -798,21 +797,12 @@ export const withLifecycleApp = async <T>(
798
797
  app: Topo,
799
798
  rootDir: string
800
799
  ) => Result<T, Error> | Promise<Result<T, Error>>
801
- ): Promise<Result<T, Error>> => {
802
- const root = resolveTrailRootDir(input.rootDir, cwd);
803
- if (root.isErr()) {
804
- return root;
805
- }
806
- const lease = await tryLoadFreshAppLease(input.module, root.value);
807
- if (lease.isErr()) {
808
- return Result.err(lease.error);
809
- }
810
- try {
811
- return await consume(lease.value.app, root.value);
812
- } finally {
813
- lease.value.release();
814
- }
815
- };
800
+ ): Promise<Result<T, Error>> =>
801
+ withOperatorRootDir(input, { cwd }, (rootDir) =>
802
+ withFreshAppLease(input.module, rootDir, (lease) =>
803
+ consume(lease.app, rootDir)
804
+ )
805
+ );
816
806
 
817
807
  export const findLifecycleTrail = (
818
808
  app: Topo,
@@ -16,6 +16,11 @@ import {
16
16
  } from '@ontrails/warden';
17
17
  import { z } from 'zod';
18
18
 
19
+ import {
20
+ createIsolatedExampleRoot,
21
+ writeIsolatedExampleTextFile,
22
+ } from '../local-state-io.js';
23
+
19
24
  import { resolveTrailRootDir } from './root-dir.js';
20
25
 
21
26
  // ---------------------------------------------------------------------------
@@ -68,6 +73,14 @@ const wardenInputSchema = z.object({
68
73
 
69
74
  type WardenTrailInput = z.infer<typeof wardenInputSchema>;
70
75
 
76
+ const createIsolatedWardenExampleRoot = (name: string): string => {
77
+ const rootDir = createIsolatedExampleRoot(
78
+ `warden-${name}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
79
+ );
80
+ writeIsolatedExampleTextFile(rootDir, 'src/clean.ts', 'export {};\n');
81
+ return rootDir;
82
+ };
83
+
71
84
  const pushFlag = (args: string[], condition: boolean, flag: string): void => {
72
85
  if (condition) {
73
86
  args.push(flag);
@@ -170,13 +183,18 @@ export const wardenTrail = trail('warden', {
170
183
  examples: [
171
184
  {
172
185
  input: {
186
+ depth: 'source',
173
187
  lock: 'skip',
188
+ rootDir: createIsolatedWardenExampleRoot('default'),
174
189
  },
175
190
  name: 'Default warden run',
176
191
  },
177
192
  {
178
193
  input: {
194
+ depth: 'source',
179
195
  format: 'github',
196
+ lock: 'skip',
197
+ rootDir: createIsolatedWardenExampleRoot('github'),
180
198
  },
181
199
  name: 'GitHub Actions annotations',
182
200
  },