17 KiB
V4.6 Scope Boundary
This document defines the frozen boundary for V4.6, a pure remediation release. V4 through V4.5 remain the architecture and behavioral baseline. V4.6 introduces no new user-facing features; it pays down the defects, duplication, and structural drift catalogued in Architecture & Code Review Report.
Every item in scope is traceable to a review finding ID. Any change that cannot be traced to a finding ID is out of scope.
Purpose
- Remove dead code, dead configuration, and duplicate implementations that create maintenance drift.
- Re-level the database schema from current SQLModel metadata, ending hand-rolled DDL while the schema is still pre-production.
- Correct read amplification, missing indexes, and query patterns that scale with table size rather than result size.
- Restore the boundaries the project already wrote down in
.github/instructions/services.instructions.mdandui.instructions.md. - Make
tyusable as a real quality gate. - Preserve every existing behavior, evidence guarantee, and provenance contract established in V4 through V4.5.
Confirmed Operating Context
These answers are frozen for V4.6 and govern every decision below.
| Question | Answer |
|---|---|
| Database | SQLite only. PostgreSQL remains the intended destination but is deferred beyond V4.6. JSONBCompat and the Postgres drivers are retained. |
| Topology | Single user, single process, single worker. A multi-user server is the stated direction, so forward-compatibility work is retained where it is cheap. |
| Schema evolution | Re-level from current metadata. No Alembic, no migration framework, no _upgrade_* chain. |
| Existing data | The development database is rebuilt from scratch during implementation and migrated from backup as the final step. |
| Release character | Pure remediation. No new features. |
| Scope band | Critical through Low, inclusive. |
In Scope
1. Dead Code and Dead Configuration Removal
src/transcription/app_state.pyis deleted. It has zero importers and contains a guaranteedTypeError([HIGH-01]).src/transcription/services/transcription.pyis deleted;build_prompt_executionhas exactly one import path ([MED-05]).- The legacy compatibility aliases in
services/store.pyare deleted ([MED-05]). ServiceBase.queueis deleted; no service allocates an unusedasyncio.Queue([MED-07]).sqlite_check_same_threadandworker_retry_backoff_secondsare either wired to real behavior or deleted, along with their tests ([MED-02]).db/operations.py:get_next_queued_jobis deleted as a divergent duplicate ([CRIT-01]).DATABASE_URLis removed fromdocker-compose.yml, and the real nestedDATABASE__*names are documented. The application never silently ignores a database configuration variable ([MED-10]).document_panzoomis either fixed or deleted; it is exported but referenced by no page ([HIGH-07]).
2. Schema Re-Level
The following changes are schema-affecting and land as one single pass against a database rebuilt from empty.
upgrade_schemaand the three_upgrade_*functions (db/operations.py:25-109) are deleted, along with their tests (tests/test_db.py:109-172) ([HIGH-05]).- The schema is generated exclusively from SQLModel metadata via
create_all(), gated by the existingSettings.should_bootstrap_schema([HIGH-05]). - The hand-written
CHAR(32)column forpreferred_execution_attempt_idceases to exist; the column type is whatever the model declares ([HIGH-05]). - A composite index on
Job.status, Job.date_createdis declared in the model, plusindex=Trueon the foreign keys the worker and detail pages filter on ([HIGH-04]). Source.preferred_execution_attempt_iddeclares its foreign key withuse_alter=True, resolving thesource/job_source/execution_attemptcycle socreate_allwill succeed on PostgreSQL when that cutover is taken ([HIGH-08]).- Relationship loading defaults change from bidirectional
lazy="selectin"tolazy="raise", with per-queryselectinload()retained or added where a load path genuinely requires it ([CRIT-02]).
No migration script runs against a populated database. No history table, revision directory, or down path is introduced.
3. Data Migration
- A one-time script under
tools/migrates the user's backed-up V4.5 data into the re-leveled schema. - The script is authored after the
lazy="raise"flip is complete, so that every relationship it traverses carries an explicit eager load. - The script is idempotent, is never invoked automatically at startup, and never runs as part of the test suite.
- Uploaded Source files, portraits, and artifact files on disk are preserved unchanged; only database rows are rewritten.
- This is the final step of V4.6.
4. Worker and Provider Reliability
read_next_queued_jobgainsLIMIT 1and stops materializing the entire queue plus its eager graph on every poll ([CRIT-01]).- The claim becomes an atomic
QUEUED→PROCESSINGtransition. On SQLite this is a bounded single-writer transaction; theFOR UPDATE SKIP LOCKEDpath is written and dialect-guarded for the multi-user direction but is not exercised in V4.6 ([CRIT-01]). - Eager relationships are loaded in a second query after the claim succeeds, keeping the hot poll a single narrow row ([CRIT-01]).
ServiceBundleand the provider client are hoisted to worker-loop scope so the HTTP connection pool and TLS session survive across jobs ([HIGH-02], [MED-06]).ServiceBundlegains afrom_session_factoryconstructor, replacing three duplicated instantiation blocks ([MED-06]).- The
le=20.0cap onworker_provider_timeout_secondsis removed, the default is raised, and an explicithttpx.Timeoutis passed to the OpenRouter client ([HIGH-03]). - The
TranscriptionProviderProtocol is extended to coveracloseand the evidence attributes; the per-callinspect.signaturereflection atsources.py:1237is deleted ([MED-03]).
5. Service Layer Consolidation
- A generic
RegistryService[ModelT]owns list, summaries, create, read, update, delete, and reference-check for semantic-key registries.DocumentTypeandPersonRolebecome thin subclasses declaring their model, error class, reference query, and noun ([MED-11]). - Label normalization, the casefold key, and the registry summary shape are defined once ([MED-11]).
ServiceBasegains_get_or_raise, and all 38 hand-written not-found guards adopt it, including the three indocuments.pythat already bypass the local helper ([MED-12]).services/media_storage.pybecomes the single implementation of validate → hash → write → wrap-error, replacingstore_source_file,store_person_portrait, and the homepage image writer ([MED-13]).source_mime_typemoves out ofservices/sources.pyto a shared module sodocuments.pyno longer imports a sibling service ([MED-14], partial).- The four query inefficiencies in
sources.pyare corrected: thejob_idfilter moves into SQL, navigation uses two bounded queries,list_processing_artifactsgains alimit, and artifact re-hashing moves off the event loop ([LOW-08]).
6. Async I/O and Configuration Hygiene
- Blocking filesystem and CPU work — media writes, artifact writes, integrity hashing, and Pillow orientation normalization — is wrapped in
asyncio.to_thread([MED-01]). functools.cacheon the engine and session factories is replaced with an explicit URL-keyed registry supporting targeted eviction ([MED-04]).- The
object.__setattr__mutation of a frozenSettingsmodel innormalize_provider_modelsis replaced withmodel_copy(update=...)or a computed property ([Pydantic V2 §3]). models.pytimestamp columns that are expected to track modification gainonupdate, soupdated_atanddate_updatedstop being stale on paths that do not set them by hand ([SQLModel §3]).- The exception swallowed to
Nonein an ORM model property is surfaced ([MED-08]).
7. UI Boundary and Duplication
- The three
ui.instructions.mdviolations are corrected ([HIGH-07]):jobs_page.pyno longer importssession_scopeor manages transactions; a service or workflow method owns the session.sources_page.pyno longer importssqlalchemy.inspect; the service returns a plaintransport_body_deferredflag on a read model.document_panzoomno longer callsget_settings(); a ready media URL is passed in.
- The duplication catalogued in the review's §4 is extracted, highest value first:
confirm_delete,media_urls,guards,formatters,upload_panel, and the hand-rolled tables that should usebuild_table(~500 lines). - The 23KB inline SVG moves to
ui/static/and is loaded through animportlib.resourcesreader alongside the existingread_css([MED-09]). people_page.py:504routes its error througherror_presenter.show_errorlike every sibling handler ([LOW-07]).- Untyped handler parameters and loosely-typed dict returns are annotated ([LOW-05]).
- The auto-refresh timer is cancelled rather than only deactivated, and its interval becomes a named constant ([LOW-06]).
8. Type Checking and Tooling Gate
- The codebase standardizes on
ty. Remaining suppressions are converted from# pyright: ignore[...]to# ty: ignore[...]([HIGH-06]). - The
lazy="raise"flip in §2 is expected to eliminate most of the ~160selectinloaddiagnostics by removing redundant eager loads. ty checkreaches zero diagnostics and is wired into the existing pre-commit setup as a gate ([HIGH-06]).- The two real bugs currently hidden in the diagnostic noise are fixed:
tests/ui/test_sources_page.py:25constructsSource(...)without the requireddocument_id, andtools/run_destructive_tests.py:76,80usesfcntl, which does not exist on the Windows development platform ([HIGH-06]). ruff checkreaches zero errors ([LOW-01]).asyncio_default_fixture_loop_scopeis configured explicitly so pytest-asyncio behavior does not change on upgrade ([Testing §3]).- The stale path in
.github/instructions/services.instructions.md:10is corrected tosrc/transcription/db/models.py([LOW-02]). list_jobsstops accepting and discardingload_docs([LOW-03]).resolve_worker_notifiervalidates itsgetattrresult ([LOW-04]).
Out of Scope
- Any new user-facing feature, page, action, or field.
- PostgreSQL enablement, Postgres-backed CI, or a Postgres cutover. The
use_alterfix unblocks it; it does not perform it. - Alembic or any migration framework, revision directory, history table, or down path.
- Multi-worker or multi-process execution. Forward-compatible code paths are written but not enabled or exercised.
- Concurrency limits, backpressure, or parallel job processing. Jobs remain strictly serial.
- Splitting
SourceServiceinto per-model services and relocatingupdate_job_source_transcriptiontoworkflows.py([MED-14]). Deferred to V4.7. It touches the transcription write path and cannot safely share a release with the schema re-level. - Any change to transcription prompt content, medium markers, quality-warning rules, or the retranscription workflow established in V4.5.
- Any change to the evidence, provenance, or immutability contracts established in V4.2 through V4.5.
- Deleting, rewriting, or reinterpreting existing
ExecutionAttemptorProcessingArtifactevidence during data migration. - Rewriting the UI table architecture, theme system, or CSS conventions beyond removing duplication.
- Performance work not traceable to a review finding.
Locked Design Decisions
A. Remediation Only
Every change traces to a review finding ID. A desirable improvement discovered during implementation that has no finding ID is recorded for a later revision rather than absorbed.
B. Re-Level, Do Not Migrate
The schema is pre-production and the data is disposable and backed up. Deleting the hand-rolled upgrade chain and regenerating from metadata is correct precisely because this window will not exist again. A migration framework is the right answer once the schema stabilizes, and V4.6 deliberately does not pretend that moment has arrived.
C. One Schema Pass
The re-level, the indexes, the use_alter fix, and the lazy="raise" flip all regenerate the same schema. They land together, are verified together, and are reverted together if verification fails. Partial application is not a valid state.
D. Data Migration Is Last
The migration script is written against the final schema and the final loading strategy. Writing it earlier guarantees rework and risks it carrying implicit lazy loads that lazy="raise" will later reject.
E. Forward Compatibility Where It Is Cheap
Single-process operation makes the atomic job claim non-urgent, not wrong. Where the correct multi-user implementation costs little more than the single-user one, V4.6 writes the correct one and guards it by dialect. Where it costs substantially more, V4.6 defers it and documents the assumption.
F. Behavior Is Preserved Exactly
A pure-remediation release that changes observable behavior has failed. The existing test suite is the contract: 264 passing tests must still pass, and any test that must change is treated as evidence that the change is not remediation.
G. The Instruction Files Are the Standard
Most findings are deviations from rules the project already wrote down. V4.6 restores conformance to services.instructions.md and ui.instructions.md rather than inventing new conventions — except where a rule is itself wrong, in which case the rule is corrected explicitly.
Acceptance Criteria
app_state.py,services/transcription.py, thestore.pyaliases,ServiceBase.queue, anddb/operations.py:get_next_queued_jobno longer exist, and the full suite passes without them.upgrade_schemaand the three_upgrade_*functions no longer exist; no rawALTER TABLEorCREATE INDEXstring appears insrc.- A database created from empty by
create_all()contains the compositeJobindex, indexed hot foreign keys, and apreferred_execution_attempt_idcolumn whose type matches the model declaration. - Compiling the metadata against the PostgreSQL dialect emits no unresolvable-cycle warning.
- No
Relationshipindb/models.pyuseslazy="selectin"as a bidirectional default; every load path that requires eager loading declares it per query, and the suite passes underlazy="raise". read_next_queued_jobreturns at most one row and issues no eager-load queries; a test asserts the emitted SQL containsLIMIT.- The worker processes two consecutive jobs against a single provider client instance; a test asserts the client is not reconstructed between jobs.
worker_provider_timeout_secondsaccepts a value above 20 seconds, and the OpenRouter client receives an explicithttpx.Timeout.inspect.signatureno longer appears in the transcription call path.DocumentTypeandPersonRoleCRUD is served by one shared implementation; the existing registry tests for both pass unchanged.ServiceBase._get_or_raiseis the only place aNOT_FOUNDguard is written for an entity fetched by id.- One media-storage implementation serves Source files, portraits, and homepage images, and its write is off the event loop.
list_sources_detailfilters byjob_idin SQL;read_source_navigationissues bounded queries;list_processing_artifactsaccepts alimit.- No page imports
session_scope,sqlalchemy.inspect, orget_settings. - The 23KB SVG literal no longer appears in any
.pyfile. ruff checkreports zero errors.ty checkreports zero diagnostics and runs as a pre-commit gate.tools/run_destructive_tests.pyruns on Windows.- All 264 pre-existing tests still pass. Any test modified during V4.6 is individually justified as a test defect rather than a behavior change.
- The migration script restores the backed-up V4.5 data into the re-leveled schema with row counts matching the backup, and no on-disk Source file, portrait, or artifact is modified.
- No new user-facing feature, page, action, or field exists in V4.6 that did not exist in V4.5.
- Database, integration, and UI verification uses isolated test data and never modifies
data/transcription.db.
Scope Freeze Gate
V4.6 is sufficiently frozen to begin implementation:
- The operating context — SQLite, single process, disposable data — is confirmed and its consequences for severity are resolved.
- The schema strategy is resolved: re-level, no Alembic, one pass, migration last.
- The severity band is resolved: Critical through Low, inclusive.
- The service-layer consolidation set is resolved, and the
SourceServicesplit is explicitly deferred to V4.7. - The release character is resolved: pure remediation, no new features.
Any expansion into PostgreSQL enablement, multi-worker execution, a migration framework, the SourceService split, or any new feature requires an explicit V4.6 scope amendment or a later revision.