feat(agent): Impressum-Tab auf Haupt-Engine + Profil/§36-Fixes

Ergebnis-Tab rendert jetzt result.results (Haupt-Doc-Check) statt des
abweichenden v3-Agenten — BMW korrekt statt False Positives:
- DocResultView: ein Dokument als Pflichtangaben-Tabelle (Label + gefundener
  Text + 3-Tier-Status), KEINE MC-IDs. ComplianceResultTabs speist Tabs aus
  result.results; ChecklistView-Bausteine exportiert + wiederverwendet.
- profile_extractor: Firmenname/Rechtsform = fruehester Treffer + ausge-
  schriebene Formen (Aktiengesellschaft) -> BMW AG statt "juris GmbH".
- 36 VSBG (MC-010): reines b2c -> POSSIBLY_APPLICABLE (Pruef-Hinweis) statt
  MEDIUM-FAIL; hart nur bei ecommerce. possibly_hint pro MC.
- McCoverage traegt label + found (Snippet); mc_possibly-Aggregat.
- AgentFindingCard/Methodik: interne check_id/mc_id nicht mehr angezeigt.

Tests: test_four_status (16) + Frontend-Vitest gruen; CI-Suite 206, v3/GT
unveraendert. Nur eigene Dateien (geteilter Tree).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-10 23:44:01 +02:00
parent a7dc12f30f
commit 3f23a64d5f
15 changed files with 450 additions and 187 deletions
@@ -10,13 +10,14 @@ Returns a dict that maps to CompanyProfile and ScopeProfilingAnswer fields.
import logging
import re
from typing import Optional
logger = logging.getLogger(__name__)
def extract_profile_from_documents(
doc_texts: dict[str, str],
business_profile: dict | None = None,
business_profile: Optional[dict] = None,
) -> dict:
"""Extract Company Profile fields from document texts.
@@ -100,28 +101,38 @@ def _extract_company_info(text: str, result: dict) -> None:
"""Extract company name, legal form, address from text."""
cp = result["company_profile"]
# GmbH / AG / UG / e.K. etc.
legal_forms = {
r"(\S+(?:\s+\S+){0,4})\s+gmbh\b": ("GmbH", "gmbh"),
r"(\S+(?:\s+\S+){0,4})\s+ag\b": ("AG", "ag"),
r"(\S+(?:\s+\S+){0,4})\s+ug\b": ("UG", "ug"),
r"(\S+(?:\s+\S+){0,4})\s+e\.?\s*k\.?\b": ("e.K.", "ek"),
r"(\S+(?:\s+\S+){0,4})\s+gbr\b": ("GbR", "gbr"),
r"(\S+(?:\s+\S+){0,4})\s+ohg\b": ("OHG", "ohg"),
r"(\S+(?:\s+\S+){0,4})\s+gmbh\s*&\s*co\.?\s*kg": ("GmbH & Co. KG", "gmbh_co_kg"),
}
# Rechtsform + Firmenname. Die Reihenfolge der Muster ist NICHT die
# Priorität — wir nehmen den FRUEHESTEN Treffer im Text: ein Impressum
# nennt den Betreiber zuerst; spätere Erwähnungen (z.B. "juris GmbH" im
# Hinweis auf gesetze-im-internet.de) sind nicht der Anbieter. Ausge-
# schriebene Formen ("Aktiengesellschaft") zählen mit (sonst wird BMW AG
# nicht erkannt und faelschlich die naechste GmbH gegriffen).
legal_forms = [
(r"(\S+(?:\s+\S+){0,4})\s+gmbh\s*&\s*co\.?\s*kg\b", "gmbh_co_kg"),
(r"(\S+(?:\s+\S+){0,4})\s+(?:aktiengesellschaft|ag)\b", "ag"),
(r"(\S+(?:\s+\S+){0,4})\s+(?:unternehmergesellschaft|ug)\b", "ug"),
(r"(\S+(?:\s+\S+){0,4})\s+gmbh\b", "gmbh"),
(r"(\S+(?:\s+\S+){0,4})\s+e\.?\s*k\.?\b", "ek"),
(r"(\S+(?:\s+\S+){0,4})\s+gbr\b", "gbr"),
(r"(\S+(?:\s+\S+){0,4})\s+ohg\b", "ohg"),
]
text_lower = text.lower()
for pattern, (form_label, form_id) in legal_forms.items():
best = None # (start, end, form_id) — frühester Treffer
for pattern, form_id in legal_forms:
m = re.search(pattern, text_lower)
if m:
raw_name = m.group(0).strip()
# Clean up: take from uppercase start
for i, ch in enumerate(text[m.start():m.end()]):
if ch.isupper():
cp["companyName"] = text[m.start() + i:m.end()].strip()
break
cp["legalForm"] = form_id
break
# frühester Treffer gewinnt; bei Gleichstand die Listen-Reihenfolge
# (GmbH & Co. KG vor GmbH).
if m and (best is None or m.start() < best[0]):
best = (m.start(), m.end(), form_id)
if best:
start, end, form_id = best
# Firmenname ab dem ersten Grossbuchstaben im Treffer (schneidet
# führende Kleinwörter wie "von der" ab).
for i, ch in enumerate(text[start:end]):
if ch.isupper():
cp["companyName"] = text[start + i:end].strip()
break
cp["legalForm"] = form_id
# PLZ + Ort
plz_match = re.search(