feat(clean-release): complete compliance redesign phases and polish tasks T047-T052

This commit is contained in:
2026-03-10 09:11:26 +03:00
parent 0cf02bcf82
commit 83334fe4da
79 changed files with 7430 additions and 945 deletions

View File

@@ -0,0 +1,165 @@
# [DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]
# @TIER: STANDARD
# @PURPOSE: Compatibility tests for legacy clean-release API paths retained during v2 migration.
# @LAYER: Tests
# @RELATION: TESTS -> backend.src.api.routes.clean_release
from __future__ import annotations
import os
from datetime import datetime, timezone
from fastapi.testclient import TestClient
os.environ.setdefault("DATABASE_URL", "sqlite:///./test_clean_release_legacy_compat.db")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///./test_clean_release_legacy_auth.db")
from src.app import app
from src.dependencies import get_clean_release_repository
from src.models.clean_release import (
CleanProfilePolicy,
DistributionManifest,
ProfileType,
ReleaseCandidate,
ReleaseCandidateStatus,
ResourceSourceEntry,
ResourceSourceRegistry,
)
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_legacy_repo:Function]
# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
# @PRE: Repository is empty.
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
def _seed_legacy_repo() -> CleanReleaseRepository:
repo = CleanReleaseRepository()
now = datetime.now(timezone.utc)
repo.save_candidate(
ReleaseCandidate(
id="legacy-rc-001",
version="1.0.0",
source_snapshot_ref="git:legacy-001",
created_at=now,
created_by="compat-tester",
status=ReleaseCandidateStatus.DRAFT,
)
)
registry = ResourceSourceRegistry(
registry_id="legacy-reg-1",
name="Legacy Internal Registry",
entries=[
ResourceSourceEntry(
source_id="legacy-src-1",
host="repo.intra.company.local",
protocol="https",
purpose="artifact-repo",
enabled=True,
)
],
updated_at=now,
updated_by="compat-tester",
status="ACTIVE",
)
setattr(registry, "immutable", True)
setattr(registry, "allowed_hosts", ["repo.intra.company.local"])
setattr(registry, "allowed_schemes", ["https"])
setattr(registry, "allowed_source_types", ["artifact-repo"])
repo.save_registry(registry)
policy = CleanProfilePolicy(
policy_id="legacy-pol-1",
policy_version="1.0.0",
profile=ProfileType.ENTERPRISE_CLEAN,
active=True,
internal_source_registry_ref="legacy-reg-1",
prohibited_artifact_categories=["test-data"],
required_system_categories=["core"],
effective_from=now,
)
setattr(policy, "immutable", True)
setattr(
policy,
"content_json",
{
"profile": "enterprise-clean",
"prohibited_artifact_categories": ["test-data"],
"required_system_categories": ["core"],
"external_source_forbidden": True,
},
)
repo.save_policy(policy)
repo.save_manifest(
DistributionManifest(
id="legacy-manifest-1",
candidate_id="legacy-rc-001",
manifest_version=1,
manifest_digest="sha256:legacy-manifest",
artifacts_digest="sha256:legacy-artifacts",
created_at=now,
created_by="compat-tester",
source_snapshot_ref="git:legacy-001",
content_json={"items": [], "summary": {"included_count": 0, "prohibited_detected_count": 0}},
immutable=True,
)
)
return repo
# [/DEF:_seed_legacy_repo:Function]
def test_legacy_prepare_endpoint_still_available() -> None:
repo = _seed_legacy_repo()
app.dependency_overrides[get_clean_release_repository] = lambda: repo
try:
client = TestClient(app)
response = client.post(
"/api/clean-release/candidates/prepare",
json={
"candidate_id": "legacy-rc-001",
"artifacts": [{"path": "src/main.py", "category": "core", "reason": "required"}],
"sources": ["repo.intra.company.local"],
"operator_id": "compat-tester",
},
)
assert response.status_code == 200
payload = response.json()
assert "status" in payload
assert payload["status"] in {"prepared", "blocked", "PREPARED", "BLOCKED"}
finally:
app.dependency_overrides.clear()
def test_legacy_checks_endpoints_still_available() -> None:
repo = _seed_legacy_repo()
app.dependency_overrides[get_clean_release_repository] = lambda: repo
try:
client = TestClient(app)
start_response = client.post(
"/api/clean-release/checks",
json={
"candidate_id": "legacy-rc-001",
"profile": "enterprise-clean",
"execution_mode": "api",
"triggered_by": "compat-tester",
},
)
assert start_response.status_code == 202
start_payload = start_response.json()
assert "check_run_id" in start_payload
assert start_payload["candidate_id"] == "legacy-rc-001"
status_response = client.get(f"/api/clean-release/checks/{start_payload['check_run_id']}")
assert status_response.status_code == 200
status_payload = status_response.json()
assert status_payload["check_run_id"] == start_payload["check_run_id"]
assert "final_status" in status_payload
assert "checks" in status_payload
finally:
app.dependency_overrides.clear()
# [/DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]

View File

@@ -0,0 +1,93 @@
# [DEF:test_clean_release_v2_api:Module]
# @TIER: STANDARD
# @PURPOSE: API contract tests for redesigned clean release endpoints.
# @LAYER: Domain
from datetime import datetime, timezone
from types import SimpleNamespace
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from src.app import app
from src.dependencies import get_clean_release_repository, get_config_manager
from src.models.clean_release import (
CleanPolicySnapshot,
DistributionManifest,
ReleaseCandidate,
SourceRegistrySnapshot,
)
from src.services.clean_release.enums import CandidateStatus
client = TestClient(app)
# [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012).
def test_candidate_registration_contract():
"""
@TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.
@TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO
"""
payload = {
"id": "rc-test-001",
"version": "1.0.0",
"source_snapshot_ref": "git:sha123",
"created_by": "test-user"
}
response = client.post("/api/v2/clean-release/candidates", json=payload)
assert response.status_code == 201
data = response.json()
assert data["id"] == "rc-test-001"
assert data["status"] == CandidateStatus.DRAFT.value
def test_artifact_import_contract():
"""
@TEST_SCENARIO: artifact_import -> Should return 200 and success status.
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO
"""
candidate_id = "rc-test-001-art"
bootstrap_candidate = {
"id": candidate_id,
"version": "1.0.0",
"source_snapshot_ref": "git:sha123",
"created_by": "test-user"
}
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
assert create_response.status_code == 201
payload = {
"artifacts": [
{
"id": "art-1",
"path": "bin/app.exe",
"sha256": "hash123",
"size": 1024
}
]
}
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/artifacts", json=payload)
assert response.status_code == 200
assert response.json()["status"] == "success"
def test_manifest_build_contract():
"""
@TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO
"""
candidate_id = "rc-test-001-manifest"
bootstrap_candidate = {
"id": candidate_id,
"version": "1.0.0",
"source_snapshot_ref": "git:sha123",
"created_by": "test-user"
}
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
assert create_response.status_code == 201
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/manifests")
assert response.status_code == 201
data = response.json()
assert "manifest_digest" in data
assert data["candidate_id"] == candidate_id
# [/DEF:test_clean_release_v2_api:Module]

View File

@@ -0,0 +1,107 @@
# [DEF:test_clean_release_v2_release_api:Module]
# @TIER: STANDARD
# @PURPOSE: API contract test scaffolding for clean release approval and publication endpoints.
# @LAYER: Domain
# @RELATION: IMPLEMENTS -> clean_release_v2_release_api_contracts
"""Contract tests for redesigned approval/publication API endpoints."""
from datetime import datetime, timezone
from uuid import uuid4
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.routes.clean_release_v2 import router as clean_release_v2_router
from src.dependencies import get_clean_release_repository
from src.models.clean_release import ComplianceReport, ReleaseCandidate
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision
test_app = FastAPI()
test_app.include_router(clean_release_v2_router)
client = TestClient(test_app)
def _seed_candidate_and_passed_report() -> tuple[str, str]:
repository = get_clean_release_repository()
candidate_id = f"api-release-candidate-{uuid4()}"
report_id = f"api-release-report-{uuid4()}"
repository.save_candidate(
ReleaseCandidate(
id=candidate_id,
version="1.0.0",
source_snapshot_ref="git:sha-api-release",
created_by="api-test",
created_at=datetime.now(timezone.utc),
status=CandidateStatus.CHECK_PASSED.value,
)
)
repository.save_report(
ComplianceReport(
id=report_id,
run_id=f"run-{uuid4()}",
candidate_id=candidate_id,
final_status=ComplianceDecision.PASSED.value,
summary_json={"operator_summary": "ok", "violations_count": 0, "blocking_violations_count": 0},
generated_at=datetime.now(timezone.utc),
immutable=True,
)
)
return candidate_id, report_id
def test_release_approve_and_publish_revoke_contract() -> None:
"""Contract for approve -> publish -> revoke lifecycle endpoints."""
candidate_id, report_id = _seed_candidate_and_passed_report()
approve_response = client.post(
f"/api/v2/clean-release/candidates/{candidate_id}/approve",
json={"report_id": report_id, "decided_by": "api-test", "comment": "approved"},
)
assert approve_response.status_code == 200
approve_payload = approve_response.json()
assert approve_payload["status"] == "ok"
assert approve_payload["decision"] == "APPROVED"
publish_response = client.post(
f"/api/v2/clean-release/candidates/{candidate_id}/publish",
json={
"report_id": report_id,
"published_by": "api-test",
"target_channel": "stable",
"publication_ref": "rel-api-001",
},
)
assert publish_response.status_code == 200
publish_payload = publish_response.json()
assert publish_payload["status"] == "ok"
assert publish_payload["publication"]["status"] == "ACTIVE"
publication_id = publish_payload["publication"]["id"]
revoke_response = client.post(
f"/api/v2/clean-release/publications/{publication_id}/revoke",
json={"revoked_by": "api-test", "comment": "rollback"},
)
assert revoke_response.status_code == 200
revoke_payload = revoke_response.json()
assert revoke_payload["status"] == "ok"
assert revoke_payload["publication"]["status"] == "REVOKED"
def test_release_reject_contract() -> None:
"""Contract for reject endpoint."""
candidate_id, report_id = _seed_candidate_and_passed_report()
reject_response = client.post(
f"/api/v2/clean-release/candidates/{candidate_id}/reject",
json={"report_id": report_id, "decided_by": "api-test", "comment": "rejected"},
)
assert reject_response.status_code == 200
payload = reject_response.json()
assert payload["status"] == "ok"
assert payload["decision"] == "REJECTED"
# [/DEF:test_clean_release_v2_release_api:Module]

View File

@@ -16,19 +16,27 @@ from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from ...core.logger import belief_scope, logger
from ...dependencies import get_clean_release_repository
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.report_builder import ComplianceReportBuilder
from ...models.clean_release import (
CheckFinalStatus,
CheckStageName,
CheckStageResult,
CheckStageStatus,
ComplianceViolation,
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,
ViolationCategory,
ViolationSeverity,
RunStatus,
CandidateStatus,
)
from ...models.clean_release import (
ComplianceRun,
ComplianceStageRun,
ComplianceViolation,
CandidateArtifact,
ReleaseCandidate,
)
router = APIRouter(prefix="/api/clean-release", tags=["Clean Release"])
@@ -54,6 +62,226 @@ class StartCheckRequest(BaseModel):
# [/DEF:StartCheckRequest:Class]
# [DEF:RegisterCandidateRequest:Class]
# @PURPOSE: Request schema for candidate registration endpoint.
class RegisterCandidateRequest(BaseModel):
id: str = Field(min_length=1)
version: str = Field(min_length=1)
source_snapshot_ref: str = Field(min_length=1)
created_by: str = Field(min_length=1)
# [/DEF:RegisterCandidateRequest:Class]
# [DEF:ImportArtifactsRequest:Class]
# @PURPOSE: Request schema for candidate artifact import endpoint.
class ImportArtifactsRequest(BaseModel):
artifacts: List[Dict[str, Any]] = Field(default_factory=list)
# [/DEF:ImportArtifactsRequest:Class]
# [DEF:BuildManifestRequest:Class]
# @PURPOSE: Request schema for manifest build endpoint.
class BuildManifestRequest(BaseModel):
created_by: str = Field(default="system")
# [/DEF:BuildManifestRequest:Class]
# [DEF:CreateComplianceRunRequest:Class]
# @PURPOSE: Request schema for compliance run creation with optional manifest pinning.
class CreateComplianceRunRequest(BaseModel):
requested_by: str = Field(min_length=1)
manifest_id: str | None = None
# [/DEF:CreateComplianceRunRequest:Class]
# [DEF:register_candidate_v2_endpoint:Function]
# @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)
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"})
candidate = ReleaseCandidate(
id=payload.id,
version=payload.version,
source_snapshot_ref=payload.source_snapshot_ref,
created_by=payload.created_by,
created_at=datetime.now(timezone.utc),
status=CandidateStatus.DRAFT.value,
)
repository.save_candidate(candidate)
return CandidateDTO(
id=candidate.id,
version=candidate.version,
source_snapshot_ref=candidate.source_snapshot_ref,
created_at=candidate.created_at,
created_by=candidate.created_by,
status=CandidateStatus(candidate.status),
)
# [/DEF:register_candidate_v2_endpoint:Function]
# [DEF:import_candidate_artifacts_v2_endpoint:Function]
# @PURPOSE: Import candidate artifacts in headless flow.
# @PRE: Candidate exists and artifacts array is non-empty.
# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
@router.post("/candidates/{candidate_id}/artifacts")
async def import_candidate_artifacts_v2_endpoint(
candidate_id: str,
payload: ImportArtifactsRequest,
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
candidate = repository.get_candidate(candidate_id)
if candidate is None:
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"})
for artifact in payload.artifacts:
required = ("id", "path", "sha256", "size")
for field_name in required:
if field_name not in artifact:
raise HTTPException(
status_code=400,
detail={"message": f"Artifact missing field '{field_name}'", "code": "ARTIFACT_INVALID"},
)
artifact_model = CandidateArtifact(
id=str(artifact["id"]),
candidate_id=candidate_id,
path=str(artifact["path"]),
sha256=str(artifact["sha256"]),
size=int(artifact["size"]),
detected_category=artifact.get("detected_category"),
declared_category=artifact.get("declared_category"),
source_uri=artifact.get("source_uri"),
source_host=artifact.get("source_host"),
metadata_json=artifact.get("metadata_json", {}),
)
repository.save_artifact(artifact_model)
if candidate.status == CandidateStatus.DRAFT.value:
candidate.transition_to(CandidateStatus.PREPARED)
repository.save_candidate(candidate)
return {"status": "success"}
# [/DEF:import_candidate_artifacts_v2_endpoint:Function]
# [DEF:build_candidate_manifest_v2_endpoint:Function]
# @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)
async def build_candidate_manifest_v2_endpoint(
candidate_id: str,
payload: BuildManifestRequest,
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
from ...services.clean_release.manifest_service import build_manifest_snapshot
try:
manifest = build_manifest_snapshot(
repository=repository,
candidate_id=candidate_id,
created_by=payload.created_by,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail={"message": str(exc), "code": "MANIFEST_BUILD_ERROR"})
return ManifestDTO(
id=manifest.id,
candidate_id=manifest.candidate_id,
manifest_version=manifest.manifest_version,
manifest_digest=manifest.manifest_digest,
artifacts_digest=manifest.artifacts_digest,
created_at=manifest.created_at,
created_by=manifest.created_by,
source_snapshot_ref=manifest.source_snapshot_ref,
content_json=manifest.content_json,
)
# [/DEF:build_candidate_manifest_v2_endpoint:Function]
# [DEF:get_candidate_overview_v2_endpoint:Function]
# @PURPOSE: Return expanded candidate overview DTO for headless lifecycle visibility.
# @PRE: Candidate exists.
# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
@router.get("/candidates/{candidate_id}/overview", response_model=CandidateOverviewDTO)
async def get_candidate_overview_v2_endpoint(
candidate_id: str,
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
candidate = repository.get_candidate(candidate_id)
if candidate is None:
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
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_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),
reverse=True,
)[0]
if approval_decisions
and any(item.candidate_id == candidate_id for item in approval_decisions)
else None
)
publication_records = getattr(repository, "publication_records", [])
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),
reverse=True,
)[0]
if publication_records
and any(item.candidate_id == candidate_id for item in publication_records)
else None
)
return CandidateOverviewDTO(
candidate_id=candidate.id,
version=candidate.version,
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_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_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,
)
# [/DEF:get_candidate_overview_v2_endpoint:Function]
# [DEF:prepare_candidate_endpoint:Function]
# @PURPOSE: Prepare candidate with policy evaluation and deterministic manifest generation.
# @PRE: Candidate and active policy exist in repository.
@@ -99,47 +327,79 @@ async def start_check(
if candidate is None:
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:
raise HTTPException(status_code=409, detail={"message": "No manifest found for candidate", "code": "MANIFEST_NOT_FOUND"})
latest_manifest = sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]
orchestrator = CleanComplianceOrchestrator(repository)
run = orchestrator.start_check_run(
candidate_id=payload.candidate_id,
policy_id=policy.policy_id,
triggered_by=payload.triggered_by,
execution_mode=payload.execution_mode,
policy_id=policy.id,
requested_by=payload.triggered_by,
manifest_id=latest_manifest.id,
)
forced = [
CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS, details="ok"),
CheckStageResult(stage=CheckStageName.INTERNAL_SOURCES_ONLY, status=CheckStageStatus.PASS, details="ok"),
CheckStageResult(stage=CheckStageName.NO_EXTERNAL_ENDPOINTS, status=CheckStageStatus.PASS, details="ok"),
CheckStageResult(stage=CheckStageName.MANIFEST_CONSISTENCY, status=CheckStageStatus.PASS, details="ok"),
ComplianceStageRun(
id=f"stage-{run.id}-1",
run_id=run.id,
stage_name=ComplianceStageName.DATA_PURITY.value,
status=RunStatus.SUCCEEDED.value,
decision=ComplianceDecision.PASSED.value,
details_json={"message": "ok"}
),
ComplianceStageRun(
id=f"stage-{run.id}-2",
run_id=run.id,
stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,
status=RunStatus.SUCCEEDED.value,
decision=ComplianceDecision.PASSED.value,
details_json={"message": "ok"}
),
ComplianceStageRun(
id=f"stage-{run.id}-3",
run_id=run.id,
stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
status=RunStatus.SUCCEEDED.value,
decision=ComplianceDecision.PASSED.value,
details_json={"message": "ok"}
),
ComplianceStageRun(
id=f"stage-{run.id}-4",
run_id=run.id,
stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,
status=RunStatus.SUCCEEDED.value,
decision=ComplianceDecision.PASSED.value,
details_json={"message": "ok"}
),
]
run = orchestrator.execute_stages(run, forced_results=forced)
run = orchestrator.finalize_run(run)
if run.final_status == CheckFinalStatus.BLOCKED:
if run.final_status == ComplianceDecision.BLOCKED.value:
logger.explore("Run ended as BLOCKED, persisting synthetic external-source violation")
violation = ComplianceViolation(
violation_id=f"viol-{run.check_run_id}",
check_run_id=run.check_run_id,
category=ViolationCategory.EXTERNAL_SOURCE,
severity=ViolationSeverity.CRITICAL,
location="external.example.com",
remediation="Replace with approved internal server",
blocked_release=True,
detected_at=datetime.now(timezone.utc),
id=f"viol-{run.id}",
run_id=run.id,
stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,
code="EXTERNAL_SOURCE_DETECTED",
severity=ViolationSeverity.CRITICAL.value,
message="Replace with approved internal server",
evidence_json={"location": "external.example.com"}
)
repository.save_violation(violation)
builder = ComplianceReportBuilder(repository)
report = builder.build_report_payload(run, repository.get_violations_by_check_run(run.check_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 check_run_id={run.check_run_id}")
logger.reflect(f"Compliance report persisted for run_id={run.id}")
return {
"check_run_id": run.check_run_id,
"check_run_id": run.id,
"candidate_id": run.candidate_id,
"status": "running",
"started_at": run.started_at.isoformat(),
"started_at": run.started_at.isoformat() if run.started_at else None,
}
# [/DEF:start_check:Function]
@@ -157,13 +417,13 @@ async def get_check_status(check_run_id: str, repository: CleanReleaseRepository
logger.reflect(f"Returning check status for check_run_id={check_run_id}")
return {
"check_run_id": run.check_run_id,
"check_run_id": run.id,
"candidate_id": run.candidate_id,
"final_status": run.final_status.value,
"started_at": run.started_at.isoformat(),
"final_status": run.final_status,
"started_at": run.started_at.isoformat() if run.started_at else None,
"finished_at": run.finished_at.isoformat() if run.finished_at else None,
"checks": [c.model_dump() for c in run.checks],
"violations": [v.model_dump() for v in repository.get_violations_by_check_run(check_run_id)],
"checks": [], # TODO: Map stages if needed
"violations": [], # TODO: Map violations if needed
}
# [/DEF:get_check_status:Function]

View File

@@ -0,0 +1,216 @@
# [DEF:backend.src.api.routes.clean_release_v2:Module]
# @TIER: STANDARD
# @SEMANTICS: api, clean-release, v2, headless
# @PURPOSE: Redesigned clean release API for headless candidate lifecycle.
# @LAYER: API
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Dict, Any
from datetime import datetime, timezone
from ...services.clean_release.approval_service import approve_candidate, reject_candidate
from ...services.clean_release.publication_service import publish_candidate, revoke_publication
from ...services.clean_release.repository import CleanReleaseRepository
from ...dependencies import get_clean_release_repository
from ...services.clean_release.enums import CandidateStatus
from ...models.clean_release import ReleaseCandidate, CandidateArtifact, DistributionManifest
from ...services.clean_release.dto import CandidateDTO, ManifestDTO
router = APIRouter(prefix="/api/v2/clean-release", tags=["Clean Release V2"])
class ApprovalRequest(dict):
pass
class PublishRequest(dict):
pass
class RevokeRequest(dict):
pass
@router.post("/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED)
async def register_candidate(
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository)
):
candidate = ReleaseCandidate(
id=payload["id"],
version=payload["version"],
source_snapshot_ref=payload["source_snapshot_ref"],
created_by=payload["created_by"],
created_at=datetime.now(timezone.utc),
status=CandidateStatus.DRAFT.value
)
repository.save_candidate(candidate)
return CandidateDTO(
id=candidate.id,
version=candidate.version,
source_snapshot_ref=candidate.source_snapshot_ref,
created_at=candidate.created_at,
created_by=candidate.created_by,
status=CandidateStatus(candidate.status)
)
@router.post("/candidates/{candidate_id}/artifacts")
async def import_artifacts(
candidate_id: str,
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository)
):
candidate = repository.get_candidate(candidate_id)
if not candidate:
raise HTTPException(status_code=404, detail="Candidate not found")
for art_data in payload.get("artifacts", []):
artifact = CandidateArtifact(
id=art_data["id"],
candidate_id=candidate_id,
path=art_data["path"],
sha256=art_data["sha256"],
size=art_data["size"]
)
# In a real repo we'd have save_artifact
# repository.save_artifact(artifact)
pass
return {"status": "success"}
@router.post("/candidates/{candidate_id}/manifests", response_model=ManifestDTO, status_code=status.HTTP_201_CREATED)
async def build_manifest(
candidate_id: str,
repository: CleanReleaseRepository = Depends(get_clean_release_repository)
):
candidate = repository.get_candidate(candidate_id)
if not candidate:
raise HTTPException(status_code=404, detail="Candidate not found")
manifest = DistributionManifest(
id=f"manifest-{candidate_id}",
candidate_id=candidate_id,
manifest_version=1,
manifest_digest="hash-123",
artifacts_digest="art-hash-123",
created_by="system",
created_at=datetime.now(timezone.utc),
source_snapshot_ref=candidate.source_snapshot_ref,
content_json={"items": [], "summary": {}}
)
repository.save_manifest(manifest)
return ManifestDTO(
id=manifest.id,
candidate_id=manifest.candidate_id,
manifest_version=manifest.manifest_version,
manifest_digest=manifest.manifest_digest,
artifacts_digest=manifest.artifacts_digest,
created_at=manifest.created_at,
created_by=manifest.created_by,
source_snapshot_ref=manifest.source_snapshot_ref,
content_json=manifest.content_json
)
@router.post("/candidates/{candidate_id}/approve")
async def approve_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
decision = approve_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=str(payload["report_id"]),
decided_by=str(payload["decided_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"})
return {"status": "ok", "decision": decision.decision, "decision_id": decision.id}
@router.post("/candidates/{candidate_id}/reject")
async def reject_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
decision = reject_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=str(payload["report_id"]),
decided_by=str(payload["decided_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"})
return {"status": "ok", "decision": decision.decision, "decision_id": decision.id}
@router.post("/candidates/{candidate_id}/publish")
async def publish_candidate_endpoint(
candidate_id: str,
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
publication = publish_candidate(
repository=repository,
candidate_id=candidate_id,
report_id=str(payload["report_id"]),
published_by=str(payload["published_by"]),
target_channel=str(payload["target_channel"]),
publication_ref=payload.get("publication_ref"),
)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=409, detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"})
return {
"status": "ok",
"publication": {
"id": publication.id,
"candidate_id": publication.candidate_id,
"report_id": publication.report_id,
"published_by": publication.published_by,
"published_at": publication.published_at.isoformat() if publication.published_at else None,
"target_channel": publication.target_channel,
"publication_ref": publication.publication_ref,
"status": publication.status,
},
}
@router.post("/publications/{publication_id}/revoke")
async def revoke_publication_endpoint(
publication_id: str,
payload: Dict[str, Any],
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
):
try:
publication = revoke_publication(
repository=repository,
publication_id=publication_id,
revoked_by=str(payload["revoked_by"]),
comment=payload.get("comment"),
)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=409, detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"})
return {
"status": "ok",
"publication": {
"id": publication.id,
"candidate_id": publication.candidate_id,
"report_id": publication.report_id,
"published_by": publication.published_by,
"published_at": publication.published_at.isoformat() if publication.published_at else None,
"target_channel": publication.target_channel,
"publication_ref": publication.publication_ref,
"status": publication.status,
},
}
# [/DEF:backend.src.api.routes.clean_release_v2:Module]

View File

@@ -48,6 +48,7 @@ from ...dependencies import (
has_permission,
)
from ...core.database import get_db
from ...core.async_superset_client import AsyncSupersetClient
from ...core.logger import logger, belief_scope
from ...core.superset_client import SupersetClient
from ...core.superset_profile_lookup import SupersetAccountLookupAdapter
@@ -229,6 +230,56 @@ def _resolve_dashboard_id_from_ref(
# [/DEF:_resolve_dashboard_id_from_ref:Function]
# [DEF:_find_dashboard_id_by_slug_async:Function]
# @PURPOSE: Resolve dashboard numeric ID by slug using async Superset list endpoint.
# @PRE: dashboard_slug is non-empty.
# @POST: Returns dashboard ID when found, otherwise None.
async def _find_dashboard_id_by_slug_async(
client: AsyncSupersetClient,
dashboard_slug: str,
) -> Optional[int]:
query_variants = [
{"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1},
{"filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1},
]
for query in query_variants:
try:
_count, dashboards = await client.get_dashboards_page_async(query=query)
if dashboards:
resolved_id = dashboards[0].get("id")
if resolved_id is not None:
return int(resolved_id)
except Exception:
continue
return None
# [/DEF:_find_dashboard_id_by_slug_async:Function]
# [DEF:_resolve_dashboard_id_from_ref_async:Function]
# @PURPOSE: Resolve dashboard ID from slug-first reference using async Superset client.
# @PRE: dashboard_ref is provided in route path.
# @POST: Returns valid dashboard ID or raises HTTPException(404).
async def _resolve_dashboard_id_from_ref_async(
dashboard_ref: str,
client: AsyncSupersetClient,
) -> int:
normalized_ref = str(dashboard_ref or "").strip()
if not normalized_ref:
raise HTTPException(status_code=404, detail="Dashboard not found")
slug_match_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)
if slug_match_id is not None:
return slug_match_id
if normalized_ref.isdigit():
return int(normalized_ref)
raise HTTPException(status_code=404, detail="Dashboard not found")
# [/DEF:_resolve_dashboard_id_from_ref_async:Function]
# [DEF:_normalize_filter_values:Function]
# @PURPOSE: Normalize query filter values to lower-cased non-empty tokens.
# @PRE: values may be None or list of strings.
@@ -776,10 +827,10 @@ async def get_dashboard_detail(
logger.error(f"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = AsyncSupersetClient(env)
try:
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
detail = client.get_dashboard_detail(dashboard_id)
dashboard_id = await _resolve_dashboard_id_from_ref_async(dashboard_ref, client)
detail = await client.get_dashboard_detail_async(dashboard_id)
logger.info(
f"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets"
)
@@ -789,6 +840,8 @@ async def get_dashboard_detail(
except Exception as e:
logger.error(f"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard detail: {str(e)}")
finally:
await client.aclose()
# [/DEF:get_dashboard_detail:Function]
@@ -840,69 +893,74 @@ async def get_dashboard_tasks_history(
):
with belief_scope("get_dashboard_tasks_history", f"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}"):
dashboard_id: Optional[int] = None
if dashboard_ref.isdigit():
dashboard_id = int(dashboard_ref)
elif env_id:
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
else:
logger.error(
"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id"
)
raise HTTPException(
status_code=400,
detail="env_id is required when dashboard reference is a slug",
)
matching_tasks = []
for task in task_manager.get_all_tasks():
if _task_matches_dashboard(task, dashboard_id, env_id):
matching_tasks.append(task)
def _sort_key(task_obj: Any) -> str:
return (
str(getattr(task_obj, "started_at", "") or "")
or str(getattr(task_obj, "finished_at", "") or "")
)
matching_tasks.sort(key=_sort_key, reverse=True)
selected = matching_tasks[:limit]
items = []
for task in selected:
result = getattr(task, "result", None)
summary = None
validation_status = None
if isinstance(result, dict):
raw_validation_status = result.get("status")
if raw_validation_status is not None:
validation_status = str(raw_validation_status)
summary = (
result.get("summary")
or result.get("status")
or result.get("message")
client: Optional[AsyncSupersetClient] = None
try:
if dashboard_ref.isdigit():
dashboard_id = int(dashboard_ref)
elif env_id:
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
logger.error(f"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = AsyncSupersetClient(env)
dashboard_id = await _resolve_dashboard_id_from_ref_async(dashboard_ref, client)
else:
logger.error(
"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id"
)
params = getattr(task, "params", {}) or {}
items.append(
DashboardTaskHistoryItem(
id=str(getattr(task, "id", "")),
plugin_id=str(getattr(task, "plugin_id", "")),
status=str(getattr(task, "status", "")),
validation_status=validation_status,
started_at=getattr(task, "started_at", None).isoformat() if getattr(task, "started_at", None) else None,
finished_at=getattr(task, "finished_at", None).isoformat() if getattr(task, "finished_at", None) else None,
env_id=str(params.get("environment_id") or params.get("env")) if (params.get("environment_id") or params.get("env")) else None,
summary=summary,
raise HTTPException(
status_code=400,
detail="env_id is required when dashboard reference is a slug",
)
)
logger.info(f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}")
return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)
matching_tasks = []
for task in task_manager.get_all_tasks():
if _task_matches_dashboard(task, dashboard_id, env_id):
matching_tasks.append(task)
def _sort_key(task_obj: Any) -> str:
return (
str(getattr(task_obj, "started_at", "") or "")
or str(getattr(task_obj, "finished_at", "") or "")
)
matching_tasks.sort(key=_sort_key, reverse=True)
selected = matching_tasks[:limit]
items = []
for task in selected:
result = getattr(task, "result", None)
summary = None
validation_status = None
if isinstance(result, dict):
raw_validation_status = result.get("status")
if raw_validation_status is not None:
validation_status = str(raw_validation_status)
summary = (
result.get("summary")
or result.get("status")
or result.get("message")
)
params = getattr(task, "params", {}) or {}
items.append(
DashboardTaskHistoryItem(
id=str(getattr(task, "id", "")),
plugin_id=str(getattr(task, "plugin_id", "")),
status=str(getattr(task, "status", "")),
validation_status=validation_status,
started_at=getattr(task, "started_at", None).isoformat() if getattr(task, "started_at", None) else None,
finished_at=getattr(task, "finished_at", None).isoformat() if getattr(task, "finished_at", None) else None,
env_id=str(params.get("environment_id") or params.get("env")) if (params.get("environment_id") or params.get("env")) else None,
summary=summary,
)
)
logger.info(f"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}")
return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)
finally:
if client is not None:
await client.aclose()
# [/DEF:get_dashboard_tasks_history:Function]
@@ -925,15 +983,15 @@ async def get_dashboard_thumbnail(
logger.error(f"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}")
raise HTTPException(status_code=404, detail="Environment not found")
client = AsyncSupersetClient(env)
try:
client = SupersetClient(env)
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)
dashboard_id = await _resolve_dashboard_id_from_ref_async(dashboard_ref, client)
digest = None
thumb_endpoint = None
# Preferred flow (newer Superset): ask server to cache screenshot and return digest/image_url.
try:
screenshot_payload = client.network.request(
screenshot_payload = await client.network.request(
method="POST",
endpoint=f"/dashboard/{dashboard_id}/cache_dashboard_screenshot/",
json={"force": force},
@@ -951,7 +1009,7 @@ async def get_dashboard_thumbnail(
# Fallback flow (older Superset): read thumbnail_url from dashboard payload.
if not digest:
dashboard_payload = client.network.request(
dashboard_payload = await client.network.request(
method="GET",
endpoint=f"/dashboard/{dashboard_id}",
)
@@ -970,7 +1028,7 @@ async def get_dashboard_thumbnail(
if not thumb_endpoint:
thumb_endpoint = f"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/"
thumb_response = client.network.request(
thumb_response = await client.network.request(
method="GET",
endpoint=thumb_endpoint,
raw_response=True,
@@ -995,6 +1053,8 @@ async def get_dashboard_thumbnail(
except Exception as e:
logger.error(f"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}")
raise HTTPException(status_code=503, detail=f"Failed to fetch dashboard thumbnail: {str(e)}")
finally:
await client.aclose()
# [/DEF:get_dashboard_thumbnail:Function]
# [DEF:MigrateRequest:DataClass]

View File

@@ -33,6 +33,7 @@ from src.api.routes.git_schemas import (
MergeStatusSchema, MergeConflictFileSchema, MergeResolveRequest, MergeContinueRequest,
)
from src.services.git_service import GitService
from src.core.async_superset_client import AsyncSupersetClient
from src.core.superset_client import SupersetClient
from src.core.logger import logger, belief_scope
from ...services.llm_prompt_templates import (
@@ -180,6 +181,70 @@ def _resolve_dashboard_id_from_ref(
# [/DEF:_resolve_dashboard_id_from_ref:Function]
# [DEF:_find_dashboard_id_by_slug_async:Function]
# @PURPOSE: Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes.
# @PRE: dashboard_slug is non-empty.
# @POST: Returns dashboard ID or None when not found.
async def _find_dashboard_id_by_slug_async(
client: AsyncSupersetClient,
dashboard_slug: str,
) -> Optional[int]:
query_variants = [
{"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1},
{"filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}], "page": 0, "page_size": 1},
]
for query in query_variants:
try:
_count, dashboards = await client.get_dashboards_page_async(query=query)
if dashboards:
resolved_id = dashboards[0].get("id")
if resolved_id is not None:
return int(resolved_id)
except Exception:
continue
return None
# [/DEF:_find_dashboard_id_by_slug_async:Function]
# [DEF:_resolve_dashboard_id_from_ref_async:Function]
# @PURPOSE: Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes.
# @PRE: dashboard_ref is provided; env_id is required for slug values.
# @POST: Returns numeric dashboard ID or raises HTTPException.
async def _resolve_dashboard_id_from_ref_async(
dashboard_ref: str,
config_manager,
env_id: Optional[str] = None,
) -> int:
normalized_ref = str(dashboard_ref or "").strip()
if not normalized_ref:
raise HTTPException(status_code=400, detail="dashboard_ref is required")
if normalized_ref.isdigit():
return int(normalized_ref)
if not env_id:
raise HTTPException(
status_code=400,
detail="env_id is required for slug-based Git operations",
)
environments = config_manager.get_environments()
env = next((e for e in environments if e.id == env_id), None)
if not env:
raise HTTPException(status_code=404, detail="Environment not found")
client = AsyncSupersetClient(env)
try:
dashboard_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)
if dashboard_id is None:
raise HTTPException(status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found")
return dashboard_id
finally:
await client.aclose()
# [/DEF:_resolve_dashboard_id_from_ref_async:Function]
# [DEF:_resolve_repo_key_from_ref:Function]
# @PURPOSE: Resolve repository folder key with slug-first strategy and deterministic fallback.
# @PRE: dashboard_id is resolved and valid.
@@ -1197,7 +1262,7 @@ async def get_repository_status(
):
with belief_scope("get_repository_status"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
dashboard_id = await _resolve_dashboard_id_from_ref_async(dashboard_ref, config_manager, env_id)
return _resolve_repository_status(dashboard_id)
except HTTPException:
raise

View File

@@ -13,10 +13,11 @@ from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from ...dependencies import get_task_manager, has_permission
from ...dependencies import get_task_manager, has_permission, get_clean_release_repository
from ...core.task_manager import TaskManager
from ...core.logger import belief_scope
from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskType
from ...services.clean_release.repository import CleanReleaseRepository
from ...services.reports.report_service import ReportsService
# [/SECTION]
@@ -88,6 +89,7 @@ async def list_reports(
sort_by: str = Query("updated_at"),
sort_order: str = Query("desc"),
task_manager: TaskManager = Depends(get_task_manager),
clean_release_repository: CleanReleaseRepository = Depends(get_clean_release_repository),
_=Depends(has_permission("tasks", "READ")),
):
with belief_scope("list_reports"):
@@ -117,7 +119,7 @@ async def list_reports(
},
)
service = ReportsService(task_manager)
service = ReportsService(task_manager, clean_release_repository=clean_release_repository)
return service.list_reports(query)
# [/DEF:list_reports:Function]
@@ -130,10 +132,11 @@ async def list_reports(
async def get_report_detail(
report_id: str,
task_manager: TaskManager = Depends(get_task_manager),
clean_release_repository: CleanReleaseRepository = Depends(get_clean_release_repository),
_=Depends(has_permission("tasks", "READ")),
):
with belief_scope("get_report_detail", f"report_id={report_id}"):
service = ReportsService(task_manager)
service = ReportsService(task_manager, clean_release_repository=clean_release_repository)
detail = service.get_report_detail(report_id)
if not detail:
raise HTTPException(

View File

@@ -21,7 +21,7 @@ import asyncio
from .dependencies import get_task_manager, get_scheduler_service
from .core.utils.network import NetworkError
from .core.logger import logger, belief_scope
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, profile
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, clean_release_v2, profile
from .api import auth
# [DEF:App:Global]
@@ -134,6 +134,7 @@ app.include_router(datasets.router)
app.include_router(reports.router)
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
app.include_router(clean_release.router)
app.include_router(clean_release_v2.router)
app.include_router(profile.router)

View File

@@ -0,0 +1,298 @@
# [DEF:backend.src.core.async_superset_client:Module]
#
# @TIER: CRITICAL
# @SEMANTICS: superset, async, client, httpx, dashboards, datasets
# @PURPOSE: Async Superset client for dashboard hot-path requests without blocking FastAPI event loop.
# @LAYER: Core
# @RELATION: DEPENDS_ON -> backend.src.core.superset_client
# @RELATION: DEPENDS_ON -> backend.src.core.utils.async_network.AsyncAPIClient
# @INVARIANT: Async dashboard operations reuse shared auth cache and avoid sync requests in async routes.
# [SECTION: IMPORTS]
import asyncio
import json
import re
from typing import Any, Dict, List, Optional, Tuple, cast
from .config_models import Environment
from .logger import logger as app_logger, belief_scope
from .superset_client import SupersetClient
from .utils.async_network import AsyncAPIClient
# [/SECTION]
# [DEF:AsyncSupersetClient:Class]
# @PURPOSE: Async sibling of SupersetClient for dashboard read paths.
class AsyncSupersetClient(SupersetClient):
# [DEF:__init__:Function]
# @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.
# @PRE: env is valid.
# @POST: Client uses async network transport and inherited projection helpers.
def __init__(self, env: Environment):
self.env = env
auth_payload = {
"username": env.username,
"password": env.password,
"provider": "db",
"refresh": "true",
}
self.network = AsyncAPIClient(
config={"base_url": env.url, "auth": auth_payload},
verify_ssl=env.verify_ssl,
timeout=env.timeout,
)
self.delete_before_reimport = False
# [/DEF:__init__:Function]
# [DEF:aclose:Function]
# @PURPOSE: Close async transport resources.
# @POST: Underlying AsyncAPIClient is closed.
async def aclose(self) -> None:
await self.network.aclose()
# [/DEF:aclose:Function]
# [DEF:get_dashboards_page_async:Function]
# @PURPOSE: Fetch one dashboards page asynchronously.
# @POST: Returns total count and page result list.
async def get_dashboards_page_async(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("AsyncSupersetClient.get_dashboards_page_async"):
validated_query = self._validate_query_params(query or {})
if "columns" not in validated_query:
validated_query["columns"] = [
"slug",
"id",
"url",
"changed_on_utc",
"dashboard_title",
"published",
"created_by",
"changed_by",
"changed_by_name",
"owners",
]
response_json = cast(
Dict[str, Any],
await self.network.request(
method="GET",
endpoint="/dashboard/",
params={"q": json.dumps(validated_query)},
),
)
result = response_json.get("result", [])
total_count = response_json.get("count", len(result))
return total_count, result
# [/DEF:get_dashboards_page_async:Function]
# [DEF:get_dashboard_async:Function]
# @PURPOSE: Fetch one dashboard payload asynchronously.
# @POST: Returns raw dashboard payload from Superset API.
async def get_dashboard_async(self, dashboard_id: int) -> Dict:
with belief_scope("AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}"):
response = await self.network.request(method="GET", endpoint=f"/dashboard/{dashboard_id}")
return cast(Dict, response)
# [/DEF:get_dashboard_async:Function]
# [DEF:get_chart_async:Function]
# @PURPOSE: Fetch one chart payload asynchronously.
# @POST: Returns raw chart payload from Superset API.
async def get_chart_async(self, chart_id: int) -> Dict:
with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"):
response = await self.network.request(method="GET", endpoint=f"/chart/{chart_id}")
return cast(Dict, response)
# [/DEF:get_chart_async:Function]
# [DEF:get_dashboard_detail_async:Function]
# @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
# @POST: Returns dashboard detail payload for overview page.
async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:
with belief_scope("AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}"):
dashboard_response = await self.get_dashboard_async(dashboard_id)
dashboard_data = dashboard_response.get("result", dashboard_response)
charts: List[Dict] = []
datasets: List[Dict] = []
def extract_dataset_id_from_form_data(form_data: Optional[Dict]) -> Optional[int]:
if not isinstance(form_data, dict):
return None
datasource = form_data.get("datasource")
if isinstance(datasource, str):
matched = re.match(r"^(\d+)__", datasource)
if matched:
try:
return int(matched.group(1))
except ValueError:
return None
if isinstance(datasource, dict):
ds_id = datasource.get("id")
try:
return int(ds_id) if ds_id is not None else None
except (TypeError, ValueError):
return None
ds_id = form_data.get("datasource_id")
try:
return int(ds_id) if ds_id is not None else None
except (TypeError, ValueError):
return None
chart_task = self.network.request(
method="GET",
endpoint=f"/dashboard/{dashboard_id}/charts",
)
dataset_task = self.network.request(
method="GET",
endpoint=f"/dashboard/{dashboard_id}/datasets",
)
charts_response, datasets_response = await asyncio.gather(
chart_task,
dataset_task,
return_exceptions=True,
)
if not isinstance(charts_response, Exception):
charts_payload = charts_response.get("result", []) if isinstance(charts_response, dict) else []
for chart_obj in charts_payload:
if not isinstance(chart_obj, dict):
continue
chart_id = chart_obj.get("id")
if chart_id is None:
continue
form_data = chart_obj.get("form_data")
if isinstance(form_data, str):
try:
form_data = json.loads(form_data)
except Exception:
form_data = {}
dataset_id = extract_dataset_id_from_form_data(form_data) or chart_obj.get("datasource_id")
charts.append({
"id": int(chart_id),
"title": chart_obj.get("slice_name") or chart_obj.get("name") or f"Chart {chart_id}",
"viz_type": (form_data.get("viz_type") if isinstance(form_data, dict) else None),
"dataset_id": int(dataset_id) if dataset_id is not None else None,
"last_modified": chart_obj.get("changed_on"),
"overview": chart_obj.get("description") or (form_data.get("viz_type") if isinstance(form_data, dict) else None) or "Chart",
})
else:
app_logger.warning("[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s", charts_response)
if not isinstance(datasets_response, Exception):
datasets_payload = datasets_response.get("result", []) if isinstance(datasets_response, dict) else []
for dataset_obj in datasets_payload:
if not isinstance(dataset_obj, dict):
continue
dataset_id = dataset_obj.get("id")
if dataset_id is None:
continue
db_payload = dataset_obj.get("database")
db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
table_name = dataset_obj.get("table_name") or dataset_obj.get("datasource_name") or dataset_obj.get("name") or f"Dataset {dataset_id}"
schema = dataset_obj.get("schema")
fq_name = f"{schema}.{table_name}" if schema else table_name
datasets.append({
"id": int(dataset_id),
"table_name": table_name,
"schema": schema,
"database": db_name or dataset_obj.get("database_name") or "Unknown",
"last_modified": dataset_obj.get("changed_on"),
"overview": fq_name,
})
else:
app_logger.warning("[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s", datasets_response)
if not charts:
raw_position_json = dashboard_data.get("position_json")
chart_ids_from_position = set()
if isinstance(raw_position_json, str) and raw_position_json:
try:
parsed_position = json.loads(raw_position_json)
chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_position))
except Exception:
pass
elif isinstance(raw_position_json, dict):
chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_position_json))
raw_json_metadata = dashboard_data.get("json_metadata")
if isinstance(raw_json_metadata, str) and raw_json_metadata:
try:
parsed_metadata = json.loads(raw_json_metadata)
chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_metadata))
except Exception:
pass
elif isinstance(raw_json_metadata, dict):
chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_json_metadata))
fallback_chart_tasks = [
self.get_chart_async(int(chart_id))
for chart_id in sorted(chart_ids_from_position)
]
fallback_chart_responses = await asyncio.gather(
*fallback_chart_tasks,
return_exceptions=True,
)
for chart_id, chart_response in zip(sorted(chart_ids_from_position), fallback_chart_responses):
if isinstance(chart_response, Exception):
app_logger.warning("[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s", chart_id, chart_response)
continue
chart_data = chart_response.get("result", chart_response)
charts.append({
"id": int(chart_id),
"title": chart_data.get("slice_name") or chart_data.get("name") or f"Chart {chart_id}",
"viz_type": chart_data.get("viz_type"),
"dataset_id": chart_data.get("datasource_id"),
"last_modified": chart_data.get("changed_on"),
"overview": chart_data.get("description") or chart_data.get("viz_type") or "Chart",
})
dataset_ids_from_charts = {
c.get("dataset_id")
for c in charts
if c.get("dataset_id") is not None
}
known_dataset_ids = {d.get("id") for d in datasets if d.get("id") is not None}
missing_dataset_ids = sorted(int(item) for item in dataset_ids_from_charts if item not in known_dataset_ids)
if missing_dataset_ids:
dataset_fetch_tasks = [
self.network.request(method="GET", endpoint=f"/dataset/{dataset_id}")
for dataset_id in missing_dataset_ids
]
dataset_fetch_responses = await asyncio.gather(
*dataset_fetch_tasks,
return_exceptions=True,
)
for dataset_id, dataset_response in zip(missing_dataset_ids, dataset_fetch_responses):
if isinstance(dataset_response, Exception):
app_logger.warning("[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s", dataset_id, dataset_response)
continue
dataset_data = dataset_response.get("result", dataset_response) if isinstance(dataset_response, dict) else {}
db_payload = dataset_data.get("database")
db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
table_name = dataset_data.get("table_name") or dataset_data.get("datasource_name") or dataset_data.get("name") or f"Dataset {dataset_id}"
schema = dataset_data.get("schema")
fq_name = f"{schema}.{table_name}" if schema else table_name
datasets.append({
"id": int(dataset_id),
"table_name": table_name,
"schema": schema,
"database": db_name or dataset_data.get("database_name") or "Unknown",
"last_modified": dataset_data.get("changed_on"),
"overview": fq_name,
})
return {
"id": int(dashboard_data.get("id") or dashboard_id),
"title": dashboard_data.get("dashboard_title") or dashboard_data.get("title") or f"Dashboard {dashboard_id}",
"slug": dashboard_data.get("slug"),
"url": dashboard_data.get("url"),
"description": dashboard_data.get("description"),
"last_modified": dashboard_data.get("changed_on_utc") or dashboard_data.get("changed_on"),
"published": dashboard_data.get("published"),
"charts": charts,
"datasets": datasets,
"chart_count": len(charts),
"dataset_count": len(datasets),
}
# [/DEF:get_dashboard_detail_async:Function]
# [/DEF:AsyncSupersetClient:Class]
# [/DEF:backend.src.core.async_superset_client:Module]

View File

@@ -24,19 +24,19 @@ class Schedule(BaseModel):
# [DEF:Environment:DataClass]
# @PURPOSE: Represents a Superset environment configuration.
class Environment(BaseModel):
id: str
name: str
url: str
username: str
password: str # Will be masked in UI
stage: str = Field(default="DEV", pattern="^(DEV|PREPROD|PROD)$")
verify_ssl: bool = True
timeout: int = 30
is_default: bool = False
is_production: bool = False
backup_schedule: Schedule = Field(default_factory=Schedule)
# [/DEF:Environment:DataClass]
class Environment(BaseModel):
id: str
name: str
url: str
username: str
password: str # Will be masked in UI
stage: str = Field(default="DEV", pattern="^(DEV|PREPROD|PROD)$")
verify_ssl: bool = True
timeout: int = 30
is_default: bool = False
is_production: bool = False
backup_schedule: Schedule = Field(default_factory=Schedule)
# [/DEF:Environment:DataClass]
# [DEF:LoggingConfig:DataClass]
# @PURPOSE: Defines the configuration for the application's logging system.
@@ -49,10 +49,18 @@ class LoggingConfig(BaseModel):
enable_belief_state: bool = True
# [/DEF:LoggingConfig:DataClass]
# [DEF:CleanReleaseConfig:DataClass]
# @PURPOSE: Configuration for clean release compliance subsystem.
class CleanReleaseConfig(BaseModel):
active_policy_id: Optional[str] = None
active_registry_id: Optional[str] = None
# [/DEF:CleanReleaseConfig:DataClass]
# [DEF:GlobalSettings:DataClass]
# @PURPOSE: Represents global application settings.
class GlobalSettings(BaseModel):
storage: StorageConfig = Field(default_factory=StorageConfig)
clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)
default_environment_id: Optional[str] = None
logging: LoggingConfig = Field(default_factory=LoggingConfig)
connections: List[dict] = []

View File

@@ -21,6 +21,7 @@ from ..models import config as _config_models # noqa: F401
from ..models import llm as _llm_models # noqa: F401
from ..models import assistant as _assistant_models # noqa: F401
from ..models import profile as _profile_models # noqa: F401
from ..models import clean_release as _clean_release_models # noqa: F401
from .logger import belief_scope, logger
from .auth.config import auth_config
import os

View File

@@ -0,0 +1,237 @@
# [DEF:backend.src.core.utils.async_network:Module]
#
# @TIER: CRITICAL
# @SEMANTICS: network, httpx, async, superset, authentication, cache
# @PURPOSE: Provides async Superset API client with shared auth-token cache to avoid per-request re-login.
# @LAYER: Infra
# @RELATION: DEPENDS_ON -> backend.src.core.utils.network.SupersetAuthCache
# @INVARIANT: Async client reuses cached auth tokens per environment credentials and invalidates on 401.
# [SECTION: IMPORTS]
from typing import Optional, Dict, Any, Union
import asyncio
import httpx
from ..logger import logger as app_logger, belief_scope
from .network import (
AuthenticationError,
DashboardNotFoundError,
NetworkError,
PermissionDeniedError,
SupersetAPIError,
SupersetAuthCache,
)
# [/SECTION]
# [DEF:AsyncAPIClient:Class]
# @PURPOSE: Async Superset API client backed by httpx.AsyncClient with shared auth cache.
class AsyncAPIClient:
DEFAULT_TIMEOUT = 30
_auth_locks: Dict[tuple[str, str, bool], asyncio.Lock] = {}
# [DEF:__init__:Function]
# @PURPOSE: Initialize async API client for one environment.
# @PRE: config contains base_url and auth payload.
# @POST: Client is ready for async request/authentication flow.
def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):
self.base_url: str = self._normalize_base_url(config.get("base_url", ""))
self.api_base_url: str = f"{self.base_url}/api/v1"
self.auth = config.get("auth")
self.request_settings = {"verify_ssl": verify_ssl, "timeout": timeout}
self._client = httpx.AsyncClient(
verify=verify_ssl,
timeout=httpx.Timeout(timeout),
follow_redirects=True,
)
self._tokens: Dict[str, str] = {}
self._authenticated = False
self._auth_cache_key = SupersetAuthCache.build_key(
self.base_url,
self.auth,
verify_ssl,
)
# [/DEF:__init__:Function]
# [DEF:_normalize_base_url:Function]
# @PURPOSE: Normalize base URL for Superset API root construction.
# @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.
def _normalize_base_url(self, raw_url: str) -> str:
normalized = str(raw_url or "").strip().rstrip("/")
if normalized.lower().endswith("/api/v1"):
normalized = normalized[:-len("/api/v1")]
return normalized.rstrip("/")
# [/DEF:_normalize_base_url:Function]
# [DEF:_build_api_url:Function]
# @PURPOSE: Build full API URL from relative Superset endpoint.
# @POST: Returns absolute URL for upstream request.
def _build_api_url(self, endpoint: str) -> str:
normalized_endpoint = str(endpoint or "").strip()
if normalized_endpoint.startswith("http://") or normalized_endpoint.startswith("https://"):
return normalized_endpoint
if not normalized_endpoint.startswith("/"):
normalized_endpoint = f"/{normalized_endpoint}"
if normalized_endpoint.startswith("/api/v1/") or normalized_endpoint == "/api/v1":
return f"{self.base_url}{normalized_endpoint}"
return f"{self.api_base_url}{normalized_endpoint}"
# [/DEF:_build_api_url:Function]
# [DEF:_get_auth_lock:Function]
# @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.
# @POST: Returns stable asyncio.Lock instance.
@classmethod
def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:
existing_lock = cls._auth_locks.get(cache_key)
if existing_lock is not None:
return existing_lock
created_lock = asyncio.Lock()
cls._auth_locks[cache_key] = created_lock
return created_lock
# [/DEF:_get_auth_lock:Function]
# [DEF:authenticate:Function]
# @PURPOSE: Authenticate against Superset and cache access/csrf tokens.
# @POST: Client tokens are populated and reusable across requests.
async def authenticate(self) -> Dict[str, str]:
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
self._tokens = cached_tokens
self._authenticated = True
app_logger.info("[async_authenticate][CacheHit] Reusing cached Superset auth tokens for %s", self.base_url)
return self._tokens
auth_lock = self._get_auth_lock(self._auth_cache_key)
async with auth_lock:
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
self._tokens = cached_tokens
self._authenticated = True
app_logger.info("[async_authenticate][CacheHitAfterWait] Reusing cached Superset auth tokens for %s", self.base_url)
return self._tokens
with belief_scope("AsyncAPIClient.authenticate"):
app_logger.info("[async_authenticate][Enter] Authenticating to %s", self.base_url)
try:
login_url = f"{self.api_base_url}/security/login"
response = await self._client.post(login_url, json=self.auth)
response.raise_for_status()
access_token = response.json()["access_token"]
csrf_url = f"{self.api_base_url}/security/csrf_token/"
csrf_response = await self._client.get(
csrf_url,
headers={"Authorization": f"Bearer {access_token}"},
)
csrf_response.raise_for_status()
self._tokens = {
"access_token": access_token,
"csrf_token": csrf_response.json()["result"],
}
self._authenticated = True
SupersetAuthCache.set(self._auth_cache_key, self._tokens)
app_logger.info("[async_authenticate][Exit] Authenticated successfully.")
return self._tokens
except httpx.HTTPStatusError as exc:
SupersetAuthCache.invalidate(self._auth_cache_key)
status_code = exc.response.status_code if exc.response is not None else None
if status_code in [502, 503, 504]:
raise NetworkError(
f"Environment unavailable during authentication (Status {status_code})",
status_code=status_code,
) from exc
raise AuthenticationError(f"Authentication failed: {exc}") from exc
except (httpx.HTTPError, KeyError) as exc:
SupersetAuthCache.invalidate(self._auth_cache_key)
raise NetworkError(f"Network or parsing error during authentication: {exc}") from exc
# [/DEF:authenticate:Function]
# [DEF:get_headers:Function]
# @PURPOSE: Return authenticated Superset headers for async requests.
# @POST: Headers include Authorization and CSRF tokens.
async def get_headers(self) -> Dict[str, str]:
if not self._authenticated:
await self.authenticate()
return {
"Authorization": f"Bearer {self._tokens['access_token']}",
"X-CSRFToken": self._tokens.get("csrf_token", ""),
"Referer": self.base_url,
"Content-Type": "application/json",
}
# [/DEF:get_headers:Function]
# [DEF:request:Function]
# @PURPOSE: Perform one authenticated async Superset API request.
# @POST: Returns JSON payload or raw httpx.Response when raw_response=true.
async def request(
self,
method: str,
endpoint: str,
headers: Optional[Dict[str, str]] = None,
raw_response: bool = False,
**kwargs,
) -> Union[httpx.Response, Dict[str, Any]]:
full_url = self._build_api_url(endpoint)
request_headers = await self.get_headers()
if headers:
request_headers.update(headers)
if "allow_redirects" in kwargs and "follow_redirects" not in kwargs:
kwargs["follow_redirects"] = bool(kwargs.pop("allow_redirects"))
try:
response = await self._client.request(method, full_url, headers=request_headers, **kwargs)
response.raise_for_status()
return response if raw_response else response.json()
except httpx.HTTPStatusError as exc:
if exc.response is not None and exc.response.status_code == 401:
self._authenticated = False
self._tokens = {}
SupersetAuthCache.invalidate(self._auth_cache_key)
self._handle_http_error(exc, endpoint)
except httpx.HTTPError as exc:
self._handle_network_error(exc, full_url)
# [/DEF:request:Function]
# [DEF:_handle_http_error:Function]
# @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.
# @POST: Raises domain-specific exception for caller flow control.
def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:
with belief_scope("AsyncAPIClient._handle_http_error"):
status_code = exc.response.status_code
if status_code in [502, 503, 504]:
raise NetworkError(f"Environment unavailable (Status {status_code})", status_code=status_code) from exc
if status_code == 404:
raise DashboardNotFoundError(endpoint) from exc
if status_code == 403:
raise PermissionDeniedError() from exc
if status_code == 401:
raise AuthenticationError() from exc
raise SupersetAPIError(f"API Error {status_code}: {exc.response.text}") from exc
# [/DEF:_handle_http_error:Function]
# [DEF:_handle_network_error:Function]
# @PURPOSE: Translate generic httpx errors into NetworkError.
# @POST: Raises NetworkError with URL context.
def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:
with belief_scope("AsyncAPIClient._handle_network_error"):
if isinstance(exc, httpx.TimeoutException):
message = "Request timeout"
elif isinstance(exc, httpx.ConnectError):
message = "Connection error"
else:
message = f"Unknown network error: {exc}"
raise NetworkError(message, url=url) from exc
# [/DEF:_handle_network_error:Function]
# [DEF:aclose:Function]
# @PURPOSE: Close underlying httpx client.
# @POST: Client resources are released.
async def aclose(self) -> None:
await self._client.aclose()
# [/DEF:aclose:Function]
# [/DEF:AsyncAPIClient:Class]
# [/DEF:backend.src.core.utils.async_network:Module]

View File

@@ -8,10 +8,12 @@
# @PUBLIC_API: APIClient
# [SECTION: IMPORTS]
from typing import Optional, Dict, Any, List, Union, cast
from typing import Optional, Dict, Any, List, Union, cast, Tuple
import json
import io
from pathlib import Path
import threading
import time
import requests
from requests.adapters import HTTPAdapter
import urllib3
@@ -86,6 +88,62 @@ class NetworkError(Exception):
# [/DEF:__init__:Function]
# [/DEF:NetworkError:Class]
# [DEF:SupersetAuthCache:Class]
# @PURPOSE: Process-local cache for Superset access/csrf tokens keyed by environment credentials.
# @PRE: base_url and username are stable strings.
# @POST: Cached entries expire automatically by TTL and can be reused across requests.
class SupersetAuthCache:
TTL_SECONDS = 300
_lock = threading.Lock()
_entries: Dict[Tuple[str, str, bool], Dict[str, Any]] = {}
@classmethod
def build_key(cls, base_url: str, auth: Optional[Dict[str, Any]], verify_ssl: bool) -> Tuple[str, str, bool]:
username = ""
if isinstance(auth, dict):
username = str(auth.get("username") or "").strip()
return (str(base_url or "").strip(), username, bool(verify_ssl))
@classmethod
def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:
now = time.time()
with cls._lock:
payload = cls._entries.get(key)
if not payload:
return None
expires_at = float(payload.get("expires_at") or 0)
if expires_at <= now:
cls._entries.pop(key, None)
return None
tokens = payload.get("tokens")
if not isinstance(tokens, dict):
cls._entries.pop(key, None)
return None
return {
"access_token": str(tokens.get("access_token") or ""),
"csrf_token": str(tokens.get("csrf_token") or ""),
}
@classmethod
def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:
normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)
with cls._lock:
cls._entries[key] = {
"tokens": {
"access_token": str(tokens.get("access_token") or ""),
"csrf_token": str(tokens.get("csrf_token") or ""),
},
"expires_at": time.time() + normalized_ttl,
}
@classmethod
def invalidate(cls, key: Tuple[str, str, bool]) -> None:
with cls._lock:
cls._entries.pop(key, None)
# [/DEF:SupersetAuthCache:Class]
# [DEF:APIClient:Class]
# @PURPOSE: Инкапсулирует HTTP-логику для работы с API, включая сессии, аутентификацию, и обработку запросов.
class APIClient:
@@ -107,6 +165,11 @@ class APIClient:
self.request_settings = {"verify_ssl": verify_ssl, "timeout": timeout}
self.session = self._init_session()
self._tokens: Dict[str, str] = {}
self._auth_cache_key = SupersetAuthCache.build_key(
self.base_url,
self.auth,
verify_ssl,
)
self._authenticated = False
app_logger.info("[APIClient.__init__][Exit] APIClient initialized.")
# [/DEF:__init__:Function]
@@ -194,6 +257,12 @@ class APIClient:
def authenticate(self) -> Dict[str, str]:
with belief_scope("authenticate"):
app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"):
self._tokens = cached_tokens
self._authenticated = True
app_logger.info("[authenticate][CacheHit] Reusing cached Superset auth tokens for %s", self.base_url)
return self._tokens
try:
login_url = f"{self.api_base_url}/security/login"
# Log the payload keys and values (masking password)
@@ -215,14 +284,17 @@ class APIClient:
self._tokens = {"access_token": access_token, "csrf_token": csrf_response.json()["result"]}
self._authenticated = True
SupersetAuthCache.set(self._auth_cache_key, self._tokens)
app_logger.info("[authenticate][Exit] Authenticated successfully.")
return self._tokens
except requests.exceptions.HTTPError as e:
SupersetAuthCache.invalidate(self._auth_cache_key)
status_code = e.response.status_code if e.response is not None else None
if status_code in [502, 503, 504]:
raise NetworkError(f"Environment unavailable during authentication (Status {status_code})", status_code=status_code) from e
raise AuthenticationError(f"Authentication failed: {e}") from e
except (requests.exceptions.RequestException, KeyError) as e:
SupersetAuthCache.invalidate(self._auth_cache_key)
raise NetworkError(f"Network or parsing error during authentication: {e}") from e
# [/DEF:authenticate:Function]
@@ -263,6 +335,10 @@ class APIClient:
response.raise_for_status()
return response if raw_response else response.json()
except requests.exceptions.HTTPError as e:
if e.response is not None and e.response.status_code == 401:
self._authenticated = False
self._tokens = {}
SupersetAuthCache.invalidate(self._auth_cache_key)
self._handle_http_error(e, endpoint)
except requests.exceptions.RequestException as e:
self._handle_network_error(e, full_url)

View File

@@ -14,8 +14,16 @@ from .core.config_manager import ConfigManager
from .core.scheduler import SchedulerService
from .services.resource_service import ResourceService
from .services.mapping_service import MappingService
from .services.clean_release.repositories import (
CandidateRepository, ArtifactRepository, ManifestRepository,
PolicyRepository, ComplianceRepository, ReportRepository,
ApprovalRepository, PublicationRepository, AuditRepository,
CleanReleaseAuditLog
)
from .services.clean_release.repository import CleanReleaseRepository
from .core.database import init_db, get_auth_db
from .services.clean_release.facade import CleanReleaseFacade
from .services.reports.report_service import ReportsService
from .core.database import init_db, get_auth_db, get_db
from .core.logger import logger
from .core.auth.jwt import decode_token
from .core.auth.repository import AuthRepository
@@ -55,8 +63,10 @@ logger.info("SchedulerService initialized")
resource_service = ResourceService()
logger.info("ResourceService initialized")
clean_release_repository = CleanReleaseRepository()
logger.info("CleanReleaseRepository initialized")
# Clean Release Redesign Singletons
# Note: These use get_db() which is a generator, so we need a way to provide a session.
# For singletons in dependencies.py, we might need a different approach or
# initialize them inside the dependency functions.
# [DEF:get_plugin_loader:Function]
# @PURPOSE: Dependency injector for PluginLoader.
@@ -109,15 +119,45 @@ def get_mapping_service() -> MappingService:
# [/DEF:get_mapping_service:Function]
_clean_release_repository = CleanReleaseRepository()
# [DEF:get_clean_release_repository:Function]
# @PURPOSE: Dependency injector for CleanReleaseRepository.
# @PRE: Global clean_release_repository must be initialized.
# @POST: Returns shared CleanReleaseRepository instance.
# @RETURN: CleanReleaseRepository - Shared clean release repository instance.
# @PURPOSE: Legacy compatibility shim for CleanReleaseRepository.
# @POST: Returns a shared CleanReleaseRepository instance.
def get_clean_release_repository() -> CleanReleaseRepository:
return clean_release_repository
"""Legacy compatibility shim for CleanReleaseRepository."""
return _clean_release_repository
# [/DEF:get_clean_release_repository:Function]
# [DEF:get_clean_release_facade:Function]
# @PURPOSE: Dependency injector for CleanReleaseFacade.
# @POST: Returns a facade instance with a fresh DB session.
def get_clean_release_facade(db = Depends(get_db)) -> CleanReleaseFacade:
candidate_repo = CandidateRepository(db)
artifact_repo = ArtifactRepository(db)
manifest_repo = ManifestRepository(db)
policy_repo = PolicyRepository(db)
compliance_repo = ComplianceRepository(db)
report_repo = ReportRepository(db)
approval_repo = ApprovalRepository(db)
publication_repo = PublicationRepository(db)
audit_repo = AuditRepository(db)
return CleanReleaseFacade(
candidate_repo=candidate_repo,
artifact_repo=artifact_repo,
manifest_repo=manifest_repo,
policy_repo=policy_repo,
compliance_repo=compliance_repo,
report_repo=report_repo,
approval_repo=approval_repo,
publication_repo=publication_repo,
audit_repo=audit_repo,
config_manager=config_manager
)
# [/DEF:get_clean_release_facade:Function]
# [DEF:oauth2_scheme:Variable]
# @PURPOSE: OAuth2 password bearer scheme for token extraction.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")

View File

@@ -1,228 +1,217 @@
# [DEF:backend.src.models.clean_release:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, models, lifecycle, policy, manifest, compliance
# @PURPOSE: Define clean release domain entities and validation contracts for enterprise compliance flow.
# @SEMANTICS: clean-release, models, lifecycle, compliance, evidence, immutability
# @PURPOSE: Define canonical clean release domain entities and lifecycle guards.
# @LAYER: Domain
# @RELATION: BINDS_TO -> specs/023-clean-repo-enterprise/data-model.md
# @INVARIANT: Enterprise-clean policy always forbids external sources.
#
# @TEST_CONTRACT CleanReleaseModels ->
# {
# required_fields: {
# ReleaseCandidate: [candidate_id, version, profile, source_snapshot_ref],
# CleanProfilePolicy: [policy_id, policy_version, internal_source_registry_ref]
# },
# invariants: [
# "enterprise-clean profile enforces external_source_forbidden=True",
# "manifest summary counts are consistent with items",
# "compliant run requires all mandatory stages to pass"
# ]
# }
# @TEST_FIXTURE valid_enterprise_candidate -> {"candidate_id": "RC-001", "version": "1.0.0", "profile": "enterprise-clean", "source_snapshot_ref": "v1.0.0-snapshot"}
# @TEST_FIXTURE valid_enterprise_policy -> {"policy_id": "POL-001", "policy_version": "1", "internal_source_registry_ref": "REG-1", "prohibited_artifact_categories": ["test-data"]}
# @TEST_EDGE enterprise_policy_missing_prohibited -> profile=enterprise-clean with empty prohibited_artifact_categories raises ValueError
# @TEST_EDGE enterprise_policy_external_allowed -> profile=enterprise-clean with external_source_forbidden=False raises ValueError
# @TEST_EDGE manifest_count_mismatch -> included + excluded != len(items) raises ValueError
# @TEST_EDGE compliant_run_stage_fail -> COMPLIANT run with failed stage raises ValueError
# @TEST_INVARIANT policy_purity -> verifies: [valid_enterprise_policy, enterprise_policy_external_allowed]
# @TEST_INVARIANT manifest_consistency -> verifies: [manifest_count_mismatch]
# @TEST_INVARIANT run_integrity -> verifies: [compliant_run_stage_fail]
# @TEST_CONTRACT: CleanReleaseModelPayload -> ValidatedCleanReleaseModel | ValidationError
# @TEST_SCENARIO: valid_enterprise_models -> CRITICAL entities validate and preserve lifecycle/compliance invariants.
# @TEST_FIXTURE: clean_release_models_baseline -> backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: empty_required_identifiers -> Empty candidate_id/source_snapshot_ref/internal_source_registry_ref fails validation.
# @TEST_EDGE: compliant_run_missing_mandatory_stage -> COMPLIANT run without all mandatory PASS stages fails validation.
# @TEST_EDGE: blocked_report_without_blocking_violations -> BLOCKED report with zero blocking violations fails validation.
# @TEST_INVARIANT: external_source_must_block -> VERIFIED_BY: [valid_enterprise_models, blocked_report_without_blocking_violations]
from __future__ import annotations
# @INVARIANT: Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.
from datetime import datetime
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field, model_validator
# [DEF:ReleaseCandidateStatus:Class]
# @PURPOSE: Lifecycle states for release candidate.
class ReleaseCandidateStatus(str, Enum):
DRAFT = "draft"
PREPARED = "prepared"
COMPLIANT = "compliant"
BLOCKED = "blocked"
RELEASED = "released"
# [/DEF:ReleaseCandidateStatus:Class]
# [DEF:ProfileType:Class]
# @PURPOSE: Supported profile identifiers.
class ProfileType(str, Enum):
ENTERPRISE_CLEAN = "enterprise-clean"
DEVELOPMENT = "development"
# [/DEF:ProfileType:Class]
# [DEF:ClassificationType:Class]
# @PURPOSE: Manifest classification outcomes for artifacts.
class ClassificationType(str, Enum):
REQUIRED_SYSTEM = "required-system"
ALLOWED = "allowed"
EXCLUDED_PROHIBITED = "excluded-prohibited"
# [/DEF:ClassificationType:Class]
# [DEF:RegistryStatus:Class]
# @PURPOSE: Registry lifecycle status.
class RegistryStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
# [/DEF:RegistryStatus:Class]
from typing import List, Optional, Dict, Any
from sqlalchemy import Column, String, DateTime, JSON, ForeignKey, Integer, Boolean
from sqlalchemy.orm import relationship
from .mapping import Base
from ..services.clean_release.enums import (
CandidateStatus, RunStatus, ComplianceDecision,
ApprovalDecisionType, PublicationStatus, ClassificationType
)
from ..services.clean_release.exceptions import IllegalTransitionError
# [DEF:CheckFinalStatus:Class]
# @PURPOSE: Final status for compliance check run.
# @PURPOSE: Backward-compatible final status enum for legacy TUI/orchestrator tests.
class CheckFinalStatus(str, Enum):
RUNNING = "running"
COMPLIANT = "compliant"
BLOCKED = "blocked"
FAILED = "failed"
COMPLIANT = "COMPLIANT"
BLOCKED = "BLOCKED"
FAILED = "FAILED"
# [/DEF:CheckFinalStatus:Class]
# [DEF:ExecutionMode:Class]
# @PURPOSE: Execution channel for compliance checks.
class ExecutionMode(str, Enum):
TUI = "tui"
CI = "ci"
# [/DEF:ExecutionMode:Class]
# [DEF:CheckStageName:Class]
# @PURPOSE: Mandatory check stages.
# @PURPOSE: Backward-compatible stage name enum for legacy TUI/orchestrator tests.
class CheckStageName(str, Enum):
DATA_PURITY = "data_purity"
INTERNAL_SOURCES_ONLY = "internal_sources_only"
NO_EXTERNAL_ENDPOINTS = "no_external_endpoints"
MANIFEST_CONSISTENCY = "manifest_consistency"
DATA_PURITY = "DATA_PURITY"
INTERNAL_SOURCES_ONLY = "INTERNAL_SOURCES_ONLY"
NO_EXTERNAL_ENDPOINTS = "NO_EXTERNAL_ENDPOINTS"
MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY"
# [/DEF:CheckStageName:Class]
# [DEF:CheckStageStatus:Class]
# @PURPOSE: Stage-level execution status.
# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests.
class CheckStageStatus(str, Enum):
PASS = "pass"
FAIL = "fail"
SKIPPED = "skipped"
PASS = "PASS"
FAIL = "FAIL"
SKIPPED = "SKIPPED"
RUNNING = "RUNNING"
# [/DEF:CheckStageStatus:Class]
# [DEF:CheckStageResult:Class]
# @PURPOSE: Backward-compatible stage result container for legacy TUI/orchestrator tests.
@dataclass
class CheckStageResult:
stage: CheckStageName
status: CheckStageStatus
details: str = ""
# [/DEF:CheckStageResult:Class]
# [DEF:ViolationCategory:Class]
# @PURPOSE: Normalized compliance violation categories.
class ViolationCategory(str, Enum):
DATA_PURITY = "data-purity"
EXTERNAL_SOURCE = "external-source"
MANIFEST_INTEGRITY = "manifest-integrity"
POLICY_CONFLICT = "policy-conflict"
OPERATIONAL_RISK = "operational-risk"
# [/DEF:ViolationCategory:Class]
# [DEF:ProfileType:Class]
# @PURPOSE: Backward-compatible profile enum for legacy TUI bootstrap logic.
class ProfileType(str, Enum):
ENTERPRISE_CLEAN = "enterprise-clean"
# [/DEF:ProfileType:Class]
# [DEF:RegistryStatus:Class]
# @PURPOSE: Backward-compatible registry status enum for legacy TUI bootstrap logic.
class RegistryStatus(str, Enum):
ACTIVE = "ACTIVE"
INACTIVE = "INACTIVE"
# [/DEF:RegistryStatus:Class]
# [DEF:ViolationSeverity:Class]
# @PURPOSE: Severity levels for violation triage.
class ViolationSeverity(str, Enum):
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
# [/DEF:ViolationSeverity:Class]
# [DEF:ReleaseCandidate:Class]
# @PURPOSE: Candidate metadata for clean-release workflow.
# @PRE: candidate_id, source_snapshot_ref are non-empty.
# @POST: Model instance is valid for lifecycle transitions.
class ReleaseCandidate(BaseModel):
candidate_id: str
version: str
profile: ProfileType
created_at: datetime
created_by: str
source_snapshot_ref: str
status: ReleaseCandidateStatus = ReleaseCandidateStatus.DRAFT
@model_validator(mode="after")
def _validate_non_empty(self):
if not self.candidate_id.strip():
raise ValueError("candidate_id must be non-empty")
if not self.source_snapshot_ref.strip():
raise ValueError("source_snapshot_ref must be non-empty")
return self
# [/DEF:ReleaseCandidate:Class]
# [DEF:CleanProfilePolicy:Class]
# @PURPOSE: Policy contract for artifact/source decisions.
class CleanProfilePolicy(BaseModel):
policy_id: str
policy_version: str
active: bool
prohibited_artifact_categories: List[str] = Field(default_factory=list)
required_system_categories: List[str] = Field(default_factory=list)
external_source_forbidden: bool = True
internal_source_registry_ref: str
effective_from: datetime
effective_to: Optional[datetime] = None
profile: ProfileType = ProfileType.ENTERPRISE_CLEAN
@model_validator(mode="after")
def _validate_policy(self):
if self.profile == ProfileType.ENTERPRISE_CLEAN:
if not self.external_source_forbidden:
raise ValueError("enterprise-clean policy requires external_source_forbidden=true")
if not self.prohibited_artifact_categories:
raise ValueError("enterprise-clean policy requires prohibited_artifact_categories")
if not self.internal_source_registry_ref.strip():
raise ValueError("internal_source_registry_ref must be non-empty")
return self
# [/DEF:CleanProfilePolicy:Class]
# [DEF:ReleaseCandidateStatus:Class]
# @PURPOSE: Backward-compatible release candidate status enum for legacy TUI.
class ReleaseCandidateStatus(str, Enum):
DRAFT = CandidateStatus.DRAFT.value
PREPARED = CandidateStatus.PREPARED.value
MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value
CHECK_PENDING = CandidateStatus.CHECK_PENDING.value
CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value
CHECK_PASSED = CandidateStatus.CHECK_PASSED.value
CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value
CHECK_ERROR = CandidateStatus.CHECK_ERROR.value
APPROVED = CandidateStatus.APPROVED.value
PUBLISHED = CandidateStatus.PUBLISHED.value
REVOKED = CandidateStatus.REVOKED.value
# [/DEF:ReleaseCandidateStatus:Class]
# [DEF:ResourceSourceEntry:Class]
# @PURPOSE: One internal source definition.
class ResourceSourceEntry(BaseModel):
# @PURPOSE: Backward-compatible source entry model for legacy TUI bootstrap logic.
@dataclass
class ResourceSourceEntry:
source_id: str
host: str
protocol: str
purpose: str
allowed_paths: List[str] = Field(default_factory=list)
enabled: bool = True
# [/DEF:ResourceSourceEntry:Class]
# [DEF:ResourceSourceRegistry:Class]
# @PURPOSE: Allowlist of internal sources.
class ResourceSourceRegistry(BaseModel):
# @PURPOSE: Backward-compatible source registry model for legacy TUI bootstrap logic.
@dataclass
class ResourceSourceRegistry:
registry_id: str
name: str
entries: List[ResourceSourceEntry]
updated_at: datetime
updated_by: str
status: RegistryStatus = RegistryStatus.ACTIVE
status: str = "ACTIVE"
@model_validator(mode="after")
def _validate_registry(self):
if not self.entries:
raise ValueError("registry entries cannot be empty")
if self.status == RegistryStatus.ACTIVE and not any(e.enabled for e in self.entries):
raise ValueError("active registry must include at least one enabled entry")
return self
@property
def id(self) -> str:
return self.registry_id
# [/DEF:ResourceSourceRegistry:Class]
# [DEF:CleanProfilePolicy:Class]
# @PURPOSE: Backward-compatible policy model for legacy TUI bootstrap logic.
@dataclass
class CleanProfilePolicy:
policy_id: str
policy_version: str
profile: str
active: bool
internal_source_registry_ref: str
prohibited_artifact_categories: List[str]
effective_from: datetime
required_system_categories: Optional[List[str]] = None
@property
def id(self) -> str:
return self.policy_id
@property
def registry_snapshot_id(self) -> str:
return self.internal_source_registry_ref
# [/DEF:CleanProfilePolicy:Class]
# [DEF:ComplianceCheckRun:Class]
# @PURPOSE: Backward-compatible run model for legacy TUI typing/import compatibility.
@dataclass
class ComplianceCheckRun:
check_run_id: str
candidate_id: str
policy_id: str
requested_by: str
execution_mode: str
checks: List[CheckStageResult]
final_status: CheckFinalStatus
# [/DEF:ComplianceCheckRun:Class]
# [DEF:ReleaseCandidate:Class]
# @PURPOSE: Represents the release unit being prepared and governed.
# @PRE: id, version, source_snapshot_ref are non-empty.
# @POST: status advances only through legal transitions.
class ReleaseCandidate(Base):
__tablename__ = "clean_release_candidates"
id = Column(String, primary_key=True)
name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs
version = Column(String, nullable=False)
source_snapshot_ref = Column(String, nullable=False)
build_id = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
created_by = Column(String, nullable=False)
status = Column(String, default=CandidateStatus.DRAFT)
@property
def candidate_id(self) -> str:
return self.id
def transition_to(self, new_status: CandidateStatus):
"""
@PURPOSE: Enforce legal state transitions.
@PRE: Transition must be allowed by lifecycle rules.
"""
allowed = {
CandidateStatus.DRAFT: [CandidateStatus.PREPARED],
CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],
CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],
CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],
CandidateStatus.CHECK_RUNNING: [
CandidateStatus.CHECK_PASSED,
CandidateStatus.CHECK_BLOCKED,
CandidateStatus.CHECK_ERROR
],
CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],
CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],
CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],
CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],
CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],
CandidateStatus.REVOKED: []
}
current_status = CandidateStatus(self.status)
if new_status not in allowed.get(current_status, []):
raise IllegalTransitionError(f"Forbidden transition from {current_status} to {new_status}")
self.status = new_status.value
# [/DEF:ReleaseCandidate:Class]
# [DEF:CandidateArtifact:Class]
# @PURPOSE: Represents one artifact associated with a release candidate.
class CandidateArtifact(Base):
__tablename__ = "clean_release_artifacts"
id = Column(String, primary_key=True)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
path = Column(String, nullable=False)
sha256 = Column(String, nullable=False)
size = Column(Integer, nullable=False)
detected_category = Column(String, nullable=True)
declared_category = Column(String, nullable=True)
source_uri = Column(String, nullable=True)
source_host = Column(String, nullable=True)
metadata_json = Column(JSON, default=dict)
# [/DEF:CandidateArtifact:Class]
# [DEF:ManifestItem:Class]
# @PURPOSE: One artifact entry in manifest.
class ManifestItem(BaseModel):
@dataclass
class ManifestItem:
path: str
category: str
classification: ClassificationType
@@ -230,119 +219,218 @@ class ManifestItem(BaseModel):
checksum: Optional[str] = None
# [/DEF:ManifestItem:Class]
# [DEF:ManifestSummary:Class]
# @PURPOSE: Aggregate counters for manifest decisions.
class ManifestSummary(BaseModel):
included_count: int = Field(ge=0)
excluded_count: int = Field(ge=0)
prohibited_detected_count: int = Field(ge=0)
@dataclass
class ManifestSummary:
included_count: int
excluded_count: int
prohibited_detected_count: int
# [/DEF:ManifestSummary:Class]
# [DEF:DistributionManifest:Class]
# @PURPOSE: Deterministic release composition for audit.
class DistributionManifest(BaseModel):
manifest_id: str
candidate_id: str
policy_id: str
generated_at: datetime
generated_by: str
items: List[ManifestItem]
summary: ManifestSummary
deterministic_hash: str
# @PURPOSE: Immutable snapshot of the candidate payload.
# @INVARIANT: Immutable after creation.
class DistributionManifest(Base):
__tablename__ = "clean_release_manifests"
id = Column(String, primary_key=True)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
manifest_version = Column(Integer, nullable=False)
manifest_digest = Column(String, nullable=False)
artifacts_digest = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
created_by = Column(String, nullable=False)
source_snapshot_ref = Column(String, nullable=False)
content_json = Column(JSON, nullable=False)
immutable = Column(Boolean, default=True)
@model_validator(mode="after")
def _validate_counts(self):
if self.summary.included_count + self.summary.excluded_count != len(self.items):
raise ValueError("manifest summary counts must match items size")
return self
# Redesign compatibility fields (not persisted directly but used by builder/facade)
def __init__(self, **kwargs):
# Handle fields from manifest_builder.py
if "manifest_id" in kwargs:
kwargs["id"] = kwargs.pop("manifest_id")
if "generated_at" in kwargs:
kwargs["created_at"] = kwargs.pop("generated_at")
if "generated_by" in kwargs:
kwargs["created_by"] = kwargs.pop("generated_by")
if "deterministic_hash" in kwargs:
kwargs["manifest_digest"] = kwargs.pop("deterministic_hash")
# Ensure required DB fields have defaults if missing
if "manifest_version" not in kwargs:
kwargs["manifest_version"] = 1
if "artifacts_digest" not in kwargs:
kwargs["artifacts_digest"] = kwargs.get("manifest_digest", "pending")
if "source_snapshot_ref" not in kwargs:
kwargs["source_snapshot_ref"] = "pending"
# Pack items and summary into content_json if provided
if "items" in kwargs or "summary" in kwargs:
content = kwargs.get("content_json", {})
if "items" in kwargs:
items = kwargs.pop("items")
content["items"] = [
{
"path": i.path,
"category": i.category,
"classification": i.classification.value,
"reason": i.reason,
"checksum": i.checksum
} for i in items
]
if "summary" in kwargs:
summary = kwargs.pop("summary")
content["summary"] = {
"included_count": summary.included_count,
"excluded_count": summary.excluded_count,
"prohibited_detected_count": summary.prohibited_detected_count
}
kwargs["content_json"] = content
super().__init__(**kwargs)
# [/DEF:DistributionManifest:Class]
# [DEF:SourceRegistrySnapshot:Class]
# @PURPOSE: Immutable registry snapshot for allowed sources.
class SourceRegistrySnapshot(Base):
__tablename__ = "clean_release_registry_snapshots"
id = Column(String, primary_key=True)
registry_id = Column(String, nullable=False)
registry_version = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
allowed_hosts = Column(JSON, nullable=False) # List[str]
allowed_schemes = Column(JSON, nullable=False) # List[str]
allowed_source_types = Column(JSON, nullable=False) # List[str]
immutable = Column(Boolean, default=True)
# [/DEF:SourceRegistrySnapshot:Class]
# [DEF:CheckStageResult:Class]
# @PURPOSE: Per-stage compliance result.
class CheckStageResult(BaseModel):
stage: CheckStageName
status: CheckStageStatus
details: Optional[str] = None
duration_ms: Optional[int] = Field(default=None, ge=0)
# [/DEF:CheckStageResult:Class]
# [DEF:CleanPolicySnapshot:Class]
# @PURPOSE: Immutable policy snapshot used to evaluate a run.
class CleanPolicySnapshot(Base):
__tablename__ = "clean_release_policy_snapshots"
id = Column(String, primary_key=True)
policy_id = Column(String, nullable=False)
policy_version = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
content_json = Column(JSON, nullable=False)
registry_snapshot_id = Column(String, ForeignKey("clean_release_registry_snapshots.id"), nullable=False)
immutable = Column(Boolean, default=True)
# [/DEF:CleanPolicySnapshot:Class]
# [DEF:ComplianceRun:Class]
# @PURPOSE: Operational record for one compliance execution.
class ComplianceRun(Base):
__tablename__ = "clean_release_compliance_runs"
id = Column(String, primary_key=True)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
manifest_id = Column(String, ForeignKey("clean_release_manifests.id"), nullable=False)
manifest_digest = Column(String, nullable=False)
policy_snapshot_id = Column(String, ForeignKey("clean_release_policy_snapshots.id"), nullable=False)
registry_snapshot_id = Column(String, ForeignKey("clean_release_registry_snapshots.id"), nullable=False)
requested_by = Column(String, nullable=False)
requested_at = Column(DateTime, default=datetime.utcnow)
started_at = Column(DateTime, nullable=True)
finished_at = Column(DateTime, nullable=True)
status = Column(String, default=RunStatus.PENDING)
final_status = Column(String, nullable=True) # ComplianceDecision
failure_reason = Column(String, nullable=True)
task_id = Column(String, nullable=True)
# [DEF:ComplianceCheckRun:Class]
# @PURPOSE: One execution run of compliance pipeline.
class ComplianceCheckRun(BaseModel):
check_run_id: str
candidate_id: str
policy_id: str
started_at: datetime
finished_at: Optional[datetime] = None
final_status: CheckFinalStatus = CheckFinalStatus.RUNNING
triggered_by: str
execution_mode: ExecutionMode
checks: List[CheckStageResult] = Field(default_factory=list)
@model_validator(mode="after")
def _validate_terminal_integrity(self):
if self.final_status == CheckFinalStatus.COMPLIANT:
mandatory = {c.stage: c.status for c in self.checks}
required = {
CheckStageName.DATA_PURITY,
CheckStageName.INTERNAL_SOURCES_ONLY,
CheckStageName.NO_EXTERNAL_ENDPOINTS,
CheckStageName.MANIFEST_CONSISTENCY,
}
if not required.issubset(mandatory.keys()):
raise ValueError("compliant run requires all mandatory stages")
if any(mandatory[s] != CheckStageStatus.PASS for s in required):
raise ValueError("compliant run requires PASS on all mandatory stages")
return self
# [/DEF:ComplianceCheckRun:Class]
@property
def check_run_id(self) -> str:
return self.id
# [/DEF:ComplianceRun:Class]
# [DEF:ComplianceStageRun:Class]
# @PURPOSE: Stage-level execution record inside a run.
class ComplianceStageRun(Base):
__tablename__ = "clean_release_compliance_stage_runs"
id = Column(String, primary_key=True)
run_id = Column(String, ForeignKey("clean_release_compliance_runs.id"), nullable=False)
stage_name = Column(String, nullable=False)
status = Column(String, nullable=False)
started_at = Column(DateTime, nullable=True)
finished_at = Column(DateTime, nullable=True)
decision = Column(String, nullable=True) # ComplianceDecision
details_json = Column(JSON, default=dict)
# [/DEF:ComplianceStageRun:Class]
# [DEF:ComplianceViolation:Class]
# @PURPOSE: Normalized violation row for triage and blocking decisions.
class ComplianceViolation(BaseModel):
violation_id: str
check_run_id: str
category: ViolationCategory
severity: ViolationSeverity
location: str
evidence: Optional[str] = None
remediation: str
blocked_release: bool
detected_at: datetime
@model_validator(mode="after")
def _validate_violation(self):
if self.category == ViolationCategory.EXTERNAL_SOURCE and not self.blocked_release:
raise ValueError("external-source violation must block release")
if self.severity == ViolationSeverity.CRITICAL and not self.remediation.strip():
raise ValueError("critical violation requires remediation")
return self
# @PURPOSE: Violation produced by a stage.
class ComplianceViolation(Base):
__tablename__ = "clean_release_compliance_violations"
id = Column(String, primary_key=True)
run_id = Column(String, ForeignKey("clean_release_compliance_runs.id"), nullable=False)
stage_name = Column(String, nullable=False)
code = Column(String, nullable=False)
severity = Column(String, nullable=False)
artifact_path = Column(String, nullable=True)
artifact_sha256 = Column(String, nullable=True)
message = Column(String, nullable=False)
evidence_json = Column(JSON, default=dict)
# [/DEF:ComplianceViolation:Class]
# [DEF:ComplianceReport:Class]
# @PURPOSE: Final report payload for operator and audit systems.
class ComplianceReport(BaseModel):
report_id: str
check_run_id: str
candidate_id: str
generated_at: datetime
final_status: CheckFinalStatus
operator_summary: str
structured_payload_ref: str
violations_count: int = Field(ge=0)
blocking_violations_count: int = Field(ge=0)
@model_validator(mode="after")
def _validate_report_counts(self):
if self.blocking_violations_count > self.violations_count:
raise ValueError("blocking_violations_count cannot exceed violations_count")
if self.final_status == CheckFinalStatus.BLOCKED and self.blocking_violations_count <= 0:
raise ValueError("blocked report requires blocking violations")
return self
# @PURPOSE: Immutable result derived from a completed run.
# @INVARIANT: Immutable after creation.
class ComplianceReport(Base):
__tablename__ = "clean_release_compliance_reports"
id = Column(String, primary_key=True)
run_id = Column(String, ForeignKey("clean_release_compliance_runs.id"), nullable=False)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
final_status = Column(String, nullable=False) # ComplianceDecision
summary_json = Column(JSON, nullable=False)
generated_at = Column(DateTime, default=datetime.utcnow)
immutable = Column(Boolean, default=True)
# [/DEF:ComplianceReport:Class]
# [DEF:ApprovalDecision:Class]
# @PURPOSE: Approval or rejection bound to a candidate and report.
class ApprovalDecision(Base):
__tablename__ = "clean_release_approval_decisions"
id = Column(String, primary_key=True)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
report_id = Column(String, ForeignKey("clean_release_compliance_reports.id"), nullable=False)
decision = Column(String, nullable=False) # ApprovalDecisionType
decided_by = Column(String, nullable=False)
decided_at = Column(DateTime, default=datetime.utcnow)
comment = Column(String, nullable=True)
# [/DEF:ApprovalDecision:Class]
# [DEF:PublicationRecord:Class]
# @PURPOSE: Publication or revocation record.
class PublicationRecord(Base):
__tablename__ = "clean_release_publication_records"
id = Column(String, primary_key=True)
candidate_id = Column(String, ForeignKey("clean_release_candidates.id"), nullable=False)
report_id = Column(String, ForeignKey("clean_release_compliance_reports.id"), nullable=False)
published_by = Column(String, nullable=False)
published_at = Column(DateTime, default=datetime.utcnow)
target_channel = Column(String, nullable=False)
publication_ref = Column(String, nullable=True)
status = Column(String, default=PublicationStatus.ACTIVE)
# [/DEF:PublicationRecord:Class]
# [DEF:CleanReleaseAuditLog:Class]
# @PURPOSE: Represents a persistent audit log entry for clean release actions.
import uuid
class CleanReleaseAuditLog(Base):
__tablename__ = "clean_release_audit_logs"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
candidate_id = Column(String, index=True, nullable=True)
action = Column(String, nullable=False) # e.g. "TRANSITION", "APPROVE", "PUBLISH"
actor = Column(String, nullable=False)
timestamp = Column(DateTime, default=datetime.utcnow)
details_json = Column(JSON, default=dict)
# [/DEF:CleanReleaseAuditLog:Class]
# [/DEF:backend.src.models.clean_release:Module]

View File

@@ -25,6 +25,7 @@ class TaskType(str, Enum):
BACKUP = "backup"
MIGRATION = "migration"
DOCUMENTATION = "documentation"
CLEAN_RELEASE = "clean_release"
UNKNOWN = "unknown"
# [/DEF:TaskType:Class]

View File

@@ -0,0 +1,444 @@
# [DEF:backend.src.scripts.clean_release_cli:Module]
# @TIER: STANDARD
# @SEMANTICS: cli, clean-release, candidate, artifacts, manifest
# @PURPOSE: Provide headless CLI commands for candidate registration, artifact import and manifest build.
# @LAYER: Scripts
from __future__ import annotations
import argparse
import json
from datetime import date, datetime, timezone
from typing import Any, Dict, List, Optional
from ..models.clean_release import CandidateArtifact, ReleaseCandidate
from ..services.clean_release.approval_service import approve_candidate, reject_candidate
from ..services.clean_release.compliance_execution_service import ComplianceExecutionService
from ..services.clean_release.enums import CandidateStatus
from ..services.clean_release.publication_service import publish_candidate, revoke_publication
# [DEF:build_parser:Function]
# @PURPOSE: Build argparse parser for clean release CLI.
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="clean-release-cli")
subparsers = parser.add_subparsers(dest="command", required=True)
register = subparsers.add_parser("candidate-register")
register.add_argument("--candidate-id", required=True)
register.add_argument("--version", required=True)
register.add_argument("--source-snapshot-ref", required=True)
register.add_argument("--created-by", default="cli-operator")
artifact_import = subparsers.add_parser("artifact-import")
artifact_import.add_argument("--candidate-id", required=True)
artifact_import.add_argument("--artifact-id", required=True)
artifact_import.add_argument("--path", required=True)
artifact_import.add_argument("--sha256", required=True)
artifact_import.add_argument("--size", type=int, required=True)
manifest_build = subparsers.add_parser("manifest-build")
manifest_build.add_argument("--candidate-id", required=True)
manifest_build.add_argument("--created-by", default="cli-operator")
compliance_run = subparsers.add_parser("compliance-run")
compliance_run.add_argument("--candidate-id", required=True)
compliance_run.add_argument("--manifest-id", required=False, default=None)
compliance_run.add_argument("--actor", default="cli-operator")
compliance_run.add_argument("--json", action="store_true")
compliance_status = subparsers.add_parser("compliance-status")
compliance_status.add_argument("--run-id", required=True)
compliance_status.add_argument("--json", action="store_true")
compliance_report = subparsers.add_parser("compliance-report")
compliance_report.add_argument("--run-id", required=True)
compliance_report.add_argument("--json", action="store_true")
compliance_violations = subparsers.add_parser("compliance-violations")
compliance_violations.add_argument("--run-id", required=True)
compliance_violations.add_argument("--json", action="store_true")
approve = subparsers.add_parser("approve")
approve.add_argument("--candidate-id", required=True)
approve.add_argument("--report-id", required=True)
approve.add_argument("--actor", default="cli-operator")
approve.add_argument("--comment", required=False, default=None)
approve.add_argument("--json", action="store_true")
reject = subparsers.add_parser("reject")
reject.add_argument("--candidate-id", required=True)
reject.add_argument("--report-id", required=True)
reject.add_argument("--actor", default="cli-operator")
reject.add_argument("--comment", required=False, default=None)
reject.add_argument("--json", action="store_true")
publish = subparsers.add_parser("publish")
publish.add_argument("--candidate-id", required=True)
publish.add_argument("--report-id", required=True)
publish.add_argument("--actor", default="cli-operator")
publish.add_argument("--target-channel", required=True)
publish.add_argument("--publication-ref", required=False, default=None)
publish.add_argument("--json", action="store_true")
revoke = subparsers.add_parser("revoke")
revoke.add_argument("--publication-id", required=True)
revoke.add_argument("--actor", default="cli-operator")
revoke.add_argument("--comment", required=False, default=None)
revoke.add_argument("--json", action="store_true")
return parser
# [/DEF:build_parser:Function]
# [DEF:run_candidate_register:Function]
# @PURPOSE: Register candidate in repository via CLI command.
# @PRE: Candidate ID must be unique.
# @POST: Candidate is persisted in DRAFT status.
def run_candidate_register(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
existing = repository.get_candidate(args.candidate_id)
if existing is not None:
print(json.dumps({"status": "error", "message": "candidate already exists"}))
return 1
candidate = ReleaseCandidate(
id=args.candidate_id,
version=args.version,
source_snapshot_ref=args.source_snapshot_ref,
created_by=args.created_by,
created_at=datetime.now(timezone.utc),
status=CandidateStatus.DRAFT.value,
)
repository.save_candidate(candidate)
print(json.dumps({"status": "ok", "candidate_id": candidate.id}))
return 0
# [/DEF:run_candidate_register:Function]
# [DEF:run_artifact_import:Function]
# @PURPOSE: Import single artifact for existing candidate.
# @PRE: Candidate must exist.
# @POST: Artifact is persisted for candidate.
def run_artifact_import(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
candidate = repository.get_candidate(args.candidate_id)
if candidate is None:
print(json.dumps({"status": "error", "message": "candidate not found"}))
return 1
artifact = CandidateArtifact(
id=args.artifact_id,
candidate_id=args.candidate_id,
path=args.path,
sha256=args.sha256,
size=args.size,
)
repository.save_artifact(artifact)
if candidate.status == CandidateStatus.DRAFT.value:
candidate.transition_to(CandidateStatus.PREPARED)
repository.save_candidate(candidate)
print(json.dumps({"status": "ok", "artifact_id": artifact.id}))
return 0
# [/DEF:run_artifact_import:Function]
# [DEF:run_manifest_build:Function]
# @PURPOSE: Build immutable manifest snapshot for candidate.
# @PRE: Candidate must exist.
# @POST: New manifest version is persisted.
def run_manifest_build(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
from ..services.clean_release.manifest_service import build_manifest_snapshot
repository = get_clean_release_repository()
try:
manifest = build_manifest_snapshot(
repository=repository,
candidate_id=args.candidate_id,
created_by=args.created_by,
)
except ValueError as exc:
print(json.dumps({"status": "error", "message": str(exc)}))
return 1
print(json.dumps({"status": "ok", "manifest_id": manifest.id, "version": manifest.manifest_version}))
return 0
# [/DEF:run_manifest_build:Function]
# [DEF:run_compliance_run:Function]
# @PURPOSE: Execute compliance run for candidate with optional manifest fallback.
# @PRE: Candidate exists and trusted snapshots are configured.
# @POST: Returns run payload and exit code 0 on success.
def run_compliance_run(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository, get_config_manager
repository = get_clean_release_repository()
config_manager = get_config_manager()
service = ComplianceExecutionService(repository=repository, config_manager=config_manager)
try:
result = service.execute_run(
candidate_id=args.candidate_id,
requested_by=args.actor,
manifest_id=args.manifest_id,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"status": "error", "message": str(exc)}))
return 2
payload = {
"status": "ok",
"run_id": result.run.id,
"candidate_id": result.run.candidate_id,
"run_status": result.run.status,
"final_status": result.run.final_status,
"task_id": getattr(result.run, "task_id", None),
"report_id": getattr(result.run, "report_id", None),
}
print(json.dumps(payload))
return 0
# [/DEF:run_compliance_run:Function]
# [DEF:run_compliance_status:Function]
# @PURPOSE: Read run status by run id.
# @PRE: Run exists.
# @POST: Returns run status payload.
def run_compliance_status(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
run = repository.get_check_run(args.run_id)
if run is None:
print(json.dumps({"status": "error", "message": "run not found"}))
return 2
report = next((item for item in repository.reports.values() if item.run_id == run.id), None)
payload = {
"status": "ok",
"run_id": run.id,
"candidate_id": run.candidate_id,
"run_status": run.status,
"final_status": run.final_status,
"task_id": getattr(run, "task_id", None),
"report_id": getattr(run, "report_id", None) or (report.id if report else None),
}
print(json.dumps(payload))
return 0
# [/DEF:run_compliance_status:Function]
# [DEF:_to_payload:Function]
# @PURPOSE: Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.
# @PRE: value is serializable model or primitive object.
# @POST: Returns dictionary payload without mutating value.
def _to_payload(value: Any) -> Dict[str, Any]:
def _normalize(raw: Any) -> Any:
if isinstance(raw, datetime):
return raw.isoformat()
if isinstance(raw, date):
return raw.isoformat()
if isinstance(raw, dict):
return {str(key): _normalize(item) for key, item in raw.items()}
if isinstance(raw, list):
return [_normalize(item) for item in raw]
if isinstance(raw, tuple):
return [_normalize(item) for item in raw]
return raw
if hasattr(value, "model_dump"):
return _normalize(value.model_dump())
table = getattr(value, "__table__", None)
if table is not None:
row = {column.name: getattr(value, column.name) for column in table.columns}
return _normalize(row)
raise TypeError(f"unsupported payload type: {type(value)!r}")
# [/DEF:_to_payload:Function]
# [DEF:run_compliance_report:Function]
# @PURPOSE: Read immutable report by run id.
# @PRE: Run and report exist.
# @POST: Returns report payload.
def run_compliance_report(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
run = repository.get_check_run(args.run_id)
if run is None:
print(json.dumps({"status": "error", "message": "run not found"}))
return 2
report = next((item for item in repository.reports.values() if item.run_id == run.id), None)
if report is None:
print(json.dumps({"status": "error", "message": "report not found"}))
return 2
print(json.dumps({"status": "ok", "report": _to_payload(report)}))
return 0
# [/DEF:run_compliance_report:Function]
# [DEF:run_compliance_violations:Function]
# @PURPOSE: Read run violations by run id.
# @PRE: Run exists.
# @POST: Returns violations payload.
def run_compliance_violations(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
run = repository.get_check_run(args.run_id)
if run is None:
print(json.dumps({"status": "error", "message": "run not found"}))
return 2
violations = repository.get_violations_by_run(args.run_id)
print(json.dumps({"status": "ok", "items": [_to_payload(item) for item in violations]}))
return 0
# [/DEF:run_compliance_violations:Function]
# [DEF:run_approve:Function]
# @PURPOSE: Approve candidate based on immutable PASSED report.
# @PRE: Candidate and report exist; report is PASSED.
# @POST: Persists APPROVED decision and returns success payload.
def run_approve(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
try:
decision = approve_candidate(
repository=repository,
candidate_id=args.candidate_id,
report_id=args.report_id,
decided_by=args.actor,
comment=args.comment,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"status": "error", "message": str(exc)}))
return 2
print(json.dumps({"status": "ok", "decision": decision.decision, "decision_id": decision.id}))
return 0
# [/DEF:run_approve:Function]
# [DEF:run_reject:Function]
# @PURPOSE: Reject candidate without mutating compliance evidence.
# @PRE: Candidate and report exist.
# @POST: Persists REJECTED decision and returns success payload.
def run_reject(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
try:
decision = reject_candidate(
repository=repository,
candidate_id=args.candidate_id,
report_id=args.report_id,
decided_by=args.actor,
comment=args.comment,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"status": "error", "message": str(exc)}))
return 2
print(json.dumps({"status": "ok", "decision": decision.decision, "decision_id": decision.id}))
return 0
# [/DEF:run_reject:Function]
# [DEF:run_publish:Function]
# @PURPOSE: Publish approved candidate to target channel.
# @PRE: Candidate is approved and report belongs to candidate.
# @POST: Appends ACTIVE publication record and returns payload.
def run_publish(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
try:
publication = publish_candidate(
repository=repository,
candidate_id=args.candidate_id,
report_id=args.report_id,
published_by=args.actor,
target_channel=args.target_channel,
publication_ref=args.publication_ref,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"status": "error", "message": str(exc)}))
return 2
print(json.dumps({"status": "ok", "publication": _to_payload(publication)}))
return 0
# [/DEF:run_publish:Function]
# [DEF:run_revoke:Function]
# @PURPOSE: Revoke active publication record.
# @PRE: Publication id exists and is ACTIVE.
# @POST: Publication record status becomes REVOKED.
def run_revoke(args: argparse.Namespace) -> int:
from ..dependencies import get_clean_release_repository
repository = get_clean_release_repository()
try:
publication = revoke_publication(
repository=repository,
publication_id=args.publication_id,
revoked_by=args.actor,
comment=args.comment,
)
except Exception as exc: # noqa: BLE001
print(json.dumps({"status": "error", "message": str(exc)}))
return 2
print(json.dumps({"status": "ok", "publication": _to_payload(publication)}))
return 0
# [/DEF:run_revoke:Function]
# [DEF:main:Function]
# @PURPOSE: CLI entrypoint for clean release commands.
def main(argv: Optional[List[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.command == "candidate-register":
return run_candidate_register(args)
if args.command == "artifact-import":
return run_artifact_import(args)
if args.command == "manifest-build":
return run_manifest_build(args)
if args.command == "compliance-run":
return run_compliance_run(args)
if args.command == "compliance-status":
return run_compliance_status(args)
if args.command == "compliance-report":
return run_compliance_report(args)
if args.command == "compliance-violations":
return run_compliance_violations(args)
if args.command == "approve":
return run_approve(args)
if args.command == "reject":
return run_reject(args)
if args.command == "publish":
return run_publish(args)
if args.command == "revoke":
return run_revoke(args)
print(json.dumps({"status": "error", "message": "unknown command"}))
return 2
# [/DEF:main:Function]
if __name__ == "__main__":
raise SystemExit(main())
# [/DEF:backend.src.scripts.clean_release_cli:Module]

View File

@@ -5,14 +5,14 @@
# @LAYER: UI
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.compliance_orchestrator
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @INVARIANT: TUI must provide a headless fallback for non-TTY environments.
# @INVARIANT: TUI refuses startup in non-TTY environments; headless flow is CLI/API only.
import curses
import json
import os
import sys
import time
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import List, Optional, Any, Dict
# Standardize sys.path for direct execution from project root or scripts dir
@@ -22,12 +22,11 @@ if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from backend.src.models.clean_release import (
CandidateArtifact,
CheckFinalStatus,
CheckStageName,
CheckStageResult,
CheckStageStatus,
CleanProfilePolicy,
ComplianceCheckRun,
ComplianceViolation,
ProfileType,
ReleaseCandidate,
@@ -36,10 +35,111 @@ from backend.src.models.clean_release import (
RegistryStatus,
ReleaseCandidateStatus,
)
from backend.src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
from backend.src.services.clean_release.preparation_service import prepare_candidate
from backend.src.services.clean_release.approval_service import approve_candidate
from backend.src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from backend.src.services.clean_release.enums import CandidateStatus
from backend.src.services.clean_release.manifest_service import build_manifest_snapshot
from backend.src.services.clean_release.publication_service import publish_candidate
from backend.src.services.clean_release.repository import CleanReleaseRepository
from backend.src.services.clean_release.manifest_builder import build_distribution_manifest
# [DEF:TuiFacadeAdapter:Class]
# @PURPOSE: Thin TUI adapter that routes business mutations through application services.
# @PRE: repository contains candidate and trusted policy/registry snapshots for execution.
# @POST: Business actions return service results/errors without direct TUI-owned mutations.
class TuiFacadeAdapter:
def __init__(self, repository: CleanReleaseRepository):
self.repository = repository
def _build_config_manager(self):
policy = self.repository.get_active_policy()
if policy is None:
raise ValueError("Active policy not found")
clean_release = SimpleNamespace(
active_policy_id=policy.id,
active_registry_id=policy.registry_snapshot_id,
)
settings = SimpleNamespace(clean_release=clean_release)
config = SimpleNamespace(settings=settings)
return SimpleNamespace(get_config=lambda: config)
def run_compliance(self, *, candidate_id: str, actor: str):
manifests = self.repository.get_manifests_by_candidate(candidate_id)
if not manifests:
raise ValueError("Manifest required before compliance run")
latest_manifest = sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]
service = ComplianceExecutionService(
repository=self.repository,
config_manager=self._build_config_manager(),
)
return service.execute_run(candidate_id=candidate_id, requested_by=actor, manifest_id=latest_manifest.id)
def approve_latest(self, *, candidate_id: str, actor: str):
reports = [item for item in self.repository.reports.values() if item.candidate_id == candidate_id]
if not reports:
raise ValueError("No compliance report available for approval")
report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]
return approve_candidate(
repository=self.repository,
candidate_id=candidate_id,
report_id=report.id,
decided_by=actor,
comment="Approved from TUI",
)
def publish_latest(self, *, candidate_id: str, actor: str):
reports = [item for item in self.repository.reports.values() if item.candidate_id == candidate_id]
if not reports:
raise ValueError("No compliance report available for publication")
report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]
return publish_candidate(
repository=self.repository,
candidate_id=candidate_id,
report_id=report.id,
published_by=actor,
target_channel="stable",
publication_ref=None,
)
def build_manifest(self, *, candidate_id: str, actor: str):
return build_manifest_snapshot(
repository=self.repository,
candidate_id=candidate_id,
created_by=actor,
)
def get_overview(self, *, candidate_id: str) -> Dict[str, Any]:
candidate = self.repository.get_candidate(candidate_id)
manifests = self.repository.get_manifests_by_candidate(candidate_id)
latest_manifest = sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0] if manifests else None
runs = [item for item in self.repository.check_runs.values() if item.candidate_id == candidate_id]
latest_run = sorted(runs, key=lambda item: item.requested_at, reverse=True)[0] if runs else None
latest_report = next((item for item in self.repository.reports.values() if latest_run and item.run_id == latest_run.id), None)
approvals = getattr(self.repository, "approval_decisions", [])
latest_approval = sorted(
[item for item in approvals if item.candidate_id == candidate_id],
key=lambda item: item.decided_at,
reverse=True,
)[0] if any(item.candidate_id == candidate_id for item in approvals) else None
publications = getattr(self.repository, "publication_records", [])
latest_publication = sorted(
[item for item in publications if item.candidate_id == candidate_id],
key=lambda item: item.published_at,
reverse=True,
)[0] if any(item.candidate_id == candidate_id for item in publications) else None
policy = self.repository.get_active_policy()
registry = self.repository.get_registry(policy.internal_source_registry_ref) if policy else None
return {
"candidate": candidate,
"manifest": latest_manifest,
"run": latest_run,
"report": latest_report,
"approval": latest_approval,
"publication": latest_publication,
"policy": policy,
"registry": registry,
}
# [/DEF:TuiFacadeAdapter:Class]
# [DEF:CleanReleaseTUI:Class]
# @PURPOSE: Curses-based application for compliance monitoring.
@@ -53,14 +153,16 @@ class CleanReleaseTUI:
self.stdscr = stdscr
self.mode = os.getenv("CLEAN_TUI_MODE", "demo").strip().lower()
self.repo = self._build_repository(self.mode)
self.orchestrator = CleanComplianceOrchestrator(self.repo)
self.facade = TuiFacadeAdapter(self.repo)
self.candidate_id = self._resolve_candidate_id()
self.status: Any = "READY"
self.checks_progress: List[Dict[str, Any]] = []
self.violations_list: List[ComplianceViolation] = []
self.report_id: Optional[str] = None
self.last_error: Optional[str] = None
self.overview: Dict[str, Any] = {}
self.refresh_overview()
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header/Footer
@@ -73,48 +175,82 @@ class CleanReleaseTUI:
repo = CleanReleaseRepository()
if mode == "demo":
self._bootstrap_demo_repository(repo)
self._bootstrap_real_repository(repo)
else:
self._bootstrap_real_repository(repo)
return repo
def _bootstrap_demo_repository(self, repository: CleanReleaseRepository) -> None:
now = datetime.now(timezone.utc)
repository.save_policy(
CleanProfilePolicy(
policy_id="POL-ENT-CLEAN",
policy_version="1",
profile=ProfileType.ENTERPRISE_CLEAN,
active=True,
internal_source_registry_ref="REG-1",
prohibited_artifact_categories=["test-data"],
effective_from=now,
policy = CleanProfilePolicy(
policy_id="POL-ENT-CLEAN",
policy_version="1",
profile=ProfileType.ENTERPRISE_CLEAN,
active=True,
internal_source_registry_ref="REG-1",
prohibited_artifact_categories=["test-data"],
effective_from=now,
)
setattr(policy, "immutable", True)
repository.save_policy(policy)
registry = ResourceSourceRegistry(
registry_id="REG-1",
name="Default Internal Registry",
entries=[
ResourceSourceEntry(
source_id="S1",
host="internal-repo.company.com",
protocol="https",
purpose="artifactory",
)
],
updated_at=now,
updated_by="system",
)
setattr(registry, "immutable", True)
setattr(registry, "allowed_hosts", ["internal-repo.company.com"])
setattr(registry, "allowed_schemes", ["https"])
setattr(registry, "allowed_source_types", ["artifactory"])
repository.save_registry(registry)
candidate = ReleaseCandidate(
id="2026.03.03-rc1",
version="1.0.0",
source_snapshot_ref="v1.0.0-rc1",
created_at=now,
created_by="system",
status=CandidateStatus.DRAFT.value,
)
candidate.transition_to(CandidateStatus.PREPARED)
repository.save_candidate(candidate)
repository.save_artifact(
CandidateArtifact(
id="demo-art-1",
candidate_id=candidate.id,
path="src/main.py",
sha256="sha256-demo-core",
size=128,
detected_category="core",
)
)
repository.save_registry(
ResourceSourceRegistry(
registry_id="REG-1",
name="Default Internal Registry",
entries=[
ResourceSourceEntry(
source_id="S1",
host="internal-repo.company.com",
protocol="https",
purpose="artifactory",
)
],
updated_at=now,
updated_by="system",
repository.save_artifact(
CandidateArtifact(
id="demo-art-2",
candidate_id=candidate.id,
path="test/data.csv",
sha256="sha256-demo-test",
size=64,
detected_category="test-data",
)
)
repository.save_candidate(
ReleaseCandidate(
candidate_id="2026.03.03-rc1",
version="1.0.0",
profile=ProfileType.ENTERPRISE_CLEAN,
source_snapshot_ref="v1.0.0-rc1",
created_at=now,
created_by="system",
)
manifest = build_manifest_snapshot(
repository=repository,
candidate_id=candidate.id,
created_by="system",
policy_id="POL-ENT-CLEAN",
)
summary = dict(manifest.content_json.get("summary", {}))
summary["prohibited_detected_count"] = 1
manifest.content_json["summary"] = summary
def _bootstrap_real_repository(self, repository: CleanReleaseRepository) -> None:
bootstrap_path = os.getenv("CLEAN_TUI_BOOTSTRAP_JSON", "").strip()
@@ -126,9 +262,8 @@ class CleanReleaseTUI:
now = datetime.now(timezone.utc)
candidate = ReleaseCandidate(
candidate_id=payload.get("candidate_id", "candidate-1"),
id=payload.get("candidate_id", "candidate-1"),
version=payload.get("version", "1.0.0"),
profile=ProfileType.ENTERPRISE_CLEAN,
source_snapshot_ref=payload.get("source_snapshot_ref", "snapshot-ref"),
created_at=now,
created_by=payload.get("created_by", "operator"),
@@ -195,9 +330,14 @@ class CleanReleaseTUI:
self.stdscr.addstr(0, 0, centered[:max_x])
self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
candidate = self.overview.get("candidate")
candidate_text = self.candidate_id or "not-set"
profile_text = "enterprise-clean"
info_line_text = f" │ Candidate: [{candidate_text}] Profile: [{profile_text}] Mode: [{self.mode}]".ljust(max_x)
lifecycle = getattr(candidate, "status", "UNKNOWN")
info_line_text = (
f" │ Candidate: [{candidate_text}] Profile: [{profile_text}] "
f"Lifecycle: [{lifecycle}] Mode: [{self.mode}]"
).ljust(max_x)
self.stdscr.addstr(2, 0, info_line_text[:max_x])
def draw_checks(self):
@@ -235,10 +375,7 @@ class CleanReleaseTUI:
def draw_sources(self):
self.stdscr.addstr(12, 3, "Allowed Internal Sources:", curses.A_BOLD)
reg = None
policy = self.repo.get_active_policy()
if policy:
reg = self.repo.get_registry(policy.internal_source_registry_ref)
reg = self.overview.get("registry")
row = 13
if reg:
for entry in reg.entries:
@@ -257,122 +394,142 @@ class CleanReleaseTUI:
if self.report_id:
self.stdscr.addstr(19, 3, f"Report ID: {self.report_id}")
approval = self.overview.get("approval")
publication = self.overview.get("publication")
if approval:
self.stdscr.addstr(20, 3, f"Approval: {approval.decision}")
if publication:
self.stdscr.addstr(20, 32, f"Publication: {publication.status}")
if self.violations_list:
self.stdscr.addstr(21, 3, f"Violations Details ({len(self.violations_list)} total):", curses.color_pair(3) | curses.A_BOLD)
row = 22
for i, v in enumerate(self.violations_list[:5]):
v_cat = str(v.category.value if hasattr(v.category, "value") else v.category)
msg_text = f"[{v_cat}] {v.remediation} (Loc: {v.location})"
v_cat = str(getattr(v, "code", "VIOLATION"))
msg = str(getattr(v, "message", "Violation detected"))
location = str(
getattr(v, "artifact_path", "")
or getattr(getattr(v, "evidence_json", {}), "get", lambda *_: "")("location", "")
)
msg_text = f"[{v_cat}] {msg} (Loc: {location})"
self.stdscr.addstr(row + i, 5, msg_text[:70], curses.color_pair(3))
if self.last_error:
self.stdscr.addstr(27, 3, f"Error: {self.last_error}"[:100], curses.color_pair(3) | curses.A_BOLD)
def draw_footer(self, max_y: int, max_x: int):
footer_text = " F5 Run Check F7 Clear History F10 Exit ".center(max_x)
footer_text = " F5 Run F6 Manifest F7 Refresh F8 Approve F9 Publish F10 Exit ".center(max_x)
self.stdscr.attron(curses.color_pair(1))
self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x])
self.stdscr.attroff(curses.color_pair(1))
# [DEF:run_checks:Function]
# @PURPOSE: Execute compliance orchestrator run and update UI state.
# @PURPOSE: Execute compliance run via facade adapter and update UI state.
# @PRE: Candidate and policy snapshots are present in repository.
# @POST: UI reflects final run/report/violation state from service result.
def run_checks(self):
self.status = "RUNNING"
self.report_id = None
self.violations_list = []
self.checks_progress = []
self.last_error = None
self.refresh_screen()
candidate = self.repo.get_candidate(self.candidate_id) if self.candidate_id else None
policy = self.repo.get_active_policy()
if not candidate or not policy:
self.status = "FAILED"
self.last_error = "Candidate or active policy not found. Set CLEAN_TUI_CANDIDATE_ID and prepare repository data."
try:
result = self.facade.run_compliance(candidate_id=self.candidate_id, actor="operator")
except Exception as exc: # noqa: BLE001
self.status = CheckFinalStatus.FAILED
self.last_error = str(exc)
self.refresh_screen()
return
if self.mode == "demo":
# Prepare a manifest with a deliberate violation for demonstration mode.
artifacts = [
{"path": "src/main.py", "category": "core", "reason": "source code", "classification": "allowed"},
{"path": "test/data.csv", "category": "test-data", "reason": "test payload", "classification": "excluded-prohibited"},
]
manifest = build_distribution_manifest(
manifest_id=f"manifest-{candidate.candidate_id}",
candidate_id=candidate.candidate_id,
policy_id=policy.policy_id,
generated_by="operator",
artifacts=artifacts
)
self.repo.save_manifest(manifest)
else:
manifest = self.repo.get_manifest(f"manifest-{candidate.candidate_id}")
if manifest is None:
artifacts_path = os.getenv("CLEAN_TUI_ARTIFACTS_JSON", "").strip()
if artifacts_path:
try:
with open(artifacts_path, "r", encoding="utf-8") as artifacts_file:
artifacts = json.load(artifacts_file)
if not isinstance(artifacts, list):
raise ValueError("Artifacts JSON must be a list")
prepare_candidate(
repository=self.repo,
candidate_id=candidate.candidate_id,
artifacts=artifacts,
sources=[],
operator_id="tui-operator",
)
manifest = self.repo.get_manifest(f"manifest-{candidate.candidate_id}")
except Exception as exc:
self.status = "FAILED"
self.last_error = f"Unable to prepare manifest from CLEAN_TUI_ARTIFACTS_JSON: {exc}"
self.refresh_screen()
return
if manifest is None:
self.status = "FAILED"
self.last_error = "Manifest not found. Prepare candidate first or provide CLEAN_TUI_ARTIFACTS_JSON."
self.refresh_screen()
return
# Init orchestrator sequence
check_run = self.orchestrator.start_check_run(candidate.candidate_id, policy.policy_id, "operator", "tui")
self.stdscr.nodelay(True)
stages = [
CheckStageName.DATA_PURITY,
CheckStageName.INTERNAL_SOURCES_ONLY,
CheckStageName.NO_EXTERNAL_ENDPOINTS,
CheckStageName.MANIFEST_CONSISTENCY
self.checks_progress = [
{
"stage": stage.stage_name,
"status": CheckStageStatus.PASS if str(stage.decision).upper() == "PASSED" else CheckStageStatus.FAIL,
}
for stage in result.stage_runs
]
for stage in stages:
self.checks_progress.append({"stage": stage, "status": "RUNNING"})
self.refresh_screen()
time.sleep(0.3) # Simulation delay
# Real logic
self.orchestrator.execute_stages(check_run)
self.orchestrator.finalize_run(check_run)
# Sync TUI state
self.checks_progress = [{"stage": c.stage, "status": c.status} for c in check_run.checks]
self.status = check_run.final_status
self.report_id = f"CCR-{datetime.now().strftime('%Y-%m-%d-%H%M%S')}"
self.violations_list = self.repo.get_violations_by_check_run(check_run.check_run_id)
self.violations_list = result.violations
self.report_id = result.report.id if result.report is not None else None
final_status = str(result.run.final_status or "").upper()
if final_status in {"BLOCKED", CheckFinalStatus.BLOCKED.value}:
self.status = CheckFinalStatus.BLOCKED
elif final_status in {"COMPLIANT", "PASSED", CheckFinalStatus.COMPLIANT.value}:
self.status = CheckFinalStatus.COMPLIANT
else:
self.status = CheckFinalStatus.FAILED
self.refresh_overview()
self.refresh_screen()
def build_manifest(self):
try:
manifest = self.facade.build_manifest(candidate_id=self.candidate_id, actor="operator")
self.status = "READY"
self.report_id = None
self.violations_list = []
self.checks_progress = []
self.last_error = f"Manifest built: {manifest.id}"
except Exception as exc: # noqa: BLE001
self.last_error = str(exc)
self.refresh_overview()
self.refresh_screen()
def clear_history(self):
self.repo.clear_history()
self.status = "READY"
self.report_id = None
self.violations_list = []
self.checks_progress = []
self.last_error = None
self.refresh_overview()
self.refresh_screen()
def approve_latest(self):
if not self.report_id:
self.last_error = "F8 disabled: no compliance report available"
self.refresh_screen()
return
try:
self.facade.approve_latest(candidate_id=self.candidate_id, actor="operator")
self.last_error = None
except Exception as exc: # noqa: BLE001
self.last_error = str(exc)
self.refresh_overview()
self.refresh_screen()
def publish_latest(self):
if not self.report_id:
self.last_error = "F9 disabled: no compliance report available"
self.refresh_screen()
return
try:
self.facade.publish_latest(candidate_id=self.candidate_id, actor="operator")
self.last_error = None
except Exception as exc: # noqa: BLE001
self.last_error = str(exc)
self.refresh_overview()
self.refresh_screen()
def refresh_overview(self):
if not self.report_id:
self.last_error = "F9 disabled: no compliance report available"
self.refresh_screen()
return
try:
self.facade.publish_latest(candidate_id=self.candidate_id, actor="operator")
self.last_error = None
except Exception as exc: # noqa: BLE001
self.last_error = str(exc)
self.refresh_overview()
self.refresh_screen()
def refresh_overview(self):
if not self.candidate_id:
self.overview = {}
return
self.overview = self.facade.get_overview(candidate_id=self.candidate_id)
def refresh_screen(self):
max_y, max_x = self.stdscr.getmaxyx()
self.stdscr.clear()
@@ -382,8 +539,8 @@ class CleanReleaseTUI:
self.draw_sources()
self.draw_status()
self.draw_footer(max_y, max_x)
except curses.error:
pass
except Exception:
pass
self.stdscr.refresh()
def loop(self):
@@ -394,8 +551,14 @@ class CleanReleaseTUI:
break
elif char == curses.KEY_F5:
self.run_checks()
elif char == curses.KEY_F6:
self.build_manifest()
elif char == curses.KEY_F7:
self.clear_history()
elif char == curses.KEY_F8:
self.approve_latest()
elif char == curses.KEY_F9:
self.publish_latest()
# [/DEF:CleanReleaseTUI:Class]
@@ -406,10 +569,13 @@ def tui_main(stdscr: curses.window):
def main() -> int:
# Headless check for CI/Tests
if not sys.stdout.isatty() or "PYTEST_CURRENT_TEST" in os.environ:
print("Enterprise Clean Release Validator (Headless Mode) - FINAL STATUS: READY")
return 0
# TUI requires interactive terminal; headless mode must use CLI/API flow.
if not sys.stdout.isatty():
print(
"TTY is required for TUI mode. Use CLI/API workflow instead.",
file=sys.stderr,
)
return 2
try:
curses.wrapper(tui_main)
return 0

View File

@@ -1,20 +1,16 @@
# [DEF:backend.src.services.clean_release:Module]
# [DEF:clean_release:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, services, package, initialization
# @PURPOSE: Initialize clean release service package and provide explicit module exports.
# @PURPOSE: Redesigned clean release compliance subsystem.
# @LAYER: Domain
# @RELATION: EXPORTS -> policy_engine, manifest_builder, preparation_service, source_isolation, compliance_orchestrator, report_builder, repository, stages, audit_service
# @INVARIANT: Package import must not execute runtime side effects beyond symbol export setup.
from backend.src.core.logger import logger
# [REASON] Initializing clean_release package.
logger.reason("Clean release compliance subsystem initialized.")
# Legacy compatibility exports are intentionally lazy to avoid import cycles.
__all__ = [
"policy_engine",
"manifest_builder",
"preparation_service",
"source_isolation",
"compliance_orchestrator",
"report_builder",
"repository",
"stages",
"audit_service",
"logger",
]
# [/DEF:backend.src.services.clean_release:Module]
# [/DEF:clean_release:Module]

View File

@@ -0,0 +1,178 @@
# [DEF:backend.src.services.clean_release.approval_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, approval, decision, lifecycle, gate
# @PURPOSE: Enforce approval/rejection gates over immutable compliance reports.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.audit_service
# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; decisions are append-only.
from __future__ import annotations
from datetime import datetime, timezone
from typing import List
from uuid import uuid4
from ...core.logger import belief_scope, logger
from ...models.clean_release import ApprovalDecision
from .audit_service import audit_preparation
from .enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision
from .exceptions import ApprovalGateError
from .repository import CleanReleaseRepository
# [DEF:_get_or_init_decisions_store:Function]
# @PURPOSE: Provide append-only in-memory storage for approval decisions.
# @PRE: repository is initialized.
# @POST: Returns mutable decision list attached to repository.
def _get_or_init_decisions_store(repository: CleanReleaseRepository) -> List[ApprovalDecision]:
decisions = getattr(repository, "approval_decisions", None)
if decisions is None:
decisions = []
setattr(repository, "approval_decisions", decisions)
return decisions
# [/DEF:_get_or_init_decisions_store:Function]
# [DEF:_latest_decision_for_candidate:Function]
# @PURPOSE: Resolve latest approval decision for candidate from append-only store.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest ApprovalDecision or None.
def _latest_decision_for_candidate(repository: CleanReleaseRepository, candidate_id: str) -> ApprovalDecision | None:
decisions = _get_or_init_decisions_store(repository)
scoped = [item for item in decisions if item.candidate_id == candidate_id]
if not scoped:
return None
return sorted(scoped, key=lambda item: item.decided_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)[0]
# [/DEF:_latest_decision_for_candidate:Function]
# [DEF:_resolve_candidate_and_report:Function]
# @PURPOSE: Validate candidate/report existence and ownership prior to decision persistence.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Returns tuple(candidate, report); raises ApprovalGateError on contract violation.
def _resolve_candidate_and_report(
repository: CleanReleaseRepository,
*,
candidate_id: str,
report_id: str,
):
candidate = repository.get_candidate(candidate_id)
if candidate is None:
raise ApprovalGateError(f"candidate '{candidate_id}' not found")
report = repository.get_report(report_id)
if report is None:
raise ApprovalGateError(f"report '{report_id}' not found")
if report.candidate_id != candidate_id:
raise ApprovalGateError("report belongs to another candidate")
return candidate, report
# [/DEF:_resolve_candidate_and_report:Function]
# [DEF:approve_candidate:Function]
# @PURPOSE: Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED.
# @PRE: Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED.
# @POST: Approval decision is appended and candidate transitions to APPROVED.
def approve_candidate(
*,
repository: CleanReleaseRepository,
candidate_id: str,
report_id: str,
decided_by: str,
comment: str | None = None,
) -> ApprovalDecision:
with belief_scope("approval_service.approve_candidate"):
logger.reason(f"[REASON] Evaluating approve gate candidate_id={candidate_id} report_id={report_id}")
if not decided_by or not decided_by.strip():
raise ApprovalGateError("decided_by must be non-empty")
candidate, report = _resolve_candidate_and_report(
repository,
candidate_id=candidate_id,
report_id=report_id,
)
if report.final_status != ComplianceDecision.PASSED.value:
raise ApprovalGateError("approve requires PASSED compliance report")
latest = _latest_decision_for_candidate(repository, candidate_id)
if latest is not None and latest.decision == ApprovalDecisionType.APPROVED.value:
raise ApprovalGateError("candidate is already approved")
if candidate.status == CandidateStatus.APPROVED.value:
raise ApprovalGateError("candidate is already approved")
try:
if candidate.status != CandidateStatus.CHECK_PASSED.value:
raise ApprovalGateError(
f"candidate status '{candidate.status}' cannot transition to APPROVED"
)
candidate.transition_to(CandidateStatus.APPROVED)
repository.save_candidate(candidate)
except ApprovalGateError:
raise
except Exception as exc: # noqa: BLE001
logger.explore(f"[EXPLORE] Candidate transition to APPROVED failed candidate_id={candidate_id}: {exc}")
raise ApprovalGateError(str(exc)) from exc
decision = ApprovalDecision(
id=f"approve-{uuid4()}",
candidate_id=candidate_id,
report_id=report_id,
decision=ApprovalDecisionType.APPROVED.value,
decided_by=decided_by,
decided_at=datetime.now(timezone.utc),
comment=comment,
)
_get_or_init_decisions_store(repository).append(decision)
audit_preparation(candidate_id, "APPROVED", repository=repository, actor=decided_by)
logger.reflect(f"[REFLECT] Approval persisted candidate_id={candidate_id} decision_id={decision.id}")
return decision
# [/DEF:approve_candidate:Function]
# [DEF:reject_candidate:Function]
# @PURPOSE: Persist immutable REJECTED decision without promoting candidate lifecycle.
# @PRE: Candidate exists and report belongs to candidate.
# @POST: Rejected decision is appended; candidate lifecycle is unchanged.
def reject_candidate(
*,
repository: CleanReleaseRepository,
candidate_id: str,
report_id: str,
decided_by: str,
comment: str | None = None,
) -> ApprovalDecision:
with belief_scope("approval_service.reject_candidate"):
logger.reason(f"[REASON] Evaluating reject decision candidate_id={candidate_id} report_id={report_id}")
if not decided_by or not decided_by.strip():
raise ApprovalGateError("decided_by must be non-empty")
_resolve_candidate_and_report(
repository,
candidate_id=candidate_id,
report_id=report_id,
)
decision = ApprovalDecision(
id=f"reject-{uuid4()}",
candidate_id=candidate_id,
report_id=report_id,
decision=ApprovalDecisionType.REJECTED.value,
decided_by=decided_by,
decided_at=datetime.now(timezone.utc),
comment=comment,
)
_get_or_init_decisions_store(repository).append(decision)
audit_preparation(candidate_id, "REJECTED", repository=repository, actor=decided_by)
logger.reflect(f"[REFLECT] Rejection persisted candidate_id={candidate_id} decision_id={decision.id}")
return decision
# [/DEF:reject_candidate:Function]
# [/DEF:backend.src.services.clean_release.approval_service:Module]

View File

@@ -8,17 +8,100 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from uuid import uuid4
from ...core.logger import logger
def audit_preparation(candidate_id: str, status: str) -> None:
def _append_event(repository, payload: Dict[str, Any]) -> None:
if repository is not None and hasattr(repository, "append_audit_event"):
repository.append_audit_event(payload)
def audit_preparation(candidate_id: str, status: str, repository=None, actor: str = "system") -> None:
logger.info(f"[REASON] clean-release preparation candidate={candidate_id} status={status}")
_append_event(
repository,
{
"id": f"audit-{uuid4()}",
"action": "PREPARATION",
"candidate_id": candidate_id,
"actor": actor,
"status": status,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
def audit_check_run(check_run_id: str, final_status: str) -> None:
def audit_check_run(
check_run_id: str,
final_status: str,
repository=None,
*,
candidate_id: Optional[str] = None,
actor: str = "system",
) -> None:
logger.info(f"[REFLECT] clean-release check_run={check_run_id} final_status={final_status}")
_append_event(
repository,
{
"id": f"audit-{uuid4()}",
"action": "CHECK_RUN",
"run_id": check_run_id,
"candidate_id": candidate_id,
"actor": actor,
"status": final_status,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
def audit_report(report_id: str, candidate_id: str) -> None:
def audit_violation(
run_id: str,
stage_name: str,
code: str,
repository=None,
*,
candidate_id: Optional[str] = None,
actor: str = "system",
) -> None:
logger.info(f"[EXPLORE] clean-release violation run_id={run_id} stage={stage_name} code={code}")
_append_event(
repository,
{
"id": f"audit-{uuid4()}",
"action": "VIOLATION",
"run_id": run_id,
"candidate_id": candidate_id,
"actor": actor,
"stage_name": stage_name,
"code": code,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
def audit_report(
report_id: str,
candidate_id: str,
repository=None,
*,
run_id: Optional[str] = None,
actor: str = "system",
) -> None:
logger.info(f"[EXPLORE] clean-release report_id={report_id} candidate={candidate_id}")
_append_event(
repository,
{
"id": f"audit-{uuid4()}",
"action": "REPORT",
"report_id": report_id,
"run_id": run_id,
"candidate_id": candidate_id,
"actor": actor,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
# [/DEF:backend.src.services.clean_release.audit_service:Module]

View File

@@ -0,0 +1,107 @@
# [DEF:backend.src.services.clean_release.candidate_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, candidate, artifacts, lifecycle, validation
# @PURPOSE: Register release candidates with validated artifacts and advance lifecycle through legal transitions.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @PRE: candidate_id must be unique; artifacts input must be non-empty and valid.
# @POST: candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.
# @INVARIANT: Candidate lifecycle transitions are delegated to domain guard logic.
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List
from ...models.clean_release import CandidateArtifact, ReleaseCandidate
from .enums import CandidateStatus
from .repository import CleanReleaseRepository
# [DEF:_validate_artifacts:Function]
# @PURPOSE: Validate raw artifact payload list for required fields and shape.
# @PRE: artifacts payload is provided by caller.
# @POST: Returns normalized artifact list or raises ValueError.
def _validate_artifacts(artifacts: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
normalized = list(artifacts)
if not normalized:
raise ValueError("artifacts must not be empty")
required_fields = ("id", "path", "sha256", "size")
for index, artifact in enumerate(normalized):
if not isinstance(artifact, dict):
raise ValueError(f"artifact[{index}] must be an object")
for field in required_fields:
if field not in artifact:
raise ValueError(f"artifact[{index}] missing required field '{field}'")
if not str(artifact["id"]).strip():
raise ValueError(f"artifact[{index}] field 'id' must be non-empty")
if not str(artifact["path"]).strip():
raise ValueError(f"artifact[{index}] field 'path' must be non-empty")
if not str(artifact["sha256"]).strip():
raise ValueError(f"artifact[{index}] field 'sha256' must be non-empty")
if not isinstance(artifact["size"], int) or artifact["size"] <= 0:
raise ValueError(f"artifact[{index}] field 'size' must be a positive integer")
return normalized
# [/DEF:_validate_artifacts:Function]
# [DEF:register_candidate:Function]
# @PURPOSE: Register a candidate and persist its artifacts with legal lifecycle transition.
# @PRE: candidate_id must be unique and artifacts must pass validation.
# @POST: Candidate exists in repository with PREPARED status and artifacts persisted.
def register_candidate(
repository: CleanReleaseRepository,
candidate_id: str,
version: str,
source_snapshot_ref: str,
created_by: str,
artifacts: Iterable[Dict[str, Any]],
) -> ReleaseCandidate:
if not candidate_id or not candidate_id.strip():
raise ValueError("candidate_id must be non-empty")
if not version or not version.strip():
raise ValueError("version must be non-empty")
if not source_snapshot_ref or not source_snapshot_ref.strip():
raise ValueError("source_snapshot_ref must be non-empty")
if not created_by or not created_by.strip():
raise ValueError("created_by must be non-empty")
existing = repository.get_candidate(candidate_id)
if existing is not None:
raise ValueError(f"candidate '{candidate_id}' already exists")
validated_artifacts = _validate_artifacts(artifacts)
candidate = ReleaseCandidate(
id=candidate_id,
version=version,
source_snapshot_ref=source_snapshot_ref,
created_by=created_by,
created_at=datetime.now(timezone.utc),
status=CandidateStatus.DRAFT.value,
)
repository.save_candidate(candidate)
for artifact_payload in validated_artifacts:
artifact = CandidateArtifact(
id=str(artifact_payload["id"]),
candidate_id=candidate_id,
path=str(artifact_payload["path"]),
sha256=str(artifact_payload["sha256"]),
size=int(artifact_payload["size"]),
detected_category=artifact_payload.get("detected_category"),
declared_category=artifact_payload.get("declared_category"),
source_uri=artifact_payload.get("source_uri"),
source_host=artifact_payload.get("source_host"),
metadata_json=artifact_payload.get("metadata_json", {}),
)
repository.save_artifact(artifact)
candidate.transition_to(CandidateStatus.PREPARED)
repository.save_candidate(candidate)
return candidate
# [/DEF:register_candidate:Function]
# [/DEF:backend.src.services.clean_release.candidate_service:Module]

View File

@@ -0,0 +1,197 @@
# [DEF:backend.src.services.clean_release.compliance_execution_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, compliance, execution, stages, immutable-evidence
# @PURPOSE: Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.policy_resolution_service
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.stages
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.report_builder
# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Iterable, List, Optional
from uuid import uuid4
from ...core.logger import belief_scope, logger
from ...models.clean_release import ComplianceReport, ComplianceRun, ComplianceStageRun, ComplianceViolation, DistributionManifest
from .audit_service import audit_check_run, audit_report, audit_violation
from .enums import ComplianceDecision, RunStatus
from .exceptions import ComplianceRunError, PolicyResolutionError
from .policy_resolution_service import resolve_trusted_policy_snapshots
from .report_builder import ComplianceReportBuilder
from .repository import CleanReleaseRepository
from .stages import build_default_stages, derive_final_status
from .stages.base import ComplianceStage, ComplianceStageContext, build_stage_run_record
# [DEF:ComplianceExecutionResult:Class]
# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts.
@dataclass
class ComplianceExecutionResult:
run: ComplianceRun
report: Optional[ComplianceReport]
stage_runs: List[ComplianceStageRun]
violations: List[ComplianceViolation]
# [/DEF:ComplianceExecutionResult:Class]
# [DEF:ComplianceExecutionService:Class]
# @PURPOSE: Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence.
# @PRE: repository and config_manager are initialized.
# @POST: run state, stage records, violations and optional report are persisted consistently.
class ComplianceExecutionService:
TASK_PLUGIN_ID = "clean-release-compliance"
def __init__(
self,
*,
repository: CleanReleaseRepository,
config_manager,
stages: Optional[Iterable[ComplianceStage]] = None,
):
self.repository = repository
self.config_manager = config_manager
self.stages = list(stages) if stages is not None else build_default_stages()
self.report_builder = ComplianceReportBuilder(repository)
# [DEF:_resolve_manifest:Function]
# @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.
# @PRE: candidate exists.
# @POST: Returns manifest snapshot or raises ComplianceRunError.
def _resolve_manifest(self, candidate_id: str, manifest_id: Optional[str]) -> DistributionManifest:
with belief_scope("ComplianceExecutionService._resolve_manifest"):
if manifest_id:
manifest = self.repository.get_manifest(manifest_id)
if manifest is None:
raise ComplianceRunError(f"manifest '{manifest_id}' not found")
if manifest.candidate_id != candidate_id:
raise ComplianceRunError("manifest does not belong to candidate")
return manifest
manifests = self.repository.get_manifests_by_candidate(candidate_id)
if not manifests:
raise ComplianceRunError(f"candidate '{candidate_id}' has no manifest")
return sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]
# [/DEF:_resolve_manifest:Function]
# [DEF:_persist_stage_run:Function]
# @PURPOSE: Persist stage run if repository supports stage records.
# @POST: Stage run is persisted when adapter is available, otherwise no-op.
def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:
if hasattr(self.repository, "save_stage_run"):
self.repository.save_stage_run(stage_run)
# [/DEF:_persist_stage_run:Function]
# [DEF:_persist_violations:Function]
# @PURPOSE: Persist stage violations via repository adapters.
# @POST: Violations are appended to repository evidence store.
def _persist_violations(self, violations: List[ComplianceViolation]) -> None:
for violation in violations:
self.repository.save_violation(violation)
# [/DEF:_persist_violations:Function]
# [DEF:execute_run:Function]
# @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.
# @PRE: candidate exists and trusted policy/registry snapshots are resolvable.
# @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.
def execute_run(
self,
*,
candidate_id: str,
requested_by: str,
manifest_id: Optional[str] = None,
) -> ComplianceExecutionResult:
with belief_scope("ComplianceExecutionService.execute_run"):
logger.reason(f"Starting compliance execution candidate_id={candidate_id}")
candidate = self.repository.get_candidate(candidate_id)
if candidate is None:
raise ComplianceRunError(f"candidate '{candidate_id}' not found")
manifest = self._resolve_manifest(candidate_id, manifest_id)
try:
policy_snapshot, registry_snapshot = resolve_trusted_policy_snapshots(
config_manager=self.config_manager,
repository=self.repository,
)
except PolicyResolutionError as exc:
raise ComplianceRunError(str(exc)) from exc
run = ComplianceRun(
id=f"run-{uuid4()}",
candidate_id=candidate_id,
manifest_id=manifest.id,
manifest_digest=manifest.manifest_digest,
policy_snapshot_id=policy_snapshot.id,
registry_snapshot_id=registry_snapshot.id,
requested_by=requested_by,
requested_at=datetime.now(timezone.utc),
started_at=datetime.now(timezone.utc),
status=RunStatus.RUNNING.value,
)
self.repository.save_check_run(run)
stage_runs: List[ComplianceStageRun] = []
violations: List[ComplianceViolation] = []
report: Optional[ComplianceReport] = None
context = ComplianceStageContext(
run=run,
candidate=candidate,
manifest=manifest,
policy=policy_snapshot,
registry=registry_snapshot,
)
try:
for stage in self.stages:
started = datetime.now(timezone.utc)
result = stage.execute(context)
finished = datetime.now(timezone.utc)
stage_run = build_stage_run_record(
run_id=run.id,
stage_name=stage.stage_name,
result=result,
started_at=started,
finished_at=finished,
)
self._persist_stage_run(stage_run)
stage_runs.append(stage_run)
if result.violations:
self._persist_violations(result.violations)
violations.extend(result.violations)
run.final_status = derive_final_status(stage_runs).value
run.status = RunStatus.SUCCEEDED.value
run.finished_at = datetime.now(timezone.utc)
self.repository.save_check_run(run)
report = self.report_builder.build_report_payload(run, violations)
report = self.report_builder.persist_report(report)
run.report_id = report.id
self.repository.save_check_run(run)
logger.reflect(f"[REFLECT] Compliance run completed run_id={run.id} final_status={run.final_status}")
except Exception as exc: # noqa: BLE001
run.status = RunStatus.FAILED.value
run.final_status = ComplianceDecision.ERROR.value
run.failure_reason = str(exc)
run.finished_at = datetime.now(timezone.utc)
self.repository.save_check_run(run)
logger.explore(f"[EXPLORE] Compliance run failed run_id={run.id}: {exc}")
return ComplianceExecutionResult(
run=run,
report=report,
stage_runs=stage_runs,
violations=violations,
)
# [/DEF:execute_run:Function]
# [/DEF:ComplianceExecutionService:Class]
# [/DEF:backend.src.services.clean_release.compliance_execution_service:Module]

View File

@@ -20,19 +20,21 @@ from datetime import datetime, timezone
from typing import List, Optional
from uuid import uuid4
from ...models.clean_release import (
CheckFinalStatus,
CheckStageName,
CheckStageResult,
CheckStageStatus,
ComplianceCheckRun,
ComplianceViolation,
from .enums import (
RunStatus,
ComplianceDecision,
ComplianceStageName,
ViolationCategory,
ViolationSeverity,
)
from ...models.clean_release import (
ComplianceRun,
ComplianceStageRun,
ComplianceViolation,
)
from .policy_engine import CleanPolicyEngine
from .repository import CleanReleaseRepository
from .stages import MANDATORY_STAGE_ORDER, derive_final_status
from .stages import derive_final_status
# [DEF:CleanComplianceOrchestrator:Class]
@@ -44,108 +46,93 @@ class CleanComplianceOrchestrator:
# [DEF:start_check_run:Function]
# @PURPOSE: Initiate a new compliance run session.
# @PRE: candidate_id and policy_id must exist in repository.
# @POST: Returns initialized ComplianceCheckRun in RUNNING state.
def start_check_run(self, candidate_id: str, policy_id: str, triggered_by: str, execution_mode: str) -> ComplianceCheckRun:
check_run = ComplianceCheckRun(
check_run_id=f"check-{uuid4()}",
# @POST: Returns initialized ComplianceRun in RUNNING state.
def start_check_run(self, candidate_id: str, policy_id: str, requested_by: str, manifest_id: str) -> ComplianceRun:
manifest = self.repository.get_manifest(manifest_id)
policy = self.repository.get_policy(policy_id)
if not manifest or not policy:
raise ValueError("Manifest or Policy not found")
check_run = ComplianceRun(
id=f"check-{uuid4()}",
candidate_id=candidate_id,
policy_id=policy_id,
started_at=datetime.now(timezone.utc),
final_status=CheckFinalStatus.RUNNING,
triggered_by=triggered_by,
execution_mode=execution_mode,
checks=[],
manifest_id=manifest_id,
manifest_digest=manifest.manifest_digest,
policy_snapshot_id=policy_id,
registry_snapshot_id=policy.registry_snapshot_id,
requested_by=requested_by,
requested_at=datetime.now(timezone.utc),
status=RunStatus.RUNNING,
)
return self.repository.save_check_run(check_run)
def execute_stages(self, check_run: ComplianceCheckRun, forced_results: Optional[List[CheckStageResult]] = None) -> ComplianceCheckRun:
def execute_stages(self, check_run: ComplianceRun, forced_results: Optional[List[ComplianceStageRun]] = None) -> ComplianceRun:
if forced_results is not None:
check_run.checks = forced_results
# In a real scenario, we'd persist these stages.
return self.repository.save_check_run(check_run)
# Real Logic Integration
candidate = self.repository.get_candidate(check_run.candidate_id)
policy = self.repository.get_policy(check_run.policy_id)
policy = self.repository.get_policy(check_run.policy_snapshot_id)
if not candidate or not policy:
check_run.final_status = CheckFinalStatus.FAILED
check_run.status = RunStatus.FAILED
return self.repository.save_check_run(check_run)
registry = self.repository.get_registry(policy.internal_source_registry_ref)
manifest = self.repository.get_manifest(f"manifest-{candidate.candidate_id}")
registry = self.repository.get_registry(check_run.registry_snapshot_id)
manifest = self.repository.get_manifest(check_run.manifest_id)
if not registry or not manifest:
check_run.final_status = CheckFinalStatus.FAILED
check_run.status = RunStatus.FAILED
return self.repository.save_check_run(check_run)
engine = CleanPolicyEngine(policy=policy, registry=registry)
stages_results = []
violations = []
# Simulate stage execution and violation detection
# 1. DATA_PURITY
purity_ok = manifest.summary.prohibited_detected_count == 0
stages_results.append(CheckStageResult(
stage=CheckStageName.DATA_PURITY,
status=CheckStageStatus.PASS if purity_ok else CheckStageStatus.FAIL,
details=f"Detected {manifest.summary.prohibited_detected_count} prohibited items" if not purity_ok else "No prohibited items found"
))
if not purity_ok:
for item in manifest.items:
if item.classification.value == "excluded-prohibited":
violations.append(ComplianceViolation(
violation_id=f"V-{uuid4()}",
check_run_id=check_run.check_run_id,
category=ViolationCategory.DATA_PURITY,
severity=ViolationSeverity.CRITICAL,
location=item.path,
remediation="Remove prohibited content",
blocked_release=True,
detected_at=datetime.now(timezone.utc)
))
# 2. INTERNAL_SOURCES_ONLY
# In a real scenario, we'd check against actual sources list.
# For simplicity in this orchestrator, we check if violations were pre-detected in manifest/preparation
# or we could re-run source validation if we had the raw sources list.
# Assuming for TUI demo we check if any "external-source" violation exists in preparation phase
# (Though preparation_service saves them to candidate status, let's keep it simple here)
stages_results.append(CheckStageResult(
stage=CheckStageName.INTERNAL_SOURCES_ONLY,
status=CheckStageStatus.PASS,
details="All sources verified against registry"
))
# 3. NO_EXTERNAL_ENDPOINTS
stages_results.append(CheckStageResult(
stage=CheckStageName.NO_EXTERNAL_ENDPOINTS,
status=CheckStageStatus.PASS,
details="Endpoint scan complete"
))
# 4. MANIFEST_CONSISTENCY
stages_results.append(CheckStageResult(
stage=CheckStageName.MANIFEST_CONSISTENCY,
status=CheckStageStatus.PASS,
details=f"Deterministic hash: {manifest.deterministic_hash[:12]}..."
))
check_run.checks = stages_results
summary = manifest.content_json.get("summary", {})
purity_ok = summary.get("prohibited_detected_count", 0) == 0
# Save violations if any
if violations:
for v in violations:
self.repository.save_violation(v)
if not purity_ok:
check_run.final_status = ComplianceDecision.BLOCKED
else:
check_run.final_status = ComplianceDecision.PASSED
check_run.status = RunStatus.SUCCEEDED
check_run.finished_at = datetime.now(timezone.utc)
return self.repository.save_check_run(check_run)
# [DEF:finalize_run:Function]
# @PURPOSE: Finalize run status based on cumulative stage results.
# @POST: Status derivation follows strict MANDATORY_STAGE_ORDER.
def finalize_run(self, check_run: ComplianceCheckRun) -> ComplianceCheckRun:
final_status = derive_final_status(check_run.checks)
check_run.final_status = final_status
def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:
# If not already set by execute_stages
if not check_run.final_status:
check_run.final_status = ComplianceDecision.PASSED
check_run.status = RunStatus.SUCCEEDED
check_run.finished_at = datetime.now(timezone.utc)
return self.repository.save_check_run(check_run)
# [/DEF:CleanComplianceOrchestrator:Class]
# [/DEF:backend.src.services.clean_release.compliance_orchestrator:Module]
# [DEF:run_check_legacy:Function]
# @PURPOSE: Legacy wrapper for compatibility with previous orchestrator call style.
# @PRE: Candidate/policy/manifest identifiers are valid for repository.
# @POST: Returns finalized ComplianceRun produced by orchestrator.
def run_check_legacy(
repository: CleanReleaseRepository,
candidate_id: str,
policy_id: str,
requested_by: str,
manifest_id: str,
) -> ComplianceRun:
orchestrator = CleanComplianceOrchestrator(repository)
run = orchestrator.start_check_run(
candidate_id=candidate_id,
policy_id=policy_id,
requested_by=requested_by,
manifest_id=manifest_id,
)
run = orchestrator.execute_stages(run)
return orchestrator.finalize_run(run)
# [/DEF:run_check_legacy:Function]
# [/DEF:backend.src.services.clean_release.compliance_orchestrator:Module]

View File

@@ -0,0 +1,50 @@
# [DEF:backend.src.services.clean_release.demo_data_service:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, demo-mode, namespace, isolation, repository
# @PURPOSE: Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers.
from __future__ import annotations
from .repository import CleanReleaseRepository
# [DEF:resolve_namespace:Function]
# @PURPOSE: Resolve canonical clean-release namespace for requested mode.
# @PRE: mode is a non-empty string identifying runtime mode.
# @POST: Returns deterministic namespace key for demo/real separation.
def resolve_namespace(mode: str) -> str:
normalized = (mode or "").strip().lower()
if normalized == "demo":
return "clean-release:demo"
return "clean-release:real"
# [/DEF:resolve_namespace:Function]
# [DEF:build_namespaced_id:Function]
# @PURPOSE: Build storage-safe physical identifier under mode namespace.
# @PRE: namespace and logical_id are non-empty strings.
# @POST: Returns deterministic "{namespace}::{logical_id}" identifier.
def build_namespaced_id(namespace: str, logical_id: str) -> str:
if not namespace or not namespace.strip():
raise ValueError("namespace must be non-empty")
if not logical_id or not logical_id.strip():
raise ValueError("logical_id must be non-empty")
return f"{namespace}::{logical_id}"
# [/DEF:build_namespaced_id:Function]
# [DEF:create_isolated_repository:Function]
# @PURPOSE: Create isolated in-memory repository instance for selected mode namespace.
# @PRE: mode is a valid runtime mode marker.
# @POST: Returns repository instance tagged with namespace metadata.
def create_isolated_repository(mode: str) -> CleanReleaseRepository:
namespace = resolve_namespace(mode)
repository = CleanReleaseRepository()
setattr(repository, "namespace", namespace)
return repository
# [/DEF:create_isolated_repository:Function]
# [/DEF:backend.src.services.clean_release.demo_data_service:Module]

View File

@@ -0,0 +1,85 @@
# [DEF:clean_release_dto:Module]
# @TIER: STANDARD
# @PURPOSE: Data Transfer Objects for clean release compliance subsystem.
# @LAYER: Application
from datetime import datetime
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
from backend.src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision
class CandidateDTO(BaseModel):
"""DTO for ReleaseCandidate."""
id: str
version: str
source_snapshot_ref: str
build_id: Optional[str] = None
created_at: datetime
created_by: str
status: CandidateStatus
class ArtifactDTO(BaseModel):
"""DTO for CandidateArtifact."""
id: str
candidate_id: str
path: str
sha256: str
size: int
detected_category: Optional[str] = None
declared_category: Optional[str] = None
source_uri: Optional[str] = None
source_host: Optional[str] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class ManifestDTO(BaseModel):
"""DTO for DistributionManifest."""
id: str
candidate_id: str
manifest_version: int
manifest_digest: str
artifacts_digest: str
created_at: datetime
created_by: str
source_snapshot_ref: str
content_json: Dict[str, Any]
class ComplianceRunDTO(BaseModel):
"""DTO for ComplianceRun status tracking."""
run_id: str
candidate_id: str
status: RunStatus
final_status: Optional[ComplianceDecision] = None
report_id: Optional[str] = None
task_id: Optional[str] = None
class ReportDTO(BaseModel):
"""Compact report view."""
report_id: str
candidate_id: str
final_status: ComplianceDecision
policy_version: str
manifest_digest: str
violation_count: int
generated_at: datetime
class CandidateOverviewDTO(BaseModel):
"""Read model for candidate overview."""
candidate_id: str
version: str
source_snapshot_ref: str
status: CandidateStatus
latest_manifest_id: Optional[str] = None
latest_manifest_digest: Optional[str] = None
latest_run_id: Optional[str] = None
latest_run_status: Optional[RunStatus] = None
latest_report_id: Optional[str] = None
latest_report_final_status: Optional[ComplianceDecision] = None
latest_policy_snapshot_id: Optional[str] = None
latest_policy_version: Optional[str] = None
latest_registry_snapshot_id: Optional[str] = None
latest_registry_version: Optional[str] = None
latest_approval_decision: Optional[str] = None
latest_publication_id: Optional[str] = None
latest_publication_status: Optional[str] = None
# [/DEF:clean_release_dto:Module]

View File

@@ -0,0 +1,72 @@
# [DEF:clean_release_enums:Module]
# @TIER: STANDARD
# @PURPOSE: Canonical enums for clean release lifecycle and compliance.
# @LAYER: Domain
from enum import Enum
class CandidateStatus(str, Enum):
"""Lifecycle states for a ReleaseCandidate."""
DRAFT = "DRAFT"
PREPARED = "PREPARED"
MANIFEST_BUILT = "MANIFEST_BUILT"
CHECK_PENDING = "CHECK_PENDING"
CHECK_RUNNING = "CHECK_RUNNING"
CHECK_PASSED = "CHECK_PASSED"
CHECK_BLOCKED = "CHECK_BLOCKED"
CHECK_ERROR = "CHECK_ERROR"
APPROVED = "APPROVED"
PUBLISHED = "PUBLISHED"
REVOKED = "REVOKED"
class RunStatus(str, Enum):
"""Execution status for a ComplianceRun."""
PENDING = "PENDING"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
class ComplianceDecision(str, Enum):
"""Final compliance result for a run or stage."""
PASSED = "PASSED"
BLOCKED = "BLOCKED"
ERROR = "ERROR"
class ApprovalDecisionType(str, Enum):
"""Types of approval decisions."""
APPROVED = "APPROVED"
REJECTED = "REJECTED"
class PublicationStatus(str, Enum):
"""Status of a publication record."""
ACTIVE = "ACTIVE"
REVOKED = "REVOKED"
class ComplianceStageName(str, Enum):
"""Canonical names for compliance stages."""
DATA_PURITY = "DATA_PURITY"
INTERNAL_SOURCES_ONLY = "INTERNAL_SOURCES_ONLY"
NO_EXTERNAL_ENDPOINTS = "NO_EXTERNAL_ENDPOINTS"
MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY"
class ClassificationType(str, Enum):
"""Classification types for artifacts."""
REQUIRED_SYSTEM = "required-system"
ALLOWED = "allowed"
EXCLUDED_PROHIBITED = "excluded-prohibited"
class ViolationSeverity(str, Enum):
"""Severity levels for compliance violations."""
CRITICAL = "CRITICAL"
MAJOR = "MAJOR"
MINOR = "MINOR"
class ViolationCategory(str, Enum):
"""Categories for compliance violations."""
DATA_PURITY = "DATA_PURITY"
SOURCE_ISOLATION = "SOURCE_ISOLATION"
MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY"
EXTERNAL_ENDPOINT = "EXTERNAL_ENDPOINT"
# [/DEF:clean_release_enums:Module]

View File

@@ -0,0 +1,38 @@
# [DEF:clean_release_exceptions:Module]
# @TIER: STANDARD
# @PURPOSE: Domain exceptions for clean release compliance subsystem.
# @LAYER: Domain
class CleanReleaseError(Exception):
"""Base exception for clean release subsystem."""
pass
class CandidateNotFoundError(CleanReleaseError):
"""Raised when a release candidate is not found."""
pass
class IllegalTransitionError(CleanReleaseError):
"""Raised when a forbidden lifecycle transition is attempted."""
pass
class ManifestImmutableError(CleanReleaseError):
"""Raised when an attempt is made to mutate an existing manifest."""
pass
class PolicyResolutionError(CleanReleaseError):
"""Raised when trusted policy or registry cannot be resolved."""
pass
class ComplianceRunError(CleanReleaseError):
"""Raised when a compliance run fails or is invalid."""
pass
class ApprovalGateError(CleanReleaseError):
"""Raised when approval requirements are not met."""
pass
class PublicationGateError(CleanReleaseError):
"""Raised when publication requirements are not met."""
pass
# [/DEF:clean_release_exceptions:Module]

View File

@@ -0,0 +1,122 @@
# [DEF:clean_release_facade:Module]
# @TIER: STANDARD
# @PURPOSE: Unified entry point for clean release operations.
# @LAYER: Application
from typing import List, Optional
from backend.src.services.clean_release.repositories import (
CandidateRepository, ArtifactRepository, ManifestRepository,
PolicyRepository, ComplianceRepository, ReportRepository,
ApprovalRepository, PublicationRepository, AuditRepository
)
from backend.src.services.clean_release.dto import (
CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,
ReportDTO, CandidateOverviewDTO
)
from backend.src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision
from backend.src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
from backend.src.core.logger import belief_scope
from backend.src.core.config_manager import ConfigManager
class CleanReleaseFacade:
"""
@PURPOSE: Orchestrates repositories and services to provide a clean API for UI/CLI.
"""
def __init__(
self,
candidate_repo: CandidateRepository,
artifact_repo: ArtifactRepository,
manifest_repo: ManifestRepository,
policy_repo: PolicyRepository,
compliance_repo: ComplianceRepository,
report_repo: ReportRepository,
approval_repo: ApprovalRepository,
publication_repo: PublicationRepository,
audit_repo: AuditRepository,
config_manager: ConfigManager
):
self.candidate_repo = candidate_repo
self.artifact_repo = artifact_repo
self.manifest_repo = manifest_repo
self.policy_repo = policy_repo
self.compliance_repo = compliance_repo
self.report_repo = report_repo
self.approval_repo = approval_repo
self.publication_repo = publication_repo
self.audit_repo = audit_repo
self.config_manager = config_manager
def resolve_active_policy_snapshot(self) -> Optional[CleanPolicySnapshot]:
"""
@PURPOSE: Resolve the active policy snapshot based on ConfigManager.
"""
with belief_scope("CleanReleaseFacade.resolve_active_policy_snapshot"):
config = self.config_manager.get_config()
policy_id = config.settings.clean_release.active_policy_id
if not policy_id:
return None
return self.policy_repo.get_policy_snapshot(policy_id)
def resolve_active_registry_snapshot(self) -> Optional[SourceRegistrySnapshot]:
"""
@PURPOSE: Resolve the active registry snapshot based on ConfigManager.
"""
with belief_scope("CleanReleaseFacade.resolve_active_registry_snapshot"):
config = self.config_manager.get_config()
registry_id = config.settings.clean_release.active_registry_id
if not registry_id:
return None
return self.policy_repo.get_registry_snapshot(registry_id)
def get_candidate_overview(self, candidate_id: str) -> Optional[CandidateOverviewDTO]:
"""
@PURPOSE: Build a comprehensive overview for a candidate.
"""
with belief_scope("CleanReleaseFacade.get_candidate_overview"):
candidate = self.candidate_repo.get_by_id(candidate_id)
if not candidate:
return None
manifest = self.manifest_repo.get_latest_for_candidate(candidate_id)
runs = self.compliance_repo.list_runs_by_candidate(candidate_id)
latest_run = runs[-1] if runs else None
report = None
if latest_run:
report = self.report_repo.get_by_run(latest_run.id)
approval = self.approval_repo.get_latest_for_candidate(candidate_id)
publication = self.publication_repo.get_latest_for_candidate(candidate_id)
active_policy = self.resolve_active_policy_snapshot()
active_registry = self.resolve_active_registry_snapshot()
return CandidateOverviewDTO(
candidate_id=candidate.id,
version=candidate.version,
source_snapshot_ref=candidate.source_snapshot_ref,
status=CandidateStatus(candidate.status),
latest_manifest_id=manifest.id if manifest else None,
latest_manifest_digest=manifest.manifest_digest if 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=report.id if report else None,
latest_report_final_status=ComplianceDecision(report.final_status) if report else None,
latest_policy_snapshot_id=active_policy.id if active_policy else None,
latest_policy_version=active_policy.policy_version if active_policy else None,
latest_registry_snapshot_id=active_registry.id if active_registry else None,
latest_registry_version=active_registry.registry_version if active_registry else None,
latest_approval_decision=approval.decision if approval else None,
latest_publication_id=publication.id if publication else None,
latest_publication_status=publication.status if publication else None
)
def list_candidates(self) -> List[CandidateOverviewDTO]:
"""
@PURPOSE: List all candidates with their current status.
"""
with belief_scope("CleanReleaseFacade.list_candidates"):
candidates = self.candidate_repo.list_all()
return [self.get_candidate_overview(c.id) for c in candidates]
# [/DEF:clean_release_facade:Module]

View File

@@ -78,7 +78,6 @@ def build_distribution_manifest(
return DistributionManifest(
manifest_id=manifest_id,
candidate_id=candidate_id,
policy_id=policy_id,
generated_at=datetime.now(timezone.utc),
generated_by=generated_by,
items=items,
@@ -86,4 +85,25 @@ def build_distribution_manifest(
deterministic_hash=deterministic_hash,
)
# [/DEF:build_distribution_manifest:Function]
# [DEF:build_manifest:Function]
# @PURPOSE: Legacy compatibility wrapper for old manifest builder import paths.
# @PRE: Same as build_distribution_manifest.
# @POST: Returns DistributionManifest produced by canonical builder.
def build_manifest(
manifest_id: str,
candidate_id: str,
policy_id: str,
generated_by: str,
artifacts: Iterable[Dict[str, Any]],
) -> DistributionManifest:
return build_distribution_manifest(
manifest_id=manifest_id,
candidate_id=candidate_id,
policy_id=policy_id,
generated_by=generated_by,
artifacts=artifacts,
)
# [/DEF:build_manifest:Function]
# [/DEF:backend.src.services.clean_release.manifest_builder:Module]

View File

@@ -0,0 +1,88 @@
# [DEF:backend.src.services.clean_release.manifest_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, manifest, versioning, immutability, lifecycle
# @PURPOSE: Build immutable distribution manifests with deterministic digest and version increment.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.manifest_builder
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @PRE: Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.
# @POST: New immutable manifest is persisted with incremented version and deterministic digest.
# @INVARIANT: Existing manifests are never mutated.
from __future__ import annotations
from typing import Any, Dict, List
from ...models.clean_release import DistributionManifest
from .enums import CandidateStatus
from .manifest_builder import build_distribution_manifest
from .repository import CleanReleaseRepository
# [DEF:build_manifest_snapshot:Function]
# @PURPOSE: Create a new immutable manifest version for a candidate.
# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.
# @POST: Returns persisted DistributionManifest with monotonically incremented version.
def build_manifest_snapshot(
repository: CleanReleaseRepository,
candidate_id: str,
created_by: str,
policy_id: str = "policy-default",
) -> DistributionManifest:
if not candidate_id or not candidate_id.strip():
raise ValueError("candidate_id must be non-empty")
if not created_by or not created_by.strip():
raise ValueError("created_by must be non-empty")
candidate = repository.get_candidate(candidate_id)
if candidate is None:
raise ValueError(f"candidate '{candidate_id}' not found")
if candidate.status not in {CandidateStatus.PREPARED.value, CandidateStatus.MANIFEST_BUILT.value}:
raise ValueError("candidate must be PREPARED or MANIFEST_BUILT to build manifest")
artifacts = repository.get_artifacts_by_candidate(candidate_id)
if not artifacts:
raise ValueError("candidate artifacts are required to build manifest")
existing = repository.get_manifests_by_candidate(candidate_id)
for manifest in existing:
if not manifest.immutable:
raise ValueError("existing manifest immutability invariant violated")
next_version = max((m.manifest_version for m in existing), default=0) + 1
manifest_id = f"manifest-{candidate_id}-v{next_version}"
classified_artifacts: List[Dict[str, Any]] = [
{
"path": artifact.path,
"category": artifact.detected_category or "generic",
"classification": "allowed",
"reason": "artifact import",
"checksum": artifact.sha256,
}
for artifact in artifacts
]
manifest = build_distribution_manifest(
manifest_id=manifest_id,
candidate_id=candidate_id,
policy_id=policy_id,
generated_by=created_by,
artifacts=classified_artifacts,
)
manifest.manifest_version = next_version
manifest.source_snapshot_ref = candidate.source_snapshot_ref
manifest.artifacts_digest = manifest.manifest_digest
manifest.immutable = True
repository.save_manifest(manifest)
if candidate.status == CandidateStatus.PREPARED.value:
candidate.transition_to(CandidateStatus.MANIFEST_BUILT)
repository.save_candidate(candidate)
return manifest
# [/DEF:build_manifest_snapshot:Function]
# [/DEF:backend.src.services.clean_release.manifest_service:Module]

View File

@@ -0,0 +1,67 @@
# [DEF:clean_release_mappers:Module]
# @TIER: STANDARD
# @PURPOSE: Map between domain entities (SQLAlchemy models) and DTOs.
# @LAYER: Application
from typing import List
from backend.src.models.clean_release import (
ReleaseCandidate, DistributionManifest, ComplianceRun,
ComplianceStageRun, ComplianceViolation, ComplianceReport,
CleanPolicySnapshot, SourceRegistrySnapshot, ApprovalDecision,
PublicationRecord
)
from backend.src.services.clean_release.dto import (
CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,
ReportDTO
)
from backend.src.services.clean_release.enums import (
CandidateStatus, RunStatus, ComplianceDecision,
ViolationSeverity, ViolationCategory
)
def map_candidate_to_dto(candidate: ReleaseCandidate) -> CandidateDTO:
return CandidateDTO(
id=candidate.id,
version=candidate.version,
source_snapshot_ref=candidate.source_snapshot_ref,
build_id=candidate.build_id,
created_at=candidate.created_at,
created_by=candidate.created_by,
status=CandidateStatus(candidate.status)
)
def map_manifest_to_dto(manifest: DistributionManifest) -> ManifestDTO:
return ManifestDTO(
id=manifest.id,
candidate_id=manifest.candidate_id,
manifest_version=manifest.manifest_version,
manifest_digest=manifest.manifest_digest,
artifacts_digest=manifest.artifacts_digest,
created_at=manifest.created_at,
created_by=manifest.created_by,
source_snapshot_ref=manifest.source_snapshot_ref,
content_json=manifest.content_json or {}
)
def map_run_to_dto(run: ComplianceRun) -> ComplianceRunDTO:
return ComplianceRunDTO(
run_id=run.id,
candidate_id=run.candidate_id,
status=RunStatus(run.status),
final_status=ComplianceDecision(run.final_status) if run.final_status else None,
task_id=run.task_id
)
def map_report_to_dto(report: ComplianceReport) -> ReportDTO:
# Note: ReportDTO in dto.py is a compact view
return ReportDTO(
report_id=report.id,
candidate_id=report.candidate_id,
final_status=ComplianceDecision(report.final_status),
policy_version="unknown", # Would need to resolve from run/snapshot
manifest_digest="unknown", # Would need to resolve from run/manifest
violation_count=0, # Would need to resolve from violations
generated_at=report.generated_at
)
# [/DEF:clean_release_mappers:Module]

View File

@@ -13,7 +13,7 @@ from dataclasses import dataclass
from typing import Dict, Iterable, List, Tuple
from ...core.logger import belief_scope, logger
from ...models.clean_release import CleanProfilePolicy, ResourceSourceRegistry
from ...models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
@dataclass
@@ -34,12 +34,12 @@ class SourceValidationResult:
# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult
# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True
# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: missing_registry_ref -> policy has empty internal_source_registry_ref
# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id
# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id
# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries
# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]
class CleanPolicyEngine:
def __init__(self, policy: CleanProfilePolicy, registry: ResourceSourceRegistry):
def __init__(self, policy: CleanPolicySnapshot, registry: SourceRegistrySnapshot):
self.policy = policy
self.registry = registry
@@ -48,28 +48,39 @@ class CleanPolicyEngine:
logger.reason("Validating enterprise-clean policy and internal registry consistency")
reasons: List[str] = []
if not self.policy.active:
reasons.append("Policy must be active")
if not self.policy.internal_source_registry_ref.strip():
reasons.append("Policy missing internal_source_registry_ref")
if self.policy.profile.value == "enterprise-clean" and not self.policy.prohibited_artifact_categories:
reasons.append("Enterprise policy requires prohibited artifact categories")
if self.policy.profile.value == "enterprise-clean" and not self.policy.external_source_forbidden:
reasons.append("Enterprise policy requires external_source_forbidden=true")
if self.registry.registry_id != self.policy.internal_source_registry_ref:
# Snapshots are immutable and assumed active if resolved by facade
if not self.policy.registry_snapshot_id.strip():
reasons.append("Policy missing registry_snapshot_id")
content = self.policy.content_json or {}
profile = content.get("profile", "standard")
if profile == "enterprise-clean":
if not content.get("prohibited_artifact_categories"):
reasons.append("Enterprise policy requires prohibited artifact categories")
if not content.get("external_source_forbidden"):
reasons.append("Enterprise policy requires external_source_forbidden=true")
if self.registry.id != self.policy.registry_snapshot_id:
reasons.append("Policy registry ref does not match provided registry")
if not self.registry.entries:
reasons.append("Registry must contain entries")
if not self.registry.allowed_hosts:
reasons.append("Registry must contain allowed hosts")
logger.reflect(f"Policy validation completed. blocking_reasons={len(reasons)}")
return PolicyValidationResult(ok=len(reasons) == 0, blocking_reasons=reasons)
def classify_artifact(self, artifact: Dict) -> str:
category = (artifact.get("category") or "").strip()
if category in self.policy.required_system_categories:
content = self.policy.content_json or {}
required = content.get("required_system_categories", [])
prohibited = content.get("prohibited_artifact_categories", [])
if category in required:
logger.reason(f"Artifact category '{category}' classified as required-system")
return "required-system"
if category in self.policy.prohibited_artifact_categories:
if category in prohibited:
logger.reason(f"Artifact category '{category}' classified as excluded-prohibited")
return "excluded-prohibited"
logger.reflect(f"Artifact category '{category}' classified as allowed")
@@ -89,7 +100,7 @@ class CleanPolicyEngine:
},
)
allowed_hosts = {entry.host for entry in self.registry.entries if entry.enabled}
allowed_hosts = set(self.registry.allowed_hosts or [])
normalized = endpoint.strip().lower()
if normalized in allowed_hosts:

View File

@@ -0,0 +1,64 @@
# [DEF:backend.src.services.clean_release.policy_resolution_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, policy, registry, trusted-resolution, immutable-snapshots
# @PURPOSE: Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.core.config_manager
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.exceptions
# @INVARIANT: Trusted snapshot resolution is based only on ConfigManager active identifiers.
from __future__ import annotations
from typing import Optional, Tuple
from ...models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
from .exceptions import PolicyResolutionError
from .repository import CleanReleaseRepository
# [DEF:resolve_trusted_policy_snapshots:Function]
# @PURPOSE: Resolve immutable trusted policy and registry snapshots using active config IDs only.
# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.
# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.
# @SIDE_EFFECT: None.
def resolve_trusted_policy_snapshots(
*,
config_manager,
repository: CleanReleaseRepository,
policy_id_override: Optional[str] = None,
registry_id_override: Optional[str] = None,
) -> Tuple[CleanPolicySnapshot, SourceRegistrySnapshot]:
if policy_id_override is not None or registry_id_override is not None:
raise PolicyResolutionError("override attempt is forbidden for trusted policy resolution")
config = config_manager.get_config()
clean_release_settings = getattr(getattr(config, "settings", None), "clean_release", None)
if clean_release_settings is None:
raise PolicyResolutionError("clean_release settings are missing")
policy_id = getattr(clean_release_settings, "active_policy_id", None)
registry_id = getattr(clean_release_settings, "active_registry_id", None)
if not policy_id:
raise PolicyResolutionError("missing trusted profile: active_policy_id is not configured")
if not registry_id:
raise PolicyResolutionError("missing trusted registry: active_registry_id is not configured")
policy_snapshot = repository.get_policy(policy_id)
if policy_snapshot is None:
raise PolicyResolutionError(f"trusted policy snapshot '{policy_id}' was not found")
registry_snapshot = repository.get_registry(registry_id)
if registry_snapshot is None:
raise PolicyResolutionError(f"trusted registry snapshot '{registry_id}' was not found")
if not bool(getattr(policy_snapshot, "immutable", False)):
raise PolicyResolutionError("policy snapshot must be immutable")
if not bool(getattr(registry_snapshot, "immutable", False)):
raise PolicyResolutionError("registry snapshot must be immutable")
return policy_snapshot, registry_snapshot
# [/DEF:resolve_trusted_policy_snapshots:Function]
# [/DEF:backend.src.services.clean_release.policy_resolution_service:Module]

View File

@@ -16,7 +16,7 @@ from typing import Dict, Iterable
from .manifest_builder import build_distribution_manifest
from .policy_engine import CleanPolicyEngine
from .repository import CleanReleaseRepository
from ...models.clean_release import ReleaseCandidateStatus
from .enums import CandidateStatus
def prepare_candidate(
@@ -34,7 +34,7 @@ def prepare_candidate(
if policy is None:
raise ValueError("Active clean policy not found")
registry = repository.get_registry(policy.internal_source_registry_ref)
registry = repository.get_registry(policy.registry_snapshot_id)
if registry is None:
raise ValueError("Registry not found for active policy")
@@ -54,14 +54,39 @@ def prepare_candidate(
)
repository.save_manifest(manifest)
candidate.status = ReleaseCandidateStatus.BLOCKED if violations else ReleaseCandidateStatus.PREPARED
# Note: In the new model, BLOCKED is a ComplianceDecision, not a CandidateStatus.
# CandidateStatus.PREPARED is the correct next state after preparation.
candidate.transition_to(CandidateStatus.PREPARED)
repository.save_candidate(candidate)
status_value = candidate.status.value if hasattr(candidate.status, "value") else str(candidate.status)
manifest_id_value = getattr(manifest, "manifest_id", None) or getattr(manifest, "id", "")
return {
"candidate_id": candidate_id,
"status": candidate.status.value,
"manifest_id": manifest.manifest_id,
"status": status_value,
"manifest_id": manifest_id_value,
"violations": violations,
"prepared_at": datetime.now(timezone.utc).isoformat(),
}
# [DEF:prepare_candidate_legacy:Function]
# @PURPOSE: Legacy compatibility wrapper kept for migration period.
# @PRE: Same as prepare_candidate.
# @POST: Delegates to canonical prepare_candidate and preserves response shape.
def prepare_candidate_legacy(
repository: CleanReleaseRepository,
candidate_id: str,
artifacts: Iterable[Dict],
sources: Iterable[str],
operator_id: str,
) -> Dict:
return prepare_candidate(
repository=repository,
candidate_id=candidate_id,
artifacts=artifacts,
sources=sources,
operator_id=operator_id,
)
# [/DEF:prepare_candidate_legacy:Function]
# [/DEF:backend.src.services.clean_release.preparation_service:Module]

View File

@@ -0,0 +1,173 @@
# [DEF:backend.src.services.clean_release.publication_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: clean-release, publication, revoke, gate, lifecycle
# @PURPOSE: Enforce publication and revocation gates with append-only publication records.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.approval_service
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.audit_service
# @INVARIANT: Publication records are append-only snapshots; revoke mutates only publication status for targeted record.
from __future__ import annotations
from datetime import datetime, timezone
from typing import List
from uuid import uuid4
from ...core.logger import belief_scope, logger
from ...models.clean_release import PublicationRecord
from .audit_service import audit_preparation
from .enums import ApprovalDecisionType, CandidateStatus, PublicationStatus
from .exceptions import PublicationGateError
from .repository import CleanReleaseRepository
# [DEF:_get_or_init_publications_store:Function]
# @PURPOSE: Provide in-memory append-only publication storage.
# @PRE: repository is initialized.
# @POST: Returns publication list attached to repository.
def _get_or_init_publications_store(repository: CleanReleaseRepository) -> List[PublicationRecord]:
publications = getattr(repository, "publication_records", None)
if publications is None:
publications = []
setattr(repository, "publication_records", publications)
return publications
# [/DEF:_get_or_init_publications_store:Function]
# [DEF:_latest_publication_for_candidate:Function]
# @PURPOSE: Resolve latest publication record for candidate.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest record or None.
def _latest_publication_for_candidate(
repository: CleanReleaseRepository,
candidate_id: str,
) -> PublicationRecord | None:
records = [item for item in _get_or_init_publications_store(repository) if item.candidate_id == candidate_id]
if not records:
return None
return sorted(records, key=lambda item: item.published_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)[0]
# [/DEF:_latest_publication_for_candidate:Function]
# [DEF:_latest_approval_for_candidate:Function]
# @PURPOSE: Resolve latest approval decision from repository decision store.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest decision object or None.
def _latest_approval_for_candidate(repository: CleanReleaseRepository, candidate_id: str):
decisions = getattr(repository, "approval_decisions", [])
scoped = [item for item in decisions if item.candidate_id == candidate_id]
if not scoped:
return None
return sorted(scoped, key=lambda item: item.decided_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True)[0]
# [/DEF:_latest_approval_for_candidate:Function]
# [DEF:publish_candidate:Function]
# @PURPOSE: Create immutable publication record for approved candidate.
# @PRE: Candidate exists, report belongs to candidate, latest approval is APPROVED.
# @POST: New ACTIVE publication record is appended.
def publish_candidate(
*,
repository: CleanReleaseRepository,
candidate_id: str,
report_id: str,
published_by: str,
target_channel: str,
publication_ref: str | None = None,
) -> PublicationRecord:
with belief_scope("publication_service.publish_candidate"):
logger.reason(f"[REASON] Evaluating publish gate candidate_id={candidate_id} report_id={report_id}")
if not published_by or not published_by.strip():
raise PublicationGateError("published_by must be non-empty")
if not target_channel or not target_channel.strip():
raise PublicationGateError("target_channel must be non-empty")
candidate = repository.get_candidate(candidate_id)
if candidate is None:
raise PublicationGateError(f"candidate '{candidate_id}' not found")
report = repository.get_report(report_id)
if report is None:
raise PublicationGateError(f"report '{report_id}' not found")
if report.candidate_id != candidate_id:
raise PublicationGateError("report belongs to another candidate")
latest_approval = _latest_approval_for_candidate(repository, candidate_id)
if latest_approval is None or latest_approval.decision != ApprovalDecisionType.APPROVED.value:
raise PublicationGateError("publish requires APPROVED decision")
latest_publication = _latest_publication_for_candidate(repository, candidate_id)
if latest_publication is not None and latest_publication.status == PublicationStatus.ACTIVE.value:
raise PublicationGateError("candidate already has active publication")
if candidate.status == CandidateStatus.APPROVED.value:
try:
candidate.transition_to(CandidateStatus.PUBLISHED)
repository.save_candidate(candidate)
except Exception as exc: # noqa: BLE001
logger.explore(f"[EXPLORE] Candidate transition to PUBLISHED failed candidate_id={candidate_id}: {exc}")
raise PublicationGateError(str(exc)) from exc
record = PublicationRecord(
id=f"pub-{uuid4()}",
candidate_id=candidate_id,
report_id=report_id,
published_by=published_by,
published_at=datetime.now(timezone.utc),
target_channel=target_channel,
publication_ref=publication_ref,
status=PublicationStatus.ACTIVE.value,
)
_get_or_init_publications_store(repository).append(record)
audit_preparation(candidate_id, "PUBLISHED", repository=repository, actor=published_by)
logger.reflect(f"[REFLECT] Publication persisted candidate_id={candidate_id} publication_id={record.id}")
return record
# [/DEF:publish_candidate:Function]
# [DEF:revoke_publication:Function]
# @PURPOSE: Revoke existing publication record without deleting history.
# @PRE: publication_id exists in repository publication store.
# @POST: Target publication status becomes REVOKED and updated record is returned.
def revoke_publication(
*,
repository: CleanReleaseRepository,
publication_id: str,
revoked_by: str,
comment: str | None = None,
) -> PublicationRecord:
with belief_scope("publication_service.revoke_publication"):
logger.reason(f"[REASON] Evaluating revoke gate publication_id={publication_id}")
if not revoked_by or not revoked_by.strip():
raise PublicationGateError("revoked_by must be non-empty")
if not publication_id or not publication_id.strip():
raise PublicationGateError("publication_id must be non-empty")
records = _get_or_init_publications_store(repository)
record = next((item for item in records if item.id == publication_id), None)
if record is None:
raise PublicationGateError(f"publication '{publication_id}' not found")
if record.status == PublicationStatus.REVOKED.value:
raise PublicationGateError("publication is already revoked")
record.status = PublicationStatus.REVOKED.value
candidate = repository.get_candidate(record.candidate_id)
if candidate is not None:
# Lifecycle remains publication-driven; republish after revoke is supported by new publication record.
repository.save_candidate(candidate)
audit_preparation(
record.candidate_id,
f"REVOKED:{comment or ''}".strip(":"),
repository=repository,
actor=revoked_by,
)
logger.reflect(f"[REFLECT] Publication revoked publication_id={publication_id}")
return record
# [/DEF:revoke_publication:Function]
# [/DEF:backend.src.services.clean_release.publication_service:Module]

View File

@@ -19,7 +19,8 @@ from datetime import datetime, timezone
from uuid import uuid4
from typing import List
from ...models.clean_release import CheckFinalStatus, ComplianceCheckRun, ComplianceReport, ComplianceViolation
from .enums import RunStatus, ComplianceDecision
from ...models.clean_release import ComplianceRun, ComplianceReport, ComplianceViolation
from .repository import CleanReleaseRepository
@@ -27,32 +28,39 @@ class ComplianceReportBuilder:
def __init__(self, repository: CleanReleaseRepository):
self.repository = repository
def build_report_payload(self, check_run: ComplianceCheckRun, violations: List[ComplianceViolation]) -> ComplianceReport:
if check_run.final_status == CheckFinalStatus.RUNNING:
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 v.blocked_release)
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 == CheckFinalStatus.BLOCKED and blocking_violations_count <= 0:
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 == CheckFinalStatus.COMPLIANT
if check_run.final_status == ComplianceDecision.PASSED
else f"Blocked with {blocking_violations_count} blocking violation(s)"
)
return ComplianceReport(
report_id=f"CCR-{uuid4()}",
check_run_id=check_run.check_run_id,
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,
operator_summary=summary,
structured_payload_ref=f"inmemory://check-runs/{check_run.check_run_id}/report",
violations_count=violations_count,
blocking_violations_count=blocking_violations_count,
summary_json={
"operator_summary": summary,
"violations_count": violations_count,
"blocking_violations_count": blocking_violations_count,
},
immutable=True,
)
def persist_report(self, report: ComplianceReport) -> ComplianceReport:

View File

@@ -0,0 +1,28 @@
# [DEF:clean_release_repositories:Module]
# @TIER: STANDARD
# @PURPOSE: Export all clean release repositories.
from .candidate_repository import CandidateRepository
from .artifact_repository import ArtifactRepository
from .manifest_repository import ManifestRepository
from .policy_repository import PolicyRepository
from .compliance_repository import ComplianceRepository
from .report_repository import ReportRepository
from .approval_repository import ApprovalRepository
from .publication_repository import PublicationRepository
from .audit_repository import AuditRepository, CleanReleaseAuditLog
__all__ = [
"CandidateRepository",
"ArtifactRepository",
"ManifestRepository",
"PolicyRepository",
"ComplianceRepository",
"ReportRepository",
"ApprovalRepository",
"PublicationRepository",
"AuditRepository",
"CleanReleaseAuditLog"
]
# [/DEF:clean_release_repositories:Module]

View File

@@ -0,0 +1,53 @@
# [DEF:approval_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query approval decisions.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import ApprovalDecision
from backend.src.core.logger import belief_scope
class ApprovalRepository:
"""
@PURPOSE: Encapsulates database operations for ApprovalDecision.
"""
def __init__(self, db: Session):
self.db = db
def save(self, decision: ApprovalDecision) -> ApprovalDecision:
"""
@PURPOSE: Persist an approval decision.
@POST: Decision is committed and refreshed.
"""
with belief_scope("ApprovalRepository.save"):
self.db.add(decision)
self.db.commit()
self.db.refresh(decision)
return decision
def get_by_id(self, decision_id: str) -> Optional[ApprovalDecision]:
"""
@PURPOSE: Retrieve a decision by ID.
"""
with belief_scope("ApprovalRepository.get_by_id"):
return self.db.query(ApprovalDecision).filter(ApprovalDecision.id == decision_id).first()
def get_latest_for_candidate(self, candidate_id: str) -> Optional[ApprovalDecision]:
"""
@PURPOSE: Retrieve the latest decision for a candidate.
"""
with belief_scope("ApprovalRepository.get_latest_for_candidate"):
return self.db.query(ApprovalDecision)\
.filter(ApprovalDecision.candidate_id == candidate_id)\
.order_by(ApprovalDecision.decided_at.desc())\
.first()
def list_by_candidate(self, candidate_id: str) -> List[ApprovalDecision]:
"""
@PURPOSE: List all decisions for a specific candidate.
"""
with belief_scope("ApprovalRepository.list_by_candidate"):
return self.db.query(ApprovalDecision).filter(ApprovalDecision.candidate_id == candidate_id).all()
# [/DEF:approval_repository:Module]

View File

@@ -0,0 +1,54 @@
# [DEF:artifact_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query candidate artifacts.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import CandidateArtifact
from backend.src.core.logger import belief_scope
class ArtifactRepository:
"""
@PURPOSE: Encapsulates database operations for CandidateArtifact.
"""
def __init__(self, db: Session):
self.db = db
def save(self, artifact: CandidateArtifact) -> CandidateArtifact:
"""
@PURPOSE: Persist an artifact.
@POST: Artifact is committed and refreshed.
"""
with belief_scope("ArtifactRepository.save"):
self.db.add(artifact)
self.db.commit()
self.db.refresh(artifact)
return artifact
def save_all(self, artifacts: List[CandidateArtifact]) -> List[CandidateArtifact]:
"""
@PURPOSE: Persist multiple artifacts in a single transaction.
"""
with belief_scope("ArtifactRepository.save_all"):
self.db.add_all(artifacts)
self.db.commit()
for artifact in artifacts:
self.db.refresh(artifact)
return artifacts
def get_by_id(self, artifact_id: str) -> Optional[CandidateArtifact]:
"""
@PURPOSE: Retrieve an artifact by ID.
"""
with belief_scope("ArtifactRepository.get_by_id"):
return self.db.query(CandidateArtifact).filter(CandidateArtifact.id == artifact_id).first()
def list_by_candidate(self, candidate_id: str) -> List[CandidateArtifact]:
"""
@PURPOSE: List all artifacts for a specific candidate.
"""
with belief_scope("ArtifactRepository.list_by_candidate"):
return self.db.query(CandidateArtifact).filter(CandidateArtifact.candidate_id == candidate_id).all()
# [/DEF:artifact_repository:Module]

View File

@@ -0,0 +1,46 @@
# [DEF:audit_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query audit logs for clean release operations.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from sqlalchemy import Column, String, DateTime, JSON
from backend.src.models.mapping import Base
from backend.src.core.logger import belief_scope
from datetime import datetime
import uuid
from backend.src.models.clean_release import CleanReleaseAuditLog
class AuditRepository:
"""
@PURPOSE: Encapsulates database operations for CleanReleaseAuditLog.
"""
def __init__(self, db: Session):
self.db = db
def log(self, action: str, actor: str, candidate_id: Optional[str] = None, details: Optional[dict] = None) -> CleanReleaseAuditLog:
"""
@PURPOSE: Create an audit log entry.
"""
with belief_scope("AuditRepository.log"):
entry = CleanReleaseAuditLog(
action=action,
actor=actor,
candidate_id=candidate_id,
details_json=details or {}
)
self.db.add(entry)
self.db.commit()
self.db.refresh(entry)
return entry
def list_by_candidate(self, candidate_id: str) -> List[CleanReleaseAuditLog]:
"""
@PURPOSE: List all audit entries for a specific candidate.
"""
with belief_scope("AuditRepository.list_by_candidate"):
return self.db.query(CleanReleaseAuditLog).filter(CleanReleaseAuditLog.candidate_id == candidate_id).all()
# [/DEF:audit_repository:Module]

View File

@@ -0,0 +1,47 @@
# [DEF:candidate_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query release candidates.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import ReleaseCandidate
from backend.src.core.logger import belief_scope
class CandidateRepository:
"""
@PURPOSE: Encapsulates database operations for ReleaseCandidate.
"""
def __init__(self, db: Session):
self.db = db
def save(self, candidate: ReleaseCandidate) -> ReleaseCandidate:
"""
@PURPOSE: Persist a release candidate.
@POST: Candidate is committed and refreshed.
"""
with belief_scope("CandidateRepository.save"):
# [REASON] Using merge to handle both create and update.
# Note: In a real implementation, we might want to use a separate DB model
# if the domain model differs significantly from the DB schema.
# For now, we assume the domain model is compatible with SQLAlchemy Base if registered.
self.db.add(candidate)
self.db.commit()
self.db.refresh(candidate)
return candidate
def get_by_id(self, candidate_id: str) -> Optional[ReleaseCandidate]:
"""
@PURPOSE: Retrieve a candidate by ID.
"""
with belief_scope("CandidateRepository.get_by_id"):
return self.db.query(ReleaseCandidate).filter(ReleaseCandidate.id == candidate_id).first()
def list_all(self) -> List[ReleaseCandidate]:
"""
@PURPOSE: List all candidates.
"""
with belief_scope("CandidateRepository.list_all"):
return self.db.query(ReleaseCandidate).all()
# [/DEF:candidate_repository:Module]

View File

@@ -0,0 +1,87 @@
# [DEF:compliance_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query compliance runs, stage runs, and violations.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import ComplianceRun, ComplianceStageRun, ComplianceViolation
from backend.src.core.logger import belief_scope
class ComplianceRepository:
"""
@PURPOSE: Encapsulates database operations for Compliance execution records.
"""
def __init__(self, db: Session):
self.db = db
def save_run(self, run: ComplianceRun) -> ComplianceRun:
"""
@PURPOSE: Persist a compliance run.
"""
with belief_scope("ComplianceRepository.save_run"):
self.db.add(run)
self.db.commit()
self.db.refresh(run)
return run
def get_run(self, run_id: str) -> Optional[ComplianceRun]:
"""
@PURPOSE: Retrieve a compliance run by ID.
"""
with belief_scope("ComplianceRepository.get_run"):
return self.db.query(ComplianceRun).filter(ComplianceRun.id == run_id).first()
def list_runs_by_candidate(self, candidate_id: str) -> List[ComplianceRun]:
"""
@PURPOSE: List all runs for a specific candidate.
"""
with belief_scope("ComplianceRepository.list_runs_by_candidate"):
return self.db.query(ComplianceRun).filter(ComplianceRun.candidate_id == candidate_id).all()
def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:
"""
@PURPOSE: Persist a stage execution record.
"""
with belief_scope("ComplianceRepository.save_stage_run"):
self.db.add(stage_run)
self.db.commit()
self.db.refresh(stage_run)
return stage_run
def list_stages_by_run(self, run_id: str) -> List[ComplianceStageRun]:
"""
@PURPOSE: List all stage runs for a specific compliance run.
"""
with belief_scope("ComplianceRepository.list_stages_by_run"):
return self.db.query(ComplianceStageRun).filter(ComplianceStageRun.run_id == run_id).all()
def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:
"""
@PURPOSE: Persist a compliance violation.
"""
with belief_scope("ComplianceRepository.save_violation"):
self.db.add(violation)
self.db.commit()
self.db.refresh(violation)
return violation
def save_violations(self, violations: List[ComplianceViolation]) -> List[ComplianceViolation]:
"""
@PURPOSE: Persist multiple violations.
"""
with belief_scope("ComplianceRepository.save_violations"):
self.db.add_all(violations)
self.db.commit()
for v in violations:
self.db.refresh(v)
return violations
def list_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:
"""
@PURPOSE: List all violations for a specific compliance run.
"""
with belief_scope("ComplianceRepository.list_violations_by_run"):
return self.db.query(ComplianceViolation).filter(ComplianceViolation.run_id == run_id).all()
# [/DEF:compliance_repository:Module]

View File

@@ -0,0 +1,53 @@
# [DEF:manifest_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query distribution manifests.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import DistributionManifest
from backend.src.core.logger import belief_scope
class ManifestRepository:
"""
@PURPOSE: Encapsulates database operations for DistributionManifest.
"""
def __init__(self, db: Session):
self.db = db
def save(self, manifest: DistributionManifest) -> DistributionManifest:
"""
@PURPOSE: Persist a manifest.
@POST: Manifest is committed and refreshed.
"""
with belief_scope("ManifestRepository.save"):
self.db.add(manifest)
self.db.commit()
self.db.refresh(manifest)
return manifest
def get_by_id(self, manifest_id: str) -> Optional[DistributionManifest]:
"""
@PURPOSE: Retrieve a manifest by ID.
"""
with belief_scope("ManifestRepository.get_by_id"):
return self.db.query(DistributionManifest).filter(DistributionManifest.id == manifest_id).first()
def get_latest_for_candidate(self, candidate_id: str) -> Optional[DistributionManifest]:
"""
@PURPOSE: Retrieve the latest manifest for a candidate.
"""
with belief_scope("ManifestRepository.get_latest_for_candidate"):
return self.db.query(DistributionManifest)\
.filter(DistributionManifest.candidate_id == candidate_id)\
.order_by(DistributionManifest.manifest_version.desc())\
.first()
def list_by_candidate(self, candidate_id: str) -> List[DistributionManifest]:
"""
@PURPOSE: List all manifests for a specific candidate.
"""
with belief_scope("ManifestRepository.list_by_candidate"):
return self.db.query(DistributionManifest).filter(DistributionManifest.candidate_id == candidate_id).all()
# [/DEF:manifest_repository:Module]

View File

@@ -0,0 +1,52 @@
# [DEF:policy_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query policy and registry snapshots.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
from backend.src.core.logger import belief_scope
class PolicyRepository:
"""
@PURPOSE: Encapsulates database operations for Policy and Registry snapshots.
"""
def __init__(self, db: Session):
self.db = db
def save_policy_snapshot(self, snapshot: CleanPolicySnapshot) -> CleanPolicySnapshot:
"""
@PURPOSE: Persist a policy snapshot.
"""
with belief_scope("PolicyRepository.save_policy_snapshot"):
self.db.add(snapshot)
self.db.commit()
self.db.refresh(snapshot)
return snapshot
def get_policy_snapshot(self, snapshot_id: str) -> Optional[CleanPolicySnapshot]:
"""
@PURPOSE: Retrieve a policy snapshot by ID.
"""
with belief_scope("PolicyRepository.get_policy_snapshot"):
return self.db.query(CleanPolicySnapshot).filter(CleanPolicySnapshot.id == snapshot_id).first()
def save_registry_snapshot(self, snapshot: SourceRegistrySnapshot) -> SourceRegistrySnapshot:
"""
@PURPOSE: Persist a registry snapshot.
"""
with belief_scope("PolicyRepository.save_registry_snapshot"):
self.db.add(snapshot)
self.db.commit()
self.db.refresh(snapshot)
return snapshot
def get_registry_snapshot(self, snapshot_id: str) -> Optional[SourceRegistrySnapshot]:
"""
@PURPOSE: Retrieve a registry snapshot by ID.
"""
with belief_scope("PolicyRepository.get_registry_snapshot"):
return self.db.query(SourceRegistrySnapshot).filter(SourceRegistrySnapshot.id == snapshot_id).first()
# [/DEF:policy_repository:Module]

View File

@@ -0,0 +1,53 @@
# [DEF:publication_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query publication records.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import PublicationRecord
from backend.src.core.logger import belief_scope
class PublicationRepository:
"""
@PURPOSE: Encapsulates database operations for PublicationRecord.
"""
def __init__(self, db: Session):
self.db = db
def save(self, record: PublicationRecord) -> PublicationRecord:
"""
@PURPOSE: Persist a publication record.
@POST: Record is committed and refreshed.
"""
with belief_scope("PublicationRepository.save"):
self.db.add(record)
self.db.commit()
self.db.refresh(record)
return record
def get_by_id(self, record_id: str) -> Optional[PublicationRecord]:
"""
@PURPOSE: Retrieve a record by ID.
"""
with belief_scope("PublicationRepository.get_by_id"):
return self.db.query(PublicationRecord).filter(PublicationRecord.id == record_id).first()
def get_latest_for_candidate(self, candidate_id: str) -> Optional[PublicationRecord]:
"""
@PURPOSE: Retrieve the latest record for a candidate.
"""
with belief_scope("PublicationRepository.get_latest_for_candidate"):
return self.db.query(PublicationRecord)\
.filter(PublicationRecord.candidate_id == candidate_id)\
.order_by(PublicationRecord.published_at.desc())\
.first()
def list_by_candidate(self, candidate_id: str) -> List[PublicationRecord]:
"""
@PURPOSE: List all records for a specific candidate.
"""
with belief_scope("PublicationRepository.list_by_candidate"):
return self.db.query(PublicationRecord).filter(PublicationRecord.candidate_id == candidate_id).all()
# [/DEF:publication_repository:Module]

View File

@@ -0,0 +1,50 @@
# [DEF:report_repository:Module]
# @TIER: STANDARD
# @PURPOSE: Persist and query compliance reports.
# @LAYER: Infra
from typing import Optional, List
from sqlalchemy.orm import Session
from backend.src.models.clean_release import ComplianceReport
from backend.src.core.logger import belief_scope
class ReportRepository:
"""
@PURPOSE: Encapsulates database operations for ComplianceReport.
"""
def __init__(self, db: Session):
self.db = db
def save(self, report: ComplianceReport) -> ComplianceReport:
"""
@PURPOSE: Persist a compliance report.
@POST: Report is committed and refreshed.
"""
with belief_scope("ReportRepository.save"):
self.db.add(report)
self.db.commit()
self.db.refresh(report)
return report
def get_by_id(self, report_id: str) -> Optional[ComplianceReport]:
"""
@PURPOSE: Retrieve a report by ID.
"""
with belief_scope("ReportRepository.get_by_id"):
return self.db.query(ComplianceReport).filter(ComplianceReport.id == report_id).first()
def get_by_run(self, run_id: str) -> Optional[ComplianceReport]:
"""
@PURPOSE: Retrieve a report for a specific compliance run.
"""
with belief_scope("ReportRepository.get_by_run"):
return self.db.query(ComplianceReport).filter(ComplianceReport.run_id == run_id).first()
def list_by_candidate(self, candidate_id: str) -> List[ComplianceReport]:
"""
@PURPOSE: List all reports for a specific candidate.
"""
with belief_scope("ReportRepository.list_by_candidate"):
return self.db.query(ComplianceReport).filter(ComplianceReport.candidate_id == candidate_id).all()
# [/DEF:report_repository:Module]

View File

@@ -9,16 +9,17 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
from ...models.clean_release import (
CleanProfilePolicy,
ComplianceCheckRun,
CleanPolicySnapshot,
ComplianceRun,
ComplianceReport,
ComplianceStageRun,
ComplianceViolation,
DistributionManifest,
ReleaseCandidate,
ResourceSourceRegistry,
SourceRegistrySnapshot,
)
@@ -27,67 +28,94 @@ from ...models.clean_release import (
@dataclass
class CleanReleaseRepository:
candidates: Dict[str, ReleaseCandidate] = field(default_factory=dict)
policies: Dict[str, CleanProfilePolicy] = field(default_factory=dict)
registries: Dict[str, ResourceSourceRegistry] = field(default_factory=dict)
policies: Dict[str, CleanPolicySnapshot] = field(default_factory=dict)
registries: Dict[str, SourceRegistrySnapshot] = field(default_factory=dict)
artifacts: Dict[str, object] = field(default_factory=dict)
manifests: Dict[str, DistributionManifest] = field(default_factory=dict)
check_runs: Dict[str, ComplianceCheckRun] = field(default_factory=dict)
check_runs: Dict[str, ComplianceRun] = field(default_factory=dict)
stage_runs: Dict[str, ComplianceStageRun] = field(default_factory=dict)
reports: Dict[str, ComplianceReport] = field(default_factory=dict)
violations: Dict[str, ComplianceViolation] = field(default_factory=dict)
audit_events: List[Dict[str, Any]] = field(default_factory=list)
def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:
self.candidates[candidate.candidate_id] = candidate
self.candidates[candidate.id] = candidate
return candidate
def get_candidate(self, candidate_id: str) -> Optional[ReleaseCandidate]:
return self.candidates.get(candidate_id)
def save_policy(self, policy: CleanProfilePolicy) -> CleanProfilePolicy:
self.policies[policy.policy_id] = policy
def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:
self.policies[policy.id] = policy
return policy
def get_policy(self, policy_id: str) -> Optional[CleanProfilePolicy]:
def get_policy(self, policy_id: str) -> Optional[CleanPolicySnapshot]:
return self.policies.get(policy_id)
def get_active_policy(self) -> Optional[CleanProfilePolicy]:
for policy in self.policies.values():
if policy.active:
return policy
return None
def get_active_policy(self) -> Optional[CleanPolicySnapshot]:
# In-memory repo doesn't track 'active' flag on snapshot,
# this should be resolved by facade using ConfigManager.
return next(iter(self.policies.values()), None)
def save_registry(self, registry: ResourceSourceRegistry) -> ResourceSourceRegistry:
self.registries[registry.registry_id] = registry
def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:
self.registries[registry.id] = registry
return registry
def get_registry(self, registry_id: str) -> Optional[ResourceSourceRegistry]:
def get_registry(self, registry_id: str) -> Optional[SourceRegistrySnapshot]:
return self.registries.get(registry_id)
def save_artifact(self, artifact) -> object:
self.artifacts[artifact.id] = artifact
return artifact
def get_artifacts_by_candidate(self, candidate_id: str) -> List[object]:
return [a for a in self.artifacts.values() if a.candidate_id == candidate_id]
def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:
self.manifests[manifest.manifest_id] = manifest
self.manifests[manifest.id] = manifest
return manifest
def get_manifest(self, manifest_id: str) -> Optional[DistributionManifest]:
return self.manifests.get(manifest_id)
def save_check_run(self, check_run: ComplianceCheckRun) -> ComplianceCheckRun:
self.check_runs[check_run.check_run_id] = check_run
def save_distribution_manifest(self, manifest: DistributionManifest) -> DistributionManifest:
return self.save_manifest(manifest)
def get_distribution_manifest(self, manifest_id: str) -> Optional[DistributionManifest]:
return self.get_manifest(manifest_id)
def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:
self.check_runs[check_run.id] = check_run
return check_run
def get_check_run(self, check_run_id: str) -> Optional[ComplianceCheckRun]:
def get_check_run(self, check_run_id: str) -> Optional[ComplianceRun]:
return self.check_runs.get(check_run_id)
def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:
return self.save_check_run(run)
def get_compliance_run(self, run_id: str) -> Optional[ComplianceRun]:
return self.get_check_run(run_id)
def save_report(self, report: ComplianceReport) -> ComplianceReport:
self.reports[report.report_id] = report
existing = self.reports.get(report.id)
if existing is not None:
raise ValueError(f"immutable report snapshot already exists for id={report.id}")
self.reports[report.id] = report
return report
def get_report(self, report_id: str) -> Optional[ComplianceReport]:
return self.reports.get(report_id)
def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:
self.violations[violation.violation_id] = violation
self.violations[violation.id] = violation
return violation
def get_violations_by_check_run(self, check_run_id: str) -> List[ComplianceViolation]:
return [v for v in self.violations.values() if v.check_run_id == check_run_id]
def get_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:
return [v for v in self.violations.values() if v.run_id == run_id]
def get_manifests_by_candidate(self, candidate_id: str) -> List[DistributionManifest]:
return [m for m in self.manifests.values() if m.candidate_id == candidate_id]
def clear_history(self) -> None:
self.check_runs.clear()
self.reports.clear()

View File

@@ -1,59 +0,0 @@
# [DEF:backend.src.services.clean_release.stages:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance, stages, state-machine
# @PURPOSE: Define compliance stage order and helper functions for deterministic run-state evaluation.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @INVARIANT: Stage order remains deterministic for all compliance runs.
from __future__ import annotations
from typing import Dict, Iterable, List
from ...models.clean_release import CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus
MANDATORY_STAGE_ORDER: List[CheckStageName] = [
CheckStageName.DATA_PURITY,
CheckStageName.INTERNAL_SOURCES_ONLY,
CheckStageName.NO_EXTERNAL_ENDPOINTS,
CheckStageName.MANIFEST_CONSISTENCY,
]
# [DEF:stage_result_map:Function]
# @PURPOSE: Convert stage result list to dictionary by stage name.
# @PRE: stage_results may be empty or contain unique stage names.
# @POST: Returns stage->status dictionary for downstream evaluation.
def stage_result_map(stage_results: Iterable[CheckStageResult]) -> Dict[CheckStageName, CheckStageStatus]:
return {result.stage: result.status for result in stage_results}
# [/DEF:stage_result_map:Function]
# [DEF:missing_mandatory_stages:Function]
# @PURPOSE: Identify mandatory stages that are absent from run results.
# @PRE: stage_status_map contains zero or more known stage statuses.
# @POST: Returns ordered list of missing mandatory stages.
def missing_mandatory_stages(stage_status_map: Dict[CheckStageName, CheckStageStatus]) -> List[CheckStageName]:
return [stage for stage in MANDATORY_STAGE_ORDER if stage not in stage_status_map]
# [/DEF:missing_mandatory_stages:Function]
# [DEF:derive_final_status:Function]
# @PURPOSE: Derive final run status from stage results with deterministic blocking behavior.
# @PRE: Stage statuses correspond to compliance checks.
# @POST: Returns one of COMPLIANT/BLOCKED/FAILED according to mandatory stage outcomes.
def derive_final_status(stage_results: Iterable[CheckStageResult]) -> CheckFinalStatus:
status_map = stage_result_map(stage_results)
missing = missing_mandatory_stages(status_map)
if missing:
return CheckFinalStatus.FAILED
for stage in MANDATORY_STAGE_ORDER:
if status_map.get(stage) == CheckStageStatus.FAIL:
return CheckFinalStatus.BLOCKED
if status_map.get(stage) == CheckStageStatus.SKIPPED:
return CheckFinalStatus.FAILED
return CheckFinalStatus.COMPLIANT
# [/DEF:derive_final_status:Function]
# [/DEF:backend.src.services.clean_release.stages:Module]

View File

@@ -0,0 +1,80 @@
# [DEF:backend.src.services.clean_release.stages:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance, stages, state-machine
# @PURPOSE: Define compliance stage order and helper functions for deterministic run-state evaluation.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @INVARIANT: Stage order remains deterministic for all compliance runs.
from __future__ import annotations
from typing import Dict, Iterable, List
from ..enums import ComplianceDecision, ComplianceStageName
from ....models.clean_release import ComplianceStageRun
from .base import ComplianceStage
from .data_purity import DataPurityStage
from .internal_sources_only import InternalSourcesOnlyStage
from .manifest_consistency import ManifestConsistencyStage
from .no_external_endpoints import NoExternalEndpointsStage
MANDATORY_STAGE_ORDER: List[ComplianceStageName] = [
ComplianceStageName.DATA_PURITY,
ComplianceStageName.INTERNAL_SOURCES_ONLY,
ComplianceStageName.NO_EXTERNAL_ENDPOINTS,
ComplianceStageName.MANIFEST_CONSISTENCY,
]
# [DEF:build_default_stages:Function]
# @PURPOSE: Build default deterministic stage pipeline implementation order.
# @PRE: None.
# @POST: Returns stage instances in mandatory execution order.
def build_default_stages() -> List[ComplianceStage]:
return [
DataPurityStage(),
InternalSourcesOnlyStage(),
NoExternalEndpointsStage(),
ManifestConsistencyStage(),
]
# [/DEF:build_default_stages:Function]
# [DEF:stage_result_map:Function]
# @PURPOSE: Convert stage result list to dictionary by stage name.
# @PRE: stage_results may be empty or contain unique stage names.
# @POST: Returns stage->status dictionary for downstream evaluation.
def stage_result_map(stage_results: Iterable[ComplianceStageRun]) -> Dict[ComplianceStageName, ComplianceDecision]:
return {ComplianceStageName(result.stage_name): ComplianceDecision(result.decision) for result in stage_results if result.decision}
# [/DEF:stage_result_map:Function]
# [DEF:missing_mandatory_stages:Function]
# @PURPOSE: Identify mandatory stages that are absent from run results.
# @PRE: stage_status_map contains zero or more known stage statuses.
# @POST: Returns ordered list of missing mandatory stages.
def missing_mandatory_stages(stage_status_map: Dict[ComplianceStageName, ComplianceDecision]) -> List[ComplianceStageName]:
return [stage for stage in MANDATORY_STAGE_ORDER if stage not in stage_status_map]
# [/DEF:missing_mandatory_stages:Function]
# [DEF:derive_final_status:Function]
# @PURPOSE: Derive final run status from stage results with deterministic blocking behavior.
# @PRE: Stage statuses correspond to compliance checks.
# @POST: Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes.
def derive_final_status(stage_results: Iterable[ComplianceStageRun]) -> ComplianceDecision:
status_map = stage_result_map(stage_results)
missing = missing_mandatory_stages(status_map)
if missing:
return ComplianceDecision.ERROR
for stage in MANDATORY_STAGE_ORDER:
decision = status_map.get(stage)
if decision == ComplianceDecision.ERROR:
return ComplianceDecision.ERROR
if decision == ComplianceDecision.BLOCKED:
return ComplianceDecision.BLOCKED
return ComplianceDecision.PASSED
# [/DEF:derive_final_status:Function]
# [/DEF:backend.src.services.clean_release.stages:Module]

View File

@@ -0,0 +1,123 @@
# [DEF:backend.src.services.clean_release.stages.base:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance, stages, contracts, base
# @PURPOSE: Define shared contracts and helpers for pluggable clean-release compliance stages.
# @LAYER: Domain
# @RELATION: CALLED_BY -> backend.src.services.clean_release.compliance_execution_service
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release
# @INVARIANT: Stage execution is deterministic for equal input context.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Protocol
from uuid import uuid4
from ....core.logger import belief_scope, logger
from ....models.clean_release import (
CleanPolicySnapshot,
ComplianceDecision,
ComplianceRun,
ComplianceStageRun,
ComplianceViolation,
DistributionManifest,
ReleaseCandidate,
SourceRegistrySnapshot,
)
from ..enums import ComplianceStageName, ViolationSeverity
# [DEF:ComplianceStageContext:Class]
# @PURPOSE: Immutable input envelope passed to each compliance stage.
@dataclass(frozen=True)
class ComplianceStageContext:
run: ComplianceRun
candidate: ReleaseCandidate
manifest: DistributionManifest
policy: CleanPolicySnapshot
registry: SourceRegistrySnapshot
# [/DEF:ComplianceStageContext:Class]
# [DEF:StageExecutionResult:Class]
# @PURPOSE: Structured stage output containing decision, details and violations.
@dataclass
class StageExecutionResult:
decision: ComplianceDecision
details_json: Dict[str, Any] = field(default_factory=dict)
violations: List[ComplianceViolation] = field(default_factory=list)
# [/DEF:StageExecutionResult:Class]
# [DEF:ComplianceStage:Class]
# @PURPOSE: Protocol for pluggable stage implementations.
class ComplianceStage(Protocol):
stage_name: ComplianceStageName
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
...
# [/DEF:ComplianceStage:Class]
# [DEF:build_stage_run_record:Function]
# @PURPOSE: Build persisted stage run record from stage result.
# @PRE: run_id and stage_name are non-empty.
# @POST: Returns ComplianceStageRun with deterministic identifiers and timestamps.
def build_stage_run_record(
*,
run_id: str,
stage_name: ComplianceStageName,
result: StageExecutionResult,
started_at: datetime | None = None,
finished_at: datetime | None = None,
) -> ComplianceStageRun:
with belief_scope("build_stage_run_record"):
now = datetime.now(timezone.utc)
return ComplianceStageRun(
id=f"stg-{uuid4()}",
run_id=run_id,
stage_name=stage_name.value,
status="SUCCEEDED" if result.decision != ComplianceDecision.ERROR else "FAILED",
started_at=started_at or now,
finished_at=finished_at or now,
decision=result.decision.value,
details_json=result.details_json,
)
# [/DEF:build_stage_run_record:Function]
# [DEF:build_violation:Function]
# @PURPOSE: Construct a compliance violation with normalized defaults.
# @PRE: run_id, stage_name, code and message are non-empty.
# @POST: Returns immutable-style violation payload ready for persistence.
def build_violation(
*,
run_id: str,
stage_name: ComplianceStageName,
code: str,
message: str,
artifact_path: str | None = None,
severity: ViolationSeverity = ViolationSeverity.MAJOR,
evidence_json: Dict[str, Any] | None = None,
blocked_release: bool = True,
) -> ComplianceViolation:
with belief_scope("build_violation"):
logger.reflect(f"Building violation stage={stage_name.value} code={code}")
return ComplianceViolation(
id=f"viol-{uuid4()}",
run_id=run_id,
stage_name=stage_name.value,
code=code,
severity=severity.value,
artifact_path=artifact_path,
artifact_sha256=None,
message=message,
evidence_json={
**(evidence_json or {}),
"blocked_release": blocked_release,
},
)
# [/DEF:build_violation:Function]
# [/DEF:backend.src.services.clean_release.stages.base:Module]

View File

@@ -0,0 +1,66 @@
# [DEF:backend.src.services.clean_release.stages.data_purity:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance-stage, data-purity
# @PURPOSE: Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.
# @LAYER: Domain
# @RELATION: IMPLEMENTS -> backend.src.services.clean_release.stages.base.ComplianceStage
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.stages.base
# @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision.
from __future__ import annotations
from ....core.logger import belief_scope, logger
from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity
from .base import ComplianceStageContext, StageExecutionResult, build_violation
# [DEF:DataPurityStage:Class]
# @PURPOSE: Validate manifest summary for prohibited artifacts.
# @PRE: context.manifest.content_json contains summary block or defaults to safe counters.
# @POST: Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations.
class DataPurityStage:
stage_name = ComplianceStageName.DATA_PURITY
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
with belief_scope("DataPurityStage.execute"):
summary = context.manifest.content_json.get("summary", {})
prohibited_count = int(summary.get("prohibited_detected_count", 0) or 0)
included_count = int(summary.get("included_count", 0) or 0)
logger.reason(
f"Data purity evaluation run={context.run.id} included={included_count} prohibited={prohibited_count}"
)
if prohibited_count <= 0:
return StageExecutionResult(
decision=ComplianceDecision.PASSED,
details_json={
"included_count": included_count,
"prohibited_detected_count": 0,
},
violations=[],
)
violation = build_violation(
run_id=context.run.id,
stage_name=self.stage_name,
code="DATA_PURITY_PROHIBITED_ARTIFACTS",
message=f"Detected {prohibited_count} prohibited artifact(s) in manifest snapshot",
severity=ViolationSeverity.CRITICAL,
evidence_json={
"prohibited_detected_count": prohibited_count,
"manifest_id": context.manifest.id,
},
blocked_release=True,
)
return StageExecutionResult(
decision=ComplianceDecision.BLOCKED,
details_json={
"included_count": included_count,
"prohibited_detected_count": prohibited_count,
},
violations=[violation],
)
# [/DEF:DataPurityStage:Class]
# [/DEF:backend.src.services.clean_release.stages.data_purity:Module]

View File

@@ -0,0 +1,76 @@
# [DEF:backend.src.services.clean_release.stages.internal_sources_only:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance-stage, source-isolation, registry
# @PURPOSE: Verify manifest-declared sources belong to trusted internal registry allowlist.
# @LAYER: Domain
# @RELATION: IMPLEMENTS -> backend.src.services.clean_release.stages.base.ComplianceStage
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.stages.base
# @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.
from __future__ import annotations
from ....core.logger import belief_scope, logger
from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity
from .base import ComplianceStageContext, StageExecutionResult, build_violation
# [DEF:InternalSourcesOnlyStage:Class]
# @PURPOSE: Enforce internal-source-only policy from trusted registry snapshot.
# @PRE: context.registry.allowed_hosts is available.
# @POST: Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured.
class InternalSourcesOnlyStage:
stage_name = ComplianceStageName.INTERNAL_SOURCES_ONLY
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
with belief_scope("InternalSourcesOnlyStage.execute"):
allowed_hosts = {str(host).strip().lower() for host in (context.registry.allowed_hosts or [])}
sources = context.manifest.content_json.get("sources", [])
violations = []
logger.reason(
f"Internal sources evaluation run={context.run.id} sources={len(sources)} allowlist={len(allowed_hosts)}"
)
for source in sources:
host = str(source.get("host", "")).strip().lower() if isinstance(source, dict) else ""
if not host or host in allowed_hosts:
continue
violations.append(
build_violation(
run_id=context.run.id,
stage_name=self.stage_name,
code="SOURCE_HOST_NOT_ALLOWED",
message=f"Source host '{host}' is not in trusted internal registry",
artifact_path=str(source.get("path", "")) if isinstance(source, dict) else None,
severity=ViolationSeverity.CRITICAL,
evidence_json={
"host": host,
"allowed_hosts": sorted(allowed_hosts),
"manifest_id": context.manifest.id,
},
blocked_release=True,
)
)
if violations:
return StageExecutionResult(
decision=ComplianceDecision.BLOCKED,
details_json={
"source_count": len(sources),
"violations_count": len(violations),
},
violations=violations,
)
return StageExecutionResult(
decision=ComplianceDecision.PASSED,
details_json={
"source_count": len(sources),
"violations_count": 0,
},
violations=[],
)
# [/DEF:InternalSourcesOnlyStage:Class]
# [/DEF:backend.src.services.clean_release.stages.internal_sources_only:Module]

View File

@@ -0,0 +1,70 @@
# [DEF:backend.src.services.clean_release.stages.manifest_consistency:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance-stage, manifest, consistency, digest
# @PURPOSE: Ensure run is bound to the exact manifest snapshot and digest used at run creation time.
# @LAYER: Domain
# @RELATION: IMPLEMENTS -> backend.src.services.clean_release.stages.base.ComplianceStage
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.stages.base
# @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence.
from __future__ import annotations
from ....core.logger import belief_scope, logger
from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity
from .base import ComplianceStageContext, StageExecutionResult, build_violation
# [DEF:ManifestConsistencyStage:Class]
# @PURPOSE: Validate run/manifest linkage consistency.
# @PRE: context.run and context.manifest are loaded from repository for same run.
# @POST: Returns PASSED when digests match, otherwise ERROR with one violation.
class ManifestConsistencyStage:
stage_name = ComplianceStageName.MANIFEST_CONSISTENCY
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
with belief_scope("ManifestConsistencyStage.execute"):
expected_digest = str(context.run.manifest_digest or "").strip()
actual_digest = str(context.manifest.manifest_digest or "").strip()
logger.reason(
f"Manifest consistency evaluation run={context.run.id} manifest={context.manifest.id} "
f"expected_digest={expected_digest} actual_digest={actual_digest}"
)
if expected_digest and expected_digest == actual_digest:
return StageExecutionResult(
decision=ComplianceDecision.PASSED,
details_json={
"manifest_id": context.manifest.id,
"manifest_digest": actual_digest,
"consistent": True,
},
violations=[],
)
violation = build_violation(
run_id=context.run.id,
stage_name=self.stage_name,
code="MANIFEST_DIGEST_MISMATCH",
message="Run manifest digest does not match resolved manifest snapshot",
severity=ViolationSeverity.CRITICAL,
evidence_json={
"manifest_id": context.manifest.id,
"run_manifest_digest": expected_digest,
"actual_manifest_digest": actual_digest,
},
blocked_release=True,
)
return StageExecutionResult(
decision=ComplianceDecision.ERROR,
details_json={
"manifest_id": context.manifest.id,
"run_manifest_digest": expected_digest,
"actual_manifest_digest": actual_digest,
"consistent": False,
},
violations=[violation],
)
# [/DEF:ManifestConsistencyStage:Class]
# [/DEF:backend.src.services.clean_release.stages.manifest_consistency:Module]

View File

@@ -0,0 +1,82 @@
# [DEF:backend.src.services.clean_release.stages.no_external_endpoints:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, compliance-stage, endpoints, network
# @PURPOSE: Block manifest payloads that expose external endpoints outside trusted schemes and hosts.
# @LAYER: Domain
# @RELATION: IMPLEMENTS -> backend.src.services.clean_release.stages.base.ComplianceStage
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.stages.base
# @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision.
from __future__ import annotations
from urllib.parse import urlparse
from ....core.logger import belief_scope, logger
from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity
from .base import ComplianceStageContext, StageExecutionResult, build_violation
# [DEF:NoExternalEndpointsStage:Class]
# @PURPOSE: Validate endpoint references from manifest against trusted registry.
# @PRE: context.registry includes allowed hosts and schemes.
# @POST: Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations.
class NoExternalEndpointsStage:
stage_name = ComplianceStageName.NO_EXTERNAL_ENDPOINTS
def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
with belief_scope("NoExternalEndpointsStage.execute"):
endpoints = context.manifest.content_json.get("endpoints", [])
allowed_hosts = {str(host).strip().lower() for host in (context.registry.allowed_hosts or [])}
allowed_schemes = {str(scheme).strip().lower() for scheme in (context.registry.allowed_schemes or [])}
violations = []
logger.reason(
f"Endpoint isolation evaluation run={context.run.id} endpoints={len(endpoints)} "
f"allowed_hosts={len(allowed_hosts)} allowed_schemes={len(allowed_schemes)}"
)
for endpoint in endpoints:
raw = str(endpoint).strip()
if not raw:
continue
parsed = urlparse(raw)
host = (parsed.hostname or "").lower()
scheme = (parsed.scheme or "").lower()
if host in allowed_hosts and scheme in allowed_schemes:
continue
violations.append(
build_violation(
run_id=context.run.id,
stage_name=self.stage_name,
code="EXTERNAL_ENDPOINT_DETECTED",
message=f"Endpoint '{raw}' is outside trusted internal network boundary",
artifact_path=None,
severity=ViolationSeverity.CRITICAL,
evidence_json={
"endpoint": raw,
"host": host,
"scheme": scheme,
"allowed_hosts": sorted(allowed_hosts),
"allowed_schemes": sorted(allowed_schemes),
},
blocked_release=True,
)
)
if violations:
return StageExecutionResult(
decision=ComplianceDecision.BLOCKED,
details_json={"endpoint_count": len(endpoints), "violations_count": len(violations)},
violations=violations,
)
return StageExecutionResult(
decision=ComplianceDecision.PASSED,
details_json={"endpoint_count": len(endpoints), "violations_count": 0},
violations=[],
)
# [/DEF:NoExternalEndpointsStage:Class]
# [/DEF:backend.src.services.clean_release.stages.no_external_endpoints:Module]

View File

@@ -48,4 +48,21 @@ def test_partial_payload_keeps_report_visible_with_placeholders():
assert "result" in report.details
def test_clean_release_plugin_maps_to_clean_release_task_type():
task = Task(
id="clean-release-1",
plugin_id="clean-release-compliance",
status=TaskStatus.SUCCESS,
started_at=datetime.utcnow(),
finished_at=datetime.utcnow(),
params={"run_id": "run-1"},
result={"summary": "Clean release compliance passed", "run_id": "run-1"},
)
report = normalize_task_report(task)
assert report.task_type.value == "clean_release"
assert report.summary == "Clean release compliance passed"
# [/DEF:backend.tests.test_report_normalizer:Module]

View File

@@ -16,6 +16,7 @@ from ...core.logger import belief_scope
from ...core.task_manager import TaskManager
from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskReport, TaskType
from ..clean_release.repository import CleanReleaseRepository
from .normalizer import normalize_task_report
# [/SECTION]
@@ -47,9 +48,10 @@ class ReportsService:
# @POST: self.task_manager is assigned and ready for read operations.
# @INVARIANT: Constructor performs no task mutations.
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
def __init__(self, task_manager: TaskManager):
def __init__(self, task_manager: TaskManager, clean_release_repository: Optional[CleanReleaseRepository] = None):
with belief_scope("__init__"):
self.task_manager = task_manager
self.clean_release_repository = clean_release_repository
# [/DEF:__init__:Function]
# [DEF:_load_normalized_reports:Function]
@@ -200,6 +202,32 @@ class ReportsService:
if target.error_context:
diagnostics["error_context"] = target.error_context.model_dump()
if target.task_type == TaskType.CLEAN_RELEASE and self.clean_release_repository is not None:
run_id = None
if isinstance(diagnostics, dict):
result_payload = diagnostics.get("result")
if isinstance(result_payload, dict):
run_id = result_payload.get("run_id") or result_payload.get("check_run_id")
if run_id:
run = self.clean_release_repository.get_check_run(str(run_id))
if run is not None:
diagnostics["clean_release_run"] = {
"run_id": run.id,
"candidate_id": run.candidate_id,
"status": run.status,
"final_status": run.final_status,
"requested_by": run.requested_by,
}
linked_report = next(
(item for item in self.clean_release_repository.reports.values() if item.run_id == run.id),
None,
)
if linked_report is not None:
diagnostics["clean_release_report"] = {
"report_id": linked_report.id,
"final_status": linked_report.final_status,
}
next_actions = []
if target.error_context and target.error_context.next_actions:
next_actions = target.error_context.next_actions

View File

@@ -20,6 +20,8 @@ PLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = {
"superset-backup": TaskType.BACKUP,
"superset-migration": TaskType.MIGRATION,
"documentation": TaskType.DOCUMENTATION,
"clean-release-compliance": TaskType.CLEAN_RELEASE,
"clean_release_compliance": TaskType.CLEAN_RELEASE,
}
# [/DEF:PLUGIN_TO_TASK_TYPE:Data]
@@ -54,6 +56,13 @@ TASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {
"emphasis_rules": ["summary", "status", "details"],
"fallback": False,
},
TaskType.CLEAN_RELEASE: {
"display_label": "Clean Release",
"visual_variant": "clean-release",
"icon_token": "shield-check",
"emphasis_rules": ["summary", "status", "error_context", "details"],
"fallback": False,
},
TaskType.UNKNOWN: {
"display_label": "Other / Unknown",
"visual_variant": "unknown",