@ontrails/regrade 1.0.0-beta.43 → 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 +27 -0
- package/package.json +11 -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/src/downstream/report.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InternalError,
|
|
3
3
|
Result,
|
|
4
|
+
ValidationError,
|
|
4
5
|
escapeRegExp,
|
|
5
6
|
includedByPathScope,
|
|
6
7
|
} from '@ontrails/core';
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
loadProjectWardenRules,
|
|
20
21
|
wardenRules,
|
|
21
22
|
} from '@ontrails/warden';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
22
24
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
23
25
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
24
26
|
import { z } from 'zod';
|
|
@@ -149,6 +151,45 @@ export interface RegradeApplySummary {
|
|
|
149
151
|
readonly unknown: number;
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Caller-authored identity for one prepared Regrade evaluation.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* const identity: PreparedRegradeRunIdentity = {
|
|
160
|
+
* lockStateHash: 'lock-sha256',
|
|
161
|
+
* planContentHash: 'plan-sha256',
|
|
162
|
+
* policyHash: 'policy-sha256',
|
|
163
|
+
* scopeHash: 'scope-sha256',
|
|
164
|
+
* toolVersion: '1.0.0',
|
|
165
|
+
* };
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export interface PreparedRegradeRunIdentity {
|
|
169
|
+
readonly lockStateHash: string;
|
|
170
|
+
readonly planContentHash: string;
|
|
171
|
+
readonly policyHash: string;
|
|
172
|
+
readonly scopeHash: string;
|
|
173
|
+
readonly toolVersion: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* In-memory class/symbol Regrade evaluation ready for freshness-checked apply.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const prepared = prepareRegradeRun({ classes, identity, root });
|
|
182
|
+
* if (prepared.isOk() && prepared.value !== null) {
|
|
183
|
+
* console.log(prepared.value.sourceStateHash);
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export interface PreparedRegradeRun {
|
|
188
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
189
|
+
readonly report: RegradeReport;
|
|
190
|
+
readonly sourceStateHash: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
152
193
|
/** Source location for a review-required match. */
|
|
153
194
|
export interface RegradeReviewSpan {
|
|
154
195
|
readonly column: number;
|
|
@@ -174,6 +215,8 @@ export interface RegradeReviewDetail {
|
|
|
174
215
|
readonly candidateReplacement?: string;
|
|
175
216
|
/** Class that produced the review detail, injected by report building. */
|
|
176
217
|
readonly classId?: string;
|
|
218
|
+
/** Exact source-line context containing the occurrence under review. */
|
|
219
|
+
readonly context?: string;
|
|
177
220
|
/** Expected target shape when the class can describe one. */
|
|
178
221
|
readonly expectedTarget?: string;
|
|
179
222
|
/** Fixture or example reference that illustrates the expected migration. */
|
|
@@ -731,6 +774,12 @@ export interface RegradeReport {
|
|
|
731
774
|
readonly unknownClassIds: readonly string[];
|
|
732
775
|
/** Source files inspected. */
|
|
733
776
|
readonly scanned: number;
|
|
777
|
+
/**
|
|
778
|
+
* Root-relative source paths used to deduplicate composed scans.
|
|
779
|
+
*
|
|
780
|
+
* @internal
|
|
781
|
+
*/
|
|
782
|
+
readonly scannedPaths?: readonly string[];
|
|
734
783
|
/** Files where a selected class produced a rewrite or review outcome. */
|
|
735
784
|
readonly matched: number;
|
|
736
785
|
/** Files with a rewrite outcome. */
|
|
@@ -775,6 +824,26 @@ export interface RegradeReport {
|
|
|
775
824
|
};
|
|
776
825
|
}
|
|
777
826
|
|
|
827
|
+
const withScannedPaths = (
|
|
828
|
+
report: RegradeReport,
|
|
829
|
+
paths: readonly string[]
|
|
830
|
+
): RegradeReport => {
|
|
831
|
+
Object.defineProperty(report, 'scannedPaths', {
|
|
832
|
+
configurable: false,
|
|
833
|
+
enumerable: false,
|
|
834
|
+
value: Object.freeze([...paths]),
|
|
835
|
+
writable: false,
|
|
836
|
+
});
|
|
837
|
+
return report;
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const cloneRegradeReport = (report: RegradeReport): RegradeReport => {
|
|
841
|
+
const clone = structuredClone(report);
|
|
842
|
+
return report.scannedPaths === undefined
|
|
843
|
+
? clone
|
|
844
|
+
: withScannedPaths(clone, report.scannedPaths);
|
|
845
|
+
};
|
|
846
|
+
|
|
778
847
|
interface RegradeRewriteCandidate {
|
|
779
848
|
readonly absolutePath: string;
|
|
780
849
|
readonly classId: string;
|
|
@@ -1044,24 +1113,27 @@ const buildRegradeEvaluation = (params: {
|
|
|
1044
1113
|
const skippedReasons = skipsByReason(allEntries);
|
|
1045
1114
|
|
|
1046
1115
|
return {
|
|
1047
|
-
report:
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1116
|
+
report: withScannedPaths(
|
|
1117
|
+
{
|
|
1118
|
+
entries,
|
|
1119
|
+
matched: rewritten + review,
|
|
1120
|
+
review,
|
|
1121
|
+
rewritten,
|
|
1122
|
+
root: params.root,
|
|
1123
|
+
scan: buildRegradeScanSummary({
|
|
1124
|
+
matchedPaths,
|
|
1125
|
+
scanned: scannedEntries.length,
|
|
1126
|
+
skipped,
|
|
1127
|
+
skippedByReason: skippedReasons,
|
|
1128
|
+
}),
|
|
1055
1129
|
scanned: scannedEntries.length,
|
|
1130
|
+
selectedClassIds: selected.map((cls) => cls.id),
|
|
1056
1131
|
skipped,
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
skipsByReason: skippedReasons,
|
|
1063
|
-
unknownClassIds,
|
|
1064
|
-
},
|
|
1132
|
+
skipsByReason: skippedReasons,
|
|
1133
|
+
unknownClassIds,
|
|
1134
|
+
},
|
|
1135
|
+
scannedEntries.map((entry) => entry.path)
|
|
1136
|
+
),
|
|
1065
1137
|
rewrites,
|
|
1066
1138
|
};
|
|
1067
1139
|
};
|
|
@@ -1131,10 +1203,16 @@ const applyRegradeEvaluation = (
|
|
|
1131
1203
|
const withApplySummary = (
|
|
1132
1204
|
report: RegradeReport,
|
|
1133
1205
|
apply: RegradeApplySummary
|
|
1134
|
-
): RegradeReport =>
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1206
|
+
): RegradeReport => {
|
|
1207
|
+
const appliedReport: RegradeReport = {
|
|
1208
|
+
...report,
|
|
1209
|
+
apply,
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
return report.scannedPaths === undefined
|
|
1213
|
+
? appliedReport
|
|
1214
|
+
: withScannedPaths(appliedReport, report.scannedPaths);
|
|
1215
|
+
};
|
|
1138
1216
|
|
|
1139
1217
|
const canReadDownstreamRoot = (root: string): boolean => {
|
|
1140
1218
|
try {
|
|
@@ -1260,13 +1338,109 @@ const packageContextFor = (
|
|
|
1260
1338
|
return undefined;
|
|
1261
1339
|
};
|
|
1262
1340
|
|
|
1263
|
-
|
|
1341
|
+
interface RegradeSourceState {
|
|
1342
|
+
readonly hash: string;
|
|
1343
|
+
readonly unreadable: readonly string[];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
interface CollectedRegradeEvaluation {
|
|
1347
|
+
readonly evaluation: RegradeEvaluation;
|
|
1348
|
+
readonly sourceState: RegradeSourceState;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
interface PrepareRegradeRunParams {
|
|
1264
1352
|
readonly root: string;
|
|
1265
1353
|
readonly classes: readonly RegradeClass[];
|
|
1266
1354
|
readonly selection?: RegradeSelection;
|
|
1267
1355
|
readonly collection?: DownstreamCollectionOptions;
|
|
1268
1356
|
readonly includeEntries?: RegradeReportEntrySelection;
|
|
1269
|
-
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const snapshotPrepareRegradeRunParams = (
|
|
1360
|
+
params: PrepareRegradeRunParams
|
|
1361
|
+
): PrepareRegradeRunParams => ({
|
|
1362
|
+
classes: params.classes.map((regradeClass) => ({
|
|
1363
|
+
...regradeClass,
|
|
1364
|
+
...(regradeClass.scanTargets === undefined
|
|
1365
|
+
? {}
|
|
1366
|
+
: {
|
|
1367
|
+
scanTargets: {
|
|
1368
|
+
...regradeClass.scanTargets,
|
|
1369
|
+
...(regradeClass.scanTargets.exclude === undefined
|
|
1370
|
+
? {}
|
|
1371
|
+
: { exclude: [...regradeClass.scanTargets.exclude] }),
|
|
1372
|
+
...(regradeClass.scanTargets.extensions === undefined
|
|
1373
|
+
? {}
|
|
1374
|
+
: { extensions: [...regradeClass.scanTargets.extensions] }),
|
|
1375
|
+
...(regradeClass.scanTargets.ignoredDirectories === undefined
|
|
1376
|
+
? {}
|
|
1377
|
+
: {
|
|
1378
|
+
ignoredDirectories: [
|
|
1379
|
+
...regradeClass.scanTargets.ignoredDirectories,
|
|
1380
|
+
],
|
|
1381
|
+
}),
|
|
1382
|
+
},
|
|
1383
|
+
}),
|
|
1384
|
+
})),
|
|
1385
|
+
...(params.collection === undefined
|
|
1386
|
+
? {}
|
|
1387
|
+
: {
|
|
1388
|
+
collection: {
|
|
1389
|
+
...params.collection,
|
|
1390
|
+
...(params.collection.exclude === undefined
|
|
1391
|
+
? {}
|
|
1392
|
+
: { exclude: [...params.collection.exclude] }),
|
|
1393
|
+
...(params.collection.extensions === undefined
|
|
1394
|
+
? {}
|
|
1395
|
+
: { extensions: [...params.collection.extensions] }),
|
|
1396
|
+
...(params.collection.ignoredDirectories === undefined
|
|
1397
|
+
? {}
|
|
1398
|
+
: {
|
|
1399
|
+
ignoredDirectories: [...params.collection.ignoredDirectories],
|
|
1400
|
+
}),
|
|
1401
|
+
...(params.collection.include === undefined
|
|
1402
|
+
? {}
|
|
1403
|
+
: { include: [...params.collection.include] }),
|
|
1404
|
+
},
|
|
1405
|
+
}),
|
|
1406
|
+
...(params.includeEntries === undefined
|
|
1407
|
+
? {}
|
|
1408
|
+
: { includeEntries: params.includeEntries }),
|
|
1409
|
+
root: params.root,
|
|
1410
|
+
...(params.selection === undefined
|
|
1411
|
+
? {}
|
|
1412
|
+
: {
|
|
1413
|
+
selection:
|
|
1414
|
+
params.selection.classIds === undefined
|
|
1415
|
+
? {}
|
|
1416
|
+
: { classIds: [...params.selection.classIds] },
|
|
1417
|
+
}),
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
const compareRegradeCodeUnits = (left: string, right: string): number => {
|
|
1421
|
+
if (left < right) {
|
|
1422
|
+
return -1;
|
|
1423
|
+
}
|
|
1424
|
+
if (left > right) {
|
|
1425
|
+
return 1;
|
|
1426
|
+
}
|
|
1427
|
+
return 0;
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
const regradeSourceStateHash = (state: {
|
|
1431
|
+
readonly manifests: readonly {
|
|
1432
|
+
readonly bytes: string;
|
|
1433
|
+
readonly path: string;
|
|
1434
|
+
}[];
|
|
1435
|
+
readonly sources: readonly {
|
|
1436
|
+
readonly bytes: string;
|
|
1437
|
+
readonly path: string;
|
|
1438
|
+
}[];
|
|
1439
|
+
}): string => createHash('sha256').update(JSON.stringify(state)).digest('hex');
|
|
1440
|
+
|
|
1441
|
+
const collectRegradeEvaluation = (
|
|
1442
|
+
params: PrepareRegradeRunParams
|
|
1443
|
+
): CollectedRegradeEvaluation | null => {
|
|
1270
1444
|
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
1271
1445
|
params.classes,
|
|
1272
1446
|
params.selection
|
|
@@ -1275,23 +1449,133 @@ const runRegradeEvaluation = (params: {
|
|
|
1275
1449
|
if (!canReadDownstreamRoot(params.root)) {
|
|
1276
1450
|
return null;
|
|
1277
1451
|
}
|
|
1278
|
-
return
|
|
1452
|
+
return {
|
|
1453
|
+
evaluation: buildRegradeEvaluation({
|
|
1454
|
+
classes: params.classes,
|
|
1455
|
+
...(params.collection === undefined
|
|
1456
|
+
? {}
|
|
1457
|
+
: { collection: params.collection }),
|
|
1458
|
+
files: [],
|
|
1459
|
+
root: params.root,
|
|
1460
|
+
skipped: [],
|
|
1461
|
+
...(params.includeEntries === undefined
|
|
1462
|
+
? {}
|
|
1463
|
+
: { includeEntries: params.includeEntries }),
|
|
1464
|
+
...(params.selection === undefined
|
|
1465
|
+
? {}
|
|
1466
|
+
: { selection: params.selection }),
|
|
1467
|
+
}),
|
|
1468
|
+
sourceState: {
|
|
1469
|
+
hash: regradeSourceStateHash({ manifests: [], sources: [] }),
|
|
1470
|
+
unreadable: [],
|
|
1471
|
+
},
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const collected = collectDownstreamSources(
|
|
1476
|
+
params.root,
|
|
1477
|
+
deriveCollectionOptions(selected, params.collection)
|
|
1478
|
+
);
|
|
1479
|
+
if (collected === null) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const files: { absolutePath: string; path: string; source: string }[] = [];
|
|
1484
|
+
const skipped: SkippedSource[] = [...collected.skipped];
|
|
1485
|
+
const sourceBytes: { readonly bytes: string; readonly path: string }[] = [];
|
|
1486
|
+
const unreadable = new Set(
|
|
1487
|
+
collected.skipped
|
|
1488
|
+
.filter((entry) => entry.reason === 'unreadable-directory')
|
|
1489
|
+
.map((entry) => entry.path)
|
|
1490
|
+
);
|
|
1491
|
+
const manifestPaths = new Set<string>();
|
|
1492
|
+
const packageContextCache = new Map<
|
|
1493
|
+
string,
|
|
1494
|
+
RegradeClassContext['package'] | undefined
|
|
1495
|
+
>();
|
|
1496
|
+
for (const file of collected.files) {
|
|
1497
|
+
try {
|
|
1498
|
+
const bytes = readFileSync(file.absolutePath);
|
|
1499
|
+
const packageContext = packageContextFor(
|
|
1500
|
+
params.root,
|
|
1501
|
+
file.absolutePath,
|
|
1502
|
+
packageContextCache
|
|
1503
|
+
);
|
|
1504
|
+
if (packageContext !== undefined) {
|
|
1505
|
+
manifestPaths.add(packageContext.path);
|
|
1506
|
+
}
|
|
1507
|
+
files.push({
|
|
1508
|
+
absolutePath: file.absolutePath,
|
|
1509
|
+
...(packageContext === undefined ? {} : { package: packageContext }),
|
|
1510
|
+
path: file.path,
|
|
1511
|
+
source: bytes.toString('utf8'),
|
|
1512
|
+
});
|
|
1513
|
+
sourceBytes.push({ bytes: bytes.toString('base64'), path: file.path });
|
|
1514
|
+
} catch {
|
|
1515
|
+
skipped.push({ path: file.path, reason: 'unreadable-file' });
|
|
1516
|
+
unreadable.add(file.path);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const manifestBytes = [...manifestPaths].toSorted().flatMap((path) => {
|
|
1521
|
+
try {
|
|
1522
|
+
return [
|
|
1523
|
+
{
|
|
1524
|
+
bytes: readFileSync(join(resolve(params.root), path)).toString(
|
|
1525
|
+
'base64'
|
|
1526
|
+
),
|
|
1527
|
+
path,
|
|
1528
|
+
},
|
|
1529
|
+
];
|
|
1530
|
+
} catch {
|
|
1531
|
+
unreadable.add(path);
|
|
1532
|
+
return [];
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
return {
|
|
1537
|
+
evaluation: buildRegradeEvaluation({
|
|
1279
1538
|
classes: params.classes,
|
|
1280
1539
|
...(params.collection === undefined
|
|
1281
1540
|
? {}
|
|
1282
1541
|
: { collection: params.collection }),
|
|
1283
|
-
files
|
|
1542
|
+
files,
|
|
1284
1543
|
root: params.root,
|
|
1285
|
-
skipped
|
|
1544
|
+
skipped,
|
|
1286
1545
|
...(params.includeEntries === undefined
|
|
1287
1546
|
? {}
|
|
1288
1547
|
: { includeEntries: params.includeEntries }),
|
|
1289
1548
|
...(params.selection === undefined
|
|
1290
1549
|
? {}
|
|
1291
1550
|
: { selection: params.selection }),
|
|
1292
|
-
})
|
|
1293
|
-
|
|
1551
|
+
}),
|
|
1552
|
+
sourceState: {
|
|
1553
|
+
hash: regradeSourceStateHash({
|
|
1554
|
+
manifests: manifestBytes,
|
|
1555
|
+
sources: sourceBytes.toSorted((left, right) =>
|
|
1556
|
+
compareRegradeCodeUnits(left.path, right.path)
|
|
1557
|
+
),
|
|
1558
|
+
}),
|
|
1559
|
+
unreadable: [...unreadable].toSorted(),
|
|
1560
|
+
},
|
|
1561
|
+
};
|
|
1562
|
+
};
|
|
1294
1563
|
|
|
1564
|
+
const collectRegradeSourceState = (
|
|
1565
|
+
params: PrepareRegradeRunParams
|
|
1566
|
+
): RegradeSourceState | null => {
|
|
1567
|
+
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
1568
|
+
params.classes,
|
|
1569
|
+
params.selection
|
|
1570
|
+
);
|
|
1571
|
+
if (selected.length === 0 && unknownClassIds.length > 0) {
|
|
1572
|
+
return canReadDownstreamRoot(params.root)
|
|
1573
|
+
? {
|
|
1574
|
+
hash: regradeSourceStateHash({ manifests: [], sources: [] }),
|
|
1575
|
+
unreadable: [],
|
|
1576
|
+
}
|
|
1577
|
+
: null;
|
|
1578
|
+
}
|
|
1295
1579
|
const collected = collectDownstreamSources(
|
|
1296
1580
|
params.root,
|
|
1297
1581
|
deriveCollectionOptions(selected, params.collection)
|
|
@@ -1299,44 +1583,226 @@ const runRegradeEvaluation = (params: {
|
|
|
1299
1583
|
if (collected === null) {
|
|
1300
1584
|
return null;
|
|
1301
1585
|
}
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1586
|
+
const sourceBytes: { readonly bytes: string; readonly path: string }[] = [];
|
|
1587
|
+
const unreadable = new Set(
|
|
1588
|
+
collected.skipped
|
|
1589
|
+
.filter((entry) => entry.reason === 'unreadable-directory')
|
|
1590
|
+
.map((entry) => entry.path)
|
|
1591
|
+
);
|
|
1592
|
+
const manifestPaths = new Set<string>();
|
|
1305
1593
|
const packageContextCache = new Map<
|
|
1306
1594
|
string,
|
|
1307
1595
|
RegradeClassContext['package'] | undefined
|
|
1308
1596
|
>();
|
|
1309
1597
|
for (const file of collected.files) {
|
|
1310
1598
|
try {
|
|
1599
|
+
const bytes = readFileSync(file.absolutePath);
|
|
1600
|
+
sourceBytes.push({ bytes: bytes.toString('base64'), path: file.path });
|
|
1311
1601
|
const packageContext = packageContextFor(
|
|
1312
1602
|
params.root,
|
|
1313
1603
|
file.absolutePath,
|
|
1314
1604
|
packageContextCache
|
|
1315
1605
|
);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
path: file.path,
|
|
1320
|
-
source: readFileSync(file.absolutePath, 'utf8'),
|
|
1321
|
-
});
|
|
1606
|
+
if (packageContext !== undefined) {
|
|
1607
|
+
manifestPaths.add(packageContext.path);
|
|
1608
|
+
}
|
|
1322
1609
|
} catch {
|
|
1323
|
-
|
|
1610
|
+
unreadable.add(file.path);
|
|
1324
1611
|
}
|
|
1325
1612
|
}
|
|
1613
|
+
const manifestBytes = [...manifestPaths]
|
|
1614
|
+
.toSorted(compareRegradeCodeUnits)
|
|
1615
|
+
.flatMap((path) => {
|
|
1616
|
+
try {
|
|
1617
|
+
return [
|
|
1618
|
+
{
|
|
1619
|
+
bytes: readFileSync(join(resolve(params.root), path)).toString(
|
|
1620
|
+
'base64'
|
|
1621
|
+
),
|
|
1622
|
+
path,
|
|
1623
|
+
},
|
|
1624
|
+
];
|
|
1625
|
+
} catch {
|
|
1626
|
+
unreadable.add(path);
|
|
1627
|
+
return [];
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
return {
|
|
1631
|
+
hash: regradeSourceStateHash({
|
|
1632
|
+
manifests: manifestBytes,
|
|
1633
|
+
sources: sourceBytes.toSorted((left, right) =>
|
|
1634
|
+
compareRegradeCodeUnits(left.path, right.path)
|
|
1635
|
+
),
|
|
1636
|
+
}),
|
|
1637
|
+
unreadable: [...unreadable].toSorted(compareRegradeCodeUnits),
|
|
1638
|
+
};
|
|
1639
|
+
};
|
|
1326
1640
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1641
|
+
interface PreparedRegradeRunState {
|
|
1642
|
+
readonly evaluation: RegradeEvaluation;
|
|
1643
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
1644
|
+
readonly params: PrepareRegradeRunParams;
|
|
1645
|
+
readonly report: RegradeReport;
|
|
1646
|
+
readonly sourceStateHash: string;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const preparedRegradeRunStates = new WeakMap<
|
|
1650
|
+
PreparedRegradeRun,
|
|
1651
|
+
PreparedRegradeRunState
|
|
1652
|
+
>();
|
|
1653
|
+
|
|
1654
|
+
const validatePreparedIdentity = (
|
|
1655
|
+
expected: PreparedRegradeRunIdentity,
|
|
1656
|
+
actual: PreparedRegradeRunIdentity
|
|
1657
|
+
): Result<void, ValidationError> => {
|
|
1658
|
+
for (const field of [
|
|
1659
|
+
'planContentHash',
|
|
1660
|
+
'policyHash',
|
|
1661
|
+
'scopeHash',
|
|
1662
|
+
'lockStateHash',
|
|
1663
|
+
'toolVersion',
|
|
1664
|
+
] as const) {
|
|
1665
|
+
if (expected[field] !== actual[field]) {
|
|
1666
|
+
return Result.err(
|
|
1667
|
+
new ValidationError(
|
|
1668
|
+
`Prepared Regrade identity field \`${field}\` is stale.`,
|
|
1669
|
+
{
|
|
1670
|
+
context: {
|
|
1671
|
+
actual: actual[field],
|
|
1672
|
+
expected: expected[field],
|
|
1673
|
+
field,
|
|
1674
|
+
},
|
|
1675
|
+
}
|
|
1676
|
+
)
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return Result.ok();
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Classify a class/symbol Regrade run once and retain it for checked apply.
|
|
1685
|
+
*
|
|
1686
|
+
* @example
|
|
1687
|
+
* ```ts
|
|
1688
|
+
* const prepared = prepareRegradeRun({ classes, identity, root });
|
|
1689
|
+
* if (prepared.isErr()) throw prepared.error;
|
|
1690
|
+
* ```
|
|
1691
|
+
*/
|
|
1692
|
+
export const prepareRegradeRun = (
|
|
1693
|
+
params: PrepareRegradeRunParams & {
|
|
1694
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
1695
|
+
}
|
|
1696
|
+
): Result<PreparedRegradeRun | null, ValidationError> => {
|
|
1697
|
+
const preparedParams = snapshotPrepareRegradeRunParams(params);
|
|
1698
|
+
const collected = collectRegradeEvaluation(preparedParams);
|
|
1699
|
+
if (collected === null) {
|
|
1700
|
+
return Result.ok(null);
|
|
1701
|
+
}
|
|
1702
|
+
if (collected.sourceState.unreadable.length > 0) {
|
|
1703
|
+
return Result.err(
|
|
1704
|
+
new ValidationError('Prepared Regrade sources must all be readable.', {
|
|
1705
|
+
context: { paths: collected.sourceState.unreadable },
|
|
1706
|
+
})
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
const prepared: PreparedRegradeRun = {
|
|
1710
|
+
identity: { ...params.identity },
|
|
1711
|
+
report: cloneRegradeReport(collected.evaluation.report),
|
|
1712
|
+
sourceStateHash: collected.sourceState.hash,
|
|
1713
|
+
};
|
|
1714
|
+
preparedRegradeRunStates.set(prepared, {
|
|
1715
|
+
evaluation: collected.evaluation,
|
|
1716
|
+
identity: { ...params.identity },
|
|
1717
|
+
params: preparedParams,
|
|
1718
|
+
report: collected.evaluation.report,
|
|
1719
|
+
sourceStateHash: collected.sourceState.hash,
|
|
1339
1720
|
});
|
|
1721
|
+
return Result.ok(prepared);
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* Validate that an in-memory class/symbol evaluation still matches its
|
|
1726
|
+
* authored identity and source bytes without applying it.
|
|
1727
|
+
*
|
|
1728
|
+
* @example
|
|
1729
|
+
* ```ts
|
|
1730
|
+
* const current = validatePreparedRegradeRun(prepared, identity);
|
|
1731
|
+
* if (current.isErr()) throw current.error;
|
|
1732
|
+
* ```
|
|
1733
|
+
*/
|
|
1734
|
+
export const validatePreparedRegradeRun = (
|
|
1735
|
+
prepared: PreparedRegradeRun,
|
|
1736
|
+
identity: PreparedRegradeRunIdentity
|
|
1737
|
+
): Result<void, ValidationError> => {
|
|
1738
|
+
const state = preparedRegradeRunStates.get(prepared);
|
|
1739
|
+
if (state === undefined) {
|
|
1740
|
+
return Result.err(
|
|
1741
|
+
new ValidationError(
|
|
1742
|
+
'Prepared Regrade run is not the original in-memory evaluation.'
|
|
1743
|
+
)
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
const identityValidation = validatePreparedIdentity(state.identity, identity);
|
|
1747
|
+
if (identityValidation.isErr()) {
|
|
1748
|
+
return identityValidation;
|
|
1749
|
+
}
|
|
1750
|
+
const current = collectRegradeSourceState(state.params);
|
|
1751
|
+
if (current === null) {
|
|
1752
|
+
return Result.err(
|
|
1753
|
+
new ValidationError('Prepared Regrade root is no longer readable.')
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
if (current.unreadable.length > 0) {
|
|
1757
|
+
return Result.err(
|
|
1758
|
+
new ValidationError('Prepared Regrade sources are no longer readable.', {
|
|
1759
|
+
context: { paths: current.unreadable },
|
|
1760
|
+
})
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
if (current.hash !== state.sourceStateHash) {
|
|
1764
|
+
return Result.err(
|
|
1765
|
+
new ValidationError('Prepared Regrade source state is stale.', {
|
|
1766
|
+
context: {
|
|
1767
|
+
actual: current.hash,
|
|
1768
|
+
expected: state.sourceStateHash,
|
|
1769
|
+
},
|
|
1770
|
+
})
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
return Result.ok();
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Apply an in-memory class/symbol evaluation after identity and source checks.
|
|
1778
|
+
*
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* const applied = applyPreparedRegradeRun(prepared, identity);
|
|
1782
|
+
* if (applied.isErr()) throw applied.error;
|
|
1783
|
+
* ```
|
|
1784
|
+
*/
|
|
1785
|
+
export const applyPreparedRegradeRun = (
|
|
1786
|
+
prepared: PreparedRegradeRun,
|
|
1787
|
+
identity: PreparedRegradeRunIdentity
|
|
1788
|
+
): Result<RegradeReport, InternalError | ValidationError> => {
|
|
1789
|
+
const state = preparedRegradeRunStates.get(prepared);
|
|
1790
|
+
const validation = validatePreparedRegradeRun(prepared, identity);
|
|
1791
|
+
if (validation.isErr()) {
|
|
1792
|
+
return validation;
|
|
1793
|
+
}
|
|
1794
|
+
if (state === undefined) {
|
|
1795
|
+
return Result.err(
|
|
1796
|
+
new ValidationError(
|
|
1797
|
+
'Prepared Regrade run is not the original in-memory evaluation.'
|
|
1798
|
+
)
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
const applyResult = applyRegradeEvaluation(state.evaluation);
|
|
1802
|
+
if (applyResult.isErr()) {
|
|
1803
|
+
return applyResult;
|
|
1804
|
+
}
|
|
1805
|
+
return Result.ok(withApplySummary(state.report, applyResult.value));
|
|
1340
1806
|
};
|
|
1341
1807
|
|
|
1342
1808
|
/**
|
|
@@ -1354,10 +1820,11 @@ export const runRegrade = (params: {
|
|
|
1354
1820
|
readonly apply?: boolean;
|
|
1355
1821
|
readonly includeEntries?: RegradeReportEntrySelection;
|
|
1356
1822
|
}): Result<RegradeReport | null, InternalError> => {
|
|
1357
|
-
const
|
|
1358
|
-
if (
|
|
1823
|
+
const collected = collectRegradeEvaluation(params);
|
|
1824
|
+
if (collected === null) {
|
|
1359
1825
|
return Result.ok(null);
|
|
1360
1826
|
}
|
|
1827
|
+
const { evaluation } = collected;
|
|
1361
1828
|
|
|
1362
1829
|
if (params.apply !== true) {
|
|
1363
1830
|
return Result.ok(evaluation.report);
|
|
@@ -1392,6 +1859,10 @@ const regradeReportEntrySchema = z.object({
|
|
|
1392
1859
|
.string()
|
|
1393
1860
|
.optional()
|
|
1394
1861
|
.describe('Class that produced the review detail'),
|
|
1862
|
+
context: z
|
|
1863
|
+
.string()
|
|
1864
|
+
.optional()
|
|
1865
|
+
.describe('Exact source-line context containing the occurrence'),
|
|
1395
1866
|
expectedTarget: z
|
|
1396
1867
|
.string()
|
|
1397
1868
|
.optional()
|