@ontrails/regrade 1.0.0-beta.29 → 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 +52 -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/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
|
+
};
|