gute
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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");
|
||||
Reference in New Issue
Block a user