@ontrails/regrade 1.0.0-beta.32 → 1.0.0-beta.41
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 +135 -0
- package/package.json +6 -5
- package/src/downstream/ast-rewrite.ts +725 -17
- package/src/downstream/collect.ts +17 -2
- package/src/downstream/export-restructure.ts +1588 -0
- package/src/downstream/report.ts +409 -48
- package/src/downstream/vocabulary-registry.ts +96 -0
- package/src/downstream/vocabulary.ts +1160 -118
- package/src/index.ts +27 -0
- package/src/literal-transform.ts +26 -20
package/src/downstream/report.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ScanTargets } from '@ontrails/core';
|
|
|
8
8
|
import type {
|
|
9
9
|
WardenDiagnostic,
|
|
10
10
|
WardenFixEdit,
|
|
11
|
+
WardenGuidance,
|
|
11
12
|
WardenRule,
|
|
12
13
|
} from '@ontrails/warden';
|
|
13
14
|
import {
|
|
@@ -16,7 +17,8 @@ import {
|
|
|
16
17
|
loadProjectWardenRules,
|
|
17
18
|
wardenRules,
|
|
18
19
|
} from '@ontrails/warden';
|
|
19
|
-
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
20
22
|
import { z } from 'zod';
|
|
21
23
|
|
|
22
24
|
import {
|
|
@@ -78,6 +80,16 @@ export interface RegradeClassContext {
|
|
|
78
80
|
readonly path: string;
|
|
79
81
|
/** Absolute path on disk, when the caller has one. */
|
|
80
82
|
readonly absolutePath?: string;
|
|
83
|
+
/** Nearest owning package facts, when filesystem collection found a manifest. */
|
|
84
|
+
readonly package?: {
|
|
85
|
+
readonly dependencies: readonly string[];
|
|
86
|
+
/** Runtime-visible dependency declarations (dependencies, optional, peer). */
|
|
87
|
+
readonly runtimeDependencies?: readonly string[];
|
|
88
|
+
readonly manifestState?: 'invalid' | 'valid';
|
|
89
|
+
readonly name?: string;
|
|
90
|
+
/** Root-relative POSIX manifest path. */
|
|
91
|
+
readonly path: string;
|
|
92
|
+
};
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/** Files a regrade class knows how to inspect. */
|
|
@@ -143,18 +155,39 @@ export interface RegradeReviewSpan {
|
|
|
143
155
|
readonly start: number;
|
|
144
156
|
}
|
|
145
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Verdict state for a review detail.
|
|
160
|
+
*
|
|
161
|
+
* - `unresolved`: the class could not complete occurrence judgment; a human or
|
|
162
|
+
* agent decision is still needed.
|
|
163
|
+
* - `preserve`: a completed verdict to keep the occurrence as-is.
|
|
164
|
+
* - `rewrite`: a completed verdict that a rewrite is intended but this run
|
|
165
|
+
* could not apply it (for example invalid or missing edits).
|
|
166
|
+
*/
|
|
167
|
+
export type RegradeReviewJudgment = 'preserve' | 'rewrite' | 'unresolved';
|
|
168
|
+
|
|
146
169
|
/** Structured detail explaining why a source match needs review. */
|
|
147
170
|
export interface RegradeReviewDetail {
|
|
171
|
+
/** Concrete replacement the class would apply if the occurrence were judged safe. */
|
|
172
|
+
readonly candidateReplacement?: string;
|
|
148
173
|
/** Class that produced the review detail, injected by report building. */
|
|
149
174
|
readonly classId?: string;
|
|
150
175
|
/** Expected target shape when the class can describe one. */
|
|
151
176
|
readonly expectedTarget?: string;
|
|
152
177
|
/** Fixture or example reference that illustrates the expected migration. */
|
|
153
178
|
readonly fixture?: string;
|
|
179
|
+
/** Whether occurrence judgment is unresolved or a preserve/rewrite verdict completed. */
|
|
180
|
+
readonly judgment?: RegradeReviewJudgment;
|
|
181
|
+
/** Exact matched source text for the occurrence under review. */
|
|
182
|
+
readonly matchedForm?: string;
|
|
154
183
|
/** AST node kind or source construct kind. */
|
|
155
184
|
readonly nodeKind?: string;
|
|
185
|
+
/** Cautions explaining why a blind rewrite of this occurrence is unsafe. */
|
|
186
|
+
readonly preserveCautions?: readonly string[];
|
|
156
187
|
/** Machine-readable reason for review. */
|
|
157
188
|
readonly reason: string;
|
|
189
|
+
/** Machine-readable provenance tags for the producing rule or class. */
|
|
190
|
+
readonly signals?: readonly string[];
|
|
158
191
|
/** Source span and line/column for the review-required match. */
|
|
159
192
|
readonly span?: RegradeReviewSpan;
|
|
160
193
|
/** Suggested validation command after the review is resolved. */
|
|
@@ -317,7 +350,9 @@ const diagnosticSpan = (
|
|
|
317
350
|
return spanForSymbolOnDiagnosticLine(source, diagnostic.line, symbol);
|
|
318
351
|
};
|
|
319
352
|
|
|
320
|
-
const
|
|
353
|
+
const diagnosticCandidateReplacement = (
|
|
354
|
+
diagnostic: WardenDiagnostic
|
|
355
|
+
): string | undefined => {
|
|
321
356
|
const replacements = new Set(
|
|
322
357
|
(diagnostic.fix?.edits ?? []).map((edit) => edit.replacement)
|
|
323
358
|
);
|
|
@@ -325,32 +360,98 @@ const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
|
325
360
|
return undefined;
|
|
326
361
|
}
|
|
327
362
|
const [replacement] = replacements;
|
|
363
|
+
return replacement;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
367
|
+
const replacement = diagnosticCandidateReplacement(diagnostic);
|
|
328
368
|
return replacement === undefined
|
|
329
369
|
? undefined
|
|
330
370
|
: `Replace with "${replacement}".`;
|
|
331
371
|
};
|
|
332
372
|
|
|
373
|
+
const isValidEditSpan = (
|
|
374
|
+
source: string,
|
|
375
|
+
edit: WardenFixEdit | undefined
|
|
376
|
+
): edit is WardenFixEdit =>
|
|
377
|
+
edit !== undefined &&
|
|
378
|
+
Number.isInteger(edit.start) &&
|
|
379
|
+
Number.isInteger(edit.end) &&
|
|
380
|
+
edit.start >= 0 &&
|
|
381
|
+
edit.end >= edit.start &&
|
|
382
|
+
edit.end <= source.length;
|
|
383
|
+
|
|
384
|
+
const diagnosticMatchedForm = (
|
|
385
|
+
source: string,
|
|
386
|
+
diagnostic: WardenDiagnostic,
|
|
387
|
+
symbol: string | undefined
|
|
388
|
+
): string | undefined => {
|
|
389
|
+
const [edit] = diagnostic.fix?.edits ?? [];
|
|
390
|
+
if (isValidEditSpan(source, edit)) {
|
|
391
|
+
return source.slice(edit.start, edit.end);
|
|
392
|
+
}
|
|
393
|
+
return symbol;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const diagnosticSignals = (diagnostic: WardenDiagnostic): readonly string[] => [
|
|
397
|
+
`warden:${diagnostic.rule}`,
|
|
398
|
+
...(diagnostic.code === undefined
|
|
399
|
+
? []
|
|
400
|
+
: [`${diagnostic.rule}:${diagnostic.code}`]),
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
interface WardenReviewMappingOptions {
|
|
404
|
+
/** Verdict state for this review path. */
|
|
405
|
+
readonly judgment: RegradeReviewJudgment;
|
|
406
|
+
/** Machine-readable review reason. */
|
|
407
|
+
readonly reason: string;
|
|
408
|
+
/** Rule-level guidance used when a finding carries none of its own. */
|
|
409
|
+
readonly ruleGuidance?: WardenGuidance;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const reviewDetailFromDiagnostic = (
|
|
413
|
+
source: string,
|
|
414
|
+
diagnostic: WardenDiagnostic,
|
|
415
|
+
options: WardenReviewMappingOptions
|
|
416
|
+
): RegradeReviewDetail => {
|
|
417
|
+
const symbol =
|
|
418
|
+
firstQuotedValue(diagnostic.fix?.reason) ??
|
|
419
|
+
firstQuotedValue(diagnostic.message);
|
|
420
|
+
const span = diagnosticSpan(source, diagnostic, symbol);
|
|
421
|
+
const target = expectedTarget(diagnostic);
|
|
422
|
+
const replacement = diagnosticCandidateReplacement(diagnostic);
|
|
423
|
+
const matchedForm = diagnosticMatchedForm(source, diagnostic, symbol);
|
|
424
|
+
const guidance = diagnostic.guidance ?? options.ruleGuidance;
|
|
425
|
+
const preserveCautions =
|
|
426
|
+
guidance === undefined
|
|
427
|
+
? undefined
|
|
428
|
+
: [guidance.summary, ...(guidance.steps ?? [])];
|
|
429
|
+
const suggestedValidation = guidance?.commands?.[0];
|
|
430
|
+
return {
|
|
431
|
+
...(replacement === undefined ? {} : { candidateReplacement: replacement }),
|
|
432
|
+
...(target === undefined ? {} : { expectedTarget: target }),
|
|
433
|
+
...(diagnostic.fix?.fixture === undefined
|
|
434
|
+
? {}
|
|
435
|
+
: { fixture: diagnostic.fix.fixture }),
|
|
436
|
+
judgment: options.judgment,
|
|
437
|
+
...(matchedForm === undefined ? {} : { matchedForm }),
|
|
438
|
+
...(preserveCautions === undefined ? {} : { preserveCautions }),
|
|
439
|
+
reason: options.reason,
|
|
440
|
+
signals: diagnosticSignals(diagnostic),
|
|
441
|
+
...(span === undefined ? {} : { span }),
|
|
442
|
+
...(suggestedValidation === undefined ? {} : { suggestedValidation }),
|
|
443
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
444
|
+
} satisfies RegradeReviewDetail;
|
|
445
|
+
};
|
|
446
|
+
|
|
333
447
|
const reviewDetailsFromDiagnostics = (
|
|
334
448
|
source: string,
|
|
335
449
|
diagnostics: readonly WardenDiagnostic[],
|
|
336
|
-
|
|
450
|
+
options: WardenReviewMappingOptions
|
|
337
451
|
): readonly RegradeReviewDetail[] | undefined => {
|
|
338
|
-
const details = diagnostics.map((diagnostic) =>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
firstQuotedValue(diagnostic.message);
|
|
342
|
-
const span = diagnosticSpan(source, diagnostic, symbol);
|
|
343
|
-
const target = expectedTarget(diagnostic);
|
|
344
|
-
return {
|
|
345
|
-
...(target === undefined ? {} : { expectedTarget: target }),
|
|
346
|
-
...(diagnostic.fix?.fixture === undefined
|
|
347
|
-
? {}
|
|
348
|
-
: { fixture: diagnostic.fix.fixture }),
|
|
349
|
-
reason,
|
|
350
|
-
...(span === undefined ? {} : { span }),
|
|
351
|
-
...(symbol === undefined ? {} : { symbol }),
|
|
352
|
-
} satisfies RegradeReviewDetail;
|
|
353
|
-
});
|
|
452
|
+
const details = diagnostics.map((diagnostic) =>
|
|
453
|
+
reviewDetailFromDiagnostic(source, diagnostic, options)
|
|
454
|
+
);
|
|
354
455
|
return details.length === 0 ? undefined : details;
|
|
355
456
|
};
|
|
356
457
|
|
|
@@ -431,10 +532,18 @@ export const createWardenTermRewriteClass = (
|
|
|
431
532
|
(diagnostic) => diagnostic.fix?.safety !== 'safe'
|
|
432
533
|
);
|
|
433
534
|
if (reviewDiagnostics.length > 0) {
|
|
535
|
+
// The rule flagged the occurrence but marked it review: occurrence
|
|
536
|
+
// judgment is unresolved and needs a human or agent decision.
|
|
434
537
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
435
538
|
source,
|
|
436
539
|
reviewDiagnostics,
|
|
437
|
-
|
|
540
|
+
{
|
|
541
|
+
judgment: 'unresolved',
|
|
542
|
+
reason: 'warden-review-required',
|
|
543
|
+
...(metadata.guidance === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { ruleGuidance: metadata.guidance }),
|
|
546
|
+
}
|
|
438
547
|
);
|
|
439
548
|
return {
|
|
440
549
|
kind: 'needs-review',
|
|
@@ -448,10 +557,18 @@ export const createWardenTermRewriteClass = (
|
|
|
448
557
|
(diagnostic) => (diagnostic.fix?.edits?.length ?? 0) === 0
|
|
449
558
|
);
|
|
450
559
|
if (diagnosticsMissingEdits.length > 0) {
|
|
560
|
+
// A safe fix without concrete edits cannot complete occurrence
|
|
561
|
+
// judgment on its own, so the verdict stays unresolved.
|
|
451
562
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
452
563
|
source,
|
|
453
564
|
diagnosticsMissingEdits,
|
|
454
|
-
|
|
565
|
+
{
|
|
566
|
+
judgment: 'unresolved',
|
|
567
|
+
reason: 'warden-fix-missing-edits',
|
|
568
|
+
...(metadata.guidance === undefined
|
|
569
|
+
? {}
|
|
570
|
+
: { ruleGuidance: metadata.guidance }),
|
|
571
|
+
}
|
|
455
572
|
);
|
|
456
573
|
return {
|
|
457
574
|
kind: 'needs-review',
|
|
@@ -466,10 +583,18 @@ export const createWardenTermRewriteClass = (
|
|
|
466
583
|
);
|
|
467
584
|
const application = applyWardenEdits(source, edits);
|
|
468
585
|
if (!application.ok) {
|
|
586
|
+
// The rule completed judgment — it authored concrete edits — but this
|
|
587
|
+
// run could not apply them, so the verdict is a rewrite left undone.
|
|
469
588
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
470
589
|
source,
|
|
471
590
|
diagnostics,
|
|
472
|
-
|
|
591
|
+
{
|
|
592
|
+
judgment: 'rewrite',
|
|
593
|
+
reason: 'warden-fix-invalid',
|
|
594
|
+
...(metadata.guidance === undefined
|
|
595
|
+
? {}
|
|
596
|
+
: { ruleGuidance: metadata.guidance }),
|
|
597
|
+
}
|
|
473
598
|
);
|
|
474
599
|
return {
|
|
475
600
|
kind: 'needs-review',
|
|
@@ -567,6 +692,9 @@ const deriveCollectionOptions = (
|
|
|
567
692
|
...(collection?.exclude === undefined
|
|
568
693
|
? {}
|
|
569
694
|
: { exclude: collection.exclude }),
|
|
695
|
+
...(collection?.include === undefined
|
|
696
|
+
? {}
|
|
697
|
+
: { include: collection.include }),
|
|
570
698
|
extensions: collection?.extensions ?? targetExtensions,
|
|
571
699
|
ignoredDirectories:
|
|
572
700
|
collection?.ignoredDirectories ??
|
|
@@ -619,6 +747,28 @@ export interface RegradeReport {
|
|
|
619
747
|
readonly apply?: RegradeApplySummary;
|
|
620
748
|
/** Vocabulary regrade run: plan, ledger, and completion report. */
|
|
621
749
|
readonly run?: VocabularyRegradeRun;
|
|
750
|
+
/** Saved active Regrade plan evidence for vocabulary regrades. */
|
|
751
|
+
readonly plan?: {
|
|
752
|
+
readonly expansionPending?: number;
|
|
753
|
+
readonly path: string;
|
|
754
|
+
readonly schemaVersion: number;
|
|
755
|
+
readonly status: 'active' | 'stale';
|
|
756
|
+
};
|
|
757
|
+
/** Saved applied Regrade history evidence for vocabulary regrades. */
|
|
758
|
+
readonly history?: {
|
|
759
|
+
readonly path: string;
|
|
760
|
+
readonly schemaVersion: number;
|
|
761
|
+
readonly status: 'applied' | 'checked' | 'replay';
|
|
762
|
+
};
|
|
763
|
+
/**
|
|
764
|
+
* @deprecated Persisted transition record evidence for vocabulary regrades.
|
|
765
|
+
* Use `plan` and `history` summaries in public surfaces.
|
|
766
|
+
*/
|
|
767
|
+
readonly record?: {
|
|
768
|
+
readonly path: string;
|
|
769
|
+
readonly schemaVersion: number;
|
|
770
|
+
readonly status: 'candidate' | 'applied' | 'checked';
|
|
771
|
+
};
|
|
622
772
|
}
|
|
623
773
|
|
|
624
774
|
interface RegradeRewriteCandidate {
|
|
@@ -690,16 +840,20 @@ const classifyFile = (
|
|
|
690
840
|
selected: readonly RegradeClass[],
|
|
691
841
|
collection?: DownstreamCollectionOptions
|
|
692
842
|
): RegradeClassifiedFile => {
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
843
|
+
// Compose safe rewrites across selected classes in memory so one governed
|
|
844
|
+
// transition can move every compatible symbol in a file. Review still wins:
|
|
845
|
+
// if any class needs judgment, no partial rewrite is returned for that file.
|
|
696
846
|
let skipped:
|
|
697
847
|
| { readonly classId: string; readonly result: RegradeClassResult }
|
|
698
848
|
| undefined;
|
|
699
849
|
let inspected = false;
|
|
850
|
+
let currentSource = source;
|
|
851
|
+
const rewriteClassIds: string[] = [];
|
|
852
|
+
const rewriteNotes: string[] = [];
|
|
700
853
|
for (const cls of selected) {
|
|
701
854
|
const result =
|
|
702
|
-
classScanTargetSkip(cls, path, collection) ??
|
|
855
|
+
classScanTargetSkip(cls, path, collection) ??
|
|
856
|
+
cls.apply(currentSource, context);
|
|
703
857
|
if (result.kind !== 'skipped') {
|
|
704
858
|
inspected = true;
|
|
705
859
|
}
|
|
@@ -715,38 +869,29 @@ const classifyFile = (
|
|
|
715
869
|
},
|
|
716
870
|
};
|
|
717
871
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
path,
|
|
723
|
-
} satisfies RegradeReportEntry;
|
|
724
|
-
return {
|
|
725
|
-
entry,
|
|
726
|
-
...(context.absolutePath === undefined
|
|
727
|
-
? {}
|
|
728
|
-
: {
|
|
729
|
-
rewrite: {
|
|
730
|
-
absolutePath: context.absolutePath,
|
|
731
|
-
classId: cls.id,
|
|
732
|
-
nextSource: result.nextSource,
|
|
733
|
-
path,
|
|
734
|
-
},
|
|
735
|
-
}),
|
|
736
|
-
};
|
|
872
|
+
currentSource = result.nextSource;
|
|
873
|
+
rewriteClassIds.push(cls.id);
|
|
874
|
+
rewriteNotes.push(...(result.notes ?? []));
|
|
875
|
+
continue;
|
|
737
876
|
}
|
|
738
877
|
if (result.kind === 'needs-review') {
|
|
739
|
-
const
|
|
878
|
+
const originalSourceResult =
|
|
879
|
+
currentSource === source ? result : cls.apply(source, context);
|
|
880
|
+
const reviewResult =
|
|
881
|
+
originalSourceResult.kind === 'needs-review'
|
|
882
|
+
? originalSourceResult
|
|
883
|
+
: result;
|
|
884
|
+
const reviewDetails = reviewResult.reviewDetails?.map((detail) => ({
|
|
740
885
|
...detail,
|
|
741
886
|
classId: detail.classId ?? cls.id,
|
|
742
887
|
}));
|
|
743
888
|
return {
|
|
744
889
|
entry: {
|
|
745
890
|
classId: cls.id,
|
|
746
|
-
notes:
|
|
891
|
+
notes: reviewResult.notes,
|
|
747
892
|
outcome: 'needs-review',
|
|
748
893
|
path,
|
|
749
|
-
reason:
|
|
894
|
+
reason: reviewResult.reason ?? 'needs-review',
|
|
750
895
|
...(reviewDetails === undefined ? {} : { reviewDetails }),
|
|
751
896
|
},
|
|
752
897
|
};
|
|
@@ -755,6 +900,28 @@ const classifyFile = (
|
|
|
755
900
|
skipped = { classId: cls.id, result };
|
|
756
901
|
}
|
|
757
902
|
}
|
|
903
|
+
if (rewriteClassIds.length > 0) {
|
|
904
|
+
const classId = rewriteClassIds.join(',');
|
|
905
|
+
const entry = {
|
|
906
|
+
classId,
|
|
907
|
+
notes: rewriteNotes,
|
|
908
|
+
outcome: 'rewrite',
|
|
909
|
+
path,
|
|
910
|
+
} satisfies RegradeReportEntry;
|
|
911
|
+
return {
|
|
912
|
+
entry,
|
|
913
|
+
...(context.absolutePath === undefined
|
|
914
|
+
? {}
|
|
915
|
+
: {
|
|
916
|
+
rewrite: {
|
|
917
|
+
absolutePath: context.absolutePath,
|
|
918
|
+
classId,
|
|
919
|
+
nextSource: currentSource,
|
|
920
|
+
path,
|
|
921
|
+
},
|
|
922
|
+
}),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
758
925
|
if (!inspected && skipped !== undefined) {
|
|
759
926
|
return {
|
|
760
927
|
entry: {
|
|
@@ -810,6 +977,7 @@ const buildRegradeEvaluation = (params: {
|
|
|
810
977
|
readonly path: string;
|
|
811
978
|
readonly source: string;
|
|
812
979
|
readonly absolutePath?: string;
|
|
980
|
+
readonly package?: RegradeClassContext['package'];
|
|
813
981
|
}[];
|
|
814
982
|
readonly skipped: readonly SkippedSource[];
|
|
815
983
|
readonly classes: readonly RegradeClass[];
|
|
@@ -831,6 +999,7 @@ const buildRegradeEvaluation = (params: {
|
|
|
831
999
|
...(file.absolutePath === undefined
|
|
832
1000
|
? {}
|
|
833
1001
|
: { absolutePath: file.absolutePath }),
|
|
1002
|
+
...(file.package === undefined ? {} : { package: file.package }),
|
|
834
1003
|
path: file.path,
|
|
835
1004
|
},
|
|
836
1005
|
selected,
|
|
@@ -899,6 +1068,7 @@ export const buildRegradeReport = (params: {
|
|
|
899
1068
|
readonly path: string;
|
|
900
1069
|
readonly source: string;
|
|
901
1070
|
readonly absolutePath?: string;
|
|
1071
|
+
readonly package?: RegradeClassContext['package'];
|
|
902
1072
|
}[];
|
|
903
1073
|
readonly skipped: readonly SkippedSource[];
|
|
904
1074
|
readonly classes: readonly RegradeClass[];
|
|
@@ -971,6 +1141,121 @@ const canReadDownstreamRoot = (root: string): boolean => {
|
|
|
971
1141
|
}
|
|
972
1142
|
};
|
|
973
1143
|
|
|
1144
|
+
const PACKAGE_DEPENDENCY_FIELDS = [
|
|
1145
|
+
'dependencies',
|
|
1146
|
+
'devDependencies',
|
|
1147
|
+
'optionalDependencies',
|
|
1148
|
+
'peerDependencies',
|
|
1149
|
+
] as const;
|
|
1150
|
+
|
|
1151
|
+
const RUNTIME_PACKAGE_DEPENDENCY_FIELDS = [
|
|
1152
|
+
'dependencies',
|
|
1153
|
+
'optionalDependencies',
|
|
1154
|
+
'peerDependencies',
|
|
1155
|
+
] as const;
|
|
1156
|
+
|
|
1157
|
+
const dependencyNames = (
|
|
1158
|
+
manifest: Readonly<Record<string, unknown>>,
|
|
1159
|
+
fields: readonly (typeof PACKAGE_DEPENDENCY_FIELDS)[number][]
|
|
1160
|
+
): readonly string[] =>
|
|
1161
|
+
fields.flatMap((field) => {
|
|
1162
|
+
const value = manifest[field];
|
|
1163
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1164
|
+
? Object.keys(value)
|
|
1165
|
+
: [];
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const packageContextFor = (
|
|
1169
|
+
root: string,
|
|
1170
|
+
absolutePath: string,
|
|
1171
|
+
cache: Map<string, RegradeClassContext['package'] | undefined>
|
|
1172
|
+
): RegradeClassContext['package'] | undefined => {
|
|
1173
|
+
const absoluteRoot = resolve(root);
|
|
1174
|
+
let current = dirname(absolutePath);
|
|
1175
|
+
const visited: string[] = [];
|
|
1176
|
+
while (true) {
|
|
1177
|
+
if (cache.has(current)) {
|
|
1178
|
+
const cached = cache.get(current);
|
|
1179
|
+
for (const directory of visited) {
|
|
1180
|
+
cache.set(directory, cached);
|
|
1181
|
+
}
|
|
1182
|
+
return cached;
|
|
1183
|
+
}
|
|
1184
|
+
visited.push(current);
|
|
1185
|
+
const fromRoot = relative(absoluteRoot, current);
|
|
1186
|
+
if (fromRoot.startsWith('..') || resolve(current) !== current) {
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
const manifestPath = join(current, 'package.json');
|
|
1190
|
+
if (existsSync(manifestPath)) {
|
|
1191
|
+
const manifestRelativePath = relative(
|
|
1192
|
+
absoluteRoot,
|
|
1193
|
+
manifestPath
|
|
1194
|
+
).replaceAll('\\', '/');
|
|
1195
|
+
try {
|
|
1196
|
+
const parsed = JSON.parse(
|
|
1197
|
+
readFileSync(manifestPath, 'utf8')
|
|
1198
|
+
) as unknown;
|
|
1199
|
+
if (
|
|
1200
|
+
typeof parsed !== 'object' ||
|
|
1201
|
+
parsed === null ||
|
|
1202
|
+
Array.isArray(parsed)
|
|
1203
|
+
) {
|
|
1204
|
+
const invalidContext = {
|
|
1205
|
+
dependencies: [],
|
|
1206
|
+
manifestState: 'invalid' as const,
|
|
1207
|
+
path: manifestRelativePath,
|
|
1208
|
+
};
|
|
1209
|
+
for (const directory of visited) {
|
|
1210
|
+
cache.set(directory, invalidContext);
|
|
1211
|
+
}
|
|
1212
|
+
return invalidContext;
|
|
1213
|
+
}
|
|
1214
|
+
const manifest = parsed as Record<string, unknown>;
|
|
1215
|
+
const dependencies = dependencyNames(
|
|
1216
|
+
manifest,
|
|
1217
|
+
PACKAGE_DEPENDENCY_FIELDS
|
|
1218
|
+
);
|
|
1219
|
+
const runtimeDependencies = dependencyNames(
|
|
1220
|
+
manifest,
|
|
1221
|
+
RUNTIME_PACKAGE_DEPENDENCY_FIELDS
|
|
1222
|
+
);
|
|
1223
|
+
const packageContext = {
|
|
1224
|
+
dependencies: [...new Set(dependencies)].toSorted(),
|
|
1225
|
+
manifestState: 'valid' as const,
|
|
1226
|
+
...(typeof manifest['name'] === 'string'
|
|
1227
|
+
? { name: manifest['name'] }
|
|
1228
|
+
: {}),
|
|
1229
|
+
path: manifestRelativePath,
|
|
1230
|
+
runtimeDependencies: [...new Set(runtimeDependencies)].toSorted(),
|
|
1231
|
+
};
|
|
1232
|
+
for (const directory of visited) {
|
|
1233
|
+
cache.set(directory, packageContext);
|
|
1234
|
+
}
|
|
1235
|
+
return packageContext;
|
|
1236
|
+
} catch {
|
|
1237
|
+
const invalidContext = {
|
|
1238
|
+
dependencies: [],
|
|
1239
|
+
manifestState: 'invalid' as const,
|
|
1240
|
+
path: manifestRelativePath,
|
|
1241
|
+
};
|
|
1242
|
+
for (const directory of visited) {
|
|
1243
|
+
cache.set(directory, invalidContext);
|
|
1244
|
+
}
|
|
1245
|
+
return invalidContext;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (current === absoluteRoot) {
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
current = dirname(current);
|
|
1252
|
+
}
|
|
1253
|
+
for (const directory of visited) {
|
|
1254
|
+
cache.set(directory, undefined);
|
|
1255
|
+
}
|
|
1256
|
+
return undefined;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
974
1259
|
const runRegradeEvaluation = (params: {
|
|
975
1260
|
readonly root: string;
|
|
976
1261
|
readonly classes: readonly RegradeClass[];
|
|
@@ -1013,10 +1298,20 @@ const runRegradeEvaluation = (params: {
|
|
|
1013
1298
|
|
|
1014
1299
|
const files: { absolutePath: string; path: string; source: string }[] = [];
|
|
1015
1300
|
const skipped: SkippedSource[] = [...collected.skipped];
|
|
1301
|
+
const packageContextCache = new Map<
|
|
1302
|
+
string,
|
|
1303
|
+
RegradeClassContext['package'] | undefined
|
|
1304
|
+
>();
|
|
1016
1305
|
for (const file of collected.files) {
|
|
1017
1306
|
try {
|
|
1307
|
+
const packageContext = packageContextFor(
|
|
1308
|
+
params.root,
|
|
1309
|
+
file.absolutePath,
|
|
1310
|
+
packageContextCache
|
|
1311
|
+
);
|
|
1018
1312
|
files.push({
|
|
1019
1313
|
absolutePath: file.absolutePath,
|
|
1314
|
+
...(packageContext === undefined ? {} : { package: packageContext }),
|
|
1020
1315
|
path: file.path,
|
|
1021
1316
|
source: readFileSync(file.absolutePath, 'utf8'),
|
|
1022
1317
|
});
|
|
@@ -1083,6 +1378,12 @@ const regradeReportEntrySchema = z.object({
|
|
|
1083
1378
|
reviewDetails: z
|
|
1084
1379
|
.array(
|
|
1085
1380
|
z.object({
|
|
1381
|
+
candidateReplacement: z
|
|
1382
|
+
.string()
|
|
1383
|
+
.optional()
|
|
1384
|
+
.describe(
|
|
1385
|
+
'Concrete replacement the class would apply if the occurrence were judged safe'
|
|
1386
|
+
),
|
|
1086
1387
|
classId: z
|
|
1087
1388
|
.string()
|
|
1088
1389
|
.optional()
|
|
@@ -1095,11 +1396,35 @@ const regradeReportEntrySchema = z.object({
|
|
|
1095
1396
|
.string()
|
|
1096
1397
|
.optional()
|
|
1097
1398
|
.describe('Fixture or example reference for the migration'),
|
|
1399
|
+
judgment: z
|
|
1400
|
+
.enum(['preserve', 'rewrite', 'unresolved'])
|
|
1401
|
+
.optional()
|
|
1402
|
+
.describe(
|
|
1403
|
+
'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'
|
|
1404
|
+
),
|
|
1405
|
+
matchedForm: z
|
|
1406
|
+
.string()
|
|
1407
|
+
.optional()
|
|
1408
|
+
.describe(
|
|
1409
|
+
'Exact matched source text for the occurrence under review'
|
|
1410
|
+
),
|
|
1098
1411
|
nodeKind: z
|
|
1099
1412
|
.string()
|
|
1100
1413
|
.optional()
|
|
1101
1414
|
.describe('AST node kind or source construct kind'),
|
|
1415
|
+
preserveCautions: z
|
|
1416
|
+
.array(z.string())
|
|
1417
|
+
.optional()
|
|
1418
|
+
.describe(
|
|
1419
|
+
'Cautions explaining why a blind rewrite of this occurrence is unsafe'
|
|
1420
|
+
),
|
|
1102
1421
|
reason: z.string().describe('Machine-readable review reason'),
|
|
1422
|
+
signals: z
|
|
1423
|
+
.array(z.string())
|
|
1424
|
+
.optional()
|
|
1425
|
+
.describe(
|
|
1426
|
+
'Machine-readable provenance tags for the producing rule or class'
|
|
1427
|
+
),
|
|
1103
1428
|
span: z
|
|
1104
1429
|
.object({
|
|
1105
1430
|
column: z.number().describe('One-based source column'),
|
|
@@ -1137,7 +1462,43 @@ export const regradeReportOutput = z.object({
|
|
|
1137
1462
|
.describe(
|
|
1138
1463
|
'Per-entry detail, sorted by path. Defaults to actionable rewrite/review entries.'
|
|
1139
1464
|
),
|
|
1465
|
+
history: z
|
|
1466
|
+
.object({
|
|
1467
|
+
path: z.string().describe('Root-relative applied history entry path'),
|
|
1468
|
+
schemaVersion: z.number().describe('Regrade history schema version'),
|
|
1469
|
+
status: z
|
|
1470
|
+
.enum(['applied', 'checked', 'replay'])
|
|
1471
|
+
.describe(
|
|
1472
|
+
'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'
|
|
1473
|
+
),
|
|
1474
|
+
})
|
|
1475
|
+
.optional()
|
|
1476
|
+
.describe('Saved applied Regrade history evidence'),
|
|
1140
1477
|
matched: z.number().describe('Files with a rewrite or review outcome'),
|
|
1478
|
+
plan: z
|
|
1479
|
+
.object({
|
|
1480
|
+
expansionPending: z
|
|
1481
|
+
.number()
|
|
1482
|
+
.optional()
|
|
1483
|
+
.describe('Pending staged expansion candidates on this plan'),
|
|
1484
|
+
path: z.string().describe('Root-relative Regrade plan path'),
|
|
1485
|
+
schemaVersion: z.number().describe('Regrade plan schema version'),
|
|
1486
|
+
status: z
|
|
1487
|
+
.enum(['active', 'stale'])
|
|
1488
|
+
.describe('Whether the saved plan still matches the source tree'),
|
|
1489
|
+
})
|
|
1490
|
+
.optional()
|
|
1491
|
+
.describe('Saved active Regrade plan evidence'),
|
|
1492
|
+
record: z
|
|
1493
|
+
.object({
|
|
1494
|
+
path: z.string().describe('Root-relative transition record path'),
|
|
1495
|
+
schemaVersion: z.number().describe('Transition record schema version'),
|
|
1496
|
+
status: z
|
|
1497
|
+
.enum(['candidate', 'applied', 'checked'])
|
|
1498
|
+
.describe('How this command used the transition record'),
|
|
1499
|
+
})
|
|
1500
|
+
.optional()
|
|
1501
|
+
.describe('Persisted transition record evidence'),
|
|
1141
1502
|
review: z.number().describe('Files routed to review'),
|
|
1142
1503
|
rewritten: z.number().describe('Files with a rewrite outcome'),
|
|
1143
1504
|
root: z.string().describe('Root the run scanned'),
|