@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only topo store consumer helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from topo-support.ts to isolate read-only store consumer helpers,
|
|
5
|
+
* keeping module boundaries clean.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ConflictError,
|
|
13
|
+
createTopoStore,
|
|
14
|
+
InternalError,
|
|
15
|
+
listTopoSnapshots,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
Result,
|
|
18
|
+
} from '@ontrails/core';
|
|
19
|
+
import type { Topo, TopoSnapshot } from '@ontrails/core';
|
|
20
|
+
import {
|
|
21
|
+
deriveTrailsDbPath,
|
|
22
|
+
deriveTrailsDir,
|
|
23
|
+
} from '@ontrails/core/internal/trails-db';
|
|
24
|
+
import { readSurfaceLockData } from '@ontrails/schema';
|
|
25
|
+
|
|
26
|
+
import type { BriefReport, SurveyListReport } from './topo-reports.js';
|
|
27
|
+
import type { TopoSummaryReport, TopoVerifyReport } from './topo-support.js';
|
|
28
|
+
import { REPORT_CONTRACT_VERSION, REPORT_VERSION } from './topo-constants.js';
|
|
29
|
+
import {
|
|
30
|
+
createCurrentTopoSnapshot,
|
|
31
|
+
deriveRootDir,
|
|
32
|
+
LOCK_PATH,
|
|
33
|
+
} from './topo-support.js';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Internal types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface StoredSurfaceMapEntry {
|
|
40
|
+
readonly detours?: readonly {
|
|
41
|
+
readonly on: string;
|
|
42
|
+
readonly maxAttempts: number;
|
|
43
|
+
}[];
|
|
44
|
+
readonly kind: 'resource' | 'signal' | 'trail';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CurrentTrailDetail {
|
|
48
|
+
readonly crosses: string[];
|
|
49
|
+
readonly description: string | null;
|
|
50
|
+
readonly detours:
|
|
51
|
+
| readonly { readonly on: string; readonly maxAttempts: number }[]
|
|
52
|
+
| null;
|
|
53
|
+
readonly examples: unknown[];
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly intent: 'destroy' | 'read' | 'write';
|
|
56
|
+
readonly kind: string;
|
|
57
|
+
readonly pattern: string | null;
|
|
58
|
+
readonly resources: string[];
|
|
59
|
+
readonly safety: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CurrentResourceDetail {
|
|
63
|
+
readonly description: string | null;
|
|
64
|
+
readonly health: 'available' | 'none';
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly kind: 'resource';
|
|
67
|
+
readonly lifetime: 'singleton';
|
|
68
|
+
readonly usedBy: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Store helpers
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const topoStoreRef = (snapshotId: string) => ({ snapshotId }) as const;
|
|
76
|
+
|
|
77
|
+
const hasCommittedLock = (trailsDir: string): boolean =>
|
|
78
|
+
existsSync(join(trailsDir, 'trails.lock'));
|
|
79
|
+
|
|
80
|
+
const readSurfaceEntries = (
|
|
81
|
+
surfaceMapJson: string
|
|
82
|
+
): readonly StoredSurfaceMapEntry[] =>
|
|
83
|
+
(
|
|
84
|
+
JSON.parse(surfaceMapJson) as {
|
|
85
|
+
readonly entries: readonly StoredSurfaceMapEntry[];
|
|
86
|
+
}
|
|
87
|
+
).entries;
|
|
88
|
+
|
|
89
|
+
const buildBriefReportFromStore = (
|
|
90
|
+
app: Topo,
|
|
91
|
+
store: ReturnType<typeof createTopoStore>,
|
|
92
|
+
ref: ReturnType<typeof topoStoreRef>,
|
|
93
|
+
snapshot: TopoSnapshot
|
|
94
|
+
): BriefReport => {
|
|
95
|
+
const trails = store.trails.list({ snapshot: ref });
|
|
96
|
+
const exportRecord = store.exports.get(ref);
|
|
97
|
+
const trailEntries =
|
|
98
|
+
exportRecord === undefined
|
|
99
|
+
? []
|
|
100
|
+
: readSurfaceEntries(exportRecord.surfaceMapJson).filter(
|
|
101
|
+
(entry) => entry.kind === 'trail'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
contractVersion: REPORT_CONTRACT_VERSION,
|
|
106
|
+
features: {
|
|
107
|
+
detours: trailEntries.some((entry) => (entry.detours ?? []).length > 0),
|
|
108
|
+
examples: trails.some((trail) => trail.hasExamples),
|
|
109
|
+
outputSchemas: trails.some((trail) => trail.hasOutput),
|
|
110
|
+
resources: snapshot.resourceCount > 0,
|
|
111
|
+
signals: snapshot.signalCount > 0,
|
|
112
|
+
},
|
|
113
|
+
name: app.name,
|
|
114
|
+
resources: snapshot.resourceCount,
|
|
115
|
+
signals: snapshot.signalCount,
|
|
116
|
+
trails: snapshot.trailCount,
|
|
117
|
+
version: REPORT_VERSION,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const buildSurveyListFromStore = (
|
|
122
|
+
store: ReturnType<typeof createTopoStore>,
|
|
123
|
+
ref: ReturnType<typeof topoStoreRef>
|
|
124
|
+
): SurveyListReport => {
|
|
125
|
+
const trails = store.trails.list({ snapshot: ref });
|
|
126
|
+
const resources = store.resources.list({ snapshot: ref });
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
count: trails.length,
|
|
130
|
+
entries: trails.map((trail) => ({
|
|
131
|
+
examples: trail.exampleCount,
|
|
132
|
+
id: trail.id,
|
|
133
|
+
kind: trail.kind,
|
|
134
|
+
safety: trail.safety,
|
|
135
|
+
})),
|
|
136
|
+
resourceCount: resources.length,
|
|
137
|
+
resources: resources.map((resource) => ({
|
|
138
|
+
description: resource.description,
|
|
139
|
+
health: resource.health,
|
|
140
|
+
id: resource.id,
|
|
141
|
+
kind: resource.kind,
|
|
142
|
+
lifetime: resource.lifetime,
|
|
143
|
+
usedBy: resource.usedBy,
|
|
144
|
+
})),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const buildTrailDetailFromStore = (
|
|
149
|
+
detail: NonNullable<
|
|
150
|
+
ReturnType<ReturnType<typeof createTopoStore>['trails']['get']>
|
|
151
|
+
>
|
|
152
|
+
): CurrentTrailDetail => ({
|
|
153
|
+
crosses: [...detail.crosses],
|
|
154
|
+
description: detail.description,
|
|
155
|
+
detours: detail.detours,
|
|
156
|
+
examples: [...detail.examples],
|
|
157
|
+
id: detail.id,
|
|
158
|
+
intent: detail.intent,
|
|
159
|
+
kind: detail.kind,
|
|
160
|
+
pattern: detail.pattern,
|
|
161
|
+
resources: [...detail.resources],
|
|
162
|
+
safety: detail.safety,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const buildResourceDetailFromStore = (
|
|
166
|
+
resource: NonNullable<
|
|
167
|
+
ReturnType<ReturnType<typeof createTopoStore>['resources']['get']>
|
|
168
|
+
>
|
|
169
|
+
): CurrentResourceDetail => ({
|
|
170
|
+
description: resource.description,
|
|
171
|
+
health: resource.health,
|
|
172
|
+
id: resource.id,
|
|
173
|
+
kind: resource.kind,
|
|
174
|
+
lifetime: resource.lifetime,
|
|
175
|
+
usedBy: [...resource.usedBy],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// withCurrentTopoStore
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run a read callback against the latest topo store state.
|
|
184
|
+
*
|
|
185
|
+
* Uses the most recent existing snapshot when available, only creating a new
|
|
186
|
+
* snapshot when no prior snapshot exists. This avoids unbounded snapshot
|
|
187
|
+
* accumulation from
|
|
188
|
+
* read-only operations like survey, guide, and show.
|
|
189
|
+
*/
|
|
190
|
+
const withCurrentTopoStore = <T>(
|
|
191
|
+
app: Topo,
|
|
192
|
+
rootDir: string,
|
|
193
|
+
read: (
|
|
194
|
+
store: ReturnType<typeof createTopoStore>,
|
|
195
|
+
ref: ReturnType<typeof topoStoreRef>,
|
|
196
|
+
snapshot: TopoSnapshot
|
|
197
|
+
) => T
|
|
198
|
+
): T => {
|
|
199
|
+
const [existingSnapshot] = listTopoSnapshots({ limit: 1, rootDir });
|
|
200
|
+
const snapshot =
|
|
201
|
+
existingSnapshot ?? createCurrentTopoSnapshot(app, { rootDir });
|
|
202
|
+
const store = createTopoStore({ rootDir });
|
|
203
|
+
return read(store, topoStoreRef(snapshot.id), snapshot);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Public read-only consumers
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export const buildTopoSummary = (
|
|
211
|
+
app: Topo,
|
|
212
|
+
options?: { readonly rootDir?: string }
|
|
213
|
+
): TopoSummaryReport => {
|
|
214
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
215
|
+
const trailsDir = deriveTrailsDir({ rootDir });
|
|
216
|
+
return withCurrentTopoStore(app, rootDir, (store, ref, snapshot) => ({
|
|
217
|
+
app: buildBriefReportFromStore(app, store, ref, snapshot),
|
|
218
|
+
dbPath: deriveTrailsDbPath({ rootDir }),
|
|
219
|
+
list: buildSurveyListFromStore(store, ref),
|
|
220
|
+
lockExists: hasCommittedLock(trailsDir),
|
|
221
|
+
lockPath: LOCK_PATH,
|
|
222
|
+
}));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const buildCurrentTopoBrief = (
|
|
226
|
+
app: Topo,
|
|
227
|
+
options?: { readonly rootDir?: string }
|
|
228
|
+
): BriefReport => {
|
|
229
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
230
|
+
return withCurrentTopoStore(app, rootDir, (store, ref, snapshot) =>
|
|
231
|
+
buildBriefReportFromStore(app, store, ref, snapshot)
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const buildCurrentTopoList = (
|
|
236
|
+
app: Topo,
|
|
237
|
+
options?: { readonly rootDir?: string }
|
|
238
|
+
): SurveyListReport => {
|
|
239
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
240
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) =>
|
|
241
|
+
buildSurveyListFromStore(store, ref)
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const buildCurrentGuideEntries = (
|
|
246
|
+
app: Topo,
|
|
247
|
+
options?: { readonly rootDir?: string }
|
|
248
|
+
): readonly {
|
|
249
|
+
readonly description: string;
|
|
250
|
+
readonly exampleCount: number;
|
|
251
|
+
readonly id: string;
|
|
252
|
+
readonly kind: string;
|
|
253
|
+
}[] => {
|
|
254
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
255
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) =>
|
|
256
|
+
store.trails.list({ snapshot: ref }).map((trail) => ({
|
|
257
|
+
description: trail.description ?? '(no description)',
|
|
258
|
+
exampleCount: trail.exampleCount,
|
|
259
|
+
id: trail.id,
|
|
260
|
+
kind: trail.kind,
|
|
261
|
+
}))
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const buildCurrentTopoDetail = (
|
|
266
|
+
app: Topo,
|
|
267
|
+
id: string,
|
|
268
|
+
options?: { readonly rootDir?: string }
|
|
269
|
+
): CurrentResourceDetail | CurrentTrailDetail | undefined => {
|
|
270
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
271
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) => {
|
|
272
|
+
const trail = store.trails.get(id, { snapshot: ref });
|
|
273
|
+
if (trail !== undefined) {
|
|
274
|
+
return buildTrailDetailFromStore(trail);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const resource = store.resources.get(id, { snapshot: ref });
|
|
278
|
+
return resource === undefined
|
|
279
|
+
? undefined
|
|
280
|
+
: buildResourceDetailFromStore(resource);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export const verifyCurrentTopo = async (
|
|
285
|
+
app: Topo,
|
|
286
|
+
options?: { readonly rootDir?: string }
|
|
287
|
+
): Promise<Result<TopoVerifyReport, Error>> => {
|
|
288
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
289
|
+
const committedLock = await readSurfaceLockData({
|
|
290
|
+
dir: deriveTrailsDir({ rootDir }),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (committedLock === null) {
|
|
294
|
+
return Result.err(
|
|
295
|
+
new NotFoundError(
|
|
296
|
+
'No committed trails.lock found. Run `trails topo export` first.'
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const currentHash = withCurrentTopoStore(
|
|
302
|
+
app,
|
|
303
|
+
rootDir,
|
|
304
|
+
(store, ref) => store.exports.get(ref)?.surfaceHash
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (currentHash === undefined) {
|
|
308
|
+
return Result.err(
|
|
309
|
+
new InternalError(
|
|
310
|
+
'No stored topo export found for the current topo snapshot'
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (committedLock.hash !== currentHash) {
|
|
316
|
+
return Result.err(
|
|
317
|
+
new ConflictError(
|
|
318
|
+
'trails.lock is stale. Run `trails topo export` to refresh it.'
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return Result.ok({
|
|
324
|
+
committedHash: committedLock.hash,
|
|
325
|
+
currentHash,
|
|
326
|
+
lockPath: LOCK_PATH,
|
|
327
|
+
stale: false,
|
|
328
|
+
});
|
|
329
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Topo, Trail } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { REPORT_CONTRACT_VERSION, REPORT_VERSION } from './topo-constants.js';
|
|
4
|
+
|
|
5
|
+
export interface BriefReport {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly version: string;
|
|
8
|
+
readonly contractVersion: string;
|
|
9
|
+
readonly features: {
|
|
10
|
+
readonly resources: boolean;
|
|
11
|
+
readonly outputSchemas: boolean;
|
|
12
|
+
readonly examples: boolean;
|
|
13
|
+
readonly detours: boolean;
|
|
14
|
+
readonly signals: boolean;
|
|
15
|
+
};
|
|
16
|
+
readonly trails: number;
|
|
17
|
+
readonly signals: number;
|
|
18
|
+
readonly resources: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SurveyListReport {
|
|
22
|
+
readonly count: number;
|
|
23
|
+
readonly entries: readonly {
|
|
24
|
+
readonly examples: number;
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly safety: string;
|
|
28
|
+
}[];
|
|
29
|
+
readonly resourceCount: number;
|
|
30
|
+
readonly resources: readonly {
|
|
31
|
+
readonly description: string | null;
|
|
32
|
+
readonly health: 'available' | 'none';
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly kind: 'resource';
|
|
35
|
+
readonly lifetime: 'singleton';
|
|
36
|
+
readonly usedBy: readonly string[];
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TrailDetailReport {
|
|
41
|
+
readonly description: string | null;
|
|
42
|
+
readonly detours:
|
|
43
|
+
| readonly { readonly on: string; readonly maxAttempts: number }[]
|
|
44
|
+
| null;
|
|
45
|
+
readonly examples: readonly unknown[];
|
|
46
|
+
readonly crosses: readonly string[];
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly intent: 'read' | 'write' | 'destroy';
|
|
49
|
+
readonly kind: string;
|
|
50
|
+
readonly pattern: string | null;
|
|
51
|
+
readonly safety: string;
|
|
52
|
+
readonly resources: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
|
|
56
|
+
if (key === 'examples' || key === 'detours') {
|
|
57
|
+
return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
|
|
58
|
+
}
|
|
59
|
+
return Boolean(raw[key]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const detectFeatures = (
|
|
63
|
+
app: Topo
|
|
64
|
+
): {
|
|
65
|
+
hasDetours: boolean;
|
|
66
|
+
hasExamples: boolean;
|
|
67
|
+
hasOutputSchemas: boolean;
|
|
68
|
+
hasResources: boolean;
|
|
69
|
+
} => {
|
|
70
|
+
const trails = [...app.trails.values()].map(
|
|
71
|
+
(item) => item as unknown as Record<string, unknown>
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
hasDetours: trails.some((r) => trailHas(r, 'detours')),
|
|
75
|
+
hasExamples: trails.some((r) => trailHas(r, 'examples')),
|
|
76
|
+
hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
|
|
77
|
+
hasResources: trails.some(
|
|
78
|
+
(r) =>
|
|
79
|
+
Array.isArray(r['resources']) &&
|
|
80
|
+
(r['resources'] as unknown[]).length > 0
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const deriveBriefReport = (app: Topo): BriefReport => {
|
|
86
|
+
const { hasDetours, hasExamples, hasOutputSchemas, hasResources } =
|
|
87
|
+
detectFeatures(app);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
contractVersion: REPORT_CONTRACT_VERSION,
|
|
91
|
+
features: {
|
|
92
|
+
detours: hasDetours,
|
|
93
|
+
examples: hasExamples,
|
|
94
|
+
outputSchemas: hasOutputSchemas,
|
|
95
|
+
resources: hasResources,
|
|
96
|
+
signals: app.signals.size > 0,
|
|
97
|
+
},
|
|
98
|
+
name: app.name,
|
|
99
|
+
resources: app.resources.size,
|
|
100
|
+
signals: app.signals.size,
|
|
101
|
+
trails: app.trails.size,
|
|
102
|
+
version: REPORT_VERSION,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const safetyLabel = (entry: {
|
|
107
|
+
intent?: 'read' | 'write' | 'destroy';
|
|
108
|
+
}): string => {
|
|
109
|
+
if (entry.intent === 'destroy') {
|
|
110
|
+
return 'destroy';
|
|
111
|
+
}
|
|
112
|
+
if (entry.intent === 'write') {
|
|
113
|
+
return 'write';
|
|
114
|
+
}
|
|
115
|
+
if (entry.intent === 'read') {
|
|
116
|
+
return 'read';
|
|
117
|
+
}
|
|
118
|
+
return '-';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const buildResourceUsage = (
|
|
122
|
+
app: Topo
|
|
123
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
124
|
+
const usage = new Map<string, string[]>();
|
|
125
|
+
|
|
126
|
+
for (const trailDef of app.list()) {
|
|
127
|
+
for (const declaredResource of trailDef.resources) {
|
|
128
|
+
const users = usage.get(declaredResource.id) ?? [];
|
|
129
|
+
users.push(trailDef.id);
|
|
130
|
+
usage.set(declaredResource.id, users);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new Map(
|
|
135
|
+
[...usage.entries()].map(([id, users]) => [id, users.toSorted()] as const)
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const resourceHealthStatus = (resource: {
|
|
140
|
+
health?: unknown;
|
|
141
|
+
}): 'available' | 'none' =>
|
|
142
|
+
resource.health === undefined ? 'none' : 'available';
|
|
143
|
+
|
|
144
|
+
export const deriveResourceDetail = (app: Topo, resourceId: string): object => {
|
|
145
|
+
const item = app.getResource(resourceId);
|
|
146
|
+
const usedBy = buildResourceUsage(app).get(resourceId) ?? [];
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
description: item?.description ?? null,
|
|
150
|
+
health: item ? resourceHealthStatus(item) : 'none',
|
|
151
|
+
id: resourceId,
|
|
152
|
+
kind: 'resource',
|
|
153
|
+
lifetime: 'singleton',
|
|
154
|
+
usedBy,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const formatResourceList = (app: Topo): SurveyListReport['resources'] => {
|
|
159
|
+
const usage = buildResourceUsage(app);
|
|
160
|
+
return app
|
|
161
|
+
.listResources()
|
|
162
|
+
.map((resource) => ({
|
|
163
|
+
description: resource.description ?? null,
|
|
164
|
+
health: resourceHealthStatus(resource),
|
|
165
|
+
id: resource.id,
|
|
166
|
+
kind: resource.kind,
|
|
167
|
+
lifetime: 'singleton' as const,
|
|
168
|
+
usedBy: usage.get(resource.id) ?? [],
|
|
169
|
+
}))
|
|
170
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const deriveSurveyList = (app: Topo): SurveyListReport => {
|
|
174
|
+
const items = app.list();
|
|
175
|
+
const entries = items.map((item) => {
|
|
176
|
+
const safety = safetyLabel(
|
|
177
|
+
item as unknown as { intent?: 'read' | 'write' | 'destroy' }
|
|
178
|
+
);
|
|
179
|
+
const examples = Array.isArray(
|
|
180
|
+
(item as unknown as { examples?: unknown[] }).examples
|
|
181
|
+
)
|
|
182
|
+
? (item as unknown as { examples: unknown[] }).examples.length
|
|
183
|
+
: 0;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
examples,
|
|
187
|
+
id: item.id,
|
|
188
|
+
kind: item.kind,
|
|
189
|
+
safety,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const resources = formatResourceList(app);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
count: items.length,
|
|
197
|
+
entries,
|
|
198
|
+
resourceCount: resources.length,
|
|
199
|
+
resources,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const deriveTrailDetail = (
|
|
204
|
+
item: Trail<unknown, unknown>
|
|
205
|
+
): TrailDetailReport => {
|
|
206
|
+
const safety = safetyLabel(
|
|
207
|
+
item as unknown as { intent?: 'read' | 'write' | 'destroy' }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
crosses: item.crosses.toSorted(),
|
|
212
|
+
description: item.description ?? null,
|
|
213
|
+
detours:
|
|
214
|
+
item.detours.length > 0
|
|
215
|
+
? item.detours.map((d) => ({
|
|
216
|
+
maxAttempts: d.maxAttempts ?? 1,
|
|
217
|
+
on: d.on.name,
|
|
218
|
+
}))
|
|
219
|
+
: null,
|
|
220
|
+
examples: item.examples ?? [],
|
|
221
|
+
id: item.id,
|
|
222
|
+
intent: item.intent,
|
|
223
|
+
kind: item.kind,
|
|
224
|
+
pattern: item.pattern ?? null,
|
|
225
|
+
resources: item.resources.map((resource) => resource.id).toSorted(),
|
|
226
|
+
safety,
|
|
227
|
+
};
|
|
228
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { loadApp } from './load-app.js';
|
|
5
|
+
import { buildCurrentTopoDetail } from './topo-read-support.js';
|
|
6
|
+
|
|
7
|
+
const trailDetailOutput = z.object({
|
|
8
|
+
crosses: z.array(z.string()),
|
|
9
|
+
description: z.unknown().nullable(),
|
|
10
|
+
detours: z.unknown().nullable(),
|
|
11
|
+
examples: z.array(z.unknown()),
|
|
12
|
+
id: z.string(),
|
|
13
|
+
intent: z.enum(['read', 'write', 'destroy']),
|
|
14
|
+
kind: z.string(),
|
|
15
|
+
resources: z.array(z.string()),
|
|
16
|
+
safety: z.string(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const resourceDetailOutput = z.object({
|
|
20
|
+
description: z.string().nullable(),
|
|
21
|
+
health: z.enum(['available', 'none']),
|
|
22
|
+
id: z.string(),
|
|
23
|
+
kind: z.literal('resource'),
|
|
24
|
+
lifetime: z.literal('singleton'),
|
|
25
|
+
usedBy: z.array(z.string()),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const topoShowTrail = trail('topo.show', {
|
|
29
|
+
blaze: async (input, ctx) => {
|
|
30
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
31
|
+
const app = await loadApp(input.module, rootDir);
|
|
32
|
+
const detail = buildCurrentTopoDetail(app, input.id, { rootDir });
|
|
33
|
+
if (detail !== undefined) {
|
|
34
|
+
return Result.ok(detail);
|
|
35
|
+
}
|
|
36
|
+
return Result.err(
|
|
37
|
+
new NotFoundError(`Trail or resource not found: ${input.id}`)
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
description: 'Show detail for a current trail or resource',
|
|
41
|
+
examples: [
|
|
42
|
+
{
|
|
43
|
+
input: { id: 'topo' },
|
|
44
|
+
name: 'Show current trail detail',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
input: z.object({
|
|
48
|
+
id: z.string().describe('Trail or resource ID to inspect'),
|
|
49
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
50
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
51
|
+
}),
|
|
52
|
+
intent: 'read',
|
|
53
|
+
output: z.union([trailDetailOutput, resourceDetailOutput]),
|
|
54
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
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 type { Topo, TopoSnapshot } from '@ontrails/core';
|
|
9
|
+
import { InternalError, Result } from '@ontrails/core';
|
|
10
|
+
import type { StoredTopoExport } from '@ontrails/core/internal/topo-store';
|
|
11
|
+
import {
|
|
12
|
+
createTopoSnapshot,
|
|
13
|
+
getStoredTopoExport,
|
|
14
|
+
} from '@ontrails/core/internal/topo-store';
|
|
15
|
+
import {
|
|
16
|
+
openWriteTrailsDb,
|
|
17
|
+
deriveTrailsDir,
|
|
18
|
+
} from '@ontrails/core/internal/trails-db';
|
|
19
|
+
import type { SurfaceLock, SurfaceMap } from '@ontrails/schema';
|
|
20
|
+
import { writeSurfaceLock, writeSurfaceMap } from '@ontrails/schema';
|
|
21
|
+
|
|
22
|
+
import type { TopoExportReport } from './topo-support.js';
|
|
23
|
+
import {
|
|
24
|
+
deriveRootDir,
|
|
25
|
+
deriveTopoCounts,
|
|
26
|
+
readGitState,
|
|
27
|
+
} from './topo-support.js';
|
|
28
|
+
|
|
29
|
+
const persistAndReadStoredExport = (
|
|
30
|
+
app: Topo,
|
|
31
|
+
db: ReturnType<typeof openWriteTrailsDb>,
|
|
32
|
+
rootDir: string
|
|
33
|
+
): Result<
|
|
34
|
+
{ snapshot: TopoSnapshot; storedExport: StoredTopoExport },
|
|
35
|
+
Error
|
|
36
|
+
> => {
|
|
37
|
+
const snapshotResult = createTopoSnapshot(db, app, {
|
|
38
|
+
...readGitState(rootDir),
|
|
39
|
+
...deriveTopoCounts(app),
|
|
40
|
+
});
|
|
41
|
+
if (snapshotResult.isErr()) {
|
|
42
|
+
return snapshotResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const snapshot = snapshotResult.value;
|
|
46
|
+
const storedExport = getStoredTopoExport(db, snapshot.id);
|
|
47
|
+
|
|
48
|
+
if (storedExport === undefined) {
|
|
49
|
+
return Result.err(
|
|
50
|
+
new InternalError(
|
|
51
|
+
`Missing stored topo export for snapshot "${snapshot.id}"`
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Result.ok({
|
|
57
|
+
snapshot,
|
|
58
|
+
storedExport,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const writeStoredExportArtifacts = async (
|
|
63
|
+
storedExport: StoredTopoExport,
|
|
64
|
+
trailsDir: string
|
|
65
|
+
): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'mapPath'>> => {
|
|
66
|
+
const mapPath = await writeSurfaceMap(
|
|
67
|
+
JSON.parse(storedExport.surfaceMapJson) as SurfaceMap,
|
|
68
|
+
{ dir: trailsDir }
|
|
69
|
+
);
|
|
70
|
+
const lockPath = await writeSurfaceLock(
|
|
71
|
+
JSON.parse(storedExport.lockContent) as SurfaceLock,
|
|
72
|
+
{ dir: trailsDir }
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
hash: storedExport.surfaceHash,
|
|
77
|
+
lockPath,
|
|
78
|
+
mapPath,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const exportCurrentTopo = async (
|
|
83
|
+
app: Topo,
|
|
84
|
+
options?: { readonly rootDir?: string }
|
|
85
|
+
): Promise<Result<TopoExportReport, Error>> => {
|
|
86
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
87
|
+
const db = openWriteTrailsDb({ rootDir });
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const persisted = persistAndReadStoredExport(app, db, rootDir);
|
|
91
|
+
if (persisted.isErr()) {
|
|
92
|
+
return persisted;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { snapshot, storedExport } = persisted.value;
|
|
96
|
+
const artifacts = await writeStoredExportArtifacts(
|
|
97
|
+
storedExport,
|
|
98
|
+
deriveTrailsDir({ rootDir })
|
|
99
|
+
);
|
|
100
|
+
return Result.ok({ ...artifacts, snapshot });
|
|
101
|
+
} finally {
|
|
102
|
+
db.close();
|
|
103
|
+
}
|
|
104
|
+
};
|