@ontrails/regrade 1.0.0-beta.30 → 1.0.0-beta.39
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 +99 -0
- package/package.json +5 -5
- package/src/downstream/ast-rewrite.ts +200 -5
- package/src/downstream/collect.ts +45 -4
- package/src/downstream/export-restructure.ts +1588 -0
- package/src/downstream/report.ts +557 -79
- package/src/downstream/scan-summary.ts +193 -0
- package/src/downstream/vocabulary-registry.ts +98 -0
- package/src/downstream/vocabulary.ts +1933 -0
- package/src/index.ts +51 -0
package/src/downstream/report.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
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,
|
|
11
|
+
WardenGuidance,
|
|
5
12
|
WardenRule,
|
|
6
13
|
} from '@ontrails/warden';
|
|
7
14
|
import {
|
|
8
15
|
getWardenRuleMetadata,
|
|
9
16
|
isWardenSourceScanTarget,
|
|
17
|
+
loadProjectWardenRules,
|
|
10
18
|
wardenRules,
|
|
11
19
|
} from '@ontrails/warden';
|
|
12
20
|
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
@@ -18,6 +26,13 @@ import {
|
|
|
18
26
|
collectDownstreamSources,
|
|
19
27
|
} from './collect.js';
|
|
20
28
|
import type { DownstreamCollectionOptions, SkippedSource } from './collect.js';
|
|
29
|
+
import {
|
|
30
|
+
buildRegradeScanSummary,
|
|
31
|
+
regradeScanSummaryOutput,
|
|
32
|
+
} from './scan-summary.js';
|
|
33
|
+
import type { RegradeScanSummary } from './scan-summary.js';
|
|
34
|
+
import type { VocabularyRegradeRun } from './vocabulary.js';
|
|
35
|
+
import { vocabularyRegradeRunOutput } from './vocabulary.js';
|
|
21
36
|
|
|
22
37
|
/**
|
|
23
38
|
* Regrade-class selection and coverage reporting (TRL-845).
|
|
@@ -67,12 +82,14 @@ export interface RegradeClassContext {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
/** Files a regrade class knows how to inspect. */
|
|
70
|
-
export
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
export type RegradeScanTargets = ScanTargets & {
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Use collection-level `exclude` globs. Preserved so existing
|
|
88
|
+
* Regrade classes can explicitly opt into directories the default collector
|
|
89
|
+
* prunes, such as `dist`, while migrating to PathScope.
|
|
90
|
+
*/
|
|
74
91
|
readonly ignoredDirectories?: readonly string[];
|
|
75
|
-
}
|
|
92
|
+
};
|
|
76
93
|
|
|
77
94
|
/** One named, contract-aware transform. */
|
|
78
95
|
export interface RegradeClass {
|
|
@@ -89,12 +106,22 @@ export interface RegradeClass {
|
|
|
89
106
|
readonly scanTargets?: RegradeScanTargets;
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
export interface RegradeWardenClassSet {
|
|
110
|
+
/** Built-in and project-local Warden term-rewrite classes. */
|
|
111
|
+
readonly classes: readonly RegradeClass[];
|
|
112
|
+
/** Diagnostics raised while loading project-local rules. */
|
|
113
|
+
readonly diagnostics: readonly WardenDiagnostic[];
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
/** Which regrade classes a run should execute. */
|
|
93
117
|
export interface RegradeSelection {
|
|
94
118
|
/** Class ids to run. Omit to run every provided class. */
|
|
95
119
|
readonly classIds?: readonly string[];
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
/** Which report entries should be returned. Counts always cover the full run. */
|
|
123
|
+
export type RegradeReportEntrySelection = 'actionable' | 'all';
|
|
124
|
+
|
|
98
125
|
/** Optional write summary for an apply-mode regrade run. */
|
|
99
126
|
export interface RegradeApplySummary {
|
|
100
127
|
/** Safe rewrite outcomes written to disk. */
|
|
@@ -117,18 +144,39 @@ export interface RegradeReviewSpan {
|
|
|
117
144
|
readonly start: number;
|
|
118
145
|
}
|
|
119
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Verdict state for a review detail.
|
|
149
|
+
*
|
|
150
|
+
* - `unresolved`: the class could not complete occurrence judgment; a human or
|
|
151
|
+
* agent decision is still needed.
|
|
152
|
+
* - `preserve`: a completed verdict to keep the occurrence as-is.
|
|
153
|
+
* - `rewrite`: a completed verdict that a rewrite is intended but this run
|
|
154
|
+
* could not apply it (for example invalid or missing edits).
|
|
155
|
+
*/
|
|
156
|
+
export type RegradeReviewJudgment = 'preserve' | 'rewrite' | 'unresolved';
|
|
157
|
+
|
|
120
158
|
/** Structured detail explaining why a source match needs review. */
|
|
121
159
|
export interface RegradeReviewDetail {
|
|
160
|
+
/** Concrete replacement the class would apply if the occurrence were judged safe. */
|
|
161
|
+
readonly candidateReplacement?: string;
|
|
122
162
|
/** Class that produced the review detail, injected by report building. */
|
|
123
163
|
readonly classId?: string;
|
|
124
164
|
/** Expected target shape when the class can describe one. */
|
|
125
165
|
readonly expectedTarget?: string;
|
|
126
166
|
/** Fixture or example reference that illustrates the expected migration. */
|
|
127
167
|
readonly fixture?: string;
|
|
168
|
+
/** Whether occurrence judgment is unresolved or a preserve/rewrite verdict completed. */
|
|
169
|
+
readonly judgment?: RegradeReviewJudgment;
|
|
170
|
+
/** Exact matched source text for the occurrence under review. */
|
|
171
|
+
readonly matchedForm?: string;
|
|
128
172
|
/** AST node kind or source construct kind. */
|
|
129
173
|
readonly nodeKind?: string;
|
|
174
|
+
/** Cautions explaining why a blind rewrite of this occurrence is unsafe. */
|
|
175
|
+
readonly preserveCautions?: readonly string[];
|
|
130
176
|
/** Machine-readable reason for review. */
|
|
131
177
|
readonly reason: string;
|
|
178
|
+
/** Machine-readable provenance tags for the producing rule or class. */
|
|
179
|
+
readonly signals?: readonly string[];
|
|
132
180
|
/** Source span and line/column for the review-required match. */
|
|
133
181
|
readonly span?: RegradeReviewSpan;
|
|
134
182
|
/** Suggested validation command after the review is resolved. */
|
|
@@ -137,9 +185,6 @@ export interface RegradeReviewDetail {
|
|
|
137
185
|
readonly symbol?: string;
|
|
138
186
|
}
|
|
139
187
|
|
|
140
|
-
const escapeRegExp = (value: string): string =>
|
|
141
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
-
|
|
143
188
|
/**
|
|
144
189
|
* Build a whole-word term-rewrite class.
|
|
145
190
|
*
|
|
@@ -198,6 +243,27 @@ type WardenEditApplication =
|
|
|
198
243
|
| { readonly ok: true; readonly nextSource: string }
|
|
199
244
|
| { readonly ok: false; readonly reason: string };
|
|
200
245
|
|
|
246
|
+
const regradeScanTargetsFromWardenFix = (
|
|
247
|
+
scanTargets: NonNullable<
|
|
248
|
+
NonNullable<ReturnType<typeof getWardenRuleMetadata>>['fix']
|
|
249
|
+
>['scanTargets']
|
|
250
|
+
): RegradeScanTargets | undefined => {
|
|
251
|
+
if (scanTargets === undefined) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
...(scanTargets.exclude === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { exclude: scanTargets.exclude }),
|
|
258
|
+
...(scanTargets.extensions === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { extensions: scanTargets.extensions }),
|
|
261
|
+
...(scanTargets.ignoredDirectories === undefined
|
|
262
|
+
? {}
|
|
263
|
+
: { ignoredDirectories: scanTargets.ignoredDirectories }),
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
201
267
|
const diagnosticNote = (diagnostic: WardenDiagnostic): string => {
|
|
202
268
|
const reason = diagnostic.fix?.reason ?? diagnostic.message;
|
|
203
269
|
return `${diagnostic.rule}:${diagnostic.line}: ${reason}`;
|
|
@@ -273,7 +339,9 @@ const diagnosticSpan = (
|
|
|
273
339
|
return spanForSymbolOnDiagnosticLine(source, diagnostic.line, symbol);
|
|
274
340
|
};
|
|
275
341
|
|
|
276
|
-
const
|
|
342
|
+
const diagnosticCandidateReplacement = (
|
|
343
|
+
diagnostic: WardenDiagnostic
|
|
344
|
+
): string | undefined => {
|
|
277
345
|
const replacements = new Set(
|
|
278
346
|
(diagnostic.fix?.edits ?? []).map((edit) => edit.replacement)
|
|
279
347
|
);
|
|
@@ -281,32 +349,98 @@ const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
|
281
349
|
return undefined;
|
|
282
350
|
}
|
|
283
351
|
const [replacement] = replacements;
|
|
352
|
+
return replacement;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
356
|
+
const replacement = diagnosticCandidateReplacement(diagnostic);
|
|
284
357
|
return replacement === undefined
|
|
285
358
|
? undefined
|
|
286
359
|
: `Replace with "${replacement}".`;
|
|
287
360
|
};
|
|
288
361
|
|
|
362
|
+
const isValidEditSpan = (
|
|
363
|
+
source: string,
|
|
364
|
+
edit: WardenFixEdit | undefined
|
|
365
|
+
): edit is WardenFixEdit =>
|
|
366
|
+
edit !== undefined &&
|
|
367
|
+
Number.isInteger(edit.start) &&
|
|
368
|
+
Number.isInteger(edit.end) &&
|
|
369
|
+
edit.start >= 0 &&
|
|
370
|
+
edit.end >= edit.start &&
|
|
371
|
+
edit.end <= source.length;
|
|
372
|
+
|
|
373
|
+
const diagnosticMatchedForm = (
|
|
374
|
+
source: string,
|
|
375
|
+
diagnostic: WardenDiagnostic,
|
|
376
|
+
symbol: string | undefined
|
|
377
|
+
): string | undefined => {
|
|
378
|
+
const [edit] = diagnostic.fix?.edits ?? [];
|
|
379
|
+
if (isValidEditSpan(source, edit)) {
|
|
380
|
+
return source.slice(edit.start, edit.end);
|
|
381
|
+
}
|
|
382
|
+
return symbol;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const diagnosticSignals = (diagnostic: WardenDiagnostic): readonly string[] => [
|
|
386
|
+
`warden:${diagnostic.rule}`,
|
|
387
|
+
...(diagnostic.code === undefined
|
|
388
|
+
? []
|
|
389
|
+
: [`${diagnostic.rule}:${diagnostic.code}`]),
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
interface WardenReviewMappingOptions {
|
|
393
|
+
/** Verdict state for this review path. */
|
|
394
|
+
readonly judgment: RegradeReviewJudgment;
|
|
395
|
+
/** Machine-readable review reason. */
|
|
396
|
+
readonly reason: string;
|
|
397
|
+
/** Rule-level guidance used when a finding carries none of its own. */
|
|
398
|
+
readonly ruleGuidance?: WardenGuidance;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const reviewDetailFromDiagnostic = (
|
|
402
|
+
source: string,
|
|
403
|
+
diagnostic: WardenDiagnostic,
|
|
404
|
+
options: WardenReviewMappingOptions
|
|
405
|
+
): RegradeReviewDetail => {
|
|
406
|
+
const symbol =
|
|
407
|
+
firstQuotedValue(diagnostic.fix?.reason) ??
|
|
408
|
+
firstQuotedValue(diagnostic.message);
|
|
409
|
+
const span = diagnosticSpan(source, diagnostic, symbol);
|
|
410
|
+
const target = expectedTarget(diagnostic);
|
|
411
|
+
const replacement = diagnosticCandidateReplacement(diagnostic);
|
|
412
|
+
const matchedForm = diagnosticMatchedForm(source, diagnostic, symbol);
|
|
413
|
+
const guidance = diagnostic.guidance ?? options.ruleGuidance;
|
|
414
|
+
const preserveCautions =
|
|
415
|
+
guidance === undefined
|
|
416
|
+
? undefined
|
|
417
|
+
: [guidance.summary, ...(guidance.steps ?? [])];
|
|
418
|
+
const suggestedValidation = guidance?.commands?.[0];
|
|
419
|
+
return {
|
|
420
|
+
...(replacement === undefined ? {} : { candidateReplacement: replacement }),
|
|
421
|
+
...(target === undefined ? {} : { expectedTarget: target }),
|
|
422
|
+
...(diagnostic.fix?.fixture === undefined
|
|
423
|
+
? {}
|
|
424
|
+
: { fixture: diagnostic.fix.fixture }),
|
|
425
|
+
judgment: options.judgment,
|
|
426
|
+
...(matchedForm === undefined ? {} : { matchedForm }),
|
|
427
|
+
...(preserveCautions === undefined ? {} : { preserveCautions }),
|
|
428
|
+
reason: options.reason,
|
|
429
|
+
signals: diagnosticSignals(diagnostic),
|
|
430
|
+
...(span === undefined ? {} : { span }),
|
|
431
|
+
...(suggestedValidation === undefined ? {} : { suggestedValidation }),
|
|
432
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
433
|
+
} satisfies RegradeReviewDetail;
|
|
434
|
+
};
|
|
435
|
+
|
|
289
436
|
const reviewDetailsFromDiagnostics = (
|
|
290
437
|
source: string,
|
|
291
438
|
diagnostics: readonly WardenDiagnostic[],
|
|
292
|
-
|
|
439
|
+
options: WardenReviewMappingOptions
|
|
293
440
|
): readonly RegradeReviewDetail[] | undefined => {
|
|
294
|
-
const details = diagnostics.map((diagnostic) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
firstQuotedValue(diagnostic.message);
|
|
298
|
-
const span = diagnosticSpan(source, diagnostic, symbol);
|
|
299
|
-
const target = expectedTarget(diagnostic);
|
|
300
|
-
return {
|
|
301
|
-
...(target === undefined ? {} : { expectedTarget: target }),
|
|
302
|
-
...(diagnostic.fix?.fixture === undefined
|
|
303
|
-
? {}
|
|
304
|
-
: { fixture: diagnostic.fix.fixture }),
|
|
305
|
-
reason,
|
|
306
|
-
...(span === undefined ? {} : { span }),
|
|
307
|
-
...(symbol === undefined ? {} : { symbol }),
|
|
308
|
-
} satisfies RegradeReviewDetail;
|
|
309
|
-
});
|
|
441
|
+
const details = diagnostics.map((diagnostic) =>
|
|
442
|
+
reviewDetailFromDiagnostic(source, diagnostic, options)
|
|
443
|
+
);
|
|
310
444
|
return details.length === 0 ? undefined : details;
|
|
311
445
|
};
|
|
312
446
|
|
|
@@ -351,10 +485,11 @@ const applyWardenEdits = (
|
|
|
351
485
|
export const createWardenTermRewriteClass = (
|
|
352
486
|
rule: WardenRule
|
|
353
487
|
): RegradeClass | null => {
|
|
354
|
-
const metadata = getWardenRuleMetadata(rule
|
|
488
|
+
const metadata = getWardenRuleMetadata(rule);
|
|
355
489
|
if (metadata?.fix?.class !== TERM_REWRITE_FIX_CLASS) {
|
|
356
490
|
return null;
|
|
357
491
|
}
|
|
492
|
+
const scanTargets = regradeScanTargetsFromWardenFix(metadata.fix.scanTargets);
|
|
358
493
|
|
|
359
494
|
return {
|
|
360
495
|
apply: (
|
|
@@ -386,10 +521,18 @@ export const createWardenTermRewriteClass = (
|
|
|
386
521
|
(diagnostic) => diagnostic.fix?.safety !== 'safe'
|
|
387
522
|
);
|
|
388
523
|
if (reviewDiagnostics.length > 0) {
|
|
524
|
+
// The rule flagged the occurrence but marked it review: occurrence
|
|
525
|
+
// judgment is unresolved and needs a human or agent decision.
|
|
389
526
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
390
527
|
source,
|
|
391
528
|
reviewDiagnostics,
|
|
392
|
-
|
|
529
|
+
{
|
|
530
|
+
judgment: 'unresolved',
|
|
531
|
+
reason: 'warden-review-required',
|
|
532
|
+
...(metadata.guidance === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { ruleGuidance: metadata.guidance }),
|
|
535
|
+
}
|
|
393
536
|
);
|
|
394
537
|
return {
|
|
395
538
|
kind: 'needs-review',
|
|
@@ -403,10 +546,18 @@ export const createWardenTermRewriteClass = (
|
|
|
403
546
|
(diagnostic) => (diagnostic.fix?.edits?.length ?? 0) === 0
|
|
404
547
|
);
|
|
405
548
|
if (diagnosticsMissingEdits.length > 0) {
|
|
549
|
+
// A safe fix without concrete edits cannot complete occurrence
|
|
550
|
+
// judgment on its own, so the verdict stays unresolved.
|
|
406
551
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
407
552
|
source,
|
|
408
553
|
diagnosticsMissingEdits,
|
|
409
|
-
|
|
554
|
+
{
|
|
555
|
+
judgment: 'unresolved',
|
|
556
|
+
reason: 'warden-fix-missing-edits',
|
|
557
|
+
...(metadata.guidance === undefined
|
|
558
|
+
? {}
|
|
559
|
+
: { ruleGuidance: metadata.guidance }),
|
|
560
|
+
}
|
|
410
561
|
);
|
|
411
562
|
return {
|
|
412
563
|
kind: 'needs-review',
|
|
@@ -421,10 +572,18 @@ export const createWardenTermRewriteClass = (
|
|
|
421
572
|
);
|
|
422
573
|
const application = applyWardenEdits(source, edits);
|
|
423
574
|
if (!application.ok) {
|
|
575
|
+
// The rule completed judgment — it authored concrete edits — but this
|
|
576
|
+
// run could not apply them, so the verdict is a rewrite left undone.
|
|
424
577
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
425
578
|
source,
|
|
426
579
|
diagnostics,
|
|
427
|
-
|
|
580
|
+
{
|
|
581
|
+
judgment: 'rewrite',
|
|
582
|
+
reason: 'warden-fix-invalid',
|
|
583
|
+
...(metadata.guidance === undefined
|
|
584
|
+
? {}
|
|
585
|
+
: { ruleGuidance: metadata.guidance }),
|
|
586
|
+
}
|
|
428
587
|
);
|
|
429
588
|
return {
|
|
430
589
|
kind: 'needs-review',
|
|
@@ -445,6 +604,7 @@ export const createWardenTermRewriteClass = (
|
|
|
445
604
|
},
|
|
446
605
|
describe: `${rule.description} (${metadata.fix.safety} ${metadata.fix.class})`,
|
|
447
606
|
id: `${metadata.fix.class}:${rule.name}`,
|
|
607
|
+
...(scanTargets === undefined ? {} : { scanTargets }),
|
|
448
608
|
};
|
|
449
609
|
};
|
|
450
610
|
|
|
@@ -479,29 +639,56 @@ export const selectRegradeClasses = (
|
|
|
479
639
|
const uniqueSorted = (values: readonly string[]): readonly string[] =>
|
|
480
640
|
[...new Set(values)].toSorted((a, b) => a.localeCompare(b));
|
|
481
641
|
|
|
642
|
+
const intersectValues = (
|
|
643
|
+
left: readonly string[],
|
|
644
|
+
right: readonly string[]
|
|
645
|
+
): readonly string[] => left.filter((value) => right.includes(value));
|
|
646
|
+
|
|
647
|
+
const deriveClassIgnoredDirectories = (
|
|
648
|
+
classes: readonly RegradeClass[]
|
|
649
|
+
): readonly string[] | undefined => {
|
|
650
|
+
const explicitTargets = classes
|
|
651
|
+
.map((cls) => cls.scanTargets?.ignoredDirectories)
|
|
652
|
+
.filter((value): value is readonly string[] => value !== undefined);
|
|
653
|
+
if (explicitTargets.length === 0) {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let common = explicitTargets[0] ?? [];
|
|
658
|
+
for (const target of explicitTargets.slice(1)) {
|
|
659
|
+
common = intersectValues(common, target);
|
|
660
|
+
}
|
|
661
|
+
return uniqueSorted(common);
|
|
662
|
+
};
|
|
663
|
+
|
|
482
664
|
const deriveCollectionOptions = (
|
|
483
665
|
classes: readonly RegradeClass[],
|
|
484
666
|
collection: DownstreamCollectionOptions | undefined
|
|
485
667
|
): DownstreamCollectionOptions => {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
? DEFAULT_SOURCE_EXTENSIONS
|
|
489
|
-
: classes.flatMap(
|
|
490
|
-
(cls) => cls.scanTargets?.extensions ?? DEFAULT_SOURCE_EXTENSIONS
|
|
491
|
-
)
|
|
492
|
-
);
|
|
493
|
-
const ignoredDirectories = uniqueSorted(
|
|
494
|
-
classes.length === 0
|
|
495
|
-
? DEFAULT_IGNORED_DIRECTORIES
|
|
496
|
-
: classes.flatMap(
|
|
497
|
-
(cls) =>
|
|
498
|
-
cls.scanTargets?.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES
|
|
499
|
-
)
|
|
668
|
+
const allExtensions = classes.some(
|
|
669
|
+
(cls) => cls.scanTargets?.extensions?.length === 0
|
|
500
670
|
);
|
|
501
|
-
|
|
671
|
+
const targetExtensions = allExtensions
|
|
672
|
+
? []
|
|
673
|
+
: uniqueSorted(
|
|
674
|
+
classes.length === 0
|
|
675
|
+
? DEFAULT_SOURCE_EXTENSIONS
|
|
676
|
+
: classes.flatMap(
|
|
677
|
+
(cls) => cls.scanTargets?.extensions ?? DEFAULT_SOURCE_EXTENSIONS
|
|
678
|
+
)
|
|
679
|
+
);
|
|
502
680
|
return {
|
|
681
|
+
...(collection?.exclude === undefined
|
|
682
|
+
? {}
|
|
683
|
+
: { exclude: collection.exclude }),
|
|
684
|
+
...(collection?.include === undefined
|
|
685
|
+
? {}
|
|
686
|
+
: { include: collection.include }),
|
|
503
687
|
extensions: collection?.extensions ?? targetExtensions,
|
|
504
|
-
ignoredDirectories:
|
|
688
|
+
ignoredDirectories:
|
|
689
|
+
collection?.ignoredDirectories ??
|
|
690
|
+
deriveClassIgnoredDirectories(classes) ??
|
|
691
|
+
DEFAULT_IGNORED_DIRECTORIES,
|
|
505
692
|
};
|
|
506
693
|
};
|
|
507
694
|
|
|
@@ -539,10 +726,38 @@ export interface RegradeReport {
|
|
|
539
726
|
readonly review: number;
|
|
540
727
|
/** Entries skipped (collection skips plus any run-level skips). */
|
|
541
728
|
readonly skipped: number;
|
|
729
|
+
/** Skipped entries grouped by reason. */
|
|
730
|
+
readonly skipsByReason: Readonly<Record<string, number>>;
|
|
731
|
+
/** Agent-facing inventory summary for the scan. */
|
|
732
|
+
readonly scan: RegradeScanSummary;
|
|
542
733
|
/** Per-entry detail, sorted by path. */
|
|
543
734
|
readonly entries: readonly RegradeReportEntry[];
|
|
544
735
|
/** Apply-mode summary; absent for dry-run report-only calls. */
|
|
545
736
|
readonly apply?: RegradeApplySummary;
|
|
737
|
+
/** Vocabulary regrade run: plan, ledger, and completion report. */
|
|
738
|
+
readonly run?: VocabularyRegradeRun;
|
|
739
|
+
/** Saved active Regrade plan evidence for vocabulary regrades. */
|
|
740
|
+
readonly plan?: {
|
|
741
|
+
readonly expansionPending?: number;
|
|
742
|
+
readonly path: string;
|
|
743
|
+
readonly schemaVersion: number;
|
|
744
|
+
readonly status: 'active' | 'stale';
|
|
745
|
+
};
|
|
746
|
+
/** Saved applied Regrade history evidence for vocabulary regrades. */
|
|
747
|
+
readonly history?: {
|
|
748
|
+
readonly path: string;
|
|
749
|
+
readonly schemaVersion: number;
|
|
750
|
+
readonly status: 'applied' | 'checked' | 'replay';
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* @deprecated Persisted transition record evidence for vocabulary regrades.
|
|
754
|
+
* Use `plan` and `history` summaries in public surfaces.
|
|
755
|
+
*/
|
|
756
|
+
readonly record?: {
|
|
757
|
+
readonly path: string;
|
|
758
|
+
readonly schemaVersion: number;
|
|
759
|
+
readonly status: 'candidate' | 'applied' | 'checked';
|
|
760
|
+
};
|
|
546
761
|
}
|
|
547
762
|
|
|
548
763
|
interface RegradeRewriteCandidate {
|
|
@@ -557,20 +772,80 @@ interface RegradeClassifiedFile {
|
|
|
557
772
|
readonly rewrite?: RegradeRewriteCandidate;
|
|
558
773
|
}
|
|
559
774
|
|
|
775
|
+
const isIgnoredByClassDirectories = (
|
|
776
|
+
path: string,
|
|
777
|
+
ignoredDirectories: readonly string[] | undefined
|
|
778
|
+
): boolean => {
|
|
779
|
+
if (ignoredDirectories === undefined || ignoredDirectories.length === 0) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
return path
|
|
783
|
+
.split('/')
|
|
784
|
+
.slice(0, -1)
|
|
785
|
+
.some((segment) => ignoredDirectories.includes(segment));
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const classScanTargetSkip = (
|
|
789
|
+
cls: RegradeClass,
|
|
790
|
+
path: string,
|
|
791
|
+
collection: DownstreamCollectionOptions | undefined
|
|
792
|
+
): RegradeClassResult | undefined => {
|
|
793
|
+
const ignoredDirectories =
|
|
794
|
+
collection?.ignoredDirectories ??
|
|
795
|
+
cls.scanTargets?.ignoredDirectories ??
|
|
796
|
+
DEFAULT_IGNORED_DIRECTORIES;
|
|
797
|
+
if (isIgnoredByClassDirectories(path, ignoredDirectories)) {
|
|
798
|
+
return {
|
|
799
|
+
kind: 'skipped',
|
|
800
|
+
notes: [`Skipped by ${cls.id} scan-target filtering.`],
|
|
801
|
+
reason: 'regrade-scan-target-filtered',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
const effectiveScanTargets: ScanTargets | undefined =
|
|
805
|
+
cls.scanTargets?.extensions === undefined &&
|
|
806
|
+
collection?.extensions === undefined
|
|
807
|
+
? {
|
|
808
|
+
...cls.scanTargets,
|
|
809
|
+
extensions: DEFAULT_SOURCE_EXTENSIONS,
|
|
810
|
+
}
|
|
811
|
+
: cls.scanTargets;
|
|
812
|
+
if (
|
|
813
|
+
effectiveScanTargets === undefined ||
|
|
814
|
+
includedByPathScope(path, effectiveScanTargets)
|
|
815
|
+
) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
kind: 'skipped',
|
|
820
|
+
notes: [`Skipped by ${cls.id} scan-target filtering.`],
|
|
821
|
+
reason: 'regrade-scan-target-filtered',
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
|
|
560
825
|
const classifyFile = (
|
|
561
826
|
path: string,
|
|
562
827
|
source: string,
|
|
563
828
|
context: RegradeClassContext,
|
|
564
|
-
selected: readonly RegradeClass[]
|
|
829
|
+
selected: readonly RegradeClass[],
|
|
830
|
+
collection?: DownstreamCollectionOptions
|
|
565
831
|
): RegradeClassifiedFile => {
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
832
|
+
// Compose safe rewrites across selected classes in memory so one governed
|
|
833
|
+
// transition can move every compatible symbol in a file. Review still wins:
|
|
834
|
+
// if any class needs judgment, no partial rewrite is returned for that file.
|
|
569
835
|
let skipped:
|
|
570
836
|
| { readonly classId: string; readonly result: RegradeClassResult }
|
|
571
837
|
| undefined;
|
|
838
|
+
let inspected = false;
|
|
839
|
+
let currentSource = source;
|
|
840
|
+
const rewriteClassIds: string[] = [];
|
|
841
|
+
const rewriteNotes: string[] = [];
|
|
572
842
|
for (const cls of selected) {
|
|
573
|
-
const result =
|
|
843
|
+
const result =
|
|
844
|
+
classScanTargetSkip(cls, path, collection) ??
|
|
845
|
+
cls.apply(currentSource, context);
|
|
846
|
+
if (result.kind !== 'skipped') {
|
|
847
|
+
inspected = true;
|
|
848
|
+
}
|
|
574
849
|
if (result.kind === 'rewrite') {
|
|
575
850
|
if (typeof result.nextSource !== 'string') {
|
|
576
851
|
return {
|
|
@@ -583,38 +858,29 @@ const classifyFile = (
|
|
|
583
858
|
},
|
|
584
859
|
};
|
|
585
860
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
path,
|
|
591
|
-
} satisfies RegradeReportEntry;
|
|
592
|
-
return {
|
|
593
|
-
entry,
|
|
594
|
-
...(context.absolutePath === undefined
|
|
595
|
-
? {}
|
|
596
|
-
: {
|
|
597
|
-
rewrite: {
|
|
598
|
-
absolutePath: context.absolutePath,
|
|
599
|
-
classId: cls.id,
|
|
600
|
-
nextSource: result.nextSource,
|
|
601
|
-
path,
|
|
602
|
-
},
|
|
603
|
-
}),
|
|
604
|
-
};
|
|
861
|
+
currentSource = result.nextSource;
|
|
862
|
+
rewriteClassIds.push(cls.id);
|
|
863
|
+
rewriteNotes.push(...(result.notes ?? []));
|
|
864
|
+
continue;
|
|
605
865
|
}
|
|
606
866
|
if (result.kind === 'needs-review') {
|
|
607
|
-
const
|
|
867
|
+
const originalSourceResult =
|
|
868
|
+
currentSource === source ? result : cls.apply(source, context);
|
|
869
|
+
const reviewResult =
|
|
870
|
+
originalSourceResult.kind === 'needs-review'
|
|
871
|
+
? originalSourceResult
|
|
872
|
+
: result;
|
|
873
|
+
const reviewDetails = reviewResult.reviewDetails?.map((detail) => ({
|
|
608
874
|
...detail,
|
|
609
875
|
classId: detail.classId ?? cls.id,
|
|
610
876
|
}));
|
|
611
877
|
return {
|
|
612
878
|
entry: {
|
|
613
879
|
classId: cls.id,
|
|
614
|
-
notes:
|
|
880
|
+
notes: reviewResult.notes,
|
|
615
881
|
outcome: 'needs-review',
|
|
616
882
|
path,
|
|
617
|
-
reason:
|
|
883
|
+
reason: reviewResult.reason ?? 'needs-review',
|
|
618
884
|
...(reviewDetails === undefined ? {} : { reviewDetails }),
|
|
619
885
|
},
|
|
620
886
|
};
|
|
@@ -623,7 +889,29 @@ const classifyFile = (
|
|
|
623
889
|
skipped = { classId: cls.id, result };
|
|
624
890
|
}
|
|
625
891
|
}
|
|
626
|
-
if (
|
|
892
|
+
if (rewriteClassIds.length > 0) {
|
|
893
|
+
const classId = rewriteClassIds.join(',');
|
|
894
|
+
const entry = {
|
|
895
|
+
classId,
|
|
896
|
+
notes: rewriteNotes,
|
|
897
|
+
outcome: 'rewrite',
|
|
898
|
+
path,
|
|
899
|
+
} satisfies RegradeReportEntry;
|
|
900
|
+
return {
|
|
901
|
+
entry,
|
|
902
|
+
...(context.absolutePath === undefined
|
|
903
|
+
? {}
|
|
904
|
+
: {
|
|
905
|
+
rewrite: {
|
|
906
|
+
absolutePath: context.absolutePath,
|
|
907
|
+
classId,
|
|
908
|
+
nextSource: currentSource,
|
|
909
|
+
path,
|
|
910
|
+
},
|
|
911
|
+
}),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (!inspected && skipped !== undefined) {
|
|
627
915
|
return {
|
|
628
916
|
entry: {
|
|
629
917
|
classId: skipped.classId,
|
|
@@ -642,6 +930,32 @@ interface RegradeEvaluation {
|
|
|
642
930
|
readonly rewrites: readonly RegradeRewriteCandidate[];
|
|
643
931
|
}
|
|
644
932
|
|
|
933
|
+
const includeEntryInReport = (
|
|
934
|
+
entry: RegradeReportEntry,
|
|
935
|
+
selection: RegradeReportEntrySelection
|
|
936
|
+
): boolean =>
|
|
937
|
+
selection === 'all' ||
|
|
938
|
+
entry.outcome === 'rewrite' ||
|
|
939
|
+
entry.outcome === 'needs-review';
|
|
940
|
+
|
|
941
|
+
const skipsByReason = (
|
|
942
|
+
entries: readonly RegradeReportEntry[]
|
|
943
|
+
): Readonly<Record<string, number>> => {
|
|
944
|
+
const counts = new Map<string, number>();
|
|
945
|
+
for (const entry of entries) {
|
|
946
|
+
if (entry.outcome !== 'skip') {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const reason = entry.reason ?? 'skipped';
|
|
950
|
+
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
951
|
+
}
|
|
952
|
+
return Object.fromEntries(
|
|
953
|
+
[...counts.entries()].toSorted(([left], [right]) =>
|
|
954
|
+
left.localeCompare(right)
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
};
|
|
958
|
+
|
|
645
959
|
/**
|
|
646
960
|
* Build a coverage report from already-read source files. Pure: no filesystem
|
|
647
961
|
* access, so coverage semantics are testable directly.
|
|
@@ -656,7 +970,10 @@ const buildRegradeEvaluation = (params: {
|
|
|
656
970
|
readonly skipped: readonly SkippedSource[];
|
|
657
971
|
readonly classes: readonly RegradeClass[];
|
|
658
972
|
readonly selection?: RegradeSelection;
|
|
973
|
+
readonly collection?: DownstreamCollectionOptions;
|
|
974
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
659
975
|
}): RegradeEvaluation => {
|
|
976
|
+
const entrySelection = params.includeEntries ?? 'actionable';
|
|
660
977
|
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
661
978
|
params.classes,
|
|
662
979
|
params.selection
|
|
@@ -672,7 +989,8 @@ const buildRegradeEvaluation = (params: {
|
|
|
672
989
|
: { absolutePath: file.absolutePath }),
|
|
673
990
|
path: file.path,
|
|
674
991
|
},
|
|
675
|
-
selected
|
|
992
|
+
selected,
|
|
993
|
+
params.collection
|
|
676
994
|
)
|
|
677
995
|
);
|
|
678
996
|
const fileEntries = classifiedFiles.map((file) => file.entry);
|
|
@@ -685,9 +1003,12 @@ const buildRegradeEvaluation = (params: {
|
|
|
685
1003
|
reason: entry.reason,
|
|
686
1004
|
}));
|
|
687
1005
|
|
|
688
|
-
const
|
|
1006
|
+
const allEntries = [...fileEntries, ...skipEntries].toSorted((a, b) =>
|
|
689
1007
|
a.path.localeCompare(b.path)
|
|
690
1008
|
);
|
|
1009
|
+
const entries = allEntries.filter((entry) =>
|
|
1010
|
+
includeEntryInReport(entry, entrySelection)
|
|
1011
|
+
);
|
|
691
1012
|
|
|
692
1013
|
// Class-level skips (e.g. scan-target filtering) are accounted as skipped, not
|
|
693
1014
|
// as scanned/clean files.
|
|
@@ -699,6 +1020,11 @@ const buildRegradeEvaluation = (params: {
|
|
|
699
1020
|
const review = scannedEntries.filter(
|
|
700
1021
|
(e) => e.outcome === 'needs-review'
|
|
701
1022
|
).length;
|
|
1023
|
+
const matchedPaths = scannedEntries
|
|
1024
|
+
.filter((e) => e.outcome === 'rewrite' || e.outcome === 'needs-review')
|
|
1025
|
+
.map((entry) => entry.path);
|
|
1026
|
+
const skipped = skipEntries.length + fileSkipCount;
|
|
1027
|
+
const skippedReasons = skipsByReason(allEntries);
|
|
702
1028
|
|
|
703
1029
|
return {
|
|
704
1030
|
report: {
|
|
@@ -707,9 +1033,16 @@ const buildRegradeEvaluation = (params: {
|
|
|
707
1033
|
review,
|
|
708
1034
|
rewritten,
|
|
709
1035
|
root: params.root,
|
|
1036
|
+
scan: buildRegradeScanSummary({
|
|
1037
|
+
matchedPaths,
|
|
1038
|
+
scanned: scannedEntries.length,
|
|
1039
|
+
skipped,
|
|
1040
|
+
skippedByReason: skippedReasons,
|
|
1041
|
+
}),
|
|
710
1042
|
scanned: scannedEntries.length,
|
|
711
1043
|
selectedClassIds: selected.map((cls) => cls.id),
|
|
712
|
-
skipped
|
|
1044
|
+
skipped,
|
|
1045
|
+
skipsByReason: skippedReasons,
|
|
713
1046
|
unknownClassIds,
|
|
714
1047
|
},
|
|
715
1048
|
rewrites,
|
|
@@ -726,6 +1059,8 @@ export const buildRegradeReport = (params: {
|
|
|
726
1059
|
readonly skipped: readonly SkippedSource[];
|
|
727
1060
|
readonly classes: readonly RegradeClass[];
|
|
728
1061
|
readonly selection?: RegradeSelection;
|
|
1062
|
+
readonly collection?: DownstreamCollectionOptions;
|
|
1063
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
729
1064
|
}): RegradeReport => buildRegradeEvaluation(params).report;
|
|
730
1065
|
|
|
731
1066
|
const applyRegradeEvaluation = (
|
|
@@ -797,6 +1132,7 @@ const runRegradeEvaluation = (params: {
|
|
|
797
1132
|
readonly classes: readonly RegradeClass[];
|
|
798
1133
|
readonly selection?: RegradeSelection;
|
|
799
1134
|
readonly collection?: DownstreamCollectionOptions;
|
|
1135
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
800
1136
|
}): RegradeEvaluation | null => {
|
|
801
1137
|
const { selected, unknownClassIds } = selectRegradeClasses(
|
|
802
1138
|
params.classes,
|
|
@@ -808,9 +1144,15 @@ const runRegradeEvaluation = (params: {
|
|
|
808
1144
|
}
|
|
809
1145
|
return buildRegradeEvaluation({
|
|
810
1146
|
classes: params.classes,
|
|
1147
|
+
...(params.collection === undefined
|
|
1148
|
+
? {}
|
|
1149
|
+
: { collection: params.collection }),
|
|
811
1150
|
files: [],
|
|
812
1151
|
root: params.root,
|
|
813
1152
|
skipped: [],
|
|
1153
|
+
...(params.includeEntries === undefined
|
|
1154
|
+
? {}
|
|
1155
|
+
: { includeEntries: params.includeEntries }),
|
|
814
1156
|
...(params.selection === undefined
|
|
815
1157
|
? {}
|
|
816
1158
|
: { selection: params.selection }),
|
|
@@ -841,9 +1183,15 @@ const runRegradeEvaluation = (params: {
|
|
|
841
1183
|
|
|
842
1184
|
return buildRegradeEvaluation({
|
|
843
1185
|
classes: params.classes,
|
|
1186
|
+
...(params.collection === undefined
|
|
1187
|
+
? {}
|
|
1188
|
+
: { collection: params.collection }),
|
|
844
1189
|
files,
|
|
845
1190
|
root: params.root,
|
|
846
1191
|
skipped,
|
|
1192
|
+
...(params.includeEntries === undefined
|
|
1193
|
+
? {}
|
|
1194
|
+
: { includeEntries: params.includeEntries }),
|
|
847
1195
|
...(params.selection === undefined ? {} : { selection: params.selection }),
|
|
848
1196
|
});
|
|
849
1197
|
};
|
|
@@ -861,6 +1209,7 @@ export const runRegrade = (params: {
|
|
|
861
1209
|
readonly selection?: RegradeSelection;
|
|
862
1210
|
readonly collection?: DownstreamCollectionOptions;
|
|
863
1211
|
readonly apply?: boolean;
|
|
1212
|
+
readonly includeEntries?: RegradeReportEntrySelection;
|
|
864
1213
|
}): Result<RegradeReport | null, InternalError> => {
|
|
865
1214
|
const evaluation = runRegradeEvaluation(params);
|
|
866
1215
|
if (evaluation === null) {
|
|
@@ -890,6 +1239,12 @@ const regradeReportEntrySchema = z.object({
|
|
|
890
1239
|
reviewDetails: z
|
|
891
1240
|
.array(
|
|
892
1241
|
z.object({
|
|
1242
|
+
candidateReplacement: z
|
|
1243
|
+
.string()
|
|
1244
|
+
.optional()
|
|
1245
|
+
.describe(
|
|
1246
|
+
'Concrete replacement the class would apply if the occurrence were judged safe'
|
|
1247
|
+
),
|
|
893
1248
|
classId: z
|
|
894
1249
|
.string()
|
|
895
1250
|
.optional()
|
|
@@ -902,11 +1257,35 @@ const regradeReportEntrySchema = z.object({
|
|
|
902
1257
|
.string()
|
|
903
1258
|
.optional()
|
|
904
1259
|
.describe('Fixture or example reference for the migration'),
|
|
1260
|
+
judgment: z
|
|
1261
|
+
.enum(['preserve', 'rewrite', 'unresolved'])
|
|
1262
|
+
.optional()
|
|
1263
|
+
.describe(
|
|
1264
|
+
'Verdict state: unresolved = occurrence judgment is incomplete and needs a human or agent decision; preserve = completed verdict to keep the occurrence; rewrite = completed verdict that a rewrite is intended but this run could not apply it'
|
|
1265
|
+
),
|
|
1266
|
+
matchedForm: z
|
|
1267
|
+
.string()
|
|
1268
|
+
.optional()
|
|
1269
|
+
.describe(
|
|
1270
|
+
'Exact matched source text for the occurrence under review'
|
|
1271
|
+
),
|
|
905
1272
|
nodeKind: z
|
|
906
1273
|
.string()
|
|
907
1274
|
.optional()
|
|
908
1275
|
.describe('AST node kind or source construct kind'),
|
|
1276
|
+
preserveCautions: z
|
|
1277
|
+
.array(z.string())
|
|
1278
|
+
.optional()
|
|
1279
|
+
.describe(
|
|
1280
|
+
'Cautions explaining why a blind rewrite of this occurrence is unsafe'
|
|
1281
|
+
),
|
|
909
1282
|
reason: z.string().describe('Machine-readable review reason'),
|
|
1283
|
+
signals: z
|
|
1284
|
+
.array(z.string())
|
|
1285
|
+
.optional()
|
|
1286
|
+
.describe(
|
|
1287
|
+
'Machine-readable provenance tags for the producing rule or class'
|
|
1288
|
+
),
|
|
910
1289
|
span: z
|
|
911
1290
|
.object({
|
|
912
1291
|
column: z.number().describe('One-based source column'),
|
|
@@ -941,14 +1320,61 @@ export const regradeReportOutput = z.object({
|
|
|
941
1320
|
.describe('Apply-mode summary; absent for dry-run report-only calls'),
|
|
942
1321
|
entries: z
|
|
943
1322
|
.array(regradeReportEntrySchema)
|
|
944
|
-
.describe(
|
|
1323
|
+
.describe(
|
|
1324
|
+
'Per-entry detail, sorted by path. Defaults to actionable rewrite/review entries.'
|
|
1325
|
+
),
|
|
1326
|
+
history: z
|
|
1327
|
+
.object({
|
|
1328
|
+
path: z.string().describe('Root-relative applied history entry path'),
|
|
1329
|
+
schemaVersion: z.number().describe('Regrade history schema version'),
|
|
1330
|
+
status: z
|
|
1331
|
+
.enum(['applied', 'checked', 'replay'])
|
|
1332
|
+
.describe(
|
|
1333
|
+
'How this command used the Regrade history file: applied = run appended, replay = identical re-run recognized and not duplicated, checked = consolidated history verified per-run'
|
|
1334
|
+
),
|
|
1335
|
+
})
|
|
1336
|
+
.optional()
|
|
1337
|
+
.describe('Saved applied Regrade history evidence'),
|
|
945
1338
|
matched: z.number().describe('Files with a rewrite or review outcome'),
|
|
1339
|
+
plan: z
|
|
1340
|
+
.object({
|
|
1341
|
+
expansionPending: z
|
|
1342
|
+
.number()
|
|
1343
|
+
.optional()
|
|
1344
|
+
.describe('Pending staged expansion candidates on this plan'),
|
|
1345
|
+
path: z.string().describe('Root-relative Regrade plan path'),
|
|
1346
|
+
schemaVersion: z.number().describe('Regrade plan schema version'),
|
|
1347
|
+
status: z
|
|
1348
|
+
.enum(['active', 'stale'])
|
|
1349
|
+
.describe('Whether the saved plan still matches the source tree'),
|
|
1350
|
+
})
|
|
1351
|
+
.optional()
|
|
1352
|
+
.describe('Saved active Regrade plan evidence'),
|
|
1353
|
+
record: z
|
|
1354
|
+
.object({
|
|
1355
|
+
path: z.string().describe('Root-relative transition record path'),
|
|
1356
|
+
schemaVersion: z.number().describe('Transition record schema version'),
|
|
1357
|
+
status: z
|
|
1358
|
+
.enum(['candidate', 'applied', 'checked'])
|
|
1359
|
+
.describe('How this command used the transition record'),
|
|
1360
|
+
})
|
|
1361
|
+
.optional()
|
|
1362
|
+
.describe('Persisted transition record evidence'),
|
|
946
1363
|
review: z.number().describe('Files routed to review'),
|
|
947
1364
|
rewritten: z.number().describe('Files with a rewrite outcome'),
|
|
948
1365
|
root: z.string().describe('Root the run scanned'),
|
|
1366
|
+
run: vocabularyRegradeRunOutput
|
|
1367
|
+
.optional()
|
|
1368
|
+
.describe('Vocabulary regrade run: plan, ledger, and completion report'),
|
|
1369
|
+
scan: regradeScanSummaryOutput.describe(
|
|
1370
|
+
'Agent-facing inventory summary for the scan'
|
|
1371
|
+
),
|
|
949
1372
|
scanned: z.number().describe('Source files inspected'),
|
|
950
1373
|
selectedClassIds: z.array(z.string()).describe('Class ids executed'),
|
|
951
1374
|
skipped: z.number().describe('Entries skipped'),
|
|
1375
|
+
skipsByReason: z
|
|
1376
|
+
.record(z.string(), z.number())
|
|
1377
|
+
.describe('Skipped entries grouped by reason'),
|
|
952
1378
|
unknownClassIds: z
|
|
953
1379
|
.array(z.string())
|
|
954
1380
|
.describe('Selected ids that did not resolve to a class'),
|
|
@@ -967,3 +1393,55 @@ export const wardenTermRewriteClasses: readonly RegradeClass[] = Object.freeze(
|
|
|
967
1393
|
return cls === null ? [] : [cls];
|
|
968
1394
|
})
|
|
969
1395
|
);
|
|
1396
|
+
|
|
1397
|
+
const duplicateClassDiagnostics = (
|
|
1398
|
+
root: string,
|
|
1399
|
+
classes: readonly RegradeClass[]
|
|
1400
|
+
): readonly WardenDiagnostic[] => {
|
|
1401
|
+
const seen = new Set<string>();
|
|
1402
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
1403
|
+
for (const cls of classes) {
|
|
1404
|
+
if (!seen.has(cls.id)) {
|
|
1405
|
+
seen.add(cls.id);
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
diagnostics.push({
|
|
1409
|
+
filePath: root,
|
|
1410
|
+
line: 1,
|
|
1411
|
+
message: `Duplicate Regrade class id "${cls.id}" from Warden term-rewrite rules.`,
|
|
1412
|
+
rule: 'regrade-warden-term-rewrite-classes',
|
|
1413
|
+
severity: 'error',
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return diagnostics;
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Load built-in and project-local Warden term-rewrite rules as Regrade classes.
|
|
1421
|
+
*
|
|
1422
|
+
* Built-ins are always available. When `root` is provided, committed
|
|
1423
|
+
* project-local Warden rules under `.trails/rules.ts` or direct
|
|
1424
|
+
* `.trails/rules/*.ts` modules are loaded and any term-rewrite-capable source
|
|
1425
|
+
* rules join the class set.
|
|
1426
|
+
*/
|
|
1427
|
+
export const loadWardenTermRewriteClasses = async (
|
|
1428
|
+
root?: string
|
|
1429
|
+
): Promise<RegradeWardenClassSet> => {
|
|
1430
|
+
if (root === undefined) {
|
|
1431
|
+
return { classes: wardenTermRewriteClasses, diagnostics: [] };
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const projectRules = await loadProjectWardenRules(root);
|
|
1435
|
+
const projectClasses = projectRules.sourceRules.flatMap((rule) => {
|
|
1436
|
+
const cls = createWardenTermRewriteClass(rule);
|
|
1437
|
+
return cls === null ? [] : [cls];
|
|
1438
|
+
});
|
|
1439
|
+
const classes = [...wardenTermRewriteClasses, ...projectClasses];
|
|
1440
|
+
return {
|
|
1441
|
+
classes,
|
|
1442
|
+
diagnostics: [
|
|
1443
|
+
...projectRules.diagnostics,
|
|
1444
|
+
...duplicateClassDiagnostics(root, classes),
|
|
1445
|
+
],
|
|
1446
|
+
};
|
|
1447
|
+
};
|