@ontrails/trails 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +12 -0
  5. package/__tests__/examples.test.ts +6 -0
  6. package/bin/trails.ts +3 -0
  7. package/dist/bin/trails.d.ts +3 -0
  8. package/dist/bin/trails.d.ts.map +1 -0
  9. package/dist/bin/trails.js +4 -0
  10. package/dist/bin/trails.js.map +1 -0
  11. package/dist/src/app.d.ts +2 -0
  12. package/dist/src/app.d.ts.map +1 -0
  13. package/dist/src/app.js +11 -0
  14. package/dist/src/app.js.map +1 -0
  15. package/dist/src/clack.d.ts +9 -0
  16. package/dist/src/clack.d.ts.map +1 -0
  17. package/dist/src/clack.js +62 -0
  18. package/dist/src/clack.js.map +1 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.d.ts.map +1 -0
  21. package/dist/src/cli.js +13 -0
  22. package/dist/src/cli.js.map +1 -0
  23. package/dist/src/trails/add-surface.d.ts +13 -0
  24. package/dist/src/trails/add-surface.d.ts.map +1 -0
  25. package/dist/src/trails/add-surface.js +88 -0
  26. package/dist/src/trails/add-surface.js.map +1 -0
  27. package/dist/src/trails/add-trail.d.ts +11 -0
  28. package/dist/src/trails/add-trail.d.ts.map +1 -0
  29. package/dist/src/trails/add-trail.js +85 -0
  30. package/dist/src/trails/add-trail.js.map +1 -0
  31. package/dist/src/trails/add-verify.d.ts +10 -0
  32. package/dist/src/trails/add-verify.d.ts.map +1 -0
  33. package/dist/src/trails/add-verify.js +67 -0
  34. package/dist/src/trails/add-verify.js.map +1 -0
  35. package/dist/src/trails/create-scaffold.d.ts +15 -0
  36. package/dist/src/trails/create-scaffold.d.ts.map +1 -0
  37. package/dist/src/trails/create-scaffold.js +288 -0
  38. package/dist/src/trails/create-scaffold.js.map +1 -0
  39. package/dist/src/trails/create.d.ts +22 -0
  40. package/dist/src/trails/create.d.ts.map +1 -0
  41. package/dist/src/trails/create.js +121 -0
  42. package/dist/src/trails/create.js.map +1 -0
  43. package/dist/src/trails/guide.d.ts +11 -0
  44. package/dist/src/trails/guide.d.ts.map +1 -0
  45. package/dist/src/trails/guide.js +80 -0
  46. package/dist/src/trails/guide.js.map +1 -0
  47. package/dist/src/trails/load-app.d.ts +4 -0
  48. package/dist/src/trails/load-app.d.ts.map +1 -0
  49. package/dist/src/trails/load-app.js +24 -0
  50. package/dist/src/trails/load-app.js.map +1 -0
  51. package/dist/src/trails/project.d.ts +8 -0
  52. package/dist/src/trails/project.d.ts.map +1 -0
  53. package/dist/src/trails/project.js +43 -0
  54. package/dist/src/trails/project.js.map +1 -0
  55. package/dist/src/trails/survey.d.ts +33 -0
  56. package/dist/src/trails/survey.d.ts.map +1 -0
  57. package/dist/src/trails/survey.js +225 -0
  58. package/dist/src/trails/survey.js.map +1 -0
  59. package/dist/src/trails/warden.d.ts +19 -0
  60. package/dist/src/trails/warden.d.ts.map +1 -0
  61. package/dist/src/trails/warden.js +88 -0
  62. package/dist/src/trails/warden.js.map +1 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/package.json +28 -0
  65. package/src/__tests__/create.test.ts +349 -0
  66. package/src/__tests__/guide.test.ts +91 -0
  67. package/src/__tests__/load-app.test.ts +15 -0
  68. package/src/__tests__/survey.test.ts +161 -0
  69. package/src/__tests__/warden.test.ts +74 -0
  70. package/src/app.ts +22 -0
  71. package/src/clack.ts +89 -0
  72. package/src/cli.ts +14 -0
  73. package/src/trails/add-surface.ts +119 -0
  74. package/src/trails/add-trail.ts +103 -0
  75. package/src/trails/add-verify.ts +87 -0
  76. package/src/trails/create-scaffold.ts +352 -0
  77. package/src/trails/create.ts +203 -0
  78. package/src/trails/guide.ts +104 -0
  79. package/src/trails/load-app.ts +37 -0
  80. package/src/trails/project.ts +51 -0
  81. package/src/trails/survey.ts +307 -0
  82. package/src/trails/warden.ts +104 -0
  83. package/tsconfig.json +9 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared Trails-project detection helpers for scaffold trails.
3
+ */
4
+
5
+ import { existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ /** Return all TypeScript entries in a project's src directory. */
9
+ const scanSourceEntries = (srcDir: string): string[] => [
10
+ ...new Bun.Glob('*.ts').scanSync({ cwd: srcDir }),
11
+ ];
12
+
13
+ /** Resolve an entry to an app import if it contains topo(). */
14
+ const toTopoImport = async (
15
+ srcDir: string,
16
+ entry: string
17
+ ): Promise<string | null> => {
18
+ const content = await Bun.file(join(srcDir, entry)).text();
19
+ return content.includes('topo(')
20
+ ? `./${entry.replace(/\.ts$/, '.js')}`
21
+ : null;
22
+ };
23
+
24
+ /** Find the app module that defines a topo inside `src/`. */
25
+ export const findTopoPath = async (cwd: string): Promise<string | null> => {
26
+ const srcDir = join(cwd, 'src');
27
+ if (!existsSync(srcDir)) {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ for (const entry of scanSourceEntries(srcDir)) {
33
+ const appImport = await toTopoImport(srcDir, entry);
34
+ if (appImport) {
35
+ return appImport;
36
+ }
37
+ }
38
+ } catch {
39
+ return null;
40
+ }
41
+
42
+ return null;
43
+ };
44
+
45
+ /** Detect whether the directory already looks like a Trails project. */
46
+ export const isInsideProject = async (cwd: string): Promise<boolean> => {
47
+ if (existsSync(join(cwd, '.trails'))) {
48
+ return true;
49
+ }
50
+ return (await findTopoPath(cwd)) !== null;
51
+ };
@@ -0,0 +1,307 @@
1
+ /**
2
+ * `survey` trail -- Full topo introspection.
3
+ *
4
+ * Lists trails, shows detail for individual trails, generates surface maps,
5
+ * and diffs against previous versions.
6
+ */
7
+
8
+ import type { Topo, Trail } from '@ontrails/core';
9
+ import { Result, trail } from '@ontrails/core';
10
+ import type { DiffResult } from '@ontrails/schema';
11
+ import {
12
+ diffSurfaceMaps,
13
+ generateSurfaceMap,
14
+ hashSurfaceMap,
15
+ readSurfaceMap,
16
+ writeSurfaceLock,
17
+ writeSurfaceMap,
18
+ } from '@ontrails/schema';
19
+ import { z } from 'zod';
20
+
21
+ import { loadApp } from './load-app.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Brief report (formerly scout)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface BriefReport {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ readonly contractVersion: string;
31
+ readonly features: {
32
+ readonly outputSchemas: boolean;
33
+ readonly examples: boolean;
34
+ readonly detours: boolean;
35
+ readonly hikes: boolean;
36
+ readonly events: boolean;
37
+ };
38
+ readonly trails: number;
39
+ readonly hikes: number;
40
+ readonly events: number;
41
+ }
42
+
43
+ /** Check if a trail has a specific feature. */
44
+ const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
45
+ if (key === 'examples') {
46
+ return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
47
+ }
48
+ return Boolean(raw[key]);
49
+ };
50
+
51
+ /** Detect which features are used across trails. */
52
+ const detectFeatures = (
53
+ app: Topo
54
+ ): { hasDetours: boolean; hasExamples: boolean; hasOutputSchemas: boolean } => {
55
+ const trails = [...app.trails.values()].map(
56
+ (item) => item as unknown as Record<string, unknown>
57
+ );
58
+ return {
59
+ hasDetours: trails.some((r) => trailHas(r, 'detours')),
60
+ hasExamples: trails.some((r) => trailHas(r, 'examples')),
61
+ hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
62
+ };
63
+ };
64
+
65
+ /** Generate a compact capability report for the given topo. */
66
+ export const generateBriefReport = (app: Topo): BriefReport => {
67
+ const { hasDetours, hasExamples, hasOutputSchemas } = detectFeatures(app);
68
+
69
+ return {
70
+ contractVersion: '2026-03',
71
+ events: app.events.size,
72
+ features: {
73
+ detours: hasDetours,
74
+ events: app.events.size > 0,
75
+ examples: hasExamples,
76
+ hikes: app.hikes.size > 0,
77
+ outputSchemas: hasOutputSchemas,
78
+ },
79
+ hikes: app.hikes.size,
80
+ name: app.name,
81
+ trails: app.trails.size,
82
+ version: '0.1.0',
83
+ };
84
+ };
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Formatting helpers
88
+ // ---------------------------------------------------------------------------
89
+
90
+ const safetyLabel = (entry: {
91
+ readOnly?: boolean;
92
+ destructive?: boolean;
93
+ }): string => {
94
+ if (entry.destructive) {
95
+ return 'destructive';
96
+ }
97
+ if (entry.readOnly) {
98
+ return 'readOnly';
99
+ }
100
+ return '-';
101
+ };
102
+
103
+ const formatTrailList = (app: Topo): object => {
104
+ const items = app.list();
105
+ const entries = items.map((item) => {
106
+ const safety = safetyLabel(
107
+ item as unknown as { readOnly?: boolean; destructive?: boolean }
108
+ );
109
+ const examples = Array.isArray(
110
+ (item as unknown as { examples?: unknown[] }).examples
111
+ )
112
+ ? (item as unknown as { examples: unknown[] }).examples.length
113
+ : 0;
114
+
115
+ return {
116
+ examples,
117
+ id: item.id,
118
+ kind: item.kind,
119
+ safety,
120
+ };
121
+ });
122
+
123
+ return { count: items.length, entries };
124
+ };
125
+
126
+ /**
127
+ * Build a human-readable detail view for a single trail.
128
+ *
129
+ * Overlaps with `trailToEntry` in `@ontrails/schema` which builds the
130
+ * surface-map entry. The two serve different audiences (human display vs
131
+ * machine-diffable surface map) so they are kept separate.
132
+ */
133
+ const formatTrailDetail = (item: Trail<unknown, unknown>): object => {
134
+ const safety = safetyLabel(
135
+ item as unknown as { readOnly?: boolean; destructive?: boolean }
136
+ );
137
+
138
+ return {
139
+ description: item.description ?? null,
140
+ detours: item.detours ?? null,
141
+ examples: item.examples ?? [],
142
+ id: item.id,
143
+ kind: item.kind,
144
+ safety,
145
+ };
146
+ };
147
+
148
+ const formatDiff = (diff: DiffResult): object => ({
149
+ breaking: diff.breaking,
150
+ hasBreaking: diff.hasBreaking,
151
+ info: diff.info,
152
+ warnings: diff.warnings,
153
+ });
154
+
155
+ const buildSurveyDiff = async (
156
+ app: Topo,
157
+ breakingOnly: boolean
158
+ ): Promise<Result<object, Error>> => {
159
+ const currentMap = generateSurfaceMap(app);
160
+ const previousMap = await readSurfaceMap();
161
+ if (!previousMap) {
162
+ return Result.err(
163
+ new Error(
164
+ 'No previous surface map found. Run `trails survey generate` first.'
165
+ )
166
+ );
167
+ }
168
+
169
+ const diff = diffSurfaceMaps(previousMap, currentMap);
170
+ return Result.ok(
171
+ breakingOnly
172
+ ? formatDiff({
173
+ ...diff,
174
+ entries: diff.breaking,
175
+ info: [],
176
+ warnings: [],
177
+ })
178
+ : formatDiff(diff)
179
+ );
180
+ };
181
+
182
+ const buildSurveyDetail = (
183
+ app: Topo,
184
+ trailId: string
185
+ ): Result<object, Error> => {
186
+ const item = app.get(trailId);
187
+ if (!item) {
188
+ return Result.err(new Error(`Trail not found: ${trailId}`));
189
+ }
190
+ return Result.ok(formatTrailDetail(item as Trail<unknown, unknown>));
191
+ };
192
+
193
+ const buildSurveyGenerate = async (
194
+ app: Topo
195
+ ): Promise<Result<object, Error>> => {
196
+ const surfaceMap = generateSurfaceMap(app);
197
+ const mapPath = await writeSurfaceMap(surfaceMap);
198
+ const hash = hashSurfaceMap(surfaceMap);
199
+ const lockPath = await writeSurfaceLock(hash);
200
+ return Result.ok({ hash, lockPath, mapPath });
201
+ };
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Trail definition
205
+ // ---------------------------------------------------------------------------
206
+
207
+ export const surveyTrail = trail('survey', {
208
+ description: 'Full topo introspection',
209
+ examples: [
210
+ {
211
+ description: 'Lists all registered trails with safety and surface info',
212
+ input: {},
213
+ name: 'List all trails',
214
+ },
215
+ {
216
+ description: 'Quick capability summary with counts and feature flags',
217
+ input: { brief: true },
218
+ name: 'Brief capability report',
219
+ },
220
+ ],
221
+ implementation: async (input, ctx) => {
222
+ const app = await loadApp(input.module, ctx.cwd ?? '.');
223
+
224
+ if (input.brief) {
225
+ return Result.ok(generateBriefReport(app));
226
+ }
227
+
228
+ if (input.diff) {
229
+ return await buildSurveyDiff(app, input.breakingOnly);
230
+ }
231
+
232
+ if (input.trailId) {
233
+ return buildSurveyDetail(app, input.trailId);
234
+ }
235
+
236
+ if (input.generate) {
237
+ return await buildSurveyGenerate(app);
238
+ }
239
+
240
+ return Result.ok(formatTrailList(app));
241
+ },
242
+ input: z.object({
243
+ breakingOnly: z
244
+ .boolean()
245
+ .default(false)
246
+ .describe('Only show breaking changes'),
247
+ brief: z.boolean().default(false).describe('Quick capability summary'),
248
+ diff: z.string().optional().describe('Diff against a git ref'),
249
+ generate: z
250
+ .boolean()
251
+ .default(false)
252
+ .describe('Generate surface map and lock file'),
253
+ module: z
254
+ .string()
255
+ .default('./src/app.ts')
256
+ .describe('Path to the app module'),
257
+ trailId: z.string().optional().describe('Trail ID for detail view'),
258
+ }),
259
+ output: z.union([
260
+ z.object({
261
+ count: z.number(),
262
+ entries: z.array(
263
+ z.object({
264
+ examples: z.number(),
265
+ id: z.string(),
266
+ kind: z.string(),
267
+ safety: z.string(),
268
+ })
269
+ ),
270
+ }),
271
+ z.object({
272
+ contractVersion: z.string(),
273
+ events: z.number(),
274
+ features: z.object({
275
+ detours: z.boolean(),
276
+ events: z.boolean(),
277
+ examples: z.boolean(),
278
+ hikes: z.boolean(),
279
+ outputSchemas: z.boolean(),
280
+ }),
281
+ hikes: z.number(),
282
+ name: z.string(),
283
+ trails: z.number(),
284
+ version: z.string(),
285
+ }),
286
+ z.object({
287
+ breaking: z.array(z.unknown()),
288
+ hasBreaking: z.boolean(),
289
+ info: z.array(z.unknown()),
290
+ warnings: z.array(z.unknown()),
291
+ }),
292
+ z.object({
293
+ description: z.unknown().nullable(),
294
+ detours: z.unknown().nullable(),
295
+ examples: z.array(z.unknown()),
296
+ id: z.string(),
297
+ kind: z.string(),
298
+ safety: z.string(),
299
+ }),
300
+ z.object({
301
+ hash: z.string(),
302
+ lockPath: z.string(),
303
+ mapPath: z.string(),
304
+ }),
305
+ ]),
306
+ readOnly: true,
307
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `warden` trail -- Governance checks.
3
+ *
4
+ * Thin wrapper around `runWarden` and `formatWardenReport` from @ontrails/warden.
5
+ */
6
+
7
+ import { Result, trail } from '@ontrails/core';
8
+ import {
9
+ formatGitHubAnnotations,
10
+ formatJson,
11
+ formatSummary,
12
+ formatWardenReport,
13
+ runWarden,
14
+ } from '@ontrails/warden';
15
+ import { z } from 'zod';
16
+
17
+ import { loadApp } from './load-app.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Trail definition
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const wardenTrail = trail('warden', {
24
+ description: 'Run governance checks (lint + drift)',
25
+ examples: [
26
+ {
27
+ input: {
28
+ driftOnly: false,
29
+ lintOnly: false,
30
+ },
31
+ name: 'Default warden run',
32
+ },
33
+ {
34
+ input: {
35
+ format: 'github',
36
+ },
37
+ name: 'GitHub Actions annotations',
38
+ },
39
+ ],
40
+ implementation: async (input, ctx) => {
41
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
42
+ // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
43
+ const topo = await loadApp('./src/app.ts', rootDir).catch(
44
+ (): undefined => undefined
45
+ );
46
+
47
+ const report = await runWarden({
48
+ driftOnly: input.driftOnly,
49
+ lintOnly: input.lintOnly,
50
+ rootDir,
51
+ topo,
52
+ });
53
+
54
+ const formatters: Record<string, (r: typeof report) => string> = {
55
+ github: formatGitHubAnnotations,
56
+ json: formatJson,
57
+ summary: formatSummary,
58
+ text: formatWardenReport,
59
+ };
60
+ const formatter = formatters[input.format] ?? formatWardenReport;
61
+ const formatted = formatter(report);
62
+
63
+ return Result.ok({
64
+ diagnostics: report.diagnostics,
65
+ drift: report.drift,
66
+ errorCount: report.errorCount,
67
+ formatted,
68
+ passed: report.passed,
69
+ warnCount: report.warnCount,
70
+ });
71
+ },
72
+ input: z.object({
73
+ driftOnly: z.boolean().default(false).describe('Only run drift detection'),
74
+ format: z
75
+ .enum(['text', 'json', 'github', 'summary'])
76
+ .default('text')
77
+ .describe('Output format: text, json, github, or summary'),
78
+ lintOnly: z.boolean().default(false).describe('Only run lint rules'),
79
+ rootDir: z.string().optional().describe('Root directory to scan'),
80
+ }),
81
+ output: z.object({
82
+ diagnostics: z.array(
83
+ z.object({
84
+ filePath: z.string(),
85
+ line: z.number(),
86
+ message: z.string(),
87
+ rule: z.string(),
88
+ severity: z.enum(['error', 'warn']),
89
+ })
90
+ ),
91
+ drift: z
92
+ .object({
93
+ committedHash: z.string().nullable(),
94
+ currentHash: z.string(),
95
+ stale: z.boolean(),
96
+ })
97
+ .nullable(),
98
+ errorCount: z.number(),
99
+ formatted: z.string(),
100
+ passed: z.boolean(),
101
+ warnCount: z.number(),
102
+ }),
103
+ readOnly: true,
104
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "."
6
+ },
7
+ "include": ["src", "bin"],
8
+ "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
+ }