Files
ss-tools/backend/src/services/clean_release/report_builder.py

68 lines
3.1 KiB
Python

# [DEF:backend.src.services.clean_release.report_builder:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, report, audit, counters, violations
# @PURPOSE: Build and persist compliance reports with consistent counter invariants.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @INVARIANT: blocking_violations_count never exceeds violations_count.
# @TEST_CONTRACT: ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport
# @TEST_FIXTURE: blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError
# @TEST_EDGE: counter_mismatch -> blocking counter cannot exceed total violations counter
# @TEST_EDGE: missing_operator_summary -> non-terminal run prevents report creation and summary generation
# @TEST_INVARIANT: blocking_count_le_total_count -> VERIFIED_BY: [counter_mismatch, empty_violations_for_blocked]
from __future__ import annotations
from datetime import datetime, timezone
from uuid import uuid4
from typing import List
from .enums import RunStatus, ComplianceDecision
from ...models.clean_release import ComplianceRun, ComplianceReport, ComplianceViolation
from .repository import CleanReleaseRepository
class ComplianceReportBuilder:
def __init__(self, repository: CleanReleaseRepository):
self.repository = repository
def build_report_payload(self, check_run: ComplianceRun, violations: List[ComplianceViolation]) -> ComplianceReport:
if check_run.status == RunStatus.RUNNING:
raise ValueError("Cannot build report for non-terminal run")
violations_count = len(violations)
blocking_violations_count = sum(
1
for v in violations
if bool(getattr(v, "blocked_release", False))
or bool(getattr(v, "evidence_json", {}).get("blocked_release", False))
)
if check_run.final_status == ComplianceDecision.BLOCKED and blocking_violations_count <= 0:
raise ValueError("Blocked run requires at least one blocking violation")
summary = (
"Compliance passed with no blocking violations"
if check_run.final_status == ComplianceDecision.PASSED
else f"Blocked with {blocking_violations_count} blocking violation(s)"
)
return ComplianceReport(
id=f"CCR-{uuid4()}",
run_id=check_run.id,
candidate_id=check_run.candidate_id,
generated_at=datetime.now(timezone.utc),
final_status=check_run.final_status,
summary_json={
"operator_summary": summary,
"violations_count": violations_count,
"blocking_violations_count": blocking_violations_count,
},
immutable=True,
)
def persist_report(self, report: ComplianceReport) -> ComplianceReport:
return self.repository.save_report(report)
# [/DEF:backend.src.services.clean_release.report_builder:Module]