The handoff brief was a work order for phases 2-5. That work is done, so the document now describes a future that already happened and would misdirect anyone who found it. The review report itself had the same problem in weaker form: its findings read as open. Adds a status banner marking it closed and retained for reasoning only. The banner also records that two of its recommendations were wrong on contact. The HIGH-03 fix as written would have stripped root-cause data from ExecutionAttempt provenance, and the HIGH-01 fix had to preserve per-page durability the report never mentioned. Leaving that unstated invites someone to 'restore' the report's version later. Co-authored-by: Copilot App <[email protected]>
47 KiB
Architecture & Code Review Report
Repository Target: transcription/
Target Stack: Python 3.12+ | FastAPI | NiceGUI | SQLModel/SQLAlchemy | Pydantic V2 | asyncio | OpenRouter
Review date: 2026-08-23
Governing procedure: .github/skills/python-code-reviewer/skill.md
Escalations applied: .github/skills/evidence-provenance-auditor/skill.md, .github/skills/test-effectiveness-auditor/skill.md
Scope: 77 Python modules / ~13k LOC under src/transcription, 57 test files (377 collected non-external tests), 23 documents under docs/, 9 active rule files.
Status: closed. Every finding below was remediated in the phases following this review. This document is retained as a record of the reasoning, not as a list of open work, and it is not canonical authority.
Two recommendations were wrong on contact and were corrected during implementation: the HIGH-03 fix as written would have stripped root-cause data from
ExecutionAttemptprovenance, and the HIGH-01 fix needed to preserve per-page durability that the report did not mention. Where this text and the current code or guard tests disagree, the code and tests are correct.
Verification commands and outcomes
| Command | Outcome |
|---|---|
uv run ruff check . |
Pass — All checks passed! |
uv run pytest -q -m "not external" |
Pass — 377 passed |
uv run ty check |
10 diagnostics — all SQLModel/SQLAlchemy column-descriptor false positives (services/photos.py ×8, tests/test_storage_reconciliation.py ×2). Advisory only; no suppression strategy exists. |
1. Executive Summary
- Overall health is good. The codebase has genuine architectural discipline: layered
ui → services → db, a single Pydantic-V2 settings source, an atomic compare-and-swap job claim, append-only evidence history, and eleven deterministic guard tests that enforce structural rules rather than describing them. - No Critical findings. The highest-risk category for this domain — secret leakage into stored provenance — was explicitly audited and passes: request headers are never persisted, response headers use an allowlist, and the API key is
SecretStrend-to-end. - The top risk is a transaction-atomicity violation on the worker hot path. Page evidence and terminal job status commit in two separate transactions (
workflows.py:549-598), directly contradictingservices.instructions.md. A crash between them leaves a transcript persisted against a job stuck inPROCESSING. - That violation is invisible to the test suite. The test-effectiveness audit confirms no test can fail on a split commit — the pipeline tests assert the happy-path end state, which passes either way. The invariant is documented and steered but not enforced.
- Stale-job recovery is startup-only (
app.py:79), with a 30-second staleness threshold. A job orphaned shortly before a fast restart is not recovered and remainsPROCESSINGindefinitely, because the worker only claimsQUEUEDrows. - The mandated error-presentation boundary is bypassed at 8 sites.
home_page.pyandpeople_page.pyhand-rollui.notify(str(exc), ...), discarding theerror_id, category, and suggestion thaterror_presenter.show_errorprovides.people_page.pyimports the correct helpers and still bypasses them. - User-facing output can leak filesystem paths.
classify_unexpected_error(errors.py:94) interpolates the raw exception into a message rendered in the UI; a SQLAlchemyOperationalErrorembeds the database file path. This contradicts an explicit rule inerror-handling.instructions.md. - The retry gate ignores error category (
workflows.py:185), so non-retriable faults would be requeued. Currently latent becauseworker_max_retriesdefaults to0. - Highest-leverage work is enforcement, not refactoring. Two atomicity tests, a
tysuppression strategy that lets the pre-commit hook become blocking, andruff format --checkin the gate would convert three documented-but-unenforced invariants into deterministic ones.
2. Executive Architecture Assessment
Verdict: architecturally sound with a concentrated reliability gap in the worker's commit boundary.
Domain cohesion is strong. The services/ layer owns transactions and business rules, ui/ owns presentation, db/ owns schema, and providers/ isolates the OpenRouter adapter behind a TranscriptionProvider protocol. Dependency direction is correct and — unusually — mechanically enforced: test_service_boundaries.py AST-scans for service-to-service imports and test_ui_boundaries.py scans pages/components for persistence access. Provider details do not leak upward; workflows.py imports only the abstract providers types, never openrouter.
The evidence/provenance model is the strongest part of the system. ExecutionAttempt is genuinely append-only, retries append rather than rewrite, projection writes onto JobSource are clearly distinguished from history mutation, and all 14 provenance-auditor invariant checks pass.
Top systemic risks:
- Split commit boundary on the worker path (High). Evidence durability and job terminal status are two transactions. This is the one place where the architecture's own written contract is contradicted by the implementation, on the hottest path in the system.
- Recovery is a startup-only, time-thresholded sweep (Medium). There is no runtime reconciliation, so the self-healing property depends on restart cadence rather than on a bounded interval.
- Enforcement coverage has known holes (Medium). Atomicity, error-presenter usage, and formatting are all documented rules with no deterministic test. The repo's own strength — routing invariants into tests — has not been applied to these three.
- Leaky transaction ownership (Medium).
workflows.pyreaches intoservices.jobs._session_scope()andservices.sources._session_scope()— private members of two different services — to open transactions. Session ownership is ambiguous exactly where it most needs to be explicit. - A 10-diagnostic type-checker baseline with no suppression policy (Low). The signal is currently ignorable, which means a real regression would blend into the noise.
3. Findings by Severity
Critical Severity
None identified.
The secret-leakage check — the only plausible Critical for this system — passes explicitly. OpenRouterProvider stores an allowlisted subset of response headers only (providers/evidence.py:130-134, SAFE_RESPONSE_HEADERS); request headers containing Authorization are never captured into TransportEvidence; and the key is held as SecretStr from config.py through to the client. Append-only evidence history is likewise intact and test-enforced.
High Severity
[HIGH-01] Page evidence and terminal job status commit in separate transactions
-
Location:
src/transcription/services/workflows.py:549-565(_finalize_batch_outcome),src/transcription/services/workflows.py:584-598(_persist_page_outcome) -
Problem & Consequence:
.github/instructions/services.instructions.mdstates: "Never commit transcript updates separately from the paired terminal/retry job status change." The implementation does exactly that._persist_page_outcomeopens its own scope and commits page evidence (line 592-594);_finalize_batch_outcomelater opens a second scope and commits the terminalJobStatus(line 558-560). For a single-page job these are two transactions with a window between them. A process crash, container eviction, or unhandled error in that window persists the transcript while the job remainsPROCESSING. Because the worker only claimsQUEUEDrows, that job is not reprocessed; it is recoverable only by the startup sweep, and only if it has aged past the staleness threshold (see MED-01). The user sees a job that never completes despite the transcription having succeeded and been billed.This is a deliberate design tension, not an oversight:
_persist_page_outcome_durably(line 568-581) wraps the page write inasyncio.shieldprecisely so per-page evidence survives cancellation mid-batch. That goal is correct for multi-page jobs. The defect is that the single-page and final-page cases inherit the split unnecessarily. -
Recommendation: Keep per-page durability for intermediate pages, but commit the final page outcome and the terminal status in one transaction.
# Before — two scopes, two commits await _persist_page_outcome_durably(job=job, services=services, page=page, session=None) ... await _finalize_batch_outcome(job=job, services=services, status=status, session=None) # After — final page and terminal status share one transaction async with services.jobs.session_scope() as tx: for page in intermediate_pages: await _persist_page_outcome_durably(job=job, services=services, page=page, session=None) await _write_page_outcome(job=job, services=services, page=final_page, session=tx) await services.jobs.mark_job_status(job.id, status, session=tx) await tx.commit()Pair this with the atomicity test in HIGH-04 so the boundary cannot silently regress.
-
Effort: M
[HIGH-02] Mandated error-presentation boundary bypassed at 8 sites
-
Location:
src/transcription/ui/pages/home_page.py:212,220,228,255;src/transcription/ui/pages/people_page.py:265,321,330,339 -
Problem & Consequence:
.github/instructions/ui.instructions.md:42requires all user-facing error display to route throughcomponents/error_presenter.py. Seven of nine pages comply. These two hand-rollui.notify(str(exc), type="negative"). The consequence is not cosmetic:show_error(error_presenter.py:52-67) surfaces the correlationerror_id, the canonical error category, and the actionablesuggestionfield. Bypassing it means a user hitting a failure on the home or people page gets a bare exception string with no error reference to report, making these two pages unsupportable in production — precisely the pages most likely to be a user's entry point.people_page.pyalready importsrun_ui_actionandshow_errorat lines 28-29 and uses them elsewhere in the same module, so the bypass is inconsistency rather than missing infrastructure. -
Recommendation: Replace each site with the canonical helper. The unused
summarize_errorhelper inerror_presenter.py(currently a retained orphan — see LOW-07) is the natural fit where a compact string is genuinely needed.# Before except AppError as exc: ui.notify(str(exc), type="negative") # After except AppError as exc: show_error(exc)Then close the hole permanently by extending
tests/test_ui_boundaries.pywith an AST check that no module underPAGES_DIRcallsui.notify(...)withtype="negative". -
Effort: S
[HIGH-03] Unexpected-error path leaks filesystem paths into user-facing output
-
Location:
src/transcription/errors.py:91-98(line 94), rendered viasrc/transcription/ui/components/error_presenter.py:52-67 -
Problem & Consequence:
classify_unexpected_errorbuildsf"Unexpected error during {operation}: {exc}"and stores it asAppError.message.show_errorrenderserror.messagedirectly to the user. Any exception whosestr()contains infrastructure detail is therefore displayed verbatim — a SQLAlchemyOperationalErrorembeds the absolute SQLite database path, and anOSErrorfrom the media layer embeds the storage root..github/instructions/error-handling.instructions.md:74states: "Never leak … local filesystem paths in user-facing output." This is the generic catch-all path, so it applies to every unanticipated failure across the application. -
Recommendation: Split the diagnostic detail from the user-facing message. Log the full exception with the
error_idas the correlation key; show the user a stable message plus that id.# Before return AppError( f"Unexpected error during {operation}: {exc}", category=ErrorCategory.INTERNAL_UNEXPECTED, ... ) # After error = AppError( f"Unexpected error during {operation}.", category=ErrorCategory.INTERNAL_UNEXPECTED, suggestion="Retry once. If it persists, report the error reference id.", retriable=False, ) logger.exception("error_id=%s operation=%s", error.error_id, operation) return errorAdd a case to
tests/ui/test_error_presenter.pyasserting that a raisedOperationalErrorcarrying a path does not surface that path in the rendered message. -
Effort: S
[HIGH-04] Transaction-atomicity invariants have no enforcing test
-
Location: Contract at
.github/instructions/services.instructions.md§"Workflow Transaction Boundaries"; gap confirmed acrosstests/integration/test_pipeline_flow.py:66-160andtests/services/test_job_service.py:41-59 -
Problem & Consequence: The test-effectiveness audit establishes that neither Transaction B (transcript +
TRANSCRIBED) nor Transaction C (retry:error_detail+retry_count+QUEUED) is enforced. The existing pipeline test asserts the final state after a successful run — which passes identically whether the writes shared one commit or used two. To fail on a split-commit regression a test must inject a fault between the writes; no such test exists.The consequence is that HIGH-01 shipped undetected and any future refactor of
advance_jobcan reintroduce it just as silently. This is a governance failure rather than a code defect: the repo's stated model is that hard rules belong in deterministic tests, and this rule is the most consequential one that never made the transition. -
Recommendation: Add
tests/integration/test_pipeline_atomicity.pywith two tests that patch the session to raise afterflush()but beforecommit(), then assert that neither side of the pair is visible in a fresh session. These tests should fail against the current implementation and pass once HIGH-01 is fixed — write them first. -
Effort: M
Medium Severity
[MED-01] Stale-job recovery runs only at startup, behind a 30-second threshold
- Location:
src/transcription/app.py:71-81(_recover_stale_processing_jobs), sole caller atapp.py:79inside_lifespan - Problem & Consequence:
requeue_stale_processing_jobshas exactly one call site, in the lifespan startup handler. There is no runtime re-check. The staleness predicate isupdated_at < now - worker_provider_timeout_seconds(default 30.0s,config.py:116). A job orphaned less than 30 seconds before a fast container restart therefore fails the predicate at the only moment recovery is attempted, and staysPROCESSINGforever — the worker claims onlyQUEUEDrows. It self-heals only on some later, unrelated restart. In a frequently-redeployed environment, restarts are exactly when orphans are created, so the recovery window is systematically misaligned with the failure it exists to handle. - Recommendation: Move the sweep onto a periodic task in the worker loop (e.g. every
max(30, provider_timeout * 2)seconds) in addition to the startup call, and derive the threshold from a dedicatedworker_stale_job_secondssetting rather than reusing the provider timeout, so the two can be tuned independently. - Effort: M
[MED-02] Retry gate ignores error_category, so non-retriable failures would be requeued
-
Location:
src/transcription/services/workflows.py:184-194 -
Problem & Consequence: The
JobStatus.FAILEDbranch gates solely onjob.retry_count < settings.worker_max_retries. It does not consulterror_categoryor theAppError.retriableflag..github/instructions/error-handling.instructions.mdclassifiesvalidation,not_found, andconflictas non-retriable; under this gate a malformed source or a missing record would be retried to exhaustion, consuming provider quota on calls that cannot succeed and delaying the terminal failure the user needs to see. There is also no backoff — retries requeue immediately.Currently latent:
worker_max_retriesdefaults to0(config.py:113) and is commented out in.env, so the branch always falls through to the max-retries log. It becomes live the moment anyone enables retries. -
Recommendation: Gate on retriability and count, and add exponential backoff before requeue.
case JobStatus.FAILED: if job.error_category in NON_RETRIABLE_CATEGORIES: logger.error("Job %s failed non-retriably (%s).", job.id, job.error_category) return if job.retry_count < settings.worker_max_retries: ...Cover with a test that a
validation-category failure is not requeued even whenworker_max_retries > 0. -
Effort: S
[MED-03] IntegrityError on the attempt-number flush is uncaught, risking evidence loss
- Location:
src/transcription/services/sources.py:540-546(attempt-number computation),sources.py:587(unguardedflush()) - Problem & Consequence:
attempt_numberis derived read-then-write asMAX(attempt_number) + 1, anduq_execution_attempt_numberenforces uniqueness (db/models.py:507, documented atdocs/schema.md:273). The siblingJobSourceinsert does catchIntegrityError(sources.py:531-534), but theExecutionAttemptflush at line 587 does not. Two concurrent attempt writes for the same job source would raise an unhandledIntegrityErrorand lose an evidence row — the one class of data this system exists to preserve. Not currently reachable: the worker is single-instance and processes sources sequentially. It becomes reachable the moment a second worker replica is deployed. - Recommendation: Mirror the
JobSourcehandling — catchIntegrityError, recomputeMAX(attempt_number) + 1, and retry the insert a bounded number of times, raising a domain error on exhaustion. Note this constraint as a horizontal-scaling precondition indocs/production-runbook.md. - Effort: M
[MED-04] Shutdown timeout is shorter than the provider timeout
- Location:
src/transcription/worker.py:146(asyncio.wait_for(worker_task, timeout=2.0)); provider timeout atconfig.py:116(default 30.0s) - Problem & Consequence: Graceful shutdown waits 2 seconds for the worker task, but the stop event is only checked between jobs and an in-flight provider call may run for up to 30 seconds. Any shutdown during a provider call therefore cancels mid-flight. Combined with HIGH-01's split commit, a cancellation that lands between the evidence commit and the status commit produces exactly the stuck-
PROCESSINGstate described there — so this finding materially raises HIGH-01's probability rather than being independent of it. - Recommendation: Derive the shutdown budget from the provider timeout (
worker_provider_timeout_seconds + small_grace) instead of hardcoding2.0, and ensure the container's termination grace period exceeds it. Document both indocs/production-runbook.md. - Effort: S
[MED-05] workflows.py reaches into two services' private _session_scope
- Location:
src/transcription/services/workflows.py:558(services.jobs._session_scope()),workflows.py:592(services.sources._session_scope()) - Problem & Consequence: The orchestration module opens transactions by calling a private member on two different service objects. This is the concrete mechanism behind HIGH-01: because transaction ownership is expressed through a private back-door rather than a declared boundary, nothing in the design makes it obvious that two scopes are being opened for one logical unit of work. It also couples
workflows.pyto a service implementation detail thattest_service_boundaries.pycannot see (it checks imports, not attribute access). - Recommendation: Promote a single explicit transaction entry point — a
session_scope()onServiceBundle, or a module-levelunit_of_work(services)helper — and makeworkflows.pyuse only that. Extendtest_service_boundaries.pywith an AST check forbidding_session_scopeattribute access outside the owning service module. - Effort: M
Low Severity
[LOW-01] hashlib.sha256 over full file bytes runs on the event loop
- Location:
src/transcription/services/store.py:401 - Problem & Consequence: Digest computation is CPU-bound and synchronous inside an
async def. For large uploads this blocks the loop, stalling both the NiceGUI UI and the worker. Every sibling I/O path in the codebase correctly usesasyncio.to_thread(media_storage.py:43,normalization.py:117,photos.py:176,sources.py:740,753), so this is an isolated deviation. - Recommendation:
digest = await asyncio.to_thread(lambda: hashlib.sha256(file_bytes).hexdigest()). - Effort: S
[LOW-02] homepage_store.py performs synchronous file I/O from async callers
- Location:
src/transcription/ui/homepage_store.py:25,32; called fromsrc/transcription/ui/pages/home_page.py:170 - Problem & Consequence: Same class as LOW-01 — reads/writes the homepage JSON directly rather than via
asyncio.to_thread. Impact is small (a tiny file), but it is a second deviation from an otherwise universal convention. - Recommendation: Wrap both calls in
asyncio.to_thread. - Effort: S
[LOW-03] Worker poll interval is hardcoded outside Settings
- Location:
src/transcription/app.py:62(poll_interval_seconds=1.0) - Problem & Consequence: The single operational knob controlling worker latency-vs-load cannot be tuned without a code change, contradicting the otherwise-clean rule that all configuration lives in
config.py(zeroos.getenvcalls exist outside it). - Recommendation: Add
worker_poll_interval_seconds: float = 1.0toSettingsand read it at the call site. - Effort: S
[LOW-04] _build_request_manifest returns None silently, producing incomplete evidence
- Location:
src/transcription/providers/openrouter.py:347 - Problem & Consequence: When
source_reference is Nonethe manifest is skipped with no log line. The attempt is still recorded but its provenance is quietly incomplete, and there is no signal that it happened — the failure mode is undetectable after the fact. - Recommendation: Log at
warningwith the job/source identifiers before returningNone, so incomplete provenance is at least attributable. - Effort: S
[LOW-05] Ten ty diagnostics with no suppression strategy
- Location:
src/transcription/services/photos.py(8),tests/test_storage_reconciliation.py(2) - Problem & Consequence: All ten are SQLModel/SQLAlchemy false positives — column descriptors are typed as their Python value type (
UUID,datetime,bool), so.is_(),.asc(),func.count(), andgroup_by()appear invalid. Because there is no suppression policy, the pre-commit hook must runtyin advisory mode, which means a genuine new type error would print alongside the known ten and block nothing. - Recommendation: Add targeted
# ty: ignore[...]comments with a one-line rationale at each of the ten sites, then flip the pre-commit hook to blocking. This converts a permanently-ignored signal into a real gate. - Effort: M
[LOW-06] ruff format is not enforced; 35 files have drifted
- Location:
.pre-commit-config.yaml,ruff.toml - Problem & Consequence:
ruff checkis blocking butruff format --checkis absent from the gate, so formatting drift accumulates silently and inflates unrelated diffs whenever anyone does run the formatter. - Recommendation: Run
uv run ruff format .once as a single isolated commit, then addruff format --checkto the pre-commit gate. - Effort: S
[LOW-07] Four retained orphans, all recorded as "uncertain — follow-up"
- Location:
tests/test_orphan_sweep.py:33-52(KNOWN_ORPHANS):BenchmarkManifest,dispose_all_engines,refresh_engine,summarize_error - Problem & Consequence: Every entry carries the weakest possible justification.
summarize_erroris the notable one: it is an unused helper inerror_presenter.pywhile two pages hand-roll error display (HIGH-02) — the orphan and the boundary violation are the same problem viewed from two directions.dispose_all_engines/refresh_engineare plausibly test-support utilities and should be classified as such rather than left uncertain. - Recommendation: Resolve each to a definite outcome —
summarize_errorbecomes used by the HIGH-02 fix; classify the engine helpers as test-support or delete them; decide onBenchmarkManifest. - Effort: S
[LOW-08] Orphan sweep only scans module-level public definitions
- Location:
tests/test_orphan_sweep.py - Problem & Consequence: Methods and private functions are out of scope, so dead code inside classes — the most common kind in a service-oriented codebase — is structurally invisible to the sweep.
- Recommendation: Extend the AST walk to public methods on service classes, seeding
KNOWN_ORPHANSwith the current result set to keep the change non-breaking. - Effort: M
[LOW-09] f-string interpolation in logging calls
- Location:
src/transcription/services/workflows.py:193and similar sites - Problem & Consequence:
logger.error(f"Job {job.id} has failed...")formats eagerly regardless of level and prevents structured-logging backends from grouping by template. Ruff'sflake8-logging-format(G) rules are not enabled, so this is unenforced. - Recommendation: Use
logger.error("Job %s has failed and reached max retries.", job.id)and enable ruff rule setG. - Effort: S
[LOW-10] Low-signal and always-true assertions in the test suite
- Location:
tests/test_traceability.py:54-57;tests/integration/test_pipeline_flow.py:135-140,446-452;tests/test_orphan_sweep.py:119;tests/services/test_workflows_reliability.py:105,178,241,317,375 - Problem & Consequence: Per the test-effectiveness audit:
test_traceability.py:54-57asserts properties of dict literals defined in the same file (can only fail if the test itself is edited);assert processed is Truein the pipeline tests is unfalsifiable becauseread_jobraises rather than returningNone; the>= 200orphan threshold is a historical snapshot that tolerates ±40 drift; and theassert result is not Noneguards are shadowed by the attribute assertions that follow. Together these overstate effective coverage. - Recommendation: Apply the prune/strengthen backlog in §6 (Testing).
- Effort: S
[LOW-11] Wall-clock timing dependencies risk CI flakiness
- Location:
tests/services/test_workflows_reliability.py:157-196(realtime.sleep(0.40), upper bound< 540mswith only 10% slack);test_workflows_reliability.py:341(asyncio.wait_for(..., timeout=2)) - Problem & Consequence: On a loaded CI runner, a 200ms asyncio task plus 400ms blocking setup can exceed the 540ms bound, producing false failures that erode trust in the suite.
- Recommendation: Widen the slack factor to
0.8or replace the blocking sleep with a controlled clock mock. - Effort: S
4. Architectural Drift & Gap Analysis
Direction is doc->code (implementation must change to match documented intent) or code->doc (an undocumented but repeatable convention that should be formalized).
| Area / Component | Direction | Documented / Intended Rule | Actual Implementation State | Severity | Recommended Resolution |
|---|---|---|---|---|---|
| Worker commit boundary | doc->code |
services.instructions.md: never commit transcript updates separately from the paired terminal status change |
workflows.py:549-598 commits page evidence and terminal status in two separate sessions |
High | Fix per HIGH-01; enforce per HIGH-04 |
| UI error presentation | doc->code |
ui.instructions.md:42: all user-facing error display routes through error_presenter.py |
8 hand-rolled ui.notify sites in home_page.py and people_page.py |
High | Fix per HIGH-02; add AST guard to test_ui_boundaries.py |
| Unexpected-error messaging | doc->code |
error-handling.instructions.md:74: never leak local filesystem paths in user-facing output |
errors.py:94 interpolates raw exc into the rendered message |
High | Fix per HIGH-03 |
| Retry policy | doc->code |
error-handling.instructions.md: validation / not_found / conflict are non-retriable |
workflows.py:185 gates on retry count only |
Medium | Fix per MED-02 |
| Stale-job recovery | code->doc |
Not documented as startup-only or time-thresholded | Single startup call site; 30s threshold reuses the provider timeout | Medium | Fix per MED-01, then document the recovery contract in docs/production-runbook.md |
| Transaction ownership | code->doc |
services.instructions.md assigns transaction ownership to services |
workflows.py opens scopes via two services' private _session_scope |
Medium | Fix per MED-05; document the single unit-of-work entry point |
| Blocking-I/O convention | code->doc |
Not stated as a rule; followed at 5 of 7 sites | store.py:401 and homepage_store.py:25,32 deviate |
Low | Fix per LOW-01/LOW-02, then state the asyncio.to_thread rule in services.instructions.md |
| Configuration centralization | code->doc |
Zero os.getenv outside config.py — a real, held convention |
Held everywhere except the hardcoded poll_interval_seconds at app.py:62 |
Low | Fix per LOW-03, then formalize the rule and add a deterministic guard |
| Type-check baseline | code->doc |
No documented policy for ty diagnostics |
10 tolerated false positives; hook is advisory-only | Low | Adopt the suppression strategy in LOW-05 and document it |
| Formatting | code->doc |
ruff.toml configures the formatter |
ruff format --check absent from the gate; 35 files drifted |
Low | Fix per LOW-06 |
| Dependency pin | — | docs/production-runbook.md "Dependency upgrade policy" records the exact nicegui==3.13.0 pin as a deliberate stability decision |
Matches | — | No action — correctly documented, not a defect |
5. Invariant Inventory & Routing Recommendations
| Invariant / Constraint | Current Location | Recommended Target Layer | Rationale |
|---|---|---|---|
| Transcript + terminal status commit atomically | Instructions only | Deterministic test (tests/integration/test_pipeline_atomicity.py) |
Highest-consequence rule in the system with zero enforcement; steering alone already failed to prevent HIGH-01 |
| Retry writes commit atomically | Instructions only | Deterministic test (same file) | Same class; a partial retry commit corrupts retry_count accounting |
All UI errors route through error_presenter |
Instructions (ui.instructions.md:42) |
Deterministic test (extend test_ui_boundaries.py) |
Mechanically checkable via AST; 8 live violations prove instructions are insufficient here |
| No filesystem paths in user-facing output | Instructions (error-handling.instructions.md:74) |
Deterministic test (extend tests/ui/test_error_presenter.py) |
Checkable by asserting a path-bearing exception does not surface its path |
| Non-retriable categories are never requeued | Instructions | Deterministic test (tests/services/test_workflows_reliability.py) |
Latent today; a test freezes the correct behavior before retries are enabled |
Blocking I/O runs via asyncio.to_thread |
Convention only (5/7 sites) | Instructions (services.instructions.md) |
Judgment-dependent (thresholds vary by payload size); steering fits better than a hard test |
| Transaction opened through one owned entry point | Convention, violated | Instructions + test | Document the entry point; AST-guard against _session_scope access outside its owning module |
Append-only ExecutionAttempt history |
Docs + 3 tests | Keep as-is | Correctly routed and genuinely mutation-sensitive; the model to imitate |
| Service/UI boundary rules | Instructions + 2 AST tests | Keep as-is | Working exactly as intended |
| Status vocabulary conformance | docs/schema.md + contract guards |
Keep as-is | Enum drift would fail the suite |
| No secrets in stored evidence | Docs + provenance skill + allowlist in code | Keep as-is | Allowlist is the right mechanism — fails closed by construction |
ty diagnostic suppression policy |
Nonexistent | Docs + blocking hook | Needs a written rationale per suppression before the gate can be trusted |
| NiceGUI exact pin | docs/production-runbook.md |
Keep as-is | Deliberate, documented, correctly excluded from review findings |
6. Stack-Specific Analysis
Python 3.12+ Best Practices
Modern syntax is used consistently: X | None unions throughout, builtin generics, no typing.List/Optional legacy forms, pathlib over os.path. Type-annotation coverage is high, with no bare Any on public service signatures. Broad except Exception appears where it belongs — the per-page handler at workflows.py:352 deliberately isolates one page's failure from the batch, which is correct. # noqa: PLR0915 / PLR1702 are used sparingly and consistently. Minor gaps: f-strings in logging (LOW-09), and two blocking-I/O deviations (LOW-01/LOW-02).
FastAPI
Lifespan is handled correctly via an asynccontextmanager _lifespan (app.py:36-68) rather than deprecated @app.on_event. Routers are domain-organized with typed path/query parameters and response_model declarations. Error handling is centralized through register_error_handlers, and the full internal→canonical category mapping is round-trip tested at the HTTP layer (tests/api/test_error_responses.py:59-95). print_api.py:42-49 performs correct relative_to-based path containment for media serving. No blocking calls found in async def route handlers.
NiceGUI
Separation of concerns is good — pages delegate to services and test_ui_boundaries.py mechanically prevents persistence access from pages and components. Client state is client-scoped; no cross-session global-state leaks found. API usage is correct for the pinned 3.13.0 release. The two defects are the error-presenter bypass (HIGH-02) and synchronous file I/O in homepage_store.py (LOW-02).
SQLModel & SQLAlchemy
The strongest layer. lazy="raise" is declared on relationships and correctly paired with expire_on_commit=False, which together make N+1 access a loud failure rather than a silent performance cost — no N+1 patterns found. The job claim is a genuine atomic compare-and-swap (jobs.py:212-222: conditional UPDATE ... WHERE status = QUEUED ... RETURNING), which is the correct primitive and correctly implemented. Hot-path indexes are declared and test-verified (test_db.py:131). Cross-dialect portability is handled for SQLite and PostgreSQL. Weaknesses are transaction ownership (MED-05, HIGH-01) rather than query construction, plus the uncaught IntegrityError at MED-03.
Pydantic V2 & Settings
Fully migrated — no @validator, no Config class, no .dict() or parse_obj anywhere. model_config = ConfigDict(...) and @field_validator are used correctly. config.py is a clean single source of truth: zero os.getenv calls exist outside it, .env is untracked and gitignored, and the API key is SecretStr end-to-end. The only deviation is the hardcoded poll interval (LOW-03).
Asyncio Workers
Task lifecycle is handled properly: task references are retained (no GC risk), CancelledError is re-raised rather than swallowed, the provider call happens outside any DB transaction, timeouts resolve to terminal states, and there is no tight polling spin. _persist_page_outcome_durably's use of asyncio.shield (workflows.py:568-581) is a thoughtful durability mechanism. The defects are the split commit boundary (HIGH-01), the shutdown-vs-provider timeout mismatch (MED-04), and startup-only recovery (MED-01).
OpenRouter / Adapter Boundary
Encapsulation is clean — workflows.py imports only abstract types from providers, never openrouter directly, so provider specifics do not leak into business logic. The AsyncClient is shared with configured timeouts and is properly closed: worker.py:248,271 → services.aclose() → sources.aclose() (sources.py:129-133) → provider aclose() (openrouter.py:86-87,233-235). Responses are Pydantic-validated. All 14 evidence-provenance-auditor invariant checks pass, including the critical one: the API key is never persisted, request headers are never stored, and TransportEvidence captures response headers through an explicit allowlist (evidence.py:130-134). Only LOW-04 applies here.
Testing & Quality Tooling
377 tests pass with -m "not external". The project test contract is honored: --strict-markers with all three markers (unit, integration, external) declared, asyncio_mode = "strict" with every async def test_ correctly decorated across all 17 async test files, external properly excluded from default runs, and no unawaited-coroutine warnings — the filterwarnings error promotion is clean.
Contract coverage is genuinely strong for structural rules. Confirmed mutation-sensitive enforcement exists for: append-only evidence history (3 independent tests, including full before/after field-tuple snapshots), stuck-in-PROCESSING prevention, the complete 10-category error mapping, and both boundary rules.
The critical gap is transaction atomicity (HIGH-04) — the audit verdict is "Effective with Conditions / Go with Conditions", blocking on the two missing atomicity tests. Secondary items are the low-signal assertions (LOW-10) and wall-clock flakiness (LOW-11).
Prune/strengthen backlog:
| Priority | Task | Location |
|---|---|---|
| High | Add Transaction B atomicity test (fault injected between transcript and status writes) | new tests/integration/test_pipeline_atomicity.py |
| High | Add Transaction C atomicity test (retry: error_detail + retry_count + QUEUED) |
same file |
| Medium | Delete tautological assertions on same-file dict literals | tests/test_traceability.py:54-57 |
| Medium | Remove unfalsifiable assert processed is True |
tests/integration/test_pipeline_flow.py:135-140,446-452 |
| Medium | Replace >= 200 snapshot threshold with set-membership assertion |
tests/test_orphan_sweep.py:119 |
| Medium | Assert mapped test files contain ≥1 test, not merely that they exist | tests/test_traceability.py:59-60 |
| Low | Widen timing slack or mock the clock | tests/services/test_workflows_reliability.py:157-196 |
| Low | Drop assert result is not None guards shadowed by following assertions |
tests/services/test_workflows_reliability.py:105,178,241,317,375 |
7. Duplication & Consolidation Report
| Pattern / Duplication | Locations | Proposed Canonical Home | Estimated Lines Removed |
|---|---|---|---|
Hand-rolled ui.notify(str(exc), type="negative") |
home_page.py:212,220,228,255; people_page.py:265,321,330,339 |
ui/components/error_presenter.py::show_error (already exists) |
~16 |
Optional-session if session is None: async with _session_scope() preamble |
workflows.py:557-561, workflows.py:591-595, and sibling service write paths |
services/base.py::unit_of_work(services, session) context manager |
~30 |
Synchronous I/O not wrapped in asyncio.to_thread |
store.py:401, homepage_store.py:25,32 |
services/base.py::run_blocking helper |
~6 |
Read-then-increment MAX(n) + 1 with uniqueness retry |
sources.py:540-546 (uncaught) vs sources.py:531-534 (caught) |
services/base.py::insert_with_sequence_retry |
~20 |
Proposed Canonical Abstractions
# src/transcription/services/base.py
@asynccontextmanager
async def unit_of_work(
services: ServiceBundle,
session: AsyncSession | None = None,
) -> AsyncIterator[AsyncSession]:
"""Single transaction entry point. Yields a session and commits once on clean exit.
Replaces the `if session is None: async with X._session_scope()` preamble and the
private-member access at workflows.py:558,592. Makes the two-commit split of
HIGH-01 structurally hard to reintroduce.
"""
async def run_blocking[T](fn: Callable[[], T]) -> T:
"""Run a CPU- or disk-bound callable off the event loop."""
return await asyncio.to_thread(fn)
async def insert_with_sequence_retry(
session: AsyncSession,
*,
build: Callable[[int], SQLModel],
next_value: Callable[[], Awaitable[int]],
attempts: int = 3,
) -> SQLModel:
"""Insert a row carrying a derived sequence number, retrying on IntegrityError."""
8. Meta-Tooling & Instruction Update Recommendations
- Add
tests/integration/test_pipeline_atomicity.py(HIGH-04). The single highest-value enforcement change. Write it before fixing HIGH-01 so it demonstrably fails first. - Extend
tests/test_ui_boundaries.pywith an AST check forbiddingui.notify(..., type="negative")inPAGES_DIR, routing all error display througherror_presenter. Convertsui.instructions.md:42from steering into enforcement. - Extend
tests/ui/test_error_presenter.pywith a case asserting that a path-bearing exception does not surface its path, enforcingerror-handling.instructions.md:74. - Adopt a
tysuppression policy — targeted# ty: ignore[...]with rationale at the 10 known sites, documented indocs/— then flip the pre-committyhook from advisory to blocking. Until this happens the type checker provides no gate. - Add
ruff format --checkto the pre-commit gate, preceded by one isolated formatting commit across the 35 drifted files. - Enable ruff rule set
G(flake8-logging-format) to catch f-string logging (LOW-09). - Extend
tests/test_orphan_sweep.pyto public methods on service classes, seedingKNOWN_ORPHANSwith current results (LOW-08). Then resolve all four existing "uncertain" entries to definite outcomes. - Extend
tests/test_service_boundaries.pywith an AST check forbidding_session_scopeattribute access outside its owning service module (MED-05). Also address the noted classification gap: the test excludes orchestration modules by hardcoded stem name (store,workflows,__init__), so a new orchestration module under a different name would be misclassified as a service. - Update
.github/instructions/services.instructions.mdto state theasyncio.to_threadrule for blocking I/O and to name the singleunit_of_worktransaction entry point. - Update
docs/production-runbook.mdwith the stale-job recovery contract (interval, threshold, and its relationship to the container termination grace period), and note single-worker as a current precondition until MED-03 is fixed. - Note for
test_ui_boundaries.py: the forbidden-import lists are fixed string sets, so a future persistence helper under a new name would escape the check. Consider inverting to an allowlist of permitted imports for pages.
9. Prioritized Dependency-Ordered Action Plan
Phase 1: Blocking fixes
- Write the two atomicity tests (HIGH-04) and confirm they fail against current
main. - Fix the split commit boundary (HIGH-01) and confirm the tests now pass.
- Fix the filesystem-path leak in
classify_unexpected_error(HIGH-03). - Replace the 8 hand-rolled error notifications with
show_error(HIGH-02).
Phase 2: Enforcement hardening
5. Add the ui.notify AST guard and the path-leak presenter test, locking in items 3-4.
6. Adopt the ty suppression policy and make the pre-commit hook blocking (LOW-05).
7. Run ruff format . as an isolated commit, then add ruff format --check to the gate (LOW-06).
8. Enable ruff rule set G and fix the resulting logging call sites (LOW-09).
Phase 3: Reliability & concurrency
9. Move stale-job recovery to a periodic worker task with a dedicated setting (MED-01).
10. Gate retries on error_category and add backoff (MED-02) — do this before ever raising worker_max_retries above 0.
11. Derive the shutdown budget from the provider timeout (MED-04).
12. Handle IntegrityError on the attempt-number flush (MED-03) — a hard precondition for running more than one worker replica.
13. Move sha256 and homepage-store I/O off the event loop (LOW-01, LOW-02); move the poll interval into Settings (LOW-03).
Phase 4: Consolidation & refactoring
14. Introduce unit_of_work and migrate workflows.py off private _session_scope access (MED-05); add the corresponding boundary guard.
15. Extract run_blocking and insert_with_sequence_retry (§7).
16. Prune the low-signal assertions and reduce timing flakiness (LOW-10, LOW-11).
Phase 5: Non-blocking governance/documentation depth
17. Extend the orphan sweep to methods and resolve the four uncertain orphans (LOW-07, LOW-08).
18. Update services.instructions.md and docs/production-runbook.md per §8 items 9-10.
19. Log incomplete request manifests (LOW-04).
20. Consider inverting the UI boundary check to an allowlist.
10. Preserved Strengths
- Evidence and provenance integrity is exemplary. All 14 provenance-auditor invariants pass.
ExecutionAttempthistory is genuinely append-only, retries append rather than rewrite, and projection writes are cleanly distinguished from history mutation. Three independent tests — including full before/after field-tuple snapshots — make any mutation regression fail loudly. - Secret hygiene is correct by construction. The response-header allowlist (
evidence.py:130-134) fails closed: a newly-introduced sensitive header is excluded by default rather than requiring someone to remember to block it. Request headers are never captured, andSecretStris used end-to-end. - Atomic job claiming.
jobs.py:212-222uses a conditionalUPDATE ... WHERE status = QUEUED ... RETURNING— a true compare-and-swap that makes double-claiming impossible under concurrency, rather than the common read-then-write race. lazy="raise"paired withexpire_on_commit=False. This combination turns accidental lazy loads into immediate errors instead of silent N+1 queries, and it is the reason no N+1 patterns exist in the codebase. Keep it.- Architectural rules are mechanically enforced, not merely documented. AST-based boundary tests for service-to-service imports and UI persistence access are the right pattern; this review's main recommendation is simply to apply that same pattern to three more rules.
- Configuration discipline. Zero
os.getenvcalls outsideconfig.py,.envuntracked and gitignored, clean Pydantic V2 throughout with no V1 residue. - Path containment on media serving.
print_api.py:42-49uses properrelative_tovalidation rather than string prefix matching. - Async worker fundamentals. Task references retained,
CancelledErrorre-raised, provider calls outside DB transactions, timeouts resolving to terminal states, no tight polling loop.asyncio.shieldin_persist_page_outcome_durablyis a genuinely thoughtful durability mechanism — the fix in HIGH-01 should preserve it for intermediate pages. - Test contract rigor.
--strict-markers,asyncio_mode = "strict"honored across all 17 async test files with no missing decorators, and coroutine-never-awaited promoted to a hard error with a clean run.