Files
VianoBugAnalysis/src/viano_bug_analysis/case/builder.py
T

92 lines
6.0 KiB
Python

from dataclasses import dataclass
from pathlib import Path
from viano_bug_analysis.lifecycle import LifecycleError, State
from viano_bug_analysis.lifecycle.checkpoints import checkpoint_chain
from viano_bug_analysis.lifecycle.storage import json_bytes
from viano_bug_analysis.package import PackageReader, PackageReadError
from viano_bug_analysis.package.validator import ARCHIVE_ERRORS
from .assembler import CaseAssembler, CaseBuilderLimits
from .contracts import read_json
from .errors import CaseBuildError
from .summary import render_summary
@dataclass(frozen=True)
class CaseBuildResult:
package: Path
case_ref: str
summary_ref: str
analysis_revision: int
reused: bool
class CaseBuilder:
def __init__(self, store, limits: CaseBuilderLimits | None = None):
self.store = store
self.assembler = CaseAssembler(limits)
def build(self, case_id: str, package_id: str) -> CaseBuildResult:
try:
latest = self.store.latest(case_id, package_id)
if latest is None:
raise CaseBuildError("CASE_NOT_FOUND", "Case must be admitted before construction")
with PackageReader(latest, self.store.limits) as reader:
manifest = reader.manifest
revision = int(manifest["analysis_revision"])
revision = revision or 1
complete_id = f"case-builder:v1:revision:{revision}:complete"
completed = next((cp for _, cp in checkpoint_chain(reader) if cp["step_id"] == complete_id), None)
if completed:
assembled = self.assembler.assemble(reader)
details = completed["details"]
if details.get("input_request_hash") != assembled["inputs"]["request_hash"]:
raise CaseBuildError("STALE_CASE", "Inputs changed after case completion; reopen as a new analysis revision")
case_ref, summary_ref = details.get("case_ref"), details.get("summary_ref")
stored = read_json(reader, case_ref, "case", self.assembler.limits.max_case_bytes)
summary = render_summary(stored).encode("utf-8")
if len(summary) > self.assembler.limits.max_summary_bytes:
raise CaseBuildError("SUMMARY_LIMIT", "Summary exceeds configured byte limit")
with reader._archive.open(summary_ref) as stream:
stored_summary = reader.validator._read_bounded(stream, min(self.assembler.limits.max_summary_bytes, self.store.limits.max_file_bytes))
if reader.manifest["artifacts"].get("case") != case_ref or reader.manifest["artifacts"].get("case_summary") != summary_ref or stored_summary != summary:
raise CaseBuildError("CASE_OUTPUT_CHANGED", "Confirmed case pointers or summary no longer match")
# Repair reconstructible metadata after a crash, without recomputing a commit.
recovered = self.store.advance(case_id, package_id, State.BUILDING_CASE, step_id=complete_id,
details=details, additions={case_ref: json_bytes(stored), summary_ref: summary},
artifacts={"case": case_ref, "case_summary": summary_ref})
if recovered != latest:
raise CaseBuildError("GENERATION_CHANGED", "Case changed while confirmed output was checked")
return CaseBuildResult(latest, case_ref, summary_ref, revision, True)
if manifest["state"] not in {State.VALIDATED, State.REOPENED, State.BUILDING_CASE}:
raise CaseBuildError("INVALID_BUILD_STATE", "CaseBuilder requires VALIDATED, REOPENED or BUILDING_CASE")
source_generation = int(manifest["package_generation"])
if manifest["state"] != State.BUILDING_CASE:
latest = self.store.advance(case_id, package_id, State.BUILDING_CASE,
step_id=f"case-builder:v1:revision:{revision}:start",
details={"operation": "case-building", "builder_version": "1"},
expected_generation=source_generation)
with PackageReader(latest, self.store.limits) as reader:
if any(cp["step_id"] == complete_id for _, cp in checkpoint_chain(reader)):
return self.build(case_id, package_id)
case = self.assembler.assemble(reader)
revision = case["case"]["analysis_revision"]
case_ref = f"case/revisions/{revision:04d}/case.json"
summary_ref = f"case/revisions/{revision:04d}/case_summary.md"
summary = render_summary(case).encode("utf-8")
if len(summary) > self.assembler.limits.max_summary_bytes:
raise CaseBuildError("SUMMARY_LIMIT", "Summary exceeds configured byte limit")
details = {"operation": "case-built", "builder_version": "1", "case_ref": case_ref, "summary_ref": summary_ref,
"input_request_hash": case["inputs"]["request_hash"]}
output = self.store.advance(case_id, package_id, State.BUILDING_CASE,
step_id=f"case-builder:v1:revision:{revision}:complete", details=details,
additions={case_ref: json_bytes(case), summary_ref: summary},
artifacts={"case": case_ref, "case_summary": summary_ref},
expected_generation=int(reader.manifest["package_generation"]))
return CaseBuildResult(output, case_ref, summary_ref, revision, False)
except (PackageReadError, LifecycleError) as error:
raise CaseBuildError(getattr(error, "code", "PACKAGE_ERROR"), str(error)) from error
except ARCHIVE_ERRORS + (TypeError, KeyError, RecursionError) as error:
raise CaseBuildError("CASE_BUILD_ERROR", str(error)) from error