@ontrails/trails 1.0.0-beta.13 → 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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +29 -0
- package/__tests__/examples.test.ts +39 -0
- package/dist/src/app.d.ts.map +1 -1
- package/dist/src/app.js +12 -1
- package/dist/src/app.js.map +1 -1
- package/dist/src/cli.js +4 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/trails/add-surface.d.ts +3 -3
- package/dist/src/trails/add-surface.d.ts.map +1 -1
- package/dist/src/trails/add-surface.js +46 -24
- package/dist/src/trails/add-surface.js.map +1 -1
- package/dist/src/trails/add-trail.d.ts +3 -1
- package/dist/src/trails/add-trail.d.ts.map +1 -1
- package/dist/src/trails/add-trail.js +49 -22
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-trailhead.d.ts +13 -0
- package/dist/src/trails/add-trailhead.d.ts.map +1 -0
- package/dist/src/trails/add-trailhead.js +88 -0
- package/dist/src/trails/add-trailhead.js.map +1 -0
- package/dist/src/trails/add-verify.d.ts +1 -1
- package/dist/src/trails/add-verify.d.ts.map +1 -1
- package/dist/src/trails/add-verify.js +17 -16
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.d.ts +1 -1
- package/dist/src/trails/create-scaffold.d.ts.map +1 -1
- package/dist/src/trails/create-scaffold.js +34 -27
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +9 -13
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +40 -35
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/dev-clean.d.ts +9 -0
- package/dist/src/trails/dev-clean.d.ts.map +1 -0
- package/dist/src/trails/dev-clean.js +66 -0
- package/dist/src/trails/dev-clean.js.map +1 -0
- package/dist/src/trails/dev-reset.d.ts +6 -0
- package/dist/src/trails/dev-reset.d.ts.map +1 -0
- package/dist/src/trails/dev-reset.js +39 -0
- package/dist/src/trails/dev-reset.js.map +1 -0
- package/dist/src/trails/dev-stats.d.ts +7 -0
- package/dist/src/trails/dev-stats.d.ts.map +1 -0
- package/dist/src/trails/dev-stats.js +61 -0
- package/dist/src/trails/dev-stats.js.map +1 -0
- package/dist/src/trails/dev-support.d.ts +64 -0
- package/dist/src/trails/dev-support.d.ts.map +1 -0
- package/dist/src/trails/dev-support.js +181 -0
- package/dist/src/trails/dev-support.js.map +1 -0
- package/dist/src/trails/draft-promote.d.ts +18 -0
- package/dist/src/trails/draft-promote.d.ts.map +1 -0
- package/dist/src/trails/draft-promote.js +400 -0
- package/dist/src/trails/draft-promote.js.map +1 -0
- package/dist/src/trails/guide.d.ts +14 -4
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +22 -41
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +9 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +404 -13
- package/dist/src/trails/load-app.js.map +1 -1
- package/dist/src/trails/project.d.ts.map +1 -1
- package/dist/src/trails/project.js +14 -3
- package/dist/src/trails/project.js.map +1 -1
- package/dist/src/trails/survey.d.ts +6 -60
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +83 -182
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/topo-constants.d.ts +3 -0
- package/dist/src/trails/topo-constants.d.ts.map +1 -0
- package/dist/src/trails/topo-constants.js +3 -0
- package/dist/src/trails/topo-constants.js.map +1 -0
- package/dist/src/trails/topo-export.d.ts +19 -0
- package/dist/src/trails/topo-export.d.ts.map +1 -0
- package/dist/src/trails/topo-export.js +31 -0
- package/dist/src/trails/topo-export.js.map +1 -0
- package/dist/src/trails/topo-history.d.ts +20 -0
- package/dist/src/trails/topo-history.d.ts.map +1 -0
- package/dist/src/trails/topo-history.js +32 -0
- package/dist/src/trails/topo-history.js.map +1 -0
- package/dist/src/trails/topo-pin.d.ts +17 -0
- package/dist/src/trails/topo-pin.d.ts.map +1 -0
- package/dist/src/trails/topo-pin.js +31 -0
- package/dist/src/trails/topo-pin.js.map +1 -0
- package/dist/src/trails/topo-read-support.d.ts +58 -0
- package/dist/src/trails/topo-read-support.d.ts.map +1 -0
- package/dist/src/trails/topo-read-support.js +167 -0
- package/dist/src/trails/topo-read-support.js.map +1 -0
- package/dist/src/trails/topo-reports.d.ts +54 -0
- package/dist/src/trails/topo-reports.d.ts.map +1 -0
- package/dist/src/trails/topo-reports.js +128 -0
- package/dist/src/trails/topo-reports.js.map +1 -0
- package/dist/src/trails/topo-show.d.ts +23 -0
- package/dist/src/trails/topo-show.d.ts.map +1 -0
- package/dist/src/trails/topo-show.js +49 -0
- package/dist/src/trails/topo-show.js.map +1 -0
- package/dist/src/trails/topo-store-support.d.ts +13 -0
- package/dist/src/trails/topo-store-support.d.ts.map +1 -0
- package/dist/src/trails/topo-store-support.js +55 -0
- package/dist/src/trails/topo-store-support.js.map +1 -0
- package/dist/src/trails/topo-support.d.ts +76 -0
- package/dist/src/trails/topo-support.d.ts.map +1 -0
- package/dist/src/trails/topo-support.js +132 -0
- package/dist/src/trails/topo-support.js.map +1 -0
- package/dist/src/trails/topo-unpin.d.ts +20 -0
- package/dist/src/trails/topo-unpin.d.ts.map +1 -0
- package/dist/src/trails/topo-unpin.js +44 -0
- package/dist/src/trails/topo-unpin.js.map +1 -0
- package/dist/src/trails/topo-verify.d.ts +5 -0
- package/dist/src/trails/topo-verify.d.ts.map +1 -0
- package/dist/src/trails/topo-verify.js +24 -0
- package/dist/src/trails/topo-verify.js.map +1 -0
- package/dist/src/trails/topo.d.ts +5 -0
- package/dist/src/trails/topo.d.ts.map +1 -0
- package/dist/src/trails/topo.js +63 -0
- package/dist/src/trails/topo.js.map +1 -0
- package/dist/src/trails/warden.d.ts +3 -2
- package/dist/src/trails/warden.d.ts.map +1 -1
- package/dist/src/trails/warden.js +37 -27
- package/dist/src/trails/warden.js.map +1 -1
- package/dist/src/versions.d.ts +12 -0
- package/dist/src/versions.d.ts.map +1 -0
- package/dist/src/versions.js +23 -0
- package/dist/src/versions.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/add-trail.test.ts +97 -0
- package/src/__tests__/create.test.ts +91 -27
- package/src/__tests__/draft-promote.test.ts +144 -0
- package/src/__tests__/guide.test.ts +10 -5
- package/src/__tests__/load-app.test.ts +406 -2
- package/src/__tests__/survey.test.ts +221 -60
- package/src/__tests__/topo-dev.test.ts +426 -0
- package/src/app.ts +24 -2
- package/src/clack.ts +1 -1
- package/src/cli.ts +4 -3
- package/src/trails/add-surface.ts +150 -0
- package/src/trails/add-trail.ts +46 -10
- package/src/trails/add-verify.ts +11 -6
- package/src/trails/create-scaffold.ts +16 -3
- package/src/trails/create.ts +76 -71
- package/src/trails/dev-clean.ts +77 -0
- package/src/trails/dev-reset.ts +45 -0
- package/src/trails/dev-stats.ts +67 -0
- package/src/trails/dev-support.ts +328 -0
- package/src/trails/draft-promote.ts +739 -0
- package/src/trails/guide.ts +23 -41
- package/src/trails/load-app.ts +556 -14
- package/src/trails/project.ts +17 -3
- package/src/trails/survey.ts +110 -285
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-export.ts +35 -0
- package/src/trails/topo-history.ts +38 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +329 -0
- package/src/trails/topo-reports.ts +228 -0
- package/src/trails/topo-show.ts +54 -0
- package/src/trails/topo-store-support.ts +104 -0
- package/src/trails/topo-support.ts +230 -0
- package/src/trails/topo-unpin.ts +56 -0
- package/src/trails/topo-verify.ts +25 -0
- package/src/trails/topo.ts +69 -0
- package/src/trails/warden.ts +13 -3
- package/src/versions.ts +43 -0
- package/tsconfig.tests.json +10 -0
- package/src/trails/add-trailhead.ts +0 -121
package/src/trails/project.ts
CHANGED
|
@@ -5,10 +5,24 @@
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
8
|
+
import { isDraftMarkedFile } from '@ontrails/warden';
|
|
9
|
+
|
|
8
10
|
/** Return all TypeScript entries in a project's src directory. */
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const sourceEntryPriority = (entry: string): number => {
|
|
12
|
+
if (entry === 'app.ts') {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
return isDraftMarkedFile(entry) ? 2 : 1;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const scanSourceEntries = (srcDir: string): string[] =>
|
|
19
|
+
[...new Bun.Glob('*.ts').scanSync({ cwd: srcDir })].toSorted((a, b) => {
|
|
20
|
+
const priority = sourceEntryPriority(a) - sourceEntryPriority(b);
|
|
21
|
+
if (priority === 0) {
|
|
22
|
+
return a.localeCompare(b);
|
|
23
|
+
}
|
|
24
|
+
return priority;
|
|
25
|
+
});
|
|
12
26
|
|
|
13
27
|
/** Resolve an entry to an app import if it contains topo(). */
|
|
14
28
|
const toTopoImport = async (
|
package/src/trails/survey.ts
CHANGED
|
@@ -1,256 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `survey` trail -- Full topo introspection.
|
|
3
3
|
*
|
|
4
|
-
* Lists trails, shows detail for individual trails, generates
|
|
4
|
+
* Lists trails, shows detail for individual trails, generates surface maps,
|
|
5
5
|
* and diffs against previous versions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import type { Topo } from '@ontrails/core';
|
|
11
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
10
12
|
import type { DiffResult } from '@ontrails/schema';
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
readTrailheadMap,
|
|
17
|
-
writeTrailheadLock,
|
|
18
|
-
writeTrailheadMap,
|
|
14
|
+
deriveSurfaceMapDiff,
|
|
15
|
+
deriveOpenApiSpec,
|
|
16
|
+
deriveSurfaceMap,
|
|
17
|
+
readSurfaceMap,
|
|
19
18
|
} from '@ontrails/schema';
|
|
20
19
|
import { z } from 'zod';
|
|
21
20
|
|
|
22
|
-
import { loadApp } from './load-app.js';
|
|
21
|
+
import { loadApp, loadFreshAppLease } from './load-app.js';
|
|
22
|
+
import {
|
|
23
|
+
buildCurrentTopoBrief,
|
|
24
|
+
buildCurrentTopoDetail,
|
|
25
|
+
buildCurrentTopoList,
|
|
26
|
+
} from './topo-read-support.js';
|
|
27
|
+
import { exportCurrentTopo } from './topo-store-support.js';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
deriveBriefReport,
|
|
31
|
+
deriveResourceDetail,
|
|
32
|
+
deriveSurveyList,
|
|
33
|
+
deriveTrailDetail,
|
|
34
|
+
} from './topo-reports.js';
|
|
35
|
+
export type {
|
|
36
|
+
BriefReport,
|
|
37
|
+
SurveyListReport,
|
|
38
|
+
TrailDetailReport,
|
|
39
|
+
} from './topo-reports.js';
|
|
23
40
|
|
|
24
41
|
// ---------------------------------------------------------------------------
|
|
25
42
|
// Brief report (formerly scout)
|
|
26
43
|
// ---------------------------------------------------------------------------
|
|
27
44
|
|
|
28
|
-
export interface BriefReport {
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly version: string;
|
|
31
|
-
readonly contractVersion: string;
|
|
32
|
-
readonly features: {
|
|
33
|
-
readonly provisions: boolean;
|
|
34
|
-
readonly outputSchemas: boolean;
|
|
35
|
-
readonly examples: boolean;
|
|
36
|
-
readonly detours: boolean;
|
|
37
|
-
readonly signals: boolean;
|
|
38
|
-
};
|
|
39
|
-
readonly trails: number;
|
|
40
|
-
readonly signals: number;
|
|
41
|
-
readonly provisions: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SurveyListReport {
|
|
45
|
-
readonly count: number;
|
|
46
|
-
readonly entries: readonly {
|
|
47
|
-
readonly examples: number;
|
|
48
|
-
readonly id: string;
|
|
49
|
-
readonly kind: string;
|
|
50
|
-
readonly safety: string;
|
|
51
|
-
}[];
|
|
52
|
-
readonly provisionCount: number;
|
|
53
|
-
readonly provisions: readonly {
|
|
54
|
-
readonly description: string | null;
|
|
55
|
-
readonly health: 'available' | 'none';
|
|
56
|
-
readonly id: string;
|
|
57
|
-
readonly kind: 'provision';
|
|
58
|
-
readonly lifetime: 'singleton';
|
|
59
|
-
readonly usedBy: readonly string[];
|
|
60
|
-
}[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface TrailDetailReport {
|
|
64
|
-
readonly description: string | null;
|
|
65
|
-
readonly detours: Trail<unknown, unknown>['detours'] | null;
|
|
66
|
-
readonly examples: readonly unknown[];
|
|
67
|
-
readonly crosses: readonly string[];
|
|
68
|
-
readonly id: string;
|
|
69
|
-
readonly intent: 'read' | 'write' | 'destroy';
|
|
70
|
-
readonly kind: string;
|
|
71
|
-
readonly safety: string;
|
|
72
|
-
readonly provisions: readonly string[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Check if a trail has a specific feature. */
|
|
76
|
-
const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
|
|
77
|
-
if (key === 'examples') {
|
|
78
|
-
return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
|
|
79
|
-
}
|
|
80
|
-
return Boolean(raw[key]);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** Detect which features are used across trails. */
|
|
84
|
-
const detectFeatures = (
|
|
85
|
-
app: Topo
|
|
86
|
-
): {
|
|
87
|
-
hasDetours: boolean;
|
|
88
|
-
hasExamples: boolean;
|
|
89
|
-
hasOutputSchemas: boolean;
|
|
90
|
-
hasProvisions: boolean;
|
|
91
|
-
} => {
|
|
92
|
-
const trails = [...app.trails.values()].map(
|
|
93
|
-
(item) => item as unknown as Record<string, unknown>
|
|
94
|
-
);
|
|
95
|
-
return {
|
|
96
|
-
hasDetours: trails.some((r) => trailHas(r, 'detours')),
|
|
97
|
-
hasExamples: trails.some((r) => trailHas(r, 'examples')),
|
|
98
|
-
hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
|
|
99
|
-
hasProvisions: trails.some(
|
|
100
|
-
(r) =>
|
|
101
|
-
Array.isArray(r['provisions']) &&
|
|
102
|
-
(r['provisions'] as unknown[]).length > 0
|
|
103
|
-
),
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** Generate a compact capability report for the given topo. */
|
|
108
|
-
export const generateBriefReport = (app: Topo): BriefReport => {
|
|
109
|
-
const { hasDetours, hasExamples, hasOutputSchemas, hasProvisions } =
|
|
110
|
-
detectFeatures(app);
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
contractVersion: '2026-03',
|
|
114
|
-
features: {
|
|
115
|
-
detours: hasDetours,
|
|
116
|
-
examples: hasExamples,
|
|
117
|
-
outputSchemas: hasOutputSchemas,
|
|
118
|
-
provisions: hasProvisions,
|
|
119
|
-
signals: app.signals.size > 0,
|
|
120
|
-
},
|
|
121
|
-
name: app.name,
|
|
122
|
-
provisions: app.provisions.size,
|
|
123
|
-
signals: app.signals.size,
|
|
124
|
-
trails: app.trails.size,
|
|
125
|
-
version: '0.1.0',
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Formatting helpers
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
const safetyLabel = (entry: {
|
|
134
|
-
intent?: 'read' | 'write' | 'destroy';
|
|
135
|
-
}): string => {
|
|
136
|
-
if (entry.intent === 'destroy') {
|
|
137
|
-
return 'destroy';
|
|
138
|
-
}
|
|
139
|
-
if (entry.intent === 'read') {
|
|
140
|
-
return 'read';
|
|
141
|
-
}
|
|
142
|
-
return '-';
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const buildProvisionUsage = (
|
|
146
|
-
app: Topo
|
|
147
|
-
): ReadonlyMap<string, readonly string[]> => {
|
|
148
|
-
const usage = new Map<string, string[]>();
|
|
149
|
-
|
|
150
|
-
for (const trailDef of app.list()) {
|
|
151
|
-
for (const declaredProvision of trailDef.provisions) {
|
|
152
|
-
const users = usage.get(declaredProvision.id) ?? [];
|
|
153
|
-
users.push(trailDef.id);
|
|
154
|
-
usage.set(declaredProvision.id, users);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return new Map(
|
|
159
|
-
[...usage.entries()].map(([id, users]) => [id, users.toSorted()] as const)
|
|
160
|
-
);
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const provisionHealthStatus = (provision: {
|
|
164
|
-
health?: unknown;
|
|
165
|
-
}): 'available' | 'none' =>
|
|
166
|
-
provision.health === undefined ? 'none' : 'available';
|
|
167
|
-
|
|
168
|
-
const formatProvisionList = (app: Topo): SurveyListReport['provisions'] => {
|
|
169
|
-
const usage = buildProvisionUsage(app);
|
|
170
|
-
return app
|
|
171
|
-
.listProvisions()
|
|
172
|
-
.map((provision) => ({
|
|
173
|
-
description: provision.description ?? null,
|
|
174
|
-
health: provisionHealthStatus(provision),
|
|
175
|
-
id: provision.id,
|
|
176
|
-
kind: provision.kind,
|
|
177
|
-
lifetime: 'singleton' as const,
|
|
178
|
-
usedBy: usage.get(provision.id) ?? [],
|
|
179
|
-
}))
|
|
180
|
-
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
export const generateSurveyList = (app: Topo): SurveyListReport => {
|
|
184
|
-
const items = app.list();
|
|
185
|
-
const entries = items.map((item) => {
|
|
186
|
-
const safety = safetyLabel(
|
|
187
|
-
item as unknown as { intent?: 'read' | 'write' | 'destroy' }
|
|
188
|
-
);
|
|
189
|
-
const examples = Array.isArray(
|
|
190
|
-
(item as unknown as { examples?: unknown[] }).examples
|
|
191
|
-
)
|
|
192
|
-
? (item as unknown as { examples: unknown[] }).examples.length
|
|
193
|
-
: 0;
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
examples,
|
|
197
|
-
id: item.id,
|
|
198
|
-
kind: item.kind,
|
|
199
|
-
safety,
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const provisions = formatProvisionList(app);
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
count: items.length,
|
|
207
|
-
entries,
|
|
208
|
-
provisionCount: provisions.length,
|
|
209
|
-
provisions,
|
|
210
|
-
};
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Build a human-readable detail view for a single trail.
|
|
215
|
-
*
|
|
216
|
-
* Overlaps with `trailToEntry` in `@ontrails/schema` which builds the
|
|
217
|
-
* trailhead-map entry. The two serve different audiences (human display vs
|
|
218
|
-
* machine-diffable trailhead map) so they are kept separate.
|
|
219
|
-
*/
|
|
220
|
-
export const generateTrailDetail = (
|
|
221
|
-
item: Trail<unknown, unknown>
|
|
222
|
-
): TrailDetailReport => {
|
|
223
|
-
const safety = safetyLabel(
|
|
224
|
-
item as unknown as { intent?: 'read' | 'write' | 'destroy' }
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
crosses: item.crosses.toSorted(),
|
|
229
|
-
description: item.description ?? null,
|
|
230
|
-
detours: item.detours ?? null,
|
|
231
|
-
examples: item.examples ?? [],
|
|
232
|
-
id: item.id,
|
|
233
|
-
intent: item.intent,
|
|
234
|
-
kind: item.kind,
|
|
235
|
-
provisions: item.provisions.map((provision) => provision.id).toSorted(),
|
|
236
|
-
safety,
|
|
237
|
-
};
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const formatProvisionDetail = (app: Topo, provisionId: string): object => {
|
|
241
|
-
const item = app.getProvision(provisionId);
|
|
242
|
-
const usedBy = buildProvisionUsage(app).get(provisionId) ?? [];
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
description: item?.description ?? null,
|
|
246
|
-
health: item ? provisionHealthStatus(item) : 'none',
|
|
247
|
-
id: provisionId,
|
|
248
|
-
kind: 'provision',
|
|
249
|
-
lifetime: 'singleton',
|
|
250
|
-
usedBy,
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
|
|
254
45
|
const formatDiff = (diff: DiffResult): object => ({
|
|
255
46
|
breaking: diff.breaking,
|
|
256
47
|
hasBreaking: diff.hasBreaking,
|
|
@@ -260,19 +51,20 @@ const formatDiff = (diff: DiffResult): object => ({
|
|
|
260
51
|
|
|
261
52
|
const buildSurveyDiff = async (
|
|
262
53
|
app: Topo,
|
|
54
|
+
rootDir: string,
|
|
263
55
|
breakingOnly: boolean
|
|
264
56
|
): Promise<Result<object, Error>> => {
|
|
265
|
-
const currentMap =
|
|
266
|
-
const previousMap = await
|
|
57
|
+
const currentMap = deriveSurfaceMap(app);
|
|
58
|
+
const previousMap = await readSurfaceMap({ dir: join(rootDir, '.trails') });
|
|
267
59
|
if (!previousMap) {
|
|
268
60
|
return Result.err(
|
|
269
|
-
new
|
|
270
|
-
'No
|
|
61
|
+
new NotFoundError(
|
|
62
|
+
'No saved surface map found. Run `trails topo export` first.'
|
|
271
63
|
)
|
|
272
64
|
);
|
|
273
65
|
}
|
|
274
66
|
|
|
275
|
-
const diff =
|
|
67
|
+
const diff = deriveSurfaceMapDiff(previousMap, currentMap);
|
|
276
68
|
return Result.ok(
|
|
277
69
|
breakingOnly
|
|
278
70
|
? formatDiff({
|
|
@@ -287,32 +79,37 @@ const buildSurveyDiff = async (
|
|
|
287
79
|
|
|
288
80
|
const buildSurveyDetail = (
|
|
289
81
|
app: Topo,
|
|
290
|
-
trailId: string
|
|
82
|
+
trailId: string,
|
|
83
|
+
rootDir: string
|
|
291
84
|
): Result<object, Error> => {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
return Result.ok(
|
|
85
|
+
const detail = buildCurrentTopoDetail(app, trailId, { rootDir });
|
|
86
|
+
if (detail !== undefined) {
|
|
87
|
+
return Result.ok(detail);
|
|
295
88
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return Result.err(new Error(`Trail or provision not found: ${trailId}`));
|
|
89
|
+
return Result.err(
|
|
90
|
+
new NotFoundError(`Trail or resource not found: ${trailId}`)
|
|
91
|
+
);
|
|
300
92
|
};
|
|
301
93
|
|
|
302
94
|
const buildSurveyGenerate = async (
|
|
303
|
-
app: Topo
|
|
95
|
+
app: Topo,
|
|
96
|
+
rootDir: string
|
|
304
97
|
): Promise<Result<object, Error>> => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return Result.ok({
|
|
98
|
+
const exported = await exportCurrentTopo(app, { rootDir });
|
|
99
|
+
if (exported.isErr()) {
|
|
100
|
+
return exported;
|
|
101
|
+
}
|
|
102
|
+
return Result.ok({
|
|
103
|
+
hash: exported.value.hash,
|
|
104
|
+
lockPath: exported.value.lockPath,
|
|
105
|
+
mapPath: exported.value.mapPath,
|
|
106
|
+
});
|
|
310
107
|
};
|
|
311
108
|
|
|
312
109
|
interface SurveyInput {
|
|
313
110
|
breakingOnly: boolean;
|
|
314
111
|
brief: boolean;
|
|
315
|
-
|
|
112
|
+
diffSaved: boolean;
|
|
316
113
|
generate: boolean;
|
|
317
114
|
openapi: boolean;
|
|
318
115
|
trailId?: string | undefined;
|
|
@@ -323,39 +120,45 @@ type SurveyMode = 'brief' | 'detail' | 'diff' | 'generate' | 'list' | 'openapi';
|
|
|
323
120
|
/** Ordered mode checks — first truthy predicate wins, otherwise 'list'. */
|
|
324
121
|
const modeChecks: readonly [(input: SurveyInput) => boolean, SurveyMode][] = [
|
|
325
122
|
[(i) => i.brief, 'brief'],
|
|
326
|
-
[(i) =>
|
|
123
|
+
[(i) => i.diffSaved, 'diff'],
|
|
327
124
|
[(i) => Boolean(i.trailId), 'detail'],
|
|
328
125
|
[(i) => i.generate, 'generate'],
|
|
329
126
|
[(i) => i.openapi, 'openapi'],
|
|
330
127
|
];
|
|
331
128
|
|
|
332
129
|
/** Determine which survey mode was requested, falling back to 'list'. */
|
|
333
|
-
const
|
|
130
|
+
const deriveSurveyMode = (input: SurveyInput): SurveyMode =>
|
|
334
131
|
modeChecks.find(([predicate]) => predicate(input))?.[1] ?? 'list';
|
|
335
132
|
|
|
336
133
|
type SurveyHandler = (
|
|
337
134
|
app: Topo,
|
|
338
|
-
input: SurveyInput
|
|
135
|
+
input: SurveyInput,
|
|
136
|
+
rootDir: string
|
|
339
137
|
) => Result<object, Error> | Promise<Result<object, Error>>;
|
|
340
138
|
|
|
341
139
|
/** Handlers keyed by survey mode. */
|
|
342
140
|
const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
|
|
343
|
-
brief: (app) =>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
141
|
+
brief: (app, _input, rootDir) =>
|
|
142
|
+
Result.ok(buildCurrentTopoBrief(app, { rootDir })),
|
|
143
|
+
detail: (app, input, rootDir) =>
|
|
144
|
+
buildSurveyDetail(app, input.trailId ?? '', rootDir),
|
|
145
|
+
diff: (app, input, rootDir) =>
|
|
146
|
+
buildSurveyDiff(app, rootDir, input.breakingOnly),
|
|
147
|
+
generate: (app, _input, rootDir) => buildSurveyGenerate(app, rootDir),
|
|
148
|
+
list: (app, _input, rootDir) =>
|
|
149
|
+
Result.ok(buildCurrentTopoList(app, { rootDir })),
|
|
150
|
+
openapi: (app) => Result.ok(deriveOpenApiSpec(app)),
|
|
349
151
|
};
|
|
350
152
|
|
|
351
153
|
/** Dispatch to the appropriate survey sub-command based on input flags. */
|
|
352
154
|
const dispatchSurvey = (
|
|
353
155
|
app: Topo,
|
|
354
|
-
input: SurveyInput
|
|
156
|
+
input: SurveyInput,
|
|
157
|
+
rootDir: string
|
|
355
158
|
): Result<object, Error> | Promise<Result<object, Error>> => {
|
|
356
|
-
const mode =
|
|
159
|
+
const mode = deriveSurveyMode(input);
|
|
357
160
|
const handler = surveyHandlers[mode];
|
|
358
|
-
return handler(app, input);
|
|
161
|
+
return handler(app, input, rootDir);
|
|
359
162
|
};
|
|
360
163
|
|
|
361
164
|
// ---------------------------------------------------------------------------
|
|
@@ -364,13 +167,35 @@ const dispatchSurvey = (
|
|
|
364
167
|
|
|
365
168
|
export const surveyTrail = trail('survey', {
|
|
366
169
|
blaze: async (input, ctx) => {
|
|
367
|
-
const
|
|
368
|
-
|
|
170
|
+
const rootDir = ctx.cwd ?? '.';
|
|
171
|
+
const mode = deriveSurveyMode(input);
|
|
172
|
+
// Fresh load only for diffSaved: comparing against a previously-saved
|
|
173
|
+
// surface map requires the current app's source state, not any cached
|
|
174
|
+
// module graph that a prior import may have frozen. Other modes read
|
|
175
|
+
// the in-memory topo and benefit from the standard import cache.
|
|
176
|
+
//
|
|
177
|
+
// For diff specifically, use a disposable lease rather than retained
|
|
178
|
+
// fresh mirrors — the returned diff result is serialisable data, not
|
|
179
|
+
// a Topo reference with deferred imports, so the mirror can be
|
|
180
|
+
// released the moment dispatchSurvey returns. That keeps MCP/dev
|
|
181
|
+
// sessions that poll diff repeatedly from growing .trails-tmp/
|
|
182
|
+
// without bound.
|
|
183
|
+
if (mode === 'diff') {
|
|
184
|
+
const lease = await loadFreshAppLease(input.module, rootDir);
|
|
185
|
+
try {
|
|
186
|
+
return await dispatchSurvey(lease.app, input, rootDir);
|
|
187
|
+
} finally {
|
|
188
|
+
lease.release();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const app = await loadApp(input.module, rootDir);
|
|
193
|
+
return dispatchSurvey(app, input, rootDir);
|
|
369
194
|
},
|
|
370
195
|
description: 'Full topo introspection',
|
|
371
196
|
examples: [
|
|
372
197
|
{
|
|
373
|
-
description: 'Lists all registered trails with safety and
|
|
198
|
+
description: 'Lists all registered trails with safety and surface info',
|
|
374
199
|
input: { module: './src/app.ts' },
|
|
375
200
|
name: 'List all trails',
|
|
376
201
|
},
|
|
@@ -391,15 +216,15 @@ export const surveyTrail = trail('survey', {
|
|
|
391
216
|
.default(false)
|
|
392
217
|
.describe('Only show breaking changes'),
|
|
393
218
|
brief: z.boolean().default(false).describe('Quick capability summary'),
|
|
394
|
-
|
|
219
|
+
diffSaved: z
|
|
220
|
+
.boolean()
|
|
221
|
+
.default(false)
|
|
222
|
+
.describe('Diff against the saved local surface map'),
|
|
395
223
|
generate: z
|
|
396
224
|
.boolean()
|
|
397
225
|
.default(false)
|
|
398
|
-
.describe('Generate
|
|
399
|
-
module: z
|
|
400
|
-
.string()
|
|
401
|
-
.default('./src/app.ts')
|
|
402
|
-
.describe('Path to the app module'),
|
|
226
|
+
.describe('Generate surface map and lock file'),
|
|
227
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
403
228
|
openapi: z.boolean().default(false).describe('Output OpenAPI 3.1 spec'),
|
|
404
229
|
trailId: z.string().optional().describe('Trail ID for detail view'),
|
|
405
230
|
}),
|
|
@@ -415,13 +240,13 @@ export const surveyTrail = trail('survey', {
|
|
|
415
240
|
safety: z.string(),
|
|
416
241
|
})
|
|
417
242
|
),
|
|
418
|
-
|
|
419
|
-
|
|
243
|
+
resourceCount: z.number(),
|
|
244
|
+
resources: z.array(
|
|
420
245
|
z.object({
|
|
421
246
|
description: z.string().nullable(),
|
|
422
247
|
health: z.enum(['available', 'none']),
|
|
423
248
|
id: z.string(),
|
|
424
|
-
kind: z.literal('
|
|
249
|
+
kind: z.literal('resource'),
|
|
425
250
|
lifetime: z.literal('singleton'),
|
|
426
251
|
usedBy: z.array(z.string()),
|
|
427
252
|
})
|
|
@@ -433,11 +258,11 @@ export const surveyTrail = trail('survey', {
|
|
|
433
258
|
detours: z.boolean(),
|
|
434
259
|
examples: z.boolean(),
|
|
435
260
|
outputSchemas: z.boolean(),
|
|
436
|
-
|
|
261
|
+
resources: z.boolean(),
|
|
437
262
|
signals: z.boolean(),
|
|
438
263
|
}),
|
|
439
264
|
name: z.string(),
|
|
440
|
-
|
|
265
|
+
resources: z.number(),
|
|
441
266
|
signals: z.number(),
|
|
442
267
|
trails: z.number(),
|
|
443
268
|
version: z.string(),
|
|
@@ -456,14 +281,14 @@ export const surveyTrail = trail('survey', {
|
|
|
456
281
|
id: z.string(),
|
|
457
282
|
intent: z.enum(['read', 'write', 'destroy']),
|
|
458
283
|
kind: z.string(),
|
|
459
|
-
|
|
284
|
+
resources: z.array(z.string()),
|
|
460
285
|
safety: z.string(),
|
|
461
286
|
}),
|
|
462
287
|
z.object({
|
|
463
288
|
description: z.string().nullable(),
|
|
464
289
|
health: z.enum(['available', 'none']),
|
|
465
290
|
id: z.string(),
|
|
466
|
-
kind: z.literal('
|
|
291
|
+
kind: z.literal('resource'),
|
|
467
292
|
lifetime: z.literal('singleton'),
|
|
468
293
|
usedBy: z.array(z.string()),
|
|
469
294
|
}),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { loadApp } from './load-app.js';
|
|
5
|
+
import { exportCurrentTopo } from './topo-store-support.js';
|
|
6
|
+
import {
|
|
7
|
+
createIsolatedExampleInput,
|
|
8
|
+
topoSnapshotOutput,
|
|
9
|
+
} from './topo-support.js';
|
|
10
|
+
|
|
11
|
+
export const topoExportTrail = trail('topo.export', {
|
|
12
|
+
blaze: async (input, ctx) => {
|
|
13
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
14
|
+
const app = await loadApp(input.module, rootDir);
|
|
15
|
+
return exportCurrentTopo(app, { rootDir });
|
|
16
|
+
},
|
|
17
|
+
description: 'Export the current topo to .trails artifacts',
|
|
18
|
+
examples: [
|
|
19
|
+
{
|
|
20
|
+
input: createIsolatedExampleInput('topo-export'),
|
|
21
|
+
name: 'Write the current topo export',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
input: z.object({
|
|
25
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
26
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
27
|
+
}),
|
|
28
|
+
intent: 'write',
|
|
29
|
+
output: z.object({
|
|
30
|
+
hash: z.string(),
|
|
31
|
+
lockPath: z.string(),
|
|
32
|
+
mapPath: z.string(),
|
|
33
|
+
snapshot: topoSnapshotOutput,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_TOPO_HISTORY_LIMIT,
|
|
6
|
+
createIsolatedExampleInput,
|
|
7
|
+
listTopoHistory,
|
|
8
|
+
topoSnapshotOutput,
|
|
9
|
+
} from './topo-support.js';
|
|
10
|
+
|
|
11
|
+
export const topoHistoryTrail = trail('topo.history', {
|
|
12
|
+
blaze: (input, ctx) => {
|
|
13
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
14
|
+
return Result.ok(listTopoHistory({ limit: input.limit, rootDir }));
|
|
15
|
+
},
|
|
16
|
+
description: 'List saved topo snapshots, including pinned references',
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
input: createIsolatedExampleInput('topo-history'),
|
|
20
|
+
name: 'Show topo history',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
input: z.object({
|
|
24
|
+
limit: z
|
|
25
|
+
.number()
|
|
26
|
+
.default(DEFAULT_TOPO_HISTORY_LIMIT)
|
|
27
|
+
.describe('Maximum number of snapshots to return'),
|
|
28
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
29
|
+
}),
|
|
30
|
+
intent: 'read',
|
|
31
|
+
output: z.object({
|
|
32
|
+
dbPath: z.string(),
|
|
33
|
+
limit: z.number(),
|
|
34
|
+
pinnedCount: z.number(),
|
|
35
|
+
snapshotCount: z.number(),
|
|
36
|
+
snapshots: z.array(topoSnapshotOutput),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { loadApp } from './load-app.js';
|
|
5
|
+
import {
|
|
6
|
+
createIsolatedExampleInput,
|
|
7
|
+
pinCurrentTopoSnapshot,
|
|
8
|
+
topoSnapshotOutput,
|
|
9
|
+
} from './topo-support.js';
|
|
10
|
+
|
|
11
|
+
export const topoPinTrail = trail('topo.pin', {
|
|
12
|
+
blaze: async (input, ctx) => {
|
|
13
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
14
|
+
const app = await loadApp(input.module, rootDir);
|
|
15
|
+
return Result.ok(
|
|
16
|
+
pinCurrentTopoSnapshot(app, { name: input.name, rootDir })
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
description: 'Pin the current topo under a durable name',
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
input: {
|
|
23
|
+
...createIsolatedExampleInput('topo-pin'),
|
|
24
|
+
name: 'before-auth-refactor',
|
|
25
|
+
},
|
|
26
|
+
name: 'Pin the current topo',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
input: z.object({
|
|
30
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
31
|
+
name: z.string().describe('Pin name'),
|
|
32
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
33
|
+
}),
|
|
34
|
+
intent: 'write',
|
|
35
|
+
output: z.object({
|
|
36
|
+
snapshot: topoSnapshotOutput,
|
|
37
|
+
}),
|
|
38
|
+
});
|