Files
ss-tools/backend/src/services/clean_release/manifest_service.py
busya 8c10632494 feat(semantic): curator-driven protocol hardening — decision memory + relation repair
- Add @RATIONALE/@REJECTED to 103+ C4/C5 contracts across backend core, services, API routes, and frontend models
- Fix 109 unresolved @RELATION edges (Auth.*, SupersetClient.*, AgentChat.*, ADR cross-refs)
- Add 13 @ingroup tags for DSA/HCA attention grouping
- Repair 29 stale graph edges via index rebuild
- Update .kilo agent prompts and skills for GRACE-Poly v2.6 compliance
- Git integration: merge routes, branch lifecycle, remote providers, UX components
- 0 broken anchor pairs, index rebuilt with 0 parse warnings
2026-07-02 08:53:19 +03:00

100 lines
4.0 KiB
Python

# #region ManifestService [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, verify, digest]
# @defgroup Services Module group.
# @BRIEF Build immutable distribution manifests with deterministic digest and version increment.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [ManifestBuilder]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @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.
# @SIDE_EFFECT May modify manifest state during processing
# @DATA_CONTRACT Manifest -> ManifestRecord; Candidate -> ManifestRecord
# @RATIONALE Manifest service wraps manifest builder with persistence, version increment, and candidate lifecycle transitions — separating builder (pure computation) from service (persistence + side effects) allows the hashing logic to be tested without a database.
# @REJECTED Combining builder logic and persistence in a single function was rejected — it would make it impossible to unit-test manifest digest computation without a repository mock.
from __future__ import annotations
from typing import Any
from ...models.clean_release import DistributionManifest
from .enums import CandidateStatus
from .manifest_builder import build_distribution_manifest
from .repository import CleanReleaseRepository
# #region build_manifest_snapshot [TYPE Function]
# @ingroup Services
# @BRIEF 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
# #endregion build_manifest_snapshot
# #endregion ManifestService