feat: control_parent_links population + traceability API + frontend

- _write_atomic_control() now uses RETURNING id and inserts into
  control_parent_links (M:N) with source_regulation, source_article,
  and obligation_candidate_id parsed from parent's source_citation
- New _parse_citation() helper for JSONB source_citation extraction
- New GET /controls/{id}/traceability endpoint returning full chain:
  parent links with obligations, child controls, source_count
- Backend: control_type filter (atomic/rich) for controls + count
- Frontend: Rechtsgrundlagen section in ControlDetail showing all
  parent links per source regulation with obligation text + strength
- Frontend: Atomic/Rich filter dropdown in Control Library list
- Frontend: GenerationStrategyBadge recognizes 'pass0b' strategy
- Tests: 3 new tests for parent_link creation + citation parsing,
  existing batch test mock updated for RETURNING clause

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-23 08:14:29 +01:00
parent 0027f78fc5
commit ac6134ce6d
7 changed files with 511 additions and 43 deletions

View File

@@ -27,7 +27,7 @@ export async function GET(request: NextRequest) {
case 'controls': {
const controlParams = new URLSearchParams()
const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category',
'target_audience', 'source', 'search', 'sort', 'order', 'limit', 'offset']
'target_audience', 'source', 'search', 'control_type', 'sort', 'order', 'limit', 'offset']
for (const key of passthrough) {
const val = searchParams.get(key)
if (val) controlParams.set(key, val)
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest) {
case 'controls-count': {
const countParams = new URLSearchParams()
const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category',
'target_audience', 'source', 'search']
'target_audience', 'source', 'search', 'control_type']
for (const key of countPassthrough) {
const val = searchParams.get(key)
if (val) countParams.set(key, val)
@@ -99,6 +99,15 @@ export async function GET(request: NextRequest) {
backendPath = '/api/compliance/v1/canonical/categories'
break
case 'traceability': {
const traceId = searchParams.get('id')
if (!traceId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(traceId)}/traceability`
break
}
case 'similar': {
const simControlId = searchParams.get('id')
if (!simControlId) {