-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate.mjs
More file actions
151 lines (132 loc) · 4.78 KB
/
validate.mjs
File metadata and controls
151 lines (132 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { readFileSync } from "fs";
const providersSchema = JSON.parse(
readFileSync("providers.schema.json", "utf8"),
);
const programsSchema = JSON.parse(
readFileSync("programs.schema.json", "utf8"),
);
const providers = JSON.parse(readFileSync("providers.json", "utf8"));
const programs = JSON.parse(readFileSync("programs.json", "utf8"));
// --- Schema validation ---
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validateProviders = ajv.compile(providersSchema);
if (!validateProviders(providers)) {
console.error("providers.json schema validation failed:");
for (const err of validateProviders.errors) {
console.error(` ${err.instancePath || "/"}: ${err.message}`);
}
process.exit(1);
}
console.log("✓ providers.json schema validation passed");
const validatePrograms = ajv.compile(programsSchema);
if (!validatePrograms(programs)) {
console.error("programs.json schema validation failed:");
for (const err of validatePrograms.errors) {
console.error(` ${err.instancePath || "/"}: ${err.message}`);
}
process.exit(1);
}
console.log("✓ programs.json schema validation passed");
// --- Cross-referential checks ---
const errors = [];
const warnings = [];
const programEntries = programs.programs;
const programKeys = new Set(Object.keys(programEntries));
// Collect all quarters defined across all programs
const allQuarters = new Set();
for (const program of Object.values(programEntries)) {
for (const q of program.year1Quarters ?? []) allQuarters.add(q);
for (const q of program.year2Quarters ?? []) allQuarters.add(q);
}
// Check unique slugs
const slugs = providers.providers.map((p) => p.slug);
const seen = new Set();
for (const slug of slugs) {
if (seen.has(slug)) errors.push(`Duplicate slug: "${slug}"`);
seen.add(slug);
}
// Check providers are sorted alphabetically by name
for (let i = 1; i < providers.providers.length; i++) {
const prev = providers.providers[i - 1].name.toLowerCase();
const curr = providers.providers[i].name.toLowerCase();
if (prev > curr) {
errors.push(
`Providers not sorted: "${providers.providers[i - 1].name}" before "${providers.providers[i].name}"`,
);
}
}
for (const provider of providers.providers) {
const { slug } = provider;
// Provider program keys must reference defined programs
for (const key of Object.keys(provider.programs)) {
if (!programKeys.has(key)) {
errors.push(`${slug}: references undefined program "${key}"`);
}
}
// Report keys must match a quarter defined in some program
for (const key of Object.keys(provider.reports)) {
if (!allQuarters.has(key)) {
errors.push(
`${slug}: report "${key}" doesn't match any program quarter`,
);
}
}
// Report quarters must belong to a program the provider participates in
const providerQuarters = new Set();
for (const programKey of Object.keys(provider.programs)) {
const program = programEntries[programKey];
if (!program) continue;
for (const q of program.year1Quarters ?? []) providerQuarters.add(q);
if (provider.programs[programKey].streamDuration === 2) {
for (const q of program.year2Quarters ?? []) providerQuarters.add(q);
}
}
for (const key of Object.keys(provider.reports)) {
if (!providerQuarters.has(key)) {
warnings.push(
`${slug}: report "${key}" doesn't belong to any program the provider participates in`,
);
}
}
// streamDuration: 2 only valid when program has year2Quarters
for (const [programKey, entry] of Object.entries(provider.programs)) {
const program = programEntries[programKey];
if (
entry.streamDuration === 2 &&
program &&
(!program.year2Quarters || program.year2Quarters.length === 0)
) {
errors.push(
`${slug}: streamDuration 2 in ${programKey} but program has no year2Quarters`,
);
}
}
}
// Check budget proposal date is before selection proposal date
for (const [key, program] of Object.entries(programEntries)) {
if (program.budgetProposal.date > program.selectionProposal.date) {
errors.push(
`${key}: budget proposal (${program.budgetProposal.id}) should be before selection (${program.selectionProposal.id})`,
);
}
}
if (warnings.length > 0) {
console.warn("Cross-validation warnings:");
for (const warn of warnings) console.warn(` ⚠ ${warn}`);
}
if (errors.length > 0) {
console.error("Cross-validation failed:");
for (const err of errors) console.error(` ${err}`);
process.exit(1);
}
console.log("✓ Cross-validation passed");
const reportCount = providers.providers.reduce(
(n, p) => n + Object.keys(p.reports).length,
0,
);
console.log(
`\n ${programKeys.size} programs, ${providers.providers.length} providers, ${reportCount} reports`,
);