@ontrails/trails 1.0.0-beta.30 → 1.0.0-beta.32

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.
@@ -2,19 +2,41 @@
2
2
  * `regrade` trail -- Run downstream migration checks and safe rewrites.
3
3
  */
4
4
 
5
- import { NotFoundError, Result, trail, validateOutput } from '@ontrails/core';
6
- import type { Result as TrailsResult } from '@ontrails/core';
7
5
  import {
6
+ InternalError,
7
+ NotFoundError,
8
+ Result,
9
+ ValidationError,
10
+ pathScopeSchema,
11
+ trail,
12
+ validateOutput,
13
+ } from '@ontrails/core';
14
+ import type { PathScope, Result as TrailsResult } from '@ontrails/core';
15
+ import {
16
+ loadWardenTermRewriteClasses,
8
17
  regradeReportOutput,
9
18
  runRegrade,
10
- wardenTermRewriteClasses,
19
+ runVocabularyRegrade,
11
20
  } from '@ontrails/regrade';
12
- import type { RegradeReport } from '@ontrails/regrade';
21
+ import type { RegradeReport, VocabularyRegradePlan } from '@ontrails/regrade';
13
22
  import { z } from 'zod';
14
23
 
24
+ import { loadRegradeConfig } from '../regrade/config.js';
15
25
  import { resolveTrailRootDir } from './root-dir.js';
16
26
 
17
- const regradeInputSchema = z.object({
27
+ const regradePathScopeInputSchema = pathScopeSchema.extend({
28
+ exclude: pathScopeSchema.shape.exclude.describe(
29
+ 'Root-relative path globs to exclude during Regrade collection'
30
+ ),
31
+ extensions: pathScopeSchema.shape.extensions.describe(
32
+ 'Source file extensions to scan during Regrade collection'
33
+ ),
34
+ include: pathScopeSchema.shape.include.describe(
35
+ 'Root-relative path patterns to include in vocabulary regrade mode'
36
+ ),
37
+ });
38
+
39
+ const regradeInputSchema = regradePathScopeInputSchema.extend({
18
40
  apply: z
19
41
  .boolean()
20
42
  .default(false)
@@ -23,19 +45,217 @@ const regradeInputSchema = z.object({
23
45
  .array(z.string())
24
46
  .optional()
25
47
  .describe('Regrade class ids to run (defaults to all built-in classes)'),
48
+ configPath: z
49
+ .string()
50
+ .optional()
51
+ .describe('Path to a Trails config file with regrade defaults'),
52
+ from: z
53
+ .string()
54
+ .min(1)
55
+ .optional()
56
+ .describe('Source vocabulary term for a vocabulary regrade'),
57
+ includeEntries: z
58
+ .enum(['actionable', 'all'])
59
+ .default('actionable')
60
+ .describe(
61
+ 'Report entry detail to include; counts always cover the full run'
62
+ ),
63
+ intent: z
64
+ .string()
65
+ .optional()
66
+ .describe('Human-authored migration intent for a vocabulary regrade'),
67
+ overrides: z
68
+ .record(z.string().min(1), z.string().min(1))
69
+ .optional()
70
+ .describe('Explicit source-form to target-form mappings'),
71
+ preserve: z
72
+ .array(z.string().min(1))
73
+ .optional()
74
+ .describe(
75
+ 'Regex or literal contexts to preserve during a vocabulary regrade'
76
+ ),
26
77
  rootDir: z.string().optional().describe('Workspace root directory'),
78
+ to: z
79
+ .string()
80
+ .min(1)
81
+ .optional()
82
+ .describe('Target vocabulary term for a vocabulary regrade'),
27
83
  });
28
84
 
85
+ const hasVocabularyInput = (input: z.output<typeof regradeInputSchema>) =>
86
+ input.from !== undefined ||
87
+ input.include !== undefined ||
88
+ input.intent !== undefined ||
89
+ input.overrides !== undefined ||
90
+ input.preserve !== undefined ||
91
+ input.to !== undefined;
92
+
93
+ const classModeCollection = (
94
+ input: z.output<typeof regradeInputSchema>,
95
+ configScope?: RegradeConfigScope | undefined
96
+ ):
97
+ | {
98
+ readonly exclude?: readonly string[];
99
+ readonly extensions?: readonly string[];
100
+ }
101
+ | undefined => {
102
+ if (
103
+ configScope?.exclude === undefined &&
104
+ configScope?.extensions === undefined &&
105
+ input.exclude === undefined &&
106
+ input.extensions === undefined
107
+ ) {
108
+ return undefined;
109
+ }
110
+
111
+ return {
112
+ ...(configScope?.exclude === undefined
113
+ ? {}
114
+ : { exclude: configScope.exclude }),
115
+ ...(configScope?.extensions === undefined
116
+ ? {}
117
+ : { extensions: configScope.extensions }),
118
+ ...(input.exclude === undefined ? {} : { exclude: input.exclude }),
119
+ ...(input.extensions === undefined ? {} : { extensions: input.extensions }),
120
+ };
121
+ };
122
+
123
+ interface RegradeConfigScope {
124
+ readonly exclude?: PathScope['exclude'] | undefined;
125
+ readonly extensions?: PathScope['extensions'] | undefined;
126
+ readonly include?: PathScope['include'] | undefined;
127
+ }
128
+
129
+ const vocabularyScopeFromConfig = (
130
+ scope: RegradeConfigScope | undefined
131
+ ): VocabularyRegradePlan['scope'] | undefined =>
132
+ scope === undefined
133
+ ? undefined
134
+ : {
135
+ ...(scope.exclude === undefined ? {} : { exclude: scope.exclude }),
136
+ ...(scope.extensions === undefined
137
+ ? {}
138
+ : { extensions: scope.extensions }),
139
+ ...(scope.include === undefined ? {} : { include: scope.include }),
140
+ };
141
+
142
+ const buildVocabularyPlan = (
143
+ input: z.output<typeof regradeInputSchema>,
144
+ configScope?: VocabularyRegradePlan['scope']
145
+ ): TrailsResult<VocabularyRegradePlan, ValidationError> => {
146
+ if (input.from === undefined || input.to === undefined) {
147
+ return Result.err(
148
+ new ValidationError('A vocabulary regrade requires both `from` and `to`.')
149
+ );
150
+ }
151
+ if (input.classIds !== undefined) {
152
+ return Result.err(
153
+ new ValidationError(
154
+ '`classIds` selects class-mode Regrade and cannot be combined with vocabulary-regrade `from`/`to`.'
155
+ )
156
+ );
157
+ }
158
+
159
+ return Result.ok({
160
+ from: input.from,
161
+ id: `vocabulary:${input.from}->${input.to}`,
162
+ kind: 'vocabulary',
163
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
164
+ ...(input.overrides === undefined ? {} : { overrides: input.overrides }),
165
+ ...(input.preserve === undefined
166
+ ? {}
167
+ : {
168
+ preserve: input.preserve.map((pattern) => ({
169
+ pattern,
170
+ reason: 'preserved-by-operator-input',
171
+ })),
172
+ }),
173
+ scope: {
174
+ ...configScope,
175
+ ...(input.exclude === undefined ? {} : { exclude: input.exclude }),
176
+ ...(input.extensions === undefined
177
+ ? {}
178
+ : { extensions: input.extensions }),
179
+ ...(input.include === undefined ? {} : { include: input.include }),
180
+ },
181
+ to: input.to,
182
+ });
183
+ };
184
+
29
185
  export const regradeTrail = trail('regrade', {
30
- blaze: (input, ctx) => {
186
+ args: ['from', 'to'],
187
+ blaze: async (input, ctx) => {
31
188
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
32
189
  if (rootDirResult.isErr()) {
33
190
  return rootDirResult;
34
191
  }
35
192
 
193
+ const configResult = await loadRegradeConfig({
194
+ ...(input.configPath === undefined
195
+ ? {}
196
+ : { configPath: input.configPath }),
197
+ env: ctx.env,
198
+ rootDir: rootDirResult.value,
199
+ });
200
+ if (configResult.isErr()) {
201
+ return configResult;
202
+ }
203
+ const configScope = configResult.value.config?.scope;
204
+
205
+ if (hasVocabularyInput(input)) {
206
+ const planResult = buildVocabularyPlan(
207
+ input,
208
+ vocabularyScopeFromConfig(configScope)
209
+ );
210
+ if (planResult.isErr()) {
211
+ return planResult;
212
+ }
213
+ const reportResult: TrailsResult<RegradeReport | null, Error> =
214
+ runVocabularyRegrade({
215
+ apply: input.apply,
216
+ includeEntries: input.includeEntries,
217
+ plan: planResult.value,
218
+ root: rootDirResult.value,
219
+ });
220
+ if (reportResult.isErr()) {
221
+ return reportResult;
222
+ }
223
+ const report = reportResult.value;
224
+ if (report === null) {
225
+ return Result.err(
226
+ new NotFoundError(
227
+ `Regrade root "${rootDirResult.value}" could not be read as a directory.`
228
+ )
229
+ );
230
+ }
231
+ const validated: TrailsResult<
232
+ z.output<typeof regradeReportOutput>,
233
+ Error
234
+ > = validateOutput(regradeReportOutput, report);
235
+ if (validated.isErr()) {
236
+ return validated;
237
+ }
238
+ return Result.ok(validated.value);
239
+ }
240
+
241
+ const classSet = await loadWardenTermRewriteClasses(rootDirResult.value);
242
+ if (classSet.diagnostics.length > 0) {
243
+ return Result.err(
244
+ new InternalError('Failed to load Regrade project Warden rules.', {
245
+ context: {
246
+ diagnostics: classSet.diagnostics,
247
+ rootDir: rootDirResult.value,
248
+ },
249
+ })
250
+ );
251
+ }
252
+
253
+ const collection = classModeCollection(input, configScope);
36
254
  const reportResult: TrailsResult<RegradeReport | null, Error> = runRegrade({
37
255
  apply: input.apply,
38
- classes: wardenTermRewriteClasses,
256
+ classes: classSet.classes,
257
+ ...(collection === undefined ? {} : { collection }),
258
+ includeEntries: input.includeEntries,
39
259
  root: rootDirResult.value,
40
260
  ...(input.classIds === undefined
41
261
  ? {}
@@ -44,7 +44,7 @@ export const reviseTrail = trail('revise', {
44
44
  }),
45
45
  intent: 'write',
46
46
  output: z.object({
47
- file: z.string(),
47
+ filePath: z.string(),
48
48
  trailId: z.string(),
49
49
  updated: z.boolean(),
50
50
  warnings: z.array(z.string()).optional(),
@@ -30,12 +30,12 @@ import { writeIsolatedExampleJsonFile } from '../local-state-io.js';
30
30
 
31
31
  import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
32
32
  import {
33
- buildCurrentTopoBrief,
34
- buildCurrentTopoList,
35
- buildCurrentTopoMatches,
36
- buildCurrentTrailDetail,
37
- buildCurrentResourceDetail,
38
- buildCurrentSignalDetail,
33
+ deriveCurrentTopoBrief,
34
+ deriveCurrentTopoList,
35
+ deriveCurrentTopoMatches,
36
+ deriveCurrentTrailDetail,
37
+ deriveCurrentResourceDetail,
38
+ deriveCurrentSignalDetail,
39
39
  readSurfaceLayerNamesFromContext,
40
40
  } from './topo-read-support.js';
41
41
  import {
@@ -511,7 +511,7 @@ const buildSurveyLookup = (
511
511
  | undefined,
512
512
  surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
513
513
  ): Result<object, Error> => {
514
- const matches = buildCurrentTopoMatches(app, entityId, {
514
+ const matches = deriveCurrentTopoMatches(app, entityId, {
515
515
  cliAliases,
516
516
  rootDir,
517
517
  surfaceLayerNames,
@@ -528,7 +528,7 @@ const buildSurveyTrailDetail = (
528
528
  | undefined,
529
529
  surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
530
530
  ): Result<object, Error> => {
531
- const detail = buildCurrentTrailDetail(app, id, {
531
+ const detail = deriveCurrentTrailDetail(app, id, {
532
532
  cliAliases,
533
533
  rootDir,
534
534
  surfaceLayerNames,
@@ -543,7 +543,7 @@ const buildSurveyResourceDetail = (
543
543
  id: string,
544
544
  rootDir: string
545
545
  ): Result<object, Error> => {
546
- const detail = buildCurrentResourceDetail(app, id, { rootDir });
546
+ const detail = deriveCurrentResourceDetail(app, id, { rootDir });
547
547
  return detail === undefined
548
548
  ? Result.err(new NotFoundError(`Resource not found: ${id}`))
549
549
  : Result.ok(detail);
@@ -554,7 +554,7 @@ const buildSurveySignalDetail = (
554
554
  id: string,
555
555
  rootDir: string
556
556
  ): Result<object, Error> => {
557
- const detail = buildCurrentSignalDetail(app, id, { rootDir });
557
+ const detail = deriveCurrentSignalDetail(app, id, { rootDir });
558
558
  return detail === undefined
559
559
  ? Result.err(new NotFoundError(`Signal not found: ${id}`))
560
560
  : Result.ok(detail);
@@ -600,7 +600,7 @@ const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
600
600
  surfaceLayerNames
601
601
  ),
602
602
  overview: (app, _input, rootDir) =>
603
- Result.ok(buildCurrentTopoList(app, { rootDir })),
603
+ Result.ok(deriveCurrentTopoList(app, { rootDir })),
604
604
  };
605
605
 
606
606
  const envelopeSurveyValue = (
@@ -824,7 +824,7 @@ export const surveyTrail = trail('survey', {
824
824
  export const surveyBriefTrail = trail('survey.brief', {
825
825
  blaze: async (input, ctx) =>
826
826
  withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
827
- Result.ok(buildCurrentTopoBrief(app, { rootDir }))
827
+ Result.ok(deriveCurrentTopoBrief(app, { rootDir }))
828
828
  ),
829
829
  description: 'Summarize topo capabilities',
830
830
  examples: [