feat(clean-release): complete compliance redesign phases and polish tasks T047-T052
This commit is contained in:
@@ -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]
|
||||
Reference in New Issue
Block a user