@ontrails/regrade 1.0.0-beta.45 → 1.0.0-beta.46
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 +17 -0
- package/package.json +6 -6
- package/src/downstream/export-restructure.ts +2 -2
- package/src/downstream/file-renames.ts +115 -28
- package/src/downstream/report.ts +522 -51
- package/src/downstream/vocabulary.ts +570 -77
- package/src/history-receipt.ts +828 -0
- package/src/index.ts +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ontrails/regrade
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.46
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`701ab85`](https://github.com/outfitter-dev/trails/commit/701ab85bd237e5fcf03725c56b70adb3612d6c15): Expose in-memory prepared class and vocabulary evaluations with deterministic identity and source freshness checks.
|
|
8
|
+
- [`c83d0b6`](https://github.com/outfitter-dev/trails/commit/c83d0b67ac928af1b44c1f2d2c4b36aa09a24a70): Inventory parser-native source comments and TSDoc as exact review-only entries
|
|
9
|
+
for governed classified vocabulary transitions across CLI, MCP, and audit.
|
|
10
|
+
- [`97ccb5c`](https://github.com/outfitter-dev/trails/commit/97ccb5c168f7dc0f157f2518c78b226d67cfa008): Add the strict v3 Regrade run-receipt contract, canonical serializer, compact classified-form judgments, and cache-independent hash-reference resolution.
|
|
11
|
+
- [`9a8b6e4`](https://github.com/outfitter-dev/trails/commit/9a8b6e4af394c76c11e6d0007e0f5f94d0be2cb3): Persist Regrade lifecycle runs as canonical v3 receipts with exact Git blob evidence and authored field provenance, and validate their compact classified-form projection independently in Warden.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`768cc79`](https://github.com/outfitter-dev/trails/commit/768cc79ca10947b8808b376e281e1a81131b4acc): Close missed projection vocabulary residue in Regrade internals and public
|
|
16
|
+
error-rendering guidance, and keep lifecycle-ambiguous governed identifiers in
|
|
17
|
+
the Warden review inventory instead of assigning them an unsafe automatic
|
|
18
|
+
target.
|
|
19
|
+
|
|
3
20
|
## 1.0.0-beta.45
|
|
4
21
|
|
|
5
22
|
## 1.0.0-beta.44
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/regrade",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.46",
|
|
4
4
|
"description": "Downstream migration reporting and safe rewrite helpers for Trails.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
31
|
-
"@ontrails/source": "^1.0.0-beta.
|
|
32
|
-
"@ontrails/warden": "^1.0.0-beta.
|
|
30
|
+
"@ontrails/core": "^1.0.0-beta.46",
|
|
31
|
+
"@ontrails/source": "^1.0.0-beta.46",
|
|
32
|
+
"@ontrails/warden": "^1.0.0-beta.46",
|
|
33
33
|
"zod": "^4.3.5"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
37
|
-
"@ontrails/topography": "^1.0.0-beta.
|
|
36
|
+
"@ontrails/testing": "^1.0.0-beta.46",
|
|
37
|
+
"@ontrails/topography": "^1.0.0-beta.46"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -1468,7 +1468,7 @@ const applyMcpTrailheadsClass = (
|
|
|
1468
1468
|
};
|
|
1469
1469
|
|
|
1470
1470
|
/**
|
|
1471
|
-
*
|
|
1471
|
+
* Convert call-site MCP trailhead maps into `surfaceOverlay({ mcp: { name:
|
|
1472
1472
|
* [selectors] } })` group bindings. Because the map usually lives in a
|
|
1473
1473
|
* different file than the app module, the default outcome is a classified
|
|
1474
1474
|
* `needs-review` handoff naming the exact target shape; when the same file
|
|
@@ -1512,7 +1512,7 @@ export const exportRestructureClasses: readonly RegradeClass[] = Object.freeze([
|
|
|
1512
1512
|
]);
|
|
1513
1513
|
|
|
1514
1514
|
/**
|
|
1515
|
-
*
|
|
1515
|
+
* Convert a Warden rule that advertises the `export-restructure` fix class
|
|
1516
1516
|
* into its registered Regrade class. Warden owns detection and fix metadata;
|
|
1517
1517
|
* Regrade owns the structural transform. Returns `null` for rules without an
|
|
1518
1518
|
* `export-restructure` fix class or without a registered transform.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
matchesAnyPathGlob,
|
|
8
8
|
} from '@ontrails/core';
|
|
9
9
|
import type { Result as TrailsResult } from '@ontrails/core';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
10
11
|
import {
|
|
11
12
|
existsSync,
|
|
12
13
|
mkdirSync,
|
|
@@ -146,6 +147,7 @@ export interface FileRenameRegradeRun {
|
|
|
146
147
|
readonly occurrencePaths: readonly string[];
|
|
147
148
|
readonly policyOccurrencePaths: readonly string[];
|
|
148
149
|
readonly report: RegradeReport;
|
|
150
|
+
readonly sourceStateHash: string;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
const asError = (error: unknown): Error =>
|
|
@@ -154,6 +156,79 @@ const asError = (error: unknown): Error =>
|
|
|
154
156
|
const normalizeRenamePath = (path: string): string =>
|
|
155
157
|
posix.normalize(path.replaceAll('\\', '/'));
|
|
156
158
|
|
|
159
|
+
const compareFileRenameCodeUnits = (left: string, right: string): number => {
|
|
160
|
+
if (left < right) {
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
if (left > right) {
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const fileRenameSourceStateHash = (params: {
|
|
170
|
+
readonly apply: boolean;
|
|
171
|
+
readonly collected: DownstreamSourceCollection;
|
|
172
|
+
readonly renames: readonly VocabularyFileRename[];
|
|
173
|
+
readonly resolved: readonly ResolvedFileRename[];
|
|
174
|
+
}): TrailsResult<string, Error> => {
|
|
175
|
+
const unreadable = params.collected.skipped
|
|
176
|
+
.filter(
|
|
177
|
+
(entry) =>
|
|
178
|
+
entry.reason === 'unreadable-file' ||
|
|
179
|
+
entry.reason === 'unreadable-directory'
|
|
180
|
+
)
|
|
181
|
+
.map((entry) => entry.path);
|
|
182
|
+
if (unreadable.length > 0) {
|
|
183
|
+
return Result.err(
|
|
184
|
+
new ValidationError('File rename Regrade sources must all be readable.', {
|
|
185
|
+
context: { paths: unreadable },
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const sourceFiles = new Map(
|
|
190
|
+
params.collected.files.map((file) => [file.path, file])
|
|
191
|
+
);
|
|
192
|
+
for (const [index, rename] of params.renames.entries()) {
|
|
193
|
+
const resolved = params.resolved[index];
|
|
194
|
+
if (resolved === undefined) {
|
|
195
|
+
return Result.err(
|
|
196
|
+
new InternalError('File rename Regrade endpoint was not resolved.')
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const useTarget = params.apply || resolved.alreadyApplied;
|
|
200
|
+
const path = normalizeRenamePath(useTarget ? rename.to : rename.from);
|
|
201
|
+
sourceFiles.set(path, {
|
|
202
|
+
absolutePath: useTarget ? resolved.to : resolved.from,
|
|
203
|
+
path,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const sources: { readonly bytes: string; readonly path: string }[] = [];
|
|
207
|
+
for (const file of [...sourceFiles.values()].toSorted((left, right) =>
|
|
208
|
+
compareFileRenameCodeUnits(left.path, right.path)
|
|
209
|
+
)) {
|
|
210
|
+
try {
|
|
211
|
+
sources.push({
|
|
212
|
+
bytes: readFileSync(file.absolutePath).toString('base64'),
|
|
213
|
+
path: file.path,
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return Result.err(
|
|
217
|
+
new ValidationError(
|
|
218
|
+
'File rename Regrade sources must all be readable.',
|
|
219
|
+
{
|
|
220
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
221
|
+
context: { paths: [file.path] },
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return Result.ok(
|
|
228
|
+
createHash('sha256').update(JSON.stringify({ sources })).digest('hex')
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
157
232
|
const openedPolicyDirectories = (
|
|
158
233
|
scope: VocabularyRegradeScope | undefined
|
|
159
234
|
): readonly string[] =>
|
|
@@ -232,16 +307,16 @@ const preserveVocabularyCase = (
|
|
|
232
307
|
: replacement;
|
|
233
308
|
};
|
|
234
309
|
|
|
235
|
-
const
|
|
310
|
+
const deriveVocabularyText = (
|
|
236
311
|
source: string,
|
|
237
312
|
plan: VocabularyRegradePlan
|
|
238
313
|
): string => {
|
|
239
|
-
let
|
|
314
|
+
let derived = source;
|
|
240
315
|
const safeForms = vocabularyRewriteFormsForPlan(plan).toSorted(
|
|
241
316
|
([left], [right]) => right.length - left.length
|
|
242
317
|
);
|
|
243
318
|
for (const [from, to] of safeForms) {
|
|
244
|
-
|
|
319
|
+
derived = derived.replaceAll(
|
|
245
320
|
new RegExp(
|
|
246
321
|
`(?<![A-Za-z0-9_$-])${escapeRegExp(from)}(?![A-Za-z0-9_$-])`,
|
|
247
322
|
plan.caseSensitive === true ? 'gu' : 'giu'
|
|
@@ -250,7 +325,7 @@ const projectVocabularyText = (
|
|
|
250
325
|
plan.caseSensitive === true ? to : preserveVocabularyCase(matched, to)
|
|
251
326
|
);
|
|
252
327
|
}
|
|
253
|
-
return
|
|
328
|
+
return derived;
|
|
254
329
|
};
|
|
255
330
|
|
|
256
331
|
const astReferenceMappings = (
|
|
@@ -372,7 +447,7 @@ const referenceMappingsForPath = (
|
|
|
372
447
|
rename.from,
|
|
373
448
|
...(vocabularyPlan === undefined
|
|
374
449
|
? []
|
|
375
|
-
: [
|
|
450
|
+
: [deriveVocabularyText(rename.from, vocabularyPlan)]),
|
|
376
451
|
].filter((value, index, values) => values.indexOf(value) === index);
|
|
377
452
|
for (const sourcePath of sourcePaths) {
|
|
378
453
|
mappings.push(
|
|
@@ -1121,7 +1196,7 @@ const isGeneratedRegradeArtifactPath = (path: string): boolean =>
|
|
|
1121
1196
|
const filterOpenedPolicyDirectories = (
|
|
1122
1197
|
collected: DownstreamSourceCollection,
|
|
1123
1198
|
opened: readonly string[],
|
|
1124
|
-
|
|
1199
|
+
derivedTargetPaths: ReadonlySet<string>,
|
|
1125
1200
|
scope: VocabularyRegradeScope | undefined,
|
|
1126
1201
|
renames: readonly VocabularyFileRename[],
|
|
1127
1202
|
apply: boolean
|
|
@@ -1137,7 +1212,7 @@ const filterOpenedPolicyDirectories = (
|
|
|
1137
1212
|
);
|
|
1138
1213
|
return (
|
|
1139
1214
|
!insideOpenedDirectory ||
|
|
1140
|
-
|
|
1215
|
+
derivedTargetPaths.has(normalizeRenamePath(file.path)) ||
|
|
1141
1216
|
policyForPath(scopePath, scope) !== undefined
|
|
1142
1217
|
);
|
|
1143
1218
|
});
|
|
@@ -1199,22 +1274,22 @@ const collectionExcludeForFileRenames = (params: {
|
|
|
1199
1274
|
const normalizeExtension = (extension: string): string =>
|
|
1200
1275
|
extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
|
|
1201
1276
|
|
|
1202
|
-
const
|
|
1277
|
+
const deriveCollectionExtensionsForFileRenames = (params: {
|
|
1203
1278
|
readonly apply: boolean;
|
|
1204
1279
|
readonly extensions: readonly string[];
|
|
1205
1280
|
readonly renames: readonly VocabularyFileRename[];
|
|
1206
1281
|
}): {
|
|
1207
1282
|
readonly extensions: readonly string[];
|
|
1208
|
-
readonly
|
|
1283
|
+
readonly derivedTargetPaths: ReadonlySet<string>;
|
|
1209
1284
|
} => {
|
|
1210
1285
|
const sourceExtensions = new Set(params.extensions.map(normalizeExtension));
|
|
1211
1286
|
if (!params.apply || sourceExtensions.size === 0) {
|
|
1212
1287
|
return {
|
|
1288
|
+
derivedTargetPaths: new Set(),
|
|
1213
1289
|
extensions: params.extensions,
|
|
1214
|
-
projectedTargetPaths: new Set(),
|
|
1215
1290
|
};
|
|
1216
1291
|
}
|
|
1217
|
-
const
|
|
1292
|
+
const derivedTargetPaths = new Set(
|
|
1218
1293
|
params.renames
|
|
1219
1294
|
.filter((rename) =>
|
|
1220
1295
|
sourceExtensions.has(
|
|
@@ -1224,18 +1299,18 @@ const collectionExtensionProjectionForFileRenames = (params: {
|
|
|
1224
1299
|
.map((rename) => normalizeRenamePath(rename.to))
|
|
1225
1300
|
);
|
|
1226
1301
|
return {
|
|
1302
|
+
derivedTargetPaths,
|
|
1227
1303
|
extensions: [
|
|
1228
1304
|
...sourceExtensions,
|
|
1229
|
-
...new Set([...
|
|
1305
|
+
...new Set([...derivedTargetPaths].map((path) => extname(path))),
|
|
1230
1306
|
],
|
|
1231
|
-
projectedTargetPaths,
|
|
1232
1307
|
};
|
|
1233
1308
|
};
|
|
1234
1309
|
|
|
1235
|
-
const
|
|
1310
|
+
const filterDerivedTargetExtensions = (
|
|
1236
1311
|
collected: DownstreamSourceCollection,
|
|
1237
1312
|
sourceExtensions: readonly string[],
|
|
1238
|
-
|
|
1313
|
+
derivedTargetPaths: ReadonlySet<string>
|
|
1239
1314
|
): DownstreamSourceCollection => {
|
|
1240
1315
|
const normalizedSourceExtensions = new Set(
|
|
1241
1316
|
sourceExtensions.map(normalizeExtension)
|
|
@@ -1246,7 +1321,7 @@ const filterProjectedTargetExtensions = (
|
|
|
1246
1321
|
const files = collected.files.filter(
|
|
1247
1322
|
(file) =>
|
|
1248
1323
|
normalizedSourceExtensions.has(extname(file.path)) ||
|
|
1249
|
-
|
|
1324
|
+
derivedTargetPaths.has(normalizeRenamePath(file.path))
|
|
1250
1325
|
);
|
|
1251
1326
|
const selected = new Set(files.map((file) => file.path));
|
|
1252
1327
|
return {
|
|
@@ -1264,7 +1339,7 @@ const filterProjectedTargetExtensions = (
|
|
|
1264
1339
|
const withExactMovedTargets = (params: {
|
|
1265
1340
|
readonly apply: boolean;
|
|
1266
1341
|
readonly collected: DownstreamSourceCollection;
|
|
1267
|
-
readonly
|
|
1342
|
+
readonly derivedTargetPaths: ReadonlySet<string>;
|
|
1268
1343
|
readonly renames: readonly VocabularyFileRename[];
|
|
1269
1344
|
readonly resolved: readonly ResolvedFileRename[];
|
|
1270
1345
|
readonly scope: VocabularyRegradeScope | undefined;
|
|
@@ -1279,7 +1354,7 @@ const withExactMovedTargets = (params: {
|
|
|
1279
1354
|
const path = normalizeRenamePath(rename.to);
|
|
1280
1355
|
const sourcePath = sourcePathForMovedTarget(path, params.renames);
|
|
1281
1356
|
if (
|
|
1282
|
-
!params.
|
|
1357
|
+
!params.derivedTargetPaths.has(path) ||
|
|
1283
1358
|
(params.scope?.include !== undefined &&
|
|
1284
1359
|
!matchesAnyPathGlob(sourcePath, params.scope.include)) ||
|
|
1285
1360
|
(params.scope?.exclude !== undefined &&
|
|
@@ -1430,13 +1505,13 @@ export const runFileRenameRegrade = (params: {
|
|
|
1430
1505
|
});
|
|
1431
1506
|
const sourceExtensions =
|
|
1432
1507
|
params.scope?.extensions ?? fileRenameSourceExtensions;
|
|
1433
|
-
const
|
|
1508
|
+
const derivedExtensions = deriveCollectionExtensionsForFileRenames({
|
|
1434
1509
|
apply,
|
|
1435
1510
|
extensions: sourceExtensions,
|
|
1436
1511
|
renames: params.renames,
|
|
1437
1512
|
});
|
|
1438
1513
|
const rawCollection = collectDownstreamSources(params.root, {
|
|
1439
|
-
extensions:
|
|
1514
|
+
extensions: derivedExtensions.extensions,
|
|
1440
1515
|
...(exclude === undefined ? {} : { exclude }),
|
|
1441
1516
|
...(include === undefined ? {} : { include }),
|
|
1442
1517
|
ignoredDirectories: DEFAULT_IGNORED_DIRECTORIES.filter(
|
|
@@ -1452,20 +1527,20 @@ export const runFileRenameRegrade = (params: {
|
|
|
1452
1527
|
const exactTargetCollection = withExactMovedTargets({
|
|
1453
1528
|
apply,
|
|
1454
1529
|
collected: rawCollection,
|
|
1455
|
-
|
|
1530
|
+
derivedTargetPaths: derivedExtensions.derivedTargetPaths,
|
|
1456
1531
|
renames: params.renames,
|
|
1457
1532
|
resolved: validated.value,
|
|
1458
1533
|
scope: params.scope,
|
|
1459
1534
|
});
|
|
1460
|
-
const extensionScopedCollection =
|
|
1535
|
+
const extensionScopedCollection = filterDerivedTargetExtensions(
|
|
1461
1536
|
exactTargetCollection,
|
|
1462
1537
|
sourceExtensions,
|
|
1463
|
-
|
|
1538
|
+
derivedExtensions.derivedTargetPaths
|
|
1464
1539
|
);
|
|
1465
1540
|
const scopedCollection = filterOpenedPolicyDirectories(
|
|
1466
1541
|
extensionScopedCollection,
|
|
1467
1542
|
opened,
|
|
1468
|
-
|
|
1543
|
+
derivedExtensions.derivedTargetPaths,
|
|
1469
1544
|
params.scope,
|
|
1470
1545
|
params.renames,
|
|
1471
1546
|
apply
|
|
@@ -1480,6 +1555,17 @@ export const runFileRenameRegrade = (params: {
|
|
|
1480
1555
|
}
|
|
1481
1556
|
: scopedCollection;
|
|
1482
1557
|
|
|
1558
|
+
const sourceStateHash = fileRenameSourceStateHash({
|
|
1559
|
+
apply,
|
|
1560
|
+
collected,
|
|
1561
|
+
renames: params.renames,
|
|
1562
|
+
resolved: validated.value,
|
|
1563
|
+
});
|
|
1564
|
+
if (sourceStateHash.isErr()) {
|
|
1565
|
+
rollbackResolvedRenames(moved.value);
|
|
1566
|
+
return sourceStateHash;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1483
1569
|
const evidence = params.renames.map(() => emptyEvidence());
|
|
1484
1570
|
const targetPaths = new Set(
|
|
1485
1571
|
params.renames.map((rename) => normalizeRenamePath(rename.to))
|
|
@@ -1516,7 +1602,7 @@ export const runFileRenameRegrade = (params: {
|
|
|
1516
1602
|
}
|
|
1517
1603
|
}
|
|
1518
1604
|
|
|
1519
|
-
const
|
|
1605
|
+
const derivedEvidence = params.renames.map((rename, index) => ({
|
|
1520
1606
|
...rename,
|
|
1521
1607
|
...(evidence[index] ?? emptyEvidence()),
|
|
1522
1608
|
}));
|
|
@@ -1548,10 +1634,10 @@ export const runFileRenameRegrade = (params: {
|
|
|
1548
1634
|
applied:
|
|
1549
1635
|
validated.value.filter((rename) => !rename.alreadyApplied)
|
|
1550
1636
|
.length +
|
|
1551
|
-
|
|
1637
|
+
derivedEvidence.reduce((sum, item) => sum + item.rewritten, 0),
|
|
1552
1638
|
filesChanged: changedFiles.size,
|
|
1553
1639
|
review,
|
|
1554
|
-
skipped:
|
|
1640
|
+
skipped: derivedEvidence.reduce(
|
|
1555
1641
|
(sum, item) => sum + item.skipped,
|
|
1556
1642
|
0
|
|
1557
1643
|
),
|
|
@@ -1582,9 +1668,10 @@ export const runFileRenameRegrade = (params: {
|
|
|
1582
1668
|
|
|
1583
1669
|
return Result.ok({
|
|
1584
1670
|
changedPaths: [...changedFiles].toSorted(),
|
|
1585
|
-
evidence:
|
|
1671
|
+
evidence: derivedEvidence,
|
|
1586
1672
|
occurrencePaths: referenceResult.value.occurrencePaths,
|
|
1587
1673
|
policyOccurrencePaths: referenceResult.value.policyOccurrencePaths,
|
|
1588
1674
|
report,
|
|
1675
|
+
sourceStateHash: sourceStateHash.value,
|
|
1589
1676
|
});
|
|
1590
1677
|
};
|