/** * 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>} */ function buildYearPresence() { /** @type {Map>} */ 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|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} */ ({})), }, }; 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");