feat(pipeline): implement Control Dependency Engine (Block 9)

Core engine (dependency_engine.py):
- 5 dependency types: prerequisite, supersedes, compensating_control,
  conditional_requirement, scope_exclusion
- Generic condition evaluator (JSONB rules with AND/OR/NOT/field ops)
- Priority-based conflict resolution
- Cycle detection (DFS) + topological sort
- Full evaluation with MCP-compatible dependency_resolution trace
- 39 tests all passing (incl. GHV scenario from user requirements)

Automatic generator (dependency_generator.py):
- Ontology-based: same normalized_object + phase sequence -> prerequisite
- Pattern-based: define->implement, implement->monitor, etc.
- Domain packs: YAML rules for GDPR, AI Act, CRA, Security, Labor Contracts
- 14 tests all passing

API routes (dependency_routes.py):
- CRUD for dependencies
- POST /evaluate with dependency resolution
- POST /generate (auto-generation with dry_run)
- POST /validate (cycle detection)
- GET /graph (nodes + edges for visualization)

Prompt enhancement (decomposition_pass.py):
- Added dependency_hints + lifecycle_phase_order to Pass 0b prompt
- Stored in generation_metadata for post-processing

DB migration: control_dependencies + control_evaluation_results tables

126 tests total, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-26 20:28:10 +02:00
parent 5aaa62dca7
commit 42ab5ead26
14 changed files with 2421 additions and 2 deletions

View File

@@ -220,6 +220,9 @@ class AtomicControlCandidate:
pass_criteria: list = field(default_factory=list)
fail_criteria: list = field(default_factory=list)
check_type: str = ""
# Dependency Engine Felder
dependency_hints: list = field(default_factory=list)
lifecycle_phase_order: int = 0
def to_dict(self) -> dict:
return {
@@ -238,6 +241,8 @@ class AtomicControlCandidate:
"pass_criteria": self.pass_criteria,
"fail_criteria": self.fail_criteria,
"check_type": self.check_type,
"dependency_hints": self.dependency_hints,
"lifecycle_phase_order": self.lifecycle_phase_order,
}
@@ -2133,7 +2138,9 @@ Antworte als JSON:
"severity": "critical|high|medium|low",
"category": "security|privacy|governance|operations|finance|reporting",
"check_type": "technical_config_check|document_clause_check|code_pattern_check|evidence_check|interview_required",
"merge_key": "action_type:normalized_object:control_phase"
"merge_key": "action_type:normalized_object:control_phase",
"dependency_hints": ["dependency_type:action_type:normalized_object (Voraussetzungen, Ersetzungen, Kompensationen)"],
"lifecycle_phase_order": "1-13 (1=scope, 2=definition, 4=implementation, 7=monitoring, 8=testing, 12=reporting)"
}}"""
@@ -2229,7 +2236,9 @@ Jedes Control hat dieses Format:
"severity": "critical|high|medium|low",
"category": "security|privacy|governance|operations|finance|reporting",
"check_type": "technical_config_check|document_clause_check|code_pattern_check|evidence_check|interview_required",
"merge_key": "action_type:normalized_object:control_phase"
"merge_key": "action_type:normalized_object:control_phase",
"dependency_hints": ["dependency_type:action_type:normalized_object (Voraussetzungen, Ersetzungen, Kompensationen)"],
"lifecycle_phase_order": "1-13 (1=scope, 2=definition, 4=implementation, 7=monitoring, 8=testing, 12=reporting)"
}}"""
@@ -2971,6 +2980,8 @@ class DecompositionPass:
pass_criteria=_ensure_list(parsed.get("pass_criteria", [])),
fail_criteria=_ensure_list(parsed.get("fail_criteria", [])),
check_type=parsed.get("check_type", ""),
dependency_hints=_ensure_list(parsed.get("dependency_hints", [])),
lifecycle_phase_order=int(parsed.get("lifecycle_phase_order", 0) or 0),
)
# Store merge_key from LLM output in metadata
llm_merge_key = parsed.get("merge_key", "")
@@ -2980,6 +2991,12 @@ class DecompositionPass:
atomic.parent_control_uuid = obl["parent_uuid"]
atomic.obligation_candidate_id = obl["candidate_id"]
# Set lifecycle_phase_order deterministically if not set by LLM
if not atomic.lifecycle_phase_order:
from services.control_ontology import classify_action, get_phase_order
action_type = classify_action(obl.get("action", "") or atomic.title)
atomic.lifecycle_phase_order = get_phase_order(action_type)
# Cap severity for implementation-specific obligations
if obl.get("is_implementation_specific") and atomic.severity in (
"critical", "high"
@@ -3438,6 +3455,9 @@ class DecompositionPass:
"pass_criteria": atomic.pass_criteria or [],
"fail_criteria": atomic.fail_criteria or [],
"check_type": atomic.check_type or "",
# Dependency Engine Felder
"dependency_hints": atomic.dependency_hints or [],
"lifecycle_phase_order": atomic.lifecycle_phase_order or 0,
}),
"framework_id": "14b1bdd2-abc7-4a43-adae-14471ee5c7cf",
},