fix: commit semantic repair changes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.src.api.routes.clean_release:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @COMPLEXITY: 4
|
||||
# @SEMANTICS: api, clean-release, candidate-preparation, compliance
|
||||
# @PURPOSE: Expose clean release endpoints for candidate preparation and subsequent compliance flow.
|
||||
# @LAYER: API
|
||||
@@ -19,10 +19,20 @@ from ...core.logger import belief_scope, logger
|
||||
from ...dependencies import get_clean_release_repository, get_config_manager
|
||||
from ...services.clean_release.preparation_service import prepare_candidate
|
||||
from ...services.clean_release.repository import CleanReleaseRepository
|
||||
from ...services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
|
||||
from ...services.clean_release.compliance_orchestrator import (
|
||||
CleanComplianceOrchestrator,
|
||||
)
|
||||
from ...services.clean_release.report_builder import ComplianceReportBuilder
|
||||
from ...services.clean_release.compliance_execution_service import ComplianceExecutionService, ComplianceRunError
|
||||
from ...services.clean_release.dto import CandidateDTO, ManifestDTO, CandidateOverviewDTO, ComplianceRunDTO
|
||||
from ...services.clean_release.compliance_execution_service import (
|
||||
ComplianceExecutionService,
|
||||
ComplianceRunError,
|
||||
)
|
||||
from ...services.clean_release.dto import (
|
||||
CandidateDTO,
|
||||
ManifestDTO,
|
||||
CandidateOverviewDTO,
|
||||
ComplianceRunDTO,
|
||||
)
|
||||
from ...services.clean_release.enums import (
|
||||
ComplianceDecision,
|
||||
ComplianceStageName,
|
||||
@@ -49,6 +59,8 @@ class PrepareCandidateRequest(BaseModel):
|
||||
artifacts: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
sources: List[str] = Field(default_factory=list)
|
||||
operator_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
# [/DEF:PrepareCandidateRequest:Class]
|
||||
|
||||
|
||||
@@ -59,6 +71,8 @@ class StartCheckRequest(BaseModel):
|
||||
profile: str = Field(default="enterprise-clean")
|
||||
execution_mode: str = Field(default="tui")
|
||||
triggered_by: str = Field(default="system")
|
||||
|
||||
|
||||
# [/DEF:StartCheckRequest:Class]
|
||||
|
||||
|
||||
@@ -69,6 +83,8 @@ class RegisterCandidateRequest(BaseModel):
|
||||
version: str = Field(min_length=1)
|
||||
source_snapshot_ref: str = Field(min_length=1)
|
||||
created_by: str = Field(min_length=1)
|
||||
|
||||
|
||||
# [/DEF:RegisterCandidateRequest:Class]
|
||||
|
||||
|
||||
@@ -76,6 +92,8 @@ class RegisterCandidateRequest(BaseModel):
|
||||
# @PURPOSE: Request schema for candidate artifact import endpoint.
|
||||
class ImportArtifactsRequest(BaseModel):
|
||||
artifacts: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
# [/DEF:ImportArtifactsRequest:Class]
|
||||
|
||||
|
||||
@@ -83,6 +101,8 @@ class ImportArtifactsRequest(BaseModel):
|
||||
# @PURPOSE: Request schema for manifest build endpoint.
|
||||
class BuildManifestRequest(BaseModel):
|
||||
created_by: str = Field(default="system")
|
||||
|
||||
|
||||
# [/DEF:BuildManifestRequest:Class]
|
||||
|
||||
|
||||
@@ -91,6 +111,8 @@ class BuildManifestRequest(BaseModel):
|
||||
class CreateComplianceRunRequest(BaseModel):
|
||||
requested_by: str = Field(min_length=1)
|
||||
manifest_id: str | None = None
|
||||
|
||||
|
||||
# [/DEF:CreateComplianceRunRequest:Class]
|
||||
|
||||
|
||||
@@ -98,14 +120,19 @@ class CreateComplianceRunRequest(BaseModel):
|
||||
# @PURPOSE: Register a clean-release candidate for headless lifecycle.
|
||||
# @PRE: Candidate identifier is unique.
|
||||
# @POST: Candidate is persisted in DRAFT status.
|
||||
@router.post("/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register_candidate_v2_endpoint(
|
||||
payload: RegisterCandidateRequest,
|
||||
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
||||
):
|
||||
existing = repository.get_candidate(payload.id)
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=409, detail={"message": "Candidate already exists", "code": "CANDIDATE_EXISTS"})
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={"message": "Candidate already exists", "code": "CANDIDATE_EXISTS"},
|
||||
)
|
||||
|
||||
candidate = ReleaseCandidate(
|
||||
id=payload.id,
|
||||
@@ -125,6 +152,8 @@ async def register_candidate_v2_endpoint(
|
||||
created_by=candidate.created_by,
|
||||
status=CandidateStatus(candidate.status),
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:register_candidate_v2_endpoint:Function]
|
||||
|
||||
|
||||
@@ -140,9 +169,15 @@ async def import_candidate_artifacts_v2_endpoint(
|
||||
):
|
||||
candidate = repository.get_candidate(candidate_id)
|
||||
if candidate is None:
|
||||
raise HTTPException(status_code=404, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"},
|
||||
)
|
||||
if not payload.artifacts:
|
||||
raise HTTPException(status_code=400, detail={"message": "Artifacts list is required", "code": "ARTIFACTS_EMPTY"})
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"message": "Artifacts list is required", "code": "ARTIFACTS_EMPTY"},
|
||||
)
|
||||
|
||||
for artifact in payload.artifacts:
|
||||
required = ("id", "path", "sha256", "size")
|
||||
@@ -150,7 +185,10 @@ async def import_candidate_artifacts_v2_endpoint(
|
||||
if field_name not in artifact:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"message": f"Artifact missing field '{field_name}'", "code": "ARTIFACT_INVALID"},
|
||||
detail={
|
||||
"message": f"Artifact missing field '{field_name}'",
|
||||
"code": "ARTIFACT_INVALID",
|
||||
},
|
||||
)
|
||||
|
||||
artifact_model = CandidateArtifact(
|
||||
@@ -172,6 +210,8 @@ async def import_candidate_artifacts_v2_endpoint(
|
||||
repository.save_candidate(candidate)
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
# [/DEF:import_candidate_artifacts_v2_endpoint:Function]
|
||||
|
||||
|
||||
@@ -179,7 +219,11 @@ async def import_candidate_artifacts_v2_endpoint(
|
||||
# @PURPOSE: Build immutable manifest snapshot for prepared candidate.
|
||||
# @PRE: Candidate exists and has imported artifacts.
|
||||
# @POST: Returns created ManifestDTO with incremented version.
|
||||
@router.post("/candidates/{candidate_id}/manifests", response_model=ManifestDTO, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/candidates/{candidate_id}/manifests",
|
||||
response_model=ManifestDTO,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def build_candidate_manifest_v2_endpoint(
|
||||
candidate_id: str,
|
||||
payload: BuildManifestRequest,
|
||||
@@ -194,7 +238,10 @@ async def build_candidate_manifest_v2_endpoint(
|
||||
created_by=payload.created_by,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail={"message": str(exc), "code": "MANIFEST_BUILD_ERROR"})
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"message": str(exc), "code": "MANIFEST_BUILD_ERROR"},
|
||||
)
|
||||
|
||||
return ManifestDTO(
|
||||
id=manifest.id,
|
||||
@@ -207,6 +254,8 @@ async def build_candidate_manifest_v2_endpoint(
|
||||
source_snapshot_ref=manifest.source_snapshot_ref,
|
||||
content_json=manifest.content_json,
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:build_candidate_manifest_v2_endpoint:Function]
|
||||
|
||||
|
||||
@@ -221,26 +270,53 @@ async def get_candidate_overview_v2_endpoint(
|
||||
):
|
||||
candidate = repository.get_candidate(candidate_id)
|
||||
if candidate is None:
|
||||
raise HTTPException(status_code=404, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"},
|
||||
)
|
||||
|
||||
manifests = repository.get_manifests_by_candidate(candidate_id)
|
||||
latest_manifest = sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0] if manifests else None
|
||||
latest_manifest = (
|
||||
sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]
|
||||
if manifests
|
||||
else None
|
||||
)
|
||||
|
||||
runs = [run for run in repository.check_runs.values() if run.candidate_id == candidate_id]
|
||||
latest_run = sorted(runs, key=lambda run: run.requested_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)[0] if runs else None
|
||||
runs = [
|
||||
run
|
||||
for run in repository.check_runs.values()
|
||||
if run.candidate_id == candidate_id
|
||||
]
|
||||
latest_run = (
|
||||
sorted(
|
||||
runs,
|
||||
key=lambda run: run.requested_at
|
||||
or datetime.min.replace(tzinfo=timezone.utc),
|
||||
reverse=True,
|
||||
)[0]
|
||||
if runs
|
||||
else None
|
||||
)
|
||||
|
||||
latest_report = None
|
||||
if latest_run is not None:
|
||||
latest_report = next((r for r in repository.reports.values() if r.run_id == latest_run.id), None)
|
||||
latest_report = next(
|
||||
(r for r in repository.reports.values() if r.run_id == latest_run.id), None
|
||||
)
|
||||
|
||||
latest_policy_snapshot = repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None
|
||||
latest_registry_snapshot = repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None
|
||||
latest_policy_snapshot = (
|
||||
repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None
|
||||
)
|
||||
latest_registry_snapshot = (
|
||||
repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None
|
||||
)
|
||||
|
||||
approval_decisions = getattr(repository, "approval_decisions", [])
|
||||
latest_approval = (
|
||||
sorted(
|
||||
[item for item in approval_decisions if item.candidate_id == candidate_id],
|
||||
key=lambda item: item.decided_at or datetime.min.replace(tzinfo=timezone.utc),
|
||||
key=lambda item: item.decided_at
|
||||
or datetime.min.replace(tzinfo=timezone.utc),
|
||||
reverse=True,
|
||||
)[0]
|
||||
if approval_decisions
|
||||
@@ -252,7 +328,8 @@ async def get_candidate_overview_v2_endpoint(
|
||||
latest_publication = (
|
||||
sorted(
|
||||
[item for item in publication_records if item.candidate_id == candidate_id],
|
||||
key=lambda item: item.published_at or datetime.min.replace(tzinfo=timezone.utc),
|
||||
key=lambda item: item.published_at
|
||||
or datetime.min.replace(tzinfo=timezone.utc),
|
||||
reverse=True,
|
||||
)[0]
|
||||
if publication_records
|
||||
@@ -266,19 +343,35 @@ async def get_candidate_overview_v2_endpoint(
|
||||
source_snapshot_ref=candidate.source_snapshot_ref,
|
||||
status=CandidateStatus(candidate.status),
|
||||
latest_manifest_id=latest_manifest.id if latest_manifest else None,
|
||||
latest_manifest_digest=latest_manifest.manifest_digest if latest_manifest else None,
|
||||
latest_manifest_digest=latest_manifest.manifest_digest
|
||||
if latest_manifest
|
||||
else None,
|
||||
latest_run_id=latest_run.id if latest_run else None,
|
||||
latest_run_status=RunStatus(latest_run.status) if latest_run else None,
|
||||
latest_report_id=latest_report.id if latest_report else None,
|
||||
latest_report_final_status=ComplianceDecision(latest_report.final_status) if latest_report else None,
|
||||
latest_policy_snapshot_id=latest_policy_snapshot.id if latest_policy_snapshot else None,
|
||||
latest_policy_version=latest_policy_snapshot.policy_version if latest_policy_snapshot else None,
|
||||
latest_registry_snapshot_id=latest_registry_snapshot.id if latest_registry_snapshot else None,
|
||||
latest_registry_version=latest_registry_snapshot.registry_version if latest_registry_snapshot else None,
|
||||
latest_report_final_status=ComplianceDecision(latest_report.final_status)
|
||||
if latest_report
|
||||
else None,
|
||||
latest_policy_snapshot_id=latest_policy_snapshot.id
|
||||
if latest_policy_snapshot
|
||||
else None,
|
||||
latest_policy_version=latest_policy_snapshot.policy_version
|
||||
if latest_policy_snapshot
|
||||
else None,
|
||||
latest_registry_snapshot_id=latest_registry_snapshot.id
|
||||
if latest_registry_snapshot
|
||||
else None,
|
||||
latest_registry_version=latest_registry_snapshot.registry_version
|
||||
if latest_registry_snapshot
|
||||
else None,
|
||||
latest_approval_decision=latest_approval.decision if latest_approval else None,
|
||||
latest_publication_id=latest_publication.id if latest_publication else None,
|
||||
latest_publication_status=latest_publication.status if latest_publication else None,
|
||||
latest_publication_status=latest_publication.status
|
||||
if latest_publication
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:get_candidate_overview_v2_endpoint:Function]
|
||||
|
||||
|
||||
@@ -311,6 +404,8 @@ async def prepare_candidate_endpoint(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"message": str(exc), "code": "CLEAN_PREPARATION_ERROR"},
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:prepare_candidate_endpoint:Function]
|
||||
|
||||
|
||||
@@ -327,27 +422,46 @@ async def start_check(
|
||||
logger.reason("Starting clean-release compliance check run")
|
||||
policy = repository.get_active_policy()
|
||||
if policy is None:
|
||||
raise HTTPException(status_code=409, detail={"message": "Active policy not found", "code": "POLICY_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={
|
||||
"message": "Active policy not found",
|
||||
"code": "POLICY_NOT_FOUND",
|
||||
},
|
||||
)
|
||||
|
||||
candidate = repository.get_candidate(payload.candidate_id)
|
||||
if candidate is None:
|
||||
raise HTTPException(status_code=409, detail={"message": "Candidate not found", "code": "CANDIDATE_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={
|
||||
"message": "Candidate not found",
|
||||
"code": "CANDIDATE_NOT_FOUND",
|
||||
},
|
||||
)
|
||||
|
||||
manifests = repository.get_manifests_by_candidate(payload.candidate_id)
|
||||
if not manifests:
|
||||
logger.explore("No manifest found for candidate; bootstrapping legacy empty manifest for compatibility")
|
||||
from ...services.clean_release.manifest_builder import build_distribution_manifest
|
||||
logger.explore(
|
||||
"No manifest found for candidate; bootstrapping legacy empty manifest for compatibility"
|
||||
)
|
||||
from ...services.clean_release.manifest_builder import (
|
||||
build_distribution_manifest,
|
||||
)
|
||||
|
||||
boot_manifest = build_distribution_manifest(
|
||||
manifest_id=f"manifest-{payload.candidate_id}",
|
||||
candidate_id=payload.candidate_id,
|
||||
policy_id=getattr(policy, "policy_id", None) or getattr(policy, "id", ""),
|
||||
policy_id=getattr(policy, "policy_id", None)
|
||||
or getattr(policy, "id", ""),
|
||||
generated_by=payload.triggered_by,
|
||||
artifacts=[],
|
||||
)
|
||||
repository.save_manifest(boot_manifest)
|
||||
manifests = [boot_manifest]
|
||||
latest_manifest = sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]
|
||||
latest_manifest = sorted(
|
||||
manifests, key=lambda m: m.manifest_version, reverse=True
|
||||
)[0]
|
||||
|
||||
orchestrator = CleanComplianceOrchestrator(repository)
|
||||
run = orchestrator.start_check_run(
|
||||
@@ -364,7 +478,7 @@ async def start_check(
|
||||
stage_name=ComplianceStageName.DATA_PURITY.value,
|
||||
status=RunStatus.SUCCEEDED.value,
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"}
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id=f"stage-{run.id}-2",
|
||||
@@ -372,7 +486,7 @@ async def start_check(
|
||||
stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,
|
||||
status=RunStatus.SUCCEEDED.value,
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"}
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id=f"stage-{run.id}-3",
|
||||
@@ -380,7 +494,7 @@ async def start_check(
|
||||
stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
|
||||
status=RunStatus.SUCCEEDED.value,
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"}
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
ComplianceStageRun(
|
||||
id=f"stage-{run.id}-4",
|
||||
@@ -388,14 +502,20 @@ async def start_check(
|
||||
stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,
|
||||
status=RunStatus.SUCCEEDED.value,
|
||||
decision=ComplianceDecision.PASSED.value,
|
||||
details_json={"message": "ok"}
|
||||
details_json={"message": "ok"},
|
||||
),
|
||||
]
|
||||
run = orchestrator.execute_stages(run, forced_results=forced)
|
||||
run = orchestrator.finalize_run(run)
|
||||
|
||||
if str(run.final_status) in {ComplianceDecision.BLOCKED.value, "CheckFinalStatus.BLOCKED", "BLOCKED"}:
|
||||
logger.explore("Run ended as BLOCKED, persisting synthetic external-source violation")
|
||||
if str(run.final_status) in {
|
||||
ComplianceDecision.BLOCKED.value,
|
||||
"CheckFinalStatus.BLOCKED",
|
||||
"BLOCKED",
|
||||
}:
|
||||
logger.explore(
|
||||
"Run ended as BLOCKED, persisting synthetic external-source violation"
|
||||
)
|
||||
violation = ComplianceViolation(
|
||||
id=f"viol-{run.id}",
|
||||
run_id=run.id,
|
||||
@@ -403,12 +523,14 @@ async def start_check(
|
||||
code="EXTERNAL_SOURCE_DETECTED",
|
||||
severity=ViolationSeverity.CRITICAL.value,
|
||||
message="Replace with approved internal server",
|
||||
evidence_json={"location": "external.example.com"}
|
||||
evidence_json={"location": "external.example.com"},
|
||||
)
|
||||
repository.save_violation(violation)
|
||||
|
||||
builder = ComplianceReportBuilder(repository)
|
||||
report = builder.build_report_payload(run, repository.get_violations_by_run(run.id))
|
||||
report = builder.build_report_payload(
|
||||
run, repository.get_violations_by_run(run.id)
|
||||
)
|
||||
builder.persist_report(report)
|
||||
logger.reflect(f"Compliance report persisted for run_id={run.id}")
|
||||
|
||||
@@ -418,6 +540,8 @@ async def start_check(
|
||||
"status": "running",
|
||||
"started_at": run.started_at.isoformat() if run.started_at else None,
|
||||
}
|
||||
|
||||
|
||||
# [/DEF:start_check:Function]
|
||||
|
||||
|
||||
@@ -426,11 +550,17 @@ async def start_check(
|
||||
# @PRE: check_run_id references an existing run.
|
||||
# @POST: Deterministic payload shape includes checks and violations arrays.
|
||||
@router.get("/checks/{check_run_id}")
|
||||
async def get_check_status(check_run_id: str, repository: CleanReleaseRepository = Depends(get_clean_release_repository)):
|
||||
async def get_check_status(
|
||||
check_run_id: str,
|
||||
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
||||
):
|
||||
with belief_scope("clean_release.get_check_status"):
|
||||
run = repository.get_check_run(check_run_id)
|
||||
if run is None:
|
||||
raise HTTPException(status_code=404, detail={"message": "Check run not found", "code": "CHECK_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"message": "Check run not found", "code": "CHECK_NOT_FOUND"},
|
||||
)
|
||||
|
||||
logger.reflect(f"Returning check status for check_run_id={check_run_id}")
|
||||
checks = [
|
||||
@@ -462,6 +592,8 @@ async def get_check_status(check_run_id: str, repository: CleanReleaseRepository
|
||||
"checks": checks,
|
||||
"violations": violations,
|
||||
}
|
||||
|
||||
|
||||
# [/DEF:get_check_status:Function]
|
||||
|
||||
|
||||
@@ -470,11 +602,17 @@ async def get_check_status(check_run_id: str, repository: CleanReleaseRepository
|
||||
# @PRE: report_id references an existing report.
|
||||
# @POST: Returns serialized report object.
|
||||
@router.get("/reports/{report_id}")
|
||||
async def get_report(report_id: str, repository: CleanReleaseRepository = Depends(get_clean_release_repository)):
|
||||
async def get_report(
|
||||
report_id: str,
|
||||
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
||||
):
|
||||
with belief_scope("clean_release.get_report"):
|
||||
report = repository.get_report(report_id)
|
||||
if report is None:
|
||||
raise HTTPException(status_code=404, detail={"message": "Report not found", "code": "REPORT_NOT_FOUND"})
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"message": "Report not found", "code": "REPORT_NOT_FOUND"},
|
||||
)
|
||||
|
||||
logger.reflect(f"Returning compliance report report_id={report_id}")
|
||||
return {
|
||||
@@ -482,11 +620,17 @@ async def get_report(report_id: str, repository: CleanReleaseRepository = Depend
|
||||
"check_run_id": report.run_id,
|
||||
"candidate_id": report.candidate_id,
|
||||
"final_status": getattr(report.final_status, "value", report.final_status),
|
||||
"generated_at": report.generated_at.isoformat() if getattr(report, "generated_at", None) else None,
|
||||
"generated_at": report.generated_at.isoformat()
|
||||
if getattr(report, "generated_at", None)
|
||||
else None,
|
||||
"operator_summary": getattr(report, "operator_summary", ""),
|
||||
"structured_payload_ref": getattr(report, "structured_payload_ref", None),
|
||||
"violations_count": getattr(report, "violations_count", 0),
|
||||
"blocking_violations_count": getattr(report, "blocking_violations_count", 0),
|
||||
"blocking_violations_count": getattr(
|
||||
report, "blocking_violations_count", 0
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# [/DEF:get_report:Function]
|
||||
# [/DEF:backend.src.api.routes.clean_release:Module]
|
||||
# [/DEF:backend.src.api.routes.clean_release:Module]
|
||||
|
||||
Reference in New Issue
Block a user