syncova-policies/scripts/compare-csv-pdf.js
Weapie 88a3eb1dcc
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m54s
Erklärungen aus der DSE-Gesamtliste neu aufbauen
Führende Quelle ist jetzt der Excel-Export des Amtes statt der einzelnen
Merkblatt-PDFs: 120 Zeilen, davon 4 bisher nicht erfasste Sozialamts-
Leistungen. Zuordnung über den Bezeichner, nicht den Titel – die aus PDF
gelesenen Titel unterscheiden sich in neun Fällen nur in Leerzeichen um
Schrägstriche und hätten Dubletten erzeugt.

Zwei Angaben stehen nicht in der Liste und werden ergänzt: der in allen
Merkblättern enthaltene Empfängersatz zur Finanzbuchhaltung (ohne ihn
hätten 27 Erklärungen gar keine Empfängerangabe) und die Bezeichnung der
Verarbeitungstätigkeit aus den Organisationsspalten.

Die zehn Bausteine folgen durchgehend den zentralen Standardtexten;
gespeichert wird nur die eine echte Abweichung: die gemeinsame
Verantwortlichkeit mit dem ZIT SH beim Online-Wohngeldantrag.

Alle 121 Erklärungen bleiben unveröffentlicht – drei Standardtexte
(Widerspruchsrecht, Drittlandübermittlung, automatisierte Entscheidungen)
sind noch nicht gepflegt und ohne sie ist keine vollständig.

Die Quellliste selbst bleibt unversioniert: Sie führt Namen von
Beschäftigten, die nicht in die Erklärungen übernommen werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:12:26 +02:00

158 lines
5.3 KiB
JavaScript
Raw 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.

/**
* Vergleicht die DSE-Gesamtliste (CSV) mit den bisherigen Merkblatt-PDFs.
* Schreibt nichts – dient nur der Prüfung vor einem Import.
*
* node scripts/compare-csv-pdf.js Zusammenfassung
* node scripts/compare-csv-pdf.js --detail jede Abweichung im Wortlaut
*/
const fs = require("fs")
const path = require("path")
const { readPolicyCsv } = require("./lib/csv-policy")
const { parsePolicyPdf, reflow, joinTitle } = require("./lib/pdf-policy")
const CSV = path.join("data", "source", "DSE_Gesamtliste_Amt_Leezen.CSV")
const PDF_DIR = path.join("data", "import")
const H = {
activity: "Bezeichnung der Verarbeitungstätigkeit",
purpose: "Ihre Daten werden zu folgendem Zweck erhoben",
legal: "Ihre Daten wurden aufgrund folgender Rechtsgrundlage erhoben",
obligation: "Ihre Pflicht zur Bereitstellung der Daten",
consequences: "Folgen, wenn Sie die Daten nicht angeben",
recipients: "Wir beabsichtigen, Ihre Daten an folgende Empfänger weiterzuleiten",
storage: "Dauer der Speicherung der personenbezogenen Daten",
source: "Wir haben Ihre Daten aus folgender Quelle erhalten",
}
const C = {
title: "Formular-Bezeichnung",
purpose: "Verarbeitungszweck",
legal: "Rechtsgrundlage f.d. Datenerhebung",
obligation: "Pflicht zur Bereitstellung der Daten",
consequences: "Folgen, wenn Daten nicht angeben werden",
recipients: "Empfänger f. d. beabsichtigte Weitergabe der Daten",
source: "Datenquelle (bei Dritterhebung)",
storage: "Dauer der Speicherung (Löschfristen)",
}
const FIELDS = [
["title", C.title, H.activity],
["purpose", C.purpose, H.purpose],
["legal", C.legal, H.legal],
["obligation", C.obligation, H.obligation],
["consequences", C.consequences, H.consequences],
["recipients", C.recipients, H.recipients],
["source", C.source, H.source],
["storage", C.storage, H.storage],
]
/** Vergleichsform: Groß-/Kleinschreibung, Leerraum und Satzzeichen egal. */
function norm(value) {
return String(value ?? "")
.replace(/\s+/g, " ")
.replace(/[.,;:]+$/g, "")
.trim()
.toLowerCase()
}
const block = (lines) => (lines && lines.length ? reflow(lines).join("\n") : "")
async function main() {
const detail = process.argv.includes("--detail")
const { records } = readPolicyCsv(CSV)
const pdfFiles = fs
.readdirSync(PDF_DIR)
.filter((f) => f.toLowerCase().endsWith(".pdf"))
const pdfByNumber = new Map(pdfFiles.map((f) => [f.replace(/\.pdf$/i, ""), f]))
const stats = {}
FIELDS.forEach(([key]) => {
stats[key] = { gleich: 0, nurCsv: 0, nurPdf: 0, unterschiedlich: 0, beideLeer: 0 }
})
const diffs = []
for (const record of records) {
const nummer = record["Formularnummer"]
const file = pdfByNumber.get(nummer)
if (!file) continue
const parsed = await parsePolicyPdf(path.join(PDF_DIR, file))
for (const [key, csvCol, pdfHeading] of FIELDS) {
const csvValue = (record[csvCol] || "").trim()
const pdfValue =
key === "title"
? joinTitle(parsed.sections[pdfHeading] || [])
: block(parsed.sections[pdfHeading])
const c = norm(csvValue)
const p = norm(pdfValue)
if (!c && !p) stats[key].beideLeer++
else if (c && !p) {
stats[key].nurCsv++
diffs.push({ nummer, key, art: "nur CSV", csvValue, pdfValue })
} else if (!c && p) {
stats[key].nurPdf++
diffs.push({ nummer, key, art: "nur PDF", csvValue, pdfValue })
} else if (c === p) stats[key].gleich++
else {
stats[key].unterschiedlich++
diffs.push({ nummer, key, art: "unterschiedlich", csvValue, pdfValue })
}
}
}
console.log("Abgeglichene Datensätze:", records.filter((r) => pdfByNumber.has(r["Formularnummer"])).length)
console.log("")
console.log("Feld gleich nur CSV nur PDF abweichend beide leer")
console.log("─".repeat(68))
for (const [key] of FIELDS) {
const s = stats[key]
console.log(
key.padEnd(15) +
String(s.gleich).padStart(6) +
String(s.nurCsv).padStart(9) +
String(s.nurPdf).padStart(9) +
String(s.unterschiedlich).padStart(12) +
String(s.beideLeer).padStart(12)
)
}
console.log("")
console.log("=== Inhaltliche Abweichungen (weder nur-CSV noch nur-PDF) ===")
const echte = diffs.filter((d) => d.art === "unterschiedlich")
console.log(`${echte.length} Fälle`)
for (const d of echte) {
console.log(`\n ${d.nummer} · ${d.key}`)
console.log(` CSV: ${d.csvValue.replace(/\n/g, " ⏎ ").slice(0, 200)}`)
console.log(` PDF: ${d.pdfValue.replace(/\n/g, " ⏎ ").slice(0, 200)}`)
}
console.log("")
console.log("=== Nur im PDF vorhanden (ginge beim CSV-Import verloren) ===")
const verlust = diffs.filter((d) => d.art === "nur PDF")
console.log(`${verlust.length} Fälle`)
const nachFeld = {}
verlust.forEach((d) => {
nachFeld[d.key] = nachFeld[d.key] || []
nachFeld[d.key].push(d)
})
for (const [key, list] of Object.entries(nachFeld)) {
console.log(`\n ${key} (${list.length}):`)
const zeigen = detail ? list : list.slice(0, 5)
zeigen.forEach((d) =>
console.log(` ${d.nummer}: ${d.pdfValue.replace(/\n/g, " ⏎ ").slice(0, 150)}`)
)
if (!detail && list.length > 5) console.log(` … und ${list.length - 5} weitere`)
}
}
main().catch((e) => {
console.error(e)
process.exitCode = 1
})