@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.
@@ -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
- entries,
1049
- matched: rewritten + review,
1050
- review,
1051
- rewritten,
1052
- root: params.root,
1053
- scan: buildRegradeScanSummary({
1054
- matchedPaths,
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
- skippedByReason: skippedReasons,
1058
- }),
1059
- scanned: scannedEntries.length,
1060
- selectedClassIds: selected.map((cls) => cls.id),
1061
- skipped,
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
- ...report,
1136
- apply,
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
- const runRegradeEvaluation = (params: {
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
- }): RegradeEvaluation | null => {
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 buildRegradeEvaluation({
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 files: { absolutePath: string; path: string; source: string }[] = [];
1304
- const skipped: SkippedSource[] = [...collected.skipped];
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
- files.push({
1317
- absolutePath: file.absolutePath,
1318
- ...(packageContext === undefined ? {} : { package: packageContext }),
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
- skipped.push({ path: file.path, reason: 'unreadable-file' });
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
- return buildRegradeEvaluation({
1328
- classes: params.classes,
1329
- ...(params.collection === undefined
1330
- ? {}
1331
- : { collection: params.collection }),
1332
- files,
1333
- root: params.root,
1334
- skipped,
1335
- ...(params.includeEntries === undefined
1336
- ? {}
1337
- : { includeEntries: params.includeEntries }),
1338
- ...(params.selection === undefined ? {} : { selection: params.selection }),
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 evaluation = runRegradeEvaluation(params);
1358
- if (evaluation === null) {
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()