286 lines
8.4 KiB
JavaScript
286 lines
8.4 KiB
JavaScript
/**
|
||
* 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");
|