feat(qa): recital detection, review split, duplicate comparison
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 42s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped

Add _detect_recital() to QA pipeline — flags controls where
source_original_text contains Erwägungsgrund markers instead of
article text (28% of controls with source text affected).

- Recital detection via regex + phrase matching in QA validation
- 10 new tests (TestRecitalDetection), 81 total
- ReviewCompare component for side-by-side duplicate comparison
- Review mode split: Duplikat-Verdacht vs Rule-3-ohne-Anchor tabs
- MkDocs: recital detection documentation
- Detection script for bulk analysis (scripts/find_recital_controls.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-18 08:20:02 +01:00
parent a9e0869205
commit 148c7ba3af
7 changed files with 657 additions and 28 deletions

View File

@@ -14,6 +14,7 @@ import {
} from './components/helpers'
import { ControlForm } from './components/ControlForm'
import { ControlDetail } from './components/ControlDetail'
import { ReviewCompare } from './components/ReviewCompare'
import { GeneratorModal } from './components/GeneratorModal'
// =============================================================================
@@ -71,6 +72,9 @@ export default function ControlLibraryPage() {
const [reviewIndex, setReviewIndex] = useState(0)
const [reviewItems, setReviewItems] = useState<CanonicalControl[]>([])
const [reviewCount, setReviewCount] = useState(0)
const [reviewTab, setReviewTab] = useState<'duplicates' | 'rule3'>('duplicates')
const [reviewDuplicates, setReviewDuplicates] = useState<CanonicalControl[]>([])
const [reviewRule3, setReviewRule3] = useState<CanonicalControl[]>([])
// Debounce search
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -303,20 +307,47 @@ export default function ControlLibraryPage() {
const enterReviewMode = async () => {
// Load review items from backend
try {
const res = await fetch(`${BACKEND_URL}?endpoint=controls&release_state=needs_review&limit=200`)
const res = await fetch(`${BACKEND_URL}?endpoint=controls&release_state=needs_review&limit=1000`)
if (res.ok) {
const items = await res.json()
const items: CanonicalControl[] = await res.json()
if (items.length > 0) {
setReviewItems(items)
// Split into duplicate suspects vs rule 3 without anchor
const dupes = items.filter(c =>
c.generation_metadata?.similar_controls &&
Array.isArray(c.generation_metadata.similar_controls) &&
(c.generation_metadata.similar_controls as unknown[]).length > 0
)
const rule3 = items.filter(c =>
!c.generation_metadata?.similar_controls ||
!Array.isArray(c.generation_metadata.similar_controls) ||
(c.generation_metadata.similar_controls as unknown[]).length === 0
)
setReviewDuplicates(dupes)
setReviewRule3(rule3)
// Start with duplicates tab if any, otherwise rule3
const startTab = dupes.length > 0 ? 'duplicates' : 'rule3'
const startItems = startTab === 'duplicates' ? dupes : rule3
setReviewTab(startTab)
setReviewItems(startItems)
setReviewMode(true)
setReviewIndex(0)
setSelectedControl(items[0])
setSelectedControl(startItems[0])
setMode('detail')
}
}
} catch { /* ignore */ }
}
const switchReviewTab = (tab: 'duplicates' | 'rule3') => {
const items = tab === 'duplicates' ? reviewDuplicates : reviewRule3
setReviewTab(tab)
setReviewItems(items)
setReviewIndex(0)
if (items.length > 0) {
setSelectedControl(items[0])
}
}
// Loading
if (loading && controls.length === 0) {
return (
@@ -363,28 +394,89 @@ export default function ControlLibraryPage() {
// DETAIL MODE
if (mode === 'detail' && selectedControl) {
const isDuplicateReview = reviewMode && reviewTab === 'duplicates'
// Review tab bar (shown above the detail/compare view in review mode)
const reviewTabBar = reviewMode ? (
<div className="border-b border-gray-200 bg-white px-6 py-2 flex items-center gap-4">
<button
onClick={() => switchReviewTab('duplicates')}
className={`px-3 py-1.5 text-sm rounded-lg font-medium ${
reviewTab === 'duplicates'
? 'bg-amber-100 text-amber-800 border border-amber-300'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
>
Duplikat-Verdacht ({reviewDuplicates.length})
</button>
<button
onClick={() => switchReviewTab('rule3')}
className={`px-3 py-1.5 text-sm rounded-lg font-medium ${
reviewTab === 'rule3'
? 'bg-purple-100 text-purple-800 border border-purple-300'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
>
Rule 3 ohne Anchor ({reviewRule3.length})
</button>
</div>
) : null
if (isDuplicateReview) {
return (
<div className="flex flex-col h-full">
{reviewTabBar}
<div className="flex-1 overflow-hidden">
<ReviewCompare
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onReview={handleReview}
onEdit={() => setMode('edit')}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
/>
</div>
</div>
)
}
return (
<ControlDetail
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onEdit={() => setMode('edit')}
onDelete={handleDelete}
onReview={handleReview}
onRefresh={fullReload}
reviewMode={reviewMode}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
/>
<div className="flex flex-col h-full">
{reviewTabBar}
<div className="flex-1 overflow-hidden">
<ControlDetail
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onEdit={() => setMode('edit')}
onDelete={handleDelete}
onReview={handleReview}
onRefresh={fullReload}
reviewMode={reviewMode}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
/>
</div>
</div>
)
}