@ontrails/regrade 1.0.0-beta.32 → 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 +54 -0
- package/package.json +5 -5
- package/src/downstream/ast-rewrite.ts +200 -5
- package/src/downstream/collect.ts +16 -1
- package/src/downstream/export-restructure.ts +1588 -0
- package/src/downstream/report.ts +269 -47
- package/src/downstream/vocabulary-registry.ts +98 -0
- package/src/downstream/vocabulary.ts +1070 -102
- package/src/index.ts +27 -0
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 {
|
|
@@ -143,18 +144,39 @@ export interface RegradeReviewSpan {
|
|
|
143
144
|
readonly start: number;
|
|
144
145
|
}
|
|
145
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
|
+
|
|
146
158
|
/** Structured detail explaining why a source match needs review. */
|
|
147
159
|
export interface RegradeReviewDetail {
|
|
160
|
+
/** Concrete replacement the class would apply if the occurrence were judged safe. */
|
|
161
|
+
readonly candidateReplacement?: string;
|
|
148
162
|
/** Class that produced the review detail, injected by report building. */
|
|
149
163
|
readonly classId?: string;
|
|
150
164
|
/** Expected target shape when the class can describe one. */
|
|
151
165
|
readonly expectedTarget?: string;
|
|
152
166
|
/** Fixture or example reference that illustrates the expected migration. */
|
|
153
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;
|
|
154
172
|
/** AST node kind or source construct kind. */
|
|
155
173
|
readonly nodeKind?: string;
|
|
174
|
+
/** Cautions explaining why a blind rewrite of this occurrence is unsafe. */
|
|
175
|
+
readonly preserveCautions?: readonly string[];
|
|
156
176
|
/** Machine-readable reason for review. */
|
|
157
177
|
readonly reason: string;
|
|
178
|
+
/** Machine-readable provenance tags for the producing rule or class. */
|
|
179
|
+
readonly signals?: readonly string[];
|
|
158
180
|
/** Source span and line/column for the review-required match. */
|
|
159
181
|
readonly span?: RegradeReviewSpan;
|
|
160
182
|
/** Suggested validation command after the review is resolved. */
|
|
@@ -317,7 +339,9 @@ const diagnosticSpan = (
|
|
|
317
339
|
return spanForSymbolOnDiagnosticLine(source, diagnostic.line, symbol);
|
|
318
340
|
};
|
|
319
341
|
|
|
320
|
-
const
|
|
342
|
+
const diagnosticCandidateReplacement = (
|
|
343
|
+
diagnostic: WardenDiagnostic
|
|
344
|
+
): string | undefined => {
|
|
321
345
|
const replacements = new Set(
|
|
322
346
|
(diagnostic.fix?.edits ?? []).map((edit) => edit.replacement)
|
|
323
347
|
);
|
|
@@ -325,32 +349,98 @@ const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
|
325
349
|
return undefined;
|
|
326
350
|
}
|
|
327
351
|
const [replacement] = replacements;
|
|
352
|
+
return replacement;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const expectedTarget = (diagnostic: WardenDiagnostic): string | undefined => {
|
|
356
|
+
const replacement = diagnosticCandidateReplacement(diagnostic);
|
|
328
357
|
return replacement === undefined
|
|
329
358
|
? undefined
|
|
330
359
|
: `Replace with "${replacement}".`;
|
|
331
360
|
};
|
|
332
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
|
+
|
|
333
436
|
const reviewDetailsFromDiagnostics = (
|
|
334
437
|
source: string,
|
|
335
438
|
diagnostics: readonly WardenDiagnostic[],
|
|
336
|
-
|
|
439
|
+
options: WardenReviewMappingOptions
|
|
337
440
|
): 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
|
-
});
|
|
441
|
+
const details = diagnostics.map((diagnostic) =>
|
|
442
|
+
reviewDetailFromDiagnostic(source, diagnostic, options)
|
|
443
|
+
);
|
|
354
444
|
return details.length === 0 ? undefined : details;
|
|
355
445
|
};
|
|
356
446
|
|
|
@@ -431,10 +521,18 @@ export const createWardenTermRewriteClass = (
|
|
|
431
521
|
(diagnostic) => diagnostic.fix?.safety !== 'safe'
|
|
432
522
|
);
|
|
433
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.
|
|
434
526
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
435
527
|
source,
|
|
436
528
|
reviewDiagnostics,
|
|
437
|
-
|
|
529
|
+
{
|
|
530
|
+
judgment: 'unresolved',
|
|
531
|
+
reason: 'warden-review-required',
|
|
532
|
+
...(metadata.guidance === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { ruleGuidance: metadata.guidance }),
|
|
535
|
+
}
|
|
438
536
|
);
|
|
439
537
|
return {
|
|
440
538
|
kind: 'needs-review',
|
|
@@ -448,10 +546,18 @@ export const createWardenTermRewriteClass = (
|
|
|
448
546
|
(diagnostic) => (diagnostic.fix?.edits?.length ?? 0) === 0
|
|
449
547
|
);
|
|
450
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.
|
|
451
551
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
452
552
|
source,
|
|
453
553
|
diagnosticsMissingEdits,
|
|
454
|
-
|
|
554
|
+
{
|
|
555
|
+
judgment: 'unresolved',
|
|
556
|
+
reason: 'warden-fix-missing-edits',
|
|
557
|
+
...(metadata.guidance === undefined
|
|
558
|
+
? {}
|
|
559
|
+
: { ruleGuidance: metadata.guidance }),
|
|
560
|
+
}
|
|
455
561
|
);
|
|
456
562
|
return {
|
|
457
563
|
kind: 'needs-review',
|
|
@@ -466,10 +572,18 @@ export const createWardenTermRewriteClass = (
|
|
|
466
572
|
);
|
|
467
573
|
const application = applyWardenEdits(source, edits);
|
|
468
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.
|
|
469
577
|
const reviewDetails = reviewDetailsFromDiagnostics(
|
|
470
578
|
source,
|
|
471
579
|
diagnostics,
|
|
472
|
-
|
|
580
|
+
{
|
|
581
|
+
judgment: 'rewrite',
|
|
582
|
+
reason: 'warden-fix-invalid',
|
|
583
|
+
...(metadata.guidance === undefined
|
|
584
|
+
? {}
|
|
585
|
+
: { ruleGuidance: metadata.guidance }),
|
|
586
|
+
}
|
|
473
587
|
);
|
|
474
588
|
return {
|
|
475
589
|
kind: 'needs-review',
|
|
@@ -567,6 +681,9 @@ const deriveCollectionOptions = (
|
|
|
567
681
|
...(collection?.exclude === undefined
|
|
568
682
|
? {}
|
|
569
683
|
: { exclude: collection.exclude }),
|
|
684
|
+
...(collection?.include === undefined
|
|
685
|
+
? {}
|
|
686
|
+
: { include: collection.include }),
|
|
570
687
|
extensions: collection?.extensions ?? targetExtensions,
|
|
571
688
|
ignoredDirectories:
|
|
572
689
|
collection?.ignoredDirectories ??
|
|
@@ -619,6 +736,28 @@ export interface RegradeReport {
|
|
|
619
736
|
readonly apply?: RegradeApplySummary;
|
|
620
737
|
/** Vocabulary regrade run: plan, ledger, and completion report. */
|
|
621
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
|
+
};
|
|
622
761
|
}
|
|
623
762
|
|
|
624
763
|
interface RegradeRewriteCandidate {
|
|
@@ -690,16 +829,20 @@ const classifyFile = (
|
|
|
690
829
|
selected: readonly RegradeClass[],
|
|
691
830
|
collection?: DownstreamCollectionOptions
|
|
692
831
|
): RegradeClassifiedFile => {
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
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.
|
|
696
835
|
let skipped:
|
|
697
836
|
| { readonly classId: string; readonly result: RegradeClassResult }
|
|
698
837
|
| undefined;
|
|
699
838
|
let inspected = false;
|
|
839
|
+
let currentSource = source;
|
|
840
|
+
const rewriteClassIds: string[] = [];
|
|
841
|
+
const rewriteNotes: string[] = [];
|
|
700
842
|
for (const cls of selected) {
|
|
701
843
|
const result =
|
|
702
|
-
classScanTargetSkip(cls, path, collection) ??
|
|
844
|
+
classScanTargetSkip(cls, path, collection) ??
|
|
845
|
+
cls.apply(currentSource, context);
|
|
703
846
|
if (result.kind !== 'skipped') {
|
|
704
847
|
inspected = true;
|
|
705
848
|
}
|
|
@@ -715,38 +858,29 @@ const classifyFile = (
|
|
|
715
858
|
},
|
|
716
859
|
};
|
|
717
860
|
}
|
|
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
|
-
};
|
|
861
|
+
currentSource = result.nextSource;
|
|
862
|
+
rewriteClassIds.push(cls.id);
|
|
863
|
+
rewriteNotes.push(...(result.notes ?? []));
|
|
864
|
+
continue;
|
|
737
865
|
}
|
|
738
866
|
if (result.kind === 'needs-review') {
|
|
739
|
-
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) => ({
|
|
740
874
|
...detail,
|
|
741
875
|
classId: detail.classId ?? cls.id,
|
|
742
876
|
}));
|
|
743
877
|
return {
|
|
744
878
|
entry: {
|
|
745
879
|
classId: cls.id,
|
|
746
|
-
notes:
|
|
880
|
+
notes: reviewResult.notes,
|
|
747
881
|
outcome: 'needs-review',
|
|
748
882
|
path,
|
|
749
|
-
reason:
|
|
883
|
+
reason: reviewResult.reason ?? 'needs-review',
|
|
750
884
|
...(reviewDetails === undefined ? {} : { reviewDetails }),
|
|
751
885
|
},
|
|
752
886
|
};
|
|
@@ -755,6 +889,28 @@ const classifyFile = (
|
|
|
755
889
|
skipped = { classId: cls.id, result };
|
|
756
890
|
}
|
|
757
891
|
}
|
|
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
|
+
}
|
|
758
914
|
if (!inspected && skipped !== undefined) {
|
|
759
915
|
return {
|
|
760
916
|
entry: {
|
|
@@ -1083,6 +1239,12 @@ const regradeReportEntrySchema = z.object({
|
|
|
1083
1239
|
reviewDetails: z
|
|
1084
1240
|
.array(
|
|
1085
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
|
+
),
|
|
1086
1248
|
classId: z
|
|
1087
1249
|
.string()
|
|
1088
1250
|
.optional()
|
|
@@ -1095,11 +1257,35 @@ const regradeReportEntrySchema = z.object({
|
|
|
1095
1257
|
.string()
|
|
1096
1258
|
.optional()
|
|
1097
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
|
+
),
|
|
1098
1272
|
nodeKind: z
|
|
1099
1273
|
.string()
|
|
1100
1274
|
.optional()
|
|
1101
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
|
+
),
|
|
1102
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
|
+
),
|
|
1103
1289
|
span: z
|
|
1104
1290
|
.object({
|
|
1105
1291
|
column: z.number().describe('One-based source column'),
|
|
@@ -1137,7 +1323,43 @@ export const regradeReportOutput = z.object({
|
|
|
1137
1323
|
.describe(
|
|
1138
1324
|
'Per-entry detail, sorted by path. Defaults to actionable rewrite/review entries.'
|
|
1139
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'),
|
|
1140
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'),
|
|
1141
1363
|
review: z.number().describe('Files routed to review'),
|
|
1142
1364
|
rewritten: z.number().describe('Files with a rewrite outcome'),
|
|
1143
1365
|
root: z.string().describe('Root the run scanned'),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { GovernedVocabularyTransition } from '@ontrails/warden';
|
|
2
|
+
import { listGovernedVocabularyTransitions } from '@ontrails/warden';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
VocabularyPreserveRule,
|
|
6
|
+
VocabularyRegradePlan,
|
|
7
|
+
} from './vocabulary.js';
|
|
8
|
+
|
|
9
|
+
const pluralizeVocabularyForm = (value: string): string =>
|
|
10
|
+
value.endsWith('s') || value.endsWith('x') || value.endsWith('ch')
|
|
11
|
+
? `${value}es`
|
|
12
|
+
: `${value}s`;
|
|
13
|
+
|
|
14
|
+
const preserveRulesFromTransition = (
|
|
15
|
+
transition: GovernedVocabularyTransition
|
|
16
|
+
): readonly VocabularyPreserveRule[] =>
|
|
17
|
+
transition.preserve.map((rule) => ({
|
|
18
|
+
...(rule.paths === undefined ? {} : { paths: rule.paths }),
|
|
19
|
+
pattern: rule.pattern,
|
|
20
|
+
reason: rule.reason,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const scopeFromTransition = (
|
|
24
|
+
transition: GovernedVocabularyTransition
|
|
25
|
+
): VocabularyRegradePlan['scope'] | undefined => {
|
|
26
|
+
const { scope } = transition;
|
|
27
|
+
if (scope === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...(scope.exclude === undefined ? {} : { exclude: scope.exclude }),
|
|
32
|
+
...(scope.extensions === undefined ? {} : { extensions: scope.extensions }),
|
|
33
|
+
...(scope.ignoredDirectories === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { ignoredDirectories: scope.ignoredDirectories }),
|
|
36
|
+
...(scope.include === undefined ? {} : { include: scope.include }),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const defaultFormsAreRegistrySafe = (
|
|
41
|
+
transition: GovernedVocabularyTransition
|
|
42
|
+
): boolean => {
|
|
43
|
+
if (transition.target.kind !== 'single') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const defaultForms = [
|
|
48
|
+
transition.from,
|
|
49
|
+
pluralizeVocabularyForm(transition.from),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return defaultForms.every((form) => {
|
|
53
|
+
const replacement = transition.safeRewriteForms[form];
|
|
54
|
+
return replacement !== undefined && !transition.reviewForms.includes(form);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const vocabularyRegradePlanFromTransition = (
|
|
59
|
+
transition: GovernedVocabularyTransition
|
|
60
|
+
): VocabularyRegradePlan | null => {
|
|
61
|
+
if (
|
|
62
|
+
transition.target.kind !== 'single' ||
|
|
63
|
+
!defaultFormsAreRegistrySafe(transition)
|
|
64
|
+
) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const scope = scopeFromTransition(transition);
|
|
69
|
+
return {
|
|
70
|
+
caseSensitive: true,
|
|
71
|
+
deferForms: transition.reviewForms,
|
|
72
|
+
from: transition.from,
|
|
73
|
+
id: transition.id,
|
|
74
|
+
intent: transition.intent,
|
|
75
|
+
kind: 'vocabulary',
|
|
76
|
+
overrides: transition.safeRewriteForms,
|
|
77
|
+
preserve: preserveRulesFromTransition(transition),
|
|
78
|
+
...(scope === undefined ? {} : { scope }),
|
|
79
|
+
to: transition.target.to,
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const listVocabularyRegradePlansFromRegistry =
|
|
84
|
+
(): readonly VocabularyRegradePlan[] =>
|
|
85
|
+
listGovernedVocabularyTransitions()
|
|
86
|
+
.map(vocabularyRegradePlanFromTransition)
|
|
87
|
+
.filter((plan): plan is VocabularyRegradePlan => plan !== null);
|
|
88
|
+
|
|
89
|
+
export const vocabularyRegradeTransitionForInput = (
|
|
90
|
+
from: string,
|
|
91
|
+
to: string
|
|
92
|
+
): GovernedVocabularyTransition | undefined =>
|
|
93
|
+
listGovernedVocabularyTransitions().find(
|
|
94
|
+
(transition) =>
|
|
95
|
+
transition.from === from &&
|
|
96
|
+
transition.target.kind === 'single' &&
|
|
97
|
+
transition.target.to === to
|
|
98
|
+
);
|