@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.22

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 (150) hide show
  1. package/CHANGELOG.md +647 -0
  2. package/README.md +26 -0
  3. package/package.json +28 -7
  4. package/src/app.ts +86 -2
  5. package/src/clack.ts +22 -0
  6. package/src/cli.ts +330 -11
  7. package/src/completions.ts +240 -0
  8. package/src/lifecycle-source-io.ts +33 -0
  9. package/src/load-app-mirror.ts +202 -0
  10. package/src/local-state-io.ts +153 -0
  11. package/src/mcp-app.ts +30 -0
  12. package/src/mcp-options.ts +77 -0
  13. package/src/mcp.ts +8 -0
  14. package/src/project-writes.ts +377 -0
  15. package/src/release/bindings.ts +39 -0
  16. package/src/release/check.ts +818 -0
  17. package/src/release/config.ts +63 -0
  18. package/src/release/contract-facts.ts +425 -0
  19. package/src/release/index.ts +85 -0
  20. package/src/release/native-bun-publish.ts +651 -0
  21. package/src/release/native-bun-registry.ts +350 -0
  22. package/src/release/packed-artifacts-smoke.ts +236 -0
  23. package/src/release/smoke.ts +46 -0
  24. package/src/release/wayfinder-dogfood-smoke.ts +226 -0
  25. package/src/retired-topo-command.ts +36 -0
  26. package/src/run-adapter-check.ts +76 -0
  27. package/src/run-collision.ts +126 -0
  28. package/src/run-completions-install.ts +179 -0
  29. package/src/run-example.ts +149 -0
  30. package/src/run-examples.ts +148 -0
  31. package/src/run-quiet.ts +75 -0
  32. package/src/run-release-check.ts +74 -0
  33. package/src/run-trace.ts +273 -0
  34. package/src/run-warden.ts +39 -0
  35. package/src/run-watch.ts +432 -0
  36. package/src/scaffold-version-sync.ts +183 -0
  37. package/src/scaffold-versions.generated.ts +12 -0
  38. package/src/trails/adapter-check.ts +244 -0
  39. package/src/trails/add-surface.ts +94 -40
  40. package/src/trails/add-trail.ts +79 -41
  41. package/src/trails/add-verify.ts +95 -25
  42. package/src/trails/compile.ts +67 -0
  43. package/src/trails/completions-complete.ts +165 -0
  44. package/src/trails/completions.ts +47 -0
  45. package/src/trails/create-adapter.ts +1084 -0
  46. package/src/trails/create-scaffold.ts +399 -104
  47. package/src/trails/create-versions.ts +62 -0
  48. package/src/trails/create.ts +185 -71
  49. package/src/trails/deprecate.ts +59 -0
  50. package/src/trails/dev-clean.ts +82 -0
  51. package/src/trails/dev-reset.ts +50 -0
  52. package/src/trails/dev-stats.ts +72 -0
  53. package/src/trails/dev-support.ts +340 -0
  54. package/src/trails/doctor.ts +56 -0
  55. package/src/trails/draft-promote.ts +949 -0
  56. package/src/trails/guide.ts +74 -68
  57. package/src/trails/load-app.ts +1143 -15
  58. package/src/trails/project.ts +17 -3
  59. package/src/trails/release-check.ts +104 -0
  60. package/src/trails/release-smoke.ts +48 -0
  61. package/src/trails/revise.ts +53 -0
  62. package/src/trails/root-dir.ts +21 -0
  63. package/src/trails/run-example.ts +491 -0
  64. package/src/trails/run-examples.ts +145 -0
  65. package/src/trails/run.ts +410 -0
  66. package/src/trails/scaffold-json.ts +58 -0
  67. package/src/trails/survey.ts +881 -226
  68. package/src/trails/topo-activation.ts +385 -0
  69. package/src/trails/topo-constants.ts +2 -0
  70. package/src/trails/topo-history.ts +47 -0
  71. package/src/trails/topo-output-schemas.ts +248 -0
  72. package/src/trails/topo-pin.ts +52 -0
  73. package/src/trails/topo-read-support.ts +313 -0
  74. package/src/trails/topo-reports.ts +807 -0
  75. package/src/trails/topo-store-support.ts +174 -0
  76. package/src/trails/topo-support.ts +220 -0
  77. package/src/trails/topo-unpin.ts +61 -0
  78. package/src/trails/topo.ts +106 -0
  79. package/src/trails/validate.ts +38 -0
  80. package/src/trails/version-lifecycle-support.ts +945 -0
  81. package/src/trails/warden-guide.ts +129 -0
  82. package/src/trails/warden.ts +165 -58
  83. package/src/versions.ts +31 -0
  84. package/.turbo/turbo-build.log +0 -1
  85. package/.turbo/turbo-lint.log +0 -3
  86. package/.turbo/turbo-typecheck.log +0 -1
  87. package/__tests__/examples.test.ts +0 -6
  88. package/dist/bin/trails.d.ts +0 -3
  89. package/dist/bin/trails.d.ts.map +0 -1
  90. package/dist/bin/trails.js +0 -4
  91. package/dist/bin/trails.js.map +0 -1
  92. package/dist/src/app.d.ts +0 -2
  93. package/dist/src/app.d.ts.map +0 -1
  94. package/dist/src/app.js +0 -11
  95. package/dist/src/app.js.map +0 -1
  96. package/dist/src/clack.d.ts +0 -9
  97. package/dist/src/clack.d.ts.map +0 -1
  98. package/dist/src/clack.js +0 -62
  99. package/dist/src/clack.js.map +0 -1
  100. package/dist/src/cli.d.ts +0 -2
  101. package/dist/src/cli.d.ts.map +0 -1
  102. package/dist/src/cli.js +0 -13
  103. package/dist/src/cli.js.map +0 -1
  104. package/dist/src/trails/add-surface.d.ts +0 -13
  105. package/dist/src/trails/add-surface.d.ts.map +0 -1
  106. package/dist/src/trails/add-surface.js +0 -88
  107. package/dist/src/trails/add-surface.js.map +0 -1
  108. package/dist/src/trails/add-trail.d.ts +0 -11
  109. package/dist/src/trails/add-trail.d.ts.map +0 -1
  110. package/dist/src/trails/add-trail.js +0 -85
  111. package/dist/src/trails/add-trail.js.map +0 -1
  112. package/dist/src/trails/add-verify.d.ts +0 -10
  113. package/dist/src/trails/add-verify.d.ts.map +0 -1
  114. package/dist/src/trails/add-verify.js +0 -67
  115. package/dist/src/trails/add-verify.js.map +0 -1
  116. package/dist/src/trails/create-scaffold.d.ts +0 -15
  117. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  118. package/dist/src/trails/create-scaffold.js +0 -288
  119. package/dist/src/trails/create-scaffold.js.map +0 -1
  120. package/dist/src/trails/create.d.ts +0 -22
  121. package/dist/src/trails/create.d.ts.map +0 -1
  122. package/dist/src/trails/create.js +0 -121
  123. package/dist/src/trails/create.js.map +0 -1
  124. package/dist/src/trails/guide.d.ts +0 -11
  125. package/dist/src/trails/guide.d.ts.map +0 -1
  126. package/dist/src/trails/guide.js +0 -80
  127. package/dist/src/trails/guide.js.map +0 -1
  128. package/dist/src/trails/load-app.d.ts +0 -4
  129. package/dist/src/trails/load-app.d.ts.map +0 -1
  130. package/dist/src/trails/load-app.js +0 -24
  131. package/dist/src/trails/load-app.js.map +0 -1
  132. package/dist/src/trails/project.d.ts +0 -8
  133. package/dist/src/trails/project.d.ts.map +0 -1
  134. package/dist/src/trails/project.js +0 -43
  135. package/dist/src/trails/project.js.map +0 -1
  136. package/dist/src/trails/survey.d.ts +0 -33
  137. package/dist/src/trails/survey.d.ts.map +0 -1
  138. package/dist/src/trails/survey.js +0 -225
  139. package/dist/src/trails/survey.js.map +0 -1
  140. package/dist/src/trails/warden.d.ts +0 -19
  141. package/dist/src/trails/warden.d.ts.map +0 -1
  142. package/dist/src/trails/warden.js +0 -88
  143. package/dist/src/trails/warden.js.map +0 -1
  144. package/dist/tsconfig.tsbuildinfo +0 -1
  145. package/src/__tests__/create.test.ts +0 -349
  146. package/src/__tests__/guide.test.ts +0 -91
  147. package/src/__tests__/load-app.test.ts +0 -15
  148. package/src/__tests__/survey.test.ts +0 -161
  149. package/src/__tests__/warden.test.ts +0 -74
  150. package/tsconfig.json +0 -9
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Stored-export pipeline for topo persistence.
3
+ *
4
+ * Extracted from topo-support.ts to isolate store persistence concerns,
5
+ * keeping module boundaries clean.
6
+ */
7
+
8
+ import { Database } from 'bun:sqlite';
9
+
10
+ import type { Topo } from '@ontrails/core';
11
+ import {
12
+ ConflictError,
13
+ deriveTrailsDir,
14
+ InternalError,
15
+ openWriteTrailsDb,
16
+ Result,
17
+ } from '@ontrails/core';
18
+ import type {
19
+ LockManifest,
20
+ TopoGraph,
21
+ TopoSnapshot,
22
+ } from '@ontrails/topographer';
23
+ import type { StoredTopoExport } from '@ontrails/topographer/backend-support';
24
+ import {
25
+ annotateTopoGraphForces,
26
+ carryForwardTopoGraphForces,
27
+ deriveTopoGraphDiff,
28
+ deriveTopoGraphHash,
29
+ readTopoGraph,
30
+ writeLockManifest,
31
+ writeTopoGraph,
32
+ } from '@ontrails/topographer';
33
+ import {
34
+ createStoredTopoSnapshot,
35
+ getStoredTopoExport,
36
+ } from '@ontrails/topographer/backend-support';
37
+
38
+ import type { TopoExportReport } from './topo-support.js';
39
+ import {
40
+ deriveRootDir,
41
+ deriveTopoCounts,
42
+ readGitState,
43
+ } from './topo-support.js';
44
+
45
+ const persistAndReadStoredExport = (
46
+ app: Topo,
47
+ db: ReturnType<typeof openWriteTrailsDb>,
48
+ rootDir: string
49
+ ): Result<
50
+ { snapshot: TopoSnapshot; storedExport: StoredTopoExport },
51
+ Error
52
+ > => {
53
+ const snapshotResult = createStoredTopoSnapshot(db, app, {
54
+ ...readGitState(rootDir),
55
+ ...deriveTopoCounts(app),
56
+ });
57
+ if (snapshotResult.isErr()) {
58
+ return snapshotResult;
59
+ }
60
+
61
+ const snapshot = snapshotResult.value;
62
+ const storedExport = getStoredTopoExport(db, snapshot.id);
63
+
64
+ if (storedExport === undefined) {
65
+ return Result.err(
66
+ new InternalError(
67
+ `Missing stored topo export for snapshot "${snapshot.id}"`
68
+ )
69
+ );
70
+ }
71
+
72
+ return Result.ok({
73
+ snapshot,
74
+ storedExport,
75
+ });
76
+ };
77
+
78
+ export const deriveCurrentTopoExport = (
79
+ app: Topo,
80
+ options?: { readonly rootDir?: string }
81
+ ): Result<StoredTopoExport, Error> => {
82
+ const rootDir = deriveRootDir(options?.rootDir);
83
+ const db = new Database(':memory:');
84
+
85
+ try {
86
+ const projected = persistAndReadStoredExport(app, db, rootDir);
87
+ return projected.isErr()
88
+ ? projected
89
+ : Result.ok(projected.value.storedExport);
90
+ } finally {
91
+ db.close();
92
+ }
93
+ };
94
+
95
+ const writeStoredExportArtifacts = async (
96
+ storedExport: StoredTopoExport,
97
+ trailsDir: string,
98
+ options?: { readonly force?: boolean | undefined }
99
+ ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>> => {
100
+ const previousTopo = await readTopoGraph({ dir: trailsDir });
101
+ const nextTopo = JSON.parse(storedExport.topoGraphJson) as TopoGraph;
102
+ const diff =
103
+ previousTopo === null
104
+ ? undefined
105
+ : deriveTopoGraphDiff(previousTopo, nextTopo);
106
+ if (diff !== undefined && diff.breaking.length > 0 && !options?.force) {
107
+ throw new ConflictError(
108
+ `Topo contains ${diff.breaking.length} breaking change(s). Add a version entry, revert the change, or rerun with --force.`
109
+ );
110
+ }
111
+
112
+ const topoGraphBase =
113
+ previousTopo === null
114
+ ? nextTopo
115
+ : carryForwardTopoGraphForces(previousTopo, nextTopo);
116
+ const topoGraph =
117
+ diff === undefined || diff.breaking.length === 0
118
+ ? topoGraphBase
119
+ : annotateTopoGraphForces(topoGraphBase, diff.breaking);
120
+ 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
+
135
+ return {
136
+ hash,
137
+ lockPath,
138
+ topoPath,
139
+ };
140
+ };
141
+
142
+ export const exportCurrentTopo = async (
143
+ app: Topo,
144
+ options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
145
+ ): Promise<Result<TopoExportReport, Error>> => {
146
+ const rootDir = deriveRootDir(options?.rootDir);
147
+ const db = openWriteTrailsDb({ rootDir });
148
+
149
+ try {
150
+ const persisted = persistAndReadStoredExport(app, db, rootDir);
151
+ if (persisted.isErr()) {
152
+ return persisted;
153
+ }
154
+
155
+ const { snapshot, storedExport } = persisted.value;
156
+ let artifacts: Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>;
157
+ try {
158
+ artifacts = await writeStoredExportArtifacts(
159
+ storedExport,
160
+ deriveTrailsDir({ rootDir }),
161
+ { force: options?.force }
162
+ );
163
+ } catch (error: unknown) {
164
+ return Result.err(
165
+ error instanceof Error
166
+ ? error
167
+ : new InternalError('Unable to write topo artifacts')
168
+ );
169
+ }
170
+ return Result.ok({ ...artifacts, snapshot });
171
+ } finally {
172
+ db.close();
173
+ }
174
+ };
@@ -0,0 +1,220 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { deriveTrailsDbPath } from '@ontrails/core';
5
+ import type { Topo } from '@ontrails/core';
6
+ import {
7
+ createTopoSnapshot as persistTopoSnapshot,
8
+ listTopoSnapshots as readTopoSnapshots,
9
+ pinTopoSnapshot,
10
+ unpinTopoSnapshot,
11
+ } from '@ontrails/topographer';
12
+ import type { TopoSnapshot } from '@ontrails/topographer';
13
+ import { z } from 'zod';
14
+
15
+ import {
16
+ createIsolatedExampleRoot,
17
+ writeIsolatedExampleAppModule,
18
+ } from '../local-state-io.js';
19
+
20
+ import { requireTrailRootDir } from './root-dir.js';
21
+ import type { BriefReport, SurveyListReport } from './topo-reports.js';
22
+
23
+ /** Output schema for a topo snapshot record. Shared across topo trails. */
24
+ export const topoSnapshotOutput = z.object({
25
+ createdAt: z.string(),
26
+ gitDirty: z.boolean(),
27
+ gitSha: z.string().optional(),
28
+ id: z.string(),
29
+ pinnedAs: z.string().optional(),
30
+ resourceCount: z.number(),
31
+ signalCount: z.number(),
32
+ trailCount: z.number(),
33
+ });
34
+
35
+ export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
36
+ export const LOCK_PATH = '.trails/trails.lock';
37
+ const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
38
+
39
+ const uniqueExampleRootName = (name: string): string =>
40
+ `${name}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
41
+
42
+ export interface TopoSummaryReport {
43
+ readonly app: BriefReport;
44
+ readonly dbPath: string;
45
+ readonly list: SurveyListReport;
46
+ readonly lockExists: boolean;
47
+ readonly lockPath: string;
48
+ }
49
+
50
+ export interface TopoHistoryReport {
51
+ readonly dbPath: string;
52
+ readonly limit: number;
53
+ readonly pinnedCount: number;
54
+ readonly snapshotCount: number;
55
+ readonly snapshots: TopoSnapshot[];
56
+ }
57
+
58
+ export interface TopoExportReport {
59
+ readonly hash: string;
60
+ readonly lockPath: string;
61
+ readonly snapshot: TopoSnapshot;
62
+ readonly topoPath: string;
63
+ }
64
+
65
+ export interface TopoValidateReport {
66
+ readonly committedHash: string;
67
+ readonly currentHash: string;
68
+ readonly lockPath: string;
69
+ readonly stale: false;
70
+ }
71
+
72
+ export const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
73
+
74
+ const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
75
+ const proc = Bun.spawnSync({
76
+ cmd: ['git', '-C', cwd, ...args],
77
+ stderr: 'ignore',
78
+ stdout: 'pipe',
79
+ });
80
+ if (!proc.success) {
81
+ return undefined;
82
+ }
83
+ const text = Buffer.from(proc.stdout).toString('utf8').trim();
84
+ return text.length === 0 ? undefined : text;
85
+ };
86
+
87
+ export const readGitState = (
88
+ rootDir: string
89
+ ): { readonly gitDirty: boolean; readonly gitSha?: string } => {
90
+ const gitSha = safeGit(rootDir, ['rev-parse', 'HEAD']);
91
+ const status = safeGit(rootDir, ['status', '--porcelain']);
92
+ return {
93
+ gitDirty: (status?.length ?? 0) > 0,
94
+ ...(gitSha === undefined ? {} : { gitSha }),
95
+ };
96
+ };
97
+
98
+ export const deriveTopoCounts = (
99
+ app: Topo
100
+ ): Pick<TopoSnapshot, 'resourceCount' | 'signalCount' | 'trailCount'> => ({
101
+ resourceCount: app.resources.size,
102
+ signalCount: app.signals.size,
103
+ trailCount: app.trails.size,
104
+ });
105
+
106
+ const emptyTopoHistory = (
107
+ dbPath: string,
108
+ limit: number
109
+ ): TopoHistoryReport => ({
110
+ dbPath,
111
+ limit,
112
+ pinnedCount: 0,
113
+ snapshotCount: 0,
114
+ snapshots: [],
115
+ });
116
+
117
+ const collectTopoHistory = (
118
+ dbPath: string,
119
+ limit: number,
120
+ snapshots: readonly TopoSnapshot[]
121
+ ): TopoHistoryReport => ({
122
+ dbPath,
123
+ limit,
124
+ pinnedCount: snapshots.filter((snapshot) => snapshot.pinnedAs !== undefined)
125
+ .length,
126
+ snapshotCount: snapshots.length,
127
+ snapshots: snapshots.slice(0, limit),
128
+ });
129
+
130
+ const buildSnapshotInput = (
131
+ app: Topo,
132
+ rootDir: string
133
+ ): {
134
+ readonly gitDirty: boolean;
135
+ readonly gitSha?: string;
136
+ readonly resourceCount: number;
137
+ readonly signalCount: number;
138
+ readonly trailCount: number;
139
+ } => ({
140
+ ...readGitState(rootDir),
141
+ ...deriveTopoCounts(app),
142
+ });
143
+
144
+ export const createIsolatedExampleInput = (
145
+ name: string
146
+ ): { readonly module: string; readonly rootDir: string } => {
147
+ const rootDir = createIsolatedExampleRoot(uniqueExampleRootName(name));
148
+ return {
149
+ module: writeIsolatedExampleAppModule(rootDir, EXAMPLE_APP_MODULE),
150
+ rootDir,
151
+ };
152
+ };
153
+
154
+ export const listTopoHistory = (options?: {
155
+ readonly limit?: number;
156
+ readonly rootDir?: string;
157
+ }): TopoHistoryReport => {
158
+ const rootDir = deriveRootDir(options?.rootDir);
159
+ const limit = options?.limit ?? DEFAULT_TOPO_HISTORY_LIMIT;
160
+ const dbPath = deriveTrailsDbPath({ rootDir });
161
+ if (!existsSync(dbPath)) {
162
+ return emptyTopoHistory(dbPath, limit);
163
+ }
164
+
165
+ return collectTopoHistory(dbPath, limit, readTopoSnapshots({ rootDir }));
166
+ };
167
+
168
+ export const pinCurrentTopoSnapshot = (
169
+ app: Topo,
170
+ input: { readonly name: string; readonly rootDir?: string }
171
+ ): { readonly snapshot: TopoSnapshot } => {
172
+ const rootDir = deriveRootDir(input.rootDir);
173
+ const created = persistTopoSnapshot(app, {
174
+ rootDir,
175
+ ...buildSnapshotInput(app, rootDir),
176
+ });
177
+ if (created.isErr()) {
178
+ throw created.error;
179
+ }
180
+
181
+ const snapshot = pinTopoSnapshot(created.value.id, input.name, {
182
+ rootDir,
183
+ });
184
+ if (snapshot === undefined) {
185
+ throw new Error(`Missing topo snapshot "${created.value.id}" to pin`);
186
+ }
187
+
188
+ return { snapshot };
189
+ };
190
+
191
+ export const removePinnedTopoSnapshot = (input: {
192
+ readonly dryRun: boolean;
193
+ readonly name: string;
194
+ readonly rootDir?: string;
195
+ }): {
196
+ readonly dryRun: boolean;
197
+ readonly removed: boolean;
198
+ readonly snapshot?: TopoSnapshot;
199
+ } => {
200
+ const rootDir = deriveRootDir(input.rootDir);
201
+ if (!existsSync(deriveTrailsDbPath({ rootDir }))) {
202
+ return { dryRun: input.dryRun, removed: false };
203
+ }
204
+
205
+ if (input.dryRun) {
206
+ const snapshot = readTopoSnapshots({ pinned: true, rootDir }).find(
207
+ (candidate) => candidate.pinnedAs === input.name
208
+ );
209
+ return snapshot === undefined
210
+ ? { dryRun: true, removed: false }
211
+ : { dryRun: true, removed: false, snapshot };
212
+ }
213
+
214
+ const snapshot = unpinTopoSnapshot(input.name, { rootDir });
215
+ return {
216
+ dryRun: false,
217
+ removed: snapshot !== undefined,
218
+ ...(snapshot === undefined ? {} : { snapshot }),
219
+ };
220
+ };
@@ -0,0 +1,61 @@
1
+ import { Result, ValidationError, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ createIsolatedExampleInput,
6
+ removePinnedTopoSnapshot,
7
+ topoSnapshotOutput,
8
+ } from './topo-support.js';
9
+ import { resolveTrailRootDir } from './root-dir.js';
10
+
11
+ export const topoUnpinTrail = trail('topo.unpin', {
12
+ blaze: (input, ctx) => {
13
+ if (input.dryRun !== true && input.yes !== true) {
14
+ return Result.err(
15
+ new ValidationError(
16
+ 'Refusing to remove a pin without `--yes` or `--dry-run`.'
17
+ )
18
+ );
19
+ }
20
+
21
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
22
+ if (rootDirResult.isErr()) {
23
+ return rootDirResult;
24
+ }
25
+ const rootDir = rootDirResult.value;
26
+ return Result.ok(
27
+ removePinnedTopoSnapshot({
28
+ dryRun: input.dryRun,
29
+ name: input.name,
30
+ rootDir,
31
+ })
32
+ );
33
+ },
34
+ description: 'Remove a named topo pin',
35
+ examples: [
36
+ {
37
+ input: {
38
+ ...createIsolatedExampleInput('topo-unpin'),
39
+ dryRun: true,
40
+ name: 'before-auth-refactor',
41
+ },
42
+ name: 'Preview pin removal',
43
+ },
44
+ ],
45
+ input: z.object({
46
+ dryRun: z
47
+ .boolean()
48
+ .default(true)
49
+ .describe('Preview the removal without changing state'),
50
+ name: z.string().describe('Pin name'),
51
+ rootDir: z.string().optional().describe('Workspace root directory'),
52
+ yes: z.boolean().default(false).describe('Confirm destructive changes'),
53
+ }),
54
+ intent: 'destroy',
55
+ output: z.object({
56
+ dryRun: z.boolean(),
57
+ removed: z.boolean(),
58
+ snapshot: topoSnapshotOutput.optional(),
59
+ }),
60
+ permit: { scopes: ['topo:write'] },
61
+ });
@@ -0,0 +1,106 @@
1
+ import { Result, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { tryLoadFreshAppLease } from './load-app.js';
5
+ import { resolveTrailRootDir } from './root-dir.js';
6
+ import { activationOverviewOutput } from './topo-output-schemas.js';
7
+ import { buildTopoSummary } from './topo-read-support.js';
8
+ import { createIsolatedExampleInput } from './topo-support.js';
9
+
10
+ const summaryOutput = z.object({
11
+ app: z.object({
12
+ contractVersion: z.string(),
13
+ features: z.object({
14
+ detours: z.boolean(),
15
+ examples: z.boolean(),
16
+ outputSchemas: z.boolean(),
17
+ resources: z.boolean(),
18
+ signals: z.boolean(),
19
+ }),
20
+ name: z.string(),
21
+ resources: z.number(),
22
+ signals: z.number(),
23
+ trails: z.number(),
24
+ version: z.string(),
25
+ }),
26
+ dbPath: z.string(),
27
+ list: z.object({
28
+ activation: activationOverviewOutput,
29
+ count: z.number(),
30
+ entries: z
31
+ .array(
32
+ z.object({
33
+ activatedBy: z.array(z.string()).readonly(),
34
+ activates: z.array(z.string()).readonly(),
35
+ examples: z.number(),
36
+ id: z.string(),
37
+ kind: z.string(),
38
+ safety: z.string(),
39
+ })
40
+ )
41
+ .readonly(),
42
+ resourceCount: z.number(),
43
+ resources: z
44
+ .array(
45
+ z.object({
46
+ description: z.string().nullable(),
47
+ health: z.enum(['available', 'none']),
48
+ id: z.string(),
49
+ kind: z.literal('resource'),
50
+ lifetime: z.literal('singleton'),
51
+ usedBy: z.array(z.string()).readonly(),
52
+ })
53
+ )
54
+ .readonly(),
55
+ signalCount: z.number(),
56
+ signals: z
57
+ .array(
58
+ z.object({
59
+ consumers: z.array(z.string()).readonly(),
60
+ description: z.string().nullable(),
61
+ examples: z.number(),
62
+ from: z.array(z.string()).readonly(),
63
+ id: z.string(),
64
+ kind: z.literal('signal'),
65
+ payloadSchema: z.boolean(),
66
+ producers: z.array(z.string()).readonly(),
67
+ })
68
+ )
69
+ .readonly(),
70
+ }),
71
+ lockExists: z.boolean(),
72
+ lockPath: z.string(),
73
+ });
74
+
75
+ 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
+ },
93
+ description: 'Show the current topo summary and entry list',
94
+ examples: [
95
+ {
96
+ input: createIsolatedExampleInput('topo-summary'),
97
+ name: 'Show the current topo summary',
98
+ },
99
+ ],
100
+ input: z.object({
101
+ module: z.string().optional().describe('Path to the app module'),
102
+ rootDir: z.string().optional().describe('Workspace root directory'),
103
+ }),
104
+ intent: 'read',
105
+ output: summaryOutput,
106
+ });
@@ -0,0 +1,38 @@
1
+ import { trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { tryLoadFreshAppLease } from './load-app.js';
5
+ import { resolveTrailRootDir } from './root-dir.js';
6
+ import { validateCurrentTopo } from './topo-read-support.js';
7
+
8
+ 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',
27
+ input: z.object({
28
+ module: z.string().optional().describe('Path to the app module'),
29
+ rootDir: z.string().optional().describe('Workspace root directory'),
30
+ }),
31
+ intent: 'read',
32
+ output: z.object({
33
+ committedHash: z.string(),
34
+ currentHash: z.string(),
35
+ lockPath: z.string(),
36
+ stale: z.literal(false),
37
+ }),
38
+ });