forked from codechefPesuecc/CodeChef-PESUECC-Chapter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate-challenges.ts
More file actions
111 lines (97 loc) · 3.42 KB
/
Copy pathvalidate-challenges.ts
File metadata and controls
111 lines (97 loc) · 3.42 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
/**
* Validates every problem JSON in `/challenges` against the Arena schema.
*
* Runs locally (`npm run challenges:validate`) and in CI, so a malformed problem
* — a missing hidden test, a bad date, a stray `points` field — is caught in
* review before it is ever seeded into the database.
*
* node --import tsx scripts/validate-challenges.ts
*/
import fs from "node:fs";
import path from "node:path";
import { CHALLENGES_DIR, ChallengeSchema, KNOWN_KEYS } from "./challenge-schema";
let files: string[];
try {
files = fs.readdirSync(CHALLENGES_DIR).filter((f) => f.endsWith(".json"));
} catch {
console.error(`No challenges directory at ${CHALLENGES_DIR}`);
process.exit(1);
}
if (files.length === 0) {
console.log("No challenge files to validate.");
process.exit(0);
}
let hadError = false;
const slugsSeen = new Map<string, string>();
for (const file of files.sort()) {
const errors: string[] = [];
const warnings: string[] = [];
const full = path.join(CHALLENGES_DIR, file);
const stem = file.replace(/\.json$/, "");
let data: unknown;
try {
data = JSON.parse(fs.readFileSync(full, "utf8"));
} catch (e) {
errors.push(`invalid JSON — ${(e as Error).message}`);
report(file, errors, warnings);
hadError = true;
continue;
}
const parsed = ChallengeSchema.safeParse(data);
if (!parsed.success) {
for (const issue of parsed.error.issues) {
const where = issue.path.length ? issue.path.join(".") : "(root)";
errors.push(`${where}: ${issue.message}`);
}
}
// Stray-key checks against the raw object (the schema ignores unknowns).
if (data && typeof data === "object" && !Array.isArray(data)) {
for (const key of Object.keys(data as Record<string, unknown>)) {
if (key === "points") {
errors.push(
"`points` was removed — scoring comes from the speed-bounty ladder, not the problem file",
);
} else if (!KNOWN_KEYS.has(key)) {
warnings.push(`unknown field \`${key}\` (ignored by the loader)`);
}
}
}
if (parsed.success) {
const c = parsed.data;
// The filename is the slug's source of truth; keep them aligned.
if (!stem.startsWith(c.date)) {
warnings.push(
`filename should start with the date — expected "${c.date}-…", got "${stem}"`,
);
}
if (c.slug && c.slug !== stem) {
warnings.push(
`slug "${c.slug}" doesn't match filename "${stem}" — the archive links by slug`,
);
}
const slug = c.slug ?? stem;
const dupe = slugsSeen.get(slug);
if (dupe) errors.push(`duplicate slug "${slug}" (also in ${dupe})`);
else slugsSeen.set(slug, file);
if (c.checker?.type === "float" && c.checker.epsilon === undefined) {
warnings.push("float checker has no epsilon — the judge will default to 1e-6");
}
}
if (errors.length) hadError = true;
report(file, errors, warnings);
}
function report(file: string, errors: string[], warnings: string[]) {
if (!errors.length && !warnings.length) {
console.log(` ok ${file}`);
return;
}
const tag = errors.length ? "FAIL" : "warn";
console.log(`${errors.length ? "✗" : "!"} ${tag} ${file}`);
for (const e of errors) console.log(` error: ${e}`);
for (const w of warnings) console.log(` warn: ${w}`);
}
if (hadError) {
console.error("\nChallenge validation failed.");
process.exit(1);
}
console.log(`\nValidated ${files.length} challenge file(s).`);