From 779604750e1570bc580fbca91a9e25a4e4baed95 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 3 Nov 2025 12:55:56 +0100 Subject: [PATCH] Add Vorgaben sanity check functionality Implement comprehensive validation system to detect conflicting Vorgaben with overlapping validity periods. Features: - Static method Vorgabe.sanity_check_vorgaben() for global conflict detection - Instance method Vorgabe.find_conflicts() for individual conflict checking - Model validation via Vorgabe.clean() to prevent conflicting data - Utility functions for date range intersection and conflict reporting - Django management command 'sanity_check_vorgaben' for manual checks - Comprehensive test suite with 17 new tests covering all functionality Validation logic ensures Vorgaben with same dokument, thema, and nummer cannot have overlapping gueltigkeit_von/gueltigkeit_bis date ranges. Handles open-ended ranges (None end dates) and provides clear error messages. Files added/modified: - dokumente/models.py: Added sanity check methods and validation - dokumente/utils.py: New utility functions for conflict detection - dokumente/management/commands/sanity_check_vorgaben.py: New management command - dokumente/tests.py: Added comprehensive test coverage - test_sanity_check.py: Standalone test script All tests pass (56/56) with no regressions. --- .../commands/sanity_check_vorgaben.py | 70 ++++ dokumente/models.py | 118 +++++++ dokumente/tests.py | 304 ++++++++++++++++++ dokumente/utils.py | 123 +++++++ test_sanity_check.py | 38 +++ 5 files changed, 653 insertions(+) create mode 100644 dokumente/management/commands/sanity_check_vorgaben.py create mode 100644 dokumente/utils.py create mode 100644 test_sanity_check.py diff --git a/dokumente/management/commands/sanity_check_vorgaben.py b/dokumente/management/commands/sanity_check_vorgaben.py new file mode 100644 index 0000000..5d6d278 --- /dev/null +++ b/dokumente/management/commands/sanity_check_vorgaben.py @@ -0,0 +1,70 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from dokumente.models import Vorgabe +import datetime + + +class Command(BaseCommand): + help = 'Run sanity checks on Vorgaben to detect conflicts' + + def add_arguments(self, parser): + parser.add_argument( + '--fix', + action='store_true', + help='Attempt to fix conflicts (not implemented yet)', + ) + parser.add_argument( + '--verbose', + action='store_true', + help='Show detailed output', + ) + + def handle(self, *args, **options): + self.verbose = options['verbose'] + + self.stdout.write(self.style.SUCCESS('Starting Vorgaben sanity check...')) + + # Run the sanity check + conflicts = Vorgabe.sanity_check_vorgaben() + + if not conflicts: + self.stdout.write(self.style.SUCCESS('✓ No conflicts found in Vorgaben')) + return + + self.stdout.write( + self.style.WARNING(f'Found {len(conflicts)} conflicts:') + ) + + for i, conflict in enumerate(conflicts, 1): + self._display_conflict(i, conflict) + + if options['fix']: + self.stdout.write(self.style.ERROR('Auto-fix not implemented yet')) + + def _display_conflict(self, index, conflict): + """Display a single conflict""" + v1 = conflict['vorgabe1'] + v2 = conflict['vorgabe2'] + + self.stdout.write(f"\n{index}. {conflict['message']}") + + if self.verbose: + self.stdout.write(f" Vorgabe 1: {v1.Vorgabennummer()}") + self.stdout.write(f" Valid from: {v1.gueltigkeit_von} to {v1.gueltigkeit_bis or 'unlimited'}") + self.stdout.write(f" Title: {v1.titel}") + + self.stdout.write(f" Vorgabe 2: {v2.Vorgabennummer()}") + self.stdout.write(f" Valid from: {v2.gueltigkeit_von} to {v2.gueltigkeit_bis or 'unlimited'}") + self.stdout.write(f" Title: {v2.titel}") + + # Show the overlapping period + overlap_start = max(v1.gueltigkeit_von, v2.gueltigkeit_von) + overlap_end = min( + v1.gueltigkeit_bis or datetime.date.max, + v2.gueltigkeit_bis or datetime.date.max + ) + + if overlap_end != datetime.date.max: + self.stdout.write(f" Overlap: {overlap_start} to {overlap_end}") + else: + self.stdout.write(f" Overlap starts: {overlap_start} (no end)") \ No newline at end of file diff --git a/dokumente/models.py b/dokumente/models.py index ce33001..a42abc6 100644 --- a/dokumente/models.py +++ b/dokumente/models.py @@ -5,6 +5,7 @@ from stichworte.models import Stichwort from referenzen.models import Referenz from rollen.models import Rolle import datetime +from django.db.models import Q class Dokumententyp(models.Model): name = models.CharField(max_length=100, primary_key=True) @@ -86,6 +87,123 @@ class Vorgabe(models.Model): def __str__(self): return f"{self.Vorgabennummer()}: {self.titel}" + @staticmethod + def sanity_check_vorgaben(): + """ + Sanity check for Vorgaben: + If there are two Vorgaben with the same number, Thema and Dokument, + their valid_from and valid_to date ranges shouldn't intersect. + + Returns: + list: List of dictionaries containing conflicts found + """ + conflicts = [] + + # Group Vorgaben by dokument, thema, and nummer + from django.db.models import Count + from itertools import combinations + + # Find Vorgaben with same dokument, thema, and nummer + duplicate_groups = ( + Vorgabe.objects.values('dokument', 'thema', 'nummer') + .annotate(count=Count('id')) + .filter(count__gt=1) + ) + + for group in duplicate_groups: + # Get all Vorgaben in this group + vorgaben = Vorgabe.objects.filter( + dokument=group['dokument'], + thema=group['thema'], + nummer=group['nummer'] + ) + + # Check all pairs for date range intersections + for vorgabe1, vorgabe2 in combinations(vorgaben, 2): + if Vorgabe._date_ranges_intersect( + vorgabe1.gueltigkeit_von, vorgabe1.gueltigkeit_bis, + vorgabe2.gueltigkeit_von, vorgabe2.gueltigkeit_bis + ): + conflicts.append({ + 'vorgabe1': vorgabe1, + 'vorgabe2': vorgabe2, + 'conflict_type': 'date_range_intersection', + 'message': f"Vorgaben {vorgabe1.Vorgabennummer()} and {vorgabe2.Vorgabennummer()} " + f"have intersecting validity periods" + }) + + return conflicts + + def clean(self): + """ + Validate the Vorgabe before saving. + """ + from django.core.exceptions import ValidationError + + # Check for conflicts with existing Vorgaben + conflicts = self.find_conflicts() + if conflicts: + conflict_messages = [c['message'] for c in conflicts] + raise ValidationError({ + '__all__': conflict_messages + }) + + def find_conflicts(self): + """ + Find conflicts with existing Vorgaben. + + Returns: + list: List of conflict dictionaries + """ + conflicts = [] + + # Find Vorgaben with same dokument, thema, and nummer (excluding self) + existing_vorgaben = Vorgabe.objects.filter( + dokument=self.dokument, + thema=self.thema, + nummer=self.nummer + ).exclude(pk=self.pk) + + for other_vorgabe in existing_vorgaben: + if self._date_ranges_intersect( + self.gueltigkeit_von, self.gueltigkeit_bis, + other_vorgabe.gueltigkeit_von, other_vorgabe.gueltigkeit_bis + ): + conflicts.append({ + 'vorgabe1': self, + 'vorgabe2': other_vorgabe, + 'conflict_type': 'date_range_intersection', + 'message': f"Vorgabe {self.Vorgabennummer()} conflicts with " + f"existing {other_vorgabe.Vorgabennummer()} " + f"due to overlapping validity periods" + }) + + return conflicts + + @staticmethod + def _date_ranges_intersect(start1, end1, start2, end2): + """ + Check if two date ranges intersect. + None end date means open-ended range. + + Args: + start1, start2: Start dates + end1, end2: End dates (can be None for open-ended) + + Returns: + bool: True if ranges intersect + """ + # If either start date is None, treat it as invalid case + if not start1 or not start2: + return False + + # If end date is None, treat it as far future + end1 = end1 or datetime.date.max + end2 = end2 or datetime.date.max + + # Ranges intersect if start1 <= end2 and start2 <= end1 + return start1 <= end2 and start2 <= end1 + class Meta: verbose_name_plural="Vorgaben" ordering = ['order'] diff --git a/dokumente/tests.py b/dokumente/tests.py index b0872f8..a9f7c67 100644 --- a/dokumente/tests.py +++ b/dokumente/tests.py @@ -1,11 +1,14 @@ from django.test import TestCase, Client from django.urls import reverse +from django.core.management import call_command from datetime import date, timedelta +from io import StringIO from .models import ( Dokumententyp, Person, Thema, Dokument, Vorgabe, VorgabeLangtext, VorgabeKurztext, Geltungsbereich, Einleitung, Checklistenfrage, Changelog ) +from .utils import check_vorgabe_conflicts, date_ranges_intersect, format_conflict_report from abschnitte.models import AbschnittTyp from referenzen.models import Referenz from stichworte.models import Stichwort @@ -513,3 +516,304 @@ class URLPatternsTest(TestCase): """Test that standard_history URL resolves correctly""" url = reverse('standard_history', kwargs={'nummer': 'TEST-001'}) self.assertEqual(url, '/dokumente/TEST-001/history/') + + +class VorgabeSanityCheckTest(TestCase): + """Test cases for Vorgabe sanity check functionality""" + + def setUp(self): + """Set up test data for sanity check tests""" + self.dokumententyp = Dokumententyp.objects.create( + name="Standard IT-Sicherheit", + verantwortliche_ve="SR-SUR-SEC" + ) + self.dokument = Dokument.objects.create( + nummer="R0066", + dokumententyp=self.dokumententyp, + name="IT Security Standard", + aktiv=True + ) + self.thema = Thema.objects.create(name="Organisation") + self.base_date = date(2023, 1, 1) + + # Create non-conflicting Vorgaben + self.vorgabe1 = Vorgabe.objects.create( + order=1, + nummer=1, + dokument=self.dokument, + thema=self.thema, + titel="First Vorgabe", + gueltigkeit_von=self.base_date, + gueltigkeit_bis=date(2023, 12, 31) + ) + + self.vorgabe2 = Vorgabe.objects.create( + order=2, + nummer=2, + dokument=self.dokument, + thema=self.thema, + titel="Second Vorgabe", + gueltigkeit_von=self.base_date, + gueltigkeit_bis=date(2023, 12, 31) + ) + + def test_date_ranges_intersect_no_overlap(self): + """Test date_ranges_intersect with non-overlapping ranges""" + # Range 1: 2023-01-01 to 2023-06-30 + # Range 2: 2023-07-01 to 2023-12-31 + result = date_ranges_intersect( + date(2023, 1, 1), date(2023, 6, 30), + date(2023, 7, 1), date(2023, 12, 31) + ) + self.assertFalse(result) + + def test_date_ranges_intersect_with_overlap(self): + """Test date_ranges_intersect with overlapping ranges""" + # Range 1: 2023-01-01 to 2023-06-30 + # Range 2: 2023-06-01 to 2023-12-31 (overlaps in June) + result = date_ranges_intersect( + date(2023, 1, 1), date(2023, 6, 30), + date(2023, 6, 1), date(2023, 12, 31) + ) + self.assertTrue(result) + + def test_date_ranges_intersect_with_none_end_date(self): + """Test date_ranges_intersect with None end date (open-ended)""" + # Range 1: 2023-01-01 to None (open-ended) + # Range 2: 2023-06-01 to 2023-12-31 + result = date_ranges_intersect( + date(2023, 1, 1), None, + date(2023, 6, 1), date(2023, 12, 31) + ) + self.assertTrue(result) + + def test_date_ranges_intersect_both_none_end_dates(self): + """Test date_ranges_intersect with both None end dates""" + # Both ranges are open-ended + result = date_ranges_intersect( + date(2023, 1, 1), None, + date(2023, 6, 1), None + ) + self.assertTrue(result) + + def test_date_ranges_intersect_identical_ranges(self): + """Test date_ranges_intersect with identical ranges""" + result = date_ranges_intersect( + date(2023, 1, 1), date(2023, 12, 31), + date(2023, 1, 1), date(2023, 12, 31) + ) + self.assertTrue(result) + + def test_sanity_check_vorgaben_no_conflicts(self): + """Test sanity_check_vorgaben with no conflicts""" + conflicts = Vorgabe.sanity_check_vorgaben() + self.assertEqual(len(conflicts), 0) + + def test_sanity_check_vorgaben_with_conflicts(self): + """Test sanity_check_vorgaben with conflicting Vorgaben""" + # Create a conflicting Vorgabe (same nummer, thema, dokument with overlapping dates) + conflicting_vorgabe = Vorgabe.objects.create( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps with vorgabe1 + gueltigkeit_bis=date(2023, 8, 31) + ) + + conflicts = Vorgabe.sanity_check_vorgaben() + self.assertEqual(len(conflicts), 1) + + conflict = conflicts[0] + self.assertEqual(conflict['conflict_type'], 'date_range_intersection') + self.assertIn('R0066.O.1', conflict['message']) + self.assertIn('intersecting validity periods', conflict['message']) + self.assertEqual(conflict['vorgabe1'], self.vorgabe1) + self.assertEqual(conflict['vorgabe2'], conflicting_vorgabe) + + def test_sanity_check_vorgaben_multiple_conflicts(self): + """Test sanity_check_vorgaben with multiple conflict groups""" + # Create first conflict group + conflicting_vorgabe1 = Vorgabe.objects.create( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe 1", + gueltigkeit_von=date(2023, 6, 1), + gueltigkeit_bis=date(2023, 8, 31) + ) + + # Create second conflict group with different nummer + conflicting_vorgabe2 = Vorgabe.objects.create( + order=4, + nummer=2, # Same as vorgabe2 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe 2", + gueltigkeit_von=date(2023, 6, 1), + gueltigkeit_bis=date(2023, 8, 31) + ) + + conflicts = Vorgabe.sanity_check_vorgaben() + self.assertEqual(len(conflicts), 2) + + # Check that we have conflicts for both nummer 1 and nummer 2 + conflict_messages = [c['message'] for c in conflicts] + self.assertTrue(any('R0066.O.1' in msg for msg in conflict_messages)) + self.assertTrue(any('R0066.O.2' in msg for msg in conflict_messages)) + + def test_find_conflicts_no_conflicts(self): + """Test find_conflicts method on Vorgabe with no conflicts""" + conflicts = self.vorgabe1.find_conflicts() + self.assertEqual(len(conflicts), 0) + + def test_find_conflicts_with_conflicts(self): + """Test find_conflicts method on Vorgabe with conflicts""" + # Create a conflicting Vorgabe + conflicting_vorgabe = Vorgabe.objects.create( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps + gueltigkeit_bis=date(2023, 8, 31) + ) + + conflicts = self.vorgabe1.find_conflicts() + self.assertEqual(len(conflicts), 1) + conflict = conflicts[0] + self.assertEqual(conflict['vorgabe1'], self.vorgabe1) + self.assertEqual(conflict['vorgabe2'], conflicting_vorgabe) + + def test_vorgabe_clean_no_conflicts(self): + """Test Vorgabe.clean() with no conflicts""" + try: + self.vorgabe1.clean() + except Exception as e: + self.fail(f"clean() raised {e} unexpectedly!") + + def test_vorgabe_clean_with_conflicts(self): + """Test Vorgabe.clean() with conflicts raises ValidationError""" + # Create a conflicting Vorgabe + conflicting_vorgabe = Vorgabe( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps + gueltigkeit_bis=date(2023, 8, 31) + ) + + with self.assertRaises(Exception) as context: + conflicting_vorgabe.clean() + + self.assertIn('conflicts with existing', str(context.exception)) + self.assertIn('overlapping validity periods', str(context.exception)) + + def test_check_vorgabe_conflicts_utility(self): + """Test check_vorgabe_conflicts utility function""" + # Initially no conflicts + conflicts = check_vorgabe_conflicts() + self.assertEqual(len(conflicts), 0) + + # Create a conflicting Vorgabe + conflicting_vorgabe = Vorgabe.objects.create( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps + gueltigkeit_bis=date(2023, 8, 31) + ) + + conflicts = check_vorgabe_conflicts() + self.assertEqual(len(conflicts), 1) + + def test_format_conflict_report_no_conflicts(self): + """Test format_conflict_report with no conflicts""" + report = format_conflict_report([]) + self.assertEqual(report, "✓ No conflicts found in Vorgaben") + + def test_format_conflict_report_with_conflicts(self): + """Test format_conflict_report with conflicts""" + # Create a conflicting Vorgabe + conflicting_vorgabe = Vorgabe.objects.create( + order=3, + nummer=1, # Same as vorgabe1 + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps + gueltigkeit_bis=date(2023, 8, 31) + ) + + conflicts = check_vorgabe_conflicts() + report = format_conflict_report(conflicts) + + self.assertIn("Found 1 conflicts:", report) + self.assertIn("R0066.O.1", report) + self.assertIn("intersecting validity periods", report) + + +class SanityCheckManagementCommandTest(TestCase): + """Test cases for sanity_check_vorgaben management command""" + + def setUp(self): + """Set up test data for management command tests""" + self.dokumententyp = Dokumententyp.objects.create( + name="Standard IT-Sicherheit", + verantwortliche_ve="SR-SUR-SEC" + ) + self.dokument = Dokument.objects.create( + nummer="R0066", + dokumententyp=self.dokumententyp, + name="IT Security Standard", + aktiv=True + ) + self.thema = Thema.objects.create(name="Organisation") + + def test_sanity_check_command_no_conflicts(self): + """Test management command with no conflicts""" + out = StringIO() + call_command('sanity_check_vorgaben', stdout=out) + + output = out.getvalue() + self.assertIn("Starting Vorgaben sanity check...", output) + self.assertIn("✓ No conflicts found in Vorgaben", output) + + def test_sanity_check_command_with_conflicts(self): + """Test management command with conflicts""" + # Create conflicting Vorgaben + Vorgabe.objects.create( + order=1, + nummer=1, + dokument=self.dokument, + thema=self.thema, + titel="First Vorgabe", + gueltigkeit_von=date(2023, 1, 1), + gueltigkeit_bis=date(2023, 12, 31) + ) + + Vorgabe.objects.create( + order=2, + nummer=1, # Same nummer, thema, dokument + dokument=self.dokument, + thema=self.thema, + titel="Conflicting Vorgabe", + gueltigkeit_von=date(2023, 6, 1), # Overlaps + gueltigkeit_bis=date(2023, 8, 31) + ) + + out = StringIO() + call_command('sanity_check_vorgaben', stdout=out) + + output = out.getvalue() + self.assertIn("Starting Vorgaben sanity check...", output) + self.assertIn("Found 1 conflicts:", output) + self.assertIn("R0066.O.1", output) + self.assertIn("intersecting validity periods", output) diff --git a/dokumente/utils.py b/dokumente/utils.py new file mode 100644 index 0000000..a0c6930 --- /dev/null +++ b/dokumente/utils.py @@ -0,0 +1,123 @@ +""" +Utility functions for Vorgaben sanity checking +""" +import datetime +from django.db.models import Count +from itertools import combinations +from dokumente.models import Vorgabe + + +def check_vorgabe_conflicts(): + """ + Check for conflicts in Vorgaben. + + Main rule: If there are two Vorgaben with the same number, Thema and Dokument, + their valid_from and valid_to date ranges shouldn't intersect. + + Returns: + list: List of conflict dictionaries + """ + conflicts = [] + + # Find Vorgaben with same dokument, thema, and nummer + duplicate_groups = ( + Vorgabe.objects.values('dokument', 'thema', 'nummer') + .annotate(count=Count('id')) + .filter(count__gt=1) + ) + + for group in duplicate_groups: + # Get all Vorgaben in this group + vorgaben = Vorgabe.objects.filter( + dokument=group['dokument'], + thema=group['thema'], + nummer=group['nummer'] + ) + + # Check all pairs for date range intersections + for vorgabe1, vorgabe2 in combinations(vorgaben, 2): + if date_ranges_intersect( + vorgabe1.gueltigkeit_von, vorgabe1.gueltigkeit_bis, + vorgabe2.gueltigkeit_von, vorgabe2.gueltigkeit_bis + ): + conflicts.append({ + 'vorgabe1': vorgabe1, + 'vorgabe2': vorgabe2, + 'conflict_type': 'date_range_intersection', + 'message': f"Vorgaben {vorgabe1.Vorgabennummer()} and {vorgabe2.Vorgabennummer()} " + f"have intersecting validity periods" + }) + + return conflicts + + +def date_ranges_intersect(start1, end1, start2, end2): + """ + Check if two date ranges intersect. + None end date means open-ended range. + + Args: + start1, start2: Start dates + end1, end2: End dates (can be None for open-ended) + + Returns: + bool: True if ranges intersect + """ + # If either start date is None, treat it as invalid case + if not start1 or not start2: + return False + + # If end date is None, treat it as far future + end1 = end1 or datetime.date.max + end2 = end2 or datetime.date.max + + # Ranges intersect if start1 <= end2 and start2 <= end1 + return start1 <= end2 and start2 <= end1 + + +def format_conflict_report(conflicts, verbose=False): + """ + Format conflicts into a readable report. + + Args: + conflicts: List of conflict dictionaries + verbose: Whether to show detailed information + + Returns: + str: Formatted report + """ + if not conflicts: + return "✓ No conflicts found in Vorgaben" + + lines = [f"Found {len(conflicts)} conflicts:"] + + for i, conflict in enumerate(conflicts, 1): + lines.append(f"\n{i}. {conflict['message']}") + + if verbose: + v1 = conflict['vorgabe1'] + v2 = conflict['vorgabe2'] + + lines.append(f" Vorgabe 1: {v1.Vorgabennummer()}") + lines.append(f" Valid from: {v1.gueltigkeit_von} to {v1.gueltigkeit_bis or 'unlimited'}") + lines.append(f" Title: {v1.titel}") + + lines.append(f" Vorgabe 2: {v2.Vorgabennummer()}") + lines.append(f" Valid from: {v2.gueltigkeit_von} to {v2.gueltigkeit_bis or 'unlimited'}") + lines.append(f" Title: {v2.titel}") + + # Show the overlapping period + v1 = conflict['vorgabe1'] + v2 = conflict['vorgabe2'] + overlap_start = max(v1.gueltigkeit_von, v2.gueltigkeit_von) + overlap_end = min( + v1.gueltigkeit_bis or datetime.date.max, + v2.gueltigkeit_bis or datetime.date.max + ) + + if overlap_end != datetime.date.max: + lines.append(f" Overlap: {overlap_start} to {overlap_end}") + else: + lines.append(f" Overlap starts: {overlap_start} (no end)") + + return "\n".join(lines) \ No newline at end of file diff --git a/test_sanity_check.py b/test_sanity_check.py new file mode 100644 index 0000000..72df591 --- /dev/null +++ b/test_sanity_check.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +""" +Simple script to test Vorgaben sanity checking +""" +import os +import sys +import django + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VorgabenUI.settings') +django.setup() + +from dokumente.utils import check_vorgabe_conflicts, format_conflict_report + + +def main(): + print("Running Vorgaben sanity check...") + print("=" * 50) + + # Check for conflicts + conflicts = check_vorgabe_conflicts() + + # Generate and display report + report = format_conflict_report(conflicts, verbose=True) + print(report) + + print("=" * 50) + + if conflicts: + print(f"\n⚠️ Found {len(conflicts)} conflicts that need attention!") + sys.exit(1) + else: + print("✅ All Vorgaben are valid!") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file