@ontrails/regrade 1.0.0-beta.45 → 1.0.0-beta.47

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @ontrails/regrade
2
2
 
3
+ ## 1.0.0-beta.47
4
+
5
+ ### Patch Changes
6
+
7
+ - [`90d394c`](https://github.com/outfitter-dev/trails/commit/90d394c005fdf6b898ba7052d0b56755af0f4954): Derive nested worktree, repository, and submodule collection boundaries in the
8
+ shared Source walker. Regrade and Warden now observe one directly targeted
9
+ working tree per run, and Regrade audit summaries expose boundary skip counts.
10
+
11
+ ## 1.0.0-beta.46
12
+
13
+ ### Minor Changes
14
+
15
+ - [`701ab85`](https://github.com/outfitter-dev/trails/commit/701ab85bd237e5fcf03725c56b70adb3612d6c15): Expose in-memory prepared class and vocabulary evaluations with deterministic identity and source freshness checks.
16
+ - [`c83d0b6`](https://github.com/outfitter-dev/trails/commit/c83d0b67ac928af1b44c1f2d2c4b36aa09a24a70): Inventory parser-native source comments and TSDoc as exact review-only entries
17
+ for governed classified vocabulary transitions across CLI, MCP, and audit.
18
+ - [`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.
19
+ - [`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.
20
+
21
+ ### Patch Changes
22
+
23
+ - [`768cc79`](https://github.com/outfitter-dev/trails/commit/768cc79ca10947b8808b376e281e1a81131b4acc): Close missed projection vocabulary residue in Regrade internals and public
24
+ error-rendering guidance, and keep lifecycle-ambiguous governed identifiers in
25
+ the Warden review inventory instead of assigning them an unsafe automatic
26
+ target.
27
+
3
28
  ## 1.0.0-beta.45
4
29
 
5
30
  ## 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.45",
3
+ "version": "1.0.0-beta.47",
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.45",
31
- "@ontrails/source": "^1.0.0-beta.45",
32
- "@ontrails/warden": "^1.0.0-beta.45",
30
+ "@ontrails/core": "^1.0.0-beta.47",
31
+ "@ontrails/source": "^1.0.0-beta.47",
32
+ "@ontrails/warden": "^1.0.0-beta.47",
33
33
  "zod": "^4.3.5"
34
34
  },
35
35
  "devDependencies": {
36
- "@ontrails/testing": "^1.0.0-beta.45",
37
- "@ontrails/topography": "^1.0.0-beta.45"
36
+ "@ontrails/testing": "^1.0.0-beta.47",
37
+ "@ontrails/topography": "^1.0.0-beta.47"
38
38
  }
39
39
  }
@@ -4,8 +4,7 @@ import {
4
4
  matchesAnyPathGlob,
5
5
  trail,
6
6
  } from '@ontrails/core';
7
- import { readdirSync } from 'node:fs';
8
- import { join, posix, relative, resolve, sep } from 'node:path';
7
+ import { collectSourceTree } from '@ontrails/source';
9
8
  import { z } from 'zod';
10
9
 
11
10
  /**
@@ -124,46 +123,10 @@ export const classifyDownstreamEntry = (
124
123
  : { action: 'skip', reason: 'unsupported-extension' };
125
124
  };
126
125
 
127
- const toPosixRelative = (root: string, absolutePath: string): string => {
128
- const rel = relative(root, absolutePath);
129
- return sep === posix.sep ? rel : rel.split(sep).join(posix.sep);
130
- };
131
-
132
126
  const isImmutableRegradeHistoryDirectory = (path: string): boolean =>
133
127
  path === '.trails/regrade/history' ||
134
128
  path.endsWith('/.trails/regrade/history');
135
129
 
136
- const direntKind = (dirent: {
137
- isDirectory(): boolean;
138
- isFile(): boolean;
139
- }): DownstreamEntryKind => {
140
- if (dirent.isDirectory()) {
141
- return 'directory';
142
- }
143
- return dirent.isFile() ? 'file' : 'other';
144
- };
145
-
146
- type DirectoryRead =
147
- | {
148
- readonly ok: true;
149
- readonly entries: readonly {
150
- readonly name: string;
151
- readonly kind: DownstreamEntryKind;
152
- }[];
153
- }
154
- | { readonly ok: false };
155
-
156
- const readDirectory = (absoluteDir: string): DirectoryRead => {
157
- try {
158
- const entries = readdirSync(absoluteDir, { withFileTypes: true }).map(
159
- (dirent) => ({ kind: direntKind(dirent), name: dirent.name })
160
- );
161
- return { entries, ok: true };
162
- } catch {
163
- return { ok: false };
164
- }
165
- };
166
-
167
130
  /**
168
131
  * Walk an explicit downstream root and collect candidate source files.
169
132
  *
@@ -175,69 +138,27 @@ const readDirectory = (absoluteDir: string): DirectoryRead => {
175
138
  export const collectDownstreamSources = (
176
139
  root: string,
177
140
  options: DownstreamCollectionOptions = {}
178
- ): DownstreamSourceCollection | null => {
179
- const absoluteRoot = resolve(root);
180
- const rootRead = readDirectory(absoluteRoot);
181
- if (!rootRead.ok) {
182
- return null;
183
- }
184
-
185
- const files: CollectedSource[] = [];
186
- const skipped: SkippedSource[] = [];
187
- const queue: string[] = [absoluteRoot];
188
-
189
- while (queue.length > 0) {
190
- const current = queue.shift() as string;
191
- const read = current === absoluteRoot ? rootRead : readDirectory(current);
192
- if (!read.ok) {
193
- skipped.push({
194
- path: toPosixRelative(absoluteRoot, current),
195
- reason: 'unreadable-directory',
196
- });
197
- continue;
198
- }
199
-
200
- for (const entry of read.entries) {
201
- const absolutePath = join(current, entry.name);
202
- const path = toPosixRelative(absoluteRoot, absolutePath);
141
+ ): DownstreamSourceCollection | null =>
142
+ collectSourceTree(root, {
143
+ classify: ({ kind, name, path }) => {
203
144
  if (matchesAnyPathGlob(path, options.exclude)) {
204
- skipped.push({ path, reason: 'ignored-glob' });
205
- continue;
145
+ return { action: 'skip', reason: 'ignored-glob' };
146
+ }
147
+ if (kind === 'directory' && isImmutableRegradeHistoryDirectory(path)) {
148
+ return { action: 'skip', reason: 'immutable-regrade-history' };
206
149
  }
150
+ const classification = classifyDownstreamEntry(name, kind, options);
207
151
  if (
208
- entry.kind === 'directory' &&
209
- isImmutableRegradeHistoryDirectory(path)
152
+ classification.action === 'collect' &&
153
+ options.include !== undefined &&
154
+ options.include.length > 0 &&
155
+ !matchesAnyPathGlob(path, options.include)
210
156
  ) {
211
- skipped.push({ path, reason: 'immutable-regrade-history' });
212
- continue;
157
+ return { action: 'skip', reason: 'not-included-glob' };
213
158
  }
214
- const classification = classifyDownstreamEntry(
215
- entry.name,
216
- entry.kind,
217
- options
218
- );
219
- if (classification.action === 'collect') {
220
- if (
221
- options.include !== undefined &&
222
- options.include.length > 0 &&
223
- !matchesAnyPathGlob(path, options.include)
224
- ) {
225
- skipped.push({ path, reason: 'not-included-glob' });
226
- } else {
227
- files.push({ absolutePath, path });
228
- }
229
- } else if (classification.action === 'recurse') {
230
- queue.push(absolutePath);
231
- } else {
232
- skipped.push({ path, reason: classification.reason });
233
- }
234
- }
235
- }
236
-
237
- files.sort((a, b) => a.path.localeCompare(b.path));
238
- skipped.sort((a, b) => a.path.localeCompare(b.path));
239
- return { files, root: absoluteRoot, skipped };
240
- };
159
+ return classification;
160
+ },
161
+ });
241
162
 
242
163
  export const collectDownstreamSourcesInput = z.object({
243
164
  exclude: z
@@ -1468,7 +1468,7 @@ const applyMcpTrailheadsClass = (
1468
1468
  };
1469
1469
 
1470
1470
  /**
1471
- * Project call-site MCP trailhead maps into `surfaceOverlay({ mcp: { name:
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
- * Project a Warden rule that advertises the `export-restructure` fix class
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 projectVocabularyText = (
310
+ const deriveVocabularyText = (
236
311
  source: string,
237
312
  plan: VocabularyRegradePlan
238
313
  ): string => {
239
- let projected = source;
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
- projected = projected.replaceAll(
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 projected;
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
- : [projectVocabularyText(rename.from, vocabularyPlan)]),
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
- projectedTargetPaths: ReadonlySet<string>,
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
- projectedTargetPaths.has(normalizeRenamePath(file.path)) ||
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 collectionExtensionProjectionForFileRenames = (params: {
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 projectedTargetPaths: ReadonlySet<string>;
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 projectedTargetPaths = new Set(
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([...projectedTargetPaths].map((path) => extname(path))),
1305
+ ...new Set([...derivedTargetPaths].map((path) => extname(path))),
1230
1306
  ],
1231
- projectedTargetPaths,
1232
1307
  };
1233
1308
  };
1234
1309
 
1235
- const filterProjectedTargetExtensions = (
1310
+ const filterDerivedTargetExtensions = (
1236
1311
  collected: DownstreamSourceCollection,
1237
1312
  sourceExtensions: readonly string[],
1238
- projectedTargetPaths: ReadonlySet<string>
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
- projectedTargetPaths.has(normalizeRenamePath(file.path))
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 projectedTargetPaths: ReadonlySet<string>;
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.projectedTargetPaths.has(path) ||
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 extensionProjection = collectionExtensionProjectionForFileRenames({
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: extensionProjection.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
- projectedTargetPaths: extensionProjection.projectedTargetPaths,
1530
+ derivedTargetPaths: derivedExtensions.derivedTargetPaths,
1456
1531
  renames: params.renames,
1457
1532
  resolved: validated.value,
1458
1533
  scope: params.scope,
1459
1534
  });
1460
- const extensionScopedCollection = filterProjectedTargetExtensions(
1535
+ const extensionScopedCollection = filterDerivedTargetExtensions(
1461
1536
  exactTargetCollection,
1462
1537
  sourceExtensions,
1463
- extensionProjection.projectedTargetPaths
1538
+ derivedExtensions.derivedTargetPaths
1464
1539
  );
1465
1540
  const scopedCollection = filterOpenedPolicyDirectories(
1466
1541
  extensionScopedCollection,
1467
1542
  opened,
1468
- extensionProjection.projectedTargetPaths,
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 projectedEvidence = params.renames.map((rename, index) => ({
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
- projectedEvidence.reduce((sum, item) => sum + item.rewritten, 0),
1637
+ derivedEvidence.reduce((sum, item) => sum + item.rewritten, 0),
1552
1638
  filesChanged: changedFiles.size,
1553
1639
  review,
1554
- skipped: projectedEvidence.reduce(
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: projectedEvidence,
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
  };