Records the implementation plan derived from the 2026-08-23 review: per-task acceptance criteria, the verification baseline, and environment constraints. Lives in docs/reviews/ so it is discoverable from the repo rather than from session state, and is indexed from docs/reviews/README.md. Non-canonical, like everything under docs/reviews/**. Co-authored-by: Copilot App <[email protected]>
13 KiB
Handoff Brief — Phases 2-5 of the 2026-08-23 Code Review
Repo: C:\Github\transcription · Branch: traumatized · Baseline commit: de18c2e
Source of truth: docs/reviews/2026-08-23-code-review.md (§9 Prioritized Action Plan)
Phase 1 is done and committed. This brief covers everything after it.
Status note. This is a dated, non-canonical artifact, like everything under
docs/reviews/**. It records a plan, not a contract. Where it disagrees with.github/instructions/**or the canonical docs, they win.
1. Environment — read this first
uvproject on Windows / PowerShell.ruff,ty, andpytestare not on PATH. Always prefix withuv run.- PowerShell has no heredoc. Don't write
python - <<'PY'. Usepython -c "..."or pipe a single-quoted here-string (@'…'@ | python -). &&only chains external commands in PowerShell. Use;before PowerShell keywords.- Ruff config (
ruff.toml): line length 120,force-single-line = true— one import per line. Never combine imports. # noqa: PLR0915/PLR1702is established repo convention; don't strip existing ones.
2. Mandatory reading before editing src/transcription/**
The repo's instruction table requires these before source edits. They are contracts, not suggestions:
.github/instructions/services.instructions.md— transaction boundaries, model ownership, no service-to-service imports.github/instructions/error-handling.instructions.md— error categories, retriability, user-safe messaging.github/instructions/ui.instructions.md— page/component boundaries.github/instructions/documentation-sync.instructions.md— docs must be updated in the same change when contracts or behavior change
3. Verification commands
uv run ruff check . # must be clean
uv run pytest -q -m "not external" # must be 381+ passing
uv run ty check # baseline is exactly 10 diagnostics
The ty baseline is 10, and all 10 are false positives — SQLModel/SQLAlchemy column
descriptors typed as UUID/datetime/bool, so .is_(), .asc(), func.count(), and
group_by() appear invalid. They are in services/photos.py (8) and
tests/test_storage_reconciliation.py (2). Do not "fix" these by changing code. Handling
them is task P2-1 below, and the fix is suppression comments, not code edits.
Hard-won gotcha: typing.Mapping trips ruff's deprecated-import, and collections.abc.Mapping
doesn't satisfy ty for SQLAlchemy row results. Sequence[RowMapping] + RowMapping is the
only spelling that satisfies both. Don't rediscover this.
4. What Phase 1 changed (context you need)
Commit de18c2e. Four things, all with tests:
-
workflows.pycommit boundary.process_queued_jobnow commits every page except the last individually, then defers the final page's write into_finalize_batch_outcomeso it shares the terminal-status transaction._finalize_batch_outcomegained afinal_pagekwarg. If you touch this function, that parameter is load-bearing.- Two invariants are in tension here — preserve both. Intermediate pages must stay
individually durable (guarded by
test_workflows_reliability.py::...::test_transcribed_page_is_committed_before_next_provider_call_finishes), and the final page must be atomic with the terminal status (guarded bytests/integration/test_pipeline_atomicity.py). Do not collapse the whole batch into one transaction to simplify things — that breaks multi-page durability.
-
AppErrorgained an internal-onlydetailfield.messageis user/API-facing and must stay generic;detailcarries the root cause and flows into evidence records viaformat_error_detailand into logs. Documented indocs/error_handling.md§"Message vs detail split". When adding error paths: never put exception text intomessage. -
8
ui.notifyerror sites replaced withshow_errorinhome_page.py/people_page.py. -
New AST guard
test_ui_boundaries.py::test_no_page_hand_rolls_error_notifications.
5. Phase 2 — Enforcement hardening
P2-1 · ty suppression policy, then make the hook blocking
Report ref: LOW-05 · Effort: M
.pre-commit-config.yaml currently runs ty in advisory mode (a Python subprocess wrapper
that forces sys.exit(0)) because of the 10 known false positives. Net effect: a genuine new
type error prints alongside the known 10 and blocks nothing.
- Add a targeted
# ty: ignore[<rule>]at each of the 10 sites, each with a one-line comment explaining it's a SQLAlchemy descriptor false positive. - Confirm
uv run ty checkreports 0. - Flip the pre-commit hook to blocking (drop the
sys.exit(0)wrapper). - Document the policy in
docs/— when a suppression is acceptable and what the comment must say.
Acceptance: uv run ty check → 0 diagnostics; introducing a deliberate type error fails
git commit; revert the deliberate error afterward.
P2-2 · ruff format enforcement
Report ref: LOW-06 · Effort: S
~35 files have formatting drift. Two separate commits, in this order:
uv run ruff format .— formatting only, no other changes in this commit.- Add
ruff format --checkto.pre-commit-config.yaml.
Keeping these separate matters: a mixed commit makes the formatting noise unreviewable.
Acceptance: uv run ruff format --check . clean; full suite still 381+.
P2-3 · Enable ruff ruleset G (flake8-logging-format)
Report ref: LOW-09 · Effort: S
f-strings in logging calls format eagerly regardless of level and break template grouping in
structured backends. Known instance: workflows.py:193. Enable G in ruff.toml, then convert
offenders to %s lazy args: logger.error("Job %s failed.", job.id).
Acceptance: uv run ruff check . clean with G enabled.
6. Phase 3 — Reliability & concurrency
P3-1 · Periodic stale-job recovery
Report ref: MED-01 · Effort: M
requeue_stale_processing_jobs has exactly one caller — app.py:79, in the lifespan startup
handler. There is no runtime re-check. The threshold reuses worker_provider_timeout_seconds
(30.0s, config.py:116).
The failure mode: a job orphaned <30s before a fast restart fails the staleness predicate at the
only moment recovery runs, so it stays PROCESSING forever (the worker only claims QUEUED).
Restarts are exactly when orphans are created, so the recovery window is systematically
misaligned with the failure it exists to handle.
- Add
worker_stale_job_secondstoSettings(don't keep overloading the provider timeout — they need independent tuning). Update.env.examplein the same change (required byservices.instructions.md). - Run the sweep periodically in the worker loop, in addition to the startup call.
- Test: a job left
PROCESSINGpast the threshold is requeued without a restart.
P3-2 · Gate retries on error category
Report ref: MED-02 · Effort: S
workflows.py:184-194 gates only on job.retry_count < settings.worker_max_retries. It never
consults error_category or AppError.retriable, so validation / not_found / conflict
failures would retry to exhaustion, burning provider quota on calls that cannot succeed.
Latent today because worker_max_retries defaults to 0 — which is exactly why this must be
fixed before anyone raises that value. Add backoff too; retries currently requeue immediately.
Acceptance: a validation-category failure is not requeued even with worker_max_retries=1.
P3-3 · Shutdown budget derived from provider timeout
Report ref: MED-04 · Effort: S
worker.py:146 waits 2.0s for the worker task, but an in-flight provider call may run 30s and
the stop event is only checked between jobs. Derive the budget from
worker_provider_timeout_seconds plus a small grace. Document the relationship to the container
termination grace period in docs/production-runbook.md.
P3-4 · Handle IntegrityError on the attempt-number flush
Report ref: MED-03 · Effort: M
sources.py:540-546 computes MAX(attempt_number) + 1; uq_execution_attempt_number enforces
uniqueness. The sibling JobSource insert catches IntegrityError at sources.py:531-534, but
the attempt flush() at sources.py:587 does not — a race loses an evidence row.
Not reachable today (single worker, sequential sources). It becomes reachable the moment a
second worker replica is deployed — treat this as a hard precondition for horizontal scaling
and note that in docs/production-runbook.md.
Mirror the JobSource handling: catch, recompute, retry bounded, raise a domain error on
exhaustion.
P3-5 · Move blocking work off the event loop
Report ref: LOW-01, LOW-02, LOW-03 · Effort: S
store.py:401—hashlib.sha256(file_bytes)is CPU-bound on the loop. Wrap inasyncio.to_thread.homepage_store.py:25,32— sync file I/O called from async page handlers. Same fix.app.py:62—poll_interval_seconds=1.0hardcoded. Move toSettings; update.env.example.
Every other I/O path already uses to_thread (media_storage.py:43, normalization.py:117,
photos.py:176, sources.py:740,753) — follow those.
7. Phase 4 — Consolidation
P4-1 · Single transaction entry point
Report ref: MED-05 · Effort: M
workflows.py opens transactions via services.jobs._session_scope() and
services.sources._session_scope() — private members of two different services. This is the
mechanism that made HIGH-01 easy to introduce: nothing in the design signals that two scopes are
being opened for one logical unit of work.
Introduce unit_of_work(services, session) (proposed signature in review §7), migrate
workflows.py onto it, then add an AST guard to test_service_boundaries.py forbidding
_session_scope access outside its owning module. Note the existing test checks imports, not
attribute access, so it can't currently see this.
Do not attempt this before Phase 1's tests are green in your working tree — it touches the same functions.
P4-2 · Extract shared helpers
Report ref: §7 · Effort: M
run_blocking and insert_with_sequence_retry, per the review's proposed signatures. Do this
after P3-4 and P3-5, so the call sites exist.
P4-3 · Prune low-signal tests
Report ref: LOW-10, LOW-11 · Effort: S
Full table in review §6 "Prune/strengthen backlog". Highlights:
test_traceability.py:54-57— asserts properties of dict literals in the same file.test_pipeline_flow.py:135-140,446-452—assert processed is Trueis unfalsifiable (read_jobraises rather than returningNone).test_orphan_sweep.py:119—>= 200snapshot threshold tolerates ±40 drift.test_workflows_reliability.py:157-196— realtime.sleep(0.40)with only 10% slack; flaky under CI load.
8. Phase 5 — Governance
- P5-1 — Extend
test_orphan_sweep.pybeyond module-level definitions to public methods (LOW-08); seedKNOWN_ORPHANSwith current results to keep it non-breaking. - P5-2 — Resolve the 4 "uncertain" orphans (LOW-07). Note
summarize_errorshould now be reachable — consider using it, or delete it. - P5-3 — Log incomplete request manifests instead of silently returning
None(openrouter.py:347, LOW-04). - P5-4 — Consider inverting the UI boundary check from a forbidden-list to an allowlist; a future persistence helper under a new name currently escapes it.
9. Ground rules
- Red first. For any behavioral fix, write the test, run it, observe it fail, then fix. That's how Phase 1 caught that its own first HIGH-03 attempt was wrong.
- One phase per commit series. Don't mix P2-2's formatting sweep with logic changes.
- Docs in the same change. Required by
documentation-sync.instructions.mdwhenever contracts, behavior, orSettingschange.Settingschanges additionally require.env.exampleupdates in the same commit. - Don't widen the NiceGUI pin.
nicegui==3.13.0is a deliberate release-stability decision recorded indocs/production-runbook.md. It is explicitly not a defect. docs/reviews/**is not canonical. It's a dated artifact; don't treat it as a contract the waydocs/schema.mdor the instruction files are.- Ask before scope-expanding. If a fix seems to require restructuring beyond its task, stop and confirm — that's the signal a Phase boundary is being crossed.
10. Suggested first command
cd C:\Github\transcription
git log --oneline -3
uv run ruff check . ; uv run pytest -q -m "not external" ; uv run ty check
Confirm the baseline (clean ruff, 381+ passing, exactly 10 ty diagnostics) before changing
anything. If that doesn't reproduce, stop and report rather than proceeding.