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
@@ -0,0 +1,144 @@
'use client'
/**
* DocResultView — EIN Dokument-Prüfergebnis der HAUPT-Engine als saubere,
* immer-offene Pflichtangaben-Tabelle: Verdikt + Gruppen + extrahierte Texte
* (matched_text) pro Prüfpunkt.
*
* Quelle = result.results[doc] (die genaue Haupt-Doc-Check-Engine), NICHT
* der v3-Agent. Zeigt menschliche Labels + gefundene Snippets, keine internen
* IDs. Wiederverwendet die Render-Bausteine aus ChecklistView.
*/
import React from 'react'
import {
CheckIcon,
type DocResult,
groupChecks,
SCENARIO_LABELS,
} from './ChecklistView'
function Snippet({ text }: { text: string }) {
return (
<div className="text-xs text-gray-500 mt-0.5 font-mono break-words">
{text}"
</div>
)
}
function ScoreBar({ label, pct, blue }: { label: string; pct: number; blue?: boolean }) {
const color = blue
? pct >= 80 ? 'bg-blue-400' : 'bg-blue-300'
: pct === 100 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500'
return (
<div className="flex items-center gap-1.5">
<span className="text-[10px] text-gray-400">{label}</span>
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${color}`} style={{ width: `${pct}%` }} />
</div>
<span className="text-gray-600 w-9 text-right">{pct}%</span>
</div>
)
}
export function DocResultView({ doc }: { doc: DocResult }) {
if (doc.error) {
return (
<div className="text-sm text-amber-700 bg-amber-50 rounded p-3">
{doc.error}
</div>
)
}
const grouped = groupChecks(doc.checks)
const l1 = doc.checks.filter(c => (c.level ?? 1) === 1)
const l1Score = l1.filter(c => c.severity !== 'INFO')
const l1Passed = l1Score.filter(c => c.passed).length
const l2 = doc.checks.filter(c => (c.level ?? 1) === 2 && !c.skipped)
const l2Passed = l2.filter(c => c.passed).length
const sc = doc.scenario ? SCENARIO_LABELS[doc.scenario] : null
return (
<div className="space-y-3">
{/* Verdikt-Kopf */}
<div className="flex items-center flex-wrap gap-3 border rounded-lg px-4 py-3 bg-slate-50">
{sc && (
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${sc.bg} ${sc.color}`}>
{sc.label}
</span>
)}
<span className="text-sm text-gray-700">
{l1Passed}/{l1Score.length} Pflichtangaben
{l2.length > 0 && <>, {l2Passed}/{l2.length} Detailprüfungen</>}
</span>
<div className="flex gap-3 ml-auto">
<ScoreBar label="Pflicht" pct={doc.completeness_pct} />
{l2.length > 0 && (
<ScoreBar label="Detail" pct={doc.correctness_pct ?? 0} blue />
)}
</div>
</div>
{/* Pflichtangaben-Tabelle */}
<div className="border rounded-lg divide-y divide-gray-100">
{grouped.map(g => {
const l1Info = g.check.severity === 'INFO' && !g.check.passed
return (
<div key={g.check.id} className="px-4 py-2">
<div className="flex items-start gap-2">
<CheckIcon passed={g.check.passed} isInfo={l1Info} />
<div className="flex-1 min-w-0">
<div className={`text-sm ${
g.check.passed ? 'text-gray-800'
: l1Info ? 'text-gray-500' : 'text-red-700 font-medium'
}`}>
{g.check.label}
</div>
{g.check.passed && g.check.matched_text && g.children.length === 0 && (
<Snippet text={g.check.matched_text} />
)}
{!g.check.passed && g.check.hint && (
<div className={`text-xs mt-0.5 ${l1Info ? 'text-gray-400' : 'text-red-600/80'}`}>
{g.check.hint}
</div>
)}
</div>
</div>
{g.children.length > 0 && (
<div className="ml-6 mt-1 space-y-1 border-l-2 border-gray-200 pl-3">
{g.children.map(ch => {
const chInfo = ch.severity === 'INFO' && !ch.passed && !ch.skipped
return (
<div key={ch.id} className="flex items-start gap-2">
<CheckIcon passed={ch.passed} skipped={ch.skipped} isInfo={chInfo} />
<div className="flex-1 min-w-0">
<div className={`text-xs ${
ch.skipped ? 'text-gray-400 italic'
: ch.passed ? 'text-gray-600'
: chInfo ? 'text-gray-400' : 'text-red-600 font-medium'
}`}>
{ch.label}{ch.skipped && ' (übersprungen)'}
</div>
{ch.passed && ch.matched_text && <Snippet text={ch.matched_text} />}
{!ch.passed && !ch.skipped && ch.hint && (
<div className={`text-xs mt-0.5 ${chInfo ? 'text-gray-400' : 'text-red-500/80'}`}>
{ch.hint}
</div>
)}
</div>
</div>
)
})}
</div>
)}
</div>
)
})}
</div>
{doc.word_count > 0 && (
<div className="text-xs text-gray-400">{doc.word_count} Wörter analysiert</div>
)}
</div>
)
}