refactor review-plan safety and validation
This commit is contained in:
+54
-78
@@ -5,9 +5,9 @@
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| Repository path | `dl-organizer` (package `vlm`) |
|
||||
| Verification date (UTC) | 2026-04-06 |
|
||||
| Git revision verified | `ea21e15` |
|
||||
| Test run | `uv run pytest -q` → **496 passed** |
|
||||
| Verification date (UTC) | 2026-04-07 |
|
||||
| Git revision verified | `working tree (post-refactor)` |
|
||||
| Test run | `pytest -q` → **507 passed** |
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -17,136 +17,112 @@
|
||||
|
||||
## Executive summary
|
||||
|
||||
Architecture and tests are solid. Verified gaps: **no `library_root` check on move/rename source** in executor; **`by_quality` falls back to index 0 without surfacing reason**; **quarantine raises `ValueError` for unsupported category** while `execute_plan` has no per-operation try/except, so one bad op can abort the run; **duplicate join uses exact string path keys**; **`find` non-zero exit still consumes stdout**.
|
||||
The planned hardening work is complete and the suite is green. The earlier review findings around unsafe move/rename sources, silent duplicate fallback, exact-string duplicate joins, quarantine run aborts, ambiguous `find` partial-success handling, and dict-typed plan loading are all addressed in the current codebase. The remaining observation is low-priority packaging overlap: `textual` is still declared in both the `dev` and `tui` optional extras, even though the runtime optional-boundary bug is fixed.
|
||||
|
||||
Weighted score (0–10): **8.0**.
|
||||
Weighted score (0–10): **9.0**.
|
||||
|
||||
## Score rubric
|
||||
|
||||
| Criterion | Score | Notes |
|
||||
|-----------|-------|--------|
|
||||
| Module boundaries / pipeline | 8.5 | Commands → domain modules; scan→parse→analyze→plan→execute is explicit. |
|
||||
| Execution safety (filesystem) | 7.0 | Destination under root checked; **source not checked** for move/rename. |
|
||||
| Planning / duplicate logic | 7.5 | Multiple strategies; string path identity; silent `by_quality` fallback; unknown `strategy` → keep index 0 (`duplicate_resolve.py:48`). |
|
||||
| Data I/O & validation | 8.0 | `io.py` validates identities/analysis/plan shapes; validated objects remain dict-typed at boundaries (`type: ignore` in places). |
|
||||
| Error handling consistency | 7.5 | Quarantine: `raise` vs `OperationResult` mismatch on category rejection. |
|
||||
| Test signal | 8.5 | Broad `tests/`; path safety and resolver covered (`test_path_safety.py`, `test_duplicate_resolve.py`, etc.). |
|
||||
| Dependencies | 9.0 | Runtime: `click`, `pyyaml` only (`pyproject.toml:7–9`). |
|
||||
| Module boundaries / pipeline | 8.8 | Commands → domain modules remain explicit and well-separated. |
|
||||
| Execution safety (filesystem) | 9.0 | Source and destination root checks are enforced for move/rename execution (`src/vlm/executor.py:204-249`). |
|
||||
| Planning / duplicate logic | 9.0 | Duplicate resolution now fails explicitly on bad inputs and canonicalizes path matching (`src/vlm/duplicate_resolve.py:16-58`, `src/vlm/planner.py:42-57`, `src/vlm/planner.py:151-203`). |
|
||||
| Data I/O & validation | 9.0 | Validated plan JSON now flows through a typed construction boundary (`src/vlm/io.py:247-326`). |
|
||||
| Error handling consistency | 9.0 | Unsupported quarantine categories return failed results; execute loop contains per-operation exceptions (`src/vlm/quarantine.py:116-140`, `src/vlm/executor.py:108-130`). |
|
||||
| Test signal | 9.2 | Full suite is green and regression coverage targets the hardened edges. |
|
||||
| Dependencies | 8.5 | Runtime dependency posture is lean, but `textual` remains duplicated across optional extras (`pyproject.toml:12-20`). |
|
||||
|
||||
## Findings
|
||||
## Resolved findings
|
||||
|
||||
### F1 [High] Move/rename: source not constrained to `library_root`
|
||||
### F1 [Resolved] Move/rename source now constrained to `library_root`
|
||||
|
||||
**Evidence:** `ExecutionEngine._perform_operation` checks destination with `is_within_root(..., self.config.library_root)` then calls `operation.source_path.rename(...)` without checking the source (`executor.py:354–377`).
|
||||
**Evidence:** `src/vlm/executor.py:204-249`, `tests/test_path_safety.py:76-137`.
|
||||
|
||||
**Impact:** Malformed or hand-edited `plan.json` can rename **from any path the process can access** into the library tree. `QuarantineManager.quarantine_file` instead requires `file_path.relative_to(self.config.library_root)` (`quarantine.py:133–137`).
|
||||
|
||||
**Recommendation:** For `move` / `rename`, require `is_within_root(operation.source_path, self.config.library_root)` (after `Path` resolution policy is defined). Fail with `OperationResult(success=False)`. Optional config gate for deliberate “import from outside,” default off. Add test alongside `tests/test_path_safety.py:76–103`.
|
||||
Execution now rejects crafted or hand-edited plans whose move/rename source or destination escapes the configured library root.
|
||||
|
||||
---
|
||||
|
||||
### F2 [Medium] `by_quality`: silent fallback to first item
|
||||
### F2 [Resolved] `by_quality` no longer silently falls back to first item
|
||||
|
||||
**Evidence:** `choose_keep_index` — `if quality_comparison is None or len(quality_comparison) != len(items): return 0` (`duplicate_resolve.py:40–42`).
|
||||
**Evidence:** `src/vlm/duplicate_resolve.py:44-58`, `src/vlm/planner.py:187-203`, `tests/test_duplicate_resolve.py:223-230`, `tests/test_planner.py:619-669`.
|
||||
|
||||
**Impact:** Behavior equals **input order**, not quality, with no structured flag in plan metadata from this function alone.
|
||||
|
||||
**Recommendation:** Return `None` and skip auto-quarantine for that group, or require CLI/plan failure when `duplicate_keep == by_quality` and rows misaligned, or write explicit `metadata` / summary line when fallback occurs.
|
||||
Missing or misaligned quality data now raises an explicit resolver error, and planning converts the group into manual review instead of quietly selecting index `0`.
|
||||
|
||||
---
|
||||
|
||||
### F3 [Medium] Duplicate group ↔ identities join: exact path strings
|
||||
### F3 [Resolved] Duplicate group ↔ identities join no longer depends on exact string equality
|
||||
|
||||
**Evidence:** `path_to_index = {str(vf.path): i for ...}` and `indices = [path_to_index[p] for p in paths if p in path_to_index]` (`planner.py:108–111`). Analysis `files` must match `str(VideoFile.path)` byte-for-byte.
|
||||
**Evidence:** `src/vlm/planner.py:42-57`, `src/vlm/planner.py:152-169`, `tests/test_planner.py:672-720`.
|
||||
|
||||
**Impact:** Symlinks, differing normalization between analysis writer and identities loader, or OS case rules can **drop** files from duplicate resolution.
|
||||
|
||||
**Recommendation:** Normalize keys (e.g. `utils.canonical_path_str`) at both analysis emission and plan consumption; document contract; regression test if symlinks are in scope.
|
||||
Canonical path keys are used consistently for duplicate-group matching and quality-comparison lookup.
|
||||
|
||||
---
|
||||
|
||||
### F4 [Low–Medium] Quarantine: `ValueError` vs `OperationResult`
|
||||
### F4 [Resolved] Quarantine failures no longer abort the whole execute pass
|
||||
|
||||
**Evidence:** Unsupported category path does `raise ValueError(error_msg)` after logging (`quarantine.py:116–129`). Other failure modes return `OperationResult`.
|
||||
**Evidence:** `src/vlm/quarantine.py:116-140`, `src/vlm/executor.py:108-130`, `tests/test_quarantine.py:84-110`, `tests/test_executor.py:951-1004`.
|
||||
|
||||
**Evidence:** `execute_plan` loop calls `execute_operation` with no try/except (`executor.py:110–112`).
|
||||
|
||||
**Impact:** One invalid quarantine operation (e.g. manual plan) can **abort** the whole execute pass instead of recording a single failed result.
|
||||
|
||||
**Recommendation:** Return failed `OperationResult` for unsupported category; reserve exceptions for invariant violations only.
|
||||
Unsupported quarantine categories now produce failed `OperationResult`s, and the execute loop continues after unexpected per-operation failures.
|
||||
|
||||
---
|
||||
|
||||
### F5 [Low] `find` non-zero exit: stdout still parsed
|
||||
### F5 [Resolved] `find` non-zero exit behavior is now explicit
|
||||
|
||||
**Evidence:** On `process.returncode != 0`, code logs stderr if present, then always parses `stdout` into paths (`scanner.py:178–192`).
|
||||
**Evidence:** `src/vlm/scanner.py:158-214`, `tests/test_scanner.py:140-186`.
|
||||
|
||||
**Impact:** Partial or stale listing possible without hard failure.
|
||||
|
||||
**Recommendation:** Document behavior; optionally fail if `returncode != 0` and empty result, or add `--strict-scan`.
|
||||
Partial stdout is retained with an explicit warning; a non-zero exit with no stdout yields an empty deterministic result.
|
||||
|
||||
---
|
||||
|
||||
### F6 [Low] Optional dependencies: `textual` in both `dev` and `tui`
|
||||
### F6 [Low] Optional dependency overlap remains
|
||||
|
||||
**Evidence:** `pyproject.toml:13–16` (`dev`), `18–19` (`tui`).
|
||||
**Evidence:** `pyproject.toml:12-20`.
|
||||
|
||||
**Impact:** Install surface ambiguity only.
|
||||
|
||||
**Recommendation:** Document that `dev` includes Textual for TUI-related tests, or slim `dev` if CI wants fewer deps.
|
||||
`textual` is still listed in both `dev` and `tui`. This is now a packaging/documentation concern only; the runtime issue was removed by the guarded import strategy in `src/vlm/review_tui.py:16-30` and lazy CLI import in `src/vlm/cli.py:567-602`.
|
||||
|
||||
---
|
||||
|
||||
### F7 [Informational] Validated JSON remains dict-typed at I/O edge
|
||||
### F7 [Resolved] Validated JSON no longer remains dict-typed at the hot execution boundary
|
||||
|
||||
**Evidence:** `validate_plan_json` returns `dict` (`io.py:224–251`); constructors may still bridge dict ↔ dataclass elsewhere.
|
||||
**Evidence:** `src/vlm/io.py:247-326`, `tests/test_io.py:112-203`.
|
||||
|
||||
**Impact:** Field drift between validator and `models.py` possible over time.
|
||||
|
||||
**Recommendation:** Single factory path: validated dict → domain objects for hot paths.
|
||||
Plan loading now validates schema shape and immediately constructs an `ExecutionPlan` object.
|
||||
|
||||
---
|
||||
|
||||
### F8 [Informational] Unknown `duplicate_keep` strategy string
|
||||
### F8 [Resolved] Unknown duplicate strategy no longer defaults to first item
|
||||
|
||||
**Evidence:** After strategy checks, `choose_keep_index` falls through to `return 0` (`duplicate_resolve.py:48`).
|
||||
**Evidence:** `src/vlm/duplicate_resolve.py:54-58`, `tests/test_duplicate_resolve.py:233-237`.
|
||||
|
||||
**Impact:** Config typo **keeps first duplicate** like `first_seen` without failing load.
|
||||
|
||||
**Recommendation:** Validate `duplicate_keep` in `config.py` / `validate_config` against an allowlist; reject unknown values.
|
||||
Unexpected strategy strings now fail explicitly via `DuplicateResolutionError`.
|
||||
|
||||
## Verified strengths
|
||||
|
||||
| Claim | Evidence |
|
||||
|--------|----------|
|
||||
| `find` invoked without shell string | argv list: `subprocess.Popen(command, ...)` (`scanner.py:171–172`, `164–169`). |
|
||||
| Path component sanitization | `sanitize_path_component` strips controls and separators (`utils.py:65–75`); planner uses it before templates (`planner.py:301`, `419`). |
|
||||
| Destination under library in planner | `is_within_root(destination, config.library_root)` (`planner.py:321`, `440`). |
|
||||
| Quarantine manifest two-phase | Pending manifest write before `rename` (`quarantine.py:221–268` region). |
|
||||
| JSON artifact validation | `_validate_parsed_identities_json`, `_validate_analysis_json`, `validate_plan_json` (`io.py:157–251`). |
|
||||
| Parallel ffprobe | `ThreadPoolExecutor` when `include_video_metadata` and more than one path (`scanner.py:85–101`). |
|
||||
| Optional TUI boundary is runtime-safe | `src/vlm/cli.py:567-602`, `src/vlm/review_tui.py:16-30`, `tests/test_cli_review_plan.py:237-343` |
|
||||
| Path component sanitization and planner root checks remain in place | `src/vlm/planner.py:42-57`, `src/vlm/planner.py:151-203` |
|
||||
| Quarantine manifest two-phase flow remains intact | `src/vlm/quarantine.py:221-320` |
|
||||
| JSON artifact validation still exists and now feeds a typed plan path | `src/vlm/io.py:165-326` |
|
||||
| Parallel ffprobe behavior remains available when metadata extraction is enabled | `src/vlm/scanner.py:85-101` |
|
||||
|
||||
## Priority order
|
||||
|
||||
1. F1
|
||||
2. F2
|
||||
3. F4
|
||||
4. F3
|
||||
5. F8, F5, F6, F7
|
||||
1. Optional-extra cleanup (`pyproject.toml:12-20`) if packaging clarity is important.
|
||||
2. Otherwise, current review items are complete and the main focus can shift to new feature work.
|
||||
|
||||
## Verification log (document vs codebase)
|
||||
## Verification log (current codebase)
|
||||
|
||||
| Statement in this doc | Checked against |
|
||||
|------------------------|-----------------|
|
||||
| F1 source not under root guard | `executor.py:321–377` |
|
||||
| F2 by_quality fallback | `duplicate_resolve.py:40–43` |
|
||||
| F3 path_to_index | `planner.py:108–111` |
|
||||
| F4 raise + no try in loop | `quarantine.py:116–129`, `executor.py:110–112` |
|
||||
| F5 find returncode | `scanner.py:178–192` |
|
||||
| F6 pyproject optional | `pyproject.toml:12–20` |
|
||||
| F7 validate_plan_json returns dict | `io.py:224–251` |
|
||||
| F8 unknown strategy | `duplicate_resolve.py:36–48` |
|
||||
| Strengths table | `scanner.py`, `utils.py`, `planner.py`, `quarantine.py`, `io.py` as cited |
|
||||
| 496 tests passed | `uv run pytest -q` on 2026-04-06 |
|
||||
| TUI runtime boundary | `src/vlm/cli.py:567-602`, `src/vlm/review_tui.py:16-30`, `tests/test_cli_review_plan.py:237-343` |
|
||||
| Move/rename source + destination checks | `src/vlm/executor.py:204-249`, `tests/test_path_safety.py:76-137` |
|
||||
| Quarantine failure contract + execute containment | `src/vlm/quarantine.py:116-140`, `src/vlm/executor.py:108-130`, `tests/test_executor.py:951-1004` |
|
||||
| Duplicate resolver fail-fast behavior | `src/vlm/duplicate_resolve.py:16-58`, `tests/test_duplicate_resolve.py:223-237` |
|
||||
| Duplicate path normalization | `src/vlm/planner.py:42-57`, `src/vlm/planner.py:152-169`, `tests/test_planner.py:672-720` |
|
||||
| Scanner partial-result contract | `src/vlm/scanner.py:158-214`, `tests/test_scanner.py:140-186` |
|
||||
| Typed plan I/O boundary | `src/vlm/io.py:247-326`, `tests/test_io.py:112-203` |
|
||||
| Full-suite baseline | `pytest -q` on 2026-04-07 → **507 passed** |
|
||||
|
||||
This document is evidence-based against the paths above; behavior not re-listed here is **not** claimed verified.
|
||||
This document reflects the current post-refactor state rather than the pre-refactor defect list.
|
||||
|
||||
Reference in New Issue
Block a user