import hashlib import json from pathlib import Path from typing import Optional, Dict def get_file_hash(content: str) -> str: """ Returns SHA256 hash of the content. [C: tests/test_summary_cache.py:test_get_file_hash, tests/test_summary_cache.py:test_summary_cache] """ return hashlib.sha256(content.encode("utf-8")).hexdigest() class SummaryCache: """ A hash-based cache for file summaries to avoid redundant processing. Invalidates when content hash changes. """ def __init__(self, cache_file: Optional[str] = None, max_entries: int = 1000): if cache_file: self.cache_file = Path(cache_file) else: # Default relative to current working directory self.cache_file = Path(".slop_cache/summary_cache.json") self.max_entries = max_entries self.cache: Dict[str, Dict[str, str]] = {} self.load() def load(self) -> None: """ Loads cache from disk. [C: src/tool_presets.py:ToolPresetManager._read_raw, src/workspace_manager.py:WorkspaceManager._load_file, tests/test_gui_phase3.py:test_create_track, tests/test_history_management.py:test_save_separation, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry, tests/test_visual_sim_gui_ux.py:test_gui_track_creation] """ if self.cache_file.exists(): try: with open(self.cache_file, "r", encoding="utf-8") as f: self.cache = json.load(f) except Exception: self.cache = {} def save(self) -> None: """Saves cache to disk.""" try: self.cache_file.parent.mkdir(parents=True, exist_ok=True) with open(self.cache_file, "w", encoding="utf-8") as f: json.dump(self.cache, f, indent=1) except Exception: pass def get_summary(self, file_path: str, content_hash: str) -> Optional[str]: """ Returns cached summary if hash matches, otherwise None. [C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru] """ entry = self.cache.get(file_path) if entry and entry.get("hash") == content_hash: # LRU: move to end val = self.cache.pop(file_path) self.cache[file_path] = val return val.get("summary") return None def set_summary(self, file_path: str, content_hash: str, summary: str) -> None: """ Stores summary in cache and saves to disk. [C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru] """ if file_path in self.cache: self.cache.pop(file_path) self.cache[file_path] = { "hash": content_hash, "summary": summary } # Enforce LRU size limit while len(self.cache) > self.max_entries: # pop first item (oldest) first_key = next(iter(self.cache)) self.cache.pop(first_key) self.save() def clear(self) -> None: """ Clears the cache both in-memory and on disk. [C: tests/conftest.py:reset_ai_client] """ self.cache.clear() if self.cache_file.exists(): try: self.cache_file.unlink() except Exception: pass def get_stats(self) -> dict: """Returns dictionary of cache statistics.""" size_bytes = 0 if self.cache_file.exists(): try: size_bytes = self.cache_file.stat().st_size except Exception: pass return { "entries": len(self.cache), "size_bytes": size_bytes }