@ontrails/regrade 1.0.0-beta.39 → 1.0.0-beta.41
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 +81 -0
- package/package.json +6 -5
- package/src/downstream/ast-rewrite.ts +567 -54
- package/src/downstream/collect.ts +1 -1
- package/src/downstream/export-restructure.ts +2 -2
- package/src/downstream/report.ts +140 -1
- package/src/downstream/vocabulary-registry.ts +4 -6
- package/src/downstream/vocabulary.ts +97 -23
- package/src/literal-transform.ts +26 -20
|
@@ -281,7 +281,7 @@ export const collectDownstreamSourcesOutput = z.object({
|
|
|
281
281
|
export const collectDownstreamSourcesTrail = trail(
|
|
282
282
|
'regrade.downstream.collect',
|
|
283
283
|
{
|
|
284
|
-
|
|
284
|
+
implementation: (input) => {
|
|
285
285
|
const collection = collectDownstreamSources(input.root, {
|
|
286
286
|
...(input.extensions === undefined
|
|
287
287
|
? {}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* emitted on a guess.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { AstNode, SourceEdit } from '@ontrails/
|
|
21
|
+
import type { AstNode, SourceEdit } from '@ontrails/source';
|
|
22
22
|
import {
|
|
23
23
|
applySourceEdits,
|
|
24
24
|
createSourceEdit,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
parseWithDiagnostics,
|
|
61
61
|
validateSourceEdits,
|
|
62
62
|
walkWithParents,
|
|
63
|
-
} from '@ontrails/
|
|
63
|
+
} from '@ontrails/source';
|
|
64
64
|
import type { WardenRule } from '@ontrails/warden';
|
|
65
65
|
import {
|
|
66
66
|
getWardenRuleMetadata,
|
package/src/downstream/report.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
loadProjectWardenRules,
|
|
18
18
|
wardenRules,
|
|
19
19
|
} from '@ontrails/warden';
|
|
20
|
-
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
21
22
|
import { z } from 'zod';
|
|
22
23
|
|
|
23
24
|
import {
|
|
@@ -79,6 +80,16 @@ export interface RegradeClassContext {
|
|
|
79
80
|
readonly path: string;
|
|
80
81
|
/** Absolute path on disk, when the caller has one. */
|
|
81
82
|
readonly absolutePath?: string;
|
|
83
|
+
/** Nearest owning package facts, when filesystem collection found a manifest. */
|
|
84
|
+
readonly package?: {
|
|
85
|
+
readonly dependencies: readonly string[];
|
|
86
|
+
/** Runtime-visible dependency declarations (dependencies, optional, peer). */
|
|
87
|
+
readonly runtimeDependencies?: readonly string[];
|
|
88
|
+
readonly manifestState?: 'invalid' | 'valid';
|
|
89
|
+
readonly name?: string;
|
|
90
|
+
/** Root-relative POSIX manifest path. */
|
|
91
|
+
readonly path: string;
|
|
92
|
+
};
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
/** Files a regrade class knows how to inspect. */
|
|
@@ -966,6 +977,7 @@ const buildRegradeEvaluation = (params: {
|
|
|
966
977
|
readonly path: string;
|
|
967
978
|
readonly source: string;
|
|
968
979
|
readonly absolutePath?: string;
|
|
980
|
+
readonly package?: RegradeClassContext['package'];
|
|
969
981
|
}[];
|
|
970
982
|
readonly skipped: readonly SkippedSource[];
|
|
971
983
|
readonly classes: readonly RegradeClass[];
|
|
@@ -987,6 +999,7 @@ const buildRegradeEvaluation = (params: {
|
|
|
987
999
|
...(file.absolutePath === undefined
|
|
988
1000
|
? {}
|
|
989
1001
|
: { absolutePath: file.absolutePath }),
|
|
1002
|
+
...(file.package === undefined ? {} : { package: file.package }),
|
|
990
1003
|
path: file.path,
|
|
991
1004
|
},
|
|
992
1005
|
selected,
|
|
@@ -1055,6 +1068,7 @@ export const buildRegradeReport = (params: {
|
|
|
1055
1068
|
readonly path: string;
|
|
1056
1069
|
readonly source: string;
|
|
1057
1070
|
readonly absolutePath?: string;
|
|
1071
|
+
readonly package?: RegradeClassContext['package'];
|
|
1058
1072
|
}[];
|
|
1059
1073
|
readonly skipped: readonly SkippedSource[];
|
|
1060
1074
|
readonly classes: readonly RegradeClass[];
|
|
@@ -1127,6 +1141,121 @@ const canReadDownstreamRoot = (root: string): boolean => {
|
|
|
1127
1141
|
}
|
|
1128
1142
|
};
|
|
1129
1143
|
|
|
1144
|
+
const PACKAGE_DEPENDENCY_FIELDS = [
|
|
1145
|
+
'dependencies',
|
|
1146
|
+
'devDependencies',
|
|
1147
|
+
'optionalDependencies',
|
|
1148
|
+
'peerDependencies',
|
|
1149
|
+
] as const;
|
|
1150
|
+
|
|
1151
|
+
const RUNTIME_PACKAGE_DEPENDENCY_FIELDS = [
|
|
1152
|
+
'dependencies',
|
|
1153
|
+
'optionalDependencies',
|
|
1154
|
+
'peerDependencies',
|
|
1155
|
+
] as const;
|
|
1156
|
+
|
|
1157
|
+
const dependencyNames = (
|
|
1158
|
+
manifest: Readonly<Record<string, unknown>>,
|
|
1159
|
+
fields: readonly (typeof PACKAGE_DEPENDENCY_FIELDS)[number][]
|
|
1160
|
+
): readonly string[] =>
|
|
1161
|
+
fields.flatMap((field) => {
|
|
1162
|
+
const value = manifest[field];
|
|
1163
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1164
|
+
? Object.keys(value)
|
|
1165
|
+
: [];
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const packageContextFor = (
|
|
1169
|
+
root: string,
|
|
1170
|
+
absolutePath: string,
|
|
1171
|
+
cache: Map<string, RegradeClassContext['package'] | undefined>
|
|
1172
|
+
): RegradeClassContext['package'] | undefined => {
|
|
1173
|
+
const absoluteRoot = resolve(root);
|
|
1174
|
+
let current = dirname(absolutePath);
|
|
1175
|
+
const visited: string[] = [];
|
|
1176
|
+
while (true) {
|
|
1177
|
+
if (cache.has(current)) {
|
|
1178
|
+
const cached = cache.get(current);
|
|
1179
|
+
for (const directory of visited) {
|
|
1180
|
+
cache.set(directory, cached);
|
|
1181
|
+
}
|
|
1182
|
+
return cached;
|
|
1183
|
+
}
|
|
1184
|
+
visited.push(current);
|
|
1185
|
+
const fromRoot = relative(absoluteRoot, current);
|
|
1186
|
+
if (fromRoot.startsWith('..') || resolve(current) !== current) {
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
const manifestPath = join(current, 'package.json');
|
|
1190
|
+
if (existsSync(manifestPath)) {
|
|
1191
|
+
const manifestRelativePath = relative(
|
|
1192
|
+
absoluteRoot,
|
|
1193
|
+
manifestPath
|
|
1194
|
+
).replaceAll('\\', '/');
|
|
1195
|
+
try {
|
|
1196
|
+
const parsed = JSON.parse(
|
|
1197
|
+
readFileSync(manifestPath, 'utf8')
|
|
1198
|
+
) as unknown;
|
|
1199
|
+
if (
|
|
1200
|
+
typeof parsed !== 'object' ||
|
|
1201
|
+
parsed === null ||
|
|
1202
|
+
Array.isArray(parsed)
|
|
1203
|
+
) {
|
|
1204
|
+
const invalidContext = {
|
|
1205
|
+
dependencies: [],
|
|
1206
|
+
manifestState: 'invalid' as const,
|
|
1207
|
+
path: manifestRelativePath,
|
|
1208
|
+
};
|
|
1209
|
+
for (const directory of visited) {
|
|
1210
|
+
cache.set(directory, invalidContext);
|
|
1211
|
+
}
|
|
1212
|
+
return invalidContext;
|
|
1213
|
+
}
|
|
1214
|
+
const manifest = parsed as Record<string, unknown>;
|
|
1215
|
+
const dependencies = dependencyNames(
|
|
1216
|
+
manifest,
|
|
1217
|
+
PACKAGE_DEPENDENCY_FIELDS
|
|
1218
|
+
);
|
|
1219
|
+
const runtimeDependencies = dependencyNames(
|
|
1220
|
+
manifest,
|
|
1221
|
+
RUNTIME_PACKAGE_DEPENDENCY_FIELDS
|
|
1222
|
+
);
|
|
1223
|
+
const packageContext = {
|
|
1224
|
+
dependencies: [...new Set(dependencies)].toSorted(),
|
|
1225
|
+
manifestState: 'valid' as const,
|
|
1226
|
+
...(typeof manifest['name'] === 'string'
|
|
1227
|
+
? { name: manifest['name'] }
|
|
1228
|
+
: {}),
|
|
1229
|
+
path: manifestRelativePath,
|
|
1230
|
+
runtimeDependencies: [...new Set(runtimeDependencies)].toSorted(),
|
|
1231
|
+
};
|
|
1232
|
+
for (const directory of visited) {
|
|
1233
|
+
cache.set(directory, packageContext);
|
|
1234
|
+
}
|
|
1235
|
+
return packageContext;
|
|
1236
|
+
} catch {
|
|
1237
|
+
const invalidContext = {
|
|
1238
|
+
dependencies: [],
|
|
1239
|
+
manifestState: 'invalid' as const,
|
|
1240
|
+
path: manifestRelativePath,
|
|
1241
|
+
};
|
|
1242
|
+
for (const directory of visited) {
|
|
1243
|
+
cache.set(directory, invalidContext);
|
|
1244
|
+
}
|
|
1245
|
+
return invalidContext;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (current === absoluteRoot) {
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
current = dirname(current);
|
|
1252
|
+
}
|
|
1253
|
+
for (const directory of visited) {
|
|
1254
|
+
cache.set(directory, undefined);
|
|
1255
|
+
}
|
|
1256
|
+
return undefined;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1130
1259
|
const runRegradeEvaluation = (params: {
|
|
1131
1260
|
readonly root: string;
|
|
1132
1261
|
readonly classes: readonly RegradeClass[];
|
|
@@ -1169,10 +1298,20 @@ const runRegradeEvaluation = (params: {
|
|
|
1169
1298
|
|
|
1170
1299
|
const files: { absolutePath: string; path: string; source: string }[] = [];
|
|
1171
1300
|
const skipped: SkippedSource[] = [...collected.skipped];
|
|
1301
|
+
const packageContextCache = new Map<
|
|
1302
|
+
string,
|
|
1303
|
+
RegradeClassContext['package'] | undefined
|
|
1304
|
+
>();
|
|
1172
1305
|
for (const file of collected.files) {
|
|
1173
1306
|
try {
|
|
1307
|
+
const packageContext = packageContextFor(
|
|
1308
|
+
params.root,
|
|
1309
|
+
file.absolutePath,
|
|
1310
|
+
packageContextCache
|
|
1311
|
+
);
|
|
1174
1312
|
files.push({
|
|
1175
1313
|
absolutePath: file.absolutePath,
|
|
1314
|
+
...(packageContext === undefined ? {} : { package: packageContext }),
|
|
1176
1315
|
path: file.path,
|
|
1177
1316
|
source: readFileSync(file.absolutePath, 'utf8'),
|
|
1178
1317
|
});
|
|
@@ -11,6 +11,9 @@ const pluralizeVocabularyForm = (value: string): string =>
|
|
|
11
11
|
? `${value}es`
|
|
12
12
|
: `${value}s`;
|
|
13
13
|
|
|
14
|
+
const defaultSourceForms = (value: string): readonly string[] =>
|
|
15
|
+
/^[A-Za-z]+$/.test(value) ? [value, pluralizeVocabularyForm(value)] : [value];
|
|
16
|
+
|
|
14
17
|
const preserveRulesFromTransition = (
|
|
15
18
|
transition: GovernedVocabularyTransition
|
|
16
19
|
): readonly VocabularyPreserveRule[] =>
|
|
@@ -44,12 +47,7 @@ const defaultFormsAreRegistrySafe = (
|
|
|
44
47
|
return false;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
transition.from,
|
|
49
|
-
pluralizeVocabularyForm(transition.from),
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
return defaultForms.every((form) => {
|
|
50
|
+
return defaultSourceForms(transition.from).every((form) => {
|
|
53
51
|
const replacement = transition.safeRewriteForms[form];
|
|
54
52
|
return replacement !== undefined && !transition.reviewForms.includes(form);
|
|
55
53
|
});
|
|
@@ -6,7 +6,14 @@ import {
|
|
|
6
6
|
matchesAnyPathGlob,
|
|
7
7
|
} from '@ontrails/core';
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
dirname,
|
|
11
|
+
extname,
|
|
12
|
+
isAbsolute,
|
|
13
|
+
join,
|
|
14
|
+
normalize,
|
|
15
|
+
relative,
|
|
16
|
+
} from 'node:path';
|
|
10
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
18
|
import { z } from 'zod';
|
|
12
19
|
|
|
@@ -213,30 +220,56 @@ const vocabularyDispositionCounts = (
|
|
|
213
220
|
);
|
|
214
221
|
};
|
|
215
222
|
|
|
216
|
-
const isVocabularyTokenCharacter = (
|
|
217
|
-
|
|
223
|
+
const isVocabularyTokenCharacter = (
|
|
224
|
+
value: string,
|
|
225
|
+
routeLike: boolean
|
|
226
|
+
): boolean => /[A-Za-z0-9_$-]/.test(value) || (routeLike && value === '/');
|
|
227
|
+
|
|
228
|
+
const isVocabularyTokenCharacterAt = (
|
|
229
|
+
source: string,
|
|
230
|
+
index: number,
|
|
231
|
+
routeLike: boolean
|
|
232
|
+
): boolean => {
|
|
233
|
+
if (index < 0 || index >= source.length) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const value = source.at(index) ?? '';
|
|
237
|
+
if (isVocabularyTokenCharacter(value, routeLike)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (!routeLike || value !== '.') {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return (
|
|
244
|
+
isVocabularyTokenCharacter(source.at(index - 1) ?? '', false) &&
|
|
245
|
+
isVocabularyTokenCharacter(source.at(index + 1) ?? '', false)
|
|
246
|
+
);
|
|
247
|
+
};
|
|
218
248
|
|
|
219
249
|
const hasWordBoundary = (
|
|
220
250
|
source: string,
|
|
221
251
|
start: number,
|
|
222
|
-
end: number
|
|
252
|
+
end: number,
|
|
253
|
+
form: string
|
|
223
254
|
): boolean => {
|
|
224
|
-
const
|
|
225
|
-
const after = end >= source.length ? '' : (source.at(end) ?? '');
|
|
255
|
+
const routeLike = form.includes('/');
|
|
226
256
|
return (
|
|
227
|
-
!
|
|
257
|
+
!isVocabularyTokenCharacterAt(source, start - 1, routeLike) &&
|
|
258
|
+
!isVocabularyTokenCharacterAt(source, end, routeLike)
|
|
228
259
|
);
|
|
229
260
|
};
|
|
230
261
|
|
|
231
262
|
const expandVocabularyNeighborSpan = (
|
|
232
263
|
source: string,
|
|
233
264
|
start: number,
|
|
234
|
-
end: number
|
|
265
|
+
end: number,
|
|
266
|
+
form: string
|
|
235
267
|
): { readonly end: number; readonly start: number } => {
|
|
268
|
+
const routeLike = form.includes('/');
|
|
236
269
|
let expandedStart = start;
|
|
237
270
|
while (
|
|
238
271
|
expandedStart > 0 &&
|
|
239
|
-
|
|
272
|
+
isVocabularyTokenCharacterAt(source, expandedStart - 1, routeLike)
|
|
240
273
|
) {
|
|
241
274
|
expandedStart -= 1;
|
|
242
275
|
}
|
|
@@ -244,7 +277,7 @@ const expandVocabularyNeighborSpan = (
|
|
|
244
277
|
let expandedEnd = end;
|
|
245
278
|
while (
|
|
246
279
|
expandedEnd < source.length &&
|
|
247
|
-
|
|
280
|
+
isVocabularyTokenCharacterAt(source, expandedEnd, routeLike)
|
|
248
281
|
) {
|
|
249
282
|
expandedEnd += 1;
|
|
250
283
|
}
|
|
@@ -288,6 +321,24 @@ const contextDetailsForOffset = (
|
|
|
288
321
|
const isMarkdownPath = (path: string): boolean =>
|
|
289
322
|
path.endsWith('.md') || path.endsWith('.mdx');
|
|
290
323
|
|
|
324
|
+
const packageRouteCodeExtensions = new Set([
|
|
325
|
+
'.cjs',
|
|
326
|
+
'.cts',
|
|
327
|
+
'.js',
|
|
328
|
+
'.jsx',
|
|
329
|
+
'.mjs',
|
|
330
|
+
'.mts',
|
|
331
|
+
'.ts',
|
|
332
|
+
'.tsx',
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const isPackageRouteCodeOccurrence = (path: string, form: string): boolean =>
|
|
336
|
+
packageRouteCodeExtensions.has(extname(path)) &&
|
|
337
|
+
/^@[^/]+\/[^/]+(?:\/.*)?$/.test(form);
|
|
338
|
+
|
|
339
|
+
const isPackageManifestPath = (path: string): boolean =>
|
|
340
|
+
path === 'package.json' || path.endsWith('/package.json');
|
|
341
|
+
|
|
291
342
|
const sourceLineBoundsForOffset = (
|
|
292
343
|
source: string,
|
|
293
344
|
start: number,
|
|
@@ -478,11 +529,13 @@ const defaultDeferredVocabularyForms = (from: string): readonly string[] => {
|
|
|
478
529
|
]).filter((form) => form !== from && form !== pluralize(from));
|
|
479
530
|
};
|
|
480
531
|
|
|
481
|
-
const defaultVocabularyForms = (from: string, to: string) =>
|
|
482
|
-
new Map<string, string>([
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
532
|
+
const defaultVocabularyForms = (from: string, to: string) => {
|
|
533
|
+
const forms = new Map<string, string>([[from, to]]);
|
|
534
|
+
if (isSimpleVocabularyWord(from) && isSimpleVocabularyWord(to)) {
|
|
535
|
+
forms.set(pluralize(from), pluralize(to));
|
|
536
|
+
}
|
|
537
|
+
return forms;
|
|
538
|
+
};
|
|
486
539
|
|
|
487
540
|
const normalizedOverrideEntries = (
|
|
488
541
|
overrides: Readonly<Record<string, string>> | undefined
|
|
@@ -806,7 +859,7 @@ const exactDeferredFormOccurrencesForFile = (
|
|
|
806
859
|
formIdentityForPlan(plan, form)
|
|
807
860
|
);
|
|
808
861
|
if (
|
|
809
|
-
!hasWordBoundary(file.source, start, end) ||
|
|
862
|
+
!hasWordBoundary(file.source, start, end, form) ||
|
|
810
863
|
occurrenceOverlaps(occurrences, start, end) ||
|
|
811
864
|
(!isAuthoredDefer && occurrenceOverlaps(targetFormSpans, start, end))
|
|
812
865
|
) {
|
|
@@ -840,7 +893,7 @@ const targetFormSpansForFile = (
|
|
|
840
893
|
const start = match.index ?? 0;
|
|
841
894
|
const end = start + match[0].length;
|
|
842
895
|
if (
|
|
843
|
-
!hasWordBoundary(file.source, start, end) ||
|
|
896
|
+
!hasWordBoundary(file.source, start, end, form) ||
|
|
844
897
|
occurrenceOverlaps(spans, start, end)
|
|
845
898
|
) {
|
|
846
899
|
continue;
|
|
@@ -869,7 +922,7 @@ const occurrencesForFile = (
|
|
|
869
922
|
const start = match.index ?? 0;
|
|
870
923
|
const end = start + match[0].length;
|
|
871
924
|
if (
|
|
872
|
-
!hasWordBoundary(file.source, start, end) ||
|
|
925
|
+
!hasWordBoundary(file.source, start, end, form) ||
|
|
873
926
|
occurrenceOverlaps(deferredOccurrences, start, end)
|
|
874
927
|
) {
|
|
875
928
|
continue;
|
|
@@ -889,10 +942,27 @@ const occurrencesForFile = (
|
|
|
889
942
|
};
|
|
890
943
|
const preserveRule = preserveRuleForOccurrence(baseOccurrence, plan);
|
|
891
944
|
const markdownCodeContext = isMarkdownCodeContext(file, start, end);
|
|
892
|
-
const
|
|
945
|
+
const packageRouteCodeContext = isPackageRouteCodeOccurrence(
|
|
946
|
+
file.path,
|
|
947
|
+
form
|
|
948
|
+
);
|
|
949
|
+
let verdict = capturedVocabularyVerdict(
|
|
893
950
|
preserveRule,
|
|
894
951
|
markdownCodeContext
|
|
895
952
|
);
|
|
953
|
+
const packageManifestContext = isPackageManifestPath(file.path);
|
|
954
|
+
if (
|
|
955
|
+
preserveRule === undefined &&
|
|
956
|
+
(packageRouteCodeContext || packageManifestContext)
|
|
957
|
+
) {
|
|
958
|
+
verdict = 'deferred';
|
|
959
|
+
}
|
|
960
|
+
let capturedReason = 'captured-form';
|
|
961
|
+
if (packageRouteCodeContext) {
|
|
962
|
+
capturedReason = 'package-route-ast-required';
|
|
963
|
+
} else if (packageManifestContext) {
|
|
964
|
+
capturedReason = 'package-manifest-structured-edit-required';
|
|
965
|
+
}
|
|
896
966
|
candidates.push({
|
|
897
967
|
absolutePath: baseOccurrence.absolutePath,
|
|
898
968
|
column: baseOccurrence.column,
|
|
@@ -909,9 +979,12 @@ const occurrencesForFile = (
|
|
|
909
979
|
reason: vocabularyOccurrenceReason(
|
|
910
980
|
preserveRule,
|
|
911
981
|
markdownCodeContext,
|
|
912
|
-
|
|
982
|
+
capturedReason
|
|
913
983
|
),
|
|
914
|
-
...(preserveRule === undefined &&
|
|
984
|
+
...(preserveRule === undefined &&
|
|
985
|
+
!markdownCodeContext &&
|
|
986
|
+
!packageRouteCodeContext &&
|
|
987
|
+
!packageManifestContext
|
|
915
988
|
? { replacement: preserveCase(match[0], replacement) }
|
|
916
989
|
: {}),
|
|
917
990
|
start: baseOccurrence.start,
|
|
@@ -972,13 +1045,14 @@ const deferredOccurrencesForFile = (
|
|
|
972
1045
|
for (const match of file.source.matchAll(pattern)) {
|
|
973
1046
|
const matchStart = match.index ?? 0;
|
|
974
1047
|
const matchEnd = matchStart + match[0].length;
|
|
975
|
-
if (hasWordBoundary(file.source, matchStart, matchEnd)) {
|
|
1048
|
+
if (hasWordBoundary(file.source, matchStart, matchEnd, form)) {
|
|
976
1049
|
continue;
|
|
977
1050
|
}
|
|
978
1051
|
const { end, start } = expandVocabularyNeighborSpan(
|
|
979
1052
|
file.source,
|
|
980
1053
|
matchStart,
|
|
981
|
-
matchEnd
|
|
1054
|
+
matchEnd,
|
|
1055
|
+
form
|
|
982
1056
|
);
|
|
983
1057
|
const matchedForm = file.source.slice(start, end);
|
|
984
1058
|
const lowerMatchedForm = matchedForm.toLowerCase();
|
package/src/literal-transform.ts
CHANGED
|
@@ -16,10 +16,10 @@ const childInput = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Parent input schema with its raw-to-
|
|
19
|
+
* Parent input schema with its raw-to-implementation transform attached.
|
|
20
20
|
*
|
|
21
21
|
* Naming the transformed schema lets examples and tests reference both its raw
|
|
22
|
-
* pre-transform input (`z.input`) and its post-transform
|
|
22
|
+
* pre-transform input (`z.input`) and its post-transform implementation input
|
|
23
23
|
* (`z.infer`) without restating either shape by hand.
|
|
24
24
|
*/
|
|
25
25
|
const regradeTransformInputToChild = regradeTransformInput.transform(
|
|
@@ -32,15 +32,15 @@ const regradeTransformInputToChild = regradeTransformInput.transform(
|
|
|
32
32
|
* Raw, pre-transform input accepted by {@link regradeTransformInputToChild}.
|
|
33
33
|
*
|
|
34
34
|
* Trail examples and `testExamples()` feed this shape through validation; the
|
|
35
|
-
* Zod transform then projects it into the
|
|
35
|
+
* Zod transform then projects it into the implementation input.
|
|
36
36
|
*/
|
|
37
37
|
type RegradeTransformRawInput = z.input<typeof regradeTransformInputToChild>;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Post-transform
|
|
40
|
+
* Post-transform implementation input shape — the trail's inferred input type `I`.
|
|
41
41
|
*
|
|
42
42
|
* TRL-842: With a `.transform()` input schema, the trail's inferred input type
|
|
43
|
-
* `I` is the transform OUTPUT (the
|
|
43
|
+
* `I` is the transform OUTPUT (the implementation input), while examples and
|
|
44
44
|
* `testExamples()` validate the raw pre-transform INPUT. Those two shapes are
|
|
45
45
|
* disjoint, so an authored example must carry raw input typed as the
|
|
46
46
|
* post-transform shape. A framework-level fix would thread a separate
|
|
@@ -49,7 +49,9 @@ type RegradeTransformRawInput = z.input<typeof regradeTransformInputToChild>;
|
|
|
49
49
|
* then the divergence is captured by the two named types here and exercised by
|
|
50
50
|
* the runtime validation test in `__tests__/literal-transform.test.ts`.
|
|
51
51
|
*/
|
|
52
|
-
type
|
|
52
|
+
type RegradeTransformImplementationInput = z.infer<
|
|
53
|
+
typeof regradeTransformInputToChild
|
|
54
|
+
>;
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Raw example input, statically checked as valid pre-transform input. If the
|
|
@@ -63,7 +65,7 @@ const codeStringExampleInput: RegradeTransformRawInput = {
|
|
|
63
65
|
export const normalizeExportConstTrail = trail(
|
|
64
66
|
'regrade.literal.normalize-export-const',
|
|
65
67
|
{
|
|
66
|
-
|
|
68
|
+
implementation: (input) => {
|
|
67
69
|
const nextSource = input.source.replaceAll('export const', 'export let');
|
|
68
70
|
return Result.ok({
|
|
69
71
|
changed: nextSource !== input.source,
|
|
@@ -81,17 +83,6 @@ export const normalizeExportConstTrail = trail(
|
|
|
81
83
|
);
|
|
82
84
|
|
|
83
85
|
export const literalRegradeTrail = trail('regrade.literal.run', {
|
|
84
|
-
blaze: async (input, ctx) => {
|
|
85
|
-
if (!ctx.compose) {
|
|
86
|
-
return Result.err(
|
|
87
|
-
new InternalError(
|
|
88
|
-
'Literal Regrade tracer requires compose-capable execution.'
|
|
89
|
-
)
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const blazeInput = input as RegradeTransformBlazeInput;
|
|
93
|
-
return await ctx.compose(normalizeExportConstTrail, blazeInput.child);
|
|
94
|
-
},
|
|
95
86
|
composes: [normalizeExportConstTrail],
|
|
96
87
|
examples: [
|
|
97
88
|
{
|
|
@@ -103,11 +94,26 @@ export const literalRegradeTrail = trail('regrade.literal.run', {
|
|
|
103
94
|
// TRL-842: author the raw pre-transform value (validated as
|
|
104
95
|
// RegradeTransformRawInput) and widen through `unknown` to the trail's
|
|
105
96
|
// inferred post-transform input type. The runtime still parses raw input;
|
|
106
|
-
// see
|
|
107
|
-
input:
|
|
97
|
+
// see RegradeTransformImplementationInput for why the two shapes diverge.
|
|
98
|
+
input:
|
|
99
|
+
codeStringExampleInput as unknown as RegradeTransformImplementationInput,
|
|
108
100
|
name: 'code-string fixture',
|
|
109
101
|
},
|
|
110
102
|
],
|
|
103
|
+
implementation: async (input, ctx) => {
|
|
104
|
+
if (!ctx.compose) {
|
|
105
|
+
return Result.err(
|
|
106
|
+
new InternalError(
|
|
107
|
+
'Literal Regrade tracer requires compose-capable execution.'
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const implementationInput = input as RegradeTransformImplementationInput;
|
|
112
|
+
return await ctx.compose(
|
|
113
|
+
normalizeExportConstTrail,
|
|
114
|
+
implementationInput.child
|
|
115
|
+
);
|
|
116
|
+
},
|
|
111
117
|
input: regradeTransformInputToChild,
|
|
112
118
|
output: regradeTransformOutput,
|
|
113
119
|
});
|