@ontrails/trails 1.0.0-beta.17 → 1.0.0-beta.19

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 (45) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +7 -10
  3. package/package.json +13 -12
  4. package/src/app.ts +14 -4
  5. package/src/cli.ts +16 -0
  6. package/src/lifecycle-source-io.ts +33 -0
  7. package/src/project-writes.ts +62 -5
  8. package/src/retired-topo-command.ts +36 -0
  9. package/src/run-adapter-check.ts +76 -0
  10. package/src/run-collision.ts +1 -0
  11. package/src/trails/adapter-check.ts +244 -0
  12. package/src/trails/add-surface.ts +18 -18
  13. package/src/trails/add-trail.ts +3 -2
  14. package/src/trails/add-verify.ts +30 -6
  15. package/src/trails/{topo-compile.ts → compile.ts} +16 -8
  16. package/src/trails/completions-complete.ts +1 -1
  17. package/src/trails/create-adapter.ts +1084 -0
  18. package/src/trails/create-scaffold.ts +243 -29
  19. package/src/trails/create.ts +118 -17
  20. package/src/trails/deprecate.ts +59 -0
  21. package/src/trails/dev-clean.ts +2 -2
  22. package/src/trails/dev-reset.ts +2 -2
  23. package/src/trails/dev-stats.ts +1 -1
  24. package/src/trails/doctor.ts +56 -0
  25. package/src/trails/draft-promote.ts +1 -0
  26. package/src/trails/guide.ts +2 -2
  27. package/src/trails/revise.ts +53 -0
  28. package/src/trails/run-example.ts +12 -7
  29. package/src/trails/run-examples.ts +3 -3
  30. package/src/trails/run.ts +7 -4
  31. package/src/trails/survey.ts +332 -25
  32. package/src/trails/topo-history.ts +1 -1
  33. package/src/trails/topo-output-schemas.ts +30 -1
  34. package/src/trails/topo-pin.ts +3 -2
  35. package/src/trails/topo-read-support.ts +49 -8
  36. package/src/trails/topo-reports.ts +39 -22
  37. package/src/trails/topo-store-support.ts +62 -16
  38. package/src/trails/topo-support.ts +1 -1
  39. package/src/trails/topo-unpin.ts +2 -2
  40. package/src/trails/topo.ts +2 -2
  41. package/src/trails/{topo-verify.ts → validate.ts} +7 -7
  42. package/src/trails/version-lifecycle-support.ts +945 -0
  43. package/src/trails/warden-guide.ts +8 -0
  44. package/src/trails/warden.ts +18 -2
  45. package/src/versions.ts +4 -1
@@ -0,0 +1,244 @@
1
+ /**
2
+ * `adapter.check` trail -- Local adapter authoring readiness checks.
3
+ */
4
+
5
+ import { adapterTargetPlacements, checkAdapters } from '@ontrails/adapter-kit';
6
+ import type { AdapterCheckReport } from '@ontrails/adapter-kit';
7
+ import { isPlainObject, Result, trail, ValidationError } from '@ontrails/core';
8
+ import { existsSync, readFileSync, statSync } from 'node:fs';
9
+ import { join, relative } from 'node:path';
10
+ import { z } from 'zod';
11
+
12
+ import { resolveTrailRootDir } from './root-dir.js';
13
+
14
+ const adapterCheckInputSchema = z.object({
15
+ rootDir: z.string().optional().describe('Root directory to scan'),
16
+ });
17
+
18
+ const adapterPlacementSchema = z.enum(adapterTargetPlacements);
19
+
20
+ const adapterCheckDiagnosticSchema = z.object({
21
+ code: z.string().describe('Stable adapter diagnostic code'),
22
+ message: z.string(),
23
+ packageJsonPath: z.string(),
24
+ packageName: z.string().optional(),
25
+ placement: adapterPlacementSchema.optional(),
26
+ severity: z.enum(['error', 'warn']),
27
+ target: z.string().optional(),
28
+ });
29
+
30
+ const adapterCheckSubjectSchema = z.object({
31
+ conformanceTestPaths: z.array(z.string()).readonly(),
32
+ key: z.string(),
33
+ ownerPackage: z.string(),
34
+ packageJsonPath: z.string(),
35
+ packageName: z.string(),
36
+ packageRoot: z.string(),
37
+ placement: adapterPlacementSchema,
38
+ target: z.string(),
39
+ targetKey: z.string(),
40
+ testingImport: z.string().optional(),
41
+ });
42
+
43
+ const adapterTargetSchema = z.object({
44
+ conformance: z
45
+ .object({
46
+ adapterType: z.string(),
47
+ casesFactory: z.string(),
48
+ runner: z.string(),
49
+ })
50
+ .optional(),
51
+ key: z.string(),
52
+ ownerPackage: z.string(),
53
+ packageJsonPath: z.string(),
54
+ packageRoot: z.string(),
55
+ placements: z.array(adapterPlacementSchema).readonly(),
56
+ supportExportTarget: z.string().optional(),
57
+ supportImport: z.string().optional(),
58
+ target: z.string(),
59
+ testingExportTarget: z.string().optional(),
60
+ testingImport: z.string().optional(),
61
+ });
62
+
63
+ const adapterCheckOutputSchema = z.object({
64
+ diagnostics: z.array(adapterCheckDiagnosticSchema).readonly(),
65
+ formatted: z.string(),
66
+ passed: z.boolean(),
67
+ subjects: z.array(adapterCheckSubjectSchema).readonly(),
68
+ targets: z.array(adapterTargetSchema).readonly(),
69
+ });
70
+
71
+ const relativeToRoot = (rootDir: string, path: string): string => {
72
+ const normalized = relative(rootDir, path).replaceAll('\\', '/');
73
+ return normalized.length === 0 || normalized.startsWith('..')
74
+ ? path
75
+ : normalized;
76
+ };
77
+
78
+ const workspacePatternsFromManifest = (
79
+ manifest: Readonly<Record<string, unknown>>
80
+ ): readonly string[] => {
81
+ const { workspaces } = manifest;
82
+ if (Array.isArray(workspaces)) {
83
+ return workspaces.filter(
84
+ (pattern): pattern is string => typeof pattern === 'string'
85
+ );
86
+ }
87
+
88
+ const packages = isPlainObject(workspaces)
89
+ ? workspaces['packages']
90
+ : undefined;
91
+ return Array.isArray(packages)
92
+ ? packages.filter(
93
+ (pattern): pattern is string => typeof pattern === 'string'
94
+ )
95
+ : [];
96
+ };
97
+
98
+ const readWorkspaceManifest = (
99
+ packageJsonPath: string
100
+ ): Result<Readonly<Record<string, unknown>>, ValidationError> => {
101
+ try {
102
+ const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
103
+ return isPlainObject(parsed)
104
+ ? Result.ok(parsed)
105
+ : Result.err(
106
+ new ValidationError(
107
+ `adapter.check root package.json must contain a JSON object: "${packageJsonPath}"`
108
+ )
109
+ );
110
+ } catch (error) {
111
+ return Result.err(
112
+ new ValidationError(
113
+ `adapter.check could not read root package.json: "${packageJsonPath}"`,
114
+ error instanceof Error ? { cause: error } : undefined
115
+ )
116
+ );
117
+ }
118
+ };
119
+
120
+ const validateAdapterCheckRoot = (
121
+ rootDir: string
122
+ ): Result<void, ValidationError> => {
123
+ if (!existsSync(rootDir)) {
124
+ return Result.err(
125
+ new ValidationError(`adapter.check rootDir does not exist: "${rootDir}"`)
126
+ );
127
+ }
128
+
129
+ if (!statSync(rootDir).isDirectory()) {
130
+ return Result.err(
131
+ new ValidationError(
132
+ `adapter.check rootDir must be a directory: "${rootDir}"`
133
+ )
134
+ );
135
+ }
136
+
137
+ const packageJsonPath = join(rootDir, 'package.json');
138
+ if (!existsSync(packageJsonPath)) {
139
+ return Result.err(
140
+ new ValidationError(
141
+ `adapter.check rootDir must contain a package.json workspace manifest: "${packageJsonPath}"`
142
+ )
143
+ );
144
+ }
145
+
146
+ const manifest = readWorkspaceManifest(packageJsonPath);
147
+ if (manifest.isErr()) {
148
+ return Result.err(manifest.error);
149
+ }
150
+
151
+ if (workspacePatternsFromManifest(manifest.value).length === 0) {
152
+ return Result.err(
153
+ new ValidationError(
154
+ `adapter.check root package.json must declare workspace packages: "${packageJsonPath}"`
155
+ )
156
+ );
157
+ }
158
+
159
+ return Result.ok();
160
+ };
161
+
162
+ export const formatAdapterCheckReport = (
163
+ report: AdapterCheckReport,
164
+ rootDir: string
165
+ ): string => {
166
+ const passed = report.diagnostics.length === 0;
167
+ const lines = [
168
+ '## Adapter Check Report',
169
+ '',
170
+ `Result: ${passed ? 'PASS' : 'FAIL'}`,
171
+ `Targets: ${report.targets.length}`,
172
+ `Adapters: ${report.subjects.length}`,
173
+ `Diagnostics: ${report.diagnostics.length}`,
174
+ ];
175
+
176
+ if (report.targets.length > 0) {
177
+ lines.push('', '### Targets');
178
+ for (const target of report.targets) {
179
+ lines.push(
180
+ `- ${target.key} (${target.placements.join(', ')}) from ${relativeToRoot(rootDir, target.packageJsonPath)}`
181
+ );
182
+ }
183
+ }
184
+
185
+ if (report.subjects.length > 0) {
186
+ lines.push('', '### Adapters');
187
+ for (const subject of report.subjects) {
188
+ const conformance =
189
+ subject.conformanceTestPaths.length === 0
190
+ ? 'no conformance tests'
191
+ : `${subject.conformanceTestPaths.length} conformance test(s)`;
192
+ lines.push(
193
+ `- ${subject.packageName} -> ${subject.targetKey} (${subject.placement}, ${conformance})`
194
+ );
195
+ }
196
+ }
197
+
198
+ if (report.diagnostics.length > 0) {
199
+ lines.push('', '### Diagnostics');
200
+ for (const diagnostic of report.diagnostics) {
201
+ lines.push(
202
+ `- ${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${relativeToRoot(rootDir, diagnostic.packageJsonPath)}: ${diagnostic.message}`
203
+ );
204
+ }
205
+ }
206
+
207
+ return lines.join('\n');
208
+ };
209
+
210
+ export const adapterCheckTrail = trail('adapter.check', {
211
+ blaze: (input, ctx) => {
212
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
213
+ if (rootDirResult.isErr()) {
214
+ return rootDirResult;
215
+ }
216
+
217
+ const rootDir = rootDirResult.value;
218
+ const validRoot = validateAdapterCheckRoot(rootDir);
219
+ if (validRoot.isErr()) {
220
+ return validRoot;
221
+ }
222
+
223
+ const report = checkAdapters(rootDir);
224
+
225
+ return Result.ok({
226
+ diagnostics: [...report.diagnostics],
227
+ formatted: formatAdapterCheckReport(report, rootDir),
228
+ passed: report.diagnostics.length === 0,
229
+ subjects: [...report.subjects],
230
+ targets: [...report.targets],
231
+ });
232
+ },
233
+ description: 'Check adapter authoring readiness',
234
+ examples: [
235
+ {
236
+ input: {},
237
+ name: 'Check adapters in the current workspace',
238
+ },
239
+ ],
240
+ input: adapterCheckInputSchema,
241
+ intent: 'read',
242
+ output: adapterCheckOutputSchema,
243
+ permit: 'public',
244
+ });
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { basename, resolve } from 'node:path';
9
9
 
10
- import { AlreadyExistsError, Result, trail } from '@ontrails/core';
10
+ import { Result, trail } from '@ontrails/core';
11
11
  import { z } from 'zod';
12
12
 
13
13
  import {
@@ -21,11 +21,14 @@ import { findTopoPath } from './project.js';
21
21
  type Surface = 'cli' | 'http' | 'mcp';
22
22
 
23
23
  const generateCliEntry = (appImportPath: string): string =>
24
- `import { surface } from '@ontrails/commander';
24
+ `import { devPermitPreset, permitPreset } from '@ontrails/cli';
25
+ import { surface } from '@ontrails/commander';
25
26
 
26
27
  import { app } from '${appImportPath}';
27
28
 
28
- await surface(app);
29
+ await surface(app, {
30
+ presets: [permitPreset(), devPermitPreset()],
31
+ });
29
32
  `;
30
33
 
31
34
  const generateMcpEntry = (appImportPath: string): string =>
@@ -134,29 +137,25 @@ export const addSurface = trail('add.surface', {
134
137
  const entryFile = getEntryFile(surface);
135
138
  const entryExists = projectPathExists(cwd, entryFile);
136
139
  if (entryExists.isErr()) {
137
- return Result.err(entryExists.error);
138
- }
139
-
140
- if (entryExists.value) {
141
- return Result.err(
142
- new AlreadyExistsError(
143
- `${surface.toUpperCase()} surface already exists. Nothing to do.`
144
- )
145
- );
140
+ return entryExists;
146
141
  }
147
142
 
148
- const created = await writeSurfaceEntry(cwd, surface);
149
- if (created.isErr()) {
150
- return Result.err(created.error);
143
+ let created: string | null = null;
144
+ if (!entryExists.value) {
145
+ const written = await writeSurfaceEntry(cwd, surface);
146
+ if (written.isErr()) {
147
+ return written;
148
+ }
149
+ created = entryFile;
151
150
  }
152
151
 
153
152
  const dependency = await updatePkgJsonForSurface(cwd, surface);
154
153
  if (dependency.isErr()) {
155
- return Result.err(dependency.error);
154
+ return dependency;
156
155
  }
157
156
 
158
157
  return Result.ok({
159
- created: created.value,
158
+ created,
160
159
  dependency: dependency.value,
161
160
  });
162
161
  },
@@ -166,7 +165,8 @@ export const addSurface = trail('add.surface', {
166
165
  surface: z.enum(['cli', 'http', 'mcp']).describe('Surface to add'),
167
166
  }),
168
167
  output: z.object({
169
- created: z.string(),
168
+ created: z.string().nullable(),
170
169
  dependency: z.string(),
171
170
  }),
171
+ permit: { scopes: ['project:write'] },
172
172
  });
@@ -83,7 +83,7 @@ export const addTrail = trail('add.trail', {
83
83
  const { id } = input;
84
84
  const validated = validateTrailId(id);
85
85
  if (validated.isErr()) {
86
- return Result.err(validated.error);
86
+ return validated;
87
87
  }
88
88
 
89
89
  const moduleName = trailIdToModuleName(validated.value);
@@ -108,7 +108,7 @@ export const addTrail = trail('add.trail', {
108
108
  for (const [relativePath, content] of files) {
109
109
  const written = await writeProjectFile(cwd, relativePath, content);
110
110
  if (written.isErr()) {
111
- return Result.err(written.error);
111
+ return written;
112
112
  }
113
113
  }
114
114
 
@@ -137,4 +137,5 @@ export const addTrail = trail('add.trail', {
137
137
  output: z.object({
138
138
  created: z.array(z.string()),
139
139
  }),
140
+ permit: { scopes: ['project:write'] },
140
141
  });
@@ -10,6 +10,7 @@ import { z } from 'zod';
10
10
  import {
11
11
  PROJECT_NAME_MESSAGE,
12
12
  PROJECT_NAME_PATTERN,
13
+ projectPathExists,
13
14
  resolveProjectDir,
14
15
  resolveProjectPath,
15
16
  writeProjectFile,
@@ -24,10 +25,24 @@ import {
24
25
  // ---------------------------------------------------------------------------
25
26
 
26
27
  const generateTestFile = (): string =>
27
- `import { testAllEstablished } from '@ontrails/testing';
28
+ `import { testAllEstablished } from '@ontrails/testing/established';
28
29
  import { app } from '../src/app.js';
29
30
 
30
- testAllEstablished(app);
31
+ const permitScopes = [
32
+ ...new Set(
33
+ app
34
+ .list()
35
+ .flatMap((trail) =>
36
+ trail.permit && trail.permit !== 'public' ? trail.permit.scopes : []
37
+ )
38
+ ),
39
+ ];
40
+
41
+ testAllEstablished(app, {
42
+ ctx: {
43
+ permit: { id: 'test-permit', scopes: permitScopes },
44
+ },
45
+ });
31
46
  `;
32
47
 
33
48
  const generateLefthookYml = (): string =>
@@ -79,7 +94,7 @@ export const addVerify = trail('add.verify', {
79
94
  blaze: async (input) => {
80
95
  const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
81
96
  if (projectDirResult.isErr()) {
82
- return Result.err(projectDirResult.error);
97
+ return projectDirResult;
83
98
  }
84
99
 
85
100
  const projectDir = projectDirResult.value;
@@ -89,6 +104,14 @@ export const addVerify = trail('add.verify', {
89
104
  relativePath: string,
90
105
  content: string
91
106
  ): Promise<Result<void, Error>> => {
107
+ const exists = projectPathExists(projectDir, relativePath);
108
+ if (exists.isErr()) {
109
+ return Result.err(exists.error);
110
+ }
111
+ if (exists.value) {
112
+ return Result.ok();
113
+ }
114
+
92
115
  const written = await writeProjectFile(projectDir, relativePath, content);
93
116
  if (written.isErr()) {
94
117
  return Result.err(written.error);
@@ -102,17 +125,17 @@ export const addVerify = trail('add.verify', {
102
125
  generateTestFile()
103
126
  );
104
127
  if (testFile.isErr()) {
105
- return Result.err(testFile.error);
128
+ return testFile;
106
129
  }
107
130
 
108
131
  const lefthookFile = await writeFile('lefthook.yml', generateLefthookYml());
109
132
  if (lefthookFile.isErr()) {
110
- return Result.err(lefthookFile.error);
133
+ return lefthookFile;
111
134
  }
112
135
 
113
136
  const packageResult = await updatePackageJsonForVerify(projectDir);
114
137
  if (packageResult.isErr()) {
115
- return Result.err(packageResult.error);
138
+ return packageResult;
116
139
  }
117
140
 
118
141
  return Result.ok({ created: files });
@@ -128,5 +151,6 @@ export const addVerify = trail('add.verify', {
128
151
  output: z.object({
129
152
  created: z.array(z.string()),
130
153
  }),
154
+ permit: { scopes: ['project:write'] },
131
155
  visibility: 'internal',
132
156
  });
@@ -1,5 +1,5 @@
1
- import { Result, trail } from '@ontrails/core';
2
- import type { Topo } from '@ontrails/core';
1
+ import { trail } from '@ontrails/core';
2
+ import type { Result, Topo } from '@ontrails/core';
3
3
  import { z } from 'zod';
4
4
 
5
5
  import { tryLoadFreshAppLease } from './load-app.js';
@@ -13,23 +13,26 @@ import {
13
13
 
14
14
  export const compileCurrentTopo = async (
15
15
  app: Topo,
16
- options?: { readonly rootDir?: string }
16
+ options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
17
17
  ): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
18
18
 
19
- export const topoCompileTrail = trail('topo.compile', {
19
+ export const compileTrail = trail('compile', {
20
20
  blaze: async (input, ctx) => {
21
21
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
22
22
  if (rootDirResult.isErr()) {
23
- return Result.err(rootDirResult.error);
23
+ return rootDirResult;
24
24
  }
25
25
  const rootDir = rootDirResult.value;
26
26
  const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
27
27
  if (leaseResult.isErr()) {
28
- return Result.err(leaseResult.error);
28
+ return leaseResult;
29
29
  }
30
30
  const lease = leaseResult.value;
31
31
  try {
32
- return await compileCurrentTopo(lease.app, { rootDir });
32
+ return await compileCurrentTopo(lease.app, {
33
+ force: input.force,
34
+ rootDir,
35
+ });
33
36
  } finally {
34
37
  lease.release();
35
38
  }
@@ -37,11 +40,15 @@ export const topoCompileTrail = trail('topo.compile', {
37
40
  description: 'Compile the current topo to .trails artifacts',
38
41
  examples: [
39
42
  {
40
- input: createIsolatedExampleInput('topo-compile'),
43
+ input: createIsolatedExampleInput('compile'),
41
44
  name: 'Compile the current topo artifacts',
42
45
  },
43
46
  ],
44
47
  input: z.object({
48
+ force: z
49
+ .boolean()
50
+ .optional()
51
+ .describe('Record graph-only force events for breaking changes'),
45
52
  module: z.string().optional().describe('Path to the app module'),
46
53
  rootDir: z.string().optional().describe('Workspace root directory'),
47
54
  }),
@@ -52,4 +59,5 @@ export const topoCompileTrail = trail('topo.compile', {
52
59
  snapshot: topoSnapshotOutput,
53
60
  topoPath: z.string(),
54
61
  }),
62
+ permit: { scopes: ['topo:write'] },
55
63
  });
@@ -121,7 +121,7 @@ export const completionsCompleteTrail = trail('completions.__complete', {
121
121
  blaze: async (input, ctx) => {
122
122
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
123
123
  if (rootDirResult.isErr()) {
124
- return Result.err(rootDirResult.error);
124
+ return rootDirResult;
125
125
  }
126
126
  const rootDir = rootDirResult.value;
127
127