@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
package/src/trails/guide.ts
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Lists trails with descriptions and examples. Detailed guidance is planned for post-v1.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import { Result, trail } from '@ontrails/core';
|
|
7
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
9
8
|
import { z } from 'zod';
|
|
10
9
|
|
|
11
10
|
import { loadApp } from './load-app.js';
|
|
11
|
+
import {
|
|
12
|
+
buildCurrentGuideEntries,
|
|
13
|
+
buildCurrentTopoDetail,
|
|
14
|
+
} from './topo-read-support.js';
|
|
12
15
|
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// Types
|
|
@@ -25,48 +28,30 @@ interface GuideEntry {
|
|
|
25
28
|
// Helpers
|
|
26
29
|
// ---------------------------------------------------------------------------
|
|
27
30
|
|
|
28
|
-
const toGuideEntries = (app: Topo): GuideEntry[] => {
|
|
29
|
-
const entries: GuideEntry[] = [];
|
|
30
|
-
|
|
31
|
-
for (const item of app.list()) {
|
|
32
|
-
const raw = item as unknown as Record<string, unknown>;
|
|
33
|
-
entries.push({
|
|
34
|
-
description:
|
|
35
|
-
typeof raw['description'] === 'string'
|
|
36
|
-
? raw['description']
|
|
37
|
-
: '(no description)',
|
|
38
|
-
exampleCount: Array.isArray(raw['examples'])
|
|
39
|
-
? (raw['examples'] as unknown[]).length
|
|
40
|
-
: 0,
|
|
41
|
-
id: item.id,
|
|
42
|
-
kind: item.kind,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return entries;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const toGuideDetail = (item: Trail<unknown, unknown>): object => ({
|
|
50
|
-
description: item.description ?? null,
|
|
51
|
-
detours: item.detours ?? null,
|
|
52
|
-
examples: item.examples ?? [],
|
|
53
|
-
id: item.id,
|
|
54
|
-
kind: item.kind,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
31
|
export const guideTrail = trail('guide', {
|
|
58
32
|
blaze: async (input, ctx) => {
|
|
59
|
-
const
|
|
33
|
+
const rootDir = ctx.cwd ?? '.';
|
|
34
|
+
const app = await loadApp(input.module, rootDir);
|
|
60
35
|
|
|
61
36
|
if (input.trailId) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return Result.err(
|
|
37
|
+
const detail = buildCurrentTopoDetail(app, input.trailId, { rootDir });
|
|
38
|
+
if (detail === undefined || detail.kind !== 'trail') {
|
|
39
|
+
return Result.err(
|
|
40
|
+
new NotFoundError(`Trail not found: ${input.trailId}`)
|
|
41
|
+
);
|
|
65
42
|
}
|
|
66
|
-
return Result.ok(
|
|
43
|
+
return Result.ok({
|
|
44
|
+
description: detail.description,
|
|
45
|
+
detours: detail.detours,
|
|
46
|
+
examples: detail.examples,
|
|
47
|
+
id: detail.id,
|
|
48
|
+
kind: detail.kind,
|
|
49
|
+
});
|
|
67
50
|
}
|
|
68
51
|
|
|
69
|
-
return Result.ok(
|
|
52
|
+
return Result.ok(
|
|
53
|
+
buildCurrentGuideEntries(app, { rootDir }) as GuideEntry[]
|
|
54
|
+
);
|
|
70
55
|
},
|
|
71
56
|
description: 'Runtime guidance for trails',
|
|
72
57
|
examples: [
|
package/src/trails/load-app.ts
CHANGED
|
@@ -1,31 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
4
|
|
|
4
5
|
import type { Topo } from '@ontrails/core';
|
|
5
6
|
|
|
6
7
|
const URL_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
9
|
+
const resolveUrlModulePath = (modulePath: string): string => {
|
|
10
|
+
const url = new URL(modulePath);
|
|
11
|
+
return url.protocol === 'file:' ? fileURLToPath(url) : modulePath;
|
|
12
|
+
};
|
|
13
13
|
|
|
14
|
+
const resolveFilesystemModulePath = (
|
|
15
|
+
modulePath: string,
|
|
16
|
+
cwd: string
|
|
17
|
+
): string => {
|
|
14
18
|
const absolutePath = isAbsolute(modulePath)
|
|
15
19
|
? modulePath
|
|
16
20
|
: resolve(cwd, modulePath);
|
|
17
|
-
|
|
21
|
+
if (!absolutePath.endsWith('.js') || existsSync(absolutePath)) {
|
|
22
|
+
return absolutePath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tsPath = absolutePath.replace(/\.js$/, '.ts');
|
|
26
|
+
return existsSync(tsPath) ? tsPath : absolutePath;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Resolve a module path from cwd so CLI defaults behave like shell paths. */
|
|
30
|
+
const resolveAbsoluteModulePath = (modulePath: string, cwd: string): string =>
|
|
31
|
+
URL_SCHEME.test(modulePath)
|
|
32
|
+
? resolveUrlModulePath(modulePath)
|
|
33
|
+
: resolveFilesystemModulePath(modulePath, cwd);
|
|
34
|
+
|
|
35
|
+
const freshModuleCopyPath = (absolutePath: string): string =>
|
|
36
|
+
join(
|
|
37
|
+
dirname(absolutePath),
|
|
38
|
+
`.__fresh-${Date.now()}-${Math.random().toString(36).slice(2)}-${basename(absolutePath)}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Import a module bypassing the ESM cache for the entry file.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Cache-busting applies to the entry module only. Transitive imports resolved
|
|
46
|
+
* by the entry file are still served from Bun's module cache. This is
|
|
47
|
+
* acceptable for the draft promotion workflow (the only caller) because
|
|
48
|
+
* promotion changes which modules the entry file imports, not the modules
|
|
49
|
+
* themselves. If a deeper cache-bust is needed in the future, consider
|
|
50
|
+
* Bun's `Loader.registry` or a full process restart.
|
|
51
|
+
*/
|
|
52
|
+
const importFreshModule = async (
|
|
53
|
+
modulePath: string,
|
|
54
|
+
cwd: string
|
|
55
|
+
): Promise<Record<string, unknown>> => {
|
|
56
|
+
const absolutePath = resolveAbsoluteModulePath(modulePath, cwd);
|
|
57
|
+
if (URL_SCHEME.test(absolutePath) && !absolutePath.startsWith('/')) {
|
|
58
|
+
const url = new URL(absolutePath);
|
|
59
|
+
url.searchParams.set('t', Date.now().toString());
|
|
60
|
+
return (await import(url.href)) as Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const freshPath = freshModuleCopyPath(absolutePath);
|
|
64
|
+
await Bun.write(freshPath, await Bun.file(absolutePath).text());
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return (await import(pathToFileURL(freshPath).href)) as Record<
|
|
68
|
+
string,
|
|
69
|
+
unknown
|
|
70
|
+
>;
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(freshPath, { force: true });
|
|
73
|
+
}
|
|
18
74
|
};
|
|
19
75
|
|
|
20
76
|
/** Load a Topo export from a module path relative to cwd. */
|
|
21
77
|
export const loadApp = async (
|
|
22
78
|
modulePath: string,
|
|
23
|
-
cwd: string
|
|
79
|
+
cwd: string,
|
|
80
|
+
options: { fresh?: boolean | undefined } = {}
|
|
24
81
|
): Promise<Topo> => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
82
|
+
const resolvedModulePath = resolveAbsoluteModulePath(modulePath, cwd);
|
|
83
|
+
const mod =
|
|
84
|
+
options.fresh === true
|
|
85
|
+
? await importFreshModule(modulePath, cwd)
|
|
86
|
+
: ((await import(
|
|
87
|
+
URL_SCHEME.test(resolvedModulePath) &&
|
|
88
|
+
!resolvedModulePath.startsWith('/')
|
|
89
|
+
? new URL(resolvedModulePath).href
|
|
90
|
+
: pathToFileURL(resolvedModulePath).href
|
|
91
|
+
)) as Record<string, unknown>);
|
|
29
92
|
const app = (mod['default'] ?? mod['app']) as Topo | undefined;
|
|
30
93
|
if (!app?.trails) {
|
|
31
94
|
throw new Error(
|
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
|
@@ -5,252 +5,41 @@
|
|
|
5
5
|
* and diffs against previous versions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Topo
|
|
9
|
-
import { Result, trail } from '@ontrails/core';
|
|
8
|
+
import type { Topo } from '@ontrails/core';
|
|
9
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
10
10
|
import type { DiffResult } from '@ontrails/schema';
|
|
11
11
|
import {
|
|
12
12
|
diffTrailheadMaps,
|
|
13
13
|
generateOpenApiSpec,
|
|
14
14
|
generateTrailheadMap,
|
|
15
|
-
hashTrailheadMap,
|
|
16
15
|
readTrailheadMap,
|
|
17
|
-
writeTrailheadLock,
|
|
18
|
-
writeTrailheadMap,
|
|
19
16
|
} from '@ontrails/schema';
|
|
20
17
|
import { z } from 'zod';
|
|
21
18
|
|
|
22
19
|
import { loadApp } from './load-app.js';
|
|
20
|
+
import {
|
|
21
|
+
buildCurrentTopoBrief,
|
|
22
|
+
buildCurrentTopoDetail,
|
|
23
|
+
buildCurrentTopoList,
|
|
24
|
+
} from './topo-read-support.js';
|
|
25
|
+
import { exportCurrentTopo } from './topo-store-support.js';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
formatProvisionDetail,
|
|
29
|
+
generateBriefReport,
|
|
30
|
+
generateSurveyList,
|
|
31
|
+
generateTrailDetail,
|
|
32
|
+
} from './topo-reports.js';
|
|
33
|
+
export type {
|
|
34
|
+
BriefReport,
|
|
35
|
+
SurveyListReport,
|
|
36
|
+
TrailDetailReport,
|
|
37
|
+
} from './topo-reports.js';
|
|
23
38
|
|
|
24
39
|
// ---------------------------------------------------------------------------
|
|
25
40
|
// Brief report (formerly scout)
|
|
26
41
|
// ---------------------------------------------------------------------------
|
|
27
42
|
|
|
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
43
|
const formatDiff = (diff: DiffResult): object => ({
|
|
255
44
|
breaking: diff.breaking,
|
|
256
45
|
hasBreaking: diff.hasBreaking,
|
|
@@ -266,8 +55,8 @@ const buildSurveyDiff = async (
|
|
|
266
55
|
const previousMap = await readTrailheadMap();
|
|
267
56
|
if (!previousMap) {
|
|
268
57
|
return Result.err(
|
|
269
|
-
new
|
|
270
|
-
'No previous trailhead map found. Run `trails
|
|
58
|
+
new NotFoundError(
|
|
59
|
+
'No previous trailhead map found. Run `trails topo export` first.'
|
|
271
60
|
)
|
|
272
61
|
);
|
|
273
62
|
}
|
|
@@ -287,26 +76,31 @@ const buildSurveyDiff = async (
|
|
|
287
76
|
|
|
288
77
|
const buildSurveyDetail = (
|
|
289
78
|
app: Topo,
|
|
290
|
-
trailId: string
|
|
79
|
+
trailId: string,
|
|
80
|
+
rootDir: string
|
|
291
81
|
): Result<object, Error> => {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
return Result.ok(
|
|
295
|
-
}
|
|
296
|
-
if (app.getProvision(trailId)) {
|
|
297
|
-
return Result.ok(formatProvisionDetail(app, trailId));
|
|
82
|
+
const detail = buildCurrentTopoDetail(app, trailId, { rootDir });
|
|
83
|
+
if (detail !== undefined) {
|
|
84
|
+
return Result.ok(detail);
|
|
298
85
|
}
|
|
299
|
-
return Result.err(
|
|
86
|
+
return Result.err(
|
|
87
|
+
new NotFoundError(`Trail or provision not found: ${trailId}`)
|
|
88
|
+
);
|
|
300
89
|
};
|
|
301
90
|
|
|
302
91
|
const buildSurveyGenerate = async (
|
|
303
|
-
app: Topo
|
|
92
|
+
app: Topo,
|
|
93
|
+
rootDir: string
|
|
304
94
|
): Promise<Result<object, Error>> => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return Result.ok({
|
|
95
|
+
const exported = await exportCurrentTopo(app, { rootDir });
|
|
96
|
+
if (exported.isErr()) {
|
|
97
|
+
return exported;
|
|
98
|
+
}
|
|
99
|
+
return Result.ok({
|
|
100
|
+
hash: exported.value.hash,
|
|
101
|
+
lockPath: exported.value.lockPath,
|
|
102
|
+
mapPath: exported.value.mapPath,
|
|
103
|
+
});
|
|
310
104
|
};
|
|
311
105
|
|
|
312
106
|
interface SurveyInput {
|
|
@@ -335,27 +129,32 @@ const resolveSurveyMode = (input: SurveyInput): SurveyMode =>
|
|
|
335
129
|
|
|
336
130
|
type SurveyHandler = (
|
|
337
131
|
app: Topo,
|
|
338
|
-
input: SurveyInput
|
|
132
|
+
input: SurveyInput,
|
|
133
|
+
rootDir: string
|
|
339
134
|
) => Result<object, Error> | Promise<Result<object, Error>>;
|
|
340
135
|
|
|
341
136
|
/** Handlers keyed by survey mode. */
|
|
342
137
|
const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
|
|
343
|
-
brief: (app) =>
|
|
344
|
-
|
|
138
|
+
brief: (app, _input, rootDir) =>
|
|
139
|
+
Result.ok(buildCurrentTopoBrief(app, { rootDir })),
|
|
140
|
+
detail: (app, input, rootDir) =>
|
|
141
|
+
buildSurveyDetail(app, input.trailId ?? '', rootDir),
|
|
345
142
|
diff: (app, input) => buildSurveyDiff(app, input.breakingOnly),
|
|
346
|
-
generate: (app) => buildSurveyGenerate(app),
|
|
347
|
-
list: (app) =>
|
|
143
|
+
generate: (app, _input, rootDir) => buildSurveyGenerate(app, rootDir),
|
|
144
|
+
list: (app, _input, rootDir) =>
|
|
145
|
+
Result.ok(buildCurrentTopoList(app, { rootDir })),
|
|
348
146
|
openapi: (app) => Result.ok(generateOpenApiSpec(app)),
|
|
349
147
|
};
|
|
350
148
|
|
|
351
149
|
/** Dispatch to the appropriate survey sub-command based on input flags. */
|
|
352
150
|
const dispatchSurvey = (
|
|
353
151
|
app: Topo,
|
|
354
|
-
input: SurveyInput
|
|
152
|
+
input: SurveyInput,
|
|
153
|
+
rootDir: string
|
|
355
154
|
): Result<object, Error> | Promise<Result<object, Error>> => {
|
|
356
155
|
const mode = resolveSurveyMode(input);
|
|
357
156
|
const handler = surveyHandlers[mode];
|
|
358
|
-
return handler(app, input);
|
|
157
|
+
return handler(app, input, rootDir);
|
|
359
158
|
};
|
|
360
159
|
|
|
361
160
|
// ---------------------------------------------------------------------------
|
|
@@ -364,8 +163,9 @@ const dispatchSurvey = (
|
|
|
364
163
|
|
|
365
164
|
export const surveyTrail = trail('survey', {
|
|
366
165
|
blaze: async (input, ctx) => {
|
|
367
|
-
const
|
|
368
|
-
|
|
166
|
+
const rootDir = ctx.cwd ?? '.';
|
|
167
|
+
const app = await loadApp(input.module, rootDir);
|
|
168
|
+
return dispatchSurvey(app, input, rootDir);
|
|
369
169
|
},
|
|
370
170
|
description: 'Full topo introspection',
|
|
371
171
|
examples: [
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
DEFAULT_APP_MODULE,
|
|
8
|
+
isolatedExampleInput,
|
|
9
|
+
topoSaveOutput,
|
|
10
|
+
} from './topo-support.js';
|
|
11
|
+
|
|
12
|
+
export const topoExportTrail = trail('topo.export', {
|
|
13
|
+
blaze: async (input, ctx) => {
|
|
14
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
15
|
+
const app = await loadApp(input.module, rootDir);
|
|
16
|
+
return exportCurrentTopo(app, { rootDir });
|
|
17
|
+
},
|
|
18
|
+
description: 'Export the current topo to .trails artifacts',
|
|
19
|
+
examples: [
|
|
20
|
+
{
|
|
21
|
+
input: isolatedExampleInput('topo-export'),
|
|
22
|
+
name: 'Write the current topo export',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
input: z.object({
|
|
26
|
+
module: z
|
|
27
|
+
.string()
|
|
28
|
+
.default(DEFAULT_APP_MODULE)
|
|
29
|
+
.describe('Path to the app module'),
|
|
30
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
31
|
+
}),
|
|
32
|
+
intent: 'write',
|
|
33
|
+
output: z.object({
|
|
34
|
+
hash: z.string(),
|
|
35
|
+
lockPath: z.string(),
|
|
36
|
+
mapPath: z.string(),
|
|
37
|
+
save: topoSaveOutput,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_TOPO_HISTORY_LIMIT,
|
|
6
|
+
isolatedExampleInput,
|
|
7
|
+
listTopoHistory,
|
|
8
|
+
topoPinOutput,
|
|
9
|
+
topoSaveOutput,
|
|
10
|
+
} from './topo-support.js';
|
|
11
|
+
|
|
12
|
+
export const topoHistoryTrail = trail('topo.history', {
|
|
13
|
+
blaze: (input, ctx) => {
|
|
14
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
15
|
+
return Result.ok(listTopoHistory({ limit: input.limit, rootDir }));
|
|
16
|
+
},
|
|
17
|
+
description: 'List saved topo metadata, including pins and recent autosaves',
|
|
18
|
+
examples: [
|
|
19
|
+
{
|
|
20
|
+
input: isolatedExampleInput('topo-history'),
|
|
21
|
+
name: 'Show topo history',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
input: z.object({
|
|
25
|
+
limit: z
|
|
26
|
+
.number()
|
|
27
|
+
.default(DEFAULT_TOPO_HISTORY_LIMIT)
|
|
28
|
+
.describe('Maximum number of autosaves to return'),
|
|
29
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
30
|
+
}),
|
|
31
|
+
intent: 'read',
|
|
32
|
+
output: z.object({
|
|
33
|
+
dbPath: z.string(),
|
|
34
|
+
limit: z.number(),
|
|
35
|
+
pinCount: z.number(),
|
|
36
|
+
pins: z.array(topoPinOutput),
|
|
37
|
+
saveCount: z.number(),
|
|
38
|
+
saves: z.array(topoSaveOutput),
|
|
39
|
+
}),
|
|
40
|
+
});
|