feat: add artifact snapshot and restore support

This commit is contained in:
sladro 2026-04-02 11:43:46 +08:00
parent db3ae7cff1
commit f261f0bf8f
2 changed files with 120 additions and 0 deletions

View File

@ -0,0 +1,59 @@
from __future__ import annotations
from dataclasses import dataclass
from fnmatch import fnmatch
from hashlib import sha256
from pathlib import Path
from difflib import unified_diff
from engine.models import BaselineSnapshot, TaskSpec
@dataclass(frozen=True)
class ArtifactManager:
task: TaskSpec
def resolve_paths(self) -> list[Path]:
root_dir = self.task.root_dir
resolved: set[Path] = set()
for pattern in self.task.artifacts.include:
for path in root_dir.glob(pattern):
if not path.is_file():
continue
relative_path = path.relative_to(root_dir).as_posix()
if any(fnmatch(relative_path, exclude) for exclude in self.task.artifacts.exclude):
continue
resolved.add(path)
return sorted(resolved)
def snapshot(self) -> BaselineSnapshot:
file_contents: dict[Path, str] = {}
file_hashes: dict[Path, str] = {}
for path in self.resolve_paths():
content = path.read_text(encoding="utf-8")
file_contents[path] = content
file_hashes[path] = sha256(content.encode("utf-8")).hexdigest()
return BaselineSnapshot(file_contents=file_contents, file_hashes=file_hashes)
def restore(self, snapshot: BaselineSnapshot) -> None:
for path, content in snapshot.file_contents.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def diff_summary(self, snapshot: BaselineSnapshot) -> str:
lines: list[str] = []
current_contents = {path: path.read_text(encoding="utf-8") for path in self.resolve_paths()}
all_paths = sorted(set(snapshot.file_contents) | set(current_contents))
for path in all_paths:
before = snapshot.file_contents.get(path, "")
after = current_contents.get(path, "")
if before == after:
continue
diff = unified_diff(
before.splitlines(keepends=True),
after.splitlines(keepends=True),
fromfile=f"{path.as_posix()} (before)",
tofile=f"{path.as_posix()} (after)",
)
lines.extend(diff)
return "".join(lines)

View File

@ -0,0 +1,61 @@
from pathlib import Path
import tempfile
import unittest
from engine.artifact_manager import ArtifactManager
from engine.models import ArtifactSpec, BaselineSnapshot, TaskSpec
from engine.models import BudgetSpec, ConstraintSpec, LoggingSpec, MutationSpec, ObjectiveSpec, PolicySpec, RunnerSpec, ScorerParseSpec, ScorerSpec
def make_task(root_dir: Path) -> TaskSpec:
return TaskSpec(
id="demo",
description="Demo",
artifacts=ArtifactSpec(include=["artifacts/*.md"], exclude=["artifacts/ignore.md"], max_files_per_iteration=1),
mutation=MutationSpec(mode="direct_edit", allowed_file_types=[".md"], max_changed_lines=20),
runner=RunnerSpec(command="python -c \"print('run')\"", cwd=".", timeout_seconds=10),
scorer=ScorerSpec(
type="command",
command="python -c \"import json; print(json.dumps({'score': 1, 'metrics': {'violation_count': 0}}))\"",
parse=ScorerParseSpec(format="json", score_field="score", metrics_field="metrics"),
),
objective=ObjectiveSpec(primary_metric="score", direction="maximize"),
constraints=[ConstraintSpec(metric="violation_count", op="<=", value=0)],
policy=PolicySpec(keep_if="better_primary", tie_breakers=[], on_failure="discard"),
budget=BudgetSpec(max_iterations=1, max_failures=1),
logging=LoggingSpec(results_file="work/results.jsonl", candidate_dir="work/candidates"),
root_dir=root_dir,
)
class ArtifactManagerTest(unittest.TestCase):
def test_snapshot_and_restore(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "artifacts"
artifact_dir.mkdir()
target = artifact_dir / "sample.md"
target.write_text("hello\n", encoding="utf-8")
manager = ArtifactManager(make_task(root))
snapshot = manager.snapshot()
target.write_text("changed\n", encoding="utf-8")
manager.restore(snapshot)
self.assertEqual(target.read_text(encoding="utf-8"), "hello\n")
def test_diff_summary_contains_changed_line(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "artifacts"
artifact_dir.mkdir()
target = artifact_dir / "sample.md"
target.write_text("before\n", encoding="utf-8")
manager = ArtifactManager(make_task(root))
snapshot = manager.snapshot()
target.write_text("after\n", encoding="utf-8")
summary = manager.diff_summary(snapshot)
self.assertIn("-before", summary)
self.assertIn("+after", summary)
if __name__ == "__main__":
unittest.main()