feat: Dreistufenmodell normative Verbindlichkeit + Duplikat-Filter + Auto-Deploy
- Source-Type-Klassifikation (58 Regulierungen: law/guideline/framework) - Backfill-Endpoint POST /controls/backfill-normative-strength - exclude_duplicates Filter fuer Control-Library (Backend + Proxy + UI-Toggle) - MkDocs-Kapitel: Normative Verbindlichkeit mit Mermaid-Diagrammen - scripts/deploy.sh: Auto-Push + Mac Mini rebuild + Coolify health monitoring - 26 Unit Tests fuer Klassifikations-Logik Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -316,6 +316,7 @@ async def list_controls(
|
||||
source: Optional[str] = Query(None, description="Filter by source_citation->source"),
|
||||
search: Optional[str] = Query(None, description="Full-text search in control_id, title, objective"),
|
||||
control_type: Optional[str] = Query(None, description="Filter: atomic, rich, or all"),
|
||||
exclude_duplicates: bool = Query(False, description="Exclude controls with release_state='duplicate'"),
|
||||
sort: Optional[str] = Query("control_id", description="Sort field: control_id, created_at, severity"),
|
||||
order: Optional[str] = Query("asc", description="Sort order: asc or desc"),
|
||||
limit: Optional[int] = Query(None, ge=1, le=5000, description="Max results"),
|
||||
@@ -329,6 +330,9 @@ async def list_controls(
|
||||
"""
|
||||
params: dict[str, Any] = {}
|
||||
|
||||
if exclude_duplicates:
|
||||
query += " AND release_state != 'duplicate'"
|
||||
|
||||
if severity:
|
||||
query += " AND severity = :sev"
|
||||
params["sev"] = severity
|
||||
@@ -398,11 +402,15 @@ async def count_controls(
|
||||
source: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
control_type: Optional[str] = Query(None),
|
||||
exclude_duplicates: bool = Query(False, description="Exclude controls with release_state='duplicate'"),
|
||||
):
|
||||
"""Count controls matching filters (for pagination)."""
|
||||
query = "SELECT count(*) FROM canonical_controls WHERE 1=1"
|
||||
params: dict[str, Any] = {}
|
||||
|
||||
if exclude_duplicates:
|
||||
query += " AND release_state != 'duplicate'"
|
||||
|
||||
if severity:
|
||||
query += " AND severity = :sev"
|
||||
params["sev"] = severity
|
||||
@@ -908,6 +916,107 @@ async def get_control_provenance(control_id: str):
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# NORMATIVE STRENGTH BACKFILL
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/controls/backfill-normative-strength")
|
||||
async def backfill_normative_strength(
|
||||
dry_run: bool = Query(True, description="Nur zaehlen, nicht aendern"),
|
||||
):
|
||||
"""
|
||||
Korrigiert normative_strength auf obligation_candidates basierend auf
|
||||
dem source_type der Quell-Regulierung.
|
||||
|
||||
Dreistufiges Modell:
|
||||
- law (Gesetz): normative_strength bleibt unveraendert
|
||||
- guideline (Leitlinie): max 'should'
|
||||
- framework (Framework): max 'can'
|
||||
|
||||
Fuer Controls mit mehreren Parent-Links gilt der hoechste source_type.
|
||||
"""
|
||||
from compliance.data.source_type_classification import (
|
||||
classify_source_regulation,
|
||||
get_highest_source_type,
|
||||
cap_normative_strength,
|
||||
)
|
||||
|
||||
with SessionLocal() as db:
|
||||
# 1. Alle Obligations mit ihren Parent-Control-Links laden
|
||||
obligations = db.execute(text("""
|
||||
SELECT oc.id, oc.candidate_id, oc.normative_strength,
|
||||
oc.parent_control_uuid
|
||||
FROM obligation_candidates oc
|
||||
WHERE oc.release_state NOT IN ('rejected', 'merged')
|
||||
AND oc.normative_strength IS NOT NULL
|
||||
ORDER BY oc.candidate_id
|
||||
""")).fetchall()
|
||||
|
||||
# 2. Fuer jeden Parent Control die source_regulations sammeln
|
||||
parent_uuids = list({str(o.parent_control_uuid) for o in obligations if o.parent_control_uuid})
|
||||
source_types_by_parent: dict[str, list[str]] = {}
|
||||
|
||||
if parent_uuids:
|
||||
# Batch-Query fuer alle Parent-Links
|
||||
links = db.execute(text("""
|
||||
SELECT control_uuid::text, source_regulation
|
||||
FROM control_parent_links
|
||||
WHERE control_uuid::text = ANY(:uuids)
|
||||
"""), {"uuids": parent_uuids}).fetchall()
|
||||
|
||||
for link in links:
|
||||
uid = link.control_uuid
|
||||
src_type = classify_source_regulation(link.source_regulation or "")
|
||||
source_types_by_parent.setdefault(uid, []).append(src_type)
|
||||
|
||||
# 3. Normative strength korrigieren
|
||||
changes = []
|
||||
stats = {"total": len(obligations), "unchanged": 0, "capped_to_should": 0, "capped_to_can": 0, "no_parent_links": 0}
|
||||
|
||||
for obl in obligations:
|
||||
parent_uid = str(obl.parent_control_uuid) if obl.parent_control_uuid else None
|
||||
source_types = source_types_by_parent.get(parent_uid, []) if parent_uid else []
|
||||
|
||||
if not source_types:
|
||||
stats["no_parent_links"] += 1
|
||||
continue
|
||||
|
||||
highest_type = get_highest_source_type(source_types)
|
||||
new_strength = cap_normative_strength(obl.normative_strength, highest_type)
|
||||
|
||||
if new_strength != obl.normative_strength:
|
||||
changes.append({
|
||||
"id": str(obl.id),
|
||||
"candidate_id": obl.candidate_id,
|
||||
"old_strength": obl.normative_strength,
|
||||
"new_strength": new_strength,
|
||||
"source_type": highest_type,
|
||||
})
|
||||
if new_strength == "should":
|
||||
stats["capped_to_should"] += 1
|
||||
elif new_strength == "can":
|
||||
stats["capped_to_can"] += 1
|
||||
else:
|
||||
stats["unchanged"] += 1
|
||||
|
||||
# 4. Aenderungen anwenden (wenn kein dry_run)
|
||||
if not dry_run and changes:
|
||||
for change in changes:
|
||||
db.execute(text("""
|
||||
UPDATE obligation_candidates
|
||||
SET normative_strength = :new_strength
|
||||
WHERE id = CAST(:oid AS uuid)
|
||||
"""), {"new_strength": change["new_strength"], "oid": change["id"]})
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"dry_run": dry_run,
|
||||
"stats": stats,
|
||||
"total_changes": len(changes),
|
||||
"sample_changes": changes[:20],
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONTROL CRUD (CREATE / UPDATE / DELETE)
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user