feat!: Produktwechsel zu KI-Antragsprüfung — Phase 1 (Datenmodell, Regelwerk, Katalog)

Deklarix war eine Pre-Publish-Kennzeichnungsprüfung für Werbe-Content
(UWG/MStV). Dieser Scope wird komplett verworfen und durch eine
KI-Antragsprüfung ersetzt: Mitarbeitende beschreiben ein KI-Vorhaben,
das System leitet Datenklasse und KI-VO-Einstufung ab, gleicht sie
gegen einen Werkzeugkatalog ab und erzeugt einen Entscheidungsvorschlag
mit Herleitung — ein Mensch entscheidet, das System bereitet nur vor.

BREAKING CHANGE: Migration 0008 droppt alle werberechtsspezifischen
Tabellen (submission, finding, extraction, evidence_package,
participant, platform_connection, asset). account/app_user/session/
audit_log bleiben (Mandantentrennung, Login, Protokollierung sind
produktunabhängig) — app_user.role wechselt von
creator/agentur/marke/kanzlei/admin zu den fünf neuen Rollen
mitarbeiter/verantwortlicher/pruefer/admin/betreiber (vier
Mandanten-Rollen + eine plattformweite, siehe CLAUDE.md).

Entfernt: internal/extract, internal/dossier, internal/evidence,
internal/socialconnect, alte rules/*.yaml (UWG-Regeln), testdata/golden
— alles ausschließlich für das alte Produkt.

Neu, Phase 1 der Baureihenfolge ("Datenmodell, Regelwerk als YAML,
Katalogstruktur"):
- Store: abteilung (Stammdaten), werkzeug + werkzeug_sperre (der
  eigentliche Wert des Produkts — zentral gepflegter Katalog mit
  mandantenspezifischen Ergänzungen/Sperrungen, Pflichtfelder
  letzte_pruefung/quelle für jede Zusicherung), antrag (Fragebogen-
  Grundgerüst, Antworten als JSONB für den adaptiven Fragebogen aus
  Phase 2).
- internal/rules komplett neu: lädt und validiert drei YAML-
  Regelwerke (Datenklasse-Ableitung, KI-VO-Einstufung, Anforderungs-
  profil) aus rules/*.yaml — noch ohne Auswertungslogik gegen echte
  Fragebogen-Antworten (das ist Phase 3, bewusst erst nach dem
  Fragebogen aus Phase 2, der die exakten Fakten-Feldnamen festlegt).
  Offene fachliche Annahmen (Rangfolge der Datenklassen, Fragebogen-
  Lücke für die "verboten"-Varianten) explizit in rules/OPEN.md
  dokumentiert statt geraten.
- Web-Layer auf Minimalgerüst reduziert, das kompiliert und die neue
  Rollenwelt trägt: Firma-Registrierung (Ebene 1, erster Nutzer wird
  admin), Login/Logout, Plattform-Bereich (Ebene 5, nur betreiber:
  Dashboard, Accounts-Übersicht, Audit-Log) — Fragebogen (Ebene 2) und
  Fachebene (Ebene 3) folgen in den nächsten Phasen.
- CLAUDE.md komplett neu geschrieben: Produktbeschreibung, Fünf-Ebenen-
  Rollenmodell, Fragebogen-Spezifikation, Ableitungstabellen,
  Werkzeugkatalog, Bewertungslogik (geplant), Onboarding, offene
  Punkte (u. a. Postgres-RLS-Frage aus der Frontend-Spezifikation
  noch nicht entschieden, "Admin und Verantwortlicher gleichzeitig"
  beim Onboarding noch nicht datenmodelliert).

Volle Testsuite inkl. echter Postgres-Tests grün. End-to-End gegen
einen laufenden Server verifiziert: Firma-Registrierung legt Account +
admin-Nutzer an, Betreiber-Login leitet zu /betreiber, mandanten-
übergreifende Accounts-Liste sichtbar für betreiber, 404 für
mitarbeiter auf /betreiber, 303 zu /login ohne Sitzung.
This commit is contained in:
noroot
2026-08-28 21:39:04 +02:00
parent 6fd7831784
commit b4d4ee8d3c
96 changed files with 2348 additions and 7612 deletions

View File

@@ -0,0 +1,104 @@
-- Best-effort-Rückbau auf die Schema-Form des alten Werberecht-Produkts
-- (Stand nach Migration 0007) — reine Struktur, keine Daten. Ein
-- kompletter Produktwechsel wird in der Praxis nicht zurückgerollt,
-- diese Datei existiert nur, damit "migrate down" nicht bricht.
DROP TABLE IF EXISTS antrag;
DROP TABLE IF EXISTS werkzeug_sperre;
DROP TABLE IF EXISTS werkzeug;
DROP TABLE IF EXISTS abteilung;
ALTER TABLE app_user DROP CONSTRAINT app_user_role_check;
ALTER TABLE app_user ADD CONSTRAINT app_user_role_check
CHECK (role IN ('creator', 'agentur', 'marke', 'kanzlei', 'admin'));
ALTER TABLE account ADD COLUMN verified BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE submission (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform TEXT NOT NULL CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'linkedin')),
post_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'checked', 'published', 'archived')),
caption TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
account_id UUID NOT NULL REFERENCES account (id)
);
CREATE TABLE asset (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES submission (id),
kind TEXT NOT NULL CHECK (kind IN ('image', 'video', 'file')),
path TEXT NOT NULL,
sha256 TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
purpose TEXT NOT NULL DEFAULT 'initial' CHECK (purpose IN ('initial', 'insights'))
);
CREATE TRIGGER asset_append_only
BEFORE UPDATE OR DELETE ON asset
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
CREATE TABLE extraction (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES submission (id),
payload JSONB NOT NULL,
model_version TEXT NOT NULL,
prompt_version TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER extraction_append_only
BEFORE UPDATE OR DELETE ON extraction
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
CREATE TABLE finding (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES submission (id),
extraction_id UUID REFERENCES extraction (id),
rule_id TEXT NOT NULL,
rule_version INTEGER NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('niedrig', 'mittel', 'hoch')),
supersedes UUID REFERENCES finding (id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
title TEXT NOT NULL,
fix TEXT NOT NULL,
sources TEXT[] NOT NULL DEFAULT '{}'
);
CREATE INDEX finding_supersedes_idx ON finding (supersedes) WHERE supersedes IS NOT NULL;
CREATE TRIGGER finding_append_only
BEFORE UPDATE OR DELETE ON finding
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
CREATE TABLE evidence_package (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES submission (id),
dossier_path TEXT NOT NULL,
sha256 TEXT NOT NULL,
timestamp_token BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER evidence_package_append_only
BEFORE UPDATE OR DELETE ON evidence_package
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
CREATE TABLE participant (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES submission (id),
role TEXT NOT NULL CHECK (role IN ('creator', 'agentur', 'marke', 'kanzlei')),
name TEXT NOT NULL,
vorgegeben BOOLEAN NOT NULL DEFAULT false,
freigegeben BOOLEAN NOT NULL DEFAULT false,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE platform_connection (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES account (id),
platform TEXT NOT NULL CHECK (platform IN ('instagram', 'tiktok')),
platform_user_id TEXT NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL DEFAULT '',
expires_at TIMESTAMPTZ,
connected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (account_id, platform)
);

View File

@@ -0,0 +1,104 @@
-- Produktwechsel: Deklarix war eine Kennzeichnungsprüfung für
-- Werbe-Content (UWG/MStV), wird jetzt eine KI-Antragsprüfung
-- (Fragebogen -> Datenklasse/KI-VO-Einstufung -> Werkzeug-Katalog ->
-- Entscheidung). Alles, was ausschließlich für das alte Werberecht-
-- Produkt existierte, wird entfernt. account/app_user/session/
-- audit_log bleiben — Mandantentrennung, Login und
-- Protokollierungsprinzip sind produktunabhängig.
DROP TABLE IF EXISTS platform_connection;
DROP TABLE IF EXISTS asset;
DROP TABLE IF EXISTS participant;
DROP TABLE IF EXISTS evidence_package;
DROP TABLE IF EXISTS finding;
DROP TABLE IF EXISTS extraction;
DROP TABLE IF EXISTS submission;
-- "verified"/Kanzlei-Verzeichnis gab es nur für die alte Berufsrecht-
-- Sonderrolle "kanzlei".
ALTER TABLE account DROP COLUMN IF EXISTS verified;
-- Fünf Rollen (Ebenen 2-5 der Frontend-Spezifikation):
-- mitarbeiter Ebene 2 — stellt Anträge, sieht nur eigene
-- verantwortlicher Ebene 3 — KI-Verantwortlicher, volles Entscheidungsrecht
-- pruefer Ebene 3 — identische Sicht wie verantwortlicher, aber
-- ohne Entscheidungsrecht (reine Prüfsicht)
-- admin Ebene 4 — Mandanten-Verwaltung (Nutzer, Abteilungen,
-- Anmeldeverfahren, eigene Werkzeug-Freigaben) — bezogen
-- auf GENAU EINEN Mandanten, nicht plattformweit
-- betreiber Ebene 5 — Netcell-IT-Personal, plattformweit
-- (Werkzeugkatalog, Regelwerk, Mandantenverwaltung);
-- entspricht der alten "admin"-Rolle vor dieser Migration
-- Die alten Rollen (creator/agentur/marke/kanzlei) haben im neuen
-- Produkt keine Bedeutung mehr.
ALTER TABLE app_user DROP CONSTRAINT app_user_role_check;
ALTER TABLE app_user ADD CONSTRAINT app_user_role_check
CHECK (role IN ('mitarbeiter', 'verantwortlicher', 'pruefer', 'admin', 'betreiber'));
-- Stammdaten: Abteilungen zur Auswahl im Fragebogen (Feld A.abteilung).
CREATE TABLE abteilung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES account (id),
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (account_id, name)
);
-- Werkzeugkatalog: der eigentliche Wert des Produkts. account_id NULL
-- markiert einen zentral gepflegten, für alle Mandanten identischen
-- Katalogeintrag; ein gesetzter account_id ist eine mandantenspezifische
-- Ergänzung (siehe Spezifikation "jeder Mandant kann zusätzlich eigene
-- Einträge ... führen"). letzte_pruefung und quelle sind Pflicht — jede
-- Zusicherung im Katalog muss belegbar sein, siehe CLAUDE.md.
CREATE TABLE werkzeug (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID REFERENCES account (id),
name TEXT NOT NULL,
anbieter TEXT NOT NULL,
verarbeitungsort TEXT NOT NULL CHECK (verarbeitungsort IN ('EU', 'USA', 'gemischt', 'on-prem')),
avv_verfuegbar BOOLEAN NOT NULL DEFAULT false,
avv_url TEXT NOT NULL DEFAULT '',
training_opt_out BOOLEAN NOT NULL DEFAULT false,
training_standard BOOLEAN NOT NULL DEFAULT false,
aufbewahrung_tage INTEGER NOT NULL DEFAULT 0,
zertifizierungen TEXT[] NOT NULL DEFAULT '{}',
geeignete_zwecke TEXT[] NOT NULL DEFAULT '{}',
einschraenkungen TEXT[] NOT NULL DEFAULT '{}',
letzte_pruefung TIMESTAMPTZ NOT NULL,
quelle TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Ein Mandant kann einen (auch zentralen) Katalogeintrag für sich
-- sperren, ohne den zentralen Katalog selbst zu verändern.
CREATE TABLE werkzeug_sperre (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES account (id),
werkzeug_id UUID NOT NULL REFERENCES werkzeug (id),
grund TEXT NOT NULL,
gesperrt_am TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (account_id, werkzeug_id)
);
-- Antrag: das Vorhaben aus Fragebogen-Abschnitt A, plus die vollständigen
-- Antworten aus B/C/D als JSON (adaptiver Fragebogen — welche Folgefragen
-- beantwortet wurden, hängt von vorherigen Antworten ab, ein starres
-- Spaltenschema würde das nicht abbilden). status ist eine normale
-- Zustandsänderung (wie submission es früher war), kein Beweis-Eintrag.
CREATE TABLE antrag (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES account (id),
ersteller_user_id UUID NOT NULL REFERENCES app_user (id),
abteilung_id UUID REFERENCES abteilung (id),
titel TEXT NOT NULL,
beschreibung TEXT NOT NULL DEFAULT '',
ergebnis TEXT NOT NULL DEFAULT '',
haeufigkeit TEXT NOT NULL DEFAULT 'einmalig'
CHECK (haeufigkeit IN ('einmalig', 'gelegentlich', 'taeglich', 'automatisiert')),
antworten JSONB NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'entwurf'
CHECK (status IN ('entwurf', 'eingereicht', 'entschieden')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);