syncova-policies/scripts/check-completeness.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

107 lines
4.0 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.

/**
* Berichtet, welche Erklärungen die Pflichtangaben aus Art. 13/14 DSGVO noch
* nicht erfüllen und deshalb nicht veröffentlicht werden können.
*
* node scripts/check-completeness.js Zusammenfassung
* node scripts/check-completeness.js --list jede Erklärung einzeln
*
* Spiegelt die Regeln aus lib/policy-completeness.ts.
*/
const { PrismaClient } = require("@prisma/client")
const { readDefaults } = require("./lib/policy-defaults")
const prisma = new PrismaClient()
/** Angaben, die die Erklärung selbst mitbringen muss. */
const EIGENE = [
["title", "Titel", ""],
["processingDescription", "Bezeichnung der Verarbeitungstätigkeit", ""],
["processingPurposes", "Zweck der Datenerhebung", "Art. 13 Abs. 1 lit. c"],
["legalBasis", "Rechtsgrundlage", "Art. 13 Abs. 1 lit. c"],
["recipients", "Geplante Empfänger", "Art. 13 Abs. 1 lit. e"],
["storageDuration", "Speicherdauer", "Art. 13 Abs. 2 lit. a"],
]
/** Bausteine, die aus der Erklärung oder dem Standardtext kommen müssen. */
const BAUSTEINE = [
["responsible", "Verantwortlicher", "Art. 13 Abs. 1 lit. a"],
["contactDPO", "Datenschutzbeauftragte/r", "Art. 13 Abs. 1 lit. b"],
["dataSubjectsRights", "Betroffenen-Rechte", "Art. 13 Abs. 2 lit. b"],
["complaintRight", "Beschwerderecht", "Art. 13 Abs. 2 lit. d"],
["withdrawalRight", "Widerrufsrecht", "Art. 13 Abs. 2 lit. c"],
["objectionRight", "Widerspruchsrecht", "Art. 21 Abs. 4"],
["thirdCountryTransfer", "Übermittlung in Drittländer", "Art. 13 Abs. 1 lit. f"],
["automatedDecision", "Automatisierte Entscheidungsfindung", "Art. 13 Abs. 2 lit. f"],
]
const hat = (v) => typeof v === "string" && v.trim().length > 0
async function main() {
const list = process.argv.includes("--list")
const defaults = await readDefaults(prisma)
const policies = await prisma.privacyPolicy.findMany({ orderBy: { title: "asc" } })
const zaehler = new Map()
const unvollstaendig = []
for (const policy of policies) {
const fehlt = []
for (const [field, label, artikel] of EIGENE) {
if (!hat(policy[field])) fehlt.push({ label, artikel, quelle: "Erklärung" })
}
for (const [field, label, artikel] of BAUSTEINE) {
if (!hat(policy[field]) && !hat(defaults[field])) {
fehlt.push({ label, artikel, quelle: "Standardtext" })
}
}
// Art. 14 greift nur bei Erhebung aus anderer Quelle
if (hat(policy.dataSource) && !hat(policy.dataCategories)) {
fehlt.push({
label: "Kategorien der verarbeiteten Daten",
artikel: "Art. 14 Abs. 1 lit. d",
quelle: "Erklärung",
})
}
fehlt.forEach((f) => {
const key = `${f.label}|${f.artikel}|${f.quelle}`
zaehler.set(key, (zaehler.get(key) || 0) + 1)
})
if (fehlt.length) unvollstaendig.push({ policy, fehlt })
}
console.log(`Erklärungen gesamt: ${policies.length}`)
console.log(`Veröffentlichungsreif: ${policies.length - unvollstaendig.length}`)
console.log(`Unvollständig: ${unvollstaendig.length}`)
console.log(`Derzeit veröffentlicht: ${policies.filter((p) => p.isPublic).length}`)
console.log("")
console.log("=== Fehlende Angaben nach Häufigkeit ===")
const sortiert = [...zaehler.entries()].sort((a, b) => b[1] - a[1])
for (const [key, anzahl] of sortiert) {
const [label, artikel, quelle] = key.split("|")
const ort = quelle === "Standardtext" ? " [zentraler Standardtext]" : ""
console.log(` ${String(anzahl).padStart(4)}× ${label}${artikel ? ` (${artikel})` : ""}${ort}`)
}
if (list) {
console.log("")
console.log("=== Erklärungen mit fehlenden Angaben ===")
for (const { policy, fehlt } of unvollstaendig) {
const eigene = fehlt.filter((f) => f.quelle === "Erklärung")
if (!eigene.length) continue
console.log(` ${policy.title}`)
eigene.forEach((f) => console.log(` fehlt: ${f.label}${f.artikel ? ` (${f.artikel})` : ""}`))
}
}
}
main()
.catch((e) => {
console.error(e)
process.exitCode = 1
})
.finally(() => prisma.$disconnect())