154 lines
5.4 KiB
JavaScript
154 lines
5.4 KiB
JavaScript
/**
|
||
* 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(/>/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();
|