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

92 lines
3.4 KiB
Python

# [DEF:backend.src.services.clean_release.preparation_service:Module]
# @TIER: STANDARD
# @SEMANTICS: clean-release, preparation, manifest, policy-evaluation
# @PURPOSE: Prepare release candidate by policy evaluation and deterministic manifest creation.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.policy_engine
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.manifest_builder
# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository
# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.
from __future__ import annotations
from datetime import datetime, timezone
from typing import Dict, Iterable
from .manifest_builder import build_distribution_manifest
from .policy_engine import CleanPolicyEngine
from .repository import CleanReleaseRepository
from .enums import CandidateStatus
def prepare_candidate(
repository: CleanReleaseRepository,
candidate_id: str,
artifacts: Iterable[Dict],
sources: Iterable[str],
operator_id: str,
) -> Dict:
candidate = repository.get_candidate(candidate_id)
if candidate is None:
raise ValueError(f"Candidate not found: {candidate_id}")
policy = repository.get_active_policy()
if policy is None:
raise ValueError("Active clean policy not found")
registry = repository.get_registry(policy.registry_snapshot_id)
if registry is None:
raise ValueError("Registry not found for active policy")
engine = CleanPolicyEngine(policy=policy, registry=registry)
validation = engine.validate_policy()
if not validation.ok:
raise ValueError(f"Invalid policy: {validation.blocking_reasons}")
classified, violations = engine.evaluate_candidate(artifacts=artifacts, sources=sources)
manifest = build_distribution_manifest(
manifest_id=f"manifest-{candidate_id}",
candidate_id=candidate_id,
policy_id=policy.policy_id,
generated_by=operator_id,
artifacts=classified,
)
repository.save_manifest(manifest)
# 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": 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]