gute
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* S04 – Stilgleiche Varianten (sparsam) + Ausgabe suggestions.ts
|
||||
* Input: scored-phrases.json (S03), Themes aus clean-phrases.json (S02)
|
||||
*
|
||||
* Regeln:
|
||||
* - ≥ 70 % Betrieb = exakte Originale
|
||||
* - ≤ 30 % stilgleiche Varianten
|
||||
* - Varianten-weight = max(1, originalWeight - 2)
|
||||
* - Schule: nur Originale
|
||||
*/
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const scoredPath = join(__dirname, "scored-phrases.json");
|
||||
const cleanPath = join(__dirname, "clean-phrases.json");
|
||||
const outTs = join(__dirname, "..", "src", "data", "suggestions.ts");
|
||||
|
||||
if (!existsSync(scoredPath)) {
|
||||
console.error("Fehlt scored-phrases.json – zuerst S03 (score-phrases.mjs) ausführen.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scored = JSON.parse(readFileSync(scoredPath, "utf8"));
|
||||
const clean = existsSync(cleanPath)
|
||||
? JSON.parse(readFileSync(cleanPath, "utf8"))
|
||||
: { themes: [], themesWeighted: [] };
|
||||
|
||||
/** @param {string} s */
|
||||
function normKey(s) {
|
||||
return String(s)
|
||||
.replace(/\u00a0/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[.…]+$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Varianten, die klar aus belegten Mustern kommen (keine neuen Themen).
|
||||
* `from` muss exakt (normiert) in den Originalen vorkommen.
|
||||
* @type {{ from: string, text: string }[]}
|
||||
*/
|
||||
const VARIANT_SPECS = [
|
||||
// Beispiele aus S04
|
||||
{ from: "Termine vereinbart", text: "Termine telefonisch vereinbart" },
|
||||
{ from: "Termine vereinbart", text: "Termine mit Kunden vereinbart" },
|
||||
{ from: "Ablage einsortiert", text: "Ablage sortiert und archiviert" },
|
||||
{ from: "Rechnungen geschrieben", text: "Rechnungen per E-Mail verschickt" },
|
||||
{ from: "Getränke geholt", text: "Getränke für unsere Belegschaft geholt" },
|
||||
|
||||
// Nah an belegten Formulierungen (Wortstellung / Kürzung / Tippfehler)
|
||||
{ from: "Telefonisch Termine vereinbart", text: "Termine telefonisch vergeben" },
|
||||
{ from: "Kunden telefonisch betreut", text: "Kunden per Telefon betreut" },
|
||||
{ from: "Kasse gezählt", text: "Kassenbuch geführt" },
|
||||
{
|
||||
from: "Kassenbuch geführt und die Kasse gezählt auf Vollständigkeit",
|
||||
text: "Kassenbuch geführt und die Kasse gezählt",
|
||||
},
|
||||
{ from: "Reifen bestellt", text: "Reifen für Kunden bestellt" },
|
||||
{ from: "Spülmaschine ausgeräumt", text: "Spülmaschine ein- und ausgeräumt" },
|
||||
{
|
||||
from: "Bewertungen gesammelt für autoservice.com",
|
||||
text: "Bewertungen für autoservice.com gesammelt",
|
||||
},
|
||||
{
|
||||
from: "Lieferung angenommen und im Lager vorsortiert.",
|
||||
text: "Lieferung angenommen und vorsortiert",
|
||||
},
|
||||
{
|
||||
from: "Mietfahrzeug verliehen an Kunden",
|
||||
text: "Mietfahrzeug an Kunden verliehen",
|
||||
},
|
||||
{
|
||||
from: "Kundenauto nach Feuchtwangen fortgebracht.",
|
||||
text: "Kundenauto nach Feuchtwangen gebracht",
|
||||
},
|
||||
{
|
||||
from: "Eingangsrechnung überprüft und telefonisch korrigieren lassen.",
|
||||
text: "Eingangsrechnung überprüft",
|
||||
},
|
||||
{
|
||||
from: "Termin wünsche per E-Mail abgearbeitet",
|
||||
text: "Terminwünsche per E-Mail abgearbeitet",
|
||||
},
|
||||
{
|
||||
from: "Auto Ersatzteil beim WM in Crailsheim abgeholt",
|
||||
text: "Teil beim WM in Crailsheim abgeholt",
|
||||
},
|
||||
{
|
||||
from: "Cybersicherheitsherausforderungen absolviert bei „Guardey“",
|
||||
text: "Cybersicherheitsherausforderungen bei Guardey absolviert",
|
||||
},
|
||||
{
|
||||
from: "Aufträge für die kommende Woche vorbereitet und einsortiert.",
|
||||
text: "Aufträge für die kommende Woche vorbereitet und einsortiert",
|
||||
},
|
||||
{ from: "Kassenbuch führen", text: "Kassenbuch kontrolliert" },
|
||||
{
|
||||
from: "Restliche Termine für den Radwechseltag vergeben",
|
||||
text: "Termine für Radwechseltag vergeben",
|
||||
},
|
||||
{
|
||||
from: "Rechnungen per E-Mail verschickt an Geschäftskunden.",
|
||||
text: "Rechnungen an Geschäftskunden per E-Mail verschickt",
|
||||
},
|
||||
{
|
||||
from: "Kunden per Telefon betreut und Termine vereinbart",
|
||||
text: "Kunden telefonisch betreut und Termine koordiniert",
|
||||
},
|
||||
{
|
||||
from: "Beim WM in Crailsheim Teile abgeholt",
|
||||
text: "Teile beim WM in Crailsheim abgeholt",
|
||||
},
|
||||
];
|
||||
|
||||
const phrases = Array.isArray(scored.phrases) ? scored.phrases : [];
|
||||
const originals = phrases.map((p) => ({
|
||||
text: String(p.text).replace(/\s+/g, " ").trim(),
|
||||
type: p.type === "schule" ? "schule" : "betrieb",
|
||||
category: String(p.category || (p.type === "schule" ? "Berufsschule" : "Betrieb")),
|
||||
weight: Math.max(1, Number(p.weight) || 1),
|
||||
original: true,
|
||||
}));
|
||||
|
||||
const byKey = new Map();
|
||||
for (const p of originals) {
|
||||
byKey.set(`${p.type}|${normKey(p.text)}`, p);
|
||||
}
|
||||
|
||||
/** @type {{ text: string, type: string, category: string, weight: number, original: boolean, from: string }[]} */
|
||||
const variants = [];
|
||||
|
||||
for (const spec of VARIANT_SPECS) {
|
||||
const fromKey = normKey(spec.from);
|
||||
const source = originals.find((p) => p.type === "betrieb" && normKey(p.text) === fromKey);
|
||||
if (!source) {
|
||||
console.warn(`S04: Original für Variante fehlt → übersprungen: "${spec.from}"`);
|
||||
continue;
|
||||
}
|
||||
const text = spec.text.replace(/\s+/g, " ").trim();
|
||||
if (text.length < 6 || text.length > 160) continue;
|
||||
const key = `betrieb|${normKey(text)}`;
|
||||
if (byKey.has(key)) continue; // schon Original oder früher angelegt
|
||||
if (variants.some((v) => normKey(v.text) === normKey(text))) continue;
|
||||
|
||||
const weight = Math.max(1, source.weight - 2);
|
||||
const item = {
|
||||
text,
|
||||
type: "betrieb",
|
||||
category: source.category,
|
||||
weight,
|
||||
original: false,
|
||||
from: source.text,
|
||||
};
|
||||
variants.push(item);
|
||||
byKey.set(key, item);
|
||||
}
|
||||
|
||||
const betriebOriginals = originals.filter((p) => p.type === "betrieb");
|
||||
const schoolOriginals = originals.filter((p) => p.type === "schule");
|
||||
|
||||
// Cap: Varianten ≤ 30 % des finalen Betrieb-Pools
|
||||
const maxVariants = Math.floor((betriebOriginals.length * 0.3) / 0.7);
|
||||
const keptVariants = variants.slice(0, maxVariants);
|
||||
|
||||
const all = [...betriebOriginals, ...keptVariants, ...schoolOriginals];
|
||||
all.sort(
|
||||
(a, b) =>
|
||||
(a.type === b.type ? 0 : a.type === "betrieb" ? -1 : 1) ||
|
||||
b.weight - a.weight ||
|
||||
a.text.localeCompare(b.text, "de"),
|
||||
);
|
||||
|
||||
const suggestions = all.map((s, i) => ({
|
||||
id: i + 1,
|
||||
text: s.text,
|
||||
type: s.type,
|
||||
category: s.category,
|
||||
weight: s.weight,
|
||||
}));
|
||||
|
||||
const themesWeighted = clean.themesWeighted ?? (clean.themes ?? []).map((t) =>
|
||||
typeof t === "string" ? { text: t, count: 1 } : t,
|
||||
);
|
||||
const themeSeen = new Set();
|
||||
const allThemes = [];
|
||||
for (const t of themesWeighted) {
|
||||
const cleaned = String(typeof t === "string" ? t : t.text)
|
||||
.replace(/>/g, ">")
|
||||
.replace(/,\s*$/, "")
|
||||
.trim();
|
||||
if (!cleaned || cleaned === "-" || cleaned.length < 3 || cleaned.length > 55) continue;
|
||||
if (/ferien|vorschriften/i.test(cleaned)) continue;
|
||||
if (/\b(abgeholt|gefahren|gebucht|auffüllen|einsortieren)\b/i.test(cleaned)) continue;
|
||||
const key = cleaned.toLowerCase();
|
||||
if (themeSeen.has(key)) continue;
|
||||
themeSeen.add(key);
|
||||
allThemes.push(cleaned);
|
||||
}
|
||||
|
||||
const betriebN = suggestions.filter((s) => s.type === "betrieb").length;
|
||||
const schoolN = suggestions.filter((s) => s.type === "schule").length;
|
||||
const originalShare = betriebN ? betriebOriginals.length / betriebN : 0;
|
||||
const variantShare = betriebN ? keptVariants.length / betriebN : 0;
|
||||
|
||||
let sourceFiles = 0;
|
||||
for (const n of [
|
||||
scored?.meta?.files,
|
||||
scored?.meta?.sourceFiles,
|
||||
scored?.meta?.hefte,
|
||||
clean?.meta?.files,
|
||||
clean?.meta?.hefte,
|
||||
]) {
|
||||
const v = Number(n);
|
||||
if (Number.isFinite(v) && v > 0) {
|
||||
sourceFiles = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!sourceFiles) {
|
||||
try {
|
||||
const rawMeta = JSON.parse(readFileSync(join(__dirname, "raw-phrases.json"), "utf8")).meta;
|
||||
const v = Number(rawMeta?.files || rawMeta?.hefte);
|
||||
if (Number.isFinite(v) && v > 0) sourceFiles = v;
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
total: suggestions.length,
|
||||
betrieb: betriebN,
|
||||
school: schoolN,
|
||||
originalsBetrieb: betriebOriginals.length,
|
||||
variants: keptVariants.length,
|
||||
originalShareBetrieb: Number(originalShare.toFixed(3)),
|
||||
variantShareBetrieb: Number(variantShare.toFixed(3)),
|
||||
sourceFiles,
|
||||
source: "scored-phrases.json",
|
||||
};
|
||||
|
||||
const content = `/* AUTO-GENERATED aus scored-phrases + sparsame Varianten (S04) · ${suggestions.length} Vorschläge · ${new Date().toISOString().slice(0, 10)} */
|
||||
export type SuggestionType = "betrieb" | "schule";
|
||||
|
||||
export type Suggestion = {
|
||||
id: number;
|
||||
text: string;
|
||||
type: SuggestionType;
|
||||
category: string;
|
||||
/** Häufigkeit/Qualität aus den Originalheften (Varianten: originalWeight - 2) */
|
||||
weight: number;
|
||||
};
|
||||
|
||||
export const SUGGESTIONS: Suggestion[] = ${JSON.stringify(suggestions, null, 2)};
|
||||
|
||||
export const WEEK_THEMES: string[] = ${JSON.stringify(allThemes, null, 2)};
|
||||
|
||||
export const CATEGORIES = [...new Set(SUGGESTIONS.map((s) => s.category))].sort();
|
||||
|
||||
export const SUGGESTION_STATS = ${JSON.stringify(stats, null, 2)} as const;
|
||||
`;
|
||||
|
||||
writeFileSync(outTs, content, "utf8");
|
||||
|
||||
console.log(`Wrote ${suggestions.length} suggestions → ${outTs}`);
|
||||
console.log(
|
||||
`Betrieb: ${betriebN} (Originale ${betriebOriginals.length} = ${(originalShare * 100).toFixed(1)}%, Varianten ${keptVariants.length} = ${(variantShare * 100).toFixed(1)}%)`,
|
||||
);
|
||||
console.log(`Schule: ${schoolN} (nur Originale, 0 Varianten)`);
|
||||
console.log(`Themes: ${allThemes.length}`);
|
||||
|
||||
if (originalShare < 0.7 || variantShare > 0.3) {
|
||||
console.error("FAIL: Mengenregel verletzt (Originale ≥70 %, Varianten ≤30 %).");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log("OK: Mengenregel Originale ≥70 % / Varianten ≤30 %");
|
||||
}
|
||||
|
||||
// Abnahme: Verhältnis + Stichprobe Varianten vs. Original
|
||||
console.log("\n--- S04 Stichprobe: Varianten ↔ Original ---");
|
||||
const sample = [...keptVariants].sort(() => Math.random() - 0.5).slice(0, 20);
|
||||
if (sample.length < 20) {
|
||||
console.log(`(nur ${sample.length} Varianten im Pool – alle gelistet)`);
|
||||
}
|
||||
for (const [i, v] of sample.entries()) {
|
||||
console.log(
|
||||
`${String(i + 1).padStart(2)}. „${v.text}“ (w=${v.weight}) ← „${v.from}“`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user