@ontrails/regrade 1.0.0-beta.41 → 1.0.0-beta.43
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 +43 -0
- package/package.json +6 -6
- package/src/downstream/ast-rewrite.ts +37 -0
- package/src/downstream/collect.ts +12 -1
- package/src/downstream/export-restructure.ts +1 -1
- package/src/downstream/file-renames.ts +1590 -0
- package/src/downstream/report.ts +12 -1
- package/src/downstream/vocabulary-registry.ts +111 -12
- package/src/downstream/vocabulary.ts +631 -37
- package/src/index.ts +16 -0
- package/src/literal-transform.ts +1 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Result,
|
|
4
4
|
ValidationError,
|
|
5
5
|
escapeRegExp,
|
|
6
|
+
isPlainObject,
|
|
6
7
|
matchesAnyPathGlob,
|
|
7
8
|
} from '@ontrails/core';
|
|
8
9
|
import { createHash } from 'node:crypto';
|
|
@@ -17,7 +18,10 @@ import {
|
|
|
17
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
19
|
import { z } from 'zod';
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_IGNORED_DIRECTORIES,
|
|
23
|
+
collectDownstreamSources,
|
|
24
|
+
} from './collect.js';
|
|
21
25
|
import type { DownstreamCollectionOptions, SkippedSource } from './collect.js';
|
|
22
26
|
import type {
|
|
23
27
|
RegradeApplySummary,
|
|
@@ -26,13 +30,14 @@ import type {
|
|
|
26
30
|
} from './report.js';
|
|
27
31
|
import { buildRegradeScanSummary } from './scan-summary.js';
|
|
28
32
|
|
|
29
|
-
export type VocabularyVerdict = 'deferred' | 'modified' | 'skipped';
|
|
33
|
+
export type VocabularyVerdict = 'applied' | 'deferred' | 'modified' | 'skipped';
|
|
30
34
|
|
|
31
35
|
export const vocabularyDispositionValues = [
|
|
32
36
|
'code-context-out-of-engine',
|
|
33
37
|
'docs-only',
|
|
34
38
|
'explicit-preserve',
|
|
35
39
|
'forward-pointer',
|
|
40
|
+
'historical-by-policy',
|
|
36
41
|
'ignored-by-scope',
|
|
37
42
|
'in-family-modified',
|
|
38
43
|
'in-family-unresolved',
|
|
@@ -58,6 +63,15 @@ export interface VocabularyPreserveInventoryEntry extends VocabularyPreserveRule
|
|
|
58
63
|
readonly source: 'derived-live-api';
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
export interface VocabularyScopePolicy {
|
|
67
|
+
readonly disposition: VocabularyDisposition;
|
|
68
|
+
readonly expectMatches?: boolean | undefined;
|
|
69
|
+
readonly paths: readonly string[];
|
|
70
|
+
readonly reason: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type VocabularyScopeTier = 'in-scope' | 'policy-classified';
|
|
74
|
+
|
|
61
75
|
export interface VocabularyRegradeScope {
|
|
62
76
|
readonly exclude?: readonly string[];
|
|
63
77
|
readonly extensions?: readonly string[];
|
|
@@ -68,11 +82,72 @@ export interface VocabularyRegradeScope {
|
|
|
68
82
|
*/
|
|
69
83
|
readonly ignoredDirectories?: readonly string[];
|
|
70
84
|
readonly include?: readonly string[];
|
|
85
|
+
readonly policyClassified?: readonly VocabularyScopePolicy[];
|
|
86
|
+
readonly teachingSurfaces?: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One authored root-relative file move in a vocabulary plan.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const rename: VocabularyFileRename = {
|
|
95
|
+
* from: 'docs/old.md',
|
|
96
|
+
* to: 'docs/new.md',
|
|
97
|
+
* };
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export interface VocabularyFileRename {
|
|
101
|
+
readonly from: string;
|
|
102
|
+
readonly to: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Derived reference-closure totals for one governed file move.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* console.log(evidence.rewritten, evidence.historical);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export interface VocabularyFileRenameEvidence extends VocabularyFileRename {
|
|
114
|
+
readonly deferred: number;
|
|
115
|
+
readonly historical: number;
|
|
116
|
+
readonly preserved: number;
|
|
117
|
+
readonly rewritten: number;
|
|
118
|
+
readonly skipped: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One deterministic form proposal synthesized from a vocabulary seed.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const proposal: VocabularyFormProposal = {
|
|
127
|
+
* from: 'legacies',
|
|
128
|
+
* kind: 'safe-rewrite',
|
|
129
|
+
* reason: 'default-morphology',
|
|
130
|
+
* source: 'default-morphology',
|
|
131
|
+
* to: 'currents',
|
|
132
|
+
* };
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export interface VocabularyFormProposal {
|
|
136
|
+
readonly from: string;
|
|
137
|
+
readonly kind: 'review' | 'safe-rewrite';
|
|
138
|
+
readonly reason: string;
|
|
139
|
+
readonly source:
|
|
140
|
+
| 'default-morphology'
|
|
141
|
+
| 'plan-defer'
|
|
142
|
+
| 'plan-override'
|
|
143
|
+
| 'seed';
|
|
144
|
+
readonly to?: string;
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
export interface VocabularyRegradePlan {
|
|
74
148
|
readonly caseSensitive?: boolean;
|
|
75
149
|
readonly deferForms?: readonly string[];
|
|
150
|
+
readonly fileRenames?: readonly VocabularyFileRename[];
|
|
76
151
|
readonly from: string;
|
|
77
152
|
readonly id?: string;
|
|
78
153
|
readonly intent?: string;
|
|
@@ -94,6 +169,7 @@ export interface VocabularyOccurrence {
|
|
|
94
169
|
readonly reason: string;
|
|
95
170
|
readonly replacement?: string;
|
|
96
171
|
readonly start: number;
|
|
172
|
+
readonly scopeTier: VocabularyScopeTier;
|
|
97
173
|
readonly verdict: VocabularyVerdict;
|
|
98
174
|
}
|
|
99
175
|
|
|
@@ -119,10 +195,17 @@ export interface VocabularyRunReport {
|
|
|
119
195
|
Readonly<Record<VocabularyDisposition, number>>
|
|
120
196
|
>;
|
|
121
197
|
readonly filesChanged: number;
|
|
198
|
+
readonly fileRenames?: readonly VocabularyFileRenameEvidence[];
|
|
122
199
|
readonly gate: VocabularyRunGate;
|
|
123
200
|
readonly modified: number;
|
|
124
201
|
readonly open: number;
|
|
125
202
|
readonly skipped: number;
|
|
203
|
+
readonly scopeTiers: Readonly<Record<VocabularyScopeTier, number>>;
|
|
204
|
+
readonly teachingSurfaces: {
|
|
205
|
+
readonly expected: readonly string[];
|
|
206
|
+
readonly missing: readonly string[];
|
|
207
|
+
readonly touched: readonly string[];
|
|
208
|
+
};
|
|
126
209
|
}
|
|
127
210
|
|
|
128
211
|
export interface VocabularyRegradeRun {
|
|
@@ -172,7 +255,7 @@ interface SourceOccurrence extends VocabularyOccurrence {
|
|
|
172
255
|
|
|
173
256
|
interface SourceOccurrenceDraft extends Omit<
|
|
174
257
|
SourceOccurrence,
|
|
175
|
-
'disposition' | 'reason' | 'verdict'
|
|
258
|
+
'disposition' | 'reason' | 'scopeTier' | 'verdict'
|
|
176
259
|
> {
|
|
177
260
|
readonly contextColumn: number;
|
|
178
261
|
}
|
|
@@ -220,6 +303,81 @@ const vocabularyDispositionCounts = (
|
|
|
220
303
|
);
|
|
221
304
|
};
|
|
222
305
|
|
|
306
|
+
const vocabularyFormVerdicts = (
|
|
307
|
+
occurrences: readonly VocabularyOccurrence[]
|
|
308
|
+
): Readonly<Record<string, VocabularyVerdict>> => {
|
|
309
|
+
const priority: Readonly<Record<VocabularyVerdict, number>> = {
|
|
310
|
+
applied: 1,
|
|
311
|
+
deferred: 3,
|
|
312
|
+
modified: 2,
|
|
313
|
+
skipped: 0,
|
|
314
|
+
};
|
|
315
|
+
const forms: Record<string, VocabularyVerdict> = {};
|
|
316
|
+
for (const occurrence of occurrences) {
|
|
317
|
+
if (occurrence.verdict === 'applied') {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const current = forms[occurrence.form];
|
|
321
|
+
if (
|
|
322
|
+
current === undefined ||
|
|
323
|
+
priority[occurrence.verdict] > priority[current]
|
|
324
|
+
) {
|
|
325
|
+
forms[occurrence.form] = occurrence.verdict;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return forms;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const vocabularyScopeTierCounts = (
|
|
332
|
+
occurrences: readonly VocabularyOccurrence[]
|
|
333
|
+
): Readonly<Record<VocabularyScopeTier, number>> => ({
|
|
334
|
+
'in-scope': occurrences.filter(
|
|
335
|
+
(occurrence) => occurrence.scopeTier === 'in-scope'
|
|
336
|
+
).length,
|
|
337
|
+
'policy-classified': occurrences.filter(
|
|
338
|
+
(occurrence) => occurrence.scopeTier === 'policy-classified'
|
|
339
|
+
).length,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const vocabularyScopeEvidence = (
|
|
343
|
+
plan: VocabularyRegradePlan,
|
|
344
|
+
occurrences: readonly VocabularyOccurrence[]
|
|
345
|
+
): {
|
|
346
|
+
readonly gateReasons: readonly string[];
|
|
347
|
+
readonly scopeTiers: Readonly<Record<VocabularyScopeTier, number>>;
|
|
348
|
+
readonly teachingSurfaces: VocabularyRunReport['teachingSurfaces'];
|
|
349
|
+
} => {
|
|
350
|
+
const expected = uniqueSorted(plan.scope?.teachingSurfaces ?? []);
|
|
351
|
+
const touched = expected.filter((pattern) =>
|
|
352
|
+
occurrences.some(
|
|
353
|
+
(occurrence) =>
|
|
354
|
+
occurrence.scopeTier === 'in-scope' &&
|
|
355
|
+
matchesAnyPathGlob(occurrence.path, [pattern])
|
|
356
|
+
)
|
|
357
|
+
);
|
|
358
|
+
const missing = expected.filter((pattern) => !touched.includes(pattern));
|
|
359
|
+
const expectedPolicyMissing =
|
|
360
|
+
plan.scope?.policyClassified?.some(
|
|
361
|
+
(policy) =>
|
|
362
|
+
policy.expectMatches === true &&
|
|
363
|
+
!occurrences.some(
|
|
364
|
+
(occurrence) =>
|
|
365
|
+
occurrence.scopeTier === 'policy-classified' &&
|
|
366
|
+
matchesAnyPathGlob(occurrence.path, policy.paths)
|
|
367
|
+
)
|
|
368
|
+
) ?? false;
|
|
369
|
+
return {
|
|
370
|
+
gateReasons: [
|
|
371
|
+
...(expectedPolicyMissing
|
|
372
|
+
? ['expected-policy-classified-evidence-missing']
|
|
373
|
+
: []),
|
|
374
|
+
...(missing.length === 0 ? [] : ['expected-teaching-surfaces-missing']),
|
|
375
|
+
],
|
|
376
|
+
scopeTiers: vocabularyScopeTierCounts(occurrences),
|
|
377
|
+
teachingSurfaces: { expected, missing, touched },
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
223
381
|
const isVocabularyTokenCharacter = (
|
|
224
382
|
value: string,
|
|
225
383
|
routeLike: boolean
|
|
@@ -559,6 +717,10 @@ const targetFormsForPlan = (
|
|
|
559
717
|
return forms;
|
|
560
718
|
};
|
|
561
719
|
|
|
720
|
+
export const vocabularyRewriteFormsForPlan = (
|
|
721
|
+
plan: VocabularyRegradePlan
|
|
722
|
+
): readonly (readonly [string, string])[] => [...targetFormsForPlan(plan)];
|
|
723
|
+
|
|
562
724
|
const deferFormsForPlan = (plan: VocabularyRegradePlan): readonly string[] => {
|
|
563
725
|
const overrideForms = new Set(
|
|
564
726
|
normalizedOverrideEntries(plan.overrides).map(([form]) =>
|
|
@@ -573,6 +735,140 @@ const deferFormsForPlan = (plan: VocabularyRegradePlan): readonly string[] => {
|
|
|
573
735
|
]);
|
|
574
736
|
};
|
|
575
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Synthesize the deterministic form proposal carried by a minimal plan seed.
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```ts
|
|
743
|
+
* const forms = deriveVocabularyFormProposals({
|
|
744
|
+
* from: 'legacy',
|
|
745
|
+
* kind: 'vocabulary',
|
|
746
|
+
* to: 'current',
|
|
747
|
+
* });
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
export const deriveVocabularyFormProposals = (
|
|
751
|
+
plan: VocabularyRegradePlan
|
|
752
|
+
): readonly VocabularyFormProposal[] => {
|
|
753
|
+
const overrideForms = new Set(Object.keys(plan.overrides ?? {}));
|
|
754
|
+
const explicitDefers = new Set(
|
|
755
|
+
(plan.deferForms ?? []).map((form) => formIdentityForPlan(plan, form))
|
|
756
|
+
);
|
|
757
|
+
const safeProposals = [...targetFormsForPlan(plan).entries()].flatMap(
|
|
758
|
+
([from, to]): readonly VocabularyFormProposal[] => {
|
|
759
|
+
if (explicitDefers.has(formIdentityForPlan(plan, from))) {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
if (from === plan.from) {
|
|
763
|
+
return [
|
|
764
|
+
{
|
|
765
|
+
from,
|
|
766
|
+
kind: 'safe-rewrite',
|
|
767
|
+
reason: 'minimal-seed',
|
|
768
|
+
source: 'seed',
|
|
769
|
+
to,
|
|
770
|
+
},
|
|
771
|
+
];
|
|
772
|
+
}
|
|
773
|
+
if (overrideForms.has(from)) {
|
|
774
|
+
return [
|
|
775
|
+
{
|
|
776
|
+
from,
|
|
777
|
+
kind: 'safe-rewrite',
|
|
778
|
+
reason: 'authored-or-governed-override',
|
|
779
|
+
source: 'plan-override',
|
|
780
|
+
to,
|
|
781
|
+
},
|
|
782
|
+
];
|
|
783
|
+
}
|
|
784
|
+
return [
|
|
785
|
+
{
|
|
786
|
+
from,
|
|
787
|
+
kind: 'safe-rewrite',
|
|
788
|
+
reason: 'default-morphology',
|
|
789
|
+
source: 'default-morphology',
|
|
790
|
+
to,
|
|
791
|
+
},
|
|
792
|
+
];
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
const casingProposals: VocabularyFormProposal[] = [];
|
|
796
|
+
if (isSimpleVocabularyWord(plan.from) && isSimpleVocabularyWord(plan.to)) {
|
|
797
|
+
const from = `${plan.from.slice(0, 1).toUpperCase()}${plan.from.slice(1)}`;
|
|
798
|
+
const to = `${plan.to.slice(0, 1).toUpperCase()}${plan.to.slice(1)}`;
|
|
799
|
+
if (from !== plan.from && !explicitDefers.has(from)) {
|
|
800
|
+
casingProposals.push({
|
|
801
|
+
from,
|
|
802
|
+
kind: 'review',
|
|
803
|
+
reason: 'uncertain-casing-or-public-name',
|
|
804
|
+
source: 'default-morphology',
|
|
805
|
+
to,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return [
|
|
810
|
+
...safeProposals,
|
|
811
|
+
...casingProposals,
|
|
812
|
+
...deferFormsForPlan(plan).map((from) => ({
|
|
813
|
+
from,
|
|
814
|
+
kind: 'review' as const,
|
|
815
|
+
reason: explicitDefers.has(from)
|
|
816
|
+
? 'authored-or-governed-defer'
|
|
817
|
+
: 'uncertain-morphology',
|
|
818
|
+
source: explicitDefers.has(from)
|
|
819
|
+
? ('plan-defer' as const)
|
|
820
|
+
: ('default-morphology' as const),
|
|
821
|
+
})),
|
|
822
|
+
].toSorted((left, right) =>
|
|
823
|
+
left.from === right.from
|
|
824
|
+
? left.kind.localeCompare(right.kind)
|
|
825
|
+
: left.from.localeCompare(right.from)
|
|
826
|
+
);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const validateVocabularyScope = (
|
|
830
|
+
scope: VocabularyRegradeScope | undefined
|
|
831
|
+
): Result<void, ValidationError> => {
|
|
832
|
+
const excludedDocsPattern = scope?.exclude?.find(
|
|
833
|
+
(pattern) =>
|
|
834
|
+
/(^|\/)docs(?:\/|$)/.test(pattern) ||
|
|
835
|
+
matchesAnyPathGlob('docs/regrade-teaching.md', [pattern]) ||
|
|
836
|
+
matchesAnyPathGlob('docs/regrade-teaching.mdx', [pattern])
|
|
837
|
+
);
|
|
838
|
+
if (excludedDocsPattern !== undefined) {
|
|
839
|
+
return Result.err(
|
|
840
|
+
new ValidationError(
|
|
841
|
+
`Vocabulary Regrade plans cannot hard-exclude docs with "${excludedDocsPattern}"; use a policyClassified rule with a reason.`
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
for (const policy of scope?.policyClassified ?? []) {
|
|
846
|
+
if (policy.paths.length === 0 || policy.reason.trim().length === 0) {
|
|
847
|
+
return Result.err(
|
|
848
|
+
new ValidationError(
|
|
849
|
+
'Vocabulary Regrade policyClassified rules require paths and a reason.'
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const excludedPolicyPath = policy.paths.find((policyPath) =>
|
|
854
|
+
scope?.exclude?.some(
|
|
855
|
+
(excludedPath) =>
|
|
856
|
+
excludedPath === policyPath ||
|
|
857
|
+
matchesAnyPathGlob(policyPath, [excludedPath]) ||
|
|
858
|
+
matchesAnyPathGlob(excludedPath, [policyPath])
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
if (excludedPolicyPath !== undefined) {
|
|
862
|
+
return Result.err(
|
|
863
|
+
new ValidationError(
|
|
864
|
+
`Vocabulary Regrade scope cannot both exclude and policy-classify "${excludedPolicyPath}".`
|
|
865
|
+
)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return Result.ok();
|
|
870
|
+
};
|
|
871
|
+
|
|
576
872
|
const validateVocabularyPlan = (
|
|
577
873
|
plan: VocabularyRegradePlan
|
|
578
874
|
): Result<void, ValidationError> => {
|
|
@@ -637,7 +933,7 @@ const validateVocabularyPlan = (
|
|
|
637
933
|
);
|
|
638
934
|
}
|
|
639
935
|
}
|
|
640
|
-
return
|
|
936
|
+
return validateVocabularyScope(plan.scope);
|
|
641
937
|
};
|
|
642
938
|
|
|
643
939
|
const validatePreserveInventory = (
|
|
@@ -768,6 +1064,43 @@ const preserveRuleForOccurrence = (
|
|
|
768
1064
|
return rule.forms === undefined && pattern.test(occurrence.context);
|
|
769
1065
|
});
|
|
770
1066
|
|
|
1067
|
+
const scopePolicyForPath = (
|
|
1068
|
+
path: string,
|
|
1069
|
+
scope: VocabularyRegradeScope | undefined
|
|
1070
|
+
): VocabularyScopePolicy | undefined =>
|
|
1071
|
+
scope?.policyClassified?.find((policy) =>
|
|
1072
|
+
matchesAnyPathGlob(path, policy.paths)
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
const scopePolicyForOccurrence = (
|
|
1076
|
+
occurrence: SourceOccurrenceDraft,
|
|
1077
|
+
plan: VocabularyRegradePlan
|
|
1078
|
+
): VocabularyScopePolicy | undefined =>
|
|
1079
|
+
scopePolicyForPath(occurrence.path, plan.scope);
|
|
1080
|
+
|
|
1081
|
+
const occurrenceClassification = (
|
|
1082
|
+
occurrence: SourceOccurrenceDraft,
|
|
1083
|
+
plan: VocabularyRegradePlan
|
|
1084
|
+
): {
|
|
1085
|
+
readonly preserveRule: VocabularyPreserveRule | undefined;
|
|
1086
|
+
readonly scopeTier: VocabularyScopeTier;
|
|
1087
|
+
} => {
|
|
1088
|
+
const policy = scopePolicyForOccurrence(occurrence, plan);
|
|
1089
|
+
const preserveRule = preserveRuleForOccurrence(occurrence, plan);
|
|
1090
|
+
return {
|
|
1091
|
+
preserveRule:
|
|
1092
|
+
preserveRule ??
|
|
1093
|
+
(policy === undefined
|
|
1094
|
+
? undefined
|
|
1095
|
+
: {
|
|
1096
|
+
disposition: policy.disposition,
|
|
1097
|
+
pattern: occurrence.form,
|
|
1098
|
+
reason: policy.reason,
|
|
1099
|
+
}),
|
|
1100
|
+
scopeTier: policy === undefined ? 'in-scope' : 'policy-classified',
|
|
1101
|
+
};
|
|
1102
|
+
};
|
|
1103
|
+
|
|
771
1104
|
const occurrenceOverlaps = (
|
|
772
1105
|
occurrences: readonly {
|
|
773
1106
|
readonly end: number;
|
|
@@ -807,7 +1140,10 @@ const deferredOccurrenceFromDraft = (
|
|
|
807
1140
|
baseOccurrence: SourceOccurrenceDraft,
|
|
808
1141
|
reason = 'unclassified-neighbor'
|
|
809
1142
|
): SourceOccurrence => {
|
|
810
|
-
const preserveRule =
|
|
1143
|
+
const { preserveRule, scopeTier } = occurrenceClassification(
|
|
1144
|
+
baseOccurrence,
|
|
1145
|
+
plan
|
|
1146
|
+
);
|
|
811
1147
|
const markdownCodeContext = isMarkdownCodeContext(
|
|
812
1148
|
file,
|
|
813
1149
|
baseOccurrence.start,
|
|
@@ -832,6 +1168,7 @@ const deferredOccurrenceFromDraft = (
|
|
|
832
1168
|
markdownCodeContext,
|
|
833
1169
|
reason
|
|
834
1170
|
),
|
|
1171
|
+
scopeTier,
|
|
835
1172
|
start: baseOccurrence.start,
|
|
836
1173
|
verdict,
|
|
837
1174
|
};
|
|
@@ -940,7 +1277,10 @@ const occurrencesForFile = (
|
|
|
940
1277
|
path: file.path,
|
|
941
1278
|
start,
|
|
942
1279
|
};
|
|
943
|
-
const preserveRule =
|
|
1280
|
+
const { preserveRule, scopeTier } = occurrenceClassification(
|
|
1281
|
+
baseOccurrence,
|
|
1282
|
+
plan
|
|
1283
|
+
);
|
|
944
1284
|
const markdownCodeContext = isMarkdownCodeContext(file, start, end);
|
|
945
1285
|
const packageRouteCodeContext = isPackageRouteCodeOccurrence(
|
|
946
1286
|
file.path,
|
|
@@ -981,6 +1321,7 @@ const occurrencesForFile = (
|
|
|
981
1321
|
markdownCodeContext,
|
|
982
1322
|
capturedReason
|
|
983
1323
|
),
|
|
1324
|
+
scopeTier,
|
|
984
1325
|
...(preserveRule === undefined &&
|
|
985
1326
|
!markdownCodeContext &&
|
|
986
1327
|
!packageRouteCodeContext &&
|
|
@@ -1246,21 +1587,7 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1246
1587
|
(occurrence) =>
|
|
1247
1588
|
occurrence.verdict === 'modified' || occurrence.verdict === 'deferred'
|
|
1248
1589
|
);
|
|
1249
|
-
const forms
|
|
1250
|
-
for (const occurrence of occurrences) {
|
|
1251
|
-
const current = forms[occurrence.form];
|
|
1252
|
-
if (occurrence.verdict === 'deferred') {
|
|
1253
|
-
forms[occurrence.form] = 'deferred';
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
|
-
if (occurrence.verdict === 'modified' && current !== 'deferred') {
|
|
1257
|
-
forms[occurrence.form] = 'modified';
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
if (current === undefined) {
|
|
1261
|
-
forms[occurrence.form] = 'skipped';
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1590
|
+
const forms = vocabularyFormVerdicts(occurrences);
|
|
1264
1591
|
const gateReasons: string[] = [];
|
|
1265
1592
|
if (modifiedOccurrences.length > 0) {
|
|
1266
1593
|
gateReasons.push(
|
|
@@ -1273,6 +1600,8 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1273
1600
|
gateReasons.push('deferred-forms-or-occurrences');
|
|
1274
1601
|
}
|
|
1275
1602
|
const open = unresolvedOccurrences.length;
|
|
1603
|
+
const scopeEvidence = vocabularyScopeEvidence(effectivePlan, occurrences);
|
|
1604
|
+
gateReasons.push(...scopeEvidence.gateReasons);
|
|
1276
1605
|
|
|
1277
1606
|
return {
|
|
1278
1607
|
entries: [
|
|
@@ -1317,7 +1646,9 @@ const buildVocabularyEvaluation = (params: {
|
|
|
1317
1646
|
},
|
|
1318
1647
|
modified: modifiedOccurrences.length,
|
|
1319
1648
|
open,
|
|
1649
|
+
scopeTiers: scopeEvidence.scopeTiers,
|
|
1320
1650
|
skipped: skippedOccurrences.length,
|
|
1651
|
+
teachingSurfaces: scopeEvidence.teachingSurfaces,
|
|
1321
1652
|
},
|
|
1322
1653
|
},
|
|
1323
1654
|
scanned: scopedFiles.length,
|
|
@@ -1347,6 +1678,63 @@ const withApplySummary = (
|
|
|
1347
1678
|
apply,
|
|
1348
1679
|
});
|
|
1349
1680
|
|
|
1681
|
+
const appliedVocabularyRunReport = (
|
|
1682
|
+
postApply: VocabularyRegradeRun,
|
|
1683
|
+
occurrences: readonly VocabularyOccurrence[],
|
|
1684
|
+
apply: RegradeApplySummary
|
|
1685
|
+
): VocabularyRunReport => {
|
|
1686
|
+
const scopeEvidence = vocabularyScopeEvidence(postApply.plan, occurrences);
|
|
1687
|
+
const reasons = uniqueSorted([
|
|
1688
|
+
...postApply.report.gate.reasons.filter(
|
|
1689
|
+
(reason) =>
|
|
1690
|
+
reason !== 'expected-policy-classified-evidence-missing' &&
|
|
1691
|
+
reason !== 'expected-teaching-surfaces-missing'
|
|
1692
|
+
),
|
|
1693
|
+
...scopeEvidence.gateReasons,
|
|
1694
|
+
]);
|
|
1695
|
+
return {
|
|
1696
|
+
...postApply.report,
|
|
1697
|
+
applied: apply.applied,
|
|
1698
|
+
dispositions: vocabularyDispositionCounts(occurrences),
|
|
1699
|
+
filesChanged: apply.filesChanged,
|
|
1700
|
+
gate: {
|
|
1701
|
+
...postApply.report.gate,
|
|
1702
|
+
reasons,
|
|
1703
|
+
status: reasons.length === 0 ? 'green' : 'open',
|
|
1704
|
+
},
|
|
1705
|
+
scopeTiers: scopeEvidence.scopeTiers,
|
|
1706
|
+
teachingSurfaces: scopeEvidence.teachingSurfaces,
|
|
1707
|
+
};
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
const vocabularyRunWithAppliedOccurrences = (
|
|
1711
|
+
postApply: VocabularyRegradeRun,
|
|
1712
|
+
dryRun: VocabularyRegradeRun,
|
|
1713
|
+
apply: RegradeApplySummary
|
|
1714
|
+
): VocabularyRegradeRun => {
|
|
1715
|
+
const appliedOccurrences = dryRun.ledger.occurrences
|
|
1716
|
+
.filter((occurrence) => occurrence.verdict === 'modified')
|
|
1717
|
+
.map((occurrence) => ({ ...occurrence, verdict: 'applied' as const }));
|
|
1718
|
+
const occurrences = [
|
|
1719
|
+
...postApply.ledger.occurrences,
|
|
1720
|
+
...appliedOccurrences,
|
|
1721
|
+
].toSorted(
|
|
1722
|
+
(left, right) =>
|
|
1723
|
+
left.path.localeCompare(right.path) ||
|
|
1724
|
+
left.start - right.start ||
|
|
1725
|
+
left.verdict.localeCompare(right.verdict)
|
|
1726
|
+
);
|
|
1727
|
+
return {
|
|
1728
|
+
...postApply,
|
|
1729
|
+
ledger: {
|
|
1730
|
+
...postApply.ledger,
|
|
1731
|
+
forms: vocabularyFormVerdicts(occurrences),
|
|
1732
|
+
occurrences,
|
|
1733
|
+
},
|
|
1734
|
+
report: appliedVocabularyRunReport(postApply, occurrences, apply),
|
|
1735
|
+
};
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1350
1738
|
const applyVocabularyEvaluation = (
|
|
1351
1739
|
files: readonly SourceFile[],
|
|
1352
1740
|
evaluation: VocabularyEvaluation
|
|
@@ -1449,12 +1837,69 @@ const buildRunVocabularyEvaluation = (params: {
|
|
|
1449
1837
|
skipped: params.skipped,
|
|
1450
1838
|
});
|
|
1451
1839
|
|
|
1840
|
+
const ignoredDirectoriesOpenedByPolicy = (
|
|
1841
|
+
scope: VocabularyRegradeScope | undefined
|
|
1842
|
+
): readonly string[] =>
|
|
1843
|
+
(scope?.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES).filter(
|
|
1844
|
+
(directory) =>
|
|
1845
|
+
scope?.policyClassified?.some((policy) =>
|
|
1846
|
+
policy.paths.some((pattern) => pattern.split('/').includes(directory))
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
const vocabularyIgnoredDirectories = (
|
|
1851
|
+
scope: VocabularyRegradeScope | undefined
|
|
1852
|
+
): readonly string[] => {
|
|
1853
|
+
const opened = ignoredDirectoriesOpenedByPolicy(scope);
|
|
1854
|
+
return (scope?.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES).filter(
|
|
1855
|
+
(directory) => !opened.includes(directory)
|
|
1856
|
+
);
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
const filterVocabularyCollection = (
|
|
1860
|
+
files: readonly { readonly absolutePath: string; readonly path: string }[],
|
|
1861
|
+
scope: VocabularyRegradeScope | undefined,
|
|
1862
|
+
sourceFilter: ((path: string) => boolean) | undefined
|
|
1863
|
+
): {
|
|
1864
|
+
readonly files: readonly {
|
|
1865
|
+
readonly absolutePath: string;
|
|
1866
|
+
readonly path: string;
|
|
1867
|
+
}[];
|
|
1868
|
+
readonly skipped: readonly SkippedSource[];
|
|
1869
|
+
} => {
|
|
1870
|
+
const opened = ignoredDirectoriesOpenedByPolicy(scope);
|
|
1871
|
+
const selected = files.filter((file) => {
|
|
1872
|
+
const openedDirectory = file.path
|
|
1873
|
+
.split('/')
|
|
1874
|
+
.some((segment) => opened.includes(segment));
|
|
1875
|
+
return (
|
|
1876
|
+
(!openedDirectory ||
|
|
1877
|
+
scopePolicyForPath(file.path, scope) !== undefined) &&
|
|
1878
|
+
(sourceFilter?.(file.path) ?? true)
|
|
1879
|
+
);
|
|
1880
|
+
});
|
|
1881
|
+
const selectedPaths = new Set(selected.map((file) => file.path));
|
|
1882
|
+
return {
|
|
1883
|
+
files: selected,
|
|
1884
|
+
skipped: files
|
|
1885
|
+
.filter((file) => !selectedPaths.has(file.path))
|
|
1886
|
+
.map((file) => ({
|
|
1887
|
+
path: file.path,
|
|
1888
|
+
reason:
|
|
1889
|
+
sourceFilter?.(file.path) === false
|
|
1890
|
+
? 'not-selected-source'
|
|
1891
|
+
: 'ignored-directory',
|
|
1892
|
+
})),
|
|
1893
|
+
};
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1452
1896
|
export const runVocabularyRegrade = (params: {
|
|
1453
1897
|
readonly apply?: boolean;
|
|
1454
1898
|
readonly includeEntries?: 'actionable' | 'all';
|
|
1455
1899
|
readonly plan: VocabularyRegradePlan;
|
|
1456
1900
|
readonly preserveInventory?: readonly VocabularyPreserveInventoryEntry[];
|
|
1457
1901
|
readonly root: string;
|
|
1902
|
+
readonly sourceFilter?: (path: string) => boolean;
|
|
1458
1903
|
}): Result<RegradeReport | null, InternalError | ValidationError> => {
|
|
1459
1904
|
const planValidation = validateVocabularyPlan(params.plan);
|
|
1460
1905
|
if (planValidation.isErr()) {
|
|
@@ -1480,15 +1925,23 @@ export const runVocabularyRegrade = (params: {
|
|
|
1480
1925
|
...(effectivePlan.scope?.include === undefined
|
|
1481
1926
|
? {}
|
|
1482
1927
|
: { include: effectivePlan.scope.include }),
|
|
1483
|
-
|
|
1484
|
-
? {}
|
|
1485
|
-
: { ignoredDirectories: effectivePlan.scope.ignoredDirectories }),
|
|
1928
|
+
ignoredDirectories: vocabularyIgnoredDirectories(effectivePlan.scope),
|
|
1486
1929
|
} satisfies DownstreamCollectionOptions);
|
|
1487
1930
|
if (collected === null) {
|
|
1488
1931
|
return Result.ok(null);
|
|
1489
1932
|
}
|
|
1490
1933
|
|
|
1491
|
-
const
|
|
1934
|
+
const filtered = filterVocabularyCollection(
|
|
1935
|
+
collected.files,
|
|
1936
|
+
effectivePlan.scope,
|
|
1937
|
+
params.sourceFilter
|
|
1938
|
+
);
|
|
1939
|
+
const { files, skipped: readSkipped } = readVocabularySourceFiles({
|
|
1940
|
+
...collected,
|
|
1941
|
+
files: filtered.files,
|
|
1942
|
+
skipped: [...collected.skipped, ...filtered.skipped],
|
|
1943
|
+
});
|
|
1944
|
+
const skipped = readSkipped;
|
|
1492
1945
|
|
|
1493
1946
|
const dryRunEffectiveEvaluation = buildRunVocabularyEvaluation({
|
|
1494
1947
|
apply: false,
|
|
@@ -1543,14 +1996,11 @@ export const runVocabularyRegrade = (params: {
|
|
|
1543
1996
|
run:
|
|
1544
1997
|
applySummary === undefined
|
|
1545
1998
|
? reportEvaluation.run
|
|
1546
|
-
:
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
filesChanged: applySummary.filesChanged,
|
|
1552
|
-
},
|
|
1553
|
-
},
|
|
1999
|
+
: vocabularyRunWithAppliedOccurrences(
|
|
2000
|
+
reportEvaluation.run,
|
|
2001
|
+
dryRunEffectiveEvaluation.run,
|
|
2002
|
+
applySummary
|
|
2003
|
+
),
|
|
1554
2004
|
scan: buildRegradeScanSummary({
|
|
1555
2005
|
matchedPaths: actionableEntries.map((entry) => entry.path),
|
|
1556
2006
|
occurrencePaths: reportEvaluation.occurrences.map(
|
|
@@ -1591,6 +2041,21 @@ const vocabularyPreserveRuleSchema = z.object({
|
|
|
1591
2041
|
reason: z.string().optional().describe('Why this form is preserved'),
|
|
1592
2042
|
});
|
|
1593
2043
|
|
|
2044
|
+
const vocabularyScopePolicySchema = z.object({
|
|
2045
|
+
disposition: z
|
|
2046
|
+
.enum(vocabularyDispositionValues)
|
|
2047
|
+
.describe('Occurrence disposition assigned within this protected scope'),
|
|
2048
|
+
expectMatches: z
|
|
2049
|
+
.boolean()
|
|
2050
|
+
.optional()
|
|
2051
|
+
.describe('Require this policy scope to contribute occurrence evidence'),
|
|
2052
|
+
paths: z
|
|
2053
|
+
.array(z.string().min(1))
|
|
2054
|
+
.min(1)
|
|
2055
|
+
.describe('Root-relative protected path patterns'),
|
|
2056
|
+
reason: z.string().min(1).describe('Why matching files are classified'),
|
|
2057
|
+
});
|
|
2058
|
+
|
|
1594
2059
|
const vocabularyPreserveInventoryEntrySchema =
|
|
1595
2060
|
vocabularyPreserveRuleSchema.extend({
|
|
1596
2061
|
evidence: z
|
|
@@ -1627,6 +2092,16 @@ const vocabularyRegradeScopeSchema = z.object({
|
|
|
1627
2092
|
.array(z.string())
|
|
1628
2093
|
.optional()
|
|
1629
2094
|
.describe('Root-relative path patterns to include in this regrade'),
|
|
2095
|
+
policyClassified: z
|
|
2096
|
+
.array(vocabularyScopePolicySchema)
|
|
2097
|
+
.optional()
|
|
2098
|
+
.describe(
|
|
2099
|
+
'Protected paths that remain scanned and counted but are never rewritten by default'
|
|
2100
|
+
),
|
|
2101
|
+
teachingSurfaces: z
|
|
2102
|
+
.array(z.string().min(1))
|
|
2103
|
+
.optional()
|
|
2104
|
+
.describe('Expected current teaching-surface path patterns'),
|
|
1630
2105
|
});
|
|
1631
2106
|
|
|
1632
2107
|
export const vocabularyRegradePlanSchema = z.object({
|
|
@@ -1638,6 +2113,15 @@ export const vocabularyRegradePlanSchema = z.object({
|
|
|
1638
2113
|
.array(z.string().min(1))
|
|
1639
2114
|
.optional()
|
|
1640
2115
|
.describe('Known forms that must be inventoried for review, not rewritten'),
|
|
2116
|
+
fileRenames: z
|
|
2117
|
+
.array(
|
|
2118
|
+
z.object({
|
|
2119
|
+
from: z.string().min(1).describe('Root-relative source file path'),
|
|
2120
|
+
to: z.string().min(1).describe('Root-relative target file path'),
|
|
2121
|
+
})
|
|
2122
|
+
)
|
|
2123
|
+
.optional()
|
|
2124
|
+
.describe('Governed file moves whose references are derived from scope'),
|
|
1641
2125
|
from: z.string().min(1).describe('Source vocabulary term or phrase'),
|
|
1642
2126
|
id: z.string().optional().describe('Stable authored regrade plan id'),
|
|
1643
2127
|
intent: z.string().optional().describe('Human-authored migration intent'),
|
|
@@ -1661,7 +2145,10 @@ export const vocabularyRegradeRunOutput = z.object({
|
|
|
1661
2145
|
.object({
|
|
1662
2146
|
cycle: z.number().describe('Observed regrade run cycle'),
|
|
1663
2147
|
forms: z
|
|
1664
|
-
.record(
|
|
2148
|
+
.record(
|
|
2149
|
+
z.string(),
|
|
2150
|
+
z.enum(['applied', 'deferred', 'modified', 'skipped'])
|
|
2151
|
+
)
|
|
1665
2152
|
.describe('Observed per-form triage verdicts for this run'),
|
|
1666
2153
|
occurrences: z
|
|
1667
2154
|
.array(
|
|
@@ -1680,9 +2167,12 @@ export const vocabularyRegradeRunOutput = z.object({
|
|
|
1680
2167
|
.string()
|
|
1681
2168
|
.optional()
|
|
1682
2169
|
.describe('Replacement text for modified verdicts'),
|
|
2170
|
+
scopeTier: z
|
|
2171
|
+
.enum(['in-scope', 'policy-classified'])
|
|
2172
|
+
.describe('Three-tier scope classification for this occurrence'),
|
|
1683
2173
|
start: z.number().describe('Source start offset'),
|
|
1684
2174
|
verdict: z
|
|
1685
|
-
.enum(['deferred', 'modified', 'skipped'])
|
|
2175
|
+
.enum(['applied', 'deferred', 'modified', 'skipped'])
|
|
1686
2176
|
.describe('Occurrence-level verdict'),
|
|
1687
2177
|
})
|
|
1688
2178
|
)
|
|
@@ -1703,6 +2193,20 @@ export const vocabularyRegradeRunOutput = z.object({
|
|
|
1703
2193
|
dispositions: z
|
|
1704
2194
|
.object(vocabularyDispositionCountSchema.shape)
|
|
1705
2195
|
.describe('Occurrence counts grouped by disposition'),
|
|
2196
|
+
fileRenames: z
|
|
2197
|
+
.array(
|
|
2198
|
+
z.object({
|
|
2199
|
+
deferred: z.number(),
|
|
2200
|
+
from: z.string(),
|
|
2201
|
+
historical: z.number(),
|
|
2202
|
+
preserved: z.number(),
|
|
2203
|
+
rewritten: z.number(),
|
|
2204
|
+
skipped: z.number(),
|
|
2205
|
+
to: z.string(),
|
|
2206
|
+
})
|
|
2207
|
+
)
|
|
2208
|
+
.optional()
|
|
2209
|
+
.describe('Governed file moves and derived reference outcomes'),
|
|
1706
2210
|
filesChanged: z.number().describe('Distinct files changed on disk'),
|
|
1707
2211
|
gate: z
|
|
1708
2212
|
.object({
|
|
@@ -1722,9 +2226,22 @@ export const vocabularyRegradeRunOutput = z.object({
|
|
|
1722
2226
|
.describe(
|
|
1723
2227
|
'Deferred or unapplied modified occurrences holding the gate open'
|
|
1724
2228
|
),
|
|
2229
|
+
scopeTiers: z
|
|
2230
|
+
.object({
|
|
2231
|
+
'in-scope': z.number(),
|
|
2232
|
+
'policy-classified': z.number(),
|
|
2233
|
+
})
|
|
2234
|
+
.describe('Occurrence counts grouped by scope tier'),
|
|
1725
2235
|
skipped: z.number().describe('Skipped occurrence count'),
|
|
2236
|
+
teachingSurfaces: z
|
|
2237
|
+
.object({
|
|
2238
|
+
expected: z.array(z.string()),
|
|
2239
|
+
missing: z.array(z.string()),
|
|
2240
|
+
touched: z.array(z.string()),
|
|
2241
|
+
})
|
|
2242
|
+
.describe('Expected and observed current teaching surfaces'),
|
|
1726
2243
|
})
|
|
1727
|
-
.describe('
|
|
2244
|
+
.describe('Derived run report'),
|
|
1728
2245
|
});
|
|
1729
2246
|
|
|
1730
2247
|
const vocabularyTransitionRecordEnvironmentSchema = z
|
|
@@ -1784,6 +2301,81 @@ export const vocabularyTransitionRecordSchema = z
|
|
|
1784
2301
|
})
|
|
1785
2302
|
.strict();
|
|
1786
2303
|
|
|
2304
|
+
const normalizeLegacyTransitionRecordScopeEvidence = (
|
|
2305
|
+
value: unknown
|
|
2306
|
+
): unknown => {
|
|
2307
|
+
if (
|
|
2308
|
+
!isPlainObject(value) ||
|
|
2309
|
+
value['schemaVersion'] !== VOCABULARY_TRANSITION_RECORD_SCHEMA_VERSION
|
|
2310
|
+
) {
|
|
2311
|
+
return value;
|
|
2312
|
+
}
|
|
2313
|
+
const { report } = value;
|
|
2314
|
+
if (!isPlainObject(report)) {
|
|
2315
|
+
return value;
|
|
2316
|
+
}
|
|
2317
|
+
const { run } = report;
|
|
2318
|
+
if (!isPlainObject(run)) {
|
|
2319
|
+
return value;
|
|
2320
|
+
}
|
|
2321
|
+
const { ledger, report: runReport } = run;
|
|
2322
|
+
if (
|
|
2323
|
+
!isPlainObject(ledger) ||
|
|
2324
|
+
!Array.isArray(ledger['occurrences']) ||
|
|
2325
|
+
!isPlainObject(runReport)
|
|
2326
|
+
) {
|
|
2327
|
+
return value;
|
|
2328
|
+
}
|
|
2329
|
+
const plan = vocabularyRegradePlanSchema.safeParse(run['plan']);
|
|
2330
|
+
if (!plan.success) {
|
|
2331
|
+
return value;
|
|
2332
|
+
}
|
|
2333
|
+
const vocabularyPlan = plan.data as VocabularyRegradePlan;
|
|
2334
|
+
const occurrences = ledger['occurrences'].map((occurrence) =>
|
|
2335
|
+
isPlainObject(occurrence) &&
|
|
2336
|
+
typeof occurrence['path'] === 'string' &&
|
|
2337
|
+
occurrence['scopeTier'] === undefined
|
|
2338
|
+
? {
|
|
2339
|
+
...occurrence,
|
|
2340
|
+
scopeTier:
|
|
2341
|
+
scopePolicyForPath(occurrence['path'], vocabularyPlan.scope) ===
|
|
2342
|
+
undefined
|
|
2343
|
+
? 'in-scope'
|
|
2344
|
+
: 'policy-classified',
|
|
2345
|
+
}
|
|
2346
|
+
: occurrence
|
|
2347
|
+
);
|
|
2348
|
+
const scopeEvidence = vocabularyScopeEvidence(
|
|
2349
|
+
vocabularyPlan,
|
|
2350
|
+
occurrences.filter(
|
|
2351
|
+
(occurrence) =>
|
|
2352
|
+
isPlainObject(occurrence) &&
|
|
2353
|
+
typeof occurrence['path'] === 'string' &&
|
|
2354
|
+
(occurrence['scopeTier'] === 'in-scope' ||
|
|
2355
|
+
occurrence['scopeTier'] === 'policy-classified')
|
|
2356
|
+
) as unknown as readonly VocabularyOccurrence[]
|
|
2357
|
+
);
|
|
2358
|
+
return {
|
|
2359
|
+
...value,
|
|
2360
|
+
report: {
|
|
2361
|
+
...report,
|
|
2362
|
+
run: {
|
|
2363
|
+
...run,
|
|
2364
|
+
ledger: { ...ledger, occurrences },
|
|
2365
|
+
report: {
|
|
2366
|
+
...runReport,
|
|
2367
|
+
...(runReport['scopeTiers'] === undefined
|
|
2368
|
+
? { scopeTiers: scopeEvidence.scopeTiers }
|
|
2369
|
+
: {}),
|
|
2370
|
+
...(runReport['teachingSurfaces'] === undefined
|
|
2371
|
+
? { teachingSurfaces: scopeEvidence.teachingSurfaces }
|
|
2372
|
+
: {}),
|
|
2373
|
+
},
|
|
2374
|
+
},
|
|
2375
|
+
},
|
|
2376
|
+
};
|
|
2377
|
+
};
|
|
2378
|
+
|
|
1787
2379
|
const transitionRecordSlug = (run: VocabularyRegradeRun): string =>
|
|
1788
2380
|
`${run.plan.from}-to-${run.plan.to}`
|
|
1789
2381
|
.toLowerCase()
|
|
@@ -1990,7 +2582,9 @@ export const readVocabularyTransitionRecord = (
|
|
|
1990
2582
|
})
|
|
1991
2583
|
);
|
|
1992
2584
|
}
|
|
1993
|
-
const parsed = vocabularyTransitionRecordSchema.safeParse(
|
|
2585
|
+
const parsed = vocabularyTransitionRecordSchema.safeParse(
|
|
2586
|
+
normalizeLegacyTransitionRecordScopeEvidence(parsedJson)
|
|
2587
|
+
);
|
|
1994
2588
|
if (!parsed.success) {
|
|
1995
2589
|
return Result.err(
|
|
1996
2590
|
new ValidationError('Invalid vocabulary transition record.', {
|