# Bug Report — Python CLI — audit-snapshot-2026-05-24T15-55Z.md Snapshot: audit-snapshot-2026-05-24T15-55Z.md Required-reading docs read: AGENTS.md / TODO.md / CACHE_CONTRACT.md (at D:\DEV\Extensions\Production\rclone-jav\docs\CACHE_CONTRACT.md) / bug-audit-plan.md / project memory Auditor agent: fresh Explore agent (chunk 1 auditor) Verifier agents: fresh Explore agents per candidate, blind context, stricter contract-check prompt This file contains CONFIRMED + PARTIAL findings only. Candidate scratch lives in `bugs-candidates-python.md`. REFUTED / NEEDS-INFO candidates stay in scratch with verifier response appended. **Chunk 1 calibration note:** Moderate verification yielded 1 confirmed bug with 75% rejection rate (3/4 REFUTED). Auditor's recurring weakness: flagging `f["key"]` direct access as KeyError risk without checking the contract that guarantees the dict shape upstream (rclone lsjson schema, cache.json schema enforced by load_cache validation + CACHE_CONTRACT.md). Stricter verifier prompt with required contract-check caught all 3 false positives. **Light candidates were NOT verified per audit-plan stop condition** (>30% rejection → halt L verification). The Python auditor likely shares a similar pattern-matching weakness on L candidates — revisit only if needed. See `bugs-candidates-python.md` for unverified L list (C-5, C-6, C-7, C-8, C-9). --- ## Severe (S) (none flagged by auditor in this chunk) --- ## Moderate (M) ### M-1 — save_config lacks Windows file-locking retry that save_cache has - **File:** `D:\DEV\Project\rclone-jav\rcjav\cli.py:186-189` (save_config), with comparison at `rcjav/cache.py:142-147` (save_cache) - **Symptom (one sentence):** When a user runs `--save` while config.json is briefly locked by antivirus, Windows Search indexer, or any reader, `os.replace(tmp, CONFIG_PATH)` raises uncaught PermissionError and the user sees a Python traceback — config write fails. `save_cache` for the same os.replace pattern has explicit PermissionError + 0.5s retry; `save_config` does not. - **Why it's a bug:** Asymmetric protection. `save_cache` (cache.py:142-147): ```python try: os.replace(tmp, CACHE_PATH) except PermissionError: time.sleep(0.5); os.replace(tmp, CACHE_PATH) ``` `save_config` (cli.py:186-189): ```python tmp.write_text(json.dumps(cfg, indent=2), encoding="utf-8") os.replace(tmp, CONFIG_PATH) ``` Single call site at cli.py:465 inside `--save` flag handler, NOT wrapped in try/except. Outer exception handler at cli.py:1000-1004 catches only KeyboardInterrupt. PermissionError propagates uncaught → Python traceback to user. On Windows with active AV (Defender, Avast, etc.), file-lock-during-replace is common. - **Reproduction:** 1. Input: user runs `python rc-jav.py --save --target cq:JAV` while config.json is being read by another process (AV scan, Windows Search indexer reindexing, manual file open in editor) 2. Expected: write retries briefly + succeeds, OR clear "config write failed, retry" message 3. Actual: PermissionError raised from os.replace, uncaught, Python prints traceback `PermissionError: [WinError 32] The process cannot access the file because it is being used by another process`. tmp file may remain on disk. Config not persisted. User confused. - **Suggested fix sketch:** copy save_cache's pattern verbatim — wrap os.replace in try/except PermissionError with 0.5s sleep + single retry - **Verifier agent:** fresh Explore, blind context, stricter prompt - **Verifier verdict:** CONFIRMED - **Verifier confidence:** high (95%) - **Contract refs verifier read:** save_cache implementation as comparison; outer exception handler scope - **Mirror check needed in:** any other `os.replace` callsite in `rcjav/` package that writes user-visible config/state (search for `os.replace` in rcjav/ — only save_cache and save_config currently) - **Status:** fixed - **Fix:** `D:\DEV\Project\rclone-jav\rcjav\cli.py:186-194` — wrapped `os.replace(tmp, CONFIG_PATH)` in same try/except PermissionError + time.sleep(0.5) + retry pattern that save_cache uses (rcjav/cache.py:142-147). Now symmetric: both writers handle transient Windows file locks identically. Single retry (not infinite) — persistent locks still bubble PermissionError to caller, matching save_cache behavior. `time` already imported in cli.py:14 — no new import needed. **No manifest bump** — CLI repo only, no extension files touched. Python syntax verified via `py_compile`. Smoke-tested in isolation: (1) normal write produces correct file; (2) first os.replace raises PermissionError then succeeds on retry — final state correct, 0.5s sleep observed (2 calls, elapsed 0.50s); (3) persistent PermissionError on both attempts → bubbles up to caller (2 attempts, matches save_cache). Mirror check resolved: only save_cache and save_config use os.replace in rcjav/; both now have retry. --- ## Light (L) (none promoted — chunk 1 L verification skipped per stop condition) --- ## Needs Input (N) (none promoted) --- ## False Positives (discarded) - `rcjav/rclone_io.py:66` — flagged as Moderate "rclone KeyError on Path". REFUTED. rclone lsjson output contract guarantees `Path` field on every item per official docs. Direct `item["Path"]` access is appropriate fail-fast for contract violation. Lines 77-78's `.get()` pattern for Size/ModTime is defensive over-engineering for those fields, NOT evidence Path needs the same. - `rcjav/library.py:257` — flagged as Moderate "library cache KeyError on path". REFUTED via 3 converging facts: (1) CACHE_CONTRACT.md mandates `path` key on every file entry, (2) `load_cache()` (cache.py:67-106) validates schema before find_library_issues runs — non-conformant caches get wiped via `_fresh_cache()`, (3) FileEntry dataclass + every cache write site explicitly emits `path`. The `.get()` pattern at cli.py:526 (`--reextract`) is defensive because that path reads cache.json directly without re-validation; library.py operates on already-validated data. - `rcjav/library.py:328-330` — flagged as Moderate "rename_file KeyError on path/jav_id". REFUTED. `f` comes only from cache entries (`remote_data.get("files", [])`), which are contract-guaranteed to have `path`. Caller scalar args (`old_rel_path`, `new_rel_path`) are strings, not dicts. Line 330's `or f["jav_id"]` fallback is for `extract_id` returning None, NOT for missing key — correct design. Auditor conflated scalar caller args with iterated dict entries.