feat(hashing): add force, dry_run, progress_callback to populate_hash_cache (ITL-500)#210
Conversation
…L-500) Spec covers progress callback, dry-run mode, force re-hash, internal _Stats accumulator, visitor/accumulator split, and CLI --dry-run/--force flags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…refined accumulator table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e ergonomics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…TL-500) The Returns block incorrectly stated that bytes_hashed is 0 for cached files, but the implementation returns file_stat.st_size for both "hashed" and "cached" outcomes. Only "error" returns 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… thread safety (ITL-500) - Remove unused Callable import from typing (fixes ruff F401) - Change _Accumulator.__init__ callback parameter from undefined "ProgressCallback | None" to None (fixes ruff F821) - Document _Accumulator thread-safety constraint in class docstring - Clarify _Stats.snapshot() docstring to explain fresh time.monotonic() reading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add force parameter to populate_hash_cache to allow re-hashing files even if they're already in the cache. The parameter is passed through to _HashVisitor which already had the capability to skip cache lookups. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ant string quote (ITL-500) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… (ITL-500) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose the dry_run and force parameters of populate_hash_cache via the warm-cache CLI. Update output to include cached GB in all runs and add dry-run-specific messaging. Add 3 CLI integration tests via CliRunner.
…nt guard (ITL-500)
…rom orcapod.hashing (ITL-500)
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR extends OrcaPod’s hashing cache population workflow by adding ergonomic controls (force re-hash, dry-run scanning, and progress callbacks), tracking cached-byte totals, and wiring the new functionality through the orcapod warm-cache CLI and public hashing API.
Changes:
- Added
dry_run,force, andprogress_callbacktopopulate_hash_cache(), plustotal_bytes_cachedtoCachePopulationStats. - Refactored cache population internals into visitor/accumulator components and added tests for the new behaviors.
- Updated
orcapod warm-cacheCLI to expose--dry-runand--force, and to include cached GB in output; exported new types fromorcapod.hashing.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_hashing/test_cache_population.py | Adds extensive coverage for dry-run/force/progress callback behavior, CLI wiring, and cached-byte stats. |
| src/orcapod/hashing/cache_population.py | Implements new API parameters and refactors logic into _Stats, _Accumulator, _HashVisitor, and _DryRunVisitor. |
| src/orcapod/hashing/init.py | Exports FileOutcome and ProgressCallback from the public hashing package API. |
| src/orcapod/cli/warm_cache.py | Adds --dry-run/--force flags and updates summary output to include cached GB. |
| superpowers/specs/2026-07-05-itl-500-populate-hash-cache-ergonomics-design.md | Adds design documentation for the feature and internal architecture. |
| superpowers/plans/2026-07-05-itl-500-populate-hash-cache-ergonomics.md | Adds a detailed implementation plan for ITL-500. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| progress_callback: Optional callable invoked once per qualifying file | ||
| (i.e. every file that passes the size filter, including those that | ||
| encounter stat or hashing errors). Receives the resolved ``Path``, | ||
| a ``FileOutcome`` string (``"hashed"``, ``"cached"``, | ||
| ``"would_hash"``, or ``"error"``), and a frozen | ||
| ``CachePopulationStats`` snapshot of running totals at that moment. | ||
| Not called for files below ``min_size_bytes`` or for directory | ||
| access errors. Defaults to ``None``. |
There was a problem hiding this comment.
Fixed. The docstring now distinguishes two cases:
- Most outcomes (
"hashed","cached","would_hash", and hashing errors): the callback receives the resolved absoluteUPath— symlinks followed,..eliminated — the path the OS kernel uses to open the file. - Stat or resolve failures: the callback receives the original
iterdir()entry. At the point theOSErroris raised,resolvedmay not have been fully computed, soentryis passed instead. This path identifies where in the tree the failure occurred but may still contain unresolved symlink components from parent directories.
Note that hashing errors (where the visitor itself raises) still receive the resolved path, because resolved is stored in pending before the worker thread starts — the error only surfaces on drain.
| dry_run: If ``True``, perform the full walk, stat, size filter, and cache | ||
| check but skip hashing and cache writes. ``stats.hashed`` reports how | ||
| many files *would* be hashed. Defaults to ``False``. |
There was a problem hiding this comment.
Fixed. Added to the dry_run Args entry: "The walk runs serially on the calling thread regardless of max_workers; max_workers is ignored when dry_run=True.". The serial path was an intentional design choice — cache lookups are fast SQLite reads, and parallelising them would add complexity for negligible benefit. The docstring now makes that explicit so callers aren't surprised when max_workers=8 has no effect in dry-run mode.
…th docs (ITL-500)
Review response (Copilot — round 1)Three documentation/type issues addressed in commit a1f4718. No logic changes. 1.
|
|
|
||
| with ThreadPoolExecutor(max_workers=max_workers) as executor: | ||
| with SqliteHashCacher(_db_path) as cacher: | ||
| # Collect the DB file and its SQLite sidecar files so they are never |
There was a problem hiding this comment.
These comments must be preserved
There was a problem hiding this comment.
Acknowledged. The thread-safety note in _HashVisitor's docstring ("Thread-safe: SqliteHashCacher uses threading.local() connections and FileHasher is stateless.") is intentional documentation of the concurrency contract and will not be removed.
| self._stats = _Stats() | ||
| self._callback = callback | ||
|
|
||
| def record(self, path: Path, outcome: FileOutcome, nbytes: int) -> None: |
There was a problem hiding this comment.
Fixed. Changed _Accumulator.record(path: Path, ...) to path: UPath. The type now matches ProgressCallback's contract end-to-end, so static checkers will no longer flag the forwarding call at self._callback(path, ...). The Path import is still needed for the _db_path local variable.
| progress_callback: Optional callable invoked once per qualifying file | ||
| (i.e. every file that passes the size filter, including those that | ||
| encounter stat or hashing errors). Receives three arguments: |
There was a problem hiding this comment.
Fixed. The opening sentence now reads: "Optional callable invoked once for each file that either meets the size threshold or fails at stat()/resolve() time (stat and resolve failures are reported before any size check)." The trailing note "Not called for files below min_size_bytes or for directory access errors" is unchanged and still accurate.
| _db_path: Path | None = Path(db_path) if db_path is not None else None | ||
| hasher = FileHasher(algorithm=algorithm, buffer_size=buffer_size) | ||
|
|
There was a problem hiding this comment.
Fixed. FileHasher is now constructed inside the else: branch, just before _HashVisitor is instantiated — so a dry-run call never touches the hasher or triggers any algorithm validation.
- Move FileHasher construction into the non-dry-run branch; dry_run=True never uses the hasher so it should not be constructed unconditionally. - Fix _Accumulator.record() path annotation: Path → UPath to match the ProgressCallback type contract. - Correct progress_callback docstring: stat/resolve failures fire the callback before any size check, not only for size-filter-passing files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round 3 — changes in 6dd8f35Three issues addressed (all in 1. 2. 3. No other changes. 47 tests passing. |
Summary
force=True,dry_run=True, andprogress_callbackparameters topopulate_hash_cache(), andtotal_bytes_cachedtoCachePopulationStats_HashVisitor,_DryRunVisitor,_Stats,_Accumulator) replacing loose local counter variables--dry-runand--forceflags toorcapod warm-cacheCLI with appropriate output formatting; adds cached GB to summary outputFileOutcomeandProgressCallbackfromorcapod.hashingpublic APINew
populate_hash_cachesignatureAll three new parameters default to safe no-op values — fully backward compatible.
Test plan
tests/test_hashing/test_cache_population.pycovering all new behaviours:TestForce,TestDryRun,TestVisitors,TestProgressCallback,TestCLI,TestCachedBytesuv run python -c "from orcapod.hashing import FileOutcome, ProgressCallback; print('OK')"→ OKFixes ITL-500