@ontrails/regrade 1.0.0-beta.45 → 1.0.0-beta.47
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 +25 -0
- package/package.json +6 -6
- package/src/downstream/collect.ts +17 -96
- package/src/downstream/export-restructure.ts +2 -2
- package/src/downstream/file-renames.ts +115 -28
- package/src/downstream/report.ts +522 -51
- package/src/downstream/vocabulary.ts +570 -77
- package/src/history-receipt.ts +828 -0
- package/src/index.ts +47 -0
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
isPlainObject,
|
|
7
7
|
matchesAnyPathGlob,
|
|
8
8
|
} from '@ontrails/core';
|
|
9
|
+
import { parseWithDiagnostics } from '@ontrails/source';
|
|
10
|
+
import type { SourceComment } from '@ontrails/source';
|
|
9
11
|
import { createHash } from 'node:crypto';
|
|
10
12
|
import {
|
|
11
13
|
dirname,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
} from './collect.js';
|
|
25
27
|
import type { DownstreamCollectionOptions, SkippedSource } from './collect.js';
|
|
26
28
|
import type {
|
|
29
|
+
PreparedRegradeRunIdentity,
|
|
27
30
|
RegradeApplySummary,
|
|
28
31
|
RegradeReport,
|
|
29
32
|
RegradeReportEntry,
|
|
@@ -32,6 +35,8 @@ import { buildRegradeScanSummary } from './scan-summary.js';
|
|
|
32
35
|
|
|
33
36
|
export type VocabularyVerdict = 'applied' | 'deferred' | 'modified' | 'skipped';
|
|
34
37
|
|
|
38
|
+
export type VocabularyOccurrenceSourceKind = 'source-comment' | 'tsdoc';
|
|
39
|
+
|
|
35
40
|
export const vocabularyDispositionValues = [
|
|
36
41
|
'code-context-out-of-engine',
|
|
37
42
|
'docs-only',
|
|
@@ -170,6 +175,7 @@ export interface VocabularyOccurrence {
|
|
|
170
175
|
readonly replacement?: string;
|
|
171
176
|
readonly start: number;
|
|
172
177
|
readonly scopeTier: VocabularyScopeTier;
|
|
178
|
+
readonly sourceKind?: VocabularyOccurrenceSourceKind;
|
|
173
179
|
readonly verdict: VocabularyVerdict;
|
|
174
180
|
}
|
|
175
181
|
|
|
@@ -247,12 +253,23 @@ interface SourceFile {
|
|
|
247
253
|
readonly absolutePath: string;
|
|
248
254
|
readonly path: string;
|
|
249
255
|
readonly source: string;
|
|
256
|
+
readonly sourceBytes: string;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
interface SourceOccurrence extends VocabularyOccurrence {
|
|
253
260
|
readonly absolutePath: string;
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
type VocabularySourceKind = 'all' | 'comments';
|
|
264
|
+
|
|
265
|
+
const sourceCommentKind = (
|
|
266
|
+
file: SourceFile,
|
|
267
|
+
comment: SourceComment
|
|
268
|
+
): VocabularyOccurrenceSourceKind =>
|
|
269
|
+
comment.type === 'Block' && file.source.startsWith('/**', comment.start)
|
|
270
|
+
? 'tsdoc'
|
|
271
|
+
: 'source-comment';
|
|
272
|
+
|
|
256
273
|
interface SourceOccurrenceDraft extends Omit<
|
|
257
274
|
SourceOccurrence,
|
|
258
275
|
'disposition' | 'reason' | 'scopeTier' | 'verdict'
|
|
@@ -450,7 +467,16 @@ const lineColumnForOffset = (
|
|
|
450
467
|
let line = 1;
|
|
451
468
|
let column = 1;
|
|
452
469
|
for (let index = 0; index < offset; index += 1) {
|
|
453
|
-
|
|
470
|
+
const codePoint = source.codePointAt(index);
|
|
471
|
+
if (
|
|
472
|
+
codePoint === 10 ||
|
|
473
|
+
codePoint === 13 ||
|
|
474
|
+
codePoint === 0x20_28 ||
|
|
475
|
+
codePoint === 0x20_29
|
|
476
|
+
) {
|
|
477
|
+
if (codePoint === 13 && source.codePointAt(index + 1) === 10) {
|
|
478
|
+
index += 1;
|
|
479
|
+
}
|
|
454
480
|
line += 1;
|
|
455
481
|
column = 1;
|
|
456
482
|
} else {
|
|
@@ -465,9 +491,32 @@ const contextDetailsForOffset = (
|
|
|
465
491
|
start: number,
|
|
466
492
|
end: number
|
|
467
493
|
): { readonly context: string; readonly contextColumn: number } => {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
494
|
+
let lineStart = start;
|
|
495
|
+
while (lineStart > 0) {
|
|
496
|
+
const codePoint = source.codePointAt(lineStart - 1);
|
|
497
|
+
if (
|
|
498
|
+
codePoint === 10 ||
|
|
499
|
+
codePoint === 13 ||
|
|
500
|
+
codePoint === 0x20_28 ||
|
|
501
|
+
codePoint === 0x20_29
|
|
502
|
+
) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
lineStart -= 1;
|
|
506
|
+
}
|
|
507
|
+
let lineEnd = end;
|
|
508
|
+
while (lineEnd < source.length) {
|
|
509
|
+
const codePoint = source.codePointAt(lineEnd);
|
|
510
|
+
if (
|
|
511
|
+
codePoint === 10 ||
|
|
512
|
+
codePoint === 13 ||
|
|
513
|
+
codePoint === 0x20_28 ||
|
|
514
|
+
codePoint === 0x20_29
|
|
515
|
+
) {
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
lineEnd += 1;
|
|
519
|
+
}
|
|
471
520
|
const rawLine = source.slice(lineStart, lineEnd);
|
|
472
521
|
const leadingTrimmed = rawLine.length - rawLine.trimStart().length;
|
|
473
522
|
return {
|
|
@@ -735,6 +784,21 @@ const deferFormsForPlan = (plan: VocabularyRegradePlan): readonly string[] => {
|
|
|
735
784
|
]);
|
|
736
785
|
};
|
|
737
786
|
|
|
787
|
+
const titleCaseVocabularyForm = (form: string): string => {
|
|
788
|
+
const first = form.at(0);
|
|
789
|
+
return first === undefined ? form : `${first.toUpperCase()}${form.slice(1)}`;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const commentReviewPlan = (
|
|
793
|
+
plan: VocabularyRegradePlan
|
|
794
|
+
): VocabularyRegradePlan => ({
|
|
795
|
+
...plan,
|
|
796
|
+
deferForms: uniqueSorted([
|
|
797
|
+
...(plan.deferForms ?? []),
|
|
798
|
+
...(plan.deferForms ?? []).map(titleCaseVocabularyForm),
|
|
799
|
+
]),
|
|
800
|
+
});
|
|
801
|
+
|
|
738
802
|
/**
|
|
739
803
|
* Synthesize the deterministic form proposal carried by a minimal plan seed.
|
|
740
804
|
*
|
|
@@ -1471,8 +1535,19 @@ const entryForOccurrences = (
|
|
|
1471
1535
|
reviewDetails: occurrences
|
|
1472
1536
|
.filter((occurrence) => occurrence.verdict === 'deferred')
|
|
1473
1537
|
.map((occurrence) => ({
|
|
1538
|
+
context: occurrence.context,
|
|
1474
1539
|
expectedTarget:
|
|
1475
1540
|
'Add an override or preserve rule to the regrade plan.',
|
|
1541
|
+
judgment: 'unresolved' as const,
|
|
1542
|
+
matchedForm: occurrence.form,
|
|
1543
|
+
...(occurrence.sourceKind === undefined
|
|
1544
|
+
? {}
|
|
1545
|
+
: {
|
|
1546
|
+
nodeKind:
|
|
1547
|
+
occurrence.sourceKind === 'tsdoc'
|
|
1548
|
+
? 'TSDocComment'
|
|
1549
|
+
: 'SourceComment',
|
|
1550
|
+
}),
|
|
1476
1551
|
reason: occurrence.reason,
|
|
1477
1552
|
span: {
|
|
1478
1553
|
column: occurrence.column,
|
|
@@ -1528,6 +1603,9 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1528
1603
|
readonly preserveInventory?: readonly VocabularyPreserveInventoryEntry[];
|
|
1529
1604
|
readonly root: string;
|
|
1530
1605
|
readonly skipped: readonly SkippedSource[];
|
|
1606
|
+
readonly sourceKindForPath?:
|
|
1607
|
+
| ((path: string) => VocabularySourceKind)
|
|
1608
|
+
| undefined;
|
|
1531
1609
|
}): VocabularyEvaluation => {
|
|
1532
1610
|
const effectivePlan = params.effectivePlan ?? params.plan;
|
|
1533
1611
|
const targetForms = targetFormsForPlan(effectivePlan);
|
|
@@ -1537,21 +1615,59 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1537
1615
|
const scopeSkipped: SkippedSource[] = params.files
|
|
1538
1616
|
.filter((file) => !includedByScope(file.path, effectivePlan.scope))
|
|
1539
1617
|
.map((file) => ({ path: file.path, reason: 'excluded-by-regrade-scope' }));
|
|
1618
|
+
const commentParseSkipped: SkippedSource[] = [];
|
|
1540
1619
|
const occurrences = scopedFiles.flatMap((file) => {
|
|
1620
|
+
const sourceKind = params.sourceKindForPath?.(file.path) ?? 'all';
|
|
1621
|
+
const scanPlan =
|
|
1622
|
+
sourceKind === 'comments'
|
|
1623
|
+
? commentReviewPlan(effectivePlan)
|
|
1624
|
+
: effectivePlan;
|
|
1541
1625
|
const deferredOccurrences = deferredOccurrencesForFile(
|
|
1542
1626
|
file,
|
|
1543
|
-
|
|
1627
|
+
scanPlan,
|
|
1544
1628
|
targetForms
|
|
1545
1629
|
);
|
|
1546
|
-
|
|
1547
|
-
...occurrencesForFile(
|
|
1548
|
-
file,
|
|
1549
|
-
effectivePlan,
|
|
1550
|
-
targetForms,
|
|
1551
|
-
deferredOccurrences
|
|
1552
|
-
),
|
|
1630
|
+
const fileOccurrences = [
|
|
1631
|
+
...occurrencesForFile(file, scanPlan, targetForms, deferredOccurrences),
|
|
1553
1632
|
...deferredOccurrences,
|
|
1554
1633
|
];
|
|
1634
|
+
if (sourceKind === 'all') {
|
|
1635
|
+
return fileOccurrences;
|
|
1636
|
+
}
|
|
1637
|
+
if (fileOccurrences.length === 0) {
|
|
1638
|
+
return [];
|
|
1639
|
+
}
|
|
1640
|
+
const parsed = parseWithDiagnostics(file.path, file.source);
|
|
1641
|
+
if (parsed.diagnostics.length > 0 || parsed.ast === null) {
|
|
1642
|
+
commentParseSkipped.push({
|
|
1643
|
+
path: file.path,
|
|
1644
|
+
reason: 'source-comment-parse-diagnostics',
|
|
1645
|
+
});
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
return fileOccurrences.flatMap((occurrence) => {
|
|
1649
|
+
const comment = parsed.comments.find(
|
|
1650
|
+
(candidate) =>
|
|
1651
|
+
candidate.start <= occurrence.start && occurrence.end <= candidate.end
|
|
1652
|
+
);
|
|
1653
|
+
if (comment === undefined) {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
const sourceKindForOccurrence = sourceCommentKind(file, comment);
|
|
1657
|
+
if (occurrence.verdict === 'skipped') {
|
|
1658
|
+
return [{ ...occurrence, sourceKind: sourceKindForOccurrence }];
|
|
1659
|
+
}
|
|
1660
|
+
const { replacement: _replacement, ...reviewOccurrence } = occurrence;
|
|
1661
|
+
return [
|
|
1662
|
+
{
|
|
1663
|
+
...reviewOccurrence,
|
|
1664
|
+
disposition: 'in-family-unresolved' as const,
|
|
1665
|
+
reason: 'source-comment-requires-review',
|
|
1666
|
+
sourceKind: sourceKindForOccurrence,
|
|
1667
|
+
verdict: 'deferred' as const,
|
|
1668
|
+
},
|
|
1669
|
+
];
|
|
1670
|
+
});
|
|
1555
1671
|
});
|
|
1556
1672
|
const occurrencesByPath = new Map<string, SourceOccurrence[]>();
|
|
1557
1673
|
for (const occurrence of occurrences) {
|
|
@@ -1599,6 +1715,9 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1599
1715
|
if (deferredForms.length > 0) {
|
|
1600
1716
|
gateReasons.push('deferred-forms-or-occurrences');
|
|
1601
1717
|
}
|
|
1718
|
+
if (commentParseSkipped.length > 0) {
|
|
1719
|
+
gateReasons.push('source-comment-parse-diagnostics');
|
|
1720
|
+
}
|
|
1602
1721
|
const open = unresolvedOccurrences.length;
|
|
1603
1722
|
const scopeEvidence = vocabularyScopeEvidence(effectivePlan, occurrences);
|
|
1604
1723
|
gateReasons.push(...scopeEvidence.gateReasons);
|
|
@@ -1616,6 +1735,11 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1616
1735
|
path: entry.path,
|
|
1617
1736
|
reason: entry.reason,
|
|
1618
1737
|
})),
|
|
1738
|
+
...commentParseSkipped.map((entry) => ({
|
|
1739
|
+
outcome: 'skip' as const,
|
|
1740
|
+
path: entry.path,
|
|
1741
|
+
reason: entry.reason,
|
|
1742
|
+
})),
|
|
1619
1743
|
].toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
1620
1744
|
occurrences,
|
|
1621
1745
|
run: {
|
|
@@ -1652,7 +1776,7 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1652
1776
|
},
|
|
1653
1777
|
},
|
|
1654
1778
|
scanned: scopedFiles.length,
|
|
1655
|
-
skipped: [...params.skipped, ...scopeSkipped],
|
|
1779
|
+
skipped: [...params.skipped, ...scopeSkipped, ...commentParseSkipped],
|
|
1656
1780
|
};
|
|
1657
1781
|
};
|
|
1658
1782
|
|
|
@@ -1670,13 +1794,37 @@ const skippedByReason = (
|
|
|
1670
1794
|
);
|
|
1671
1795
|
};
|
|
1672
1796
|
|
|
1797
|
+
const withScannedPaths = (
|
|
1798
|
+
report: RegradeReport,
|
|
1799
|
+
paths: readonly string[]
|
|
1800
|
+
): RegradeReport => {
|
|
1801
|
+
Object.defineProperty(report, 'scannedPaths', {
|
|
1802
|
+
configurable: false,
|
|
1803
|
+
enumerable: false,
|
|
1804
|
+
value: Object.freeze([...paths]),
|
|
1805
|
+
writable: false,
|
|
1806
|
+
});
|
|
1807
|
+
return report;
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const cloneVocabularyReport = (report: RegradeReport): RegradeReport => {
|
|
1811
|
+
const clone = structuredClone(report);
|
|
1812
|
+
return report.scannedPaths === undefined
|
|
1813
|
+
? clone
|
|
1814
|
+
: withScannedPaths(clone, report.scannedPaths);
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1673
1817
|
const withApplySummary = (
|
|
1674
1818
|
report: RegradeReport,
|
|
1675
1819
|
apply: RegradeApplySummary
|
|
1676
|
-
): RegradeReport =>
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1820
|
+
): RegradeReport =>
|
|
1821
|
+
withScannedPaths(
|
|
1822
|
+
{
|
|
1823
|
+
...report,
|
|
1824
|
+
apply,
|
|
1825
|
+
},
|
|
1826
|
+
report.scannedPaths ?? []
|
|
1827
|
+
);
|
|
1680
1828
|
|
|
1681
1829
|
const appliedVocabularyRunReport = (
|
|
1682
1830
|
postApply: VocabularyRegradeRun,
|
|
@@ -1802,10 +1950,12 @@ const readVocabularySourceFiles = (
|
|
|
1802
1950
|
const skipped: SkippedSource[] = [...collected.skipped];
|
|
1803
1951
|
for (const file of collected.files) {
|
|
1804
1952
|
try {
|
|
1953
|
+
const bytes = readFileSync(file.absolutePath);
|
|
1805
1954
|
files.push({
|
|
1806
1955
|
absolutePath: file.absolutePath,
|
|
1807
1956
|
path: file.path,
|
|
1808
|
-
source:
|
|
1957
|
+
source: bytes.toString('utf8'),
|
|
1958
|
+
sourceBytes: bytes.toString('base64'),
|
|
1809
1959
|
});
|
|
1810
1960
|
} catch {
|
|
1811
1961
|
skipped.push({ path: file.path, reason: 'unreadable-file' });
|
|
@@ -1824,6 +1974,9 @@ const buildRunVocabularyEvaluation = (params: {
|
|
|
1824
1974
|
| undefined;
|
|
1825
1975
|
readonly root: string;
|
|
1826
1976
|
readonly skipped: readonly SkippedSource[];
|
|
1977
|
+
readonly sourceKindForPath?:
|
|
1978
|
+
| ((path: string) => VocabularySourceKind)
|
|
1979
|
+
| undefined;
|
|
1827
1980
|
}): VocabularyEvaluation =>
|
|
1828
1981
|
buildVocabularyEvaluation({
|
|
1829
1982
|
apply: params.apply,
|
|
@@ -1835,6 +1988,7 @@ const buildRunVocabularyEvaluation = (params: {
|
|
|
1835
1988
|
: { preserveInventory: params.preserveInventory }),
|
|
1836
1989
|
root: params.root,
|
|
1837
1990
|
skipped: params.skipped,
|
|
1991
|
+
sourceKindForPath: params.sourceKindForPath,
|
|
1838
1992
|
});
|
|
1839
1993
|
|
|
1840
1994
|
const ignoredDirectoriesOpenedByPolicy = (
|
|
@@ -1893,30 +2047,48 @@ const filterVocabularyCollection = (
|
|
|
1893
2047
|
};
|
|
1894
2048
|
};
|
|
1895
2049
|
|
|
1896
|
-
|
|
1897
|
-
readonly
|
|
2050
|
+
interface VocabularyRunInputs {
|
|
2051
|
+
readonly collectedRoot: string;
|
|
2052
|
+
readonly effectivePlan: VocabularyRegradePlan;
|
|
2053
|
+
readonly files: readonly SourceFile[];
|
|
2054
|
+
readonly skipped: readonly SkippedSource[];
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
interface PrepareVocabularyRegradeRunParams {
|
|
1898
2058
|
readonly includeEntries?: 'actionable' | 'all';
|
|
1899
2059
|
readonly plan: VocabularyRegradePlan;
|
|
1900
2060
|
readonly preserveInventory?: readonly VocabularyPreserveInventoryEntry[];
|
|
1901
2061
|
readonly root: string;
|
|
1902
2062
|
readonly sourceFilter?: (path: string) => boolean;
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2063
|
+
readonly sourceKindForPath?: (path: string) => VocabularySourceKind;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
const snapshotPrepareVocabularyRegradeRunParams = (
|
|
2067
|
+
params: PrepareVocabularyRegradeRunParams
|
|
2068
|
+
): PrepareVocabularyRegradeRunParams => ({
|
|
2069
|
+
...(params.includeEntries === undefined
|
|
2070
|
+
? {}
|
|
2071
|
+
: { includeEntries: params.includeEntries }),
|
|
2072
|
+
plan: structuredClone(params.plan),
|
|
2073
|
+
...(params.preserveInventory === undefined
|
|
2074
|
+
? {}
|
|
2075
|
+
: { preserveInventory: structuredClone(params.preserveInventory) }),
|
|
2076
|
+
root: params.root,
|
|
2077
|
+
...(params.sourceFilter === undefined
|
|
2078
|
+
? {}
|
|
2079
|
+
: { sourceFilter: params.sourceFilter }),
|
|
2080
|
+
...(params.sourceKindForPath === undefined
|
|
2081
|
+
? {}
|
|
2082
|
+
: { sourceKindForPath: params.sourceKindForPath }),
|
|
2083
|
+
});
|
|
1914
2084
|
|
|
2085
|
+
const collectVocabularyRunInputs = (
|
|
2086
|
+
params: PrepareVocabularyRegradeRunParams
|
|
2087
|
+
): VocabularyRunInputs | null => {
|
|
1915
2088
|
const effectivePlan = effectivePlanForRun(
|
|
1916
2089
|
params.plan,
|
|
1917
2090
|
params.preserveInventory
|
|
1918
2091
|
);
|
|
1919
|
-
|
|
1920
2092
|
const collected = collectDownstreamSources(params.root, {
|
|
1921
2093
|
extensions: effectivePlan.scope?.extensions ?? VOCABULARY_SOURCE_EXTENSIONS,
|
|
1922
2094
|
...(effectivePlan.scope?.exclude === undefined
|
|
@@ -1928,20 +2100,365 @@ export const runVocabularyRegrade = (params: {
|
|
|
1928
2100
|
ignoredDirectories: vocabularyIgnoredDirectories(effectivePlan.scope),
|
|
1929
2101
|
} satisfies DownstreamCollectionOptions);
|
|
1930
2102
|
if (collected === null) {
|
|
1931
|
-
return
|
|
2103
|
+
return null;
|
|
1932
2104
|
}
|
|
1933
|
-
|
|
1934
2105
|
const filtered = filterVocabularyCollection(
|
|
1935
2106
|
collected.files,
|
|
1936
2107
|
effectivePlan.scope,
|
|
1937
2108
|
params.sourceFilter
|
|
1938
2109
|
);
|
|
1939
|
-
const { files, skipped
|
|
2110
|
+
const { files, skipped } = readVocabularySourceFiles({
|
|
1940
2111
|
...collected,
|
|
1941
2112
|
files: filtered.files,
|
|
1942
2113
|
skipped: [...collected.skipped, ...filtered.skipped],
|
|
1943
2114
|
});
|
|
1944
|
-
|
|
2115
|
+
return { collectedRoot: collected.root, effectivePlan, files, skipped };
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
const vocabularyReportFromEvaluation = (params: {
|
|
2119
|
+
readonly applySummary?: RegradeApplySummary;
|
|
2120
|
+
readonly collectedRoot: string;
|
|
2121
|
+
readonly dryRunEvaluation: VocabularyEvaluation;
|
|
2122
|
+
readonly effectivePlan: VocabularyRegradePlan;
|
|
2123
|
+
readonly entrySelection: 'actionable' | 'all';
|
|
2124
|
+
readonly files: readonly SourceFile[];
|
|
2125
|
+
readonly plan: VocabularyRegradePlan;
|
|
2126
|
+
readonly reportEvaluation: VocabularyEvaluation;
|
|
2127
|
+
readonly skipped: readonly SkippedSource[];
|
|
2128
|
+
}): RegradeReport => {
|
|
2129
|
+
const reportEntries = params.reportEvaluation.entries;
|
|
2130
|
+
const actionableEntries = reportEntries.filter(
|
|
2131
|
+
(entry) => entry.outcome === 'rewrite' || entry.outcome === 'needs-review'
|
|
2132
|
+
);
|
|
2133
|
+
const reportSkippedByReason = skippedByReason(
|
|
2134
|
+
params.reportEvaluation.skipped
|
|
2135
|
+
);
|
|
2136
|
+
const report: RegradeReport = withScannedPaths(
|
|
2137
|
+
{
|
|
2138
|
+
entries:
|
|
2139
|
+
params.entrySelection === 'all' ? reportEntries : actionableEntries,
|
|
2140
|
+
matched: actionableEntries.length,
|
|
2141
|
+
review: reportEntries.filter((entry) => entry.outcome === 'needs-review')
|
|
2142
|
+
.length,
|
|
2143
|
+
rewritten: reportEntries.filter((entry) => entry.outcome === 'rewrite')
|
|
2144
|
+
.length,
|
|
2145
|
+
root: params.collectedRoot,
|
|
2146
|
+
run:
|
|
2147
|
+
params.applySummary === undefined
|
|
2148
|
+
? params.reportEvaluation.run
|
|
2149
|
+
: vocabularyRunWithAppliedOccurrences(
|
|
2150
|
+
params.reportEvaluation.run,
|
|
2151
|
+
params.dryRunEvaluation.run,
|
|
2152
|
+
params.applySummary
|
|
2153
|
+
),
|
|
2154
|
+
scan: buildRegradeScanSummary({
|
|
2155
|
+
matchedPaths: actionableEntries.map((entry) => entry.path),
|
|
2156
|
+
occurrencePaths: params.reportEvaluation.occurrences.map(
|
|
2157
|
+
(occurrence) => occurrence.path
|
|
2158
|
+
),
|
|
2159
|
+
scanned: params.reportEvaluation.scanned,
|
|
2160
|
+
skipped: params.reportEvaluation.skipped.length,
|
|
2161
|
+
skippedByReason: reportSkippedByReason,
|
|
2162
|
+
}),
|
|
2163
|
+
scanned: params.reportEvaluation.scanned,
|
|
2164
|
+
selectedClassIds: [
|
|
2165
|
+
params.plan.id ?? `vocabulary:${params.plan.from}->${params.plan.to}`,
|
|
2166
|
+
],
|
|
2167
|
+
skipped: params.reportEvaluation.skipped.length,
|
|
2168
|
+
skipsByReason: reportSkippedByReason,
|
|
2169
|
+
unknownClassIds: [],
|
|
2170
|
+
},
|
|
2171
|
+
params.files
|
|
2172
|
+
.filter((file) => includedByScope(file.path, params.effectivePlan.scope))
|
|
2173
|
+
.map((file) => file.path)
|
|
2174
|
+
);
|
|
2175
|
+
return params.applySummary === undefined
|
|
2176
|
+
? report
|
|
2177
|
+
: withApplySummary(report, params.applySummary);
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* In-memory vocabulary evaluation ready for freshness-checked apply.
|
|
2182
|
+
*
|
|
2183
|
+
* @example
|
|
2184
|
+
* ```ts
|
|
2185
|
+
* const prepared = prepareVocabularyRegradeRun({ identity, plan, root });
|
|
2186
|
+
* if (prepared.isOk() && prepared.value !== null) {
|
|
2187
|
+
* console.log(prepared.value.report.run?.gate.status);
|
|
2188
|
+
* }
|
|
2189
|
+
* ```
|
|
2190
|
+
*/
|
|
2191
|
+
export interface PreparedVocabularyRegradeRun {
|
|
2192
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
2193
|
+
readonly report: RegradeReport;
|
|
2194
|
+
readonly sourceStateHash: string;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
interface PreparedVocabularyRegradeRunState {
|
|
2198
|
+
readonly dryRunEvaluation: VocabularyEvaluation;
|
|
2199
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
2200
|
+
readonly inputs: VocabularyRunInputs;
|
|
2201
|
+
readonly params: PrepareVocabularyRegradeRunParams;
|
|
2202
|
+
readonly sourceStateHash: string;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const preparedVocabularyRunStates = new WeakMap<
|
|
2206
|
+
PreparedVocabularyRegradeRun,
|
|
2207
|
+
PreparedVocabularyRegradeRunState
|
|
2208
|
+
>();
|
|
2209
|
+
|
|
2210
|
+
const vocabularySourceStateHash = (files: readonly SourceFile[]): string =>
|
|
2211
|
+
createHash('sha256')
|
|
2212
|
+
.update(
|
|
2213
|
+
JSON.stringify({
|
|
2214
|
+
sources: files
|
|
2215
|
+
.map((file) => ({ bytes: file.sourceBytes, path: file.path }))
|
|
2216
|
+
.toSorted((left, right) => {
|
|
2217
|
+
if (left.path < right.path) {
|
|
2218
|
+
return -1;
|
|
2219
|
+
}
|
|
2220
|
+
if (left.path > right.path) {
|
|
2221
|
+
return 1;
|
|
2222
|
+
}
|
|
2223
|
+
return 0;
|
|
2224
|
+
}),
|
|
2225
|
+
})
|
|
2226
|
+
)
|
|
2227
|
+
.digest('hex');
|
|
2228
|
+
|
|
2229
|
+
const unreadableVocabularyPaths = (
|
|
2230
|
+
skipped: readonly SkippedSource[]
|
|
2231
|
+
): readonly string[] =>
|
|
2232
|
+
skipped
|
|
2233
|
+
.filter(
|
|
2234
|
+
(entry) =>
|
|
2235
|
+
entry.reason === 'unreadable-file' ||
|
|
2236
|
+
entry.reason === 'unreadable-directory'
|
|
2237
|
+
)
|
|
2238
|
+
.map((entry) => entry.path);
|
|
2239
|
+
|
|
2240
|
+
const validateVocabularyPreparedIdentity = (
|
|
2241
|
+
expected: PreparedRegradeRunIdentity,
|
|
2242
|
+
actual: PreparedRegradeRunIdentity
|
|
2243
|
+
): Result<void, ValidationError> => {
|
|
2244
|
+
for (const field of [
|
|
2245
|
+
'planContentHash',
|
|
2246
|
+
'policyHash',
|
|
2247
|
+
'scopeHash',
|
|
2248
|
+
'lockStateHash',
|
|
2249
|
+
'toolVersion',
|
|
2250
|
+
] as const) {
|
|
2251
|
+
if (expected[field] !== actual[field]) {
|
|
2252
|
+
return Result.err(
|
|
2253
|
+
new ValidationError(
|
|
2254
|
+
`Prepared vocabulary Regrade identity field \`${field}\` is stale.`,
|
|
2255
|
+
{
|
|
2256
|
+
context: {
|
|
2257
|
+
actual: actual[field],
|
|
2258
|
+
expected: expected[field],
|
|
2259
|
+
field,
|
|
2260
|
+
},
|
|
2261
|
+
}
|
|
2262
|
+
)
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return Result.ok();
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* Classify a vocabulary Regrade run once and retain it for checked apply.
|
|
2271
|
+
*
|
|
2272
|
+
* @example
|
|
2273
|
+
* ```ts
|
|
2274
|
+
* const prepared = prepareVocabularyRegradeRun({ identity, plan, root });
|
|
2275
|
+
* if (prepared.isErr()) throw prepared.error;
|
|
2276
|
+
* ```
|
|
2277
|
+
*/
|
|
2278
|
+
export const prepareVocabularyRegradeRun = (
|
|
2279
|
+
params: PrepareVocabularyRegradeRunParams & {
|
|
2280
|
+
readonly identity: PreparedRegradeRunIdentity;
|
|
2281
|
+
}
|
|
2282
|
+
): Result<PreparedVocabularyRegradeRun | null, ValidationError> => {
|
|
2283
|
+
const preparedParams = snapshotPrepareVocabularyRegradeRunParams(params);
|
|
2284
|
+
const planValidation = validateVocabularyPlan(preparedParams.plan);
|
|
2285
|
+
if (planValidation.isErr()) {
|
|
2286
|
+
return planValidation;
|
|
2287
|
+
}
|
|
2288
|
+
const inventoryValidation = validatePreserveInventory(
|
|
2289
|
+
preparedParams.preserveInventory
|
|
2290
|
+
);
|
|
2291
|
+
if (inventoryValidation.isErr()) {
|
|
2292
|
+
return inventoryValidation;
|
|
2293
|
+
}
|
|
2294
|
+
const inputs = collectVocabularyRunInputs(preparedParams);
|
|
2295
|
+
if (inputs === null) {
|
|
2296
|
+
return Result.ok(null);
|
|
2297
|
+
}
|
|
2298
|
+
const unreadable = unreadableVocabularyPaths(inputs.skipped);
|
|
2299
|
+
if (unreadable.length > 0) {
|
|
2300
|
+
return Result.err(
|
|
2301
|
+
new ValidationError(
|
|
2302
|
+
'Prepared vocabulary Regrade sources must all be readable.',
|
|
2303
|
+
{ context: { paths: unreadable } }
|
|
2304
|
+
)
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
const dryRunEvaluation = buildRunVocabularyEvaluation({
|
|
2308
|
+
apply: false,
|
|
2309
|
+
effectivePlan: inputs.effectivePlan,
|
|
2310
|
+
files: inputs.files,
|
|
2311
|
+
plan: preparedParams.plan,
|
|
2312
|
+
preserveInventory: preparedParams.preserveInventory,
|
|
2313
|
+
root: preparedParams.root,
|
|
2314
|
+
skipped: inputs.skipped,
|
|
2315
|
+
sourceKindForPath: preparedParams.sourceKindForPath,
|
|
2316
|
+
});
|
|
2317
|
+
const report = vocabularyReportFromEvaluation({
|
|
2318
|
+
collectedRoot: inputs.collectedRoot,
|
|
2319
|
+
dryRunEvaluation,
|
|
2320
|
+
effectivePlan: inputs.effectivePlan,
|
|
2321
|
+
entrySelection: preparedParams.includeEntries ?? 'actionable',
|
|
2322
|
+
files: inputs.files,
|
|
2323
|
+
plan: preparedParams.plan,
|
|
2324
|
+
reportEvaluation: dryRunEvaluation,
|
|
2325
|
+
skipped: inputs.skipped,
|
|
2326
|
+
});
|
|
2327
|
+
const prepared: PreparedVocabularyRegradeRun = {
|
|
2328
|
+
identity: { ...params.identity },
|
|
2329
|
+
report: cloneVocabularyReport(report),
|
|
2330
|
+
sourceStateHash: vocabularySourceStateHash(inputs.files),
|
|
2331
|
+
};
|
|
2332
|
+
preparedVocabularyRunStates.set(prepared, {
|
|
2333
|
+
dryRunEvaluation,
|
|
2334
|
+
identity: { ...params.identity },
|
|
2335
|
+
inputs,
|
|
2336
|
+
params: preparedParams,
|
|
2337
|
+
sourceStateHash: prepared.sourceStateHash,
|
|
2338
|
+
});
|
|
2339
|
+
return Result.ok(prepared);
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* Apply an in-memory vocabulary evaluation after identity and source checks.
|
|
2344
|
+
*
|
|
2345
|
+
* @example
|
|
2346
|
+
* ```ts
|
|
2347
|
+
* const applied = applyPreparedVocabularyRegradeRun(prepared, identity);
|
|
2348
|
+
* if (applied.isErr()) throw applied.error;
|
|
2349
|
+
* ```
|
|
2350
|
+
*/
|
|
2351
|
+
export const applyPreparedVocabularyRegradeRun = (
|
|
2352
|
+
prepared: PreparedVocabularyRegradeRun,
|
|
2353
|
+
identity: PreparedRegradeRunIdentity
|
|
2354
|
+
): Result<RegradeReport, InternalError | ValidationError> => {
|
|
2355
|
+
const state = preparedVocabularyRunStates.get(prepared);
|
|
2356
|
+
if (state === undefined) {
|
|
2357
|
+
return Result.err(
|
|
2358
|
+
new ValidationError(
|
|
2359
|
+
'Prepared vocabulary Regrade run is not the original in-memory evaluation.'
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
const identityValidation = validateVocabularyPreparedIdentity(
|
|
2364
|
+
state.identity,
|
|
2365
|
+
identity
|
|
2366
|
+
);
|
|
2367
|
+
if (identityValidation.isErr()) {
|
|
2368
|
+
return identityValidation;
|
|
2369
|
+
}
|
|
2370
|
+
const current = collectVocabularyRunInputs(state.params);
|
|
2371
|
+
if (current === null) {
|
|
2372
|
+
return Result.err(
|
|
2373
|
+
new ValidationError(
|
|
2374
|
+
'Prepared vocabulary Regrade root is no longer readable.'
|
|
2375
|
+
)
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
const unreadable = unreadableVocabularyPaths(current.skipped);
|
|
2379
|
+
if (unreadable.length > 0) {
|
|
2380
|
+
return Result.err(
|
|
2381
|
+
new ValidationError(
|
|
2382
|
+
'Prepared vocabulary Regrade sources are no longer readable.',
|
|
2383
|
+
{ context: { paths: unreadable } }
|
|
2384
|
+
)
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const currentSourceStateHash = vocabularySourceStateHash(current.files);
|
|
2388
|
+
if (currentSourceStateHash !== state.sourceStateHash) {
|
|
2389
|
+
return Result.err(
|
|
2390
|
+
new ValidationError(
|
|
2391
|
+
'Prepared vocabulary Regrade source state is stale.',
|
|
2392
|
+
{
|
|
2393
|
+
context: {
|
|
2394
|
+
actual: currentSourceStateHash,
|
|
2395
|
+
expected: state.sourceStateHash,
|
|
2396
|
+
},
|
|
2397
|
+
}
|
|
2398
|
+
)
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
const applyResult = applyVocabularyEvaluation(
|
|
2402
|
+
state.inputs.files,
|
|
2403
|
+
state.dryRunEvaluation
|
|
2404
|
+
);
|
|
2405
|
+
if (applyResult.isErr()) {
|
|
2406
|
+
return applyResult;
|
|
2407
|
+
}
|
|
2408
|
+
const appliedFiles = state.inputs.files.map((file) => ({
|
|
2409
|
+
...file,
|
|
2410
|
+
source: readFileSync(file.absolutePath, 'utf8'),
|
|
2411
|
+
}));
|
|
2412
|
+
const postApplyEvaluation = buildRunVocabularyEvaluation({
|
|
2413
|
+
apply: true,
|
|
2414
|
+
effectivePlan: state.inputs.effectivePlan,
|
|
2415
|
+
files: appliedFiles,
|
|
2416
|
+
plan: state.params.plan,
|
|
2417
|
+
preserveInventory: state.params.preserveInventory,
|
|
2418
|
+
root: state.params.root,
|
|
2419
|
+
skipped: state.inputs.skipped,
|
|
2420
|
+
sourceKindForPath: state.params.sourceKindForPath,
|
|
2421
|
+
});
|
|
2422
|
+
return Result.ok(
|
|
2423
|
+
vocabularyReportFromEvaluation({
|
|
2424
|
+
applySummary: applyResult.value,
|
|
2425
|
+
collectedRoot: state.inputs.collectedRoot,
|
|
2426
|
+
dryRunEvaluation: state.dryRunEvaluation,
|
|
2427
|
+
effectivePlan: state.inputs.effectivePlan,
|
|
2428
|
+
entrySelection: state.params.includeEntries ?? 'actionable',
|
|
2429
|
+
files: appliedFiles,
|
|
2430
|
+
plan: state.params.plan,
|
|
2431
|
+
reportEvaluation: postApplyEvaluation,
|
|
2432
|
+
skipped: state.inputs.skipped,
|
|
2433
|
+
})
|
|
2434
|
+
);
|
|
2435
|
+
};
|
|
2436
|
+
|
|
2437
|
+
export const runVocabularyRegrade = (params: {
|
|
2438
|
+
readonly apply?: boolean;
|
|
2439
|
+
readonly includeEntries?: 'actionable' | 'all';
|
|
2440
|
+
readonly plan: VocabularyRegradePlan;
|
|
2441
|
+
readonly preserveInventory?: readonly VocabularyPreserveInventoryEntry[];
|
|
2442
|
+
readonly root: string;
|
|
2443
|
+
readonly sourceFilter?: (path: string) => boolean;
|
|
2444
|
+
readonly sourceKindForPath?: (path: string) => VocabularySourceKind;
|
|
2445
|
+
}): Result<RegradeReport | null, InternalError | ValidationError> => {
|
|
2446
|
+
const planValidation = validateVocabularyPlan(params.plan);
|
|
2447
|
+
if (planValidation.isErr()) {
|
|
2448
|
+
return planValidation;
|
|
2449
|
+
}
|
|
2450
|
+
const inventoryValidation = validatePreserveInventory(
|
|
2451
|
+
params.preserveInventory
|
|
2452
|
+
);
|
|
2453
|
+
if (inventoryValidation.isErr()) {
|
|
2454
|
+
return inventoryValidation;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const inputs = collectVocabularyRunInputs(params);
|
|
2458
|
+
if (inputs === null) {
|
|
2459
|
+
return Result.ok(null);
|
|
2460
|
+
}
|
|
2461
|
+
const { collectedRoot, effectivePlan, files, skipped } = inputs;
|
|
1945
2462
|
|
|
1946
2463
|
const dryRunEffectiveEvaluation = buildRunVocabularyEvaluation({
|
|
1947
2464
|
apply: false,
|
|
@@ -1951,6 +2468,7 @@ export const runVocabularyRegrade = (params: {
|
|
|
1951
2468
|
preserveInventory: params.preserveInventory,
|
|
1952
2469
|
root: params.root,
|
|
1953
2470
|
skipped,
|
|
2471
|
+
sourceKindForPath: params.sourceKindForPath,
|
|
1954
2472
|
});
|
|
1955
2473
|
let reportEvaluation = dryRunEffectiveEvaluation;
|
|
1956
2474
|
let applySummary: RegradeApplySummary | undefined;
|
|
@@ -1976,51 +2494,22 @@ export const runVocabularyRegrade = (params: {
|
|
|
1976
2494
|
preserveInventory: params.preserveInventory,
|
|
1977
2495
|
root: params.root,
|
|
1978
2496
|
skipped,
|
|
2497
|
+
sourceKindForPath: params.sourceKindForPath,
|
|
1979
2498
|
});
|
|
1980
2499
|
}
|
|
1981
2500
|
|
|
1982
|
-
const entrySelection = params.includeEntries ?? 'actionable';
|
|
1983
|
-
const reportEntries = reportEvaluation.entries;
|
|
1984
|
-
const actionableEntries = reportEntries.filter(
|
|
1985
|
-
(entry) => entry.outcome === 'rewrite' || entry.outcome === 'needs-review'
|
|
1986
|
-
);
|
|
1987
|
-
const reportSkippedByReason = skippedByReason(reportEvaluation.skipped);
|
|
1988
|
-
const report: RegradeReport = {
|
|
1989
|
-
entries: entrySelection === 'all' ? reportEntries : actionableEntries,
|
|
1990
|
-
matched: actionableEntries.length,
|
|
1991
|
-
review: reportEntries.filter((entry) => entry.outcome === 'needs-review')
|
|
1992
|
-
.length,
|
|
1993
|
-
rewritten: reportEntries.filter((entry) => entry.outcome === 'rewrite')
|
|
1994
|
-
.length,
|
|
1995
|
-
root: collected.root,
|
|
1996
|
-
run:
|
|
1997
|
-
applySummary === undefined
|
|
1998
|
-
? reportEvaluation.run
|
|
1999
|
-
: vocabularyRunWithAppliedOccurrences(
|
|
2000
|
-
reportEvaluation.run,
|
|
2001
|
-
dryRunEffectiveEvaluation.run,
|
|
2002
|
-
applySummary
|
|
2003
|
-
),
|
|
2004
|
-
scan: buildRegradeScanSummary({
|
|
2005
|
-
matchedPaths: actionableEntries.map((entry) => entry.path),
|
|
2006
|
-
occurrencePaths: reportEvaluation.occurrences.map(
|
|
2007
|
-
(occurrence) => occurrence.path
|
|
2008
|
-
),
|
|
2009
|
-
scanned: reportEvaluation.scanned,
|
|
2010
|
-
skipped: reportEvaluation.skipped.length,
|
|
2011
|
-
skippedByReason: reportSkippedByReason,
|
|
2012
|
-
}),
|
|
2013
|
-
scanned: reportEvaluation.scanned,
|
|
2014
|
-
selectedClassIds: [
|
|
2015
|
-
params.plan.id ?? `vocabulary:${params.plan.from}->${params.plan.to}`,
|
|
2016
|
-
],
|
|
2017
|
-
skipped: reportEvaluation.skipped.length,
|
|
2018
|
-
skipsByReason: reportSkippedByReason,
|
|
2019
|
-
unknownClassIds: [],
|
|
2020
|
-
};
|
|
2021
|
-
|
|
2022
2501
|
return Result.ok(
|
|
2023
|
-
|
|
2502
|
+
vocabularyReportFromEvaluation({
|
|
2503
|
+
...(applySummary === undefined ? {} : { applySummary }),
|
|
2504
|
+
collectedRoot,
|
|
2505
|
+
dryRunEvaluation: dryRunEffectiveEvaluation,
|
|
2506
|
+
effectivePlan,
|
|
2507
|
+
entrySelection: params.includeEntries ?? 'actionable',
|
|
2508
|
+
files,
|
|
2509
|
+
plan: params.plan,
|
|
2510
|
+
reportEvaluation,
|
|
2511
|
+
skipped,
|
|
2512
|
+
})
|
|
2024
2513
|
);
|
|
2025
2514
|
};
|
|
2026
2515
|
|
|
@@ -2170,6 +2659,10 @@ export const vocabularyRegradeRunOutput = z.object({
|
|
|
2170
2659
|
scopeTier: z
|
|
2171
2660
|
.enum(['in-scope', 'policy-classified'])
|
|
2172
2661
|
.describe('Three-tier scope classification for this occurrence'),
|
|
2662
|
+
sourceKind: z
|
|
2663
|
+
.enum(['source-comment', 'tsdoc'])
|
|
2664
|
+
.optional()
|
|
2665
|
+
.describe('Exact source-comment construct for comment inventory'),
|
|
2173
2666
|
start: z.number().describe('Source start offset'),
|
|
2174
2667
|
verdict: z
|
|
2175
2668
|
.enum(['applied', 'deferred', 'modified', 'skipped'])
|