@ontrails/regrade 1.0.0-beta.30 → 1.0.0-beta.32
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 +45 -0
- package/package.json +5 -5
- package/src/downstream/collect.ts +29 -3
- package/src/downstream/report.ts +291 -35
- package/src/downstream/scan-summary.ts +193 -0
- package/src/downstream/vocabulary.ts +965 -0
- package/src/index.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @ontrails/regrade
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
|
|
8
|
+
- fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
|
|
9
|
+
- Updated dependencies [3e5c0fc]
|
|
10
|
+
- Updated dependencies [f3c4fef]
|
|
11
|
+
- Updated dependencies [cb0a9d8]
|
|
12
|
+
- Updated dependencies [21c6dda]
|
|
13
|
+
- Updated dependencies [fe72b84]
|
|
14
|
+
- @ontrails/core@1.0.0-beta.32
|
|
15
|
+
- @ontrails/warden@1.0.0-beta.32
|
|
16
|
+
|
|
17
|
+
## 1.0.0-beta.31
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- e2f3d23: Default Regrade reports to actionable entries, add skip counts grouped by
|
|
22
|
+
reason, and expose an `includeEntries` option for full report inventories.
|
|
23
|
+
- 9be2b7e: Load project-local Warden term-rewrite rules from the Regrade root so repo-owned
|
|
24
|
+
migration classes can run through `trails regrade`.
|
|
25
|
+
- 47f782c: Add occurrence-level vocabulary regrade reports with plan, ledger,
|
|
26
|
+
and completion-gate facts. The Trails `regrade` operator command now supports
|
|
27
|
+
positional `<from> <to>` regrade runs and exposes the same capability through
|
|
28
|
+
the curated MCP surface.
|
|
29
|
+
- ee9f3ae: Let Warden fix capabilities declare downstream scan targets and have Regrade
|
|
30
|
+
honor those targets for Warden-backed term-rewrite classes.
|
|
31
|
+
|
|
32
|
+
Dogfood the first safe facet-to-trailhead prose rewrite through project-local
|
|
33
|
+
Warden rules and Regrade.
|
|
34
|
+
|
|
35
|
+
- 982a4d7: Add Regrade path-scope exclusion globs for vocabulary runs and expose them
|
|
36
|
+
through the `trails regrade` CLI/MCP contract.
|
|
37
|
+
- 1540233: Add Regrade scan inventory summaries that group matched files by extension and
|
|
38
|
+
top-level path, with occurrence counts for vocabulary regrade reports.
|
|
39
|
+
- a079073: Rename Regrade path-scope scan controls from `ignore` to `exclude` across CLI, MCP, and project config.
|
|
40
|
+
- Updated dependencies [ee9f3ae]
|
|
41
|
+
- Updated dependencies [a0126d9]
|
|
42
|
+
- Updated dependencies [4cd5d4e]
|
|
43
|
+
- Updated dependencies [6a26a08]
|
|
44
|
+
- Updated dependencies [38907cc]
|
|
45
|
+
- @ontrails/warden@1.0.0-beta.31
|
|
46
|
+
- @ontrails/core@1.0.0-beta.31
|
|
47
|
+
|
|
3
48
|
## 1.0.0-beta.30
|
|
4
49
|
|
|
5
50
|
### Patch Changes
|
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.32",
|
|
4
4
|
"description": "Downstream migration reporting and safe rewrite helpers for Trails.",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/**/*.ts",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
26
|
-
"@ontrails/warden": "^1.0.0-beta.
|
|
25
|
+
"@ontrails/core": "^1.0.0-beta.32",
|
|
26
|
+
"@ontrails/warden": "^1.0.0-beta.32",
|
|
27
27
|
"zod": "^4.3.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
31
|
-
"@ontrails/topographer": "^1.0.0-beta.
|
|
30
|
+
"@ontrails/testing": "^1.0.0-beta.32",
|
|
31
|
+
"@ontrails/topographer": "^1.0.0-beta.32"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NotFoundError,
|
|
3
|
+
Result,
|
|
4
|
+
matchesAnyPathGlob,
|
|
5
|
+
trail,
|
|
6
|
+
} from '@ontrails/core';
|
|
2
7
|
import { readdirSync } from 'node:fs';
|
|
3
8
|
import { join, posix, relative, resolve, sep } from 'node:path';
|
|
4
9
|
import { z } from 'zod';
|
|
@@ -66,6 +71,8 @@ export interface DownstreamCollectionOptions {
|
|
|
66
71
|
readonly ignoredDirectories?: readonly string[];
|
|
67
72
|
/** Source extensions to collect. Defaults to {@link DEFAULT_SOURCE_EXTENSIONS}. */
|
|
68
73
|
readonly extensions?: readonly string[];
|
|
74
|
+
/** Root-relative path globs to skip before collection. */
|
|
75
|
+
readonly exclude?: readonly string[];
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/** Outcome of classifying a single directory entry. */
|
|
@@ -79,6 +86,16 @@ const extensionOf = (name: string): string => {
|
|
|
79
86
|
return dot <= 0 ? '' : name.slice(dot);
|
|
80
87
|
};
|
|
81
88
|
|
|
89
|
+
const normalizeExtension = (extension: string): string =>
|
|
90
|
+
extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
|
|
91
|
+
|
|
92
|
+
const collectionExtensions = (
|
|
93
|
+
extensions: readonly string[] | undefined
|
|
94
|
+
): readonly string[] =>
|
|
95
|
+
extensions === undefined
|
|
96
|
+
? DEFAULT_SOURCE_EXTENSIONS
|
|
97
|
+
: extensions.map(normalizeExtension);
|
|
98
|
+
|
|
82
99
|
/**
|
|
83
100
|
* Decide what to do with a single directory entry. Pure: no filesystem access,
|
|
84
101
|
* so collection policy can be tested directly with synthetic entries.
|
|
@@ -90,7 +107,7 @@ export const classifyDownstreamEntry = (
|
|
|
90
107
|
): DownstreamEntryClassification => {
|
|
91
108
|
const ignoredDirectories =
|
|
92
109
|
options.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES;
|
|
93
|
-
const extensions = options.extensions
|
|
110
|
+
const extensions = collectionExtensions(options.extensions);
|
|
94
111
|
|
|
95
112
|
if (kind === 'directory') {
|
|
96
113
|
return ignoredDirectories.includes(name)
|
|
@@ -100,7 +117,7 @@ export const classifyDownstreamEntry = (
|
|
|
100
117
|
if (kind === 'other') {
|
|
101
118
|
return { action: 'skip', reason: 'unsupported-entry' };
|
|
102
119
|
}
|
|
103
|
-
return extensions.includes(extensionOf(name))
|
|
120
|
+
return extensions.length === 0 || extensions.includes(extensionOf(name))
|
|
104
121
|
? { action: 'collect' }
|
|
105
122
|
: { action: 'skip', reason: 'unsupported-extension' };
|
|
106
123
|
};
|
|
@@ -177,6 +194,10 @@ export const collectDownstreamSources = (
|
|
|
177
194
|
for (const entry of read.entries) {
|
|
178
195
|
const absolutePath = join(current, entry.name);
|
|
179
196
|
const path = toPosixRelative(absoluteRoot, absolutePath);
|
|
197
|
+
if (matchesAnyPathGlob(path, options.exclude)) {
|
|
198
|
+
skipped.push({ path, reason: 'ignored-glob' });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
180
201
|
const classification = classifyDownstreamEntry(
|
|
181
202
|
entry.name,
|
|
182
203
|
entry.kind,
|
|
@@ -198,6 +219,10 @@ export const collectDownstreamSources = (
|
|
|
198
219
|
};
|
|
199
220
|
|
|
200
221
|
export const collectDownstreamSourcesInput = z.object({
|
|
222
|
+
exclude: z
|
|
223
|
+
.array(z.string())
|
|
224
|
+
.optional()
|
|
225
|
+
.describe('Root-relative path globs to skip before collection'),
|
|
201
226
|
extensions: z
|
|
202
227
|
.array(z.string())
|
|
203
228
|
.optional()
|
|
@@ -247,6 +272,7 @@ export const collectDownstreamSourcesTrail = trail(
|
|
|
247
272
|
...(input.extensions === undefined
|
|
248
273
|
? {}
|
|
249
274
|
: { extensions: input.extensions }),
|
|
275
|
+
...(input.exclude === undefined ? {} : { exclude: input.exclude }),
|
|
250
276
|
...(input.ignoredDirectories === undefined
|
|
251
277
|
? {}
|
|
252
278
|
: { ignoredDirectories: input.ignoredDirectories }),
|
package/src/downstream/report.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
InternalError,
|
|
3
|
+
Result,
|
|
4
|
+
escapeRegExp,
|
|
5
|
+
includedByPathScope,
|
|
6
|
+
} from '@ontrails/core';
|
|
7
|
+
import type { ScanTargets } from '@ontrails/core';
|
|
2
8
|
import type {
|
|
3
9
|
WardenDiagnostic,
|
|
4
10
|
WardenFixEdit,
|
|
@@ -7,6 +13,7 @@ import type {
|
|
|
7
13
|
import {
|
|
8
14
|
getWardenRuleMetadata,
|
|
9
15
|
isWardenSourceScanTarget,
|
|
16
|
+
loadProjectWardenRules,
|
|
10
17
|
wardenRules,
|
|
11
18
|
} from '@ontrails/warden';
|
|
12
19
|
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
@@ -18,6 +25,13 @@ import {
|
|
|
18
25
|
collectDownstreamSources,
|
|
19
26
|
} from './collect.js';
|
|
20
27
|
import type { DownstreamCollectionOptions, SkippedSource } from './collect.js';
|
|
28
|
+
import {
|
|
29
|
+
buildRegradeScanSummary,
|
|
30
|
+
regradeScanSummaryOutput,
|
|
31
|
+
} from './scan-summary.js';
|
|
32
|
+
import type { RegradeScanSummary } from './scan-summary.js';
|
|
33
|
+
import type { VocabularyRegradeRun } from './vocabulary.js';
|
|
34
|
+
import { vocabularyRegradeRunOutput } from './vocabulary.js';
|
|
21
35
|
|
|
22
36
|
/**
|
|
23
37
|
* Regrade-class selection and coverage reporting (TRL-845).
|
|
@@ -67,12 +81,14 @@ export interface RegradeClassContext {
|
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
/** Files a regrade class knows how to inspect. */
|
|
70
|
-
export
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
export type RegradeScanTargets = ScanTargets & {
|
|
85
|
+
/**
|
|
86
|
+
* @deprecated Use collection-level `exclude` globs. Preserved so existing
|
|
87
|
+
* Regrade classes can explicitly opt into directories the default collector
|
|
88
|
+
* prunes, such as `dist`, while migrating to PathScope.
|
|
89
|
+
*/
|
|
74
90
|
readonly ignoredDirectories?: readonly string[];
|
|
75
|
-
}
|
|
91
|
+
};
|
|
76
92
|
|
|
77
93
|
/** One named, contract-aware transform. */
|
|
78
94
|
export interface RegradeClass {
|
|
@@ -89,12 +105,22 @@ export interface RegradeClass {
|
|
|
89
105
|
readonly scanTargets?: RegradeScanTargets;
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
export interface RegradeWardenClassSet {
|
|
109
|
+
/** Built-in and project-local Warden term-rewrite classes. */
|
|
110
|
+
readonly classes: readonly RegradeClass[];
|
|
111
|
+
/** Diagnostics raised while loading project-local rules. */
|
|
112
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
/** Which regrade classes a run should execute. */
|
|
93
116
|
export interface RegradeSelection {
|
|
94
117
|
/** Class ids to run. Omit to run every provided class. */
|
|
95
118
|
readonly classIds?: readonly string[];
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/** Which report entries should be returned. Counts always cover the full run. */
|
|
122
|
+
export type RegradeReportEntrySelection = 'actionable' | 'all';
|
|
123
|
+
|
|
98
124
|
/** Optional write summary for an apply-mode regrade run. */
|
|
99
125
|
export interface RegradeApplySummary {
|
|
100
126
|
/** Safe rewrite outcomes written to disk. */
|
|
@@ -137,9 +163,6 @@ export interface RegradeReviewDetail {
|
|
|
137
163
|
readonly symbol?: string;
|
|
138
164
|
}
|
|
139
165
|
|
|
140
|
-
const escapeRegExp = (value: string): string =>
|
|
141
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
-
|
|
143
166
|
/**
|
|
144
167
|
* Build a whole-word term-rewrite class.
|
|
145
168
|
*
|
|
@@ -198,6 +221,27 @@ type WardenEditApplication =
|
|
|
198
221
|
| { readonly ok: true; readonly nextSource: string }
|
|
199
222
|
| { readonly ok: false; readonly reason: string };
|
|
200
223
|
|
|
224
|
+
const regradeScanTargetsFromWardenFix = (
|
|
225
|
+
scanTargets: NonNullable<
|
|
226
|
+
NonNullable<ReturnType<typeof getWardenRuleMetadata>>['fix']
|
|
227
|
+
>['scanTargets']
|
|
228
|
+
): RegradeScanTargets | undefined => {
|
|
229
|
+
if (scanTargets === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
...(scanTargets.exclude === undefined
|
|
234
|
+
? {}
|
|
235
|
+
: { exclude: scanTargets.exclude }),
|
|
236
|
+
...(scanTargets.extensions === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { extensions: scanTargets.extensions }),
|
|
239
|
+
...(scanTargets.ignoredDirectories === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { ignoredDirectories: scanTargets.ignoredDirectories }),
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
201
245
|
const diagnosticNote = (diagnostic: WardenDiagnostic): string => {
|
|
202
246
|
const reason = diagnostic.fix?.reason ?? diagnostic.message;
|
|
203
247
|
return `${diagnostic.rule}:${diagnostic.line}: ${reason}`;
|
|
@@ -351,10 +395,11 @@ const applyWardenEdits = (
|
|
|
351
395
|
export const createWardenTermRewriteClass = (
|
|
352
396
|
rule: WardenRule
|
|
353
397
|
): RegradeClass | null => {
|
|
354
|
-
const metadata = getWardenRuleMetadata(rule
|
|
398
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
355
399
|
if (metadata?.fix?.class !== TERM_REWRITE_FIX_CLASS) {
|
|
356
400
|
return null;
|
|
357
401
|
}
|
|
402
|
+
const scanTargets = regradeScanTargetsFromWardenFix(metadata.fix.scanTargets);
|
|
358
403
|
|
|
359
404
|
return {
|
|
360
405
|
apply: (
|
|
@@ -445,6 +490,7 @@ export const createWardenTermRewriteClass = (
|
|
|
445
490
|
},
|
|
446
491
|
describe: `${rule.description} (${metadata.fix.safety} ${metadata.fix.class})`,
|
|
447
492
|
id: `${metadata.fix.class}:${rule.name}`,
|
|
493
|
+
...(scanTargets === undefined ? {} : { scanTargets }),
|
|
448
494
|
};
|
|
449
495
|
};
|
|
450
496
|
|
|
@@ -479,29 +525,53 @@ export const selectRegradeClasses = (
|
|
|
479
525
|
const uniqueSorted = (values: readonly string[]): readonly string[] =>
|
|
480
526
|
[...new Set(values)].toSorted((a, b) => a.localeCompare(b));
|
|
481
527
|
|
|
528
|
+
const intersectValues = (
|
|
529
|
+
left: readonly string[],
|
|
530
|
+
right: readonly string[]
|
|
531
|
+
): readonly string[] => left.filter((value) => right.includes(value));
|
|
532
|
+
|
|
533
|
+
const deriveClassIgnoredDirectories = (
|
|
534
|
+
classes: readonly RegradeClass[]
|
|
535
|
+
): readonly string[] | undefined => {
|
|
536
|
+
const explicitTargets = classes
|
|
537
|
+
.map((cls) => cls.scanTargets?.ignoredDirectories)
|
|
538
|
+
.filter((value): value is readonly string[] => value !== undefined);
|
|
539
|
+
if (explicitTargets.length === 0) {
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let common = explicitTargets[0] ?? [];
|
|
544
|
+
for (const target of explicitTargets.slice(1)) {
|
|
545
|
+
common = intersectValues(common, target);
|
|
546
|
+
}
|
|
547
|
+
return uniqueSorted(common);
|
|
548
|
+
};
|
|
549
|
+
|
|
482
550
|
const deriveCollectionOptions = (
|
|
483
551
|
classes: readonly RegradeClass[],
|
|
484
552
|
collection: DownstreamCollectionOptions | undefined
|
|
485
553
|
): DownstreamCollectionOptions => {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
? DEFAULT_SOURCE_EXTENSIONS
|
|
489
|
-
: classes.flatMap(
|
|
490
|
-
(cls) => cls.scanTargets?.extensions ?? DEFAULT_SOURCE_EXTENSIONS
|
|
491
|
-
)
|
|
554
|
+
const allExtensions = classes.some(
|
|
555
|
+
(cls) => cls.scanTargets?.extensions?.length === 0
|
|
492
556
|
);
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
557
|
+
const targetExtensions = allExtensions
|
|
558
|
+
? []
|
|
559
|
+
: uniqueSorted(
|
|
560
|
+
classes.length === 0
|
|
561
|
+
? DEFAULT_SOURCE_EXTENSIONS
|
|
562
|
+
: classes.flatMap(
|
|
563
|
+
(cls) => cls.scanTargets?.extensions ?? DEFAULT_SOURCE_EXTENSIONS
|
|
564
|
+
)
|
|
565
|
+
);
|
|
502
566
|
return {
|
|
567
|
+
...(collection?.exclude === undefined
|
|
568
|
+
? {}
|
|
569
|
+
: { exclude: collection.exclude }),
|
|
503
570
|
extensions: collection?.extensions ?? targetExtensions,
|
|
504
|
-
ignoredDirectories:
|
|
571
|
+
ignoredDirectories:
|
|
572
|
+
collection?.ignoredDirectories ??
|
|
573
|
+
deriveClassIgnoredDirectories(classes) ??
|
|
574
|
+
DEFAULT_IGNORED_DIRECTORIES,
|
|
505
575
|
};
|
|
506
576
|
};
|
|
507
577
|
|
|
@@ -539,10 +609,16 @@ export interface RegradeReport {
|
|
|
539
609
|
readonly review: number;
|
|
540
610
|
/** Entries skipped (collection skips plus any run-level skips). */
|
|
541
611
|
readonly skipped: number;
|
|
612
|
+
/** Skipped entries grouped by reason. */
|
|
613
|
+
readonly skipsByReason: Readonly<Record<string, number>>;
|
|
614
|
+
/** Agent-facing inventory summary for the scan. */
|
|
615
|
+
readonly scan: RegradeScanSummary;
|
|
542
616
|
/** Per-entry detail, sorted by path. */
|
|
543
617
|
readonly entries: readonly RegradeReportEntry[];
|
|
544
618
|
/** Apply-mode summary; absent for dry-run report-only calls. */
|
|
545
619
|
readonly apply?: RegradeApplySummary;
|
|
620
|
+
/** Vocabulary regrade run: plan, ledger, and completion report. */
|
|
621
|
+
readonly run?: VocabularyRegradeRun;
|
|
546
622
|
}
|
|
547
623
|
|
|
548
624
|
interface RegradeRewriteCandidate {
|
|
@@ -557,20 +633,76 @@ interface RegradeClassifiedFile {
|
|
|
557
633
|
readonly rewrite?: RegradeRewriteCandidate;
|
|
558
634
|
}
|
|
559
635
|
|
|
636
|
+
const isIgnoredByClassDirectories = (
|
|
637
|
+
path: string,
|
|
638
|
+
ignoredDirectories: readonly string[] | undefined
|
|
639
|
+
): boolean => {
|
|
640
|
+
if (ignoredDirectories === undefined || ignoredDirectories.length === 0) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
return path
|
|
644
|
+
.split('/')
|
|
645
|
+
.slice(0, -1)
|
|
646
|
+
.some((segment) => ignoredDirectories.includes(segment));
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const classScanTargetSkip = (
|
|
650
|
+
cls: RegradeClass,
|
|
651
|
+
path: string,
|
|
652
|
+
collection: DownstreamCollectionOptions | undefined
|
|
653
|
+
): RegradeClassResult | undefined => {
|
|
654
|
+
const ignoredDirectories =
|
|
655
|
+
collection?.ignoredDirectories ??
|
|
656
|
+
cls.scanTargets?.ignoredDirectories ??
|
|
657
|
+
DEFAULT_IGNORED_DIRECTORIES;
|
|
658
|
+
if (isIgnoredByClassDirectories(path, ignoredDirectories)) {
|
|
659
|
+
return {
|
|
660
|
+
kind: 'skipped',
|
|
661
|
+
notes: [`Skipped by ${cls.id} scan-target filtering.`],
|
|
662
|
+
reason: 'regrade-scan-target-filtered',
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const effectiveScanTargets: ScanTargets | undefined =
|
|
666
|
+
cls.scanTargets?.extensions === undefined &&
|
|
667
|
+
collection?.extensions === undefined
|
|
668
|
+
? {
|
|
669
|
+
...cls.scanTargets,
|
|
670
|
+
extensions: DEFAULT_SOURCE_EXTENSIONS,
|
|
671
|
+
}
|
|
672
|
+
: cls.scanTargets;
|
|
673
|
+
if (
|
|
674
|
+
effectiveScanTargets === undefined ||
|
|
675
|
+
includedByPathScope(path, effectiveScanTargets)
|
|
676
|
+
) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
kind: 'skipped',
|
|
681
|
+
notes: [`Skipped by ${cls.id} scan-target filtering.`],
|
|
682
|
+
reason: 'regrade-scan-target-filtered',
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
|
|
560
686
|
const classifyFile = (
|
|
561
687
|
path: string,
|
|
562
688
|
source: string,
|
|
563
689
|
context: RegradeClassContext,
|
|
564
|
-
selected: readonly RegradeClass[]
|
|
690
|
+
selected: readonly RegradeClass[],
|
|
691
|
+
collection?: DownstreamCollectionOptions
|
|
565
692
|
): RegradeClassifiedFile => {
|
|
566
693
|
// First selected class that matches (rewrite or review) wins, mirroring the
|
|
567
|
-
// "run one class" emphasis.
|
|
568
|
-
//
|
|
694
|
+
// "run one class" emphasis. Scan-target skips only own the file when no
|
|
695
|
+
// selected class inspects it; a later no-op still counts as a clean scan.
|
|
569
696
|
let skipped:
|
|
570
697
|
| { readonly classId: string; readonly result: RegradeClassResult }
|
|
571
698
|
| undefined;
|
|
699
|
+
let inspected = false;
|
|
572
700
|
for (const cls of selected) {
|
|
573
|
-
const result =
|
|
701
|
+
const result =
|
|
702
|
+
classScanTargetSkip(cls, path, collection) ?? cls.apply(source, context);
|
|
703
|
+
if (result.kind !== 'skipped') {
|
|
704
|
+
inspected = true;
|
|
705
|
+
}
|
|
574
706
|
if (result.kind === 'rewrite') {
|
|
575
707
|
if (typeof result.nextSource !== 'string') {
|
|
576
708
|
return {
|
|
@@ -623,7 +755,7 @@ const classifyFile = (
|
|
|
623
755
|
skipped = { classId: cls.id, result };
|
|
624
756
|
}
|
|
625
757
|
}
|
|
626
|
-
if (skipped !== undefined) {
|
|
758
|
+
if (!inspected && skipped !== undefined) {
|
|
627
759
|
return {
|
|
628
760
|
entry: {
|
|
629
761
|
classId: skipped.classId,
|
|
@@ -642,6 +774,32 @@ interface RegradeEvaluation {
|
|
|
642
774
|
readonly rewrites: readonly RegradeRewriteCandidate[];
|
|
643
775
|
}
|
|
644
776
|
|
|
777
|
+
const includeEntryInReport = (
|
|
778
|
+
entry: RegradeReportEntry,
|
|
779
|
+
selection: RegradeReportEntrySelection
|
|
780
|
+
): boolean =>
|
|
781
|
+
selection === 'all' ||
|
|
782
|
+
entry.outcome === 'rewrite' ||
|
|
783
|
+
entry.outcome === 'needs-review';
|
|
784
|
+
|
|
785
|
+
const skipsByReason = (
|
|
786
|
+
entries: readonly RegradeReportEntry[]
|
|
787
|
+
): Readonly<Record<string, number>> => {
|
|
788
|
+
const counts = new Map<string, number>();
|
|
789
|
+
for (const entry of entries) {
|
|
790
|
+
if (entry.outcome !== 'skip') {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const reason = entry.reason ?? 'skipped';
|
|
794
|
+
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
795
|
+
}
|
|
796
|
+
return Object.fromEntries(
|
|
797
|
+
[...counts.entries()].toSorted(([left], [right]) =>
|
|
798
|
+
left.localeCompare(right)
|
|
799
|
+
)
|
|
800
|
+
);
|
|
801
|
+
};
|
|
802
|
+
|
|
645
803
|
/**
|
|
646
804
|
* Build a coverage report from already-read source files. Pure: no filesystem
|
|
647
805
|
* access, so coverage semantics are testable directly.
|
|
@@ -656,7 +814,10 @@ const buildRegradeEvaluation = (params: {
|
|
|
656
814
|
readonly skipped: readonly SkippedSource[];
|
|
657
815
|
readonly classes: readonly RegradeClass[];
|
|
658
816
|
readonly selection?: RegradeSelection;
|
|
817
|
+
readonly collection?: DownstreamCollectionOptions;
|
|
818
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
659
819
|
}): RegradeEvaluation => {
|
|
820
|
+
const entrySelection = params.includeEntries ?? 'actionable';
|
|
660
821
|
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
661
822
|
params.classes,
|
|
662
823
|
params.selection
|
|
@@ -672,7 +833,8 @@ const buildRegradeEvaluation = (params: {
|
|
|
672
833
|
: { absolutePath: file.absolutePath }),
|
|
673
834
|
path: file.path,
|
|
674
835
|
},
|
|
675
|
-
selected
|
|
836
|
+
selected,
|
|
837
|
+
params.collection
|
|
676
838
|
)
|
|
677
839
|
);
|
|
678
840
|
const fileEntries = classifiedFiles.map((file) => file.entry);
|
|
@@ -685,9 +847,12 @@ const buildRegradeEvaluation = (params: {
|
|
|
685
847
|
reason: entry.reason,
|
|
686
848
|
}));
|
|
687
849
|
|
|
688
|
-
const
|
|
850
|
+
const allEntries = [...fileEntries, ...skipEntries].toSorted((a, b) =>
|
|
689
851
|
a.path.localeCompare(b.path)
|
|
690
852
|
);
|
|
853
|
+
const entries = allEntries.filter((entry) =>
|
|
854
|
+
includeEntryInReport(entry, entrySelection)
|
|
855
|
+
);
|
|
691
856
|
|
|
692
857
|
// Class-level skips (e.g. scan-target filtering) are accounted as skipped, not
|
|
693
858
|
// as scanned/clean files.
|
|
@@ -699,6 +864,11 @@ const buildRegradeEvaluation = (params: {
|
|
|
699
864
|
const review = scannedEntries.filter(
|
|
700
865
|
(e) => e.outcome === 'needs-review'
|
|
701
866
|
).length;
|
|
867
|
+
const matchedPaths = scannedEntries
|
|
868
|
+
.filter((e) => e.outcome === 'rewrite' || e.outcome === 'needs-review')
|
|
869
|
+
.map((entry) => entry.path);
|
|
870
|
+
const skipped = skipEntries.length + fileSkipCount;
|
|
871
|
+
const skippedReasons = skipsByReason(allEntries);
|
|
702
872
|
|
|
703
873
|
return {
|
|
704
874
|
report: {
|
|
@@ -707,9 +877,16 @@ const buildRegradeEvaluation = (params: {
|
|
|
707
877
|
review,
|
|
708
878
|
rewritten,
|
|
709
879
|
root: params.root,
|
|
880
|
+
scan: buildRegradeScanSummary({
|
|
881
|
+
matchedPaths,
|
|
882
|
+
scanned: scannedEntries.length,
|
|
883
|
+
skipped,
|
|
884
|
+
skippedByReason: skippedReasons,
|
|
885
|
+
}),
|
|
710
886
|
scanned: scannedEntries.length,
|
|
711
887
|
selectedClassIds: selected.map((cls) => cls.id),
|
|
712
|
-
skipped
|
|
888
|
+
skipped,
|
|
889
|
+
skipsByReason: skippedReasons,
|
|
713
890
|
unknownClassIds,
|
|
714
891
|
},
|
|
715
892
|
rewrites,
|
|
@@ -726,6 +903,8 @@ export const buildRegradeReport = (params: {
|
|
|
726
903
|
readonly skipped: readonly SkippedSource[];
|
|
727
904
|
readonly classes: readonly RegradeClass[];
|
|
728
905
|
readonly selection?: RegradeSelection;
|
|
906
|
+
readonly collection?: DownstreamCollectionOptions;
|
|
907
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
729
908
|
}): RegradeReport => buildRegradeEvaluation(params).report;
|
|
730
909
|
|
|
731
910
|
const applyRegradeEvaluation = (
|
|
@@ -797,6 +976,7 @@ const runRegradeEvaluation = (params: {
|
|
|
797
976
|
readonly classes: readonly RegradeClass[];
|
|
798
977
|
readonly selection?: RegradeSelection;
|
|
799
978
|
readonly collection?: DownstreamCollectionOptions;
|
|
979
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
800
980
|
}): RegradeEvaluation | null => {
|
|
801
981
|
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
802
982
|
params.classes,
|
|
@@ -808,9 +988,15 @@ const runRegradeEvaluation = (params: {
|
|
|
808
988
|
}
|
|
809
989
|
return buildRegradeEvaluation({
|
|
810
990
|
classes: params.classes,
|
|
991
|
+
...(params.collection === undefined
|
|
992
|
+
? {}
|
|
993
|
+
: { collection: params.collection }),
|
|
811
994
|
files: [],
|
|
812
995
|
root: params.root,
|
|
813
996
|
skipped: [],
|
|
997
|
+
...(params.includeEntries === undefined
|
|
998
|
+
? {}
|
|
999
|
+
: { includeEntries: params.includeEntries }),
|
|
814
1000
|
...(params.selection === undefined
|
|
815
1001
|
? {}
|
|
816
1002
|
: { selection: params.selection }),
|
|
@@ -841,9 +1027,15 @@ const runRegradeEvaluation = (params: {
|
|
|
841
1027
|
|
|
842
1028
|
return buildRegradeEvaluation({
|
|
843
1029
|
classes: params.classes,
|
|
1030
|
+
...(params.collection === undefined
|
|
1031
|
+
? {}
|
|
1032
|
+
: { collection: params.collection }),
|
|
844
1033
|
files,
|
|
845
1034
|
root: params.root,
|
|
846
1035
|
skipped,
|
|
1036
|
+
...(params.includeEntries === undefined
|
|
1037
|
+
? {}
|
|
1038
|
+
: { includeEntries: params.includeEntries }),
|
|
847
1039
|
...(params.selection === undefined ? {} : { selection: params.selection }),
|
|
848
1040
|
});
|
|
849
1041
|
};
|
|
@@ -861,6 +1053,7 @@ export const runRegrade = (params: {
|
|
|
861
1053
|
readonly selection?: RegradeSelection;
|
|
862
1054
|
readonly collection?: DownstreamCollectionOptions;
|
|
863
1055
|
readonly apply?: boolean;
|
|
1056
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
864
1057
|
}): Result<RegradeReport | null, InternalError> => {
|
|
865
1058
|
const evaluation = runRegradeEvaluation(params);
|
|
866
1059
|
if (evaluation === null) {
|
|
@@ -941,14 +1134,25 @@ export const regradeReportOutput = z.object({
|
|
|
941
1134
|
.describe('Apply-mode summary; absent for dry-run report-only calls'),
|
|
942
1135
|
entries: z
|
|
943
1136
|
.array(regradeReportEntrySchema)
|
|
944
|
-
.describe(
|
|
1137
|
+
.describe(
|
|
1138
|
+
'Per-entry detail, sorted by path. Defaults to actionable rewrite/review entries.'
|
|
1139
|
+
),
|
|
945
1140
|
matched: z.number().describe('Files with a rewrite or review outcome'),
|
|
946
1141
|
review: z.number().describe('Files routed to review'),
|
|
947
1142
|
rewritten: z.number().describe('Files with a rewrite outcome'),
|
|
948
1143
|
root: z.string().describe('Root the run scanned'),
|
|
1144
|
+
run: vocabularyRegradeRunOutput
|
|
1145
|
+
.optional()
|
|
1146
|
+
.describe('Vocabulary regrade run: plan, ledger, and completion report'),
|
|
1147
|
+
scan: regradeScanSummaryOutput.describe(
|
|
1148
|
+
'Agent-facing inventory summary for the scan'
|
|
1149
|
+
),
|
|
949
1150
|
scanned: z.number().describe('Source files inspected'),
|
|
950
1151
|
selectedClassIds: z.array(z.string()).describe('Class ids executed'),
|
|
951
1152
|
skipped: z.number().describe('Entries skipped'),
|
|
1153
|
+
skipsByReason: z
|
|
1154
|
+
.record(z.string(), z.number())
|
|
1155
|
+
.describe('Skipped entries grouped by reason'),
|
|
952
1156
|
unknownClassIds: z
|
|
953
1157
|
.array(z.string())
|
|
954
1158
|
.describe('Selected ids that did not resolve to a class'),
|
|
@@ -967,3 +1171,55 @@ export const wardenTermRewriteClasses: readonly RegradeClass[] = Object.freeze(
|
|
|
967
1171
|
return cls === null ? [] : [cls];
|
|
968
1172
|
})
|
|
969
1173
|
);
|
|
1174
|
+
|
|
1175
|
+
const duplicateClassDiagnostics = (
|
|
1176
|
+
root: string,
|
|
1177
|
+
classes: readonly RegradeClass[]
|
|
1178
|
+
): readonly WardenDiagnostic[] => {
|
|
1179
|
+
const seen = new Set<string>();
|
|
1180
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
1181
|
+
for (const cls of classes) {
|
|
1182
|
+
if (!seen.has(cls.id)) {
|
|
1183
|
+
seen.add(cls.id);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
diagnostics.push({
|
|
1187
|
+
filePath: root,
|
|
1188
|
+
line: 1,
|
|
1189
|
+
message: `Duplicate Regrade class id "${cls.id}" from Warden term-rewrite rules.`,
|
|
1190
|
+
rule: 'regrade-warden-term-rewrite-classes',
|
|
1191
|
+
severity: 'error',
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
return diagnostics;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Load built-in and project-local Warden term-rewrite rules as Regrade classes.
|
|
1199
|
+
*
|
|
1200
|
+
* Built-ins are always available. When `root` is provided, committed
|
|
1201
|
+
* project-local Warden rules under `.trails/rules.ts` or direct
|
|
1202
|
+
* `.trails/rules/*.ts` modules are loaded and any term-rewrite-capable source
|
|
1203
|
+
* rules join the class set.
|
|
1204
|
+
*/
|
|
1205
|
+
export const loadWardenTermRewriteClasses = async (
|
|
1206
|
+
root?: string
|
|
1207
|
+
): Promise<RegradeWardenClassSet> => {
|
|
1208
|
+
if (root === undefined) {
|
|
1209
|
+
return { classes: wardenTermRewriteClasses, diagnostics: [] };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const projectRules = await loadProjectWardenRules(root);
|
|
1213
|
+
const projectClasses = projectRules.sourceRules.flatMap((rule) => {
|
|
1214
|
+
const cls = createWardenTermRewriteClass(rule);
|
|
1215
|
+
return cls === null ? [] : [cls];
|
|
1216
|
+
});
|
|
1217
|
+
const classes = [...wardenTermRewriteClasses, ...projectClasses];
|
|
1218
|
+
return {
|
|
1219
|
+
classes,
|
|
1220
|
+
diagnostics: [
|
|
1221
|
+
...projectRules.diagnostics,
|
|
1222
|
+
...duplicateClassDiagnostics(root, classes),
|
|
1223
|
+
],
|
|
1224
|
+
};
|
|
1225
|
+
};
|