refactor review-plan safety and validation

This commit is contained in:
windyboy
2026-04-07 11:00:47 +08:00
parent fb128c70d6
commit 0a6bddcc7e
19 changed files with 1246 additions and 628 deletions
+54 -78
View File
@@ -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 (010): **8.0**.
Weighted score (010): **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:79`). |
| 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:354377`).
**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:133137`).
**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:76103`.
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:4042`).
**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:108111`). 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 [LowMedium] 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:116129`). 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:110112`).
**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:178192`).
**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:1316` (`dev`), `1819` (`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:224251`); 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:171172`, `164169`). |
| Path component sanitization | `sanitize_path_component` strips controls and separators (`utils.py:6575`); 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:221268` region). |
| JSON artifact validation | `_validate_parsed_identities_json`, `_validate_analysis_json`, `validate_plan_json` (`io.py:157251`). |
| Parallel ffprobe | `ThreadPoolExecutor` when `include_video_metadata` and more than one path (`scanner.py:85101`). |
| 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:321377` |
| F2 by_quality fallback | `duplicate_resolve.py:4043` |
| F3 path_to_index | `planner.py:108111` |
| F4 raise + no try in loop | `quarantine.py:116129`, `executor.py:110112` |
| F5 find returncode | `scanner.py:178192` |
| F6 pyproject optional | `pyproject.toml:1220` |
| F7 validate_plan_json returns dict | `io.py:224251` |
| F8 unknown strategy | `duplicate_resolve.py:3648` |
| 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.