@ontrails/trails 1.0.0-beta.24 → 1.0.0-beta.29

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 (48) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/README.md +6 -5
  3. package/package.json +16 -14
  4. package/src/app.ts +42 -3
  5. package/src/cli.ts +17 -3
  6. package/src/local-state-io.ts +20 -0
  7. package/src/mcp-app.ts +10 -0
  8. package/src/mcp-options.ts +2 -3
  9. package/src/project-writes.ts +11 -11
  10. package/src/release/check.ts +41 -25
  11. package/src/release/index.ts +40 -0
  12. package/src/release/native-bun-registry.ts +282 -19
  13. package/src/release/pack-coherence.ts +457 -0
  14. package/src/release/policy.ts +1684 -0
  15. package/src/release/semver.ts +104 -0
  16. package/src/release/wayfinder-dogfood-smoke.ts +600 -66
  17. package/src/run-completions-install.ts +2 -2
  18. package/src/run-schema.ts +41 -0
  19. package/src/run-wayfind-outline.ts +166 -0
  20. package/src/trails/adapter-check.ts +1 -1
  21. package/src/trails/add-surface.ts +1 -1
  22. package/src/trails/add-verify.ts +3 -3
  23. package/src/trails/compile.ts +17 -25
  24. package/src/trails/create-scaffold.ts +14 -14
  25. package/src/trails/create.ts +5 -5
  26. package/src/trails/dev-support.ts +44 -24
  27. package/src/trails/doctor.ts +23 -2
  28. package/src/trails/draft-promote.ts +7 -7
  29. package/src/trails/guide.ts +15 -19
  30. package/src/trails/load-app.ts +58 -9
  31. package/src/trails/operator-context.ts +66 -0
  32. package/src/trails/regrade.ts +72 -0
  33. package/src/trails/release-check.ts +1 -0
  34. package/src/trails/run-example.ts +21 -37
  35. package/src/trails/run-examples.ts +16 -32
  36. package/src/trails/run.ts +31 -44
  37. package/src/trails/survey.ts +76 -62
  38. package/src/trails/topo-output-schemas.ts +11 -0
  39. package/src/trails/topo-pin.ts +6 -20
  40. package/src/trails/topo-read-support.ts +91 -41
  41. package/src/trails/topo-reports.ts +4 -2
  42. package/src/trails/topo-store-support.ts +172 -39
  43. package/src/trails/topo-support.ts +1 -2
  44. package/src/trails/topo.ts +5 -19
  45. package/src/trails/validate.ts +9 -20
  46. package/src/trails/version-lifecycle-support.ts +10 -20
  47. package/src/trails/warden.ts +18 -0
  48. package/src/trails/wayfind.ts +1001 -0
@@ -7,7 +7,7 @@
7
7
  import { NotFoundError, Result, trail } from '@ontrails/core';
8
8
  import { z } from 'zod';
9
9
 
10
- import { tryLoadFreshAppLease } from './load-app.js';
10
+ import { withFreshOperatorApp } from './operator-context.js';
11
11
  import { trailDetailOutput } from './topo-output-schemas.js';
12
12
  import {
13
13
  buildCurrentGuideEntries,
@@ -15,7 +15,6 @@ import {
15
15
  readSurfaceLayerNamesFromContext,
16
16
  } from './topo-read-support.js';
17
17
  import { createIsolatedExampleInput } from './topo-support.js';
18
- import { resolveTrailRootDir } from './root-dir.js';
19
18
 
20
19
  // ---------------------------------------------------------------------------
21
20
  // Types
@@ -28,6 +27,16 @@ interface GuideEntry {
28
27
  readonly kind: 'trail';
29
28
  }
30
29
 
30
+ type GuideTrailOutput =
31
+ | {
32
+ readonly entries: GuideEntry[];
33
+ readonly mode: 'list';
34
+ }
35
+ | {
36
+ readonly detail: z.output<typeof trailDetailOutput>;
37
+ readonly mode: 'detail';
38
+ };
39
+
31
40
  const guideTrailInputSchema = z.object({
32
41
  module: z.string().optional().describe('Path to the app module'),
33
42
  rootDir: z.string().optional().describe('Workspace root directory'),
@@ -41,21 +50,11 @@ type GuideTrailInput = z.output<typeof guideTrailInputSchema>;
41
50
  // ---------------------------------------------------------------------------
42
51
 
43
52
  export const guideTrail = trail('guide', {
44
- blaze: async (input: GuideTrailInput, ctx) => {
45
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
46
- if (rootDirResult.isErr()) {
47
- return rootDirResult;
48
- }
49
- const rootDir = rootDirResult.value;
50
- const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
51
- if (leaseResult.isErr()) {
52
- return leaseResult;
53
- }
54
- const lease = leaseResult.value;
55
-
56
- try {
53
+ blaze: async (input: GuideTrailInput, ctx) =>
54
+ withFreshOperatorApp<GuideTrailOutput>(input, ctx, ({ lease, rootDir }) => {
57
55
  if (input.trailId) {
58
56
  const detail = buildCurrentTopoDetail(lease.app, input.trailId, {
57
+ cliAliases: lease.cliAliases,
59
58
  rootDir,
60
59
  surfaceLayerNames: readSurfaceLayerNamesFromContext(ctx),
61
60
  });
@@ -76,10 +75,7 @@ export const guideTrail = trail('guide', {
76
75
  }) as GuideEntry[],
77
76
  mode: 'list' as const,
78
77
  });
79
- } finally {
80
- lease.release();
81
- }
82
- },
78
+ }),
83
79
  description: 'Runtime guidance for trails',
84
80
  examples: [
85
81
  {
@@ -23,7 +23,7 @@ import {
23
23
  Result,
24
24
  ValidationError,
25
25
  } from '@ontrails/core';
26
- import type { Topo } from '@ontrails/core';
26
+ import type { CliCommandAliasInput, Topo } from '@ontrails/core';
27
27
  import { findAppModule } from '@ontrails/cli';
28
28
 
29
29
  import {
@@ -1054,8 +1054,52 @@ const resolveLoadedTopo = (
1054
1054
  return app;
1055
1055
  };
1056
1056
 
1057
+ type LoadedCliAliases = Readonly<
1058
+ Record<string, readonly CliCommandAliasInput[]>
1059
+ >;
1060
+
1061
+ const isStringArray = (value: unknown): value is readonly string[] =>
1062
+ Array.isArray(value) && value.every((item) => typeof item === 'string');
1063
+
1064
+ const isCliAliasInput = (value: unknown): value is CliCommandAliasInput =>
1065
+ typeof value === 'string' || isStringArray(value);
1066
+
1067
+ const resolveLoadedCliAliases = (
1068
+ effectivePath: string,
1069
+ mod: Record<string, unknown>
1070
+ ): LoadedCliAliases | undefined => {
1071
+ const value = mod['trailsCliAliases'] ?? mod['cliAliases'];
1072
+ if (value === undefined) {
1073
+ return undefined;
1074
+ }
1075
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
1076
+ throw new ValidationError(
1077
+ `CLI alias export in "${effectivePath}" must be a record from trail ID to alias list.`
1078
+ );
1079
+ }
1080
+
1081
+ const aliases = value as Record<string, unknown>;
1082
+ for (const [trailId, trailAliases] of Object.entries(aliases)) {
1083
+ if (!Array.isArray(trailAliases)) {
1084
+ throw new ValidationError(
1085
+ `CLI alias export for trail "${trailId}" in "${effectivePath}" must be an array.`
1086
+ );
1087
+ }
1088
+ for (const alias of trailAliases) {
1089
+ if (!isCliAliasInput(alias)) {
1090
+ throw new ValidationError(
1091
+ `CLI alias export for trail "${trailId}" in "${effectivePath}" must contain string aliases or string-array paths.`
1092
+ );
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ return aliases as LoadedCliAliases;
1098
+ };
1099
+
1057
1100
  export interface FreshAppLease {
1058
1101
  readonly app: Topo;
1102
+ readonly cliAliases?: LoadedCliAliases | undefined;
1059
1103
  readonly mirrorRoot: string;
1060
1104
  readonly release: () => void;
1061
1105
  }
@@ -1071,14 +1115,18 @@ const noopRelease = (): void => undefined;
1071
1115
  const createUrlSchemeLease = async (
1072
1116
  absolutePath: string,
1073
1117
  effectivePath: string
1074
- ): Promise<FreshAppLease> => ({
1075
- app: resolveLoadedTopo(
1076
- effectivePath,
1077
- await importWithCacheBust(absolutePath)
1078
- ),
1079
- mirrorRoot: absolutePath,
1080
- release: noopRelease,
1081
- });
1118
+ ): Promise<FreshAppLease> => {
1119
+ const mod = (await importWithCacheBust(absolutePath)) as Record<
1120
+ string,
1121
+ unknown
1122
+ >;
1123
+ return {
1124
+ app: resolveLoadedTopo(effectivePath, mod),
1125
+ cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1126
+ mirrorRoot: absolutePath,
1127
+ release: noopRelease,
1128
+ };
1129
+ };
1082
1130
 
1083
1131
  const createFilesystemLease = async (
1084
1132
  absolutePath: string,
@@ -1096,6 +1144,7 @@ const createFilesystemLease = async (
1096
1144
 
1097
1145
  return {
1098
1146
  app: resolveLoadedTopo(effectivePath, mod),
1147
+ cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1099
1148
  mirrorRoot,
1100
1149
  release,
1101
1150
  };
@@ -0,0 +1,66 @@
1
+ import type { Result } from '@ontrails/core';
2
+
3
+ import { tryLoadFreshAppLease } from './load-app.js';
4
+ import type { FreshAppLease } from './load-app.js';
5
+ import { resolveTrailRootDir } from './root-dir.js';
6
+
7
+ interface RootDirInput {
8
+ readonly rootDir?: string | undefined;
9
+ }
10
+
11
+ interface FreshAppInput extends RootDirInput {
12
+ readonly module?: string | undefined;
13
+ }
14
+
15
+ interface RootDirContext {
16
+ readonly cwd?: string | undefined;
17
+ }
18
+
19
+ interface FreshAppContext {
20
+ readonly lease: FreshAppLease;
21
+ readonly rootDir: string;
22
+ }
23
+
24
+ export const withOperatorRootDir = async <T>(
25
+ input: RootDirInput,
26
+ ctx: RootDirContext,
27
+ consume: (rootDir: string) => Result<T, Error> | Promise<Result<T, Error>>
28
+ ): Promise<Result<T, Error>> => {
29
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
30
+ if (rootDirResult.isErr()) {
31
+ return rootDirResult;
32
+ }
33
+ return await consume(rootDirResult.value);
34
+ };
35
+
36
+ export const withFreshAppLease = async <T>(
37
+ modulePath: string | undefined,
38
+ rootDir: string,
39
+ consume: (
40
+ lease: FreshAppLease
41
+ ) => Result<T, Error> | Promise<Result<T, Error>>
42
+ ): Promise<Result<T, Error>> => {
43
+ const leaseResult = await tryLoadFreshAppLease(modulePath, rootDir);
44
+ if (leaseResult.isErr()) {
45
+ return leaseResult;
46
+ }
47
+ const lease = leaseResult.value;
48
+ try {
49
+ return await consume(lease);
50
+ } finally {
51
+ lease.release();
52
+ }
53
+ };
54
+
55
+ export const withFreshOperatorApp = async <T>(
56
+ input: FreshAppInput,
57
+ ctx: RootDirContext,
58
+ consume: (
59
+ context: FreshAppContext
60
+ ) => Result<T, Error> | Promise<Result<T, Error>>
61
+ ): Promise<Result<T, Error>> =>
62
+ withOperatorRootDir(input, ctx, (rootDir) =>
63
+ withFreshAppLease(input.module, rootDir, (lease) =>
64
+ consume({ lease, rootDir })
65
+ )
66
+ );
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `regrade` trail -- Run downstream migration checks and safe rewrites.
3
+ */
4
+
5
+ import { NotFoundError, Result, trail, validateOutput } from '@ontrails/core';
6
+ import type { Result as TrailsResult } from '@ontrails/core';
7
+ import {
8
+ regradeReportOutput,
9
+ runRegrade,
10
+ wardenTermRewriteClasses,
11
+ } from '@ontrails/regrade';
12
+ import type { RegradeReport } from '@ontrails/regrade';
13
+ import { z } from 'zod';
14
+
15
+ import { resolveTrailRootDir } from './root-dir.js';
16
+
17
+ const regradeInputSchema = z.object({
18
+ apply: z
19
+ .boolean()
20
+ .default(false)
21
+ .describe('Write safe rewrites to disk; dry-run report only by default'),
22
+ classIds: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe('Regrade class ids to run (defaults to all built-in classes)'),
26
+ rootDir: z.string().optional().describe('Workspace root directory'),
27
+ });
28
+
29
+ export const regradeTrail = trail('regrade', {
30
+ blaze: (input, ctx) => {
31
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
32
+ if (rootDirResult.isErr()) {
33
+ return rootDirResult;
34
+ }
35
+
36
+ const reportResult: TrailsResult<RegradeReport | null, Error> = runRegrade({
37
+ apply: input.apply,
38
+ classes: wardenTermRewriteClasses,
39
+ root: rootDirResult.value,
40
+ ...(input.classIds === undefined
41
+ ? {}
42
+ : { selection: { classIds: input.classIds } }),
43
+ });
44
+ if (reportResult.isErr()) {
45
+ return reportResult;
46
+ }
47
+
48
+ const report = reportResult.value;
49
+ if (report === null) {
50
+ return Result.err(
51
+ new NotFoundError(
52
+ `Regrade root "${rootDirResult.value}" could not be read as a directory.`
53
+ )
54
+ );
55
+ }
56
+
57
+ const validated: TrailsResult<
58
+ z.output<typeof regradeReportOutput>,
59
+ Error
60
+ > = validateOutput(regradeReportOutput, report);
61
+ if (validated.isErr()) {
62
+ return validated;
63
+ }
64
+
65
+ return Result.ok(validated.value);
66
+ },
67
+ description: 'Run downstream migration checks and safe rewrites',
68
+ input: regradeInputSchema,
69
+ intent: 'write',
70
+ output: regradeReportOutput,
71
+ permit: 'public',
72
+ });
@@ -45,6 +45,7 @@ const contractReleaseFactSchema = z.object({
45
45
  });
46
46
 
47
47
  const releaseCheckOutputSchema = z.object({
48
+ activePackageChangesetsWithoutReleaseFacts: z.array(z.string()).readonly(),
48
49
  affectedPackages: z.array(z.string()).readonly(),
49
50
  changedChangesets: z.array(z.string()).readonly(),
50
51
  configPath: z.string().optional(),
@@ -13,9 +13,8 @@ import {
13
13
  import type { BasePermit, StructuredTrailExample, Topo } from '@ontrails/core';
14
14
  import { z } from 'zod';
15
15
 
16
- import { tryLoadFreshAppLease } from './load-app.js';
16
+ import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
17
17
  import { resolveRunModulePath } from './run.js';
18
- import { resolveTrailRootDir } from './root-dir.js';
19
18
  import { createIsolatedExampleInput } from './topo-support.js';
20
19
 
21
20
  export const RUN_EXAMPLE_COMPARISON_KIND = 'example-comparison' as const;
@@ -355,7 +354,7 @@ const buildComparisonEnvelope = async (
355
354
  ): Promise<Result<RunExampleComparison, Error>> => {
356
355
  const exampleResult = findExample(app, trailId, exampleName);
357
356
  if (exampleResult.isErr()) {
358
- return Result.err(exampleResult.error);
357
+ return exampleResult;
359
358
  }
360
359
  const example = exampleResult.value;
361
360
  const mode = determineMode(example);
@@ -440,42 +439,27 @@ type RunExampleTrailInput = z.output<typeof runExampleTrailInputSchema>;
440
439
 
441
440
  export const runExampleTrail = trail('run.example', {
442
441
  args: ['id', 'exampleName'],
443
- blaze: async (input: RunExampleTrailInput, ctx) => {
444
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
445
- if (rootDirResult.isErr()) {
446
- return rootDirResult;
447
- }
448
- const rootDir = rootDirResult.value;
449
- const moduleResolution = await resolveRunModulePath(
450
- rootDir,
451
- input.module,
452
- input.id,
453
- input.app
454
- );
455
- if (moduleResolution.isErr()) {
456
- return moduleResolution;
457
- }
458
-
459
- const leaseResult = await tryLoadFreshAppLease(
460
- moduleResolution.value,
461
- rootDir
462
- );
463
- if (leaseResult.isErr()) {
464
- return leaseResult;
465
- }
466
- const lease = leaseResult.value;
467
-
468
- try {
469
- return await buildComparisonEnvelope(
470
- lease.app,
442
+ blaze: async (input: RunExampleTrailInput, ctx) =>
443
+ withOperatorRootDir(input, ctx, async (rootDir) => {
444
+ const moduleResolution = await resolveRunModulePath(
445
+ rootDir,
446
+ input.module,
471
447
  input.id,
472
- input.exampleName,
473
- ctx.permit
448
+ input.app
474
449
  );
475
- } finally {
476
- lease.release();
477
- }
478
- },
450
+ if (moduleResolution.isErr()) {
451
+ return moduleResolution;
452
+ }
453
+
454
+ return withFreshAppLease(moduleResolution.value, rootDir, (lease) =>
455
+ buildComparisonEnvelope(
456
+ lease.app,
457
+ input.id,
458
+ input.exampleName,
459
+ ctx.permit
460
+ )
461
+ );
462
+ }),
479
463
  description: 'Run a named example on a trail and compare actual vs expected',
480
464
  examples: [
481
465
  {
@@ -11,9 +11,8 @@ import {
11
11
  import type { StructuredTrailExample, Topo } from '@ontrails/core';
12
12
  import { z } from 'zod';
13
13
 
14
- import { tryLoadFreshAppLease } from './load-app.js';
14
+ import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
15
15
  import { resolveRunModulePath } from './run.js';
16
- import { resolveTrailRootDir } from './root-dir.js';
17
16
  import { createIsolatedExampleInput } from './topo-support.js';
18
17
 
19
18
  export const RUN_EXAMPLES_LISTING_KIND = 'examples-listing' as const;
@@ -100,37 +99,22 @@ type RunExamplesTrailInput = z.output<typeof runExamplesTrailInputSchema>;
100
99
 
101
100
  export const runExamplesTrail = trail('run.examples', {
102
101
  args: ['id'],
103
- blaze: async (input: RunExamplesTrailInput, ctx) => {
104
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
105
- if (rootDirResult.isErr()) {
106
- return rootDirResult;
107
- }
108
- const rootDir = rootDirResult.value;
109
- const moduleResolution = await resolveRunModulePath(
110
- rootDir,
111
- input.module,
112
- input.id,
113
- input.app
114
- );
115
- if (moduleResolution.isErr()) {
116
- return moduleResolution;
117
- }
118
-
119
- const leaseResult = await tryLoadFreshAppLease(
120
- moduleResolution.value,
121
- rootDir
122
- );
123
- if (leaseResult.isErr()) {
124
- return leaseResult;
125
- }
126
- const lease = leaseResult.value;
102
+ blaze: async (input: RunExamplesTrailInput, ctx) =>
103
+ withOperatorRootDir(input, ctx, async (rootDir) => {
104
+ const moduleResolution = await resolveRunModulePath(
105
+ rootDir,
106
+ input.module,
107
+ input.id,
108
+ input.app
109
+ );
110
+ if (moduleResolution.isErr()) {
111
+ return moduleResolution;
112
+ }
127
113
 
128
- try {
129
- return buildExamplesListing(lease.app, input.id);
130
- } finally {
131
- lease.release();
132
- }
133
- },
114
+ return withFreshAppLease(moduleResolution.value, rootDir, (lease) =>
115
+ buildExamplesListing(lease.app, input.id)
116
+ );
117
+ }),
134
118
  description: "List a trail's examples without executing it",
135
119
  examples: [
136
120
  {
package/src/trails/run.ts CHANGED
@@ -50,8 +50,7 @@ import {
50
50
  writeIsolatedExampleTextFile,
51
51
  } from '../local-state-io.js';
52
52
 
53
- import { tryLoadFreshAppLease } from './load-app.js';
54
- import { resolveTrailRootDir } from './root-dir.js';
53
+ import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
55
54
  import { createIsolatedExampleInput } from './topo-support.js';
56
55
 
57
56
  export const INNER_TRAIL_RESULT_KIND = 'inner-trail-result' as const;
@@ -372,51 +371,39 @@ const resolveInnerTrailInput = (
372
371
 
373
372
  export const runTrail = trail('run', {
374
373
  args: ['id'],
375
- blaze: async (input: RunTrailInput, ctx) => {
376
- const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
377
- if (rootDirResult.isErr()) {
378
- return rootDirResult;
379
- }
380
- const rootDir = rootDirResult.value;
381
- const innerInput = resolveInnerTrailInput(input);
382
- if (innerInput.isErr()) {
383
- return innerInput;
384
- }
385
-
386
- // Single-app back-compat: if the caller provided `module`, trust it.
387
- const moduleResolution = await resolveRunModulePath(
388
- rootDir,
389
- input.module,
390
- input.id,
391
- input.app
392
- );
393
- if (moduleResolution.isErr()) {
394
- return moduleResolution;
395
- }
396
- const modulePath = moduleResolution.value;
397
-
398
- const leaseResult = await tryLoadFreshAppLease(modulePath, rootDir);
399
- if (leaseResult.isErr()) {
400
- return leaseResult;
401
- }
402
- const lease = leaseResult.value;
374
+ blaze: async (input: RunTrailInput, ctx) =>
375
+ withOperatorRootDir(input, ctx, async (rootDir) => {
376
+ const innerInput = resolveInnerTrailInput(input);
377
+ if (innerInput.isErr()) {
378
+ return innerInput;
379
+ }
403
380
 
404
- try {
405
- const result = await run(lease.app, input.id, innerInput.value, {
406
- ctx: ctx.permit === undefined ? {} : { permit: ctx.permit },
407
- });
408
- if (result.isErr()) {
409
- return Result.err(result.error);
381
+ // Single-app back-compat: if the caller provided `module`, trust it.
382
+ const moduleResolution = await resolveRunModulePath(
383
+ rootDir,
384
+ input.module,
385
+ input.id,
386
+ input.app
387
+ );
388
+ if (moduleResolution.isErr()) {
389
+ return moduleResolution;
410
390
  }
411
- return Result.ok({
412
- kind: INNER_TRAIL_RESULT_KIND,
413
- trailId: input.id,
414
- value: result.value,
391
+ const modulePath = moduleResolution.value;
392
+
393
+ return withFreshAppLease(modulePath, rootDir, async (lease) => {
394
+ const result = await run(lease.app, input.id, innerInput.value, {
395
+ ctx: ctx.permit === undefined ? {} : { permit: ctx.permit },
396
+ });
397
+ if (result.isErr()) {
398
+ return result;
399
+ }
400
+ return Result.ok({
401
+ kind: INNER_TRAIL_RESULT_KIND,
402
+ trailId: input.id,
403
+ value: result.value,
404
+ });
415
405
  });
416
- } finally {
417
- lease.release();
418
- }
419
- },
406
+ }),
420
407
  description:
421
408
  'Resolve a trail by ID in the current app and execute it through the shared pipeline',
422
409
  examples: [