lila/data-pipeline/utils/verify-enriched-file.ts
2026-07-06 13:09:30 +02:00

96 lines
3 KiB
TypeScript

import fs from "fs";
interface VerificationResult {
valid: boolean;
errors: string[];
}
/**
* Verifies that an enriched JSON file matches the expected schema.
* Returns detailed error messages for any violations.
*/
export function verifyEnrichedFile(filePath: string): VerificationResult {
const errors: string[] = [];
if (!fs.existsSync(filePath)) {
return { valid: false, errors: ["File does not exist"] };
}
let data: unknown;
try {
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch (_error: unknown) {
return { valid: false, errors: ["Invalid JSON syntax"] };
}
if (typeof data !== "object" || data === null || Array.isArray(data)) {
return { valid: false, errors: ["Root must be an object"] };
}
const obj = data as Record<string, unknown>;
// Required top-level fields
const requiredFields = ["word", "language", "pos", "senses"];
for (const field of requiredFields) {
if (!(field in obj)) {
errors.push(`Missing required field: "${field}"`);
}
}
// Validate senses array
if (!Array.isArray(obj["senses"])) {
errors.push('"senses" must be an array');
} else if (obj["senses"].length === 0) {
errors.push('"senses" array cannot be empty');
} else {
for (let i = 0; i < obj["senses"].length; i++) {
const sense = obj["senses"][i] as Record<string, unknown>;
const sensePrefix = `senses[${i}]`;
if (!sense["sense"] || typeof sense["sense"] !== "string") {
errors.push(`${sensePrefix}: missing or invalid "sense"`);
}
if (!sense["example"] || typeof sense["example"] !== "string") {
errors.push(`${sensePrefix}: missing or invalid "example"`);
}
if (
!["easy", "medium", "hard"].includes(
sense["difficulty_level"] as string,
)
) {
errors.push(`${sensePrefix}: invalid "difficulty_level"`);
}
if (!sense["translations"] || typeof sense["translations"] !== "object") {
errors.push(`${sensePrefix}: missing "translations"`);
} else {
const trans = sense["translations"] as Record<string, unknown>;
for (const lang of ["de", "it", "es", "fr"]) {
if (!Array.isArray(trans[lang])) {
errors.push(
`${sensePrefix}: missing or invalid "${lang}" translations`,
);
} else {
for (let j = 0; j < (trans[lang] as unknown[]).length; j++) {
const t = (trans[lang] as unknown[])[j] as Record<
string,
unknown
>;
if (!t["word"] || typeof t["word"] !== "string") {
errors.push(`${sensePrefix}.${lang}[${j}]: missing "word"`);
}
if (
!["masculine", "feminine", "neuter", null].includes(
t["gender"] as string | null,
)
) {
errors.push(`${sensePrefix}.${lang}[${j}]: invalid "gender"`);
}
}
}
}
}
}
}
return { valid: errors.length === 0, errors };
}