436 lines
17 KiB
Python
436 lines
17 KiB
Python
# [DEF:backend.src.models.clean_release:Module]
|
|
# @TIER: CRITICAL
|
|
# @SEMANTICS: clean-release, models, lifecycle, compliance, evidence, immutability
|
|
# @PURPOSE: Define canonical clean release domain entities and lifecycle guards.
|
|
# @LAYER: Domain
|
|
# @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, 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: Backward-compatible final status enum for legacy TUI/orchestrator tests.
|
|
class CheckFinalStatus(str, Enum):
|
|
COMPLIANT = "COMPLIANT"
|
|
BLOCKED = "BLOCKED"
|
|
FAILED = "FAILED"
|
|
# [/DEF:CheckFinalStatus:Class]
|
|
|
|
# [DEF:CheckStageName:Class]
|
|
# @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"
|
|
# [/DEF:CheckStageName:Class]
|
|
|
|
# [DEF:CheckStageStatus:Class]
|
|
# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests.
|
|
class CheckStageStatus(str, Enum):
|
|
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: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: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: Backward-compatible source entry model for legacy TUI bootstrap logic.
|
|
@dataclass
|
|
class ResourceSourceEntry:
|
|
source_id: str
|
|
host: str
|
|
protocol: str
|
|
purpose: str
|
|
enabled: bool = True
|
|
# [/DEF:ResourceSourceEntry:Class]
|
|
|
|
# [DEF:ResourceSourceRegistry:Class]
|
|
# @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: str = "ACTIVE"
|
|
|
|
@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]
|
|
@dataclass
|
|
class ManifestItem:
|
|
path: str
|
|
category: str
|
|
classification: ClassificationType
|
|
reason: str
|
|
checksum: Optional[str] = None
|
|
# [/DEF:ManifestItem:Class]
|
|
|
|
# [DEF:ManifestSummary:Class]
|
|
@dataclass
|
|
class ManifestSummary:
|
|
included_count: int
|
|
excluded_count: int
|
|
prohibited_detected_count: int
|
|
# [/DEF:ManifestSummary:Class]
|
|
|
|
# [DEF:DistributionManifest:Class]
|
|
# @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)
|
|
|
|
# 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: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)
|
|
|
|
@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: 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: 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] |