feat(agent): 4-Status-Modell (NOT_APPLICABLE/INSUFFICIENT_EVIDENCE/POSSIBLY_APPLICABLE) für Impressum
Kanonisches Compliance-Datenmodell, Impressum-Agent als Referenz: - CheckStatus-Enum + Finding.status GETRENNT von severity (Verdikt ≠ Risiko) - Unbestimmte Rechtsform (weder Text noch Wizard) → INSUFFICIENT_EVIDENCE (INFO) statt hartem HIGH-FAIL; legal_form_dependent-Gate + detect_legal_form_present - §18-MStV-Graubereich (Corporate-Blog via has_editorial_content) → POSSIBLY_APPLICABLE (LOW Prüf-Hinweis); 3-stufig via scope_disposition - Recommendations nur aus echten FAILs; mc_insufficient/mc_possibly-Aggregate - Frontend: Verdikt-Pill + Coverage-Vokabular - 19 neue Tests (test_four_status.py, AgentFindingCard); CI-Suite 204 grün, v3 25 / GT 13 unverändert Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ import {
|
||||
METHODIK_SHORT,
|
||||
SEVERITY_BG,
|
||||
SEVERITY_COLOR,
|
||||
STATUS_LABEL,
|
||||
STATUS_STYLE,
|
||||
} from './_agentTypes'
|
||||
|
||||
export function AgentFindingCard({ f }: { f: Finding }) {
|
||||
@@ -32,6 +34,10 @@ export function AgentFindingCard({ f }: { f: Finding }) {
|
||||
const color = SEVERITY_COLOR[sev]
|
||||
const bg = SEVERITY_BG[sev]
|
||||
const sources = f.sources || []
|
||||
// Verdikt-Pill nur für Nicht-FAIL-Status (Applicability/Unknown) —
|
||||
// macht klar: kein Verstoß, sondern Hinweis/unbestimmt.
|
||||
const statusLabel = f.status ? STATUS_LABEL[f.status] : undefined
|
||||
const statusStyle = f.status ? STATUS_STYLE[f.status] : undefined
|
||||
return (
|
||||
<div
|
||||
className="rounded border-l-4 p-3 space-y-2"
|
||||
@@ -44,6 +50,14 @@ export function AgentFindingCard({ f }: { f: Finding }) {
|
||||
>
|
||||
{sev}
|
||||
</span>
|
||||
{statusLabel && statusStyle && (
|
||||
<span
|
||||
className="text-[10px] font-semibold px-1.5 py-0.5 rounded"
|
||||
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
)}
|
||||
<code className="text-[11px] text-gray-500">{f.check_id}</code>
|
||||
{sources.map((s, i) => (
|
||||
<MethodikBadge key={i} src={s.source_type} sourceId={s.source_id} />
|
||||
@@ -78,9 +92,12 @@ export function AgentFindingCard({ f }: { f: Finding }) {
|
||||
s.source_type === 'llm_cloud'
|
||||
)
|
||||
? 'Empfehlung (LLM-Vorschlag)'
|
||||
: sev === 'HIGH'
|
||||
? 'Pflicht-Maßnahme'
|
||||
: 'Best-Practice-Empfehlung'
|
||||
: f.status === 'insufficient_evidence' ||
|
||||
f.status === 'possibly_applicable'
|
||||
? 'Prüf-Hinweis'
|
||||
: sev === 'HIGH'
|
||||
? 'Pflicht-Maßnahme'
|
||||
: 'Best-Practice-Empfehlung'
|
||||
}
|
||||
tone="green"
|
||||
>
|
||||
|
||||
@@ -16,6 +16,8 @@ const STATUS_COLOR: Record<string, string> = {
|
||||
high: '#dc2626',
|
||||
medium: '#f59e0b',
|
||||
low: '#3b82f6',
|
||||
insufficient_evidence: '#64748b',
|
||||
possibly_applicable: '#ca8a04',
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
@@ -25,6 +27,8 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
high: 'HIGH',
|
||||
medium: 'MEDIUM',
|
||||
low: 'LOW',
|
||||
insufficient_evidence: 'unklar',
|
||||
possibly_applicable: 'evtl. relevant',
|
||||
}
|
||||
|
||||
export function AgentMcCoverage({ coverage }: { coverage: McCoverage[] }) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
import { AgentFindingCard } from '../AgentFindingCard'
|
||||
import type { Finding } from '../_agentTypes'
|
||||
|
||||
const BASE: Finding = {
|
||||
check_id: 'IMP-handelsregister', agent: 'impressum', agent_version: '3.0',
|
||||
field_id: 'handelsregister', severity: 'HIGH', title: 'X',
|
||||
norm: '§ 5 Abs. 1 Nr. 4 TMG', evidence: '', action: 'Tu etwas.',
|
||||
confidence: 0.4,
|
||||
sources: [{ source_type: 'regex', source_id: 'IMP-MC-004', confidence: 0.4 }],
|
||||
}
|
||||
|
||||
describe('AgentFindingCard — 4-Status', () => {
|
||||
it('INSUFFICIENT_EVIDENCE zeigt Verdikt-Pill + Prüf-Hinweis statt FAIL', () => {
|
||||
const f: Finding = {
|
||||
...BASE, status: 'insufficient_evidence', severity: 'INFO',
|
||||
title: 'Handelsregister-Eintrag: Rechtsform nicht erkennbar',
|
||||
}
|
||||
render(<AgentFindingCard f={f} />)
|
||||
expect(screen.getByText('Unzureichende Evidenz')).toBeInTheDocument()
|
||||
expect(screen.getByText('Prüf-Hinweis')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Pflicht-Maßnahme')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FAIL/HIGH zeigt KEINE Verdikt-Pill, aber Pflicht-Maßnahme', () => {
|
||||
const f: Finding = { ...BASE, status: 'fail', severity: 'HIGH' }
|
||||
render(<AgentFindingCard f={f} />)
|
||||
expect(screen.queryByText('Unzureichende Evidenz')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Pflicht-Maßnahme')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,15 @@
|
||||
|
||||
export type Severity = 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
|
||||
|
||||
// Verdikt eines Checks — getrennt vom Risiko (severity).
|
||||
// Applicability ≠ Compliance · Unknown ≠ Fail.
|
||||
export type CheckStatus =
|
||||
| 'pass'
|
||||
| 'fail'
|
||||
| 'not_applicable'
|
||||
| 'insufficient_evidence'
|
||||
| 'possibly_applicable'
|
||||
|
||||
export type SourceType =
|
||||
| 'mc'
|
||||
| 'regex'
|
||||
@@ -31,6 +40,7 @@ export interface Finding {
|
||||
agent: string
|
||||
agent_version: string
|
||||
field_id?: string
|
||||
status?: CheckStatus
|
||||
severity: Severity
|
||||
severity_reason?: string
|
||||
title: string
|
||||
@@ -52,7 +62,8 @@ export interface Recommendation {
|
||||
|
||||
export interface McCoverage {
|
||||
mc_id: string
|
||||
status: 'ok' | 'na' | 'high' | 'medium' | 'low' | 'skipped'
|
||||
status: 'ok' | 'na' | 'high' | 'medium' | 'low' | 'skipped' |
|
||||
'insufficient_evidence'
|
||||
reason?: string
|
||||
}
|
||||
|
||||
@@ -79,6 +90,8 @@ export interface SlotOutput {
|
||||
mc_high: number
|
||||
mc_medium: number
|
||||
mc_low: number
|
||||
mc_insufficient?: number
|
||||
mc_possibly?: number
|
||||
duration_ms: number
|
||||
confidence: number
|
||||
notes?: string
|
||||
@@ -152,6 +165,21 @@ export const SEVERITY_BG: Record<Severity, string> = {
|
||||
INFO: '#f8fafc',
|
||||
}
|
||||
|
||||
// Verdikt-Pill — nur für die Nicht-FAIL-Status (FAIL trägt die Severity).
|
||||
export const STATUS_LABEL: Partial<Record<CheckStatus, string>> = {
|
||||
not_applicable: 'Nicht anwendbar',
|
||||
insufficient_evidence: 'Unzureichende Evidenz',
|
||||
possibly_applicable: 'Evtl. relevant',
|
||||
}
|
||||
|
||||
export const STATUS_STYLE: Partial<
|
||||
Record<CheckStatus, { bg: string; fg: string }>
|
||||
> = {
|
||||
not_applicable: { bg: '#f1f5f9', fg: '#64748b' },
|
||||
insufficient_evidence: { bg: '#e2e8f0', fg: '#475569' },
|
||||
possibly_applicable: { bg: '#fef9c3', fg: '#854d0e' },
|
||||
}
|
||||
|
||||
// Ein Output gilt als "übersprungen" (Dokument nicht ladbar), wenn MCs
|
||||
// existieren, aber keiner ausgewertet wurde.
|
||||
export function isOutputSkipped(o: SlotOutput): boolean {
|
||||
|
||||
Reference in New Issue
Block a user