@ontrails/regrade 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,150 @@
1
+ # @ontrails/regrade
2
+
3
+ ## 1.0.0-beta.29
4
+
5
+ ### Patch Changes
6
+
7
+ - @ontrails/core@1.0.0-beta.29
8
+ - @ontrails/warden@1.0.0-beta.29
9
+
10
+ ## 1.0.0-beta.28
11
+
12
+ ### Patch Changes
13
+
14
+ - @ontrails/core@1.0.0-beta.28
15
+ - @ontrails/warden@1.0.0-beta.28
16
+
17
+ ## 1.0.0-beta.27
18
+
19
+ ### Patch Changes
20
+
21
+ - @ontrails/core@1.0.0-beta.27
22
+ - @ontrails/warden@1.0.0-beta.27
23
+
24
+ ## 1.0.0-beta.26
25
+
26
+ ### Patch Changes
27
+
28
+ - 4e75b85: Carry structured review details from Warden-backed term-rewrite diagnostics into Regrade review reports.
29
+ - Updated dependencies [1307568]
30
+ - Updated dependencies [ef09e46]
31
+ - Updated dependencies [38cd9d6]
32
+ - Updated dependencies [f8403c4]
33
+ - Updated dependencies [371d19e]
34
+ - Updated dependencies [ff48e41]
35
+ - @ontrails/core@1.0.0-beta.26
36
+ - @ontrails/warden@1.0.0-beta.26
37
+
38
+ ## 1.0.0-beta.25
39
+
40
+ ### Patch Changes
41
+
42
+ - b991263: Retire the package-owned `regrade.downstream.report` trail wrapper so the Trails operator app owns the public Regrade surface while `@ontrails/regrade` exposes the reusable engine APIs and report schema.
43
+ - c36aca9: Preserve existing Result error boundaries directly and widen Warden pass-through
44
+ coaching beyond trail blazes.
45
+ - 6250729: Expands the public AST guard/accessor surface and migrates Warden/Regrade AST
46
+ consumers onto the typed helpers instead of rule-local node-field casts.
47
+ - f757cd7: Publish Regrade's downstream report and AST rewrite APIs, and expose a dry-run
48
+ by default `trails regrade` operator command with explicit apply mode.
49
+ - Updated dependencies [a9fdbc7]
50
+ - Updated dependencies [f8fd6ca]
51
+ - Updated dependencies [0fcc42b]
52
+ - Updated dependencies [c36aca9]
53
+ - Updated dependencies [f556559]
54
+ - Updated dependencies [6250729]
55
+ - Updated dependencies [d73c38e]
56
+ - Updated dependencies [3befcf1]
57
+ - Updated dependencies [a8e4dc3]
58
+ - Updated dependencies [a4f9cf6]
59
+ - Updated dependencies [9bcf34e]
60
+ - Updated dependencies [00c0cf8]
61
+ - Updated dependencies [b313c58]
62
+ - Updated dependencies [f245fa0]
63
+ - Updated dependencies [f1e6efa]
64
+ - Updated dependencies [caff950]
65
+ - Updated dependencies [df13faf]
66
+ - @ontrails/warden@1.0.0-beta.25
67
+ - @ontrails/core@1.0.0-beta.25
68
+
69
+ ## 1.0.0-beta.24
70
+
71
+ ### Patch Changes
72
+
73
+ - @ontrails/core@1.0.0-beta.24
74
+ - @ontrails/warden@1.0.0-beta.24
75
+
76
+ ## 1.0.0-beta.23
77
+
78
+ ### Patch Changes
79
+
80
+ - @ontrails/core@1.0.0-beta.23
81
+ - @ontrails/warden@1.0.0-beta.23
82
+
83
+ ## 1.0.0-beta.22
84
+
85
+ ### Patch Changes
86
+
87
+ - @ontrails/core@1.0.0-beta.22
88
+ - @ontrails/warden@1.0.0-beta.22
89
+
90
+ ## 1.0.0-beta.21
91
+
92
+ ### Patch Changes
93
+
94
+ - Updated dependencies [99523f2]
95
+ - Updated dependencies [5be032c]
96
+ - @ontrails/core@1.0.0-beta.21
97
+ - @ontrails/warden@1.0.0-beta.21
98
+
99
+ ## 1.0.0-beta.20
100
+
101
+ ### Patch Changes
102
+
103
+ - Updated dependencies [851a2a3]
104
+ - Updated dependencies [8bc0708]
105
+ - Updated dependencies [6901776]
106
+ - @ontrails/core@1.0.0-beta.20
107
+ - @ontrails/warden@1.0.0-beta.20
108
+
109
+ ## 1.0.0-beta.19
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies [e41c382]
114
+ - Updated dependencies [1eb5bdc]
115
+ - Updated dependencies [f8d80b9]
116
+ - Updated dependencies [846a597]
117
+ - Updated dependencies [f0f7e2f]
118
+ - Updated dependencies [223aaad]
119
+ - Updated dependencies [3125f4d]
120
+ - Updated dependencies [2494dc6]
121
+ - Updated dependencies [120caf5]
122
+ - Updated dependencies [2d53717]
123
+ - Updated dependencies [16cb740]
124
+ - Updated dependencies [8894ecb]
125
+ - Updated dependencies [fdf7ec9]
126
+ - Updated dependencies [d76be13]
127
+ - Updated dependencies [84f56a5]
128
+ - Updated dependencies [64fb15a]
129
+ - Updated dependencies [431b04c]
130
+ - Updated dependencies [5d88104]
131
+ - Updated dependencies [f04a9ef]
132
+ - Updated dependencies [1c975c3]
133
+ - Updated dependencies [48d5ff4]
134
+ - Updated dependencies [d5d518e]
135
+ - Updated dependencies [216bf10]
136
+ - Updated dependencies [678cb1c]
137
+ - Updated dependencies [5874fd6]
138
+ - Updated dependencies [619cb15]
139
+ - Updated dependencies [4642268]
140
+ - Updated dependencies [9bab0cf]
141
+ - Updated dependencies [3ceeba8]
142
+ - Updated dependencies [beafd03]
143
+ - Updated dependencies [7b173e0]
144
+ - Updated dependencies [6e50e7b]
145
+ - Updated dependencies [48edf8d]
146
+ - Updated dependencies [12ffa3b]
147
+ - Updated dependencies [2f262f7]
148
+ - Updated dependencies [58b01f2]
149
+ - @ontrails/core@1.0.0-beta.19
150
+ - @ontrails/warden@1.0.0-beta.19
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ontrails/regrade",
3
+ "version": "1.0.0-beta.29",
4
+ "description": "Downstream migration reporting and safe rewrite helpers for Trails.",
5
+ "files": [
6
+ "src/**/*.ts",
7
+ "!src/**/__tests__/**",
8
+ "!src/**/*.test.ts",
9
+ "!src/**/*.test-d.ts",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -b",
19
+ "test": "bun test",
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "oxlint ./src",
22
+ "clean": "rm -rf dist *.tsbuildinfo"
23
+ },
24
+ "dependencies": {
25
+ "@ontrails/core": "^1.0.0-beta.29",
26
+ "@ontrails/warden": "^1.0.0-beta.29",
27
+ "zod": "^4.3.5"
28
+ },
29
+ "devDependencies": {
30
+ "@ontrails/testing": "^1.0.0-beta.29",
31
+ "@ontrails/topographer": "^1.0.0-beta.29"
32
+ }
33
+ }
@@ -0,0 +1,286 @@
1
+ import type {
2
+ AstNode,
3
+ AstScopeContext,
4
+ SourceEdit,
5
+ } from '@ontrails/warden/ast';
6
+ import {
7
+ applySourceEdits,
8
+ createSourceEdit,
9
+ identifierName,
10
+ offsetToLineColumn,
11
+ parseWithDiagnostics,
12
+ validateSourceEdits,
13
+ walkWithScopeContext,
14
+ } from '@ontrails/warden/ast';
15
+
16
+ import type {
17
+ RegradeClass,
18
+ RegradeClassContext,
19
+ RegradeClassResult,
20
+ RegradeReviewDetail,
21
+ } from './report.js';
22
+
23
+ export interface AstRewriteContext extends AstScopeContext {
24
+ readonly path: string;
25
+ readonly source: string;
26
+ }
27
+
28
+ export type AstRewriteMatch =
29
+ | {
30
+ readonly edit: SourceEdit;
31
+ readonly kind: 'edit';
32
+ readonly note?: string;
33
+ }
34
+ | {
35
+ readonly detail?: RegradeReviewDetail;
36
+ readonly kind: 'review';
37
+ readonly note?: string;
38
+ readonly reason: string;
39
+ };
40
+
41
+ export type AstRewriteVisitResult =
42
+ | AstRewriteMatch
43
+ | AstRewriteMatch[]
44
+ | null
45
+ | undefined;
46
+
47
+ export interface AstRewriteClassOptions {
48
+ readonly describe: string;
49
+ readonly id: string;
50
+ readonly shouldScan?: (context: RegradeClassContext) => boolean;
51
+ readonly visit: (
52
+ node: AstNode,
53
+ context: AstRewriteContext
54
+ ) => AstRewriteVisitResult;
55
+ }
56
+
57
+ const defaultRegradeClassContext: RegradeClassContext = {
58
+ path: '<regrade-source>',
59
+ };
60
+
61
+ const toArray = (result: AstRewriteVisitResult): readonly AstRewriteMatch[] => {
62
+ if (result === undefined || result === null) {
63
+ return [];
64
+ }
65
+ return Array.isArray(result) ? result : [result];
66
+ };
67
+
68
+ const dedupeEdits = (edits: readonly SourceEdit[]): readonly SourceEdit[] => {
69
+ const seen = new Set<string>();
70
+ const unique: SourceEdit[] = [];
71
+
72
+ for (const edit of edits) {
73
+ const key = `${edit.start}:${edit.end}:${edit.replacement}`;
74
+ if (seen.has(key)) {
75
+ continue;
76
+ }
77
+ seen.add(key);
78
+ unique.push(edit);
79
+ }
80
+
81
+ return unique;
82
+ };
83
+
84
+ const notesFor = (matches: readonly AstRewriteMatch[]): readonly string[] =>
85
+ matches.flatMap((match) => {
86
+ if (match.note) {
87
+ return [match.note];
88
+ }
89
+ if (match.kind === 'review') {
90
+ return [match.reason];
91
+ }
92
+ return [];
93
+ });
94
+
95
+ const invalidEditsResult = (error: unknown): RegradeClassResult => ({
96
+ kind: 'needs-review',
97
+ notes: [
98
+ error instanceof Error
99
+ ? `AST rewrite edits could not be applied: ${error.message}`
100
+ : 'AST rewrite edits could not be applied.',
101
+ ],
102
+ reason: 'ast-rewrite-invalid-edits',
103
+ });
104
+
105
+ const astRewriteFailureResult = (
106
+ context: RegradeClassContext,
107
+ reason: string,
108
+ error: unknown
109
+ ): RegradeClassResult => ({
110
+ kind: 'needs-review',
111
+ notes: [
112
+ error instanceof Error
113
+ ? `AST rewrite failed for ${context.path}: ${error.message}`
114
+ : `AST rewrite failed for ${context.path}.`,
115
+ ],
116
+ reason,
117
+ });
118
+
119
+ const reviewDetailsFor = (
120
+ classId: string,
121
+ matches: readonly AstRewriteMatch[]
122
+ ): readonly RegradeReviewDetail[] | undefined => {
123
+ const details = matches.flatMap((match) => {
124
+ if (match.kind !== 'review' || match.detail === undefined) {
125
+ return [];
126
+ }
127
+ return [{ ...match.detail, classId: match.detail.classId ?? classId }];
128
+ });
129
+ return details.length === 0 ? undefined : details;
130
+ };
131
+
132
+ export const createAstRewriteClass = (
133
+ options: AstRewriteClassOptions
134
+ ): RegradeClass => ({
135
+ apply: (
136
+ source: string,
137
+ context: RegradeClassContext = defaultRegradeClassContext
138
+ ): RegradeClassResult => {
139
+ if (options.shouldScan) {
140
+ try {
141
+ if (!options.shouldScan(context)) {
142
+ return {
143
+ kind: 'skipped',
144
+ notes: ['Skipped by AST rewrite scan-target filtering.'],
145
+ reason: 'ast-rewrite-scan-target-filtered',
146
+ };
147
+ }
148
+ } catch (error: unknown) {
149
+ return astRewriteFailureResult(
150
+ context,
151
+ 'ast-rewrite-scan-target-failed',
152
+ error
153
+ );
154
+ }
155
+ }
156
+
157
+ const parsed = parseWithDiagnostics(context.path, source);
158
+ if (!parsed.ast || parsed.diagnostics.length > 0) {
159
+ return {
160
+ kind: 'needs-review',
161
+ notes:
162
+ parsed.diagnostics.length > 0
163
+ ? parsed.diagnostics.map(
164
+ (diagnostic) =>
165
+ `Could not safely parse ${context.path}: ${diagnostic.message}`
166
+ )
167
+ : [`Could not parse ${context.path} for AST rewrite.`],
168
+ reason: 'ast-rewrite-parse-failed',
169
+ };
170
+ }
171
+
172
+ const matches: AstRewriteMatch[] = [];
173
+ try {
174
+ walkWithScopeContext(parsed.ast, (node, scopeContext) => {
175
+ matches.push(
176
+ ...toArray(
177
+ options.visit(node, {
178
+ ...scopeContext,
179
+ path: context.path,
180
+ source,
181
+ })
182
+ )
183
+ );
184
+ });
185
+ } catch (error: unknown) {
186
+ return astRewriteFailureResult(
187
+ context,
188
+ 'ast-rewrite-visitor-failed',
189
+ error
190
+ );
191
+ }
192
+
193
+ const reviewMatches = matches.filter((match) => match.kind === 'review');
194
+ if (reviewMatches.length > 0) {
195
+ const reviewDetails = reviewDetailsFor(options.id, reviewMatches);
196
+ return {
197
+ kind: 'needs-review',
198
+ notes: notesFor(reviewMatches),
199
+ reason: reviewMatches[0]?.reason ?? 'ast-rewrite-review-required',
200
+ ...(reviewDetails === undefined ? {} : { reviewDetails }),
201
+ };
202
+ }
203
+
204
+ const edits = dedupeEdits(
205
+ matches.flatMap((match) => (match.kind === 'edit' ? [match.edit] : []))
206
+ );
207
+ if (edits.length === 0) {
208
+ return {
209
+ kind: 'no-op',
210
+ notes: ['No AST rewrite matches found.'],
211
+ };
212
+ }
213
+
214
+ try {
215
+ validateSourceEdits(edits);
216
+ return {
217
+ kind: 'rewrite',
218
+ nextSource: applySourceEdits(source, edits),
219
+ notes: notesFor(matches),
220
+ };
221
+ } catch (error) {
222
+ return invalidEditsResult(error);
223
+ }
224
+ },
225
+ describe: options.describe,
226
+ id: options.id,
227
+ });
228
+
229
+ export interface AstIdentifierRenameClassOptions {
230
+ readonly describe?: string;
231
+ readonly from: string;
232
+ readonly id?: string;
233
+ readonly reviewDeclarationTypes?: ReadonlySet<string>;
234
+ readonly to: string;
235
+ }
236
+
237
+ const isIdentifierNamed = (node: AstNode, name: string): boolean =>
238
+ node.type === 'Identifier' && identifierName(node) === name;
239
+
240
+ export const createAstIdentifierRenameClass = (
241
+ options: AstIdentifierRenameClassOptions
242
+ ): RegradeClass => {
243
+ const reviewDeclarationTypes =
244
+ options.reviewDeclarationTypes ?? new Set<string>();
245
+
246
+ return createAstRewriteClass({
247
+ describe:
248
+ options.describe ??
249
+ `Rename identifier "${options.from}" to "${options.to}".`,
250
+ id: options.id ?? `ast-identifier-rename:${options.from}->${options.to}`,
251
+ visit: (node, context) => {
252
+ if (!isIdentifierNamed(node, options.from)) {
253
+ return null;
254
+ }
255
+
256
+ const declaration = context.getDeclaration(options.from);
257
+ if (declaration && reviewDeclarationTypes.has(declaration.type)) {
258
+ const location = offsetToLineColumn(context.source, node.start);
259
+ return {
260
+ detail: {
261
+ expectedTarget: `Rename identifier "${options.from}" to "${options.to}".`,
262
+ nodeKind: node.type,
263
+ reason: 'ast-identifier-review-declaration',
264
+ span: {
265
+ column: location.column,
266
+ end: node.end,
267
+ line: location.line,
268
+ start: node.start,
269
+ },
270
+ suggestedValidation: 'bun run typecheck',
271
+ symbol: options.from,
272
+ },
273
+ kind: 'review',
274
+ note: `Identifier "${options.from}" resolves to ${declaration.type}; routed to review.`,
275
+ reason: 'ast-identifier-review-declaration',
276
+ };
277
+ }
278
+
279
+ return {
280
+ edit: createSourceEdit(node.start, node.end, options.to),
281
+ kind: 'edit',
282
+ note: `Renamed identifier "${options.from}" to "${options.to}".`,
283
+ };
284
+ },
285
+ });
286
+ };