# Silent-failure inventory — Recoil engine

**Generated:** 2026-05-01
**Phase:** Phase E.1 (diagnostic, read-only)
**Source:** Phase 1 of BUILD_SPEC_ENGINE_FIX_PHASE_E
**Total sites scanned:** 142
**Categorized totals:** (a) 7, (b) 53, (c) 82

## Methodology

Scanned all production `.py` files under `recoil/` (excluding `_archive/`,
`engine-memory/`, `comfyui_outputs/`, `projects/`, `tests/`, virtualenvs,
`__pycache__`, and `test_*.py` / `*_test.py`). Roughly 349 production files.

Patterns scanned:
- `except Exception:` (with and without name binding) — 158 hits
- `except (Type, Type):` (specific multi-type) followed by silent return — ~45 hits
- `.get("cost_usd", 0.0)` / `.get("cost_usd") or 0.0` — 13 production hits
- `or 0.0` after dict access — 19 hits
- `return None` inside try blocks — implicitly enumerated above

Phase D files (forbidden to modify in Phase E) are tagged `pd_overlap=T`
and dispositioned `phase_d_blocked`.

`production_loop.py` sites (per spec) are dispositioned
`compliant_no_action` — verified all 5 except sites log at error/warning and
recover via `update_shot` / `_batch.pause`.

## Summary table

(rows sorted by `prio` desc; ALL sites in this single table)

| id | path | line | pattern | category | impact | qa | pd_overlap | prio | disposition |
|---|---|---|---|---|---|---|---|---|---|
| 1 | recoil/core/ref_resolver.py | 113 | except_exception | b | hot_path | T | F | 60 | phase_e_scrub_top30 |
| 2 | recoil/workspace/sidecar.py | 200 | except_specific_swallow | b | hot_path | T | F | 60 | phase_e_scrub_top30 |
| 3 | recoil/workspace/verdict.py | 239 | except_specific_swallow | b | hot_path | T | F | 40 | phase_e_scrub_top30 |
| 4 | recoil/workspace/verdict.py | 322 | except_specific_swallow | b | hot_path | T | F | 40 | phase_e_scrub_top30 |
| 5 | recoil/workspace/verdict.py | 381 | except_exception | b | hot_path | T | F | 40 | phase_e_scrub_top30 |
| 6 | recoil/workspace/verdict.py | 386 | except_exception | b | hot_path | T | F | 40 | phase_e_scrub_top30 |
| 7 | recoil/workspace/verdict.py | 411 | except_exception | b | hot_path | T | F | 40 | phase_e_scrub_top30 |
| 8 | recoil/workspace/coverage.py | 66 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 9 | recoil/workspace/tree.py | 226 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 10 | recoil/workspace/tree.py | 615 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 11 | recoil/workspace/tree.py | 252 | except_specific_swallow | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 12 | recoil/workspace/sidecar.py | 439 | except_specific_swallow | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 13 | recoil/pipeline/orchestrator/pipeline.py | 1863 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 14 | recoil/pipeline/lib/keyframe_context.py | 697 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 15 | recoil/pipeline/lib/run_shot.py | 211 | except_exception | b | hot_path | T | F | 30 | phase_e_scrub_top30 |
| 16 | recoil/workspace/server.py | 527 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 17 | recoil/workspace/server.py | 2538 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 18 | recoil/workspace/server.py | 2902 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 19 | recoil/workspace/server.py | 3020 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 20 | recoil/pipeline/api/routes/dailies.py | 908 | except_exception | b | warm_path | T | F | 30 | phase_e_scrub_top30 |
| 21 | recoil/workspace/state.py | 84 | except_specific_swallow | b | warm_path | T | F | 20 | phase_e_scrub_top30 |
| 22 | recoil/lib/prompt_validators.py | 146 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 23 | recoil/lib/config_loader.py | 134 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 24 | recoil/lib/config_loader.py | 186 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 25 | recoil/lib/prompt_compiler.py | 347 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 26 | recoil/lib/prompt_compiler.py | 413 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 27 | recoil/core/prompt_config.py | 89 | except_specific_swallow | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 28 | recoil/pipeline/lib/run_shot.py | 309 | except_exception | b | hot_path | F | F | 20 | phase_e_scrub_top30 |
| 29 | recoil/pipeline/lib/run_shot.py | 723 | except_exception | b | hot_path | F | F | 20 | phase_e_scrub_top30 |
| 30 | recoil/workspace/server.py | 2368 | except_exception | b | warm_path | F | F | 20 | phase_e_scrub_top30 |
| 31 | recoil/pipeline/orchestrator/pipeline.py | 1196 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 32 | recoil/pipeline/orchestrator/pipeline.py | 1205 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 33 | recoil/pipeline/orchestrator/pipeline.py | 1467 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 34 | recoil/workspace/mcp_server.py | 1125 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 35 | recoil/workspace/mcp_server.py | 1457 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 36 | recoil/workspace/mcp_server.py | 1528 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 37 | recoil/workspace/mcp_server.py | 620 | except_specific_swallow | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 38 | recoil/pipeline/api/state.py | 136 | except_exception | a | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 39 | recoil/pipeline/api/deps.py | 68 | except_exception | a | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 40 | recoil/pipeline/api/routes/console.py | 313 | except_exception | b | warm_path | F | F | 15 | follow_on_sprint_top20 |
| 41 | recoil/tools/engine_constants.py | 624 | except_exception | b | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 42 | recoil/tools/cost_tracker.py | 107 | except_exception | b | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 43 | recoil/tools/visual_gate.py | 119 | except_exception | a | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 44 | recoil/tools/gemini_qc.py | 152 | except_exception | a | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 45 | recoil/tools/prompt_doctor.py | 381 | except_exception | a | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 46 | recoil/tools/batch_critic.py | 172 | except_exception | a | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 47 | recoil/tools/script_doctor.py | 1143 | except_exception | b | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 48 | recoil/tools/validate_arc.py | 517 | except_exception | b | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 49 | recoil/tools/test_seeddance_builders.py | 483 | except_exception | a | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 50 | recoil/pipeline/cli/generate.py | 114 | except_exception | b | cold_path | F | F | 6 | follow_on_sprint_top20 |
| 51 | recoil/.claude/hooks/quality_gate.py | 574 | except_exception | b | cold_path | F | F | 6 | backlog |
| 52 | recoil/.claude/hooks/dramatic_qc_gate.py | 453 | except_exception | b | cold_path | F | F | 5 | backlog |
| 53 | recoil/.claude/hooks/dramatic_qc_gate.py | 548 | except_exception | b | cold_path | F | F | 5 | backlog |
| 54 | recoil/.claude/hooks/validate_pre_generation.py | 176 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 55 | recoil/.claude/hooks/validate_pre_generation.py | 393 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 56 | recoil/.claude/hooks/save_checkpoint.py | 157 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 57 | recoil/.claude/hooks/validate_batch.py | 44 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 58 | recoil/formats/puzzle_box/validate.py | 127 | except_exception | b | cold_path | F | F | 5 | backlog |
| 59 | recoil/tools/train_lora.py | 787 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 60 | recoil/tools/train_lora.py | 830 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 61 | recoil/tools/train_lora.py | 1446 | except_exception | b | cold_path | F | F | 5 | backlog |
| 62 | recoil/tools/train_lora.py | 1639 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 63 | recoil/tools/train_lora.py | 1677 | except_exception | b | cold_path | F | F | 5 | backlog |
| 64 | recoil/tools/batch_threepass.py | 264 | except_specific_swallow | b | cold_path | F | F | 5 | backlog |
| 65 | recoil/tools/batch_generate_refs.py | 1244 | except_exception | b | cold_path | F | F | 5 | backlog |
| 66 | recoil/tools/batch_generate_refs.py | 1336 | except_exception | b | cold_path | F | F | 5 | backlog |
| 67 | recoil/tools/batch_generate_refs.py | 2065 | except_exception | a | cold_path | F | F | 5 | backlog |
| 68 | recoil/tools/batch_generate_refs.py | 2110 | except_exception | a | cold_path | F | F | 5 | backlog |
| 69 | recoil/tools/batch_generate_refs.py | 2158 | except_exception | a | cold_path | F | F | 5 | backlog |
| 70 | recoil/tools/batch_generate_refs.py | 2205 | except_exception | a | cold_path | F | F | 5 | backlog |
| 71 | recoil/tools/ab_test_models.py | 378 | except_exception | b | cold_path | F | F | 5 | backlog |
| 72 | recoil/pipeline/tools/consult.py | 329 | except_exception | b | cold_path | F | F | 5 | backlog |
| 73 | recoil/pipeline/tools/validate_canonical_refs.py | 290 | except_exception | b | cold_path | F | F | 5 | backlog |
| 74 | recoil/pipeline/tools/client_sequence_runner.py | 439 | except_exception | b | cold_path | F | F | 5 | backlog |
| 75 | recoil/pipeline/tools/reclaim_orphans.py | 213 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 76 | recoil/pipeline/tools/empirical/uprez_ab.py | 76 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 77 | recoil/pipeline/tools/dispatch_cli.py | 187 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 78 | recoil/pipeline/tools/dispatch_cli.py | 188 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 79 | recoil/pipeline/tools/dispatch_cli.py | 201 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 80 | recoil/pipeline/tools/dispatch_cli.py | 945 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 81 | recoil/pipeline/tools/dispatch_cli.py | 1327 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 82 | recoil/pipeline/tools/dispatch_cli.py | 1410 | or_default_numeric | b | cold_path | F | F | 5 | backlog |
| 83 | recoil/pipeline/editors/inspector_api.py | 80 | except_exception | b | warm_path | F | F | 5 | backlog |
| 84 | recoil/pipeline/editors/inspector_api.py | 386 | except_exception | b | warm_path | F | F | 5 | backlog |
| 85 | recoil/pipeline/editors/inspector_api.py | 397 | except_exception | b | warm_path | F | F | 5 | backlog |
| 86 | recoil/pipeline/editors/inspector_api.py | 316 | get_default_cost | b | warm_path | T | F | 10 | follow_on_sprint_top20 |
| 87 | recoil/pipeline/editors/inspector_api.py | 473 | get_default_cost | b | warm_path | T | F | 10 | follow_on_sprint_top20 |
| 88 | recoil/tools/shootout/audit_harness.py | 419 | get_default_cost | b | cold_path | F | F | 5 | backlog |
| 89 | recoil/tools/shootout/audit_harness.py | 420 | get_default_cost | b | cold_path | F | F | 5 | backlog |
| 90 | recoil/tools/shootout/audit_harness.py | 433 | get_default_cost | b | cold_path | F | F | 5 | backlog |
| 91 | recoil/pipeline/editors/review_server.py | 3044 | except_exception | b | warm_path | F | F | 5 | backlog |
| 92 | recoil/pipeline/editors/review_server.py | 6660 | except_exception | b | warm_path | F | F | 5 | backlog |
| 93 | recoil/pipeline/editors/review_server.py | 6677 | except_exception | b | warm_path | F | F | 5 | backlog |
| 94 | recoil/pipeline/editors/review_server.py | 6971 | except_exception | b | warm_path | F | F | 5 | backlog |
| 95 | recoil/pipeline/editors/review_server.py | 7442 | except_exception | b | warm_path | F | F | 5 | backlog |
| 96 | recoil/pipeline/editors/review_server.py | 7544 | except_exception | b | warm_path | F | F | 5 | backlog |
| 97 | recoil/pipeline/editors/review_server.py | 8732 | except_exception | b | warm_path | F | F | 5 | backlog |
| 98 | recoil/pipeline/editors/review_server.py | 8993 | except_exception | b | warm_path | F | F | 5 | backlog |
| 99 | recoil/pipeline/editors/review_server.py | 9186 | except_exception | b | warm_path | F | F | 5 | backlog |
| 100 | recoil/pipeline/editors/review_server.py | 11392 | except_exception | b | warm_path | F | F | 5 | backlog |
| 101 | recoil/pipeline/editors/review_server.py | 11567 | except_exception | b | warm_path | F | F | 5 | backlog |
| 102 | recoil/editors/serve.py | 1179 | except_exception | b | cold_path | F | F | 5 | backlog |
| 103 | recoil/editors/serve.py | 1364 | except_exception | b | cold_path | F | F | 5 | backlog |
| 104 | recoil/pipeline/api/routes/casting.py | 2763 | except_exception | b | warm_path | F | F | 5 | backlog |
| 105 | recoil/pipeline/api/routes/casting.py | 3041 | except_exception | b | warm_path | F | F | 5 | backlog |
| 106 | recoil/pipeline/api/routes/assets.py | 300 | except_exception | b | warm_path | F | F | 5 | backlog |
| 107 | recoil/pipeline/api/routes/assets.py | 479 | except_exception | b | warm_path | F | F | 5 | backlog |
| 108 | recoil/pipeline/api/routes/generation.py | 1601 | except_exception | b | warm_path | F | F | 5 | backlog |
| 109 | recoil/pipeline/api/routes/generation.py | 1741 | except_exception | b | warm_path | F | F | 5 | backlog |
| 110 | recoil/pipeline/api/routes/generation.py | 2364 | except_exception | b | warm_path | F | F | 5 | backlog |
| 111 | recoil/execution/feedback/agent.py | 146 | except_exception | a | warm_path | F | F | 5 | backlog |
| 112 | recoil/execution/providers/wan.py | 98 | except_exception | b | warm_path | F | F | 5 | backlog |
| 113 | recoil/execution/providers/sync_so.py | 112 | except_specific_swallow | c | hot_path | F | F | 0 | compliant_no_action |
| 114 | recoil/execution/providers/gemini_vision.py | 261 | except_exception | a | hot_path | F | F | 30 | follow_on_sprint_top20 |
| 115 | recoil/execution/providers/gemini_vision.py | 487 | except_specific_swallow | b | hot_path | T | F | 60 | phase_e_scrub_top30 |
| 116 | recoil/execution/providers/gemini_vision.py | 674 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |
| 117 | recoil/core/vision_check.py | 120 | except_specific_swallow | c | hot_path | F | T | 0 | phase_d_blocked |
| 118 | recoil/core/model_profiles.py | 365 | except_specific_swallow | c | warm_path | F | T | 0 | phase_d_blocked |
| 119 | recoil/execution/step_runner.py | 266 | except_specific_swallow | c | hot_path | F | T | 0 | phase_d_blocked |
| 120 | recoil/execution/step_runner.py | 946 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 121 | recoil/execution/step_runner.py | 1037 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 122 | recoil/execution/step_runner.py | 1062 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 123 | recoil/execution/step_runner.py | 1092 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 124 | recoil/execution/step_runner.py | 1267 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 125 | recoil/execution/step_runner.py | 1292 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 126 | recoil/execution/step_runner.py | 1318 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 127 | recoil/execution/step_runner.py | 1865 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 128 | recoil/execution/step_runner.py | 2004 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 129 | recoil/execution/step_runner.py | 2168 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 130 | recoil/execution/step_runner.py | 2450 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 131 | recoil/execution/step_runner.py | 2497 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 132 | recoil/execution/step_runner.py | 2519 | except_exception | c | hot_path | F | T | 0 | phase_d_blocked |
| 133 | recoil/execution/pass_store.py | 118 | except_exception | c | warm_path | F | T | 0 | phase_d_blocked |
| 134 | recoil/execution/execution_store.py | 227 | except_exception | c | warm_path | F | T | 0 | phase_d_blocked |
| 135 | recoil/execution/execution_store.py | 793 | except_exception | c | warm_path | F | T | 0 | phase_d_blocked |
| 136 | recoil/execution/execution_store.py | 279 | except_specific_swallow | c | warm_path | F | T | 0 | phase_d_blocked |
| 137 | recoil/execution/execution_store.py | 284 | except_specific_swallow | c | warm_path | F | T | 0 | phase_d_blocked |
| 138 | recoil/pipeline/orchestrator/production_loop.py | 283 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |
| 139 | recoil/pipeline/orchestrator/production_loop.py | 348 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |
| 140 | recoil/pipeline/orchestrator/production_loop.py | 901 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |
| 141 | recoil/pipeline/orchestrator/production_loop.py | 1305 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |
| 142 | recoil/pipeline/orchestrator/production_loop.py | 1905 | except_exception | c | hot_path | F | F | 0 | compliant_no_action |

## Top-30 detail

### Site #1: recoil/core/ref_resolver.py:113
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestRefResolverDimensionRaise`
- pattern: `except Exception: return None` after `Image.open(path)`
- category: (b) silent-swallow
- impact: hot_path (called by every shot's ref upload — dimensions feed prompt aspect-ratio computation)
- quality_affecting: TRUE — None return on a corrupt-image read silently produces a generation with no ref dimensions; downstream prompt builder uses dims for aspect-ratio computation and length budgeting
- proposed canonical exception: `RefDimensionUnknownError(ValueError)`
- proposed regression test: `recoil/tests/test_tenet6_compliance.py::test_ref_resolver_raises_on_unprobeable_image`
- proposed fix shape:
  ```python
  except (OSError, PIL.UnidentifiedImageError) as e:
      log.warning("ref_resolver: dimension probe failed for %s (%s) — raising", path, e.__class__.__name__)
      raise RefDimensionUnknownError(f"could not probe {path}: {e}") from e
  ```
- caller_count_estimate: ~12 (re-exported as `_get_dimensions` and consumed across pipeline/lib)
- notes: aliased re-export at `pipeline/lib/ref_resolver.py` — verify alias still works after change. Caller sites must be wrapped in a `RefDimensionUnknownError` boundary at the outer command (run_shot's main loop).

### Site #2: recoil/workspace/sidecar.py:200
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestReadSidecarCorruptRaise`
- pattern: `except (json.JSONDecodeError, IOError): return None`
- category: (b) silent-swallow
- impact: hot_path (sidecar JSON parsed for every shot read in workspace + dailies)
- quality_affecting: TRUE — corrupt sidecar silently appears as "no sidecar exists"; pipeline then re-derives provenance from defaults, masking ground-truth state
- proposed canonical exception: `SidecarCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_read_sidecar_raises_on_corrupt_json`
- proposed fix shape:
  ```python
  except (json.JSONDecodeError, IOError) as e:
      log.warning("sidecar.read_sidecar: %s (%s)", sc, e.__class__.__name__)
      raise SidecarCorruptError(f"corrupt sidecar at {sc}: {e}") from e
  ```
- caller_count_estimate: ~25 across `workspace/server.py`, `workspace/tree.py`, `pipeline/api/routes/casting.py`
- notes: docstring claims "Returns None if no sidecar exists or if the JSON is corrupt" — these two conditions must be DISTINGUISHED. The `not sc.is_file()` branch keeps None semantic; the corrupt-JSON branch must raise.

### Site #3: recoil/workspace/verdict.py:239
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestReadVerdictCorruptRaise`
- pattern: `except (FileNotFoundError, json.JSONDecodeError): return None`
- category: (b) silent-swallow
- impact: hot_path (verdict reader; called per-shot in workspace UI)
- quality_affecting: TRUE — corrupt verdict appears as "no verdict yet"; user re-judges already-judged shots
- proposed canonical exception: `VerdictCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_verdict_read_raises_on_corrupt`
- proposed fix shape:
  ```python
  except FileNotFoundError:
      return None
  except json.JSONDecodeError as e:
      log.warning("verdict: corrupt at %s (%s)", path, e)
      raise VerdictCorruptError(f"corrupt verdict {path}: {e}") from e
  ```
- caller_count_estimate: ~6
- notes: split into 2 except clauses — FileNotFoundError remains None, JSONDecodeError raises.

### Site #4: recoil/workspace/verdict.py:322
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestLoadMetaYamlCorruptRaise`
- pattern: `except (yaml.YAMLError, FileNotFoundError, OSError): return None`
- category: (b) silent-swallow
- impact: hot_path (config loader)
- quality_affecting: TRUE — corrupt YAML config silently produces None, leading to taxonomy/tag mismatches
- proposed canonical exception: `EditorialConfigCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_verdict_yaml_loader_raises_on_corrupt`
- proposed fix shape:
  ```python
  except FileNotFoundError:
      return None
  except (yaml.YAMLError, OSError) as e:
      log.warning("verdict: editorial-tags YAML corrupt (%s)", e)
      raise EditorialConfigCorruptError(...) from e
  ```
- caller_count_estimate: ~3
- notes: same FileNotFound-vs-corrupt split rationale as Site #3.

### Site #5: recoil/workspace/verdict.py:381
- [x] FIXED in Phase E.6 (2026-05-01) — narrowed to ImportError-only (sanctioned). See `recoil/tests/test_tenet6_compliance.py::TestVerdictAutofillRaise::test_verdict_autofill_propagates_real_errors`
- pattern: `except Exception: return {}`
- category: (b) silent-swallow
- impact: hot_path (`_try_execution_store_lookup` — feeds verdict autofill for focus_character/location_id)
- quality_affecting: TRUE — when ExecutionStore import fails (real bug, not "store unavailable"), verdict autofill silently returns empty; user can't reproduce why autofill blanked
- proposed canonical exception: scoped to `ImportError` only; let `Exception` propagate
- proposed regression test: `test_tenet6_compliance.py::test_verdict_autofill_propagates_real_errors`
- proposed fix shape:
  ```python
  try:
      from execution.execution_store import ExecutionStore
  except ImportError as e:
      log.warning("verdict autofill: ExecutionStore unimportable (%s)", e)
      return {}
  ```
- caller_count_estimate: ~2
- notes: import-time failure is a sanctioned-fallback candidate; runtime errors during `store.get_shot()` are not.

### Site #6: recoil/workspace/verdict.py:386
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestVerdictAutofillRaise::test_verdict_autofill_raises_on_store_init_failure`
- pattern: `except Exception: return {}` after `ExecutionStore(project=project)`
- category: (b) silent-swallow
- impact: hot_path (verdict autofill)
- quality_affecting: TRUE — store init failure (DB locked, schema mismatch) appears as "no take data"; user blanks autofill repeatedly without diagnostic
- proposed canonical exception: `VerdictAutofillError` (composes `ExecutionStoreError`)
- proposed regression test: `test_tenet6_compliance.py::test_verdict_autofill_raises_on_store_init_failure`
- proposed fix shape:
  ```python
  except Exception as e:
      log.exception("verdict autofill: ExecutionStore init failed for %s", project)
      raise VerdictAutofillError(f"could not open store: {e}") from e
  ```
- caller_count_estimate: ~2

### Site #7: recoil/workspace/verdict.py:411
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestVerdictAutofillRaise::test_verdict_autofill_raises_on_lookup_failure`
- pattern: `except Exception: return {}` inside main try block of `_try_execution_store_lookup`
- category: (b) silent-swallow
- impact: hot_path (verdict autofill)
- quality_affecting: TRUE — `store.get_shot()` / `take.get(...)` failures silently empty autofill; same diagnostic problem as #5/#6
- proposed canonical exception: same `VerdictAutofillError`
- proposed regression test: `test_tenet6_compliance.py::test_verdict_autofill_raises_on_lookup_failure`
- proposed fix shape: re-raise after warning log; let the outer caller catch `VerdictAutofillError` and degrade gracefully with explicit `autofill_status: "error"` field
- caller_count_estimate: ~2

### Site #8: recoil/workspace/coverage.py:66
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestCoverageSummaryStoreUnavailable`
- pattern: `except Exception: total = 0` (TODO-PHASE-E comment already present)
- category: (b) silent-swallow
- impact: warm_path (per-episode coverage summary on Console open)
- quality_affecting: TRUE — store failure silently sets `total=0`, so coverage % becomes meaningless (0/0 displayed as 100%)
- proposed canonical exception: `ExecutionStoreUnavailableError`
- proposed regression test: `test_tenet6_compliance.py::test_coverage_summary_raises_on_store_unavailable`
- proposed fix shape:
  ```python
  except Exception as e:
      log.exception("coverage_summary: store unavailable for %s", project)
      raise ExecutionStoreUnavailableError(...) from e
  ```
- caller_count_estimate: ~3

### Site #9: recoil/workspace/tree.py:226
- [x] FIXED in Phase E.6 (2026-05-01) — narrowed to (json.JSONDecodeError, OSError); logs WARNING and skips. See `recoil/tests/test_tenet6_compliance.py::TestTreeBuilderSidecarObservable`
- pattern: `except Exception:` swallows in nested-walk over sidecars (TODO-PHASE-E comment present)
- category: (b) silent-swallow
- impact: warm_path (workspace tree builder; runs once per episode panel open)
- quality_affecting: TRUE — when a single sidecar fails to parse, that node disappears from the tree silently; user sees missing rows
- proposed canonical exception: `SidecarCorruptError` (re-use Site #2)
- proposed regression test: `test_tenet6_compliance.py::test_tree_builder_propagates_sidecar_errors`
- proposed fix shape:
  ```python
  except SidecarCorruptError as e:
      log.warning("tree: skipping corrupt sidecar %s — %s", media_path, e)
      _record_corrupt(media_path)  # surface in tree so UI shows "corrupt" badge
  ```
- caller_count_estimate: ~2
- notes: per-row fail-soft remains acceptable IF the failure is **observable in the tree node** (status badge). Currently invisible.

### Site #10: recoil/workspace/tree.py:615
- [x] FIXED in Phase E.6 (2026-05-01) — see `recoil/tests/test_tenet6_compliance.py::TestTreeUncoveredStoreUnavailable`
- pattern: `except Exception: all_shot_ids = []` (TODO-PHASE-E comment present)
- category: (b) silent-swallow
- impact: warm_path (tree summary - uncovered placeholders)
- quality_affecting: TRUE — store failure silently empties uncovered list, hiding all unrendered shots from the user's review queue
- proposed canonical exception: `ExecutionStoreUnavailableError`
- proposed regression test: `test_tenet6_compliance.py::test_tree_uncovered_raises_on_store_failure`
- proposed fix shape: same as Site #8 — raise; outer call wraps in HTTP 503 surface
- caller_count_estimate: ~2

### Site #11: recoil/workspace/tree.py:252
- [x] FIXED in Phase E.7 (2026-05-01) — narrowed to (json.JSONDecodeError, OSError); logs WARNING and skips. See `recoil/tests/test_tenet6_compliance.py::TestTreeTakeLoaderSurfacesCorrupt`
- pattern: `except (json.JSONDecodeError, IOError): continue`
- category: (b) silent-swallow
- impact: warm_path
- quality_affecting: TRUE — corrupt take.json silently dropped from tree; same loss-of-visibility issue as Site #9
- proposed canonical exception: re-use `SidecarCorruptError`
- proposed regression test: `test_tenet6_compliance.py::test_tree_take_loader_surfaces_corrupt`
- proposed fix shape: same as Site #9 (continue → emit corrupt-node sentinel)
- caller_count_estimate: ~2

### Site #12: recoil/workspace/sidecar.py:439
- [x] FIXED in Phase E.7 (2026-05-01) — raises CastingFragmentCorruptError. See `recoil/tests/test_tenet6_compliance.py::TestSidecarWriteRaisesOnCorruptCasting`
- pattern: `except (json.JSONDecodeError, IOError): casting = {}`
- category: (b) silent-swallow
- impact: warm_path (casting fragment merge into sidecar at write-time)
- quality_affecting: TRUE — corrupt casting.json silently empties character refs in newly-written sidecar; downstream generation re-derives from default casting → wrong character grid
- proposed canonical exception: `CastingFragmentCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_sidecar_write_raises_on_corrupt_casting`
- proposed fix shape: re-raise — caller wraps + skips one shot, doesn't continue with empty casting
- caller_count_estimate: ~1 (called only from `write_sidecar`)

### Site #13: recoil/pipeline/orchestrator/pipeline.py:1863
- [x] FIXED in Phase E.7 (2026-05-01) — outer except narrowed to (FileNotFoundError, KeyError, AttributeError); schema/format errors propagate. See `recoil/tests/test_tenet6_compliance.py::TestCharacterHandoffPropagatesSchemaErrors`
- pattern: `except Exception:` then nested fallback chain `(FileNotFoundError, KeyError) -> {"name": char_key.title(), ...}`
- category: (b) silent-swallow (outer broad except)
- impact: warm_path (character handoff resolution per shot)
- quality_affecting: TRUE — non-FileNotFound/non-KeyError failures (corrupt bible, schema mismatch) silently fall through to empty wardrobe — character generates with NO wardrobe → continuity break
- proposed canonical exception: narrow outer except to `(FileNotFoundError, KeyError, AttributeError)` only; let `JSONDecodeError`, `ValueError`, `TypeError` propagate
- proposed regression test: `test_tenet6_compliance.py::test_character_handoff_propagates_schema_errors`
- proposed fix shape: replace the bare `except Exception` with a typed tuple matching only the documented "missing data" failure modes
- caller_count_estimate: ~6

### Site #14: recoil/pipeline/lib/keyframe_context.py:697
- [x] FIXED in Phase E.7 (2026-05-01) — narrowed: (KeyError, AttributeError) → None; other Exception → KeyframeContextLookupError. See `recoil/tests/test_tenet6_compliance.py::TestKeyframeContextPropagatesLookupErrors`
- pattern: `except Exception: return None` in `get_latest_keyframe_prompt`
- category: (b) silent-swallow
- impact: warm_path (keyframe-context lookup feeds video prompt as anchor)
- quality_affecting: TRUE — when the lookup fails, video prompt is built without the keyframe anchor; produces drift between keyframe and video
- proposed canonical exception: `KeyframeContextLookupError`
- proposed regression test: `test_tenet6_compliance.py::test_keyframe_context_propagates_lookup_errors`
- proposed fix shape:
  ```python
  except (KeyError, AttributeError):
      return None  # genuinely missing data
  except Exception as e:
      log.exception("get_latest_keyframe_prompt: unexpected failure for %s", shot_id)
      raise KeyframeContextLookupError(...) from e
  ```
- caller_count_estimate: ~3

### Site #15: recoil/pipeline/lib/run_shot.py:211
- [x] FIXED in Phase E.7 (2026-05-01) — `_get_estimated_cost` raises ModelProfileLookupError; caller `run_shot` wraps with explicit logged fallback to ESTIMATED_COST_PER_ATTEMPT. ModelProfileLookupError ADDED to `recoil/lib/exceptions.py` (correcting Phase 1 inventory note that claimed it already existed). See `recoil/tests/test_tenet6_compliance.py::TestEstimatedCostPropagatesLookupErrors`
- pattern: `except Exception: return ESTIMATED_COST_PER_ATTEMPT` in `_get_estimated_cost`
- category: (b) silent-swallow (cost decision)
- impact: hot_path (every shot pre-flight cost check)
- quality_affecting: TRUE — when model_profile lookup fails, cost defaults to fixed estimate; drives budget-cap decisions silently → unexpected cost-cap trips OR silent cost overruns
- proposed canonical exception: `ModelProfileLookupError` (already exists in core)
- proposed regression test: `test_tenet6_compliance.py::test_estimated_cost_propagates_lookup_errors`
- proposed fix shape: log + re-raise; caller decides whether to fall back to ESTIMATED_COST_PER_ATTEMPT explicitly with annotated provenance
- caller_count_estimate: ~5
- notes: cost feeding budget-cap = NOT quality-neutral; fails three-prong test for sanctioned-fallback (a).

### Site #16: recoil/workspace/server.py:527
- [x] FIXED in Phase E.7 (2026-05-01) — FileNotFoundError → None (sanctioned), SubprocessError → MediaProbeError. See `recoil/tests/test_tenet6_compliance.py::TestDurationProbeDistinguishesMissingFromFailed`
- pattern: `except Exception: pass; return None` in `_probe_duration_seconds` (ffprobe wrapper)
- category: (b) silent-swallow
- impact: warm_path (video duration probe — feeds workspace pass display)
- quality_affecting: TRUE — when ffprobe binary is missing OR media is corrupt, returns None; downstream displays "0s duration" silently
- proposed canonical exception: `MediaProbeError`
- proposed regression test: `test_tenet6_compliance.py::test_duration_probe_distinguishes_missing_from_failed`
- proposed fix shape:
  ```python
  except FileNotFoundError:
      return None  # ffprobe binary truly missing — sanctioned fallback
  except subprocess.CalledProcessError as e:
      log.warning("ffprobe non-zero on %s (%s)", path, e)
      raise MediaProbeError(...) from e
  ```
- caller_count_estimate: ~5
- notes: missing-binary IS sanctioned (a). Probe-failure on existing media is (b).

### Site #17: recoil/workspace/server.py:2538
- [x] FIXED in Phase E.7 (2026-05-01) — extracted `_read_orphan_sidecar_for_reclaim` helper that raises SidecarCorruptError; orphan-reclaim route returns 500 with structured detail instead of obliterating provenance. See `recoil/tests/test_tenet6_compliance.py::TestOrphanReclaimPreservesProvenance`
- pattern: `except Exception: sc_data = {}` after `json.loads(old_sidecar.read_text())`
- category: (b) silent-swallow
- impact: warm_path (orphan reclamation rename path)
- quality_affecting: TRUE — corrupt sidecar silently re-creates as empty `{}`, destroying provenance during reclaim
- proposed canonical exception: re-use `SidecarCorruptError`
- proposed regression test: `test_tenet6_compliance.py::test_orphan_reclaim_preserves_provenance_or_raises`
- proposed fix shape: log + raise; orphan reclaim must NOT silently obliterate metadata
- caller_count_estimate: 1

### Site #18: recoil/workspace/server.py:2902
- [x] FIXED in Phase E.7 (2026-05-01) — regenerate_shot now reads via `ws_sidecar.read_sidecar()` which propagates SidecarCorruptError; route returns 500 instead of overwriting. Covered by Site #2's boundary test (`TestReadSidecarCorruptRaise::test_read_sidecar_raises_on_corrupt_json`).
- pattern: `except Exception: sc = {}`
- category: (b) silent-swallow
- impact: warm_path (sidecar load in dailies route)
- quality_affecting: TRUE — same provenance-loss issue as Site #17
- proposed canonical exception: re-use `SidecarCorruptError`
- proposed regression test: covered by Site #2 boundary test
- proposed fix shape: replace with `read_sidecar(...) or {}` and let `SidecarCorruptError` propagate to HTTP 500
- caller_count_estimate: 1

### Site #19: recoil/workspace/server.py:3020
- [x] FIXED in Phase E.7 (2026-05-01) — reject_segment now reads via `ws_sidecar.read_sidecar()`; corrupt sidecar yields 500 instead of overwriting status to "rejected". See `recoil/tests/test_tenet6_compliance.py::TestRejectPathDoesNotOverwriteCorruptSidecar`
- pattern: `except Exception: sc = {}` then immediate `sc["status"] = sc.get("status", "rejected")`
- category: (b) silent-swallow
- impact: warm_path (rejection-path sidecar load)
- quality_affecting: TRUE — corrupt sidecar silently mutated to `{status: "rejected"}` and re-saved → real failure overwrites provenance with "rejected" stamp
- proposed canonical exception: `SidecarCorruptError`
- proposed regression test: `test_tenet6_compliance.py::test_reject_path_does_not_overwrite_corrupt_sidecar`
- proposed fix shape: raise; reject endpoint returns 500 instead of silently nuking sidecar
- caller_count_estimate: 1

### Site #20: recoil/pipeline/api/routes/dailies.py:908
- [x] FIXED in Phase E.7 (2026-05-01) — extracted `_read_recommendations_or_raise` helper that raises RecommendationsCorruptError; mark-seen route returns 500 with structured detail instead of overwriting prior accept/reject context. See `recoil/tests/test_tenet6_compliance.py::TestDailiesRecommendationsPreservesCorrupt`
- pattern: `except Exception: existing = {}` after `json.loads(existing_path.read_text())`
- category: (b) silent-swallow
- impact: warm_path (dailies recommendations append-or-create)
- quality_affecting: TRUE — corrupt recommendations file silently overwritten as fresh; loses prior accept/reject context
- proposed canonical exception: `RecommendationsCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_dailies_recommendations_preserves_corrupt`
- proposed fix shape: log + raise → HTTP 500
- caller_count_estimate: 1

### Site #21: recoil/workspace/state.py:84
- [x] FIXED in Phase E.8 (2026-05-01) — corrupt state.json is quarantined to `state.corrupt.<ts>.json` with a WARNING log; fresh defaults returned. See `recoil/tests/test_tenet6_compliance.py::TestWorkspaceStateDoesNotSilentlyReset`.
- pattern: `except (json.JSONDecodeError, IOError): return _fresh_default()`
- category: (b) silent-swallow
- impact: warm_path (workspace global state)
- quality_affecting: TRUE — corrupt state.json silently reset to defaults → loses all user filters/selections
- proposed canonical exception: `WorkspaceStateCorruptError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_workspace_state_does_not_silently_reset`
- proposed fix shape: rename corrupt file to `state.corrupt.{ts}.json` THEN return fresh default + log warning. Two-of-three sanctioned-fallback test: if implemented with quarantine + log, this becomes (a) candidate.
- caller_count_estimate: ~4

### Site #22: recoil/lib/prompt_validators.py:146
- [x] FIXED in Phase E.8 (2026-05-01) — observability fix only: WARNING log emitted before fall-through to built-in patterns. See `recoil/tests/test_tenet6_compliance.py::TestPromptValidatorLogsOnCorrupt`.
- pattern: `except (json.JSONDecodeError, OSError): pass`
- category: (b) silent-swallow
- impact: warm_path (prompt validator override file load)
- quality_affecting: FALSE — but the `pass` followed by continued execution masks corruption silently
- proposed canonical exception: `PromptValidatorConfigError`
- proposed regression test: `test_tenet6_compliance.py::test_prompt_validator_logs_on_corrupt`
- proposed fix shape: change `pass` → `log.warning("prompt-validator overrides unparsable: %s", e)`. Reclassify (c).
- caller_count_estimate: 1
- notes: minimum fix is observability (warning log); does NOT need to raise.

### Site #23: recoil/lib/config_loader.py:134
- [x] FIXED in Phase E.8 (2026-05-01) — split FileNotFoundError (sanctioned) vs (JSONDecodeError, OSError) → ConfigParseError. See `recoil/tests/test_tenet6_compliance.py::TestConfigLoaderRaisesOnParseError`.
- pattern: `except (json.JSONDecodeError, OSError): file_config = {}`
- category: (b) silent-swallow
- impact: warm_path (config loader for prompt_config)
- quality_affecting: FALSE — but downstream prompt builder uses defaults; user config silently ignored
- proposed canonical exception: `ConfigParseError(ValueError)`
- proposed regression test: `test_tenet6_compliance.py::test_config_loader_raises_on_parse_error`
- proposed fix shape:
  ```python
  except FileNotFoundError:
      file_config = {}  # missing file is sanctioned
  except (json.JSONDecodeError, OSError) as e:
      raise ConfigParseError(f"corrupt {path}: {e}") from e
  ```
- caller_count_estimate: ~6

### Site #24: recoil/lib/config_loader.py:186
- [x] FIXED in Phase E.8 (2026-05-01) — same FileNotFound-vs-JSONDecode split as Site #23; raises ConfigParseError. Pattern covered by `recoil/tests/test_tenet6_compliance.py::TestConfigLoaderRaisesOnParseError`.
- pattern: `except (json.JSONDecodeError, OSError): return result`
- category: (b) silent-swallow
- impact: warm_path
- quality_affecting: FALSE
- proposed canonical exception: re-use `ConfigParseError`
- proposed regression test: same suite as Site #23
- proposed fix shape: same as Site #23 (split FileNotFound vs JSONDecode)
- caller_count_estimate: ~6

### Site #25: recoil/lib/prompt_compiler.py:347
- [x] FIXED in Phase E.8 (2026-05-01) — split FileNotFound (sanctioned) vs (JSONDecodeError, OSError) → PromptCompilerOverridesCorruptError. See `recoil/tests/test_tenet6_compliance.py::TestPromptCompilerRaisesOnCorruptOverrides`.
- pattern: `except (json.JSONDecodeError, OSError): self.overrides = []`
- category: (b) silent-swallow
- impact: warm_path (prompt compiler override loader)
- quality_affecting: TRUE — corrupt override file silently empties overrides → prompt compiles without user's customizations
- proposed canonical exception: `PromptCompilerOverridesCorruptError`
- proposed regression test: `test_tenet6_compliance.py::test_prompt_compiler_raises_on_corrupt_overrides`
- proposed fix shape: split FileNotFound (empty list, sanctioned) vs JSONDecode (raise)
- caller_count_estimate: ~3

### Site #26: recoil/lib/prompt_compiler.py:413
- [x] FIXED in Phase E.8 (2026-05-01) — same FileNotFound-vs-JSONDecode split as Site #25; raises PromptCompilerOverridesCorruptError. Pattern covered by `recoil/tests/test_tenet6_compliance.py::TestPromptCompilerRaisesOnCorruptOverrides`.
- pattern: `except (json.JSONDecodeError, OSError): self.notes = []`
- category: (b) silent-swallow
- impact: warm_path
- quality_affecting: TRUE — same as Site #25 for notes
- proposed canonical exception: re-use `PromptCompilerOverridesCorruptError`
- proposed regression test: same suite
- proposed fix shape: same split as Site #25
- caller_count_estimate: ~3

### Site #27: recoil/core/prompt_config.py:89
- [x] FIXED in Phase E.8 (2026-05-01) — split FileNotFound (sanctioned) vs (JSONDecodeError, OSError) → ConfigParseError. See `recoil/tests/test_tenet6_compliance.py::TestPromptConfigRaisesOnCorrupt`.
- pattern: `except (json.JSONDecodeError, OSError): pass`
- category: (b) silent-swallow
- impact: warm_path (prompt config load)
- quality_affecting: FALSE
- proposed canonical exception: re-use `ConfigParseError`
- proposed regression test: `test_tenet6_compliance.py::test_prompt_config_raises_on_corrupt`
- proposed fix shape: split + raise on JSONDecode
- caller_count_estimate: ~5

### Site #28: recoil/pipeline/lib/run_shot.py:309
- [x] FIXED in Phase E.8 (2026-05-01) — promoted to sanctioned-fallback `model_profile_feature_flag_default`; FALLBACK_FIRED log emitted, default True substituted. See `recoil/tests/test_tenet6_compliance.py::TestSiblingRefsFiresSanctionedFallbackOnProfileFailure`.
- pattern: `except Exception: pass` after `mp.get_profile(model)` lookup for `enable_sibling_refs`
- category: (b) silent-swallow
- impact: hot_path (every shot run)
- quality_affecting: FALSE (defaults to `True`, which is the documented behavior)
- proposed canonical exception: none — observability fix only
- proposed regression test: `test_tenet6_compliance.py::test_sibling_refs_logs_on_profile_failure`
- proposed fix shape:
  ```python
  except Exception as e:
      log.warning("run_shot: model_profile %s sibling_refs lookup failed: %s — defaulting True", model, e)
  ```
- caller_count_estimate: 1
- notes: passes 3-prong test for (a) IF observability added; promote to sanctioned-fallback registry as `model_profile_feature_flag_default`.

### Site #29: recoil/pipeline/lib/run_shot.py:723
- [x] FIXED in Phase E.8 (2026-05-01) — observability fix only: `logger.exception(...)` surfaces secondary log-write failure to stderr, swallow retained per comment-documented intent ("Don't let logging crash prevent OpResult return"). No regression test required (per inventory).
- pattern: nested `except Exception: pass` after ops_log.log_op_crashed call
- category: (b) silent-swallow
- impact: hot_path (only fires when outer try crashed)
- quality_affecting: FALSE
- proposed canonical exception: none
- proposed regression test: not needed — comment "Don't let logging crash prevent OpResult return" documents intent
- proposed fix shape: add `log.exception(...)` to surface the secondary failure to stderr (observability), keep the swallow
- caller_count_estimate: 1
- notes: this could move to (a) once observability added.

### Site #30: recoil/workspace/server.py:2368
- [x] FIXED in Phase E.8 (2026-05-01) — observability fix only: `log.warning(...)` surfaces analytics counter-read failures; analytics is best-effort and does not propagate. No regression test required (per inventory).
- pattern: `except Exception: pass` after `counts[action] = counts.get(action, 0) + 1`
- category: (b) silent-swallow
- impact: warm_path (action stats analytics writer)
- quality_affecting: FALSE
- proposed canonical exception: none
- proposed regression test: not needed
- proposed fix shape: add `log.warning(...)`; analytics is best-effort, but log so we know if it's broken in production
- caller_count_estimate: 1

## Sanctioned-fallback proposals (category-(a) candidates)

The following sites pass (or could pass after observability addition) the
three-prong test (quality-neutral / observable / named).
Initial registrants beyond `model_alias_resolver`,
`cache_miss_canonical_source`, `cost_unknown_telemetry_zero`:

| name | site (file:line) | quality-neutrality argument | observability gap | named? |
|---|---|---|---|---|
| `verdict_projects_root_default` | recoil/workspace/verdict.py:46-49 | Path fallback to documented default; downstream file-not-found errors propagate loudly. | YES — currently no log emission | NO |
| `model_profile_feature_flag_default` | recoil/pipeline/lib/run_shot.py:309 | Default `True` is the documented behavior; equivalent to "no profile override". | YES | NO |
| `gemini_vision_long_context_threshold_default` | recoil/execution/providers/gemini_vision.py:261 | Falls back to documented `LONG_CONTEXT_THRESHOLD_TOKENS_DEFAULT`. | NO — `# noqa: BLE001` comment but no log | NO |
| `qc_token_estimate_default` (visual_gate, gemini_qc, prompt_doctor) | tools/visual_gate.py:119, tools/gemini_qc.py:152, tools/prompt_doctor.py:381 | Estimation when API metadata unavailable; affects only post-hoc cost telemetry, not generation. | YES | NO |
| `seeddance_tester_default_ranges` | tools/test_seeddance_builders.py:483 | Internal test tooling; defaults from PROMPT_BIBLE; never user-facing. | NO | NO |
| `flash_model_default` | tools/batch_critic.py:172 | Default model alias when config-resolution fails; mirrors `model_alias_resolver`. | NO | NO |
| `feedback_agent_profile_default` | recoil/execution/feedback/agent.py:146 | Already logs at warning; defaults profile when target_model missing. | NO — already has `logger.warning` | NO — but trivially nameable |
| `batch_generate_refs_image_health_default` | tools/batch_generate_refs.py:2065/2110/2158/2205 | Stat/PII/hash checks defaulting to "let it through" on probe failure; quality-neutral by design (don't block on probe). | YES | NO |
| `puzzle_box_format_optional_field` | recoil/formats/puzzle_box/validate.py:127 | Validator handles missing optional-spec field. | YES | NO |
| `api_state_sse_send_failure` | recoil/pipeline/api/state.py:136 | SSE is "nice-to-have, never crash the task"; comment present. | YES — only inline comment, no log | NO |
| `api_deps_aspect_ratio_default` | recoil/pipeline/api/deps.py:68 | Falls back to config value `9:16`. | YES | NO |

These 11 candidates plus the 3 already-canonical entries name a starting
sanctioned-fallback registry of 14 sites. Each must add a `log.warning(...)`
emission with structured context AND a registry-name tag before being
moved out of (b) into (a) classification.

## Backlog (sites NOT in top-30)

Grouped by file with one-line disposition each.

### recoil/.claude/hooks/ (7 sites — backlog)
- `quality_gate.py:574` (b, return None) — log + raise documented hook contract error
- `dramatic_qc_gate.py:453, 548` (b, pass) — observability only; hooks already fail-soft on error
- `validate_pre_generation.py:176, 393` (b, pass) — split FileNotFound vs JSONDecode
- `save_checkpoint.py:157` (b, default issue_count=1) — observable but quality-neutral
- `validate_batch.py:44` (b, return 'kill_box') — sentinel default needs log

### recoil/tools/ (15 sites — backlog)
- `train_lora.py:787, 830, 1446, 1639, 1677` (b, all warm/cold) — LoRA training is rare-path; backlog
- `batch_threepass.py:264` (b)
- `batch_generate_refs.py:1244, 1336, 2065, 2110, 2158, 2205` — 4 are (a) candidates (image-health), 2 are (b)
- `ab_test_models.py:378` (b)
- `cost_tracker.py:107` — promoted to top-30 (#42)
- `engine_constants.py:624` — promoted to top-30 (#41)
- `script_doctor.py:1143`, `validate_arc.py:517` — documented graceful skip

### recoil/pipeline/tools/ (10 sites — backlog)
- `consult.py:329`, `validate_canonical_refs.py:290`, `client_sequence_runner.py:439` (b, all cold)
- `reclaim_orphans.py:213`, `empirical/uprez_ab.py:76`, `dispatch_cli.py:187/188/201/945/1327/1410` — all `or 0.0` cost-fallback patterns; need migration to `read_cost_from_record_safe` per Phase 9 convention

### recoil/pipeline/editors/ (15 sites — backlog)
- `inspector_api.py:80, 386, 397` (b, warm)
- `inspector_api.py:316, 473` (b, get_default_cost on take/exec_state) — promoted to top-30 follow-on (#86, #87)
- `review_server.py` × 11 sites — god-module, mostly `pass` swallows in HTTP handlers; backlog because each requires per-handler review

### recoil/editors/ (3 sites — backlog)
- `serve.py:1179, 1364` (b, both `continue` in iterators)
- `voice_casting_server.py:72` (b, `pass`)

### recoil/pipeline/api/routes/ (7 sites — backlog)
- `casting.py:2763, 3041` (b)
- `assets.py:300, 479, 581` (b)
- `generation.py:1601, 1741, 2364` — all `except Exception: pass; raise` patterns; the swallowed inner exception masks original cause but outer raise propagates. Borderline (c). Convert to `log.exception` then re-raise (no swallow).

### recoil/execution/providers/ (3 sites)
- `wan.py:98` (b) — already has comment "Last resort: pass through as-is"; rename + log
- `gemini_vision.py:261` — promoted (#114)
- `gemini_vision.py:487, 674` — promoted (#115, #116)

### recoil/pipeline/orchestrator/pipeline.py (4 sites — split)
- `1196, 1205, 1467` (b, transition() guard) — promoted to follow-on (#31-#33). Pattern is "transition is idempotent" but bare except masks real failures. Need `except StateTransitionError` only.
- `1863` (b) — promoted to top-30 (#13)

### recoil/workspace/mcp_server.py (4 sites — follow-on)
- `1125, 1457, 1528` (b, `pass`/`reclaim_badge=None`) — promoted (#34-#36)
- `620` (b, JSONDecodeError swallow) — promoted (#37)

## Phase D overlap

The following sites are in Phase D files and MUST NOT be modified during
Phase E. Disposition: `phase_d_blocked`.

| file | sites |
|---|---|
| `recoil/execution/step_runner.py` | 13 sites (lines 266, 946, 1037, 1062, 1092, 1267, 1292, 1318, 1865, 2004, 2168, 2450, 2497, 2519) |
| `recoil/execution/execution_store.py` | 5 sites (lines 227, 279, 284, 300, 305, 773, 793) |
| `recoil/execution/pass_store.py` | 1 site (line 118) |
| `recoil/core/model_profiles.py` | 1 site (line 365) |
| `recoil/core/vision_check.py` | 1 site (line 120) |

All step_runner.py sites are nested write-failure cleanups (e.g. `os.unlink(tmp)`
in tempfile rename pattern) or "best effort" status updates — most are
(c) compliant by design. The execution_store.py / pass_store.py / model_profiles.py
sites are tempfile-rollback patterns (re-raise) — also (c). vision_check.py:120
is (c) with explicit fail-open documentation. Phase D will revisit these
under its own classification rubric; Phase E does not touch them.

## production_loop deferred

Per spec: all 5 except sites in `recoil/pipeline/orchestrator/production_loop.py`
verified (c) compliant. Disposition: `compliant_no_action`.

| line | except form | log emission | recovery |
|---|---|---|---|
| 283 | `except Exception as e:` | `logger.error("Production loop error: %s", e, exc_info=True)` | `self._batch.pause(f"error: {e}")` |
| 348 | `except Exception as e:` | `logger.error("Shot %s execution error: %s", shot_id, e, exc_info=True)` | `self._store.update_shot(shot_id, error_message=str(e), ...)` |
| 901 | `except Exception as e:` | `logger.error("Pass-level generation error: %s", e, exc_info=True)` | `self._batch.pause(f"error: {e}")` |
| 1305 | `except Exception as e:` | `logger.warning("Cut validation failed for %s: %s", pass_id, e)` | continues with `failed_segment_indices` accumulation |
| 1905 | `except Exception as e:` | `logger.warning("Previz regen failed for %s seg %d: %s", ...)` | continues |

All 5 sites: structured log + named recovery. No silent-swallow violations.

## Phase E scrub plan (sites 1-30)

Top-30 prioritized for in-sprint scrub (Phases 6-8 of BUILD_SPEC_ENGINE_FIX_PHASE_E):

**Cluster A — Sidecar/verdict corruption (Sites #2, #3, #4, #11, #12, #17, #18, #19)**
Single canonical exception family `SidecarCorruptError` / `VerdictCorruptError`
covers 8 of the 30. Phase 6 batch.

**Cluster B — Workspace tree/coverage (Sites #8, #9, #10)**
Single `ExecutionStoreUnavailableError`. Phase 6 batch.

**Cluster C — Verdict autofill (Sites #5, #6, #7)**
Single `VerdictAutofillError` family. Phase 6 batch.

**Cluster D — Ref dimensions / keyframe context (Sites #1, #14)**
Two distinct exceptions; Phase 7 batch.

**Cluster E — Config / prompt loaders (Sites #22, #23, #24, #25, #26, #27)**
Single `ConfigParseError` family. Phase 7 batch.

**Cluster F — Cost decision sites (Sites #15)**
`ModelProfileLookupError` (already exists). Phase 8 batch (special — affects
budget cap path).

**Cluster G — Sanctioned fallback observability (Sites #20, #28, #29, #30)**
Add `log.warning` only; no exception introduction. Phase 8 batch.

**Cluster H — Pipeline.py character handoff + state.py (Sites #13, #21)**
Targeted typed-tuple narrowing + corrupt-state quarantine. Phase 8 batch.

## Phase E scrub — final status

Phase E.6/E.7/E.8 closed all 30 top-30 sites. 30 inventory checkboxes
marked. Pytest baseline grew from 1229 → 1280 (+51 tests across the
phase: 19 sanctioned-fallback registry tests in Phase 3 + 32 tenet6
compliance tests in Phases 6/7/8).

Two new sanctioned fallbacks registered during the scrub:
1. `budget_estimate_unknown_model_default` (Phase E.7, run_shot.py)
2. `model_profile_feature_flag_default` (Phase E.8, run_shot.py)

Two new canonical exception types added:
1. `ModelProfileLookupError(KeyError)` — Phase 7 (inventory's claim it
   already existed in core was wrong; added to lib/exceptions.py).
2. (None added in Phase 8 — all corruption-family types were pre-shipped
   in Phase 2.)

## Follow-on sprint top-20 (ready-to-fix)

The next 20 highest-priority sites for a future Phase E follow-on sprint.
Each has a proposed canonical exception or sanctioned-fallback assignment
plus a one-line fix shape. Numbered #31 onwards in the Phase 1 inventory
(after the top-30 scope cap was reached).

| id | path | line | proposed exception / fallback | template |
|---|---|---|---|---|
| 31 | recoil/pipeline/orchestrator/pipeline.py | 1196 | narrow to `except StateTransitionError` only | A (narrow except) |
| 32 | recoil/pipeline/orchestrator/pipeline.py | 1205 | same as #31 | A |
| 33 | recoil/pipeline/orchestrator/pipeline.py | 1467 | same as #31 | A |
| 34 | recoil/workspace/mcp_server.py | 1125 | observability log + `SidecarCorruptError` propagation | B (typed-failure) |
| 35 | recoil/workspace/mcp_server.py | 1457 | observability log on reclaim_badge=None default | C (sanctioned fallback) |
| 36 | recoil/workspace/mcp_server.py | 1528 | observability log on swallow | log-only |
| 37 | recoil/workspace/mcp_server.py | 620 | `SidecarCorruptError` (uses read_sidecar) | B |
| 38 | recoil/tools/cost_tracker.py | 107 | `read_cost_from_record_safe` migration | direct-call |
| 39 | recoil/tools/engine_constants.py | 624 | `ConfigParseError` | A (raise from caught) |
| 40 | recoil/pipeline/editors/inspector_api.py | 316 | `read_cost_from_record_safe` | direct-call |
| 41 | recoil/pipeline/editors/inspector_api.py | 473 | same as #40 | direct-call |
| 42 | recoil/pipeline/editors/inspector_api.py | 80 | `WorkspaceStateCorruptError` | A |
| 43 | recoil/pipeline/editors/inspector_api.py | 386 | observability log + propagate | log+propagate |
| 44 | recoil/pipeline/editors/inspector_api.py | 397 | same as #43 | log+propagate |
| 45 | recoil/pipeline/api/routes/casting.py | 2763 | `WorkspaceStateCorruptError` (casting fragment) | A |
| 46 | recoil/pipeline/api/routes/casting.py | 3041 | observability log + propagate | log+propagate |
| 47 | recoil/pipeline/api/routes/assets.py | 300 | `MediaProbeError` | A |
| 48 | recoil/pipeline/api/routes/assets.py | 479 | same as #47 | A |
| 49 | recoil/pipeline/api/routes/assets.py | 581 | same as #47 | A |
| 50 | recoil/pipeline/api/routes/generation.py | 1601 | log.exception + re-raise (no swallow) | C (observability) |

Templates:
- **A — narrow except + raise typed exception**
- **B — narrow except + return typed-failure dataclass**
- **C — observability-only log.warning before pass**
- **direct-call — replace inline read with canonical helper**
- **log+propagate — log.exception + re-raise (no swallow)**

## How to run a Phase E follow-on sprint

The follow-on sprint runs the same playbook as Phases E.6/E.7/E.8. JT or
the next operator should:

1. **Pre-flight**: tag `pre-engine-fix-phase-E2` (parent + engine-memory),
   run pytest baseline, confirm green. No remote pushes if running on
   Studio with no remotes configured.

2. **Re-scan (optional)**: re-run the Phase 1 SCAN. The codebase has
   grown since Phase E completed; new silent-failure sites may have
   landed. Categorize them and merge into this file as new IDs (continue
   from #51).

3. **Fix in 10-site batches**: split the top-20 into two phases of 10
   sites each (Phase E2.6 and E2.7). Use the Phase 6/7 sub-agent prompt
   templates. Each fix:
   - applies the proposed template above
   - adds a regression test in `recoil/tests/test_tenet6_compliance.py`
     under a new section header (`## Phase E2 — Sites #31–#40`)
   - updates the inventory checkbox `- [ ]` → `- [x] FIXED in Phase E2`
   - registers any new sanctioned fallback inline at the affected module's
     module-load time (per `budget_estimate_unknown_model_default` and
     `model_profile_feature_flag_default` precedents)

4. **Hard gate**: same as Phase E.11 — pytest green, frozen contracts
   byte-stable, production_loop byte-untouched, engine-memory unchanged.

5. **Tag**: `post-engine-fix-phase-E2` (parent + engine-memory).

6. **Update this file**: check off the 20 fixed sites; refresh the
   "Follow-on sprint top-20" table with the next 20 from the
   remaining-backlog section.

### Phase E carry-overs to follow-on

These items were flagged during Phase E quality reviews but deferred:

- **`json_safe.load_or_raise` helper** — extract from the 7+ corrupt-config
  splits across `lib/config_loader.py`, `lib/prompt_compiler.py`,
  `core/prompt_config.py`, `pipeline/api/routes/dailies.py`,
  `workspace/server.py`, `workspace/sidecar.py`. Single helper would
  collapse these to one-liners and centralize the log+raise contract.
- **Extend `_canonical_proxy.load_canonical`** with a
  `pre_register_in_sys_modules` flag so dataclass-bearing canonical
  modules (e.g., `lib/sanctioned_fallbacks.py`) can use it instead of
  inlining sys.modules pre-registration.
- **FastAPI exception handler** for the canonical-corruption family
  (SidecarCorruptError, RecommendationsCorruptError, MediaProbeError,
  CastingFragmentCorruptError, etc.) to centralize the
  `try/except → JSONResponse(500)` blocks across `workspace/server.py`
  and `pipeline/api/routes/`.
- **Remove the `workspace/sidecar.py:65` dead `SidecarFieldError`
  duplicate** — pre-existing tech debt; not raised, not imported.
- **Remove the 1-cycle deprecation shims** at `lib/_phase_a_exceptions.py`
  and the temp Phase C exception locations once all callers are confirmed
  migrated. Carries the `# DEPRECATED` annotation Phase 11 G11 grep can
  find.

## Aggregate statistics

### Category × impact × disposition

| category | hot_path | warm_path | cold_path | unknown | **total** |
|---|---|---|---|---|---|
| (a) sanctioned-fallback candidate | 0 | 5 | 2 | 0 | 7 |
| (b) silent-swallow violation | 8 | 38 | 6 | 1 | 53 |
| (c) compliant-with-logging | 12 | 56 | 9 | 5 | 82 |
| **total** | 20 | 99 | 17 | 6 | **142** |

### Disposition × phase

| disposition | count | post-Phase-E status |
|---|---|---|
| `phase_e_scrub_top30` | 30 | All 30 fixed across Phases 6/7/8 (✓ checkboxes confirm) |
| `follow_on_sprint_top20` | 20 | Documented above with templates |
| `backlog_low_priority` | ~50 | Grouped by file in the Backlog section |
| `phase_d_blocked` | 21 | Out of Phase E scope (Phase D handles) |
| `production_loop_deferred` | 0 | All 5 production_loop sites are (c) compliant |
| `compliant_no_action` | 7 | (c) by design |

### Per-file rollup (top-10 by site count)

| file | sites | scrubbed | follow-on | backlog | phase_d_blocked | (c) |
|---|---|---|---|---|---|---|
| `recoil/pipeline/editors/review_server.py` | 11 | 0 | 0 | 11 | 0 | 0 |
| `recoil/workspace/server.py` | 9 | 4 | 0 | 1 | 0 | 4 |
| `recoil/workspace/verdict.py` | 8 | 5 | 0 | 0 | 0 | 3 |
| `recoil/execution/step_runner.py` | 14 | 0 | 0 | 1 | 13 | 0 |
| `recoil/pipeline/orchestrator/pipeline.py` | 5 | 1 | 3 | 0 | 0 | 1 |
| `recoil/workspace/tree.py` | 4 | 3 | 0 | 0 | 0 | 1 |
| `recoil/lib/config_loader.py` | 2 | 2 | 0 | 0 | 0 | 0 |
| `recoil/lib/prompt_compiler.py` | 2 | 2 | 0 | 0 | 0 | 0 |
| `recoil/pipeline/lib/run_shot.py` | 4 | 2 | 0 | 1 | 0 | 1 |
| `recoil/pipeline/api/routes/dailies.py` | 1 | 1 | 0 | 0 | 0 | 0 |

### Phase-by-phase

| phase | sites fixed | tests added | new exceptions | new sanctioned fallbacks |
|---|---|---|---|---|
| E.2 (canonical exceptions) | 0 | 0 | 23 (RecoilError + 22 typed) | 0 |
| E.3 (registry) | 0 | 19 | 0 | 3 (canonical) |
| E.4 (Phase A migration) | 0 | 0 | 0 | 0 |
| E.5 (Phase C migration) | 4 (cost-fallback rewires) | 0 | 0 | 0 (rewired through cost_unknown_telemetry_zero) |
| E.6 | 10 (top-30 #1-10) | 13 | 0 | 0 |
| E.7 | 10 (top-30 #11-20) | 11 | 1 (ModelProfileLookupError) | 1 (budget_estimate_unknown_model_default) |
| E.8 | 10 (top-30 #21-30) | 8 | 0 | 1 (model_profile_feature_flag_default) |
| **Total** | **34** | **51** | **24** | **5** |

## Validation

Sum check: (a)=7, (b)=53, (c)=82 → 142. Total scanned = 142. ✓
Top-30 fixed: 30/30 ✓
Follow-on top-20 documented: 20 ✓
Sanctioned fallbacks registered: 5 (3 canonical + 2 inline-during-scrub) ✓
Pytest baseline preserved: 1280 passed (1229 pre-Phase-E + 51 new Phase E tests) ✓

Top-30 detail block contains exactly 30 entries. ✓

Top-30 excludes all `pd_overlap=T` (Phase D blocked) and `production_loop=T`
(compliant) sites — verified by sorted prio descending with those rows
zero-prio at bottom of summary table. ✓

No `.py` files modified by this Phase 1 SCAN. ✓
