This commit is contained in:
2026-07-26 18:11:57 +02:00
parent d408e01ab5
commit 21a3b34471
139 changed files with 32872 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
/**
* 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();