@ontrails/trails 1.0.0-beta.14 → 1.0.0-beta.15

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 (155) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/__tests__/examples.test.ts +29 -4
  4. package/dist/src/app.js +2 -2
  5. package/dist/src/app.js.map +1 -1
  6. package/dist/src/clack.d.ts +1 -1
  7. package/dist/src/clack.js +1 -1
  8. package/dist/src/cli.js +4 -3
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/trails/add-surface.d.ts +3 -3
  11. package/dist/src/trails/add-surface.d.ts.map +1 -1
  12. package/dist/src/trails/add-surface.js +46 -24
  13. package/dist/src/trails/add-surface.js.map +1 -1
  14. package/dist/src/trails/add-trail.d.ts +3 -1
  15. package/dist/src/trails/add-trail.d.ts.map +1 -1
  16. package/dist/src/trails/add-trail.js +38 -11
  17. package/dist/src/trails/add-trail.js.map +1 -1
  18. package/dist/src/trails/add-trailhead.d.ts +1 -1
  19. package/dist/src/trails/add-trailhead.d.ts.map +1 -1
  20. package/dist/src/trails/add-trailhead.js +4 -4
  21. package/dist/src/trails/add-verify.d.ts +1 -1
  22. package/dist/src/trails/add-verify.d.ts.map +1 -1
  23. package/dist/src/trails/add-verify.js +7 -6
  24. package/dist/src/trails/add-verify.js.map +1 -1
  25. package/dist/src/trails/create-scaffold.d.ts +1 -1
  26. package/dist/src/trails/create-scaffold.d.ts.map +1 -1
  27. package/dist/src/trails/create-scaffold.js +10 -3
  28. package/dist/src/trails/create-scaffold.js.map +1 -1
  29. package/dist/src/trails/create.d.ts +10 -14
  30. package/dist/src/trails/create.d.ts.map +1 -1
  31. package/dist/src/trails/create.js +42 -37
  32. package/dist/src/trails/create.js.map +1 -1
  33. package/dist/src/trails/dev-clean.d.ts +4 -4
  34. package/dist/src/trails/dev-clean.d.ts.map +1 -1
  35. package/dist/src/trails/dev-clean.js +22 -21
  36. package/dist/src/trails/dev-clean.js.map +1 -1
  37. package/dist/src/trails/dev-reset.d.ts +1 -1
  38. package/dist/src/trails/dev-reset.d.ts.map +1 -1
  39. package/dist/src/trails/dev-reset.js +3 -2
  40. package/dist/src/trails/dev-reset.js.map +1 -1
  41. package/dist/src/trails/dev-stats.d.ts +4 -4
  42. package/dist/src/trails/dev-stats.d.ts.map +1 -1
  43. package/dist/src/trails/dev-stats.js +19 -19
  44. package/dist/src/trails/dev-stats.js.map +1 -1
  45. package/dist/src/trails/dev-support.d.ts +17 -17
  46. package/dist/src/trails/dev-support.d.ts.map +1 -1
  47. package/dist/src/trails/dev-support.js +60 -57
  48. package/dist/src/trails/dev-support.js.map +1 -1
  49. package/dist/src/trails/draft-promote.d.ts +1 -1
  50. package/dist/src/trails/draft-promote.d.ts.map +1 -1
  51. package/dist/src/trails/draft-promote.js +30 -16
  52. package/dist/src/trails/draft-promote.js.map +1 -1
  53. package/dist/src/trails/guide.d.ts +2 -2
  54. package/dist/src/trails/guide.d.ts.map +1 -1
  55. package/dist/src/trails/guide.js +1 -4
  56. package/dist/src/trails/guide.js.map +1 -1
  57. package/dist/src/trails/load-app.d.ts +7 -1
  58. package/dist/src/trails/load-app.d.ts.map +1 -1
  59. package/dist/src/trails/load-app.js +374 -26
  60. package/dist/src/trails/load-app.js.map +1 -1
  61. package/dist/src/trails/survey.d.ts +5 -5
  62. package/dist/src/trails/survey.d.ts.map +1 -1
  63. package/dist/src/trails/survey.js +51 -29
  64. package/dist/src/trails/survey.js.map +1 -1
  65. package/dist/src/trails/topo-export.d.ts +5 -4
  66. package/dist/src/trails/topo-export.d.ts.map +1 -1
  67. package/dist/src/trails/topo-export.js +4 -7
  68. package/dist/src/trails/topo-export.js.map +1 -1
  69. package/dist/src/trails/topo-history.d.ts +6 -10
  70. package/dist/src/trails/topo-history.d.ts.map +1 -1
  71. package/dist/src/trails/topo-history.js +7 -8
  72. package/dist/src/trails/topo-history.js.map +1 -1
  73. package/dist/src/trails/topo-pin.d.ts +5 -9
  74. package/dist/src/trails/topo-pin.d.ts.map +1 -1
  75. package/dist/src/trails/topo-pin.js +5 -9
  76. package/dist/src/trails/topo-pin.js.map +1 -1
  77. package/dist/src/trails/topo-read-support.d.ts +12 -8
  78. package/dist/src/trails/topo-read-support.d.ts.map +1 -1
  79. package/dist/src/trails/topo-read-support.js +71 -82
  80. package/dist/src/trails/topo-read-support.js.map +1 -1
  81. package/dist/src/trails/topo-reports.d.ts +15 -11
  82. package/dist/src/trails/topo-reports.d.ts.map +1 -1
  83. package/dist/src/trails/topo-reports.js +40 -34
  84. package/dist/src/trails/topo-reports.js.map +1 -1
  85. package/dist/src/trails/topo-show.d.ts +4 -4
  86. package/dist/src/trails/topo-show.d.ts.map +1 -1
  87. package/dist/src/trails/topo-show.js +8 -12
  88. package/dist/src/trails/topo-show.js.map +1 -1
  89. package/dist/src/trails/topo-store-support.d.ts +2 -2
  90. package/dist/src/trails/topo-store-support.d.ts.map +1 -1
  91. package/dist/src/trails/topo-store-support.js +22 -22
  92. package/dist/src/trails/topo-store-support.js.map +1 -1
  93. package/dist/src/trails/topo-support.d.ts +19 -30
  94. package/dist/src/trails/topo-support.d.ts.map +1 -1
  95. package/dist/src/trails/topo-support.js +63 -96
  96. package/dist/src/trails/topo-support.js.map +1 -1
  97. package/dist/src/trails/topo-unpin.d.ts +9 -4
  98. package/dist/src/trails/topo-unpin.d.ts.map +1 -1
  99. package/dist/src/trails/topo-unpin.js +9 -4
  100. package/dist/src/trails/topo-unpin.js.map +1 -1
  101. package/dist/src/trails/topo-verify.d.ts +2 -2
  102. package/dist/src/trails/topo-verify.d.ts.map +1 -1
  103. package/dist/src/trails/topo-verify.js +1 -5
  104. package/dist/src/trails/topo-verify.js.map +1 -1
  105. package/dist/src/trails/topo.d.ts +2 -2
  106. package/dist/src/trails/topo.d.ts.map +1 -1
  107. package/dist/src/trails/topo.js +6 -10
  108. package/dist/src/trails/topo.js.map +1 -1
  109. package/dist/src/trails/warden.d.ts +3 -2
  110. package/dist/src/trails/warden.d.ts.map +1 -1
  111. package/dist/src/trails/warden.js +11 -2
  112. package/dist/src/trails/warden.js.map +1 -1
  113. package/dist/src/versions.d.ts +12 -0
  114. package/dist/src/versions.d.ts.map +1 -0
  115. package/dist/src/versions.js +23 -0
  116. package/dist/src/versions.js.map +1 -0
  117. package/dist/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +8 -8
  119. package/src/__tests__/add-trail.test.ts +97 -0
  120. package/src/__tests__/create.test.ts +91 -27
  121. package/src/__tests__/guide.test.ts +10 -5
  122. package/src/__tests__/load-app.test.ts +365 -4
  123. package/src/__tests__/survey.test.ts +143 -67
  124. package/src/__tests__/topo-dev.test.ts +48 -46
  125. package/src/app.ts +2 -2
  126. package/src/clack.ts +1 -1
  127. package/src/cli.ts +4 -3
  128. package/src/trails/add-surface.ts +150 -0
  129. package/src/trails/add-trail.ts +46 -10
  130. package/src/trails/add-verify.ts +11 -6
  131. package/src/trails/create-scaffold.ts +16 -3
  132. package/src/trails/create.ts +76 -71
  133. package/src/trails/dev-clean.ts +25 -21
  134. package/src/trails/dev-reset.ts +3 -2
  135. package/src/trails/dev-stats.ts +22 -19
  136. package/src/trails/dev-support.ts +88 -86
  137. package/src/trails/draft-promote.ts +70 -35
  138. package/src/trails/guide.ts +1 -4
  139. package/src/trails/load-app.ts +511 -32
  140. package/src/trails/survey.ts +60 -35
  141. package/src/trails/topo-export.ts +5 -9
  142. package/src/trails/topo-history.ts +8 -10
  143. package/src/trails/topo-pin.ts +9 -13
  144. package/src/trails/topo-read-support.ts +103 -106
  145. package/src/trails/topo-reports.ts +54 -47
  146. package/src/trails/topo-show.ts +8 -12
  147. package/src/trails/topo-store-support.ts +36 -28
  148. package/src/trails/topo-support.ts +90 -134
  149. package/src/trails/topo-unpin.ts +11 -6
  150. package/src/trails/topo-verify.ts +1 -5
  151. package/src/trails/topo.ts +6 -10
  152. package/src/trails/warden.ts +12 -3
  153. package/src/versions.ts +43 -0
  154. package/tsconfig.tests.json +10 -0
  155. package/src/trails/add-trailhead.ts +0 -121
@@ -1,52 +1,60 @@
1
1
  /**
2
2
  * Stored-export pipeline for topo persistence.
3
3
  *
4
- * Extracted from topo-support.ts so this branch (trl-131) owns its own file,
5
- * keeping absorb routing clean across the stack.
4
+ * Extracted from topo-support.ts to isolate store persistence concerns,
5
+ * keeping module boundaries clean.
6
6
  */
7
7
 
8
- import type { Topo } from '@ontrails/core';
8
+ import type { Topo, TopoSnapshot } from '@ontrails/core';
9
9
  import { InternalError, Result } from '@ontrails/core';
10
- import type { TopoSaveRecord } from '@ontrails/core/internal/topo-saves';
11
10
  import type { StoredTopoExport } from '@ontrails/core/internal/topo-store';
12
11
  import {
12
+ createTopoSnapshot,
13
13
  getStoredTopoExport,
14
- persistEstablishedTopoSave,
15
14
  } from '@ontrails/core/internal/topo-store';
16
15
  import {
17
16
  openWriteTrailsDb,
18
- resolveTrailsDir,
17
+ deriveTrailsDir,
19
18
  } from '@ontrails/core/internal/trails-db';
20
- import type { TrailheadLock, TrailheadMap } from '@ontrails/schema';
21
- import { writeTrailheadLock, writeTrailheadMap } from '@ontrails/schema';
19
+ import type { SurfaceLock, SurfaceMap } from '@ontrails/schema';
20
+ import { writeSurfaceLock, writeSurfaceMap } from '@ontrails/schema';
22
21
 
23
22
  import type { TopoExportReport } from './topo-support.js';
24
- import { currentGitState, resolveRootDir, topoCounts } from './topo-support.js';
23
+ import {
24
+ deriveRootDir,
25
+ deriveTopoCounts,
26
+ readGitState,
27
+ } from './topo-support.js';
25
28
 
26
29
  const persistAndReadStoredExport = (
27
30
  app: Topo,
28
31
  db: ReturnType<typeof openWriteTrailsDb>,
29
32
  rootDir: string
30
- ): Result<{ save: TopoSaveRecord; storedExport: StoredTopoExport }, Error> => {
31
- const saveResult = persistEstablishedTopoSave(db, app, {
32
- ...currentGitState(rootDir),
33
- ...topoCounts(app),
33
+ ): Result<
34
+ { snapshot: TopoSnapshot; storedExport: StoredTopoExport },
35
+ Error
36
+ > => {
37
+ const snapshotResult = createTopoSnapshot(db, app, {
38
+ ...readGitState(rootDir),
39
+ ...deriveTopoCounts(app),
34
40
  });
35
- if (saveResult.isErr()) {
36
- return saveResult;
41
+ if (snapshotResult.isErr()) {
42
+ return snapshotResult;
37
43
  }
38
44
 
39
- const save = saveResult.value;
40
- const storedExport = getStoredTopoExport(db, save.id);
45
+ const snapshot = snapshotResult.value;
46
+ const storedExport = getStoredTopoExport(db, snapshot.id);
41
47
 
42
48
  if (storedExport === undefined) {
43
49
  return Result.err(
44
- new InternalError(`Missing stored topo export for save "${save.id}"`)
50
+ new InternalError(
51
+ `Missing stored topo export for snapshot "${snapshot.id}"`
52
+ )
45
53
  );
46
54
  }
47
55
 
48
56
  return Result.ok({
49
- save,
57
+ snapshot,
50
58
  storedExport,
51
59
  });
52
60
  };
@@ -55,17 +63,17 @@ const writeStoredExportArtifacts = async (
55
63
  storedExport: StoredTopoExport,
56
64
  trailsDir: string
57
65
  ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'mapPath'>> => {
58
- const mapPath = await writeTrailheadMap(
59
- JSON.parse(storedExport.trailheadMapJson) as TrailheadMap,
66
+ const mapPath = await writeSurfaceMap(
67
+ JSON.parse(storedExport.surfaceMapJson) as SurfaceMap,
60
68
  { dir: trailsDir }
61
69
  );
62
- const lockPath = await writeTrailheadLock(
63
- JSON.parse(storedExport.lockContent) as TrailheadLock,
70
+ const lockPath = await writeSurfaceLock(
71
+ JSON.parse(storedExport.lockContent) as SurfaceLock,
64
72
  { dir: trailsDir }
65
73
  );
66
74
 
67
75
  return {
68
- hash: storedExport.trailheadHash,
76
+ hash: storedExport.surfaceHash,
69
77
  lockPath,
70
78
  mapPath,
71
79
  };
@@ -75,7 +83,7 @@ export const exportCurrentTopo = async (
75
83
  app: Topo,
76
84
  options?: { readonly rootDir?: string }
77
85
  ): Promise<Result<TopoExportReport, Error>> => {
78
- const rootDir = resolveRootDir(options?.rootDir);
86
+ const rootDir = deriveRootDir(options?.rootDir);
79
87
  const db = openWriteTrailsDb({ rootDir });
80
88
 
81
89
  try {
@@ -84,12 +92,12 @@ export const exportCurrentTopo = async (
84
92
  return persisted;
85
93
  }
86
94
 
87
- const { save, storedExport } = persisted.value;
95
+ const { snapshot, storedExport } = persisted.value;
88
96
  const artifacts = await writeStoredExportArtifacts(
89
97
  storedExport,
90
- resolveTrailsDir({ rootDir })
98
+ deriveTrailsDir({ rootDir })
91
99
  );
92
- return Result.ok({ ...artifacts, save });
100
+ return Result.ok({ ...artifacts, snapshot });
93
101
  } finally {
94
102
  db.close();
95
103
  }
@@ -3,60 +3,33 @@ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- import type { Topo } from '@ontrails/core';
7
- import type {
8
- TopoPinRecord,
9
- TopoSaveRecord,
10
- } from '@ontrails/core/internal/topo-saves';
11
6
  import {
12
- getTopoPin,
13
- listTopoPins,
14
- listTopoSaves,
15
- pinTopoSave,
16
- unpinTopoSave,
17
- } from '@ontrails/core/internal/topo-saves';
18
- import { persistEstablishedTopoSave } from '@ontrails/core/internal/topo-store';
19
- import {
20
- openReadTrailsDb,
21
- openWriteTrailsDb,
22
- resolveTrailsDbPath,
23
- } from '@ontrails/core/internal/trails-db';
7
+ createTopoSnapshot as persistTopoSnapshot,
8
+ listTopoSnapshots as readTopoSnapshots,
9
+ pinTopoSnapshot,
10
+ unpinTopoSnapshot,
11
+ } from '@ontrails/core';
12
+ import type { Topo, TopoSnapshot } from '@ontrails/core';
13
+ import { deriveTrailsDbPath } from '@ontrails/core/internal/trails-db';
24
14
  import { z } from 'zod';
25
15
 
26
16
  import type { BriefReport, SurveyListReport } from './topo-reports.js';
27
17
 
28
- /** Output schema for a topo save record. Shared across topo trails. */
29
- export const topoSaveOutput = z.object({
18
+ /** Output schema for a topo snapshot record. Shared across topo trails. */
19
+ export const topoSnapshotOutput = z.object({
30
20
  createdAt: z.string(),
31
21
  gitDirty: z.boolean(),
32
22
  gitSha: z.string().optional(),
33
23
  id: z.string(),
34
- provisionCount: z.number(),
24
+ pinnedAs: z.string().optional(),
25
+ resourceCount: z.number(),
35
26
  signalCount: z.number(),
36
27
  trailCount: z.number(),
37
28
  });
38
29
 
39
- /** Output schema for a topo pin record. Shared across topo trails. */
40
- export const topoPinOutput = z.object({
41
- createdAt: z.string(),
42
- name: z.string(),
43
- saveId: z.string(),
44
- });
45
-
46
30
  export const DEFAULT_APP_MODULE = './src/app.ts';
47
31
  export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
48
32
  export const LOCK_PATH = '.trails/trails.lock';
49
- export const LEGACY_LOCK_PATH = '.trails/trailhead.lock';
50
-
51
- /** Resolve the lockfile path, preferring the current name with legacy fallback. */
52
- export const resolveLockPath = (trailsDir: string): string => {
53
- const primary = join(trailsDir, 'trails.lock');
54
- if (existsSync(primary)) {
55
- return primary;
56
- }
57
- const legacy = join(trailsDir, 'trailhead.lock');
58
- return existsSync(legacy) ? legacy : primary;
59
- };
60
33
  const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
61
34
 
62
35
  export interface TopoSummaryReport {
@@ -70,17 +43,16 @@ export interface TopoSummaryReport {
70
43
  export interface TopoHistoryReport {
71
44
  readonly dbPath: string;
72
45
  readonly limit: number;
73
- readonly pinCount: number;
74
- readonly pins: TopoPinRecord[];
75
- readonly saveCount: number;
76
- readonly saves: TopoSaveRecord[];
46
+ readonly pinnedCount: number;
47
+ readonly snapshotCount: number;
48
+ readonly snapshots: TopoSnapshot[];
77
49
  }
78
50
 
79
51
  export interface TopoExportReport {
80
52
  readonly hash: string;
81
53
  readonly lockPath: string;
82
54
  readonly mapPath: string;
83
- readonly save: TopoSaveRecord;
55
+ readonly snapshot: TopoSnapshot;
84
56
  }
85
57
 
86
58
  export interface TopoVerifyReport {
@@ -90,7 +62,7 @@ export interface TopoVerifyReport {
90
62
  readonly stale: false;
91
63
  }
92
64
 
93
- export const resolveRootDir = (cwd?: string): string => cwd ?? process.cwd();
65
+ export const deriveRootDir = (cwd?: string): string => cwd ?? process.cwd();
94
66
 
95
67
  const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
96
68
  const proc = Bun.spawnSync({
@@ -105,7 +77,7 @@ const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
105
77
  return text.length === 0 ? undefined : text;
106
78
  };
107
79
 
108
- export const currentGitState = (
80
+ export const readGitState = (
109
81
  rootDir: string
110
82
  ): { readonly gitDirty: boolean; readonly gitSha?: string } => {
111
83
  const gitSha = safeGit(rootDir, ['rev-parse', 'HEAD']);
@@ -116,10 +88,10 @@ export const currentGitState = (
116
88
  };
117
89
  };
118
90
 
119
- export const topoCounts = (
91
+ export const deriveTopoCounts = (
120
92
  app: Topo
121
- ): Pick<TopoSaveRecord, 'provisionCount' | 'signalCount' | 'trailCount'> => ({
122
- provisionCount: app.provisions.size,
93
+ ): Pick<TopoSnapshot, 'resourceCount' | 'signalCount' | 'trailCount'> => ({
94
+ resourceCount: app.resources.size,
123
95
  signalCount: app.signals.size,
124
96
  trailCount: app.trails.size,
125
97
  });
@@ -130,40 +102,39 @@ const emptyTopoHistory = (
130
102
  ): TopoHistoryReport => ({
131
103
  dbPath,
132
104
  limit,
133
- pinCount: 0,
134
- pins: [],
135
- saveCount: 0,
136
- saves: [],
105
+ pinnedCount: 0,
106
+ snapshotCount: 0,
107
+ snapshots: [],
137
108
  });
138
109
 
139
- const collectedTopoHistory = (
110
+ const collectTopoHistory = (
140
111
  dbPath: string,
141
112
  limit: number,
142
- pins: readonly TopoPinRecord[],
143
- allSaves: readonly TopoSaveRecord[]
113
+ snapshots: readonly TopoSnapshot[]
144
114
  ): TopoHistoryReport => ({
145
115
  dbPath,
146
116
  limit,
147
- pinCount: pins.length,
148
- pins: [...pins],
149
- saveCount: allSaves.length,
150
- saves: allSaves.slice(0, limit),
117
+ pinnedCount: snapshots.filter((snapshot) => snapshot.pinnedAs !== undefined)
118
+ .length,
119
+ snapshotCount: snapshots.length,
120
+ snapshots: snapshots.slice(0, limit),
151
121
  });
152
122
 
153
- const removeTopoPinWithDb = (
154
- input: { readonly dryRun: boolean; readonly name: string },
155
- pin: TopoPinRecord,
156
- db: Parameters<typeof unpinTopoSave>[0]
123
+ const buildSnapshotInput = (
124
+ app: Topo,
125
+ rootDir: string
157
126
  ): {
158
- readonly dryRun: boolean;
159
- readonly pin?: TopoPinRecord;
160
- readonly removed: boolean;
161
- } =>
162
- input.dryRun
163
- ? { dryRun: true, pin, removed: false }
164
- : { dryRun: false, pin, removed: unpinTopoSave(db, input.name) };
127
+ readonly gitDirty: boolean;
128
+ readonly gitSha?: string;
129
+ readonly resourceCount: number;
130
+ readonly signalCount: number;
131
+ readonly trailCount: number;
132
+ } => ({
133
+ ...readGitState(rootDir),
134
+ ...deriveTopoCounts(app),
135
+ });
165
136
 
166
- export const isolatedExampleInput = (
137
+ export const createIsolatedExampleInput = (
167
138
  name: string
168
139
  ): { readonly module: string; readonly rootDir: string } => {
169
140
  const rootDir = join(tmpdir(), 'ontrails-trails-examples', name);
@@ -175,100 +146,85 @@ export const isolatedExampleInput = (
175
146
  };
176
147
  };
177
148
 
178
- export const createCurrentTopoSave = (
149
+ export const createCurrentTopoSnapshot = (
179
150
  app: Topo,
180
151
  options?: { readonly rootDir?: string }
181
- ): TopoSaveRecord => {
182
- const rootDir = resolveRootDir(options?.rootDir);
183
- const db = openWriteTrailsDb({ rootDir });
184
-
185
- try {
186
- const result = persistEstablishedTopoSave(db, app, {
187
- ...currentGitState(rootDir),
188
- ...topoCounts(app),
189
- });
190
- if (result.isErr()) {
191
- throw result.error;
192
- }
193
- return result.value;
194
- } finally {
195
- db.close();
152
+ ): TopoSnapshot => {
153
+ const rootDir = deriveRootDir(options?.rootDir);
154
+ const result = persistTopoSnapshot(app, {
155
+ rootDir,
156
+ ...buildSnapshotInput(app, rootDir),
157
+ });
158
+ if (result.isErr()) {
159
+ throw result.error;
196
160
  }
161
+ return result.value;
197
162
  };
198
163
 
199
164
  export const listTopoHistory = (options?: {
200
165
  readonly limit?: number;
201
166
  readonly rootDir?: string;
202
167
  }): TopoHistoryReport => {
203
- const rootDir = resolveRootDir(options?.rootDir);
168
+ const rootDir = deriveRootDir(options?.rootDir);
204
169
  const limit = options?.limit ?? DEFAULT_TOPO_HISTORY_LIMIT;
205
- const dbPath = resolveTrailsDbPath({ rootDir });
170
+ const dbPath = deriveTrailsDbPath({ rootDir });
206
171
  if (!existsSync(dbPath)) {
207
172
  return emptyTopoHistory(dbPath, limit);
208
173
  }
209
- const db = openReadTrailsDb({ rootDir });
210
174
 
211
- try {
212
- return collectedTopoHistory(
213
- dbPath,
214
- limit,
215
- listTopoPins(db),
216
- listTopoSaves(db)
217
- );
218
- } finally {
219
- db.close();
220
- }
175
+ return collectTopoHistory(dbPath, limit, readTopoSnapshots({ rootDir }));
221
176
  };
222
177
 
223
- export const pinCurrentTopo = (
178
+ export const pinCurrentTopoSnapshot = (
224
179
  app: Topo,
225
180
  input: { readonly name: string; readonly rootDir?: string }
226
- ): { readonly pin: TopoPinRecord; readonly save: TopoSaveRecord } => {
227
- const rootDir = resolveRootDir(input.rootDir);
228
- const db = openWriteTrailsDb({ rootDir });
181
+ ): { readonly snapshot: TopoSnapshot } => {
182
+ const rootDir = deriveRootDir(input.rootDir);
183
+ const created = persistTopoSnapshot(app, {
184
+ rootDir,
185
+ ...buildSnapshotInput(app, rootDir),
186
+ });
187
+ if (created.isErr()) {
188
+ throw created.error;
189
+ }
229
190
 
230
- try {
231
- const result = persistEstablishedTopoSave(db, app, {
232
- ...currentGitState(rootDir),
233
- ...topoCounts(app),
234
- });
235
- if (result.isErr()) {
236
- throw result.error;
237
- }
238
- const pin = pinTopoSave(db, {
239
- name: input.name,
240
- saveId: result.value.id,
241
- });
242
- return { pin, save: result.value };
243
- } finally {
244
- db.close();
191
+ const snapshot = pinTopoSnapshot(created.value.id, input.name, {
192
+ rootDir,
193
+ });
194
+ if (snapshot === undefined) {
195
+ throw new Error(`Missing topo snapshot "${created.value.id}" to pin`);
245
196
  }
197
+
198
+ return { snapshot };
246
199
  };
247
200
 
248
- export const removeTopoPin = (input: {
201
+ export const removePinnedTopoSnapshot = (input: {
249
202
  readonly dryRun: boolean;
250
203
  readonly name: string;
251
204
  readonly rootDir?: string;
252
205
  }): {
253
206
  readonly dryRun: boolean;
254
- readonly pin?: TopoPinRecord;
255
207
  readonly removed: boolean;
208
+ readonly snapshot?: TopoSnapshot;
256
209
  } => {
257
- const rootDir = resolveRootDir(input.rootDir);
258
- if (!existsSync(resolveTrailsDbPath({ rootDir }))) {
210
+ const rootDir = deriveRootDir(input.rootDir);
211
+ if (!existsSync(deriveTrailsDbPath({ rootDir }))) {
259
212
  return { dryRun: input.dryRun, removed: false };
260
213
  }
261
- const db = input.dryRun
262
- ? openReadTrailsDb({ rootDir })
263
- : openWriteTrailsDb({ rootDir });
264
214
 
265
- try {
266
- const pin = getTopoPin(db, input.name);
267
- if (pin === undefined) {
268
- return { dryRun: input.dryRun, removed: false };
269
- }
270
- return removeTopoPinWithDb(input, pin, db);
271
- } finally {
272
- db.close();
215
+ if (input.dryRun) {
216
+ const snapshot = readTopoSnapshots({ pinned: true, rootDir }).find(
217
+ (candidate) => candidate.pinnedAs === input.name
218
+ );
219
+ return snapshot === undefined
220
+ ? { dryRun: true, removed: false }
221
+ : { dryRun: true, removed: false, snapshot };
273
222
  }
223
+
224
+ const snapshot = unpinTopoSnapshot(input.name, { rootDir });
225
+ return {
226
+ dryRun: false,
227
+ removed: snapshot !== undefined,
228
+ ...(snapshot === undefined ? {} : { snapshot }),
229
+ };
274
230
  };
@@ -2,9 +2,9 @@ import { Result, ValidationError, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import {
5
- isolatedExampleInput,
6
- removeTopoPin,
7
- topoPinOutput,
5
+ createIsolatedExampleInput,
6
+ removePinnedTopoSnapshot,
7
+ topoSnapshotOutput,
8
8
  } from './topo-support.js';
9
9
 
10
10
  export const topoUnpinTrail = trail('topo.unpin', {
@@ -19,14 +19,18 @@ export const topoUnpinTrail = trail('topo.unpin', {
19
19
 
20
20
  const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
21
21
  return Result.ok(
22
- removeTopoPin({ dryRun: input.dryRun, name: input.name, rootDir })
22
+ removePinnedTopoSnapshot({
23
+ dryRun: input.dryRun,
24
+ name: input.name,
25
+ rootDir,
26
+ })
23
27
  );
24
28
  },
25
29
  description: 'Remove a named topo pin',
26
30
  examples: [
27
31
  {
28
32
  input: {
29
- ...isolatedExampleInput('topo-unpin'),
33
+ ...createIsolatedExampleInput('topo-unpin'),
30
34
  dryRun: true,
31
35
  name: 'before-auth-refactor',
32
36
  },
@@ -45,7 +49,8 @@ export const topoUnpinTrail = trail('topo.unpin', {
45
49
  intent: 'destroy',
46
50
  output: z.object({
47
51
  dryRun: z.boolean(),
48
- pin: topoPinOutput.optional(),
49
52
  removed: z.boolean(),
53
+ snapshot: topoSnapshotOutput.optional(),
50
54
  }),
55
+ permit: { scopes: ['topo:delete'] },
51
56
  });
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
 
4
4
  import { loadApp } from './load-app.js';
5
5
  import { verifyCurrentTopo } from './topo-read-support.js';
6
- import { DEFAULT_APP_MODULE } from './topo-support.js';
7
6
 
8
7
  export const topoVerifyTrail = trail('topo.verify', {
9
8
  blaze: async (input, ctx) => {
@@ -13,10 +12,7 @@ export const topoVerifyTrail = trail('topo.verify', {
13
12
  },
14
13
  description: 'Verify that the committed lockfile matches the current topo',
15
14
  input: z.object({
16
- module: z
17
- .string()
18
- .default(DEFAULT_APP_MODULE)
19
- .describe('Path to the app module'),
15
+ module: z.string().optional().describe('Path to the app module'),
20
16
  rootDir: z.string().optional().describe('Workspace root directory'),
21
17
  }),
22
18
  intent: 'read',
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
 
4
4
  import { loadApp } from './load-app.js';
5
5
  import { buildTopoSummary } from './topo-read-support.js';
6
- import { DEFAULT_APP_MODULE } from './topo-support.js';
7
6
 
8
7
  const summaryOutput = z.object({
9
8
  app: z.object({
@@ -12,11 +11,11 @@ const summaryOutput = z.object({
12
11
  detours: z.boolean(),
13
12
  examples: z.boolean(),
14
13
  outputSchemas: z.boolean(),
15
- provisions: z.boolean(),
14
+ resources: z.boolean(),
16
15
  signals: z.boolean(),
17
16
  }),
18
17
  name: z.string(),
19
- provisions: z.number(),
18
+ resources: z.number(),
20
19
  signals: z.number(),
21
20
  trails: z.number(),
22
21
  version: z.string(),
@@ -32,13 +31,13 @@ const summaryOutput = z.object({
32
31
  safety: z.string(),
33
32
  })
34
33
  ),
35
- provisionCount: z.number(),
36
- provisions: z.array(
34
+ resourceCount: z.number(),
35
+ resources: z.array(
37
36
  z.object({
38
37
  description: z.string().nullable(),
39
38
  health: z.enum(['available', 'none']),
40
39
  id: z.string(),
41
- kind: z.literal('provision'),
40
+ kind: z.literal('resource'),
42
41
  lifetime: z.literal('singleton'),
43
42
  usedBy: z.array(z.string()),
44
43
  })
@@ -62,10 +61,7 @@ export const topoTrail = trail('topo', {
62
61
  },
63
62
  ],
64
63
  input: z.object({
65
- module: z
66
- .string()
67
- .default(DEFAULT_APP_MODULE)
68
- .describe('Path to the app module'),
64
+ module: z.string().optional().describe('Path to the app module'),
69
65
  rootDir: z.string().optional().describe('Workspace root directory'),
70
66
  }),
71
67
  intent: 'read',
@@ -23,9 +23,14 @@ import { loadApp } from './load-app.js';
23
23
  export const wardenTrail = trail('warden', {
24
24
  blaze: async (input, ctx) => {
25
25
  const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
26
- // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
27
- const topo = await loadApp('./src/app.ts', rootDir).catch(
28
- (): undefined => undefined
26
+ // oxlint-disable-next-line prefer-await-to-then -- catch preserves graceful degradation
27
+ const topo = await loadApp(input.module, rootDir).catch(
28
+ (error: unknown): undefined => {
29
+ ctx.logger?.warn('Could not load app for topo-aware governance', {
30
+ error: error instanceof Error ? error.message : String(error),
31
+ });
32
+ return undefined;
33
+ }
29
34
  );
30
35
 
31
36
  const report = await runWarden({
@@ -76,6 +81,10 @@ export const wardenTrail = trail('warden', {
76
81
  .default('text')
77
82
  .describe('Output format: text, json, github, or summary'),
78
83
  lintOnly: z.boolean().default(false).describe('Only run lint rules'),
84
+ module: z
85
+ .string()
86
+ .optional()
87
+ .describe('App module path (auto-discovered if omitted)'),
79
88
  rootDir: z.string().optional().describe('Root directory to scan'),
80
89
  }),
81
90
  intent: 'read',
@@ -0,0 +1,43 @@
1
+ interface PackageJson {
2
+ readonly catalog?: Record<string, string>;
3
+ readonly dependencies?: Record<string, string>;
4
+ readonly devDependencies?: Record<string, string>;
5
+ readonly version?: string;
6
+ }
7
+
8
+ const readPackageJson = async (url: URL): Promise<PackageJson> =>
9
+ (await Bun.file(url).json()) as PackageJson;
10
+
11
+ const appPackageJson = await readPackageJson(
12
+ new URL('../package.json', import.meta.url)
13
+ );
14
+ const rootPackageJson = await readPackageJson(
15
+ new URL('../../../package.json', import.meta.url)
16
+ );
17
+
18
+ const requireVersion = (value: string | undefined, label: string): string => {
19
+ if (typeof value !== 'string' || value.length === 0) {
20
+ throw new Error(`Missing version for ${label}`);
21
+ }
22
+ return value;
23
+ };
24
+
25
+ const rootCatalog = rootPackageJson.catalog ?? {};
26
+ const rootDevDependencies = rootPackageJson.devDependencies ?? {};
27
+
28
+ export const trailsPackageVersion = requireVersion(
29
+ appPackageJson.version,
30
+ '@ontrails/trails'
31
+ );
32
+
33
+ export const ontrailsPackageRange = `^${trailsPackageVersion}`;
34
+
35
+ export const scaffoldDependencyVersions = {
36
+ bunTypes: requireVersion(rootDevDependencies['@types/bun'], '@types/bun'),
37
+ commander: requireVersion(rootCatalog['commander'], 'commander'),
38
+ lefthook: requireVersion(rootDevDependencies['lefthook'], 'lefthook'),
39
+ oxlint: requireVersion(rootDevDependencies['oxlint'], 'oxlint'),
40
+ typescript: requireVersion(rootDevDependencies['typescript'], 'typescript'),
41
+ ultracite: requireVersion(rootDevDependencies['ultracite'], 'ultracite'),
42
+ zod: requireVersion(rootCatalog['zod'], 'zod'),
43
+ } as const;
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": ".",
6
+ "types": ["bun"]
7
+ },
8
+ "include": ["src/**/*.test.ts", "src/__tests__/**/*.ts", "__tests__/**/*.ts"],
9
+ "exclude": []
10
+ }