Files
berichtsheftgenerator/web/scripts/verify-suggestions.mjs
T
2026-07-26 18:11:57 +02:00

154 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Qualitätstor für Suggestions (S06).
* Failt bei Müll, ungültigem type/weight, Duplikaten oder zu wenig Originalen.
*
* Usage: npm run check:suggestions
*/
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
const SUGGESTIONS_PATH = join(ROOT, "src", "data", "suggestions.ts");
const CLEAN_PATH = join(__dirname, "clean-phrases.json");
const MIN_ORIGINAL_SHARE = 0.7;
const VALID_TYPES = new Set(["betrieb", "schule"]);
/** Müll: Header/Signatur/Nummern-Artefakte nicht echte Sätze mit „zur Unterschrift …“. */
const JUNK_RE =
/Datum und Unterschrift|\bLfd\.?\s*Nr\b|……|…………|\.{4,}|\d{5,}|Ausbildungsnachweis|Für die Zeit|^Unterschrift\b|^Abteilung\b|Zuordnung zum Lernziel/i;
/**
* @param {string} src
* @returns {{ suggestions: any[], themes: string[] }}
*/
function parseSuggestionsTs(src) {
const sugKey = "export const SUGGESTIONS: Suggestion[] = ";
const themeKey = "export const WEEK_THEMES: string[] = ";
const sugStart = src.indexOf(sugKey);
if (sugStart < 0) throw new Error("SUGGESTIONS-Export nicht gefunden");
const afterSug = sugStart + sugKey.length;
const themeStart = src.indexOf(themeKey, afterSug);
if (themeStart < 0) throw new Error("WEEK_THEMES-Export nicht gefunden");
const sugJson = src.slice(afterSug, themeStart).trim().replace(/;$/, "");
const afterTheme = themeStart + themeKey.length;
const nextExport = src.indexOf("\nexport const ", afterTheme);
if (nextExport < 0) throw new Error("Ende von WEEK_THEMES nicht gefunden");
const themeJson = src.slice(afterTheme, nextExport).trim().replace(/;$/, "");
return {
suggestions: JSON.parse(sugJson),
themes: JSON.parse(themeJson),
};
}
function fail(msg) {
console.error(`FAIL: ${msg}`);
}
function main() {
const errors = [];
const { suggestions, themes } = parseSuggestionsTs(
readFileSync(SUGGESTIONS_PATH, "utf8"),
);
const clean = JSON.parse(readFileSync(CLEAN_PATH, "utf8"));
const originalTexts = new Set(
(clean.activities ?? []).map((a) =>
(typeof a === "string" ? a : a.text).toLowerCase().replace(/\s+/g, " ").trim(),
),
);
const themeText = (t) => (typeof t === "string" ? t : t?.text);
const realThemes = new Set(
[...(clean.themes ?? []), ...(clean.themesWeighted ?? [])]
.map(themeText)
.map((t) =>
String(t ?? "")
.replace(/&gt;/g, ">")
.replace(/,\s*$/, "")
.trim()
.toLowerCase(),
)
.filter(Boolean),
);
if (!Array.isArray(suggestions) || suggestions.length === 0) {
errors.push("SUGGESTIONS leer oder ungültig");
}
const seen = new Map();
let junkCount = 0;
let badType = 0;
let badWeight = 0;
let dupCount = 0;
for (const s of suggestions) {
const text = String(s?.text ?? "").replace(/\s+/g, " ").trim();
if (JUNK_RE.test(text)) {
junkCount++;
if (junkCount <= 8) fail(`Müll-Regex: id=${s.id} "${text}"`);
}
if (!VALID_TYPES.has(s?.type)) {
badType++;
if (badType <= 8) fail(`type fehlt/ungültig: id=${s?.id} type=${s?.type}`);
}
if (!(Number(s?.weight) >= 1)) {
badWeight++;
if (badWeight <= 8) fail(`weight < 1: id=${s?.id} weight=${s?.weight}`);
}
const key = text.toLowerCase();
if (seen.has(key)) {
dupCount++;
if (dupCount <= 8) fail(`Duplikat: "${text}" (ids ${seen.get(key)}, ${s.id})`);
} else {
seen.set(key, s.id);
}
}
if (junkCount) errors.push(`${junkCount} Phrase(n) matchen Müll-Regex`);
if (badType) errors.push(`${badType} Phrase(n) ohne gültigen type`);
if (badWeight) errors.push(`${badWeight} Phrase(n) mit weight < 1`);
if (dupCount) errors.push(`${dupCount} case-insensitive Duplikat(e)`);
const betrieb = suggestions.filter((s) => s.type === "betrieb");
const schule = suggestions.filter((s) => s.type === "schule");
const exactBetrieb = betrieb.filter((s) =>
originalTexts.has(String(s.text).toLowerCase().replace(/\s+/g, " ").trim()),
);
const originalShare = betrieb.length ? exactBetrieb.length / betrieb.length : 0;
if (originalShare < MIN_ORIGINAL_SHARE) {
errors.push(
`Original-Anteil Betrieb ${(originalShare * 100).toFixed(1)}% < ${MIN_ORIGINAL_SHARE * 100}% (${exactBetrieb.length}/${betrieb.length})`,
);
}
const fakeThemes = themes.filter((t) => !realThemes.has(String(t).trim().toLowerCase()));
if (fakeThemes.length) {
errors.push(
`${fakeThemes.length} WEEK_THEMES nicht in clean-phrases: ${fakeThemes.slice(0, 5).join(" · ")}`,
);
}
const top10 = [...betrieb]
.sort((a, b) => b.weight - a.weight || a.text.localeCompare(b.text, "de"))
.slice(0, 10);
console.log("=== Suggestions Qualitätstor ===");
console.log(`Gesamt: ${suggestions.length} (Betrieb ${betrieb.length}, Schule ${schule.length})`);
console.log(
`Original-Anteil Betrieb: ${(originalShare * 100).toFixed(1)}% (${exactBetrieb.length}/${betrieb.length})`,
);
console.log(`WEEK_THEMES: ${themes.length} (echte Themen)`);
console.log("Top-10 Betrieb:");
for (const s of top10) console.log(` ${s.weight}× ${s.text}`);
if (errors.length) {
console.error("\nQualitätstor FEHLGESCHLAGEN:");
for (const e of errors) console.error(` - ${e}`);
process.exit(1);
}
console.log("\nQualitätstor OK");
}
main();