Files
2026-07-26 18:11:57 +02:00

286 lines
8.4 KiB
JavaScript
Raw Permalink 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.
/**
* S03 Kategorisieren + Gewichten
* Liest clean-phrases.json → schreibt scored-phrases.json
* (jede Phrase: type, category, weight).
*/
import { readFileSync, writeFileSync, existsSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const cleanPath = join(__dirname, "clean-phrases.json");
const dumpPath = join(__dirname, "extracted-all.txt");
const outPath = join(__dirname, "scored-phrases.json");
const raw = JSON.parse(readFileSync(cleanPath, "utf8"));
/** @param {string} text */
function categorizeBetrieb(text) {
const t = text.toLowerCase();
if (
/rechnung|gutschrift|zahlungs|mahnung|avis|skonto|betrag|\bbar\b|ec-|kassenladenabschluss|eingangsrechnung|lieferschein/.test(
t,
)
)
return "Rechnungswesen";
if (
/termin|kunde|kundin|telefon|bewertung|mobilita|mobilitäts|angebot|autoservice|digitale kundenkarte/.test(
t,
)
)
return "Kunden";
if (
/reifen|lager|lieferung|bestell|ware|artikel|\bwm\b|retour|teile|batterien|additive|warenrückgabe|lieferscheine/.test(
t,
)
)
return "Lager";
if (/kasse|kassenbuch/.test(t)) return "Kasse";
if (
/fahrzeug|abmeld|zulassung|versicherung|unfall|schaden|getriebe|mietfahrzeug|tanken|ausgeputzt|rausgeputzt/.test(
t,
)
)
return "Fahrzeuge";
if (
/gefahren|gebracht|abgeholt|feuchtwangen|crailsheim|rothenburg|getränke|getranke|bosch|guardey|karlsruhe/.test(
t,
)
)
return "Fahrten";
if (
/ablage|spül|auftr[aä]ge?|büro|cyber|kalender|archiv|papier|wartebereich|betriebsurlaub|organisiert|einsortiert/.test(
t,
)
)
return "Organisation";
return "Betrieb";
}
/** @param {string} text */
function categorizeSchool(text) {
const t = text.toLowerCase();
if (
/bwl|kalkulation|beschaffung|lagerkenn|polypol|bruttoinlands|bestell|markt|preis|abc-analyse|abc-kunden|wettbewerbs|absatzpolit|bip\b|wohlstands|werbung|personalauswahl/.test(
t,
)
)
return "BWL";
if (
/buchhaltung|buchung|rechnung bestandteil|umsatzsteuer|sozialabgaben|inventar|bilanz|guv|konten/.test(
t,
)
)
return "Buchhaltung";
if (
/englisch|introducing|listening|dialogues|briefe im englischen|telefonate|mediation|enquiry|hörverstehen/.test(
t,
)
)
return "Englisch";
if (
/deutsch|das-\/dass|rechtschreib|stellenanzeige|leserbrief|argumentation|protokoll|ausdrucksvermögen|deutschbuch/.test(
t,
)
)
return "Deutsch";
if (
/excel|word|textverarbeitung|formatvorlage|diagramm|ergonomie|seriendruck|powerpoint|summen-funktionen|min-funktionen/.test(
t,
)
)
return "IT";
if (
/geschichte|migration|medien|politik|tarif|arbeitsschutz|demograf|bundestag|mauer|religion|demokratie|volksentscheid|integration|assimilation|homeoffice/.test(
t,
)
)
return "Gesellschaft";
if (
/projekt|präsentation|kommunikation|meeting|checkliste|e-mail|büromanagement|\bbfk\b|zwischenprüfung|zeitmanagement|zeugnis/.test(
t,
)
)
return "BFK";
return "Berufsschule";
}
/**
* Optional: Phrasen, die in beiden Ausbildungsjahren vorkommen → weight +1..+2
* Quelle: extracted-all.txt (Jahr-Marker „Erstes/Zweites Ausbildungsjahr“).
* @returns {Map<string, Set<number>>}
*/
function buildYearPresence() {
/** @type {Map<string, Set<number>>} */
const yearsByKey = new Map();
if (!existsSync(dumpPath)) return yearsByKey;
const dump = readFileSync(dumpPath, "utf8");
const blocks = dump.split(/^===== .+ =====$/m).slice(1);
for (const block of blocks) {
let year = 0;
if (/Erstes Ausbildungsjahr/i.test(block)) year = 1;
else if (/Zweites Ausbildungsjahr/i.test(block)) year = 2;
if (!year) continue;
const lines = block.split(/\r?\n/);
for (const line of lines) {
const key = line.replace(/\s+/g, " ").trim().toLowerCase();
if (key.length < 4) continue;
let set = yearsByKey.get(key);
if (!set) {
set = new Set();
yearsByKey.set(key, set);
}
set.add(year);
}
}
return yearsByKey;
}
/**
* @param {number} count
* @param {Set<number>|undefined} years
*/
function computeWeight(count, years) {
let weight = Math.max(1, count || 1);
if (years && years.size >= 2) {
// leichte Anhebung: häufige Phrasen +2, sonst +1
weight += weight >= 3 ? 2 : 1;
}
return weight;
}
/** @param {unknown} item */
function asEntry(item) {
if (typeof item === "string") return { text: item.trim(), count: 1 };
if (item && typeof item === "object" && "text" in item) {
const text = String(/** @type {{text: unknown}} */ (item).text).trim();
const count =
"count" in item && typeof /** @type {{count: unknown}} */ (item).count === "number"
? /** @type {{count: number}} */ (item).count
: 1;
return { text, count };
}
return null;
}
const yearPresence = buildYearPresence();
/** @type {{ text: string, type: 'betrieb'|'schule', category: string, weight: number, count: number, years: number[] }[]} */
const scored = [];
const seen = new Set();
/**
* @param {string} text
* @param {'betrieb'|'schule'} type
* @param {string} category
* @param {number} count
*/
function pushPhrase(text, type, category, count) {
const t = text.replace(/\s+/g, " ").trim();
if (!t) return;
const key = `${type}::${t.toLowerCase()}`;
if (seen.has(key)) return;
seen.add(key);
const years = yearPresence.get(t.toLowerCase());
const yearList = years ? [...years].sort() : [];
scored.push({
text: t,
type,
category,
weight: computeWeight(count, years),
count: Math.max(1, count || 1),
years: yearList,
});
}
const activities = raw.activities ?? [];
for (const a of activities) {
const e = asEntry(a);
if (!e) continue;
pushPhrase(e.text, "betrieb", categorizeBetrieb(e.text), e.count);
}
const schoolSource =
Array.isArray(raw.schoolWeighted) && raw.schoolWeighted.length
? raw.schoolWeighted
: (raw.school ?? []);
for (const s of schoolSource) {
const e = asEntry(s);
if (!e) continue;
pushPhrase(e.text, "schule", categorizeSchool(e.text), e.count);
}
scored.sort(
(a, b) =>
(a.type === b.type ? 0 : a.type === "betrieb" ? -1 : 1) ||
b.weight - a.weight ||
a.text.localeCompare(b.text, "de"),
);
const out = {
phrases: scored.map(({ text, type, category, weight }) => ({
text,
type,
category,
weight,
})),
meta: {
source: "clean-phrases.json",
total: scored.length,
betrieb: scored.filter((p) => p.type === "betrieb").length,
schule: scored.filter((p) => p.type === "schule").length,
multiYearBoosted: scored.filter((p) => p.years.length >= 2).length,
yearPresenceFrom: existsSync(dumpPath) ? "extracted-all.txt" : null,
generatedAt: new Date().toISOString(),
// Debug-Hilfen für Abnahme (nicht Teil des Datenmodells)
topBetrieb: scored
.filter((p) => p.type === "betrieb")
.slice(0, 10)
.map((p) => `${p.text}×${p.weight} [${p.category}]`),
topSchule: scored
.filter((p) => p.type === "schule")
.slice(0, 10)
.map((p) => `${p.text}×${p.weight} [${p.category}]`),
categoryCounts: scored.reduce((acc, p) => {
acc[p.category] = (acc[p.category] || 0) + 1;
return acc;
}, /** @type {Record<string, number>} */ ({})),
},
};
writeFileSync(outPath, JSON.stringify(out, null, 2), "utf8");
// Abnahme-Checks
const missing = out.phrases.filter((p) => !p.type || !p.category || !p.weight);
const schoolAsBetrieb = out.phrases.filter(
(p) =>
p.type === "betrieb" &&
/bwl|englisch|polypol|introducing|klassenarbeit|berufsschule|excel klassen|hörverstehen|umsatzsteuer|das-\/dass/i.test(
p.text,
),
);
console.log(
`Wrote ${out.phrases.length} scored phrases (betrieb ${out.meta.betrieb}, schule ${out.meta.schule}) → ${outPath}`,
);
console.log(`Multi-year boost: ${out.meta.multiYearBoosted}`);
console.log("Top Betrieb:", out.meta.topBetrieb.slice(0, 6).join(" · "));
console.log("Top Schule:", out.meta.topSchule.slice(0, 6).join(" · "));
if (missing.length) {
console.error(`FAIL: ${missing.length} Phrasen ohne type/category/weight`);
process.exit(1);
}
if (schoolAsBetrieb.length) {
console.warn(
`WARN Stichprobe: ${schoolAsBetrieb.length} mögliche Schule-als-Betrieb:`,
schoolAsBetrieb.slice(0, 5).map((p) => p.text),
);
} else {
console.log("OK: Schule-Stichworte nicht als betrieb gelabelt");
}
console.log("OK: jede Phrase hat type, category, weight");