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 6ee54d95a8
commit 87b81a365a
79 changed files with 7430 additions and 945 deletions

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