@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 +150 -0
- package/package.json +33 -0
- package/src/downstream/ast-rewrite.ts +286 -0
- package/src/downstream/collect.ts +267 -0
- package/src/downstream/report.ts +969 -0
- package/src/index.ts +37 -0
- package/src/literal-transform.ts +118 -0
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
|
+
};
|