@ontrails/trails 1.0.0-beta.13 → 1.0.0-beta.14
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 +16 -0
- package/__tests__/examples.test.ts +14 -0
- package/dist/src/app.d.ts.map +1 -1
- package/dist/src/app.js +13 -2
- package/dist/src/app.js.map +1 -1
- package/dist/src/clack.d.ts +1 -1
- package/dist/src/clack.js +1 -1
- package/dist/src/cli.js +2 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/trails/add-trail.js +13 -13
- 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.js +10 -10
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.js +26 -26
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +6 -6
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +29 -29
- 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 +65 -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 +38 -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 +178 -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 +386 -0
- package/dist/src/trails/draft-promote.js.map +1 -0
- package/dist/src/trails/guide.d.ts +13 -3
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +21 -37
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +3 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +53 -10
- 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 +4 -58
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +52 -173
- 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 +18 -0
- package/dist/src/trails/topo-export.d.ts.map +1 -0
- package/dist/src/trails/topo-export.js +34 -0
- package/dist/src/trails/topo-export.js.map +1 -0
- package/dist/src/trails/topo-history.d.ts +24 -0
- package/dist/src/trails/topo-history.d.ts.map +1 -0
- package/dist/src/trails/topo-history.js +33 -0
- package/dist/src/trails/topo-history.js.map +1 -0
- package/dist/src/trails/topo-pin.d.ts +21 -0
- package/dist/src/trails/topo-pin.d.ts.map +1 -0
- package/dist/src/trails/topo-pin.js +35 -0
- package/dist/src/trails/topo-pin.js.map +1 -0
- package/dist/src/trails/topo-read-support.d.ts +54 -0
- package/dist/src/trails/topo-read-support.d.ts.map +1 -0
- package/dist/src/trails/topo-read-support.js +178 -0
- package/dist/src/trails/topo-read-support.js.map +1 -0
- package/dist/src/trails/topo-reports.d.ts +50 -0
- package/dist/src/trails/topo-reports.d.ts.map +1 -0
- package/dist/src/trails/topo-reports.js +122 -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 +53 -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 +87 -0
- package/dist/src/trails/topo-support.d.ts.map +1 -0
- package/dist/src/trails/topo-support.js +165 -0
- package/dist/src/trails/topo-support.js.map +1 -0
- package/dist/src/trails/topo-unpin.d.ts +15 -0
- package/dist/src/trails/topo-unpin.d.ts.map +1 -0
- package/dist/src/trails/topo-unpin.js +39 -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 +28 -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 +67 -0
- package/dist/src/trails/topo.js.map +1 -0
- package/dist/src/trails/warden.d.ts +1 -1
- package/dist/src/trails/warden.d.ts.map +1 -1
- package/dist/src/trails/warden.js +28 -27
- package/dist/src/trails/warden.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/draft-promote.test.ts +144 -0
- package/src/__tests__/load-app.test.ts +43 -0
- package/src/__tests__/survey.test.ts +85 -0
- package/src/__tests__/topo-dev.test.ts +424 -0
- package/src/app.ts +22 -0
- package/src/trails/dev-clean.ts +73 -0
- package/src/trails/dev-reset.ts +44 -0
- package/src/trails/dev-stats.ts +64 -0
- package/src/trails/dev-support.ts +326 -0
- package/src/trails/draft-promote.ts +704 -0
- package/src/trails/guide.ts +22 -37
- package/src/trails/load-app.ts +76 -13
- package/src/trails/project.ts +17 -3
- package/src/trails/survey.ts +56 -256
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-export.ts +39 -0
- package/src/trails/topo-history.ts +40 -0
- package/src/trails/topo-pin.ts +42 -0
- package/src/trails/topo-read-support.ts +332 -0
- package/src/trails/topo-reports.ts +221 -0
- package/src/trails/topo-show.ts +58 -0
- package/src/trails/topo-store-support.ts +96 -0
- package/src/trails/topo-support.ts +274 -0
- package/src/trails/topo-unpin.ts +51 -0
- package/src/trails/topo-verify.ts +29 -0
- package/src/trails/topo.ts +73 -0
- package/src/trails/warden.ts +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { loadApp } from './load-app.js';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_APP_MODULE,
|
|
7
|
+
isolatedExampleInput,
|
|
8
|
+
pinCurrentTopo,
|
|
9
|
+
topoPinOutput,
|
|
10
|
+
topoSaveOutput,
|
|
11
|
+
} from './topo-support.js';
|
|
12
|
+
|
|
13
|
+
export const topoPinTrail = trail('topo.pin', {
|
|
14
|
+
blaze: async (input, ctx) => {
|
|
15
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
16
|
+
const app = await loadApp(input.module, rootDir);
|
|
17
|
+
return Result.ok(pinCurrentTopo(app, { name: input.name, rootDir }));
|
|
18
|
+
},
|
|
19
|
+
description: 'Pin the current topo under a durable name',
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
input: {
|
|
23
|
+
...isolatedExampleInput('topo-pin'),
|
|
24
|
+
name: 'before-auth-refactor',
|
|
25
|
+
},
|
|
26
|
+
name: 'Pin the current topo',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
input: z.object({
|
|
30
|
+
module: z
|
|
31
|
+
.string()
|
|
32
|
+
.default(DEFAULT_APP_MODULE)
|
|
33
|
+
.describe('Path to the app module'),
|
|
34
|
+
name: z.string().describe('Pin name'),
|
|
35
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
36
|
+
}),
|
|
37
|
+
intent: 'write',
|
|
38
|
+
output: z.object({
|
|
39
|
+
pin: topoPinOutput,
|
|
40
|
+
save: topoSaveOutput,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only topo store consumer helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from topo-support.ts so this branch (trl-132) owns its own file,
|
|
5
|
+
* keeping absorb routing clean across the stack.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import type { Topo } from '@ontrails/core';
|
|
12
|
+
import {
|
|
13
|
+
ConflictError,
|
|
14
|
+
createTopoStore,
|
|
15
|
+
InternalError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
Result,
|
|
18
|
+
} from '@ontrails/core';
|
|
19
|
+
import type { TopoSaveRecord } from '@ontrails/core/internal/topo-saves';
|
|
20
|
+
import { listTopoSaves } from '@ontrails/core/internal/topo-saves';
|
|
21
|
+
import {
|
|
22
|
+
openReadTrailsDb,
|
|
23
|
+
resolveTrailsDbPath,
|
|
24
|
+
resolveTrailsDir,
|
|
25
|
+
} from '@ontrails/core/internal/trails-db';
|
|
26
|
+
import { readTrailheadLockData } from '@ontrails/schema';
|
|
27
|
+
|
|
28
|
+
import type { BriefReport, SurveyListReport } from './topo-reports.js';
|
|
29
|
+
import type { TopoSummaryReport, TopoVerifyReport } from './topo-support.js';
|
|
30
|
+
import { REPORT_CONTRACT_VERSION, REPORT_VERSION } from './topo-constants.js';
|
|
31
|
+
import {
|
|
32
|
+
createCurrentTopoSave,
|
|
33
|
+
LOCK_PATH,
|
|
34
|
+
resolveRootDir,
|
|
35
|
+
} from './topo-support.js';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Internal types
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
interface StoredTrailheadMapEntry {
|
|
42
|
+
readonly detours?: Readonly<Record<string, readonly string[]>>;
|
|
43
|
+
readonly kind: 'provision' | 'signal' | 'trail';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CurrentTrailDetail {
|
|
47
|
+
readonly crosses: string[];
|
|
48
|
+
readonly description: string | null;
|
|
49
|
+
readonly detours: Readonly<Record<string, readonly string[]>> | null;
|
|
50
|
+
readonly examples: unknown[];
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly intent: 'destroy' | 'read' | 'write';
|
|
53
|
+
readonly kind: string;
|
|
54
|
+
readonly provisions: string[];
|
|
55
|
+
readonly safety: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CurrentProvisionDetail {
|
|
59
|
+
readonly description: string | null;
|
|
60
|
+
readonly health: 'available' | 'none';
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly kind: 'provision';
|
|
63
|
+
readonly lifetime: 'singleton';
|
|
64
|
+
readonly usedBy: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Store helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const topoStoreRef = (saveId: string) => ({ saveId }) as const;
|
|
72
|
+
|
|
73
|
+
const hasCommittedLock = (trailsDir: string): boolean =>
|
|
74
|
+
existsSync(join(trailsDir, 'trails.lock')) ||
|
|
75
|
+
existsSync(join(trailsDir, 'trailhead.lock'));
|
|
76
|
+
|
|
77
|
+
const readTrailheadEntries = (
|
|
78
|
+
trailheadMapJson: string
|
|
79
|
+
): readonly StoredTrailheadMapEntry[] =>
|
|
80
|
+
(
|
|
81
|
+
JSON.parse(trailheadMapJson) as {
|
|
82
|
+
readonly entries: readonly StoredTrailheadMapEntry[];
|
|
83
|
+
}
|
|
84
|
+
).entries;
|
|
85
|
+
|
|
86
|
+
const buildBriefReportFromStore = (
|
|
87
|
+
app: Topo,
|
|
88
|
+
store: ReturnType<typeof createTopoStore>,
|
|
89
|
+
ref: ReturnType<typeof topoStoreRef>,
|
|
90
|
+
save: TopoSaveRecord
|
|
91
|
+
): BriefReport => {
|
|
92
|
+
const trails = store.trails.list({ save: ref });
|
|
93
|
+
const exportRecord = store.exports.get(ref);
|
|
94
|
+
const trailEntries =
|
|
95
|
+
exportRecord === undefined
|
|
96
|
+
? []
|
|
97
|
+
: readTrailheadEntries(exportRecord.trailheadMapJson).filter(
|
|
98
|
+
(entry) => entry.kind === 'trail'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
contractVersion: REPORT_CONTRACT_VERSION,
|
|
103
|
+
features: {
|
|
104
|
+
detours: trailEntries.some(
|
|
105
|
+
(entry) => (Object.keys(entry.detours ?? {}).length ?? 0) > 0
|
|
106
|
+
),
|
|
107
|
+
examples: trails.some((trail) => trail.hasExamples),
|
|
108
|
+
outputSchemas: trails.some((trail) => trail.hasOutput),
|
|
109
|
+
provisions: save.provisionCount > 0,
|
|
110
|
+
signals: save.signalCount > 0,
|
|
111
|
+
},
|
|
112
|
+
name: app.name,
|
|
113
|
+
provisions: save.provisionCount,
|
|
114
|
+
signals: save.signalCount,
|
|
115
|
+
trails: save.trailCount,
|
|
116
|
+
version: REPORT_VERSION,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const buildSurveyListFromStore = (
|
|
121
|
+
store: ReturnType<typeof createTopoStore>,
|
|
122
|
+
ref: ReturnType<typeof topoStoreRef>
|
|
123
|
+
): SurveyListReport => {
|
|
124
|
+
const trails = store.trails.list({ save: ref });
|
|
125
|
+
const provisions = store.provisions.list({ save: ref });
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
count: trails.length,
|
|
129
|
+
entries: trails.map((trail) => ({
|
|
130
|
+
examples: trail.exampleCount,
|
|
131
|
+
id: trail.id,
|
|
132
|
+
kind: trail.kind,
|
|
133
|
+
safety: trail.safety,
|
|
134
|
+
})),
|
|
135
|
+
provisionCount: provisions.length,
|
|
136
|
+
provisions: provisions.map((provision) => ({
|
|
137
|
+
description: provision.description,
|
|
138
|
+
health: provision.health,
|
|
139
|
+
id: provision.id,
|
|
140
|
+
kind: provision.kind,
|
|
141
|
+
lifetime: provision.lifetime,
|
|
142
|
+
usedBy: provision.usedBy,
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const buildTrailDetailFromStore = (
|
|
148
|
+
detail: ReturnType<ReturnType<typeof createTopoStore>['trails']['get']>
|
|
149
|
+
): CurrentTrailDetail => ({
|
|
150
|
+
crosses: [...(detail?.crosses ?? [])],
|
|
151
|
+
description: detail?.description ?? null,
|
|
152
|
+
detours: detail?.detours ?? null,
|
|
153
|
+
examples: [...(detail?.examples ?? [])],
|
|
154
|
+
id: detail?.id ?? '',
|
|
155
|
+
intent: detail?.intent ?? 'write',
|
|
156
|
+
kind: detail?.kind ?? 'trail',
|
|
157
|
+
provisions: [...(detail?.provisions ?? [])],
|
|
158
|
+
safety: detail?.safety ?? '-',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const buildProvisionDetailFromStore = (
|
|
162
|
+
provision: NonNullable<
|
|
163
|
+
ReturnType<ReturnType<typeof createTopoStore>['provisions']['get']>
|
|
164
|
+
>
|
|
165
|
+
): CurrentProvisionDetail => ({
|
|
166
|
+
description: provision.description,
|
|
167
|
+
health: provision.health,
|
|
168
|
+
id: provision.id,
|
|
169
|
+
kind: provision.kind,
|
|
170
|
+
lifetime: provision.lifetime,
|
|
171
|
+
usedBy: [...provision.usedBy],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// withCurrentTopoStore
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run a read callback against the latest topo store state.
|
|
180
|
+
*
|
|
181
|
+
* Uses the most recent existing save when available, only creating a new save
|
|
182
|
+
* when no prior save exists. This avoids unbounded save accumulation from
|
|
183
|
+
* read-only operations like survey, guide, and show.
|
|
184
|
+
*/
|
|
185
|
+
const withCurrentTopoStore = <T>(
|
|
186
|
+
app: Topo,
|
|
187
|
+
rootDir: string,
|
|
188
|
+
read: (
|
|
189
|
+
store: ReturnType<typeof createTopoStore>,
|
|
190
|
+
ref: ReturnType<typeof topoStoreRef>,
|
|
191
|
+
save: TopoSaveRecord
|
|
192
|
+
) => T
|
|
193
|
+
): T => {
|
|
194
|
+
const dbPath = resolveTrailsDbPath({ rootDir });
|
|
195
|
+
const existingSave = existsSync(dbPath)
|
|
196
|
+
? (() => {
|
|
197
|
+
const db = openReadTrailsDb({ rootDir });
|
|
198
|
+
try {
|
|
199
|
+
return listTopoSaves(db)[0];
|
|
200
|
+
} finally {
|
|
201
|
+
db.close();
|
|
202
|
+
}
|
|
203
|
+
})()
|
|
204
|
+
: undefined;
|
|
205
|
+
|
|
206
|
+
const save = existingSave ?? createCurrentTopoSave(app, { rootDir });
|
|
207
|
+
const store = createTopoStore({ rootDir });
|
|
208
|
+
return read(store, topoStoreRef(save.id), save);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Public read-only consumers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
export const buildTopoSummary = (
|
|
216
|
+
app: Topo,
|
|
217
|
+
options?: { readonly rootDir?: string }
|
|
218
|
+
): TopoSummaryReport => {
|
|
219
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
220
|
+
const trailsDir = resolveTrailsDir({ rootDir });
|
|
221
|
+
return withCurrentTopoStore(app, rootDir, (store, ref, save) => ({
|
|
222
|
+
app: buildBriefReportFromStore(app, store, ref, save),
|
|
223
|
+
dbPath: resolveTrailsDbPath({ rootDir }),
|
|
224
|
+
list: buildSurveyListFromStore(store, ref),
|
|
225
|
+
lockExists: hasCommittedLock(trailsDir),
|
|
226
|
+
lockPath: LOCK_PATH,
|
|
227
|
+
}));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const buildCurrentTopoBrief = (
|
|
231
|
+
app: Topo,
|
|
232
|
+
options?: { readonly rootDir?: string }
|
|
233
|
+
): BriefReport => {
|
|
234
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
235
|
+
return withCurrentTopoStore(app, rootDir, (store, ref, save) =>
|
|
236
|
+
buildBriefReportFromStore(app, store, ref, save)
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const buildCurrentTopoList = (
|
|
241
|
+
app: Topo,
|
|
242
|
+
options?: { readonly rootDir?: string }
|
|
243
|
+
): SurveyListReport => {
|
|
244
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
245
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) =>
|
|
246
|
+
buildSurveyListFromStore(store, ref)
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const buildCurrentGuideEntries = (
|
|
251
|
+
app: Topo,
|
|
252
|
+
options?: { readonly rootDir?: string }
|
|
253
|
+
): readonly {
|
|
254
|
+
readonly description: string;
|
|
255
|
+
readonly exampleCount: number;
|
|
256
|
+
readonly id: string;
|
|
257
|
+
readonly kind: string;
|
|
258
|
+
}[] => {
|
|
259
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
260
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) =>
|
|
261
|
+
store.trails.list({ save: ref }).map((trail) => ({
|
|
262
|
+
description: trail.description ?? '(no description)',
|
|
263
|
+
exampleCount: trail.exampleCount,
|
|
264
|
+
id: trail.id,
|
|
265
|
+
kind: trail.kind,
|
|
266
|
+
}))
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const buildCurrentTopoDetail = (
|
|
271
|
+
app: Topo,
|
|
272
|
+
id: string,
|
|
273
|
+
options?: { readonly rootDir?: string }
|
|
274
|
+
): CurrentProvisionDetail | CurrentTrailDetail | undefined => {
|
|
275
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
276
|
+
return withCurrentTopoStore(app, rootDir, (store, ref) => {
|
|
277
|
+
const trail = store.trails.get(id, { save: ref });
|
|
278
|
+
if (trail !== undefined) {
|
|
279
|
+
return buildTrailDetailFromStore(trail);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const provision = store.provisions.get(id, { save: ref });
|
|
283
|
+
return provision === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: buildProvisionDetailFromStore(provision);
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export const verifyCurrentTopo = async (
|
|
290
|
+
app: Topo,
|
|
291
|
+
options?: { readonly rootDir?: string }
|
|
292
|
+
): Promise<Result<TopoVerifyReport, Error>> => {
|
|
293
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
294
|
+
const committedLock = await readTrailheadLockData({
|
|
295
|
+
dir: resolveTrailsDir({ rootDir }),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (committedLock === null) {
|
|
299
|
+
return Result.err(
|
|
300
|
+
new NotFoundError(
|
|
301
|
+
'No committed trails.lock found. Run `trails topo export` first.'
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const currentHash = withCurrentTopoStore(
|
|
307
|
+
app,
|
|
308
|
+
rootDir,
|
|
309
|
+
(store, ref) => store.exports.get(ref)?.trailheadHash
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (currentHash === undefined) {
|
|
313
|
+
return Result.err(
|
|
314
|
+
new InternalError('No stored topo export found for the current topo save')
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (committedLock.hash !== currentHash) {
|
|
319
|
+
return Result.err(
|
|
320
|
+
new ConflictError(
|
|
321
|
+
'trails.lock is stale. Run `trails topo export` to refresh it.'
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return Result.ok({
|
|
327
|
+
committedHash: committedLock.hash,
|
|
328
|
+
currentHash,
|
|
329
|
+
lockPath: LOCK_PATH,
|
|
330
|
+
stale: false,
|
|
331
|
+
});
|
|
332
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
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 provisions: 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 provisions: 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 provisionCount: number;
|
|
30
|
+
readonly provisions: readonly {
|
|
31
|
+
readonly description: string | null;
|
|
32
|
+
readonly health: 'available' | 'none';
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly kind: 'provision';
|
|
35
|
+
readonly lifetime: 'singleton';
|
|
36
|
+
readonly usedBy: readonly string[];
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TrailDetailReport {
|
|
41
|
+
readonly description: string | null;
|
|
42
|
+
readonly detours: Trail<unknown, unknown>['detours'] | null;
|
|
43
|
+
readonly examples: readonly unknown[];
|
|
44
|
+
readonly crosses: readonly string[];
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly intent: 'read' | 'write' | 'destroy';
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
readonly safety: string;
|
|
49
|
+
readonly provisions: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
|
|
53
|
+
if (key === 'examples') {
|
|
54
|
+
return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
|
|
55
|
+
}
|
|
56
|
+
return Boolean(raw[key]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const detectFeatures = (
|
|
60
|
+
app: Topo
|
|
61
|
+
): {
|
|
62
|
+
hasDetours: boolean;
|
|
63
|
+
hasExamples: boolean;
|
|
64
|
+
hasOutputSchemas: boolean;
|
|
65
|
+
hasProvisions: boolean;
|
|
66
|
+
} => {
|
|
67
|
+
const trails = [...app.trails.values()].map(
|
|
68
|
+
(item) => item as unknown as Record<string, unknown>
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
hasDetours: trails.some((r) => trailHas(r, 'detours')),
|
|
72
|
+
hasExamples: trails.some((r) => trailHas(r, 'examples')),
|
|
73
|
+
hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
|
|
74
|
+
hasProvisions: trails.some(
|
|
75
|
+
(r) =>
|
|
76
|
+
Array.isArray(r['provisions']) &&
|
|
77
|
+
(r['provisions'] as unknown[]).length > 0
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const generateBriefReport = (app: Topo): BriefReport => {
|
|
83
|
+
const { hasDetours, hasExamples, hasOutputSchemas, hasProvisions } =
|
|
84
|
+
detectFeatures(app);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
contractVersion: REPORT_CONTRACT_VERSION,
|
|
88
|
+
features: {
|
|
89
|
+
detours: hasDetours,
|
|
90
|
+
examples: hasExamples,
|
|
91
|
+
outputSchemas: hasOutputSchemas,
|
|
92
|
+
provisions: hasProvisions,
|
|
93
|
+
signals: app.signals.size > 0,
|
|
94
|
+
},
|
|
95
|
+
name: app.name,
|
|
96
|
+
provisions: app.provisions.size,
|
|
97
|
+
signals: app.signals.size,
|
|
98
|
+
trails: app.trails.size,
|
|
99
|
+
version: REPORT_VERSION,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const safetyLabel = (entry: {
|
|
104
|
+
intent?: 'read' | 'write' | 'destroy';
|
|
105
|
+
}): string => {
|
|
106
|
+
if (entry.intent === 'destroy') {
|
|
107
|
+
return 'destroy';
|
|
108
|
+
}
|
|
109
|
+
if (entry.intent === 'write') {
|
|
110
|
+
return 'write';
|
|
111
|
+
}
|
|
112
|
+
if (entry.intent === 'read') {
|
|
113
|
+
return 'read';
|
|
114
|
+
}
|
|
115
|
+
return '-';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const buildProvisionUsage = (
|
|
119
|
+
app: Topo
|
|
120
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
121
|
+
const usage = new Map<string, string[]>();
|
|
122
|
+
|
|
123
|
+
for (const trailDef of app.list()) {
|
|
124
|
+
for (const declaredProvision of trailDef.provisions) {
|
|
125
|
+
const users = usage.get(declaredProvision.id) ?? [];
|
|
126
|
+
users.push(trailDef.id);
|
|
127
|
+
usage.set(declaredProvision.id, users);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Map(
|
|
132
|
+
[...usage.entries()].map(([id, users]) => [id, users.toSorted()] as const)
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const provisionHealthStatus = (provision: {
|
|
137
|
+
health?: unknown;
|
|
138
|
+
}): 'available' | 'none' =>
|
|
139
|
+
provision.health === undefined ? 'none' : 'available';
|
|
140
|
+
|
|
141
|
+
export const formatProvisionDetail = (
|
|
142
|
+
app: Topo,
|
|
143
|
+
provisionId: string
|
|
144
|
+
): object => {
|
|
145
|
+
const item = app.getProvision(provisionId);
|
|
146
|
+
const usedBy = buildProvisionUsage(app).get(provisionId) ?? [];
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
description: item?.description ?? null,
|
|
150
|
+
health: item ? provisionHealthStatus(item) : 'none',
|
|
151
|
+
id: provisionId,
|
|
152
|
+
kind: 'provision',
|
|
153
|
+
lifetime: 'singleton',
|
|
154
|
+
usedBy,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const formatProvisionList = (app: Topo): SurveyListReport['provisions'] => {
|
|
159
|
+
const usage = buildProvisionUsage(app);
|
|
160
|
+
return app
|
|
161
|
+
.listProvisions()
|
|
162
|
+
.map((provision) => ({
|
|
163
|
+
description: provision.description ?? null,
|
|
164
|
+
health: provisionHealthStatus(provision),
|
|
165
|
+
id: provision.id,
|
|
166
|
+
kind: provision.kind,
|
|
167
|
+
lifetime: 'singleton' as const,
|
|
168
|
+
usedBy: usage.get(provision.id) ?? [],
|
|
169
|
+
}))
|
|
170
|
+
.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const generateSurveyList = (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 provisions = formatProvisionList(app);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
count: items.length,
|
|
197
|
+
entries,
|
|
198
|
+
provisionCount: provisions.length,
|
|
199
|
+
provisions,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const generateTrailDetail = (
|
|
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: item.detours ?? null,
|
|
214
|
+
examples: item.examples ?? [],
|
|
215
|
+
id: item.id,
|
|
216
|
+
intent: item.intent,
|
|
217
|
+
kind: item.kind,
|
|
218
|
+
provisions: item.provisions.map((provision) => provision.id).toSorted(),
|
|
219
|
+
safety,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import { DEFAULT_APP_MODULE } from './topo-support.js';
|
|
7
|
+
|
|
8
|
+
const trailDetailOutput = z.object({
|
|
9
|
+
crosses: z.array(z.string()),
|
|
10
|
+
description: z.unknown().nullable(),
|
|
11
|
+
detours: z.unknown().nullable(),
|
|
12
|
+
examples: z.array(z.unknown()),
|
|
13
|
+
id: z.string(),
|
|
14
|
+
intent: z.enum(['read', 'write', 'destroy']),
|
|
15
|
+
kind: z.string(),
|
|
16
|
+
provisions: z.array(z.string()),
|
|
17
|
+
safety: z.string(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const provisionDetailOutput = z.object({
|
|
21
|
+
description: z.string().nullable(),
|
|
22
|
+
health: z.enum(['available', 'none']),
|
|
23
|
+
id: z.string(),
|
|
24
|
+
kind: z.literal('provision'),
|
|
25
|
+
lifetime: z.literal('singleton'),
|
|
26
|
+
usedBy: z.array(z.string()),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const topoShowTrail = trail('topo.show', {
|
|
30
|
+
blaze: async (input, ctx) => {
|
|
31
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
32
|
+
const app = await loadApp(input.module, rootDir);
|
|
33
|
+
const detail = buildCurrentTopoDetail(app, input.id, { rootDir });
|
|
34
|
+
if (detail !== undefined) {
|
|
35
|
+
return Result.ok(detail);
|
|
36
|
+
}
|
|
37
|
+
return Result.err(
|
|
38
|
+
new NotFoundError(`Trail or provision not found: ${input.id}`)
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
description: 'Show detail for a current trail or provision',
|
|
42
|
+
examples: [
|
|
43
|
+
{
|
|
44
|
+
input: { id: 'topo' },
|
|
45
|
+
name: 'Show current trail detail',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
input: z.object({
|
|
49
|
+
id: z.string().describe('Trail or provision ID to inspect'),
|
|
50
|
+
module: z
|
|
51
|
+
.string()
|
|
52
|
+
.default(DEFAULT_APP_MODULE)
|
|
53
|
+
.describe('Path to the app module'),
|
|
54
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
55
|
+
}),
|
|
56
|
+
intent: 'read',
|
|
57
|
+
output: z.union([trailDetailOutput, provisionDetailOutput]),
|
|
58
|
+
});
|