generated from john/python-template
Compare commits
11
Commits
edcfba9cb2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5d7d8c7b | ||
|
|
939b0e46e9 | ||
|
|
15af11ecb5 | ||
|
|
6dc58a8d50 | ||
|
|
e90dbe4958 | ||
|
|
c9682b0399 | ||
|
|
dd80cd60cf | ||
|
|
754a273ead | ||
|
|
06bb4290be | ||
|
|
21478a904c | ||
|
|
b94d5d2863 |
@@ -1,13 +0,0 @@
|
|||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.vscode
|
|
||||||
.venv
|
|
||||||
.pytest_cache
|
|
||||||
.ruff_cache
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*.db
|
|
||||||
.env
|
|
||||||
tests/
|
|
||||||
docs/
|
|
||||||
uploads/
|
|
||||||
+7
-55
@@ -1,56 +1,8 @@
|
|||||||
# --- NiceGUI Server ---
|
PROVIDER=openrouter
|
||||||
# HOST=`0.0.0.0` (default)
|
OPENROUTER_API_KEY=sk-or-...
|
||||||
# PORT=8000 (default)
|
# PROVIDER_MODEL= # optional: OpenRouter adapter supplies default
|
||||||
# LOG_LEVEL: [`critical`, `error`, `warning`, `info` (default), `debug`, `trace`]
|
|
||||||
# RELOAD=false (default)
|
|
||||||
|
|
||||||
# --- AI provider ---
|
|
||||||
# PROVIDER=[`openrouter`(default), `google_genai`]
|
|
||||||
PROVIDER=openrouter
|
|
||||||
# OPENROUTER_API_KEY - Required when `PROVIDER=openrouter`
|
|
||||||
OPENROUTER_API_KEY=your-api-key-goes-here
|
|
||||||
# GEMINI_API_KEY - Required when `PROVIDER=google_genai`
|
|
||||||
# PROVIDER_MODEL= specify model. If left blank OpenRouter will supply default.
|
|
||||||
PROVIDER_MODEL=google/gemini-2.5-flash
|
|
||||||
# Optional JSON allowlist for model selection. The default above is always first.
|
|
||||||
# PROVIDER_MODELS=["google/gemini-2.5-flash","google/gemini-2.5-pro","anthropic/claude-sonnet-4"]
|
|
||||||
# OPENROUTER_HTTP_REFERER=https://example.com
|
# OPENROUTER_HTTP_REFERER=https://example.com
|
||||||
# OPENROUTER_APP_TITLE="Google: Gemini 2.5 Flash (openrouter)"
|
# OPENROUTER_APP_TITLE=Historical Transcription MVP
|
||||||
|
# DATABASE_URL=sqlite:///./transcription.db
|
||||||
# --- runtime environment ---
|
# UPLOAD_DIR=./uploads
|
||||||
# ENVIRONMENT: [`development`(default), `test`, `production`]
|
# PROMPT_DIR=./prompts
|
||||||
|
|
||||||
# --- persistence ---
|
|
||||||
# Use nested settings with double underscore because env_nested_delimiter="__".
|
|
||||||
# SQLite example:
|
|
||||||
# DATABASE__DRIVER=sqlite
|
|
||||||
# DATABASE__PATH=app.db
|
|
||||||
#
|
|
||||||
# SQLite with custom relative path:
|
|
||||||
# DATABASE__DRIVER=sqlite
|
|
||||||
DATABASE__PATH=./data/transcription.db
|
|
||||||
#
|
|
||||||
# Postgres example:
|
|
||||||
# DATABASE__DRIVER=postgres
|
|
||||||
# DATABASE__HOST=localhost
|
|
||||||
# DATABASE__PORT=5432
|
|
||||||
# DATABASE__DATABASE=transcription
|
|
||||||
# DATABASE__USER=postgres
|
|
||||||
# DATABASE__PASSWORD=change-me
|
|
||||||
#
|
|
||||||
# Optional persistence flags:
|
|
||||||
# BOOTSTRAP_SCHEMA_ON_STARTUP=false
|
|
||||||
# SQLITE_CHECK_SAME_THREAD=false
|
|
||||||
|
|
||||||
# --- filesystem paths ---
|
|
||||||
UPLOAD_DIR="./data"
|
|
||||||
PROMPT_DIR="./prompts"
|
|
||||||
|
|
||||||
# --- worker reliability ---
|
|
||||||
WORKER_MAX_RETRIES=0
|
|
||||||
WORKER_RETRY_BACKOFF_SECONDS=0
|
|
||||||
# WORKER_PROVIDER_TIMEOUT_SECONDS=180
|
|
||||||
WORKER_PROVIDER_TIMEOUT_SECONDS=180
|
|
||||||
WORKER_MIN_TRANSCRIPTION_CHARS=0
|
|
||||||
WORKER_MIN_TRANSCRIPTION_LINES=0
|
|
||||||
WORKER_FAIL_ON_FINISH_REASON_LENGTH=false
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: Python Architect Reviewer
|
|
||||||
description: Evidence-based senior architect reviewer for FastAPI, NiceGUI, and SQLModel codebases.
|
|
||||||
tools:
|
|
||||||
- read_file
|
|
||||||
- list_dir
|
|
||||||
- file_search
|
|
||||||
- grep_search
|
|
||||||
- run_in_terminal
|
|
||||||
skills:
|
|
||||||
- python-code-reviewer
|
|
||||||
---
|
|
||||||
|
|
||||||
# Python Architect Reviewer
|
|
||||||
|
|
||||||
You are a Senior Python Architect performing an evidence-based, read-only code review.
|
|
||||||
|
|
||||||
## Operating Principles
|
|
||||||
|
|
||||||
- **Stack Context:** Python 3.12+, FastAPI, NiceGUI, SQLModel, SQLAlchemy (SQLite/PostgreSQL), Pydantic V2, asyncio workers, and OpenRouter adapters.
|
|
||||||
- **Evidence-Based:** Always inspect real files. Every finding must reference concrete file paths and line numbers (e.g., `app/services/worker.py:45-78`). Do not speculate.
|
|
||||||
- **Tool Verification:** Run linters and tests via the terminal (`ruff check`, `pytest`, `ty`) to verify issues before reporting.
|
|
||||||
- **Skill Execution:** Adhere strictly to the review dimensions, duplication analysis, and report scaffolding defined in the `python-code-reviewer` skill.
|
|
||||||
- **Report Target:** Output all complete review reports as Markdown files written to `./docs`.
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
---
|
|
||||||
description: Follow these guidelines when editing the services
|
|
||||||
applyTo: 'src/transcription/services/*.py'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Services
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
- Project core data models are defined in [models](../../src/transcription/db/models.py)
|
|
||||||
- One service class per **aggregate**, not per table. An aggregate is a root model plus
|
|
||||||
the models that have no independent lifecycle of their own. `DocumentType` has no
|
|
||||||
meaning without `Document`, so it belongs to `DocumentService`; it does not get its
|
|
||||||
own service. Splitting per table produces services that must reach across each other
|
|
||||||
for every real operation, which is what line 13 forbids.
|
|
||||||
- Only services interact with the database, and only through async methods.
|
|
||||||
- **A service module must not import another service module.** This is enforced by
|
|
||||||
[test_service_boundaries](../../tests/test_service_boundaries.py). Shared types go in a
|
|
||||||
neutral module that defines no service class (see [errors](../../src/transcription/services/errors.py)).
|
|
||||||
- Not every module in this package is a service. Helper modules that define no `*Service`
|
|
||||||
class (`base`, `errors`, `normalization`, `prompts`, `quality`, `media_storage`,
|
|
||||||
`source_media`) are free-function modules and are exempt from the service rules below.
|
|
||||||
|
|
||||||
## Model Ownership
|
|
||||||
|
|
||||||
Every model has exactly one owning service. The owner defines that model's invariants and
|
|
||||||
is the only service that may **create or delete** its rows.
|
|
||||||
|
|
||||||
| Model | Owner |
|
|
||||||
| --- | --- |
|
|
||||||
| `Document`, `DocumentType` | `DocumentService` |
|
|
||||||
| `Source`, `JobSource` | `SourceService` |
|
|
||||||
| `Job` | `JobService` |
|
|
||||||
| `Person`, `PersonRole`, `DocumentPerson` | `PeopleService` |
|
|
||||||
| `ExecutionAttempt` | `EvidenceService` |
|
|
||||||
|
|
||||||
### Junction tables
|
|
||||||
|
|
||||||
A junction table is owned by the service that **creates and deletes its rows** — its
|
|
||||||
lifecycle owner. The service on the other side may read through the junction (via
|
|
||||||
`selectinload`) but must not create rows in it.
|
|
||||||
|
|
||||||
- `document_person` -> `PeopleService`. Every write is there; `DocumentService` only
|
|
||||||
eager-loads through it.
|
|
||||||
- `job_source` -> `SourceService`, which creates the row, records each page's outcome,
|
|
||||||
and deletes it.
|
|
||||||
|
|
||||||
Two consequences follow, and both are deliberate:
|
|
||||||
|
|
||||||
- **Cascade deletion is not a violation.** A service deleting the aggregate root it owns
|
|
||||||
may delete junction rows referencing that root, because they cannot outlive it
|
|
||||||
(`JobService.delete_job_with_guardrails`).
|
|
||||||
- **Ownership governs creation and deletion, not every state transition.** `job_source` is
|
|
||||||
both a link and the transcription work queue. `JobService.cancel_job` and
|
|
||||||
`resubmit_failed_sources` transition `job_source.status` across a whole job, because that
|
|
||||||
transition is a Job lifecycle event, not a per-page outcome. They create and delete
|
|
||||||
nothing.
|
|
||||||
|
|
||||||
`EvidenceService.promote_machine_attempt` writes two fields on `Source`
|
|
||||||
(`preferred_execution_attempt_id`, `raw_transcription`). This is allowed on the same
|
|
||||||
principle: selecting which attempt a Source presents is an evidence decision that happens
|
|
||||||
to land on `Source`. It is scoped to those two projection fields.
|
|
||||||
|
|
||||||
If a new operation cannot be expressed within one owner, it belongs in an orchestration
|
|
||||||
module, not in a cross-service import.
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
- Errors used by a single service are defined at the top of that module and inherit from `AppError`.
|
|
||||||
- Errors shared by more than one service go in [errors](../../src/transcription/services/errors.py),
|
|
||||||
which defines no service class and is therefore importable by any of them.
|
|
||||||
- Use a context manager for large `try/except` blocks, like `handle_transcription_errors` in
|
|
||||||
[sources](../../src/transcription/services/sources.py).
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Uses `ServiceBase` for common logic
|
|
||||||
- [ ] Session kwarg for `AsyncSession` to pass a session object into each method
|
|
||||||
- [ ] Services use `self._session_scope` in their methods to pass the session through
|
|
||||||
- Multiple operations on the same object(s) require sharing a session between all the methods used
|
|
||||||
- [ ] Every model the module touches is either owned by it or reached read-only
|
|
||||||
|
|
||||||
## CRUD Methods
|
|
||||||
|
|
||||||
- Name format `<operation>_<model>`, for example `create_document` or `update_job`.
|
|
||||||
- Where a service exposes create/read/update/delete for its root model, define them at the
|
|
||||||
top of the class in that order, before derived reads and workflow helpers.
|
|
||||||
- Not every aggregate needs all four. `ExecutionAttempt` is append-only evidence written by
|
|
||||||
`workflows.py`, so `EvidenceService` deliberately exposes reads and no create or delete.
|
|
||||||
Do not add unused CRUD methods to satisfy symmetry.
|
|
||||||
- `RegistryService` is generic across small lookup models and uses `<operation>_entry`
|
|
||||||
naming instead.
|
|
||||||
|
|
||||||
## Transaction Finalization
|
|
||||||
|
|
||||||
When a service method accepts an optional `session` kwarg, write methods must use `self._finalize` to finalize the transaction properly according to whether or not they are sharing a session.
|
|
||||||
|
|
||||||
- If `session` is `None`: the method owns the transaction and should `commit()`.
|
|
||||||
- If `session` is provided: the method must **not** commit; it should `flush()` so IDs and FK values are available to the caller's transaction.
|
|
||||||
- Use `refresh()` on returned ORM objects when the caller needs DB-populated values (defaults, triggers, merged state).
|
|
||||||
|
|
||||||
Recommended helper behavior:
|
|
||||||
|
|
||||||
- Inputs: active session object, original `session` arg (or a boolean ownership flag), and an optional list of objects to refresh.
|
|
||||||
- Logic: `commit` when service-owned session, `flush` when caller-owned session, then refresh requested objects.
|
|
||||||
|
|
||||||
This keeps orchestration functions atomic: they can pass one shared session across multiple services and commit exactly once at the workflow boundary.
|
|
||||||
|
|
||||||
## Workflow Transaction Boundaries
|
|
||||||
|
|
||||||
For multi-step job lifecycles (for example queued transcription jobs), orchestration functions must use explicit transaction phases.
|
|
||||||
|
|
||||||
Required boundary model:
|
|
||||||
|
|
||||||
- **Transaction A (claim):** transition `JobStatus.QUEUED -> JobStatus.PROCESSING` and commit immediately.
|
|
||||||
- Perform provider/network work **outside** database transactions.
|
|
||||||
- **Transaction B (terminal success):** write transcript content and set `JobStatus.TRANSCRIBED` in the same shared-session commit.
|
|
||||||
- **Transaction B (terminal failure):** write transcript error detail and set `JobStatus.FAILED` in the same shared-session commit.
|
|
||||||
- **Transaction C (retry path):** write transcript error detail, increment retry count, and set `JobStatus.QUEUED` in one shared-session commit.
|
|
||||||
|
|
||||||
Atomicity rules:
|
|
||||||
|
|
||||||
- Never commit transcript updates separately from the paired terminal/retry job status change.
|
|
||||||
- Terminal state (`TRANSCRIBED` or `FAILED`) and transcript row changes must succeed or roll back together.
|
|
||||||
- Retry persistence (`QUEUED` + retry increment + error detail) must succeed or roll back together.
|
|
||||||
|
|
||||||
Separation of concerns:
|
|
||||||
|
|
||||||
- Worker modules should stay lightweight and delegate lifecycle transitions to service/workflow orchestration functions.
|
|
||||||
- In `workflows.py`, `process_queued_job` should own one complete attempt lifecycle: `QUEUED -> PROCESSING -> TRANSCRIBED|FAILED`.
|
|
||||||
- In `workflows.py`, `advance_job` should coordinate broader status progression around attempts (for example retry scheduling from `FAILED -> QUEUED`).
|
|
||||||
- Services should expose session-aware write helpers (flush on caller-owned session) so orchestration controls commit boundaries.
|
|
||||||
- Backoff/sleep behavior must run outside transactional scopes.
|
|
||||||
|
|
||||||
# Service Composition
|
|
||||||
|
|
||||||
A service method may read across models it does not own, using eager loads from its own
|
|
||||||
aggregate root. What it may not do is import another service.
|
|
||||||
|
|
||||||
Operations that must **write** models owned by more than one service — uploading a picture,
|
|
||||||
for example — are composed in an orchestration module
|
|
||||||
([store](../../src/transcription/services/store.py),
|
|
||||||
[workflows](../../src/transcription/services/workflows.py)). Orchestration modules define no
|
|
||||||
service class, may import any service, and own the commit boundary.
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Use when modifying the NiceGUI application under src/transcription/ui. Defines ownership and dependency boundaries for UI registration, pages, components, services, persistence, state, and static assets."
|
|
||||||
applyTo: 'src/transcription/ui/**/*.py'
|
|
||||||
---
|
|
||||||
|
|
||||||
# UI Conceptual Boundaries
|
|
||||||
|
|
||||||
Keep dependencies flowing in this direction:
|
|
||||||
|
|
||||||
`ui/__init__.py` -> `pages` -> `components`
|
|
||||||
|
|
||||||
Pages may depend on application services and framework-provided dependencies. Components may depend on smaller components and shared presentation helpers. Services and domain modules must never depend on the UI.
|
|
||||||
|
|
||||||
## Package Root
|
|
||||||
|
|
||||||
- Keep `ui/__init__.py` as the UI composition root: register global assets, register pages, and mount NiceGUI on FastAPI.
|
|
||||||
- Do not put feature rendering, service calls, persistence, or route-specific state in the package root.
|
|
||||||
|
|
||||||
## Pages
|
|
||||||
|
|
||||||
- Pages own route registration and route-level orchestration.
|
|
||||||
- Resolve request or application dependencies, call [services](../../src/transcription/services/), adapt returned data for presentation when needed, and coordinate refresh, navigation, and notifications here.
|
|
||||||
- Do not query, mutate, commit, or roll back the database from a page. Do not import database engines, sessions, operations, or query-building APIs. Persistence belongs to services or workflow functions.
|
|
||||||
- Framework dependency types may cross into page handlers only to construct or invoke services; do not pass sessions or session factories into components.
|
|
||||||
- Keep business rules, lifecycle transitions, transaction boundaries, and cross-service workflows out of page callbacks.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
- Components own reusable rendering, widget-local state, input normalization, and presentation-only formatting.
|
|
||||||
- Expose user actions through typed callback parameters. The calling page decides which service or workflow runs and what refresh or navigation follows.
|
|
||||||
- Do not register routes, resolve request/app state, instantiate services, or access persistence from components.
|
|
||||||
- Components may accept ORM models returned by services as read-only snapshots. Only use fields and relationships that the service loaded eagerly; never mutate models, trigger lazy loading, or expose session behavior.
|
|
||||||
- A component may compose lower-level components, but it must not import from `pages`.
|
|
||||||
|
|
||||||
## Shared UI Infrastructure
|
|
||||||
|
|
||||||
- Keep app-wide navigation and layout primitives in `components/app_shell.py`.
|
|
||||||
- Keep generic table/event adaptation in `components/table/common.py`; feature-specific columns, row read models, and formatting belong in the feature table module.
|
|
||||||
- Keep exception normalization and user-facing error display in `components/error_presenter.py`; preserve `AppError` details and operation identifiers at page/component boundaries.
|
|
||||||
|
|
||||||
## CSS Assets
|
|
||||||
|
|
||||||
- Keep all application CSS in `ui/static/theme.css`; do not add page- or component-specific stylesheets or embed style blocks in Python components.
|
|
||||||
- Load `theme.css` once from the composition root with `ui.add_css(..., shared=True)`.
|
|
||||||
- Read stylesheet text through `importlib.resources.files(...)` so loading works from installed packages and is independent of the working directory.
|
|
||||||
- Centralize CSS reading in one typed helper cached by resource path with `functools.cache` or an equivalent unbounded `lru_cache`. Cache the immutable stylesheet text to avoid repeated resource I/O; keep NiceGUI registration at the composition root.
|
|
||||||
- Do not encode application behavior in CSS or other static assets.
|
|
||||||
|
|
||||||
## State and Side Effects
|
|
||||||
|
|
||||||
- Limit component state to ephemeral interaction state such as loading flags, form values, dialogs, and expansion state.
|
|
||||||
- Application and worker state must be resolved at the page or application boundary and passed through narrow interfaces such as callbacks or notifier protocols.
|
|
||||||
- Keep filesystem, network, provider, and worker orchestration behind application services or dedicated adapters. UI code may trigger those operations but must not implement them.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Review Python Architecture
|
|
||||||
description: Run an evidence-based architectural code review using the Python Architect Reviewer agent and python-code-reviewer skill.
|
|
||||||
agent: Python Architect Reviewer
|
|
||||||
---
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
|
|
||||||
Execute a comprehensive, evidence-based code review of the target codebase.
|
|
||||||
|
|
||||||
## Target Scope
|
|
||||||
- **Review Target:** ${{input:target_path:./}}
|
|
||||||
- **Source Root:** `src/`
|
|
||||||
- **Docs Root:** `docs/`
|
|
||||||
- **Focus Areas:** FastAPI endpoints, NiceGUI components, SQLModel persistence, asyncio workers, Pydantic V2 models, and OpenRouter provider adapters.
|
|
||||||
|
|
||||||
## Execution Rules
|
|
||||||
1. Map repository layout, dependency manifests, and configuration files from the project root before inspecting modules.
|
|
||||||
2. Read real code modules under `src/` (or the specified target path); cite exact file paths and line ranges for every finding.
|
|
||||||
3. Validate issues using terminal tools (`ruff check`, `pytest`, `ty`) where appropriate.
|
|
||||||
4. Check for duplication, divergent implementations, and extractable helpers.
|
|
||||||
5. Format the entire review following the standardized 6-section template defined in the `python-code-reviewer` skill.
|
|
||||||
6. Write the final report as a Markdown file to `./docs/code-review-${{current_date}}.md`.
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
---
|
|
||||||
name: python-code-reviewer
|
|
||||||
description: Perform an evidence-based, senior architect code review for Python codebases using FastAPI, NiceGUI, SQLModel, SQLAlchemy, Pydantic V2, asyncio, and OpenRouter. Use when asked to review Python repositories, perform architectural or code audits, or evaluate code against Python 3.12+ best practices.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Python Code Reviewer
|
|
||||||
|
|
||||||
Perform thorough, evidence-based code reviews for Python projects. Every finding must cite concrete file paths and line ranges, avoid speculation, and include recommended fixes.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Performing an architectural or code quality review of a Python codebase.
|
|
||||||
- Auditing applications using FastAPI, NiceGUI, SQLModel/SQLAlchemy, Pydantic V2, or asyncio workers.
|
|
||||||
- Generating structured Markdown review reports in `./docs`.
|
|
||||||
|
|
||||||
## Technical Stack Scope
|
|
||||||
|
|
||||||
- **Runtime:** Python 3.12+
|
|
||||||
- **Web Application:** FastAPI and NiceGUI
|
|
||||||
- **Persistence:** SQLModel, SQLAlchemy (SQLite and PostgreSQL support)
|
|
||||||
- **Validation & Settings:** Pydantic V2 and pydantic-settings
|
|
||||||
- **Concurrency:** Python asyncio workers
|
|
||||||
- **Vision/LLM Integration:** OpenRouter / provider adapters
|
|
||||||
- **Quality & Testing:** pytest, pytest-asyncio, Ruff, and ty
|
|
||||||
|
|
||||||
## Review Workflow
|
|
||||||
|
|
||||||
1. **Map the Repository First:** Inspect entry points, package layout, configurations, dependency manifests, and any project-specific rule files (`AGENTS.md`, `CLAUDE.md`, `.github/instructions/`). Project-specific conventions override generic advice.
|
|
||||||
2. **Read Representative Modules:** Sample across all layers (routes/pages, UI components, services, workers, persistence, provider adapters, settings, tests) before drawing conclusions.
|
|
||||||
3. **Verify Claims:** Run or reference project tooling (`ruff check`, `ty`, `pytest`) rather than guessing.
|
|
||||||
4. **Prioritize Hot Paths:** Focus deeply on request handling, database sessions, background workers, and external API calls.
|
|
||||||
5. **Enforce Read-Only Safety:** Do not modify code unless explicitly instructed.
|
|
||||||
|
|
||||||
## Core Review Areas
|
|
||||||
|
|
||||||
### 1. Python Best Practices (3.12+)
|
|
||||||
- **Type Annotations:** Ensure completeness, modern syntax (`X | None`, builtin generics, `Self`, `type` statements), and avoid unparameterized containers or bare `Any`.
|
|
||||||
- **Error Handling:** Identify bare/broad `except`, swallowed exceptions, missing `raise ... from`, and exceptions used for control flow.
|
|
||||||
- **Resource Management:** Verify context managers for files, DB sessions, HTTP clients, and locks. Check for leaked tasks or connections.
|
|
||||||
- **Data Modeling:** Check proper use of dataclasses vs. Pydantic models vs. dictionaries. Eliminate mutable default arguments and stringly-typed payloads.
|
|
||||||
- **Idioms & Clean Code:** Verify `pathlib` usage over `os.path`, comprehensions vs manual loops, removal of dead code, and elimination of magic numbers.
|
|
||||||
|
|
||||||
### 2. FastAPI
|
|
||||||
- **Dependency Injection:** Verify `Depends` is used for shared resources (DB sessions, settings, clients) rather than global singletons.
|
|
||||||
- **Route Design:** Validate HTTP verbs, status codes, path/query/body typing, `response_model`, and domain-based router organization.
|
|
||||||
- **Lifecycle & Concurrency:** Ensure lifespan handlers are used instead of deprecated `@app.on_event`. Flag blocking synchronous calls in `async def` endpoints.
|
|
||||||
|
|
||||||
### 3. NiceGUI
|
|
||||||
- **Separation of Concerns:** Ensure UI components delegate business logic and persistence to service layers.
|
|
||||||
- **Client State Handling:** Verify correct use of client-scoped state vs global state to avoid state leaks across sessions.
|
|
||||||
- **Async Execution:** Check for blocking operations on the UI event loop and unbounded timers/pollers.
|
|
||||||
|
|
||||||
### 4. Persistence (SQLModel / SQLAlchemy)
|
|
||||||
- **Session Lifecycle:** Enforce one session per request/unit of work with explicit commit/rollback/close boundaries.
|
|
||||||
- **Query Optimization:** Detect N+1 patterns, missing eager loads (`selectinload`/`joinedload`), queries inside loops, and unindexed filters.
|
|
||||||
- **Cross-Dialect Portability:** Check compatibility for both SQLite (WAL mode, pragmas) and PostgreSQL (JSONB, locking, autoincrement).
|
|
||||||
|
|
||||||
### 5. Pydantic V2 & Settings
|
|
||||||
- **V2 Migration:** Flag legacy V1 patterns (`@validator`, `Config` class, `.dict()`, `parse_obj`) and use V2 equivalents (`@field_validator`, `model_config = ConfigDict(...)`, `model_dump()`).
|
|
||||||
- **Settings Management:** Ensure `BaseSettings` is the single source of truth without scattered `os.getenv` calls or committed secrets.
|
|
||||||
|
|
||||||
### 6. Concurrency & Asyncio Workers
|
|
||||||
- **Task Lifecycle:** Flag unreferenced `create_task` calls that risk garbage collection, missing cancellation handling, and lack of graceful shutdown.
|
|
||||||
- **Backpressure & Synchronization:** Check for appropriate use of `asyncio.Queue`, `TaskGroup`, `Lock`, and backoff retries.
|
|
||||||
|
|
||||||
### 7. Provider Adapters (OpenRouter / APIs)
|
|
||||||
- **Adapter Encapsulation:** Verify provider-specific details (headers, model names, payload formats) do not leak into UI or business logic.
|
|
||||||
- **Client Lifecycle:** Reuse shared `AsyncClient` instances with proper connection pooling and timeouts. Validate API responses using Pydantic schemas.
|
|
||||||
|
|
||||||
### 8. Testing & Quality Tooling
|
|
||||||
- **Test Isolation:** Verify tests do not rely on live external services, real clocks, or shared global state.
|
|
||||||
- **Async Test Setup:** Check `pytest-asyncio` configuration and fixture lifecycle.
|
|
||||||
|
|
||||||
### 9. Duplication & Consolidation
|
|
||||||
- Identify repeated code blocks, candidate helper abstractions, divergent patterns for identical operations, and duplicated domain constants.
|
|
||||||
|
|
||||||
## Output Report Structure & Template
|
|
||||||
|
|
||||||
Generate Markdown reports in `./docs` following this exact template structure:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Architecture & Code Review Report
|
|
||||||
|
|
||||||
**Repository Target:** `project-root/`
|
|
||||||
**Target Stack:** Python 3.12+ | FastAPI | NiceGUI | SQLModel/SQLAlchemy | Pydantic V2 | asyncio | OpenRouter
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
|
||||||
- 5-10 bullets on overall health, top risks, and high-leverage refactors.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Findings by Severity
|
|
||||||
|
|
||||||
### Critical Severity
|
|
||||||
#### [CRIT-01] Title
|
|
||||||
- **Location:** `path/to/file.py:lines`
|
|
||||||
- **Problem & Consequence:** Concrete consequence, not a style opinion.
|
|
||||||
- **Recommendation:** Fix with before/after sketch.
|
|
||||||
- **Effort:** S / M / L
|
|
||||||
|
|
||||||
### High Severity
|
|
||||||
#### [HIGH-01] Title
|
|
||||||
...
|
|
||||||
|
|
||||||
### Medium Severity
|
|
||||||
#### [MED-01] Title
|
|
||||||
...
|
|
||||||
|
|
||||||
### Low Severity
|
|
||||||
#### [LOW-01] Title
|
|
||||||
...
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Stack-Specific Analysis
|
|
||||||
- Python 3.12+ Best Practices
|
|
||||||
- FastAPI
|
|
||||||
- NiceGUI
|
|
||||||
- SQLModel & SQLAlchemy
|
|
||||||
- Pydantic V2 & Settings
|
|
||||||
- Asyncio Workers
|
|
||||||
- OpenRouter / Adapter Boundary
|
|
||||||
- Testing & Quality Tooling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Duplication & Consolidation Report
|
|
||||||
| Pattern / Duplication | Locations | Proposed Canonical Home | Estimated Lines Removed |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
|
|
||||||
### Proposed Canonical Abstractions
|
|
||||||
- Code signatures and implementation homes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Prioritized Action Plan
|
|
||||||
1. **Phase 1: Quick Wins (PR 1-2)**
|
|
||||||
2. **Phase 2: Reliability & Concurrency (PR 3-4)**
|
|
||||||
3. **Phase 3: Consolidation & Refactoring (PR 5-6)**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Preserved Strengths
|
|
||||||
- Existing patterns worth maintaining.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
name: Quality Gate
|
|
||||||
|
|
||||||
# V4.7 Phase 6 / review log [40]. Before this, ruff, ty and pytest were enforced
|
|
||||||
# only by .pre-commit-config.yaml, and only for developers who had actually run
|
|
||||||
# `pre-commit install`.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
gate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out the commit under test
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
run: |
|
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
||||||
|
|
||||||
- name: Install dependencies from the lockfile
|
|
||||||
# --locked fails if uv.lock has drifted from pyproject.toml, so a stale
|
|
||||||
# lockfile is caught here rather than producing an untested dependency set.
|
|
||||||
run: uv sync --locked
|
|
||||||
|
|
||||||
- name: Write placeholder configuration
|
|
||||||
# Settings requires openrouter_api_key and 115 tests cannot construct
|
|
||||||
# Settings without it. This is written to a .env file rather than exported
|
|
||||||
# as an environment variable on purpose: the external tests guard on
|
|
||||||
# os.getenv("OPENROUTER_API_KEY"), which reads the process environment and
|
|
||||||
# not the file, so writing the file reproduces the local result exactly -
|
|
||||||
# the 4 external tests skip instead of running against a fake key and
|
|
||||||
# failing. Exporting it instead produces 3 failures.
|
|
||||||
run: echo "OPENROUTER_API_KEY=ci-placeholder-not-a-real-key" > .env
|
|
||||||
|
|
||||||
- name: Lint and type check
|
|
||||||
# Runs the hooks defined in .pre-commit-config.yaml instead of repeating
|
|
||||||
# "ruff check" and "ty check" here. The commands then have one definition,
|
|
||||||
# so the local and CI gates cannot drift apart.
|
|
||||||
run: uv run pre-commit run --all-files --show-diff-on-failure
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
run: uv run pytest
|
|
||||||
@@ -14,10 +14,3 @@ wheels/
|
|||||||
|
|
||||||
# SQLite database
|
# SQLite database
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
# Document images
|
|
||||||
uploads/*
|
|
||||||
data/*
|
|
||||||
|
|
||||||
# Local destructive-test backups
|
|
||||||
.test-backups/
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Quality gate for V4.6 [HIGH-06]. Both hooks are blocking: a regression in
|
|
||||||
# `ruff check` or `ty check` fails the commit.
|
|
||||||
repos:
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: ruff
|
|
||||||
name: ruff check
|
|
||||||
entry: ruff check
|
|
||||||
language: system
|
|
||||||
types_or: [python, pyi]
|
|
||||||
require_serial: true
|
|
||||||
- id: ty
|
|
||||||
name: ty check
|
|
||||||
entry: ty check
|
|
||||||
language: system
|
|
||||||
types_or: [python, pyi]
|
|
||||||
pass_filenames: false
|
|
||||||
require_serial: true
|
|
||||||
Vendored
-23
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Python: Debug transcription app",
|
|
||||||
"type": "debugpy",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "debugpy",
|
|
||||||
"args": [
|
|
||||||
"-m",
|
|
||||||
"transcription",
|
|
||||||
"--host", "127.0.0.1",
|
|
||||||
"--port", "9999",
|
|
||||||
"--database.driver", "sqlite"
|
|
||||||
],
|
|
||||||
"justMyCode": true,
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"env": {
|
|
||||||
"PYTHONPATH": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"chat.sessionSync.enabled": true
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
FROM python:3.12-slim AS builder
|
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
UV_LINK_MODE=copy
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.5.24 /uv /uvx /bin/
|
|
||||||
|
|
||||||
COPY pyproject.toml uv.lock README.md ./
|
|
||||||
RUN uv sync --frozen --no-dev --no-install-project
|
|
||||||
|
|
||||||
COPY src ./src
|
|
||||||
COPY prompts ./prompts
|
|
||||||
RUN uv sync --frozen --no-dev
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.12-slim AS runtime
|
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PATH="/app/.venv/bin:$PATH" \
|
|
||||||
PYTHONPATH="/app/src" \
|
|
||||||
UPLOAD_DIR="/app/uploads" \
|
|
||||||
PROMPT_DIR="/app/prompts"
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN groupadd --system --gid 1001 appgroup \
|
|
||||||
&& useradd --system --uid 1001 --gid appgroup --create-home appuser
|
|
||||||
|
|
||||||
COPY --from=builder /app/.venv /app/.venv
|
|
||||||
COPY --from=builder /app/src /app/src
|
|
||||||
COPY --from=builder /app/prompts /app/prompts
|
|
||||||
|
|
||||||
RUN mkdir -p /app/uploads /app/data \
|
|
||||||
&& chown -R appuser:appgroup /app
|
|
||||||
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=3s --retries=3 \
|
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"
|
|
||||||
|
|
||||||
CMD ["uvicorn", "transcription.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
|
|
||||||
@@ -22,105 +22,73 @@ uv sync
|
|||||||
|
|
||||||
### 2) Configure environment
|
### 2) Configure environment
|
||||||
|
|
||||||
Create a `.env` file in the project root with the required OpenRouter API key:
|
Create a `.env` file in the project root (minimum required setting shown):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
OPENROUTER_API_KEY=your_openrouter_api_key
|
OPENROUTER_API_KEY=your_openrouter_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
Settings are read from CLI arguments first, then environment variables, then `.env`, then the defaults below.
|
Optional settings (defaults shown):
|
||||||
|
|
||||||
### Configuration Source Precedence
|
|
||||||
|
|
||||||
When the same setting is provided in multiple places, the value is chosen in this order (highest priority first):
|
|
||||||
|
|
||||||
1. CLI arguments (for example `--port 8000`)
|
|
||||||
2. Settings constructor arguments (used mainly in tests)
|
|
||||||
3. Environment variables
|
|
||||||
4. `.env` file values
|
|
||||||
5. Model defaults in code
|
|
||||||
|
|
||||||
Practical examples:
|
|
||||||
|
|
||||||
- `--port 8000` overrides both `PORT=8000` in the shell and `PORT=7000` in `.env`.
|
|
||||||
- `DATABASE__PATH=prod.db` in the shell overrides `DATABASE__PATH=dev.db` in `.env`.
|
|
||||||
|
|
||||||
#### Server and runtime
|
|
||||||
|
|
||||||
| Environment variable | Default | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `HOST` | `0.0.0.0` | Address on which the server listens. |
|
|
||||||
| `PORT` | `8000` | Server port. |
|
|
||||||
| `LOG_LEVEL` | `info` | Uvicorn and application log level. |
|
|
||||||
| `RELOAD` | `false` | Restart the development server when source files change. |
|
|
||||||
| `ENVIRONMENT` | `development` | Runtime environment: `development`, `test`, or `production`. |
|
|
||||||
|
|
||||||
#### Provider
|
|
||||||
|
|
||||||
| Environment variable | Default | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `PROVIDER` | `openrouter` | Transcription provider. |
|
|
||||||
| `OPENROUTER_API_KEY` | Required | OpenRouter API key. |
|
|
||||||
| `PROVIDER_MODEL` | Provider default | Optional model override. |
|
|
||||||
| `OPENROUTER_HTTP_REFERER` | Unset | Optional OpenRouter attribution URL. |
|
|
||||||
| `OPENROUTER_APP_TITLE` | Unset | Optional OpenRouter attribution title. |
|
|
||||||
|
|
||||||
#### Database and files
|
|
||||||
|
|
||||||
Use nested env vars for database settings (recommended):
|
|
||||||
|
|
||||||
```env
|
```env
|
||||||
DATABASE__DRIVER=sqlite
|
DATABASE_URL=sqlite:///./transcription.db
|
||||||
DATABASE__PATH=app.db
|
|
||||||
# BOOTSTRAP_SCHEMA_ON_STARTUP=true
|
|
||||||
SQLITE_CHECK_SAME_THREAD=false
|
|
||||||
UPLOAD_DIR=./uploads
|
UPLOAD_DIR=./uploads
|
||||||
PROMPT_DIR=./prompts
|
PROMPT_DIR=./prompts
|
||||||
DEFAULT_PROMPT_NAME=transcribe_document.md
|
MAX_UPLOAD_BYTES=15728640
|
||||||
# TRANSCRIPTION_TEMPERATURE=0.2 # range: 0.0-2.0
|
OPERATOR_ACCESS_ENABLED=false
|
||||||
# TRANSCRIPTION_TOP_P=0.9 # range: 0.0-1.0
|
OPERATOR_USERNAME=operator
|
||||||
|
# OPERATOR_PASSWORD=replace_with_secure_value
|
||||||
```
|
```
|
||||||
|
|
||||||
For PostgreSQL:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE__DRIVER=postgres
|
|
||||||
DATABASE__HOST=localhost
|
|
||||||
DATABASE__PORT=5432
|
|
||||||
DATABASE__DATABASE=transcription
|
|
||||||
DATABASE__USER=postgres
|
|
||||||
DATABASE__PASSWORD=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
This uses Pydantic nested settings (`env_nested_delimiter='__'`) and avoids JSON blobs in `.env`. A top-level `DATABASE={...}` JSON value is still supported as a fallback, and nested keys such as `DATABASE__PATH` take precedence over conflicting JSON keys.
|
|
||||||
|
|
||||||
`BOOTSTRAP_SCHEMA_ON_STARTUP` creates missing tables when the app starts. When unset, it is enabled in `development` and `test`, and disabled in `production`; set it explicitly to override that policy. `SQLITE_CHECK_SAME_THREAD` defaults to `false`.
|
|
||||||
|
|
||||||
#### Worker
|
|
||||||
|
|
||||||
```env
|
|
||||||
WORKER_MAX_RETRIES=0
|
|
||||||
WORKER_RETRY_BACKOFF_SECONDS=0
|
|
||||||
WORKER_PROVIDER_TIMEOUT_SECONDS=20
|
|
||||||
WORKER_MIN_TRANSCRIPTION_CHARS=0
|
|
||||||
WORKER_MIN_TRANSCRIPTION_LINES=0
|
|
||||||
WORKER_FAIL_ON_FINISH_REASON_LENGTH=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3) Run the app
|
### 3) Run the app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run python -m transcription --port 8000 --reload --database.driver sqlite --bootstrap-schema-on-startup
|
uv run uvicorn transcription.app:create_app --factory --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
This starts the development server with SQLite, creates missing tables, and enables automatic reload. Run `uv run python -m transcription --help` for all CLI options; CLI names use kebab case and nested database options use dot notation, such as `--database.path ./data/transcription.db`.
|
### 4) (Optional) Run explicit migrations/checks
|
||||||
|
|
||||||
### 4) Open in browser
|
Use the migration runner for Step 4 schema safety workflows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m transcription.migration_runner --list
|
||||||
|
uv run python -m transcription.migration_runner --apply
|
||||||
|
uv run python -m transcription.migration_runner --check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5) Open in browser
|
||||||
|
|
||||||
|
|
||||||
|
- GUI: [http://[IP_ADDRESS]:8000/ui](http://[IP_ADDRESS]:8000/ui)
|
||||||
|
- Health check: [http://[IP_ADDRESS]:8000/healthz](http://[IP_ADDRESS]:8000/healthz)
|
||||||
|
|
||||||
|
### Schema safety settings
|
||||||
|
|
||||||
|
Optional environment settings (defaults shown):
|
||||||
|
|
||||||
|
```env
|
||||||
|
MIGRATION_AUTO_APPLY_ON_STARTUP=false
|
||||||
|
VALIDATE_SCHEMA_ON_STARTUP=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5 security settings
|
||||||
|
|
||||||
|
Use this baseline for trusted private-network operation:
|
||||||
|
|
||||||
|
```env
|
||||||
|
OPERATOR_ACCESS_ENABLED=true
|
||||||
|
OPERATOR_USERNAME=operator
|
||||||
|
OPERATOR_PASSWORD=replace_with_strong_local_secret
|
||||||
|
MAX_UPLOAD_BYTES=15728640
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `/healthz` remains unauthenticated for operational checks.
|
||||||
|
- `/ui` and `/api` require HTTP Basic credentials when operator access is enabled.
|
||||||
|
- Keep `OPERATOR_PASSWORD` in environment variables only (never commit secrets).
|
||||||
|
|
||||||
- GUI: [http://localhost:8000/ui](http://localhost:8000/ui)
|
|
||||||
- Health check: [http://localhost:8000/healthz](http://localhost:8000/healthz)
|
|
||||||
|
|
||||||
Replace `localhost` with the server's hostname or IP address when connecting from another machine.
|
|
||||||
|
|
||||||
## How to navigate the GUI
|
## How to navigate the GUI
|
||||||
|
|
||||||
@@ -141,65 +109,7 @@ Replace `localhost` with the server's hostname or IP address when connecting fro
|
|||||||
|
|
||||||
## Prompt artifacts
|
## Prompt artifacts
|
||||||
|
|
||||||
Prompt files are stored directly in `PROMPT_DIR` (default: `./prompts`). `DEFAULT_PROMPT_NAME` must be a filename,
|
Prompt files are stored in `prompts/` and loaded from `PROMPT_DIR` (default: `./prompts`).
|
||||||
not a path. Each job snapshots the validated prompt text, SHA-256 hash, and sampling values for reproducibility.
|
|
||||||
|
|
||||||
The canonical MVP prompt is:
|
The canonical MVP prompt is:
|
||||||
- `prompts/transcribe_document.md`
|
- `prompts/transcribe_document.md`
|
||||||
|
|
||||||
## Destructive test procedure (with data backup)
|
|
||||||
|
|
||||||
AI execution policy: before the first unit-test run in a test/fix cycle, create one backup of `./data`. Reuse that same backup for every subsequent test run in the cycle. After tests succeed, always pause and ask whether to restore now.
|
|
||||||
|
|
||||||
Use the cross-platform Python wrapper below whenever an AI agent runs tests against this repository.
|
|
||||||
|
|
||||||
1. Create one backup of `./data` and mark it as the active test-cycle backup.
|
|
||||||
2. Run your test command.
|
|
||||||
3. On failure, fix the errors and run the wrapper again; it reuses the active backup and never backs up post-test data.
|
|
||||||
4. On success, always prompt whether to restore now (do not auto-restore unless explicitly approved).
|
|
||||||
5. Close the cycle only by restoring the active backup or explicitly accepting the current data.
|
|
||||||
|
|
||||||
Preflight behavior:
|
|
||||||
|
|
||||||
- Backup preflight is warning-only when `data/transcription.db` appears in use.
|
|
||||||
- Restore preflight is blocking: the script prompts you to close conflicting applications, then type `retry` to re-check or `cancel` to skip restore.
|
|
||||||
|
|
||||||
### Run with confirmation-gated restore (default)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python tools/run_destructive_tests.py -- pytest tests/services/test_job_service.py tests/ui/test_jobs_page.py
|
|
||||||
```
|
|
||||||
|
|
||||||
After tests pass, the script asks whether to restore backup immediately.
|
|
||||||
|
|
||||||
This is the required default mode for AI-assisted test runs because it gives time to verify and accept code changes before any restoration happens.
|
|
||||||
|
|
||||||
### Run with automatic restore (non-interactive)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python tools/run_destructive_tests.py --auto-restore -- pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run without terminal prompt (decide restore later)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python tools/run_destructive_tests.py --skip-restore-prompt -- pytest
|
|
||||||
```
|
|
||||||
|
|
||||||
This keeps both the current post-test state and the backup, so restore can be decided explicitly later.
|
|
||||||
|
|
||||||
Repeated wrapper invocations reuse the backup recorded in `.test-backups/.active-backup`. If that backup is missing, the wrapper stops rather than creating a replacement from potentially destructive post-test data.
|
|
||||||
|
|
||||||
### Restore later from a saved backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python tools/run_destructive_tests.py --restore-from data-backup-YYYYMMDD-HHMMSS
|
|
||||||
```
|
|
||||||
|
|
||||||
To keep the current data and close the active cycle without restoring:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python tools/run_destructive_tests.py --accept-current-data
|
|
||||||
```
|
|
||||||
|
|
||||||
Backups are stored in `.test-backups/` and ignored by git.
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
services:
|
|
||||||
transcription:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: transcription-app
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# Database configuration uses nested settings names (env_nested_delimiter="__").
|
|
||||||
# DATABASE_URL is NOT read by the application and must not be used here.
|
|
||||||
DATABASE__DRIVER: sqlite
|
|
||||||
DATABASE__PATH: /app/data/transcription.db
|
|
||||||
UPLOAD_DIR: /app/uploads
|
|
||||||
PROMPT_DIR: /app/prompts
|
|
||||||
ports:
|
|
||||||
- "8002:8000"
|
|
||||||
volumes:
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- transcription_data:/app/data
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
transcription_data:
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Historical Document Transcription
|
||||||
|
I have several thousand pages of family history told through letters, postcards, books, and other documents that I want to transcribe to text.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
1. Preserve our family history
|
||||||
|
2. Unburden my family (and descendants) from having to store and care for the physical media. Once the documents have been transcribed and organized, they can be donated (or kept by a family member that wants to retain them).
|
||||||
|
3. Make the text easily available and searchable by family members, as well as AI (which may have different requirements).
|
||||||
|
4. Ability create timelines or assemble the historical record of the family or specific individuals from across the complete document archive. Perhaps use AI to create the timelines in a more narrative form.
|
||||||
|
|
||||||
|
## Source material
|
||||||
|
1. **letters, cards, diaries** - handwritten; mostly stored in tubs, with little organization
|
||||||
|
2. **books** - typed or typeset; mostly self-published books 50-100 pages in length. This may be expanded to include selected pages from other publications.
|
||||||
|
3. **newspaper clippings, event programs, invitations, and other ephemera**
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
### Verbatim vs. Clean Copy
|
||||||
|
Transcriptions should be Verbatim and follow scholarly research guidelines, with no modifications to the original text.
|
||||||
|
|
||||||
|
### Prompt Curation Policy
|
||||||
|
Transcription behavior should be implemented with prompt assets that are human-maintainable over time.
|
||||||
|
|
||||||
|
1. Each transcription prompt is stored as an individual Markdown file.
|
||||||
|
2. Prompt files are refined iteratively as document quality and edge cases are discovered.
|
||||||
|
3. Prompt changes should be scoped to one prompt file at a time whenever possible to keep review history clear.
|
||||||
|
|
||||||
|
### Potential Document Issues
|
||||||
|
| Document Issue | How to Handle It | Example |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Misspellings & Errors** | Retain original spelling and insert italicized `[sic]` directly after the error. | `The weather was very cold and publick [sic] business delayed.` |
|
||||||
|
| **Missing Words / Slips** | Insert the missing word inside square brackets to restore basic readability. | `We went [to] the store to buy supplies.` |
|
||||||
|
| **Uncertain / Guesswork** | Place your best hypothesis followed by a question mark inside square brackets. | `He went to [Boston?] yesterday to meet the governor.` |
|
||||||
|
| **Completely Illegible** | Use a clear descriptive term like `[illegible]` or specify the reason (e.g., `[torn]`, `[ink blot]`). | `The total cost was [illegible] dollars.` or `The letter ends here [remainder of page torn].` |
|
||||||
|
| **Crossed-out Text** | Wrap the removed word or phrase in a deleted tag to preserve the author's edits. | `We left at [deleted: noon] one o'clock instead.` |
|
||||||
|
| **Squeezed-in Text** | Wrap text that was added above the line or in a tight space in an inserted tag. | `The [inserted: red] house on the hill was abandoned.` |
|
||||||
|
| **Superscripts & Abbreviations** | Bring raised letters down to the main line, or optionally expand them in brackets. | `Change Gen^l to Genl` OR `Change to Gen[era]l depending on project preference.` |
|
||||||
|
| **Images / Seals / Signs** | Describe the non-textual element using italicized text inside square brackets. | `[wax notary seal attached here]` or `[sketch of a fort layout]` |
|
||||||
|
| **Marginalia / Notes** | Note the spatial transition clearly before transcribing the note itself. | `[written in left margin:] Do not share this with anyone.` |
|
||||||
|
| **Line Breaks / Hyphens** | Rejoin words split across a page margin silently, dropping the line-break hyphen. | `Original: "estab- / lishment" becomes "establishment"` |
|
||||||
|
| **Ambiguous Capitalization** | Default to modern capitalization rules unless an archaic uppercase letter is clearly intentional. | `If a standard noun like 'Farm' looks randomly capitalized, type 'farm'.` |
|
||||||
|
**Hierarchical Outlines** | Preserve exact numbering characters (including lowercase Roman numerals or terminal 'j'). Replicate indentation levels using spaces/tabs. Do not correct math or sequence errors silently. | `I. Main Topic`<br>` a. Sub-point`<br>` b. Next point`<br>`III. [sic] Third Topic` |
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# ADR-0001: Lifespan-owned runtime resources
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
MVP initialized core runtime resources (database engine and worker dependencies) through module-level globals and startup side effects. `REQ-7` requires lifespan-owned runtime resources with explicit ownership and cleanup.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Adopt lifespan-owned runtime resource initialization in `transcription.app`:
|
||||||
|
|
||||||
|
1. Initialize database runtime during app lifespan startup.
|
||||||
|
2. Store runtime handles on `app.state`.
|
||||||
|
3. Pass runtime-owned dependencies (engine) to worker startup.
|
||||||
|
4. Dispose runtime resources explicitly during lifespan shutdown.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Explicit startup and shutdown ownership.
|
||||||
|
- Predictable cleanup ordering.
|
||||||
|
- Reduced hidden global side effects.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Minor wiring complexity in app startup.
|
||||||
|
- Some call-sites still support fallback lazy initialization for compatibility.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Keep module-level global ownership**
|
||||||
|
- Rejected: conflicts with `REQ-7` and increases ambiguity.
|
||||||
|
2. **Introduce full async DB stack immediately**
|
||||||
|
- Rejected for Step 1: too broad for architecture-consolidation scope.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# ADR-0002: Explicit schema bootstrap policy
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
MVP called schema bootstrap (`create_all`) on every startup. `REQ-10` requires explicit, opt-in schema bootstrap behavior so normal production startup does not mutate schema.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add environment-aware bootstrap policy:
|
||||||
|
|
||||||
|
1. New settings:
|
||||||
|
- `environment`: `development` | `test` | `production`
|
||||||
|
- `bootstrap_schema_on_startup`: optional explicit override
|
||||||
|
2. Default behavior:
|
||||||
|
- Development/test: bootstrap enabled
|
||||||
|
- Production: bootstrap disabled
|
||||||
|
3. App startup calls `create_all` only when policy evaluates true.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Production startup behavior is safer and policy-driven.
|
||||||
|
- Local development remains simple by default.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Deployments now require explicit schema management in production.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Always bootstrap in all environments**
|
||||||
|
- Rejected: violates `REQ-10` intent.
|
||||||
|
2. **Disable bootstrap everywhere immediately**
|
||||||
|
- Rejected: hurts local developer workflow without migration tool replacement yet.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ADR-0003: Persistence baseline and transition path
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Architecture targets PostgreSQL baseline (optional MongoDB), while MVP currently runs on SQLite by default. V1 needs a clear transition path without destabilizing ongoing work.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Preserve database URL configurability through centralized settings.
|
||||||
|
2. Keep SQLite functional for local dev/test and fast feedback.
|
||||||
|
3. Treat PostgreSQL as production baseline target for V1 completion.
|
||||||
|
4. Keep persistence access behind `transcription.db` runtime/session access points.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Clear migration path without immediate broad rewrite.
|
||||||
|
- Controlled risk while preserving velocity.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Temporary dual-path assumptions (SQLite local vs PostgreSQL target).
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Immediate forced PostgreSQL-only migration**
|
||||||
|
- Rejected: higher short-term disruption risk.
|
||||||
|
2. **Remain SQLite-only for V1**
|
||||||
|
- Rejected: inconsistent with architecture and requirement trajectory.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# ADR-0004: In-process worker topology for V1
|
||||||
|
|
||||||
|
- **Status:** accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The current system uses an in-process background worker. Architecture docs allow this in foundation stage and permit later hardening (optional external worker/queue).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Retain in-process worker topology for V1, with improved lifecycle ownership:
|
||||||
|
|
||||||
|
1. Worker starts/stops via app lifespan.
|
||||||
|
2. Worker receives runtime-owned DB engine dependency explicitly.
|
||||||
|
3. Extension path to external worker remains behind existing service/adapter seams.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Keeps operational complexity low for personal-scale use.
|
||||||
|
- Preserves delivery focus on V1 completion.
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
- Throughput/scaling limits remain compared to external queue-based topology.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
1. **Immediate queue/external worker introduction**
|
||||||
|
- Rejected: premature complexity for current scale.
|
||||||
|
2. **Ad hoc thread lifecycle management outside lifespan**
|
||||||
|
- Rejected: weaker shutdown guarantees and poorer ownership clarity.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
This directory records significant architecture decisions for Version 1.
|
||||||
|
|
||||||
|
## ADR Format
|
||||||
|
|
||||||
|
Each ADR should include:
|
||||||
|
|
||||||
|
1. **Status** (`proposed`, `accepted`, `superseded`)
|
||||||
|
2. **Context**
|
||||||
|
3. **Decision**
|
||||||
|
4. **Consequences**
|
||||||
|
5. **Alternatives Considered**
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [ADR-0001: Lifespan-owned runtime resources](ADR-0001-lifespan-owned-runtime-resources.md)
|
||||||
|
- [ADR-0002: Explicit schema bootstrap policy](ADR-0002-explicit-schema-bootstrap-policy.md)
|
||||||
|
- [ADR-0003: Persistence baseline and transition path](ADR-0003-persistence-baseline-and-transition-path.md)
|
||||||
|
- [ADR-0004: In-process worker topology for V1](ADR-0004-in-process-worker-topology.md)
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document describes the production architecture of the personal historical-document transcription system. The system is intentionally optimized for single-user operation, low operational overhead, and clean internal boundaries that support future growth without rewrites.
|
||||||
|
|
||||||
|
## Architecture Objectives
|
||||||
|
|
||||||
|
The production architecture is designed to:
|
||||||
|
|
||||||
|
- preserve verbatim family-history source material as searchable text
|
||||||
|
- keep operational complexity low for a personal deployment
|
||||||
|
- support asynchronous transcription without requiring distributed infrastructure
|
||||||
|
- maintain clear module boundaries so extensions can be added incrementally
|
||||||
|
|
||||||
|
## Production Scope And Scale
|
||||||
|
|
||||||
|
The deployed system targets personal use and a corpus of several thousand documents processed over time. The architecture favors simple, composable building blocks over distributed orchestration.
|
||||||
|
|
||||||
|
Current scope includes:
|
||||||
|
|
||||||
|
- document upload and metadata capture
|
||||||
|
- asynchronous transcription jobs
|
||||||
|
- prompt-library driven transcription behavior, with one Markdown file per prompt
|
||||||
|
- transcript review and revision history
|
||||||
|
- full-text search over accepted transcripts
|
||||||
|
- export of transcript data
|
||||||
|
|
||||||
|
## Deployment Topology
|
||||||
|
|
||||||
|
The production deployment uses [Docker Compose](https://docs.docker.com/compose/) and treats containerized databases as extremely lightweight operational dependencies.
|
||||||
|
|
||||||
|
Running [PostgreSQL](https://www.postgresql.org/docs/) in its own container is considered simple by default for this system.
|
||||||
|
|
||||||
|
Running [MongoDB](https://www.mongodb.com/docs/) in its own container is also considered simple when document-centric storage is enabled.
|
||||||
|
|
||||||
|
Container count is not a hard architectural limit; a three-container deployment (app, PostgreSQL, MongoDB) is an acceptable baseline.
|
||||||
|
|
||||||
|
### Baseline Topology (Two Containers)
|
||||||
|
|
||||||
|
- one application container
|
||||||
|
- one PostgreSQL container
|
||||||
|
- embedded background worker execution inside the app process
|
||||||
|
|
||||||
|
### Expanded Topology (Three Containers)
|
||||||
|
|
||||||
|
- application container
|
||||||
|
- PostgreSQL container
|
||||||
|
- MongoDB container
|
||||||
|
|
||||||
|
No additional queue, scheduler, or search-engine containers are required in the baseline production setup.
|
||||||
|
|
||||||
|
## Runtime Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
User[Browser User] --> App[FastAPI + NiceGUI Service]
|
||||||
|
App --> Worker[In-process Background Worker]
|
||||||
|
App --> PG[(PostgreSQL)]
|
||||||
|
App --> MG[(MongoDB Document Store)]
|
||||||
|
Worker --> AI[Transcription Provider]
|
||||||
|
Worker --> PG
|
||||||
|
Worker --> MG
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Ownership And Startup Policy (V1 Step 1)
|
||||||
|
|
||||||
|
The current implementation now uses explicit lifespan-owned runtime resources.
|
||||||
|
|
||||||
|
- application lifespan initializes and disposes database runtime resources
|
||||||
|
- worker lifecycle is owned by application lifespan startup/shutdown
|
||||||
|
- worker receives lifespan-owned database engine dependency explicitly
|
||||||
|
- schema bootstrap policy is environment-aware and explicit:
|
||||||
|
- development/test default to bootstrap enabled
|
||||||
|
- production defaults to bootstrap disabled
|
||||||
|
- explicit override is available via configuration
|
||||||
|
|
||||||
|
This aligns implementation toward REQ-7 and REQ-10 while preserving personal-scale operational simplicity.
|
||||||
|
|
||||||
|
## Layered Module Structure
|
||||||
|
|
||||||
|
### Interface Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- HTTP API and UI routes
|
||||||
|
- request/response validation
|
||||||
|
- status and result presentation
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- business-rule enforcement
|
||||||
|
- data-access implementation
|
||||||
|
|
||||||
|
### Application Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- upload and job orchestration
|
||||||
|
- state transitions and retry policy
|
||||||
|
- coordination across domain and infrastructure ports
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- provider-specific protocol details
|
||||||
|
- ORM or storage-specific logic
|
||||||
|
|
||||||
|
### Domain Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- verbatim transcription policy
|
||||||
|
- revision and provenance invariants
|
||||||
|
- confidence and annotation semantics
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- web framework concerns
|
||||||
|
- database and network I/O
|
||||||
|
|
||||||
|
### Infrastructure Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- persistence adapters (PostgreSQL and MongoDB)
|
||||||
|
- transcription-provider adapter
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- business policy decisions
|
||||||
|
|
||||||
|
## Processing Workflow
|
||||||
|
|
||||||
|
Production transcription flow:
|
||||||
|
|
||||||
|
1. A user uploads an image or PDF through the UI or API.
|
||||||
|
2. The application validates payloads and creates document and job records.
|
||||||
|
3. The in-process worker dequeues the job and calls the transcription provider.
|
||||||
|
4. The application persists transcript output, confidence metadata, and provenance events.
|
||||||
|
5. Job status transitions from queued to processing to transcribed or failed.
|
||||||
|
6. The UI and API expose status, revision history, and searchable transcript text.
|
||||||
|
|
||||||
|
## Data Model Ownership
|
||||||
|
|
||||||
|
System-of-record entities:
|
||||||
|
|
||||||
|
- documents and pages
|
||||||
|
- transcription jobs and status events
|
||||||
|
- transcript revisions
|
||||||
|
- provenance metadata
|
||||||
|
|
||||||
|
Storage strategy:
|
||||||
|
|
||||||
|
- PostgreSQL for relational system-of-record entities
|
||||||
|
- MongoDB for document-oriented payloads and large transcription artifacts
|
||||||
|
- versioned prompt artifacts stored as individual Markdown files for human editing and refinement
|
||||||
|
- in-memory execution state treated as ephemeral
|
||||||
|
|
||||||
|
## Transcription Prompt Asset Policy
|
||||||
|
|
||||||
|
The production system treats transcription prompts as maintainable content assets.
|
||||||
|
|
||||||
|
- each transcription prompt is stored in its own Markdown file
|
||||||
|
- prompt files are designed for direct human editing and iterative refinement
|
||||||
|
- prompt updates are independent and do not require bundling unrelated prompt changes
|
||||||
|
- prompt file identity and revision history are tracked through normal repository version control
|
||||||
|
|
||||||
|
## Simplicity Guardrails
|
||||||
|
|
||||||
|
The production system enforces these constraints to prevent accidental over-engineering:
|
||||||
|
|
||||||
|
- PostgreSQL in a container is treated as a lightweight default dependency
|
||||||
|
- MongoDB in a container is treated as a lightweight optional dependency
|
||||||
|
- three containers (app, PostgreSQL, MongoDB) is an acceptable simple deployment
|
||||||
|
- no dedicated queue or search cluster is introduced without measured need
|
||||||
|
- external infrastructure is added only behind existing ports/adapters
|
||||||
|
|
||||||
|
## Extension Path
|
||||||
|
|
||||||
|
The architecture supports additive growth without changing domain contracts.
|
||||||
|
|
||||||
|
### Stage 1: Foundation (Current)
|
||||||
|
|
||||||
|
- upload, transcription, review, search, export
|
||||||
|
- in-process worker execution
|
||||||
|
- single provider adapter
|
||||||
|
- app plus PostgreSQL deployment
|
||||||
|
|
||||||
|
### Stage 2: Throughput Hardening
|
||||||
|
|
||||||
|
- optional MongoDB document-store enablement
|
||||||
|
- optional external worker/queue process
|
||||||
|
- stronger retry and dead-letter handling
|
||||||
|
|
||||||
|
### Stage 3: Intelligence Features
|
||||||
|
|
||||||
|
- entity extraction and cross-document linking
|
||||||
|
- timeline and narrative assembly
|
||||||
|
- optional multi-provider routing
|
||||||
|
|
||||||
|
Each stage preserves existing module boundaries and keeps migration risk low.
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
The test strategy is aligned to personal-scale operation with fast, deterministic feedback.
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- domain transcription rules and annotation behavior
|
||||||
|
- revision-history invariants
|
||||||
|
- job state-transition logic
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- repository behavior and transaction boundaries
|
||||||
|
- persistence-adapter and provider adapter contract mapping
|
||||||
|
- upload-to-persistence roundtrip
|
||||||
|
|
||||||
|
### End-to-End Tests
|
||||||
|
|
||||||
|
- happy path: upload, transcribe, review, search, export
|
||||||
|
- failure path: provider error, retry, surfaced failed status
|
||||||
|
|
||||||
|
### CI Execution Model
|
||||||
|
|
||||||
|
- fast suite on each push
|
||||||
|
- optional slower provider-sandbox checks on scheduled runs
|
||||||
|
|
||||||
|
## Risks And Controls
|
||||||
|
|
||||||
|
### Runtime Responsiveness
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- long jobs can reduce responsiveness in a single-process deployment
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- bounded concurrency and visible job status in the UI
|
||||||
|
|
||||||
|
### Database Concurrency Limits
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- contention can appear under sustained concurrent writes in personal-scale infrastructure
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- tuned connection pooling and phased use of MongoDB for document-heavy workloads
|
||||||
|
|
||||||
|
### Provider Output Variance
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- transcription quality varies by document type, handwriting legibility, and image quality
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- first-class human review and immutable revision history
|
||||||
|
|
||||||
|
## Technology References
|
||||||
|
|
||||||
|
- [FastAPI documentation](https://fastapi.tiangolo.com/)
|
||||||
|
- [NiceGUI documentation](https://nicegui.io/documentation)
|
||||||
|
- [Docker Compose documentation](https://docs.docker.com/compose/)
|
||||||
|
- [PostgreSQL documentation](https://www.postgresql.org/docs/)
|
||||||
|
- [MongoDB documentation](https://www.mongodb.com/docs/)
|
||||||
|
|
||||||
|
## Related Pages
|
||||||
|
|
||||||
|
- [System overview](index.md)
|
||||||
|
- [Version 1 plan](ver1/ver1.md)
|
||||||
|
- [Version 1 Step 1 plan](ver1/ver1-step1.md)
|
||||||
|
- [Version 1 Step 1 results](ver1/ver1-step1-results.md)
|
||||||
|
- [Architecture decision records index](adr/README.md)
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Adapter: A component that translates between internal interfaces and external systems such as databases or AI services.
|
||||||
|
- Background job: Work executed outside the request/response path so the UI remains responsive.
|
||||||
|
- Boundary: A strict separation between modules with different responsibilities.
|
||||||
|
- CI (Continuous Integration): Automated test execution for code changes.
|
||||||
|
- Contract test: A test that verifies an adapter follows expected input/output behavior at a boundary.
|
||||||
|
- Domain layer: The module that contains core business rules and invariants.
|
||||||
|
- End-to-end test: A test that validates a full user flow across the running system.
|
||||||
|
- Full-text search: Text indexing and querying optimized for natural-language search.
|
||||||
|
- In-process worker: A background executor that runs within the same application process.
|
||||||
|
- Integration test: A test that verifies interactions between real modules and infrastructure components.
|
||||||
|
- MongoDB: A document-oriented database used for flexible, high-variance data structures.
|
||||||
|
- Modular monolith: A single deployable application with strongly separated internal modules.
|
||||||
|
- Port/Interface: A stable contract used by application/domain code to call infrastructure implementations.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and can be revised independently.
|
||||||
|
- Provenance: Metadata that records where generated data came from and how it was produced.
|
||||||
|
- Revision history: Versioned record of transcript edits over time.
|
||||||
|
- System of record: The authoritative persistent store for canonical data.
|
||||||
|
- Vertical slice: A minimal end-to-end feature path spanning UI/API, application logic, and persistence.
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
# Architecture & Code Review Report
|
|
||||||
|
|
||||||
> ## Status: Historical Snapshot - Superseded
|
|
||||||
>
|
|
||||||
> **This document describes the codebase as it stood on 2026-08-17, before the V4.6 remediation release.** It is retained because it is the canonical registry of the finding IDs (`CRIT-01`, `HIGH-06`, `MED-14`, and so on) cited throughout the V4.6, V4.7, and V4.8 planning documents. Six documents reference it; deleting it would orphan all 32 finding IDs.
|
|
||||||
>
|
|
||||||
> **Do not read it as a description of current state.** Every file path, line number, and metric below is pre-V4.6 and most are now wrong. The baseline figures in particular are stale: `ruff check` is clean, `ty check` reports 0 diagnostics, and the suite is at 292 passed / 4 skipped.
|
|
||||||
>
|
|
||||||
> **Disposition of all 32 findings:**
|
|
||||||
>
|
|
||||||
> | Status | Findings |
|
|
||||||
> | :--- | :--- |
|
|
||||||
> | Addressed in V4.6 | All 32 were dispositioned - fixed, consciously accepted, or explicitly deferred. See [V4.6 scope boundary](ver4.6/scope_boundary_v4_6.md) and [V4.6 implementation plan](ver4.6/implementation_plan_v4_6.md). |
|
|
||||||
> | Carried into V4.7 | `MED-14` (SourceService decomposition) and `HIGH-06` (CI enforcement of the quality gate). See [V4.7 scope boundary](ver4.7/scope_boundary_v4_7.md). |
|
|
||||||
>
|
|
||||||
> **Where later thinking supersedes this report:** the [V4.6 review log](ver4.6/review_log_v4_6.md) records the decisions, deviations, and revisions made during implementation. Where this report and a committed planning document disagree, **the planning document wins**.
|
|
||||||
>
|
|
||||||
> Two recommendations here were later revised on evidence. `MED-14` proposed extracting a `services/artifacts.py`; V4.7 cancels that in favour of deleting the `ProcessingArtifact` subsystem outright. The report also treats `job_source` and `execution_attempt` as complementary; measurement showed their evidence columns are fully duplicated.
|
|
||||||
|
|
||||||
**Repository Target:** `C:\GitHub\transcription\`
|
|
||||||
**Target Stack:** Python 3.12+ | FastAPI | NiceGUI | SQLModel/SQLAlchemy | Pydantic V2 | asyncio | OpenRouter
|
|
||||||
**Review Date:** 2026-08-17
|
|
||||||
**Baseline verified:** `pytest` → 264 passed, 4 skipped. `ruff check` → 6 errors. `ty check` → 197 diagnostics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
|
||||||
|
|
||||||
- **The codebase is disciplined and unusually well-structured for its size (~9k src LOC).** Error taxonomy (`errors.py`), evidence capture (`providers/evidence.py`), transaction-ownership helpers (`ServiceBase._finalize`), and CSS/asset discipline in the UI are genuinely strong and should be preserved.
|
|
||||||
- **Top risk is the job-claim path.** `JobService.read_next_queued_job` (`services/jobs.py:170-187`) selects *every* `QUEUED` job with three levels of eager loading, has no `LIMIT`, no `FOR UPDATE SKIP LOCKED`, and no compare-and-swap on the status transition. The code even carries a comment acknowledging the race (`services/workflows.py:193-194`) without fixing it.
|
|
||||||
- **Second risk is model-level eager loading.** Nearly every `Relationship` in `db/models.py` sets `lazy="selectin"` on *both* sides of bidirectional links (`Document.jobs` ↔ `Job.document`, `Job.job_sources` ↔ `JobSource.job`, `JobSource.source` ↔ `Source.job_sources`). Reading one `Job` cascades into loading effectively the whole related graph, and it makes every explicit `selectinload()` in the services redundant.
|
|
||||||
- **No index exists on `Job.status` or `Job.date_created`**, yet the worker polls `WHERE status='queued' ORDER BY date_created` once per second. Every poll is a full table scan.
|
|
||||||
- **`worker_provider_timeout_seconds` is hard-capped at `le=20.0`** (`config.py:110`). Vision transcription of a full document page routinely exceeds 20s; this cap makes systematic timeouts unconfigurable-away.
|
|
||||||
- **The provider HTTP client is destroyed and rebuilt for every single job** (`worker.py:157-174`), defeating connection pooling and TLS session reuse on the hottest path.
|
|
||||||
- **`app_state.py` is dead code containing a guaranteed `TypeError`** (verified at runtime): `resolve_session_factory` calls `get_session_factory()` with no arguments. `@functools.cache` erases the signature, so `ty` cannot see it.
|
|
||||||
- **`ty` is configured as a dev dependency but is not usable as a gate.** 197 diagnostics, ~160 of which are SQLModel relationship false positives already suppressed with `# pyright: ignore` comments that `ty` does not honor.
|
|
||||||
- **Schema evolution is hand-rolled** in `db/operations.py` with raw `ALTER TABLE`/`CREATE INDEX IF NOT EXISTS` and a SQLite-shaped `CHAR(32)` UUID column. There is no Alembic. Postgres portability is claimed but not actually exercised.
|
|
||||||
- **Meaningful duplication exists in the UI layer** (~500 lines): media-URL resolution, `_parse_uuid`, settings resolution, delete-confirmation scaffolds, and hand-rolled tables are each reimplemented 3-5 times.
|
|
||||||
- **Meaningful duplication also exists in the service layer** (~400 lines): `DocumentType` and `PersonRole` registry CRUD are structurally identical, 38 "not-found" raises are hand-written, and three media-storage flows are reimplemented.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1a. Post-Review Addendum
|
|
||||||
|
|
||||||
The findings below were established during the V4.6 scoping discussion that followed the original review. They restate severity in light of the project's confirmed operating context and add findings discovered during that discussion. **The original finding IDs are stable and remain the canonical reference for the V4.6 documents.**
|
|
||||||
|
|
||||||
### Confirmed Operating Context
|
|
||||||
|
|
||||||
| Question | Answer |
|
|
||||||
| :--- | :--- |
|
|
||||||
| Database | **SQLite only.** PostgreSQL is the intended destination but is deferred beyond V4.6. `JSONBCompat` is retained. |
|
|
||||||
| Topology | **Single user, single process** today. Multi-user server is the stated direction. |
|
|
||||||
| Schema evolution | **Re-level from current metadata.** No Alembic. The app is pre-production and the schema is still moving. |
|
|
||||||
| Existing data | Rebuilt from scratch during implementation; migrated from backup as the final step. |
|
|
||||||
| Release character | **Pure remediation.** No new features. |
|
|
||||||
| Scope band | Critical through Low, inclusive. |
|
|
||||||
|
|
||||||
### Severity Re-Grades
|
|
||||||
|
|
||||||
| ID | Original | Re-graded | Rationale |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| CRIT-01 | Critical | **High** | With one process and one worker there is no live duplicate-processing race. The missing `.limit(1)` and the eager-load cost remain genuine defects; the atomic claim becomes forward-compatibility work for the multi-user direction rather than an active-incident fix. |
|
|
||||||
| CRIT-02 | Critical | **Critical** (unchanged) | Read amplification is independent of both topology and dialect. It costs on every read today. |
|
|
||||||
| HIGH-05 | High | **High** (reframed) | The remedy is **not** Alembic. Because the schema is pre-production and the data is disposable, the correct fix is to delete `upgrade_schema` and the three `_upgrade_*` functions outright and re-level the schema from current SQLModel metadata. This automatically resolves the `CHAR(32)` defect. |
|
|
||||||
| MED-01 | Medium | **Medium** (low urgency) | Single-user operation means event-loop stalls are self-inflicted only. Remains in scope. |
|
|
||||||
|
|
||||||
### Items Added During Scoping
|
|
||||||
|
|
||||||
These are recorded as [HIGH-08], [MED-10] through [MED-14], and [LOW-08] below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Findings by Severity
|
|
||||||
|
|
||||||
### Critical Severity
|
|
||||||
|
|
||||||
#### [CRIT-01] Queued-job claim has no row lock, no CAS, and no LIMIT — duplicate processing and full-queue load
|
|
||||||
> **Re-graded to High.** See [§1a](#severity-re-grades). The single-process deployment removes the live duplicate-processing race; the missing `.limit(1)` and the eager-load cost are still real, and the atomic claim is retained as forward-compatibility work.
|
|
||||||
- **Location:** `src/transcription/services/jobs.py:170-187`; claim logic at `src/transcription/services/workflows.py:188-196`; divergent duplicate at `src/transcription/db/operations.py:143-151`
|
|
||||||
- **Problem & Consequence:** `read_next_queued_job` issues `SELECT ... WHERE status = 'queued' ORDER BY date_created, id` with `selectinload(Job.document)` and `selectinload(Job.job_sources).selectinload(JobSource.source)` — and **no `.limit(1)`**. It materializes the entire queue plus its document/job_source/source graph on every worker tick just to call `.first()`. With a backlog of N jobs this is O(N) rows and several extra SELECT round-trips per second.
|
|
||||||
|
|
||||||
Worse, the claim is a read-then-write with no atomicity: `process_queued_job` reads status `QUEUED`, then separately calls `mark_job_status(job.id, PROCESSING)`. Two workers (or an app replica plus the in-process worker) can both read the same row as `QUEUED` and both transcribe it — double provider spend and duplicate `ExecutionAttempt` evidence rows. The comment at `workflows.py:193-194` explicitly names this hazard ("otherwise other workers may see the job as still QUEUED") but the committed fix only narrows the window rather than closing it.
|
|
||||||
|
|
||||||
Note also that `db/operations.py:get_next_queued_job` is a second, *different* implementation of the same concept that *does* have `.limit(1)`. The worse implementation is the live one.
|
|
||||||
- **Recommendation:** Replace the read-then-write with a single atomic claim, and delete the duplicate.
|
|
||||||
```python
|
|
||||||
# Before (services/jobs.py) — no limit, no lock
|
|
||||||
query = select(Job).options(...).where(Job.status == JobStatus.QUEUED).order_by(Job.date_created, Job.id)
|
|
||||||
return (await _session.exec(query)).first()
|
|
||||||
|
|
||||||
# After — atomic claim, one row, dialect-aware
|
|
||||||
async def claim_next_queued_job(self, *, session=None) -> Job | None:
|
|
||||||
async with self._session_scope(session) as s:
|
|
||||||
stmt = (
|
|
||||||
select(Job)
|
|
||||||
.where(Job.status == JobStatus.QUEUED)
|
|
||||||
.order_by(Job.date_created, Job.id)
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
if s.bind.dialect.name == "postgresql":
|
|
||||||
stmt = stmt.with_for_update(skip_locked=True)
|
|
||||||
job = (await s.exec(stmt)).first()
|
|
||||||
if job is None:
|
|
||||||
return None
|
|
||||||
job.status = JobStatus.PROCESSING
|
|
||||||
job.date_updated = datetime.now(UTC)
|
|
||||||
await self._finalize(session=s, caller_session=session, refresh=(job,))
|
|
||||||
return job
|
|
||||||
```
|
|
||||||
Load the eager relationships in a *second* query after the claim succeeds, so the hot poll stays a single narrow row. On SQLite, wrap the claim in `BEGIN IMMEDIATE` or accept single-worker-only and document it.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [CRIT-02] Bidirectional `lazy="selectin"` on every relationship causes cascading read amplification
|
|
||||||
- **Location:** `src/transcription/db/models.py:71-73, 89-91, 108-115, 160-168, 211-212, 269-276, 332-333`
|
|
||||||
- **Problem & Consequence:** Every `Relationship` in the domain model sets `sa_relationship_kwargs={"lazy": "selectin"}`, including both sides of each pair. Fetching a single `Job` triggers: `Job` → `Job.document` → `Document.jobs` (all jobs for that document) → `Document.sources` → `Document.document_people` → `DocumentPerson.person` / `.role_ref` → each `Job.job_sources` → `JobSource.source` → `Source.job_sources` → … SQLAlchemy's identity map prevents infinite recursion but does **not** prevent the extra SELECT round trips per level.
|
|
||||||
|
|
||||||
Two concrete consequences: (a) the per-second worker poll is far more expensive than it appears from reading `jobs.py`; (b) the dozens of explicit `selectinload(...)` options in `documents.py`, `jobs.py`, `sources.py`, and `people.py` are dead weight — the relationship default already does it — and they are the source of ~160 of the 197 `ty` diagnostics.
|
|
||||||
- **Recommendation:** Flip the model default to `lazy="raise"` (or `"noload"`, as already correctly done for `Source.processing_artifacts` at `models.py:279` and `JobSource.execution_attempts` at `models.py:336`) and rely on the per-query `selectinload()` that services already declare. `lazy="raise"` converts silent N+1 into a loud test failure and would prove which eager loads are actually needed.
|
|
||||||
```python
|
|
||||||
# models.py
|
|
||||||
jobs: list["Job"] = Relationship(back_populates="document", sa_relationship_kwargs={"lazy": "raise"})
|
|
||||||
```
|
|
||||||
Roll out per-model with the existing test suite as the safety net; the suite already covers the read paths.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
### High Severity
|
|
||||||
|
|
||||||
#### [HIGH-01] `app_state.py` is unreferenced dead code containing a guaranteed `TypeError`
|
|
||||||
- **Location:** `src/transcription/app_state.py:29-34`; the called function at `src/transcription/db/session.py:20-26`
|
|
||||||
- **Problem & Consequence:** `resolve_session_factory` falls back to `get_session_factory()` with no arguments, but the signature is `get_session_factory(database_url: str)`. Verified at runtime:
|
|
||||||
```
|
|
||||||
TypeError: get_session_factory() missing 1 required positional argument: 'database_url'
|
|
||||||
```
|
|
||||||
`@functools.cache` wraps the function in a `_lru_cache_wrapper`, which erases the signature — so `ty check src\transcription\app_state.py` reports "All checks passed". The whole module has **zero importers** anywhere in `src`, `tests`, or `tools`, so the bug is currently latent; anyone wiring this helper up hits an immediate crash on the fallback path.
|
|
||||||
- **Recommendation:** Delete `app_state.py`. Its three live behaviors already exist elsewhere (`db/session.py:resolve_session_factory`, `db/runtime.py:get_database_runtime`, `worker.py:resolve_worker_notifier`). If retained instead, fix the fallback to `resolve_session_factory()` from `db.session`, and add a typed non-cached wrapper around cached functions so type checkers keep the signature.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [HIGH-02] Provider HTTP client is rebuilt and torn down once per job
|
|
||||||
- **Location:** `src/transcription/worker.py:148-174` (`finally: await services.sources.aclose()`), driven by the tight inner loop at `src/transcription/worker.py:134-142`; client construction at `src/transcription/providers/openrouter.py:197-201`
|
|
||||||
- **Problem & Consequence:** `process_next_queued_job` constructs a fresh `ServiceBundle` per call and unconditionally closes the provider in `finally`. Since `workflows.py:243` accesses `services.sources.provider`, a new `httpx.AsyncClient` + `OpenRouter` SDK client is created and destroyed for **every job**. This throws away the connection pool and forces a full TLS handshake per job — added latency on the single most latency-sensitive path, plus churn of file descriptors during backlog drain.
|
|
||||||
- **Recommendation:** Hoist the `ServiceBundle` to worker-loop scope (or reuse `app.state.services`, which the lifespan already builds at `app.py:45-50`) and close the provider once at loop shutdown.
|
|
||||||
```python
|
|
||||||
# worker.py — before
|
|
||||||
async def process_next_queued_job(...):
|
|
||||||
services = ServiceBundle(...)
|
|
||||||
try: ...
|
|
||||||
finally: await services.sources.aclose()
|
|
||||||
|
|
||||||
# after: build once in run_worker_loop / lifespan, pass in, close in the lifespan finally
|
|
||||||
async def run_worker_loop(*, services: ServiceBundle, ...):
|
|
||||||
try:
|
|
||||||
while True: ... await process_next_queued_job(services=services, ...)
|
|
||||||
finally:
|
|
||||||
await services.sources.aclose()
|
|
||||||
```
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [HIGH-03] Provider timeout is capped at 20 seconds by configuration
|
|
||||||
- **Location:** `src/transcription/config.py:110`
|
|
||||||
- **Problem & Consequence:** `worker_provider_timeout_seconds: float = Field(default=20.0, gt=0.0, le=20.0)`. The `le=20.0` bound makes 20s both the default *and* the maximum. `workflows.py:238-248` wraps the provider call in `asyncio.wait_for(..., timeout=that_value)`. Multi-modal transcription of a full-page historical document commonly exceeds 20s; operators cannot raise the ceiling without editing source. Every such job fails with `failure_phase="local_timeout"`, and with `worker_max_retries` defaulting to `0` (`config.py:108`) it fails permanently on the first attempt.
|
|
||||||
|
|
||||||
Compounding this, `httpx.AsyncClient(follow_redirects=True)` at `openrouter.py:198` sets no explicit `timeout`, so it inherits httpx's 5-second default for connect/read/write/pool unless the OpenRouter SDK overrides it.
|
|
||||||
- **Recommendation:** Remove the `le=20.0` cap (keep `gt=0.0`), raise the default to something realistic (120s), and set an explicit `httpx.Timeout` derived from the same setting so the transport and the `wait_for` agree.
|
|
||||||
```python
|
|
||||||
worker_provider_timeout_seconds: float = Field(default=120.0, gt=0.0)
|
|
||||||
# openrouter.py
|
|
||||||
httpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(settings.worker_provider_timeout_seconds))
|
|
||||||
```
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [HIGH-04] No index on the columns the worker polls every second
|
|
||||||
- **Location:** `src/transcription/db/models.py:171-212` (`Job.status`, `Job.date_created`, `Job.document_id` all lack `index=True`); also `Source.document_id:252`, `JobSource.job_id:313`, `JobSource.source_id:314`
|
|
||||||
- **Problem & Consequence:** The worker executes `WHERE status = 'queued' ORDER BY date_created` once per second (`worker.py:130`, `jobs.py:183-185`). Without a composite index this is a full scan plus sort on every tick, and it grows linearly with total job history — not with queue depth. The `JobSource` foreign keys are joined on every job read; PostgreSQL does not auto-index FKs.
|
|
||||||
- **Recommendation:** Add a composite index for the poll and plain indexes on the hot FKs.
|
|
||||||
```python
|
|
||||||
class Job(SQLModel, table=True):
|
|
||||||
__table_args__ = (Index("ix_job_status_date_created", "status", "date_created"),)
|
|
||||||
document_id: UUID = Field(foreign_key="document.id", index=True)
|
|
||||||
```
|
|
||||||
Note these must also be added to the hand-rolled upgrade path in `db/operations.py` (see [HIGH-05]).
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [HIGH-05] Hand-rolled schema migrations with SQLite-shaped DDL block the claimed Postgres support
|
|
||||||
- **Location:** `src/transcription/db/operations.py:25-109`
|
|
||||||
- **Problem & Consequence:** Schema evolution is a chain of `_upgrade_*` functions issuing raw `ALTER TABLE` / `CREATE INDEX IF NOT EXISTS` against whatever database is present, executed inside `create_all()`. Specific defects:
|
|
||||||
- `operations.py:77` adds `preferred_execution_attempt_id CHAR(32)` — but the model declares it a `UUID` FK to `execution_attempt.id` (`models.py:260-264`). On PostgreSQL this creates a `char(32)` column that will not compare or join against a native `uuid` column, and the declared foreign key is never created at all.
|
|
||||||
- Every upgrade is unversioned and re-inspected on each startup; there is no down path, no history table, and no way to tell whether a production database is current.
|
|
||||||
- `asyncpg` and `psycopg2-binary` are both dependencies (`pyproject.toml:17,21`) and `JSONBCompat` (`models.py:27-35`) carefully supports JSONB, so Postgres is clearly an intended target — but no test exercises it. All 264 tests run on SQLite.
|
|
||||||
- **Recommendation:** **Re-level the schema from current metadata; do not adopt Alembic.** The application is pre-production, the schema is still evolving, and the existing data is disposable and backed up. Delete `upgrade_schema` and the three `_upgrade_*` functions (`operations.py:25-109`) together with their tests (`tests/test_db.py:109-172`), drop the database, and let `create_all()` generate the schema from SQLModel metadata. This removes the `CHAR(32)` defect at the root rather than patching it, because SQLModel emits the correct column type per dialect automatically (verified: it emits native `UUID` and `JSONB` under the PostgreSQL dialect). `Settings.should_bootstrap_schema` (`config.py:140-145`) already gates the bootstrap path correctly. Reintroduce a migration tool only when the schema stabilizes and real data must survive upgrades.
|
|
||||||
- **Sequencing:** This must land in the *same* pass as [HIGH-04] (missing indexes), [CRIT-02] (`lazy` flip), and [HIGH-08] (`use_alter`), because all four regenerate the same schema.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [HIGH-06] `ty` is a configured dev tool but produces 197 diagnostics and cannot gate CI
|
|
||||||
- **Location:** `pyproject.toml:38`; suppression comments throughout, e.g. `src/transcription/services/jobs.py:67-68,103-104,123-124,156,180-181`
|
|
||||||
- **Problem & Consequence:** The project pins `ty` as its type checker, but the codebase suppresses SQLModel relationship typing with `# pyright: ignore[reportArgumentType]` — a *pyright* directive that `ty` does not honor. Result: `ty check` emits 197 diagnostics (160 `invalid-argument-type`, 18 `unresolved-attribute`, 13 `not-subscriptable`), so nobody can run it as a gate, and genuine errors hide in the noise. Two real bugs are buried in there:
|
|
||||||
- `tests/ui/test_sources_page.py:25` — `Source(...)` constructed without the required `document_id`.
|
|
||||||
- `tools/run_destructive_tests.py:76,80` — `fcntl` is imported and used, but `fcntl` does not exist on Windows, which is this project's development platform.
|
|
||||||
- **Recommendation:** Pick one checker and commit to it. If `ty`: replace `# pyright: ignore[...]` with `# ty: ignore[...]`, or better, eliminate the root cause by adopting [CRIT-02]'s `lazy="raise"` change plus typed column accessors, which removes most `selectinload` diagnostics outright. Then wire `ty check` into pre-commit (`pre-commit` is already a dev dependency at `pyproject.toml:35`).
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [HIGH-07] UI pages own persistence and ORM-loader concerns (violates `ui.instructions.md`)
|
|
||||||
- **Location:** `src/transcription/ui/pages/jobs_page.py:17,185-192`; `src/transcription/ui/pages/sources_page.py:13,439`; `src/transcription/ui/components/document_panzoom.py:12,61,65`
|
|
||||||
- **Problem & Consequence:** `ui.instructions.md` states pages must not import sessions or manage transactions, and components must not resolve app state. Three violations:
|
|
||||||
- `jobs_page.py` imports `transcription.db.session.session_scope` and manages the session lifecycle itself around `create_job_for_document`, while every sibling call site goes through a service.
|
|
||||||
- `sources_page.py:439` imports `sqlalchemy.inspect` and reads `inspect(attempt).unloaded` to decide rendering — the presentation layer is now coupled to the loader strategy, and will silently misbehave if a service changes its deferred columns.
|
|
||||||
- `document_panzoom.py` calls `get_settings()` inside a component and re-implements upload-path resolution.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [HIGH-08] Circular foreign-key cycle makes `create_all` fail on PostgreSQL
|
|
||||||
- **Location:** `src/transcription/db/models.py:260-264` (`Source.preferred_execution_attempt_id`), with the cycle running `source` → `job_source` → `execution_attempt` → `source`
|
|
||||||
- **Problem & Consequence:** Verified by compiling the SQLModel metadata against the PostgreSQL dialect, which emits:
|
|
||||||
|
|
||||||
> `SAWarning: Cannot correctly sort tables; there are unresolvable cycles between tables "execution_attempt, job_source, source", which is usually caused by mutually dependent foreign key constraints.`
|
|
||||||
|
|
||||||
The resulting sort order places `execution_attempt` **before** `source`, but `execution_attempt.source_id` is a foreign key to `source.id`. On PostgreSQL, where foreign keys are enforced inline at `CREATE TABLE` time, this is a hard `create_all()` failure. SQLite does not enforce the ordering, so the defect is completely invisible on the current test suite and will surface only at the moment of the Postgres cutover.
|
|
||||||
- **Recommendation:** Mark the nullable leg of the cycle with `use_alter=True` so SQLAlchemy emits it as a deferred `ALTER TABLE ... ADD CONSTRAINT` after all tables exist. Verified to silence the warning and produce a correct ordering.
|
|
||||||
```python
|
|
||||||
# models.py — Source
|
|
||||||
preferred_execution_attempt_id: UUID | None = Field(
|
|
||||||
default=None,
|
|
||||||
sa_column=Column(
|
|
||||||
GUID(),
|
|
||||||
ForeignKey("execution_attempt.id", use_alter=True, name="fk_source_preferred_attempt"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
This is cheap, harmless on SQLite, and should land with the schema re-level ([HIGH-05]) so the Postgres path is unblocked whenever it is taken.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
### Medium Severity
|
|
||||||
|
|
||||||
#### [MED-01] Blocking filesystem and CPU work on the async event loop
|
|
||||||
- **Location:** `src/transcription/services/store.py:363`; `src/transcription/services/people.py:623`; `src/transcription/services/sources.py:908-923,941,1297,1354`; `src/transcription/services/normalization.py:52-101`; `src/transcription/services/prompts.py:138,162,173-176`; `src/transcription/ui/homepage_store.py:22,28,41`; `src/transcription/ui/pages/home_page.py:87,92`; `src/transcription/ui/pages/people_page.py:495`
|
|
||||||
- **Problem & Consequence:** All media persistence and artifact I/O is synchronous, called from `async def` paths. `_write_external_artifact` (`sources.py:908`) additionally calls `os.fsync()`, which can block for tens of milliseconds. `normalize_orientation` (`normalization.py:52`) runs full Pillow decode/transpose/re-encode at `quality=95, subsampling=0` inline — that is CPU-bound work measured in hundreds of milliseconds for a scanned page. Every one of these stalls the single event loop shared by the FastAPI API, all NiceGUI clients, and the worker.
|
|
||||||
- **Recommendation:** Route blocking work through `asyncio.to_thread` at the service boundary (one wrapper per operation, not per call site). For NiceGUI handlers, `nicegui.run.io_bound` / `run.cpu_bound` are the idiomatic equivalents. `normalize_orientation` is the highest-value single conversion.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [MED-02] Dead configuration surface: three settings are defined and tested but never read
|
|
||||||
- **Location:** `src/transcription/config.py:99` (`sqlite_check_same_thread`), `:109` (`worker_retry_backoff_seconds`)
|
|
||||||
- **Problem & Consequence:** `sqlite_check_same_thread` is never read — `engine.py:43` hardcodes `{"check_same_thread": False}`. `worker_retry_backoff_seconds` is never read either; `tests/test_config.py:152` asserts its default, which gives false confidence that backoff exists. `services.instructions.md:59` mandates a retry path with backoff, and `workflows.py:159-169` implements the `FAILED → QUEUED` transition, but nothing ever sleeps between attempts. Additionally, `advance_job` is invoked exactly once per `process_next_queued_job` call, so a job that transitions `FAILED → QUEUED` is only retried on a later poll — a documented behavior that reads as accidental.
|
|
||||||
- **Recommendation:** Either wire `worker_retry_backoff_seconds` into the retry scheduler (a `next_attempt_at` column filtered in the claim query is the correct shape — sleeping in the worker loop would stall all other jobs) or delete both settings and their tests. Honor `sqlite_check_same_thread` in `engine.py:43` or remove it.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-03] Runtime `inspect.signature` and `getattr` duck-typing at the provider boundary
|
|
||||||
- **Location:** `src/transcription/services/sources.py:1237-1238,1242-1244`; `src/transcription/services/sources.py:152-154`; `src/transcription/services/workflows.py:297-302`
|
|
||||||
- **Problem & Consequence:** The `TranscriptionProvider` Protocol (`providers/base.py:102-117`) already declares `requested_model` as a parameter, yet `sources.py:1237` re-checks for it at runtime via `inspect.signature(adapter.transcribe).parameters` on **every transcription call**, then builds an untyped `dict` of kwargs. Similarly, `aclose` and `current_request_manifest` / `current_transport_evidence` are accessed via `getattr(..., None)` even though they are part of the de-facto contract. This defeats static checking on the most important interface in the system, adds per-call reflection overhead, and means a provider that silently drops `requested_model` fails only at runtime.
|
|
||||||
- **Recommendation:** Extend the Protocol to declare `aclose()`, `current_request_manifest`, and `current_transport_evidence`; then call `adapter.transcribe(...)` with real keyword arguments and drop the `inspect` import.
|
|
||||||
```python
|
|
||||||
class TranscriptionProvider(Protocol):
|
|
||||||
current_request_manifest: RequestManifest | None
|
|
||||||
current_transport_evidence: TransportEvidence | None
|
|
||||||
async def transcribe(self, *, prompt_text: str, ..., requested_model: str | None = None) -> TranscriptionResult: ...
|
|
||||||
async def aclose(self) -> None: ...
|
|
||||||
```
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-04] `@cache` on `get_settings(**kwargs)` and on engine/session factories creates cross-test and cross-tenant coupling
|
|
||||||
- **Location:** `src/transcription/config.py:148-151`; `src/transcription/db/engine.py:39-55`; `src/transcription/db/session.py:20-26`
|
|
||||||
- **Problem & Consequence:** `get_settings(**kwargs: Any)` is `@cache`-decorated with arbitrary keyword arguments — any unhashable value raises `TypeError`, and the cache key is the kwargs tuple, so `get_settings()` and `get_settings(environment="test")` return different singletons. More seriously, `dispose_engine(database_url)` (`engine.py:50-55`) calls `get_engine.cache_clear()`, which evicts **all** cached engines, not just the one being disposed; a multi-database process would silently lose its other engines' pools. The same pattern applies to `dispose_session_factory` (`session.py:48-50`).
|
|
||||||
- **Recommendation:** Replace the caches with an explicit registry keyed by URL that supports targeted eviction. `db/runtime.py` already models lifespan-owned resources correctly — extend that pattern rather than layering `functools.cache` beneath it. Separately, drop `**kwargs` from `get_settings` and keep it a true zero-argument singleton.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [MED-05] Dead compatibility aliases and a three-way import path for one function
|
|
||||||
- **Location:** `src/transcription/services/store.py:35,382-383`; `src/transcription/services/transcription.py:12,36`; imports at `store.py:26`, `workflows.py:42`, `sources.py:1263`
|
|
||||||
- **Problem & Consequence:** `build_prompt_execution` is defined in `sources.py:1263` and imported through three different paths: `store.py` uses `from .transcription import build_prompt_execution`, `workflows.py` uses `from .sources import ...`, and `tests/test_prompts.py:12` uses a third. `transcription.py` (41 lines) exists solely as a re-export shim. Alongside it, `UploadError = SourceStorageError` (`store.py:35`), `create_upload_job = create_document_job` (`store.py:382`), and `store_file = store_source_file` (`store.py:383`) are aliases with zero remaining callers.
|
|
||||||
- **Recommendation:** Delete the three aliases and the `transcription.py` shim; standardize all imports on `services.sources`.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-06] `ServiceBundle` default factories construct four services against global settings
|
|
||||||
- **Location:** `src/transcription/services/__init__.py:15-22`; consumed at `src/transcription/worker.py:157-158`
|
|
||||||
- **Problem & Consequence:** `ServiceBundle` declares `field(default_factory=DocumentService)` for all four services. Instantiating `ServiceBundle()` therefore calls `get_settings()` and `resolve_session_factory()` four times, binding to process-global state. `worker.py:157` takes exactly this path whenever `session_factory is None`. This is the "global singleton instead of injected dependency" pattern the FastAPI DI system exists to avoid, and it makes the worker's database target implicit.
|
|
||||||
- **Recommendation:** Remove the default factories and require explicit construction, plus a single `ServiceBundle.from_session_factory(factory, settings)` classmethod — which also removes the four-way duplication of the same construction block at `app.py:45-50` and `worker.py:160-165`.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-07] Unused `asyncio.Queue` allocated in every service instance
|
|
||||||
- **Location:** `src/transcription/services/base.py:20,26,30`
|
|
||||||
- **Problem & Consequence:** `ServiceBase.__init__` does `self.queue = queue or asyncio.Queue()`. No code anywhere reads `self.queue`. The annotation is the unparameterized `asyncio.Queue`. Constructing an `asyncio.Queue` also binds to the running event loop policy, so building a `ServiceBundle` outside a loop is a latent hazard, and per [MED-06] this happens four times per bundle.
|
|
||||||
- **Recommendation:** Delete the `queue` attribute and constructor parameter.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-08] Exception swallowed to `None` in an ORM model property
|
|
||||||
- **Location:** `src/transcription/db/models.py:220-233`
|
|
||||||
- **Problem & Consequence:** `Job.filename` reaches into `job_source.__dict__` to dodge lazy loading, then catches `DetachedInstanceError` *and* bare `Exception` (`models.py:227`), returning the string `"unknown"`. Any genuine error — a corrupted row, a mapper misconfiguration — is silently rendered as "unknown" in the UI with no log line. The workaround exists only because of the eager-loading design in [CRIT-02].
|
|
||||||
- **Recommendation:** Remove the property from the model and compute the display value in the feature table read model (`ui/components/table/jobs.py`), which is where `ui.instructions.md` says presentation formatting belongs. If it stays, drop the bare `except Exception` and log the `DetachedInstanceError` case.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-09] Large inline SVG asset embedded in a Python module
|
|
||||||
- **Location:** `src/transcription/ui/theme.py:36-40` (single 23,317-character line)
|
|
||||||
- **Problem & Consequence:** `VIBESCRIBE_LOGO_SVG` is a 23KB string literal inside a Python source file. It trips `ruff`'s `line-too-long`, makes the module unreadable and undiffable, and contradicts `ui.instructions.md`'s rule that static assets live under `ui/static/` and be read via `importlib.resources`. The project already has exactly the right helper for this — `ui/resources.py:10-19`'s cached `importlib.resources` reader.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-10] `DATABASE_URL` is silently ignored by `Settings`
|
|
||||||
- **Location:** `src/transcription/config.py` (`Settings`, nested `database` config); `docker-compose.yml:10`
|
|
||||||
- **Problem & Consequence:** `docker-compose.yml:10` sets `DATABASE_URL`, plainly intending to point the application at a different database. `Settings` reads its database configuration from a *nested* `database` model with `env_nested_delimiter="__"` and `extra="ignore"`, so `DATABASE_URL` matches nothing and is discarded without warning. Verified at runtime: with `DATABASE_URL=postgresql://...` exported, `get_settings().database` still resolves to `driver='sqlite' path='./data/transcription.db'`. An operator following the committed compose file gets SQLite while believing they configured PostgreSQL — silent, and the failure mode is data written to the wrong place.
|
|
||||||
- **Recommendation:** Pick one contract and make the other loud. Either add an explicit `DATABASE_URL` field that parses a full URL into the nested settings, or delete `DATABASE_URL` from `docker-compose.yml` and document `DATABASE__DRIVER` / `DATABASE__PATH`. Given [HIGH-05] defers PostgreSQL, the correct V4.6 action is to remove the misleading compose variable and document the real nested names. Escalates to Critical the moment PostgreSQL is enabled.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [MED-11] `DocumentType` and `PersonRole` registry CRUD is duplicated wholesale
|
|
||||||
- **Location:** `src/transcription/services/documents.py:49-61,64-72,350-500`; `src/transcription/services/people.py:49-79,214-378`
|
|
||||||
- **Problem & Consequence:** The two models are structurally identical (`id, semantic_key, label, normalized_label, is_active, created_at, updated_at`) and carry identical operation sets, guards, and error mappings:
|
|
||||||
|
|
||||||
| Operation | `DocumentType` | `PersonRole` |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| label normalizer + casefold key | `documents.py:49-61` | `people.py:49-75` |
|
|
||||||
| summary dataclass | `documents.py:64-72` | `people.py:79` |
|
|
||||||
| list / list summaries with counts | `documents.py:350-388` | `people.py:214-249` |
|
|
||||||
| create, `IntegrityError` → conflict | `documents.py:390-413` | `people.py:251-274` |
|
|
||||||
| read, not-found raise | `documents.py:415-430` | `people.py:276-291` |
|
|
||||||
| update, `IntegrityError` → conflict | `documents.py:432-461` | `people.py:293-322` |
|
|
||||||
| delete, built-in guard + referenced guard | `documents.py:463-491` | `people.py:324-352` |
|
|
||||||
| `is_*_referenced` | `documents.py:493-500` | `people.py:354-378` |
|
|
||||||
|
|
||||||
The duplication extends to the wording of the user-facing suggestion strings ("Deactivate the type instead" / "Deactivate the role instead"). Any fix to one — a normalization bug, a missing guard, an error-category correction — has to be remembered twice.
|
|
||||||
- **Recommendation:** Introduce a generic `RegistryService[ModelT]` base that owns the eight operations, the label normalization, and the `IntegrityError` mapping. Each concrete registry declares its model, its error class, its reference query, and its noun for message templating. Collapses roughly 200 lines and makes a third registry nearly free.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [MED-12] 38 hand-written "not found" raises; the helper that solves it exists and is used once
|
|
||||||
- **Location:** `src/transcription/services/people.py` (15 sites), `sources.py` (14), `documents.py` (9); helper at `documents.py:123-132`
|
|
||||||
- **Problem & Consequence:** The pattern `entity = await session.get(Model, id)` / `if entity is None: raise <Error>(f"... {id} not found", category=ErrorCategory.NOT_FOUND, suggestion=...)` is written out longhand 38 times across the service layer, roughly 150 lines. `DocumentService._get_document_or_raise` (`documents.py:123-132`) already implements exactly this — but it is called from only one site (`documents.py:533`), while the identical block is still hand-written at `documents.py:174`, `210`, and `291` in the same file. The abstraction was created and then not adopted, which is the worst of both outcomes: the maintenance burden of a helper plus the drift risk of copies.
|
|
||||||
- **Recommendation:** Promote the helper to `ServiceBase` and adopt it everywhere.
|
|
||||||
```python
|
|
||||||
# services/base.py
|
|
||||||
async def _get_or_raise[T](
|
|
||||||
self, session: AsyncSession, model: type[T], entity_id: UUID, *,
|
|
||||||
error: type[AppError], noun: str, suggestion: str,
|
|
||||||
) -> T: ...
|
|
||||||
```
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [MED-13] Three parallel media-storage implementations
|
|
||||||
- **Location:** `src/transcription/services/store.py:319-379`; `src/transcription/services/people.py:596-631`; `src/transcription/ui/homepage_store.py:31-44`
|
|
||||||
- **Problem & Consequence:** `store_source_file`, `store_person_portrait`, and the homepage image writer each independently perform: empty-content check → extension allowlist check → `mkdir(parents=True, exist_ok=True)` → `write_bytes` → wrap `OSError` in a domain error → log. They differ in which of those steps they actually do, so the guarantees are inconsistent — only one of the three hashes its content. All three also block the event loop ([MED-01]).
|
|
||||||
- **Recommendation:** Consolidate into `services/media_storage.py` per §4, wrapping the write in `asyncio.to_thread`. Resolves this finding and [MED-01] together.
|
|
||||||
- **Effort:** M
|
|
||||||
|
|
||||||
#### [MED-14] `SourceService` owns four domain models, violating the project's own service rule
|
|
||||||
- **Location:** `src/transcription/services/sources.py` (1254 lines)
|
|
||||||
- **Problem & Consequence:** `.github/instructions/services.instructions.md:12` states "1 service class per data model." `SourceService` owns `Source`, `JobSource`, `ExecutionAttempt`, and `ProcessingArtifact`:
|
|
||||||
|
|
||||||
| Responsibility | Lines |
|
|
||||||
| :--- | :--- |
|
|
||||||
| Source CRUD, navigation, listing | 157-345 |
|
|
||||||
| JobSource association CRUD | 347-510 |
|
|
||||||
| Evidence write (`update_job_source_transcription`) | 511-670 |
|
|
||||||
| Attempt promotion and listing | 672-725 |
|
|
||||||
| Artifact storage (JSON, binary, external, verify) | 727-992 |
|
|
||||||
| Evidence export | 994-1091 |
|
|
||||||
| Revisions | 1093-1141 |
|
|
||||||
|
|
||||||
The clearest symptom is `update_job_source_transcription` — 160 lines, 17 keyword parameters, mutating five models in one call. The same instruction file (line 13) says an operation spanning more than one service "needs to have a separate orchestration function"; this method *is* that orchestration function, living inside a service. The size also made `sources.py` an import hub: `documents.py:24` and `store.py:24-25` both import from it, and `documents.py:24` importing `source_mime_type` violates the "services are completely independent" rule at line 13.
|
|
||||||
- **Recommendation:** Extract `ExecutionAttempt` and `ProcessingArtifact` into their own services and relocate `update_job_source_transcription` to `workflows.py` as orchestration. Keep `Source` and `JobSource` together — they are written in the same transaction on every path, and separating them would add ceremony without benefit. Move `source_mime_type` to a shared module so `documents.py` no longer imports a sibling service.
|
|
||||||
- **Deferred to V4.7.** This touches the transcription write path and is too large to absorb alongside the V4.6 schema re-level.
|
|
||||||
- **Effort:** L
|
|
||||||
|
|
||||||
### Low Severity
|
|
||||||
|
|
||||||
#### [LOW-01] `ruff check` fails on 6 issues, 5 auto-fixable
|
|
||||||
- **Location:** `src/transcription/ui/theme.py:38,40`; `tests/test_app.py:23`; `tests/ui/test_upload_page.py:44`; plus 2 others
|
|
||||||
- **Recommendation:** Run `ruff check --fix`; the only non-trivial one is the SVG line, addressed by [MED-09].
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-02] Stale path reference in project instructions
|
|
||||||
- **Location:** `.github/instructions/services.instructions.md:10`
|
|
||||||
- **Problem:** Points to `src/transcription/models.py`; the actual location is `src/transcription/db/models.py`.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-03] `list_jobs` accepts and discards a parameter
|
|
||||||
- **Location:** `src/transcription/services/jobs.py:113-120` (`_ = load_docs`)
|
|
||||||
- **Problem:** A dead parameter kept alive only to satisfy `ARG` linting. Callers may believe it changes behavior.
|
|
||||||
- **Recommendation:** Remove the parameter and update callers.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-04] `resolve_worker_notifier` returns unvalidated `getattr` results
|
|
||||||
- **Location:** `src/transcription/worker.py:54-61`
|
|
||||||
- **Problem:** Any non-`None` attribute is returned as a `WorkerNotifier` without checking it has `notify`. Compare `app_state.py:15-18`, which correctly uses `isinstance`.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-05] Untyped handler parameters and loosely-typed dict returns in UI
|
|
||||||
- **Location:** `ui/pages/jobs_page.py:491`; `ui/pages/people_page.py:492`; `ui/pages/home_page.py:85`; `_render_document_form_fields` / `_render_person_form_fields` returning `dict[str, Any]`
|
|
||||||
- **Recommendation:** Annotate with `nicegui.events.UploadEventArguments`; replace the form-field dicts with frozen dataclasses.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-06] Auto-refresh timer deactivated but never cancelled; magic interval
|
|
||||||
- **Location:** `ui/pages/jobs_page.py:245,251,253`
|
|
||||||
- **Problem:** `ui.timer(4.0, refresh_job)` is toggled via `.active = False` rather than `.cancel()`; `4.0` is an unnamed literal. Client-scoped, so impact is bounded.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-07] `people_page.py:504` catches `Exception` and discards it entirely
|
|
||||||
- **Location:** `ui/pages/people_page.py:504`
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
#### [LOW-08] Four avoidable query inefficiencies in `sources.py`
|
|
||||||
- **Location:** `src/transcription/services/sources.py:233-244, 338-343, 961, 1012-1013`
|
|
||||||
- **Problem & Consequence:**
|
|
||||||
- `list_sources_detail:338-343` filters by `job_id` **in Python**, after loading every `Source` row and its eager graph, instead of joining `JobSource` in SQL. Cost grows with the whole table rather than with the result set.
|
|
||||||
- `read_source_navigation:233-244` fetches the complete ordered id list for a document to identify two neighbours. Two `LIMIT 1` queries (`page_number < n ORDER BY page_number DESC`, and the mirror) return the same answer at constant cost.
|
|
||||||
- `list_processing_artifacts:961` has no `limit` parameter while its sibling `list_processing_artifact_summaries:980` does, and it loads `inline_payload` blobs that the caller frequently does not need.
|
|
||||||
- `build_evidence_export:1012-1013` re-reads and re-hashes every external artifact file synchronously on the event loop before serializing. Integrity verification is correct to perform, but it belongs in `asyncio.to_thread` ([MED-01]).
|
|
||||||
- **Recommendation:** Push the `job_id` filter into SQL, replace the navigation scan with two bounded queries, add a `limit` to `list_processing_artifacts`, and move artifact hashing off the loop.
|
|
||||||
- **Effort:** S
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Stack-Specific Analysis
|
|
||||||
|
|
||||||
### Python 3.12+ Best Practices
|
|
||||||
Modern syntax is used consistently and correctly: `type` statements (`db/session.py:17,45,73,102`), `X | None` unions, `StrEnum`, `match` statements (`db/engine.py:16-31`, `db/session.py:84-92`, `workflows.py:153-171`), frozen `dataclass(slots=True)`, and `pathlib` throughout — no `os.path` anywhere. Gaps: unparameterized `asyncio.Queue` ([MED-07]), untyped `prompt_execution` parameters (`store.py:192,247`), the untyped kwargs dict at `sources.py:1229-1238` ([MED-03]), and the swallowed exception at `models.py:227` ([MED-08]). Broad `except Exception` appears frequently but is almost always accompanied by `# noqa: BLE001` and immediate normalization through `classify_unexpected_error` — that is a defensible boundary pattern, not a defect.
|
|
||||||
|
|
||||||
### FastAPI
|
|
||||||
`create_app` (`app.py:87-111`) is a clean factory using the modern `lifespan` context manager, not the deprecated `@app.on_event`. Routers are domain-organized with prefixes and tags. `response_model` is declared on every route. Dependency injection is used correctly in `api/v4_documents.py:111-128`, with the useful touch that `get_document_service` prefers lifespan-owned state and falls back gracefully. Two gaps: service methods called from `async def` endpoints perform synchronous file I/O ([MED-01]), and `_recover_stale_processing_jobs` (`app.py:73-84`) constructs a throwaway `JobService` rather than using the bundle built five lines earlier.
|
|
||||||
|
|
||||||
### NiceGUI
|
|
||||||
The strongest layer of the codebase in terms of convention adherence. CSS discipline is exemplary: a single `ui.add_css(read_css("theme.css"), shared=True)` at the composition root (`ui/__init__.py:28`), read through `importlib.resources` with a `@cache`-backed loader and path validation (`ui/resources.py:10-19`), and zero inline `.style()` calls or `<style>` blocks in components. **No cross-client state leakage was found** — per-request state lives in page-function closures, and the only module-level globals are idempotent registration flags (`theme.py:15`, `_register_panzoom_assets`'s `lru_cache`). `error_presenter.show_error/summarize_error` is applied uniformly and preserves `AppError` id/category/suggestion. The table architecture (generic `build_table` + per-feature row read models) matches the documented split. Defects are the boundary violations in [HIGH-07], the blocking I/O in [MED-01], and the duplication catalogued in §4.
|
|
||||||
|
|
||||||
### SQLModel & SQLAlchemy
|
|
||||||
Session lifecycle is the clear high point: `ServiceBase._finalize` (`services/base.py:41-60`) implements a genuinely well-reasoned commit-vs-flush ownership protocol that lets orchestration functions commit exactly once at the workflow boundary, and `session_scope` / `transaction_scope` (`db/session.py:53-105`) express the two modes cleanly, with `transaction_scope` correctly rejecting a supplied session that has no active transaction. `JSONBCompat` (`models.py:27-35`) is the right cross-dialect abstraction, and `BigInteger` for `file_size_bytes` and `StaticPool` for in-memory SQLite show real attention to portability. Against that, the eager-loading defaults ([CRIT-02]), missing indexes ([HIGH-04]), unlocked job claim ([CRIT-01]), and hand-rolled DDL ([HIGH-05]) are the four issues that most need attention. Note also that `models.py` sets `updated_at` / `date_updated` via `default_factory` only — there is no `onupdate`, so these columns are stale unless a service sets them by hand (`jobs.py:166` does; most other update paths do not).
|
|
||||||
|
|
||||||
### Pydantic V2 & Settings
|
|
||||||
Fully V2-native. No `@validator`, no `class Config`, no `.dict()`, no `parse_obj` anywhere. `model_config = ConfigDict(...)` is used consistently, usually with `extra="forbid", frozen=True` — a good default that catches provider payload drift. `Settings` is a single `BaseSettings` source of truth with `env_nested_delimiter`, a discriminated `DatabaseSettings` union, `SecretStr` for credentials, and constrained `Annotated` types (`NonEmptyStr`, `Probability`, `Temperature`). There are **no scattered `os.getenv` calls** in `src`. The one wart is `object.__setattr__` in `normalize_provider_models` (`config.py:130,137`) to mutate a frozen model — functional but fragile; `model_copy(update=...)` or a computed property would express it more safely. Issues: [MED-02] dead settings, [HIGH-03] the timeout cap, [MED-04] the `@cache` signature.
|
|
||||||
|
|
||||||
### Asyncio Workers
|
|
||||||
`worker_consumer_lifespan` (`worker.py:64-94`) is well-built: it holds a strong reference to the task, sets the stop event, wakes the loop, waits with a bounded timeout, and escalates to `cancel()` + `suppress(CancelledError)` on timeout — a correct graceful-shutdown sequence. The `WorkerNotifier` Protocol with `Event`/`Noop` implementations is a clean seam. `_persist_page_outcome_durably` (`workflows.py:486-499`) uses `asyncio.shield` with correct cancellation re-raise so a shutdown mid-job cannot lose provider evidence — a genuinely subtle piece of code done right. Remaining concerns: the single-worker assumption is unenforced ([CRIT-01]), there is no backpressure or concurrency limit (jobs are processed strictly serially, so a large backlog drains slowly while the provider sits idle), and blocking I/O inside the loop ([MED-01]) stalls both the worker and all HTTP/UI clients on the same loop.
|
|
||||||
|
|
||||||
### OpenRouter / Adapter Boundary
|
|
||||||
Encapsulation is good — no OpenRouter-specific header, model name, or payload shape appears in `services`, `api`, or `ui`. `providers/__init__.py`'s factory keeps the concrete adapter behind `get_transcription_provider`. Responses are validated through real Pydantic schemas (`OpenRouterResponse`, `ResponseChoice`, `ResponseUsage`), and `_CapturingAsyncClient` / `_CapturingAsyncByteStream` (`openrouter.py:45-91`) is a thoughtful mechanism for retaining raw transport bytes for evidence without disturbing SDK parsing. The failures are lifecycle and typing: per-job client churn ([HIGH-02]), no explicit httpx timeout ([HIGH-03]), and runtime `inspect`/`getattr` duck-typing instead of an honest Protocol ([MED-03]).
|
|
||||||
|
|
||||||
### Testing & Quality Tooling
|
|
||||||
264 tests pass with 4 skipped; markers (`unit`/`integration`/`external`) are declared and `--strict-markers` is on; `filterwarnings` escalates never-awaited coroutines to errors — a good async-specific guard. Coverage is broad across services, providers, API, and UI pages. The material gaps: (a) `asyncio_mode = "strict"` is set but no `asyncio_default_fixture_loop_scope` is configured, which pytest-asyncio warns about and which will change behavior on upgrade; (b) **every test runs on SQLite**, so the Postgres support that `JSONBCompat`, `asyncpg`, and `psycopg2-binary` all exist to provide is entirely unverified — [HIGH-05]'s `CHAR(32)` bug is exactly the class of defect this would catch; (c) no test asserts concurrent job-claim safety, which is why [CRIT-01] survives; (d) `ty` cannot gate ([HIGH-06]); (e) `tools/run_destructive_tests.py:76,80` uses `fcntl`, unavailable on this project's Windows development platform.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Duplication & Consolidation Report
|
|
||||||
|
|
||||||
| Pattern / Duplication | Locations | Proposed Canonical Home | Est. Lines Removed |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| Delete-confirmation page scaffold (blocked-deps card + confirm/cancel row) | `ui/pages/documents_page.py:354-433`; `jobs_page.py:358-428`; `sources_page.py:204-277`; `people_page.py:~270-310` | `ui/components/confirm_delete.py` | ~120 |
|
|
||||||
| Upload/media URL resolution (`_resolve_*_src`, `_to_absolute_upload_url`) | `ui/pages/sources_page.py:711-770`; `people_page.py:525-579`; `ui/components/document_panzoom.py:59-75` | `ui/components/media_urls.py` (pure, takes `upload_dir` + `base_url`) | ~110 |
|
|
||||||
| Invalid-id / not-found guard (parse → red label → return) | `documents_page.py:172-184,219-231,275-287,359-371`; `jobs_page.py:212-221,310-319,363-372`; `sources_page.py:111-137,207-222` | `ui/components/guards.py:load_or_render_error(...)` | ~90 |
|
|
||||||
| Hand-rolled `ui.table` instead of `build_table` | `ui/pages/settings_page.py:65-113` + person-roles table; `ui/components/linked_people.py:66-75`; `print_preview_page.py:125-161` | `ui/components/table/common.py:build_table` (add selection / no-search options) | ~70 |
|
|
||||||
| File-picker upload wiring | `people_page.py:491-522`; `jobs_page.py:449-503`; `home_page.py:38-40,85-89` | `ui/components/upload_panel.py` | ~50 |
|
|
||||||
| `_parse_uuid` | `documents_page.py:591`; `jobs_page.py:558`; `sources_page.py:780`; `people_page.py:589`; `linked_people.py:175` | `ui/components/formatters.py` | ~35 |
|
|
||||||
| `_resolve_runtime_settings(request)` | `jobs_page.py:567`; `sources_page.py:773`; `people_page.py:582` | Shared page-helper module | ~18 |
|
|
||||||
| `_parse_iso_date` | `documents_page.py:600`; `people_page.py:598` | `ui/components/formatters.py` | ~14 |
|
|
||||||
| `ServiceBundle` construction block (4 identical service instantiations) | `app.py:45-50`; `worker.py:160-165`; `services/__init__.py:19-22` | `ServiceBundle.from_session_factory(...)` classmethod | ~20 |
|
|
||||||
| "Next queued job" query, two divergent implementations | `services/jobs.py:170-187` (no `LIMIT`); `db/operations.py:143-151` (has `LIMIT`) | `JobService.claim_next_queued_job` (per [CRIT-01]); delete the `operations.py` copy | ~12 |
|
|
||||||
| `build_prompt_execution` re-export shim + legacy aliases | `services/transcription.py` (whole module); `services/store.py:35,382,383` | `services/sources.py` (single import path) | ~45 |
|
|
||||||
| `store_source_file` / `store_person_portrait` / `store_homepage_image` — three near-identical validate-hash-write-bytes flows | `services/store.py:319-379`; `services/people.py:596-631`; `ui/homepage_store.py:31-44` | `services/media_storage.py` (one async, `to_thread`-wrapped writer) | ~60 |
|
|
||||||
| Registry CRUD (list / summaries / create / read / update / delete / referenced) for `DocumentType` and `PersonRole` | `services/documents.py:350-500`; `services/people.py:214-378` | `services/registry.py:RegistryService[ModelT]` ([MED-11]) | ~200 |
|
|
||||||
| Label normalization + casefold key + summary dataclass | `services/documents.py:49-72`; `services/people.py:49-79` | `services/registry.py` (base) | ~35 |
|
|
||||||
| `get(...)` → `if None: raise ...NOT_FOUND` guard, written longhand 38 times | `services/people.py` (15), `sources.py` (14), `documents.py` (9) | `ServiceBase._get_or_raise` ([MED-12]) | ~150 |
|
|
||||||
|
|
||||||
### Proposed Canonical Abstractions
|
|
||||||
|
|
||||||
```python
|
|
||||||
# src/transcription/services/media_storage.py
|
|
||||||
async def store_media(
|
|
||||||
*, filename: str, content: bytes, root: Path, relative_directory: Path | None = None,
|
|
||||||
filename_stem: str | None = None, validate: Callable[[str, bytes], None] | None = None,
|
|
||||||
) -> StoredMedia: ... # StoredMedia = frozen dataclass(path, sha256, byte_size, media_type)
|
|
||||||
# wraps the blocking write in asyncio.to_thread — resolves [MED-01]
|
|
||||||
|
|
||||||
# src/transcription/services/__init__.py
|
|
||||||
@classmethod
|
|
||||||
def from_session_factory(cls, factory: SessionFactory, settings: Settings | None = None) -> ServiceBundle: ...
|
|
||||||
|
|
||||||
# src/transcription/services/jobs.py
|
|
||||||
async def claim_next_queued_job(self, *, session: AsyncSession | None = None) -> Job | None: ...
|
|
||||||
# atomic QUEUED -> PROCESSING with LIMIT 1 + FOR UPDATE SKIP LOCKED
|
|
||||||
|
|
||||||
# src/transcription/services/registry.py
|
|
||||||
class RegistryService[ModelT: RegistryModel](ServiceBase):
|
|
||||||
"""Shared CRUD for semantic-key registries (DocumentType, PersonRole)."""
|
|
||||||
model: type[ModelT]
|
|
||||||
error: type[AppError]
|
|
||||||
noun: str
|
|
||||||
async def list_all(self, *, active_only: bool = True, session=None) -> Sequence[ModelT]: ...
|
|
||||||
async def list_summaries(self, *, session=None) -> Sequence[RegistrySummary]: ...
|
|
||||||
async def create(self, *, label: str, is_active: bool = True, session=None) -> ModelT: ...
|
|
||||||
async def read(self, entity_id: UUID, *, session=None) -> ModelT: ...
|
|
||||||
async def update(self, entity_id: UUID, *, label: str, is_active: bool, session=None) -> ModelT: ...
|
|
||||||
async def delete(self, entity_id: UUID, *, session=None) -> None: ...
|
|
||||||
async def is_referenced(self, entity_id: UUID, *, session=None) -> bool: ...
|
|
||||||
def _reference_query(self, entity: ModelT) -> Select[tuple[UUID]]: ... # subclass hook
|
|
||||||
|
|
||||||
# src/transcription/services/base.py
|
|
||||||
async def _get_or_raise[T](
|
|
||||||
self, session: AsyncSession, model: type[T], entity_id: UUID, *,
|
|
||||||
error: type[AppError], noun: str, suggestion: str,
|
|
||||||
) -> T: ... # absorbs 38 hand-written not-found blocks — resolves [MED-12]
|
|
||||||
|
|
||||||
# src/transcription/ui/components/media_urls.py
|
|
||||||
def build_upload_url(*, file_path: Path, upload_dir: Path, base_url: str) -> str | None: ...
|
|
||||||
|
|
||||||
# src/transcription/ui/components/confirm_delete.py
|
|
||||||
def render_confirm_delete(
|
|
||||||
*, title: str, blockers: Sequence[str], on_confirm: Callable[[], Awaitable[None]],
|
|
||||||
on_cancel: Callable[[], None],
|
|
||||||
) -> None: ...
|
|
||||||
|
|
||||||
# src/transcription/ui/components/guards.py
|
|
||||||
def parse_uuid_or_render_error(raw: str, *, entity: str) -> UUID | None: ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Prioritized Action Plan
|
|
||||||
|
|
||||||
> **Superseded for V4.6.** The three phases below are the original review's sequencing. The V4.6 release restructures this into seven phases against the confirmed operating context in [§1a](#1a-post-review-addendum); see [`ver4.6/implementation_plan_v4_6.md`](ver4.6/implementation_plan_v4_6.md). The material differences are: Alembic is replaced by a schema re-level; the schema-affecting items are merged into a single pass; the service-layer consolidation ([MED-11], [MED-12], [MED-13]) is added; and the `SourceService` split ([MED-14]) is deferred to V4.7.
|
|
||||||
|
|
||||||
### Phase 1: Quick Wins (PR 1-2)
|
|
||||||
1. Delete `src/transcription/app_state.py` — dead module with a live `TypeError` ([HIGH-01]).
|
|
||||||
2. Remove `le=20.0` from `worker_provider_timeout_seconds`, raise the default, and pass an explicit `httpx.Timeout` to the OpenRouter client ([HIGH-03]).
|
|
||||||
3. Add `Index("ix_job_status_date_created", "status", "date_created")` and `index=True` on the hot foreign keys ([HIGH-04]).
|
|
||||||
4. Add `.limit(1)` to `read_next_queued_job` — a one-line change that removes the full-queue load ahead of the full [CRIT-01] fix.
|
|
||||||
5. Delete `services/transcription.py`, the three `store.py` aliases, and `ServiceBase.queue`; standardize `build_prompt_execution` imports ([MED-05], [MED-07]).
|
|
||||||
6. Move the 23KB SVG to `ui/static/` and run `ruff check --fix` ([MED-09], [LOW-01]).
|
|
||||||
7. Resolve or delete `sqlite_check_same_thread` and `worker_retry_backoff_seconds` ([MED-02]).
|
|
||||||
|
|
||||||
### Phase 2: Reliability & Concurrency (PR 3-4)
|
|
||||||
8. Implement `claim_next_queued_job` with `LIMIT 1` + `FOR UPDATE SKIP LOCKED`, delete the `db/operations.py` duplicate, and add a concurrency test that runs two claimers against one queued job ([CRIT-01]).
|
|
||||||
9. Hoist `ServiceBundle` and the provider client to worker-loop scope so the HTTP connection pool survives across jobs ([HIGH-02], [MED-06]).
|
|
||||||
10. Wrap blocking media/artifact I/O and Pillow normalization in `asyncio.to_thread` behind a single `services/media_storage.py` ([MED-01]).
|
|
||||||
11. ~~Adopt Alembic~~ — **superseded**: re-level the schema from current metadata and delete the `_upgrade_*` chain ([HIGH-05]), landing together with [HIGH-04], [HIGH-08], and [CRIT-02] in one pass.
|
|
||||||
12. Extend `TranscriptionProvider` Protocol to cover `aclose` and the evidence attributes; delete the `inspect.signature` reflection ([MED-03]).
|
|
||||||
|
|
||||||
### Phase 3: Consolidation & Refactoring (PR 5-6)
|
|
||||||
13. Flip relationship defaults to `lazy="raise"` model by model, letting the existing suite prove which explicit `selectinload()` calls are load-bearing ([CRIT-02]). This also removes most of the `# pyright: ignore` comments.
|
|
||||||
14. Standardize on `ty`, convert remaining suppressions to `# ty: ignore[...]`, and wire `ty check` into the existing pre-commit setup ([HIGH-06]).
|
|
||||||
15. Fix the three UI boundary violations: session ownership in `jobs_page`, `sqlalchemy.inspect` in `sources_page`, `get_settings()` in `document_panzoom` ([HIGH-07]).
|
|
||||||
16. Extract the UI duplication per §4, highest value first: `confirm_delete` → `media_urls` → `guards` → `formatters` (~500 lines removed).
|
|
||||||
17. Replace `functools.cache` on engine/session factories with an explicit URL-keyed registry supporting targeted eviction ([MED-04]).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Preserved Strengths
|
|
||||||
|
|
||||||
- **`ServiceBase._finalize` (`services/base.py:41-60`)** — the commit-vs-flush ownership protocol is the single best idea in the codebase. It lets orchestration functions compose multiple services into one atomic transaction without any service knowing about the others, and it is documented in `services.instructions.md`. Keep it and keep enforcing it.
|
|
||||||
- **Error taxonomy (`errors.py`)** — `AppError` carrying `category`, `suggestion`, `retriable`, and a short shareable `error_id`, with `classify_unexpected_error` normalizing at every boundary and `format_error_detail` producing a stable persisted string. It is applied consistently from services through API handlers to `ui/components/error_presenter.py`.
|
|
||||||
- **Evidence capture pipeline** — `_CapturingAsyncClient` / `_CapturingAsyncByteStream` (`openrouter.py:45-91`) plus `ExecutionAttempt` / `ProcessingArtifact` with content-addressed digests and integrity verification (`sources.py:925-959`) is a serious, well-executed provenance design that is rare to see done properly.
|
|
||||||
- **`asyncio.shield` around page-outcome persistence (`workflows.py:486-499`)** — correctly written, including the `await task` before re-raising `CancelledError`, so provider results survive shutdown mid-job.
|
|
||||||
- **Worker lifespan shutdown (`worker.py:64-94`)** — strong task reference, stop event, wake, bounded wait, then cancel-and-suppress. Textbook correct.
|
|
||||||
- **Pydantic V2 discipline** — zero V1 residue, `extra="forbid"` + `frozen=True` as the house default, constrained `Annotated` types, `SecretStr` for credentials, discriminated union for database config, and no `os.getenv` anywhere in `src`.
|
|
||||||
- **UI CSS and asset discipline** — one `add_css` at the composition root, `importlib.resources` with a `@cache`d reader and path validation, semantic `ui-*` classes, no inline styles. This is exactly what `ui.instructions.md` prescribes, followed without exception.
|
|
||||||
- **No cross-client state leakage in NiceGUI** — per-request state lives in page-function closures; the only module globals are idempotent registration flags. This is the most common NiceGUI defect and this codebase avoids it entirely.
|
|
||||||
- **Cross-dialect care** — `JSONBCompat`, `BigInteger` for byte sizes, `native_enum=False` with `values_callable` for stable enum storage, `StaticPool` for in-memory SQLite. The intent is right; it just needs Postgres CI to make it real.
|
|
||||||
- **The instruction files themselves** — `.github/instructions/services.instructions.md` and `ui.instructions.md` are specific, enforceable, and largely followed. Most findings in this report are deviations from rules the project already wrote down, which is a much healthier position than having no rules at all.
|
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
# Error Handling
|
||||||
|
|
||||||
|
This document defines the canonical error-handling policy for the document transcription system. It is the single source of truth for how errors are classified, surfaced to users, logged for diagnosis, and handled across UI, API, service, worker, and provider boundaries.
|
||||||
|
|
||||||
|
## Error Handling Objectives
|
||||||
|
|
||||||
|
The production error-handling model is designed to:
|
||||||
|
|
||||||
|
- make failures visible to the user in clear, actionable language
|
||||||
|
- preserve enough diagnostic detail for fast troubleshooting
|
||||||
|
- keep module behavior consistent across all boundaries
|
||||||
|
- distinguish expected domain failures from unexpected defects
|
||||||
|
- support safe retries for transient failures without hiding persistent faults
|
||||||
|
|
||||||
|
## Scope And Authority
|
||||||
|
|
||||||
|
This page governs error-handling behavior for:
|
||||||
|
|
||||||
|
- UI interactions (NiceGUI pages)
|
||||||
|
- API endpoints (FastAPI routes)
|
||||||
|
- application services and orchestration logic
|
||||||
|
- in-process background worker execution
|
||||||
|
- external provider adapters and persistence adapters
|
||||||
|
|
||||||
|
If implementation behavior conflicts with this document, this document is authoritative and implementation should be updated.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
- **Clarity first:** user-facing messages should explain what failed in plain language.
|
||||||
|
- **Actionability required:** each surfaced error should include a suggested next step.
|
||||||
|
- **Safety by default:** internal details are logged; sensitive details are not exposed by default in UI/API.
|
||||||
|
- **Consistency across boundaries:** category and structure should remain stable from source to surface.
|
||||||
|
- **Fail explicitly:** silent failure is prohibited.
|
||||||
|
- **Traceability:** every non-trivial error should be traceable with an error reference ID.
|
||||||
|
|
||||||
|
## Error Taxonomy
|
||||||
|
|
||||||
|
The system uses stable, implementation-independent categories:
|
||||||
|
|
||||||
|
| Category | Definition | Typical Source | Retriable |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `validation_error` | Payload or parameter shape/content is invalid | UI/API input validation, service guards | no |
|
||||||
|
| `user_input_error` | User-provided artifact is unacceptable though structurally valid | unsupported file type, empty file, oversized upload | sometimes |
|
||||||
|
| `not_found_error` | Requested resource does not exist | missing job/document/transcript | no |
|
||||||
|
| `conflict_error` | Requested operation violates current state constraints | invalid state transition | no |
|
||||||
|
| `external_provider_error` | External AI/provider call fails | upstream HTTP/API/provider failures | sometimes |
|
||||||
|
| `infrastructure_transient_error` | Temporary environment issue | network timeout, DB connection reset | yes |
|
||||||
|
| `infrastructure_persistent_error` | Non-transient environment issue | missing permissions, misconfiguration | no |
|
||||||
|
| `internal_unexpected_error` | Unhandled defect or unknown failure | uncaught exceptions, logic errors | unknown (default no) |
|
||||||
|
|
||||||
|
### Classification Rules
|
||||||
|
|
||||||
|
- Classification occurs as close as possible to the origin boundary.
|
||||||
|
- Provider-specific exceptions must be normalized into taxonomy categories before crossing service boundaries.
|
||||||
|
- Unknown exceptions are classified as `internal_unexpected_error` and logged with traceback.
|
||||||
|
- Category names are stable contracts and must not be changed casually.
|
||||||
|
|
||||||
|
## User-Facing Error Experience Contract
|
||||||
|
|
||||||
|
When an error is shown in the GUI, it must include:
|
||||||
|
|
||||||
|
1. **Title** (short context, e.g., “Upload failed”)
|
||||||
|
2. **Message** (plain-language explanation)
|
||||||
|
3. **Suggested action** (explicit next step)
|
||||||
|
4. **Error reference ID** (for support/debug traceability)
|
||||||
|
5. **Technical details** (optional/collapsible for advanced users)
|
||||||
|
|
||||||
|
### UI Message Rules
|
||||||
|
|
||||||
|
- Do not expose raw stack traces by default.
|
||||||
|
- Do not expose secrets, credentials, connection strings, or filesystem internals unless explicitly in debug tooling.
|
||||||
|
- Prefer domain language over implementation language.
|
||||||
|
- Use persistent visibility for important failures (dialog/card), not only transient toasts.
|
||||||
|
|
||||||
|
### Suggested Action Requirements
|
||||||
|
|
||||||
|
Every user-visible error must include a suggested course of action, such as:
|
||||||
|
|
||||||
|
- retry the operation
|
||||||
|
- check file type/size constraints
|
||||||
|
- refresh the jobs page
|
||||||
|
- verify environment configuration
|
||||||
|
- contact operator with error ID and timestamp
|
||||||
|
|
||||||
|
## API Error Response Contract
|
||||||
|
|
||||||
|
API errors should return a structured envelope with stable fields:
|
||||||
|
|
||||||
|
- `error_id`: short unique reference ID
|
||||||
|
- `category`: taxonomy category
|
||||||
|
- `message`: safe human-readable summary
|
||||||
|
- `suggestion`: recommended next step
|
||||||
|
- `details`: optional, only when safe and appropriate
|
||||||
|
- `timestamp`: UTC ISO-8601
|
||||||
|
|
||||||
|
HTTP status mapping guidance:
|
||||||
|
|
||||||
|
- `validation_error`, `user_input_error` -> `400`
|
||||||
|
- `not_found_error` -> `404`
|
||||||
|
- `conflict_error` -> `409`
|
||||||
|
- `external_provider_error` -> `502` or `503` (depending on failure mode)
|
||||||
|
- `infrastructure_transient_error` -> `503`
|
||||||
|
- `infrastructure_persistent_error` -> `500`
|
||||||
|
- `internal_unexpected_error` -> `500`
|
||||||
|
|
||||||
|
## Logging And Observability Contract
|
||||||
|
|
||||||
|
All logged errors must include, where available:
|
||||||
|
|
||||||
|
- `error_id`
|
||||||
|
- `category`
|
||||||
|
- `operation` (e.g., `upload.submit`, `worker.process_job`, `jobs.refresh`)
|
||||||
|
- `exception_type`
|
||||||
|
- `job_id`, `document_id` (when relevant)
|
||||||
|
- UTC timestamp
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Use structured logging fields where practical.
|
||||||
|
- Use full traceback for unexpected errors (`internal_unexpected_error`).
|
||||||
|
- Log at boundary handoff points to preserve causal trail.
|
||||||
|
- Avoid duplicate noisy logging for the same exception at every layer.
|
||||||
|
|
||||||
|
## Recovery And Retry Policy
|
||||||
|
|
||||||
|
### Retriable Conditions
|
||||||
|
|
||||||
|
Retriable failures include:
|
||||||
|
|
||||||
|
- transient network/provider timeouts
|
||||||
|
- intermittent provider unavailability
|
||||||
|
- temporary DB/network interruptions
|
||||||
|
|
||||||
|
### Non-Retriable Conditions
|
||||||
|
|
||||||
|
Non-retriable failures include:
|
||||||
|
|
||||||
|
- invalid file formats
|
||||||
|
- missing required data
|
||||||
|
- permission/configuration failures
|
||||||
|
- deterministic domain conflicts
|
||||||
|
|
||||||
|
### Worker Behavior
|
||||||
|
|
||||||
|
- The worker must classify and persist failure details consistently.
|
||||||
|
- Retries should be bounded by configured limits.
|
||||||
|
- Exhausted retries must end in explicit failed status with recorded reason.
|
||||||
|
- No infinite retry loops are allowed.
|
||||||
|
|
||||||
|
## Boundary-Specific Responsibilities
|
||||||
|
|
||||||
|
### UI Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- display user-safe error summaries and suggested actions
|
||||||
|
- show persistent error visibility for critical failures
|
||||||
|
- include error reference IDs in visible output
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- low-level exception parsing
|
||||||
|
- provider-specific protocol interpretation
|
||||||
|
|
||||||
|
### API Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- map application exceptions into stable error envelopes and HTTP statuses
|
||||||
|
- preserve category and error_id continuity
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- domain-specific remediation logic
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- classify domain and infrastructure exceptions
|
||||||
|
- convert adapter-specific failures into taxonomy categories
|
||||||
|
- return deterministic error types to callers
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- presentation formatting for UI
|
||||||
|
|
||||||
|
### Worker Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- execute retry policy for retriable failures
|
||||||
|
- persist terminal failure details for jobs
|
||||||
|
- emit operational logs with category and identifiers
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- direct UI messaging
|
||||||
|
|
||||||
|
### Provider Adapter Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- normalize provider SDK/HTTP failures into domain-neutral exceptions
|
||||||
|
- preserve raw provider context for logs (safely)
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- choosing user-facing wording
|
||||||
|
|
||||||
|
## Error Lifecycle Workflow
|
||||||
|
|
||||||
|
Standard lifecycle:
|
||||||
|
|
||||||
|
1. Failure occurs at a boundary or operation.
|
||||||
|
2. Exception is classified into taxonomy category.
|
||||||
|
3. `error_id` is created (or propagated).
|
||||||
|
4. Error is logged with required structured fields.
|
||||||
|
5. User/API receives safe message + suggested action.
|
||||||
|
6. Persistent job/resource state is updated when applicable.
|
||||||
|
7. Tests verify contract behavior for the pathway.
|
||||||
|
|
||||||
|
## Test Strategy For Error Handling
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- category classification behavior
|
||||||
|
- retry eligibility decisions
|
||||||
|
- exception-to-message mapping safety
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- UI pathways show clear message + suggested action for known failures
|
||||||
|
- API returns structured error envelope with expected status/category
|
||||||
|
- worker persists failed status and failure detail as required
|
||||||
|
|
||||||
|
### Regression Tests
|
||||||
|
|
||||||
|
- each previously observed production issue should have a guarding test
|
||||||
|
- contract tests must cover adapter error normalization behavior
|
||||||
|
|
||||||
|
## Known Failure Patterns And Prescribed Responses
|
||||||
|
|
||||||
|
| Pattern | Category | User Message | Suggested Action |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Upload payload cannot be parsed by UI handler | `internal_unexpected_error` (until narrowed) | Upload failed due to unexpected processing error | Retry once; if repeated, report error ID and check runtime version compatibility |
|
||||||
|
| Unsupported extension | `user_input_error` | File type is not supported | Upload JPG, PNG, TIFF, or PDF |
|
||||||
|
| Empty file upload | `validation_error` | Uploaded file is empty | Choose a valid non-empty file and retry |
|
||||||
|
| Provider timeout | `external_provider_error` or `infrastructure_transient_error` | Transcription provider timed out | Retry from jobs page; if repeated, check provider status |
|
||||||
|
| Job lookup missing | `not_found_error` | Requested job was not found | Refresh jobs list and open a valid job |
|
||||||
|
|
||||||
|
## Governance And Update Process
|
||||||
|
|
||||||
|
This document is a living policy artifact.
|
||||||
|
|
||||||
|
Update this document when:
|
||||||
|
|
||||||
|
- new error categories are introduced
|
||||||
|
- handling behavior changes at any boundary
|
||||||
|
- a production incident reveals missing guidance
|
||||||
|
- API/UI error contracts change
|
||||||
|
|
||||||
|
Change requirements:
|
||||||
|
|
||||||
|
- update this document and associated tests in the same change set
|
||||||
|
- preserve taxonomy stability; if changed, document migration impact
|
||||||
|
- record noteworthy policy changes in project release notes or changelog
|
||||||
|
|
||||||
|
## Related Pages
|
||||||
|
|
||||||
|
- [System overview](index.md)
|
||||||
|
- [Architecture](architecture.md)
|
||||||
|
- [Requirements](requirements.md)
|
||||||
|
- [Intent](intent.md)
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Error category: Stable classification used to drive handling, messaging, and status mapping.
|
||||||
|
- Error envelope: Structured API payload describing a failure.
|
||||||
|
- Error reference ID: Short identifier used to correlate user-visible failure with logs.
|
||||||
|
- Retriable error: Failure likely to succeed on a later attempt without code changes.
|
||||||
|
- Terminal failure: Failure state after retries are exhausted or retry is not allowed.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## Document Transcription System
|
||||||
|
|
||||||
|
This project is a production application for transcribing and preserving historical family documents. It is intentionally designed for personal-scale use, with a simplicity-first architecture that is easy to operate and easy to extend.
|
||||||
|
|
||||||
|
## Start Here
|
||||||
|
|
||||||
|
Read [architecture.md](architecture.md) first.
|
||||||
|
|
||||||
|
Then review [ver1/ver1.md](ver1/ver1.md) for completion scope.
|
||||||
|
|
||||||
|
The architecture page is the primary technical reference and defines:
|
||||||
|
|
||||||
|
- deployed topology and infrastructure limits
|
||||||
|
- module boundaries and dependency flow
|
||||||
|
- processing life cycle and data ownership
|
||||||
|
- test strategy, risk controls, and extension path
|
||||||
|
|
||||||
|
## What The Application Does
|
||||||
|
|
||||||
|
At a high level, users upload images of handwritten, typed, or typeset documents, run asynchronous transcription jobs, review and edit transcript revisions, and search across accepted text.
|
||||||
|
|
||||||
|
Core capabilities:
|
||||||
|
|
||||||
|
- document upload and metadata capture
|
||||||
|
- asynchronous transcription with visible job status
|
||||||
|
- transcription prompt management with one Markdown file per prompt for human refinement over time
|
||||||
|
- revision history for transcript edits
|
||||||
|
- full-text search over accepted transcripts
|
||||||
|
- export of transcript data
|
||||||
|
|
||||||
|
## Production Operating Model
|
||||||
|
|
||||||
|
The system runs with minimal operational overhead:
|
||||||
|
|
||||||
|
- PostgreSQL in a dedicated Docker container is considered extremely lightweight and simple for this system
|
||||||
|
- MongoDB in a dedicated Docker container is also considered extremely lightweight and simple for document-centric persistence
|
||||||
|
- a three-container deployment (app, PostgreSQL, MongoDB) is a simple and acceptable baseline
|
||||||
|
- no required queue or search-engine containers in the baseline setup
|
||||||
|
|
||||||
|
This operating model keeps deployment and maintenance simple while preserving clean boundaries for future scale.
|
||||||
|
|
||||||
|
## Documentation Map
|
||||||
|
|
||||||
|
- Version 1 implementation plan: [ver1/ver1.md](ver1/ver1.md)
|
||||||
|
- Architecture and technical design: [architecture.md](architecture.md)
|
||||||
|
- Architecture decision records (ADR index): [adr/README.md](adr/README.md)
|
||||||
|
- Runtime and deployment requirements: [requirements.md](requirements.md)
|
||||||
|
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
||||||
|
- Domain context and transcription policy: [intent.md](intent.md)
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Document-oriented persistence: Storing data as flexible records instead of fixed relational rows.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and is edited independently.
|
||||||
|
- System of record: The authoritative persistent store for canonical data.
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# Digital Evidence and AI Processing Provenance (Invariant)
|
|
||||||
|
|
||||||
## 1. Purpose
|
|
||||||
|
|
||||||
This document defines non-negotiable evidence and provenance rules for the transcription application.
|
|
||||||
|
|
||||||
The application exists to preserve historical source material and produce useful transcriptions without losing the ability to inspect, reinterpret, or reprocess the evidence later. Provider integrations, model names, schemas, and user interfaces may change; the principles below must remain true.
|
|
||||||
|
|
||||||
## 2. Evidence Model
|
|
||||||
|
|
||||||
The application distinguishes five kinds of information:
|
|
||||||
|
|
||||||
1. **Source evidence**: the original uploaded media and the facts needed to identify and verify it.
|
|
||||||
2. **Execution specification**: the frozen instructions, parameters, source identity, and software context for one processing attempt.
|
|
||||||
3. **Transport evidence**: the response received at the application/provider boundary, including safe protocol metadata.
|
|
||||||
4. **Normalized data**: selected fields extracted for search, display, accounting, and workflow behavior.
|
|
||||||
5. **Derived artifacts**: outputs produced from source evidence, such as transcription text, OCR geometry, confidence data, layout analysis, or entity extraction.
|
|
||||||
|
|
||||||
Normalized data and derived artifacts never replace source or transport evidence.
|
|
||||||
|
|
||||||
## 3. Core Invariants
|
|
||||||
|
|
||||||
### 3.1 Original Source Preservation
|
|
||||||
|
|
||||||
1. The original uploaded bytes are the primary evidence and must be preserved without transformation.
|
|
||||||
2. Each source must have a cryptographic content digest, byte size, and stable identity.
|
|
||||||
3. Processing may use transformed derivatives, but those derivatives must not overwrite the original.
|
|
||||||
4. A derivative used for processing must record its relationship to the original, its transformation, and its own digest.
|
|
||||||
5. Moving or renaming a stored file must not change its evidence identity.
|
|
||||||
|
|
||||||
### 3.2 Append-Only Processing History
|
|
||||||
|
|
||||||
1. Every processing attempt must have a distinct execution record, whether it succeeds, partially succeeds, times out, or fails.
|
|
||||||
2. A later attempt must not overwrite the evidence from an earlier attempt.
|
|
||||||
3. A convenient “latest transcription” value may be maintained as a cache or projection, but it is not the authoritative execution history.
|
|
||||||
4. Human revisions must remain distinguishable from all machine-generated outputs.
|
|
||||||
5. Reprocessing a source must create new evidence rather than rewriting historical evidence.
|
|
||||||
|
|
||||||
### 3.3 Frozen Execution Specification
|
|
||||||
|
|
||||||
Each execution must preserve enough information to understand what the application asked the processor to do:
|
|
||||||
|
|
||||||
1. Requested provider, model, and provider-routing constraints.
|
|
||||||
2. Full effective system and user instructions.
|
|
||||||
3. Prompt asset name and content digest when a prompt asset is used.
|
|
||||||
4. Every explicitly supplied generation or processing parameter.
|
|
||||||
5. Whether an optional parameter was explicitly set or omitted.
|
|
||||||
6. Source and derivative digests, media type, dimensions or page geometry when known, and page identity.
|
|
||||||
7. A secret-safe representation of the request structure.
|
|
||||||
8. Application, provider-adapter, and client-library versions sufficient to interpret the execution.
|
|
||||||
|
|
||||||
The execution specification must not contain credentials, authorization headers, secret query values, or unnecessary duplicate source binaries.
|
|
||||||
|
|
||||||
### 3.4 Evidence-Layer Terminology
|
|
||||||
|
|
||||||
The following terms are not interchangeable:
|
|
||||||
|
|
||||||
- **Transport response**: the status, safe headers, and exact response body received by the application at its HTTP boundary.
|
|
||||||
- **Router-normalized response**: a response transformed by an intermediary into its common schema.
|
|
||||||
- **SDK-parsed response**: an object created when a client library validates or filters a response.
|
|
||||||
- **Normalized metadata**: application-selected fields derived from a response.
|
|
||||||
- **Native provider response**: the upstream provider's own response before any intermediary transformation.
|
|
||||||
|
|
||||||
The application and its documentation must identify which layer is stored. A response must not be described as “raw,” “complete,” or “native” without naming the boundary at which that claim is true.
|
|
||||||
|
|
||||||
### 3.5 Transport Evidence
|
|
||||||
|
|
||||||
1. Preserve the exact successful response body received at the application's transport boundary before SDK model parsing can discard unknown fields.
|
|
||||||
2. Preserve the response status and an allowlisted set of non-secret headers needed for correlation, content interpretation, rate-limit diagnosis, or audit.
|
|
||||||
3. Preserve provider/router request and generation identifiers when available.
|
|
||||||
4. Preserve safe response evidence for unsuccessful calls when a response was received.
|
|
||||||
5. Record explicitly when no response was received, such as a local timeout or connection failure.
|
|
||||||
6. Retain parsed and normalized forms only as additional representations of the preserved response.
|
|
||||||
|
|
||||||
Wire-level packet capture, TLS session data, credentials, and unrestricted headers are neither required nor permitted.
|
|
||||||
|
|
||||||
These requirements apply to executions performed after transport capture is implemented. For earlier executions, the absence of transport evidence must be represented explicitly. An SDK snapshot or normalized record must never be relabeled or backfilled as transport evidence.
|
|
||||||
|
|
||||||
### 3.6 Derived Artifact Provenance
|
|
||||||
|
|
||||||
1. Every derived artifact must identify its source evidence and producing execution.
|
|
||||||
2. Each artifact must declare its semantic type, media/serialization format, schema name and version, producer, producer version, and creation time.
|
|
||||||
3. Artifact content must be stored directly or referenced by a stable path or object identifier and protected by a cryptographic digest.
|
|
||||||
4. Coordinates must declare their coordinate system, units, origin, page/image dimensions, and transformation history.
|
|
||||||
5. Confidence values must identify the producer and scope to which they apply; values from different producers must not be treated as directly comparable without validation.
|
|
||||||
6. Provider-specific payloads may be retained, but durable application behavior must not depend on undocumented provider fields.
|
|
||||||
|
|
||||||
This model must accommodate future OCR text, word or line polygons, layout regions, confidence data, alternate transcriptions, and structured extraction without adding a dedicated column for every possible feature.
|
|
||||||
|
|
||||||
### 3.7 Integrity and Auditability
|
|
||||||
|
|
||||||
1. Stored evidence must be exportable with enough identifiers and metadata to verify relationships and digests outside the application.
|
|
||||||
2. Evidence mutation, deletion, and retention behavior must be explicit and testable.
|
|
||||||
3. Schema upgrades must preserve existing evidence and its original meaning.
|
|
||||||
4. Backfills must be identified as backfills; they must not imply that previously uncaptured evidence existed.
|
|
||||||
5. Integrity verification must distinguish a missing file, digest mismatch, unavailable external artifact, and malformed metadata.
|
|
||||||
|
|
||||||
### 3.8 Security and Privacy
|
|
||||||
|
|
||||||
1. API keys, authorization headers, cookies, and credentials must never be persisted as provenance.
|
|
||||||
2. Persist only headers and metadata fields that appear on an explicit allowlist of known-safe fields. Discard all other fields before storage; never persist an unrestricted capture and attempt to redact it afterward.
|
|
||||||
3. Request manifests should reference source content by identity instead of duplicating base64 source data.
|
|
||||||
4. Diagnostic displays and exports must avoid exposing secrets or machine-local details that are not necessary for evidence interpretation.
|
|
||||||
|
|
||||||
## 4. Reproducibility Limits
|
|
||||||
|
|
||||||
Provenance supports explanation, comparison, and best-effort reproduction; it does not guarantee identical output.
|
|
||||||
|
|
||||||
Identical requests may produce different results because of model updates, provider routing, nondeterministic computation, undocumented defaults, safety systems, or retired endpoints. The application must preserve whether a parameter was omitted rather than pretending to know the provider default used at that time.
|
|
||||||
|
|
||||||
Likewise, preserving a general vision-model response does not create OCR coordinates that were never returned. Future coordinate extraction remains possible because the original source evidence is preserved and can be processed again by a suitable system.
|
|
||||||
|
|
||||||
## 5. Model Evaluation Policy
|
|
||||||
|
|
||||||
Model selection must be based on a representative sample of the actual archive rather than vendor claims alone.
|
|
||||||
|
|
||||||
Evaluation should:
|
|
||||||
|
|
||||||
1. Use manually reviewed reference transcriptions following the project's [Transcription Methodology](transcription_methodology.md).
|
|
||||||
2. Represent printed, typed, handwritten, degraded, tabular, multilingual, and spatially complex material present in the archive.
|
|
||||||
3. Measure character and word error rates where appropriate.
|
|
||||||
4. Separately record silent corrections, invented text, omitted text, uncertainty handling, layout fidelity, cost, and latency.
|
|
||||||
5. Preserve the exact model, endpoint or route, parameters, prompt, source digest, and scoring method for every comparison.
|
|
||||||
6. Treat model rankings as corpus- and version-specific, not permanent declarations of a universal “best” model.
|
|
||||||
|
|
||||||
Benchmark material containing family records remains private application data unless explicitly approved for publication.
|
|
||||||
|
|
||||||
## 6. Ownership and Change Policy
|
|
||||||
|
|
||||||
1. Versioned architecture, schema, scope, and implementation documents define how a release satisfies this invariant.
|
|
||||||
2. Provider adapters own the capture of provider-boundary evidence.
|
|
||||||
3. Services own validation, persistence, retention, and export behavior.
|
|
||||||
4. UI pages may inspect evidence through service contracts but do not define evidence semantics.
|
|
||||||
5. If implementation conflicts with this invariant, either correct the implementation or explicitly revise this document before accepting the behavior.
|
|
||||||
6. Revisions to this document require deliberate review because they change the long-term preservation contract.
|
|
||||||
|
|
||||||
## 7. Related Invariants
|
|
||||||
|
|
||||||
- [Historical Document Transcription Design Intent](intent.md)
|
|
||||||
- [Transcription Methodology & Style Guide](transcription_methodology.md)
|
|
||||||
- [UI Style Guide](ui_style_guide.md)
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# Error Handling (Invariant)
|
|
||||||
|
|
||||||
## 1. Purpose
|
|
||||||
|
|
||||||
This document defines the non-negotiable failure-handling principles for the transcription application.
|
|
||||||
|
|
||||||
Error categories, API envelopes, status codes, framework integrations, and persistence fields may change between versions. Failures must nevertheless remain visible, safe, diagnosable, and consistent across every application boundary.
|
|
||||||
|
|
||||||
## 2. Core Invariants
|
|
||||||
|
|
||||||
### 2.1 Failures Are Visible
|
|
||||||
|
|
||||||
1. An operation must not report success when all or part of the requested work failed.
|
|
||||||
2. Invalid input, unavailable dependencies, persistence failures, provider failures, and unexpected defects must be surfaced through the application's established error path.
|
|
||||||
3. Code must not silently discard an exception, provider response, invalid value, or failed state transition.
|
|
||||||
4. When work can partially succeed, the successful and failed portions must be identified separately.
|
|
||||||
|
|
||||||
### 2.2 Messages Are Actionable
|
|
||||||
|
|
||||||
1. Operator-facing errors must explain what failed in concise language.
|
|
||||||
2. When a safe corrective action is known, the error must state it.
|
|
||||||
3. Expected validation or conflict failures must not be presented as unexplained internal defects.
|
|
||||||
4. Internal diagnostics must not replace a usable operator-facing message.
|
|
||||||
|
|
||||||
### 2.3 Errors Have Stable Identity and Classification
|
|
||||||
|
|
||||||
1. Every surfaced failure must have a stable correlation identifier or equivalent trace identity.
|
|
||||||
2. Failures must be classified into a documented, machine-readable category.
|
|
||||||
3. Boundary-specific representations must preserve the original category and correlation identity.
|
|
||||||
4. Unknown exceptions must be converted at an explicit boundary, retain their causal chain for diagnostics, and be classified as unexpected rather than disguised as an expected failure.
|
|
||||||
|
|
||||||
### 2.4 Boundary Translation Is Consistent
|
|
||||||
|
|
||||||
1. UI, API, service, worker, persistence, and provider boundaries must use one shared error model or deterministic translations between documented models.
|
|
||||||
2. A boundary may simplify presentation, but it must not change the meaning, retryability, or identity of a failure.
|
|
||||||
3. Domain and service code must not depend on UI notifications or HTTP response types.
|
|
||||||
4. UI and API layers must not infer error categories by parsing message text.
|
|
||||||
|
|
||||||
### 2.5 State Changes Are Safe
|
|
||||||
|
|
||||||
1. A failed atomic operation must leave persisted state unchanged.
|
|
||||||
2. Batch operations may preserve successful independent items only when partial success is an explicit part of the workflow contract.
|
|
||||||
3. A failed item must retain enough state to identify what was attempted and whether retry is safe.
|
|
||||||
4. Error handling must not overwrite earlier successful results or historical execution evidence.
|
|
||||||
|
|
||||||
### 2.6 Retry Is Explicit and Bounded
|
|
||||||
|
|
||||||
1. Validation, authorization, policy, conflict, and other deterministic failures must not be retried automatically without a relevant input or state change.
|
|
||||||
2. Automatic retry is permitted only for failures classified as transient and only when the operation is idempotent or otherwise protected from duplicate effects.
|
|
||||||
3. Retry count, delay, and terminal behavior must be bounded and observable.
|
|
||||||
4. Exhausted retries must end in a visible terminal failure rather than an indefinitely pending state.
|
|
||||||
|
|
||||||
### 2.7 Diagnostics Are Preserved Safely
|
|
||||||
|
|
||||||
1. Logs and persisted diagnostic evidence must retain enough context to correlate the failure with the affected operation and record.
|
|
||||||
2. Provider and infrastructure failures must preserve safe diagnostic evidence at the boundary where it is available.
|
|
||||||
3. Credentials, authorization headers, cookies, secret values, and unnecessary personal data must not appear in errors, logs, notifications, or exports.
|
|
||||||
4. Diagnostic metadata capture must use explicit safe-field allowlists where unrestricted content could contain secrets.
|
|
||||||
5. User-facing messages must not expose stack traces, local filesystem details, database credentials, or raw internal exceptions.
|
|
||||||
|
|
||||||
AI execution failures also follow the evidence rules in [Digital Evidence and AI Processing Provenance](ai_evidence_and_provenance.md).
|
|
||||||
|
|
||||||
### 2.8 Cancellation and Timeout Are Distinct Outcomes
|
|
||||||
|
|
||||||
1. User cancellation, application shutdown, local timeout, remote timeout, and provider rejection must remain distinguishable.
|
|
||||||
2. Cancellation must not be converted into success or a generic unexpected error.
|
|
||||||
3. Timeout handling must identify whether a provider response was received when that fact is known.
|
|
||||||
4. Cleanup after cancellation or timeout must preserve consistency and must not conceal a completed side effect.
|
|
||||||
|
|
||||||
### 2.9 Logging Must Support Audit Without Becoming the Record
|
|
||||||
|
|
||||||
1. Structured logs must include correlation identity, operation, category, and relevant non-secret record identifiers.
|
|
||||||
2. Expected operator errors may be logged less severely than unexpected defects, but they must remain observable.
|
|
||||||
3. Logs are operational diagnostics and do not replace required database state or archival evidence.
|
|
||||||
4. Duplicate logging of the same failure at every layer should be avoided; ownership of the authoritative log event must be clear.
|
|
||||||
|
|
||||||
## 3. Verification Policy
|
|
||||||
|
|
||||||
Each version must verify:
|
|
||||||
|
|
||||||
1. Every documented error category reaches the intended UI and API representation.
|
|
||||||
2. Failed atomic writes roll back completely.
|
|
||||||
3. Partial-success workflows preserve successful independent results and identify failed items.
|
|
||||||
4. Retry behavior is bounded and restricted to eligible failures.
|
|
||||||
5. Unexpected exceptions retain correlation and causal information without exposing sensitive details.
|
|
||||||
6. Logs, persisted evidence, UI messages, and exports contain no credentials.
|
|
||||||
7. Cancellation, timeout, provider response failure, and no-response failure remain distinguishable.
|
|
||||||
|
|
||||||
## 4. Versioned Ownership
|
|
||||||
|
|
||||||
1. Version-specific error taxonomies, envelopes, HTTP mappings, model fields, and framework behavior belong in the applicable version documentation.
|
|
||||||
2. Each versioned error-handling document must state how it satisfies this invariant.
|
|
||||||
3. A version may add stricter safeguards but must not weaken these principles without first revising this invariant deliberately.
|
|
||||||
4. Implementation and tests must be updated together when a versioned error contract changes.
|
|
||||||
|
|
||||||
## 5. Related Invariants
|
|
||||||
|
|
||||||
- [Historical Document Transcription Design Intent](intent.md)
|
|
||||||
- [Digital Evidence and AI Processing Provenance](ai_evidence_and_provenance.md)
|
|
||||||
- [UI Style Guide](ui_style_guide.md)
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Historical Document Transcription Design Intent
|
|
||||||
I have several thousand pages of family history told through letters, postcards, books, and other documents that I want to transcribe to text.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
1. Preserve our family history
|
|
||||||
2. Unburden my family (and descendants) from having to store and care for the physical media. Once the documents have been transcribed and organized, they can be donated (or kept by a family member that wants to retain and preserve them).
|
|
||||||
3. Make the document text easily available and easily searchable.
|
|
||||||
4. Ability create timelines for individuals and/or families through document dates or the data contained in them. Perhaps even use AI to generate biographies or family histories.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Source material
|
|
||||||
1. **letters, cards, diaries** - handwritten; mostly stored in boxes and tubs with little organization
|
|
||||||
2. **books** - typed or typeset; mostly self-published books 50-100 pages in length. This may be expanded to include selected pages from other publications.
|
|
||||||
3. **photos** - notes written on the backs of photos and the pages of photo albums
|
|
||||||
4. **other ephemera** - newspaper clippings, event programs, invitations, military records, immigration records, etc
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Methodology
|
|
||||||
|
|
||||||
1. Follow current best practices per **A Guide to Documentary Editing** by Mary-Jo Kline. (See [Transcription Methodology](transcription_methodology.md))
|
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# Transcription Methodology & Style Guide
|
|
||||||
|
|
||||||
## 1. Overview & Core Philosophy
|
|
||||||
|
|
||||||
This document defines the formal transcription standard for processing historical manuscripts, letters, diaries, and printed ephemera.
|
|
||||||
|
|
||||||
Following the principles established by Mary-Jo Kline in A Guide to Documentary Editing, this project adheres to a Strict Literal Transcription (Verbatim) model as its foundational layer. The primary goal is total textual fidelity—capturing what the author wrote, not what they intended to write—while ensuring the output remains machine-readable and indexable for downstream digital query and search systems.
|
|
||||||
|
|
||||||
## 2. Textual Policy
|
|
||||||
|
|
||||||
Transcribers (human or AI) must record the exact text of the source document without silent corrections, modernizations, or stylistic smoothing except where explicitly instructed in this guide.
|
|
||||||
|
|
||||||
* **Substantives:** Words, letter forms, structural layout, and semantic content must be recorded strictly as presented in the original document.
|
|
||||||
|
|
||||||
* **Accidentals:** Punctuation, capitalization, misspellings, and archaic character representations must be preserved unless an explicit rule below allows for standardization.
|
|
||||||
|
|
||||||
## 3. Standard Transcription Rules & Markup
|
|
||||||
|
|
||||||
The following rules map directly to editorial conventions for handling common manuscript anomalies and physical document features.
|
|
||||||
|
|
||||||
### 3.1 Textual Anomalies & Corrections
|
|
||||||
|
|
||||||
| Document Feature | Rule | Standard Markup Format | Output Example |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **Misspellings & Errors** | Retain original spelling verbatim. Insert an italicized [sic] immediately following the error. Do not correct spelling silently. | [sic] | The weather was very cold and publick [sic] business delayed. |
|
|
||||||
| **Missing Words / Omissions** | Insert necessary words required to restore basic grammatical sense inside square brackets. | [word] | We went [to] the store to buy supplies. |
|
|
||||||
| **Uncertain / Conjectural** | Place best hypothesis followed by a question mark inside square brackets when handwriting is doubtful. | [word?] | He went to [Boston?] yesterday to meet the governor. |
|
|
||||||
| **Completely Illegible** | Use [illegible] for unreadable script. Use explicit damage descriptors when physical impairment prevents reading. | [illegible] or [reason] | The total cost was [illegible] dollars. or The letter ends here [remainder of page torn]. |
|
|
||||||
| **Canceled / Struck-through** | Wrap text removed by the author inside a [deleted: ...] tag to preserve authorial revisions. | [deleted: text] | We left at [deleted: noon] one o'clock instead. |
|
|
||||||
| **Interlineations / Additions** | Wrap text inserted above, below, or in margins into the narrative flow inside an [inserted: ...] tag. | [inserted: text] | The [inserted: red] house on the hill was abandoned. |
|
|
||||||
|
|
||||||
### 3.2 Typography, Characters & Layout
|
|
||||||
|
|
||||||
| Document Feature | Rule | Standard Markup Format | Output Example |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **Superscripts & Abbreviations** | Bring raised letters down to the main line. Optionally expand abbreviations within square brackets based on project configuration. | [expanded] | Gen^l becomes Genl or Gen[era]l. |
|
|
||||||
| **Line-End Hyphenation** | Rejoin words split across a page or line boundary silently, dropping the soft hyphen. | Silently rejoin | Original: "estab- / lishment" becomes establishment |
|
|
||||||
| **Capitalization** | Preserve explicit capitalization. Default to modern capitalization rules only when authorial intent is ambiguous or archaic forms confuse sentence structure. | Literal / Contextual | If a standard noun like 'Farm' is clearly capitalized, record 'Farm'. If ambiguous, default to 'farm'. |
|
|
||||||
| **Hierarchical Outlines** | Preserve exact numbering characters (including lowercase Roman numerals or terminal 'j'). Replicate indentation levels using standard spacing. Do not correct sequence or mathematical errors. | Preserve syntax | I. Main Topic a. Sub-point b. Next pointIII. [sic] Third Topic |
|
|
||||||
|
|
||||||
### 3.3 Visual & Spatial Elements
|
|
||||||
|
|
||||||
| Document Feature | Rule | Standard Markup Format | Output Example |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **Non-Textual Artifacts** | Record non-textual elements (seals, stamps, sketches, physical damage) using brief descriptive text inside square brackets. | [description] | [wax notary seal attached here] or [sketch of a fort layout] |
|
|
||||||
| **Marginalia & Addenda** | Explicitly indicate spatial transitions before transcribing content located in margins or non-standard orientations. | [location:] | [written in left margin:] Do not share this with anyone. |
|
|
||||||
|
|
||||||
### 3.4 Document-Body Medium
|
|
||||||
|
|
||||||
Every transcript must identify the predominant document-body medium exactly once at the beginning:
|
|
||||||
|
|
||||||
| Medium | Use | Standard Markup |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| **Handwritten** | The main body was written by hand. | `[document body handwritten]` |
|
|
||||||
| **Typewritten** | The main body was produced with a typewriter. Uneven impressions, monospaced characters, and mechanical defects remain typewritten rather than handwritten. | `[document body typewritten]` |
|
|
||||||
| **Typeset** | The main body was composed for printing or produced as printed text rather than with a typewriter. | `[document body typeset]` |
|
|
||||||
| **Mixed** | No single medium predominates, or handwritten and printed/typewritten content are structurally interleaved. | `[document body mixed]` |
|
|
||||||
|
|
||||||
- Use exactly one document-body marker.
|
|
||||||
- Do not wrap each line in `[handwritten: ...]` after declaring the body handwritten.
|
|
||||||
- In typewritten or typeset documents, use localized handwriting markers only for genuinely handwritten annotations, insertions, or signatures.
|
|
||||||
- In mixed documents, identify handwritten portions locally while preserving their reading context.
|
|
||||||
- Preserve tables of contents, tables, forms, columns, captions, marginalia, page numbers, dotted leaders, and associated references in their logical reading order.
|
|
||||||
- Produce plain text characters rather than HTML entities for ordinary transcription content.
|
|
||||||
|
|
||||||
## 4. Prompt Asset Integration
|
|
||||||
|
|
||||||
When executing programmatic transcriptions via LLM APIs or local models, processing instructions must be packaged into single-purpose system prompts aligned with these rules.
|
|
||||||
|
|
||||||
1. **Isolation:** Each transcription prompt file exists as an independent Markdown asset in the repository.
|
|
||||||
2. **Deterministic Output:** Prompts must explicitly instruct models to follow the markup standards in Section 3 without introducing conversational wrappers, extra prose, or structural markdown outside the source document's native layout.
|
|
||||||
3. **Iterative Scoping:** Rule modifications or edge-case additions must be submitted as isolated delta commits to individual prompt files to maintain clean revision tracking.
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# UI Style Guide (Invariant)
|
|
||||||
|
|
||||||
## 1. Purpose
|
|
||||||
This guide defines non-negotiable UI styling rules for the transcription application.
|
|
||||||
|
|
||||||
The design system is token-first and class-driven:
|
|
||||||
1. Theme tokens are defined in [src/transcription/ui/static/theme.css](src/transcription/ui/static/theme.css).
|
|
||||||
2. Python UI code composes semantic classes instead of inline color values.
|
|
||||||
3. Pages and components should share a single visual language across Documents, Jobs, People, and Sources flows.
|
|
||||||
|
|
||||||
## 2. Source of Truth
|
|
||||||
Use these files as the style authority:
|
|
||||||
1. [src/transcription/ui/static/theme.css](src/transcription/ui/static/theme.css) for color tokens, semantic utility classes, table styles, and viewer surfaces.
|
|
||||||
2. [src/transcription/ui/theme.py](src/transcription/ui/theme.py) for runtime NiceGUI theme bridge and shared UI helpers.
|
|
||||||
|
|
||||||
If this document conflicts with implementation, update this document to match the code immediately after intentional style changes.
|
|
||||||
|
|
||||||
## 3. Core Design Invariants
|
|
||||||
1. Flat, high-density surfaces over decorative depth.
|
|
||||||
2. Strong content hierarchy with subdued backgrounds and border-based separation.
|
|
||||||
3. Viewer area remains the highest contrast region in image/transcription workflows.
|
|
||||||
4. Primary actions are consistent and visually recognizable.
|
|
||||||
5. Accessible focus rings are always visible for keyboard users.
|
|
||||||
|
|
||||||
## 4. Token System
|
|
||||||
|
|
||||||
### 4.1 Palette Tokens
|
|
||||||
Base palette variables live under :root in [src/transcription/ui/static/theme.css](src/transcription/ui/static/theme.css):
|
|
||||||
1. --palette-carbon-black: #1c2321
|
|
||||||
2. --palette-cool-steel: #7d98a1
|
|
||||||
3. --palette-blue-slate: #5e6572
|
|
||||||
4. --palette-powder-blue: #a9b4c2
|
|
||||||
5. --palette-platinum: #eef1ef
|
|
||||||
|
|
||||||
### 4.2 Semantic Theme Tokens
|
|
||||||
Do not style components directly with palette tokens when a semantic token exists.
|
|
||||||
|
|
||||||
Semantic tokens currently include:
|
|
||||||
1. --theme-text and --theme-text-muted
|
|
||||||
2. --theme-page, --theme-surface, --theme-surface-raised, --theme-surface-muted
|
|
||||||
3. --theme-border
|
|
||||||
4. --theme-primary and --theme-primary-hover
|
|
||||||
5. --theme-secondary and --theme-focus
|
|
||||||
6. --theme-inverse-text
|
|
||||||
7. --theme-viewer, --theme-viewer-border, --theme-viewer-muted
|
|
||||||
|
|
||||||
## 5. Approved Semantic Classes
|
|
||||||
|
|
||||||
### 5.1 Text and Background
|
|
||||||
1. ui-text-primary
|
|
||||||
2. ui-text-muted
|
|
||||||
3. ui-text-inverse
|
|
||||||
4. ui-bg-page
|
|
||||||
5. ui-bg-surface
|
|
||||||
6. ui-bg-surface-raised
|
|
||||||
7. ui-bg-surface-muted
|
|
||||||
8. ui-bg-viewer
|
|
||||||
9. ui-bg-viewer-overlay
|
|
||||||
10. ui-bg-viewer-overlay-soft
|
|
||||||
|
|
||||||
### 5.2 Borders and Surfaces
|
|
||||||
1. ui-border-subtle
|
|
||||||
2. ui-border-viewer
|
|
||||||
3. ui-header-divider
|
|
||||||
4. ui-card-surface
|
|
||||||
5. ui-row-surface
|
|
||||||
6. ui-note-box
|
|
||||||
7. ui-card-error
|
|
||||||
|
|
||||||
### 5.3 Interactive Elements
|
|
||||||
1. ui-btn-primary
|
|
||||||
2. ui-btn-secondary
|
|
||||||
3. ui-link-primary
|
|
||||||
4. ui-text-accent
|
|
||||||
5. ui-chip-primary
|
|
||||||
6. ui-badge-secondary
|
|
||||||
7. ui-status and ui-status--<status>
|
|
||||||
|
|
||||||
### 5.4 Table Patterns
|
|
||||||
1. ui-table
|
|
||||||
2. ui-table-header
|
|
||||||
3. ui-table-body
|
|
||||||
|
|
||||||
Use existing class combinations from [src/transcription/ui/components](src/transcription/ui/components) and [src/transcription/ui/pages](src/transcription/ui/pages) as reference implementations.
|
|
||||||
|
|
||||||
## 6. Legacy Class Policy
|
|
||||||
Legacy `vibe-` presentation classes are prohibited. Use `ui-` semantic classes from `theme.css`.
|
|
||||||
|
|
||||||
## 7. Prohibited Patterns
|
|
||||||
1. Inline hex colors in Python UI class strings or style blocks, except in isolated bridge code explicitly marked for migration.
|
|
||||||
2. Ad-hoc one-off class names that duplicate existing semantic class intent.
|
|
||||||
3. Page-specific palette forks that bypass theme tokens.
|
|
||||||
4. Hidden or low-contrast focus states on interactive controls.
|
|
||||||
5. Embedded `<style>` blocks or NiceGUI `.style(...)` calls in Python UI code.
|
|
||||||
6. Additional page- or component-specific stylesheets; `theme.css` is the single CSS source.
|
|
||||||
|
|
||||||
## 8. Implementation Rules For Contributors
|
|
||||||
1. Prefer composing existing semantic classes before creating new ones.
|
|
||||||
2. If a new class is required, add it to [src/transcription/ui/static/theme.css](src/transcription/ui/static/theme.css) with a semantic name, then reuse it.
|
|
||||||
3. Keep behavior ownership in Python and appearance ownership in CSS.
|
|
||||||
4. Update UI tests that assert exact text or labels when intentional copy changes are made.
|
|
||||||
5. Avoid introducing class churn unrelated to the feature being changed.
|
|
||||||
|
|
||||||
## 9. Verification Checklist
|
|
||||||
Before merging UI changes, verify:
|
|
||||||
1. No new inline hex colors were introduced in UI pages/components.
|
|
||||||
2. New styles are token-backed and added to [src/transcription/ui/static/theme.css](src/transcription/ui/static/theme.css).
|
|
||||||
3. Primary buttons, links, cards, and tables still render with consistent semantics.
|
|
||||||
4. Keyboard focus ring visibility is preserved.
|
|
||||||
5. Relevant UI and integration tests pass.
|
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
# Step 1 Implementation Plan: `config.py` + `models.py` + `db.py`
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Establish the foundational data layer and configuration system that every subsequent MVP step builds on. At the end of this step, the project has a runnable Python package with a validated schema, typed configuration, and a test suite proving the data layer works — before any UI, worker, or AI provider code exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Prerequisite: Project Structure Scaffolding
|
||||||
|
|
||||||
|
Before writing any logic, create the package skeleton so imports work correctly.
|
||||||
|
|
||||||
|
### Files to create (empty `__init__.py` stubs)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
└── transcription/
|
||||||
|
├── __init__.py
|
||||||
|
├── providers/
|
||||||
|
│ └── __init__.py
|
||||||
|
├── services/
|
||||||
|
│ └── __init__.py
|
||||||
|
└── ui/
|
||||||
|
└── __init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to create (with logic — the Step 1 deliverables)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/transcription/config.py
|
||||||
|
src/transcription/models.py
|
||||||
|
src/transcription/db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test files to create
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── __init__.py
|
||||||
|
├── conftest.py
|
||||||
|
├── test_config.py
|
||||||
|
├── test_models.py
|
||||||
|
└── test_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update `pyproject.toml`
|
||||||
|
|
||||||
|
Add the dependencies that Step 1 requires and won't change later:
|
||||||
|
|
||||||
|
```toml pyproject.toml
|
||||||
|
[project]
|
||||||
|
name = "transcription"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Historical document transcription system"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"openrouter>=0.7.0",
|
||||||
|
"pydantic>=2.13.4",
|
||||||
|
"pydantic-settings>=2.9.1",
|
||||||
|
"sqlmodel>=0.0.25",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.25",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "--strict-markers -q"
|
||||||
|
markers = [
|
||||||
|
"unit: pure logic tests with no external dependencies",
|
||||||
|
"integration: tests that touch framework or database contracts",
|
||||||
|
"external: tests that call external services (slow, requires credentials)",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Key additions:
|
||||||
|
- **`openrouter`** — official OpenRouter Python SDK used for model calls
|
||||||
|
- **`pydantic-settings`** — for `BaseSettings` with env-var loading (this was split out of `pydantic` core in v2)
|
||||||
|
- **`sqlmodel`** — provides SQLModel (which bundles SQLAlchemy + Pydantic model integration) and the SQLite driver
|
||||||
|
- **`pytest` + `pytest-asyncio`** — in `dev` extras for test execution
|
||||||
|
- **`[tool.pytest.ini_options]`** — strict marker checking enabled from the start; markers registered upfront per pytesting skill conventions
|
||||||
|
|
||||||
|
### Delete `hello.py`
|
||||||
|
|
||||||
|
The placeholder file is no longer needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `config.py` — Centralized Configuration
|
||||||
|
|
||||||
|
**Satisfies:** REQ-8 (centralized config and logging at startup)
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Use `pydantic-settings` `BaseSettings` | Type-safe, validates on construction, loads from env vars and `.env` files automatically |
|
||||||
|
| `PROVIDER` constrained to `openrouter` for MVP | Keeps configuration explicit while avoiding premature multi-provider complexity |
|
||||||
|
| `OPENROUTER_API_KEY` required | Matches official SDK docs and avoids ambiguous provider-agnostic naming |
|
||||||
|
| `PROVIDER_MODEL` defaults to `None` | OpenRouter adapter (Step 3) supplies a sensible default when `None` |
|
||||||
|
| `OPENROUTER_HTTP_REFERER` and `OPENROUTER_APP_TITLE` optional | Matches SDK optional app-attribution fields |
|
||||||
|
| `DATABASE_URL` defaults to SQLite | Zero-setup local development; PostgreSQL swap is a single env-var change post-MVP |
|
||||||
|
| `UPLOAD_DIR` and `PROMPT_DIR` as `Path` objects | Enables `.mkdir(parents=True, exist_ok=True)` and path validation at startup |
|
||||||
|
| Logging configured via `logging.config.dictConfig` in `setup_logging()` | Centralized, explicit formatter/handler/root logger topology; called once at startup with `disable_existing_loggers=False` |
|
||||||
|
|
||||||
|
### Proposed Implementation
|
||||||
|
|
||||||
|
```python src/transcription/config.py
|
||||||
|
"""Centralized application configuration.
|
||||||
|
|
||||||
|
All settings are loaded from environment variables (or a .env file)
|
||||||
|
once at startup. Provider-specific defaults (model names, base URLs)
|
||||||
|
are resolved by the provider adapters, not here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(StrEnum):
|
||||||
|
OPENROUTER = "openrouter"
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- AI provider ---
|
||||||
|
provider: Provider = Provider.OPENROUTER
|
||||||
|
openrouter_api_key: str
|
||||||
|
provider_model: str | None = None
|
||||||
|
openrouter_http_referer: str | None = None
|
||||||
|
openrouter_app_title: str | None = None
|
||||||
|
|
||||||
|
# --- persistence ---
|
||||||
|
database_url: str = "sqlite:///./transcription.db"
|
||||||
|
|
||||||
|
# --- filesystem paths ---
|
||||||
|
upload_dir: Path = Path("./uploads")
|
||||||
|
prompt_dir: Path = Path("./prompts")
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING_CONFIG: dict[str, object] = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"standard": {
|
||||||
|
"format": "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
||||||
|
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "standard",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
"""Return the singleton Settings instance.
|
||||||
|
|
||||||
|
Cached so the entire application shares one validated config.
|
||||||
|
"""
|
||||||
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging() -> None:
|
||||||
|
"""Configure root logging once at startup."""
|
||||||
|
logging.config.dictConfig(LOGGING_CONFIG)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Behaviors
|
||||||
|
|
||||||
|
- **Startup validation**: If `OPENROUTER_API_KEY` is missing from the environment, `Settings()` raises a `ValidationError` immediately — the app won't start with a missing key.
|
||||||
|
- **`.env` support**: Developers can create a `.env` file in the project root for local keys; it's never committed (already covered by the existing `.gitignore` pattern or a new entry).
|
||||||
|
- **`extra="ignore"`**: Unknown env vars don't cause errors, keeping the config resilient to unrelated environment variables.
|
||||||
|
- **`lru_cache`**: `get_settings()` is the single access point. All modules import and call this function rather than constructing `Settings` directly.
|
||||||
|
- **Centralized logging**: `setup_logging()` calls `dictConfig` exactly once at startup; all modules should use `logging.getLogger(__name__)` and avoid `basicConfig`.
|
||||||
|
|
||||||
|
### `.env` template (not committed — add to `.gitignore`)
|
||||||
|
|
||||||
|
```bash .env.example
|
||||||
|
PROVIDER=openrouter
|
||||||
|
OPENROUTER_API_KEY=sk-or-...
|
||||||
|
# PROVIDER_MODEL= # optional: OpenRouter adapter supplies default
|
||||||
|
# OPENROUTER_HTTP_REFERER=https://example.com
|
||||||
|
# OPENROUTER_APP_TITLE=Historical Transcription MVP
|
||||||
|
# DATABASE_URL=sqlite:///./transcription.db
|
||||||
|
# UPLOAD_DIR=./uploads
|
||||||
|
# PROMPT_DIR=./prompts
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.gitignore` addition
|
||||||
|
|
||||||
|
```gitignore .gitignore
|
||||||
|
# ... existing entries ...
|
||||||
|
|
||||||
|
# Environment secrets
|
||||||
|
.env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `models.py` — SQLModel Domain Models
|
||||||
|
|
||||||
|
**Satisfies:** REQ-3 (persist and expose job states), REQ-4 (persist transcription output and failure details)
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Three models: `Document`, `Job`, `Transcript` | Minimal set from MVP Feature 5. One-to-many from Document→Job and one-to-one from Job→Transcript |
|
||||||
|
| `JobStatus` as a `StrEnum` | Readable in the database (`"queued"` not `1`), type-safe in Python, trivially serializable to JSON for the UI |
|
||||||
|
| Status values: `queued`, `processing`, `transcribed`, `failed` | Matches MVP Feature 2 lifecycle. REQ-3 also lists `upload` and `completed` — these are deferred to post-MVP when revision/review workflows exist |
|
||||||
|
| UUIDs for primary keys | Avoids auto-increment collision concerns if we later move to PostgreSQL; safe for distributed ID generation; `uuid4` is simple |
|
||||||
|
| `uploaded_at`, `created_at`, `updated_at` as UTC `datetime` | Timezone-naive UTC by convention for MVP. Sufficient for single-user, single-timezone operation |
|
||||||
|
| `Transcript.text` is nullable | A failed job creates a Transcript with `text=None` and `error_detail` populated, keeping the query model uniform |
|
||||||
|
| Relationships via SQLModel `Relationship` | Enables `document.jobs` and `job.transcript` navigation in service code without manual joins |
|
||||||
|
|
||||||
|
### Proposed Implementation
|
||||||
|
|
||||||
|
- `resource://skills/fastapi-async-sqlalchemy-modernization/document`
|
||||||
|
|
||||||
|
```python src/transcription/models.py
|
||||||
|
"""SQLModel domain models for the transcription system.
|
||||||
|
|
||||||
|
Three models capture the MVP lifecycle:
|
||||||
|
Document → one-to-many → Job → one-to-one → Transcript
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from enum import StrEnum
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class JobStatus(StrEnum):
|
||||||
|
QUEUED = "queued"
|
||||||
|
PROCESSING = "processing"
|
||||||
|
TRANSCRIBED = "transcribed"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class Document(SQLModel, table=True):
|
||||||
|
"""An uploaded document image."""
|
||||||
|
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
|
filename: str
|
||||||
|
file_path: str
|
||||||
|
uploaded_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- relationships ---
|
||||||
|
jobs: list["Job"] = Relationship(back_populates="document")
|
||||||
|
|
||||||
|
|
||||||
|
class Job(SQLModel, table=True):
|
||||||
|
"""A transcription job tied to a single document."""
|
||||||
|
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
|
document_id: UUID = Field(foreign_key="document.id")
|
||||||
|
status: JobStatus = Field(default=JobStatus.QUEUED)
|
||||||
|
created_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
updated_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- relationships ---
|
||||||
|
document: Document = Relationship(back_populates="jobs")
|
||||||
|
transcript: "Transcript | None" = Relationship(back_populates="job")
|
||||||
|
|
||||||
|
|
||||||
|
class Transcript(SQLModel, table=True):
|
||||||
|
"""The output of a transcription job."""
|
||||||
|
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
|
job_id: UUID = Field(foreign_key="job.id", unique=True)
|
||||||
|
text: str | None = None
|
||||||
|
error_detail: str | None = None
|
||||||
|
created_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- relationships ---
|
||||||
|
job: Job = Relationship(back_populates="transcript")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity-Relationship Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐ ┌──────────┐ ┌─────────────┐
|
||||||
|
│ Document │ 1───* │ Job │ 1───1 │ Transcript │
|
||||||
|
├──────────┤ ├──────────┤ ├─────────────┤
|
||||||
|
│ id (PK) │ │ id (PK) │ │ id (PK) │
|
||||||
|
│ filename │ │ doc_id │──FK──▶│ job_id (FK) │
|
||||||
|
│ file_path│ │ status │ │ text │
|
||||||
|
│ uploaded │ │ created │ │ error_detail│
|
||||||
|
│ │ │ updated │ │ created │
|
||||||
|
└──────────┘ └──────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why Only Four Status Values
|
||||||
|
|
||||||
|
REQ-3 lists six states: `upload`, `queued`, `processing`, `transcribed`, `failed`, `completed`. The MVP simplifies this:
|
||||||
|
|
||||||
|
| REQ-3 State | MVP Treatment |
|
||||||
|
|-------------|---------------|
|
||||||
|
| `upload` | Implicit — the Document record exists before a Job is created. No separate job state needed. |
|
||||||
|
| `queued` | ✅ Included — job created, waiting for worker pickup |
|
||||||
|
| `processing` | ✅ Included — worker is actively transcribing |
|
||||||
|
| `transcribed` | ✅ Included — AI output received and stored |
|
||||||
|
| `failed` | ✅ Included — error captured |
|
||||||
|
| `completed` | Deferred — implies human review/acceptance. In MVP, `transcribed` is the terminal success state. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `db.py` — Database Engine and Session Management
|
||||||
|
|
||||||
|
**Satisfies:** MVP Feature 5 (SQLite auto-created on first startup)
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Module-level `create_engine` + `Session` factory | REQ-7 (lifespan-owned resources) is deferred. A module-level engine is adequate for MVP's single-process, single-user operation |
|
||||||
|
| `create_all()` as an explicit function | Called at app startup. MVP auto-creates tables (REQ-10 deferred), but the function is isolated so it's easy to gate behind a flag later |
|
||||||
|
| `get_session()` as a generator | Standard FastAPI/SQLModel pattern — yields a session, ensures cleanup. Compatible with `Depends()` when the API layer arrives in Step 5 |
|
||||||
|
| `echo=False` default | Keeps logs clean. Can be toggled for debugging |
|
||||||
|
|
||||||
|
### Proposed Implementation
|
||||||
|
|
||||||
|
```python src/transcription/db.py
|
||||||
|
"""Database engine, session factory, and schema bootstrap.
|
||||||
|
|
||||||
|
MVP uses SQLite with auto-create-tables at startup.
|
||||||
|
PostgreSQL migration is a post-MVP configuration change.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
|
|
||||||
|
from transcription.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def _build_engine():
|
||||||
|
settings = get_settings()
|
||||||
|
connect_args = {}
|
||||||
|
if settings.database_url.startswith("sqlite"):
|
||||||
|
connect_args["check_same_thread"] = False
|
||||||
|
return create_engine(
|
||||||
|
settings.database_url,
|
||||||
|
echo=False,
|
||||||
|
connect_args=connect_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
engine = _build_engine()
|
||||||
|
|
||||||
|
|
||||||
|
def create_all() -> None:
|
||||||
|
"""Create all tables. Called once at application startup."""
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def get_session() -> Generator[Session]:
|
||||||
|
"""Yield a database session and ensure cleanup."""
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite-Specific Note
|
||||||
|
|
||||||
|
`check_same_thread=False` is required for SQLite when the session may be accessed from different threads (e.g., a background worker on a different thread than the request handler). This setting is harmless and ignored for PostgreSQL connection strings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Test Plan
|
||||||
|
|
||||||
|
Refer to these resources for rules and guidelines about structure:
|
||||||
|
|
||||||
|
- `resource://skills/pytesting/document`
|
||||||
|
- `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
- `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
|
||||||
|
Hierarchy pattern used in this step:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/
|
||||||
|
conftest.py
|
||||||
|
test_config.py
|
||||||
|
TestSettingsLoading
|
||||||
|
test_loads_from_env
|
||||||
|
test_requires_api_key
|
||||||
|
TestProviderSettings
|
||||||
|
test_defaults_to_openrouter
|
||||||
|
test_rejects_invalid_value
|
||||||
|
test_optional_fields_default_to_none
|
||||||
|
TestPathSettings
|
||||||
|
test_path_fields_are_path_objects
|
||||||
|
test_models.py
|
||||||
|
TestDocumentModel
|
||||||
|
test_can_be_persisted
|
||||||
|
test_defaults_are_populated
|
||||||
|
TestJobModel
|
||||||
|
test_can_be_created_for_document
|
||||||
|
test_defaults_are_populated
|
||||||
|
test_transitions_to_transcribed
|
||||||
|
test_transitions_to_failed
|
||||||
|
TestTranscriptModel
|
||||||
|
test_success_record_persists
|
||||||
|
test_failure_record_persists
|
||||||
|
test_job_id_is_unique
|
||||||
|
TestRelationships
|
||||||
|
test_document_exposes_jobs
|
||||||
|
test_job_exposes_transcript
|
||||||
|
test_db.py
|
||||||
|
TestSchemaBootstrap
|
||||||
|
test_create_all_creates_expected_tables
|
||||||
|
TestSessionFactory
|
||||||
|
test_get_session_yields_session
|
||||||
|
test_session_is_closed_after_generator_exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tests/conftest.py` — Shared Fixtures
|
||||||
|
|
||||||
|
```python tests/conftest.py
|
||||||
|
"""Shared test fixtures.
|
||||||
|
|
||||||
|
Every test gets a fresh in-memory SQLite database so tests are
|
||||||
|
isolated, fast, and leave no artifacts on disk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlmodel import Session, SQLModel, create_engine
|
||||||
|
from sqlmodel.pool import StaticPool
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def session():
|
||||||
|
"""Provide a clean database session for each test."""
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite://",
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
poolclass=StaticPool,
|
||||||
|
)
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
```
|
||||||
|
|
||||||
|
`StaticPool` ensures a single in-memory SQLite connection is shared across threads, which is required when `TestClient` (Step 5) spawns threads that would otherwise get separate in-memory databases. Establishing it now keeps the fixture stable across all future steps.
|
||||||
|
|
||||||
|
### `tests/test_config.py` — Configuration Hierarchy
|
||||||
|
|
||||||
|
| Class | Method | What It Verifies |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| `TestSettingsLoading` | `test_loads_from_env` | `Settings` constructs successfully when `OPENROUTER_API_KEY` is set via env var |
|
||||||
|
| `TestSettingsLoading` | `test_requires_api_key` | `Settings()` raises `ValidationError` when `OPENROUTER_API_KEY` is missing |
|
||||||
|
| `TestProviderSettings` | `test_defaults_to_openrouter` | Default provider is `openrouter` when not explicitly set |
|
||||||
|
| `TestProviderSettings` | `test_rejects_invalid_value` | Setting `PROVIDER=invalid` raises `ValidationError` |
|
||||||
|
| `TestProviderSettings` | `test_optional_fields_default_to_none` | `provider_model`, `openrouter_http_referer`, and `openrouter_app_title` are `None` when unset |
|
||||||
|
| `TestPathSettings` | `test_path_fields_are_path_objects` | `upload_dir` and `prompt_dir` are `Path` instances |
|
||||||
|
|
||||||
|
### `tests/test_models.py` — Model & Relationship Hierarchy
|
||||||
|
|
||||||
|
| Class | Method | What It Verifies |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| `TestDocumentModel` | `test_can_be_persisted` | A `Document` can be persisted and read back with correct fields |
|
||||||
|
| `TestDocumentModel` | `test_defaults_are_populated` | `id` is auto-generated UUID, `uploaded_at` is populated |
|
||||||
|
| `TestJobModel` | `test_can_be_created_for_document` | A `Job` linked to a `Document` via FK persists correctly |
|
||||||
|
| `TestJobModel` | `test_defaults_are_populated` | Default status is `queued`, `created_at` and `updated_at` are populated |
|
||||||
|
| `TestJobModel` | `test_transitions_to_transcribed` | Status can be updated from `queued` → `processing` → `transcribed` |
|
||||||
|
| `TestJobModel` | `test_transitions_to_failed` | Status can be updated from `processing` → `failed` |
|
||||||
|
| `TestTranscriptModel` | `test_success_record_persists` | A `Transcript` with `text` set and `error_detail=None` persists correctly |
|
||||||
|
| `TestTranscriptModel` | `test_failure_record_persists` | A `Transcript` with `text=None` and `error_detail` set persists correctly |
|
||||||
|
| `TestRelationships` | `test_document_exposes_jobs` | `document.jobs` returns the linked `Job` list |
|
||||||
|
| `TestRelationships` | `test_job_exposes_transcript` | `job.transcript` returns the linked `Transcript` |
|
||||||
|
| `TestTranscriptModel` | `test_job_id_is_unique` | Inserting two transcripts with the same `job_id` raises an integrity error |
|
||||||
|
|
||||||
|
### `tests/test_db.py` — Database Bootstrap Hierarchy
|
||||||
|
|
||||||
|
| Class | Method | What It Verifies |
|
||||||
|
|------|--------|------------------|
|
||||||
|
| `TestSchemaBootstrap` | `test_create_all_creates_expected_tables` | After `create_all()`, the expected tables (`document`, `job`, `transcript`) exist in the database |
|
||||||
|
| `TestSessionFactory` | `test_get_session_yields_session` | `get_session()` yields a usable `Session` object |
|
||||||
|
| `TestSessionFactory` | `test_session_is_closed_after_generator_exit` | After the generator is exhausted, the session is closed |
|
||||||
|
|
||||||
|
### Marker Strategy (Step 1)
|
||||||
|
|
||||||
|
- Markers (`unit`, `integration`, `external`) are registered upfront in `pyproject.toml` with `--strict-markers` enabled, per pytesting skill conventions.
|
||||||
|
- All Step 1 tests are unmarked — they run in the default lane since they are fast, deterministic, and have no external dependencies.
|
||||||
|
- When slower integration or external tests are introduced in later steps, apply explicit markers and keep test names unchanged.
|
||||||
|
|
||||||
|
### Test Workflow
|
||||||
|
|
||||||
|
Follow the two-phase approach from `resource://catalog/prompts/pytest-scaffold` and `resource://catalog/prompts/pytest-fill-scaffold`:
|
||||||
|
|
||||||
|
1. **Scaffold phase**: Create test files with class hierarchy, method names, and one-line docstrings only. Validate collection:
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
2. **Fill phase**: Implement assertions, fixtures, and minimal test data. Treat scaffolded names and docstrings as locked. Validate execution:
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
Scaffolded structure is treated as a stable baseline — do not rename, move, merge, split, or re-nest tests once the scaffold is reviewed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Step 1 Completion Checklist
|
||||||
|
|
||||||
|
When all of the following are true, Step 1 is done and Step 2 can begin:
|
||||||
|
|
||||||
|
| # | Criterion | How to Verify |
|
||||||
|
|---|-----------|---------------|
|
||||||
|
| 1 | `src/transcription/` package exists with `config.py`, `models.py`, `db.py` | `ls` / file inspection |
|
||||||
|
| 2 | Empty `__init__.py` stubs exist for `providers/`, `services/`, `ui/` | `ls` / file inspection |
|
||||||
|
| 3 | `Settings` loads from environment and validates `OPENROUTER_API_KEY` is present | `test_config.py` passes |
|
||||||
|
| 4 | `Document`, `Job`, `Transcript` models create tables in SQLite | `test_models.py` passes |
|
||||||
|
| 5 | `JobStatus` enum has exactly four values: `queued`, `processing`, `transcribed`, `failed` | `test_models.py` passes |
|
||||||
|
| 6 | Foreign key relationships work: Document→Job→Transcript | `test_models.py` passes |
|
||||||
|
| 7 | `create_all()` bootstraps the schema; `get_session()` yields a working session | `test_db.py` passes |
|
||||||
|
| 8 | All tests pass: `uv run pytest -q` | CI / local run |
|
||||||
|
| 9 | `hello.py` is deleted | File inspection |
|
||||||
|
| 10 | `pyproject.toml` includes `openrouter`, `sqlmodel`, `pydantic-settings`, `pytest`, `pytest-asyncio` | File inspection |
|
||||||
|
| 10a | `pyproject.toml` has `[tool.pytest.ini_options]` with `--strict-markers` and registered markers | File inspection |
|
||||||
|
| 11 | `.env.example` documents all config vars; `.env` is in `.gitignore` | File inspection |
|
||||||
|
| 12 | `setup_logging()` uses `logging.config.dictConfig` with centralized formatter/handler/root config | File inspection |
|
||||||
|
| 13 | `uv run pytest --collect-only -q` shows expected test hierarchy | Local run |
|
||||||
|
| 14 | `uv run pytest -q` passes all tests | Local run |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. What This Step Does NOT Include
|
||||||
|
|
||||||
|
Explicitly out of scope to prevent scope creep:
|
||||||
|
|
||||||
|
| Excluded | Reason |
|
||||||
|
|----------|--------|
|
||||||
|
| FastAPI / NiceGUI app entrypoint | Step 5 |
|
||||||
|
| Additional provider adapters beyond OpenRouter | Post-MVP |
|
||||||
|
| Upload service logic | Step 4 |
|
||||||
|
| Worker / background processing | Step 4 |
|
||||||
|
| Transcription prompt files | Step 2 |
|
||||||
|
| Alembic or migration tooling | Post-MVP (REQ-10 deferred) |
|
||||||
|
| Async session factory | Post-MVP (REQ-7 deferred) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This plan produces a fully tested, importable data foundation. Every subsequent step imports from `transcription.config`, `transcription.models`, and `transcription.db` without modification.
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
## Step 2: prompts/transcribe_document.md
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Implement the MVP prompt artifact system by creating a curated transcription prompt file:
|
||||||
|
|
||||||
|
- `prompts/transcribe_document.md`
|
||||||
|
|
||||||
|
This step primarily satisfies:
|
||||||
|
|
||||||
|
- **REQ-12**: prompts stored as individual Markdown artifacts
|
||||||
|
- MVP Feature 3: prompt-driven verbatim transcription behavior grounded in `docs/intent.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 2
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
1. Create prompt artifact directory and first prompt file.
|
||||||
|
2. Encode transcription rules from `docs/intent.md` into a model-facing prompt.
|
||||||
|
3. Define stable prompt structure so future revisions are easy to diff/review.
|
||||||
|
4. Add lightweight tests that validate artifact presence and baseline quality constraints.
|
||||||
|
5. Update docs/README references so Step 3 can consume prompt file directly.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Provider integration logic (Step 3)
|
||||||
|
- Worker/job orchestration (Step 4)
|
||||||
|
- UI behavior (Step 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Deliverables
|
||||||
|
|
||||||
|
1. **`prompts/transcribe_document.md`**
|
||||||
|
- production prompt text for historical document transcription
|
||||||
|
|
||||||
|
2. **`prompts/README.md`** (recommended)
|
||||||
|
- conventions for prompt files, revision policy, naming
|
||||||
|
|
||||||
|
3. **`tests/test_prompts.py`** (recommended)
|
||||||
|
- artifact existence + structure checks
|
||||||
|
|
||||||
|
4. **Small docs update** (README or docs reference)
|
||||||
|
- indicate that prompts are file-based and loaded from `PROMPT_DIR`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Work Breakdown
|
||||||
|
|
||||||
|
### 1) Create prompt artifact folder and canonical file
|
||||||
|
- Add `prompts/` at repo root.
|
||||||
|
- Add `transcribe_document.md` as the first curated artifact.
|
||||||
|
- Keep filename stable; this becomes the default in Step 3 unless overridden.
|
||||||
|
|
||||||
|
### 2) Author prompt content using a strict, sectioned format
|
||||||
|
Use section headers so future diffs are clean and policy changes are isolated.
|
||||||
|
|
||||||
|
Suggested sections:
|
||||||
|
|
||||||
|
1. **Purpose**
|
||||||
|
- verbatim scholarly transcription of historical documents
|
||||||
|
|
||||||
|
2. **Output requirements**
|
||||||
|
- plain text only
|
||||||
|
- no summaries, no paraphrasing
|
||||||
|
- preserve reading order and meaningful structure
|
||||||
|
|
||||||
|
3. **Core fidelity rules**
|
||||||
|
- preserve original wording and punctuation
|
||||||
|
- don’t silently normalize grammar/spelling
|
||||||
|
- no invented content
|
||||||
|
|
||||||
|
4. **Issue-handling rules (mapped from Intent table)**
|
||||||
|
- misspellings with `[sic]`
|
||||||
|
- missing words with `[word]`
|
||||||
|
- uncertainty with `[guess?]`
|
||||||
|
- illegible with `[illegible]` / reason tags
|
||||||
|
- crossed-out text as `[deleted: ...]`
|
||||||
|
- inserted text as `[inserted: ...]`
|
||||||
|
- superscripts handling guidance
|
||||||
|
- non-text elements as `[description]`
|
||||||
|
- marginalia format `[written in left margin: ...]`
|
||||||
|
- line-break hyphen rejoin behavior
|
||||||
|
- capitalization policy
|
||||||
|
- hierarchical outline preservation (including unusual numbering)
|
||||||
|
|
||||||
|
5. **Confidence/ambiguity policy**
|
||||||
|
- prefer explicit uncertainty markers over hallucination
|
||||||
|
|
||||||
|
6. **Final self-checklist for model**
|
||||||
|
- did I preserve structure?
|
||||||
|
- did I mark uncertain text?
|
||||||
|
- did I avoid silent corrections?
|
||||||
|
|
||||||
|
### 3) Add prompt-library conventions (`prompts/README.md`)
|
||||||
|
Recommended conventions:
|
||||||
|
- one prompt per file
|
||||||
|
- snake_case names
|
||||||
|
- each file starts with purpose + behavior contract
|
||||||
|
- iterative edits, one prompt per PR where possible
|
||||||
|
- no secrets in prompt files
|
||||||
|
|
||||||
|
### 4) Add tests for prompt assets (`tests/test_prompts.py`)
|
||||||
|
Keep tests robust but not brittle.
|
||||||
|
|
||||||
|
Recommended tests:
|
||||||
|
1. `test_prompt_file_exists`
|
||||||
|
2. `test_prompt_file_is_not_empty`
|
||||||
|
3. `test_prompt_mentions_verbatim_behavior`
|
||||||
|
4. `test_prompt_includes_uncertainty_and_illegible_markers`
|
||||||
|
5. `test_prompt_includes_deleted_and_inserted_conventions`
|
||||||
|
|
||||||
|
Avoid exact full-text matching; verify key semantic anchors only.
|
||||||
|
|
||||||
|
### 5) Optional config alignment check
|
||||||
|
Current config already has:
|
||||||
|
- `prompt_dir: Path = Path("./prompts")`
|
||||||
|
|
||||||
|
In Step 2, ensure docs reflect this and that Step 3 will resolve:
|
||||||
|
- `PROMPT_DIR / "transcribe_document.md"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task-by-Task Execution Checklist
|
||||||
|
|
||||||
|
## Phase A — Scaffold files
|
||||||
|
|
||||||
|
- [ ] **A1. Create prompt directory**
|
||||||
|
- Path: `prompts/`
|
||||||
|
- Verify: directory exists at repo root
|
||||||
|
|
||||||
|
- [ ] **A2. Create canonical prompt file**
|
||||||
|
- Path: `prompts/transcribe_document.md`
|
||||||
|
- Verify: file exists and is non-empty
|
||||||
|
|
||||||
|
- [ ] **A3. (Recommended) Create prompt library README**
|
||||||
|
- Path: `prompts/README.md`
|
||||||
|
- Verify: includes naming + revision conventions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Author prompt content (core work)
|
||||||
|
|
||||||
|
- [ ] **B1. Add Purpose section**
|
||||||
|
- States verbatim historical transcription objective
|
||||||
|
- Explicitly disallows summarization/paraphrase
|
||||||
|
|
||||||
|
- [ ] **B2. Add Output Contract section**
|
||||||
|
- Plain text output expectation
|
||||||
|
- Preserve meaningful structure and reading order
|
||||||
|
- No fabricated text
|
||||||
|
|
||||||
|
- [ ] **B3. Add Rule Set from `docs/intent.md`**
|
||||||
|
- Misspellings/errors: `[sic]`
|
||||||
|
- Missing words: `[word]`
|
||||||
|
- Uncertain readings: `[guess?]`
|
||||||
|
- Illegible regions: `[illegible]` / reason labels
|
||||||
|
- Crossed-out text: `[deleted: ...]`
|
||||||
|
- Squeezed-in text: `[inserted: ...]`
|
||||||
|
- Superscripts/abbrev handling guidance
|
||||||
|
- Non-text visuals: bracketed descriptive labels
|
||||||
|
- Marginalia formatting cue
|
||||||
|
- Rejoin line-break hyphenated words silently
|
||||||
|
- Ambiguous capitalization policy
|
||||||
|
- Hierarchical outline numbering preservation
|
||||||
|
|
||||||
|
- [ ] **B4. Add Ambiguity and Confidence policy**
|
||||||
|
- “Mark uncertainty instead of guessing”
|
||||||
|
- “Never silently normalize uncertain passages”
|
||||||
|
|
||||||
|
- [ ] **B5. Add Final Self-Check section**
|
||||||
|
- Checklist for fidelity, uncertainty labeling, and format compliance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Add validations (tests)
|
||||||
|
|
||||||
|
- [ ] **C1. Create prompt tests file**
|
||||||
|
- Path: `tests/test_prompts.py`
|
||||||
|
|
||||||
|
- [ ] **C2. Add existence/health checks**
|
||||||
|
- Prompt file exists
|
||||||
|
- Prompt file has content (non-whitespace)
|
||||||
|
|
||||||
|
- [ ] **C3. Add semantic anchor checks**
|
||||||
|
- Mentions verbatim behavior
|
||||||
|
- Mentions uncertainty marker pattern (`?` in brackets conceptually)
|
||||||
|
- Mentions illegible handling
|
||||||
|
- Mentions deleted/inserted conventions
|
||||||
|
|
||||||
|
- [ ] **C4. Keep tests resilient**
|
||||||
|
- Avoid exact full-file snapshot assertions
|
||||||
|
- Assert required concepts, not precise phrasing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Documentation alignment
|
||||||
|
|
||||||
|
- [ ] **D1. Update top-level docs/README reference**
|
||||||
|
- Mention that prompts live in `prompts/`
|
||||||
|
- Mention Step 3 loads from `PROMPT_DIR`
|
||||||
|
|
||||||
|
- [ ] **D2. Confirm config compatibility**
|
||||||
|
- `src/transcription/config.py` already uses `prompt_dir = Path("./prompts")`
|
||||||
|
- No code change needed unless naming/path mismatch appears
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Verification
|
||||||
|
|
||||||
|
- [ ] **E1. Run targeted test file**
|
||||||
|
- `uv run pytest tests/test_prompts.py -q`
|
||||||
|
|
||||||
|
- [ ] **E2. Run full suite**
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
- [ ] **E3. Confirm no regressions**
|
||||||
|
- All existing tests still green (expected: previous 20 + new prompt tests)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Commit plan (recommended granularity)
|
||||||
|
|
||||||
|
- [ ] **F1. Commit 1: scaffold**
|
||||||
|
- `prompts/transcribe_document.md` (initial structure)
|
||||||
|
- `prompts/README.md` (if included)
|
||||||
|
|
||||||
|
- [ ] **F2. Commit 2: finalized prompt content**
|
||||||
|
- full rule-complete prompt text
|
||||||
|
|
||||||
|
- [ ] **F3. Commit 3: tests + docs alignment**
|
||||||
|
- `tests/test_prompts.py`
|
||||||
|
- README/docs mention of prompt artifact pattern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done Criteria (quick gate)
|
||||||
|
|
||||||
|
- [ ] Canonical prompt exists and is curated for verbatim transcription.
|
||||||
|
- [ ] Prompt encodes all high-value handling rules from `docs/intent.md`.
|
||||||
|
- [ ] Prompt tests pass.
|
||||||
|
- [ ] Full project tests pass with `uv`.
|
||||||
|
- [ ] Ready for Step 3 provider integration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria (Definition of Done)
|
||||||
|
|
||||||
|
Step 2 is complete when all are true:
|
||||||
|
|
||||||
|
1. `prompts/transcribe_document.md` exists and is committed.
|
||||||
|
2. Prompt includes all critical handling rules from `docs/intent.md`.
|
||||||
|
3. Prompt is structured with stable section headings for future curation.
|
||||||
|
4. Prompt tests pass under `uv run pytest -q`.
|
||||||
|
5. Existing tests remain green (total suite still passes).
|
||||||
|
6. Docs indicate prompt artifact location and curation policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk: prompt too vague → hallucinated reconstructions**
|
||||||
|
- Mitigation: explicit uncertainty/illegible conventions and “no invention” rule.
|
||||||
|
|
||||||
|
2. **Risk: prompt too rigid for mixed document types**
|
||||||
|
- Mitigation: include neutral defaults + clear annotation formats.
|
||||||
|
|
||||||
|
3. **Risk: brittle tests block iterative prompt tuning**
|
||||||
|
- Mitigation: test semantic anchors, not exact wording.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 3
|
||||||
|
|
||||||
|
After Step 2, Step 3 can immediately:
|
||||||
|
1. Load `transcribe_document.md` from `PROMPT_DIR`
|
||||||
|
2. Inject prompt into OpenRouter request
|
||||||
|
3. Start validating real transcription behavior with minimal glue code
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
## Step 3: services/transcription.py + providers/
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
|
||||||
|
Implement the **AI transcription integration layer** so the app can:
|
||||||
|
|
||||||
|
1. Read the curated prompt from `PROMPT_DIR`
|
||||||
|
2. Send prompt + image to the configured provider (OpenRouter)
|
||||||
|
3. Return normalized transcription output (or structured failure)
|
||||||
|
|
||||||
|
This corresponds to MVP Step 3 from `docs/mvp.md`:
|
||||||
|
- `services/transcription.py`
|
||||||
|
- `providers/` adapter(s)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 3
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- Provider abstraction and OpenRouter adapter
|
||||||
|
- Prompt file loading utility in service layer
|
||||||
|
- Image payload preparation
|
||||||
|
- One high-level transcription service function usable by Step 4 worker
|
||||||
|
- Unit tests (mocked provider SDK, no external calls)
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Job polling/background loop (Step 4)
|
||||||
|
- DB status transition orchestration in worker loop (Step 4)
|
||||||
|
- UI invocation/wiring (Step 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planned Deliverables
|
||||||
|
|
||||||
|
### Source files
|
||||||
|
- `src/transcription/providers/base.py`
|
||||||
|
- `src/transcription/providers/openrouter.py`
|
||||||
|
- `src/transcription/providers/__init__.py` (exports + factory)
|
||||||
|
- `src/transcription/services/transcription.py`
|
||||||
|
- `src/transcription/services/__init__.py` (optional export)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- `tests/providers/test_openrouter.py`
|
||||||
|
- `tests/services/test_transcription.py`
|
||||||
|
|
||||||
|
### Test directory convention
|
||||||
|
- Mirror source domains under `tests/`.
|
||||||
|
- Provider adapter tests live under `tests/providers/`.
|
||||||
|
- Service-layer tests live under `tests/services/`.
|
||||||
|
- Prefer one focused test module per production module (for Step 3: `test_openrouter.py`, `test_transcription.py`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Decisions (before coding)
|
||||||
|
|
||||||
|
1. **Provider interface first**
|
||||||
|
- Define a stable contract independent of SDK specifics.
|
||||||
|
- Prevent Step 4 from depending on raw SDK response shapes.
|
||||||
|
|
||||||
|
2. **Service returns normalized result object**
|
||||||
|
- Include: `text`, `provider`, `model`, `raw_error`/exception metadata.
|
||||||
|
- Worker can map this cleanly to `Transcript` and `JobStatus`.
|
||||||
|
|
||||||
|
3. **Prompt loaded from file at call time**
|
||||||
|
- Uses `get_settings().prompt_dir / "transcribe_document.md"`.
|
||||||
|
- Keeps prompt edits hot-swappable without code changes.
|
||||||
|
|
||||||
|
4. **Clear exception boundary**
|
||||||
|
- SDK/network/model failures become predictable domain exceptions:
|
||||||
|
- `ProviderError`
|
||||||
|
- `PromptLoadError`
|
||||||
|
- `TranscriptionError` (optional top-level wrapper)
|
||||||
|
|
||||||
|
5. **Model resolution policy**
|
||||||
|
- Use `settings.provider_model` if set
|
||||||
|
- Otherwise use adapter default constant (e.g., vision-capable model slug)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task-by-Task Execution Checklist
|
||||||
|
|
||||||
|
## Phase A — Provider contract
|
||||||
|
|
||||||
|
- [ ] Create `src/transcription/providers/base.py`
|
||||||
|
- [ ] Define protocol/ABC for transcription providers:
|
||||||
|
- [ ] method signature accepts prompt text + image bytes (or data URL) + mime type
|
||||||
|
- [ ] returns normalized text result (and optional metadata)
|
||||||
|
- [ ] Define shared provider exceptions:
|
||||||
|
- [ ] `ProviderError`
|
||||||
|
- [ ] optional subclasses (`ProviderAuthError`, `ProviderResponseError`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — OpenRouter adapter
|
||||||
|
|
||||||
|
- [ ] Create `src/transcription/providers/openrouter.py`
|
||||||
|
- [ ] Implement `OpenRouterTranscriptionProvider` with:
|
||||||
|
- [ ] config-driven API key usage
|
||||||
|
- [ ] optional referer/title attribution headers
|
||||||
|
- [ ] model resolution fallback when `provider_model` is unset
|
||||||
|
- [ ] Implement request building:
|
||||||
|
- [ ] prompt included as instruction content
|
||||||
|
- [ ] image included in supported format for vision call
|
||||||
|
- [ ] Implement response parsing:
|
||||||
|
- [ ] extract final transcript text from SDK response
|
||||||
|
- [ ] validate non-empty text
|
||||||
|
- [ ] Wrap SDK failures into `ProviderError` with clean message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Provider factory
|
||||||
|
|
||||||
|
- [ ] Update `src/transcription/providers/__init__.py`
|
||||||
|
- [ ] Add `get_transcription_provider()` factory:
|
||||||
|
- [ ] reads `settings.provider`
|
||||||
|
- [ ] returns OpenRouter adapter for `openrouter`
|
||||||
|
- [ ] raises explicit error for unsupported provider values
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Transcription service (Step 3 core)
|
||||||
|
|
||||||
|
- [ ] Create `src/transcription/services/transcription.py`
|
||||||
|
- [ ] Add prompt loader function:
|
||||||
|
- [ ] default file: `transcribe_document.md`
|
||||||
|
- [ ] raises `PromptLoadError` on missing/empty file
|
||||||
|
- [ ] Add image loader/validator:
|
||||||
|
- [ ] path existence check
|
||||||
|
- [ ] allowed mime detection (`.jpg/.jpeg/.png/.tiff/.pdf` policy aligned to MVP)
|
||||||
|
- [ ] Add high-level function (name example):
|
||||||
|
- [ ] `transcribe_document_image(image_path, prompt_name="transcribe_document.md")`
|
||||||
|
- [ ] loads prompt + image
|
||||||
|
- [ ] calls provider from factory
|
||||||
|
- [ ] returns normalized transcription result object
|
||||||
|
- [ ] Add structured logging at key boundaries:
|
||||||
|
- [ ] prompt loaded
|
||||||
|
- [ ] provider invoked
|
||||||
|
- [ ] success/failure outcome (no sensitive data in logs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Tests (two-phase scaffold -> fill)
|
||||||
|
|
||||||
|
### Required execution resources
|
||||||
|
|
||||||
|
Load and reference these directly during test planning/implementation so the two-phase flow is enforced:
|
||||||
|
|
||||||
|
- [ ] `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
- [ ] `resource://prompts/pytest-scaffold/document`
|
||||||
|
- [ ] `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
- [ ] `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
|
||||||
|
### Phase E1 — Scaffold test structure first
|
||||||
|
|
||||||
|
Prompt: `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
|
||||||
|
Suggested arguments:
|
||||||
|
- [ ] `target_modules` = `src/transcription/providers/openrouter.py`, `src/transcription/services/transcription.py`
|
||||||
|
- [ ] `mode` = `scaffold`
|
||||||
|
- [ ] `path_strategy` = `src-to-tests-mirror`
|
||||||
|
- [ ] `naming_style` = `concise-behavior`
|
||||||
|
|
||||||
|
Expected scaffold outcomes:
|
||||||
|
- [ ] `tests/providers/test_openrouter.py` exists with class/method skeletons and one-line docstrings
|
||||||
|
- [ ] `tests/services/test_transcription.py` exists with class/method skeletons and one-line docstrings
|
||||||
|
- [ ] collection succeeds on scaffold-only tests
|
||||||
|
|
||||||
|
Scaffold coverage targets:
|
||||||
|
- [ ] adapter initializes from settings
|
||||||
|
- [ ] model fallback when `provider_model is None`
|
||||||
|
- [ ] referer/title options included when set
|
||||||
|
- [ ] successful SDK response parses transcript text
|
||||||
|
- [ ] SDK exception maps to `ProviderError`
|
||||||
|
- [ ] empty/invalid response maps to `ProviderError`
|
||||||
|
- [ ] prompt loader reads canonical prompt file
|
||||||
|
- [ ] missing prompt raises `PromptLoadError`
|
||||||
|
- [ ] transcription function loads file and calls provider once
|
||||||
|
- [ ] image path missing raises clear error
|
||||||
|
- [ ] provider error is propagated/wrapped predictably
|
||||||
|
- [ ] returned result includes transcript text and metadata
|
||||||
|
|
||||||
|
### Phase E2 — Fill scaffolded tests with assertions
|
||||||
|
|
||||||
|
Prompt: `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
|
||||||
|
Suggested arguments:
|
||||||
|
- [ ] `target_files` = `tests/providers/test_openrouter.py`, `tests/services/test_transcription.py`
|
||||||
|
- [ ] `stack` = `pure-python`
|
||||||
|
- [ ] `strategy` = `minimal`
|
||||||
|
- [ ] `marker_lane` = `unit`
|
||||||
|
|
||||||
|
Fill constraints:
|
||||||
|
- [ ] preserve scaffold class/method names and one-line docstrings
|
||||||
|
- [ ] keep mocks to an absolute minimum; mock only network boundaries and non-deterministic failures
|
||||||
|
- [ ] keep one behavior target per test method
|
||||||
|
|
||||||
|
> Default suite should remain deterministic and fast, but mocking should be minimal and intentional.
|
||||||
|
|
||||||
|
### Optional real-endpoint validation lane
|
||||||
|
|
||||||
|
- [ ] Add an opt-in integration lane for real provider calls (for example `@pytest.mark.integration` and `@pytest.mark.live_api`).
|
||||||
|
- [ ] Gate live tests behind explicit env vars (for example `OPENROUTER_API_KEY`, optional `RUN_LIVE_API_TESTS=1`).
|
||||||
|
- [ ] Exclude live tests from default CI/local runs unless explicitly requested.
|
||||||
|
- [ ] Keep at least one thin smoke path that can validate request/response compatibility against the real endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Verification commands
|
||||||
|
|
||||||
|
- [ ] E1 scaffold validation: `uv run pytest --collect-only -q`
|
||||||
|
- [ ] E2 fill validation (unit lane): `uv run pytest -m unit -q`
|
||||||
|
- [ ] E2 targeted provider file: `uv run pytest tests/providers/test_openrouter.py -q`
|
||||||
|
- [ ] E2 targeted service file: `uv run pytest tests/services/test_transcription.py -q`
|
||||||
|
- [ ] E2 final full-suite check: `uv run pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes / Guardrails
|
||||||
|
|
||||||
|
- Avoid coupling Step 3 service to DB models directly (that belongs in Step 4 orchestration).
|
||||||
|
- Do not silently swallow provider errors.
|
||||||
|
- Keep prompt filename stable (`transcribe_document.md`) unless explicitly parameterized.
|
||||||
|
- Keep request/response normalization inside provider adapter, not worker/UI layers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done (Step 3)
|
||||||
|
|
||||||
|
Step 3 is done when:
|
||||||
|
|
||||||
|
1. Provider abstraction exists and OpenRouter adapter is implemented.
|
||||||
|
2. Service can transcribe a local image using prompt file content.
|
||||||
|
3. Failures are returned as structured exceptions, not raw SDK traceback noise.
|
||||||
|
4. Unit tests for provider and service pass.
|
||||||
|
5. Full suite remains green under `uv run pytest -q`.
|
||||||
|
6. Step 4 can call a single service function to process queued jobs.
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
## Step 4: `services/upload.py` + `worker.py`
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
|
||||||
|
Implement the MVP upload and background-processing pipeline so the system can:
|
||||||
|
|
||||||
|
1. Save uploaded files into `UPLOAD_DIR`
|
||||||
|
2. Create `Document` + `Job(status="queued")`
|
||||||
|
3. Process queued jobs in a worker loop:
|
||||||
|
- `queued -> processing`
|
||||||
|
- call Step 3 transcription service
|
||||||
|
- persist `Transcript`
|
||||||
|
- finalize as `transcribed` or `failed`
|
||||||
|
|
||||||
|
This step advances MVP Feature 1 + Feature 2 and supports REQ-1, REQ-2, REQ-3, REQ-4, REQ-6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
- Upload persistence logic and initial job creation
|
||||||
|
- Worker polling and single-job lifecycle execution
|
||||||
|
- Deterministic test coverage for upload + worker (default suite)
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- UI integration and pages (Step 5)
|
||||||
|
- Queue infrastructure beyond in-process loop
|
||||||
|
- Async DB/session architecture refactor
|
||||||
|
- Broad production hardening beyond MVP needs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planned Deliverables
|
||||||
|
|
||||||
|
### Source files
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
- `src/transcription/services/__init__.py` (export updates as needed)
|
||||||
|
|
||||||
|
### Test files
|
||||||
|
- `tests/services/test_upload.py`
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
|
||||||
|
### Optional external lane (already present pattern)
|
||||||
|
- reuse `external` marker for live-provider checks where appropriate
|
||||||
|
- keep external out of default lane
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required MCP Prompt References (for test workflow)
|
||||||
|
|
||||||
|
Apply these resources directly during Step 4 test creation:
|
||||||
|
|
||||||
|
1. `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
2. `resource://prompts/pytest-scaffold/document`
|
||||||
|
3. `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
4. `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
|
||||||
|
And (as referenced by those prompts) apply relevant pytest skill references for:
|
||||||
|
- naming/hierarchy
|
||||||
|
- marker defaults
|
||||||
|
- SQLAlchemy sync testing behavior where applicable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
1. **Upload service owns initial file + record creation**
|
||||||
|
- Writes file, creates `Document`, creates queued `Job`, returns IDs/path.
|
||||||
|
|
||||||
|
2. **Worker owns lifecycle transitions**
|
||||||
|
- Worker is the single owner of `queued -> processing -> terminal` job state changes.
|
||||||
|
|
||||||
|
3. **Worker uses Step 3 service boundary**
|
||||||
|
- Worker calls `transcribe_document_image(...)`; no provider-specific SDK logic in worker.
|
||||||
|
|
||||||
|
4. **Failure information is always persisted**
|
||||||
|
- On failure: store `Transcript(text=None, error_detail=...)` and set `Job.status=failed`.
|
||||||
|
|
||||||
|
5. **Loop remains simple and stoppable**
|
||||||
|
- In-process polling loop with stop event and poll interval for MVP simplicity and testability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task-by-Task Execution Checklist
|
||||||
|
|
||||||
|
## Phase A — Implement upload service (`src/transcription/services/upload.py`)
|
||||||
|
|
||||||
|
- [ ] Create `UploadError` exception
|
||||||
|
- [ ] Create `UploadJobResult` dataclass with:
|
||||||
|
- [ ] `document_id`
|
||||||
|
- [ ] `job_id`
|
||||||
|
- [ ] `stored_path`
|
||||||
|
- [ ] `original_filename`
|
||||||
|
- [ ] Add filename safety handling:
|
||||||
|
- [ ] normalize to basename
|
||||||
|
- [ ] avoid path traversal
|
||||||
|
- [ ] collision-safe stored name (e.g., UUID prefix/suffix)
|
||||||
|
- [ ] Validate upload payload:
|
||||||
|
- [ ] non-empty bytes required
|
||||||
|
- [ ] extension in supported set (`.jpg/.jpeg/.png/.tif/.tiff/.pdf`)
|
||||||
|
- [ ] Ensure upload directory exists (`mkdir(parents=True, exist_ok=True)`)
|
||||||
|
- [ ] Write file bytes to `UPLOAD_DIR`
|
||||||
|
- [ ] Persist DB records in one transaction:
|
||||||
|
- [ ] `Document(filename, file_path)`
|
||||||
|
- [ ] `Job(document_id=..., status=queued)`
|
||||||
|
- [ ] Return `UploadJobResult`
|
||||||
|
- [ ] Add logging for success/failure boundaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Implement worker core (`src/transcription/worker.py`)
|
||||||
|
|
||||||
|
- [ ] Add `process_next_queued_job(...) -> bool`
|
||||||
|
- [ ] Fetch oldest queued job
|
||||||
|
- [ ] Return `False` when no queued jobs exist
|
||||||
|
- [ ] Transition picked job to `processing` and update timestamp
|
||||||
|
- [ ] Resolve associated `Document.file_path`
|
||||||
|
- [ ] Call `transcribe_document_image(image_path=...)`
|
||||||
|
- [ ] On success:
|
||||||
|
- [ ] insert/update transcript text
|
||||||
|
- [ ] clear error detail
|
||||||
|
- [ ] mark job `transcribed`
|
||||||
|
- [ ] update timestamp
|
||||||
|
- [ ] On failure:
|
||||||
|
- [ ] insert/update transcript with `text=None`, `error_detail=...`
|
||||||
|
- [ ] mark job `failed`
|
||||||
|
- [ ] update timestamp
|
||||||
|
- [ ] Commit terminal state and return `True`
|
||||||
|
- [ ] Add logs around job pickup, transition, and terminal outcome
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Implement worker loop (`src/transcription/worker.py`)
|
||||||
|
|
||||||
|
- [ ] Add `run_worker_loop(...)`
|
||||||
|
- [ ] Accept configurable stop event/signal
|
||||||
|
- [ ] Accept configurable poll interval
|
||||||
|
- [ ] Repeatedly call `process_next_queued_job`
|
||||||
|
- [ ] Sleep only when queue is empty
|
||||||
|
- [ ] Exit cleanly when stop event is set
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Exports
|
||||||
|
|
||||||
|
- [ ] Update `src/transcription/services/__init__.py` to expose upload APIs
|
||||||
|
- [ ] Keep existing transcription exports intact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Tests via MCP scaffold -> fill flow
|
||||||
|
|
||||||
|
## E1 Scaffold (structure only)
|
||||||
|
|
||||||
|
Use scaffold prompt workflow first for:
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
|
||||||
|
Expected scaffold targets:
|
||||||
|
- `tests/services/test_upload.py`
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
|
||||||
|
Scaffold rules:
|
||||||
|
- [ ] Class hierarchy + method names + one-line docstrings only
|
||||||
|
- [ ] No assertions or implementation details in scaffold phase
|
||||||
|
- [ ] Keep method names concise and behavior-focused
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
|
||||||
|
## E2 Fill scaffold (implementation)
|
||||||
|
|
||||||
|
Use fill prompt workflow for:
|
||||||
|
- `tests/services/test_upload.py`
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
- stack: `sqlalchemy-sync` (or `mixed` if combining pure + DB behaviors)
|
||||||
|
- marker lane preference: `unit` and `integration` as appropriate
|
||||||
|
- strategy: minimal deterministic implementation
|
||||||
|
|
||||||
|
Fill rules (invariants):
|
||||||
|
- [ ] Preserve scaffold class names, method names, and one-line docstrings
|
||||||
|
- [ ] Do not rename/re-nest scaffolded tests unless explicitly approved
|
||||||
|
- [ ] One behavior target per test
|
||||||
|
- [ ] Minimal mocking; mock only network/nondeterministic boundaries
|
||||||
|
|
||||||
|
Suggested test coverage:
|
||||||
|
|
||||||
|
### `tests/services/test_upload.py`
|
||||||
|
- [ ] creates file + document + queued job (`integration`)
|
||||||
|
- [ ] rejects empty bytes (`unit`)
|
||||||
|
- [ ] rejects unsupported extension (`unit`)
|
||||||
|
- [ ] writes collision-safe unique filename (`integration`)
|
||||||
|
- [ ] persisted job status is `queued` (`integration`)
|
||||||
|
|
||||||
|
### `tests/services/test_worker.py`
|
||||||
|
- [ ] returns `False` when queue empty (`integration`)
|
||||||
|
- [ ] transitions `queued -> processing -> transcribed` on success (`integration`)
|
||||||
|
- [ ] stores transcript text on success (`integration`)
|
||||||
|
- [ ] transitions to `failed` and stores `error_detail` on failure (`integration`)
|
||||||
|
- [ ] updates existing transcript instead of duplicate create (`integration`)
|
||||||
|
- [ ] worker loop exits when stop event set (`unit`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Marker Strategy
|
||||||
|
|
||||||
|
- `unit`: pure logic tests (filename handling, loop stop behavior, validation logic)
|
||||||
|
- `integration`: DB + service orchestration tests (SQLite/session/contracts)
|
||||||
|
- `external`: opt-in live provider tests only (not part of default Step 4 lane)
|
||||||
|
|
||||||
|
No new marker needed; reuse existing marker registration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Sequence (strict order)
|
||||||
|
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
- [ ] `uv run pytest -m unit -q` *(if unit tests touched)*
|
||||||
|
- [ ] `uv run pytest tests/services/test_upload.py -q`
|
||||||
|
- [ ] `uv run pytest tests/services/test_worker.py -q`
|
||||||
|
- [ ] `uv run pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Requirements (after implementation)
|
||||||
|
|
||||||
|
Implementation report must include:
|
||||||
|
|
||||||
|
1. Files created/updated
|
||||||
|
2. Fixture and marker decisions
|
||||||
|
3. MCP references used and why
|
||||||
|
4. Validation command results
|
||||||
|
5. Remaining risks/open questions (only blockers)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- Keep Step 4 independent from UI concerns.
|
||||||
|
- Do not call provider SDK directly from worker.
|
||||||
|
- Do not silently swallow exceptions.
|
||||||
|
- Always persist terminal job outcome.
|
||||||
|
- Keep default suite deterministic and fast.
|
||||||
|
- Preserve scaffold invariants during fill phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done (Step 4)
|
||||||
|
|
||||||
|
Step 4 is complete when:
|
||||||
|
|
||||||
|
- [ ] Upload service writes file and creates `Document` + queued `Job`
|
||||||
|
- [ ] Worker processes queued jobs end-to-end using Step 3 transcription service
|
||||||
|
- [ ] Success path persists transcript text and sets `transcribed`
|
||||||
|
- [ ] Failure path persists error detail and sets `failed`
|
||||||
|
- [ ] Queue-empty path returns cleanly
|
||||||
|
- [ ] New tests pass and full suite is green (`uv run pytest -q`)
|
||||||
|
- [ ] Output report includes MCP reference usage + validation evidence
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
Step 5 is implemented and passing ✅
|
||||||
|
|
||||||
|
### What I implemented
|
||||||
|
|
||||||
|
#### App/bootstrap
|
||||||
|
- Added `src/transcription/app.py`:
|
||||||
|
- `create_app()` factory
|
||||||
|
- FastAPI lifespan startup/shutdown:
|
||||||
|
- `setup_logging()`
|
||||||
|
- `create_all()`
|
||||||
|
- ensure `upload_dir` and `prompt_dir`
|
||||||
|
- start/stop worker thread (`run_worker_loop`)
|
||||||
|
- includes health router
|
||||||
|
- registers NiceGUI pages
|
||||||
|
|
||||||
|
#### API
|
||||||
|
- Added `src/transcription/api/__init__.py`
|
||||||
|
- Added `src/transcription/api/health.py`:
|
||||||
|
- `GET /healthz` returns `{"status": "ok"}`
|
||||||
|
|
||||||
|
#### UI
|
||||||
|
- Added `src/transcription/ui/upload_page.py`:
|
||||||
|
- upload page route
|
||||||
|
- accepted extensions helper
|
||||||
|
- upload submit helper to Step 4 service (`create_upload_job`)
|
||||||
|
- success/error state messaging
|
||||||
|
- Added `src/transcription/ui/jobs_page.py`:
|
||||||
|
- jobs list page
|
||||||
|
- job detail page
|
||||||
|
- `fetch_jobs()` and `fetch_job_detail()` helpers
|
||||||
|
- Updated `src/transcription/ui/__init__.py`:
|
||||||
|
- explicit page registration
|
||||||
|
- NiceGUI mounted via `ui.run_with(app, mount_path="/ui", ...)`
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
- Added runtime dependencies:
|
||||||
|
- `fastapi`
|
||||||
|
- `nicegui`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tests created and filled (scaffold → fill)
|
||||||
|
|
||||||
|
- `tests/test_app.py`
|
||||||
|
- `tests/api/test_health.py`
|
||||||
|
- `tests/ui/test_pages_registration.py`
|
||||||
|
- `tests/ui/test_upload_page.py`
|
||||||
|
- `tests/ui/test_jobs_page.py`
|
||||||
|
|
||||||
|
All scaffolded method names/docstrings were preserved and filled with executable assertions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation results (strict sequence)
|
||||||
|
|
||||||
|
- `uv run pytest --collect-only -q` ✅
|
||||||
|
- `uv run pytest -m unit -q` ✅
|
||||||
|
- `uv run pytest tests/api/test_health.py -q` ✅
|
||||||
|
- `uv run pytest tests/ui/test_pages_registration.py -q` ✅
|
||||||
|
- `uv run pytest tests/test_app.py -q` ✅
|
||||||
|
- `uv run pytest tests/ui/test_upload_page.py -q` ✅
|
||||||
|
- `uv run pytest tests/ui/test_jobs_page.py -q` ✅
|
||||||
|
- `uv run pytest -q` ✅
|
||||||
|
|
||||||
|
Full suite is green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Note
|
||||||
|
You’ll see one warning from FastAPI/Starlette test client about `httpx` deprecation; it does not affect correctness and all tests pass.
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
## Step 5: `app.py` + UI Pages (NiceGUI + FastAPI composition)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement the MVP user-facing application layer so users can:
|
||||||
|
|
||||||
|
1. Upload a document from the UI
|
||||||
|
2. Trigger Step 4 upload/job creation flow
|
||||||
|
3. See live job lifecycle status (`queued`, `processing`, `transcribed`, `failed`)
|
||||||
|
4. Open a job detail view to read transcript text or failure details
|
||||||
|
|
||||||
|
This step composes Steps 1–4 into a usable UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Summary (NiceGUI-aligned)
|
||||||
|
|
||||||
|
Step 5 uses a **FastAPI app factory + lifespan orchestration** and mounts/registers NiceGUI pages via explicit page modules.
|
||||||
|
|
||||||
|
Reference baseline: `resource://skills/nicegui/document`
|
||||||
|
|
||||||
|
### Core architecture decisions
|
||||||
|
|
||||||
|
- **App factory:** `create_app()`
|
||||||
|
- **Lifespan-managed resources:** worker start/stop managed in startup/shutdown
|
||||||
|
- **Modular pages:** upload and jobs pages in separate modules (no monolithic UI file)
|
||||||
|
- **Health endpoint:** FastAPI-side `/healthz`
|
||||||
|
- **UI composition:** route pages stay modular and reusable shared shell/components live under `ui/components` as needed
|
||||||
|
- **Styling architecture:** shared CSS loaded once at startup; avoid ad-hoc per-page styling drift
|
||||||
|
- **Dependency direction (one-way):**
|
||||||
|
- `app` -> `config/logging/db/worker/ui/api`
|
||||||
|
- `ui/pages` -> `ui/components` + `services`
|
||||||
|
- `services` -> `db/models/providers`
|
||||||
|
- no reverse imports from services into UI/API
|
||||||
|
|
||||||
|
### DB and AI stance (explicit)
|
||||||
|
|
||||||
|
- **DB:** already enabled (SQLModel + SQLite), session lifecycle remains request/service-scoped as built in prior steps.
|
||||||
|
- **AI workflow:** already in place via Step 3 transcription service + Step 4 worker; UI does not call provider SDK directly.
|
||||||
|
- **Mounted docs:** not in Step 5 scope; docs mounting remains disabled for MVP.
|
||||||
|
|
||||||
|
### Async and responsiveness stance
|
||||||
|
|
||||||
|
- Prefer `async def` for page handlers and service boundaries when I/O is involved.
|
||||||
|
- Keep UI handlers non-blocking (no blocking sleeps or synchronous long I/O calls).
|
||||||
|
- For long-running user actions, always provide explicit loading/progress/error states.
|
||||||
|
- Keep cancellation/timeout behavior explicit for refresh/poll operations where applicable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- `src/transcription/app.py`
|
||||||
|
- `src/transcription/ui/upload_page.py`
|
||||||
|
- `src/transcription/ui/jobs_page.py`
|
||||||
|
- `src/transcription/ui/__init__.py`
|
||||||
|
- `src/transcription/api/health.py` (or equivalent FastAPI health route module)
|
||||||
|
- UI/app tests with MCP scaffold->fill flow
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Auth
|
||||||
|
- advanced filtering/search UX
|
||||||
|
- batch upload UX beyond MVP
|
||||||
|
- deployment/container hardening
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planned Deliverables
|
||||||
|
|
||||||
|
### Source files
|
||||||
|
- `src/transcription/app.py` (app factory + lifespan wiring)
|
||||||
|
- `src/transcription/api/health.py` (GET `/healthz`)
|
||||||
|
- `src/transcription/ui/upload_page.py` (upload flow)
|
||||||
|
- `src/transcription/ui/jobs_page.py` (status list + detail)
|
||||||
|
- `src/transcription/ui/__init__.py` (explicit `register_pages(...)` export)
|
||||||
|
- `src/transcription/ui/components/*` (shared shell/navigation/status components if introduced)
|
||||||
|
- `src/transcription/ui/static/*.css` (optional shared CSS loaded once at startup)
|
||||||
|
|
||||||
|
### Test files
|
||||||
|
- `tests/test_app.py`
|
||||||
|
- `tests/api/test_health.py`
|
||||||
|
- `tests/ui/test_pages_registration.py`
|
||||||
|
- `tests/ui/test_upload_page.py`
|
||||||
|
- `tests/ui/test_jobs_page.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan + Checklist
|
||||||
|
|
||||||
|
Plan baseline and guardrails source: `resource://skills/nicegui/document`
|
||||||
|
|
||||||
|
## Phase A — App factory and lifespan orchestration
|
||||||
|
|
||||||
|
- [ ] Create `create_app()` in `src/transcription/app.py`
|
||||||
|
- [ ] Add FastAPI lifespan startup/shutdown handlers
|
||||||
|
- [ ] Startup responsibilities:
|
||||||
|
- [ ] `setup_logging()`
|
||||||
|
- [ ] `create_all()`
|
||||||
|
- [ ] ensure directories exist (`upload_dir`, `prompt_dir`)
|
||||||
|
- [ ] create worker stop event
|
||||||
|
- [ ] start worker background thread/task
|
||||||
|
- [ ] Shutdown responsibilities:
|
||||||
|
- [ ] signal stop event
|
||||||
|
- [ ] join/cleanup worker thread/task cleanly
|
||||||
|
- [ ] Register API router(s), including health route
|
||||||
|
- [ ] Register NiceGUI pages via explicit page registration function
|
||||||
|
- [ ] Load shared CSS once at startup (if present)
|
||||||
|
|
||||||
|
## Phase B — FastAPI health endpoint
|
||||||
|
|
||||||
|
- [ ] Create `src/transcription/api/health.py`
|
||||||
|
- [ ] Add `GET /healthz` returning simple healthy payload
|
||||||
|
- [ ] Wire route into app factory
|
||||||
|
|
||||||
|
## Phase C — Upload page (`ui/upload_page.py`)
|
||||||
|
|
||||||
|
- [ ] Add upload route/page registration function
|
||||||
|
- [ ] Render file input accepting supported extensions
|
||||||
|
- [ ] On submit:
|
||||||
|
- [ ] show loading/progress state
|
||||||
|
- [ ] call `create_upload_job(filename, file_bytes, ...)`
|
||||||
|
- [ ] show success state with job reference/link
|
||||||
|
- [ ] On error:
|
||||||
|
- [ ] show user-safe error message
|
||||||
|
- [ ] restore ready UI state
|
||||||
|
- [ ] Ensure non-blocking I/O in UI event handlers; offload CPU-heavy work to worker path
|
||||||
|
- [ ] Make timeout/cancellation behavior explicit for any long-running action
|
||||||
|
|
||||||
|
## Phase D — Jobs page (`ui/jobs_page.py`)
|
||||||
|
|
||||||
|
- [ ] Add jobs list route/page registration function
|
||||||
|
- [ ] Display jobs with status + timestamps
|
||||||
|
- [ ] Add job detail route/view
|
||||||
|
- [ ] Show transcript on success, error detail on failure
|
||||||
|
- [ ] Include explicit refresh action and loading state
|
||||||
|
- [ ] Ensure error states are surfaced to user and logged
|
||||||
|
- [ ] Keep refresh path async and bounded to avoid UI freeze
|
||||||
|
|
||||||
|
## Phase E — UI registration module
|
||||||
|
|
||||||
|
- [ ] Update `src/transcription/ui/__init__.py`
|
||||||
|
- [ ] Export `register_pages(...)`
|
||||||
|
- [ ] Ensure each page module exports `register_page(...)`
|
||||||
|
- [ ] Keep page registration explicit and modular
|
||||||
|
|
||||||
|
## Phase F — Shared components and style consistency
|
||||||
|
|
||||||
|
- [ ] Add `ui/components` module only for reusable shell elements (header/nav/status chips), not page-local logic
|
||||||
|
- [ ] Keep structural layout in Python; keep visual polish in shared CSS
|
||||||
|
- [ ] Avoid one-off styling duplication across upload/jobs pages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Testing Workflow (Required)
|
||||||
|
|
||||||
|
Use these resources directly:
|
||||||
|
|
||||||
|
- `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
- `resource://prompts/pytest-scaffold/document`
|
||||||
|
- `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
- `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
|
||||||
|
## E1 — Scaffold tests first (structure only)
|
||||||
|
|
||||||
|
Target modules:
|
||||||
|
- `src/transcription/app.py`
|
||||||
|
- `src/transcription/api/health.py`
|
||||||
|
- `src/transcription/ui/upload_page.py`
|
||||||
|
- `src/transcription/ui/jobs_page.py`
|
||||||
|
|
||||||
|
Scaffold test files:
|
||||||
|
- `tests/test_app.py`
|
||||||
|
- `tests/api/test_health.py`
|
||||||
|
- `tests/ui/test_pages_registration.py`
|
||||||
|
- `tests/ui/test_upload_page.py`
|
||||||
|
- `tests/ui/test_jobs_page.py`
|
||||||
|
|
||||||
|
Scaffold constraints:
|
||||||
|
- [ ] class/method skeletons only
|
||||||
|
- [ ] one-line docstrings
|
||||||
|
- [ ] concise behavior-focused names
|
||||||
|
- [ ] no implementation assertions yet
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
|
||||||
|
## E2 — Fill scaffold tests
|
||||||
|
|
||||||
|
Fill constraints from MCP guidance:
|
||||||
|
- [ ] preserve scaffold class/method names and docstrings (locked baseline)
|
||||||
|
- [ ] one behavior target per method
|
||||||
|
- [ ] deterministic tests preferred
|
||||||
|
- [ ] minimal mocking; only nondeterministic boundaries
|
||||||
|
|
||||||
|
Stack:
|
||||||
|
- [ ] `fastapi` (or `mixed` if needed for UI+DB fixture combination)
|
||||||
|
|
||||||
|
Suggested coverage:
|
||||||
|
|
||||||
|
### `tests/api/test_health.py`
|
||||||
|
- [ ] `/healthz` returns success status and expected payload shape
|
||||||
|
|
||||||
|
### `tests/ui/test_pages_registration.py`
|
||||||
|
- [ ] page registration wiring succeeds
|
||||||
|
- [ ] expected routes are present
|
||||||
|
|
||||||
|
### `tests/test_app.py`
|
||||||
|
- [ ] startup path initializes runtime dependencies
|
||||||
|
- [ ] worker start is invoked on startup
|
||||||
|
- [ ] worker shutdown signal/cleanup is invoked on shutdown
|
||||||
|
|
||||||
|
### `tests/ui/test_upload_page.py`
|
||||||
|
- [ ] upload action calls upload service
|
||||||
|
- [ ] success feedback displayed
|
||||||
|
- [ ] error feedback displayed for `UploadError`
|
||||||
|
- [ ] loading/progress state behavior covered
|
||||||
|
- [ ] timeout/cancellation behavior covered (if implemented)
|
||||||
|
|
||||||
|
### `tests/ui/test_jobs_page.py`
|
||||||
|
- [ ] list renders job statuses
|
||||||
|
- [ ] detail shows transcript text for successful job
|
||||||
|
- [ ] detail shows error detail for failed job
|
||||||
|
- [ ] refresh/loading state behavior covered
|
||||||
|
|
||||||
|
Marker strategy:
|
||||||
|
- [ ] `unit` for pure helpers/state formatting
|
||||||
|
- [ ] `integration` for app/page/service+DB contracts
|
||||||
|
- [ ] `external` not required for default Step 5 lane
|
||||||
|
|
||||||
|
Async behavior assertions:
|
||||||
|
- [ ] long-running actions keep button/inputs in expected disabled state
|
||||||
|
- [ ] completion/failure returns controls to ready state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Sequence (strict)
|
||||||
|
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
- [ ] `uv run pytest -m unit -q` *(if unit tests touched)*
|
||||||
|
- [ ] `uv run pytest tests/api/test_health.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_pages_registration.py -q`
|
||||||
|
- [ ] `uv run pytest tests/test_app.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_upload_page.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_jobs_page.py -q`
|
||||||
|
- [ ] `uv run pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guardrails (NiceGUI + MVP)
|
||||||
|
|
||||||
|
- [ ] Do not collapse pages into one file.
|
||||||
|
- [ ] Do not use implicit global side effects for runtime wiring.
|
||||||
|
- [ ] Keep UI responsive with explicit loading/progress/error states.
|
||||||
|
- [ ] Do not block UI handlers with synchronous long I/O.
|
||||||
|
- [ ] Do not place provider SDK calls in UI handlers.
|
||||||
|
- [ ] Keep dependency direction one-way and maintainable.
|
||||||
|
- [ ] Keep shared UI in `ui/components`; keep service logic out of page modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] App factory + lifespan are in place
|
||||||
|
- [ ] Health endpoint exists and is tested
|
||||||
|
- [ ] Upload page creates queued jobs through service boundary
|
||||||
|
- [ ] Jobs list/detail pages render status/transcript/failure data
|
||||||
|
- [ ] Worker lifecycle is started/stopped by app lifespan
|
||||||
|
- [ ] Async UI states (loading/success/error) are deterministic and tested
|
||||||
|
- [ ] Scaffold->fill testing flow completed and validated
|
||||||
|
- [ ] Full suite passes: `uv run pytest -q`
|
||||||
|
|
||||||
|
## Completion Checks (NiceGUI skill aligned)
|
||||||
|
|
||||||
|
- [ ] Uses app factory and FastAPI lifespan
|
||||||
|
- [ ] Pages are modularized (not single-file UI)
|
||||||
|
- [ ] Health endpoint exists on FastAPI side
|
||||||
|
- [ ] Dependency direction is clean and one-way
|
||||||
|
- [ ] Async-first guidance is applied where I/O exists, with explicit non-blocking UX states
|
||||||
|
- [ ] DB/AI/docs decisions are explicit and reflected in structure
|
||||||
|
- [ ] Plan references baseline URI: `resource://skills/nicegui/document`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR Checklist (Integrated)
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- [ ] `app.py` app factory + lifespan implemented
|
||||||
|
- [ ] FastAPI health route (`/healthz`) implemented
|
||||||
|
- [ ] `ui/upload_page.py` implemented
|
||||||
|
- [ ] `ui/jobs_page.py` implemented
|
||||||
|
- [ ] `ui/__init__.py` explicit page registration implemented
|
||||||
|
- [ ] Worker startup/shutdown managed by lifespan
|
||||||
|
|
||||||
|
### Testing (MCP-compliant)
|
||||||
|
- [ ] Scaffold phase completed first for all Step 5 tests
|
||||||
|
- [ ] `--collect-only` passed on scaffolds
|
||||||
|
- [ ] Fill phase completed without renaming/re-nesting scaffolded tests
|
||||||
|
- [ ] Marker decisions documented (`unit` vs `integration`)
|
||||||
|
- [ ] Targeted tests passed
|
||||||
|
- [ ] Full suite passed
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
- [ ] Validation command outputs captured
|
||||||
|
- [ ] Files created/updated listed
|
||||||
|
- [ ] MCP prompt resources referenced in implementation notes
|
||||||
|
- [ ] Any residual risks/questions documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
Step 6 implemented ✅
|
||||||
|
|
||||||
|
### Files added
|
||||||
|
- `tests/integration/test_pipeline_flow.py`
|
||||||
|
- `tests/test_traceability.py`
|
||||||
|
|
||||||
|
### What was implemented
|
||||||
|
|
||||||
|
#### 1) End-to-end pipeline integration coverage
|
||||||
|
`tests/integration/test_pipeline_flow.py` now verifies:
|
||||||
|
|
||||||
|
- **Success path**
|
||||||
|
- upload creates queued job
|
||||||
|
- worker processes job
|
||||||
|
- job becomes `transcribed`
|
||||||
|
- transcript text is persisted
|
||||||
|
- error detail remains `None`
|
||||||
|
|
||||||
|
- **Failure path**
|
||||||
|
- upload creates queued job
|
||||||
|
- worker processing failure is handled
|
||||||
|
- job becomes `failed`
|
||||||
|
- transcript has `text=None`
|
||||||
|
- `error_detail` contains provider failure reason
|
||||||
|
|
||||||
|
#### 2) MVP requirement traceability check
|
||||||
|
`tests/test_traceability.py` adds a concrete map:
|
||||||
|
|
||||||
|
- REQ-0, REQ-1, REQ-2, REQ-3, REQ-4, REQ-5, REQ-6, REQ-8, REQ-12
|
||||||
|
- each REQ maps to one or more existing test files
|
||||||
|
- test asserts each mapped path exists (guards against drift)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation run results
|
||||||
|
|
||||||
|
Executed and passed:
|
||||||
|
|
||||||
|
1. `uv run pytest --collect-only -q` ✅
|
||||||
|
2. `uv run pytest -m unit -q` ✅
|
||||||
|
3. `uv run pytest -m integration -q` ✅
|
||||||
|
4. `uv run pytest -m "not external" -q` ✅
|
||||||
|
5. `uv run pytest tests/integration/test_pipeline_flow.py -q` ✅
|
||||||
|
6. `uv run pytest tests/ui/test_upload_page.py -q` ✅
|
||||||
|
7. `uv run pytest tests/ui/test_jobs_page.py -q` ✅
|
||||||
|
8. `uv run pytest -q` ✅
|
||||||
|
|
||||||
|
All green.
|
||||||
|
(Only existing non-blocking FastAPI TestClient deprecation warning remains.)
|
||||||
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
## Step 6: Test and Verification Hardening (MVP closeout)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Complete MVP verification by building a **requirements-traceable, deterministic test strategy** across unit/integration/external lanes, then enforcing stable validation commands and reporting.
|
||||||
|
|
||||||
|
This step finalizes the MVP implementation sequence from `docs/mvp.md` (Step 6 in the build order: tests and automated verification).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Resource Integration (what was applied)
|
||||||
|
|
||||||
|
I reviewed all top-level skills/prompts from `john-stream-mcp` and integrated the relevant guidance into this plan:
|
||||||
|
|
||||||
|
### Directly applied
|
||||||
|
- `resource://skills/pytesting/document`
|
||||||
|
- `resource://catalog/prompts/pytest-scaffold`
|
||||||
|
- `resource://prompts/pytest-scaffold/document`
|
||||||
|
- `resource://catalog/prompts/pytest-fill-scaffold`
|
||||||
|
- `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
- `resource://skills/nicegui/document`
|
||||||
|
- `resource://skills/nicegui-ui-customization/document`
|
||||||
|
- `resource://skills/fastapi-uv-docker/document`
|
||||||
|
- `resource://skills/python-logging-dictconfig/document`
|
||||||
|
- `resource://skills/python-typing/document`
|
||||||
|
- `resource://skills/ruff-linting-formating/document`
|
||||||
|
|
||||||
|
### Reviewed but informational/non-blocking for Step 6
|
||||||
|
- `copilot-customization`, `mcp-details`, `vscode-configuration`, `zensical-docs`, and authoring/shim prompts.
|
||||||
|
- These are primarily customization/documentation tooling resources, not core MVP test-lane blockers.
|
||||||
|
- Step 6 includes optional workflow follow-ups where relevant (e.g., VS Code task conveniences).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- Strengthen and complete test coverage for the shipped MVP slice (Steps 1–5)
|
||||||
|
- Add requirement-to-test traceability for REQ-0..REQ-12 (MVP subset emphasized)
|
||||||
|
- Enforce deterministic default lanes (`unit`, `integration`)
|
||||||
|
- Keep `external` lane opt-in and isolated
|
||||||
|
- Validate app/UI/service/worker contracts end-to-end at test level
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Major architecture rewrites (async SQLAlchemy migration, queue system, etc.)
|
||||||
|
- Full production deployment rollout
|
||||||
|
- Post-MVP feature expansion (revision history, search, export)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planned Deliverables
|
||||||
|
|
||||||
|
### Test files (new/updated)
|
||||||
|
- `tests/test_traceability.py` *(or docs-based traceability matrix if preferred)*
|
||||||
|
- `tests/integration/test_pipeline_flow.py` *(upload -> queued -> worker -> transcript/failed)*
|
||||||
|
- `tests/ui/test_upload_page.py` (augment loading/error/ready-state checks as practical)
|
||||||
|
- `tests/ui/test_jobs_page.py` (augment refresh/error behavior checks as practical)
|
||||||
|
- Existing tests touched only when needed; preserve naming/hierarchy unless explicitly approved.
|
||||||
|
|
||||||
|
### Optional docs output
|
||||||
|
- `docs/tests.md` or `docs/verification.md` with lane definitions and command matrix
|
||||||
|
- REQ-to-test mapping table
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design and Policy Decisions (MCP-aligned)
|
||||||
|
|
||||||
|
1. **Scaffold-first, fill-second workflow is mandatory**
|
||||||
|
- First create/adjust skeletons and collect.
|
||||||
|
- Then fill test bodies.
|
||||||
|
- Preserve scaffold names/docstrings during fill.
|
||||||
|
|
||||||
|
2. **Deterministic-first default lanes**
|
||||||
|
- `unit` and `integration` run by default.
|
||||||
|
- `external` remains explicit opt-in.
|
||||||
|
|
||||||
|
3. **One behavior target per test**
|
||||||
|
- Short, behavior-focused names.
|
||||||
|
- Precise assertions on observable outcomes.
|
||||||
|
|
||||||
|
4. **Test double discipline (from pytesting skill)**
|
||||||
|
- Prefer real-input/real-object paths first.
|
||||||
|
- If monkeypatch/mocks/fakes are needed for a boundary, keep narrowly scoped.
|
||||||
|
- Avoid call-only assertions.
|
||||||
|
|
||||||
|
5. **NiceGUI responsiveness expectations**
|
||||||
|
- Verify loading/success/error state transitions where testable.
|
||||||
|
- Ensure user-facing feedback behavior is covered.
|
||||||
|
|
||||||
|
6. **FastAPI/ops baseline checks**
|
||||||
|
- Keep `/healthz` route validation in default lanes.
|
||||||
|
- Keep startup/shutdown lifecycle assertions present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan + Checklist
|
||||||
|
|
||||||
|
## Phase A — Coverage and traceability audit
|
||||||
|
|
||||||
|
- [ ] Build a REQ-to-test matrix for MVP requirements:
|
||||||
|
- [ ] REQ-0, REQ-1, REQ-2, REQ-3, REQ-4, REQ-5, REQ-6, REQ-8, REQ-12
|
||||||
|
- [ ] Identify weak spots:
|
||||||
|
- [ ] full pipeline integration (service + worker + persistence)
|
||||||
|
- [ ] UI state transition assertions (loading/error/ready)
|
||||||
|
- [ ] failure-path persistence verification robustness
|
||||||
|
- [ ] Record current baseline command results before edits
|
||||||
|
|
||||||
|
## Phase B — Scaffold phase (pytest-scaffold resources)
|
||||||
|
|
||||||
|
Target modules/areas:
|
||||||
|
- pipeline integration flow
|
||||||
|
- UI behavior augmentations
|
||||||
|
- traceability checks/document validators (if test-backed)
|
||||||
|
|
||||||
|
- [ ] Scaffold new/adjusted test files/classes/methods only
|
||||||
|
- [ ] Keep one-line intent docstrings
|
||||||
|
- [ ] Keep behavior-focused names
|
||||||
|
- [ ] Run: `uv run pytest --collect-only -q`
|
||||||
|
|
||||||
|
## Phase C — Fill phase (pytest-fill-scaffold resources)
|
||||||
|
|
||||||
|
- [ ] Fill scaffolded methods with deterministic setup/assertions
|
||||||
|
- [ ] Preserve scaffold names/hierarchy/docstrings
|
||||||
|
- [ ] Add/adjust fixtures at nearest useful scope
|
||||||
|
- [ ] Keep DB tests in `integration`; pure helper tests in `unit`
|
||||||
|
|
||||||
|
### Required coverage additions
|
||||||
|
|
||||||
|
#### Pipeline integration
|
||||||
|
- [ ] Upload service creates document/job and file path persists
|
||||||
|
- [ ] Worker success path creates transcript and terminal status
|
||||||
|
- [ ] Worker failure path persists error detail and terminal failed status
|
||||||
|
- [ ] Queue-empty behavior remains stable (`False` return / no side effects)
|
||||||
|
|
||||||
|
#### UI behavior (practical, testable boundaries)
|
||||||
|
- [ ] Upload helper flow success and UploadError surfacing
|
||||||
|
- [ ] Jobs data helpers return stable normalized view models
|
||||||
|
- [ ] Refresh/detail fallback behavior for missing/invalid job IDs
|
||||||
|
|
||||||
|
#### Traceability
|
||||||
|
- [ ] Every in-scope MVP REQ has at least one mapped test/assertion point
|
||||||
|
- [ ] Document and/or enforce mapping consistency
|
||||||
|
|
||||||
|
## Phase D — External lane stability
|
||||||
|
|
||||||
|
- [ ] Keep real-image external tests isolated under `@pytest.mark.external`
|
||||||
|
- [ ] Ensure no external test leaks into default runs
|
||||||
|
- [ ] Confirm artifact capture behavior remains stable
|
||||||
|
|
||||||
|
## Phase E — Quality gates and workflow
|
||||||
|
|
||||||
|
- [ ] Confirm logging/lifecycle startup tests still pass after changes
|
||||||
|
- [ ] (If enabled) add/update lint/type check commands in docs:
|
||||||
|
- [ ] Ruff lane (if configured)
|
||||||
|
- [ ] typing lane (if configured)
|
||||||
|
- [ ] Optionally add VS Code task aliases for test lanes (non-blocking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Marker and Fixture Strategy
|
||||||
|
|
||||||
|
- `unit`: pure logic, helper behavior, formatting/normalization
|
||||||
|
- `integration`: DB + service + app lifecycle contracts
|
||||||
|
- `external`: live provider/real image checks only
|
||||||
|
|
||||||
|
Fixture policy:
|
||||||
|
- Prefer reusable fixtures in `tests/conftest.py` only when broadly shared
|
||||||
|
- Use subtree/local fixtures for domain-specific setup
|
||||||
|
- Keep setup explicit and readable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Sequence (strict)
|
||||||
|
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
- [ ] `uv run pytest -m unit -q`
|
||||||
|
- [ ] `uv run pytest -m integration -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
- [ ] `uv run pytest tests/integration/test_pipeline_flow.py -q` *(if added)*
|
||||||
|
- [ ] `uv run pytest tests/ui/test_upload_page.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_jobs_page.py -q`
|
||||||
|
- [ ] `uv run pytest -q`
|
||||||
|
|
||||||
|
Optional external verification:
|
||||||
|
- [ ] `uv run pytest -m external -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- Do not rename/re-nest scaffolded tests during fill unless explicitly requested.
|
||||||
|
- Do not broaden external dependencies in default lane.
|
||||||
|
- Do not add flaky timing-based assertions; keep deterministic boundaries.
|
||||||
|
- Keep business logic out of UI tests; test through service/helper boundaries.
|
||||||
|
- Preserve one-way dependency direction in test setup patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done (Step 6)
|
||||||
|
|
||||||
|
- [ ] MVP requirement coverage is explicitly traceable
|
||||||
|
- [ ] Deterministic lanes (`unit` + `integration`) are stable and green
|
||||||
|
- [ ] External lane remains opt-in and green when enabled
|
||||||
|
- [ ] Pipeline success/failure lifecycle paths are verified end-to-end
|
||||||
|
- [ ] UI helper/state behavior has explicit success/error assertions
|
||||||
|
- [ ] Full suite passes with `uv run pytest -q`
|
||||||
|
- [ ] Verification evidence is captured in implementation report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR Checklist (Step 6)
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- [ ] Added/updated test files per scoped gaps
|
||||||
|
- [ ] Added REQ traceability mapping
|
||||||
|
- [ ] Kept default lanes deterministic
|
||||||
|
- [ ] Preserved scaffold invariants during fill
|
||||||
|
|
||||||
|
### Testing (MCP-compliant)
|
||||||
|
- [ ] Used scaffold prompt flow first
|
||||||
|
- [ ] Used fill prompt flow second
|
||||||
|
- [ ] Preserved naming/docstrings/hierarchy
|
||||||
|
- [ ] Marker usage documented (`unit`, `integration`, `external`)
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
- [ ] Collected command outputs in strict order
|
||||||
|
- [ ] Listed files changed
|
||||||
|
- [ ] Listed MCP resources used and why
|
||||||
|
- [ ] Noted residual risks/open questions (if any)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
## Step 7 Results: Error Handling Standardization and Operational Visibility
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 7 was implemented across the MVP runtime boundaries with a shared error taxonomy, actionable UI error surfacing, worker failure normalization, and API error envelope handling.
|
||||||
|
|
||||||
|
All required validation gates in `docs/step7.md` were executed and passed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope Delivered
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
- Shared application error contract and taxonomy
|
||||||
|
- Service-layer error normalization (upload + transcription)
|
||||||
|
- UI error presentation helpers with suggested actions and error references
|
||||||
|
- Worker failure persistence format with category/suggestion/error_id markers
|
||||||
|
- API exception handlers for structured error responses
|
||||||
|
- Targeted tests for new error contract behavior
|
||||||
|
|
||||||
|
### Not implemented in this step
|
||||||
|
- External lane execution (`-m external`) was not required for Step 7 completion and was not run in this pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Added
|
||||||
|
|
||||||
|
- `src/transcription/errors.py`
|
||||||
|
- `src/transcription/api/errors.py`
|
||||||
|
- `src/transcription/ui/error_presenter.py`
|
||||||
|
- `tests/test_errors.py`
|
||||||
|
- `tests/api/test_error_responses.py`
|
||||||
|
- `docs/step7.md`
|
||||||
|
|
||||||
|
## Files Updated
|
||||||
|
|
||||||
|
- `src/transcription/app.py`
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- `src/transcription/services/transcription.py`
|
||||||
|
- `src/transcription/ui/upload_page.py`
|
||||||
|
- `src/transcription/ui/jobs_page.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
- `tests/services/test_upload.py`
|
||||||
|
- `tests/services/test_transcription.py`
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
- `tests/integration/test_pipeline_flow.py`
|
||||||
|
- `uv.lock`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes by Phase
|
||||||
|
|
||||||
|
### Phase A/B (Foundation)
|
||||||
|
- Added `ErrorCategory` enum and `AppError` base type in `src/transcription/errors.py`.
|
||||||
|
- Added helper utilities:
|
||||||
|
- `new_error_id()`
|
||||||
|
- `build_error_envelope(...)`
|
||||||
|
- `classify_unexpected_error(...)`
|
||||||
|
- `format_error_detail(...)`
|
||||||
|
|
||||||
|
### Phase C (Service/Provider normalization)
|
||||||
|
- `UploadError` now extends `AppError` and includes category/suggestion/retriable metadata.
|
||||||
|
- `PromptLoadError` and `TranscriptionError` now extend `AppError`.
|
||||||
|
- Provider failures are mapped with deterministic category semantics (auth/payload/provider-failure cases).
|
||||||
|
|
||||||
|
### Phase D (UI visibility)
|
||||||
|
- Added `src/transcription/ui/error_presenter.py`.
|
||||||
|
- Upload and jobs pages now use centralized UI error rendering and summary helpers.
|
||||||
|
- UI error paths now include more visible/actionable guidance and reference IDs.
|
||||||
|
|
||||||
|
### Phase E (Worker failure handling)
|
||||||
|
- Worker now normalizes exception handling into structured persisted `error_detail` strings with:
|
||||||
|
- category marker
|
||||||
|
- suggestion marker
|
||||||
|
- error_id marker
|
||||||
|
- Logging now includes category/error_id context in failure paths.
|
||||||
|
|
||||||
|
### Phase F (API envelope)
|
||||||
|
- Added `src/transcription/api/errors.py` and registered handlers in app factory.
|
||||||
|
- AppError and unexpected exceptions now serialize to stable API envelopes with mapped status codes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Commands and Outcomes
|
||||||
|
|
||||||
|
All commands were executed with `uv run python -m pytest ...` and completed successfully.
|
||||||
|
|
||||||
|
1. `uv run python -m pytest tests/test_errors.py -q` ✅
|
||||||
|
2. `uv run python -m pytest tests/services/test_upload.py -q` ✅
|
||||||
|
3. `uv run python -m pytest tests/services/test_transcription.py -q` ✅
|
||||||
|
4. `uv run python -m pytest tests/providers/test_openrouter.py -q` ✅
|
||||||
|
5. `uv run python -m pytest tests/services/test_worker.py -q` ✅
|
||||||
|
6. `uv run python -m pytest tests/integration/test_pipeline_flow.py -q` ✅
|
||||||
|
7. `uv run python -m pytest tests/api/test_error_responses.py -q` ✅
|
||||||
|
8. `uv run python -m pytest tests/ui/test_upload_page.py -q` ✅
|
||||||
|
9. `uv run python -m pytest tests/ui/test_jobs_page.py -q` ✅
|
||||||
|
10. `uv run python -m pytest -m "not external" -q` ✅
|
||||||
|
11. `uv run python -m pytest --collect-only -q` ✅
|
||||||
|
12. `uv run python -m pytest -m unit -q` ✅
|
||||||
|
13. `uv run python -m pytest -m integration -q` ✅
|
||||||
|
14. `uv run python -m pytest tests/integration/test_pipeline_flow.py -q` ✅
|
||||||
|
15. `uv run python -m pytest tests/ui/test_upload_page.py -q` ✅
|
||||||
|
16. `uv run python -m pytest tests/ui/test_jobs_page.py -q` ✅
|
||||||
|
17. `uv run python -m pytest -q` ✅
|
||||||
|
|
||||||
|
Observed warning (non-blocking): Starlette/FastAPI TestClient deprecation warning related to `httpx` package naming.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Policy Alignment Check (`docs/error_handling.md`)
|
||||||
|
|
||||||
|
Aligned items:
|
||||||
|
- Stable taxonomy categories are implemented.
|
||||||
|
- Unexpected errors are normalized.
|
||||||
|
- User-facing UI paths include actionable guidance and references.
|
||||||
|
- Worker persistence includes trace-friendly failure detail.
|
||||||
|
- API error responses are structured and category-aware.
|
||||||
|
|
||||||
|
Follow-up candidates:
|
||||||
|
- Add richer UI tests that validate rendered suggested-action content end-to-end (current tests focus helper/service contracts).
|
||||||
|
- Consider typed storage fields for error metadata instead of packed `error_detail` strings in a future schema revision.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7 Definition of Done Status
|
||||||
|
|
||||||
|
- [x] Shared error taxonomy implemented across MVP layers
|
||||||
|
- [x] GUI error paths upgraded for visibility/actionability
|
||||||
|
- [x] Worker failure persistence and log context standardized
|
||||||
|
- [x] API error envelope handling added and tested
|
||||||
|
- [x] Phase-level and full-suite validation gates passed
|
||||||
|
- [x] Results documented in this report
|
||||||
|
|
||||||
|
Step 7 is complete.
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
## Step 7: Error Handling Standardization and Operational Visibility
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Apply the canonical error policy from `docs/error_handling.md` to the MVP implementation so failures are:
|
||||||
|
|
||||||
|
- consistently classified
|
||||||
|
- visibly surfaced in the GUI
|
||||||
|
- paired with suggested corrective actions
|
||||||
|
- traceable through logs via error reference IDs
|
||||||
|
- validated through deterministic tests after each phase
|
||||||
|
|
||||||
|
This step extends MVP hardening by converting current ad hoc exception behavior into a stable cross-layer contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- Introduce a shared application error contract and taxonomy implementation
|
||||||
|
- Normalize service/provider exceptions into taxonomy categories
|
||||||
|
- Improve GUI error visibility and suggested-action UX
|
||||||
|
- Standardize worker failure persistence and logging context
|
||||||
|
- Add API error-envelope policy hooks for current/future endpoints
|
||||||
|
- Add targeted tests and phase-level/full-suite validation gates
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Major architecture rewrites (distributed queue, multi-service decomposition)
|
||||||
|
- Post-MVP feature expansion unrelated to error handling
|
||||||
|
- Full observability platform rollout (tracing backends, APM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Policy Source of Truth
|
||||||
|
|
||||||
|
- Canonical policy document: `docs/error_handling.md`
|
||||||
|
- If implementation and policy diverge, policy is authoritative and code/tests must be updated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Planned Deliverables
|
||||||
|
|
||||||
|
### Runtime code
|
||||||
|
- `src/transcription/errors.py` *(new shared contract module)*
|
||||||
|
- `src/transcription/ui/error_presenter.py` *(new UI error rendering helper)*
|
||||||
|
- Updates to:
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- `src/transcription/services/transcription.py`
|
||||||
|
- `src/transcription/providers/openrouter.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
- `src/transcription/ui/upload_page.py`
|
||||||
|
- `src/transcription/ui/jobs_page.py`
|
||||||
|
- `src/transcription/api/*` *(as needed for envelope/handlers)*
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- `tests/test_errors.py` *(new shared error contract tests)*
|
||||||
|
- updates/additions in:
|
||||||
|
- `tests/services/test_upload.py`
|
||||||
|
- `tests/services/test_transcription.py` *(add if missing)*
|
||||||
|
- `tests/providers/test_openrouter.py`
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
- `tests/ui/test_upload_page.py`
|
||||||
|
- `tests/ui/test_jobs_page.py`
|
||||||
|
- `tests/api/test_error_responses.py` *(new, if API handlers added)*
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Update `docs/error_handling.md` only if implementation reveals policy gaps
|
||||||
|
- Capture validation evidence in a Step 7 results artifact (`docs/step7-results.md`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design and Policy Decisions
|
||||||
|
|
||||||
|
1. **Stable taxonomy contract**
|
||||||
|
- Use policy categories as stable identifiers (`validation_error`, `user_input_error`, etc.).
|
||||||
|
|
||||||
|
2. **Actionable UX is mandatory**
|
||||||
|
- User-visible errors must include a suggested course of action.
|
||||||
|
|
||||||
|
3. **Traceability by default**
|
||||||
|
- Non-trivial errors include an `error_id` in both logs and user-facing output.
|
||||||
|
|
||||||
|
4. **Safe surface / rich logs**
|
||||||
|
- UI/API show safe summaries; logs retain diagnostic detail and traceback.
|
||||||
|
|
||||||
|
5. **Deterministic verification cadence**
|
||||||
|
- Targeted tests after each change batch, then phase-level regression gates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan + Checklist
|
||||||
|
|
||||||
|
## Phase A — Baseline Validation and Gap Confirmation
|
||||||
|
|
||||||
|
- [ ] Run baseline tests before changes
|
||||||
|
- [ ] Record baseline outputs and any known flaky behavior
|
||||||
|
- [ ] Confirm current behavior against `docs/error_handling.md` requirements
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
- [ ] `uv run pytest -q`
|
||||||
|
|
||||||
|
## Phase B — Shared Error Contract Foundation
|
||||||
|
|
||||||
|
- [ ] Add `src/transcription/errors.py` with:
|
||||||
|
- [ ] stable category enum
|
||||||
|
- [ ] base `AppError` (category/message/suggestion/error_id/retriable)
|
||||||
|
- [ ] helpers for error-id generation and fallback classification
|
||||||
|
- [ ] Keep category names aligned with `docs/error_handling.md`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] Add `tests/test_errors.py`
|
||||||
|
- [ ] category stability assertions
|
||||||
|
- [ ] error_id creation behavior
|
||||||
|
- [ ] fallback classification for unexpected exceptions
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest tests/test_errors.py -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
|
||||||
|
## Phase C — Service and Provider Normalization
|
||||||
|
|
||||||
|
- [ ] Refactor upload service exceptions to shared taxonomy
|
||||||
|
- [ ] Refactor transcription service exceptions to shared taxonomy
|
||||||
|
- [ ] Normalize provider adapter failures into deterministic categories
|
||||||
|
- [ ] Preserve causal chaining (`raise ... from exc`)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] Extend `tests/services/test_upload.py`:
|
||||||
|
- [ ] empty payload category/suggestion
|
||||||
|
- [ ] unsupported extension category/suggestion
|
||||||
|
- [ ] persistence failure category mapping
|
||||||
|
- [ ] Add/extend `tests/services/test_transcription.py`:
|
||||||
|
- [ ] missing/empty prompt behavior
|
||||||
|
- [ ] unsupported file type behavior
|
||||||
|
- [ ] provider failure mapping behavior
|
||||||
|
- [ ] Extend `tests/providers/test_openrouter.py`:
|
||||||
|
- [ ] auth error mapping
|
||||||
|
- [ ] malformed response mapping
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest tests/services/test_upload.py -q`
|
||||||
|
- [ ] `uv run pytest tests/services/test_transcription.py -q`
|
||||||
|
- [ ] `uv run pytest tests/providers/test_openrouter.py -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
|
||||||
|
## Phase D — GUI Visibility and Suggested Actions
|
||||||
|
|
||||||
|
- [ ] Add `src/transcription/ui/error_presenter.py`
|
||||||
|
- [ ] Update upload/jobs pages to use centralized error presentation
|
||||||
|
- [ ] Ensure GUI surfaces:
|
||||||
|
- [ ] user-safe message
|
||||||
|
- [ ] suggested action
|
||||||
|
- [ ] error reference ID
|
||||||
|
- [ ] optional technical details panel
|
||||||
|
- [ ] Replace raw `str(exc)` UX where policy requires safer messaging
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] Extend `tests/ui/test_upload_page.py` for actionable error UX paths
|
||||||
|
- [ ] Extend `tests/ui/test_jobs_page.py` for refresh/detail error guidance
|
||||||
|
- [ ] Add `tests/ui/test_error_presenter.py` *(optional but recommended)*
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest tests/ui/test_upload_page.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_jobs_page.py -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
|
||||||
|
## Phase E — Worker Failure Persistence and Logging Context
|
||||||
|
|
||||||
|
- [ ] Update worker failure handling to classify errors before persistence
|
||||||
|
- [ ] Ensure failed jobs persist actionable, structured error detail
|
||||||
|
- [ ] Add log context fields where available (`error_id`, `category`, `operation`, `job_id`)
|
||||||
|
- [ ] Ensure retry semantics are explicit and bounded (or clearly documented as deferred)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] Extend `tests/services/test_worker.py`:
|
||||||
|
- [ ] missing document failure contract
|
||||||
|
- [ ] provider/transcription failure contract
|
||||||
|
- [ ] persisted error detail includes category/suggestion/error_id markers
|
||||||
|
- [ ] Validate integration failure flow in `tests/integration/test_pipeline_flow.py`
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest tests/services/test_worker.py -q`
|
||||||
|
- [ ] `uv run pytest tests/integration/test_pipeline_flow.py -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
|
||||||
|
## Phase F — API Error Envelope Alignment (Current + Future Routes)
|
||||||
|
|
||||||
|
- [ ] Add shared API error serialization utilities/handlers (as needed)
|
||||||
|
- [ ] Ensure API responses can include:
|
||||||
|
- [ ] `error_id`
|
||||||
|
- [ ] `category`
|
||||||
|
- [ ] `message`
|
||||||
|
- [ ] `suggestion`
|
||||||
|
- [ ] `timestamp`
|
||||||
|
- [ ] Map categories to HTTP status guidance from `docs/error_handling.md`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] Add `tests/api/test_error_responses.py` *(if handlers added)*
|
||||||
|
- [ ] Keep `tests/api/test_health.py` passing
|
||||||
|
|
||||||
|
### Validation gate
|
||||||
|
- [ ] `uv run pytest tests/api/test_error_responses.py -q` *(if added)*
|
||||||
|
- [ ] `uv run pytest tests/api/test_health.py -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
|
||||||
|
## Phase G — Final Regression and Documentation Closure
|
||||||
|
|
||||||
|
- [ ] Reconcile implementation details with `docs/error_handling.md`
|
||||||
|
- [ ] Update policy doc only where required by confirmed implementation learning
|
||||||
|
- [ ] Capture execution evidence in `docs/step7-results.md`
|
||||||
|
|
||||||
|
### Final validation sequence (strict)
|
||||||
|
- [ ] `uv run pytest --collect-only -q`
|
||||||
|
- [ ] `uv run pytest -m unit -q`
|
||||||
|
- [ ] `uv run pytest -m integration -q`
|
||||||
|
- [ ] `uv run pytest -m "not external" -q`
|
||||||
|
- [ ] `uv run pytest tests/integration/test_pipeline_flow.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_upload_page.py -q`
|
||||||
|
- [ ] `uv run pytest tests/ui/test_jobs_page.py -q`
|
||||||
|
- [ ] `uv run pytest -q`
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
- [ ] `uv run pytest -m external -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- Do not weaken user-facing clarity to expose raw internals.
|
||||||
|
- Do not introduce silent exception swallowing.
|
||||||
|
- Do not break category-name stability without policy update.
|
||||||
|
- Do not merge phase changes without passing that phase validation gate.
|
||||||
|
- Keep targeted tests fast and deterministic; isolate external-provider tests under `external`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done (Step 7)
|
||||||
|
|
||||||
|
- [ ] Shared error taxonomy is implemented and used across MVP layers
|
||||||
|
- [ ] GUI error experiences are visible, actionable, and traceable
|
||||||
|
- [ ] Worker persists and logs failure context consistently
|
||||||
|
- [ ] API error contract path is aligned for current/future endpoints
|
||||||
|
- [ ] Phase-by-phase test gates pass
|
||||||
|
- [ ] Full suite remains green (`uv run pytest -q`)
|
||||||
|
- [ ] Step 7 results are documented with evidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR Checklist (Step 7)
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- [ ] Added shared error contract module
|
||||||
|
- [ ] Updated service/provider/worker/UI error handling paths
|
||||||
|
- [ ] Added actionable GUI guidance for user-visible failures
|
||||||
|
- [ ] Added error reference IDs for traceability
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Added/updated tests per phase scope
|
||||||
|
- [ ] Ran targeted phase tests after each change batch
|
||||||
|
- [ ] Ran `not external` regression at each phase boundary
|
||||||
|
- [ ] Ran full suite before closeout
|
||||||
|
|
||||||
|
### Documentation and Evidence
|
||||||
|
- [ ] `docs/error_handling.md` reviewed for alignment
|
||||||
|
- [ ] `docs/step7-results.md` includes executed command outputs
|
||||||
|
- [ ] Residual risks and deferred items explicitly recorded
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
## MVP Definition: Historical Document Transcription System
|
||||||
|
|
||||||
|
### 1. MVP Objective
|
||||||
|
Deliver the thinnest possible end-to-end vertical slice — a user uploads an image of a document, the system transcribes it via the OpenRouter Python SDK, and the user reads the resulting transcript — with just enough persistence and structure to validate the core value proposition: *can AI-driven transcription, guided by curated prompts, produce useful verbatim transcripts of historical family documents?*
|
||||||
|
|
||||||
|
The MVP deliberately defers full-text search, export, revision history, MongoDB, and timeline assembly. These are additive features that don't need validation before the core transcription loop is proven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Core User Story
|
||||||
|
*As a family historian, I can upload a photo of a historical document, wait for it to be transcribed, and read the verbatim transcript — so I can evaluate whether this system will work for my thousands of documents.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. In-Scope Requirements (from ```requirements.md```)
|
||||||
|
|
||||||
|
| Requirement | ID | MVP Rationale |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| End-to-end transcription with lifecycle state | REQ-0 | This is the MVP. |
|
||||||
|
| Upload one or more images from the web UI | REQ-1 | Core entry point. MVP supports single-image upload (multi-image is a stretch goal). |
|
||||||
|
| Asynchronous processing → transcription or failure | REQ-2 | Validates the AI transcription pipeline. |
|
||||||
|
| Persist and expose job states (queued → processing → transcribed/failed) | REQ-3 | Minimum feedback loop for the user. |
|
||||||
|
| Persist transcription output and failure details | REQ-4 | User must be able to read the result. |
|
||||||
|
| UI views for status and transcript reading | REQ-5 | The user needs to see what happened. |
|
||||||
|
| Background processing to keep UI responsive | REQ-6 | Essential for usability during long AI calls. |
|
||||||
|
| Centralized config and logging at startup | REQ-8 | Small effort, high payoff for debugging. |
|
||||||
|
| Store transcription prompts as Markdown files | REQ-12 | Core to the Prompt Curation Policy in intent.md. Start with a single prompt file. |
|
||||||
|
|
||||||
|
|
||||||
|
### Deferred to Post-MVP
|
||||||
|
| Requirement | ID | Why Deferred |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Lifespan-owned runtime resources (engine, session factory, etc.) | REQ-7 | Important for production robustness, but a simple global or module-level setup is adequate for MVP validation. |
|
||||||
|
| Docker Compose (app + PostgreSQL + optional MongoDB) | REQ-9 | MVP runs locally with SQLite to eliminate container overhead during rapid iteration. PostgreSQL migration is Stage 1 hardening. |
|
||||||
|
| Explicit, opt-in schema bootstrap | REQ-10 | MVP uses auto-create-tables at startup (SQLModel create_all). Production schema discipline comes after the model stabilizes. |
|
||||||
|
| Service-backed persistence for core data | REQ-11 | MVP uses a thin repository layer over SQLite. Full service abstraction follows once the domain model is proven. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. MVP Feature Set
|
||||||
|
#### Feature 1: Document Upload (UI)
|
||||||
|
* A single NiceGUI page with a file-upload widget (accepts .jpg, .png, .tiff, .pdf).
|
||||||
|
* On upload: save the file to a local uploads/ directory, create a Document record, create a Job record with status queued.
|
||||||
|
* Minimal metadata capture: original filename, upload timestamp.
|
||||||
|
|
||||||
|
#### Feature 2: Asynchronous Transcription Worker
|
||||||
|
* An in-process background worker (Python asyncio task or BackgroundTasks) that:
|
||||||
|
1. Picks up queued jobs.
|
||||||
|
2. Transitions status to processing.
|
||||||
|
3. Sends the image + the curated Markdown prompt to an AI vision model via OpenRouter.
|
||||||
|
4. On success: saves the transcript text, transitions to transcribed.
|
||||||
|
5. On failure: saves the error detail, transitions to failed.
|
||||||
|
|
||||||
|
#### Feature 3: Transcription Prompt (Markdown Asset)
|
||||||
|
* A single Markdown file (prompts/transcribe_document.md) encoding the verbatim transcription rules from intent.md (the Document Issues table, scholarly guidelines, etc.).
|
||||||
|
* The worker reads this file at invocation time and injects it as the system/user prompt.
|
||||||
|
|
||||||
|
#### Feature 4: Job Status & Transcript Viewer (UI)
|
||||||
|
* A job list page showing all jobs with their current status (queued / processing / transcribed / failed).
|
||||||
|
* A transcript detail page showing:
|
||||||
|
* The original uploaded image (rendered inline).
|
||||||
|
* The transcription text (or the failure reason).
|
||||||
|
* Timestamp metadata.
|
||||||
|
|
||||||
|
#### Feature 5: Minimal Persistence (SQLite + SQLModel)
|
||||||
|
* Three tables/models:
|
||||||
|
* Document: id, filename, file_path, uploaded_at.
|
||||||
|
* Job: id, document_id (FK), status, created_at, updated_at.
|
||||||
|
* Transcript: id, job_id (FK), text, error_detail, created_at.
|
||||||
|
* SQLite database file stored locally. Auto-created on first startup.
|
||||||
|
|
||||||
|
#### Feature 6: Centralized Configuration
|
||||||
|
* A single config.py (or Pydantic BaseSettings) loading:
|
||||||
|
* PROVIDER (fixed to openrouter for MVP)
|
||||||
|
* OPENROUTER_API_KEY (required)
|
||||||
|
* PROVIDER_MODEL (default: OpenRouter model slug for vision transcription)
|
||||||
|
* OPENROUTER_HTTP_REFERER (optional; app attribution)
|
||||||
|
* OPENROUTER_APP_TITLE (optional; app attribution)
|
||||||
|
* DATABASE_URL (default: sqlite:///./transcription.db)
|
||||||
|
* UPLOAD_DIR (default: ./uploads)
|
||||||
|
* PROMPT_DIR (default: ./prompts)
|
||||||
|
|
||||||
|
#### Feature 7: MVP Dependency Baseline (OpenRouter-Centric)
|
||||||
|
* Runtime dependencies:
|
||||||
|
* openrouter (official OpenRouter Python SDK)
|
||||||
|
* pydantic
|
||||||
|
* pydantic-settings
|
||||||
|
* sqlmodel
|
||||||
|
* Explicitly out of MVP runtime dependencies:
|
||||||
|
* google-genai (deferred until/if Gemini is introduced post-MVP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. MVP Architecture (Simplified)
|
||||||
|
|
||||||
|
```Apply
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ NiceGUI Web UI │
|
||||||
|
│ ┌──────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ Upload Page │ │ Jobs / Transcript │ │
|
||||||
|
│ └──────┬───────┘ └───────┬───────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌───────────────────────────┐ │
|
||||||
|
│ │ Application Service │ │
|
||||||
|
│ │ (upload, job lifecycle) │ │
|
||||||
|
│ └─────┬─────────────┬───────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌─────▼─────┐ ┌─────▼───────────────┐ │
|
||||||
|
│ │ SQLite DB │ │ Background Worker │ │
|
||||||
|
│ │ (SQLModel)│ │ → AI Vision Provider│ │
|
||||||
|
│ └───────────┘ └─────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────▼──────┐ │
|
||||||
|
│ │ prompts/ │ │
|
||||||
|
│ │ *.md files │ │
|
||||||
|
│ └────────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. Proposed File Structure
|
||||||
|
|
||||||
|
```Apply
|
||||||
|
project-root/
|
||||||
|
├── docs/ # (existing)
|
||||||
|
├── prompts/
|
||||||
|
│ └── transcribe_document.md # curated transcription prompt
|
||||||
|
├── src/
|
||||||
|
│ └── transcription/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── app.py # FastAPI + NiceGUI app entrypoint
|
||||||
|
│ ├── config.py # Pydantic BaseSettings
|
||||||
|
│ ├── models.py # SQLModel: Document, Job, Transcript
|
||||||
|
│ ├── db.py # engine, session, create_all
|
||||||
|
│ ├── providers/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── base.py # provider interface (transcribe contract)
|
||||||
|
│ │ ├── openrouter.py # OpenRouter via official Python SDK
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── upload.py # save file + create records
|
||||||
|
│ │ └── transcription.py # call provider, update job
|
||||||
|
│ ├── worker.py # background job loop
|
||||||
|
│ └── ui/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── upload_page.py # NiceGUI upload page
|
||||||
|
│ └── jobs_page.py # NiceGUI job list + detail
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_models.py
|
||||||
|
│ ├── test_upload.py
|
||||||
|
│ └── test_transcription.py
|
||||||
|
├── pyproject.toml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. MVP Validation Criteria
|
||||||
|
The MVP is considered validated when:
|
||||||
|
|
||||||
|
1. ✅ A user can upload an image of a document through the browser.
|
||||||
|
2. ✅ The system asynchronously sends the image to the configured AI vision model with the curated prompt.
|
||||||
|
3. ✅ The transcript (or failure reason) is persisted and visible in the UI.
|
||||||
|
4. ✅ The transcription follows verbatim scholarly rules defined in intent.md (spot-checked by the user on real family documents).
|
||||||
|
5. ✅ The transcription prompt is stored as a standalone Markdown file and can be edited without code changes.
|
||||||
|
6. ✅ Job status transitions are visible: queued → processing → transcribed/failed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Key Feedback Questions the MVP Should Answer
|
||||||
|
These are the real unknowns this MVP exists to resolve:
|
||||||
|
|
||||||
|
| # | Question | How We Learn |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Is AI transcription quality good enough for this document corpus? | User reviews 20–50 real transcriptions against originals. |
|
||||||
|
| 2 | Does the verbatim prompt produce scholarly-quality output, or does it need major rework? | Compare output to the Document Issues table rules in intent.md. |
|
||||||
|
| 3 | What document types are hardest (old cursive, faded ink, pencil, postcards)? | Track which uploads produce failed or low-quality results. |
|
||||||
|
| 4 | Is single-image upload sufficient, or is batch upload needed early? | User friction during real scanning sessions. |
|
||||||
|
| 5 | What metadata is missing that the user wishes they could capture at upload time? | User feedback after processing real batches. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 9. What Comes After MVP (Immediate Post-MVP)
|
||||||
|
Once the core transcription loop is validated, the next priorities (aligned to Architecture Stage 1) are:
|
||||||
|
|
||||||
|
1. **Multi-image upload** — process a batch from a scanning session.
|
||||||
|
2. **PostgreSQL migration** — swap SQLite for containerized PostgreSQL (REQ-9, REQ-10).
|
||||||
|
3. **Revision history** — allow the user to edit/correct transcripts with immutable version tracking.
|
||||||
|
4. **Full-text search** — search across all accepted transcripts.
|
||||||
|
5. **Repository/service layer formalization** — proper ports/adapters as the domain model stabilizes.
|
||||||
|
6. **Docker Compose deployment** — containerize the app for reproducible operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. Implementation Approach
|
||||||
|
Recommended build order for the MVP (each step produces a testable increment):
|
||||||
|
|
||||||
|
| Step | Deliverable | Validates |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | config.py + models.py + db.py — data layer with SQLite | Schema and config foundation |
|
||||||
|
| 2 | prompts/transcribe_document.md — curated prompt from intent.md | Prompt asset pattern |
|
||||||
|
| 3 | services/transcription.py + providers/ — call AI vision provider with prompt + image | Core AI integration |
|
||||||
|
| 4 | services/upload.py + worker.py — upload handling + background job loop | End-to-end pipeline (CLI-testable) |
|
||||||
|
| 5 | ui/upload_page.py + ui/jobs_page.py — NiceGUI pages | User-facing interface |
|
||||||
|
| 6 | tests/ — unit + integration tests Automated verification |
|
||||||
|
|
||||||
|
This MVP is deliberately narrow: **one prompt, one provider (OpenRouter), one user, one image at a time, SQLite, no containers**. Every omission is intentional — the goal is to get real family documents through the transcription pipeline as fast as possible and let the quality of the output guide every subsequent decision.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
## Document Transcription System Requirements
|
||||||
|
|
||||||
|
This page captures a SysML v1.6-style requirements baseline for the production system described in [index.md](index.md). The model is represented as concise tables and traceability lists that preserve SysML-style IDs and relationship semantics.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- System of interest: the single Python application service (NiceGUI + FastAPI) with PostgreSQL as the relational system of record and optional MongoDB for document-oriented persistence.
|
||||||
|
- Operational context: local-first execution with Docker Compose and an intentionally lightweight production trajectory.
|
||||||
|
- Primary concern: end-to-end transcription job lifecycle from upload through completion or failure.
|
||||||
|
|
||||||
|
## Requirements Model (Concise Text Form)
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
| ID | Category | Requirement | Risk | Verify Method |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| REQ-0 | System | Provide end-to-end document transcription with persistent, inspectable lifecycle state. | medium | demonstration |
|
||||||
|
| REQ-1 | Functional | Allow users to upload one or more document images from the web UI. | low | test |
|
||||||
|
| REQ-2 | Functional | Run each upload through asynchronous processing that returns a transcription or explicit failure. | high | test |
|
||||||
|
| REQ-3 | Functional | Persist and expose job states: upload, queued, processing, transcribed, failed, completed. | high | inspection |
|
||||||
|
| REQ-4 | Functional | Persist transcription output, processing history, and failure details. | medium | test |
|
||||||
|
| REQ-5 | Interface | Expose API and UI views for status inspection and completed transcription reading. | medium | demonstration |
|
||||||
|
| REQ-6 | Performance | Trigger background processing on upload to preserve UI responsiveness. | medium | analysis |
|
||||||
|
| REQ-7 | Design Constraint | Keep lifespan-owned runtime resources: SQLAlchemy engine, async session factory, worker resources, provider clients. | medium | inspection |
|
||||||
|
| REQ-8 | Design Constraint | Initialize configuration and logging once at startup through centralized mechanisms. | low | inspection |
|
||||||
|
| REQ-9 | Design Constraint | Use Docker Compose baseline of app plus PostgreSQL; allow optional MongoDB container when enabled. | medium | demonstration |
|
||||||
|
| REQ-10 | Design Constraint | Keep schema bootstrap explicit and opt-in; normal startup does not mutate production schema. | high | inspection |
|
||||||
|
| REQ-11 | Design Constraint | Use service-backed persistence for core document and job data. | medium | inspection |
|
||||||
|
| REQ-12 | Design Constraint | Store transcription prompts as individual Markdown artifacts for iterative refinement. | medium | inspection |
|
||||||
|
|
||||||
|
### Requirement Relationships
|
||||||
|
|
||||||
|
- Contains: REQ-0 contains REQ-1 through REQ-12.
|
||||||
|
- Derives: REQ-2 -> REQ-3, REQ-3 -> REQ-4.
|
||||||
|
- Traces: REQ-5 -> REQ-3.
|
||||||
|
- Refines: REQ-6 -> REQ-2.
|
||||||
|
|
||||||
|
### Architecture Elements
|
||||||
|
|
||||||
|
| Element | Type | Doc Reference |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| UI | NiceGUI pages | src/transcription/ui/pages |
|
||||||
|
| API | FastAPI routes | src/transcription/api/routes.py |
|
||||||
|
| GRAPH | Async processing workflow | src/transcription/services, src/transcription/ai |
|
||||||
|
| DBREL | PostgreSQL + SQLModel relational persistence | src/transcription/db |
|
||||||
|
| DBDOC | MongoDB document persistence | src/transcription/db, src/transcription/services |
|
||||||
|
| OPS | Docker Compose runtime | docker-compose.yml |
|
||||||
|
| PROMPTS | Transcription prompt artifact library (Markdown files) | .github/prompts, docs |
|
||||||
|
| TESTS | Pytest verification suite | tests |
|
||||||
|
|
||||||
|
### Satisfaction Mapping
|
||||||
|
|
||||||
|
- UI satisfies REQ-1, REQ-5.
|
||||||
|
- API satisfies REQ-5.
|
||||||
|
- GRAPH satisfies REQ-2, REQ-6.
|
||||||
|
- DBREL satisfies REQ-3, REQ-10.
|
||||||
|
- DBDOC satisfies REQ-4, REQ-11.
|
||||||
|
- OPS satisfies REQ-9.
|
||||||
|
- PROMPTS satisfies REQ-12.
|
||||||
|
|
||||||
|
### Verification Mapping
|
||||||
|
|
||||||
|
- TESTS verifies REQ-1, REQ-2, REQ-3, REQ-4, REQ-5, REQ-10, REQ-11, REQ-12.
|
||||||
|
|
||||||
|
## Requirement Notes
|
||||||
|
|
||||||
|
- Requirement IDs (`REQ-*`) are stable references for planning, implementation, and test traceability.
|
||||||
|
- The model uses compact tables and traceability lists for renderer compatibility while preserving SysML-style requirement IDs and relationship semantics.
|
||||||
|
- Requirement categories (functional, interface, performance, and design constraints) are preserved as explicit REQ entries and relationship labels to keep change impact visible.
|
||||||
|
- PostgreSQL containerization and optional MongoDB containerization are both treated as extremely lightweight and simple operational choices in this architecture.
|
||||||
|
|
||||||
|
## Verification Intent
|
||||||
|
|
||||||
|
- Demonstration: validate end-to-end behavior via running system flows and operator-visible outcomes.
|
||||||
|
- Inspection: verify architecture and startup/runtime policies in code and configuration.
|
||||||
|
- Analysis: evaluate asynchronous execution behavior and design sufficiency.
|
||||||
|
- Test: automate behavioral checks through pytest suites and service-level tests.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Document-oriented persistence: A storage approach that uses flexible document structures for variable data shapes.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and is revised independently.
|
||||||
|
- SysML: Systems Modeling Language used to express structured requirements and traceability.
|
||||||
|
- System of record: The authoritative persistent store for canonical business data.
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# UI Behavioral Contracts
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This directory defines the current user-facing behavior of the NiceGUI application. It records what each page is for, which routes and actions it exposes, what information it presents, and how success, empty, validation, and failure states behave.
|
|
||||||
|
|
||||||
These documents are written for maintainers and AI contributors. They are behavioral contracts, not historical implementation notes and not substitutes for the database schema.
|
|
||||||
|
|
||||||
## Current Page Contracts
|
|
||||||
|
|
||||||
- [Home](pages/home.md)
|
|
||||||
- [Documents](pages/documents.md)
|
|
||||||
- [People](pages/people.md)
|
|
||||||
- [Jobs](pages/jobs.md)
|
|
||||||
- [Sources](pages/sources.md)
|
|
||||||
|
|
||||||
NiceGUI registers the routes shown in each contract without the `/ui` prefix. The application mounts NiceGUI under `/ui`, so `/documents` in page code is served to a browser as `/ui/documents`.
|
|
||||||
|
|
||||||
## Authority Hierarchy
|
|
||||||
|
|
||||||
When documents disagree, use this order:
|
|
||||||
|
|
||||||
1. User-facing page intent and accepted behavior: the page contracts in this directory.
|
|
||||||
2. Visual and interaction styling: [UI Style Guide](../invariant/ui_style_guide.md).
|
|
||||||
3. UI dependency and ownership boundaries: [UI contributor instructions](../../.github/instructions/ui.instructions.md).
|
|
||||||
4. Durable failure behavior: [Error Handling invariant](../invariant/error_handling.md).
|
|
||||||
5. Durable AI evidence behavior: [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md).
|
|
||||||
6. Data definitions and relationships: current models plus the [V4 schema](../ver4/schema_v4.md).
|
|
||||||
7. Planned behavior changes: the applicable V4.x scope and implementation documents.
|
|
||||||
8. Implementation truth: current code and tests.
|
|
||||||
|
|
||||||
If code intentionally changes accepted page behavior, update the corresponding page contract in the same change. If code accidentally differs, correct the implementation rather than rewriting intent to match a defect.
|
|
||||||
|
|
||||||
## Contract Contents
|
|
||||||
|
|
||||||
Each page contract contains:
|
|
||||||
|
|
||||||
1. Purpose and user goals.
|
|
||||||
2. Registered routes and navigation context.
|
|
||||||
3. List, detail, and form behavior.
|
|
||||||
4. Editable and system-managed information.
|
|
||||||
5. Validation, empty, loading, and failure states.
|
|
||||||
6. A concise acceptance checklist.
|
|
||||||
7. Current implementation and test anchors.
|
|
||||||
8. Known limitations and deferred work.
|
|
||||||
|
|
||||||
## Maintenance Rules
|
|
||||||
|
|
||||||
- Describe current accepted behavior in present tense.
|
|
||||||
- Do not mix an obsolete “first release” design with current behavior.
|
|
||||||
- Keep future changes in versioned scope documents and link to them from a Deferred Work section.
|
|
||||||
- Do not reproduce the complete database field inventory here; include only fields that affect page behavior.
|
|
||||||
- Keep service, file, and test anchors current.
|
|
||||||
- Do not create separate current-state, target-state, and traceability copies of the same contract.
|
|
||||||
- Keep cross-page visual rules in the UI Style Guide instead of repeating them on each page.
|
|
||||||
- Keep database joins such as `DocumentPerson` and `JobSource` in schema/architecture documentation unless they directly affect a page interaction.
|
|
||||||
|
|
||||||
## Current Baseline
|
|
||||||
|
|
||||||
These contracts describe the completed V4 through V4.5 behavior.
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Documents Page Contract
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Documents manages the archival record for each historical artifact independently of its source files and transcription jobs. A Document can be created first, linked to people in one or more roles, and used later as the parent for Sources and Jobs.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
| Route | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `/documents` | Searchable archival Document list. |
|
|
||||||
| `/documents/new` | Create a Document. |
|
|
||||||
| `/documents/{document_id}` | View one Document and its related records. |
|
|
||||||
| `/documents/{document_id}/edit` | Edit metadata and the complete Linked People set. |
|
|
||||||
| `/documents/{document_id}/delete` | Confirm or block deletion. |
|
|
||||||
| `/documents/{document_id}/jobs` | Show Jobs belonging to the Document. |
|
|
||||||
| `/documents/{document_id}/sources` | Redirect to the Document-filtered Sources list. |
|
|
||||||
| `/documents/{document_id}/print` | Preview and browser-print the persisted Document. |
|
|
||||||
|
|
||||||
## List Behavior
|
|
||||||
|
|
||||||
- The title is **Archival Documents**.
|
|
||||||
- **Create new document** opens the create route.
|
|
||||||
- The table defaults to Document Title order and supports search and column sorting.
|
|
||||||
- Columns are Document Title, Type, Author, Document Date, and Archive Ref.
|
|
||||||
- Document Title is left-aligned; the remaining columns are centered.
|
|
||||||
- Author lists all linked people in the `author` role.
|
|
||||||
- Date display prefers exact date, then approximate date, then `Unknown`.
|
|
||||||
- Selecting a row opens Document Detail.
|
|
||||||
- No records displays `No documents found in repository.`
|
|
||||||
|
|
||||||
## Create and Edit Behavior
|
|
||||||
|
|
||||||
Required:
|
|
||||||
|
|
||||||
- Document name.
|
|
||||||
- Document type selected from the Document Type registry.
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
- Exact date.
|
|
||||||
- Approximate date.
|
|
||||||
- Document location.
|
|
||||||
- Archive identifier.
|
|
||||||
- Notes.
|
|
||||||
- Linked People, with exactly one Person Role per linked Person.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Exact date must parse as `YYYY-MM-DD`; browser presentation may follow locale.
|
|
||||||
- Existing people appear with disambiguating labels.
|
|
||||||
- **Create new person** opens Person creation.
|
|
||||||
- `person_id` may preselect that Person in the author role on Document creation.
|
|
||||||
- An invalid requested Person produces a warning rather than a broken form.
|
|
||||||
- `return_to=jobs_new` returns a successful create to Job creation with the new Document selected.
|
|
||||||
- Edit includes active and inactive Document Types so historical values remain maintainable.
|
|
||||||
- One Linked People table contains Select, Person, and Role columns.
|
|
||||||
- Add and Edit use an inline Person/Role editor; Save, Cancel, and Delete change staged UI state only.
|
|
||||||
- A Person may appear once per Document regardless of role.
|
|
||||||
- Existing inactive-role links remain visible; only active roles may be newly assigned.
|
|
||||||
- Document fields and the complete staged link set commit atomically on the main save.
|
|
||||||
- Save success returns to Document Detail.
|
|
||||||
|
|
||||||
## Detail Behavior
|
|
||||||
|
|
||||||
- The heading shows name, type, and internal ID.
|
|
||||||
- The first Source, when present, appears in the dark-room viewer.
|
|
||||||
- Archival Metadata shows authors, Document Type, compact Document date, location, and archive identifier. Notes appear in a separate archival-notes block within the same card.
|
|
||||||
- System Logistics shows created and updated timestamps.
|
|
||||||
- Related People are grouped by role and link to Person Detail.
|
|
||||||
- **Sources & Pipeline Jobs** shows counts and actions for filtered Sources, Document Jobs, and adding a Job.
|
|
||||||
- **Edit Document**, **Print**, and **Delete** are available from the header.
|
|
||||||
- Invalid IDs and missing Documents produce explicit states without rendering a partial page.
|
|
||||||
|
|
||||||
## Print Behavior
|
|
||||||
|
|
||||||
- Print opens a dedicated preview for persisted Document data.
|
|
||||||
- **Facsimile** places each Source image beside its current transcription and starts every Source on a new printed sheet.
|
|
||||||
- **Text only** omits images, joins single line breaks inside paragraphs, and preserves blank-line paragraph boundaries.
|
|
||||||
- Non-null revised text takes precedence over raw transcription, including an intentionally empty revision.
|
|
||||||
- Archival metadata resolves Author through the hidden built-in semantic identity, not its mutable label.
|
|
||||||
- Archival metadata includes the Document Type label.
|
|
||||||
- Metadata tables use a narrow non-wrapping label column and wider wrapping data columns rather than stretching across the page.
|
|
||||||
- Job metadata uses one oldest-to-newest column per Job and ends with Status.
|
|
||||||
- Stored text is escaped and Source media uses record-validated application URLs rather than local file paths.
|
|
||||||
- Printing uses the browser print dialog; server-generated PDFs are not provided.
|
|
||||||
|
|
||||||
## Document Jobs Behavior
|
|
||||||
|
|
||||||
- The page lists the Document's Jobs newest first with status and Job ID.
|
|
||||||
- **Open Job** navigates to Job Detail.
|
|
||||||
- **Create Job** opens Job creation with the Document selected.
|
|
||||||
- No jobs displays an explicit empty state.
|
|
||||||
|
|
||||||
## Delete Behavior
|
|
||||||
|
|
||||||
- Deletion is blocked while any Source or Job belongs to the Document.
|
|
||||||
- The blocked state names the dependency categories and provides navigation back and to Jobs.
|
|
||||||
- An unlinked Document requires an explicit permanent-delete action.
|
|
||||||
- Success returns to the Documents list.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
- List columns, alignment, search, sorting, date fallback, and row navigation match this contract.
|
|
||||||
- Create/edit enforce name, registered type, and valid exact-date input.
|
|
||||||
- Linked People staging enforces one role and one row per Person.
|
|
||||||
- Document and Linked People writes never partially commit.
|
|
||||||
- Person-first Document creation preselects the requested Person as author.
|
|
||||||
- Detail links people, Sources, and Jobs to the correct records.
|
|
||||||
- Delete never removes a Document with Source or Job dependencies.
|
|
||||||
- Both print formats preserve the frozen content, ordering, text-precedence, and safety contracts.
|
|
||||||
- Service failures use the shared error presenter and never report false success.
|
|
||||||
|
|
||||||
## Implementation Anchors
|
|
||||||
|
|
||||||
- `src/transcription/ui/pages/documents_page.py`
|
|
||||||
- `src/transcription/ui/components/table/documents.py`
|
|
||||||
- `src/transcription/services/documents.py`
|
|
||||||
- `src/transcription/services/people.py`
|
|
||||||
- `src/transcription/services/workflows.py`
|
|
||||||
- `src/transcription/ui/components/linked_people.py`
|
|
||||||
- `src/transcription/ui/pages/print_preview_page.py`
|
|
||||||
- `src/transcription/api/v4_print.py`
|
|
||||||
- `tests/ui/test_documents_page.py`
|
|
||||||
- `tests/services/test_document_service.py`
|
|
||||||
|
|
||||||
## Known Limitations and Deferred Work
|
|
||||||
|
|
||||||
- Source page ordering remains read-only in V4.4.
|
|
||||||
- Printing other entities, batch printing, and server-side export formats are deferred.
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Home Page Contract
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Home provides a user-maintained landing page for the local archive. It combines one current image with Markdown text and lets the operator edit both without changing application source or prompt assets.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
| Route | Browser path | Purpose |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/homepage` | `/ui/homepage` | View current homepage image and Markdown. |
|
|
||||||
| `/homepage/edit` | `/ui/homepage/edit` | Upload an image and edit Markdown. |
|
|
||||||
|
|
||||||
The application root and `/ui` redirect to `/ui/homepage`.
|
|
||||||
|
|
||||||
## View Behavior
|
|
||||||
|
|
||||||
- The visible page heading is **Home**; the browser tab title is **VibeScribe Home**.
|
|
||||||
- The latest homepage image appears in the shared dark-room viewer.
|
|
||||||
- Saved Markdown is rendered in the **Home Text** card.
|
|
||||||
- Missing text displays `No homepage text saved yet.`
|
|
||||||
- Missing image displays the viewer's empty state.
|
|
||||||
- **Edit Home Page** opens the edit route.
|
|
||||||
|
|
||||||
## Edit Behavior
|
|
||||||
|
|
||||||
- The image upload accepts JPEG, PNG, GIF, WebP, BMP, and TIFF files.
|
|
||||||
- A successful upload immediately stores the file, updates the preview to that image, and displays a positive notification.
|
|
||||||
- The Markdown textarea is initialized from the currently stored homepage text.
|
|
||||||
- **Save** writes the textarea content, displays `Homepage saved`, and returns to Home.
|
|
||||||
- **Cancel** returns to Home without saving textarea changes. An image already uploaded during the edit session remains stored.
|
|
||||||
|
|
||||||
## Storage Contract
|
|
||||||
|
|
||||||
- Homepage content is mutable application data under `data/homepage`.
|
|
||||||
- Markdown is stored in `homepage.md`.
|
|
||||||
- Uploaded images keep a sanitized basename.
|
|
||||||
- The view selects the supported image with the most recent modification time.
|
|
||||||
- Homepage files are not transcription prompts and are not database records.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
- `/`, `/ui`, and the application brand reach Home.
|
|
||||||
- Home renders with or without stored Markdown and image content.
|
|
||||||
- Edit loads existing Markdown.
|
|
||||||
- A supported image upload updates the preview and becomes the latest homepage image.
|
|
||||||
- Save persists Markdown and returns to Home.
|
|
||||||
- Cancel does not save changed Markdown.
|
|
||||||
|
|
||||||
## Implementation Anchors
|
|
||||||
|
|
||||||
- `src/transcription/ui/pages/home_page.py`
|
|
||||||
- `src/transcription/ui/homepage_store.py`
|
|
||||||
- `src/transcription/ui/components/app_shell.py`
|
|
||||||
- `tests/ui/test_upload_page.py`
|
|
||||||
- `tests/ui/test_navigation_and_mounts.py`
|
|
||||||
- `tests/ui/test_pages_registration.py`
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
- Homepage storage is fixed under the repository/application `data` directory rather than a configured application-data root.
|
|
||||||
- Uploading an image is immediate and is not rolled back by Cancel.
|
|
||||||
- The editor does not currently delete or select among previously uploaded images.
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Jobs Page Contract
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Jobs manages transcription processing runs. A Job belongs to one Document, links one or more Source pages, records processing provenance, and exposes lifecycle actions without making lifecycle fields directly editable.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
| Route | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `/jobs` | Searchable processing Job list. |
|
|
||||||
| `/jobs/new` | Create and queue a Job. |
|
|
||||||
| `/jobs/{job_id}` | View status, execution logistics, and related records. |
|
|
||||||
| `/jobs/{job_id}/cancel` | Confirm cancellation. |
|
|
||||||
| `/jobs/{job_id}/resubmit` | Confirm resubmission of failed Sources. |
|
|
||||||
| `/jobs/{job_id}/delete` | Confirm or block deletion. |
|
|
||||||
|
|
||||||
## List Behavior
|
|
||||||
|
|
||||||
- The title is **Transcription Pipeline Jobs**.
|
|
||||||
- **Create job** opens Job creation and **Refresh** reloads the table.
|
|
||||||
- Columns are Job ID, Status, Source Filename, Retries, Created, and Updated.
|
|
||||||
- Search covers Job ID, filename, and status.
|
|
||||||
- Status is displayed as a semantic status chip.
|
|
||||||
- Selecting a row opens Job Detail.
|
|
||||||
- No records displays `No job records found in repository.`
|
|
||||||
|
|
||||||
## Create Behavior
|
|
||||||
|
|
||||||
- A Target Document and at least one source file are required.
|
|
||||||
- `document_id` may preselect a Target Document.
|
|
||||||
- If no Documents exist, the page explains the prerequisite and links to Document creation with a return path.
|
|
||||||
- Provider and Model are optional request overrides.
|
|
||||||
- Upload accepts JPEG, PNG, TIFF, and PDF files and supports multiple/folder selection.
|
|
||||||
- The visible upload queue is sorted alphabetically by original filename.
|
|
||||||
- Files can be removed individually or cleared before submission.
|
|
||||||
- Helper text explains numeric filename prefixes for page ordering.
|
|
||||||
- Submission creates the Job, Source records, and JobSource links, notifies the worker, and opens Job Detail.
|
|
||||||
- When opened with `source_id`, creation becomes a retranscription flow: Source and Document are locked, Provider is
|
|
||||||
read-only, Model is restricted to `PROVIDER_MODELS`, no upload is accepted, and one existing Source is linked.
|
|
||||||
|
|
||||||
## Detail and Lifecycle Behavior
|
|
||||||
|
|
||||||
- The heading shows Job ID and a status badge.
|
|
||||||
- Execution Logistics shows provider, model, prompt, retry count, and last update.
|
|
||||||
- Document Links open the parent Document and Job-filtered Sources.
|
|
||||||
- Queued and processing Jobs show an auto-refresh notice and reload every four seconds.
|
|
||||||
- Polling stops when the Job becomes terminal or a refresh fails.
|
|
||||||
- Queued and processing Jobs expose **Cancel**.
|
|
||||||
- Jobs other than `transcribed` expose **Resubmit** under the current UI rule. The service blocks resubmission while processing is active or when no failed Sources exist.
|
|
||||||
- All Jobs expose **Delete Job**, subject to explicit evidence-deletion guardrails.
|
|
||||||
- Invalid and missing IDs produce explicit states.
|
|
||||||
|
|
||||||
## Cancel Behavior
|
|
||||||
|
|
||||||
- The confirmation explains that processing stops and remaining non-transcribed Sources become failed.
|
|
||||||
- The service decides whether the current state permits cancellation.
|
|
||||||
- Success updates the Job, notifies the worker, and returns to Job Detail.
|
|
||||||
|
|
||||||
## Resubmit Behavior
|
|
||||||
|
|
||||||
- The page shows current status and failed Source count.
|
|
||||||
- The page explains that resubmission queues failed linked Sources while preserving immutable prior attempt evidence.
|
|
||||||
- The service blocks submission while processing is active or when no failed Sources exist.
|
|
||||||
- `JobSource` remains the latest compatibility projection, while every provider call appends an `ExecutionAttempt`.
|
|
||||||
- The selected `Source.raw_transcription` projection remains available while a retry is pending or fails.
|
|
||||||
- Success reports the number of resubmitted Sources and returns to Job Detail.
|
|
||||||
|
|
||||||
## Delete Behavior
|
|
||||||
|
|
||||||
- Deletion is blocked while status is `processing`.
|
|
||||||
- Allowed deletion explicitly warns that related `JobSource` projections,
|
|
||||||
immutable execution attempts, captured transport responses, and attempt-owned
|
|
||||||
artifacts are permanently removed.
|
|
||||||
- Source records and source files remain available for separate deletion.
|
|
||||||
- Success returns to the Jobs list.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
- Job creation cannot proceed without a valid Document and at least one Source.
|
|
||||||
- Upload ordering and removal controls match the displayed queue.
|
|
||||||
- Detail shows current status and provenance summary with correct related links.
|
|
||||||
- Active Jobs refresh without overlapping permanent polling after terminal state.
|
|
||||||
- Cancel, resubmit, and delete honor service guardrails and show actionable failures.
|
|
||||||
- Lifecycle fields cannot be edited directly.
|
|
||||||
|
|
||||||
## Implementation Anchors
|
|
||||||
|
|
||||||
- `src/transcription/ui/pages/jobs_page.py`
|
|
||||||
- `src/transcription/ui/components/table/jobs.py`
|
|
||||||
- `src/transcription/services/jobs.py`
|
|
||||||
- `src/transcription/services/store.py`
|
|
||||||
- `src/transcription/services/workflows.py`
|
|
||||||
- `tests/ui/test_jobs_page.py`
|
|
||||||
- `tests/services/test_job_service.py`
|
|
||||||
- `tests/services/test_store.py`
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# People Page Contract
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
People manages reusable historical-person records. A Person may appear in many Documents under different relationship roles and may optionally carry a portrait and FamilySearch identifier.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
| Route | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `/people` | Searchable People list. |
|
|
||||||
| `/people/new` | Create a Person. |
|
|
||||||
| `/people/{person_id}` | View one Person and linked Documents. |
|
|
||||||
| `/people/{person_id}/edit` | Edit the Person. |
|
|
||||||
| `/people/{person_id}/delete` | Confirm permanent deletion. |
|
|
||||||
|
|
||||||
## List Behavior
|
|
||||||
|
|
||||||
- The title is **Archival Entities: People**.
|
|
||||||
- **Create new person** opens the create route.
|
|
||||||
- The table defaults to Full Name order and supports search and column sorting.
|
|
||||||
- Columns are Full Name, Display Name, Maiden Name, Birth Date, and Death Date.
|
|
||||||
- Full Name is left-aligned; Display Name, Maiden Name, and date columns are centered.
|
|
||||||
- Birth and death values independently prefer exact date, then approximate date, then `Unknown`.
|
|
||||||
- Selecting a row opens Person Detail.
|
|
||||||
- No records displays `No person records found in repository.`
|
|
||||||
|
|
||||||
## Create and Edit Behavior
|
|
||||||
|
|
||||||
Required:
|
|
||||||
|
|
||||||
- Full name.
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
- Display name and maiden name.
|
|
||||||
- Exact and approximate birth/death dates.
|
|
||||||
- Birth/death places.
|
|
||||||
- Biography.
|
|
||||||
- Portrait path or uploaded portrait.
|
|
||||||
- FamilySearch ID.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Missing Full name blocks save with a warning.
|
|
||||||
- Exact date inputs are native browser date inputs.
|
|
||||||
- FamilySearch IDs are normalized and validated by `PeopleService`.
|
|
||||||
- Portrait uploads are stored under the configured upload root in a Person-specific directory and update Portrait path.
|
|
||||||
- Metadata JSON remains hidden.
|
|
||||||
- Save success returns to Person Detail.
|
|
||||||
|
|
||||||
## Detail Behavior
|
|
||||||
|
|
||||||
- The header provides **New Document**, **Edit Person**, and **Delete**.
|
|
||||||
- **New Document** opens Document creation with this Person requested for author preselection.
|
|
||||||
- The portrait viewer resolves supported relative upload paths and absolute HTTP/data URLs.
|
|
||||||
- Biographical Record shows names, compact birth/death dates, places, and an **Open in FamilySearch** link when an ID exists.
|
|
||||||
- Biography has an explicit empty value.
|
|
||||||
- Linked Documents show Document name, relationship role, and an action to open Document Detail.
|
|
||||||
- No links shows both an empty state and guidance to link from a Document workflow.
|
|
||||||
- System Logistics shows created and updated timestamps.
|
|
||||||
|
|
||||||
## Delete Behavior
|
|
||||||
|
|
||||||
- The page warns when linked Document relationships exist.
|
|
||||||
- Confirmed deletion removes the Person and its relationship links; it does not delete Documents.
|
|
||||||
- Success returns to the People list.
|
|
||||||
- Missing or already-deleted records return to a safe list state.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
- List fields, alignment, date fallback, search, sorting, and navigation match this contract.
|
|
||||||
- Full name is enforced on create and edit.
|
|
||||||
- FamilySearch ID validation and link generation use the fixed supported identifier format.
|
|
||||||
- Portrait upload and rendering remain constrained to supported media paths.
|
|
||||||
- New Document carries the Person context.
|
|
||||||
- Linked Documents show the correct role and target.
|
|
||||||
- Delete wording distinguishes removal of relationship links from deletion of Documents.
|
|
||||||
|
|
||||||
## Implementation Anchors
|
|
||||||
|
|
||||||
- `src/transcription/ui/pages/people_page.py`
|
|
||||||
- `src/transcription/ui/components/table/people.py`
|
|
||||||
- `src/transcription/services/people.py`
|
|
||||||
- `tests/ui/test_people_page.py`
|
|
||||||
- `tests/services/test_v2_crud.py`
|
|
||||||
|
|
||||||
## Deferred Work
|
|
||||||
|
|
||||||
- Structured name fields, merge/deduplication, advanced metadata editing, and Person-side relationship editing are not current behavior.
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# Sources Page Contract
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Sources manages individual archived page/file records. It provides source-media viewing, current processing context, provider evidence inspection, previous/next page navigation, and human revision without allowing machine output to be edited.
|
|
||||||
|
|
||||||
## Routes
|
|
||||||
|
|
||||||
| Route | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `/sources` | Global or filtered Source list. |
|
|
||||||
| `/sources/{source_id}` | View media, transcription, revision, metadata, and evidence. |
|
|
||||||
| `/sources/{source_id}/delete` | Confirm or block deletion. |
|
|
||||||
|
|
||||||
The list accepts optional `document_id` and `job_id` query parameters. Document context takes precedence if both parse successfully.
|
|
||||||
|
|
||||||
## List Behavior
|
|
||||||
|
|
||||||
- The title is **Source Asset Records**, **Sources for Document**, or **Sources for Job** according to context.
|
|
||||||
- Global context provides **Create Job**.
|
|
||||||
- Filtered context provides **Back to Document** or **Back to Job**.
|
|
||||||
- Rows are ordered by page number and then upload name.
|
|
||||||
- Columns are Document Name, Page Number, Upload Title, Status, and Error Detail.
|
|
||||||
- Document Name, Upload Title, and Error Detail are left-aligned; Status is centered.
|
|
||||||
- Stored Filename is intentionally absent from the list.
|
|
||||||
- Selecting a row opens Source Detail.
|
|
||||||
- No records displays `No source asset records found in repository.`
|
|
||||||
|
|
||||||
## Detail Behavior
|
|
||||||
|
|
||||||
- The heading shows page number, upload name, and Source ID.
|
|
||||||
- **Back to Sources** returns to the global list.
|
|
||||||
- **Retranscribe Source** opens Create Processing Job with this Source and its Document locked.
|
|
||||||
- **Delete Source** opens the guarded delete route.
|
|
||||||
- Previous and Next navigate only among Sources belonging to the same Document in page order; unavailable boundary actions are disabled.
|
|
||||||
- The media viewer resolves the stored Source path through the configured upload root.
|
|
||||||
- Transcription Text is read-only and displays the preferred machine projection, with a legacy latest-JobSource
|
|
||||||
fallback only when no Source projection exists.
|
|
||||||
- Editable Revision is seeded from an existing revision or the machine transcription.
|
|
||||||
- Source Metadata shows upload name, stored filename, page number, Document Name, Document ID, and stored path. Source ID appears in the page-header subtitle.
|
|
||||||
- SourceJob Metadata shows latest status, Job ID, execution time, provider, model, prompt, and failure detail.
|
|
||||||
- Revision Logistics shows revised state, last-revised time, and upload time.
|
|
||||||
- Candidate Machine Transcriptions remains compact until a candidate is expanded, then compares it with the preferred
|
|
||||||
machine result and requires confirmation before **Use this transcription**.
|
|
||||||
- Candidate promotion does not alter a human revision. Empty states distinguish no machine result from no candidates.
|
|
||||||
- An orientation-normalized artifact appears in evidence only when recognized metadata required a physical rotation.
|
|
||||||
|
|
||||||
## Provider Evidence
|
|
||||||
|
|
||||||
- Provider Evidence is associated with the latest JobSource execution.
|
|
||||||
- New attempts display separate expandable Request Manifest, Transport Response, OpenRouter SDK Response Snapshot,
|
|
||||||
Normalized Metadata, Software Context, and Derived Artifacts sections.
|
|
||||||
- Historical `raw_api_response` values are labeled as OpenRouter SDK response snapshots.
|
|
||||||
- Missing evidence has an explicit empty state.
|
|
||||||
- Historical executions explicitly state that exact transport evidence was not captured.
|
|
||||||
- Quality warning artifacts remain attached to their machine attempt and are not recomputed during page rendering.
|
|
||||||
- **Export Evidence** downloads a versioned package containing source identity, attempts, artifacts, relationships,
|
|
||||||
schema versions, and integrity digests without source binaries, credentials, or machine-local source paths.
|
|
||||||
|
|
||||||
## Revision Behavior
|
|
||||||
|
|
||||||
- Machine transcription is never edited directly.
|
|
||||||
- A revision must contain non-whitespace text.
|
|
||||||
- Save persists revised text and updates the saved timestamp without leaving the page.
|
|
||||||
- Reset restores the in-memory revision from page load or the most recent successful save. When no revision exists, it restores the machine transcription; it does not re-read the database.
|
|
||||||
- A failed latest execution displays guidance that a human revision can preserve corrected text.
|
|
||||||
|
|
||||||
## Delete Behavior
|
|
||||||
|
|
||||||
- Deletion is allowed only when the Source has no JobSource links.
|
|
||||||
- A linked Source shows cleanup guidance and navigation to Jobs.
|
|
||||||
- An unlinked Source requires explicit permanent deletion.
|
|
||||||
- Success returns to the Sources list.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
- Global, Document-filtered, and Job-filtered lists show the correct context and return action.
|
|
||||||
- List columns and alignments match this contract and omit Stored Filename.
|
|
||||||
- Previous/next navigation never crosses Document boundaries.
|
|
||||||
- Detail keeps machine output read-only and human revision separately editable.
|
|
||||||
- Retranscription, candidate comparison, warnings, and explicit promotion preserve every prior attempt.
|
|
||||||
- Empty, failed, and missing-evidence states remain explicit.
|
|
||||||
- JSON evidence is readable without being mislabeled as native transport evidence.
|
|
||||||
- Delete cannot remove a Source with processing-history links.
|
|
||||||
|
|
||||||
## Implementation Anchors
|
|
||||||
|
|
||||||
- `src/transcription/ui/pages/sources_page.py`
|
|
||||||
- `src/transcription/ui/components/table/sources.py`
|
|
||||||
- `src/transcription/services/sources.py`
|
|
||||||
- `tests/ui/test_sources_page.py`
|
|
||||||
- `tests/services/test_transcription_service.py`
|
|
||||||
- `tests/services/test_v2_crud.py`
|
|
||||||
|
|
||||||
## Planned Changes
|
|
||||||
|
|
||||||
- Source page reordering is deferred beyond V4.3 and may be reconsidered if a demonstrated workflow need emerges.
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Ver1 Step 1/2 Carry-Forward Checklist
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Track open Step 1 and Step 2 follow-ups through later V1 steps, with lightweight verification evidence and requirement traceability.
|
||||||
|
|
||||||
|
This artifact implements the carry-forward approach defined in:
|
||||||
|
- `docs/ver1/ver1-step1-2_revised.md`
|
||||||
|
|
||||||
|
Historical records remain unchanged:
|
||||||
|
- `docs/ver1/ver1-step1.md`
|
||||||
|
- `docs/ver1/ver1-step1-results.md`
|
||||||
|
- `docs/ver1/ver1-step2.md`
|
||||||
|
- `docs/ver1/ver1-step2-results.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status Legend
|
||||||
|
|
||||||
|
- `not started`
|
||||||
|
- `in progress`
|
||||||
|
- `done`
|
||||||
|
- `deferred`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Carry-Forward Mapping Matrix
|
||||||
|
|
||||||
|
| ID | Carry-Forward Task | Source | Related REQ | Owning V1 Step(s) | Validation Method | Status | Evidence Link/Note |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| CF-A1 | Confirm remaining implicit/global runtime ownership and lift only high-impact resources to lifespan ownership | Step 1 residual follow-up | REQ-7 | Step 3, Step 9 | Inspection + test | in progress | Step 3 added `services/library.py` and `api/routes.py` using existing service/session access patterns; no new module-global runtime resource ownership introduced. Reconfirm in Step 9 release readiness. |
|
||||||
|
| CF-A2 | Finalize migration + rollback runbook usage and rehearse on representative local data | Step 1 residual follow-up | REQ-10 | Step 4, Step 9 | Demonstration + test | not started | |
|
||||||
|
| CF-A3 | Maintain lightweight boundary enforcement (review checklist and/or simple import checks) | Step 1 residual follow-up | REQ-7, REQ-11 | Step 3, Step 7 | Inspection | in progress | Step 3 implementation keeps UI/API composition thin and pushes revision/search/export logic to `services/library.py`; continue with Step 7 checks. |
|
||||||
|
| CF-B1 | Build compact error-path inventory for major failure paths and category mapping | Step 2 governance follow-up | REQ-2, REQ-3, REQ-4, REQ-5 | Step 6, Step 7 | Inspection | not started | Use `docs/ver1/ver1-step2-error-path-inventory.md` |
|
||||||
|
| CF-B2 | Standardize required logging fields at critical boundary handoffs | Step 2 residual follow-up | REQ-3, REQ-4, REQ-8 | Step 6 | Inspection + test | not started | |
|
||||||
|
| CF-B3 | Revisit retry backoff strategy only if observed runtime behavior justifies extra complexity | Step 2 residual follow-up | REQ-2, REQ-6 | Step 6, Step 8 | Analysis + test | deferred | Keep fixed backoff unless evidence suggests change |
|
||||||
|
| CF-C1 | Integrate Step 1/2 completed outcomes and open follow-ups into V1 traceability tracking | Revision-plan workstream | REQ-0..REQ-12 (traceability) | Step 3, Step 10 | Inspection | done | Step 3 artifacts added: `docs/ver1/ver1-step3.md`, `docs/ver1/ver1-step3-results.md`, and this checklist updated with Step 3 evidence and routing. |
|
||||||
|
| CF-C2 | Keep carry-forward routing aligned with revised V1 plan (architecture via 3/4/9, reliability via 6/7) | Revision-plan workstream | REQ-0..REQ-12 (execution alignment) | Step 3+ | Inspection | in progress | Step 3 execution followed routing: functional features implemented in Step 3; migration/rollback items remain in Step 4/9; logging/error-path standardization remains Step 6/7. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Notes
|
||||||
|
|
||||||
|
### Step 3 (Functional Completion)
|
||||||
|
- Use CF-A1 and CF-A3 during requirement-slice implementation reviews.
|
||||||
|
- Record any discovered boundary/runtime ownership gaps in this checklist.
|
||||||
|
|
||||||
|
### Step 4 (Data Model and Migration Safety)
|
||||||
|
- Execute CF-A2 rehearsal and link evidence (commands, runbook notes, outcomes).
|
||||||
|
|
||||||
|
### Step 6 (Minimal Observability & Operability)
|
||||||
|
- Execute CF-B1 and CF-B2 with focused artifacts and log-field verification.
|
||||||
|
|
||||||
|
### Step 7 (Test Coverage and Practical Quality Gates)
|
||||||
|
- Add/verify tests supporting CF-A3 and CF-B1/B2 where meaningful.
|
||||||
|
|
||||||
|
### Step 8 (Performance Validation)
|
||||||
|
- Reassess CF-B3 only if retries/backoff are observed to cause practical issues.
|
||||||
|
|
||||||
|
### Step 9 (Release Readiness)
|
||||||
|
- Reconfirm CF-A1/A2 readiness in release checklist and rollback drill.
|
||||||
|
|
||||||
|
### Step 10 (Documentation Completion)
|
||||||
|
- Ensure final V1 docs reference outcomes from this checklist where relevant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Check for Carry-Forward Completion
|
||||||
|
|
||||||
|
- [ ] Historical Step 1/2 documents remain unchanged.
|
||||||
|
- [ ] Every open Step 1/2 follow-up has an owning V1 step and validation method.
|
||||||
|
- [ ] Evidence links are recorded for each completed carry-forward item.
|
||||||
|
- [ ] No carry-forward item introduces unnecessary complexity for personal-scale operation.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Ver1 Step 1 & Step 2 Revision Plan (Additive)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define a **targeted implementation follow-through plan** for Step 1 and Step 2 outcomes so remaining V1 work stays aligned with `docs/ver1/ver1.md`:
|
||||||
|
|
||||||
|
- personal-scale operation
|
||||||
|
- single operator
|
||||||
|
- private-network assumptions
|
||||||
|
- low operational overhead
|
||||||
|
- practical, testable controls
|
||||||
|
|
||||||
|
This document is additive and does **not** replace or revise historical Step 1/Step 2 records.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Documents Reviewed
|
||||||
|
|
||||||
|
- `docs/ver1/ver1.md`
|
||||||
|
- `docs/ver1/ver1-step1.md`
|
||||||
|
- `docs/ver1/ver1-step1-results.md`
|
||||||
|
- `docs/ver1/ver1-step2.md`
|
||||||
|
- `docs/ver1/ver1-step2-results.md`
|
||||||
|
- `docs/architecture.md`
|
||||||
|
- `docs/error_handling.md`
|
||||||
|
- `docs/requirements.md`
|
||||||
|
- `docs/index.md`
|
||||||
|
- `docs/intent.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Revision Goals
|
||||||
|
|
||||||
|
1. Preserve all completed Step 1/Step 2 technical hardening work.
|
||||||
|
2. Keep historical Step 1/Step 2 documents unchanged.
|
||||||
|
3. Convert residual risks/follow-ups into concrete implementation tasks for subsequent V1 steps.
|
||||||
|
4. Preserve traceability to requirements and implemented evidence.
|
||||||
|
5. Maintain alignment with personal-scale architecture and operating model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
- Define carry-forward implementation tasks based on Step 1/2 residual risks and open items.
|
||||||
|
- Map carry-forward tasks to later V1 steps (especially Steps 3, 4, 6, 7, and 9).
|
||||||
|
- Define lightweight verification evidence expected for each carry-forward task.
|
||||||
|
- Update V1 traceability references to include completed Step 1/2 outcomes and deferred follow-ups.
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
- Simplifying tone/structure of existing Step 1/2 documents
|
||||||
|
- Clarifying or rewriting historical Step 1/2 plan/results content
|
||||||
|
- Editing `docs/ver1/ver1-step1.md`
|
||||||
|
- Editing `docs/ver1/ver1-step1-results.md`
|
||||||
|
- Editing `docs/ver1/ver1-step2.md`
|
||||||
|
- Editing `docs/ver1/ver1-step2-results.md`
|
||||||
|
- Re-implementing Step 1/2 code changes
|
||||||
|
- Rewriting `docs/ver1/ver1.md`
|
||||||
|
- Deleting historical sections/results
|
||||||
|
- Altering requirements IDs or architecture principles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Carry-Forward Implementation Plan
|
||||||
|
|
||||||
|
## Workstream A — Close Step 1 follow-ups through later V1 steps
|
||||||
|
|
||||||
|
### A1) Runtime ownership completion (REQ-7 continuity)
|
||||||
|
- Confirm whether any remaining runtime resources still use implicit/global ownership.
|
||||||
|
- Move only high-impact remaining resources to explicit lifespan ownership when needed.
|
||||||
|
- Keep ownership model simple and documented.
|
||||||
|
|
||||||
|
### A2) Schema/migration operations readiness (REQ-10 continuity)
|
||||||
|
- Finalize practical migration + rollback runbook usage in Step 4 execution.
|
||||||
|
- Rehearse upgrade and rollback on representative local data.
|
||||||
|
- Keep production startup free from implicit schema mutation.
|
||||||
|
|
||||||
|
### A3) Boundary enforcement (lightweight only)
|
||||||
|
- Keep architecture boundary checks lightweight (review checklist and/or simple import checks).
|
||||||
|
- Avoid heavy governance tooling unless clear recurring drift appears.
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
Step 1 architecture hardening remains intact and is completed pragmatically where open items remain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream B — Close Step 2 follow-ups through later V1 steps
|
||||||
|
|
||||||
|
### B1) Error-path inventory and coverage visibility
|
||||||
|
- Create a compact error-path inventory artifact (or equivalent matrix section) covering major failure paths.
|
||||||
|
- Ensure each critical path maps to category, retriable policy, and surfaced behavior.
|
||||||
|
|
||||||
|
### B2) Logging field consistency at key boundaries
|
||||||
|
- Standardize required fields at critical failure handoffs (`error_id`, `category`, `operation`, identifiers when available).
|
||||||
|
- Prioritize worker/API/service boundaries first.
|
||||||
|
|
||||||
|
### B3) Retry policy refinement (only if needed)
|
||||||
|
- Keep current bounded retry baseline.
|
||||||
|
- Revisit richer backoff strategy only if observed behavior justifies added complexity.
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
Step 2 reliability behavior stays stable, diagnosable, and right-sized for personal-scale operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream C — Integrate Step 1/2 outputs into ongoing V1 governance
|
||||||
|
|
||||||
|
### C1) Traceability integration
|
||||||
|
- Link completed Step 1/2 outcomes and deferred follow-ups to the V1 traceability matrix.
|
||||||
|
- Ensure open follow-ups have owning step and validation method.
|
||||||
|
|
||||||
|
### C2) Execution alignment with revised V1 plan
|
||||||
|
- Route architecture follow-ups primarily through Steps 3/4/9.
|
||||||
|
- Route reliability/diagnostics follow-ups primarily through Steps 6/7.
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
Step 1/2 work is fully carried forward without revising historical documents.
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
1. This document (`docs/ver1/ver1-step1-2_revised.md`) as the carry-forward implementation plan.
|
||||||
|
2. A compact Step 1/2 carry-forward checklist linked to V1 steps and validation methods.
|
||||||
|
3. Traceability updates showing where each open Step 1/2 follow-up will be closed.
|
||||||
|
4. Optional new artifact for error-path inventory (if created during Step 6/7 execution).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Historical Step 1/Step 2 documents remain unchanged.
|
||||||
|
- Open Step 1/2 follow-ups are explicitly mapped to later V1 steps with validation expectations.
|
||||||
|
- No loss of core technical intent (REQ-7, REQ-10, error taxonomy, retry safety, traceability).
|
||||||
|
- No conflicts introduced with `docs/architecture.md`, `docs/error_handling.md`, or `docs/ver1/ver1.md`.
|
||||||
|
- Carry-forward tasks remain right-sized for personal-scale operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Keep existing Step 1/Step 2 docs unchanged as historical records.
|
||||||
|
2. Define carry-forward tasks and owning V1 steps in this document.
|
||||||
|
3. Create and maintain carry-forward traceability artifacts:
|
||||||
|
- `docs/ver1/ver1-step1-2-carry-forward-checklist.md`
|
||||||
|
- `docs/ver1/ver1-step2-error-path-inventory.md`
|
||||||
|
4. Execute carry-forward tasks during Steps 3+ and capture evidence in step results docs.
|
||||||
|
5. Perform final consistency pass across `docs/ver1/*` references.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Open Step 1/2 items are forgotten as Step 3+ work proceeds.
|
||||||
|
**Mitigation:** Track each follow-up in the V1 traceability matrix with owning step and evidence expectation.
|
||||||
|
|
||||||
|
2. **Risk:** Carry-forward work expands beyond personal-scale needs.
|
||||||
|
**Mitigation:** Apply simplicity guardrails from `docs/architecture.md` before accepting additional hardening tasks.
|
||||||
|
|
||||||
|
3. **Risk:** Reliability follow-ups become fragmented across multiple steps.
|
||||||
|
**Mitigation:** Keep one consolidated carry-forward checklist and update it at milestone check-ins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
This revision effort is scope-alignment and implementation-follow-through focused.
|
||||||
|
Historical Step 1/Step 2 documents are intentionally preserved as-is.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Ver1 Step 1 Results: Architecture Consolidation
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 1 implementation has been completed for the primary architecture-consolidation objectives:
|
||||||
|
|
||||||
|
1. Lifespan-owned runtime resource model introduced for DB runtime ownership.
|
||||||
|
2. Schema bootstrap policy changed from implicit-always to explicit/environment-aware.
|
||||||
|
3. Worker startup now receives lifespan-owned DB engine dependency.
|
||||||
|
4. ADR set established for key V1 architectural decisions.
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Runtime ownership
|
||||||
|
|
||||||
|
- Updated `src/transcription/db.py`:
|
||||||
|
- Added `DatabaseRuntime` resource model.
|
||||||
|
- Added explicit runtime lifecycle methods:
|
||||||
|
- `initialize_database_runtime(...)`
|
||||||
|
- `get_database_runtime()`
|
||||||
|
- `dispose_database_runtime()`
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Lifespan initializes DB runtime and stores it on `app.state`.
|
||||||
|
- Lifespan disposes DB runtime on shutdown.
|
||||||
|
|
||||||
|
### 2) Schema bootstrap policy (REQ-10 alignment)
|
||||||
|
|
||||||
|
- Updated `src/transcription/config.py`:
|
||||||
|
- Added `environment` setting (`development`, `test`, `production`).
|
||||||
|
- Added `bootstrap_schema_on_startup` explicit override setting.
|
||||||
|
- Updated `src/transcription/db.py`:
|
||||||
|
- Added `should_bootstrap_schema(settings)` policy function.
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Startup now calls `create_all(...)` only when policy allows.
|
||||||
|
|
||||||
|
### 3) Worker dependency ownership
|
||||||
|
|
||||||
|
- Updated `src/transcription/worker.py`:
|
||||||
|
- `process_next_queued_job(..., engine=None)` now supports explicit engine injection.
|
||||||
|
- `run_worker_loop(..., engine=None, ...)` now supports explicit engine injection.
|
||||||
|
- Updated `src/transcription/app.py`:
|
||||||
|
- Worker thread is started with lifespan-owned engine.
|
||||||
|
|
||||||
|
### 4) ADR governance
|
||||||
|
|
||||||
|
Created:
|
||||||
|
- `docs/adr/README.md`
|
||||||
|
- `docs/adr/ADR-0001-lifespan-owned-runtime-resources.md`
|
||||||
|
- `docs/adr/ADR-0002-explicit-schema-bootstrap-policy.md`
|
||||||
|
- `docs/adr/ADR-0003-persistence-baseline-and-transition-path.md`
|
||||||
|
- `docs/adr/ADR-0004-in-process-worker-topology.md`
|
||||||
|
|
||||||
|
## Test Evidence
|
||||||
|
|
||||||
|
Targeted regression checks executed successfully:
|
||||||
|
|
||||||
|
- `uv run pytest tests/test_app.py tests/test_db.py tests/services/test_worker.py -q`
|
||||||
|
- Result: pass
|
||||||
|
|
||||||
|
## Residual Risks / Follow-ups
|
||||||
|
|
||||||
|
1. Full REQ-7 completion may still require broader runtime ownership coverage for additional resources as V1 expands.
|
||||||
|
2. Production schema management workflow (migrations/runbook tooling) should be finalized in subsequent V1 steps.
|
||||||
|
3. Additional boundary enforcement automation (import-lint style checks) can be added in later hardening.
|
||||||
|
|
||||||
|
## Step 1 Exit Assessment
|
||||||
|
|
||||||
|
- Architecture ownership clarity: **met**
|
||||||
|
- Schema bootstrap policy hardening: **met**
|
||||||
|
- Worker lifecycle dependency clarity: **met**
|
||||||
|
- ADR baseline established: **met**
|
||||||
|
|
||||||
|
## Completion Checklist With Evidence
|
||||||
|
|
||||||
|
| Criterion | Status | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Architecture conformance matrix approved | partial | Consolidation implemented and documented in `docs/ver1/ver1-step1.md` + this results doc; formal matrix artifact can be added as a follow-up appendix. |
|
||||||
|
| REQ-7 ownership gaps resolved or explicitly deferred | met | Lifespan-owned DB runtime and explicit worker engine wiring implemented in `src/transcription/app.py`, `src/transcription/db.py`, `src/transcription/worker.py`. Residual scope documented under follow-ups. |
|
||||||
|
| REQ-10 explicit bootstrap policy implemented and verified | met | Policy implemented via `environment` + `bootstrap_schema_on_startup` in `src/transcription/config.py`, `should_bootstrap_schema(...)` in `src/transcription/db.py`, startup gate in `src/transcription/app.py`, tested in `tests/test_db.py`. |
|
||||||
|
| Dependency direction rules documented and enforced | partial | Layering and runtime ownership documented in `docs/architecture.md`. Lightweight enforcement exists via review and test discipline; automated import-lint remains a follow-up. |
|
||||||
|
| ADR set created for major Step 1 decisions | met | `docs/adr/README.md` and ADR-0001 through ADR-0004 created. |
|
||||||
|
| Architecture/index docs updated to match implementation | met | `docs/architecture.md` and `docs/index.md` updated with V1 Step 1 runtime policy and links to V1/ADR artifacts. |
|
||||||
|
| Regression and full test suites pass | met | Targeted: `uv run pytest tests/test_app.py tests/test_db.py tests/services/test_worker.py -q`; full suite: `uv run pytest -q`. |
|
||||||
|
| Step 1 results artifact published | met | This document (`docs/ver1/ver1-step1-results.md`) created and updated with summary, evidence, risks, and checklist. |
|
||||||
|
|
||||||
|
Step 1 is complete and ready to hand off to Ver1 Step 2.
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Step 1 Implementation Plan: Architecture Consolidation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Align the implemented MVP codebase with the production architecture and V1 constraints documented in:
|
||||||
|
|
||||||
|
- `docs/architecture.md`
|
||||||
|
- `docs/requirements.md`
|
||||||
|
- `docs/error_handling.md`
|
||||||
|
- `docs/index.md`
|
||||||
|
- `docs/intent.md`
|
||||||
|
- `docs/ver1/ver1.md` (Step 1)
|
||||||
|
|
||||||
|
This step hardens architecture boundaries and ownership without expanding product scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Skill and Guide Inputs Incorporated
|
||||||
|
|
||||||
|
This plan explicitly incorporates patterns and guardrails from john-stream-mcp resources:
|
||||||
|
|
||||||
|
1. `resource://skills/fastapi-uv-docker/document`
|
||||||
|
- App factory and lifespan ownership
|
||||||
|
- Health endpoint and cloud-native baseline expectations
|
||||||
|
- Environment-driven configuration and startup discipline
|
||||||
|
|
||||||
|
2. `resource://skills/fastapi-async-sqlalchemy-modernization/document`
|
||||||
|
- Current-state gap audit first
|
||||||
|
- Target runtime model before refactor
|
||||||
|
- Explicit resource lifecycle ownership
|
||||||
|
- Transaction/session boundary clarity
|
||||||
|
- Phased migration with rollback points
|
||||||
|
|
||||||
|
3. `resource://skills/nicegui/document`
|
||||||
|
- Clear dependency direction
|
||||||
|
- UI/page registration as composition, not business logic container
|
||||||
|
- Async responsiveness and boundary separation
|
||||||
|
|
||||||
|
4. `resource://prompts/greenfield-architecture/document`
|
||||||
|
- Pattern-comparison-first planning
|
||||||
|
- Explicit tradeoffs and staged implementation
|
||||||
|
- Output contract with risks, open questions, and next steps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current-State Gap Summary (Architecture vs Implementation)
|
||||||
|
|
||||||
|
Based on docs and current `src/transcription` code:
|
||||||
|
|
||||||
|
1. **REQ-7 gap (lifespan-owned resources)**
|
||||||
|
- DB engine/session factory are module globals in `db.py`, not app lifespan-owned.
|
||||||
|
- Worker thread lifecycle is owned by lifespan (good), but DB/provider resource ownership is mixed.
|
||||||
|
|
||||||
|
2. **REQ-10 gap (explicit opt-in schema bootstrap)**
|
||||||
|
- `create_all()` is executed unconditionally on startup in `app.py`.
|
||||||
|
|
||||||
|
3. **Data store target gap (REQ-9 + architecture baseline)**
|
||||||
|
- Runtime still defaults to SQLite MVP setup; production architecture targets PostgreSQL baseline with optional MongoDB.
|
||||||
|
|
||||||
|
4. **Layering clarity gap (architecture layer model)**
|
||||||
|
- Boundaries exist but are not yet formally enforced (interface/app/domain/infra dependency rules are implicit, not codified).
|
||||||
|
|
||||||
|
5. **Decision record gap**
|
||||||
|
- No ADR set documenting key V1 architectural decisions and deviations from MVP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 1
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
1. Produce architecture conformance audit and decision records.
|
||||||
|
2. Define and implement target runtime ownership model for core resources.
|
||||||
|
3. Establish explicit schema bootstrap policy (opt-in in production paths).
|
||||||
|
4. Consolidate module boundaries and dependency direction rules.
|
||||||
|
5. Update architecture docs to reflect implemented reality and V1 trajectory.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Full async SQLAlchemy rewrite (plan and seams only if deferred)
|
||||||
|
- MongoDB feature implementation
|
||||||
|
- New user-facing features
|
||||||
|
- Major worker architecture replacement (in-process worker remains baseline)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target Architecture Decisions for V1
|
||||||
|
|
||||||
|
1. **Keep modular monolith topology** (FastAPI + NiceGUI + in-process worker).
|
||||||
|
2. **Preserve container-light simplicity guardrails** from `architecture.md`.
|
||||||
|
3. **Move runtime ownership to lifespan** for:
|
||||||
|
- DB engine/session factory lifecycle
|
||||||
|
- Worker runtime resources
|
||||||
|
- Provider client factory/config lifecycle
|
||||||
|
4. **Adopt explicit schema bootstrap policy**:
|
||||||
|
- Dev/test: opt-in auto-bootstrap allowed
|
||||||
|
- Production: startup must not mutate schema implicitly
|
||||||
|
5. **Formalize boundary map**:
|
||||||
|
- Interface (`api`, `ui`) -> Application (`services`) -> Domain (`models/rules`) -> Infrastructure (`db`, `providers`)
|
||||||
|
- No reverse imports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Work Breakdown
|
||||||
|
|
||||||
|
## Phase A — Architecture Audit and Baseline Freeze
|
||||||
|
|
||||||
|
- [ ] **A1. Produce architecture conformance matrix**
|
||||||
|
- Map each architecture section to current modules/files.
|
||||||
|
- Classify each row: `aligned`, `partial`, `not aligned`.
|
||||||
|
|
||||||
|
- [ ] **A2. Produce REQ-7/REQ-9/REQ-10 focused gap report**
|
||||||
|
- Explicitly capture current vs required state.
|
||||||
|
- Include operational risk if left unresolved.
|
||||||
|
|
||||||
|
- [ ] **A3. Freeze MVP architecture baseline**
|
||||||
|
- Record current baseline behavior and known temporary shortcuts.
|
||||||
|
- Link this baseline from `docs/ver1/ver1.md`.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- `docs/ver1/ver1-step1-audit.md` (or equivalent section in this doc)
|
||||||
|
- Architecture conformance table
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- No architecture changes begin before gap matrix and baseline are approved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Resource Ownership Consolidation (Lifespan-Centric)
|
||||||
|
|
||||||
|
- [ ] **B1. Define runtime resource ownership contract**
|
||||||
|
- `app.py` lifespan owns resource initialization and cleanup order.
|
||||||
|
- `app.state` carries resource handles/factories.
|
||||||
|
- No hidden module-global side-effect initialization for runtime resources.
|
||||||
|
|
||||||
|
- [ ] **B2. Refactor DB ownership model**
|
||||||
|
- Replace module-global engine singleton pattern with lifespan-initialized resource model.
|
||||||
|
- Define one canonical session-factory access path for app/worker/services.
|
||||||
|
|
||||||
|
- [ ] **B3. Normalize worker dependencies**
|
||||||
|
- Ensure worker uses lifespan-owned resources/factories rather than implicit globals.
|
||||||
|
- Preserve deterministic startup/shutdown behavior.
|
||||||
|
|
||||||
|
- [ ] **B4. Define provider adapter ownership**
|
||||||
|
- Provider client creation strategy is centralized and lifecycle-aware.
|
||||||
|
- Avoid per-call hidden client construction when unnecessary.
|
||||||
|
|
||||||
|
### MCP-Guided Guardrails
|
||||||
|
- Use explicit lifecycle composition patterns from `fastapi-async-sqlalchemy-modernization`.
|
||||||
|
- Maintain app-factory + lifespan structure per `fastapi-uv-docker`.
|
||||||
|
- Keep UI registration as composition only per `nicegui`.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Core runtime resources have one owner and one cleanup path.
|
||||||
|
- No critical resource has ambiguous ownership.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Schema Bootstrap Policy (REQ-10 Alignment)
|
||||||
|
|
||||||
|
- [ ] **C1. Define environment-aware bootstrap policy**
|
||||||
|
- `auto_create_schema` (or equivalent) disabled in production by default.
|
||||||
|
- Startup schema mutation is explicit and intentional.
|
||||||
|
|
||||||
|
- [ ] **C2. Split startup responsibilities**
|
||||||
|
- App startup performs health-critical initialization only.
|
||||||
|
- Schema bootstrap path is moved to explicit command/flag workflow.
|
||||||
|
|
||||||
|
- [ ] **C3. Update deployment/runbook docs**
|
||||||
|
- Document migration/bootstrap flow for dev, staging, prod.
|
||||||
|
- Ensure policy is testable and auditable.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Normal production startup path does not call schema auto-create implicitly.
|
||||||
|
- Bootstrap behavior is explicit and documented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Module Boundary Enforcement
|
||||||
|
|
||||||
|
- [ ] **D1. Publish dependency direction rules**
|
||||||
|
- Allowed import directions across `api`, `ui`, `services`, `models/domain`, `db/providers`.
|
||||||
|
- Explicitly disallow reverse dependencies.
|
||||||
|
|
||||||
|
- [ ] **D2. Reconcile package map with docs**
|
||||||
|
- Ensure docs’ architecture elements match real package layout and naming.
|
||||||
|
- Update docs where intentional deviations remain.
|
||||||
|
|
||||||
|
- [ ] **D3. Isolate cross-layer responsibilities**
|
||||||
|
- Keep API/UI presentation concerns out of services.
|
||||||
|
- Keep provider/DB specifics out of interface layer.
|
||||||
|
|
||||||
|
- [ ] **D4. Add lightweight architecture checks**
|
||||||
|
- Add static/import checks and/or review checklist in CI/review process.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Boundary rules are documented and applied.
|
||||||
|
- Architectural drift can be detected during review/CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
- [ ] **E1. Create ADR index**
|
||||||
|
- Add `docs/adr/README.md` with template and status model.
|
||||||
|
|
||||||
|
- [ ] **E2. Record minimum V1 ADR set**
|
||||||
|
1. Runtime ownership model (lifespan-owned resources)
|
||||||
|
2. Schema bootstrap policy (explicit vs implicit)
|
||||||
|
3. Persistence baseline (PostgreSQL target; SQLite transition strategy)
|
||||||
|
4. Worker topology (in-process for V1, extension path preserved)
|
||||||
|
|
||||||
|
- [ ] **E3. Cross-link ADRs**
|
||||||
|
- Link from architecture and V1 docs.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Major architecture decisions are explicit, versioned, and discoverable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Documentation Consolidation
|
||||||
|
|
||||||
|
- [ ] **F1. Update `docs/architecture.md`**
|
||||||
|
- Reflect real implementation and V1 target state separately.
|
||||||
|
- Mark transitional choices clearly.
|
||||||
|
|
||||||
|
- [ ] **F2. Update `docs/index.md` navigation consistency**
|
||||||
|
- Ensure architecture/readme references match actual docs/files.
|
||||||
|
|
||||||
|
- [ ] **F3. Update `docs/requirements.md` traceability notes**
|
||||||
|
- Mark REQ-7/REQ-10 status and verification approach after consolidation.
|
||||||
|
|
||||||
|
- [ ] **F4. Add Step 1 result summary**
|
||||||
|
- Create `docs/ver1/ver1-step1-results.md` after implementation.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Docs are internally consistent and match runtime architecture reality.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Plan
|
||||||
|
|
||||||
|
## Architecture Verification Matrix (Step 1)
|
||||||
|
|
||||||
|
1. **Inspection**
|
||||||
|
- Resource ownership map exists and matches code.
|
||||||
|
- Schema bootstrap policy is explicit and environment-aware.
|
||||||
|
- ADRs exist for each key architecture decision.
|
||||||
|
|
||||||
|
2. **Automated checks**
|
||||||
|
- Existing test suite remains green.
|
||||||
|
- New/updated tests validate startup policy (no implicit schema mutation in production mode).
|
||||||
|
- Import/dependency-direction checks pass (if introduced).
|
||||||
|
|
||||||
|
3. **Demonstration**
|
||||||
|
- App starts in dev mode with explicit expected behavior.
|
||||||
|
- App starts in production mode without mutating schema implicitly.
|
||||||
|
- Worker lifecycle starts/stops cleanly with app lifespan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Refactor destabilizes MVP behavior
|
||||||
|
**Mitigation:** Phase changes with small PRs and regression checks after each phase.
|
||||||
|
|
||||||
|
2. **Risk:** Over-rotation into premature async rewrite
|
||||||
|
**Mitigation:** Keep this step focused on lifecycle ownership and boundaries; defer full async migration unless required.
|
||||||
|
|
||||||
|
3. **Risk:** Schema policy changes break local DX
|
||||||
|
**Mitigation:** Keep explicit dev bootstrap path simple and documented.
|
||||||
|
|
||||||
|
4. **Risk:** Boundary rules become “doc only”
|
||||||
|
**Mitigation:** Add CI/review enforcement and architecture checklist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Phase A — Audit and baseline freeze
|
||||||
|
2. Phase B — Resource ownership consolidation
|
||||||
|
3. Phase C — Schema bootstrap policy
|
||||||
|
4. Phase D — Boundary enforcement
|
||||||
|
5. Phase E — ADR authoring
|
||||||
|
6. Phase F — Documentation consolidation
|
||||||
|
|
||||||
|
This order minimizes risk: diagnose first, then refactor ownership, then lock policy, then enforce boundaries, and finally finalize docs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 Completion Checklist
|
||||||
|
|
||||||
|
- [ ] Architecture conformance matrix approved.
|
||||||
|
- [ ] REQ-7 ownership gaps resolved or explicitly deferred with owner/date.
|
||||||
|
- [ ] REQ-10 explicit bootstrap policy implemented and verified.
|
||||||
|
- [ ] Dependency direction rules documented and enforced.
|
||||||
|
- [ ] ADR set created for all major Step 1 decisions.
|
||||||
|
- [ ] Architecture and index docs updated to match implementation.
|
||||||
|
- [ ] Full test suite passes after consolidation.
|
||||||
|
- [ ] `docs/ver1/ver1-step1-results.md` created with evidence and residual risks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 2
|
||||||
|
|
||||||
|
Once Step 1 completes, Step 2 (Error Handling & Reliability Hardening) can proceed on stable architecture seams:
|
||||||
|
|
||||||
|
- consistent lifecycle ownership,
|
||||||
|
- explicit startup policy,
|
||||||
|
- clear module boundaries,
|
||||||
|
- documented architecture decisions.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Ver1 Step 2 Error-Path Inventory (Carry-Forward)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provide a compact inventory of major failure paths with taxonomy mapping and retry behavior, aligned with:
|
||||||
|
- `docs/error_handling.md`
|
||||||
|
- `docs/ver1/ver1-step2-results.md`
|
||||||
|
- `docs/ver1/ver1-step1-2-carry-forward-checklist.md` (CF-B1)
|
||||||
|
|
||||||
|
This is a lightweight operational artifact for Step 6/7 follow-through.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inventory Table
|
||||||
|
|
||||||
|
| Path ID | Boundary/Operation | Typical Failure Source | Category | Retriable | Surface Behavior | Current Coverage | Notes |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| EP-API-001 | API upload request validation | invalid payload / empty file metadata | `validation_error` | no | structured API error envelope (400) | partial | confirm all upload variants |
|
||||||
|
| EP-API-002 | API resource lookup | missing job/document | `not_found_error` | no | structured API error envelope (404) | partial | verify consistency for all lookup routes |
|
||||||
|
| EP-SVC-001 | Service provider-call mapping | provider SDK/HTTP failure | `external_provider_error` | sometimes | normalized AppError and safe message | partial | ensure consistent mapping in service boundary tests |
|
||||||
|
| EP-WKR-001 | Worker provider timeout | timeout/unavailable upstream | `external_provider_error` or `infrastructure_transient_error` | yes | retry or terminal failed with persisted reason | partial | validate category mapping remains deterministic |
|
||||||
|
| EP-WKR-002 | Worker non-retriable domain/input failure | deterministic invalid input/state | `user_input_error` or `conflict_error` | no | immediate terminal failed with persisted reason | partial | ensure no retry on non-retriable categories |
|
||||||
|
| EP-WKR-003 | Worker retry exhaustion | repeated retriable failure | category from source; terminal state | capped then no | explicit failed status + error detail | met | implemented in Step 2; keep regression coverage |
|
||||||
|
| EP-UI-001 | UI upload action failure | surfaced AppError or fallback exception | category-based safe user message | category-driven | title + message + suggestion + error id | partial | verify consistency on all primary UI actions |
|
||||||
|
| EP-LOG-001 | Cross-boundary error logging | missing/uneven fields | n/a | n/a | logs include `error_id`, `category`, `operation`, ids when available | partial | complete in Step 6 (CF-B2) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Targets (Step 6/7)
|
||||||
|
|
||||||
|
1. Every critical path has category + retriable policy defined.
|
||||||
|
2. API/UI behavior remains safe and actionable.
|
||||||
|
3. Worker terminal failures are explicit and persisted.
|
||||||
|
4. Logging fields are consistent at critical handoffs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evidence Links
|
||||||
|
|
||||||
|
- Step 2 implementation results: `docs/ver1/ver1-step2-results.md`
|
||||||
|
- Carry-forward tracking: `docs/ver1/ver1-step1-2-carry-forward-checklist.md`
|
||||||
|
- Canonical contract: `docs/error_handling.md`
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Ver1 Step 2 Results: Error Handling & Reliability Hardening
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 2 implementation is complete for the planned reliability and error-handling hardening scope:
|
||||||
|
|
||||||
|
1. Worker retries are now explicit, bounded, and category-driven.
|
||||||
|
2. Error behavior is more consistent across worker/API/UI boundaries.
|
||||||
|
3. Logging now includes stronger boundary context in key failure paths.
|
||||||
|
4. Test coverage was expanded for retry policy and new reliability settings.
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Worker retry policy and terminal behavior
|
||||||
|
|
||||||
|
- Updated `src/transcription/models.py`:
|
||||||
|
- Added `Job.retry_count` with default `0`.
|
||||||
|
- Updated `src/transcription/config.py`:
|
||||||
|
- Added `worker_max_retries`.
|
||||||
|
- Added `worker_retry_backoff_seconds`.
|
||||||
|
- Updated `src/transcription/worker.py`:
|
||||||
|
- Added bounded retry decision path (`_should_retry`).
|
||||||
|
- Added requeue behavior (`_requeue_for_retry`) for retriable errors.
|
||||||
|
- Added deterministic terminal failure behavior (`_finalize_failed_job`).
|
||||||
|
- Preserved transcript failure detail persistence (`error_id`, `category`, suggestion).
|
||||||
|
|
||||||
|
### 2) API fallback normalization hardening
|
||||||
|
|
||||||
|
- Updated `src/transcription/api/errors.py`:
|
||||||
|
- Fallback handler now emits safe generic internal message for unhandled exceptions.
|
||||||
|
- Added structured boundary logging fields including operation and exception type.
|
||||||
|
|
||||||
|
### 3) UI interaction reliability guard
|
||||||
|
|
||||||
|
- Updated `src/transcription/ui/upload_page.py`:
|
||||||
|
- Added duplicate in-flight submission guard to prevent repeated upload handling while busy.
|
||||||
|
|
||||||
|
### 4) Observability/logging improvements
|
||||||
|
|
||||||
|
- Updated worker logs in `src/transcription/worker.py` to include operation and domain identifiers in key transitions:
|
||||||
|
- pick
|
||||||
|
- retry
|
||||||
|
- transcribed
|
||||||
|
- failed
|
||||||
|
|
||||||
|
## Test Coverage Added/Updated
|
||||||
|
|
||||||
|
- Updated `tests/test_models.py`:
|
||||||
|
- Assert `retry_count` default.
|
||||||
|
- Updated `tests/test_config.py`:
|
||||||
|
- Added worker retry settings default test.
|
||||||
|
- Updated `tests/services/test_worker.py`:
|
||||||
|
- Added retriable requeue test.
|
||||||
|
- Added retry-exhaustion terminal failure test.
|
||||||
|
- Updated existing tests for settings-driven worker behavior.
|
||||||
|
- Existing API error tests remained green with fallback behavior updates:
|
||||||
|
- `tests/api/test_error_responses.py`
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
Executed and passing:
|
||||||
|
|
||||||
|
- `uv run pytest tests/services/test_worker.py tests/test_models.py tests/test_config.py tests/api/test_error_responses.py -q`
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
## Residual Risks / Follow-ups
|
||||||
|
|
||||||
|
1. Retry policy currently uses simple fixed backoff; richer strategy (exponential/jitter) can be added in later hardening.
|
||||||
|
2. Full cross-layer structured logging standardization can be expanded in Step 6 observability work.
|
||||||
|
3. A formal Step 2 error-path inventory artifact (`ver1-step2-audit.md`) is still recommended for governance completeness.
|
||||||
|
|
||||||
|
## Step 2 Exit Assessment
|
||||||
|
|
||||||
|
- Error taxonomy and envelope stability: **met**
|
||||||
|
- Bounded retry and terminal failure behavior: **met**
|
||||||
|
- Worker reliability controls: **met**
|
||||||
|
- UI interaction hardening for duplicate actions: **met**
|
||||||
|
- Test coverage expansion and full-suite regression safety: **met**
|
||||||
|
|
||||||
|
Step 2 is complete and ready to hand off to Ver1 Step 3.
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
# Step 2 Implementation Plan: Error Handling & Reliability Hardening
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Implement **Ver1 Step 2** from `docs/ver1/ver1.md` by standardizing failure behavior and reliability controls so the system fails safely, predictably, and transparently across UI, API, services, worker, and provider boundaries.
|
||||||
|
|
||||||
|
Primary governing docs:
|
||||||
|
|
||||||
|
- `docs/error_handling.md` (authoritative contract)
|
||||||
|
- `docs/requirements.md` (REQ-2, REQ-3, REQ-4, REQ-5, REQ-6)
|
||||||
|
- `docs/architecture.md` (boundary ownership and worker lifecycle)
|
||||||
|
- `docs/ver1/ver1.md` (Step 2 objective)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Skill and Guide Inputs Incorporated
|
||||||
|
|
||||||
|
This plan integrates guidance from john-stream-mcp resources:
|
||||||
|
|
||||||
|
1. `resource://skills/python-logging-dictconfig/document`
|
||||||
|
- centralized `dictConfig` logging
|
||||||
|
- startup-only configuration
|
||||||
|
- stable named loggers and boundary-level logging discipline
|
||||||
|
|
||||||
|
2. `resource://skills/pytesting/document`
|
||||||
|
- deterministic, behavior-first tests
|
||||||
|
- explicit marker usage and fast/slow lane discipline
|
||||||
|
- integration checks for boundary behavior and error contracts
|
||||||
|
|
||||||
|
3. `resource://skills/fastapi-async-sqlalchemy-modernization/document`
|
||||||
|
- classify at source boundary
|
||||||
|
- explicit transaction/session behavior under failure
|
||||||
|
- phased rollout with quality gates and rollback awareness
|
||||||
|
|
||||||
|
4. `resource://skills/nicegui-ui-customization/document`
|
||||||
|
- explicit user-facing error feedback for each interaction
|
||||||
|
- prevent duplicate actions during in-flight operations
|
||||||
|
- preserve one-way dependency boundaries from UI -> services
|
||||||
|
|
||||||
|
5. `resource://skills/fastapi-uv-docker/document` (applied selectively)
|
||||||
|
- lifespan-safe startup/shutdown behavior
|
||||||
|
- health/readiness posture and cloud-native operational checks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current-State Gap Summary
|
||||||
|
|
||||||
|
The project already has a strong baseline (`AppError`, taxonomy enum, API envelope, worker persistence), but Step 2 needs completion-level hardening:
|
||||||
|
|
||||||
|
1. **Error contract consistency**
|
||||||
|
- API envelope exists, but consistency must be verified for all error pathways.
|
||||||
|
2. **Cross-boundary category normalization**
|
||||||
|
- Provider/service/worker mappings exist, but require stricter policy checks and tests.
|
||||||
|
3. **Retry policy implementation depth**
|
||||||
|
- Step 2 requires bounded retry policy and clear terminal behavior for retriable failures.
|
||||||
|
4. **Operational traceability**
|
||||||
|
- Logging exists; Step 2 requires consistent structured fields at critical boundaries.
|
||||||
|
5. **UI failure UX consistency**
|
||||||
|
- UI error handling exists; Step 2 requires explicit contract coverage and anti-duplication safeguards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 2
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
1. Enforce canonical error taxonomy and envelope across all boundaries.
|
||||||
|
2. Standardize logging fields and boundary-level error traceability.
|
||||||
|
3. Implement/complete bounded retry and terminal failure behavior in worker paths.
|
||||||
|
4. Improve UI/API error presentation consistency and actionable guidance.
|
||||||
|
5. Add comprehensive Step 2 test coverage and verification matrix.
|
||||||
|
6. Update documentation to reflect final Step 2 policies and behavior.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- Major architecture/topology changes (external queue, distributed worker)
|
||||||
|
- New end-user feature expansion outside reliability/error handling
|
||||||
|
- Full async ORM migration (unless required by bug fix)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target Decisions for Step 2
|
||||||
|
|
||||||
|
1. **Taxonomy stability is mandatory**
|
||||||
|
- `ErrorCategory` values remain stable contract identifiers.
|
||||||
|
2. **Classification occurs at source boundary**
|
||||||
|
- adapters/services normalize early; UI/API only present safely.
|
||||||
|
3. **User safety over internal detail leakage**
|
||||||
|
- expose safe message + suggestion + error_id; keep sensitive detail in logs.
|
||||||
|
4. **Retry is explicit and bounded**
|
||||||
|
- only retriable categories may retry; retries are capped; terminal failures persist reason.
|
||||||
|
5. **Boundary logs carry correlation fields**
|
||||||
|
- include `error_id`, `category`, `operation`, and domain identifiers where available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Work Breakdown
|
||||||
|
|
||||||
|
## Phase A — Error Contract Audit and Policy Lock
|
||||||
|
|
||||||
|
- [ ] **A1. Build error-path inventory**
|
||||||
|
- Enumerate all failure entry points across:
|
||||||
|
- `api/`
|
||||||
|
- `ui/`
|
||||||
|
- `services/`
|
||||||
|
- `worker.py`
|
||||||
|
- `providers/`
|
||||||
|
|
||||||
|
- [ ] **A2. Produce taxonomy mapping table**
|
||||||
|
- For each known exception path, map:
|
||||||
|
- source exception type
|
||||||
|
- target `ErrorCategory`
|
||||||
|
- retriable flag
|
||||||
|
- API status (if exposed)
|
||||||
|
|
||||||
|
- [ ] **A3. Reconcile with `docs/error_handling.md`**
|
||||||
|
- Resolve any mismatch in category semantics, status codes, or suggested actions.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- `docs/ver1/ver1-step2-audit.md` (recommended)
|
||||||
|
- taxonomy mapping table
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Every known failure path has explicit category + retriable policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — API and Service Contract Hardening
|
||||||
|
|
||||||
|
- [ ] **B1. Enforce API envelope completeness**
|
||||||
|
- Ensure all API errors return:
|
||||||
|
- `error_id`, `category`, `message`, `suggestion`, `timestamp`
|
||||||
|
|
||||||
|
- [ ] **B2. Verify category-to-status mapping consistency**
|
||||||
|
- Confirm `api/errors.py` matches `docs/error_handling.md` mapping guidance.
|
||||||
|
|
||||||
|
- [ ] **B3. Normalize service exceptions at boundary**
|
||||||
|
- Services should raise `AppError` subclasses for known failures.
|
||||||
|
- Unknown exceptions must become `internal_unexpected_error` with traceable `error_id`.
|
||||||
|
|
||||||
|
- [ ] **B4. Ensure safe detail handling**
|
||||||
|
- API/UI messages remain safe.
|
||||||
|
- Diagnostic context remains in logs/persisted failure detail where appropriate.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- No unstructured/unclassified exception escapes core boundaries.
|
||||||
|
- API responses are contract-stable for all tested failure modes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Worker Retry and Terminal Failure Policy
|
||||||
|
|
||||||
|
- [ ] **C1. Define bounded retry policy**
|
||||||
|
- Add configurable retry settings (attempt limit/backoff policy).
|
||||||
|
- Limit retries to retriable categories.
|
||||||
|
|
||||||
|
- [ ] **C2. Implement terminal failure persistence**
|
||||||
|
- On retry exhaustion, persist clear terminal reason and `error_id`.
|
||||||
|
- Ensure job status transitions end deterministically at `failed`.
|
||||||
|
|
||||||
|
- [ ] **C3. Add duplicate-processing safety checks**
|
||||||
|
- Prevent duplicate terminal updates when job already resolved.
|
||||||
|
|
||||||
|
- [ ] **C4. Validate worker lifecycle under repeated transient failures**
|
||||||
|
- Ensure loop remains stable and responsive.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Retries are bounded and policy-driven.
|
||||||
|
- Exhausted retries produce deterministic failed state with evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Logging and Observability Contract Enforcement
|
||||||
|
|
||||||
|
- [ ] **D1. Central logging conformance check**
|
||||||
|
- Confirm startup-only `dictConfig` use remains canonical.
|
||||||
|
- No module-level `basicConfig` use.
|
||||||
|
|
||||||
|
- [ ] **D2. Standardize error log fields**
|
||||||
|
- Require at minimum when available:
|
||||||
|
- `error_id`, `category`, `operation`, `exception_type`, `job_id`, `document_id`
|
||||||
|
|
||||||
|
- [ ] **D3. Boundary handoff logging**
|
||||||
|
- Add/normalize logs at transitions:
|
||||||
|
- UI action -> service
|
||||||
|
- service -> provider/db
|
||||||
|
- worker pickup -> terminal state
|
||||||
|
|
||||||
|
- [ ] **D4. Log noise control**
|
||||||
|
- Avoid duplicate stack-trace logging across layers for same exception.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Critical failure events are traceable end-to-end via logs and `error_id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — UI Error UX Consistency and Interaction Hardening
|
||||||
|
|
||||||
|
- [ ] **E1. Standardize user error presentation**
|
||||||
|
- For upload/jobs interactions, ensure:
|
||||||
|
- clear title
|
||||||
|
- plain-language message
|
||||||
|
- suggested action
|
||||||
|
- visible error reference id
|
||||||
|
|
||||||
|
- [ ] **E2. Add in-flight interaction guards**
|
||||||
|
- Prevent duplicate submits/click storms during pending operations.
|
||||||
|
|
||||||
|
- [ ] **E3. Ensure deterministic UI state recovery**
|
||||||
|
- controls re-enable after failure
|
||||||
|
- status text remains actionable
|
||||||
|
|
||||||
|
- [ ] **E4. Keep UI boundary clean**
|
||||||
|
- no provider/protocol details leaked into page modules
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- All primary UI actions have consistent success/failure interaction behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Test Expansion and Verification
|
||||||
|
|
||||||
|
Apply pytesting guidance: behavior-first assertions, deterministic fixtures, strict markers.
|
||||||
|
|
||||||
|
- [ ] **F1. API error contract tests**
|
||||||
|
- verify envelope fields and status mapping for each category class.
|
||||||
|
|
||||||
|
- [ ] **F2. Service classification tests**
|
||||||
|
- verify known failures map to expected `AppError` subclasses/categories.
|
||||||
|
|
||||||
|
- [ ] **F3. Worker retry policy tests**
|
||||||
|
- retriable failure retries and eventual success
|
||||||
|
- retriable failure exhaustion -> terminal failed
|
||||||
|
- non-retriable failure -> immediate failed
|
||||||
|
|
||||||
|
- [ ] **F4. UI error behavior tests**
|
||||||
|
- upload/jobs actions show actionable feedback on failures
|
||||||
|
- duplicate action guard behavior
|
||||||
|
|
||||||
|
- [ ] **F5. Regression guard tests**
|
||||||
|
- at least one test per previously observed production/real-world failure mode
|
||||||
|
|
||||||
|
### Validation Commands
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
- `uv run pytest -m unit -q`
|
||||||
|
- `uv run pytest -m "not external" -q`
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- All Step 2 reliability/error contract tests pass.
|
||||||
|
- Existing suite remains green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Phase A — audit and policy lock
|
||||||
|
2. Phase B — API/service contract hardening
|
||||||
|
3. Phase C — worker retry and terminal policy
|
||||||
|
4. Phase D — logging/traceability normalization
|
||||||
|
5. Phase E — UI consistency hardening
|
||||||
|
6. Phase F — test expansion and full verification
|
||||||
|
|
||||||
|
This order reduces risk by locking policy first, then applying behavior changes at core boundaries before UI polish.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Overly broad retry policy causes hidden failure loops
|
||||||
|
**Mitigation:** strict category-based retry eligibility + hard cap + terminal persistence.
|
||||||
|
|
||||||
|
2. **Risk:** User-facing messages become too technical
|
||||||
|
**Mitigation:** enforce safe message + suggestion contract in tests.
|
||||||
|
|
||||||
|
3. **Risk:** Logging becomes noisy/redundant
|
||||||
|
**Mitigation:** boundary logging rules and single-trace ownership.
|
||||||
|
|
||||||
|
4. **Risk:** Reliability work introduces regressions in happy path
|
||||||
|
**Mitigation:** run full suite continuously; preserve integration pipeline tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 Completion Checklist
|
||||||
|
|
||||||
|
- [ ] Error taxonomy mapping table completed and approved.
|
||||||
|
- [ ] API envelope and HTTP status behavior verified for all relevant failure categories.
|
||||||
|
- [ ] Service/provider exception normalization is consistent and tested.
|
||||||
|
- [ ] Worker retry behavior is bounded, explicit, and terminal-state safe.
|
||||||
|
- [ ] Structured error logging fields are present at boundary handoffs.
|
||||||
|
- [ ] UI failure flows provide clear, actionable, and traceable feedback.
|
||||||
|
- [ ] Full test suite passes with new Step 2 coverage included.
|
||||||
|
- [ ] `docs/ver1/ver1-step2-results.md` created with evidence and residual risks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 3
|
||||||
|
|
||||||
|
After Step 2 completion, Step 3 (Functional Completion by Requirement Domain) proceeds on a hardened foundation:
|
||||||
|
|
||||||
|
- stable failure contracts,
|
||||||
|
- predictable retries and terminal behavior,
|
||||||
|
- actionable user/API error semantics,
|
||||||
|
- improved diagnostic traceability.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Ver1 Step 3 Results: Functional Completion by Requirement Domain
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 3 implementation has been completed for the planned functional-completion scope in a practical personal-scale form.
|
||||||
|
|
||||||
|
Implemented in this step:
|
||||||
|
|
||||||
|
1. Revision history and acceptance workflows for transcripts.
|
||||||
|
2. Search over accepted transcript revisions.
|
||||||
|
3. Export of accepted transcript data.
|
||||||
|
4. API routes for jobs, revisions, search, and export.
|
||||||
|
5. UI pathways for revision management, search, and export.
|
||||||
|
6. Carry-forward integration updates for Step 1/2 follow-ups owned by Step 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Data model expansion (functional domain)
|
||||||
|
|
||||||
|
Updated `src/transcription/models.py`:
|
||||||
|
|
||||||
|
- Added `JobStatus.COMPLETED`.
|
||||||
|
- Added `TranscriptRevision` table/model:
|
||||||
|
- `job_id`
|
||||||
|
- `revision_number`
|
||||||
|
- `text`
|
||||||
|
- `source`
|
||||||
|
- `accepted`
|
||||||
|
- `created_at`
|
||||||
|
- Added `Job.revisions` relationship.
|
||||||
|
|
||||||
|
This supports immutable revision history and accepted-transcript semantics for search/export.
|
||||||
|
|
||||||
|
### 2) Step 3 service layer
|
||||||
|
|
||||||
|
Created `src/transcription/services/library.py` with service-backed functional operations:
|
||||||
|
|
||||||
|
- `list_jobs(...)`
|
||||||
|
- `get_job_detail(...)`
|
||||||
|
- `add_revision(...)`
|
||||||
|
- `accept_revision(...)`
|
||||||
|
- `list_revisions(...)`
|
||||||
|
- `search_accepted_transcripts(...)`
|
||||||
|
- `export_transcripts(...)`
|
||||||
|
|
||||||
|
Key behavior:
|
||||||
|
|
||||||
|
- revisions are append-only and incrementing
|
||||||
|
- accepted revision is unique per job
|
||||||
|
- accepting a revision syncs canonical transcript and sets job to `completed`
|
||||||
|
- search scope is accepted revisions only
|
||||||
|
- export emits deterministic record payloads for archive workflows
|
||||||
|
|
||||||
|
### 3) Worker integration for revision provenance
|
||||||
|
|
||||||
|
Updated `src/transcription/worker.py`:
|
||||||
|
|
||||||
|
- Success path now calls `add_revision(..., source="worker", accepted=False)`.
|
||||||
|
- Worker still persists canonical transcript and `transcribed` job state.
|
||||||
|
- Initial machine transcription now appears in revision history.
|
||||||
|
|
||||||
|
### 4) API functional completion
|
||||||
|
|
||||||
|
Created `src/transcription/api/routes.py` and wired in `src/transcription/app.py`.
|
||||||
|
|
||||||
|
New endpoints:
|
||||||
|
|
||||||
|
- `GET /api/jobs`
|
||||||
|
- `GET /api/jobs/{job_id}`
|
||||||
|
- `GET /api/jobs/{job_id}/revisions`
|
||||||
|
- `POST /api/jobs/{job_id}/revisions`
|
||||||
|
- `POST /api/revisions/{revision_id}/accept`
|
||||||
|
- `GET /api/search?query=...`
|
||||||
|
- `GET /api/export?accepted_only=true|false`
|
||||||
|
|
||||||
|
### 5) UI functional completion
|
||||||
|
|
||||||
|
Updated `src/transcription/ui/jobs_page.py`:
|
||||||
|
|
||||||
|
- Job detail now includes revision history panel.
|
||||||
|
- Added user revision submission.
|
||||||
|
- Added revision accept action.
|
||||||
|
- Added `/search` page for accepted transcript search.
|
||||||
|
- Added `/export` page for accepted transcript export preview.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Evidence
|
||||||
|
|
||||||
|
### Added/Updated Tests
|
||||||
|
|
||||||
|
1. `tests/services/test_library.py`
|
||||||
|
- revision append/accept behavior
|
||||||
|
- accepted-only search behavior
|
||||||
|
- export payload behavior
|
||||||
|
|
||||||
|
2. `tests/api/test_routes.py`
|
||||||
|
- jobs/revisions/search/export API serialization and contract behavior
|
||||||
|
|
||||||
|
3. `tests/test_models.py`
|
||||||
|
- `completed` status transition coverage
|
||||||
|
- `TranscriptRevision` persistence and relationship coverage
|
||||||
|
|
||||||
|
4. `tests/services/test_worker.py`
|
||||||
|
- success-path now verifies initial worker-generated revision persistence
|
||||||
|
|
||||||
|
### Full Validation Run
|
||||||
|
|
||||||
|
Executed and passing:
|
||||||
|
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirement Slice Coverage (Step 3)
|
||||||
|
|
||||||
|
| Slice | REQ Coverage | Status | Evidence |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Core lifecycle completion and visibility | REQ-0, REQ-2, REQ-3, REQ-5, REQ-6 | met | worker integration + API/UI jobs routes + tests |
|
||||||
|
| Revision history and acceptance | REQ-3, REQ-4, REQ-5, REQ-11 | met | `TranscriptRevision`, `services/library.py`, UI revision panel, tests |
|
||||||
|
| Search over accepted transcripts | REQ-5, REQ-11 | met | `search_accepted_transcripts`, `/api/search`, `/ui/search`, tests |
|
||||||
|
| Export transcript data | REQ-4, REQ-5, REQ-11 | met | `export_transcripts`, `/api/export`, `/ui/export`, tests |
|
||||||
|
| Prompt and verbatim flow continuity | REQ-12 | met (continued) | worker transcription flow unchanged in prompt-loading contract |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Carry-Forward Integration Updates
|
||||||
|
|
||||||
|
Updated:
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step1-2-carry-forward-checklist.md`
|
||||||
|
|
||||||
|
Step 3 updates recorded for:
|
||||||
|
|
||||||
|
- CF-A1: in progress with Step 3 inspection evidence
|
||||||
|
- CF-A3: in progress with boundary-discipline evidence
|
||||||
|
- CF-C1: done (Step 3 traceability artifacts integrated)
|
||||||
|
- CF-C2: in progress (routing preserved for later steps)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Residual Follow-ups
|
||||||
|
|
||||||
|
1. Step 4: migration rehearsal and rollback runbook execution for schema changes.
|
||||||
|
2. Step 6/7: broader error-path inventory closure and logging field normalization.
|
||||||
|
3. Step 9: release readiness reconfirmation for runtime ownership and migration behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 Exit Assessment
|
||||||
|
|
||||||
|
- Requirement-domain functional completion: **met**
|
||||||
|
- Data integrity and state consistency for new flows: **met**
|
||||||
|
- API/UI parity for new Step 3 features: **met**
|
||||||
|
- Test and regression safety: **met**
|
||||||
|
- Carry-forward integration obligations (Step 3-owned): **met/in progress as routed**
|
||||||
|
|
||||||
|
Step 3 is complete and ready to hand off to Step 4.
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# Step 3 Implementation Plan: Functional Completion by Requirement Domain
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Implement **Ver1 Step 3** from `docs/ver1/ver1.md` by completing all in-scope V1 functional requirements in a practical, user-first order while preserving:
|
||||||
|
|
||||||
|
- personal-scale operation
|
||||||
|
- single-operator workflow
|
||||||
|
- private-network deployment assumptions
|
||||||
|
- low operational overhead
|
||||||
|
- clean architecture boundaries
|
||||||
|
|
||||||
|
Primary governing docs:
|
||||||
|
|
||||||
|
- `docs/ver1/ver1.md` (Step 3 objective and sequencing)
|
||||||
|
- `docs/architecture.md` (module boundaries, workflow, simplicity guardrails)
|
||||||
|
- `docs/requirements.md` (REQ-0 through REQ-12 traceability)
|
||||||
|
- `docs/error_handling.md` (error contract across boundaries)
|
||||||
|
- `docs/intent.md` (verbatim transcription policy and prompt curation)
|
||||||
|
- `docs/ver1/ver1-step1-2-carry-forward-checklist.md` (Step 1/2 carry-forward integration)
|
||||||
|
- `docs/ver1/ver1-step2-error-path-inventory.md` (failure-path coverage visibility)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Resources Reviewed and Applied
|
||||||
|
|
||||||
|
All resources on `john-stream-mcp` were reviewed. Step 3 applies the following guidance directly:
|
||||||
|
|
||||||
|
1. `resource://skills/nicegui/document`
|
||||||
|
- modular page registration
|
||||||
|
- one-way dependency flow (`ui/api -> services -> infra`)
|
||||||
|
- async-first UI responsiveness expectations
|
||||||
|
|
||||||
|
2. `resource://skills/nicegui-ui-customization/document`
|
||||||
|
- reusable UI component extraction for repeated patterns
|
||||||
|
- in-flight guards and explicit success/failure user feedback
|
||||||
|
- event-driven updates over ad-hoc polling
|
||||||
|
|
||||||
|
3. `resource://skills/fastapi-async-sqlalchemy-modernization/document`
|
||||||
|
- explicit transaction/session boundaries
|
||||||
|
- deterministic resource ownership and cleanup continuity from Step 1
|
||||||
|
- incremental migration strategy with rollback-aware checkpoints
|
||||||
|
|
||||||
|
4. `resource://skills/pydantic-settings/document`
|
||||||
|
- typed configuration as single source of runtime truth
|
||||||
|
- explicit source precedence and environment-safe defaults
|
||||||
|
|
||||||
|
5. `resource://skills/python-logging-dictconfig/document`
|
||||||
|
- centralized startup-only logging configuration
|
||||||
|
- named logger discipline and boundary-level structured fields
|
||||||
|
|
||||||
|
6. `resource://skills/pytesting/document`
|
||||||
|
- deterministic test structure and marker discipline
|
||||||
|
- behavior-first tests with clear fast-path and full-suite validation
|
||||||
|
|
||||||
|
7. `resource://skills/fastapi-uv-docker/document`
|
||||||
|
- health endpoint and runtime startup/shutdown hygiene
|
||||||
|
- compose/deployment readiness constraints relevant to functional completion
|
||||||
|
|
||||||
|
8. `resource://skills/python-typing/document`
|
||||||
|
- modern typing updates where touched by Step 3 work
|
||||||
|
|
||||||
|
9. `resource://skills/ruff-linting-formating/document`
|
||||||
|
- maintain lint/format consistency in all modified modules
|
||||||
|
|
||||||
|
10. `resource://prompts/greenfield-architecture/document`
|
||||||
|
- explicit staged delivery with tradeoff-aware sequencing and test strategy
|
||||||
|
|
||||||
|
11. `resource://prompts/pytest-scaffold/document`
|
||||||
|
12. `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
- structure-first test planning, then deterministic implementation fill-in
|
||||||
|
|
||||||
|
Resources reviewed but not directly in Step 3 execution scope (no changes required now):
|
||||||
|
|
||||||
|
- `copilot-customization`, `mcp-details`, `vscode-configuration`, `zensical-docs`
|
||||||
|
- prompts: `authoring`, `mcp-consumer-repo-shim`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 Success Criteria
|
||||||
|
|
||||||
|
Step 3 is complete when:
|
||||||
|
|
||||||
|
1. All Step 3-targeted requirement slices are implemented and verified.
|
||||||
|
2. Functional behavior is available through UI/API where required.
|
||||||
|
3. Core data integrity and state transitions are deterministic.
|
||||||
|
4. Error behavior follows `docs/error_handling.md` contracts.
|
||||||
|
5. Carry-forward Step 1/2 items mapped to Step 3 are updated with evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirement-Slice Execution Model (Applied to Every Slice)
|
||||||
|
|
||||||
|
For each slice, execute this sequence:
|
||||||
|
|
||||||
|
1. Confirm contract/schema and boundary ownership.
|
||||||
|
2. Implement service/domain logic.
|
||||||
|
3. Implement persistence/state transitions.
|
||||||
|
4. Integrate API and/or UI behavior.
|
||||||
|
5. Add/update unit + integration + targeted end-to-end tests.
|
||||||
|
6. Update docs and traceability artifacts.
|
||||||
|
|
||||||
|
Definition of done per slice:
|
||||||
|
|
||||||
|
- behavior is functional
|
||||||
|
- tests pass in intended marker lanes
|
||||||
|
- error pathways are classified and surfaced correctly
|
||||||
|
- requirement traceability is updated with evidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Workstreams
|
||||||
|
|
||||||
|
## Workstream A — Functional Baseline Audit and Slice Backlog Lock
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- establish exact Step 3 functional delta from current implementation
|
||||||
|
- lock a practical slice backlog before coding
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Build Step 3 requirement matrix (REQ -> current status -> gap -> target slice).
|
||||||
|
2. Map each gap to one of these domains:
|
||||||
|
- Upload and lifecycle integrity
|
||||||
|
- Review and revision history
|
||||||
|
- Search over accepted transcripts
|
||||||
|
- Export workflows
|
||||||
|
- Prompt asset management behavior
|
||||||
|
- API/UI parity and status visibility
|
||||||
|
3. Align each slice with architecture boundary ownership and persistence strategy.
|
||||||
|
4. Link open carry-forward items from checklist:
|
||||||
|
- CF-A1, CF-A3 (architecture continuity in Step 3)
|
||||||
|
- CF-C1, CF-C2 (traceability/execution continuity)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- Step 3 requirement-slice matrix (appendix in this doc or separate artifact)
|
||||||
|
- prioritized slice backlog with owner and validation method
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- every Step 3 slice maps to REQ IDs and a validation method
|
||||||
|
- no ambiguous ownership remains for in-scope slices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream B — Core End-User Flows (Upload -> Transcribe -> Review)
|
||||||
|
|
||||||
|
### Related Requirements
|
||||||
|
|
||||||
|
- REQ-0, REQ-1, REQ-2, REQ-3, REQ-4, REQ-5, REQ-6, REQ-12
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- guarantee end-to-end reliability and usability of the primary user flow
|
||||||
|
- ensure review experience supports transcript acceptance and correction
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Validate and close any lifecycle-state gaps:
|
||||||
|
- enforce valid transitions (`queued -> processing -> transcribed/failed/completed`)
|
||||||
|
- ensure transition visibility in UI/API
|
||||||
|
2. Review experience completion:
|
||||||
|
- transcript detail display stability
|
||||||
|
- failure detail readability and actionability
|
||||||
|
- acceptance/edit path for human review
|
||||||
|
3. Ensure prompt-asset integration remains file-based and auditable:
|
||||||
|
- one prompt per Markdown file
|
||||||
|
- prompt selection/usage traceability in job outcomes (if available in model)
|
||||||
|
4. Confirm worker/UI interactions remain responsive under long-running jobs:
|
||||||
|
- in-flight guards
|
||||||
|
- clear status refresh behavior
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- complete end-user flow behavior with stable lifecycle visibility
|
||||||
|
- test coverage for happy path and failure path
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- user can run upload -> process -> review reliably
|
||||||
|
- failed and successful outcomes are both actionable and traceable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream C — Revision History and Provenance Completion
|
||||||
|
|
||||||
|
### Related Requirements
|
||||||
|
|
||||||
|
- REQ-3, REQ-4, REQ-5, REQ-11
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- finalize immutable transcript revision behavior and provenance consistency
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Define/confirm revision invariants:
|
||||||
|
- append-only revision history
|
||||||
|
- clear current/accepted revision indicator
|
||||||
|
2. Persist revision events consistently through service layer boundaries.
|
||||||
|
3. Ensure UI/API expose revision timeline and selected revision details.
|
||||||
|
4. Align error handling for revision conflicts and missing resources.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- revision-history feature completeness
|
||||||
|
- provenance and history read-path coverage
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- transcript edits produce deterministic revision records
|
||||||
|
- previous revisions remain inspectable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream D — Search Completion (Accepted Transcript Scope)
|
||||||
|
|
||||||
|
### Related Requirements
|
||||||
|
|
||||||
|
- REQ-0, REQ-5, REQ-11
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- provide practical search over accepted transcripts for personal corpus usage
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Finalize searchable scope and indexing rules (accepted/current text only).
|
||||||
|
2. Implement service-backed search query behavior.
|
||||||
|
3. Expose search in UI/API with clear result metadata (document/job/revision context).
|
||||||
|
4. Add guardrails for empty/no-result/error scenarios with actionable messaging.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- functional search pathway with deterministic results for accepted text
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- operator can find transcripts reliably by text queries
|
||||||
|
- no-result and error states are clear and non-silent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream E — Export Completion
|
||||||
|
|
||||||
|
### Related Requirements
|
||||||
|
|
||||||
|
- REQ-0, REQ-4, REQ-5, REQ-11
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- deliver practical export of transcript data for personal archive use
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Finalize export contract (format, included fields, scope filters).
|
||||||
|
2. Implement export service with deterministic data mapping.
|
||||||
|
3. Add UI/API trigger path and user-visible completion/failure feedback.
|
||||||
|
4. Validate export integrity against persisted source-of-record entities.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- end-to-end export capability with operator-visible outcomes
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- export output is complete, consistent, and usable for downstream personal archive workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream F — API/UI Parity and Interaction Hardening
|
||||||
|
|
||||||
|
### Related Requirements
|
||||||
|
|
||||||
|
- REQ-5 plus cross-cutting REQ-2/3/4
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- ensure UI and API expose coherent feature behavior and error contracts
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Verify API/UI parity matrix for each Step 3 slice.
|
||||||
|
2. Standardize interaction behavior:
|
||||||
|
- loading and in-flight states
|
||||||
|
- success/failure notifications
|
||||||
|
- stable error_id visibility where user-facing
|
||||||
|
3. Ensure route/page modules remain composition-focused (business logic in services).
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- API/UI parity checklist with resolved gaps
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- no major flow exists in one interface with conflicting semantics in the other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream G — Carry-Forward Integration During Step 3
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- close Step 1/2 follow-ups that are Step 3-owned
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Update checklist item CF-A1 as Step 3 slices touch runtime resources.
|
||||||
|
2. Update checklist item CF-A3 with lightweight boundary enforcement evidence.
|
||||||
|
3. Update CF-C1/CF-C2 traceability mapping with Step 3 outcomes.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- updated `docs/ver1/ver1-step1-2-carry-forward-checklist.md` evidence entries
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Step 3-owned carry-forward items are either completed or explicitly routed with evidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test and Validation Plan
|
||||||
|
|
||||||
|
Apply `pytesting` guidance with deterministic, behavior-focused coverage.
|
||||||
|
|
||||||
|
### Validation Lanes
|
||||||
|
|
||||||
|
1. Structure/collection:
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
2. Fast feedback lane:
|
||||||
|
- `uv run pytest -m unit -q`
|
||||||
|
3. Main verification lane:
|
||||||
|
- `uv run pytest -m "not external" -q`
|
||||||
|
4. Full suite:
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
### Required Coverage Areas
|
||||||
|
|
||||||
|
- lifecycle transition invariants
|
||||||
|
- revision history invariants
|
||||||
|
- search query behavior and result mapping
|
||||||
|
- export integrity and failure handling
|
||||||
|
- UI interaction guards and actionable failure feedback
|
||||||
|
- API envelope and status consistency for new/changed flows
|
||||||
|
|
||||||
|
### Test Design Rules
|
||||||
|
|
||||||
|
- one behavior target per test
|
||||||
|
- minimize heavy mocking; prefer real-path behavior checks where practical
|
||||||
|
- keep markers explicit and strict
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging, Error, and Config Guardrails for Step 3 Changes
|
||||||
|
|
||||||
|
1. Logging
|
||||||
|
- keep centralized startup logging config (`dictConfig`) as canonical
|
||||||
|
- include required error fields at boundary failures (`error_id`, `category`, `operation`, identifiers where available)
|
||||||
|
|
||||||
|
2. Error handling
|
||||||
|
- preserve taxonomy stability from `docs/error_handling.md`
|
||||||
|
- map any new failure pathways into existing categories
|
||||||
|
- surface actionable suggestions in UI/API
|
||||||
|
|
||||||
|
3. Configuration
|
||||||
|
- use typed settings and avoid ad-hoc env reads in business modules
|
||||||
|
- keep environment behavior explicit and documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order (Detailed)
|
||||||
|
|
||||||
|
1. Workstream A: audit and backlog lock
|
||||||
|
2. Workstream B: core flow completion
|
||||||
|
3. Workstream C: revision/provenance completion
|
||||||
|
4. Workstream D: search completion
|
||||||
|
5. Workstream E: export completion
|
||||||
|
6. Workstream F: API/UI parity hardening
|
||||||
|
7. Workstream G: carry-forward integration updates
|
||||||
|
8. Full validation pass + docs/traceability updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
1. Step 3 requirement-slice matrix with REQ mapping and evidence links
|
||||||
|
2. implemented Step 3 functional slices across service/persistence/API/UI
|
||||||
|
3. updated tests and passing validation lanes
|
||||||
|
4. updated carry-forward checklist entries (`CF-A1`, `CF-A3`, `CF-C1`, `CF-C2` as applicable)
|
||||||
|
5. Step 3 results document (`docs/ver1/ver1-step3-results.md`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Scope creep from optional enhancements during feature completion
|
||||||
|
- **Mitigation:** enforce REQ-mapped slice backlog and defer non-REQ enhancements
|
||||||
|
|
||||||
|
2. **Risk:** Functional parity drift between UI and API
|
||||||
|
- **Mitigation:** maintain parity matrix and verify both surfaces per slice
|
||||||
|
|
||||||
|
3. **Risk:** Data-model changes introduce migration surprises
|
||||||
|
- **Mitigation:** coordinate with Step 4 runbook expectations early and test on representative data
|
||||||
|
|
||||||
|
4. **Risk:** Reliability regressions while adding functionality
|
||||||
|
- **Mitigation:** run full error-path regression checks and keep Step 2 contracts intact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 Completion Checklist
|
||||||
|
|
||||||
|
- [ ] Step 3 requirement-slice matrix completed and linked to REQ IDs.
|
||||||
|
- [ ] Core end-user flow is functionally complete and verified.
|
||||||
|
- [ ] Revision history/provenance behavior is complete and test-covered.
|
||||||
|
- [ ] Search over accepted transcripts is complete and test-covered.
|
||||||
|
- [ ] Export flow is complete and test-covered.
|
||||||
|
- [ ] API/UI parity checklist has no unresolved high-impact gaps.
|
||||||
|
- [ ] Step 3-owned carry-forward items are updated with evidence.
|
||||||
|
- [ ] Validation lanes pass (`collect-only`, unit, non-external, full).
|
||||||
|
- [ ] `docs/ver1/ver1-step3-results.md` is created with evidence and residual follow-ups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 4
|
||||||
|
|
||||||
|
Step 3 completion enables Step 4 (Data Model and Migration Safety) with:
|
||||||
|
|
||||||
|
- finalized functional domain behavior
|
||||||
|
- stable persistence expectations
|
||||||
|
- traceable requirement evidence
|
||||||
|
- clarified migration-impact surface
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Ver1 Step 4 Migration and Rollback Runbook
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provide a concise, operator-safe procedure for schema migration execution,
|
||||||
|
compatibility validation, and rollback/mitigation for personal-scale deployments.
|
||||||
|
|
||||||
|
This runbook supports `docs/ver1/ver1-step4.md` and REQ-10 by keeping normal
|
||||||
|
production startup non-mutating unless explicitly configured otherwise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
1. Application version to deploy is known and checked out.
|
||||||
|
2. `.env` values are configured for target environment.
|
||||||
|
3. Database backup path is prepared.
|
||||||
|
4. Application process is stopped before migration on production-like systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Use explicit migration runner operations:
|
||||||
|
|
||||||
|
1. List pending migrations:
|
||||||
|
- `uv run python -m transcription.migration_runner --list`
|
||||||
|
2. Apply pending migrations:
|
||||||
|
- `uv run python -m transcription.migration_runner --apply`
|
||||||
|
3. Validate schema compatibility:
|
||||||
|
- `uv run python -m transcription.migration_runner --check`
|
||||||
|
|
||||||
|
Recommended execution order:
|
||||||
|
|
||||||
|
1. `--list`
|
||||||
|
2. backup database
|
||||||
|
3. `--apply`
|
||||||
|
4. `--check`
|
||||||
|
5. start application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup Procedure (SQLite Baseline)
|
||||||
|
|
||||||
|
For SQLite deployments, copy the DB file before migration:
|
||||||
|
|
||||||
|
- Example DB path default: `./transcription.db`
|
||||||
|
- Keep timestamped backup copy in a safe location.
|
||||||
|
|
||||||
|
If the file is in active use, stop the app first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
After migration apply:
|
||||||
|
|
||||||
|
1. `--check` exits successfully.
|
||||||
|
2. `schema_migration_history` includes applied revisions.
|
||||||
|
3. Application starts successfully.
|
||||||
|
4. Health endpoint responds: `/healthz`.
|
||||||
|
5. Critical flows smoke-check:
|
||||||
|
- upload
|
||||||
|
- job processing
|
||||||
|
- revision listing/acceptance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback and Mitigation Decision Tree
|
||||||
|
|
||||||
|
1. If migration fails before changes commit:
|
||||||
|
- fix issue
|
||||||
|
- re-run apply
|
||||||
|
2. If migration partially applied or compatibility check fails:
|
||||||
|
- stop app
|
||||||
|
- restore from backup
|
||||||
|
- investigate and produce forward-fix migration if needed
|
||||||
|
3. If app starts but functional invariants fail:
|
||||||
|
- stop app
|
||||||
|
- restore backup
|
||||||
|
- add corrective migration/backfill and rehearse before retry
|
||||||
|
|
||||||
|
For this Step 4 baseline, backup restore is the primary rollback mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Failure Classification Guidance
|
||||||
|
|
||||||
|
Classify migration failures using `docs/error_handling.md` categories:
|
||||||
|
|
||||||
|
- transient connection issues -> `infrastructure_transient_error`
|
||||||
|
- permissions/misconfiguration -> `infrastructure_persistent_error`
|
||||||
|
- unexpected migration logic defects -> `internal_unexpected_error`
|
||||||
|
|
||||||
|
Record failure details with operation context and timestamp.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
|
||||||
|
- `migration_auto_apply_on_startup` defaults to `False`.
|
||||||
|
- `validate_schema_on_startup` defaults to `True`.
|
||||||
|
- Startup schema validation fails fast on incompatibility.
|
||||||
|
|
||||||
|
This protects production from accidental schema drift.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Step-4 Follow-Up
|
||||||
|
|
||||||
|
If migration complexity grows beyond lightweight revision scripts,
|
||||||
|
introduce a dedicated migration framework in a future step while preserving
|
||||||
|
this runbook structure and operator-first workflow.
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Ver1 Step 4 Results: Data Model and Migration Safety
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 4 implementation status: **complete (baseline scope)**.
|
||||||
|
|
||||||
|
This document records completed migration-safety work, validation evidence, and remaining follow-ups for Ver1 Step 4.
|
||||||
|
|
||||||
|
Implemented in this step:
|
||||||
|
|
||||||
|
1. Added explicit migration framework module with revision history tracking.
|
||||||
|
2. Added schema compatibility validation and startup guardrails.
|
||||||
|
3. Added migration runner CLI for list/apply/check operations.
|
||||||
|
4. Added migration tests and Step 4 validation evidence.
|
||||||
|
5. Added Step 4 migration/rollback runbook.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Schema audit and invariant lock
|
||||||
|
|
||||||
|
Implemented read-only compatibility checks in `src/transcription/db.py`:
|
||||||
|
|
||||||
|
- `validate_schema_compatibility(...)` verifies required V1 tables:
|
||||||
|
- `document`
|
||||||
|
- `job`
|
||||||
|
- `transcript`
|
||||||
|
- `transcriptrevision`
|
||||||
|
- verifies required `job.retry_count` column
|
||||||
|
- returns explicit issue identifiers (non-mutating check)
|
||||||
|
|
||||||
|
### 2) Migration policy/tooling lock
|
||||||
|
|
||||||
|
Added explicit migration revision model in `src/transcription/migrations.py`:
|
||||||
|
|
||||||
|
- `MigrationRevision` dataclass
|
||||||
|
- ordered `MIGRATIONS` registry
|
||||||
|
- migration history table: `schema_migration_history`
|
||||||
|
- explicit pending-list and apply operations
|
||||||
|
|
||||||
|
### 3) Forward migration implementation
|
||||||
|
|
||||||
|
Implemented two baseline forward migrations:
|
||||||
|
|
||||||
|
1. `0001_add_retry_count_to_job`
|
||||||
|
2. `0002_create_transcriptrevision_table`
|
||||||
|
|
||||||
|
Each migration is idempotent and recorded in migration history.
|
||||||
|
|
||||||
|
### 4) Rollback and mitigation runbook
|
||||||
|
|
||||||
|
Created `docs/ver1/ver1-step4-migration-runbook.md` with:
|
||||||
|
|
||||||
|
- preconditions
|
||||||
|
- list/apply/check command sequence
|
||||||
|
- backup-first procedure
|
||||||
|
- verification checklist
|
||||||
|
- rollback/mitigation decision tree
|
||||||
|
- error classification guidance aligned to `docs/error_handling.md`
|
||||||
|
|
||||||
|
### 5) Backfill implementation or explicit no-backfill decision
|
||||||
|
|
||||||
|
No backfill required for this baseline Step 4 scope.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- additive migration operations only
|
||||||
|
- default values and new-table creation do not require historical row rewrites for current V1 invariants
|
||||||
|
- residual advanced backfill scenarios deferred unless future schema evolution introduces incompatible transforms
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test and Verification Evidence
|
||||||
|
|
||||||
|
### Added/Updated Tests
|
||||||
|
|
||||||
|
1. `tests/test_migrations.py`
|
||||||
|
- pending migration discovery
|
||||||
|
- migration apply + history recording
|
||||||
|
- idempotent re-apply behavior
|
||||||
|
2. `tests/test_db.py`
|
||||||
|
- compatibility-check behavior on fresh schema
|
||||||
|
- table expectation updates for `transcriptrevision`
|
||||||
|
3. `tests/test_config.py`
|
||||||
|
- migration safety setting defaults
|
||||||
|
4. `tests/test_app.py`
|
||||||
|
- lifespan test compatibility with migration/validation startup hooks
|
||||||
|
### Validation Runs
|
||||||
|
|
||||||
|
Run and record outcomes:
|
||||||
|
|
||||||
|
- `uv run pytest --collect-only -q` -> passed
|
||||||
|
- `uv run pytest -m unit -q` -> passed
|
||||||
|
- `uv run pytest -m "not external" -q` -> passed
|
||||||
|
- `uv run pytest -q` -> passed
|
||||||
|
### Migration Rehearsal Evidence
|
||||||
|
|
||||||
|
Migration rehearsal details (test-based):
|
||||||
|
|
||||||
|
- baseline data set used: in-memory SQLite legacy-shaped schema fixture (`job` table missing Step 4 additions)
|
||||||
|
- forward migration result: pending revisions applied successfully (`0001`, `0002`)
|
||||||
|
- post-migration verification result: schema checks pass and migration history recorded
|
||||||
|
- rollback/mitigation rehearsal result: runbook defined backup-restore primary rollback class for personal-scale SQLite deployment
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirement Traceability (Step 4)
|
||||||
|
|
||||||
|
| Step 4 Area | REQ Coverage | Status | Evidence |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Schema lifecycle and state persistence safety | REQ-3, REQ-4, REQ-11 | met | `src/transcription/migrations.py`, `tests/test_migrations.py`, `tests/test_db.py` |
|
||||||
|
| Lifespan/runtime ownership continuity | REQ-7 | met | `src/transcription/app.py` startup checks + existing lifespan ownership model |
|
||||||
|
| Explicit non-mutating production startup policy | REQ-10 | met | `migration_auto_apply_on_startup=False` default + explicit runner workflow + startup validation gate |
|
||||||
|
| Prompt/data continuity constraints | REQ-12 | met (continued) | no prompt-contract mutation in Step 4 changes |
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operational Artifacts Produced
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step4.md`
|
||||||
|
- `docs/ver1/ver1-step4-migration-runbook.md`
|
||||||
|
- `src/transcription/migrations.py`
|
||||||
|
- `src/transcription/migration_runner.py`
|
||||||
|
- README migration workflow updates
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks, Exceptions, and Follow-Ups
|
||||||
|
|
||||||
|
1. This lightweight migration system is appropriate for current personal-scale scope but may require a dedicated framework as schema complexity grows.
|
||||||
|
2. Rollback remains backup-restore primary; reversible down-migration coverage is intentionally limited in this baseline.
|
||||||
|
3. Startup compatibility checks currently fail fast with generic runtime error text and can be further normalized under API/operator error envelopes in later hardening.
|
||||||
|
|
||||||
|
Open follow-ups to carry forward:
|
||||||
|
|
||||||
|
- Evaluate migration framework escalation criteria in Step 9/10 readiness updates.
|
||||||
|
- Add optional richer structured migration logging fields if observability scope expands.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 Exit Assessment
|
||||||
|
|
||||||
|
- Schema validation against finalized V1 domain: **met**
|
||||||
|
- Forward migration path safety and repeatability: **met (baseline scope)**
|
||||||
|
- Rollback/mitigation readiness: **met (backup-restore primary path)**
|
||||||
|
- Backfill risk closure: **met (no backfill required for current deltas)**
|
||||||
|
- Test and regression safety: **met**
|
||||||
|
|
||||||
|
Step 4 completion status: **complete (baseline scope)**
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 5
|
||||||
|
|
||||||
|
Once Step 4 is marked complete, Step 5 can proceed with:
|
||||||
|
|
||||||
|
- verified migration safety baseline
|
||||||
|
- explicit rollback and recovery procedures
|
||||||
|
- reduced data-integrity risk entering private-network safety hardening
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
# Step 4 Implementation Plan: Data Model and Migration Safety
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Implement **Ver1 Step 4** from `docs/ver1/ver1.md` by making data-model evolution safe, explicit, and repeatable for personal-scale deployment.
|
||||||
|
|
||||||
|
Step 4 ensures schema changes are handled through deterministic migration workflows rather than implicit startup mutation, while preserving:
|
||||||
|
|
||||||
|
- personal-scale operational simplicity
|
||||||
|
- single-operator deployment model
|
||||||
|
- lifecycle-owned runtime resource boundaries
|
||||||
|
- stable requirement traceability and low rollback risk
|
||||||
|
|
||||||
|
Primary governing docs:
|
||||||
|
|
||||||
|
- `docs/ver1/ver1.md` (Step 4 objective and sequencing)
|
||||||
|
- `docs/architecture.md` (runtime ownership, persistence boundaries, simplicity guardrails)
|
||||||
|
- `docs/requirements.md` (REQ-3, REQ-4, REQ-7, REQ-10, REQ-11, REQ-12 emphasis)
|
||||||
|
- `docs/error_handling.md` (failure classification and safe error surfacing)
|
||||||
|
- `docs/intent.md` (verbatim/transcription/revision domain behavior)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Resources Reviewed and Applied
|
||||||
|
|
||||||
|
All currently available resources on `john-stream-mcp` were reviewed. Step 4 applies the following guidance directly:
|
||||||
|
|
||||||
|
1. `resource://skills/fastapi-async-sqlalchemy-modernization/document`
|
||||||
|
- explicit engine/session lifecycle ownership
|
||||||
|
- transaction boundary clarity for schema transitions and backfills
|
||||||
|
- phased rollout with rollback-aware checkpoints
|
||||||
|
|
||||||
|
2. `resource://skills/pydantic-settings/document`
|
||||||
|
- typed migration/runtime safety settings
|
||||||
|
- explicit source-precedence behavior for operational toggles
|
||||||
|
- fail-fast config semantics for unsafe startup paths
|
||||||
|
|
||||||
|
3. `resource://skills/pytesting/document`
|
||||||
|
- deterministic migration verification lanes
|
||||||
|
- strict marker discipline
|
||||||
|
- behavior-first test coverage for migration outcomes
|
||||||
|
|
||||||
|
4. `resource://skills/python-logging-dictconfig/document`
|
||||||
|
- startup-centralized logging configuration
|
||||||
|
- structured migration and rollback event traceability
|
||||||
|
|
||||||
|
5. `resource://skills/fastapi-uv-docker/document`
|
||||||
|
- deployment and rehearsal discipline
|
||||||
|
- startup/health posture validation during migration windows
|
||||||
|
|
||||||
|
6. `resource://skills/python-typing/document`
|
||||||
|
- modern typing hygiene for touched migration/persistence modules
|
||||||
|
|
||||||
|
7. `resource://skills/ruff-linting-formating/document`
|
||||||
|
- lint/format consistency for migration scripts and database modules
|
||||||
|
|
||||||
|
Planning methodology inputs also applied:
|
||||||
|
|
||||||
|
8. `resource://prompts/greenfield-architecture/document`
|
||||||
|
- staged execution with explicit risk and extension handling
|
||||||
|
|
||||||
|
9. `resource://prompts/pytest-scaffold/document`
|
||||||
|
10. `resource://prompts/pytest-fill-scaffold/document`
|
||||||
|
- test-structure-first and deterministic fill-in sequencing
|
||||||
|
|
||||||
|
Reviewed but not directly Step 4 execution-critical:
|
||||||
|
|
||||||
|
- skills: `copilot-customization`, `mcp-details`, `nicegui`, `nicegui-ui-customization`, `vscode-configuration`, `zensical-docs`
|
||||||
|
- prompts: `authoring`, `mcp-consumer-repo-shim`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current-State Gap Summary (Step 4 Scope)
|
||||||
|
|
||||||
|
Based on Step 1–3 outcomes and current docs/tests:
|
||||||
|
|
||||||
|
1. **Bootstrap policy baseline is present**
|
||||||
|
- Environment-aware schema bootstrap policy exists and aligns with REQ-10 intent.
|
||||||
|
2. **Functional model expanded in Step 3**
|
||||||
|
- Revision/acceptance features introduce schema evolution requirements that need formal migration safety rehearsal.
|
||||||
|
3. **Runbook maturity required**
|
||||||
|
- Step 4 requires explicit migration + rollback procedures and evidence.
|
||||||
|
4. **Backfill risk must be evaluated**
|
||||||
|
- New/changed fields and semantics must be checked for historical data reconciliation needs.
|
||||||
|
5. **Release-path integration needed**
|
||||||
|
- Step 4 artifacts must feed Step 9 release readiness and Step 10 docs completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 4
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
1. Validate final V1 schema against implemented domain behavior (post-Step 3 reality).
|
||||||
|
2. Define and implement forward-safe migration path for expected upgrades.
|
||||||
|
3. Define and document rollback/mitigation strategy for migration failures.
|
||||||
|
4. Implement backfill scripts only if required, with idempotent behavior.
|
||||||
|
5. Rehearse migration + rollback locally using representative sample data.
|
||||||
|
6. Add Step 4-specific verification tests and operational checks.
|
||||||
|
7. Produce operator-facing migration/rollback runbook and Step 4 results evidence.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Distributed/externally orchestrated migration systems
|
||||||
|
- Major persistence-architecture rewrites beyond V1 scope
|
||||||
|
- Non-V1 enhancement migrations unrelated to implemented requirement slices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target Decisions for Step 4
|
||||||
|
|
||||||
|
1. **Production startup remains non-mutating by default**
|
||||||
|
- Preserve REQ-10 posture and avoid implicit schema mutation at normal startup.
|
||||||
|
|
||||||
|
2. **Schema changes are explicit operator workflows**
|
||||||
|
- Migrations run as deliberate operational actions, not hidden side effects.
|
||||||
|
|
||||||
|
3. **Migration safety beats migration speed**
|
||||||
|
- Additive and reversible-first patterns are preferred where possible.
|
||||||
|
|
||||||
|
4. **Rollback policy is explicit per change**
|
||||||
|
- Each migration must declare rollback class:
|
||||||
|
- direct rollback supported
|
||||||
|
- forward-fix required
|
||||||
|
- backup restore required
|
||||||
|
|
||||||
|
5. **Backfills are optional and minimal**
|
||||||
|
- Introduce only when required by correctness/invariants, never by convenience.
|
||||||
|
|
||||||
|
6. **Migration observability is mandatory**
|
||||||
|
- Structured logs include operation, migration identifier, status, and failure classification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Work Breakdown
|
||||||
|
|
||||||
|
## Phase A — Schema and Domain Invariant Audit
|
||||||
|
|
||||||
|
- [ ] **A1. Build canonical V1 schema inventory**
|
||||||
|
- Enumerate all persisted entities and key fields:
|
||||||
|
- document records
|
||||||
|
- jobs and statuses
|
||||||
|
- transcripts
|
||||||
|
- transcript revisions
|
||||||
|
- failure/provenance fields
|
||||||
|
- [ ] **A2. Validate invariants against implemented behavior**
|
||||||
|
- Cross-check Step 3 functionality and current domain expectations:
|
||||||
|
- append-only revision history
|
||||||
|
- accepted revision semantics
|
||||||
|
- canonical transcript synchronization behavior
|
||||||
|
- [ ] **A3. Classify required schema deltas**
|
||||||
|
- Categorize deltas:
|
||||||
|
- additive and safe
|
||||||
|
- compatibility-sensitive
|
||||||
|
- potentially destructive (must be staged or deferred)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step4-schema-audit.md` (recommended)
|
||||||
|
- schema-delta matrix with risk class and owning module
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- all required schema changes have explicit rationale and risk classification
|
||||||
|
- no ambiguous domain invariant remains
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Migration Policy and Tooling Lock
|
||||||
|
|
||||||
|
- [ ] **B1. Lock migration workflow policy**
|
||||||
|
- Define canonical migration execution path and artifact conventions.
|
||||||
|
- [ ] **B2. Define migration authoring checklist**
|
||||||
|
- Include:
|
||||||
|
- preconditions
|
||||||
|
- forward steps
|
||||||
|
- rollback class
|
||||||
|
- post-verification checks
|
||||||
|
- [ ] **B3. Align policy with runtime startup safeguards**
|
||||||
|
- Ensure production startup remains explicit/non-mutating by default.
|
||||||
|
- [ ] **B4. Define operator invocation standard**
|
||||||
|
- One documented command path for local and production-like workflows.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- migration policy section (this doc + runbook)
|
||||||
|
- migration authoring/review checklist
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- one unambiguous migration process exists and is documented
|
||||||
|
- startup policy and migration policy are consistent and non-conflicting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Forward Migration Implementation
|
||||||
|
|
||||||
|
- [ ] **C1. Implement required migration set**
|
||||||
|
- Build migration artifacts for all approved Step 4 deltas.
|
||||||
|
- [ ] **C2. Preserve compatibility where needed**
|
||||||
|
- Use staged expand/contract strategy when direct cutover is unsafe.
|
||||||
|
- [ ] **C3. Add migration logging checkpoints**
|
||||||
|
- Log start, phase boundaries, completion, and failure details.
|
||||||
|
- [ ] **C4. Verify post-migration schema state**
|
||||||
|
- Confirm expected tables/columns/constraints/indexes are present.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- migration artifacts/scripts for V1 target schema
|
||||||
|
- schema verification checklist outputs
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- baseline-to-target forward migration executes successfully
|
||||||
|
- post-migration checks pass deterministically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Rollback and Mitigation Strategy
|
||||||
|
|
||||||
|
- [ ] **D1. Define rollback classes per migration**
|
||||||
|
- direct downgrade vs forward-fix vs backup-restore.
|
||||||
|
- [ ] **D2. Create rollback decision tree**
|
||||||
|
- trigger conditions, safe stop points, and recovery path.
|
||||||
|
- [ ] **D3. Align failure classification with `error_handling.md`**
|
||||||
|
- normalize migration failures into canonical categories:
|
||||||
|
- `infrastructure_transient_error`
|
||||||
|
- `infrastructure_persistent_error`
|
||||||
|
- `internal_unexpected_error` (as needed)
|
||||||
|
- [ ] **D4. Rehearse rollback flow**
|
||||||
|
- run at least one migration failure simulation and execute chosen recovery path.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- rollback/mitigation decision tree
|
||||||
|
- rehearsal evidence notes
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- operator can execute rollback/mitigation without undocumented steps
|
||||||
|
- migration failure paths are diagnosable and classified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Backfill Decision and Execution (Conditional)
|
||||||
|
|
||||||
|
- [ ] **E1. Determine backfill necessity**
|
||||||
|
- inspect whether existing records violate new invariants.
|
||||||
|
- [ ] **E2. If required, implement idempotent backfill**
|
||||||
|
- resumable, batch-safe, and deterministic update semantics.
|
||||||
|
- [ ] **E3. Add post-backfill verification**
|
||||||
|
- validate:
|
||||||
|
- revision sequencing integrity
|
||||||
|
- accepted/current transcript consistency
|
||||||
|
- job lifecycle consistency
|
||||||
|
- [ ] **E4. If not required, record explicit “no backfill needed” evidence**
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- backfill script(s) and checklist (if applicable)
|
||||||
|
- no-backfill rationale artifact (if not applicable)
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- required backfills completed and verified OR formally ruled out with evidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Verification and Test Expansion
|
||||||
|
|
||||||
|
Apply `pytesting` guidance (deterministic, behavior-first, strict markers).
|
||||||
|
|
||||||
|
- [ ] **F1. Migration application tests**
|
||||||
|
- verify forward migration from representative baseline.
|
||||||
|
- [ ] **F2. Post-migration schema contract tests**
|
||||||
|
- verify expected schema shape and key constraints.
|
||||||
|
- [ ] **F3. Rollback/mitigation tests**
|
||||||
|
- verify chosen rollback class behavior where practical.
|
||||||
|
- [ ] **F4. Startup policy regression tests**
|
||||||
|
- confirm production-mode startup does not mutate schema implicitly.
|
||||||
|
- [ ] **F5. Backfill behavior tests (if applicable)**
|
||||||
|
- idempotency and invariants after repeated execution.
|
||||||
|
|
||||||
|
### Validation Commands
|
||||||
|
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
- `uv run pytest -m unit -q`
|
||||||
|
- `uv run pytest -m "not external" -q`
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- all Step 4 migration-safety checks pass
|
||||||
|
- no REQ-10 regression introduced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase G — Runbook and Documentation Closure
|
||||||
|
|
||||||
|
- [ ] **G1. Create migration and rollback runbook**
|
||||||
|
- include:
|
||||||
|
- prerequisites
|
||||||
|
- backup step
|
||||||
|
- migration execution
|
||||||
|
- verification
|
||||||
|
- rollback/mitigation
|
||||||
|
- [ ] **G2. Update traceability artifacts**
|
||||||
|
- map Step 4 outcomes to REQ IDs and evidence.
|
||||||
|
- [ ] **G3. Prepare Step 4 handoff artifacts**
|
||||||
|
- ensure outputs feed Step 9 release readiness and Step 10 docs completion.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step4-migration-runbook.md` (recommended)
|
||||||
|
- `docs/ver1/ver1-step4-results.md`
|
||||||
|
- updated traceability references where needed
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- migration operations are executable using docs alone
|
||||||
|
- Step 4 evidence is complete and auditable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Phase A — schema/invariant audit
|
||||||
|
2. Phase B — migration policy and tooling lock
|
||||||
|
3. Phase C — forward migration implementation
|
||||||
|
4. Phase D — rollback/mitigation strategy + rehearsal
|
||||||
|
5. Phase E — backfill decision and execution (conditional)
|
||||||
|
6. Phase F — test and verification expansion
|
||||||
|
7. Phase G — runbook + traceability closure
|
||||||
|
|
||||||
|
This sequence minimizes risk by locking policy and scope before irreversible data changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Data loss from unsafe schema transitions
|
||||||
|
- **Mitigation:** backup-first gate, staged migration strategies, post-check verification.
|
||||||
|
|
||||||
|
2. **Risk:** Startup policy drift reintroduces implicit schema mutation
|
||||||
|
- **Mitigation:** explicit regression tests for production startup behavior (REQ-10 guard).
|
||||||
|
|
||||||
|
3. **Risk:** Rollback path is incomplete or untested
|
||||||
|
- **Mitigation:** mandatory rollback class declaration + rehearsal evidence.
|
||||||
|
|
||||||
|
4. **Risk:** Backfill scripts cause partial/inconsistent state
|
||||||
|
- **Mitigation:** idempotent design, batching, and invariant-focused verification.
|
||||||
|
|
||||||
|
5. **Risk:** Migration failure diagnostics are unclear
|
||||||
|
- **Mitigation:** structured logging + error category mapping per `error_handling.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 Completion Checklist
|
||||||
|
|
||||||
|
- [ ] V1 schema audit completed and approved.
|
||||||
|
- [ ] Migration workflow policy is locked and documented.
|
||||||
|
- [ ] Required forward migrations are implemented and validated.
|
||||||
|
- [ ] Rollback/mitigation decision tree is documented and rehearsed.
|
||||||
|
- [ ] Backfill required/not-required decision is evidenced.
|
||||||
|
- [ ] Migration-safety test coverage is added and passing.
|
||||||
|
- [ ] Startup non-mutation policy remains verified in production mode.
|
||||||
|
- [ ] Step 4 runbook and results artifacts are completed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 5
|
||||||
|
|
||||||
|
Step 4 completion enables Step 5 (Private-Network Safety Baseline) with:
|
||||||
|
|
||||||
|
- stable, explicit schema evolution mechanics
|
||||||
|
- reduced upgrade risk for single-operator deployments
|
||||||
|
- migration/rollback procedures suitable for personal-scale production
|
||||||
|
- traceable evidence for release-readiness gates
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Ver1 Step 5 Results: Private-Network Safety Baseline
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Step 5 implementation status: **complete**.
|
||||||
|
|
||||||
|
This document records completed private-network safety controls, validation evidence, and residual risks for Ver1 Step 5.
|
||||||
|
|
||||||
|
Implemented in this step:
|
||||||
|
|
||||||
|
1. Added private-network security assumptions and control matrix (`docs/ver1/ver1-step5-security-assumptions.md`).
|
||||||
|
2. Implemented optional single-operator access control for `/ui*` and `/api*` via HTTP Basic auth.
|
||||||
|
3. Added upload-size guardrails (`MAX_UPLOAD_BYTES`) and config fail-fast validation for operator credential requirements.
|
||||||
|
4. Hardened unexpected-error user-facing messaging to reduce sensitive detail leakage.
|
||||||
|
5. Added Step 5 tests for access control, security settings, and upload size boundaries.
|
||||||
|
6. Executed dependency/security scans (`pip-audit`, `bandit`) with no critical/high findings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implemented Changes
|
||||||
|
|
||||||
|
### 1) Security assumptions and threat model
|
||||||
|
|
||||||
|
Completed.
|
||||||
|
|
||||||
|
- Added `docs/ver1/ver1-step5-security-assumptions.md` defining:
|
||||||
|
- trusted private-network deployment assumptions
|
||||||
|
- single-operator usage model
|
||||||
|
- explicit out-of-scope classes (enterprise IAM, internet-facing zero-trust, multi-tenant controls)
|
||||||
|
- Added Step 5 control/ownership matrix and residual-risk notes.
|
||||||
|
|
||||||
|
### 2) Single-operator access control baseline
|
||||||
|
|
||||||
|
Completed.
|
||||||
|
|
||||||
|
- New module: `src/transcription/security.py`
|
||||||
|
- `is_protected_path(...)` protects `/ui*` and `/api*`
|
||||||
|
- `enforce_request_access(...)` enforces optional operator auth
|
||||||
|
- robust Basic auth parsing and safe denial responses via `AccessDeniedError`
|
||||||
|
- App middleware added in `src/transcription/app.py`:
|
||||||
|
- enforces auth on protected paths
|
||||||
|
- returns consistent `401` envelope and `WWW-Authenticate: Basic` for denied requests
|
||||||
|
- Health endpoint `/healthz` remains intentionally unauthenticated.
|
||||||
|
|
||||||
|
### 3) Input validation and safe-output hardening
|
||||||
|
|
||||||
|
Completed baseline.
|
||||||
|
|
||||||
|
- `src/transcription/services/upload.py`
|
||||||
|
- added size-based validation guard (`max_upload_bytes`)
|
||||||
|
- emits `user_input_error` with actionable guidance on over-limit uploads
|
||||||
|
- `src/transcription/errors.py`
|
||||||
|
- `classify_unexpected_error(...)` now returns operation-only message without embedding raw exception text
|
||||||
|
- preserves traceability via existing `error_id` and taxonomy while reducing accidental sensitive leak risk
|
||||||
|
|
||||||
|
### 4) Secret handling and configuration safety
|
||||||
|
|
||||||
|
Completed baseline.
|
||||||
|
|
||||||
|
- `src/transcription/config.py` additions:
|
||||||
|
- `max_upload_bytes` (default `15 * 1024 * 1024`)
|
||||||
|
- `operator_access_enabled` (default `False`)
|
||||||
|
- `operator_username` (default `operator`)
|
||||||
|
- `operator_password` (optional, required when auth enabled)
|
||||||
|
- Added settings validator enforcing fail-fast config safety:
|
||||||
|
- raises validation error if `OPERATOR_ACCESS_ENABLED=true` and `OPERATOR_PASSWORD` unset
|
||||||
|
- `README.md` updated with Step 5 security env settings and explicit secret-handling guidance.
|
||||||
|
|
||||||
|
### 5) Dependency/security scanning baseline
|
||||||
|
|
||||||
|
Completed.
|
||||||
|
|
||||||
|
- Dependency vulnerability scan:
|
||||||
|
- `uvx pip-audit`
|
||||||
|
- Result: **No known vulnerabilities found**
|
||||||
|
- Static security scan:
|
||||||
|
- `uvx bandit -r src/transcription`
|
||||||
|
- Result: **No issues identified** (0 low/medium/high)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test and Verification Evidence
|
||||||
|
|
||||||
|
### Added/Updated Tests
|
||||||
|
|
||||||
|
1. `tests/api/test_access_control.py`
|
||||||
|
- unauthorized protected API denied (`401` + challenge)
|
||||||
|
- invalid credentials denied
|
||||||
|
- valid credentials accepted
|
||||||
|
- `/ui` protected when auth enabled
|
||||||
|
- `/healthz` remains unprotected
|
||||||
|
2. `tests/services/test_upload.py`
|
||||||
|
- added rejection test for payloads above `MAX_UPLOAD_BYTES`
|
||||||
|
3. `tests/test_config.py`
|
||||||
|
- added security defaults assertions
|
||||||
|
- added fail-fast assertion for missing `OPERATOR_PASSWORD` when auth enabled
|
||||||
|
4. `tests/test_errors.py`
|
||||||
|
- updated expectations for sanitized unexpected-error message behavior
|
||||||
|
5. Updated integration expectations where failure detail should no longer include raw exception text:
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
- `tests/integration/test_pipeline_flow.py`
|
||||||
|
6. `tests/test_app.py` updated for new middleware wiring.
|
||||||
|
|
||||||
|
### Validation Runs
|
||||||
|
|
||||||
|
Run and record outcomes:
|
||||||
|
|
||||||
|
- `uv run pytest --collect-only -q` -> passed
|
||||||
|
- `uv run pytest -m unit -q` -> passed
|
||||||
|
- `uv run pytest -m "not external" -q` -> passed
|
||||||
|
- `uv run pytest -q` -> passed
|
||||||
|
|
||||||
|
### Security Scan Evidence
|
||||||
|
|
||||||
|
Record scan commands and outcomes:
|
||||||
|
|
||||||
|
- dependency scan command(s): `uvx pip-audit`
|
||||||
|
- static/security lint command(s): `uvx bandit -r src/transcription`
|
||||||
|
- critical/high findings: none
|
||||||
|
- remediation/defer decisions: no remediations required for Step 5 baseline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirement Traceability (Step 5)
|
||||||
|
|
||||||
|
| Step 5 Area | REQ Coverage | Status | Evidence |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Private-network and single-operator safety posture | REQ-9 | met | `docs/ver1/ver1-step5-security-assumptions.md`, README security section |
|
||||||
|
| Access control behavior at UI/API boundaries | REQ-5, REQ-7 | met | `src/transcription/security.py`, `src/transcription/app.py`, `tests/api/test_access_control.py` |
|
||||||
|
| Input validation and safe user-facing error behavior | REQ-1, REQ-2, REQ-5 | met | `src/transcription/services/upload.py`, `src/transcription/errors.py`, updated tests |
|
||||||
|
| Config and startup safety controls | REQ-8, REQ-10 | met | `src/transcription/config.py`, `tests/test_config.py`, `README.md` |
|
||||||
|
| Persistence and domain integrity continuity | REQ-11, REQ-12 | met (no regressions) | full test lane pass including integration and worker flows |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operational Artifacts Produced
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step5.md`
|
||||||
|
- `docs/ver1/ver1-step5-results.md`
|
||||||
|
- `docs/ver1/ver1-step5-security-assumptions.md`
|
||||||
|
- `src/transcription/security.py`
|
||||||
|
- `tests/api/test_access_control.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks, Exceptions, and Follow-Ups
|
||||||
|
|
||||||
|
1. Basic auth is intentionally right-sized for trusted private-network use; if deployment posture changes, stronger identity controls are required.
|
||||||
|
2. Current model remains single shared operator credential (no per-user audit identity).
|
||||||
|
3. No built-in brute-force/rate-limit controls in Step 5 scope; evaluate in future hardening if threat model expands.
|
||||||
|
|
||||||
|
Open follow-ups to carry forward:
|
||||||
|
|
||||||
|
- Consider stronger auth/session model if system becomes multi-user or internet-accessible.
|
||||||
|
- Consider request throttling/rate limiting if threat model changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 Exit Assessment
|
||||||
|
|
||||||
|
- Private-network assumptions and controls: **met**
|
||||||
|
- Access-control baseline effectiveness: **met**
|
||||||
|
- Validation and safe-output safety: **met (baseline)**
|
||||||
|
- Secret handling and config safety: **met**
|
||||||
|
- Dependency/security risk closure: **met (no critical/high findings)**
|
||||||
|
- Test and regression safety: **met**
|
||||||
|
|
||||||
|
Step 5 completion status: **complete**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 6
|
||||||
|
|
||||||
|
Once Step 5 is marked complete, Step 6 can proceed with:
|
||||||
|
|
||||||
|
- clearer operational security assumptions for logs/runbooks
|
||||||
|
- hardened boundary behavior for diagnosis and support
|
||||||
|
- reduced risk posture for personal-scale ongoing operations
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Ver1 Step 5 Security Assumptions (Private-Network Baseline)
|
||||||
|
|
||||||
|
## Operating Model
|
||||||
|
|
||||||
|
This system is operated as:
|
||||||
|
|
||||||
|
1. single operator
|
||||||
|
2. trusted private network
|
||||||
|
3. non-public deployment (no direct internet exposure for UI/API)
|
||||||
|
|
||||||
|
Out of scope for Step 5:
|
||||||
|
|
||||||
|
- enterprise IAM/SSO/RBAC
|
||||||
|
- internet-facing zero-trust edge controls
|
||||||
|
- multi-tenant user isolation
|
||||||
|
|
||||||
|
## Step 5 Controls and Ownership
|
||||||
|
|
||||||
|
| Control | Boundary Owner | Verification |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Optional operator authentication for `/ui*` and `/api*` routes | `src/transcription/security.py`, `src/transcription/app.py` | `tests/api/test_access_control.py` |
|
||||||
|
| Unauthorized contract (`401` + safe envelope + `WWW-Authenticate`) | `src/transcription/api/errors.py` | `tests/api/test_access_control.py` |
|
||||||
|
| Upload size guard (`MAX_UPLOAD_BYTES`) | `src/transcription/services/upload.py`, `src/transcription/config.py` | `tests/services/test_upload.py` |
|
||||||
|
| Fail-fast auth config when enabled | `src/transcription/config.py` | `tests/test_config.py` |
|
||||||
|
| Safe unexpected error messaging (reduced leak surface) | `src/transcription/errors.py` | `tests/test_errors.py`, worker/integration failure tests |
|
||||||
|
|
||||||
|
## Access-Control Policy (Step 5)
|
||||||
|
|
||||||
|
- Health endpoint (`/healthz`) remains unauthenticated for operability checks.
|
||||||
|
- When `OPERATOR_ACCESS_ENABLED=true`, protected paths require HTTP Basic auth:
|
||||||
|
- `/ui`
|
||||||
|
- `/ui/...`
|
||||||
|
- `/api/...`
|
||||||
|
- Credentials are runtime-configured:
|
||||||
|
- `OPERATOR_USERNAME` (default `operator`)
|
||||||
|
- `OPERATOR_PASSWORD` (required when access is enabled)
|
||||||
|
|
||||||
|
## Secrets Policy
|
||||||
|
|
||||||
|
- Secrets must be provided via runtime environment variables.
|
||||||
|
- Secrets must not be committed to source control.
|
||||||
|
- Secrets must not be logged.
|
||||||
|
- Example secret values in docs must always be placeholders.
|
||||||
|
|
||||||
|
## Residual Risks (Accepted for Step 5)
|
||||||
|
|
||||||
|
1. HTTP Basic credentials are suitable only for trusted private-network deployment.
|
||||||
|
2. No per-user identity model (single shared operator credential).
|
||||||
|
3. No advanced brute-force/rate-limit controls in Step 5 scope.
|
||||||
|
|
||||||
|
These are carried forward for future hardening only if deployment posture changes.
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
# Step 5 Implementation Plan: Private-Network Safety Baseline
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Implement **Ver1 Step 5** from `docs/ver1/ver1.md` by applying right-sized security controls for a single-user system running on a trusted private network.
|
||||||
|
|
||||||
|
Step 5 focuses on practical risk reduction without introducing unnecessary complexity, while preserving:
|
||||||
|
|
||||||
|
- personal-scale operational simplicity
|
||||||
|
- single-operator workflow
|
||||||
|
- explicit boundary ownership from `docs/architecture.md`
|
||||||
|
- safety and diagnostics behavior defined in `docs/error_handling.md`
|
||||||
|
|
||||||
|
Primary governing docs:
|
||||||
|
|
||||||
|
- `docs/ver1/ver1.md` (Step 5 objective and sequencing)
|
||||||
|
- `docs/architecture.md` (deployment model and module boundaries)
|
||||||
|
- `docs/error_handling.md` (safe user output and diagnostic boundaries)
|
||||||
|
- `docs/requirements.md` (REQ-1, REQ-2, REQ-5, REQ-7, REQ-8, REQ-9, REQ-10, REQ-11, REQ-12)
|
||||||
|
- `docs/intent.md` (domain integrity priorities)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Resources Reviewed and Applied
|
||||||
|
|
||||||
|
All currently available resources on `john-stream-mcp` were reviewed. Step 5 applies the following guidance directly:
|
||||||
|
|
||||||
|
1. `resource://skills/pydantic-settings/document`
|
||||||
|
- typed security-related runtime settings
|
||||||
|
- explicit env/source precedence
|
||||||
|
- fail-fast handling for missing/invalid required values
|
||||||
|
|
||||||
|
2. `resource://skills/fastapi-uv-docker/document`
|
||||||
|
- environment and deployment safety defaults
|
||||||
|
- startup/health posture and container hygiene assumptions
|
||||||
|
- local secret handling expectations
|
||||||
|
|
||||||
|
3. `resource://skills/pytesting/document`
|
||||||
|
- deterministic security-behavior test lanes
|
||||||
|
- marker discipline and behavior-first assertions
|
||||||
|
|
||||||
|
4. `resource://skills/python-logging-dictconfig/document`
|
||||||
|
- centralized logging discipline
|
||||||
|
- avoid leaking sensitive values in logs
|
||||||
|
|
||||||
|
5. `resource://skills/nicegui-ui-customization/document`
|
||||||
|
- user-safe failure messaging in UI
|
||||||
|
- resilient interaction behavior and clear error feedback
|
||||||
|
|
||||||
|
6. `resource://skills/ruff-linting-formating/document`
|
||||||
|
- keep lint quality baseline stable during safety changes
|
||||||
|
|
||||||
|
Planning methodology input:
|
||||||
|
|
||||||
|
7. `resource://prompts/greenfield-architecture/document`
|
||||||
|
- explicit tradeoff-oriented staging
|
||||||
|
- scope discipline for minimally sufficient security controls
|
||||||
|
|
||||||
|
Reviewed but not directly Step 5 execution-critical:
|
||||||
|
|
||||||
|
- skills: `copilot-customization`, `fastapi-async-sqlalchemy-modernization`, `mcp-details`, `nicegui`, `python-typing`, `vscode-configuration`, `zensical-docs`
|
||||||
|
- prompts: `authoring`, `mcp-consumer-repo-shim`, `pytest-scaffold`, `pytest-fill-scaffold`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current-State Gap Summary (Step 5 Scope)
|
||||||
|
|
||||||
|
Based on current implementation and prior Step outputs:
|
||||||
|
|
||||||
|
1. **Private-network assumptions are implicit, not fully codified**
|
||||||
|
- Need explicit, documented security posture and operator constraints.
|
||||||
|
|
||||||
|
2. **Access control for UI/API is minimal or absent**
|
||||||
|
- Step 5 requires basic single-operator gating appropriate for private-network use.
|
||||||
|
|
||||||
|
3. **Input validation baseline exists but needs security-oriented audit closure**
|
||||||
|
- Upload and API validation should be verified for abuse-resistant boundaries.
|
||||||
|
|
||||||
|
4. **Safe error output baseline exists (Step 2), but needs security confirmation pass**
|
||||||
|
- Must ensure no sensitive internals leak through API/UI error payloads.
|
||||||
|
|
||||||
|
5. **Secret handling documentation needs formalization in Step 5 artifacts**
|
||||||
|
- Local workflow should clearly prohibit secrets in repo-tracked files and logs.
|
||||||
|
|
||||||
|
6. **Dependency/security scanning is not yet formalized as a recurring gate**
|
||||||
|
- Step 5 requires lightweight scanning and triage of high-risk findings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope for Step 5
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
1. Codify private-network and single-operator security assumptions in docs and config.
|
||||||
|
2. Add basic access control for UI/API actions (right-sized for trusted network model).
|
||||||
|
3. Audit and harden input-validation boundaries (upload, API params/payloads, operational flags).
|
||||||
|
4. Verify safe error surface behavior (UI/API) and prevent sensitive leak paths.
|
||||||
|
5. Formalize local secret handling policy and usage examples.
|
||||||
|
6. Add lightweight dependency/security scan workflow and triage policy.
|
||||||
|
7. Add Step 5 verification tests and results artifact.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Internet-facing zero-trust security architecture
|
||||||
|
- Enterprise IAM/SSO/role systems
|
||||||
|
- Full cryptographic key-management infrastructure
|
||||||
|
- Major security product integrations beyond lightweight V1 needs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target Decisions for Step 5
|
||||||
|
|
||||||
|
1. **Threat model is explicitly private-network + single operator**
|
||||||
|
- Security controls are right-sized to this posture and documented as assumptions.
|
||||||
|
|
||||||
|
2. **Access control is required, even in private network mode**
|
||||||
|
- Basic gate (single shared operator credential/token) protects UI/API mutation paths.
|
||||||
|
|
||||||
|
3. **Validation and output safety are strict defaults**
|
||||||
|
- Reject invalid inputs early; never expose sensitive internals in user-facing outputs.
|
||||||
|
|
||||||
|
4. **Secrets are runtime-only**
|
||||||
|
- No secrets committed to source control; no plaintext secret logging.
|
||||||
|
|
||||||
|
5. **Security scanning is lightweight but mandatory**
|
||||||
|
- Add recurring dependency/security checks with high-risk triage and closure workflow.
|
||||||
|
|
||||||
|
6. **No security control may violate Step 1–4 operational simplicity guardrails**
|
||||||
|
- Preserve deployability and maintainability for personal-scale use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Work Breakdown
|
||||||
|
|
||||||
|
## Phase A — Security Posture Definition and Gap Lock
|
||||||
|
|
||||||
|
- [ ] **A1. Define Step 5 threat model**
|
||||||
|
- trusted private network
|
||||||
|
- single operator
|
||||||
|
- local deployment assumptions
|
||||||
|
- explicit out-of-scope threat classes
|
||||||
|
|
||||||
|
- [ ] **A2. Produce security baseline checklist**
|
||||||
|
- access control
|
||||||
|
- validation boundaries
|
||||||
|
- safe error behavior
|
||||||
|
- secret handling
|
||||||
|
- dependency risk checks
|
||||||
|
|
||||||
|
- [ ] **A3. Map controls to architecture boundaries**
|
||||||
|
- UI
|
||||||
|
- API
|
||||||
|
- service
|
||||||
|
- config/runtime
|
||||||
|
- operator runbooks
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step5-security-assumptions.md` (recommended)
|
||||||
|
- Step 5 control matrix (control -> owner -> validation method)
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- private-network safety posture is explicit and approved
|
||||||
|
- each in-scope control has boundary ownership and verification path
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Basic Single-Operator Access Control
|
||||||
|
|
||||||
|
- [ ] **B1. Select access mechanism**
|
||||||
|
- minimal approach suitable for private-network model
|
||||||
|
- explicitly document tradeoffs and operator ergonomics
|
||||||
|
|
||||||
|
- [ ] **B2. Protect mutating operations first**
|
||||||
|
- upload/create/accept/export-trigger endpoints
|
||||||
|
- UI actions that trigger persistence changes
|
||||||
|
|
||||||
|
- [ ] **B3. Protect read operations as policy requires**
|
||||||
|
- determine read-path gating expectations and apply consistently
|
||||||
|
|
||||||
|
- [ ] **B4. Add clear unauthorized behavior contract**
|
||||||
|
- stable API status and safe message
|
||||||
|
- UI feedback with actionable operator guidance
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- access-control policy and implementation notes
|
||||||
|
- unauthorized behavior matrix (UI/API)
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- unauthorized actions are blocked consistently
|
||||||
|
- authorized operator flows remain usable and deterministic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — Input Validation and Safe Output Hardening
|
||||||
|
|
||||||
|
- [ ] **C1. Validation audit for all entry points**
|
||||||
|
- file uploads (type/size/content guards)
|
||||||
|
- route/query/body constraints
|
||||||
|
- service-layer invariants
|
||||||
|
|
||||||
|
- [ ] **C2. Normalize validation failures to canonical taxonomy**
|
||||||
|
- `validation_error` vs `user_input_error` consistency
|
||||||
|
|
||||||
|
- [ ] **C3. Confirm safe error output policy under security lens**
|
||||||
|
- no stack traces/secrets/internal paths in UI/API default outputs
|
||||||
|
- preserve error reference IDs for traceability
|
||||||
|
|
||||||
|
- [ ] **C4. Add abuse-resistant guardrails where practical**
|
||||||
|
- basic request-size and payload-shape constraints
|
||||||
|
- anti-duplication interaction safeguards (where missing)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- validation-path inventory and hardening checklist
|
||||||
|
- safe-output verification notes
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- input boundaries are deterministic and tested
|
||||||
|
- user-facing error outputs remain safe and actionable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase D — Secrets Handling and Configuration Safety
|
||||||
|
|
||||||
|
- [ ] **D1. Define secret handling policy**
|
||||||
|
- where secrets are allowed (runtime env only)
|
||||||
|
- where secrets are prohibited (source files, docs examples beyond placeholders)
|
||||||
|
|
||||||
|
- [ ] **D2. Enforce settings expectations**
|
||||||
|
- required secret fields fail fast
|
||||||
|
- avoid fallback defaults that silently weaken safety
|
||||||
|
|
||||||
|
- [ ] **D3. Add operator documentation for local secret workflow**
|
||||||
|
- how to set environment values safely
|
||||||
|
- how to rotate/update credentials locally
|
||||||
|
|
||||||
|
- [ ] **D4. Validate logging does not leak secret values**
|
||||||
|
- startup/config logs
|
||||||
|
- error logs for provider/config failures
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- secret-handling section in runbook/README/docs
|
||||||
|
- settings and logging safety verification notes
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- no secret leakage paths remain in normal operations
|
||||||
|
- operator can configure secrets safely using docs only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase E — Dependency and Security Scanning Baseline
|
||||||
|
|
||||||
|
- [ ] **E1. Select lightweight scanning commands for V1**
|
||||||
|
- dependency vulnerability scan
|
||||||
|
- optional static security scan if practical
|
||||||
|
|
||||||
|
- [ ] **E2. Define triage policy for findings**
|
||||||
|
- severity classification
|
||||||
|
- required closure criteria for Step 5 completion
|
||||||
|
|
||||||
|
- [ ] **E3. Run scans and capture evidence**
|
||||||
|
- record command outputs/summaries
|
||||||
|
- remediate or formally defer with risk notes
|
||||||
|
|
||||||
|
- [ ] **E4. Add recurring execution guidance**
|
||||||
|
- local pre-release checklist integration
|
||||||
|
- future CI gate handoff for Step 7/9
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- Step 5 scan report artifact (recommended)
|
||||||
|
- triage log of resolved/deferred findings
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- no unresolved critical vulnerabilities in Step 5 scope
|
||||||
|
- high-risk findings are resolved or explicitly risk-accepted with rationale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase F — Verification and Test Expansion
|
||||||
|
|
||||||
|
Apply `pytesting` guidance (deterministic, behavior-first, strict markers).
|
||||||
|
|
||||||
|
- [ ] **F1. Access-control tests**
|
||||||
|
- unauthorized requests are rejected as expected
|
||||||
|
- authorized operator requests succeed
|
||||||
|
|
||||||
|
- [ ] **F2. Validation and abuse-boundary tests**
|
||||||
|
- invalid payloads rejected with stable category/status
|
||||||
|
- file-type/size constraints enforced
|
||||||
|
|
||||||
|
- [ ] **F3. Safe-output tests**
|
||||||
|
- API/UI error responses avoid sensitive details
|
||||||
|
- error IDs and suggestions remain present
|
||||||
|
|
||||||
|
- [ ] **F4. Config/secret safety tests**
|
||||||
|
- required secrets fail fast when missing
|
||||||
|
- no unsafe fallback behavior introduced
|
||||||
|
|
||||||
|
### Validation Commands
|
||||||
|
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
- `uv run pytest -m unit -q`
|
||||||
|
- `uv run pytest -m "not external" -q`
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Step 5 safety behavior is test-covered and passing
|
||||||
|
- no regression in core upload/transcribe/review workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase G — Documentation and Risk Closure
|
||||||
|
|
||||||
|
- [ ] **G1. Create Step 5 results artifact**
|
||||||
|
- `docs/ver1/ver1-step5-results.md`
|
||||||
|
|
||||||
|
- [ ] **G2. Update operator-facing docs**
|
||||||
|
- security assumptions and local deployment cautions
|
||||||
|
- credential handling and recovery basics
|
||||||
|
|
||||||
|
- [ ] **G3. Update traceability and carry-forward notes**
|
||||||
|
- map Step 5 controls to REQ and evidence
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- `docs/ver1/ver1-step5-results.md`
|
||||||
|
- updated security assumptions checklist and risk summary
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Step 5 controls and residual risks are fully documented
|
||||||
|
- handoff is ready for Step 6 observability and Step 7 quality gates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Phase A — posture definition and gap lock
|
||||||
|
2. Phase B — access control baseline
|
||||||
|
3. Phase C — validation/output hardening
|
||||||
|
4. Phase D — secrets and config safety
|
||||||
|
5. Phase E — dependency/security scan baseline
|
||||||
|
6. Phase F — test expansion and verification
|
||||||
|
7. Phase G — docs and risk closure
|
||||||
|
|
||||||
|
This order reduces risk by locking assumptions first, then applying controls at highest-impact boundaries before final verification and documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 Execution Checklist (Phase-by-Phase)
|
||||||
|
|
||||||
|
Use this checklist to execute Step 5 in implementation order and record progress/evidence.
|
||||||
|
|
||||||
|
### Phase A — Security Posture Definition and Gap Lock
|
||||||
|
|
||||||
|
- [ ] Publish `docs/ver1/ver1-step5-security-assumptions.md`.
|
||||||
|
- [ ] Record explicit in-scope and out-of-scope threat classes.
|
||||||
|
- [ ] Produce Step 5 control matrix (control, owner, validation method).
|
||||||
|
- [ ] Confirm boundary ownership for each control (UI/API/service/config/docs).
|
||||||
|
|
||||||
|
### Phase B — Basic Single-Operator Access Control
|
||||||
|
|
||||||
|
- [ ] Choose and document access mechanism (with rationale and tradeoffs).
|
||||||
|
- [ ] Implement enforcement for mutating API operations.
|
||||||
|
- [ ] Implement corresponding UI-side access behavior for protected actions.
|
||||||
|
- [ ] Decide and enforce read-path protection policy.
|
||||||
|
- [ ] Add unauthorized API/UI contract tests.
|
||||||
|
|
||||||
|
### Phase C — Input Validation and Safe Output Hardening
|
||||||
|
|
||||||
|
- [ ] Complete input-validation inventory for upload/API/service boundaries.
|
||||||
|
- [ ] Tighten payload/file constraints where gaps are found.
|
||||||
|
- [ ] Ensure validation failure categories match `docs/error_handling.md`.
|
||||||
|
- [ ] Verify user-facing errors remain safe, actionable, and traceable.
|
||||||
|
- [ ] Add regression tests for invalid/boundary inputs.
|
||||||
|
|
||||||
|
### Phase D — Secrets Handling and Configuration Safety
|
||||||
|
|
||||||
|
- [ ] Document secrets policy (runtime-only, no repo storage).
|
||||||
|
- [ ] Verify required secret settings fail fast when missing.
|
||||||
|
- [ ] Audit logs for accidental secret leakage risk paths.
|
||||||
|
- [ ] Update operator docs for local secret setup/rotation workflow.
|
||||||
|
- [ ] Add tests for config safety expectations where practical.
|
||||||
|
|
||||||
|
### Phase E — Dependency and Security Scanning Baseline
|
||||||
|
|
||||||
|
- [ ] Select scanning commands and record tool versions.
|
||||||
|
- [ ] Run baseline scans and capture outputs.
|
||||||
|
- [ ] Triage findings by severity and exploitability in private-network context.
|
||||||
|
- [ ] Resolve/mitigate critical findings; document accepted residual risk.
|
||||||
|
- [ ] Add recurring scan guidance for release workflow handoff.
|
||||||
|
|
||||||
|
### Phase F — Verification and Test Expansion
|
||||||
|
|
||||||
|
- [ ] Run `uv run pytest --collect-only -q`.
|
||||||
|
- [ ] Run `uv run pytest -m unit -q`.
|
||||||
|
- [ ] Run `uv run pytest -m "not external" -q`.
|
||||||
|
- [ ] Run `uv run pytest -q`.
|
||||||
|
- [ ] Confirm no regressions in upload/transcribe/review core flows.
|
||||||
|
|
||||||
|
### Phase G — Documentation and Risk Closure
|
||||||
|
|
||||||
|
- [ ] Complete `docs/ver1/ver1-step5-results.md` with evidence.
|
||||||
|
- [ ] Update docs/README/runbooks with final Step 5 security posture.
|
||||||
|
- [ ] Record REQ traceability updates and residual risks.
|
||||||
|
- [ ] Confirm Step 5 completion checklist items are all closed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
1. **Risk:** Over-engineering beyond private-network needs
|
||||||
|
- **Mitigation:** enforce Step 5 scope discipline and threat-model constraints.
|
||||||
|
|
||||||
|
2. **Risk:** Access controls disrupt operator usability
|
||||||
|
- **Mitigation:** keep mechanism minimal and test primary workflows thoroughly.
|
||||||
|
|
||||||
|
3. **Risk:** Sensitive details leak through errors/logging
|
||||||
|
- **Mitigation:** apply safe-output and log-sanitization checks with tests.
|
||||||
|
|
||||||
|
4. **Risk:** Unpatched dependency vulnerabilities remain invisible
|
||||||
|
- **Mitigation:** formalize scan + triage + evidence capture workflow.
|
||||||
|
|
||||||
|
5. **Risk:** Secret handling remains ad hoc
|
||||||
|
- **Mitigation:** fail-fast settings + explicit operator documentation + review checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 Completion Checklist
|
||||||
|
|
||||||
|
- [ ] Private-network and single-operator security assumptions are documented.
|
||||||
|
- [ ] Basic single-operator access control is implemented and verified.
|
||||||
|
- [ ] Input-validation boundaries are audited, hardened, and test-covered.
|
||||||
|
- [ ] UI/API error output safety is confirmed under security tests.
|
||||||
|
- [ ] Secret handling policy and local workflow docs are complete.
|
||||||
|
- [ ] Dependency/security scans are run; critical findings are resolved.
|
||||||
|
- [ ] Step 5 tests pass across all validation lanes.
|
||||||
|
- [ ] `docs/ver1/ver1-step5-results.md` is completed with evidence and residual risks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff to Step 6
|
||||||
|
|
||||||
|
Step 5 completion enables Step 6 (Minimal Observability & Operability) with:
|
||||||
|
|
||||||
|
- explicit security assumptions for operator context
|
||||||
|
- access and validation controls suitable for private-network operation
|
||||||
|
- safer runtime/configuration handling for ongoing operations
|
||||||
|
- dependency-risk visibility feeding release-readiness gates
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
## Step 6 Goal (from `docs/ver1/ver1.md`)
|
||||||
|
Implement **minimal observability & operability** so a single operator can quickly diagnose and recover from common failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Current-State Assessment (what already exists)
|
||||||
|
|
||||||
|
### Already in place
|
||||||
|
- Central startup logging initialization via `setup_logging()` and `dictConfig` (`src/transcription/config.py`, `src/transcription/app.py`).
|
||||||
|
- Error taxonomy and `error_id` envelope contract (`src/transcription/errors.py`) aligned with `docs/error_handling.md`.
|
||||||
|
- Error handling for API and worker includes category + error IDs in some paths (`src/transcription/api/errors.py`, `src/transcription/worker.py`).
|
||||||
|
- Basic health endpoint `/healthz` (`src/transcription/api/health.py`).
|
||||||
|
- UI error display already shows actionable message + error reference (`src/transcription/ui/error_presenter.py`).
|
||||||
|
|
||||||
|
### Gaps to close for Step 6
|
||||||
|
1. **Structured logging is inconsistent** (many logs are free-form text with embedded key/value; no enforced schema).
|
||||||
|
2. **Boundary coverage is incomplete** (UI/service/API/worker don’t all emit consistent operation logs).
|
||||||
|
3. `/healthz` is very basic; no lightweight readiness/startup diagnostics endpoint/reporting.
|
||||||
|
4. No concise **operator runbook** yet (start/stop, log interpretation, recovery playbooks).
|
||||||
|
5. Minimal counters/timings are not yet standardized.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) MCP Guidance Incorporated (relevant items)
|
||||||
|
|
||||||
|
From `john-stream-mcp`, these are directly applied:
|
||||||
|
|
||||||
|
- **`python-logging-dictconfig`**: keep one centralized `dictConfig`, configure once at startup, named loggers in modules.
|
||||||
|
- **`fastapi-async-sqlalchemy-modernization`**: include observability + health/readiness checks; explicit lifecycle and deterministic startup/shutdown checks.
|
||||||
|
- **`fastapi-uv-docker`**: keep `/healthz`; add practical readiness/ops checks for deployment clarity.
|
||||||
|
- **`pytesting`**: deterministic tests, concise structure, validation lanes (`collect-only`, `unit`, `not external`, full).
|
||||||
|
- **`pydantic-settings`**: keep typed settings as single source for logging/health behavior flags.
|
||||||
|
- **`nicegui` + `nicegui-ui-customization`**: preserve clear, actionable user-facing error feedback and non-blocking UI flows.
|
||||||
|
- **`zensical-docs`**: produce focused, navigable operator docs.
|
||||||
|
|
||||||
|
(Other MCP resources were reviewed but are not core to Step 6 implementation scope.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Detailed Implementation Plan for Step 6
|
||||||
|
|
||||||
|
## Workstream A — Structured Logging Contract
|
||||||
|
|
||||||
|
### A1. Define a canonical log event schema
|
||||||
|
Create a project log schema (doc + code-level constants) with required keys:
|
||||||
|
- `timestamp` (UTC)
|
||||||
|
- `level`
|
||||||
|
- `logger`
|
||||||
|
- `operation`
|
||||||
|
- `event`
|
||||||
|
- `error_id` (when error)
|
||||||
|
- `category` (when error)
|
||||||
|
- `exception_type` (when error)
|
||||||
|
- `job_id`, `document_id` (when relevant)
|
||||||
|
- optional: `duration_ms`, `retry_count`, `status`
|
||||||
|
|
||||||
|
### A2. Standardize log emission helpers
|
||||||
|
Add small logging helpers (or adapter utilities) to reduce drift:
|
||||||
|
- `log_operation_start(...)`
|
||||||
|
- `log_operation_success(...)`
|
||||||
|
- `log_operation_error(...)`
|
||||||
|
|
||||||
|
Keep this minimal and avoid heavy observability frameworks.
|
||||||
|
|
||||||
|
### A3. Update formatter to structured output
|
||||||
|
Use `dictConfig` to emit either:
|
||||||
|
- JSON lines (preferred for structure), or
|
||||||
|
- strict key-value line format with fixed fields.
|
||||||
|
|
||||||
|
**Recommendation:** JSON lines to satisfy “structured logging” unambiguously while still simple.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream B — Boundary-by-Boundary Instrumentation
|
||||||
|
|
||||||
|
### B1. API boundary (`src/transcription/api/*`)
|
||||||
|
- Add request-level operation logs for key routes (`upload.submit`, `jobs.list`, `jobs.get`, etc.).
|
||||||
|
- Ensure API exception handler logs always include `error_id`, `category`, `operation`, `exception_type`.
|
||||||
|
|
||||||
|
### B2. Service boundary (`src/transcription/services/*`)
|
||||||
|
- Add operation logs around:
|
||||||
|
- upload validation/persist,
|
||||||
|
- transcription orchestration,
|
||||||
|
- revision add/accept,
|
||||||
|
- search/export.
|
||||||
|
- Add timing (`duration_ms`) for high-value operations only.
|
||||||
|
|
||||||
|
### B3. Worker boundary (`src/transcription/worker.py`)
|
||||||
|
- Standardize all worker log events to schema.
|
||||||
|
- Ensure retry logs include: `retriable`, `retry_count`, `max_retries`, `backoff_seconds`.
|
||||||
|
- Ensure terminal failure logs include error contract fields.
|
||||||
|
|
||||||
|
### B4. UI boundary (`src/transcription/ui/*`)
|
||||||
|
- Keep user-safe UI messages as-is.
|
||||||
|
- Add backend/UI logger events for user-triggered failures (operation + error_id + category) so UI-visible errors correlate to server logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream C — Health, Readiness, Startup Operability
|
||||||
|
|
||||||
|
### C1. Keep `/healthz` lightweight
|
||||||
|
- Return “process is running” status quickly.
|
||||||
|
|
||||||
|
### C2. Add lightweight `/readyz`
|
||||||
|
Include small checks:
|
||||||
|
- DB connectivity ping.
|
||||||
|
- Worker thread alive check.
|
||||||
|
- Optional prompt directory existence check.
|
||||||
|
|
||||||
|
Return structured status payload with per-check pass/fail.
|
||||||
|
|
||||||
|
### C3. Startup self-check summary log
|
||||||
|
At startup, emit one concise ops summary event:
|
||||||
|
- environment
|
||||||
|
- schema validation result
|
||||||
|
- worker started
|
||||||
|
- directories checked
|
||||||
|
- bootstrap/migration mode flags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream D — Minimal Counters & Timings
|
||||||
|
|
||||||
|
Add only high-value diagnostics:
|
||||||
|
1. `worker_jobs_processed_total`
|
||||||
|
2. `worker_jobs_failed_total`
|
||||||
|
3. `worker_retries_total`
|
||||||
|
4. `transcription_duration_ms` (per job)
|
||||||
|
5. `upload_persist_duration_ms` (per upload path)
|
||||||
|
|
||||||
|
Implementation can be log-derived counters (no external metrics backend required).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream E — Operator Runbook
|
||||||
|
|
||||||
|
Create concise runbook doc (recommended: `docs/ver1/ver1-step6-operator-runbook.md`) with:
|
||||||
|
|
||||||
|
1. **Start/Stop**
|
||||||
|
- local `uv` run mode
|
||||||
|
- docker compose mode (if applicable)
|
||||||
|
|
||||||
|
2. **Where logs are**
|
||||||
|
- stdout, docker logs commands, filtering by `error_id` / `operation`.
|
||||||
|
|
||||||
|
3. **Common failure patterns → recovery**
|
||||||
|
- provider timeout
|
||||||
|
- auth denied
|
||||||
|
- missing prompt dir
|
||||||
|
- DB unavailable
|
||||||
|
- job stuck/failed with retry exhausted
|
||||||
|
|
||||||
|
4. **Recovery procedures**
|
||||||
|
- restart sequence
|
||||||
|
- verify health/readiness
|
||||||
|
- when to requeue/re-upload
|
||||||
|
|
||||||
|
5. **Escalation artifacts**
|
||||||
|
- capture timestamp + error_id + operation + job_id/document_id
|
||||||
|
|
||||||
|
Also update `README.md` with short links to the runbook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstream F — Verification & Quality Gates
|
||||||
|
|
||||||
|
### Tests to add/update
|
||||||
|
- `tests/api/test_health.py`
|
||||||
|
- `/healthz` baseline
|
||||||
|
- `/readyz` pass/fail behavior
|
||||||
|
- `tests/api/test_error_responses.py` / `tests/api/test_routes.py`
|
||||||
|
- logs include `error_id/category/operation` on failures
|
||||||
|
- `tests/services/test_worker.py`
|
||||||
|
- retry/failure log fields + timing presence
|
||||||
|
- `tests/ui/*`
|
||||||
|
- ensure UI error correlation path includes operation/ref id behavior
|
||||||
|
|
||||||
|
### Validation commands (per MCP pytest guidance)
|
||||||
|
- `uv run pytest --collect-only -q`
|
||||||
|
- `uv run pytest -m unit -q`
|
||||||
|
- `uv run pytest -m "not external" -q`
|
||||||
|
- `uv run pytest -q`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Traceability to Governing Docs
|
||||||
|
|
||||||
|
- **`docs/ver1/ver1.md` Step 6:** all 5 implementation bullets covered.
|
||||||
|
- **`docs/error_handling.md`:** logging contract fields and error taxonomy continuity enforced.
|
||||||
|
- **`docs/architecture.md`:** respects modular boundaries, in-process worker model, low-complexity ops.
|
||||||
|
- **`docs/requirements.md`:**
|
||||||
|
- REQ-8 (startup logging/config centralization) strengthened,
|
||||||
|
- REQ-5 (status visibility) improved operationally,
|
||||||
|
- REQ-7 lifecycle ownership observability improved.
|
||||||
|
- **`docs/intent.md`:** keeps operation simple for personal-scale archival workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Suggested Execution Order (low risk)
|
||||||
|
|
||||||
|
1. Logging schema + formatter + helpers
|
||||||
|
2. Worker/API instrumentation (highest value)
|
||||||
|
3. Service/UI instrumentation
|
||||||
|
4. `/readyz` + startup summary check
|
||||||
|
5. Runbook + README links
|
||||||
|
6. Tests + Step 6 results artifact (`docs/ver1/ver1-step6-results.md`)
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# Version 1 Implementation Plan
|
||||||
|
|
||||||
|
This plan defines the path from MVP to **Version 1 complete**.
|
||||||
|
The objective is to deliver the full scoped product with readiness for reliable personal-scale operation, while explicitly separating refinements/enhancements into a future document.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0) Plan Governance & Scope Control (Foundation)
|
||||||
|
|
||||||
|
**Goal:** Keep execution focused on V1 completion and avoid unnecessary process overhead.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Create and maintain a **V1 Traceability Matrix**:
|
||||||
|
- Requirement ID
|
||||||
|
- Current status (`done`, `partial`, `not started`)
|
||||||
|
- Validation method
|
||||||
|
2. Define V1 completion gates:
|
||||||
|
- Functional complete
|
||||||
|
- Operationally complete
|
||||||
|
- Personal-deployment ready
|
||||||
|
3. Snapshot the MVP baseline (tag/changelog reference).
|
||||||
|
4. Keep a standing rule: non-V1 ideas go to a separate enhancements backlog, and enter V1 only by explicit approval.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- `docs/ver1/ver1.md` (this plan)
|
||||||
|
- V1 traceability artifact:
|
||||||
|
- `docs/ver1/ver1-step1-2-carry-forward-checklist.md`
|
||||||
|
- `docs/ver1/ver1-step2-error-path-inventory.md` (supporting artifact)
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Every in-scope requirement has explicit status and validation evidence.
|
||||||
|
- Scope-change discipline is followed consistently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Architecture Consolidation
|
||||||
|
|
||||||
|
**Goal:** Align implementation with intended architecture while preserving simplicity.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Compare implemented modules/components with architecture documentation.
|
||||||
|
2. Identify and classify architectural debt:
|
||||||
|
- Temporary coupling
|
||||||
|
- Missing interfaces
|
||||||
|
- Placeholder services/components
|
||||||
|
3. Resolve architecture gaps that threaten reliability, maintainability, or clear boundaries.
|
||||||
|
4. Record material decisions and tradeoffs in ADRs.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Updated architecture diagrams and boundaries
|
||||||
|
- ADR entries for material decisions
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Architecture documentation reflects system reality.
|
||||||
|
- High-impact architecture risks are addressed or explicitly scheduled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Error Handling & Reliability Hardening
|
||||||
|
|
||||||
|
**Goal:** Ensure predictable, diagnosable behavior under expected failure conditions.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Apply the canonical taxonomy and response model from `docs/error_handling.md` across UI/API/service/worker boundaries.
|
||||||
|
2. Ensure clear distinction between:
|
||||||
|
- User-facing safe messages
|
||||||
|
- Internal diagnostic detail
|
||||||
|
- Retryable vs non-retryable failures
|
||||||
|
3. Implement practical resilience controls where needed:
|
||||||
|
- Timeouts
|
||||||
|
- Bounded retries with backoff
|
||||||
|
- Explicit terminal failure states
|
||||||
|
4. Add failure-path tests for critical workflows.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Error handling reference aligned with `docs/error_handling.md`
|
||||||
|
- Failure-mode test coverage for critical paths
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Error behavior is consistent across major flows.
|
||||||
|
- Known failure scenarios are tested and pass.
|
||||||
|
- Failed jobs include actionable, traceable failure detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Functional Completion by Requirement Domain
|
||||||
|
|
||||||
|
**Goal:** Complete all V1 requirements in a practical, user-first order.
|
||||||
|
|
||||||
|
### Recommended Order
|
||||||
|
1. End-user core flows (upload → transcribe → review)
|
||||||
|
2. Data integrity and persistence behavior
|
||||||
|
3. Minimal operator controls needed for personal use
|
||||||
|
4. In-scope UX quality improvements
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
For each requirement slice:
|
||||||
|
1. Confirm contract/schema
|
||||||
|
2. Implement service/domain logic
|
||||||
|
3. Implement persistence/state transitions
|
||||||
|
4. Integrate API/UI behavior
|
||||||
|
5. Add or update automated tests
|
||||||
|
6. Update relevant docs
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Requirement completion report with validation evidence linked to REQ IDs
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- All V1 must-have requirements are complete and verified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Data Model and Migration Safety
|
||||||
|
|
||||||
|
**Goal:** Keep schema evolution safe and simple for personal-scale deployment.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Validate schema against finalized V1 domain needs.
|
||||||
|
2. Implement forward-safe migrations for expected upgrades.
|
||||||
|
3. Define a simple rollback/mitigation path for migration failures.
|
||||||
|
4. Add backfill scripts only where truly required.
|
||||||
|
5. Rehearse migration + rollback locally using representative sample data.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Migration and rollback runbook
|
||||||
|
- Backfill checklist (if applicable)
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Migration path is tested and documented.
|
||||||
|
- No unresolved data-loss risk for V1 upgrade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Private-Network Safety Baseline
|
||||||
|
|
||||||
|
**Goal:** Apply right-sized security controls for a single-user system on a trusted private network.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Enforce private-network deployment assumptions in docs and configuration.
|
||||||
|
2. Ensure basic single-operator access control for UI/API actions.
|
||||||
|
3. Enforce input validation and safe error output behavior.
|
||||||
|
4. Keep secrets out of source control; document local secret handling.
|
||||||
|
5. Run lightweight dependency/security scanning and resolve high-risk findings.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Security assumptions checklist (private network, single operator)
|
||||||
|
- Basic risk update for V1 scope
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- No unresolved critical vulnerabilities.
|
||||||
|
- Access behavior and validation rules are verified for intended operating model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Minimal Observability & Operability
|
||||||
|
|
||||||
|
**Goal:** Keep operation and troubleshooting simple, clear, and reliable.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Standardize structured logging across UI/API/service/worker boundaries.
|
||||||
|
2. Ensure logged errors include category and error reference IDs per `error_handling.md`.
|
||||||
|
3. Add lightweight health/startup checks.
|
||||||
|
4. Document a concise operator runbook:
|
||||||
|
- start/stop
|
||||||
|
- log locations
|
||||||
|
- common failure patterns and recovery steps
|
||||||
|
5. Add minimal counters/timings only where they clearly improve diagnosis.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Logging and error-traceability baseline
|
||||||
|
- Operator runbook
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Operator can diagnose common failures using logs + runbook.
|
||||||
|
- System recovery procedures are documented and repeatable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Test Coverage and Practical Quality Gates
|
||||||
|
|
||||||
|
**Goal:** Prevent regressions in critical flows without overbuilding test infrastructure.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Expand unit and integration tests for all V1 requirement slices.
|
||||||
|
2. Add end-to-end tests for critical journeys:
|
||||||
|
- upload
|
||||||
|
- process/transcribe
|
||||||
|
- view result
|
||||||
|
- failure visibility
|
||||||
|
3. Add targeted contract tests where adapter boundaries are error-prone.
|
||||||
|
4. Keep CI gates focused on high-value checks (tests, lint, type checks, dependency scan).
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- V1 test matrix mapped to requirements and critical flows
|
||||||
|
- CI quality-gate checklist
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Critical-path regressions are automatically detected.
|
||||||
|
- Test suite gives consistent release confidence for personal-scale operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Performance Validation for Personal Scale
|
||||||
|
|
||||||
|
**Goal:** Confirm acceptable responsiveness for expected personal-use workload.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Define practical performance expectations for key flows.
|
||||||
|
2. Run representative tests using real document samples.
|
||||||
|
3. Address obvious bottlenecks in queries, file handling, or worker concurrency.
|
||||||
|
4. Document known limits and expected operating bounds.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Short performance validation note
|
||||||
|
- Known-limits summary
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Core flows remain responsive for expected corpus size and usage patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Release Readiness and Environment Simplicity
|
||||||
|
|
||||||
|
**Goal:** Make deployment and rollback repeatable for a single-operator Docker Compose setup.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Define a simple release checklist:
|
||||||
|
- run tests
|
||||||
|
- run one end-to-end transcription check
|
||||||
|
- verify migration compatibility
|
||||||
|
2. Document environment configuration requirements clearly.
|
||||||
|
3. Validate deployment and rollback steps in a local rehearsal.
|
||||||
|
4. Add backup/restore verification for core persisted data.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Release checklist
|
||||||
|
- Environment and rollback guide
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Deployment/rollback is rehearsed and documented.
|
||||||
|
- Operator can release safely without hidden steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) Documentation Completion
|
||||||
|
|
||||||
|
**Goal:** Ensure V1 can be built, operated, and supported from documentation.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Update core project docs to match final V1 behavior:
|
||||||
|
- Architecture
|
||||||
|
- Error handling
|
||||||
|
- Requirements status
|
||||||
|
- Index/navigation
|
||||||
|
- Intent alignment summary
|
||||||
|
2. Add operator troubleshooting guides.
|
||||||
|
3. Add integration/API examples for the operator and future maintainers.
|
||||||
|
4. Publish changelog/version notes for V1.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Updated documentation set for V1
|
||||||
|
- V1 release notes
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- A future maintainer can run and support the system using docs alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11) Final Validation and Launch
|
||||||
|
|
||||||
|
**Goal:** Confirm V1 readiness and launch with low operational risk.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Run end-to-end acceptance validation against the V1 traceability matrix.
|
||||||
|
2. Complete operator acceptance checks on representative real documents.
|
||||||
|
3. Execute launch checklist (including backup, migration, and rollback readiness).
|
||||||
|
4. Launch and monitor logs/status closely during initial use.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Acceptance validation record
|
||||||
|
- Launch checklist completion record
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- V1 requirements are validated.
|
||||||
|
- Initial launch behavior is stable and recoverable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12) Post-Launch Stabilization
|
||||||
|
|
||||||
|
**Goal:** Address early issues quickly and lock in a reliable V1 baseline.
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
1. Track defects and operational pain points observed after launch.
|
||||||
|
2. Prioritize short-cycle stabilization fixes.
|
||||||
|
3. Remove temporary launch-only workarounds when safe.
|
||||||
|
4. Capture a brief retrospective and update the next-phase backlog.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Stabilization summary
|
||||||
|
- Updated backlog for post-V1 enhancements
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Major launch issues are resolved.
|
||||||
|
- System transitions to steady personal-use operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Execution Rhythm
|
||||||
|
|
||||||
|
- **Weekly:** Requirement closure + risk review
|
||||||
|
- **As needed (small batch releases):** Run release checklist and deploy
|
||||||
|
- **Milestone check-ins:** After phases 2, 6, 9, and 11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope Discipline Rule (V1 Focus)
|
||||||
|
|
||||||
|
To preserve delivery focus:
|
||||||
|
- V1 execution prioritizes completion of scoped requirements.
|
||||||
|
- Refinements/enhancements are captured in a separate future document and backlog.
|
||||||
|
- Only explicitly approved scope changes may enter this plan.
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.1)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Deliver the V4.1 usability revision as a small, behavior-safe increment over the V4 baseline.
|
|
||||||
|
|
||||||
## Implementation Principles
|
|
||||||
|
|
||||||
- Keep presentation formatting in UI components and route orchestration in pages.
|
|
||||||
- Keep persistence and cross-record queries behind service boundaries.
|
|
||||||
- Reuse shared table and date-label helpers instead of duplicating fallback logic.
|
|
||||||
- Make the FamilySearch schema change additive and nullable.
|
|
||||||
- Add focused tests for changed behavior before broad regression verification.
|
|
||||||
|
|
||||||
## Current Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Persistence | Add nullable `Person.family_search_id`; provide the repository's supported schema-upgrade path for existing databases. |
|
|
||||||
| People service | Normalize and validate FamilySearch IDs at the domain/service boundary if model validation does not fully cover writes. |
|
|
||||||
| Documents UI | Add table data, improve relationship labels/links, compact date display, and combine processing navigation. |
|
|
||||||
| People UI | Add table date fields, Person-first Document creation, compact date display, and FamilySearch controls. |
|
|
||||||
| Sources service/UI | Query adjacent document Sources and add bounded navigation; revise list columns and wrapping. |
|
|
||||||
| Jobs UI | Refresh the active detail read model on a timer until terminal status. |
|
|
||||||
| Shared UI | Add reusable constrained/wrapped table presentation and compact date formatting where appropriate. |
|
|
||||||
| Tests | Update model/service and UI coverage for all affected workflows. |
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Add Shared Presentation Rules
|
|
||||||
|
|
||||||
- Review `ui/components/table/common.py` and packaged theme CSS for the narrowest reusable table-width solution.
|
|
||||||
- Add reusable styles or column slots for constrained, wrapping, left-aligned text.
|
|
||||||
- Add a shared formatter for exact/approximate/unknown dates if it can be reused without coupling components to persistence.
|
|
||||||
- Preserve sorting and search behavior for rendered display values.
|
|
||||||
|
|
||||||
### 2. Update Archival List Tables
|
|
||||||
|
|
||||||
- Extend the Document table read model with author names and the compact document date.
|
|
||||||
- Build author display from eagerly loaded document-person links using the `author` role.
|
|
||||||
- Apply title/type alignment and constrained title wrapping.
|
|
||||||
- Extend the Person table read model with compact birth and death date values.
|
|
||||||
- Apply Display Name and Maiden Name alignment.
|
|
||||||
- Remove Stored Filename from the Source table read model only if no other list behavior consumes it; always remove its rendered column.
|
|
||||||
- Constrain and left-align the requested Source columns.
|
|
||||||
- Add or update UI component tests for serialized rows, columns, and fallback formatting.
|
|
||||||
|
|
||||||
### 3. Improve Document Relationship Workflows
|
|
||||||
|
|
||||||
- Introduce one person-label formatter that combines preferred Display Name, Full Name context, and known birth year without implying uniqueness.
|
|
||||||
- Use Person UUIDs as selector values.
|
|
||||||
- Apply the formatter to every relationship role selector.
|
|
||||||
- Change Related People rows into actions that navigate to `/people/{person_id}`.
|
|
||||||
- Replace separate exact/approximate rows in view mode with one conditional Document Date row.
|
|
||||||
- Combine Pipeline Jobs and Sources into one related-processing card beneath Related People.
|
|
||||||
- Preserve existing job/source counts and navigation actions.
|
|
||||||
|
|
||||||
### 4. Add the Person-First Document Workflow
|
|
||||||
|
|
||||||
- Add a New Document action on Person Detail.
|
|
||||||
- Pass the Person UUID through a narrowly defined query parameter to `/documents/new`.
|
|
||||||
- Validate the requested UUID against the loaded people list.
|
|
||||||
- Preselect that person in the intended default relationship role. Use `author` unless a different role is explicitly encoded later.
|
|
||||||
- Ignore invalid or unavailable preselection values with the application's normal visible error/notification behavior.
|
|
||||||
- Confirm ordinary `/documents/new` behavior remains unchanged.
|
|
||||||
|
|
||||||
### 5. Add FamilySearch Person References
|
|
||||||
|
|
||||||
- Add nullable, unique `family_search_id` to the `Person` model and schema.
|
|
||||||
- Implement a non-destructive upgrade for existing SQLite and PostgreSQL databases using the repository's established schema-management approach.
|
|
||||||
- Normalize values by trimming and uppercasing.
|
|
||||||
- Validate the `XXXX-XXX` alphanumeric identifier shape and return a clear validation error for malformed input.
|
|
||||||
- Report duplicate identifiers as a deterministic conflict rather than a generic persistence failure.
|
|
||||||
- Add the field to Person create/edit forms and preserve it during updates.
|
|
||||||
- Add a URL builder that safely inserts only a validated identifier into the fixed FamilySearch details URL.
|
|
||||||
- Render a FamilySearch action on Person Detail only when an identifier is present.
|
|
||||||
- Add persistence, normalization, validation, form, and link-generation tests.
|
|
||||||
|
|
||||||
### 6. Add Source Page Navigation
|
|
||||||
|
|
||||||
- Add a Sources service query that returns previous/current/next context for a Source within its Document.
|
|
||||||
- Define ordering by `page_number`, with a stable secondary key such as Source UUID for defensive determinism.
|
|
||||||
- Keep navigation bounded to the current `document_id`.
|
|
||||||
- Render previous and next actions adjacent to the source viewer or detail header.
|
|
||||||
- Disable or omit unavailable boundary actions.
|
|
||||||
- Test first, middle, last, single-page, and cross-document cases.
|
|
||||||
|
|
||||||
### 7. Add Job Detail Auto-Refresh
|
|
||||||
|
|
||||||
- Make Job Detail content refreshable without rebuilding unrelated global navigation.
|
|
||||||
- Start a NiceGUI timer only for queued or processing jobs.
|
|
||||||
- On each tick, re-read the Job through `JobService` and refresh the detail content.
|
|
||||||
- Use a 4-second default interval.
|
|
||||||
- Stop or deactivate the timer when status becomes completed, partial success, failed, or cancelled, according to the model's actual terminal states.
|
|
||||||
- Prevent overlapping refresh callbacks.
|
|
||||||
- Retain existing error presentation if a refresh read fails.
|
|
||||||
- Add UI tests for timer creation, refresh, and terminal-state stopping.
|
|
||||||
|
|
||||||
### 8. Simplify View-Mode Date Rows
|
|
||||||
|
|
||||||
- On Document Detail, show exact date, else approximate date, else one not-set value.
|
|
||||||
- On Person Detail, apply the same independent rule to birth and death.
|
|
||||||
- Do not hide either input in create/edit mode.
|
|
||||||
- Test each exact, approximate, and absent state.
|
|
||||||
|
|
||||||
### 9. Verification and Documentation Alignment
|
|
||||||
|
|
||||||
- Run the focused model/service/UI tests covering changed surfaces.
|
|
||||||
- Run the existing regression suite appropriate to persistence and UI changes.
|
|
||||||
- Confirm SQLite and PostgreSQL model compatibility at the schema-definition level.
|
|
||||||
- Update V4.1 documentation if implementation reveals a necessary boundary change; do not silently expand scope.
|
|
||||||
|
|
||||||
## Recommended Delivery Order
|
|
||||||
|
|
||||||
1. Shared formatters and table presentation.
|
|
||||||
2. Additive Person schema change and FamilySearch validation.
|
|
||||||
3. Document and Person list/detail changes.
|
|
||||||
4. Person-first Document workflow.
|
|
||||||
5. Source navigation.
|
|
||||||
6. Job polling.
|
|
||||||
7. Focused and regression verification.
|
|
||||||
|
|
||||||
## Done When
|
|
||||||
|
|
||||||
- Every V4.1 acceptance criterion is demonstrated or covered by a focused test.
|
|
||||||
- Existing Person rows remain valid after the nullable schema addition.
|
|
||||||
- Duplicate FamilySearch references cannot be assigned to multiple local Person records.
|
|
||||||
- FamilySearch links are generated only from normalized, validated IDs.
|
|
||||||
- Auto-refresh performs no polling after a terminal job state.
|
|
||||||
- Adjacent Source navigation never crosses Document boundaries.
|
|
||||||
- The existing V4 workflows remain operational.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Page reordering.
|
|
||||||
- Settings management.
|
|
||||||
- External genealogy API integration.
|
|
||||||
- Raw `.env` editing.
|
|
||||||
- Theme editing.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.1 Scope Boundary](scope_boundary_v4_1.md)
|
|
||||||
- [V4 Implementation Plan](../ver4/implementation_plan_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [V4 Error Handling Policy](../ver4/error_handling_v4.md)
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# V4.1 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the scope of the first incremental revision to Version 4. V4 remains the product and architecture baseline; V4.1 adds focused usability improvements and one additive Person field.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Improve common archival record workflows without redesigning the application.
|
|
||||||
- Resolve table overflow, ambiguous person selection, and unnecessary navigation.
|
|
||||||
- Add a manually maintained FamilySearch person reference without introducing external API integration.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Archival Documents List
|
|
||||||
|
|
||||||
- Keep every table column within the available page width.
|
|
||||||
- Limit and wrap long Document Title values.
|
|
||||||
- Left-align Document Title and Type.
|
|
||||||
- Add Author and Document Date columns.
|
|
||||||
- Display all people linked through the `author` role in the Author column.
|
|
||||||
- Display exact document date when present, otherwise approximate date when present, otherwise `Unknown`.
|
|
||||||
|
|
||||||
### 2. Document Detail and Editing
|
|
||||||
|
|
||||||
- Use an unambiguous label in person selectors. The label should prefer Display Name, retain Full Name for context, and include the birth year when known.
|
|
||||||
- Do not require Display Name to be unique.
|
|
||||||
- Link each Related People entry to its Person Detail page.
|
|
||||||
- In view mode, display only the populated exact or approximate document date row. Display a single unknown/not-set state when neither exists.
|
|
||||||
- Keep both exact and approximate inputs available in create/edit mode.
|
|
||||||
- Move source navigation out from beneath the media viewer.
|
|
||||||
- Present Pipeline Jobs and Sources together in one related-processing card with counts and actions.
|
|
||||||
|
|
||||||
### 3. People List
|
|
||||||
|
|
||||||
- Left-align Display Name and Maiden Name.
|
|
||||||
- Display exact birth date when present, otherwise approximate birth date when present, otherwise `Unknown`.
|
|
||||||
- Add a Death Date column with the same fallback rule.
|
|
||||||
|
|
||||||
### 4. Person Detail and Editing
|
|
||||||
|
|
||||||
- Add a New Document action that opens Document creation with the current person preselected.
|
|
||||||
- Preserve the existing Document-first workflow.
|
|
||||||
- In view mode, display only the populated exact or approximate row for each of birth and death date. Display a single unknown/not-set state when neither value exists.
|
|
||||||
- Keep both exact and approximate inputs available in create/edit mode.
|
|
||||||
|
|
||||||
### 5. FamilySearch Reference
|
|
||||||
|
|
||||||
- Add a nullable, unique `family_search_id` field to `Person`.
|
|
||||||
- Allow the field to be entered and changed in Person create/edit flows.
|
|
||||||
- Trim whitespace, normalize the identifier to uppercase, and validate it against the supported
|
|
||||||
`XXXX-XXX` alphanumeric shape before persistence.
|
|
||||||
- When an identifier exists, show a FamilySearch action on Person Detail linking to:
|
|
||||||
`https://www.familysearch.org/tree/person/details/{family_search_id}`
|
|
||||||
- Construct the URL in application code; do not persist the full URL.
|
|
||||||
|
|
||||||
### 6. Source List and Detail
|
|
||||||
|
|
||||||
- Keep every Source Asset Records table column within the available page width.
|
|
||||||
- Limit and wrap long Document Name, Upload Title, and Error Detail values.
|
|
||||||
- Left-align Document Name, Upload Title, and Error Detail.
|
|
||||||
- Remove Stored Filename only from the Source Asset Records table. Continue storing it and showing it on Source Detail.
|
|
||||||
- On Source Detail, add previous and next navigation for Sources belonging to the same Document, ordered by `page_number`.
|
|
||||||
- Disable or omit the previous/next action at the first/last page.
|
|
||||||
|
|
||||||
### 7. Job Detail
|
|
||||||
|
|
||||||
- Automatically refresh Job Detail while the job is in a non-terminal state.
|
|
||||||
- Use a modest interval in the 3-5 second range.
|
|
||||||
- Stop polling when the job reaches a terminal state or the page is no longer active.
|
|
||||||
- Preserve manual navigation and existing job actions.
|
|
||||||
|
|
||||||
### 8. Homepage Storage Decision
|
|
||||||
|
|
||||||
- Continue treating homepage markdown and images as mutable application data, not prompt artifacts or packaged source assets.
|
|
||||||
- Keep homepage content separate from `prompts`.
|
|
||||||
- Defer relocation to a configurable application-data root unless the existing location prevents normal installed or deployed operation.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Source page renumbering or reordering.
|
|
||||||
- A Settings page.
|
|
||||||
- Editing `.env` or secrets through the UI.
|
|
||||||
- Runtime theme editing.
|
|
||||||
- FamilySearch authentication, API calls, search, import, synchronization, or conflict resolution.
|
|
||||||
- Ancestry references or other genealogy providers.
|
|
||||||
- Google Maps links from place fields.
|
|
||||||
- Enforcing unique Display Name values.
|
|
||||||
- Changes to transcription execution or provider behavior.
|
|
||||||
- Changes to the V4 API solely to expose the V4.1 presentation enhancements.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. Person Selector Identity
|
|
||||||
|
|
||||||
- Selection values remain internal Person UUIDs.
|
|
||||||
- Display labels provide disambiguating context but are not identity keys.
|
|
||||||
- Duplicate Full Name and Display Name values remain valid.
|
|
||||||
|
|
||||||
### B. Date Presentation
|
|
||||||
|
|
||||||
- Exact dates take precedence over approximate/raw dates for compact list and view presentation.
|
|
||||||
- Create/edit forms retain both fields so either representation can be maintained.
|
|
||||||
- V4.1 does not introduce a new mutual-exclusion database constraint.
|
|
||||||
|
|
||||||
### C. FamilySearch Storage
|
|
||||||
|
|
||||||
- Store only the FamilySearch person identifier.
|
|
||||||
- Treat a FamilySearch person identifier as unique across local Person records.
|
|
||||||
- Use one dedicated nullable Person field while FamilySearch is the only supported external genealogy reference.
|
|
||||||
- Reconsider a generic external-reference model only when a second provider or multiple references per person are required.
|
|
||||||
|
|
||||||
### D. Stored Filename
|
|
||||||
|
|
||||||
- Stored Filename remains part of the Source model and Source Detail diagnostics.
|
|
||||||
- Only the list-table column is removed.
|
|
||||||
|
|
||||||
## Data and Compatibility Policy
|
|
||||||
|
|
||||||
- The `family_search_id` addition must be nullable and non-destructive for existing Person rows.
|
|
||||||
- Existing records, routes, relationships, jobs, Sources, prompt provenance, and uploaded media remain valid.
|
|
||||||
- UI changes must preserve both Document-first and Person-first workflows.
|
|
||||||
- V4.1 must remain portable across SQLite and PostgreSQL.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. Document, Person, and Source tables fit their page containers at supported desktop widths without losing requested columns.
|
|
||||||
2. Long table text wraps or is constrained without forcing important columns outside the table container.
|
|
||||||
3. Duplicate-named people can be distinguished in every document relationship selector.
|
|
||||||
4. Related People entries navigate to the correct Person Detail page.
|
|
||||||
5. Compact date displays consistently use exact, then approximate, then unknown fallback behavior.
|
|
||||||
6. Starting from Person Detail can create a Document with that person preselected without breaking normal Document creation.
|
|
||||||
7. A valid FamilySearch ID is persisted and produces the correct Person Detail hyperlink; absent IDs produce no action.
|
|
||||||
8. Source previous/next actions remain within the same Document and follow `page_number`.
|
|
||||||
9. Active Job Detail pages update without manual refresh and stop polling after terminal status.
|
|
||||||
10. Stored Filename is absent from the Source list table but remains available on Source Detail.
|
|
||||||
11. Focused automated tests pass and unaffected V4 behavior remains intact.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.1 Implementation Plan](implementation_plan_v4_1.md)
|
|
||||||
- [V4 Scope Boundary](../ver4/scope_boundary_v4.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.2)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Make processing evidence precise, append-only, secret-safe, and exportable while preserving every existing record and creating a provider-neutral home for future OCR/layout artifacts.
|
|
||||||
|
|
||||||
## Implementation Principles
|
|
||||||
|
|
||||||
- Implement the [Digital Evidence and AI Processing Provenance invariant](../invariant/ai_evidence_and_provenance.md), not a provider-specific approximation of it.
|
|
||||||
- Capture transport evidence before SDK parsing.
|
|
||||||
- Keep exact evidence separate from parsed and normalized representations.
|
|
||||||
- Prefer additive schema evolution and explicit compatibility behavior.
|
|
||||||
- Reference source content by digest rather than duplicating it in request JSON.
|
|
||||||
- Use allowlists for safe metadata capture.
|
|
||||||
- Keep persistence and evidence semantics behind service boundaries.
|
|
||||||
- Do not change the default model until a representative benchmark supports that decision.
|
|
||||||
|
|
||||||
## Current-State Gaps
|
|
||||||
|
|
||||||
| Current behavior | Gap to close |
|
|
||||||
| --- | --- |
|
|
||||||
| `Source` stores original file path, digest, and size. | Media type and image/page geometry used for an execution are not frozen with that execution. |
|
|
||||||
| `Job` stores prompt text/hash, requested model after resolution, temperature, and `top_p`. | The complete effective request structure, omitted-versus-explicit parameter state, routing constraints, and software versions are not frozen. |
|
|
||||||
| `JobSource.raw_api_response` stores `model_dump()` output from the OpenRouter SDK. | The exact HTTP body can be normalized by OpenRouter and filtered again by the SDK before persistence. |
|
|
||||||
| `JobSource.ai_metadata` stores finish reason and basic token counts. | Detailed accounting remains only in the SDK snapshot and is not a substitute for exact evidence. |
|
|
||||||
| Provider exceptions become application errors. | Safe HTTP error bodies, statuses, headers, and no-response distinctions are not persisted. |
|
|
||||||
| Worker logs elapsed time. | Execution duration is not stored on `JobSource`. |
|
|
||||||
| Source Detail displays AI metadata and the SDK snapshot. | The UI does not identify evidence layers or expose request/transport/software provenance. |
|
|
||||||
| No generic processing-artifact model exists. | Future OCR geometry would require ad hoc provider fields or an unrelated schema. |
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Database models and upgrades | Add execution-specification, transport-evidence, timing, software-context, and generic artifact storage without removing existing columns. |
|
|
||||||
| OpenRouter adapter | Introduce a transport boundary that can capture exact body/status/safe headers before typed SDK parsing, or use supported SDK hooks that expose the unparsed response reliably. |
|
|
||||||
| Provider contract | Return structured evidence for success and failure without leaking provider-specific transport concerns into workflow orchestration. |
|
|
||||||
| Source and workflow services | Persist one append-only execution outcome and its artifacts transactionally; retain compatibility projections. |
|
|
||||||
| UI | Label and inspect evidence layers; export safe evidence packages through service operations. |
|
|
||||||
| Benchmarking | Add a private manifest and repeatable evaluator using the literal-transcription methodology. |
|
|
||||||
| Tests and documentation | Add compatibility, capture, security, integrity, export, and benchmark-scoring coverage; correct overstated V4 evidence language. |
|
|
||||||
|
|
||||||
## Proposed Data Design
|
|
||||||
|
|
||||||
Exact names should be confirmed against existing conventions before migration code is written. The design should provide the following logical records.
|
|
||||||
|
|
||||||
### 1. Execution Evidence
|
|
||||||
|
|
||||||
Extend `JobSource` or associate it one-to-one with a new execution-evidence record containing:
|
|
||||||
|
|
||||||
- Request manifest JSON and manifest schema version.
|
|
||||||
- Transport status, body bytes or exact decoded body plus encoding/content type, and safe headers.
|
|
||||||
- Parsed SDK snapshot retained separately from transport content.
|
|
||||||
- Application, adapter, SDK, and runtime version metadata.
|
|
||||||
- Start, finish, and duration values.
|
|
||||||
- Router/provider request and generation identifiers when available.
|
|
||||||
- Failure phase and whether an HTTP response was received.
|
|
||||||
|
|
||||||
The implementation should evaluate a companion table rather than continuing to widen `JobSource`. A companion record better isolates large/optional evidence and permits clear one-to-one compatibility semantics.
|
|
||||||
|
|
||||||
### 2. Generic Processing Artifact
|
|
||||||
|
|
||||||
Add a one-to-many artifact model associated with a source and, when applicable, a producing execution:
|
|
||||||
|
|
||||||
- Stable artifact UUID.
|
|
||||||
- `source_id` and optional execution/`job_source_id`.
|
|
||||||
- Semantic artifact type.
|
|
||||||
- Media/serialization format.
|
|
||||||
- Schema name and version.
|
|
||||||
- Producer and producer version.
|
|
||||||
- Inline JSON payload or external location.
|
|
||||||
- Payload digest and byte size.
|
|
||||||
- Coordinate-system metadata when relevant.
|
|
||||||
- Creation timestamp.
|
|
||||||
|
|
||||||
Enforce exactly one content location: inline payload or external reference. An external artifact must be written durably and hashed before its database record commits.
|
|
||||||
|
|
||||||
### 3. Compatibility Projections
|
|
||||||
|
|
||||||
- Keep `JobSource.raw_api_response` unchanged for existing and new compatibility reads until a later deprecation decision.
|
|
||||||
- Keep `JobSource.ai_metadata` for indexed/display-ready normalized values.
|
|
||||||
- Keep `Source.raw_transcription` as the latest successful machine-output projection while treating per-execution `JobSource.raw_transcription` as history.
|
|
||||||
- Document that older rows have an SDK snapshot but no exact transport capture.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Correct Terminology and Define Typed Contracts
|
|
||||||
|
|
||||||
- Add typed domain models for request manifests, software context, transport metadata, failure phase, and artifact descriptors.
|
|
||||||
- Version every persisted JSON contract from its first release.
|
|
||||||
- Define the safe response-header allowlist. Begin with correlation, content type/encoding, date, retry/rate-limit, and router-specific generation identifiers only when documented and non-secret.
|
|
||||||
- Define size limits and external-storage thresholds for exact bodies and artifacts.
|
|
||||||
- Correct `docs/ver4/schema_v4.md` under “Page-Level Execution and AI Outputs” so the existing column is described as an SDK-serialized OpenRouter response snapshot, not a complete provider envelope, exact HTTP body, or native upstream-provider response. Apply the same terminology to architecture and UI schema references.
|
|
||||||
- Add serialization and secret-rejection unit tests before provider changes.
|
|
||||||
|
|
||||||
### 2. Add Additive Persistence and Upgrade Behavior
|
|
||||||
|
|
||||||
- Add the selected execution-evidence and artifact models.
|
|
||||||
- Add foreign keys, uniqueness constraints, and indexes for source/execution lookup.
|
|
||||||
- Implement idempotent upgrades following the repository's existing schema-upgrade policy.
|
|
||||||
- Do not populate exact response fields for historical rows.
|
|
||||||
- Do not write a capture-time classification onto historical rows during migration. Compatibility reads may describe a populated legacy `raw_api_response` as an SDK snapshot, but exports must identify that description as a later compatibility interpretation rather than execution-time metadata.
|
|
||||||
- Verify JSON portability and large-payload behavior for SQLite and PostgreSQL.
|
|
||||||
- Add upgrade tests starting from a representative pre-V4.2 schema.
|
|
||||||
|
|
||||||
### 3. Build Secret-Safe Request Manifests
|
|
||||||
|
|
||||||
- Build the manifest from the concrete outgoing request body immediately before transport, not from a narrower typed projection that may discard unrecognized request fields.
|
|
||||||
- Replace each image payload in that concrete representation with a source reference containing source UUID, digest, byte size, media type, dimensions, and transformation identity.
|
|
||||||
- Store exact prompt content and preserve omitted-versus-explicit parameter state.
|
|
||||||
- Include requested model, routing preferences, response-format requirements, and timeout/retry policy.
|
|
||||||
- Record application version/commit when available, adapter contract version, SDK package/version, and request-manifest schema version.
|
|
||||||
- Hash the canonical manifest representation for integrity checks.
|
|
||||||
- Test that credentials and embedded image data cannot enter the persisted manifest.
|
|
||||||
- Test that every field actually sent to the provider, including routing and future provider options, is represented or explicitly excluded by the manifest transform.
|
|
||||||
|
|
||||||
### 4. Capture OpenRouter Transport Evidence
|
|
||||||
|
|
||||||
- Evaluate the installed OpenRouter SDK hooks/client injection first.
|
|
||||||
- If hooks cannot expose an exact stable response before typed parsing, implement the non-streaming OpenRouter call through the existing async HTTP client boundary while retaining typed validation in the adapter.
|
|
||||||
- Read the response body once, preserve it exactly, then parse and normalize it.
|
|
||||||
- Store status, content type/encoding, allowlisted headers, request/generation ID, and timing.
|
|
||||||
- Maintain current authentication, referer/title headers, timeout behavior, and error classification.
|
|
||||||
- Explicitly document that the captured body is the OpenRouter-normalized transport response, not Gemini/Anthropic/OpenAI native upstream JSON.
|
|
||||||
- Add fixture-based tests proving unknown response fields survive transport capture even if a typed parser ignores them.
|
|
||||||
|
|
||||||
### 5. Preserve Failure Evidence
|
|
||||||
|
|
||||||
- Return or raise a typed provider failure that carries safe evidence separately from its user-facing error.
|
|
||||||
- Persist non-success status/body/allowlisted headers before marking an execution failed.
|
|
||||||
- Represent DNS/connect/TLS/local timeout failures as no-response outcomes with a failure phase and safe diagnostic category.
|
|
||||||
- Preserve response-validation failures with both the exact body and validation details.
|
|
||||||
- Keep transcription-quality rejection distinct from provider failure because a valid provider response was received.
|
|
||||||
- Ensure error strings and logs do not contain authorization data or embedded image payloads.
|
|
||||||
- Add tests for 4xx, 5xx, malformed JSON, schema mismatch, timeout, connection failure, and quality rejection.
|
|
||||||
|
|
||||||
### 6. Make Execution History Reliably Append-Only
|
|
||||||
|
|
||||||
- Confirm retry behavior creates a distinct execution attempt rather than reusing and overwriting a completed evidence record.
|
|
||||||
- Separate queue linkage from execution-attempt identity; the current update-in-place behavior cannot serve as append-only execution history.
|
|
||||||
- Assign each attempt a deterministic, monotonically increasing attempt number scoped to its Job and Source, enforced by a database uniqueness constraint.
|
|
||||||
- Update the latest-transcription projection only after a successful attempt.
|
|
||||||
- Never update prior response bodies, manifests, timings, or artifacts during a retry.
|
|
||||||
- Select the latest attempt and latest successful attempt by the persisted attempt number with a stable identifier as a defensive secondary key, never by timestamp alone.
|
|
||||||
- Add service/workflow tests covering retries, partial success, interrupted jobs, and historical projection behavior.
|
|
||||||
|
|
||||||
### 7. Add Generic Artifact Persistence
|
|
||||||
|
|
||||||
- Implement service operations to create, read, list, verify, export, and, only under explicit retention policy, delete artifacts.
|
|
||||||
- Validate semantic type, schema/version, digest, media type, and coordinate metadata.
|
|
||||||
- Support JSON artifacts inline initially when within the agreed size threshold.
|
|
||||||
- Support external artifacts through a constrained application-data root with atomic write, digest verification, and explicit missing-file errors.
|
|
||||||
- Add a provider-neutral example fixture representing OCR words/lines with polygons and confidence values.
|
|
||||||
- Do not integrate a live OCR vendor in this phase.
|
|
||||||
|
|
||||||
### 8. Add Evidence Inspection and Export
|
|
||||||
|
|
||||||
- Rename the current Source Detail label to identify historical values as an OpenRouter SDK Response Snapshot.
|
|
||||||
- Add separate sections for Request Manifest, Transport Response, Normalized Metadata, Software Context, and Derived Artifacts.
|
|
||||||
- Show an explicit “not captured for this historical execution” state instead of an empty object.
|
|
||||||
- Keep large bodies collapsed by default and avoid rendering embedded source data.
|
|
||||||
- Add a service-owned export that packages a versioned manifest, evidence JSON/body files, artifact content or references, and digest inventory.
|
|
||||||
- Exclude secrets and machine-local paths that are not required to interpret the evidence.
|
|
||||||
- Add UI and export tests for new, historical, failed, and large-evidence records.
|
|
||||||
|
|
||||||
### 9. Establish the Private Benchmark
|
|
||||||
|
|
||||||
- Select a small initial corpus, then expand only when it exposes meaningful differences.
|
|
||||||
- Stratify examples by printed/typed text, handwriting style, degradation, layout complexity, language, and editorial anomaly.
|
|
||||||
- Reference existing Source UUIDs and digests in a private manifest; do not copy family documents into public test fixtures.
|
|
||||||
- Create manually reviewed reference transcriptions following the invariant methodology.
|
|
||||||
- Implement or adopt existing project-compatible CER/WER calculations without changing dependencies unless justified.
|
|
||||||
- Score omissions, inventions, silent modernization, uncertainty markup, and layout fidelity separately from CER/WER.
|
|
||||||
- Record cost and latency from preserved execution evidence.
|
|
||||||
- Run the current `google/gemini-2.5-flash` configuration as the baseline before testing alternatives.
|
|
||||||
- Treat results as model-version/route/corpus specific and preserve each comparison run.
|
|
||||||
|
|
||||||
### 10. Verify, Migrate, and Align Documentation
|
|
||||||
|
|
||||||
- Run the smallest focused model, provider, service, workflow, UI, upgrade, and export test groups first.
|
|
||||||
- Run broader regression tests only after focused validation passes.
|
|
||||||
- Execute all destructive tests through `tools/run_destructive_tests.py`.
|
|
||||||
- Verify backup creation and required restoration behavior before any test touching real application data.
|
|
||||||
- Confirm existing Source Detail records remain readable after upgrade.
|
|
||||||
- Update V4 architecture, schema, requirements, and UI schema mappings to point to V4.2 semantics.
|
|
||||||
- Record any deliberate deviation from this plan in the V4.2 scope before release.
|
|
||||||
|
|
||||||
## Recommended Delivery Order
|
|
||||||
|
|
||||||
1. Typed/versioned evidence contracts and terminology.
|
|
||||||
2. Additive execution-evidence persistence.
|
|
||||||
3. Secret-safe request manifests.
|
|
||||||
4. Exact OpenRouter transport capture.
|
|
||||||
5. Failure evidence and append-only retry semantics.
|
|
||||||
6. Generic artifact persistence.
|
|
||||||
7. Inspection and export.
|
|
||||||
8. Private benchmark tooling and baseline run.
|
|
||||||
9. Migration, regression verification, and documentation alignment.
|
|
||||||
|
|
||||||
## Key Implementation Decisions to Resolve
|
|
||||||
|
|
||||||
1. Whether execution evidence is a one-to-one companion to `JobSource` or part of a new execution-attempt model required for append-only retries.
|
|
||||||
2. Whether exact response bodies remain database values at expected sizes or move to hashed external files above a threshold.
|
|
||||||
3. The canonical JSON algorithm used to hash request manifests.
|
|
||||||
4. The safe-header allowlist supported by OpenRouter and future adapters.
|
|
||||||
5. The application version identity available in local, packaged, and uncommitted development builds.
|
|
||||||
6. The initial inline/external artifact size threshold and application-data root.
|
|
||||||
7. Whether evidence exports include original source binaries by default, optionally, or only by reference.
|
|
||||||
8. The minimum private benchmark corpus size and review process before model comparisons influence defaults.
|
|
||||||
|
|
||||||
These decisions must be settled before their corresponding implementation phase; they do not weaken the invariant or expand V4.2 into live OCR integration.
|
|
||||||
|
|
||||||
## Resolved Implementation Decisions
|
|
||||||
|
|
||||||
1. `JobSource` remains queue linkage and a compatibility projection; immutable retries use a one-to-many
|
|
||||||
`ExecutionAttempt` model with a unique `(job_id, source_id, attempt_number)` constraint.
|
|
||||||
2. Exact OpenRouter response bytes remain database values for V4.2. Generic artifacts use inline canonical JSON up
|
|
||||||
to 1 MiB by default and constrained, atomically written external files above that threshold.
|
|
||||||
3. Request manifests use `transcription-canonical-json-v1`: UTF-8 JSON with sorted keys, compact separators,
|
|
||||||
preserved Unicode, and non-finite numbers rejected.
|
|
||||||
4. Safe response headers are explicitly allowlisted in the evidence contract; all others are discarded before
|
|
||||||
persistence.
|
|
||||||
5. Software identity records the package version, optional `TRANSCRIPTION_COMMIT`, adapter contract version,
|
|
||||||
OpenRouter SDK version, and Python version.
|
|
||||||
6. The artifact root defaults to `data/artifacts` and stores source-scoped relative references.
|
|
||||||
7. Evidence exports include source identity and digest by reference, not original source binaries.
|
|
||||||
8. The benchmark manifest is private and digest-referenced. Corpus size remains archive-dependent, but every run
|
|
||||||
uses preserved execution-attempt identity and the fixed literal scoring contract.
|
|
||||||
|
|
||||||
## Done When
|
|
||||||
|
|
||||||
- Every V4.2 acceptance criterion is satisfied by focused tests or an explicit demonstration.
|
|
||||||
- Existing SDK snapshots retain their content and are labeled accurately.
|
|
||||||
- New successful and failed calls preserve secret-safe provider-boundary evidence.
|
|
||||||
- Unknown transport fields survive even when the typed SDK/parser does not recognize them.
|
|
||||||
- Retries cannot overwrite prior execution evidence.
|
|
||||||
- A generic versioned artifact can represent OCR geometry and pass integrity verification.
|
|
||||||
- Evidence can be safely inspected and exported with schema identities and digests.
|
|
||||||
- The current model has a reproducible private benchmark baseline.
|
|
||||||
- No credential or embedded source payload appears in persisted manifests, safe headers, logs, or exports.
|
|
||||||
- Existing V4.1 behavior remains compatible.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Live OCR/document-AI provider integration.
|
|
||||||
- Automatic model switching.
|
|
||||||
- Archive-wide reprocessing.
|
|
||||||
- Native upstream-provider response capture through OpenRouter when OpenRouter does not expose it.
|
|
||||||
- Guarantees of deterministic hosted-model output.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.2 Scope Boundary](scope_boundary_v4_2.md)
|
|
||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [Draft V4.3 Implementation Plan](../ver4.3/implementation_plan_v4_3.md)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# V4.2 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the boundary for the digital-evidence and AI-provenance revision that follows V4.1 and precedes the planned V4.3 settings work. V4 remains the architecture baseline; V4.2 makes the existing evidence claims precise and adds a provider-neutral foundation for future processing artifacts.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Align the application with the [Digital Evidence and AI Processing Provenance invariant](../invariant/ai_evidence_and_provenance.md).
|
|
||||||
- Preserve provider-boundary evidence before SDK parsing can remove unknown fields.
|
|
||||||
- Make successful and failed processing attempts inspectable without storing secrets.
|
|
||||||
- Support future OCR and layout outputs without coupling the database to one vendor.
|
|
||||||
- Establish a repeatable method for comparing transcription models against this archive.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Evidence Terminology and Existing-Data Compatibility
|
|
||||||
|
|
||||||
- Define transport response, router-normalized response, SDK response, normalized metadata, and derived artifact consistently in code, schema documentation, and UI labels.
|
|
||||||
- Treat existing `JobSource.raw_api_response` values as historical SDK response snapshots.
|
|
||||||
- Preserve every existing `Job`, `Source`, and `JobSource` row.
|
|
||||||
- Use additive migrations and compatibility reads; do not reinterpret previously stored values as exact transport captures.
|
|
||||||
- Correct the “Page-Level Execution and AI Outputs” rule in `docs/ver4/schema_v4.md` that currently describes `JOB_SOURCE` as storing a complete provider response envelope. The corrected rule must identify `raw_api_response` as an SDK-serialized OpenRouter response snapshot and state that it is neither the exact HTTP body nor the native upstream-provider response.
|
|
||||||
|
|
||||||
### 2. Secret-Safe Request Manifests
|
|
||||||
|
|
||||||
- Persist the effective request specification for each page execution without storing credentials or duplicate base64 media.
|
|
||||||
- Include requested provider/model, routing constraints, prompt content and hash, explicitly supplied parameters, source digest, media type, dimensions when known, and page identity.
|
|
||||||
- Distinguish an omitted optional parameter from an explicitly supplied null or value.
|
|
||||||
- Record application, provider-adapter, Python client, and relevant schema versions.
|
|
||||||
- Use source or derivative references in place of embedded media bytes.
|
|
||||||
|
|
||||||
### 3. Provider-Boundary Response Capture
|
|
||||||
|
|
||||||
- Capture the exact HTTP response body before OpenRouter SDK parsing for non-streaming transcription calls.
|
|
||||||
- Store HTTP status and an explicit allowlist of safe response headers.
|
|
||||||
- Store router request/generation identifiers and resolved model/provider-routing metadata when exposed.
|
|
||||||
- Preserve the current parsed SDK snapshot and normalized metadata where useful.
|
|
||||||
- Keep exact body, parsed representation, and normalized fields distinguishable.
|
|
||||||
|
|
||||||
### 4. Failure Evidence and Timing
|
|
||||||
|
|
||||||
- Create or update a page execution record for every attempted provider call.
|
|
||||||
- Persist safe response evidence for non-success HTTP responses.
|
|
||||||
- Distinguish HTTP response failures, connection failures, local timeouts, response-validation failures, and transcription-quality failures.
|
|
||||||
- Store execution start/end times or duration using a clearly defined clock policy.
|
|
||||||
- Do not collapse a provider error body into only a generic user-facing message.
|
|
||||||
|
|
||||||
### 5. Generic Processing Artifacts
|
|
||||||
|
|
||||||
- Add a provider-neutral representation for versioned derived artifacts.
|
|
||||||
- Support inline JSON and externally stored payloads with a digest and stable reference.
|
|
||||||
- Record artifact type, format, schema/version, producer/version, source, producing execution, and creation time.
|
|
||||||
- Define coordinate-system metadata sufficient for word, line, block, or page geometry.
|
|
||||||
- Permit future OCR/layout/confidence results without implementing a vendor-specific table for each provider.
|
|
||||||
|
|
||||||
### 6. Evidence Inspection and Export
|
|
||||||
|
|
||||||
- Expand Source Detail and/or Job Detail to identify the evidence layer being displayed.
|
|
||||||
- Provide readable JSON inspection for request manifests, transport metadata, parsed responses, normalized metadata, and derived artifacts.
|
|
||||||
- Provide a safe export containing evidence content or references, relationships, schema versions, and digests.
|
|
||||||
- Clearly label evidence that was not captured for historical records.
|
|
||||||
- Do not display or export credentials, unrestricted headers, or embedded base64 source media.
|
|
||||||
|
|
||||||
### 7. Representative-Corpus Benchmark Protocol
|
|
||||||
|
|
||||||
- Define a private benchmark manifest referencing source digests rather than duplicating archival media.
|
|
||||||
- Include representative printed, typed, handwritten, degraded, tabular, and spatially complex pages.
|
|
||||||
- Pair each benchmark item with a manually reviewed literal transcription.
|
|
||||||
- Score character error rate, word error rate, omissions, inventions, silent normalization, uncertainty handling, layout fidelity, cost, and latency.
|
|
||||||
- Preserve the complete execution provenance for every benchmark run.
|
|
||||||
- Keep the current model as a baseline; do not change the application default solely from vendor benchmarks.
|
|
||||||
|
|
||||||
### 8. Migration, Integrity, and Verification
|
|
||||||
|
|
||||||
- Provide non-destructive upgrade behavior for supported SQLite and PostgreSQL deployments.
|
|
||||||
- Backfill only facts that can be derived reliably from existing records.
|
|
||||||
- Mark unavailable historical evidence as unavailable rather than fabricating it.
|
|
||||||
- Add digest, serialization, header-allowlist, failure-path, compatibility, artifact, export, and UI inspection tests.
|
|
||||||
- Run destructive tests only through the repository's required backup-and-restore wrapper.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Selecting or declaring a permanent best transcription model.
|
|
||||||
- Changing the default transcription model without benchmark evidence and a separate decision.
|
|
||||||
- Integrating Azure Document Intelligence, Google Document AI, Transkribus, Mistral OCR, or another OCR provider in V4.2.
|
|
||||||
- Generating bounding boxes retroactively for existing transcriptions.
|
|
||||||
- Bulk reprocessing the archive.
|
|
||||||
- Packet capture, TLS evidence, full unrestricted request/response headers, or credential retention.
|
|
||||||
- Storing duplicate base64 source images in request manifests.
|
|
||||||
- Guaranteeing byte-identical reproduction from nondeterministic or updated hosted models.
|
|
||||||
- Automatic entity extraction, biography generation, or genealogical inference.
|
|
||||||
- Replacing the relational database with an event store or content-addressed object store.
|
|
||||||
- Destructive renaming or removal of `raw_api_response`.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. The Original Source Is Primary Evidence
|
|
||||||
|
|
||||||
- Original uploaded bytes and their digest remain authoritative.
|
|
||||||
- Processing derivatives and outputs are independently identified derived evidence.
|
|
||||||
- Future OCR/layout work reuses the original or a documented derivative.
|
|
||||||
|
|
||||||
### B. Evidence Is Layered
|
|
||||||
|
|
||||||
- Exact transport evidence, SDK-parsed objects, normalized metadata, and transcription text serve different purposes.
|
|
||||||
- One representation must not silently stand in for another.
|
|
||||||
- UI and export labels name the stored evidence layer.
|
|
||||||
|
|
||||||
### C. History Is Append-Only
|
|
||||||
|
|
||||||
- A retry or reprocessing attempt creates new execution evidence.
|
|
||||||
- Convenience caches may change, but historical execution output does not.
|
|
||||||
- Human revisions remain separate from machine output.
|
|
||||||
|
|
||||||
### D. Capture Is Secret-Safe by Construction
|
|
||||||
|
|
||||||
- Safe headers are allowlisted.
|
|
||||||
- Authorization, cookies, API keys, and unrestricted headers are never persisted.
|
|
||||||
- Request manifests reference source digests instead of embedding source bytes.
|
|
||||||
|
|
||||||
### E. Derived Artifacts Are Generic and Versioned
|
|
||||||
|
|
||||||
- Artifact storage is not limited to bounding boxes.
|
|
||||||
- Coordinate metadata declares units, origin, dimensions, and transformations.
|
|
||||||
- Provider-specific payloads may be retained without making provider-specific fields the durable application contract.
|
|
||||||
|
|
||||||
### F. Existing Evidence Keeps Its Original Meaning
|
|
||||||
|
|
||||||
- Existing `raw_api_response` data remains an SDK response snapshot.
|
|
||||||
- A migration may label or classify it but may not claim that missing transport data was captured.
|
|
||||||
- Historical nulls and absent fields remain distinguishable from new explicitly captured values.
|
|
||||||
|
|
||||||
## Data and Compatibility Policy
|
|
||||||
|
|
||||||
- All schema changes are additive in V4.2.
|
|
||||||
- Existing source files, hashes, transcriptions, revisions, prompts, jobs, and relationships remain valid.
|
|
||||||
- Compatibility reads continue to display historical SDK snapshots.
|
|
||||||
- Large derived artifacts may be stored outside the database when the database retains a stable reference, digest, media type, and schema identity.
|
|
||||||
- JSON evidence must remain portable across SQLite and PostgreSQL.
|
|
||||||
- Exports use explicit schema versions so later releases can interpret older packages.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. A new execution can be traced from its source digest through its frozen request manifest, transport response, parsed/normalized data, and derived outputs.
|
|
||||||
2. Exact response content is captured before SDK parsing and is clearly distinguished from the existing SDK snapshot.
|
|
||||||
3. Failed HTTP calls retain safe provider evidence; calls with no response record that fact explicitly.
|
|
||||||
4. Omitted parameters remain distinguishable from explicit values.
|
|
||||||
5. No persisted request, header set, UI display, log, or export contains API credentials.
|
|
||||||
6. Retrying or reprocessing does not overwrite prior execution evidence.
|
|
||||||
7. Historical records remain readable and are not mislabeled as exact transport captures.
|
|
||||||
8. A versioned generic artifact can represent OCR/layout JSON and its coordinate system without a provider-specific schema change.
|
|
||||||
9. Evidence exports include relationships, schema identities, and digests sufficient for independent integrity checks.
|
|
||||||
10. The benchmark protocol can compare the current baseline with another model on the same private corpus and scoring rules.
|
|
||||||
11. Additive migrations and focused tests work across the supported persistence model.
|
|
||||||
12. All destructive-test runs comply with the backup-and-restore protocol.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.2 Implementation Plan](implementation_plan_v4_2.md)
|
|
||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.3)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Deliver constrained, installation-local application settings while preserving the completed V4.2 behavioral baseline and historical provenance.
|
|
||||||
|
|
||||||
## Planning Constraints
|
|
||||||
|
|
||||||
- V4, V4.1, and V4.2 remain the behavioral baseline.
|
|
||||||
- Settings must use explicit domain operations rather than direct database, environment-file, or arbitrary filesystem access from UI pages.
|
|
||||||
- Prompt changes must preserve historical Job provenance and use a defined safe-write policy.
|
|
||||||
- Source Page Reordering is excluded.
|
|
||||||
- Database, integration, and UI tests must use confirmed isolated test data and must never modify `data/transcription.db`.
|
|
||||||
- Potentially destructive tests must run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Documents service | Expand controlled Document Type maintenance operations. |
|
|
||||||
| People service | Expand controlled Person Role maintenance operations. |
|
|
||||||
| Prompt adapter/service | Add constrained listing, reading, validation, atomic writing, backup, and explicit recovery of existing prompt artifacts. |
|
|
||||||
| UI composition/navigation | Register Settings routes and navigation without moving persistence into UI code. |
|
|
||||||
| Tests | Add isolated registry lifecycle, prompt safety, and UI workflow coverage. |
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Define Service Contracts
|
|
||||||
|
|
||||||
- Define Document Type maintenance commands for create, relabel, activate, deactivate, and delete-if-unreferenced.
|
|
||||||
- Define Person Role maintenance commands for create, relabel, activate, deactivate, and delete-if-unreferenced.
|
|
||||||
- Define a Prompt Store interface for constrained list, read, write, backup-status, and explicit recovery behavior.
|
|
||||||
- Map validation, conflict, not-found, dependency, and filesystem failures to existing `AppError` categories.
|
|
||||||
|
|
||||||
### 2. Expand Registry Maintenance Services
|
|
||||||
|
|
||||||
- Reuse existing Document and People service ownership.
|
|
||||||
- Add explicit write methods rather than passing UI-mutated ORM objects directly where practical.
|
|
||||||
- Normalize Document Type labels and reject case-insensitive duplicates deterministically.
|
|
||||||
- Keep Person Role stable-code validation and duplicate rejection.
|
|
||||||
- Permit deletion only after a service-owned reference check proves the entry is unreferenced.
|
|
||||||
- Reject deletion of referenced entries deterministically without partial mutation.
|
|
||||||
- Permit label changes whether or not an entry is referenced.
|
|
||||||
- Preserve inactive entries for historical reads.
|
|
||||||
- Order Document Types alphabetically by normalized label.
|
|
||||||
- Order Person Roles deterministically by label and then code without adding a schema field.
|
|
||||||
- Add service tests for create, relabel, activation, deactivation, duplicates, immutable codes, ordering, allowed deletion, and blocked referenced deletion.
|
|
||||||
|
|
||||||
### 3. Add Constrained Prompt Storage
|
|
||||||
|
|
||||||
- Place filesystem access behind a dedicated Prompt Store/service boundary.
|
|
||||||
- Resolve all filenames directly beneath the configured prompt root and reject traversal.
|
|
||||||
- Permit only existing files with the agreed Markdown extension and reject empty content.
|
|
||||||
- Exclude prompt creation and deletion.
|
|
||||||
- Write new content to a sibling temporary file, flush and sync it, preserve the active file as the sole previous-version backup, and atomically replace the active file.
|
|
||||||
- Expose explicit backup recovery through the same filename validation and safe-write path; never perform automatic rollback.
|
|
||||||
- Clean up temporary files after failed writes while preserving the active prompt and any valid backup.
|
|
||||||
- Preserve file encoding and provide explicit failures for read-only or unavailable storage.
|
|
||||||
- Do not modify any Job row when prompt defaults change.
|
|
||||||
- Add unit tests for valid reads/writes, traversal, invalid names, nonexistent-file creation attempts, empty content, atomic replacement failures, single-backup rotation, explicit recovery, filesystem failures, and unchanged Job provenance.
|
|
||||||
|
|
||||||
### 4. Build the Settings UI
|
|
||||||
|
|
||||||
- Register a Settings landing page and navigation entry.
|
|
||||||
- Add separate pages or panels for Document Types, Person Roles, and Prompts.
|
|
||||||
- Keep pages responsible for orchestration and notifications only.
|
|
||||||
- Use service callbacks for all mutations.
|
|
||||||
- Explain inactive historical entries and future-only prompt effects in the UI.
|
|
||||||
- Present deletion only for unreferenced registry entries and preserve clear conflict feedback if references appear before submission.
|
|
||||||
- Present prompt backup availability and recovery as an explicit operator action.
|
|
||||||
- Do not render raw environment values or secrets.
|
|
||||||
- Add no settings API routes.
|
|
||||||
|
|
||||||
### 5. Verification and Rollout
|
|
||||||
|
|
||||||
- Confirm every database, integration, and UI test is configured for an isolated test database before execution.
|
|
||||||
- Never run those tests against live data and never modify or replace `data/transcription.db`.
|
|
||||||
- Invoke potentially destructive tests only through `tools/run_destructive_tests.py`.
|
|
||||||
- Run focused service tests before UI integration tests.
|
|
||||||
- Verify inactive registry behavior in both historical display and create/edit selectors.
|
|
||||||
- Verify referenced entries can be relabeled or deactivated but not deleted.
|
|
||||||
- Verify unreferenced entries can be deleted.
|
|
||||||
- Verify prompt changes are picked up by newly created Jobs while historical Jobs retain frozen content/hash.
|
|
||||||
- Run the relevant regression suite.
|
|
||||||
|
|
||||||
## Migration and Compatibility Notes
|
|
||||||
|
|
||||||
- Existing registry records remain valid.
|
|
||||||
- Prompt editing changes mutable application files, not database provenance already captured on Jobs.
|
|
||||||
- V4.3 must not require users to recreate existing Sources, Documents, People, roles, or types.
|
|
||||||
- Person Role ordering requires no schema migration.
|
|
||||||
- Registry deletion introduces no cascade behavior; references always block deletion.
|
|
||||||
|
|
||||||
## Delivery Order
|
|
||||||
|
|
||||||
1. Implement registry maintenance service operations.
|
|
||||||
2. Implement the Prompt Store and safety policy.
|
|
||||||
3. Build Settings pages.
|
|
||||||
4. Run isolated integration and regression verification.
|
|
||||||
|
|
||||||
## Done Criteria
|
|
||||||
|
|
||||||
- All V4.3 acceptance criteria are testable and satisfied.
|
|
||||||
- Settings mutations cross explicit service or adapter boundaries.
|
|
||||||
- Document Types use UUID-only identity and unique labels; Person Role codes cannot be accidentally changed.
|
|
||||||
- Referenced registry entries can be relabeled or deactivated but cannot be deleted.
|
|
||||||
- Unreferenced registry entries can be deleted without cascade behavior.
|
|
||||||
- Prompt writes cannot escape the configured directory or rewrite historical provenance.
|
|
||||||
- Prompt writes are atomic, retain one backup, and support explicit recovery.
|
|
||||||
- No secret or raw environment editor exists.
|
|
||||||
- No settings API surface exists.
|
|
||||||
- V4.1 and V4.2 workflows remain intact.
|
|
||||||
- Verification does not touch live data or `data/transcription.db`.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.3 Scope Boundary](scope_boundary_v4_3.md)
|
|
||||||
- [V4.2 Implementation Plan](../ver4.2/implementation_plan_v4_2.md)
|
|
||||||
- [V4.1 Implementation Plan](../ver4.1/implementation_plan_v4_1.md)
|
|
||||||
- [V4 Implementation Plan](../ver4/implementation_plan_v4.md)
|
|
||||||
- [V4 Error Handling Policy](../ver4/error_handling_v4.md)
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# V4.3 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the frozen boundary for the constrained-settings revision that follows the completed V4.2 evidence-and-provenance work. V4, V4.1, and V4.2 remain the behavioral and architecture baseline.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Provide a constrained Settings area for safe maintenance of selected application-managed configuration.
|
|
||||||
- Avoid exposing secrets, restart-sensitive settings, or unrestricted filesystem editing through the UI.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Settings Navigation
|
|
||||||
|
|
||||||
- Add a Settings entry to application navigation.
|
|
||||||
- Provide separate, clearly described settings areas rather than a raw configuration editor.
|
|
||||||
- Restrict V4.3 settings to application-managed values that can be validated and safely changed at runtime.
|
|
||||||
|
|
||||||
### 2. Document Type Maintenance
|
|
||||||
|
|
||||||
- List active and inactive Document Types.
|
|
||||||
- Add new types with a unique user-facing label.
|
|
||||||
- Edit labels and active state.
|
|
||||||
- Activate or deactivate types without invalidating historical Documents.
|
|
||||||
- Allow deletion only when no Document references the type.
|
|
||||||
- Allow label changes regardless of whether the type is referenced.
|
|
||||||
- Display types alphabetically by label.
|
|
||||||
|
|
||||||
### 3. Person Role Maintenance
|
|
||||||
|
|
||||||
- List active and inactive Person Roles.
|
|
||||||
- Add new roles with a stable unique code and user-facing label.
|
|
||||||
- Edit mutable labels.
|
|
||||||
- Activate or deactivate roles without invalidating historical links.
|
|
||||||
- Do not allow changing a stable code after creation.
|
|
||||||
- Order roles deterministically by label and then code; do not add persisted role sort order.
|
|
||||||
- Allow deletion only when no document-person link references the role.
|
|
||||||
- Allow label changes regardless of whether the role is referenced.
|
|
||||||
|
|
||||||
### 4. Prompt Maintenance
|
|
||||||
|
|
||||||
- List prompt markdown files from the configured prompt directory.
|
|
||||||
- View a prompt with a concise explanation of its purpose and use.
|
|
||||||
- Edit an existing prompt as plain markdown text.
|
|
||||||
- Validate the filename boundary and reject empty prompt content.
|
|
||||||
- Save changes explicitly and report filesystem failures.
|
|
||||||
- Preserve submission-time prompt text and hash already frozen on existing Jobs.
|
|
||||||
- Edit existing prompt files only; prompt creation and deletion are excluded.
|
|
||||||
- Save through a sibling temporary file, flush and sync file content, retain one previous-version backup, and atomically replace the active file.
|
|
||||||
- Provide an explicit recovery operation that restores the retained backup through the same safe-write path; do not silently roll back a failed or unwanted edit.
|
|
||||||
|
|
||||||
### 5. Deployment Boundary
|
|
||||||
|
|
||||||
- Settings changes apply only to the current installation.
|
|
||||||
- V4.3 adds no settings API endpoints.
|
|
||||||
- Service contracts must remain independent of the UI so a separately authorized API can be considered later.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Viewing or editing raw `.env` files.
|
|
||||||
- Displaying or changing provider API keys and other secrets.
|
|
||||||
- Editing host, port, database connection, upload paths, or other restart-sensitive runtime settings.
|
|
||||||
- Arbitrary file browsing or arbitrary prompt paths.
|
|
||||||
- Runtime theme/CSS editing.
|
|
||||||
- Installing themes or plugins.
|
|
||||||
- Source page renumbering or reordering.
|
|
||||||
- Source movement between Documents.
|
|
||||||
- Automatic ordering based on filenames, OCR, or image content.
|
|
||||||
- Prompt creation, deletion, and multi-version history.
|
|
||||||
- Persisted sort-order maintenance for Person Roles.
|
|
||||||
- Settings read or write API endpoints.
|
|
||||||
- FamilySearch API synchronization.
|
|
||||||
- A generic external-reference registry.
|
|
||||||
- Ancestry references and Google Maps links.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. Registry Identity and Lifecycle
|
|
||||||
|
|
||||||
- Document Types use UUID identity and case-insensitively unique labels; no separate code is exposed or stored.
|
|
||||||
- Person Role codes remain stable identifiers.
|
|
||||||
- Labels and active state remain mutable.
|
|
||||||
- Historical references remain valid when a registry entry is inactive.
|
|
||||||
- Labels may be updated for referenced and unreferenced entries.
|
|
||||||
- Unreferenced entries may be deleted; referenced entries may only be deactivated.
|
|
||||||
|
|
||||||
### B. No Raw Environment Editor
|
|
||||||
|
|
||||||
- `.env` may contain secrets and values that are not safely reloadable.
|
|
||||||
- V4.3 exposes only purpose-built forms backed by explicit validation and service methods.
|
|
||||||
|
|
||||||
### C. Prompt Editing Is Constrained
|
|
||||||
|
|
||||||
- Prompt maintenance is limited to direct children of the configured prompt directory.
|
|
||||||
- Existing Job provenance is never rewritten when a prompt file changes.
|
|
||||||
- The UI must distinguish editing the default for future submissions from inspecting historical Job prompts.
|
|
||||||
|
|
||||||
### D. Prompt Writes Are Atomic and Recoverable
|
|
||||||
|
|
||||||
- Writes use a sibling temporary file and atomic replacement so readers observe either the old or new complete prompt.
|
|
||||||
- The immediately previous prompt version is retained as the sole backup.
|
|
||||||
- Recovery is an explicit operator action and uses the same validated safe-write path.
|
|
||||||
- Prompt creation and deletion are not available in V4.3.
|
|
||||||
|
|
||||||
### E. Person Role Ordering Is Deterministic, Not Persisted
|
|
||||||
|
|
||||||
- Person Roles are ordered by label and then stable code.
|
|
||||||
- V4.3 does not add a `sort_order` field to Person Roles.
|
|
||||||
- Document Types use alphabetical label ordering and have no persisted sort order.
|
|
||||||
|
|
||||||
### F. Settings Are Installation-Local
|
|
||||||
|
|
||||||
- V4.3 provides Settings through the local application UI and domain services only.
|
|
||||||
- No settings API surface is introduced.
|
|
||||||
|
|
||||||
## Data and Compatibility Policy
|
|
||||||
|
|
||||||
- V4.3 does not rewrite existing Documents, document-person links, Jobs, Sources, execution evidence, or prompt provenance.
|
|
||||||
- Deactivation preserves referenced registry entries for historical display while excluding them from default create selectors.
|
|
||||||
- Deletion checks are performed at the service boundary and must fail deterministically when references exist.
|
|
||||||
- Prompt files are constrained to existing Markdown files that are direct children of the configured prompt root.
|
|
||||||
- Settings UI code performs no direct database, environment-file, or arbitrary filesystem mutations.
|
|
||||||
- Source page numbering and ordering behavior is unchanged.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. Document Type UUID identity and Person Role stable codes preserve historical references.
|
|
||||||
2. Inactive registry entries remain visible on historical records but are excluded from default create selectors.
|
|
||||||
3. Labels can be changed for referenced or unreferenced registry entries.
|
|
||||||
4. An unreferenced Document Type or Person Role can be deleted, while deletion of a referenced entry fails without partial mutation.
|
|
||||||
5. Document Types use alphabetical label ordering; Person Roles use deterministic label/code ordering.
|
|
||||||
6. Prompt edits are restricted to existing Markdown files directly beneath the configured prompt directory.
|
|
||||||
7. Prompt saves use atomic replacement, retain exactly one previous-version backup, and support explicit recovery.
|
|
||||||
8. A prompt edit affects future Jobs only and leaves stored Job provenance unchanged.
|
|
||||||
9. No Settings page exposes secrets, unrestricted filesystem access, or a settings API.
|
|
||||||
10. Focused service and UI tests pass without regressing V4.1 or V4.2 workflows.
|
|
||||||
11. Database, integration, and UI tests use confirmed isolated test data and never modify `data/transcription.db`; potentially destructive tests run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Scope Freeze Gate
|
|
||||||
|
|
||||||
V4.3 is sufficiently frozen to begin implementation:
|
|
||||||
|
|
||||||
- V4.2 is the completed behavioral baseline.
|
|
||||||
- Registry lifecycle and ordering behavior are resolved.
|
|
||||||
- Prompt lifecycle, atomic-write, backup, and recovery behavior are resolved.
|
|
||||||
- The installation-local deployment boundary is resolved.
|
|
||||||
- The implementation plan is a committed delivery plan.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.3 Implementation Plan](implementation_plan_v4_3.md)
|
|
||||||
- [V4.2 Scope Boundary](../ver4.2/scope_boundary_v4_2.md)
|
|
||||||
- [V4.1 Scope Boundary](../ver4.1/scope_boundary_v4_1.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.4)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Deliver hidden semantic identity for built-in registries, a single atomic Linked People workflow, and safe browser-native printing of archival Documents and their current transcriptions.
|
|
||||||
|
|
||||||
## Planning Constraints
|
|
||||||
|
|
||||||
- V4.3 is the completed implementation baseline.
|
|
||||||
- V4.4 may replace V4/V4.3 registry and document-person contracts only as specified by the V4.4 scope.
|
|
||||||
- Semantic keys are internal and immutable; UI and public API contracts use UUIDs and labels.
|
|
||||||
- Document and link edits must not partially commit.
|
|
||||||
- Print output must not execute stored text or expose machine-local source paths.
|
|
||||||
- Source page reordering remains excluded.
|
|
||||||
- Database, integration, and UI tests must use confirmed isolated data and never modify `data/transcription.db`.
|
|
||||||
- Potentially destructive tests must run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Models and schema bootstrap | Add nullable unique semantic keys, simplify document-person identity, and seed frozen built-ins. |
|
|
||||||
| Document service | Maintain built-in Document Types, usage summaries, UUID assignment, and atomic Document/link writes. |
|
|
||||||
| People service | Maintain built-in Person Roles, link summaries, UUID-only role assignment, and one-person-per-document enforcement. |
|
|
||||||
| V4 document API | Remove role-code selectors and compatibility role fields; enforce UUID-only relationship writes. |
|
|
||||||
| Settings UI | Use matching table workflows for Document Types and Person Roles. |
|
|
||||||
| Document Create/Edit | Replace role-specific multiselects with one staged Linked People table and inline editor. |
|
|
||||||
| Document Detail/printing | Add format selection, print preview, safe Source media rendering, print CSS, and job metadata. |
|
|
||||||
| Tests and documentation | Replace superseded cardinality/identity assertions and add isolated registry, editor, transaction, and print coverage. |
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Align Durable Registry Contracts
|
|
||||||
|
|
||||||
- Add nullable, unique `semantic_key` fields to `DocumentType` and `PersonRole`.
|
|
||||||
- Keep UUIDs as primary and foreign-key identity.
|
|
||||||
- Add normalized-label storage and uniqueness to Person Roles using the same trim and case-normalization policy as Document Types.
|
|
||||||
- Remove the user-created Person Role code contract.
|
|
||||||
- Define built-in detection as `semantic_key is not null`.
|
|
||||||
- Centralize the frozen built-in definitions in one domain-owned location.
|
|
||||||
- Seed six Document Types and three Person Roles idempotently.
|
|
||||||
- Ensure label edits never change semantic keys.
|
|
||||||
- Reject deletion of every built-in before checking references.
|
|
||||||
- Continue blocking deletion of referenced custom entries.
|
|
||||||
- Return deterministic validation, conflict, dependency, and not-found errors through existing error categories.
|
|
||||||
|
|
||||||
### 2. Establish the Clean Schema
|
|
||||||
|
|
||||||
- Remove legacy `DocumentPerson.role` compatibility storage and the fixed `DocumentPersonRole` enum.
|
|
||||||
- Make `DocumentPerson.role_id` required.
|
|
||||||
- Replace role-specific uniqueness with a unique `(document_id, person_id)` constraint.
|
|
||||||
- Remove obsolete Document Type and Person Role migration paths that exist only for disposable development data.
|
|
||||||
- Keep fresh schema creation and built-in seeding portable across SQLite and PostgreSQL.
|
|
||||||
- Make configured development-database recreation a separate operator-confirmed step that displays the resolved target path rather than assuming `app.db` or `data/transcription.db`.
|
|
||||||
- Never invoke recreation from application startup or test setup.
|
|
||||||
- Add isolated schema tests for fresh creation, seed idempotence, semantic-key uniqueness, normalized-label uniqueness, required roles, and link uniqueness.
|
|
||||||
|
|
||||||
### 3. Refine Registry Services and API Contracts
|
|
||||||
|
|
||||||
- Add summary queries for Document counts and Person Role link counts without per-row queries.
|
|
||||||
- Order both registries by normalized label with UUID as a deterministic tie-breaker.
|
|
||||||
- Expose built-in status as a derived read value where the Settings UI needs it.
|
|
||||||
- Keep semantic-key lookup behind service methods for application-owned behavior such as resolving authors.
|
|
||||||
- Ensure create operations always produce custom entries with null semantic keys.
|
|
||||||
- Ensure update operations accept only label and active state.
|
|
||||||
- Remove `role_code` request alternatives and compatibility role responses from the V4 document API.
|
|
||||||
- Require `role_id` for document-person creation and updates.
|
|
||||||
- Add service and API tests for hidden semantic identity, relabeling, activation, built-in protection, custom deletion, counts, ordering, UUID-only writes, and conflicts.
|
|
||||||
|
|
||||||
### 4. Build Matching Settings Tables
|
|
||||||
|
|
||||||
- Retain the existing Document Types table workflow and add the Built-in column.
|
|
||||||
- Replace the current per-row Person Role controls with the same selection-based table pattern.
|
|
||||||
- Render the agreed columns and usage counts.
|
|
||||||
- Keep labels as the only registry text shown in selectors.
|
|
||||||
- Add creates custom entries only.
|
|
||||||
- Edit dialogs expose label and active state only.
|
|
||||||
- Delete reports protected-built-in and referenced-custom conflicts clearly.
|
|
||||||
- Avoid direct persistence queries from the Settings page.
|
|
||||||
- Add component-level UI assertions for columns, actions, label-only selectors, and immutable built-in presentation.
|
|
||||||
|
|
||||||
### 5. Add a Staged Linked People Editor
|
|
||||||
|
|
||||||
- Introduce a small typed UI-state model for staged `(person_id, role_id)` rows rather than storing raw widget values.
|
|
||||||
- Share the editor component between Create Document and Edit Document.
|
|
||||||
- Render a multi-selection table with Person and Role labels.
|
|
||||||
- Add an inline editor whose mode is explicitly Add or Edit.
|
|
||||||
- Disable already-linked People when adding; retain the edited Person as an option during Edit.
|
|
||||||
- Require exactly one row for Edit and allow one or more rows for Delete.
|
|
||||||
- Save and Delete mutate only staged UI state.
|
|
||||||
- Cancel discards only the active inline edit.
|
|
||||||
- Preserve inactive-role historical rows in Edit while restricting new assignments and changes to active roles.
|
|
||||||
- Preserve `person_id` preselection by staging that Person with the active built-in `author` role, with warning behavior for invalid or unavailable selections.
|
|
||||||
- Preserve the `return_to=jobs_new` success path.
|
|
||||||
- Keep navigation to Person creation separate; V4.4 does not add an embedded Person editor.
|
|
||||||
- Add UI tests for staging, duplicate prevention, selection rules, inactive roles, cancel behavior, and both Document forms.
|
|
||||||
|
|
||||||
### 6. Persist Document and Links Atomically
|
|
||||||
|
|
||||||
- Add service commands for Create Document with complete links and Update Document with complete links.
|
|
||||||
- Validate Document Type, every Person, every Person Role, active assignment rules, and duplicate People before mutation.
|
|
||||||
- Compute deterministic add, update, and remove deltas for Edit.
|
|
||||||
- Apply Document and link mutations in one database transaction and commit once.
|
|
||||||
- Roll back the complete operation on any validation, conflict, or persistence failure.
|
|
||||||
- Return the persisted Document detail required by the UI after success.
|
|
||||||
- Reuse these commands from UI orchestration rather than sequencing independent service commits.
|
|
||||||
- Add failure-injection tests proving no partial Document or link mutation survives.
|
|
||||||
|
|
||||||
### 7. Define a Print Projection
|
|
||||||
|
|
||||||
- Add a read-only service projection containing:
|
|
||||||
- Document title and selected archival metadata.
|
|
||||||
- Authors resolved by the `author` semantic key.
|
|
||||||
- Notes.
|
|
||||||
- Ordered Sources with application media URLs and current transcription text.
|
|
||||||
- Ordered Job metadata.
|
|
||||||
- Load the projection with bounded queries and deterministic ordering.
|
|
||||||
- Use non-null `revised_text`, including an intentionally empty revision; otherwise fall back to `raw_transcription`.
|
|
||||||
- Map empty or whitespace-only current text to the explicit unavailable state without falling back past an intentional revision.
|
|
||||||
- Represent unavailable text and optional metadata explicitly.
|
|
||||||
- Do not expose semantic keys, direct file paths, full prompts, provider evidence, or raw API responses.
|
|
||||||
- Keep the projection independent of NiceGUI rendering so formatting tests can use plain typed values.
|
|
||||||
|
|
||||||
### 8. Build Print Preview and Styles
|
|
||||||
|
|
||||||
- Add a Print action to Document Detail.
|
|
||||||
- Open a dedicated persisted-Document print route with a Facsimile/Text-only format choice.
|
|
||||||
- Render the exact content order frozen in the scope.
|
|
||||||
- Keep print metadata tables content-sized, with a non-wrapping label column and wider wrapping value columns.
|
|
||||||
- Render stored Notes and transcription as escaped text.
|
|
||||||
- For Text-only mode, normalize whitespace by joining single line breaks inside paragraphs while preserving blank-line paragraph boundaries.
|
|
||||||
- For Facsimile mode, preserve line breaks and use a two-column Source layout.
|
|
||||||
- Start each Facsimile Source on a new printed sheet with CSS page breaks.
|
|
||||||
- Allow long transcription content to continue rather than clipping it.
|
|
||||||
- Fetch images through an application-controlled Source media route.
|
|
||||||
- Add print-only CSS that hides navigation, controls, and non-document chrome.
|
|
||||||
- Invoke the browser print dialog only from an explicit user action.
|
|
||||||
- Add rendering tests for both modes, missing data, long text, special characters, image URLs, and page ordering.
|
|
||||||
|
|
||||||
### 9. Align Documentation and Verification
|
|
||||||
|
|
||||||
- Update V4 architecture, requirements, schema, and Document UI contracts to reflect:
|
|
||||||
- UUID plus hidden semantic-key registries.
|
|
||||||
- Built-in protection.
|
|
||||||
- One Person per Document.
|
|
||||||
- UUID-only role API writes.
|
|
||||||
- Atomic Document/link synchronization.
|
|
||||||
- Browser-native print projection and formats.
|
|
||||||
- Confirm every database, integration, and UI test target is isolated before execution.
|
|
||||||
- Run focused registry and service tests first.
|
|
||||||
- Run schema tests only through the destructive-test wrapper when they are potentially destructive.
|
|
||||||
- Run Linked People UI and print rendering tests against isolated fixtures.
|
|
||||||
- Run the broader non-external regression suite after focused coverage passes.
|
|
||||||
- Verify that `data/transcription.db` was not changed by test execution.
|
|
||||||
|
|
||||||
## Delivery Order
|
|
||||||
|
|
||||||
1. Registry and clean-schema contracts.
|
|
||||||
2. Registry services, API changes, and Settings tables.
|
|
||||||
3. Atomic Document/link service commands.
|
|
||||||
4. Shared staged Linked People editor.
|
|
||||||
5. Print projection.
|
|
||||||
6. Print preview and styles.
|
|
||||||
7. Documentation alignment and regression verification.
|
|
||||||
|
|
||||||
## Done Criteria
|
|
||||||
|
|
||||||
- All V4.4 acceptance criteria are implemented and testable.
|
|
||||||
- UI and API contracts use UUID identity and never expose semantic keys.
|
|
||||||
- Built-in registries retain meaning after relabeling and cannot be deleted.
|
|
||||||
- Custom registries retain reference-aware deletion.
|
|
||||||
- Both Document forms use one Linked People table.
|
|
||||||
- One Person cannot be linked twice to the same Document.
|
|
||||||
- Main Document saves are atomic across fields and relationships.
|
|
||||||
- Print preview provides both frozen formats and content sections.
|
|
||||||
- Print output uses current human-preferred text, deterministic ordering, escaped content, and application media URLs.
|
|
||||||
- Job metadata lists every Job oldest-to-newest and ends with Status.
|
|
||||||
- Source page reordering and server-generated PDFs are not introduced.
|
|
||||||
- Verification uses isolated data and does not modify `data/transcription.db`.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.4 Scope Boundary](scope_boundary_v4_4.md)
|
|
||||||
- [V4.3 Scope Boundary](../ver4.3/scope_boundary_v4_3.md)
|
|
||||||
- [V4.3 Implementation Plan](../ver4.3/implementation_plan_v4_3.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [V4 Error Handling Policy](../ver4/error_handling_v4.md)
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
# V4.4 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the frozen boundary for the semantic-registry, linked-people, and document-printing revision that follows the completed V4.3 Settings work. V4 through V4.3 remain the architecture and behavioral baseline except where this document explicitly replaces a registry or document-person contract.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Keep registry identifiers stable without exposing duplicate machine codes in Settings tables or selectors.
|
|
||||||
- Replace role-specific person selectors with one coherent Linked People editor.
|
|
||||||
- Provide an archival print view containing document metadata, source pages, current transcription text, and transcription-job metadata.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Semantic Registry Identity
|
|
||||||
|
|
||||||
- `DocumentType` and `PersonRole` use UUIDs as their canonical record and relationship identity.
|
|
||||||
- Both registries may carry a nullable, unique, immutable `semantic_key` used only for application-defined built-ins.
|
|
||||||
- Semantic keys are internal implementation details. Settings tables, selectors, and public API payloads do not display or accept them.
|
|
||||||
- Labels are trimmed, case-insensitively unique, editable, and used for all user-facing display.
|
|
||||||
- Active state remains editable. Inactive entries remain valid for historical records and are excluded from default assignment selectors.
|
|
||||||
- Built-in status is derived from the presence of a semantic key and is displayed as a read-only Yes/No value.
|
|
||||||
- Built-in entries cannot be deleted or converted to custom entries.
|
|
||||||
- Custom entries have no semantic key and may be deleted only when unreferenced.
|
|
||||||
- Users may create custom entries but cannot create, change, or assign semantic keys through the UI or API.
|
|
||||||
|
|
||||||
### 2. Built-In Document Types
|
|
||||||
|
|
||||||
- Seed these built-in semantic keys and initial labels:
|
|
||||||
|
|
||||||
| Semantic key | Initial label |
|
|
||||||
| --- | --- |
|
|
||||||
| `book` | Book |
|
|
||||||
| `letter` | Letter |
|
|
||||||
| `postcard` | Postcard |
|
|
||||||
| `photo` | Photo |
|
|
||||||
| `journal` | Journal |
|
|
||||||
| `form` | Form |
|
|
||||||
|
|
||||||
- The Document Types Settings table contains Select, Label, Documents, Active, and Built-in columns.
|
|
||||||
- Document Types are ordered alphabetically by normalized label.
|
|
||||||
- Add, Edit, and Delete actions operate on table selection.
|
|
||||||
- Add creates a custom type. Edit changes only label and active state.
|
|
||||||
- The Documents count is the number of Documents referencing the type.
|
|
||||||
- Document Type selectors display labels only and submit UUIDs.
|
|
||||||
|
|
||||||
### 3. Built-In Person Roles
|
|
||||||
|
|
||||||
- Seed these built-in semantic keys and initial labels:
|
|
||||||
|
|
||||||
| Semantic key | Initial label |
|
|
||||||
| --- | --- |
|
|
||||||
| `author` | Author |
|
|
||||||
| `recipient` | Recipient |
|
|
||||||
| `mentioned` | Mentioned |
|
|
||||||
|
|
||||||
- Application behavior that requires authorship resolves the built-in `author` semantic key rather than matching a mutable label.
|
|
||||||
- The Person Roles Settings table contains Select, Label, Links, Active, and Built-in columns.
|
|
||||||
- Person Roles are ordered alphabetically by normalized label.
|
|
||||||
- Add, Edit, and Delete actions operate on table selection.
|
|
||||||
- Add creates a custom role. Edit changes only label and active state.
|
|
||||||
- The Links count is the number of document-person relationships referencing the role.
|
|
||||||
- Person Role selectors display labels only and submit UUIDs.
|
|
||||||
|
|
||||||
### 4. Linked People Editor
|
|
||||||
|
|
||||||
- Replace the separate role-specific person selectors on both Create Document and Edit Document with one Linked People table.
|
|
||||||
- The table contains Select, Person, and Role columns.
|
|
||||||
- Add opens an inline editor beneath the table with Person and Person Role selectors.
|
|
||||||
- Edit requires exactly one selected row and loads it into the inline editor.
|
|
||||||
- Save stages the inline addition or edit in the table.
|
|
||||||
- Cancel exits the inline editor without changing the staged link set.
|
|
||||||
- Delete stages removal of one or more selected rows.
|
|
||||||
- A Person may be linked to a Document only once, regardless of role.
|
|
||||||
- Every link has exactly one Person Role.
|
|
||||||
- Already-linked People are unavailable when adding another row.
|
|
||||||
- Existing links using inactive roles remain visible and unchanged unless explicitly edited.
|
|
||||||
- Only active roles are available for new links or role changes.
|
|
||||||
- Create Document preserves the existing `person_id` preselection workflow by staging that Person with the active built-in `author` role. An invalid Person or unavailable Author role produces a warning rather than an invalid link.
|
|
||||||
- Create Document preserves the existing `return_to=jobs_new` success path.
|
|
||||||
- Linked People changes remain staged until the main Create Document or Save Changes action.
|
|
||||||
- The Document and its complete staged link set are persisted atomically. A conflict or validation failure leaves both unchanged.
|
|
||||||
- The API and service contracts identify roles by `role_id`; role-code selectors and compatibility role strings are removed.
|
|
||||||
- Persistence enforces uniqueness on `(document_id, person_id)`.
|
|
||||||
|
|
||||||
### 5. Document Print View
|
|
||||||
|
|
||||||
- Add a Print action to Document Detail.
|
|
||||||
- The action opens a dedicated print-preview page for the persisted Document.
|
|
||||||
- The preview offers two formats:
|
|
||||||
- **Facsimile:** source image on the left and current transcription on the right. Original transcription line breaks are preserved, and each Source begins on a new printed sheet.
|
|
||||||
- **Text only:** no source images. Single line breaks inside a paragraph are reflowed as spaces, while blank-line paragraph boundaries remain.
|
|
||||||
- Both formats use browser printing through a dedicated print stylesheet and the browser print dialog.
|
|
||||||
- Server-generated PDF files are not part of V4.4; users may select the browser's Save as PDF destination.
|
|
||||||
- Sources are ordered by existing `page_number`, with UUID as a deterministic tie-breaker.
|
|
||||||
- The current transcription for each Source is the non-null `revised_text`, including an intentionally empty revision, otherwise the latest successful machine-output projection in `raw_transcription`.
|
|
||||||
- Empty or whitespace-only current text displays the explicit unavailable message rather than falling back past an intentional revision.
|
|
||||||
- A Source with no current transcription displays an explicit unavailable message.
|
|
||||||
- Transcription and Notes content is treated as text and escaped; model output is not executed as arbitrary HTML.
|
|
||||||
- Facsimile images use an application-controlled Source media route. Generated markup does not expose direct machine-local file paths.
|
|
||||||
|
|
||||||
### 6. Printed Content Contract
|
|
||||||
|
|
||||||
The print view contains, in this order:
|
|
||||||
|
|
||||||
1. Document title using the Document name.
|
|
||||||
2. Archival Metadata table:
|
|
||||||
- Author, containing People linked through the built-in `author` role.
|
|
||||||
- Document Type.
|
|
||||||
- Date.
|
|
||||||
- Location Created.
|
|
||||||
- Archival Identifier.
|
|
||||||
3. Notes.
|
|
||||||
4. Document section containing one numbered section per Source.
|
|
||||||
5. Transcription Job Metadata table.
|
|
||||||
|
|
||||||
Empty metadata values remain visible as `Not set`. Empty Notes display `No notes recorded`.
|
|
||||||
|
|
||||||
The job metadata table:
|
|
||||||
|
|
||||||
- Lists field names in the first column and adds one column for every Job associated with the Document.
|
|
||||||
- Orders Job columns from oldest to newest by creation date, then UUID.
|
|
||||||
- Includes every Job status: `queued`, `processing`, `transcribed`, `completed`, `partial_success`, and `failed`.
|
|
||||||
- Contains these rows in order:
|
|
||||||
- Job ID.
|
|
||||||
- Date, using the Job creation/submission timestamp with timezone.
|
|
||||||
- Provider.
|
|
||||||
- Model.
|
|
||||||
- Prompt, using the frozen prompt filename/name rather than full prompt content.
|
|
||||||
- Retry Count.
|
|
||||||
- Status as the final row.
|
|
||||||
- Displays `Not set` for unavailable optional metadata.
|
|
||||||
|
|
||||||
### 7. Clean Development Schema
|
|
||||||
|
|
||||||
- V4.4 does not require preservation or migration of rows in the operator-configured development database.
|
|
||||||
- Implementation may recreate the configured development database, including `data/transcription.db` when it is the explicitly selected target, only through a separate operator-confirmed action that identifies the resolved path. Startup and test execution never delete it automatically.
|
|
||||||
- Fresh schema creation seeds the agreed built-in Document Types and Person Roles idempotently.
|
|
||||||
- No test may use, modify, replace, or restore `data/transcription.db`.
|
|
||||||
- Database, integration, and UI tests use confirmed isolated databases.
|
|
||||||
- Potentially destructive tests run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- User creation, editing, deletion, or direct display of semantic keys.
|
|
||||||
- Treating custom registry entries as built-ins.
|
|
||||||
- Additional built-in Document Types or Person Roles beyond the frozen lists.
|
|
||||||
- Assigning more than one role to the same Person on the same Document.
|
|
||||||
- Preserving multiple historical links that violate the new one-person-per-document constraint.
|
|
||||||
- Source page renumbering or reordering.
|
|
||||||
- Printing unsaved Create/Edit Document state.
|
|
||||||
- Print actions on Job Detail or other pages.
|
|
||||||
- Batch printing multiple Documents.
|
|
||||||
- Server-side PDF generation or PDF file storage.
|
|
||||||
- Markdown, DOCX, or evidence-package export through the print feature.
|
|
||||||
- User-editable print templates, fonts, margins, headers, or footers.
|
|
||||||
- Full frozen prompt content, prompt hashes, transport evidence, API responses, or execution-attempt details in the print footer.
|
|
||||||
- Rendering transcription text as unrestricted Markdown or HTML.
|
|
||||||
- Pixel-identical pagination across browsers and printer drivers.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. UUID Identifies the Row; Semantic Key Identifies Built-In Meaning
|
|
||||||
|
|
||||||
- UUIDs remain the only relationship and API identity.
|
|
||||||
- A hidden semantic key permits reliable built-in behavior after a label is renamed.
|
|
||||||
- Mutable labels are never used to infer built-in meaning.
|
|
||||||
|
|
||||||
### B. Built-Ins Are Protected but Mutable in Presentation
|
|
||||||
|
|
||||||
- Built-in labels and active state may change.
|
|
||||||
- Built-in semantic identity cannot change, and built-ins cannot be deleted.
|
|
||||||
- Custom entries remain reference-aware and deletable when unreferenced.
|
|
||||||
|
|
||||||
### C. Linked People Is a Single-Role Relationship
|
|
||||||
|
|
||||||
- One `(document_id, person_id)` row represents the complete relationship.
|
|
||||||
- Changing a role updates that row rather than adding another relationship.
|
|
||||||
- The main Document save owns one atomic Document-and-links transaction.
|
|
||||||
|
|
||||||
### D. Printing Uses the Current Human-Preferred Text
|
|
||||||
|
|
||||||
- Human-revised text takes precedence over the latest successful machine-output projection.
|
|
||||||
- Job metadata provides processing context but does not claim that a later human revision is raw output from a listed Job.
|
|
||||||
|
|
||||||
### E. Printing Is Browser-Native
|
|
||||||
|
|
||||||
- A print-specific HTML view and CSS support physical printing and browser Save as PDF.
|
|
||||||
- Source media is served through application-controlled routes, and all textual content is escaped.
|
|
||||||
|
|
||||||
### F. Source Order Is Read-Only in V4.4
|
|
||||||
|
|
||||||
- Print order follows existing page numbers.
|
|
||||||
- Source page reordering remains explicitly excluded.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. Registry selectors and Settings forms never display a machine code or semantic key.
|
|
||||||
2. Document Types and Person Roles use UUID relationship identity and case-insensitively unique labels.
|
|
||||||
3. The six Document Type and three Person Role built-ins are seeded with immutable internal semantic keys.
|
|
||||||
4. Built-ins may be relabeled or disabled but cannot be deleted.
|
|
||||||
5. Unreferenced custom entries may be deleted; referenced custom entries may only be relabeled or disabled.
|
|
||||||
6. Settings tables show the agreed columns, alphabetical label order, usage counts, and selection-based actions.
|
|
||||||
7. Create and Edit Document use one Linked People table with inline staged Add/Edit/Save/Cancel and multi-row Delete.
|
|
||||||
8. The same Person cannot be staged or persisted twice for one Document, even under different roles.
|
|
||||||
9. Document fields and Linked People changes commit atomically.
|
|
||||||
10. Historical inactive roles remain displayable, while only active roles are assignable.
|
|
||||||
11. Document Detail opens a print preview with Facsimile and Text-only formats.
|
|
||||||
12. Print pages use current revised text when available and deterministic Source ordering.
|
|
||||||
13. Printed archival metadata resolves authors through the hidden `author` semantic key after any label change.
|
|
||||||
14. The job table contains one oldest-to-newest column per Job and ends with the Status row.
|
|
||||||
15. Print output escapes stored text and does not disclose direct local source paths.
|
|
||||||
16. Source reordering, server PDF generation, and print-template editing are absent.
|
|
||||||
17. Database, integration, and UI verification uses isolated test data and never modifies `data/transcription.db`.
|
|
||||||
|
|
||||||
## Scope Freeze Gate
|
|
||||||
|
|
||||||
V4.4 is sufficiently frozen to begin implementation:
|
|
||||||
|
|
||||||
- Built-in registry identity, membership, lifecycle, display, and selector behavior are resolved.
|
|
||||||
- Linked People selection, editing, uniqueness, inactive-role, staging, and transaction behavior are resolved.
|
|
||||||
- Print entry point, formats, content order, transcription precedence, page order, job metadata, and output mechanism are resolved.
|
|
||||||
- Clean development-schema and destructive-test boundaries are resolved.
|
|
||||||
- Source page reordering remains excluded.
|
|
||||||
|
|
||||||
Any expansion of the built-in catalogs, relationship cardinality, print formats, export formats, or print customization requires an explicit V4.4 scope amendment or a later revision.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.4 Implementation Plan](implementation_plan_v4_4.md)
|
|
||||||
- [V4.3 Scope Boundary](../ver4.3/scope_boundary_v4_3.md)
|
|
||||||
- [V4.3 Implementation Plan](../ver4.3/implementation_plan_v4_3.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.5)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Normalize metadata-directed image orientation for provider input, improve transcription-medium instructions and deterministic quality warnings, and support user-initiated single-Source retranscription with approved alternate models and explicit candidate promotion.
|
|
||||||
|
|
||||||
## Planning Status
|
|
||||||
|
|
||||||
- V4.4 is the completed implementation baseline.
|
|
||||||
- The V4.5 scope is frozen and sufficiently detailed to begin implementation.
|
|
||||||
- Scope additions require an explicit amendment or a later revision.
|
|
||||||
|
|
||||||
## Planning Constraints
|
|
||||||
|
|
||||||
- Original uploaded Source files remain immutable.
|
|
||||||
- Provider input must remain traceable to the original Source and any normalized derivative.
|
|
||||||
- Normalization is limited to recognized orientation metadata; no enhancement pipeline is introduced.
|
|
||||||
- Quality warnings never mutate transcription text or trigger paid requests automatically.
|
|
||||||
- Retranscription creates new immutable Job and execution evidence.
|
|
||||||
- A retranscription result remains a candidate until explicitly promoted.
|
|
||||||
- Human revision remains separate from and takes precedence over machine selection.
|
|
||||||
- Provider work occurs outside database transactions.
|
|
||||||
- Database, integration, and UI tests use confirmed isolated data and never modify `data/transcription.db`.
|
|
||||||
- Potentially destructive tests run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Configuration | Add a validated provider-model allowlist while retaining one default model. |
|
|
||||||
| Image processing | Add metadata-directed orientation normalization and model-input artifact creation. |
|
|
||||||
| Evidence model | Link each request to the exact model-input artifact and record preferred machine-attempt provenance. |
|
|
||||||
| Prompt contract | Add explicit body-medium classification and structured-layout rules. |
|
|
||||||
| Quality service | Add deterministic, non-mutating warnings for known output defects. |
|
|
||||||
| Job creation | Support a Source-locked retranscription Job and approved model selection. |
|
|
||||||
| Worker workflows | Preserve candidates without automatically replacing preferred machine text. |
|
|
||||||
| Source Detail | Add Retranscribe Source, candidate summaries, comparison, promotion, and normalization indicators. |
|
|
||||||
| Tests and documentation | Add isolated normalization, configuration, warning, retranscription, promotion, and UI coverage. |
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Align Configuration Contracts
|
|
||||||
|
|
||||||
- Add `provider_models` as an immutable validated collection in Settings.
|
|
||||||
- Parse `PROVIDER_MODELS` using the standard Pydantic-settings JSON representation.
|
|
||||||
- Preserve `PROVIDER_MODEL` as the default.
|
|
||||||
- If the allowlist is omitted, derive a one-entry list from the default.
|
|
||||||
- Normalize whitespace, reject empty values, and deduplicate while preserving the relative order of non-default values.
|
|
||||||
- Ensure the default appears exactly once and first in the effective selector order.
|
|
||||||
- Validate a submitted model against the allowlist in the service or workflow boundary, not only in the UI.
|
|
||||||
- Document `.env.example` behavior without adding real credentials.
|
|
||||||
- Add configuration tests for omitted, valid, duplicate, malformed, and empty model lists.
|
|
||||||
|
|
||||||
### 2. Define Orientation-Normalized Artifacts
|
|
||||||
|
|
||||||
- Reuse `ProcessingArtifact` for the model-input derivative and transformation metadata.
|
|
||||||
- Define a versioned orientation-normalization artifact schema containing:
|
|
||||||
- Original Source identity and digest.
|
|
||||||
- Original orientation value.
|
|
||||||
- Applied rotation.
|
|
||||||
- Original and derivative dimensions, media types, byte sizes, and digests.
|
|
||||||
- Processor name and version.
|
|
||||||
- Add one domain-owned orientation-normalization service or adapter; keep image-library details out of UI and provider modules.
|
|
||||||
- Apply recognized metadata orientation physically to raster pixels.
|
|
||||||
- Reset or remove orientation metadata on the derivative.
|
|
||||||
- Store derivatives under application-managed artifact storage with safe relative references.
|
|
||||||
- Avoid creating a derivative when no supported transformation is required.
|
|
||||||
- Return a typed provider-input reference that identifies whether the request uses the original or a derivative.
|
|
||||||
- Never mutate or delete the original Source as part of normalization.
|
|
||||||
|
|
||||||
### 3. Integrate Normalization with Provider Input
|
|
||||||
|
|
||||||
- Resolve the exact model input before constructing the request manifest.
|
|
||||||
- Use the normalized derivative when orientation metadata requires it; otherwise use the original Source.
|
|
||||||
- Extend the existing `SourceEvidenceReference` or associated artifact reference so the manifest identifies:
|
|
||||||
- Original Source.
|
|
||||||
- Derivative artifact when present.
|
|
||||||
- Transformation schema and digest.
|
|
||||||
- Ensure normalization completes and persists before provider network work begins.
|
|
||||||
- Bind the immutable model-input artifact reference to the exact ExecutionAttempt that consumed it before terminal attempt persistence completes.
|
|
||||||
- If normalized bytes are reused, retain attempt-specific association while preserving one content identity and digest.
|
|
||||||
- If normalization fails, do not send a provider request.
|
|
||||||
- Preserve safe failure evidence and an actionable error category.
|
|
||||||
- Confirm provider payload loading and evidence hashing read the same resolved bytes.
|
|
||||||
|
|
||||||
### 4. Revise the Transcription Prompt
|
|
||||||
|
|
||||||
- Align the prompt with the durable medium rules in `docs/invariant/transcription_methodology.md`.
|
|
||||||
- Add the four frozen document-body markers.
|
|
||||||
- Define operational differences among handwritten, typewritten, typeset, and mixed content.
|
|
||||||
- State that mechanical typewriter variation is not handwriting.
|
|
||||||
- Require exactly one body marker.
|
|
||||||
- Prohibit repeated whole-line handwriting wrappers after a whole-body handwritten marker.
|
|
||||||
- Permit localized handwriting markers only for actual annotations, signatures, or mixed-body portions.
|
|
||||||
- Add layout instructions for tables of contents, tables, forms, columns, captions, marginalia, page numbers, dotted leaders, and associated references.
|
|
||||||
- Require plain-text characters rather than HTML entities.
|
|
||||||
- Retain verbatim, uncertainty, damage, deletion, insertion, and line-break-hyphenation rules.
|
|
||||||
- Update prompt fixtures and prompt-hash expectations without rewriting historical Job prompt evidence.
|
|
||||||
|
|
||||||
### 5. Add Deterministic Quality Analysis
|
|
||||||
|
|
||||||
- Introduce a small typed warning model with stable warning codes and human-readable detail.
|
|
||||||
- Analyze successful output without modifying it.
|
|
||||||
- Implement warnings for:
|
|
||||||
- Unicode replacement characters.
|
|
||||||
- Multiple document-body markers.
|
|
||||||
- Whole-body handwritten plus repeated line-level handwriting wrappers.
|
|
||||||
- Likely unresolved HTML entities.
|
|
||||||
- Keep warning rules deterministic and provider-independent.
|
|
||||||
- Persist warnings once as an immutable, versioned `ProcessingArtifact` tied to the successful ExecutionAttempt.
|
|
||||||
- Source Detail renders stored warnings and never recomputes historical attempts under newer warning rules.
|
|
||||||
- Make warning analysis idempotent and versioned so newer rules apply only to newly analyzed attempts unless a separate future reanalysis workflow is introduced.
|
|
||||||
- Do not implement confidence scoring or automatic retries.
|
|
||||||
|
|
||||||
### 6. Model Retranscription and Selection State
|
|
||||||
|
|
||||||
- Add a durable Job purpose or equivalent discriminator for normal transcription versus Source retranscription.
|
|
||||||
- Ensure a retranscription Job contains exactly one JobSource for the locked Source.
|
|
||||||
- Add durable preferred-machine-attempt provenance for each Source.
|
|
||||||
- Retain `Source.raw_transcription` as the preferred-machine-text projection for compatibility.
|
|
||||||
- Define legacy behavior for Sources whose current projection predates ExecutionAttempt provenance.
|
|
||||||
- On the first successful result with no preferred machine output, select the successful attempt automatically regardless of Job purpose.
|
|
||||||
- Once preferred provenance exists, preserve every later successful result as an unselected candidate regardless of Job purpose.
|
|
||||||
- Prevent normal and retranscription workflows from writing `Source.raw_transcription` directly when preferred provenance already exists.
|
|
||||||
- Add a promotion command that:
|
|
||||||
- Loads the Source and successful ExecutionAttempt.
|
|
||||||
- Verifies ownership and successful text.
|
|
||||||
- Updates preferred-attempt provenance and `Source.raw_transcription` in one transaction.
|
|
||||||
- Leaves `Source.revised_text` and all attempts unchanged.
|
|
||||||
- Reject failed, unrelated, missing, or textless candidates deterministically.
|
|
||||||
|
|
||||||
### 7. Add the Retranscribe Source Workflow
|
|
||||||
|
|
||||||
- Add a Source Detail **Retranscribe Source** action.
|
|
||||||
- Navigate to Create Processing Job with an explicit `source_id` query parameter.
|
|
||||||
- Load the Source and derive its Document server-side.
|
|
||||||
- Render Source identity and filename as locked context.
|
|
||||||
- Render Provider from Settings as read-only.
|
|
||||||
- Render Model as a selector using the effective allowlist and default.
|
|
||||||
- Reuse the frozen default prompt unless a later scope addition explicitly allows prompt selection.
|
|
||||||
- Create a new queued retranscription Job and one pending JobSource atomically.
|
|
||||||
- Notify the worker only after the transaction commits.
|
|
||||||
- Preserve current preferred machine text and human revision throughout queueing, processing, success, and failure.
|
|
||||||
- Return to the new Job Detail after successful creation.
|
|
||||||
|
|
||||||
### 8. Adapt Worker Success Semantics
|
|
||||||
|
|
||||||
- Select the first successful result automatically for Sources without preferred machine output, including a successful retranscription after earlier failures.
|
|
||||||
- For every later success, persist JobSource and ExecutionAttempt text without replacing the Source projection, regardless of normal or retranscription Job purpose.
|
|
||||||
- Run deterministic quality analysis after successful normalization of provider output.
|
|
||||||
- Persist candidate warnings with the attempt.
|
|
||||||
- Keep all terminal Job status and execution evidence updates atomic according to the existing workflow boundary.
|
|
||||||
- Ensure a failed retranscription cannot clear or change preferred machine text.
|
|
||||||
|
|
||||||
### 9. Build Candidate Review and Promotion UI
|
|
||||||
|
|
||||||
- Extend Source Detail with concise machine-output sections:
|
|
||||||
- Preferred machine transcription.
|
|
||||||
- Human revision.
|
|
||||||
- Candidate machine transcriptions.
|
|
||||||
- List candidates with creation date, provider, model, Job ID, status, and warning indicator.
|
|
||||||
- Default to a compact candidate list; do not render every full transcript simultaneously.
|
|
||||||
- When no successful machine result exists, render an explicit empty state without comparison controls.
|
|
||||||
- When preferred output exists without candidates, render an explicit no-candidates state.
|
|
||||||
- Allow one candidate to be opened for comparison with the preferred machine transcription.
|
|
||||||
- Label both sides with provider, model, Job ID, and date.
|
|
||||||
- Add **Use this transcription** only for a successful unselected candidate.
|
|
||||||
- Require explicit confirmation before promotion.
|
|
||||||
- After promotion, refresh Source Detail and retain the former preferred result in attempt history.
|
|
||||||
- If a human revision exists, explain that promotion changes machine selection but not the human-preferred displayed/printed text.
|
|
||||||
- When orientation normalization occurred, show a compact indicator and link to transformation evidence; do not require routine side-by-side image display.
|
|
||||||
|
|
||||||
### 10. Align API and Service Contracts
|
|
||||||
|
|
||||||
- Keep arbitrary provider and model identifiers out of public write contracts.
|
|
||||||
- If an API is added for retranscription, accept Source UUID and one configured model identifier and validate both server-side.
|
|
||||||
- If an API is added for promotion, accept Source UUID and ExecutionAttempt UUID and validate their relationship.
|
|
||||||
- Return stable validation, conflict, not-found, provider, and persistence errors through the existing taxonomy.
|
|
||||||
- Keep filesystem paths, raw credentials, and unrestricted artifact references out of responses.
|
|
||||||
|
|
||||||
### 11. Verification
|
|
||||||
|
|
||||||
- Add pure unit tests for:
|
|
||||||
- Orientation metadata interpretation.
|
|
||||||
- No-op versus transformed input selection.
|
|
||||||
- Derivative metadata and hashing.
|
|
||||||
- Model allowlist normalization.
|
|
||||||
- Prompt marker rules.
|
|
||||||
- Every quality-warning code.
|
|
||||||
- Add isolated service and workflow tests for:
|
|
||||||
- Original Source immutability.
|
|
||||||
- Normalization failure before provider invocation.
|
|
||||||
- Request manifests referencing exact provider-input bytes.
|
|
||||||
- Single-Source retranscription Job creation.
|
|
||||||
- Candidate preservation.
|
|
||||||
- Initial automatic selection.
|
|
||||||
- Explicit candidate promotion.
|
|
||||||
- Atomic rollback on invalid promotion.
|
|
||||||
- Human revision preservation.
|
|
||||||
- Add UI tests for:
|
|
||||||
- Retranscribe Source navigation.
|
|
||||||
- Locked Source and Document context.
|
|
||||||
- Read-only Provider and allowlisted Model selector.
|
|
||||||
- Candidate list, warnings, comparison, confirmation, and promotion.
|
|
||||||
- Normalization indicator without mandatory image comparison.
|
|
||||||
- Use fixed image fixtures with known EXIF orientation and hashes.
|
|
||||||
- Use fake providers only; no focused or regression test sends an external provider request.
|
|
||||||
- Confirm all database, integration, and UI targets use isolated databases.
|
|
||||||
- Run potentially destructive schema tests only through `tools/run_destructive_tests.py`.
|
|
||||||
- Run the broader non-external regression suite after focused coverage passes.
|
|
||||||
- Verify that `data/transcription.db` and curated Source files were not changed by test execution.
|
|
||||||
|
|
||||||
### 12. Align Authoritative Documentation
|
|
||||||
|
|
||||||
- Update V4 architecture, requirements, and schema for:
|
|
||||||
- Original versus model-input artifacts.
|
|
||||||
- Preferred machine-attempt provenance.
|
|
||||||
- Retranscription Job purpose.
|
|
||||||
- Candidate and promotion semantics.
|
|
||||||
- Quality-warning evidence.
|
|
||||||
- Update Source and Job UI contracts after implementation behavior is accepted.
|
|
||||||
- Update the transcription methodology and evidence invariant only where V4.5 establishes a durable cross-version rule.
|
|
||||||
- Keep historical prompt and execution evidence immutable.
|
|
||||||
|
|
||||||
## Delivery Order
|
|
||||||
|
|
||||||
1. Configuration and model allowlist.
|
|
||||||
2. Orientation artifact schema and normalization adapter.
|
|
||||||
3. Provider-input and evidence integration.
|
|
||||||
4. Prompt revision and deterministic warning analysis.
|
|
||||||
5. Retranscription and preferred-attempt persistence.
|
|
||||||
6. Worker candidate semantics.
|
|
||||||
7. Retranscription creation UI.
|
|
||||||
8. Candidate comparison and promotion UI.
|
|
||||||
9. Authoritative documentation and regression verification.
|
|
||||||
|
|
||||||
## Done Criteria
|
|
||||||
|
|
||||||
- All frozen V4.5 acceptance criteria are implemented and testable.
|
|
||||||
- EXIF-oriented images are physically upright for provider processing while originals remain byte-for-byte unchanged.
|
|
||||||
- Every provider request identifies its exact original and normalized inputs.
|
|
||||||
- The prompt classifies body medium consistently and avoids redundant handwriting wrappers.
|
|
||||||
- Quality defects produce deterministic warnings without silent rewriting or automatic cost.
|
|
||||||
- Source Detail can create one-Source retranscription Jobs using configured alternate models.
|
|
||||||
- Retranscription results remain candidates until explicitly promoted.
|
|
||||||
- Promotion updates exact preferred-attempt provenance and the compatibility projection atomically.
|
|
||||||
- Human revisions remain unchanged and retain display/print precedence.
|
|
||||||
- Previous Jobs and attempts remain immutable and inspectable.
|
|
||||||
- No manual image editor, visual orientation inference, confidence percentage, automatic retry, or arbitrary model entry is introduced.
|
|
||||||
- Verification uses isolated data, fake providers, and does not modify `data/transcription.db` or curated Source files.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.5 Scope Boundary](scope_boundary_v4_5.md)
|
|
||||||
- [V4.4 Scope Boundary](../ver4.4/scope_boundary_v4_4.md)
|
|
||||||
- [V4.4 Implementation Plan](../ver4.4/implementation_plan_v4_4.md)
|
|
||||||
- [V4.2 Evidence and Provenance Scope](../ver4.2/scope_boundary_v4_2.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [V4 Error Handling Policy](../ver4/error_handling_v4.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
- [AI Evidence and Provenance Invariant](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
# V4.5 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the frozen boundary for transcription input normalization and selective transcription-quality improvement after the completed V4.4 revision. V4 through V4.4 remain the architecture and behavioral baseline except where this document explicitly changes Source processing, transcription selection, or Source Detail behavior.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Ensure model inputs are physically upright when curated image files rely on orientation metadata.
|
|
||||||
- Distinguish typewritten, typeset, handwritten, and mixed document bodies consistently.
|
|
||||||
- Let the user selectively retranscribe an unsatisfactory Source with an approved alternate vision model.
|
|
||||||
- Preserve every machine result while allowing the user to choose which result is the preferred machine transcription.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Metadata-Driven Orientation Normalization
|
|
||||||
|
|
||||||
- The original uploaded Source remains immutable archival evidence.
|
|
||||||
- Before a supported raster image is sent to a transcription provider, the application reads recognized orientation metadata.
|
|
||||||
- When the metadata requires rotation, the application creates a physically upright model-input derivative and resets or removes the derivative's orientation metadata.
|
|
||||||
- The provider receives the normalized derivative rather than upside-down stored pixels.
|
|
||||||
- When no supported orientation transformation is required, the original Source may remain the provider input.
|
|
||||||
- The derivative records:
|
|
||||||
- Source UUID.
|
|
||||||
- Original and derivative SHA-256 digests and byte sizes.
|
|
||||||
- Original and derivative dimensions and media types.
|
|
||||||
- Applied orientation transformation.
|
|
||||||
- Transformation implementation and version.
|
|
||||||
- Creation timestamp.
|
|
||||||
- The derivative uses the existing processing-artifact and evidence architecture rather than replacing the Source file.
|
|
||||||
- PDF orientation, visual orientation inference, manual rotation controls, deskewing, cropping, contrast changes, and general image enhancement are not part of V4.5.
|
|
||||||
|
|
||||||
### 2. Transcription Medium Contract
|
|
||||||
|
|
||||||
- The durable document-medium rules are defined in the cross-version [Transcription Methodology](../invariant/transcription_methodology.md).
|
|
||||||
- The transcription prompt distinguishes these document-body media:
|
|
||||||
- `[document body handwritten]`
|
|
||||||
- `[document body typewritten]`
|
|
||||||
- `[document body typeset]`
|
|
||||||
- `[document body mixed]`
|
|
||||||
- A typewriter's uneven impressions, monospaced characters, or mechanical defects do not by themselves indicate handwriting.
|
|
||||||
- The transcript contains exactly one applicable document-body marker.
|
|
||||||
- A wholly handwritten body uses the one body marker rather than wrapping every line in `[handwritten: ...]`.
|
|
||||||
- Typewritten and typeset bodies do not use handwriting wrappers unless a genuinely handwritten annotation or signature appears.
|
|
||||||
- A mixed body may use localized handwriting markers only for the handwritten portions.
|
|
||||||
- Stored transcription output remains plain text. Model-generated HTML entities are not required for ordinary characters.
|
|
||||||
- The prompt includes layout guidance for tables of contents, tables, forms, columns, captions, marginalia, page numbers, dotted leaders, and associated page references.
|
|
||||||
- Line-break hyphenation rules continue to preserve intentional hyphens while rejoining words split only by line wrapping.
|
|
||||||
|
|
||||||
### 3. Quality Warnings
|
|
||||||
|
|
||||||
- The application evaluates successful machine output for deterministic warning conditions, including:
|
|
||||||
- Unicode replacement characters such as `�`.
|
|
||||||
- A whole-body handwritten marker combined with repeated whole-line handwriting wrappers.
|
|
||||||
- More than one document-body medium marker.
|
|
||||||
- Unresolved HTML entities in otherwise plain transcription text.
|
|
||||||
- Warnings do not silently rewrite model output.
|
|
||||||
- Warnings do not automatically trigger another paid provider request.
|
|
||||||
- Source Detail displays warnings with the relevant machine result so the user can decide whether to revise or retranscribe it.
|
|
||||||
- V4.5 does not assign or display a transcription-confidence percentage. Provider self-assessments and token probabilities are not treated as calibrated transcription confidence.
|
|
||||||
|
|
||||||
### 4. Source Retranscription Entry Point
|
|
||||||
|
|
||||||
- Source Detail adds a **Retranscribe Source** action.
|
|
||||||
- The action opens Create Processing Job with the Source preselected and locked.
|
|
||||||
- The Source's existing Document is derived from its relationship and cannot be changed in this flow.
|
|
||||||
- The new Job contains a `JobSource` only for the selected Source; it does not retranscribe every Source in the Document.
|
|
||||||
- Provider is populated from the configured `PROVIDER` value and is read-only while only one provider is configured.
|
|
||||||
- Model is selected from an operator-configured allowlist.
|
|
||||||
- The configured default model is initially selected.
|
|
||||||
- Creating the Job freezes the selected provider, model, prompt, prompt hash, parameters, and Source evidence according to the existing provenance contract.
|
|
||||||
- Retranscription creates a new Job and new execution evidence. It does not reuse, mutate, or erase a previous Job.
|
|
||||||
|
|
||||||
### 5. Configured Vision-Model Allowlist
|
|
||||||
|
|
||||||
- `PROVIDER_MODEL` remains the default transcription model.
|
|
||||||
- `PROVIDER_MODELS` defines the models available in the Create Processing Job model selector.
|
|
||||||
- The environment representation is a JSON array, for example:
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
PROVIDER=openrouter
|
|
||||||
PROVIDER_MODEL=google/gemini-2.5-flash
|
|
||||||
PROVIDER_MODELS=["google/gemini-2.5-flash","google/gemini-2.5-pro","anthropic/claude-sonnet-4"]
|
|
||||||
```
|
|
||||||
|
|
||||||
- If `PROVIDER_MODELS` is omitted, the selector contains only `PROVIDER_MODEL`.
|
|
||||||
- The default model appears exactly once and first; remaining configured models retain their relative order.
|
|
||||||
- Empty, malformed, or duplicate values produce deterministic configuration validation.
|
|
||||||
- The UI never accepts an arbitrary model identifier outside the configured allowlist.
|
|
||||||
- The allowlist controls availability, not claims of quality, price, or provider compatibility. The operator is responsible for configuring models supported by the selected provider.
|
|
||||||
|
|
||||||
### 6. Candidate Machine Transcriptions
|
|
||||||
|
|
||||||
- The first successful transcription becomes preferred automatically whenever the Source has no preferred machine output, regardless of whether it came from an initial or retranscription Job.
|
|
||||||
- Once a Source has a preferred machine transcription, every later successful result is stored as a candidate regardless of Job purpose and cannot replace the preferred result automatically.
|
|
||||||
- Every candidate remains associated with its immutable Job, JobSource, ExecutionAttempt, provider, model, prompt, parameters, timestamps, warnings, and normalized-input evidence.
|
|
||||||
- Source Detail presents:
|
|
||||||
- The current preferred machine transcription.
|
|
||||||
- Available successful candidates with date, provider, model, Job ID, and warning state.
|
|
||||||
- A comparison between the current preferred machine transcription and one selected candidate.
|
|
||||||
- A **Use this transcription** action for a successful candidate.
|
|
||||||
- Before any successful result exists, Source Detail displays an explicit no-machine-transcription state.
|
|
||||||
- When a preferred result exists but no candidates exist, Source Detail omits comparison controls and displays an explicit no-candidates state.
|
|
||||||
- Promoting a candidate:
|
|
||||||
- Verifies that the successful execution belongs to the Source.
|
|
||||||
- Records the selected execution as the preferred machine-output provenance.
|
|
||||||
- Updates `Source.raw_transcription` as the preferred-machine-text projection.
|
|
||||||
- Does not modify `Source.revised_text`.
|
|
||||||
- Does not delete or alter any previous machine result.
|
|
||||||
- If a human revision exists, it remains the current human-preferred text used by normal display and printing after a machine candidate is promoted.
|
|
||||||
|
|
||||||
### 7. Evidence and Failure Behavior
|
|
||||||
|
|
||||||
- The original Source and every normalized derivative are content-addressed and traceable.
|
|
||||||
- Every provider request identifies the exact original Source and model-input artifact used.
|
|
||||||
- Every execution attempt records the exact immutable model-input artifact it consumed, including when normalized bytes are reused.
|
|
||||||
- Every retranscription attempt follows the V4.2 immutable execution-attempt contract.
|
|
||||||
- A normalization failure prevents the provider request and produces an explicit actionable error.
|
|
||||||
- A provider or persistence failure leaves the current preferred machine transcription and human revision unchanged.
|
|
||||||
- Candidate promotion is atomic: provenance selection and the preferred-machine-text projection either both commit or both remain unchanged.
|
|
||||||
- Provider network work occurs outside database transactions.
|
|
||||||
|
|
||||||
### 8. Source Detail Terminology
|
|
||||||
|
|
||||||
- **Original Source** means the immutable uploaded file.
|
|
||||||
- **Model input** means the original Source or normalized derivative actually sent to the provider.
|
|
||||||
- **Machine attempt** means one immutable provider execution.
|
|
||||||
- **Candidate transcription** means a successful machine result not currently selected as preferred.
|
|
||||||
- **Preferred machine transcription** means the selected machine result projected through `Source.raw_transcription`.
|
|
||||||
- **Human revision** means `Source.revised_text`, which remains independent of every machine result.
|
|
||||||
- These distinctions use concise labels and progressive disclosure; routine satisfactory Sources do not display a mandatory side-by-side original/normalized image comparison.
|
|
||||||
- When normalization occurred, Source Detail displays an orientation-normalized indicator and makes transformation evidence inspectable through the existing evidence UI.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Manual image rotation or image-editing controls.
|
|
||||||
- Visual orientation detection when metadata is absent or incorrect.
|
|
||||||
- Deskewing, cropping, contrast normalization, denoising, sharpening, or restoration.
|
|
||||||
- Replacing or modifying original Source files.
|
|
||||||
- Automatically retranscribing every Source.
|
|
||||||
- Automatic provider retries triggered by quality warnings.
|
|
||||||
- Arbitrary model identifiers entered by users.
|
|
||||||
- Multiple provider selection in the UI.
|
|
||||||
- Model benchmarking, pricing recommendations, or automatic model ranking.
|
|
||||||
- A provider-independent transcription-confidence percentage.
|
|
||||||
- Silent cleanup or rewriting of model output.
|
|
||||||
- Deleting unsuccessful, superseded, or unselected machine attempts.
|
|
||||||
- Promoting a machine candidate over a human revision.
|
|
||||||
- Source page renumbering or reordering.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. Curated Originals Remain Authoritative
|
|
||||||
|
|
||||||
- V4.5 corrects metadata-directed orientation only for model processing.
|
|
||||||
- The archival upload is never replaced by the normalized derivative.
|
|
||||||
|
|
||||||
### B. Orientation Is Automatic and Metadata-Driven
|
|
||||||
|
|
||||||
- No manual orientation workflow is introduced.
|
|
||||||
- V4.5 does not guess orientation from page content.
|
|
||||||
|
|
||||||
### C. Selective Retranscription Replaces Automatic Escalation
|
|
||||||
|
|
||||||
- The normal default model remains efficient for satisfactory Sources.
|
|
||||||
- The user explicitly chooses when an alternate approved model is worth another provider request.
|
|
||||||
- Quality warnings inform that choice but never incur cost automatically.
|
|
||||||
|
|
||||||
### D. Retranscription Produces Candidates
|
|
||||||
|
|
||||||
- Alternate results remain immutable and comparable.
|
|
||||||
- The user explicitly promotes the preferred machine result.
|
|
||||||
- Human revision remains a separate, higher-precedence layer.
|
|
||||||
|
|
||||||
### E. Model Choice Is Operator-Controlled
|
|
||||||
|
|
||||||
- Environment configuration defines the finite allowed model set.
|
|
||||||
- Job records freeze the actual selected model and request parameters.
|
|
||||||
|
|
||||||
### F. Confidence Is Evidence-Based, Not Invented
|
|
||||||
|
|
||||||
- V4.5 does not present model self-rating as objective confidence.
|
|
||||||
- Review uses visible output, deterministic warnings, provenance, and human judgment.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. A JPEG with EXIF Orientation 3 produces an upright model-input derivative while the original bytes remain unchanged.
|
|
||||||
2. A Source requiring no recognized orientation transformation is not unnecessarily altered.
|
|
||||||
3. Orientation transformation metadata and hashes identify the exact provider input.
|
|
||||||
4. The prompt distinguishes handwritten, typewritten, typeset, and mixed bodies with exactly one body marker.
|
|
||||||
5. Typewritten text is not wrapped line by line as handwriting.
|
|
||||||
6. Tables of contents retain row associations and page references without handwriting wrappers.
|
|
||||||
7. Deterministic warnings identify replacement characters and contradictory body markers without changing output.
|
|
||||||
8. Source Detail provides Retranscribe Source for an existing Source.
|
|
||||||
9. Create Processing Job locks the Source and Document, uses configured Provider, and restricts Model to the configured allowlist.
|
|
||||||
10. Retranscription creates a new single-Source Job with complete frozen request and execution evidence.
|
|
||||||
11. The first successful result is selected automatically; every later successful result remains a candidate and cannot replace the preferred machine transcription automatically.
|
|
||||||
12. Source Detail can compare the preferred machine transcription with one candidate and promote that candidate explicitly.
|
|
||||||
13. Candidate promotion records exact successful-execution provenance and updates the machine-text projection atomically.
|
|
||||||
14. Candidate promotion never changes or clears a human revision.
|
|
||||||
15. Earlier machine attempts remain inspectable after retranscription and promotion.
|
|
||||||
16. No confidence percentage, manual image editor, automatic quality retry, or arbitrary model input is introduced.
|
|
||||||
17. Database, integration, and UI verification uses isolated test data and never modifies `data/transcription.db`.
|
|
||||||
|
|
||||||
## Scope Freeze Gate
|
|
||||||
|
|
||||||
V4.5 is sufficiently frozen to begin implementation:
|
|
||||||
|
|
||||||
- Orientation behavior and preservation rules are resolved.
|
|
||||||
- Prompt medium categories and marker behavior are resolved.
|
|
||||||
- Warning behavior and the absence of automatic retry are resolved.
|
|
||||||
- Retranscription entry point, single-Source scope, and model configuration are resolved.
|
|
||||||
- Candidate preservation, comparison, promotion, and human-revision precedence are resolved.
|
|
||||||
- The broader future-feature list has been reviewed, and no additional V4.5 features are required.
|
|
||||||
|
|
||||||
Any expansion into manual image editing, visual orientation inference, automatic retries, multiple providers, confidence scoring, or additional processing features requires an explicit V4.5 scope amendment or a later revision.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.5 Implementation Plan](implementation_plan_v4_5.md)
|
|
||||||
- [V4.4 Scope Boundary](../ver4.4/scope_boundary_v4_4.md)
|
|
||||||
- [V4.4 Implementation Plan](../ver4.4/implementation_plan_v4_4.md)
|
|
||||||
- [V4.2 Evidence and Provenance Scope](../ver4.2/scope_boundary_v4_2.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [V4 Requirements](../ver4/requirements_v4.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
- [AI Evidence and Provenance Invariant](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.6)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Pay down the defects, duplication, and structural drift identified in the [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md) without changing any observable behavior. Re-level the database schema from current SQLModel metadata, correct read amplification and missing indexes, consolidate duplicated service and UI code, restore the project's own documented boundaries, and make `ty` a real quality gate.
|
|
||||||
|
|
||||||
## Planning Status
|
|
||||||
|
|
||||||
- V4.5 is the completed implementation baseline.
|
|
||||||
- The V4.6 scope is frozen and sufficiently detailed to begin implementation.
|
|
||||||
- Every task traces to a review finding ID. A change without a finding ID is a scope addition and requires an explicit amendment.
|
|
||||||
- The `SourceService` split ([MED-14]) is deferred to V4.7 by decision, not by omission.
|
|
||||||
|
|
||||||
## Planning Constraints
|
|
||||||
|
|
||||||
- **Behavior is preserved exactly.** All 264 pre-existing tests must still pass. A test that must change is evidence the change is not remediation.
|
|
||||||
- The application targets **SQLite only** in V4.6. PostgreSQL is unblocked but not enabled.
|
|
||||||
- The deployment is **single user, single process, single worker**. Forward-compatible code is written where cheap and dialect-guarded.
|
|
||||||
- The schema is re-leveled from metadata. **No Alembic, no revision directory, no history table, no down path.**
|
|
||||||
- All schema-affecting changes land in **one pass**; partial application is not a valid state.
|
|
||||||
- The data migration script is authored **last**, against the final schema and final loading strategy.
|
|
||||||
- Uploaded Source files, portraits, and artifact files on disk are never modified.
|
|
||||||
- Original Source files remain immutable; every V4.2–V4.5 evidence and provenance contract is preserved.
|
|
||||||
- Provider network work continues to occur outside database transactions.
|
|
||||||
- Database, integration, and UI tests use confirmed isolated data and never modify `data/transcription.db`.
|
|
||||||
- Potentially destructive tests run only through `tools/run_destructive_tests.py`.
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Expected impact |
|
|
||||||
| --- | --- |
|
|
||||||
| Dead code | Remove `app_state.py`, `services/transcription.py`, legacy aliases, `ServiceBase.queue`, and a duplicate queued-job query. |
|
|
||||||
| Persistence | Delete the hand-rolled DDL chain; generate schema from metadata with correct indexes, FK ordering, and loading strategy. |
|
|
||||||
| Query behavior | Bounded queued-job poll, SQL-side filtering, bounded navigation queries, explicit eager loads. |
|
|
||||||
| Worker | Provider client and service bundle live for the worker's lifetime rather than per job. |
|
|
||||||
| Configuration | Remove the timeout cap and the silently-ignored `DATABASE_URL`; resolve dead settings; replace frozen-model mutation. |
|
|
||||||
| Service layer | Generic registry service, shared not-found guard, single media-storage implementation (~400 lines removed). |
|
|
||||||
| UI layer | Fix three boundary violations; extract duplicated components (~500 lines removed); externalize the SVG asset. |
|
|
||||||
| Async I/O | Move filesystem, hashing, and image work off the event loop. |
|
|
||||||
| Tooling | `ruff` and `ty` both reach zero and gate on pre-commit. |
|
|
||||||
| Data | One-time migration of backed-up V4.5 data into the re-leveled schema. |
|
|
||||||
| Tests and documentation | Add index, FK-cycle, claim-boundedness, client-reuse, and registry-parity coverage; correct the stale instruction path. |
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Deletions and Quick Wins
|
|
||||||
|
|
||||||
Independent of every other phase. Land first to shrink the surface everything else must consider.
|
|
||||||
|
|
||||||
- Delete `src/transcription/app_state.py` and confirm zero importers remain in `src`, `tests`, and `tools` ([HIGH-01]).
|
|
||||||
- Delete `src/transcription/services/transcription.py` and standardize every `build_prompt_execution` import on `services/sources.py` ([MED-05]).
|
|
||||||
- Delete the legacy compatibility aliases in `services/store.py:35,382,383` ([MED-05]).
|
|
||||||
- Delete `ServiceBase.queue` and its unparameterized `asyncio.Queue` ([MED-07]).
|
|
||||||
- Delete `db/operations.py:get_next_queued_job` as a divergent duplicate of the live implementation ([CRIT-01]).
|
|
||||||
- Resolve `sqlite_check_same_thread` and `worker_retry_backoff_seconds`: wire each to real behavior or delete it together with its test ([MED-02]).
|
|
||||||
- Remove `DATABASE_URL` from `docker-compose.yml` and document the real `DATABASE__DRIVER` / `DATABASE__PATH` nested names in `.env.example` ([MED-10]).
|
|
||||||
- Correct the stale path in `.github/instructions/services.instructions.md:10` to `src/transcription/db/models.py` ([LOW-02]).
|
|
||||||
- Remove the discarded `load_docs` parameter from `list_jobs` ([LOW-03]).
|
|
||||||
- Validate the `getattr` result in `resolve_worker_notifier` ([LOW-04]).
|
|
||||||
- Move `VIBESCRIBE_LOGO_SVG` to `ui/static/vibescribe_logo.svg` and load it through a `read_svg` sibling of `ui/resources.py:read_css` ([MED-09]).
|
|
||||||
- Run `ruff check --fix` and resolve the remainder by hand ([LOW-01]).
|
|
||||||
- Route `people_page.py:504` through `error_presenter.show_error` ([LOW-07]).
|
|
||||||
- Cancel the auto-refresh timer rather than only deactivating it, and name its interval constant ([LOW-06]).
|
|
||||||
|
|
||||||
**Verification:** full suite green, `ruff check` reports zero, no import of a deleted symbol remains.
|
|
||||||
|
|
||||||
### 2. Schema Re-Level — Single Pass
|
|
||||||
|
|
||||||
This phase is atomic. Every task below regenerates the same schema and must be verified together.
|
|
||||||
|
|
||||||
- Delete `upgrade_schema` and `_upgrade_*` (`db/operations.py:25-109`) and their tests (`tests/test_db.py:109-172`) ([HIGH-05]).
|
|
||||||
- Confirm `create_all()` remains gated by `Settings.should_bootstrap_schema` (`config.py:140-145`) ([HIGH-05]).
|
|
||||||
- Declare the composite index in the model: `Index("ix_job_status_date_created", "status", "date_created")`, plus `index=True` on the foreign keys the worker and detail pages filter on ([HIGH-04]).
|
|
||||||
- Declare `Source.preferred_execution_attempt_id`'s foreign key with `use_alter=True` and an explicit constraint name, breaking the `source` / `job_source` / `execution_attempt` cycle ([HIGH-08]).
|
|
||||||
- Flip relationship loading from bidirectional `lazy="selectin"` to `lazy="raise"`, model by model ([CRIT-02]):
|
|
||||||
- Work one model at a time with the suite as the safety net.
|
|
||||||
- Where a test fails with a lazy-load error, add an explicit `selectinload()` to the *service query* that feeds it — never restore the model-level default.
|
|
||||||
- Where an existing explicit `selectinload()` proves redundant, delete it; this is the primary source of the ~160 `ty` diagnostics addressed in Phase 6.
|
|
||||||
- Follow the two correct precedents already in the codebase: `Source.processing_artifacts:279` and `JobSource.execution_attempts:336`.
|
|
||||||
- Rebuild the development database from empty. Do **not** attempt to upgrade the existing file.
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
- A test asserts the composite `Job` index and the hot foreign-key indexes exist in a freshly created schema.
|
|
||||||
- A test compiles the metadata against the PostgreSQL dialect and asserts **no** unresolvable-cycle warning is emitted.
|
|
||||||
- A test asserts `preferred_execution_attempt_id`'s column type matches the model declaration.
|
|
||||||
- Full suite green under `lazy="raise"`.
|
|
||||||
- No raw `ALTER TABLE` or `CREATE INDEX` string remains anywhere in `src`.
|
|
||||||
|
|
||||||
**Rollback:** this phase reverts as a unit. A partially applied schema pass is not a valid state.
|
|
||||||
|
|
||||||
### 3. Worker and Provider Reliability
|
|
||||||
|
|
||||||
Depends on Phase 2, because the claim query's cost profile is only correct once eager-loading defaults are fixed.
|
|
||||||
|
|
||||||
- Add `.limit(1)` to the queued-job selection and remove its eager-load options from the hot poll ([CRIT-01]).
|
|
||||||
- Convert the read-then-write claim into an atomic `QUEUED` → `PROCESSING` transition in one transaction ([CRIT-01]):
|
|
||||||
- Write the dialect-guarded `with_for_update(skip_locked=True)` branch for the multi-user direction.
|
|
||||||
- On SQLite, the claim executes as a bounded single-writer transaction.
|
|
||||||
- Load the eager relationships in a **second** query after the claim succeeds.
|
|
||||||
- Update the stale comment at `workflows.py:193-194` to describe the actual guarantee rather than the known hazard.
|
|
||||||
- Hoist `ServiceBundle` and the provider client out of the per-job body in `worker.py:157-174` to worker-loop scope; `aclose()` the client once at loop shutdown, not once per job ([HIGH-02]).
|
|
||||||
- Add `ServiceBundle.from_session_factory(...)`, replacing the three duplicated instantiation blocks at `app.py:45-50`, `worker.py:160-165`, and `services/__init__.py:19-22`. Have `_recover_stale_processing_jobs` (`app.py:73-84`) use the bundle built five lines earlier ([MED-06]).
|
|
||||||
- Remove `le=20.0` from `worker_provider_timeout_seconds` (`config.py:110`), raise the default to a realistic vision-transcription duration, and pass an explicit `httpx.Timeout` to the OpenRouter `AsyncClient` (`openrouter.py:198`) ([HIGH-03]).
|
|
||||||
- Extend the `TranscriptionProvider` Protocol to declare `aclose` and the evidence attributes; delete the per-call `inspect.signature(adapter.transcribe).parameters` reflection at `sources.py:1237` and the associated untyped kwargs dict ([MED-03]).
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
- A test asserts the emitted claim SQL contains `LIMIT` and no `selectinload` join.
|
|
||||||
- A test asserts the worker processes two consecutive jobs against the same provider client instance.
|
|
||||||
- A test asserts a timeout value above 20 seconds is accepted by `Settings`.
|
|
||||||
- A test asserts the transcription call path resolves `requested_model` through the Protocol without reflection.
|
|
||||||
|
|
||||||
### 4. Service Layer Consolidation
|
|
||||||
|
|
||||||
Depends on Phase 2 only for the loading strategy; otherwise independent of Phase 3.
|
|
||||||
|
|
||||||
- Introduce `services/registry.py` with a generic `RegistryService[ModelT]` owning list, summaries with counts, create with `IntegrityError` → conflict mapping, read with not-found, update, delete with built-in and referenced guards, and `is_referenced` ([MED-11]):
|
|
||||||
- Define label normalization, the casefold key, and the summary shape once.
|
|
||||||
- Reduce `DocumentService`'s document-type methods (`documents.py:350-500`) and `PeopleService`'s person-role methods (`people.py:214-378`) to subclasses declaring model, error class, reference query, and noun.
|
|
||||||
- Preserve every existing user-facing message, error category, and suggestion string verbatim; template the noun only.
|
|
||||||
- Add `ServiceBase._get_or_raise(...)` and adopt it at all 38 not-found sites, including `documents.py:174,210,291`, which currently bypass the local `_get_document_or_raise` helper. Delete the now-redundant local helper ([MED-12]).
|
|
||||||
- Introduce `services/media_storage.py` as the single validate → hash → `mkdir` → write → wrap-`OSError` implementation, replacing `store.py:319-379`, `people.py:596-631`, and `ui/homepage_store.py:31-44`. Wrap the write in `asyncio.to_thread` ([MED-13], [MED-01]).
|
|
||||||
- Move `source_mime_type` out of `services/sources.py` into a shared module so `documents.py:24` no longer imports a sibling service, restoring the independence rule at `services.instructions.md:13` ([MED-14], partial).
|
|
||||||
- Correct the four query inefficiencies in `sources.py` ([LOW-08]):
|
|
||||||
- `list_sources_detail:338-343` — move the `job_id` filter from Python into a SQL join on `JobSource`.
|
|
||||||
- `read_source_navigation:233-244` — replace the full ordered-id scan with two `LIMIT 1` queries.
|
|
||||||
- `list_processing_artifacts:961` — add a `limit` parameter matching its summary sibling.
|
|
||||||
- `build_evidence_export:1012-1013` — move artifact integrity hashing into `asyncio.to_thread`.
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
- Existing `DocumentType` and `PersonRole` tests pass **unchanged** against the shared implementation. This is the primary proof that behavior is preserved.
|
|
||||||
- A test asserts `list_sources_detail` filtered by `job_id` emits a join rather than loading the full table.
|
|
||||||
- No module in `services/` imports another concrete service module.
|
|
||||||
|
|
||||||
### 5. UI Boundaries and Duplication
|
|
||||||
|
|
||||||
Independent of Phases 2–4 except where a service signature changes.
|
|
||||||
|
|
||||||
- Fix the three `ui.instructions.md` violations ([HIGH-07]):
|
|
||||||
- Add a `JobService` or workflow method that owns `session_scope` internally; remove the import and transaction management from `jobs_page.py:17,185-192`.
|
|
||||||
- Have `SourceService` return a plain `transport_body_deferred: bool` on a read model; remove `sqlalchemy.inspect` from `sources_page.py:13,439`.
|
|
||||||
- Pass a ready media URL into `document_panzoom`, or delete the component — it is exported from `components/__init__.py` but used by no page ([HIGH-07]).
|
|
||||||
- Extract the duplication catalogued in review §4, highest value first:
|
|
||||||
- `ui/components/confirm_delete.py` — the blocked-deps card plus confirm/cancel row, from four pages (~120 lines).
|
|
||||||
- `ui/components/media_urls.py` — pure upload-URL resolution taking `upload_dir` and `base_url`, from three call sites (~110 lines).
|
|
||||||
- `ui/components/guards.py` — parse → error label → return, from nine call sites (~90 lines).
|
|
||||||
- `build_table` adoption for the remaining hand-rolled `ui.table` instances, adding selection and no-search options as needed (~70 lines).
|
|
||||||
- `ui/components/upload_panel.py` — file-picker wiring, from three pages (~50 lines).
|
|
||||||
- `ui/components/formatters.py` — `_parse_uuid` (five copies) and `_parse_iso_date` (two copies) (~49 lines).
|
|
||||||
- A shared page-helper for `_resolve_runtime_settings(request)` (three copies, ~18 lines).
|
|
||||||
- Annotate untyped handler parameters and replace loosely-typed dict returns with read models ([LOW-05]).
|
|
||||||
|
|
||||||
**Verification:** UI page tests pass unchanged; no page module imports `session_scope`, `sqlalchemy.inspect`, or `get_settings`.
|
|
||||||
|
|
||||||
### 6. Async I/O and Configuration Hygiene
|
|
||||||
|
|
||||||
- Wrap the remaining blocking work in `asyncio.to_thread`: Pillow orientation normalization, artifact writes, and evidence hashing not already covered by Phase 4 ([MED-01]).
|
|
||||||
- Replace `functools.cache` on the engine and session factories with an explicit URL-keyed registry supporting targeted eviction, removing the cross-test and cross-tenant coupling and restoring a visible call signature ([MED-04]).
|
|
||||||
- Replace `object.__setattr__` in `normalize_provider_models` (`config.py:130,137`) with `model_copy(update=...)` or a computed property.
|
|
||||||
- Add `onupdate` to the `updated_at` / `date_updated` columns that are expected to track modification, so they stop being stale on the update paths that do not set them by hand. Remove the now-redundant manual assignment at `jobs.py:166` and its siblings.
|
|
||||||
- Surface the exception currently swallowed to `None` in the ORM model property at `models.py:227` ([MED-08]).
|
|
||||||
|
|
||||||
**Note:** the `onupdate` change is schema-affecting in principle but not in emitted DDL, since `onupdate` is a Python-side default. If implementation reveals it alters generated DDL, it moves into Phase 2 and Phase 2 is re-verified.
|
|
||||||
|
|
||||||
**Verification:** a test asserts an update through a service advances `updated_at`; a test asserts two different database URLs produce two distinct engines and that evicting one leaves the other intact.
|
|
||||||
|
|
||||||
### 7. Type Checking and Tooling Gate
|
|
||||||
|
|
||||||
Depends on Phase 2, which is expected to remove most diagnostics by deleting redundant eager loads.
|
|
||||||
|
|
||||||
- Re-baseline `ty check` after Phase 2 and measure the remaining diagnostic count ([HIGH-06]).
|
|
||||||
- Convert every surviving `# pyright: ignore[...]` to `# ty: ignore[...]`, since `ty` does not honor pyright directives ([HIGH-06]).
|
|
||||||
- Fix the two real bugs currently hidden in the noise ([HIGH-06]):
|
|
||||||
- `tests/ui/test_sources_page.py:25` constructs `Source(...)` without the required `document_id`.
|
|
||||||
- `tools/run_destructive_tests.py:76,80` uses `fcntl`, which does not exist on Windows; use a cross-platform lock or guard by platform.
|
|
||||||
- Drive `ty check` to zero diagnostics and wire it into the existing pre-commit setup as a blocking gate.
|
|
||||||
- Configure `asyncio_default_fixture_loop_scope` explicitly so pytest-asyncio behavior does not change on upgrade.
|
|
||||||
|
|
||||||
**Verification:** `ty check` and `ruff check` both report zero; pre-commit fails when either regresses; `tools/run_destructive_tests.py` runs on Windows.
|
|
||||||
|
|
||||||
### 8. Data Migration
|
|
||||||
|
|
||||||
The final phase. Authored against the completed schema and the completed loading strategy.
|
|
||||||
|
|
||||||
- Write a one-time script under `tools/` that reads the backed-up V4.5 database and writes into the re-leveled schema (review §1a, "Items Added During Scoping").
|
|
||||||
- Because `lazy="raise"` is in force, every relationship traversal in the script carries an explicit eager load. This is the reason the script is written last.
|
|
||||||
- Preserve identity: UUIDs, digests, timestamps, attempt numbers, and `preferred_execution_attempt_id` selections carry across unchanged.
|
|
||||||
- Do not reinterpret, normalize, or regenerate any `ExecutionAttempt` or `ProcessingArtifact` evidence.
|
|
||||||
- Do not modify any on-disk Source file, portrait, or artifact file.
|
|
||||||
- The script is idempotent, is never invoked from application startup, and never runs in the test suite.
|
|
||||||
|
|
||||||
**Verification:** post-migration row counts match the backup for every table (`document` 8, `document_person` 11, `document_type` 7, `execution_attempt` 80, `job` 11, `job_source` 79, `person` 5, `person_role` 3, `processing_artifact` 2, `source` 76); artifact integrity verification passes for every migrated artifact; on-disk file hashes are unchanged.
|
|
||||||
|
|
||||||
## Sequencing Constraint
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
P1[1. Deletions & Quick Wins]
|
|
||||||
P2[2. Schema Re-Level<br/>SINGLE ATOMIC PASS]
|
|
||||||
P3[3. Worker & Provider]
|
|
||||||
P4[4. Service Consolidation]
|
|
||||||
P5[5. UI Boundaries & Duplication]
|
|
||||||
P6[6. Async I/O & Config]
|
|
||||||
P7[7. Type-Check Gate]
|
|
||||||
P8[8. Data Migration]
|
|
||||||
|
|
||||||
P1 --> P2
|
|
||||||
P2 --> P3
|
|
||||||
P2 --> P4
|
|
||||||
P2 --> P7
|
|
||||||
P1 --> P5
|
|
||||||
P4 --> P5
|
|
||||||
P4 --> P6
|
|
||||||
P3 --> P8
|
|
||||||
P5 --> P8
|
|
||||||
P6 --> P8
|
|
||||||
P7 --> P8
|
|
||||||
```
|
|
||||||
|
|
||||||
The binding constraints are:
|
|
||||||
|
|
||||||
1. **Phase 2 is indivisible.** `create_all` from metadata, the indexes, `use_alter`, and the `lazy` flip all regenerate the same schema. They land together or not at all.
|
|
||||||
2. **Phase 7 follows Phase 2.** Measuring the `ty` baseline before the redundant eager loads are deleted would chase diagnostics that Phase 2 removes for free.
|
|
||||||
3. **Phase 8 is last.** The migration script must be written against the final schema and the final loading strategy.
|
|
||||||
|
|
||||||
## Test Strategy
|
|
||||||
|
|
||||||
- **The existing suite is the contract.** 264 tests pass today and must pass at every phase boundary. A test that requires modification is treated as a defect in that test, justified individually in the commit, and never as license to change behavior.
|
|
||||||
- **Registry parity is the key proof.** The `DocumentType` and `PersonRole` tests must pass *unchanged* against the shared `RegistryService`. If they need edits, the abstraction is wrong.
|
|
||||||
- **New tests are structural, not behavioral.** They assert schema shape, emitted SQL, dialect compatibility, and object lifetime — properties the current suite does not cover and that the review found were the reason these defects survived.
|
|
||||||
- New coverage to add:
|
|
||||||
|
|
||||||
| Assertion | Finding |
|
|
||||||
| :--- | :--- |
|
|
||||||
| Composite `Job` index and hot FK indexes exist in a fresh schema | [HIGH-04] |
|
|
||||||
| PostgreSQL-dialect metadata compilation emits no cycle warning | [HIGH-08] |
|
|
||||||
| `preferred_execution_attempt_id` column type matches the model | [HIGH-05] |
|
|
||||||
| Full suite passes under `lazy="raise"` | [CRIT-02] |
|
|
||||||
| Claim SQL contains `LIMIT` and no eager-load join | [CRIT-01] |
|
|
||||||
| Provider client instance is reused across two consecutive jobs | [HIGH-02] |
|
|
||||||
| `Settings` accepts a provider timeout above 20 seconds | [HIGH-03] |
|
|
||||||
| `list_sources_detail` emits a join rather than a full-table load | [LOW-08] |
|
|
||||||
| An update through a service advances `updated_at` | [SQLModel §3] |
|
|
||||||
| Distinct database URLs yield distinct, individually evictable engines | [MED-04] |
|
|
||||||
| Post-migration row counts match the backup | [Phase 8] |
|
|
||||||
|
|
||||||
- All database, integration, and UI tests continue to use isolated data and never touch `data/transcription.db`.
|
|
||||||
- Destructive tests continue to run only through `tools/run_destructive_tests.py`, which must first be made to run on Windows.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
| Risk | Likelihood | Impact | Mitigation |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| The `lazy="raise"` flip surfaces load paths the tests do not cover, breaking a UI page at runtime | High | Medium | Flip one model at a time; exercise every page manually at the phase boundary; `lazy="raise"` fails loudly rather than silently, which is the point |
|
|
||||||
| Phase 2 is partially applied and leaves an inconsistent schema | Medium | High | Treat Phase 2 as one commit; rebuild from empty rather than upgrading; verify all four schema assertions before proceeding |
|
|
||||||
| `RegistryService` generalization subtly changes a user-facing message or error category | Medium | Medium | Preserve message strings verbatim, templating only the noun; require the existing registry tests to pass unchanged |
|
|
||||||
| The atomic claim behaves differently on SQLite than the `FOR UPDATE SKIP LOCKED` path it is written to support | Medium | Low | Single worker in V4.6 means the SQLite path is the only one exercised; the Postgres branch is dialect-guarded and explicitly unverified until the cutover |
|
|
||||||
| Removing the timeout cap allows a pathological hang | Low | Medium | Pair the removal with an explicit `httpx.Timeout` so the client, not the config bound, enforces the ceiling |
|
|
||||||
| The migration script loses or reinterprets evidence | Low | High | Verify row counts per table, verify artifact integrity hashes post-migration, and never touch on-disk files |
|
|
||||||
| Remediation quietly becomes feature work | Medium | Medium | Every commit cites a finding ID; anything without one is recorded for a later revision |
|
|
||||||
| `ty` cannot reach zero without unsound suppressions | Medium | Low | Suppressions are acceptable where SQLModel typing is genuinely unrepresentable, but each must be `# ty: ignore[<rule>]` with a specific rule, never blanket |
|
|
||||||
|
|
||||||
## Delivery Order
|
|
||||||
|
|
||||||
1. Phase 1 — Deletions and Quick Wins
|
|
||||||
2. Phase 2 — Schema Re-Level (single atomic pass)
|
|
||||||
3. Phase 3 — Worker and Provider Reliability
|
|
||||||
4. Phase 4 — Service Layer Consolidation
|
|
||||||
5. Phase 5 — UI Boundaries and Duplication
|
|
||||||
6. Phase 6 — Async I/O and Configuration Hygiene
|
|
||||||
7. Phase 7 — Type Checking and Tooling Gate
|
|
||||||
8. Phase 8 — Data Migration
|
|
||||||
|
|
||||||
## Done Criteria
|
|
||||||
|
|
||||||
V4.6 is complete when every acceptance criterion in the [V4.6 Scope Boundary](scope_boundary_v4_6.md) is satisfied, specifically:
|
|
||||||
|
|
||||||
- All 264 pre-existing tests pass, with every modified test individually justified.
|
|
||||||
- `ruff check` and `ty check` both report zero and gate on pre-commit.
|
|
||||||
- No hand-rolled DDL, dead module, dead setting, or duplicate implementation identified in the review remains.
|
|
||||||
- The schema is generated from metadata, correctly indexed, cycle-free under the PostgreSQL dialect, and free of bidirectional `lazy="selectin"`.
|
|
||||||
- Roughly 900 lines of duplication are removed across the service and UI layers.
|
|
||||||
- The backed-up V4.5 data is restored into the re-leveled schema with matching row counts and unmodified on-disk files.
|
|
||||||
- No new user-facing feature exists that did not exist in V4.5.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.6 Scope Boundary](scope_boundary_v4_6.md)
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
|
||||||
- [V4.5 Scope Boundary](../ver4.5/scope_boundary_v4_5.md)
|
|
||||||
- [V4.5 Implementation Plan](../ver4.5/implementation_plan_v4_5.md)
|
|
||||||
- [V4.2 Evidence and Provenance Scope](../ver4.2/scope_boundary_v4_2.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
- [AI Evidence and Provenance Invariant](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
# V4.6 Implementation Review Log
|
|
||||||
|
|
||||||
Working record kept during the V4.6 remediation release and the V4.7 / V4.8 planning that followed.
|
|
||||||
|
|
||||||
This file is the canonical reference for citations of the form **`review log [N]`** in the V4.6, V4.7, and V4.8 planning documents. The numbers below are those `N` values.
|
|
||||||
|
|
||||||
The log was maintained live in a session-scoped database and exported here so the citations remain resolvable in later sessions. It is a historical record: entries are not rewritten after the fact, so some capture reasoning that was later revised. Where an entry conflicts with a committed planning document, **the planning document wins**.
|
|
||||||
|
|
||||||
## Legend
|
|
||||||
|
|
||||||
| Field | Meaning |
|
|
||||||
| :--- | :--- |
|
|
||||||
| `kind` | `question` - needed a decision; `comment` - observation; `deviation` - departure from plan; `risk` - identified hazard |
|
|
||||||
| `status` | `open` - unresolved; `answered` - resolved by a decision; `noted` - recorded, no action required |
|
|
||||||
| `finding` | Finding ID in [architecture_code_review_2026-08-17.md](../architecture_code_review_2026-08-17.md), where one applies |
|
|
||||||
|
|
||||||
**70 entries** - 8 open, 30 answered, 32 noted.
|
|
||||||
|
|
||||||
## Still Open
|
|
||||||
|
|
||||||
These carry forward. Most are scoped into V4.7; see [V4.7 scope boundary](../ver4.7/scope_boundary_v4_7.md).
|
|
||||||
|
|
||||||
| ID | Finding | Summary | Disposition |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| [8] | - | handle_worker_exceptions swallows everything | V4.7 Phase 5 |
|
|
||||||
| [18] | - | One flaky failure observed once, then five clean full runs | Watch item - no action |
|
|
||||||
| [40] | HIGH-06 | No CI workflow enforces the gate | V4.7 Phase 6 |
|
|
||||||
| [45] | n/a | The same JobSourceStatus enum is persisted two different ways | V4.7 Phase 2 (absorbed into the enum migration) |
|
|
||||||
| [50] | HIGH-03 | Worst-case stall latency is now 60s (2 x 30s), down from 360s | Accepted risk - mitigated by 30s timeout and max_retries=1 |
|
|
||||||
| [53] | HIGH-03 | PROVIDER_MODELS still offers two models that cannot finish a dense page within 30s | Operator judgement - deliberately left open |
|
|
||||||
| [54] | n/a | Run-time telemetry is captured but has no aggregate view | V4.8, gated on V4.7 Phase 4 |
|
|
||||||
| [55] | n/a | duration_ms measures end-to-end page processing, not provider latency | V4.7 Phase 4 |
|
|
||||||
|
|
||||||
## Full Log
|
|
||||||
|
|
||||||
### V4.6 Phase 1 - deletions and quick wins
|
|
||||||
|
|
||||||
#### [1] worker_retry_backoff_seconds deleted, not wired
|
|
||||||
|
|
||||||
*deviation* - `LOW-05` - **noted**
|
|
||||||
|
|
||||||
No backoff behavior existed anywhere in the codebase. Wiring it would have been a new feature, which V4.6 forbids. Deleted the setting and its test instead.
|
|
||||||
|
|
||||||
#### [2] sqlite_check_same_thread wired, not deleted
|
|
||||||
|
|
||||||
*deviation* - `MED-02` - **noted**
|
|
||||||
|
|
||||||
Opposite call from the one above: the engine hardcoded the setting's own default value, so wiring it through preserved behavior exactly.
|
|
||||||
|
|
||||||
### V4.6 Phase 2 - schema re-level
|
|
||||||
|
|
||||||
#### [3] Dev DB safe to discard?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Asked before the atomic schema re-level. You chose "Safe to discard, proceed". The old file was moved to data/transcription.db.pre-v46.bak rather than deleted, because Phase 8 needs it as the migration source.
|
|
||||||
|
|
||||||
#### [4] Guard test found 3 relationships the review missed
|
|
||||||
|
|
||||||
*comment* - `CRIT-02` - **noted**
|
|
||||||
|
|
||||||
The new lazy-load regression test caught ExecutionAttempt.job_source, ProcessingArtifact.execution_attempt, and ProcessingArtifact.source declaring no lazy strategy at all, so they silently defaulted to "select". The review had only catalogued the 16 explicit selectinload cases.
|
|
||||||
|
|
||||||
#### [5] lazy="raise" not smoke-tested in a browser
|
|
||||||
|
|
||||||
*risk* - `CRIT-02` - **answered**
|
|
||||||
|
|
||||||
Every relationship access was audited against its feeding service method and all resolve to detail variants with complete eager loads, and the suite is green. But no manual UI walkthrough was done. A missed path would raise at render time rather than silently N+1. [PARTIALLY RESOLVED] Post-migration smoke test against the real 282-row corpus: a service-layer walk over all 8 documents and every source exercised list_documents, list_jobs, read_document, list_sources, read_source_navigation and list_processing_artifacts with no lazy-load error, and all six /ui pages returned HTTP 200. NiceGUI renders over websocket, so this is not a substitute for clicking through a live browser session, but every query path is now exercised against real data.
|
|
||||||
|
|
||||||
### V4.6 Phase 3 - worker and provider reliability
|
|
||||||
|
|
||||||
#### [6] httpx default timeout was the real bug
|
|
||||||
|
|
||||||
*comment* - `HIGH-03` - **noted**
|
|
||||||
|
|
||||||
The review said the 20s cap was too low. The actual defect was larger: httpx.AsyncClient was built with no timeout at all, so every phase defaulted to 5s and the outer asyncio.wait_for could never bind. Real read budget was 5s, not 20s.
|
|
||||||
|
|
||||||
#### [7] Your .env still pins WORKER_PROVIDER_TIMEOUT_SECONDS=20
|
|
||||||
|
|
||||||
*question* - `HIGH-03` - **answered**
|
|
||||||
|
|
||||||
I deliberately did not edit your .env. The new default is 180s but your local file overrides it. Do you want it raised, and to what value? [RESOLVED 2026-08-17] .env lines 51-52 replaced: the stale "[0-20]" comment is gone and WORKER_PROVIDER_TIMEOUT_SECONDS is now 180.0. Verified effective value via get_settings() = 180.0. .env is gitignored, so this is a local-only change with no commit. [REVISED 2026-08-18] User challenged 180.0 as too long to feel responsive. Queried the 80 migrated execution_attempt rows: 77 succeeded with max 18.5s, median 4.9s, p95 18.5s. The only 3 attempts over 20s are exactly the 3 local_timeout FAILURES (20.4/20.8/22.0s), so the distribution is CENSORED - the true duration of those 3 is unknown. Set to 60.0: ~3.2x the slowest observed success, while keeping time-to-discovery of a stall at one minute rather than three. User was right; evidence does not support 180.
|
|
||||||
|
|
||||||
#### [8] handle_worker_exceptions swallows everything
|
|
||||||
|
|
||||||
*comment* - **open**
|
|
||||||
|
|
||||||
A stale monkeypatched signature raised TypeError, which the handler suppressed, spinning run_worker_loop forever. Same hazard exists in production code: any programming error inside the loop becomes an infinite silent retry. Out of V4.6 scope.
|
|
||||||
|
|
||||||
### V4.6 Phase 4 - service layer consolidation
|
|
||||||
|
|
||||||
#### [9] Registry consolidation did not reduce line count much
|
|
||||||
|
|
||||||
*comment* - `MED-11` - **noted**
|
|
||||||
|
|
||||||
Public method names had to survive so registry tests could pass unchanged, so each service keeps thin delegating wrappers. Net production code is down ~150 lines overall, but the registry work itself is roughly break-even. The win is single-source-of-truth behavior, not brevity.
|
|
||||||
|
|
||||||
#### [10] MED-14 done only for documents.py
|
|
||||||
|
|
||||||
*deviation* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
store.py and workflows.py still import sources.py. Both are orchestration modules, which services.instructions.md:75-77 permits, so I scoped the boundary test to service-class modules only. Flagging in case you read the rule more strictly. [DEFERRED TO V4.7 by user, 2026-08-17] The MED-14 boundary test stays scoped to service-class modules for V4.6; store.py and workflows.py continue to import sources.py under the Service Composition allowance. V4.7 should decide whether to widen the rule and the AST guard.
|
|
||||||
|
|
||||||
#### [11] Two behavior changes in the media writer
|
|
||||||
|
|
||||||
*deviation* - `MED-13` - **answered**
|
|
||||||
|
|
||||||
mkdir failures now raise a domain error instead of escaping as raw OSError, and homepage image writes gained error handling they never had. Both are strictly better but are behavior changes, not pure refactors. [VERIFIED 2026-08-17] Grepped all of src/ for "except OSError": every hit is either the new media_storage.py:44 wrapper itself, a prompts.py file read, a normalization.py decode guard, or a best-effort cleanup/unlink (store.py:325-330, documents.py:259, jobs.py:349, sources.py:504). No caller wraps a media WRITE in except OSError, so translating mkdir failures into a domain error changes no existing handler behaviour.
|
|
||||||
|
|
||||||
#### [12] DocumentService print path no longer raises TranscriptionError
|
|
||||||
|
|
||||||
*deviation* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
_print_media_type raises DocumentError for an unsupported extension where it previously raised TranscriptionError. Only reachable with a corrupt stored filename. Changed because a DocumentService emitting a transcription error is itself the boundary leak MED-14 is about. [VERIFIED 2026-08-17] Grepped all of src/ for "except TranscriptionError": the single hit is store.py:353, which wraps validate_source_content on the UPLOAD path and is unrelated to the print projection in documents.py. No handler anywhere catches TranscriptionError around _print_media_type, so the class change is behaviourally inert. Also clarified to the user that this item is a disclosure of a change already made, not an outstanding error, and is unrelated to .env.
|
|
||||||
|
|
||||||
### V4.6 Phase 5 - UI boundaries and duplication
|
|
||||||
|
|
||||||
#### [13] document_panzoom: delete or fix?
|
|
||||||
|
|
||||||
*question* - `HIGH-07` - **answered**
|
|
||||||
|
|
||||||
Unused 170-line component plus ~35 lines of CSS, superseded in practice by dark_room_viewer. You chose to delete it now and rebuild it cleanly in the next revision alongside other photo/image features. ACTION FOR V4.7: pan-zoom must be reintroduced.
|
|
||||||
|
|
||||||
### V4.6 Phase 5 - UI boundaries and duplication
|
|
||||||
|
|
||||||
#### [14] sources_page transport_body deferral question (answered; premise corrected)
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Initially believed the deferred-body branch was dead. Re-check showed _transport_display() is called with latest_attempt from read_latest_execution_attempt(), which DOES defer transport_body. Current behavior is already "Omitted from Source Detail". Fix is therefore a pure boundary move: read_latest_execution_attempt returns a LatestExecutionAttempt read model carrying transport_body_deferred: bool, and sources_page drops sqlalchemy.inspect. No behavior change. User preference recorded: simplest, most supportable, most robust; full bytes remain persisted and retrievable via Export Evidence.
|
|
||||||
|
|
||||||
#### [15] Guard-message ordering changed on two Source routes
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
sources_page previously parsed the route id BEFORE rendering the navigation header, then rendered the invalid-id message after it. Adopting the shared parsed_record_id() helper moved the nav header above the parse. Net rendered output is identical; only the internal call order changed.
|
|
||||||
|
|
||||||
#### [16] Settings-page registry tables now render inside build_table
|
|
||||||
|
|
||||||
*deviation* - **answered**
|
|
||||||
|
|
||||||
The two label-registry tables on the settings page were hand-rolled ui.table calls. They now go through build_table via a new components/table/registry.py. build_table wraps its table in a ui.column, so the tables gain one extra container div. Search is disabled and rows-per-page stays 0, so visible behavior is unchanged. | RESOLVED (user directed consolidation): build_table gained row_key; linked_people.py converted; print_preview_page.py shares a local _render_print_table helper (print tables intentionally bypass build_table - no pagination, no search). New AST guard test_only_the_designated_owners_construct_a_raw_table pins ui.table() to exactly table/common.py and print_preview_page.py.
|
|
||||||
|
|
||||||
#### [17] Two hand-rolled ui.table instances deliberately left alone
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
print_preview_page.py has two print-layout tables and linked_people.py has a component-local editor table. Neither wants build_table search or pagination, so converting them would add indirection without removing duplication. Flagging in case you want them unified later. | CLOSED AS ENVIRONMENTAL: unreproduced after ~54 sequential full-suite runs (incl. a dedicated 25-run soak with -rA traceback capture, 0 failures) plus 5 concurrent-process runs (2x tests/ui, 3x full suite). Not attributable to any V4.6 change; the single observed failure occurred immediately after a burst of bulk file rewrites. No code change made. Re-open if it recurs.
|
|
||||||
|
|
||||||
#### [18] One flaky failure observed once, then five clean full runs
|
|
||||||
|
|
||||||
*risk* - **open**
|
|
||||||
|
|
||||||
tests/ui/test_jobs_page.py::test_job_delete_page_allows_deletion_for_queued_or_completed_job failed once and passed on every subsequent run (5 consecutive full-suite runs, 275 passed / 4 skipped). This matches the known pre-existing aiosqlite event-loop teardown noise that lands on a random test. Not introduced by Phase 5, but worth confirming during Phase 6/7.
|
|
||||||
|
|
||||||
#### [19] store.create_document_job / create_job_for_document now own their session
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
To remove session_scope from jobs_page, both orchestration functions accept an optional session plus an optional session_factory and open their own scope when neither is supplied. Existing callers that pass a session are unaffected; tests pass unchanged.
|
|
||||||
|
|
||||||
#### [20] Upload accept lists are now derived from SOURCE_EXTENSIONS
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
The job upload picker previously hard-coded .jpg,.jpeg,.png,.tif,.tiff,.pdf. It now derives the list from services.source_media.SOURCE_EXTENSIONS, so adding a Source format in one place updates the picker. The portrait and homepage pickers share a separate IMAGE_UPLOAD_EXTENSIONS list because they accept gif/webp/bmp, which are not valid Source formats.
|
|
||||||
|
|
||||||
### V4.6 Phase 6 - async I/O and configuration hygiene
|
|
||||||
|
|
||||||
#### [21] model_copy(update=...) rejected by pydantic-settings
|
|
||||||
|
|
||||||
*deviation* - `MED-04` - **noted**
|
|
||||||
|
|
||||||
Plan offered "model_copy(update=...) or a computed property" to replace object.__setattr__ in normalize_provider_models. model_copy failed: pydantic-settings warns "A custom validator is returning a value other than self ... isn't supported when validating via __init__" and 3 config tests failed. A computed property would have required renaming the env-facing provider_models field. Implemented as a model_validator(mode="before") over the raw input dict instead, so the derived value is produced by normal construction with no frozen-instance mutation. All 23 config tests pass.
|
|
||||||
|
|
||||||
#### [22] provider_model is now trimmed
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
The old object.__setattr__ path assigned provider_model without stripping whitespace; only the provider_models tuple entries were stripped. The before-validator now strips provider_model too. This is a behavior change, judged a correctness improvement since an untrimmed model id would be sent to the provider. No test asserted the old behavior.
|
|
||||||
|
|
||||||
#### [23] onupdate confirmed DDL-neutral
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
Plan said onupdate moves to Phase 2 if it alters emitted DDL. Verified by hashing CreateTable output for every table on both the sqlite and postgresql dialects before and after the change: identical (b33ad56a...). onupdate stays in Phase 6; Phase 2 does not need re-verification.
|
|
||||||
|
|
||||||
#### [24] No-op updates no longer bump the timestamp
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
Removing the 10 manual "updated_at = datetime.now(UTC)" assignments means an update call that changes nothing no longer marks the row dirty, so onupdate does not fire and the timestamp stays put. Previously the manual assignment always bumped it. Judged more correct for a column that is supposed to track modification, but it is an observable change for any caller that relied on update-as-touch.
|
|
||||||
|
|
||||||
#### [25] Homepage markdown I/O left unwrapped
|
|
||||||
|
|
||||||
*question* - `MED-01` - **answered**
|
|
||||||
|
|
||||||
ui/homepage_store.py reads and writes a single small local markdown file synchronously from home_page.py handlers. MED-01 names Pillow normalization, artifact writes, and evidence hashing; this is none of those and the payload is trivial. Left unwrapped to avoid scope creep. Flagging in case you want it wrapped anyway. [RESOLVED 2026-08-17] User decision: leave it synchronous. No change made.
|
|
||||||
|
|
||||||
#### [26] Evidence manifest hashing left on the loop
|
|
||||||
|
|
||||||
*comment* - `MED-01` - **noted**
|
|
||||||
|
|
||||||
providers/evidence.py digest() hashes a small in-memory JSON manifest (microseconds), so it was left inline. The hashing that actually mattered was over page-sized image bytes: the derivative digest is now precomputed inside normalize_orientation (already off-loop) and the artifact digest now shares the same worker-thread hop as the write.
|
|
||||||
|
|
||||||
#### [27] dispose_engine on an unknown URL changed behavior
|
|
||||||
|
|
||||||
*comment* - `MED-04` - **noted**
|
|
||||||
|
|
||||||
The old functools.cache version called get_engine(url) inside dispose_engine, which would construct an engine just to dispose it, and then cache_clear() wiped every other engine too. The registry version pops only the requested URL and no-ops on an unknown one. Covered by tests/test_engine_registry.py.
|
|
||||||
|
|
||||||
#### [28] Added homepage_dir setting (user-approved scope addition)
|
|
||||||
|
|
||||||
*deviation* - **answered**
|
|
||||||
|
|
||||||
ui/homepage_store.py was the only storage path in the codebase derived from Path(__file__).parents[3] rather than from Settings, making it unconfigurable and wrong under a wheel install (it would resolve into site-packages). Not tied to a review finding ID, so it is a deliberate scope addition, approved by the user in-flight. Added Settings.homepage_dir (default ./data/homepage) and rewrote the module to resolve from Settings, with an optional settings parameter on every function. Covered by tests/ui/test_homepage_store.py.
|
|
||||||
|
|
||||||
#### [29] homepage default is now CWD-relative
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
The old default resolved to <repo>/data/homepage regardless of working directory. The new default Path("./data/homepage") is relative to the process CWD, matching artifact_dir and upload_dir. Running the app from the repo root gives the identical location; running it from elsewhere does not. Consistent with every other storage root, but worth confirming against your deployment/launch scripts. [RESOLVED 2026-08-17] User confirmed the app is only ever launched from the repo root, so CWD-relative ./data/homepage and the old repo-anchored path are identical. Verified live: resolves to C:\GitHub\transcription\data\homepage containing the real homepage.md and portrait. No change needed. Revisit only if a service or scheduled task with its own working directory is introduced.
|
|
||||||
|
|
||||||
#### [30] Homepage markdown I/O stays synchronous
|
|
||||||
|
|
||||||
*comment* - `MED-01` - **answered**
|
|
||||||
|
|
||||||
User question resolved: the async-wrapping question was dropped as negligible (one small local markdown file). The underlying concern turned out to be the hardcoded storage path, addressed separately via Settings.homepage_dir.
|
|
||||||
|
|
||||||
### V4.6 Phase 7 - type checking and quality gate
|
|
||||||
|
|
||||||
#### [31] selectinload varargs is not equivalent to chaining
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
selectinload(A.b, B.c) and selectinload(A.b).selectinload(B.c) produce an identical .path but the varargs form applies the selectin strategy ONLY to the last element. With lazy="raise" everywhere (Phase 2) the varargs form raises InvalidRequestError at render time. Cost 12 test failures before it was caught. Documented in the db/loading.py docstring.
|
|
||||||
|
|
||||||
#### [32] New module src/transcription/db/loading.py
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
Rather than sprinkle 42 suppressions, the SQLModel-field to QueryableAttribute reinterpretation now has one documented home: orm_attribute(), selectinload(), defer(). All 42 "# pyright: ignore[reportArgumentType]" comments in documents/jobs/people/sources were removed as a result.
|
|
||||||
|
|
||||||
#### [33] transaction_scope no longer accepts or yields AsyncSessionTransaction
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
AsyncSessionTransaction appeared nowhere outside db/session.py; no caller ever passed one, and sessionmaker.begin() was verified at runtime to yield an AsyncSession. The branch was also latently buggy: services call .exec() which a transaction object does not have. Removing the union cleared 7 downstream workflows.py diagnostics.
|
|
||||||
|
|
||||||
#### [34] RegistryService is now bound by a RegistryEntry Protocol
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
RegistryService[ModelT: SQLModel] gave ty no visibility into id/label/normalized_label/is_active. A structural Protocol replaces the bare SQLModel bound - a genuine typing improvement rather than a suppression. Cleared 9 diagnostics.
|
|
||||||
|
|
||||||
#### [35] normalization.py now uses isinstance(image, TiffImageFile) instead of image.format == "TIFF"
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
tag_v2 only exists on TiffImageFile. The isinstance check is semantically equivalent and types correctly.
|
|
||||||
|
|
||||||
#### [36] linked_people.render switched from @ui.refreshable to @ui.refreshable_method
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
refreshable_method is the NiceGUI API intended for bound methods; the plain decorator mistyped self. render.refresh() call sites are unchanged.
|
|
||||||
|
|
||||||
#### [37] read_source_navigation now wraps literal bounds in sqlalchemy.literal()
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
tuple_() rejects raw Python values under typing. literal() is the correct explicit coercion and preserves the emitted SQL.
|
|
||||||
|
|
||||||
#### [38] openrouter capturing client re-raises ResponseNotRead for a sync stream
|
|
||||||
|
|
||||||
*deviation* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
response.stream is typed SyncByteStream | AsyncByteStream. The narrowing guard re-raises rather than silently mis-wrapping, which is the honest behavior on an async client.
|
|
||||||
|
|
||||||
#### [39] No pre-commit config existed; one was created
|
|
||||||
|
|
||||||
*comment* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
The plan said "wire it into the existing pre-commit setup", but there was no .pre-commit-config.yaml (pre-commit was only a dev dependency, and there are no CI workflows either). A local-repo config with blocking ruff and ty hooks was created and negative-tested. NOTE: hooks use language: system, so the venv Scripts dir must be on PATH.
|
|
||||||
|
|
||||||
#### [40] No CI workflow enforces the gate
|
|
||||||
|
|
||||||
*risk* - `HIGH-06` - **open**
|
|
||||||
|
|
||||||
.github/workflows/ is empty, so ruff/ty/pytest are only enforced locally via pre-commit, and only if the developer has installed the hooks (pre-commit install). Consider adding a CI workflow in a later release.
|
|
||||||
|
|
||||||
#### [41] ty check driven from 207 diagnostics to 0
|
|
||||||
|
|
||||||
*comment* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
Two real bugs were fixed en route: tools/run_destructive_tests.py imported ctypes.wintypes at module scope (raising on non-Windows) and used fcntl unconditionally; tests/ui/test_sources_page.py constructed Source(...) without the required document_id. Only two suppressions remain in the whole tree: one "# ty: ignore[invalid-assignment]" in tests/test_prompts.py which deliberately assigns to a frozen field to assert ValidationError.
|
|
||||||
|
|
||||||
#### [42] asyncio_default_fixture_loop_scope pinned to "function"
|
|
||||||
|
|
||||||
*comment* - `HIGH-06` - **noted**
|
|
||||||
|
|
||||||
Set explicitly in pyproject.toml so pytest-asyncio behavior does not shift on upgrade.
|
|
||||||
|
|
||||||
### V4.6 Phase 8 - data migration
|
|
||||||
|
|
||||||
#### [43] V4.6 re-level changed no columns at all
|
|
||||||
|
|
||||||
*comment* - `review 1a` - **noted**
|
|
||||||
|
|
||||||
Diffing the backup schema against the current SQLModel metadata showed identical table sets and identical column sets for all 10 tables. What V4.6 actually changed is index coverage (9 new indexes: ix_document_document_type_id, ix_document_person_document_id, ix_document_person_person_id, ix_document_person_role_id, ix_job_document_id, ix_job_source_job_id, ix_job_source_source_id, ix_job_status_date_created, ix_source_document_id - none lost), the use_alter break in the FK cycle, and the relationship loading strategy. The migration is therefore a faithful FK-ordered row copy rather than a transformation.
|
|
||||||
|
|
||||||
#### [44] Migration reads the backup with raw sqlite3, not the ORM
|
|
||||||
|
|
||||||
*deviation* - `review 1a` - **noted**
|
|
||||||
|
|
||||||
The plan anticipated ORM reads carrying explicit eager loads under lazy="raise". Reading raw rows is strictly safer: the V4.5 file is not guaranteed to satisfy the V4.6 mappers, and no relationship is ever traversed, so lazy="raise" cannot bite at all. Writes still go through SQLAlchemy Core against the live metadata, so the script will work against PostgreSQL unchanged.
|
|
||||||
|
|
||||||
#### [45] The same JobSourceStatus enum is persisted two different ways
|
|
||||||
|
|
||||||
*risk* - **open**
|
|
||||||
|
|
||||||
job_source.status declares values_callable and stores lowercase VALUES ("transcribed"); execution_attempt.status does not and stores uppercase NAMES ("TRANSCRIBED"). Both columns use the identical JobSourceStatus enum. This is a genuine latent inconsistency: any raw SQL, reporting query, or future cross-dialect move has to know which spelling each column uses. It is NOT a finding in the review, so under the pure-remediation rule I did not change it - the migration accepts either spelling and round-trips both faithfully. RECOMMEND scheduling this for V4.7.
|
|
||||||
|
|
||||||
#### [46] Should the migration be applied to the live data/transcription.db?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
The script is fully verified against a throwaway target: 282 rows copied, every table byte-identical to the backup cell-for-cell, idempotent re-run inserts 0, artifact integrity passes, no on-disk file touched. The live data/transcription.db currently holds only bootstrap seed rows (document_type 6, person_role 3) whose UUIDs differ from the backup, so a straight migration would ADD the backup rows alongside the seeds and likely trip the normalized_label uniqueness constraint. Applying cleanly requires replacing the live file. Awaiting user decision. [RESOLVED] User chose to back up and replace. data/transcription.db.seed-20260817-200555.bak holds the old seed file; a fresh DB was created and all 282 rows migrated with artifact integrity verified.
|
|
||||||
|
|
||||||
#### [47] Provider timeout set to 60s on evidence, not on the review's suggested figure
|
|
||||||
|
|
||||||
*comment* - `HIGH-03` - **noted**
|
|
||||||
|
|
||||||
The review recommended 120s and the V4.6 plan used 180s, both chosen without data. The migrated corpus provides data: 77/80 attempts succeeded, all within 18.5s. 60s is the smallest value with real headroom that still surfaces a stall quickly. Revisit only if a genuine local_timeout occurs at 60s.
|
|
||||||
|
|
||||||
#### [48] Three historical local_timeout failures are worth re-running
|
|
||||||
|
|
||||||
*deviation* - **answered**
|
|
||||||
|
|
||||||
All 3 FAILED execution_attempts were killed by the old 20s ceiling and carry response_received=1, meaning a response had begun arriving when the budget expired. With the ceiling now at 60s these three pages may well succeed on a retry. Their evidence rows were migrated unchanged, so the originals are preserved either way. | RESOLVED 2026-08-17: not 3 pages but ONE page (source 302aa684) x 3 models. Re-ran each model 2x with a 300s uncensored ceiling: gemini-flash 9.0/22.5s, claude-opus-5 27.0/27.6s, gpt-5.6 64.5/75.3s. All 6 succeeded - no hangs. gpt-5.6 exceeds the 60s value that was set, so .env raised to 120.0 (~1.6x slowest success). Historical stats were ~96% gemini-flash and understated the budget.
|
|
||||||
|
|
||||||
#### [49] Reporting gap: 28 review_log entries were never surfaced to the user
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
My end-of-run summaries filtered on status IN (open, answered), which silently excluded every entry recorded as "noted" - 28 of 46. The user caught this. All 28 are now presented. Lesson: "noted" is not the same as "reported".
|
|
||||||
|
|
||||||
### Post-V4.6 - timeout calibration and tuning
|
|
||||||
|
|
||||||
#### [50] Worst-case stall latency is now 60s (2 x 30s), down from 360s
|
|
||||||
|
|
||||||
*risk* - `HIGH-03` - **open**
|
|
||||||
|
|
||||||
Superseded by the 2026-08-18 calibration: WORKER_MAX_RETRIES=1 and WORKER_PROVIDER_TIMEOUT_SECONDS=30.0 give a worst case of 60s. The underlying concern stands but is much reduced: handle_worker_exceptions (review_log id 8) still swallows every exception, so a programming error would burn 2 attempts silently with no UI feedback. Keep id 8 as the real fix.
|
|
||||||
|
|
||||||
#### [51] Removed WORKER_RETRY_BACKOFF_SECONDS from .env
|
|
||||||
|
|
||||||
*comment* - `HIGH-03` - **noted**
|
|
||||||
|
|
||||||
The setting was deleted from Settings in Phase 1 (LOW-05). Because Settings uses extra="ignore" it sat in .env silently inert, which is exactly the DATABASE_URL trap the review flagged. Removed from .env so the file matches the model. No behavior change.
|
|
||||||
|
|
||||||
#### [52] Timeout set to 30.0s and max_retries to 1 by user decision
|
|
||||||
|
|
||||||
*comment* - `HIGH-03` - **answered**
|
|
||||||
|
|
||||||
Full dropdown measured twice on the densest page in the corpus with a 300s uncensored ceiling. Fast cluster: gemini-2.5-flash 9.0/22.5, gpt-4o 21.6/22.9, claude-sonnet-4 26.2/26.7, claude-opus-5 27.0/27.6. Slow cluster: gemini-2.5-pro 49.5/79.4, gpt-5.6 64.5/75.3. User chose 30.0s at the low edge of the 27.6-49.5s gap because dense forms are <10 of ~3k documents and ejecting a stalled outlier is preferred over waiting. Worst case is now 2x30=60s. Agent recommended 40s for margin; user declined with stated rationale. Accepted.
|
|
||||||
|
|
||||||
#### [53] PROVIDER_MODELS still offers two models that cannot finish a dense page within 30s
|
|
||||||
|
|
||||||
*risk* - `HIGH-03` - **open**
|
|
||||||
|
|
||||||
gemini-2.5-pro and gpt-5.6 remain selectable in the jobs page dropdown (ui/pages/jobs_page.py:146 reads settings.provider_models). Both exceed 30s on dense forms by design of the chosen budget, though both should still succeed on the ~99.7% of pages that are not dense forms. Left in the list deliberately - not removed - so the user retains them for quality comparison. Revisit if dense-form failures become noisy.
|
|
||||||
|
|
||||||
### Post-V4.6 - V4.7 candidates identified
|
|
||||||
|
|
||||||
#### [54] Run-time telemetry is captured but has no aggregate view
|
|
||||||
|
|
||||||
*comment* - **open**
|
|
||||||
|
|
||||||
execution_attempt.duration_ms is a required non-null field written on all three paths in services/workflows.py (success 278, TimeoutError 295, general failure 330); failures use a monotonic clock, so timeout durations are trustworthy. started_at/finished_at are also stored, and normalized_metadata.usage carries token counts on the same row, so tokens/sec is already derivable per attempt. Gaps: (1) sources_page.py:400 renders it raw as "27612 ms" rather than seconds; (2) it is only visible for the latest attempt of one source at a time - there is no rollup, so answering "which model is slow" required hand-written SQL against the database. A small model-performance rollup is a V4.7 candidate.
|
|
||||||
|
|
||||||
#### [55] duration_ms measures end-to-end page processing, not provider latency
|
|
||||||
|
|
||||||
*comment* - **open**
|
|
||||||
|
|
||||||
services/workflows.py:221 sets monotonic_started_at BEFORE provider_input preparation (image normalization, artifact persistence, session.commit() at line 228), and line 251 computes elapsed_seconds from it. But the asyncio.wait_for timeout at lines 240-249 wraps ONLY _call_transcriber. So duration_ms covers a strictly wider window than the budget that governs it. Empirical proof: the three historical local_timeout rows recorded 20.4/20.8/22.0s against a 20.0s timeout, i.e. roughly 0.4-2.0s of non-provider work is folded in. Consequence: duration_ms cannot be used to isolate provider performance, and any model-performance rollup built on it would be polluted by preprocessing time that varies with image size. V4.7 candidate: record provider latency as a separate column, or move monotonic_started_at to just before the wait_for.
|
|
||||||
|
|
||||||
### V4.7 planning
|
|
||||||
|
|
||||||
#### [56] Release split agreed: V4.7 = architectural cleanup, V4.8 = features
|
|
||||||
|
|
||||||
*comment* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
User asked whether the sources.py decomposition (architectural) should be separated from pan-zoom and photo work (features). Agreed and documented. Rationale: V4.6 succeeded because it had a binary gate - behavior-identical, suite unchanged. A refactor can be held to that standard; features cannot, since they require new tests. Bundling them destroys the ability to attribute a test delta to a bug versus expected new behavior. The two also touch disjoint trees under different instruction files (services vs ui). Created docs/ver4.7/scope_boundary_v4_7.md, docs/ver4.7/implementation_plan_v4_7.md, docs/ver4.8/feature_backlog_v4_8.md. All cross-links verified.
|
|
||||||
|
|
||||||
#### [57] Rejected the original proposal to move update_job_source_transcription to workflows.py
|
|
||||||
|
|
||||||
*deviation* - `MED-14` - **noted**
|
|
||||||
|
|
||||||
The V4.6 Phase 5 deferral note suggested moving it as orchestration. Rejected in the V4.7 boundary. services.instructions.md:63-65 requires transcript updates and the paired terminal status change to commit or roll back together, and the method writes JobSource plus ExecutionAttempt in one session scope, deriving attempt_number from ExecutionAttempt at lines 595-600. Line 72 assigns session-aware write helpers to services and commit-boundary control to orchestration, so moving a multi-table write into workflows.py inverts the stated architecture. Scope reduced from three moves to two (artifacts.py, evidence.py); expected sources.py ~930 lines rather than the deeper cut originally implied.
|
|
||||||
|
|
||||||
#### [58] Pan-zoom renumbered from V4.7 to V4.8, intent preserved
|
|
||||||
|
|
||||||
*comment* - `HIGH-07` - **noted**
|
|
||||||
|
|
||||||
Commit 6a3ee26 states pan-zoom would return "in V4.7 alongside the other photo/image work". It now sits in the V4.8 backlog. The commit intent was grouping with the photo work, not the specific number, and that grouping is preserved. Recorded in the V4.8 backlog so the git history is not silently contradicted. Also noted there: document_panzoom.py was exported but wired to no page, so no user has seen it - which makes reintroduction a new feature rather than a restoration, and is what puts it on the feature side of the split.
|
|
||||||
|
|
||||||
#### [59] Service ownership model for ExecutionAttempt / ProcessingArtifact is undecided
|
|
||||||
|
|
||||||
*question* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
services.instructions.md:11 says "1 service class per data model" but there are 10 persisted models and 6 service classes. The 4 unnamed models landed arbitrarily: DocumentPerson->people.py, and JobSource + ExecutionAttempt + ProcessingArtifact all -> sources.py. Line count tracks model count: jobs.py 438L (1 model), documents.py 473L (1+registry), people.py 554L (2+registry), sources.py 1389L (4). Evidence gathered 2026-08-18: both tables were added in V4.2 commit 6bd4cbb ("Updated what ai_raw_response data is being captured"), i.e. AFTER the 4-component design. ExecutionAttempt is docstringed "Immutable evidence for one provider call attempt" (request manifest, transport body, router ids, sdk snapshot, software context, timing); ProcessingArtifact is "Provider-neutral, versioned output derived from a Source" with a XOR CheckConstraint on inline vs external content, and a NULLABLE execution_attempt_id, so an artifact can exist with no attempt. Both are append-only provenance, not mutable domain entities. Four options were put to the user (evidence-as-own-subsystem; evidence split with ProcessingArtifact under Source; strict lifecycle into JobService/SourceService; draft the revised instructions first). USER DEFERRED - continuing the discussion interactively, formulating further questions. Do not proceed with V4.7 Phase 1/2 until this is settled, since the chosen model determines the module split.
|
|
||||||
|
|
||||||
#### [60] V4.7 boundary overstates the case against moving update_job_source_transcription
|
|
||||||
|
|
||||||
*deviation* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
The scope boundary as written says moving it to workflows.py would violate services.instructions.md. On re-reading, lines 38-47 (the _finalize contract: commit when service-owned, flush when caller-owned) and line 72 describe exactly the mechanism that makes a multi-service atomic write safe, so the document permits it. The honest objection is weaker: keeping the paired JobSource + ExecutionAttempt write in one method makes atomicity enforced by locality, whereas splitting it makes atomicity depend on every future caller sharing the session correctly. That is a robustness argument, not a rule violation. Correct the wording in docs/ver4.7/scope_boundary_v4_7.md section 1 before that document is treated as frozen.
|
|
||||||
|
|
||||||
### V4.7 design - evidence model simplification
|
|
||||||
|
|
||||||
#### [61] job_source duplicates execution_attempt columns byte-for-byte
|
|
||||||
|
|
||||||
*comment* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
Measured on live DB: job_source.raw_transcription 77/77 identical to latest attempt; ai_metadata vs normalized_metadata 77/77; raw_api_response vs sdk_response_snapshot 77/77; error_detail 2/2. The table split itself is justified by cardinality (1:N attempts) but the 1:N is exercised in only 1 of 79 job_source rows. The 4 duplicated columns are an undocumented, unenforced denormalized cache.
|
|
||||||
|
|
||||||
#### [62] status mismatch cross-confirms the enum persistence defect
|
|
||||||
|
|
||||||
*risk* - `45` - **answered**
|
|
||||||
|
|
||||||
job_source.status vs execution_attempt.status compared 0/79 identical - job_source stores lowercase (transcribed), execution_attempt stores uppercase (TRANSCRIBED). Independent confirmation of finding [45].
|
|
||||||
|
|
||||||
#### [63] Orientation normalization was NOT a red herring - proven visually
|
|
||||||
|
|
||||||
*comment* - `ProcessingArtifact` - **answered**
|
|
||||||
|
|
||||||
59 of 79 source images carry EXIF orientation=3 (rotate 180). Rendered the exact page the user described (Pioneer Days page 00, a typed table of contents): raw decoded pixels are genuinely upside down; the 180-rotated version is upright. So sending raw bytes did send an inverted page to the model. resolve_provider_input (workflows.py:226 -> sources.py:850) is on the current hot path, so normalization now runs, but only 1 orientation artifact exists - the other 58 rotated pages were transcribed before normalization was wired.
|
|
||||||
|
|
||||||
#### [64] job_source cannot be deleted - it is the work queue, not just a junction
|
|
||||||
|
|
||||||
*risk* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
store.py:249,313 create JobSource with status=PENDING at job creation, before any provider call. workflows.py:438-440 selects pending work by status != TRANSCRIBED. jobs.py:411-424 retry mutates FAILED back to PENDING and clears fields. jobs.py:378-384 cancel writes FAILED/Cancelled by user with NO provider call, so no execution_attempt row could exist to carry it. An append-only table cannot express queued-not-yet-attempted or cancelled-before-call. Recommend STRIP not DELETE: keep (id, job_id, source_id, status); drop raw_transcription, ai_metadata, raw_api_response, executed_at.
|
|
||||||
|
|
||||||
#### [65] Entire artifact subsystem has executed exactly once
|
|
||||||
|
|
||||||
*comment* - `ProcessingArtifact` - **answered**
|
|
||||||
|
|
||||||
Only 2 rows exist, both from the same job on 2026-08-16 (13:59:57 orientation, 14:00:07 quality). workflows.py:560-571 writes a transcription_quality_warnings artifact on EVERY successful page, yet 77 successful transcriptions produced 1 row - so the code path postdates nearly all data. Ingest already copies bytes via media_storage.py:57 write_bytes, so normalize-at-upload is viable. Caveat: Pillow re-encodes JPEG at quality 95, a permanent generational loss for an archival corpus - recommend retaining original bytes as a sibling file.
|
|
||||||
|
|
||||||
#### [66] Retry history already exists in execution_attempt - no resubmitted flag needed
|
|
||||||
|
|
||||||
*comment* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
ExecutionAttempt UniqueConstraint(job_id, source_id, attempt_number) at models.py:389 already implements keep-the-failed-row-and-add-a-new-one. Proven in live data: attempt 1 FAILED/local_timeout 20.4s and attempt 2 TRANSCRIBED 13.6s both retained. Adding a second job_source row would duplicate that and break the one-row-per-(job,page) assumption in read_job_source_for_job and sources.py:570-574, where uniqueness is enforced in CODE not by a DB constraint.
|
|
||||||
|
|
||||||
#### [67] Add JobSourceStatus.CANCELLED to retire job_source.error_detail
|
|
||||||
|
|
||||||
*comment* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
Cancel currently overloads FAILED plus free text Cancelled by user (jobs.py:378-384). A distinct CANCELLED status separates user cancellation from genuine provider failure and removes the last consumer of job_source.error_detail, reducing job_source from 9 columns to 4: id, job_id, source_id, status.
|
|
||||||
|
|
||||||
#### [68] Lossless 180-degree JPEG rotation is viable for 57 of 58 rotated images
|
|
||||||
|
|
||||||
*comment* - `ProcessingArtifact` - **answered**
|
|
||||||
|
|
||||||
Pillow always round-trips through decoded pixels (normalization.py:76-86), so quality=95 re-encode loss is inherent to the library, not required by the task. A 180 rotation is expressible as a lossless DCT transform when both dimensions are multiples of the 16px MCU. Measured across the corpus: 57/58 qualify; the sole exception is 2306x2019. Alternative that needs no new dependency: normalize at upload and retain the original bytes as the archival master.
|
|
||||||
|
|
||||||
#### [69] Quantization-table reuse beats both current settings and the lossless-DCT route
|
|
||||||
|
|
||||||
*comment* - `ProcessingArtifact` - **answered**
|
|
||||||
|
|
||||||
Measured single-generation rotate-and-restore on 5 rotated JPEGs. Current settings (quality=95, subsampling=0, normalization.py:84-85): PSNR 50.0-53.5 dB, file size +38 percent. Reusing the source quantization tables and subsampling (qtables=im.quantization, subsampling=JpegImagePlugin.get_sampling(im), optimize=True): PSNR 51.5-55.0 dB, max channel delta 7-9/255, file size slightly SMALLER (636KB->595KB). Better on quality and size simultaneously. Critically it works at any dimensions, so the 2306x2019 MCU-misaligned outlier needs no rejection path - the edge case only exists on the lossless-jpegtran route, which would also require an external C binary. Recommend Pillow with qtables reuse; drop the lossless-DCT option.
|
|
||||||
|
|
||||||
#### [70] Evidence-model simplification decisions settled by user
|
|
||||||
|
|
||||||
*comment* - `DECISIONS` - **answered**
|
|
||||||
|
|
||||||
1) job_source is STRIPPED not deleted - keeps id, job_id, source_id, status (9 columns to 4). Drop raw_transcription, ai_metadata, raw_api_response, executed_at, error_detail. All evidence reads move to execution_attempt. 2) Add JobSourceStatus.CANCELLED so cancel no longer overloads FAILED plus free text, retiring error_detail. 3) Retry keeps its current FAILED-to-PENDING reset - history already lives in execution_attempt via UniqueConstraint(job_id, source_id, attempt_number). 4) PENDING-at-job-creation is unchanged. 5) ProcessingArtifact table REMOVED; orientation normalization moves to upload/ingest; transcription_quality_warnings payload folds into execution_attempt.normalized_metadata. 6) Rotation uses Pillow with qtables + subsampling reuse (visually lossless, ~52 dB PSNR, no size growth, no external dependency, no MCU rejection path). No archival master retained. 7) One-time backfill of the 58 already-ingested EXIF-orientation-3 images.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md) - finding IDs
|
|
||||||
- [V4.6 Scope Boundary](scope_boundary_v4_6.md)
|
|
||||||
- [V4.6 Implementation Plan](implementation_plan_v4_6.md)
|
|
||||||
- [V4.7 Scope Boundary](../ver4.7/scope_boundary_v4_7.md)
|
|
||||||
- [V4.7 Implementation Plan](../ver4.7/implementation_plan_v4_7.md)
|
|
||||||
- [V4.8 Feature Backlog](../ver4.8/feature_backlog_v4_8.md)
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
# 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](../architecture_code_review_2026-08-17.md).
|
|
||||||
|
|
||||||
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.md` and `ui.instructions.md`.
|
|
||||||
- Make `ty` usable 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.py` is deleted. It has zero importers and contains a guaranteed `TypeError` ([HIGH-01]).
|
|
||||||
- `src/transcription/services/transcription.py` is deleted; `build_prompt_execution` has exactly one import path ([MED-05]).
|
|
||||||
- The legacy compatibility aliases in `services/store.py` are deleted ([MED-05]).
|
|
||||||
- `ServiceBase.queue` is deleted; no service allocates an unused `asyncio.Queue` ([MED-07]).
|
|
||||||
- `sqlite_check_same_thread` and `worker_retry_backoff_seconds` are either wired to real behavior or deleted, along with their tests ([MED-02]).
|
|
||||||
- `db/operations.py:get_next_queued_job` is deleted as a divergent duplicate ([CRIT-01]).
|
|
||||||
- `DATABASE_URL` is removed from `docker-compose.yml`, and the real nested `DATABASE__*` names are documented. The application never silently ignores a database configuration variable ([MED-10]).
|
|
||||||
- `document_panzoom` is 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_schema` and 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 existing `Settings.should_bootstrap_schema` ([HIGH-05]).
|
|
||||||
- The hand-written `CHAR(32)` column for `preferred_execution_attempt_id` ceases to exist; the column type is whatever the model declares ([HIGH-05]).
|
|
||||||
- A composite index on `Job.status, Job.date_created` is declared in the model, plus `index=True` on the foreign keys the worker and detail pages filter on ([HIGH-04]).
|
|
||||||
- `Source.preferred_execution_attempt_id` declares its foreign key with `use_alter=True`, resolving the `source` / `job_source` / `execution_attempt` cycle so `create_all` will succeed on PostgreSQL when that cutover is taken ([HIGH-08]).
|
|
||||||
- Relationship loading defaults change from bidirectional `lazy="selectin"` to `lazy="raise"`, with per-query `selectinload()` 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_job` gains `LIMIT 1` and stops materializing the entire queue plus its eager graph on every poll ([CRIT-01]).
|
|
||||||
- The claim becomes an atomic `QUEUED` → `PROCESSING` transition. On SQLite this is a bounded single-writer transaction; the `FOR UPDATE SKIP LOCKED` path 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]).
|
|
||||||
- `ServiceBundle` and the provider client are hoisted to worker-loop scope so the HTTP connection pool and TLS session survive across jobs ([HIGH-02], [MED-06]).
|
|
||||||
- `ServiceBundle` gains a `from_session_factory` constructor, replacing three duplicated instantiation blocks ([MED-06]).
|
|
||||||
- The `le=20.0` cap on `worker_provider_timeout_seconds` is removed, the default is raised, and an explicit `httpx.Timeout` is passed to the OpenRouter client ([HIGH-03]).
|
|
||||||
- The `TranscriptionProvider` Protocol is extended to cover `aclose` and the evidence attributes; the per-call `inspect.signature` reflection at `sources.py:1237` is 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. `DocumentType` and `PersonRole` become 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]).
|
|
||||||
- `ServiceBase` gains `_get_or_raise`, and all 38 hand-written not-found guards adopt it, including the three in `documents.py` that already bypass the local helper ([MED-12]).
|
|
||||||
- `services/media_storage.py` becomes the single implementation of validate → hash → write → wrap-error, replacing `store_source_file`, `store_person_portrait`, and the homepage image writer ([MED-13]).
|
|
||||||
- `source_mime_type` moves out of `services/sources.py` to a shared module so `documents.py` no longer imports a sibling service ([MED-14], partial).
|
|
||||||
- The four query inefficiencies in `sources.py` are corrected: the `job_id` filter moves into SQL, navigation uses two bounded queries, `list_processing_artifacts` gains a `limit`, 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.cache` on the engine and session factories is replaced with an explicit URL-keyed registry supporting targeted eviction ([MED-04]).
|
|
||||||
- The `object.__setattr__` mutation of a frozen `Settings` model in `normalize_provider_models` is replaced with `model_copy(update=...)` or a computed property ([Pydantic V2 §3]).
|
|
||||||
- `models.py` timestamp columns that are expected to track modification gain `onupdate`, so `updated_at` and `date_updated` stop being stale on paths that do not set them by hand ([SQLModel §3]).
|
|
||||||
- The exception swallowed to `None` in an ORM model property is surfaced ([MED-08]).
|
|
||||||
|
|
||||||
### 7. UI Boundary and Duplication
|
|
||||||
|
|
||||||
- The three `ui.instructions.md` violations are corrected ([HIGH-07]):
|
|
||||||
- `jobs_page.py` no longer imports `session_scope` or manages transactions; a service or workflow method owns the session.
|
|
||||||
- `sources_page.py` no longer imports `sqlalchemy.inspect`; the service returns a plain `transport_body_deferred` flag on a read model.
|
|
||||||
- `document_panzoom` no longer calls `get_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 use `build_table` (~500 lines).
|
|
||||||
- The 23KB inline SVG moves to `ui/static/` and is loaded through an `importlib.resources` reader alongside the existing `read_css` ([MED-09]).
|
|
||||||
- `people_page.py:504` routes its error through `error_presenter.show_error` like 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 ~160 `selectinload` diagnostics by removing redundant eager loads.
|
|
||||||
- `ty check` reaches 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:25` constructs `Source(...)` without the required `document_id`, and `tools/run_destructive_tests.py:76,80` uses `fcntl`, which does not exist on the Windows development platform ([HIGH-06]).
|
|
||||||
- `ruff check` reaches zero errors ([LOW-01]).
|
|
||||||
- `asyncio_default_fixture_loop_scope` is configured explicitly so pytest-asyncio behavior does not change on upgrade ([Testing §3]).
|
|
||||||
- The stale path in `.github/instructions/services.instructions.md:10` is corrected to `src/transcription/db/models.py` ([LOW-02]).
|
|
||||||
- `list_jobs` stops accepting and discarding `load_docs` ([LOW-03]).
|
|
||||||
- `resolve_worker_notifier` validates its `getattr` result ([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_alter` fix 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 `SourceService` into per-model services and relocating `update_job_source_transcription` to `workflows.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 `ExecutionAttempt` or `ProcessingArtifact` evidence 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
|
|
||||||
|
|
||||||
1. `app_state.py`, `services/transcription.py`, the `store.py` aliases, `ServiceBase.queue`, and `db/operations.py:get_next_queued_job` no longer exist, and the full suite passes without them.
|
|
||||||
2. `upgrade_schema` and the three `_upgrade_*` functions no longer exist; no raw `ALTER TABLE` or `CREATE INDEX` string appears in `src`.
|
|
||||||
3. A database created from empty by `create_all()` contains the composite `Job` index, indexed hot foreign keys, and a `preferred_execution_attempt_id` column whose type matches the model declaration.
|
|
||||||
4. Compiling the metadata against the PostgreSQL dialect emits **no** unresolvable-cycle warning.
|
|
||||||
5. No `Relationship` in `db/models.py` uses `lazy="selectin"` as a bidirectional default; every load path that requires eager loading declares it per query, and the suite passes under `lazy="raise"`.
|
|
||||||
6. `read_next_queued_job` returns at most one row and issues no eager-load queries; a test asserts the emitted SQL contains `LIMIT`.
|
|
||||||
7. The worker processes two consecutive jobs against a single provider client instance; a test asserts the client is not reconstructed between jobs.
|
|
||||||
8. `worker_provider_timeout_seconds` accepts a value above 20 seconds, and the OpenRouter client receives an explicit `httpx.Timeout`.
|
|
||||||
9. `inspect.signature` no longer appears in the transcription call path.
|
|
||||||
10. `DocumentType` and `PersonRole` CRUD is served by one shared implementation; the existing registry tests for both pass unchanged.
|
|
||||||
11. `ServiceBase._get_or_raise` is the only place a `NOT_FOUND` guard is written for an entity fetched by id.
|
|
||||||
12. One media-storage implementation serves Source files, portraits, and homepage images, and its write is off the event loop.
|
|
||||||
13. `list_sources_detail` filters by `job_id` in SQL; `read_source_navigation` issues bounded queries; `list_processing_artifacts` accepts a `limit`.
|
|
||||||
14. No page imports `session_scope`, `sqlalchemy.inspect`, or `get_settings`.
|
|
||||||
15. The 23KB SVG literal no longer appears in any `.py` file.
|
|
||||||
16. `ruff check` reports zero errors.
|
|
||||||
17. `ty check` reports zero diagnostics and runs as a pre-commit gate.
|
|
||||||
18. `tools/run_destructive_tests.py` runs on Windows.
|
|
||||||
19. 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.
|
|
||||||
20. 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.
|
|
||||||
21. No new user-facing feature, page, action, or field exists in V4.6 that did not exist in V4.5.
|
|
||||||
22. 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 `SourceService` split 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.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.6 Implementation Plan](implementation_plan_v4_6.md)
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
|
||||||
- [V4.5 Scope Boundary](../ver4.5/scope_boundary_v4_5.md)
|
|
||||||
- [V4.5 Implementation Plan](../ver4.5/implementation_plan_v4_5.md)
|
|
||||||
- [V4.2 Evidence and Provenance Scope](../ver4.2/scope_boundary_v4_2.md)
|
|
||||||
- [V4 Architecture](../ver4/architecture_v4.md)
|
|
||||||
- [V4 Schema](../ver4/schema_v4.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
- [AI Evidence and Provenance Invariant](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
# Implementation Plan (Version 4.7)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Collapse the duplicated evidence model, remove the `ProcessingArtifact` subsystem and move orientation normalization to ingest, complete the `SourceService` decomposition deferred from V4.6 ([MED-14]), correct the run-time measurement window, and close the remaining correctness and tooling items opened during V4.6. No new user-facing behavior.
|
|
||||||
|
|
||||||
## Planning Status
|
|
||||||
|
|
||||||
**Frozen.** The boundary is [`scope_boundary_v4_7.md`](scope_boundary_v4_7.md). Feature work is parked in [`../ver4.8/feature_backlog_v4_8.md`](../ver4.8/feature_backlog_v4_8.md).
|
|
||||||
|
|
||||||
## Planning Constraints
|
|
||||||
|
|
||||||
- Every change traces to a finding ID or a V4.6 review-log entry.
|
|
||||||
- `ruff check` clean and `ty check` at **0 diagnostics** at the end of every phase, matching the V4.6 exit state.
|
|
||||||
- The full suite passes at the end of every phase.
|
|
||||||
- **Test changes are expected in Phases 1 and 2.** This differs from V4.6, where the mechanical moves required no test logic changes. Measured blast radius: 33 references to the removed `job_source` evidence fields across 8 test files, and 10 `ProcessingArtifact` references across 3. Only Phase 4 retains the "no test logic changes" rule.
|
|
||||||
- Use `.\.venv\Scripts\python.exe -m pytest` (the system interpreter has no packages).
|
|
||||||
- Back up `data/transcription.db` **and** `data/documents/` before running any migration step. The image backfill rewrites files in place.
|
|
||||||
- One phase, one commit.
|
|
||||||
|
|
||||||
## Expected Project Impact
|
|
||||||
|
|
||||||
| Area | Before | After |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| `job_source` columns | 9 | **4** - `id`, `job_id`, `source_id`, `status` |
|
|
||||||
| `JobSourceStatus` on disk | two spellings across two tables | one spelling, plus a new `CANCELLED` member |
|
|
||||||
| `processing_artifact` | table, model, ~283 lines of service code, 2 rows | removed |
|
|
||||||
| Orientation normalization | derived per transcription, artifact-backed | applied once at ingest, no derivative |
|
|
||||||
| Stored image rotation | `quality=95, subsampling=0`, +38% size | qtables reuse, ~6% smaller, higher PSNR |
|
|
||||||
| `services/sources.py` | 1,389 lines, 4 domain models | ~900 lines, `Source` + `JobSource` |
|
|
||||||
| `services/evidence.py` | does not exist | ~174 lines, `ExecutionAttempt` reads and export |
|
|
||||||
| `services/artifacts.py` | planned | **cancelled** - deleted rather than extracted |
|
|
||||||
| `duration_ms` | provider call + normalization + artifact write + commit | the operation the timeout governs |
|
|
||||||
| Worker loop errors | every `Exception` logged and suppressed | programming errors distinguishable from provider faults |
|
|
||||||
| Quality gate | local pre-commit only, inert until installed | enforced in CI |
|
|
||||||
|
|
||||||
## Migration Handling
|
|
||||||
|
|
||||||
All schema and data changes are delivered by a single idempotent `tools/migrate_v46_to_v47.py`, following the `tools/migrate_v45_to_v46.py` conventions: never invoked at startup, never run by the test suite.
|
|
||||||
|
|
||||||
The tool is **built incrementally** - Phase 1 creates it with its own step, Phase 2 appends the next - and is **run at the end of each of those phases** so the live database stays usable at every phase boundary. Idempotency is what makes re-running safe.
|
|
||||||
|
|
||||||
Steps, in execution order:
|
|
||||||
|
|
||||||
1. Rotate the 58 stored images carrying EXIF orientation 3, in place, using qtables reuse; strip the orientation tag.
|
|
||||||
2. Drop the `processing_artifact` table and remove its external artifact files.
|
|
||||||
3. Normalize `execution_attempt.status` to the single declared spelling ([45]).
|
|
||||||
4. Drop `job_source.raw_transcription`, `ai_metadata`, `raw_api_response`, `executed_at`, `error_detail`.
|
|
||||||
|
|
||||||
`tools/migrate_v45_to_v46.py` deliberately accepts both enum spellings because it reads historical backups. Leave that tolerance in place.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Ingest Normalization and ProcessingArtifact Removal
|
|
||||||
|
|
||||||
Deletion comes first, so that Phase 4 never restructures code that is on its way out.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Move orientation normalization into the ingest path in `media_storage`, ahead of `write_bytes` (`media_storage.py:57`). Rotate, strip the EXIF orientation tag, then store.
|
|
||||||
2. Change the encode settings at `normalization.py:84-85` from `quality=95, subsampling=0` to `qtables=im.quantization`, `subsampling=JpegImagePlugin.get_sampling(im)`, `optimize=True`. Import `JpegImagePlugin` explicitly - it is not reachable as an attribute of `PIL.Image`.
|
|
||||||
3. Delete `resolve_provider_input` and its call at `workflows.py:226`. The stored file is now already upright, so the transcription path reads it directly.
|
|
||||||
4. Fold the `transcription_quality_warnings` payload (`workflows.py:560-571`) into `execution_attempt.normalized_metadata`.
|
|
||||||
5. Delete the artifact cluster from `sources.py` (lines 732-1015) and the artifact branch of `build_evidence_export`.
|
|
||||||
6. Delete the `ProcessingArtifact` model and the `CheckConstraint`.
|
|
||||||
7. Remove the "Orientation normalized" badge at `sources_page.py:174-178`, the artifact evidence dump at line 417, and the quality-warnings render at line 661.
|
|
||||||
8. Update `test_normalization.py`, `test_v42_evidence.py`, and `test_db.py`. Normalization tests should now assert on ingest behavior rather than on artifact creation.
|
|
||||||
9. Create `tools/migrate_v46_to_v47.py` with steps 1 and 2. Back up, run, verify.
|
|
||||||
|
|
||||||
Verification: re-check EXIF orientation across `data/documents/` - no stored image should report orientation 3, 6, or 8. Spot-check one backfilled page visually.
|
|
||||||
|
|
||||||
Exit: suite green, `ty check` at 0, `processing_artifact` gone from schema and code.
|
|
||||||
|
|
||||||
### 2. Evidence Model Simplification
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Add `JobSourceStatus.CANCELLED`. Update `jobs.py:378-384` to write it instead of `FAILED` plus `"Cancelled by user"`.
|
|
||||||
2. Declare one spelling for `JobSourceStatus` across both `job_source.status` and `execution_attempt.status` ([45]). `job_source.status` already declares `values_callable`; `execution_attempt.status` does not.
|
|
||||||
3. Remove `raw_transcription`, `ai_metadata`, `raw_api_response`, `executed_at`, and `error_detail` from the `JobSource` model.
|
|
||||||
4. Redirect every read to `execution_attempt`:
|
|
||||||
- `transcript.py:103-119` sorts by `executed_at` - sort by `ExecutionAttempt.finished_at`.
|
|
||||||
- `sources_page.py:390-393` reads `ai_metadata` and `raw_api_response`.
|
|
||||||
- `sources_page.py:337-375` renders status, executed time, and error detail.
|
|
||||||
- `models.py:266-278` and `models.py:328-343` derive transcript and error from `job_sources`.
|
|
||||||
5. Shrink `update_job_source_transcription` (`sources.py:524-679`) to write only the surviving `JobSource` columns. Keep the method in `sources.py` and keep both writes in one session scope.
|
|
||||||
6. Confirm `jobs.py:411-424` retry still works unchanged. It resets `FAILED` to `PENDING`; the attempt history it appears to discard is preserved by `ExecutionAttempt`'s unique constraint.
|
|
||||||
7. Confirm `workflows.py:432-442` work selection is unaffected. It filters `status != TRANSCRIBED` within `job.job_sources`, so `CANCELLED` pages are excluded from a re-run only if that is the intent - **decide explicitly** whether cancelled pages should be re-attempted, and encode the answer in the filter rather than leaving it implicit.
|
|
||||||
8. Update the 33 affected test references across the 8 files identified.
|
|
||||||
9. Append migration steps 3 and 4. Back up, run, verify row counts before and after.
|
|
||||||
|
|
||||||
Exit: suite green, `ty check` at 0, `job_source` at 4 columns.
|
|
||||||
|
|
||||||
### 3. Stage B - Extract `services/evidence.py` ([MED-14])
|
|
||||||
|
|
||||||
Read-side only, now applied to a substantially smaller `sources.py`.
|
|
||||||
|
|
||||||
Move:
|
|
||||||
|
|
||||||
- `read_latest_execution_attempt` (216-245), including the `LatestExecutionAttempt` read model
|
|
||||||
- `promote_machine_attempt` (679-710)
|
|
||||||
- `list_execution_attempts` (710-732)
|
|
||||||
- `build_evidence_export` (1015-1107)
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Create `services/evidence.py` with an `EvidenceService(ServiceBase)` following the `DocumentService` conventions.
|
|
||||||
2. Move the methods and the `LatestExecutionAttempt` dataclass verbatim. Preserve signatures, keyword-only arguments, error types, and `_session_scope` usage exactly.
|
|
||||||
3. Preserve every explicit `selectinload()` chain. **Chain, never varargs** - `selectinload(A.b).selectinload(B.c)` and `selectinload(A.b, B.c)` produce an identical `.path` but are not equivalent, and under the `lazy="raise"` default set in V4.6 the varargs form raises at render time. See `db/loading.py`.
|
|
||||||
4. Update `ServiceBundle` to construct and expose the new service via the `from_session_factory` constructor added in V4.6.
|
|
||||||
5. Update call sites in `sources_page.py` and `workflows.py`.
|
|
||||||
6. Run `ruff check --fix` **in the same pass** as the import edits - autofix removes imports that are unused at that moment.
|
|
||||||
7. **Revise `.github/instructions/services.instructions.md` to describe the boundaries this decomposition actually produced** (review log [59]). Do this *after* the move, not before - the refactor is the empirical test of the rule, and a rule written in advance would have to be bent to fit. Known defects to correct:
|
|
||||||
- **Line 11, `1 service class per data model`** - the rule is table-shaped rather than aggregate-shaped, and is the measured cause of `sources.py` reaching 1,389 lines. Replace with aggregate ownership.
|
|
||||||
- **No home for junction tables.** The rule names the four core components (Document, Source, Job, Person) but is silent on `job_source` and `document_person`, where they intersect. Add an explicit model-ownership table naming the owning service for every model, including junctions and `ExecutionAttempt`.
|
|
||||||
- **Lines 30-32**, mandatory CRUD for every model, is already false: `prompts.py` does not comply. Soften to describe intent rather than mandate a method set.
|
|
||||||
- **Line 13 vs lines 75-77** read as contradictory on whether a service may touch more than one table. Reword the composition section so the ownership rule and the multi-table-operation guidance agree.
|
|
||||||
- **Line 77** typo: `picutre`.
|
|
||||||
|
|
||||||
Note that line numbers above are pre-Phase-1 positions and will have shifted. Locate by symbol, not by line.
|
|
||||||
|
|
||||||
Exit: suite green **with no test logic changes** beyond import paths, `ty check` at 0, and `services.instructions.md` consistent with the post-refactor module layout. Walk every `/ui/*` page and confirm a 200, since `lazy="raise"` turns a missed eager load into a runtime error rather than a slow query.
|
|
||||||
|
|
||||||
### 4. Run-Time Measurement Window (review log [55])
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. In `workflows.py`, make the recorded duration cover only the operation the `wait_for` at lines 240-249 governs. Either move `monotonic_started_at` (line 221) to immediately before the `wait_for`, or capture provider latency separately and record that.
|
|
||||||
2. Apply the same treatment to all three write sites: success (line 278), `TimeoutError` (line 295), and general failure (line 330). The failure paths must keep using the monotonic clock.
|
|
||||||
3. If preprocessing time is still worth keeping, record it as its own value rather than folding it into `duration_ms`.
|
|
||||||
4. Update `sources_page.py:400`, which renders the raw integer as `"27612 ms"`.
|
|
||||||
|
|
||||||
Phase 1 already removes normalization and the artifact write from this window, which narrows the gap but does not close it - the `session.commit()` at line 228 remains inside it.
|
|
||||||
|
|
||||||
Verification: a recorded timeout duration should sit at or just under the configured budget, not 0.4-2.0 s above it as in the three historical `local_timeout` rows.
|
|
||||||
|
|
||||||
### 5. Worker Exception Handling (review log [8])
|
|
||||||
|
|
||||||
`worker.py:96-106`.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Separate genuinely retriable faults from programming errors. `classify_unexpected_error` is already called at line 101 and its result is currently only logged.
|
|
||||||
2. Ensure a non-retriable error reaches a terminal state instead of being retried.
|
|
||||||
3. Keep terminal-state and retry persistence atomic per `services.instructions.md:63-65`.
|
|
||||||
4. Add a test that a deliberate programming error in the loop does not silently retry.
|
|
||||||
|
|
||||||
Context: with `WORKER_MAX_RETRIES=1` and a 30 s timeout the worst-case silent burn is 60 s, down from 360 s, so this is no longer urgent - but it remains the real fix behind that risk.
|
|
||||||
|
|
||||||
### 6. CI Enforcement ([HIGH-06], review log [40])
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Add a workflow under `.github/workflows/` running `ruff check`, `ty check`, and `pytest` on push and pull request.
|
|
||||||
2. Use the same commands as `.pre-commit-config.yaml` so local and CI gates cannot drift.
|
|
||||||
3. Confirm the 4 tests that skip without `OPENROUTER_API_KEY` skip cleanly in CI rather than failing.
|
|
||||||
4. Negative-test the workflow by pushing a deliberate lint error on a scratch branch.
|
|
||||||
|
|
||||||
## Sequencing Constraints
|
|
||||||
|
|
||||||
- **Phase 1 before Phase 3.** Code scheduled for deletion is never extracted first. This is why the previously planned `services/artifacts.py` is cancelled.
|
|
||||||
- **Phase 1 before Phase 2.** Both touch `workflows.py` write paths; separating them keeps any regression attributable.
|
|
||||||
- **Phase 4 before any V4.8 telemetry work.** A model-performance rollup built on the current `duration_ms` would chart preprocessing mixed with provider latency.
|
|
||||||
|
|
||||||
## Test Strategy
|
|
||||||
|
|
||||||
- **Phase 1:** normalization tests move from asserting artifact creation to asserting ingest behavior. Add a test that a stored image never retains EXIF orientation 3, 6, or 8.
|
|
||||||
- **Phase 2:** assert that evidence reads resolve through `execution_attempt`; assert `CANCELLED` is distinguishable from `FAILED`; verify both status columns round-trip identically and existing rows read back correctly after the fix-up.
|
|
||||||
- **Phase 3:** the suite passes with **no test logic changes**. Only import paths update. A required behavioral change signals the move was not mechanical - stop and re-examine.
|
|
||||||
- **Phase 4:** assert the recorded duration is bounded by the configured timeout.
|
|
||||||
- **Phase 5:** new test that a programming error does not silently retry.
|
|
||||||
- **Phase 6:** CI must fail on an injected lint error.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
| :--- | :--- |
|
|
||||||
| The image backfill corrupts originals - it rewrites files in place with no archival master | Back up `data/documents/` before running; idempotent step keyed on the EXIF tag so a second run is a no-op; visually spot-check a backfilled page |
|
|
||||||
| Dropping `job_source` columns loses data that turns out not to be duplicated | Verified 77/77 identical on all three evidence columns before dropping; re-run that comparison inside the migration and abort on any mismatch |
|
|
||||||
| A read still expects a removed `job_source` column and fails only at render time | Grep-driven checklist in Phase 2 task 4; walk every `/ui/*` page after the phase |
|
|
||||||
| Cancelled pages are silently re-attempted, or silently never re-attempted | Phase 2 task 7 forces an explicit decision in the work-selection filter |
|
|
||||||
| A moved query loses an eager load and trips `lazy="raise"` at render time | Preserve `selectinload` chains verbatim; walk every `/ui/*` page after Phase 3 |
|
|
||||||
| `ruff check --fix` deletes an import mid-move | Edit imports and usages in the same pass, as in V4.6 |
|
|
||||||
| Circular imports between `sources.py` and `evidence.py` | Composition is sanctioned by `services.instructions.md:75-77`; keep the dependency one-directional |
|
|
||||||
| Scope creep into refactoring `update_job_source_transcription` | Out of scope; the boundary records why |
|
|
||||||
|
|
||||||
## Delivery Order
|
|
||||||
|
|
||||||
1. Ingest normalization and `ProcessingArtifact` removal
|
|
||||||
2. Evidence model simplification
|
|
||||||
3. Stage B - `evidence.py`, then revise `services.instructions.md` to match
|
|
||||||
4. Measurement window
|
|
||||||
5. Worker exception handling
|
|
||||||
6. CI enforcement
|
|
||||||
|
|
||||||
## Done Criteria
|
|
||||||
|
|
||||||
Every item in the scope boundary's Acceptance Criteria is satisfied, the suite is green, `ty check` reports 0 diagnostics, and no user-facing behavior has changed.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.7 Scope Boundary](scope_boundary_v4_7.md)
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
|
||||||
- [V4.6 Review Log](../ver4.6/review_log_v4_6.md) - resolves the `review log [N]` citations used throughout this document
|
|
||||||
- [V4.6 Implementation Plan](../ver4.6/implementation_plan_v4_6.md)
|
|
||||||
- [V4.8 Feature Backlog](../ver4.8/feature_backlog_v4_8.md)
|
|
||||||
- `.github/instructions/services.instructions.md`
|
|
||||||
- `src/transcription/db/loading.py` - the `selectinload` varargs trap
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
# V4.7 Implementation Review Log
|
|
||||||
|
|
||||||
Working record kept during the V4.7 architectural cleanup release.
|
|
||||||
|
|
||||||
This file is the canonical reference for citations of the form **`review log [N]`** in V4.7 and later planning documents. The numbers below are those `N` values. They are independent of the [V4.6 log](../ver4.6/review_log_v4_6.md), which has its own numbering.
|
|
||||||
|
|
||||||
The log was maintained live in a session-scoped database and exported here so the citations remain resolvable in later sessions. It is a historical record: entries are not rewritten after the fact, except where a later phase resolved an entry that was open at the time, in which case the resolution is appended to the body and marked `RESOLVED:`. Where an entry conflicts with a committed planning document, **the planning document wins**.
|
|
||||||
|
|
||||||
## Legend
|
|
||||||
|
|
||||||
| Field | Meaning |
|
|
||||||
| :--- | :--- |
|
|
||||||
| `kind` | `question` - needed a decision; `comment` - observation; `deviation` - departure from plan; `risk` - identified hazard |
|
|
||||||
| `status` | `open` - unresolved; `answered` - resolved by a decision; `noted` - recorded, no action required |
|
|
||||||
| `finding` | Finding ID in [architecture_code_review_2026-08-17.md](../architecture_code_review_2026-08-17.md), where one applies. Most V4.7 entries have none, because V4.7 works from the [implementation plan](implementation_plan_v4_7.md) rather than from that pre-V4.6 snapshot. Where an entry carries a one-line summary instead, it appears as a bold lead-in to the body. |
|
|
||||||
|
|
||||||
**50 entries** - 1 open, 18 answered, 31 noted.
|
|
||||||
|
|
||||||
## Still Open
|
|
||||||
|
|
||||||
These carry forward past V4.7.
|
|
||||||
|
|
||||||
| ID | Finding | Summary | Disposition |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| [32] | - | /ui/documents/{id}/sources redirects to /sources, dropping the /ui prefix | Pre-existing and outside the V4.7 scope boundary - deliberately left unfixed |
|
|
||||||
|
|
||||||
## Full Log
|
|
||||||
|
|
||||||
### Phase 0 - baseline and backups
|
|
||||||
|
|
||||||
#### [1] Backups taken and verified
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
data/transcription.db and data/documents/ copied to C:\GitHub\_backups\transcription_v47_20260818-092616. All 76 document files SHA256-identical to source. A consistent SQLite snapshot (transcription.consistent.db) was also produced via the sqlite3 backup API because the live DB file is locked by a running app process, making a plain file copy potentially torn.
|
|
||||||
|
|
||||||
#### [2] The application appears to be running and holds data/transcription.db
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
Two python processes started 2026-08-18 04:58 (PID 14340 is .venv python). Get-FileHash on data/transcription.db failed with a sharing violation. The Phase 1 migration rewrites data/documents/ JPEGs in place and later phases ALTER the live schema; both are unsafe while the app is running. Need the user to stop the app before any migration run. RESOLVED: the user stopped the app before the migration ran; the DB accepted an immediate write lock beforehand.
|
|
||||||
|
|
||||||
#### [3] Working tree is clean; the V4.7 doc edits are already committed
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
The brief said both V4.7 docs have uncommitted edits on disk. git status --short is empty and HEAD is 246d7f9 "V4.7 final scope changes", which contains them. Nothing was reverted or stashed; the working tree content matches what the brief described.
|
|
||||||
|
|
||||||
#### [4] Baseline is not fully green: 1 pre-existing test failure caused by .env leakage
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
tests/test_config.py::TestWorkerReliabilitySettings::test_worker_retry_defaults asserts worker_max_retries == 0 but the local .env sets WORKER_MAX_RETRIES=1, and _make_settings() reads .env. ruff and ty are both clean. This is a test-isolation defect predating V4.7 (introduced when the 2026-08-18 calibration wrote WORKER_MAX_RETRIES into .env), not a code defect. It would pass in CI, where no .env exists, which makes it a latent local/CI divergence relevant to Phase 6. Awaiting a decision on whether to fix it. RESOLVED: tests/test_config.py::_make_settings now builds Settings with _env_file=None, isolating it from the local .env. The local/CI divergence this entry flagged is gone, and CI run 182 confirms the same 295 passed / 4 skipped result on Linux with no .env of the developer's.
|
|
||||||
|
|
||||||
### Phase 1 - artifact removal and ingest rotation
|
|
||||||
|
|
||||||
#### [5] Where does ingest-time rotation actually belong: media_storage or store.py?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Plan Phase 1 task 1 says rotate inside media_storage ahead of write_bytes. But store.py computes file_hash and file_size_bytes from the ORIGINAL file_bytes (store.py:93, 168) independently of write_media_bytes, which returns only a Path. Rotating inside write_media_bytes would leave Source.file_hash and file_size_bytes describing bytes that were never stored. write_media_bytes is also shared with person portraits and homepage images. Recommend rotating the bytes once at the Source-ingest boundary (store_source_file or its two callers in store.py) so hash, size and stored file all describe the same upright bytes, and leaving media_storage a generic byte writer. [ANSWERED 2026-08-18 by user] Rotate at the Source-ingest boundary, before file_hash/file_size_bytes are computed, so the hash and size describe the stored upright bytes. media_storage stays a generic byte writer. This is a deliberate deviation from Phase 1 task 1 as written.
|
|
||||||
|
|
||||||
#### [6] Fate of artifact_dir / artifact_inline_threshold_bytes settings and data/artifacts/
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Removing ProcessingArtifact orphans Settings.artifact_dir and Settings.artifact_inline_threshold_bytes (config.py:104,106), the JobService artifact deletion path (jobs.py:307-350), and the on-disk data/artifacts/ tree. config.py is outside the services instruction file. Proposal: delete both settings and the jobs.py deletion path as part of the same removal, and have the migration delete the external artifact files (migration step 2 already says so). Confirm. [ANSWERED 2026-08-18 by user] Delete both Settings.artifact_dir and Settings.artifact_inline_threshold_bytes.
|
|
||||||
|
|
||||||
#### [7] Phase 1 blast radius is wider than the plan task list
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
Beyond the listed sites, ProcessingArtifact is also referenced by: sources.py delete guards (lines 296-300 and 450-462, which block Source deletion when artifacts exist), jobs.py job-deletion artifact cleanup (307-350), Source.processing_artifacts and ExecutionAttempt.artifacts relationships (models.py:322,431), the selectinload(ExecutionAttempt.artifacts) in list_execution_attempts (sources.py:719), and the ProcessingArtifact lookup that validates model_input_artifact_id inside update_job_source_transcription (sources.py:648-664). All must go with the model.
|
|
||||||
|
|
||||||
#### [10] Quality-warnings render: delete (per plan) or re-point at normalized_metadata?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Plan Phase 1 task 7 says delete the render at sources_page.py:661, but task 4 folds the same payload into execution_attempt.normalized_metadata and decision A forbids user-facing change. The render has never fired in practice because it reads attempt.artifacts and only 2 artifact rows exist. [ANSWERED 2026-08-18 by user] Keep the display and re-point it at normalized_metadata. Deviation from Phase 1 task 7 as written; task 4 now has a consumer.
|
|
||||||
|
|
||||||
#### [11] SourceEvidenceReference.derivative_id / transformation kept but no longer populated
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
With ProcessingArtifact gone there is no derivative to reference, so both fields are always None. They were left in place rather than removed: RequestManifest is a frozen, versioned evidence contract whose canonical bytes feed request_manifest_sha256, so removing fields would change the digest of every future manifest and arguably require a schema_version bump - cost out of proportion to deleting two optional fields. Raise if you would rather see the contract cleaned up.
|
|
||||||
|
|
||||||
#### [12] The session.commit() at workflows.py:228 is removed with resolve_provider_input
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
That commit existed to make the artifact row written during provider-input resolution durable before the provider call. With no artifact write there is nothing pending to commit - the PROCESSING claim was already committed at line 198 / 426 - so the call is removed rather than left as a no-op. This also removes one of the two things Phase 4 has to get out of the duration measurement window.
|
|
||||||
|
|
||||||
#### [13] The image backfill must also update source.file_hash and file_size_bytes
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
Migration step 1 as written only rotates the stored JPEGs and strips the EXIF tag. But source.file_hash and source.file_size_bytes were computed from the pre-rotation bytes, and after Phase 1 the transcription path derives the evidence digest (SourceEvidenceReference.digest_sha256) straight from source.file_hash. Rotating the file without updating the row would make every backfilled Source advertise a digest that does not match the bytes actually sent to the provider - the exact class of defect the evidence model exists to prevent. The migration therefore rewrites both columns for each rotated image in the same transaction. Not a change of intent, an omission in the step description.
|
|
||||||
|
|
||||||
#### [14] Orientation normalization must not change what ingest accepts
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Undecodable upload bytes are a normalization no-op, not a rejection**
|
|
||||||
|
|
||||||
validate_source_content only checks emptiness and filename; it never decoded the image, so bytes that Pillow cannot open (e.g. the b"image-bytes" fixture in tests/services/test_store.py) were accepted and stored. Moving rotation into store_source_file initially turned that into an OrientationNormalizationError, i.e. a user-facing rejection of previously accepted uploads. Decision A forbids user-facing change, so Image.open failure now logs and returns None; the error is retained only for a decode that succeeded and a rewrite that then failed.
|
|
||||||
|
|
||||||
#### [15] Artifact-only tests removed with the subsystem
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Two tests deleted rather than rewritten**
|
|
||||||
|
|
||||||
tests/test_v42_evidence.py::test_large_json_artifact_uses_constrained_atomic_storage and ::test_rejects_inline_artifact_with_incorrect_integrity exercised only external artifact storage and inline artifact integrity. Both behaviours are deleted by Phase 1, so the tests have no surviving subject. tests/ui/test_sources_page.py lost one assertion ("Derived Artifacts"), and tests/test_db.py lost the processing_artifact table assertion.
|
|
||||||
|
|
||||||
#### [16] Fate of the superseded v4.5 to v4.6 migration tool
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**tools/migrate_v45_to_v46.py no longer type-checks**
|
|
||||||
|
|
||||||
The old migration references ProcessingArtifact (line 157), SourceService._verify_artifacts_integrity (line 169), and carries processing_artifact: 2 in EXPECTED_SOURCE_COUNTS (line 83). All three are gone. It is currently the only remaining ty failure. The plan says to keep its enum-spelling tolerance but does not address this. Options: delete the completed one-time tool; or strip the artifact code path from it. RESOLVED: the completed one-time tool was deleted (Phase 1). tools/ now contains only migrate_v46_to_v47.py, and ty is clean.
|
|
||||||
|
|
||||||
#### [17] tools/migrate_v45_to_v46.py removed rather than repaired
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Superseded v4.5 to v4.6 migration deleted**
|
|
||||||
|
|
||||||
User decision. The migration is complete, the live database is already V4.6, and after V4.7 it would restore a V4.5 backup into a schema that no longer matches (job_source is stripped in Phase 2). Two doc references remain, both citing it only as a conventions template: implementation_plan_v4_7.md lines 39 and 50, scope_boundary_v4_7.md line 160. Line 50 (enum-spelling tolerance) is now moot. Recoverable from git history if ever needed.
|
|
||||||
|
|
||||||
#### [18] DEFAULT_ARTIFACT_DIR constant in migrate_v46_to_v47.py
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Migration records the deleted artifact_dir default itself**
|
|
||||||
|
|
||||||
Step 2 must delete external artifact files, but Settings.artifact_dir was deleted in the same phase. The migration therefore carries the historical V4.6 default (data/artifacts) as its own constant with a --artifact-dir override, rather than depending on a setting that no longer exists. One external file was present and removed; the directory is now empty.
|
|
||||||
|
|
||||||
#### [19] Phase 1 migration outcome
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
**Migration executed and verified against the live corpus**
|
|
||||||
|
|
||||||
Ran after the user stopped the app and after a fresh pre-migration backup to C:\GitHub\_backups\transcription_v47_premigration_20260818-101232. Result: 58 images rotated, 18 already upright, 0 missing; processing_artifact dropped (2 rows) and its 1 external file removed. Verification: no stored image reports orientation 3/6/8; source.file_hash and file_size_bytes match every file on disk (0 mismatches over 76); 58 of 76 files differ from the backup; PSNR against the un-rotated backup is 50.3 / 51.1 / 56.1 dB (min/median/max) across the 57 JPEGs, allowing for the -6 percent size reduction; a re-run reports rotated=0 and table already absent, confirming idempotency. Visual spot-check of 1547e555 confirmed the page was genuinely stored upside down and is now upright.
|
|
||||||
|
|
||||||
### Phase 2 - evidence model simplification
|
|
||||||
|
|
||||||
#### [8] Should CANCELLED pages be re-attempted when a job is re-run?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
Plan Phase 2 task 7. _resolve_job_sources (workflows.py:432-442) selects work by status != TRANSCRIBED, so once CANCELLED exists as a distinct status a re-run would silently pick cancelled pages back up. Options: (a) exclude CANCELLED from work selection, so cancelling is sticky and a page must be explicitly re-queued; (b) include it, so re-running a job means "do everything not yet transcribed"; (c) clear CANCELLED back to PENDING in the existing retry path (jobs.py:411-424) and exclude it from work selection, which makes re-attempt an explicit user action through the retry button. Decision required before Phase 2 task 1. RESOLVED: re-attempt them. Resubmit accepts FAILED and CANCELLED. Rationale: today cancel writes FAILED, so resubmit already resets cancelled pages to PENDING; introducing a distinct CANCELLED status without widening the resubmit filter would silently make cancelled work unrecoverable, a user-facing regression that decision A forbids. The decision is encoded in the resubmit candidate filter, which is the real decision point - _resolve_job_sources only ever sees these rows after resubmit has already set PENDING. UI copy on both the cancel and resubmit pages is updated to match.
|
|
||||||
|
|
||||||
#### [20] Plan task 4 targets a dead module
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**ui/components/transcript.py deleted instead of redirected**
|
|
||||||
|
|
||||||
Phase 2 task 4 directs transcript.py:103-119 to sort by ExecutionAttempt.finished_at instead of job_source.executed_at. Investigation showed the module is entirely unreferenced: no import of transcription.ui.components.transcript exists in src, tests, or docs, and both public functions (render_original_transcription_card, render_revision_row) have zero callers. Rewriting it would mean maintaining unreachable code against the new evidence model. User decision: delete the module. Recoverable from git history.
|
|
||||||
|
|
||||||
#### [21] Defect [45] fixed by declaring one enum spelling
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**execution_attempt.status gains values_callable**
|
|
||||||
|
|
||||||
ExecutionAttempt.status was a bare JobSourceStatus annotation, so SQLAlchemy persisted enum names (TRANSCRIBED) while job_source.status persisted values (transcribed) via values_callable. That is why the two columns matched on 0 of 79 rows. execution_attempt.status now declares the identical SAEnum with values_callable and native_enum=False. Existing rows carry the old spelling and are rewritten by migration step 3.
|
|
||||||
|
|
||||||
#### [22] Dead property made more expensive by the evidence move
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Job.error_detail deleted rather than re-derived**
|
|
||||||
|
|
||||||
Plan task 4 lists models.py:266-278 (Job.error_detail) for redirection. A full-repo search found zero readers: JobTableRow has no such field and the job detail page never calls it. Re-deriving it from ExecutionAttempt would require a two-level eager load (job_sources -> execution_attempts) on every Job, across a lazy=raise then lazy=noload chain, where a missing load returns an empty list and the property would silently answer None instead of raising. No information is lost: error_detail survives on ExecutionAttempt and is reachable via list_execution_attempts and read_latest_execution_attempt. A future job-level failure view should query attempts directly anyway, since first-error-across-pages is the wrong shape for a partial-success job. User decision: delete.
|
|
||||||
|
|
||||||
#### [23] Replacement ordering key after executed_at is dropped
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Source.latest_job_source orders by Job.date_created**
|
|
||||||
|
|
||||||
JobSource retains only id, job_id, source_id and status, so max(job_sources, key=executed_at) needs a key from a neighbour. Job.date_created is chosen over the latest ExecutionAttempt.finished_at: it is always present (a PENDING page has no attempt at all), it is already eager-loaded by read_source_detail, and since (job_id, source_id) is unique per source the ordering is exactly most recent job. The two differ only when a job created earlier finishes later, which the single-worker queue does not produce. User decision.
|
|
||||||
|
|
||||||
#### [24] The "Cancelled by user" string has no home after job_source is stripped
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Cancel no longer records a reason string**
|
|
||||||
|
|
||||||
cancel_job previously wrote error_detail="Cancelled by user" onto job_source. That column is gone, and cancel deliberately makes no provider call so it writes no ExecutionAttempt. The reason is now carried by JobSourceStatus.CANCELLED itself, which is strictly more precise than a free-text string. UI copy on the cancel page was updated to say "cancelled" and to state that cancelled sources can be resubmitted.
|
|
||||||
|
|
||||||
#### [25] jobs_page "Failed Sources" became "Resubmittable Sources"
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Resubmit UI counter renamed**
|
|
||||||
|
|
||||||
The resubmit candidate filter now accepts FAILED and CANCELLED per the user decision in entry 8, so the page counter had to count both. Renamed the metadata row and the blocked-error message accordingly.
|
|
||||||
|
|
||||||
#### [26] sources_page no longer renders ai_metadata/raw_api_response when no attempt exists
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**Legacy job_source evidence fallback deleted from the detail page**
|
|
||||||
|
|
||||||
The "no ExecutionAttempt" branch of _render_provider_evidence used to fall back to the job_source JSON columns for historical rows. Those columns are gone, so the branch now renders only the empty state. Verified against the evidence baseline: all 77 successful transcriptions have a matching execution_attempt row, so no live row loses its evidence display.
|
|
||||||
|
|
||||||
#### [27] latest_error_detail reads through job_sources -> execution_attempts
|
|
||||||
|
|
||||||
*risk* - **noted**
|
|
||||||
|
|
||||||
**Model properties now require a two-level eager load**
|
|
||||||
|
|
||||||
Source.latest_error_detail feeds a visible "Error Detail" column on the sources table. Because JobSource.execution_attempts is lazy="noload" it returns empty rather than raising when not loaded, so a caller that forgets the chained selectinload gets a silent blank instead of an error. list_sources_detail and the model-property test were both updated to chain selectinload(...).selectinload(orm_attribute(...)). Any new caller must do the same.
|
|
||||||
|
|
||||||
#### [28] job_source.status and execution_attempt.status now agree on every row
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**Defect [45] verified fixed against the live database**
|
|
||||||
|
|
||||||
Before: 0/79 rows matched, because execution_attempt persisted enum names and job_source persisted values. After migration step 3: 79/80 join rows agree. The single disagreement is job_source 09cd5f77 which has two attempts - attempt 1 failed, attempt 2 transcribed - so the queue row correctly reflects the final outcome while the history preserves the failure. Comparing job_source against its LATEST attempt gives 79/79.
|
|
||||||
|
|
||||||
#### [29] list_sources_detail resolves latest_status and latest_error_detail for all 76 rows
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**Two-level eager load verified against live data, not just tests**
|
|
||||||
|
|
||||||
Ran SourceService.list_sources_detail against the migrated production database: 76 sources, 75 transcribed / 1 failed, and the one failed row still exposes latest_error_detail - now read from execution_attempt rather than the dropped job_source column. This closes the silent-blank risk recorded in entry 27 for the shipped call path.
|
|
||||||
|
|
||||||
### Phase 3 - evidence service extraction and the ownership rule
|
|
||||||
|
|
||||||
#### [9] Junction ownership: which service owns job_source and document_person?
|
|
||||||
|
|
||||||
*question* - `MED-14` - **answered**
|
|
||||||
|
|
||||||
services.instructions.md names four core components (Document, Source, Job, Person) and is silent on the two junctions, which is exactly where two owners intersect. Candidate tie-break rules: (a) the junction belongs to the service that creates its rows; (b) it belongs to the aggregate whose lifecycle it shares (job_source dies with the Job, document_person dies with the Document); (c) it belongs to the side that reads it most. These do not agree for job_source: it is created by store.py orchestration, its lifecycle is the Job, and it is read predominantly through Source pages, which is how it ended up in sources.py. Decision required at Phase 3 task 7. RESOLVED: measurement showed document_person has a single writer (people.py, every create/delete/sync) and needs no tie-break; documents.py only eager-loads through it. job_source is genuinely contested between sources.py (row existence + per-page outcome) and jobs.py (job-lifecycle status transitions). User selected the LIFECYCLE rule: the service that creates and deletes rows owns the junction, so job_source -> SourceService. Two scoped carve-outs written into the rule: (1) cascade deletion of junction rows when a service deletes its own aggregate root (JobService.delete_job_with_guardrails); (2) status transitions that create and delete nothing (cancel_job, resubmit_failed_sources), because those are Job lifecycle events. No code was moved.
|
|
||||||
|
|
||||||
#### [30] Where should the shared transcription error hierarchy live?
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Extraction immediately violated the existing no-sibling-import rule**
|
|
||||||
|
|
||||||
tests/test_service_boundaries.py enforces services.instructions.md:13 - a service module must not import a sibling. evidence.py needed TranscriptionNotFoundError, which sources.py also raises, so the extraction failed the rule on the first run. Measured ownership: CandidatePromotionError is now raised only in evidence.py; PromptLoadError and SourceDeleteBlockedError only in sources.py; TranscriptionNotFoundError in both; TranscriptionError is the shared base, caught by store.py. User chose to move the whole five-class hierarchy to a neutral services/errors.py: one obvious home, one import path, and the exception a caller catches no longer changes when an operation moves between services.
|
|
||||||
|
|
||||||
#### [31] Two test bundles broke on adding a fifth service, not on the refactor itself
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**ServiceBundle default factories silently bind to the real database**
|
|
||||||
|
|
||||||
test_v45_candidates and test_workflows_reliability constructed ServiceBundle(...) field by field. Adding the evidence field meant it fell back to field(default_factory=EvidenceService), which resolves the process-global session factory rather than the test one - so the tests silently queried the wrong database instead of failing loudly. Both were changed to ServiceBundle.from_session_factory(...), which is immune to future additions. This is the same global-singleton hazard recorded in the 2026-08-17 review at line 272.
|
|
||||||
|
|
||||||
#### [32] /ui/documents/{id}/sources redirects to /sources, dropping the /ui prefix
|
|
||||||
|
|
||||||
*risk* - **open**
|
|
||||||
|
|
||||||
**Pre-existing broken redirect found during the UI walk**
|
|
||||||
|
|
||||||
The Phase 3 exit criterion requires walking every /ui/* page. 24 of 25 routes return 200. documents_page.py returns RedirectResponse(url=f"/sources?document_id=...") without the /ui mount prefix, so following the 307 lands on a 404. Confirmed pre-existing: documents_page.py has no uncommitted diff and was last touched in 6a3ee26, well before V4.7. Out of the V4.7 scope boundary, so NOT fixed - raised for the user to decide.
|
|
||||||
|
|
||||||
#### [33] Instruction-file defects corrected
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**services.instructions.md rewritten after the decomposition, per the mandated order**
|
|
||||||
|
|
||||||
All five defects from plan Phase 3 task 7 fixed. (a) Line 11 "1 service class per data model" replaced with one service class per AGGREGATE, with DocumentType-under-DocumentService as the worked example; this is the measured cause of sources.py reaching 1,389 lines. (b) Added a Model Ownership section with a table covering every model plus an explicit junction-table rule, which the file previously had no home for. (c) The mandatory-CRUD rule (old lines 30-32) was already false: prompts.py, quality.py, normalization.py, media_storage.py and source_media.py define no service class at all, EvidenceService deliberately exposes no create/delete because ExecutionAttempt is append-only, and RegistryService uses generic <op>_entry naming. Softened to intent plus an explicit "do not add unused CRUD to satisfy symmetry". (d) Old line 13 (services fully independent) read as contradicting old lines 75-77 (compose across tables); reworded to separate READING across models via eager loads from the owning root, which is allowed, from IMPORTING another service, which is not. (e) Typo "picutre" removed. Also recorded the real enforcement mechanism: tests/test_service_boundaries.py, and errors.py as the neutral shared-type home.
|
|
||||||
|
|
||||||
#### [34] Line-number citation removed from the boundary test
|
|
||||||
|
|
||||||
*risk* - **noted**
|
|
||||||
|
|
||||||
**test_service_boundaries.py cited the rule by line number**
|
|
||||||
|
|
||||||
The test docstring pinned .github/instructions/services.instructions.md:13. Rewriting the file invalidated that anchor. Replaced with a section-name citation ("Structure") so future edits to the instruction file cannot silently desynchronise the test docstring. errors.py was also added to the docstring list of neutral modules.
|
|
||||||
|
|
||||||
### Phase 4 - measurement window
|
|
||||||
|
|
||||||
#### [35] Both cited offenders were already deleted
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Phase 4 premise partly overtaken by Phase 1**
|
|
||||||
|
|
||||||
The plan states the session.commit() at line 228 "remains inside" the measurement window. Diffed against f86c0ff~1: at V4.6 the window held resolve_provider_input (async; normalization + artifact write + DB work) and that commit. Phase 1 deleted both. What remains between the clock and the wait_for is build_provider_input, now pure field copying because normalization moved to ingest and file_hash is already stored. Measured at 6.2 us per call with zero awaits, so it cannot yield to the event loop. Plan tasks 1-2 were therefore already satisfied in substance; the clock was still moved to make the property structural rather than incidental.
|
|
||||||
|
|
||||||
#### [36] No preprocessing left to record separately
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Plan task 3 declined**
|
|
||||||
|
|
||||||
Task 3 offered recording preprocessing time as its own value. After Phase 1 there is no preprocessing in the window: 6.2 us of attribute copying. Adding a preprocessing_ms column to measure that is unnecessary complexity and was declined under the guiding principle. Raised rather than decided silently.
|
|
||||||
|
|
||||||
#### [37] Undocumented 475ms contributor the plan did not identify
|
|
||||||
|
|
||||||
*risk* - **answered**
|
|
||||||
|
|
||||||
**Lazy provider construction was inside the timed region**
|
|
||||||
|
|
||||||
The regression test measured 890ms where ~200ms was expected. Cause: services.sources.provider is a lazy property, and it appears as an argument expression to _call_transcriber, so it is evaluated after the clock starts but before wait_for begins timing. Measured 475ms to construct OpenRouterTranscriptionProvider on first access and 0.001ms after. The first attempt of every worker process therefore booked ~0.5s of HTTP client construction as provider latency. This plausibly accounts for the low end of the historical 0.4-2.0s local_timeout overshoot, and Phase 1 did not touch it. The property is loop-invariant, so it was hoisted above the per-source loop, which also removes the repeated attribute lookup from the two evidence-capture sites.
|
|
||||||
|
|
||||||
#### [38] test_timeout_duration_excludes_pre_call_setup
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**Regression guard added**
|
|
||||||
|
|
||||||
New test in tests/services/test_workflows_reliability.py simulates 400ms of blocking setup against a 200ms provider budget and asserts the recorded duration_ms sits near the budget and well clear of budget+setup. Verified to fail on the pre-fix code (625 < 540 assertion error) and pass after, so it is a real guard rather than a tautology. This is the plan Phase 4 verification criterion expressed as a test.
|
|
||||||
|
|
||||||
#### [39] sources_page.py no longer prints raw milliseconds
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**Duration render scaled**
|
|
||||||
|
|
||||||
Plan task 4. _format_duration renders >=1s as "27.6 s" and below that as "612 ms", per user selection. No test asserted the old format.
|
|
||||||
|
|
||||||
### Phase 5 - worker fault containment
|
|
||||||
|
|
||||||
#### [40] Probed behaviour: the defect is a stranded job, not a silent retry
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Plan task 4 describes a failure mode that does not occur**
|
|
||||||
|
|
||||||
The plan asks for a test that a deliberate programming error "does not silently retry". Probed empirically with an injected AttributeError. Mode A, error raised after the claim commits (inside advance_job): raised exactly ONCE, job left at PROCESSING, retry_count 0, and never re-claimed because claim_next_queued_job filters status == QUEUED. That is a permanently stranded job with one swallowed log line, not a retry. advance_job PROCESSING branch, commented "Recover mid-flight jobs", is unreachable from the worker for the same reason. Mode B, error raised before or during the claim: 20 raises in 1.2s, an unbounded hot spin at the poll interval. The plan context says worst-case silent burn is 60s under WORKER_MAX_RETRIES=1, but Mode B never reaches the per-job retry machinery so nothing caps it. Both modes share the root cause the plan correctly identifies.
|
|
||||||
|
|
||||||
#### [41] Flag set in 9 places, read in none
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**retriable was decorative**
|
|
||||||
|
|
||||||
Measured across src/: retriable is assigned at errors.py:40/47/79, sources.py:877/884, store.py:127/205/366, workflows.py:284/580/593/606 and read nowhere. classify_unexpected_error already returns retriable=False, so the classification existed and was discarded. Phase 5 makes it load-bearing in two places.
|
|
||||||
|
|
||||||
#### [42] User chose: stop the worker loop
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Loop policy for a non-retriable error with no job to mark**
|
|
||||||
|
|
||||||
Mode B has no claimed job, so there is no row to mark FAILED and no reason to expect the next poll to differ. Options offered were stop the loop, circuit-breaker after N consecutive failures, or exponential backoff. User selected stopping the loop, logged at CRITICAL, returning cleanly so the exception does not surface only at app shutdown via worker_consumer_lifespan wait_for.
|
|
||||||
|
|
||||||
#### [43] User chose: mark FAILED and keep going
|
|
||||||
|
|
||||||
*question* - **answered**
|
|
||||||
|
|
||||||
**Loop policy for a non-retriable error where the job CAN be marked failed**
|
|
||||||
|
|
||||||
Distinct from entry 42 and not covered by it. Mode A can contain the failure on the job row, so stopping the loop would let one poison job halt transcription for every other job. User selected containment: mark the job FAILED, which is visible in the UI and resubmittable, and continue polling.
|
|
||||||
|
|
||||||
#### [44] Containment write uses its own transaction
|
|
||||||
|
|
||||||
*risk* - **noted**
|
|
||||||
|
|
||||||
**Terminal write runs on a possibly dirty session**
|
|
||||||
|
|
||||||
_advance_job_with_containment rolls back the caller session before marking the job FAILED, and calls update_job_state with no session so the service owns and commits its own transaction. This satisfies plan task 3 atomicity: the terminal write cannot be left half-applied by whatever failure poisoned the caller session.
|
|
||||||
|
|
||||||
#### [45] test_run_worker_loop_survives_process_next_exception replaced
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**An existing test encoded the defective behaviour**
|
|
||||||
|
|
||||||
That test asserted the loop SURVIVES a RuntimeError and continues, which is exactly the Mode B defect. It was replaced by test_run_worker_loop_stops_on_non_retriable_exception, plus a new test_run_worker_loop_survives_retriable_exception so suppression of genuinely transient faults stays covered. Unlike Phase 3, changing test logic here is the point of the phase. Both new guards plus the Mode A guard were verified to FAIL on pre-fix code: the Mode B test times out, which is the infinite spin made visible.
|
|
||||||
|
|
||||||
### Phase 6 - CI enforcement
|
|
||||||
|
|
||||||
#### [46] Remote is Gitea 1.27.2, not GitHub
|
|
||||||
|
|
||||||
*comment* - **noted**
|
|
||||||
|
|
||||||
**The plan assumes GitHub Actions**
|
|
||||||
|
|
||||||
Remote is bbchops/transcription on Gitea 1.27.2, which reads .github/workflows/ and proxies actions/checkout@v4 to GitHub. Workflow syntax needed no change. Note the remote default branch is traumatized, not main.
|
|
||||||
|
|
||||||
#### [47] Runner availability cannot be confirmed via the API
|
|
||||||
|
|
||||||
*risk* - **noted**
|
|
||||||
|
|
||||||
**Repo-scoped runner list returns 0; admin endpoint returns 403**
|
|
||||||
|
|
||||||
Existence of CI could not be asserted by query on this host. Proven instead by observation: an instance-level runner named docker-runner executed the jobs. Anyone re-verifying this must trigger a run rather than trust the runner API.
|
|
||||||
|
|
||||||
#### [48] CI writes a .env file instead of exporting an env var
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Settings reads the .env file; the external-test skip guard reads os.getenv**
|
|
||||||
|
|
||||||
The two read different sources, and locally both conditions hold at once, which is why 4 tests skip. Measured in CI: no .env = 115 failed / 18 errors; exported dummy var = 3 failed (externals un-skip and hit the network); written .env file = the exact local baseline. Only openrouter_api_key is required.
|
|
||||||
|
|
||||||
#### [49] CI invokes pre-commit rather than repeating ruff/ty commands
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**Plan task 2 asks CI to run the same checks as local**
|
|
||||||
|
|
||||||
Satisfied structurally rather than by copying command strings: CI runs uv run pre-commit run --all-files, so the checks have a single definition in .pre-commit-config.yaml and CI cannot drift from local. Hooks are language: system and uv run puts .venv on PATH.
|
|
||||||
|
|
||||||
#### [50] Platform-dependent prompt name guard, caught by CI on its first green run
|
|
||||||
|
|
||||||
*deviation* - **noted**
|
|
||||||
|
|
||||||
**The direct-child name guard relied on Path(name).name != name**
|
|
||||||
|
|
||||||
On POSIX, backslash is an ordinary filename character, so nested\prompt.md passed the direct-child guard and failed later as NOT_FOUND instead of VALIDATION. Windows can never reproduce it. No traversal was possible because the path.parent != root check still held, so severity is a wrong error category plus a red gate. Fixed by rejecting / and \ explicitly, matching the ^[^/\\]+$ pattern config.PromptFilename already used. User approved the code fix over weakening the test.
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# V4.7 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the frozen boundary for V4.7, an **architectural cleanup and evidence-model re-alignment release**. V4.6 remains the behavioral baseline. V4.7 introduces **no new user-facing features**; it completes the structural work V4.6 deferred, simplifies the evidence model down to what the application actually uses, and closes the correctness items opened during V4.6 implementation.
|
|
||||||
|
|
||||||
Every item in scope is traceable either to a review finding ID in [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md) or to a numbered entry in the V4.6 implementation review log. Any change that cannot be traced to one of those is out of scope.
|
|
||||||
|
|
||||||
All image *presentation*, media, and telemetry-presentation work is deferred to V4.8. See [`ver4.8/feature_backlog_v4_8.md`](../ver4.8/feature_backlog_v4_8.md).
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
- Collapse the duplicated evidence model so that `job_source` records **membership and queue state** and `execution_attempt` records **evidence**, with no overlap.
|
|
||||||
- Remove the `ProcessingArtifact` subsystem, which has executed exactly once in the application's history, and move orientation normalization to ingest where it belongs.
|
|
||||||
- Complete [MED-14] by decomposing `SourceService`, which still owns four domain models.
|
|
||||||
- Correct the run-time measurement window so provider latency can be trusted before anything is built on top of it.
|
|
||||||
- Stop the worker from silently swallowing programming errors.
|
|
||||||
- End the dual-spelling persistence of `JobSourceStatus`.
|
|
||||||
- Make the `ruff` / `ty` gate enforceable in CI rather than only on a developer machine that has run `pre-commit install`.
|
|
||||||
|
|
||||||
## Confirmed Operating Context
|
|
||||||
|
|
||||||
These answers are frozen for V4.7 and govern every decision below.
|
|
||||||
|
|
||||||
| Question | Answer |
|
|
||||||
| :--- | :--- |
|
|
||||||
| Database | **SQLite only.** PostgreSQL remains the intended destination. The V4.6 re-level already resolved the FK cycle with `use_alter=True`. |
|
|
||||||
| Topology | **Single user, single process, single worker.** Unchanged from V4.6. |
|
|
||||||
| Schema evolution | **Re-level from current metadata**, exactly as V4.6. No Alembic, no `_upgrade_*` chain. |
|
|
||||||
| Existing data | The live database is populated. V4.7 is **schema-affecting**: three structural changes plus a one-time image backfill, delivered by a single `tools/migrate_v46_to_v47.py`. |
|
|
||||||
| Release character | **Architectural cleanup and evidence-model re-alignment.** No new features. |
|
|
||||||
| Image fidelity | **Visually lossless is sufficient.** Measured at 51.5-55.0 dB PSNR for a single re-encode generation. Bit-exact preservation was considered and rejected as unnecessary complexity. |
|
|
||||||
| Provider settings | `WORKER_PROVIDER_TIMEOUT_SECONDS=30.0`, `WORKER_MAX_RETRIES=1`, calibrated 2026-08-18. Not revisited in V4.7. |
|
|
||||||
|
|
||||||
## Evidence Gathered
|
|
||||||
|
|
||||||
The decisions below rest on measurements taken against the live database on 2026-08-18, not on inspection alone.
|
|
||||||
|
|
||||||
| Measurement | Result |
|
|
||||||
| :--- | :--- |
|
|
||||||
| `job_source.raw_transcription` vs latest attempt | **77/77 identical** |
|
|
||||||
| `job_source.ai_metadata` vs `normalized_metadata` | **77/77 identical** |
|
|
||||||
| `job_source.raw_api_response` vs `sdk_response_snapshot` | **77/77 identical** |
|
|
||||||
| `job_source.error_detail` vs attempt `error_detail` | 2/2 identical |
|
|
||||||
| `job_source.status` vs `execution_attempt.status` | 0/79 textually identical - the dual-spelling defect [45] |
|
|
||||||
| `job_source` rows with more than one attempt | 1 of 79 |
|
|
||||||
| `processing_artifact` rows in existence | **2**, both from one job on 2026-08-16, against 77 successful transcriptions |
|
|
||||||
| Source images carrying EXIF orientation 3 | **58 of 79**, of which only 1 was ever normalized |
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Evidence Model Simplification
|
|
||||||
|
|
||||||
`job_source` began as the many-to-many link between `job` and `source` and accreted response-capture fields over time. `execution_attempt`, added later in V4.2 (commit `6bd4cbb`), captures the same information in more detail. The measurements above show the overlap is total, not partial.
|
|
||||||
|
|
||||||
**`job_source` is stripped, not deleted.** It cannot be folded into `execution_attempt`, because it carries state that exists when no provider call has occurred:
|
|
||||||
|
|
||||||
- `store.py:249,313` create rows with `status=PENDING` **at job creation**, before any call.
|
|
||||||
- `workflows.py:432-442` selects work by `status != TRANSCRIBED` on `job.job_sources`.
|
|
||||||
- `jobs.py:378-384` cancel writes a terminal state with **no provider call at all**, so no attempt row could carry it.
|
|
||||||
|
|
||||||
An append-only evidence table cannot express "queued, not yet attempted" or "cancelled before any call". The junction survives; the duplicated evidence does not.
|
|
||||||
|
|
||||||
**Retained:** `id`, `job_id`, `source_id`, `status`.
|
|
||||||
|
|
||||||
**Removed:** `raw_transcription`, `ai_metadata`, `raw_api_response`, `executed_at`, `error_detail`.
|
|
||||||
|
|
||||||
**Added:** `JobSourceStatus.CANCELLED`, so cancellation stops overloading `FAILED` plus the free-text string `"Cancelled by user"`. This is what retires `error_detail`.
|
|
||||||
|
|
||||||
**Unchanged:** the retry reset at `jobs.py:411-424`. Flipping `FAILED` back to `PENDING` loses no history, because `ExecutionAttempt`'s `UniqueConstraint(job_id, source_id, attempt_number)` (`models.py:389`) already preserves every prior attempt. This is confirmed in live data: attempt 1 `FAILED`/`local_timeout` and attempt 2 `TRANSCRIBED` are both retained. Adding a second `job_source` row per retry would duplicate that mechanism and break the one-row-per-`(job, page)` assumption in `read_job_source_for_job` and `sources.py:570-574` - where uniqueness is enforced **in code, not by a database constraint**.
|
|
||||||
|
|
||||||
This item absorbs review log [45], since both changes rewrite `JobSourceStatus` persistence and must land as one migration.
|
|
||||||
|
|
||||||
### 2. ProcessingArtifact Removal and Ingest Normalization
|
|
||||||
|
|
||||||
`ProcessingArtifact` is a generic container for derived data products, with a `CheckConstraint` enforcing that content is either inline JSON or an external file, never both. Two rows exist. The quality-warnings path at `workflows.py:560-571` writes one on **every** successful page, yet 77 successful transcriptions produced a single row, so the subsystem postdates nearly all data and has effectively never run.
|
|
||||||
|
|
||||||
Orientation normalization itself is **not** dispensable and was **not** a red herring. 58 of 79 stored images carry EXIF orientation 3, and the raw decoded pixels of the page that prompted the original investigation are genuinely upside down. Sending those bytes unrotated sends an inverted page to the model.
|
|
||||||
|
|
||||||
The fix is to normalize at ingest rather than derive at transcription time:
|
|
||||||
|
|
||||||
- Rotate on upload, in `media_storage`, before the image is stored. Every stored byte is then already upright and no derivative needs to exist.
|
|
||||||
- Use Pillow with `qtables=im.quantization`, `subsampling=JpegImagePlugin.get_sampling(im)`, `optimize=True`. Measured against the current `quality=95, subsampling=0` settings at `normalization.py:84-85`, this is **better on both axes**: 51.5-55.0 dB PSNR versus 50.0-53.5 dB, and roughly 6% smaller output versus 38% larger.
|
|
||||||
- Strip the EXIF orientation tag after rotating.
|
|
||||||
- No archival master is retained. No external `jpegtran` dependency is introduced. No MCU-alignment rejection path is needed, because Pillow handles any dimensions - including the single 2306x2019 outlier.
|
|
||||||
|
|
||||||
Then delete: the `processing_artifact` table, the `ProcessingArtifact` model, the ~283-line artifact cluster in `sources.py` (lines 732-1015), `resolve_provider_input`, and the artifact branch of `build_evidence_export`. The `transcription_quality_warnings` payload folds into `execution_attempt.normalized_metadata`.
|
|
||||||
|
|
||||||
Deleting stored images is not involved; the 58 already-ingested rotated images are rotated **in place** by the migration. No live integrity check is invalidated: `Source` has no digest column, and the only stored digests are `ExecutionAttempt.request_manifest_sha256` - a hash of the request manifest, correct as history - and `ProcessingArtifact.payload_sha256`, which is removed with the table.
|
|
||||||
|
|
||||||
### 3. SourceService Decomposition ([MED-14])
|
|
||||||
|
|
||||||
`services/sources.py` is **1,389 lines** and `SourceService` owns `Source`, `JobSource`, `ExecutionAttempt`, and `ProcessingArtifact`.
|
|
||||||
|
|
||||||
Item 2 removes the `ProcessingArtifact` responsibility by **deletion rather than extraction**. The previously planned `services/artifacts.py` is therefore cancelled - extracting ~283 lines into a new module and then deleting that module would be wasted work.
|
|
||||||
|
|
||||||
What remains is the `ExecutionAttempt` cluster, moved to **`services/evidence.py` (~174 lines)**: `read_latest_execution_attempt` (216-245) with its `LatestExecutionAttempt` read model, `promote_machine_attempt` (679-710), `list_execution_attempts` (710-732), and `build_evidence_export` (1015-1107).
|
|
||||||
|
|
||||||
**`update_job_source_transcription` stays in `sources.py`.** The V4.6 deferral note proposed moving it to `workflows.py` as orchestration; that proposal is not adopted. The method writes `JobSource` and `ExecutionAttempt` inside one session scope and derives `attempt_number` at lines 595-600, and `services.instructions.md:63-65` requires the transcript update and the paired terminal status change to commit or roll back together. `services.instructions.md:72` assigns session-aware write helpers to services and commit-boundary control to orchestration, so the current placement already satisfies the instruction file. Splitting the two writes across modules is the most plausible way that atomicity later gets broken. The method will shrink under item 1, since several of the fields it writes cease to exist.
|
|
||||||
|
|
||||||
Expected result: `sources.py` lands near **900 lines**.
|
|
||||||
|
|
||||||
**`.github/instructions/services.instructions.md` is revised as part of this item** (review log [59]), after the move rather than before. The instruction file's line 11 rule, `1 service class per data model`, is table-shaped rather than aggregate-shaped and is the measured cause of the 1,389-line module this item exists to break up; leaving it unchanged would license the same growth again. The file is also silent on `job_source` and `document_person`, the junctions where the four core components intersect, so ownership of those has never been written down. Sequencing the revision after the decomposition makes the refactor the empirical test of the rule: if the new rule is right, the resulting module boundaries follow from it, and if the code has to be bent to fit, the rule is wrong.
|
|
||||||
|
|
||||||
### 4. Run-Time Measurement Window (review log [55])
|
|
||||||
|
|
||||||
`services/workflows.py:221` sets `monotonic_started_at` **before** provider-input preparation and the `session.commit()` at line 228. Line 251 computes `elapsed_seconds` from it. But the `asyncio.wait_for` timeout at lines 240-249 wraps **only** `_call_transcriber`.
|
|
||||||
|
|
||||||
`duration_ms` therefore measures a strictly wider window than the budget that governs it. This is observable in the migrated data: three historical `local_timeout` rows recorded 20.4 / 20.8 / 22.0 s against a 20.0 s timeout.
|
|
||||||
|
|
||||||
In scope: either record provider latency as a distinct value, or move `monotonic_started_at` to immediately before the `wait_for`. Whichever is chosen, the resulting figure must be the quantity the timeout actually governs. Item 2 also removes normalization from this window entirely, which shrinks the discrepancy but does not by itself fix it.
|
|
||||||
|
|
||||||
This item **must land before any V4.8 telemetry presentation work**.
|
|
||||||
|
|
||||||
### 5. Worker Exception Handling (review log [8])
|
|
||||||
|
|
||||||
`worker.py:96-106`, `handle_worker_exceptions`, catches bare `Exception`, logs it, and suppresses it. A programming error inside the worker loop is therefore indistinguishable from a transient provider fault and is retried silently with no UI signal.
|
|
||||||
|
|
||||||
In scope: distinguish genuinely retriable faults from programming errors, and ensure a non-retriable error surfaces rather than looping. Retry counting and terminal-state transitions remain governed by `services.instructions.md:63-65`.
|
|
||||||
|
|
||||||
### 6. CI Enforcement of the Quality Gate ([HIGH-06], review log [40])
|
|
||||||
|
|
||||||
`.github/workflows/` is empty. The `ruff check` and `ty check` gate established in V4.6 Phase 7 exists only in `.pre-commit-config.yaml`, which is inert until a developer runs `pre-commit install`.
|
|
||||||
|
|
||||||
In scope: a CI workflow running `ruff check`, `ty check`, and `pytest` on push and pull request, using the same commands as the local hooks so the two cannot drift.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- **All image and media presentation work.** Pan and zoom on Source Detail, the homepage gallery, multi-portrait support, image descriptions, and background wallpaper are V4.8.
|
|
||||||
- **The model-performance rollup** (review log [54]). It depends on item 4 and is a new user-facing view.
|
|
||||||
- **Reducing `update_job_source_transcription`.** See section 3.
|
|
||||||
- **Bit-exact image preservation.** Considered and rejected; see Confirmed Operating Context.
|
|
||||||
- **PostgreSQL cutover.**
|
|
||||||
- **Re-tuning `WORKER_PROVIDER_TIMEOUT_SECONDS` or `WORKER_MAX_RETRIES`.** Calibrated 2026-08-18 against measured per-model durations.
|
|
||||||
- **Removing slow models from `PROVIDER_MODELS`** (review log [53]). A configuration judgement, deliberately left with the operator.
|
|
||||||
- **Any new feature.**
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. Cleanup Only
|
|
||||||
|
|
||||||
V4.7 changes structure and correctness. It does not change what the application does for a user. If a change would be visible on a page as new capability, it belongs in V4.8.
|
|
||||||
|
|
||||||
### B. One Home Per Fact
|
|
||||||
|
|
||||||
After V4.7, any given piece of evidence is stored in exactly one place. `job_source` holds membership and state; `execution_attempt` holds evidence. Denormalized convenience copies are not reintroduced, and if a read becomes awkward the fix is a query or a read model, not a duplicated column.
|
|
||||||
|
|
||||||
### C. Delete Before Refactor
|
|
||||||
|
|
||||||
Item 2 deletes the artifact subsystem before item 3 restructures what remains. Code scheduled for deletion is never extracted, renamed, or moved first.
|
|
||||||
|
|
||||||
### D. Simplicity Over Edge-Case Management
|
|
||||||
|
|
||||||
Where two approaches both satisfy the requirement, the one with fewer moving parts wins. This is why rotation uses Pillow rather than a lossless DCT transform, and why no archival master is kept.
|
|
||||||
|
|
||||||
### E. Measurement Before Presentation
|
|
||||||
|
|
||||||
Item 4 precedes all V4.8 telemetry work. A dashboard built on a conflated metric looks authoritative and quietly misleads.
|
|
||||||
|
|
||||||
### F. One Migration, Backed Up
|
|
||||||
|
|
||||||
All schema and data changes land in a single `tools/migrate_v46_to_v47.py`: idempotent, never invoked at startup, never run by the test suite, following the `tools/migrate_v45_to_v46.py` conventions. `data/transcription.db` **and** `data/documents/` are backed up before it runs, because the image backfill rewrites files in place.
|
|
||||||
|
|
||||||
### G. The Instruction Files Are the Standard
|
|
||||||
|
|
||||||
`.github/instructions/services.instructions.md` and `ui.instructions.md` govern. Where this document and an instruction file disagree, the instruction file wins.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- `job_source` carries exactly `id`, `job_id`, `source_id`, `status`; every evidence read resolves through `execution_attempt`.
|
|
||||||
- `JobSourceStatus.CANCELLED` exists and cancellation no longer writes free text into a removed column.
|
|
||||||
- `job_source.status` and `execution_attempt.status` persist with one spelling, and existing rows are consistent.
|
|
||||||
- The `processing_artifact` table, its model, and its service cluster no longer exist.
|
|
||||||
- Newly uploaded images are stored upright with no EXIF orientation tag, and the 58 pre-existing rotated images have been backfilled.
|
|
||||||
- `services/sources.py` is materially smaller, with `ExecutionAttempt` responsibilities in `services/evidence.py` and `update_job_source_transcription` unmoved.
|
|
||||||
- `services.instructions.md` states an aggregate-shaped ownership rule, names an owning service for every model including the junctions, and no longer contradicts itself on multi-table operations.
|
|
||||||
- The recorded duration reflects only the operation the timeout governs.
|
|
||||||
- A programming error in the worker loop is distinguishable from a provider fault.
|
|
||||||
- `ruff check` reports no findings; `ty check` reports **0 diagnostics**, the V4.6 exit state.
|
|
||||||
- The full test suite passes.
|
|
||||||
- CI runs the same `ruff` / `ty` / `pytest` gate as the local hooks.
|
|
||||||
- No new user-facing behavior.
|
|
||||||
|
|
||||||
## Scope Freeze Gate
|
|
||||||
|
|
||||||
This boundary is frozen. Adding an item requires a finding ID or a review-log entry, and an explicit note recording the addition.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.7 Implementation Plan](implementation_plan_v4_7.md)
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md) - finding IDs
|
|
||||||
- [V4.6 Review Log](../ver4.6/review_log_v4_6.md) - resolves the `review log [N]` citations used throughout this document
|
|
||||||
- [V4.6 Scope Boundary](../ver4.6/scope_boundary_v4_6.md) - the baseline this release builds on
|
|
||||||
- [V4.6 Implementation Plan](../ver4.6/implementation_plan_v4_6.md)
|
|
||||||
- [V4.8 Feature Backlog](../ver4.8/feature_backlog_v4_8.md) - where deferred feature work is parked
|
|
||||||
- `.github/instructions/services.instructions.md`
|
|
||||||
- `.github/instructions/ui.instructions.md`
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# V4.8 Feature Backlog
|
|
||||||
|
|
||||||
**Status: not scoped.** This is a parking document, not a frozen boundary. It records feature work deferred out of V4.6 and V4.7 together with the evidence gathered so far, so that scoping V4.8 does not start from a blank page.
|
|
||||||
|
|
||||||
V4.8 is the first release since V4.5 to add **new user-facing behavior**. V4.6 was pure remediation and V4.7 is architectural cleanup; both were held to "no new features." That constraint ends here, which means V4.8 needs a different verification gate: V4.6 and V4.7 could be validated by "the suite still passes unchanged," and V4.8 cannot.
|
|
||||||
|
|
||||||
## Dependency on V4.7
|
|
||||||
|
|
||||||
**The model-performance rollup below must not begin until V4.7 Phase 4 lands.** `duration_ms` currently measures provider call *plus* image normalization, artifact persistence, and a DB commit, while the timeout governs only the provider call. A rollup built on it would chart preprocessing time mixed with provider latency and look authoritative while quietly misleading. V4.7 Phase 1 removes normalization and artifact persistence from that window, but the commit remains inside it until Phase 4. See [V4.7 scope boundary section 4](../ver4.7/scope_boundary_v4_7.md).
|
|
||||||
|
|
||||||
## Candidate Features
|
|
||||||
|
|
||||||
### 1. Pan and Zoom on Source Detail
|
|
||||||
|
|
||||||
**Practicality: high. Effort: S.**
|
|
||||||
|
|
||||||
`ui/components/document_panzoom.py` existed and was **deleted in V4.6 Phase 5** (`6a3ee26`) because it was exported but wired to no page. It is 136 lines and recoverable:
|
|
||||||
|
|
||||||
```
|
|
||||||
git show 6a3ee26^:src/transcription/ui/components/document_panzoom.py
|
|
||||||
```
|
|
||||||
|
|
||||||
It already handled both images and PDFs (the latter via an iframe).
|
|
||||||
|
|
||||||
Two things must change on reintroduction - this is not a straight revert:
|
|
||||||
|
|
||||||
- It loaded Panzoom from the **unpkg CDN**. For an archival application the library should be vendored locally, otherwise the viewer breaks offline and depends on a third party staying available.
|
|
||||||
- It carried its own `_document_url()` helper. V4.6 Phase 5 extracted exactly that logic into `ui/components/media_urls.py` as `resolve_media_url`. Reintroducing the old helper would recreate the duplication Phase 5 removed.
|
|
||||||
|
|
||||||
Scope note: apply it to **Source Detail only**. `dark_room_viewer` (`ui/components/viewers.py`) is shared by four pages - `sources_page.py:268`, `home_page.py:25` and `:88`, `people_page.py:453`, `documents_page.py:524` - so a flag on it would leak pan-zoom into the homepage and document detail, which is not wanted. Add a separate component and use it only at `sources_page.py:268`.
|
|
||||||
|
|
||||||
Numbering note: the Phase 5 commit message states pan-zoom would return "in V4.7 alongside the other photo/image work." Moving it to V4.8 preserves that **intent** - it stays grouped with the photo work - and changes only the release number.
|
|
||||||
|
|
||||||
### 2. Homepage Image Gallery
|
|
||||||
|
|
||||||
**Practicality: high. Effort: S. Recommended first feature.**
|
|
||||||
|
|
||||||
The storage layer is already built:
|
|
||||||
|
|
||||||
- `ui/homepage_store.py:82` `list_homepage_images()` already returns **every** stored image, sorted by modification time.
|
|
||||||
- `store_homepage_image()` already accumulates files rather than overwriting.
|
|
||||||
- Today the UI calls only `latest_homepage_image()` and displays one image. `list_homepage_images()` is currently exercised **only by tests**.
|
|
||||||
|
|
||||||
So multi-image upload is effectively done; what is missing is presentation. NiceGUI 3.13.0 provides `ui.carousel` for left/right navigation and `ui.timer` for rotation.
|
|
||||||
|
|
||||||
Sub-items:
|
|
||||||
|
|
||||||
- Multi-image display with left/right navigation - small, mostly wiring.
|
|
||||||
- Optional slideshow rotating every ~10 minutes.
|
|
||||||
|
|
||||||
**Performance caveat:** `list_homepage_images()` performs a directory scan with a `stat()` per file on every call, and `home_page.py` already performs blocking I/O in the page handler (V4.6 review log [25], which was deliberately left alone). A rotating timer that re-enumerates on every tick would repeat that scan indefinitely. Enumerate once at page load and cache the list.
|
|
||||||
|
|
||||||
### 3. Multiple Person Portraits
|
|
||||||
|
|
||||||
**Practicality: medium. Effort: M/L. Defer behind item 2.**
|
|
||||||
|
|
||||||
`Person.portrait_path` is a **single string column**. Supporting multiple portraits requires a new table, a data migration, and upload UI - a materially larger job than item 2, and a different one.
|
|
||||||
|
|
||||||
### 4. Image Descriptions
|
|
||||||
|
|
||||||
**Practicality: medium, conditional. Effort: M.**
|
|
||||||
|
|
||||||
Homepage images are **filesystem-only with no metadata store**, so a caption has nowhere to live today. This needs either a sidecar JSON file or a real table.
|
|
||||||
|
|
||||||
This is cheap **only if** item 3 is being done at the same time, since both need the same metadata layer. Designing that layer twice would be wasteful; design it once or not at all.
|
|
||||||
|
|
||||||
### 5. Model-Performance Rollup (V4.6 review log [54])
|
|
||||||
|
|
||||||
**Practicality: high, but blocked. Effort: M.**
|
|
||||||
|
|
||||||
Run-time telemetry is already captured and is per page: `execution_attempt.duration_ms` is a required non-null field written on all three paths in `workflows.py` (success 278, `TimeoutError` 295, general failure 330), with failures using a monotonic clock. Verified against the live database: 80 rows across 80 distinct (job, source, attempt) combinations, one row per page - the largest job has 60 attempts across 60 distinct pages - and zero nulls. Token counts live on the same row in `normalized_metadata.usage`, so tokens-per-second is already derivable without a join.
|
|
||||||
|
|
||||||
What is missing is **aggregation**. The figure is visible only for the latest attempt of one source at a time (`sources_page.py:400`), rendered raw as `"27612 ms"`. There is no rollup by model, prompt, or document.
|
|
||||||
|
|
||||||
The gap is concrete: calibrating the provider timeout on 2026-08-18 required hand-written SQL against the database, because the application could not answer "which model is slow."
|
|
||||||
|
|
||||||
Proposed shape: median / p95 / max duration, tokens per second, and a timeout rate, grouped by model. **Blocked on V4.7 Phase 4.**
|
|
||||||
|
|
||||||
### 6. Desaturated Background Wallpaper
|
|
||||||
|
|
||||||
**Practicality: low. Recommendation: do not build, or gate behind a setting defaulted off.**
|
|
||||||
|
|
||||||
Trivial to implement (`ui.add_css` with a CSS `filter`), but this is a dense archival data application - transcripts, JSON evidence panels, data tables. A background image behind all of that costs contrast and legibility on every page, for aesthetic gain only.
|
|
||||||
|
|
||||||
## Suggested Grouping
|
|
||||||
|
|
||||||
If V4.8 is scoped as one release, the natural split is:
|
|
||||||
|
|
||||||
**Track A - image experience:** items 1 and 2. Both are small, both are self-contained UI work, and item 2's storage layer already exists. This is the highest value for the least risk.
|
|
||||||
|
|
||||||
**Track B - metadata layer:** items 3 and 4 together, since they share a table. Only worth starting if both are wanted.
|
|
||||||
|
|
||||||
**Track C - telemetry:** item 5, gated on V4.7 Phase 4.
|
|
||||||
|
|
||||||
Item 6 is not recommended.
|
|
||||||
|
|
||||||
## Open Questions for Scoping
|
|
||||||
|
|
||||||
- Should Track B happen at all, or is one portrait per person sufficient?
|
|
||||||
- Should the slideshow interval be configurable, or fixed?
|
|
||||||
- Should the model-performance rollup be its own page, or a panel on an existing one?
|
|
||||||
- Should vendored Panzoom be committed to the repository, or fetched at build time?
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4.7 Scope Boundary](../ver4.7/scope_boundary_v4_7.md) - the blocking dependency for item 5
|
|
||||||
- [V4.6 Scope Boundary](../ver4.6/scope_boundary_v4_6.md)
|
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
|
||||||
- `.github/instructions/ui.instructions.md`
|
|
||||||
- `src/transcription/ui/homepage_store.py` - existing multi-image storage
|
|
||||||
- `src/transcription/ui/components/media_urls.py` - canonical URL resolution
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# System Architecture (Version 4)
|
|
||||||
|
|
||||||
This document describes the production architecture of the document transcription system.
|
|
||||||
|
|
||||||
## Architecture Objectives
|
|
||||||
|
|
||||||
- Preserve original source material, per-execution machine output, and separate human revision.
|
|
||||||
- Support batching one or more images into ordered multi-page documents.
|
|
||||||
- Capture submission-time prompt provenance and a per-page OpenRouter SDK response snapshot.
|
|
||||||
- Execute page transcription concurrently with bounded `asyncio` workers.
|
|
||||||
- Maintain relational portability across SQLite and PostgreSQL.
|
|
||||||
- Keep operator workflows cross-platform and Python-driven.
|
|
||||||
- Support one role-bearing link per Person and Document through an extensible role registry.
|
|
||||||
- Support registry-driven document classification with protected semantic built-ins.
|
|
||||||
|
|
||||||
## Core Capabilities
|
|
||||||
|
|
||||||
- Ingest one or more images into sequential `Source` pages under a `Document`.
|
|
||||||
- Execute asynchronous vision transcription with bounded worker concurrency.
|
|
||||||
- Preserve original source files with SHA-256 digests and byte sizes.
|
|
||||||
- Freeze prompt text, prompt hash, model, and explicitly configured sampling parameters on each `Job`.
|
|
||||||
- Preserve page-level machine output, normalized metadata, and an SDK-serialized OpenRouter response snapshot on `JobSource`.
|
|
||||||
- Organize historical `Person` records through UUID-identified Document links and extensible roles.
|
|
||||||
- Classify Documents through a UUID-identified registry with hidden semantic built-ins and unique labels.
|
|
||||||
- Maintain human revision separately from machine-generated text.
|
|
||||||
- Isolate page failures so multi-page jobs can complete with partial success.
|
|
||||||
- Operate across supported platforms through Python-based application and maintenance tooling.
|
|
||||||
|
|
||||||
V4.2 extends this baseline with immutable execution attempts, exact OpenRouter transport evidence, safe
|
|
||||||
versioned exports, and provider-neutral derived-artifact provenance. `JobSource` remains the mutable queue and
|
|
||||||
compatibility projection; `ExecutionAttempt` is the authoritative append-only processing history. See the
|
|
||||||
[V4.2 Scope Boundary](../ver4.2/scope_boundary_v4_2.md).
|
|
||||||
|
|
||||||
## Technical Stack
|
|
||||||
|
|
||||||
- **Runtime:** Python 3.12 or later.
|
|
||||||
- **Web application:** FastAPI and NiceGUI.
|
|
||||||
- **Persistence:** SQLModel and SQLAlchemy, with SQLite and PostgreSQL support.
|
|
||||||
- **Validation and settings:** Pydantic V2 and pydantic-settings.
|
|
||||||
- **Concurrency:** Python `asyncio` workers.
|
|
||||||
- **Vision integration:** OpenRouter through the application's provider adapter.
|
|
||||||
- **Testing and quality:** pytest, pytest-asyncio, Ruff, and ty.
|
|
||||||
|
|
||||||
## Runtime Topology
|
|
||||||
|
|
||||||
The runtime operates as an asynchronous Python application:
|
|
||||||
|
|
||||||
- FastAPI + NiceGUI web application process.
|
|
||||||
- In-process `asyncio` worker engine for transcription execution.
|
|
||||||
- Relational persistence via SQLModel / SQLAlchemy.
|
|
||||||
- Pydantic V2 validation across API payloads, prompt configuration, and structured metadata.
|
|
||||||
|
|
||||||
^^^mermaid
|
|
||||||
flowchart LR
|
|
||||||
U[Browser User] --> A[FastAPI + NiceGUI App]
|
|
||||||
A --> W[Asyncio Worker Engine]
|
|
||||||
A --> DB[(Relational DB)]
|
|
||||||
W --> P[Vision Provider APIs]
|
|
||||||
W --> DB
|
|
||||||
^^^
|
|
||||||
|
|
||||||
## Lifecycle Ownership
|
|
||||||
|
|
||||||
Application lifespan owns runtime setup and teardown:
|
|
||||||
|
|
||||||
- Initialize logging, settings, directories, and prompt configuration.
|
|
||||||
- Manage asynchronous database engine connection pools.
|
|
||||||
- Execute database bootstrap or migrations.
|
|
||||||
- Recover stale or interrupted jobs on startup.
|
|
||||||
- Manage graceful shutdown of active background tasks.
|
|
||||||
|
|
||||||
## Layered Module Structure
|
|
||||||
|
|
||||||
### Interface Layer
|
|
||||||
|
|
||||||
- `src/transcription/ui/**`
|
|
||||||
- `src/transcription/api/**`
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- Render document, source, person, job, and classification views.
|
|
||||||
- Accept user input for uploads, editing, linking, and revisions.
|
|
||||||
- Present structured validation and conflict feedback.
|
|
||||||
|
|
||||||
### Application and Async Worker Layer
|
|
||||||
|
|
||||||
- `src/transcription/services/workflows.py`
|
|
||||||
- `src/transcription/worker.py`
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- Orchestrate uploads, job creation, and status transitions.
|
|
||||||
- Execute per-page provider calls through bounded concurrency.
|
|
||||||
- Persist page-level outcomes and update aggregate job state.
|
|
||||||
|
|
||||||
### Domain and Service Layer
|
|
||||||
|
|
||||||
- `src/transcription/db/models.py`
|
|
||||||
- `src/transcription/services/documents.py`
|
|
||||||
- `src/transcription/services/sources.py`
|
|
||||||
- `src/transcription/services/jobs.py`
|
|
||||||
- `src/transcription/services/people.py`
|
|
||||||
- `src/transcription/services/workflows.py`
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- Keep one primary service boundary per aggregate: Documents, Sources, Jobs, and People.
|
|
||||||
- Documents own document records and the document-type registry.
|
|
||||||
- Sources own source records, revisions, source media formats, MIME resolution, and page execution evidence.
|
|
||||||
- Jobs own job lifecycle state and transitions.
|
|
||||||
- People own person records, relationship roles, document-person links, and portrait media.
|
|
||||||
- Apply deterministic conflict handling for relationship-role writes.
|
|
||||||
- Synchronize each Document's complete Person link set in the same transaction as Document fields.
|
|
||||||
- Resolve and validate registry records by UUID; use hidden semantic keys only for application-owned built-in behavior.
|
|
||||||
|
|
||||||
### Source Media Policy
|
|
||||||
|
|
||||||
- `services/sources.py` is the single authority for accepted Source extensions and canonical MIME types.
|
|
||||||
- Storage and provider payload loading must call the same Source validation functions.
|
|
||||||
- Supported Source formats are JPEG, PNG, TIFF, and PDF.
|
|
||||||
- Upload is an interface action, not a domain aggregate. Service names, errors, and workflow variables use
|
|
||||||
`Source` terminology; compatibility aliases may remain temporarily at old import boundaries.
|
|
||||||
|
|
||||||
### Infrastructure Layer
|
|
||||||
|
|
||||||
- `src/transcription/db/**`
|
|
||||||
- `src/transcription/providers/**`
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- Provide async database sessions and engine configuration.
|
|
||||||
- Provide provider adapters for vision model execution.
|
|
||||||
|
|
||||||
## Core Workflows
|
|
||||||
|
|
||||||
### 1. Multi-Page Transcription
|
|
||||||
|
|
||||||
1. User uploads one or more images for a `Document`.
|
|
||||||
2. System stores files, hashes them, creates ordered `Source` rows, and creates a `Job`.
|
|
||||||
3. Worker claims the job, marks it `processing`, resolves metadata-directed orientation, and sends either the
|
|
||||||
immutable original or an exact normalized derivative to the provider.
|
|
||||||
4. Each provider call appends an `ExecutionAttempt` with its request manifest, transport evidence, SDK snapshot,
|
|
||||||
normalized metadata, timing, and outcome.
|
|
||||||
5. The linked `JobSource` is updated as a compatibility projection. The first successful attempt establishes
|
|
||||||
`Source.preferred_execution_attempt_id` and `Source.raw_transcription`; later successes remain candidates.
|
|
||||||
6. Aggregate status becomes `completed`, `partial_success`, or `failed`.
|
|
||||||
|
|
||||||
### 2. Document-Person Relationship Management
|
|
||||||
|
|
||||||
1. User opens Document Create or Edit.
|
|
||||||
2. UI loads one Linked People table containing Person and Role.
|
|
||||||
3. Add, Edit, and Delete operations change staged UI state only.
|
|
||||||
4. Service validates the complete desired set and computes deterministic add, update, and remove deltas.
|
|
||||||
5. Document fields and links commit once in one transaction; any failure leaves both unchanged.
|
|
||||||
|
|
||||||
### 3. Document Type Management
|
|
||||||
|
|
||||||
1. User selects a registry-backed document type for a document.
|
|
||||||
2. Service resolves the Document Type UUID.
|
|
||||||
3. Persistence stores the `document_type_id` reference.
|
|
||||||
4. Inactive types remain valid for historical rows but are excluded from default selectors.
|
|
||||||
|
|
||||||
### 4. Document Printing
|
|
||||||
|
|
||||||
1. User opens Print from persisted Document Detail.
|
|
||||||
2. Service builds a safe projection containing archival metadata, semantic Author links, ordered Sources, current text,
|
|
||||||
and oldest-to-newest Job metadata.
|
|
||||||
3. The preview renders Facsimile or Text-only HTML without exposing local file paths.
|
|
||||||
4. An explicit action opens the browser print dialog; browser Save as PDF remains available.
|
|
||||||
|
|
||||||
## V4 Domain Rules
|
|
||||||
|
|
||||||
- `JobSource.raw_transcription` preserves page output for its Job execution.
|
|
||||||
- `Source.raw_transcription` is the selected preferred-machine-output projection for a page.
|
|
||||||
- `Source.preferred_execution_attempt_id` identifies its exact immutable provenance; candidate promotion updates
|
|
||||||
both fields atomically.
|
|
||||||
- Human corrections occur only in `Source.revised_text`.
|
|
||||||
- Prompt and parameter provenance is frozen on `Job` at submission time.
|
|
||||||
- The SDK-serialized OpenRouter response snapshot is stored on `JobSource` for each successful page execution.
|
|
||||||
- Every V4.2 provider call appends a distinct `ExecutionAttempt`; retries never rewrite earlier attempts.
|
|
||||||
- Exact response bytes identify the OpenRouter HTTP boundary and are not labeled as native upstream-provider JSON.
|
|
||||||
- Generic `ProcessingArtifact` records use versioned schemas, digests, and one inline or external content location.
|
|
||||||
- Orientation-normalized model inputs and deterministic quality warnings are versioned `ProcessingArtifact` evidence
|
|
||||||
attached to the consuming `ExecutionAttempt`.
|
|
||||||
- A `retranscription` Job contains one locked existing Source and freezes one configured allowlisted model.
|
|
||||||
- `DocumentPerson` links are unique for `(document_id, person_id)` and require one `role_id`.
|
|
||||||
- Relationship mutations are deterministic, set-based, and atomic with Document writes.
|
|
||||||
- `DocumentType.id` and `PersonRole.id` are canonical relationship identities; unique labels may evolve.
|
|
||||||
- Nullable immutable `semantic_key` values identify protected application-defined built-ins and are never public selectors.
|
|
||||||
- Current printable text uses non-null `Source.revised_text`; otherwise it uses `Source.raw_transcription`.
|
|
||||||
|
|
||||||
## Data Model Summary
|
|
||||||
|
|
||||||
- `Document` has one `DocumentType`, many `Source` pages, many `Job` runs, and many `Person` records through `DocumentPerson`.
|
|
||||||
- `Source` belongs to one `Document` and may participate in many `JobSource` executions.
|
|
||||||
- `Job` has many `JobSource` rows.
|
|
||||||
- `PersonRole` defines available relationship roles; `DocumentType` and `PersonRole` may carry hidden semantic identity.
|
|
||||||
|
|
||||||
## Test Strategy
|
|
||||||
|
|
||||||
- Unit tests for models, validation, hashing, and registry resolution.
|
|
||||||
- Service tests for registry protection, atomic link synchronization, uniqueness conflicts, and print projections.
|
|
||||||
- Async workflow tests for page isolation, partial failure handling, and stored evidence.
|
|
||||||
- UI integration tests for Linked People staging, registry selection, and safe print rendering.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [System Overview](index_v4.md)
|
|
||||||
- [System Requirements](requirements_v4.md)
|
|
||||||
- [Data Model](schema_v4.md)
|
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
|
||||||
- [Error Handling Invariant](../invariant/error_handling.md)
|
|
||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# Error Handling Policy (Version 4)
|
|
||||||
|
|
||||||
This document defines the Version 4 taxonomy, contracts, and framework behavior used to satisfy the cross-version [Error Handling invariant](../invariant/error_handling.md).
|
|
||||||
|
|
||||||
## Invariant Alignment
|
|
||||||
|
|
||||||
Version 4 implements the invariant through:
|
|
||||||
|
|
||||||
- The shared error taxonomy below.
|
|
||||||
- Structured error envelopes with correlation IDs.
|
|
||||||
- Page-level failure isolation and explicit aggregate job status.
|
|
||||||
- Atomic relationship and classification writes.
|
|
||||||
- Consistent translation across API, UI, service, worker, persistence, and provider boundaries.
|
|
||||||
- Bounded retry guidance based on category and idempotency.
|
|
||||||
|
|
||||||
## Scope and Authority
|
|
||||||
|
|
||||||
This policy governs error behavior across:
|
|
||||||
|
|
||||||
- NiceGUI pages
|
|
||||||
- FastAPI routes
|
|
||||||
- Service-layer orchestration
|
|
||||||
- `asyncio` worker tasks
|
|
||||||
- Database interactions
|
|
||||||
- Provider adapters
|
|
||||||
|
|
||||||
## Error Taxonomy
|
|
||||||
|
|
||||||
| Category | Definition | Retriable |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `validation_error` | Payload, parameter, or schema validation failure | no |
|
|
||||||
| `user_input_error` | Unacceptable file, invalid selection, or malformed request from the operator | no |
|
|
||||||
| `not_found_error` | Requested `Document`, `Source`, `Person`, `Job`, role, or type does not exist | no |
|
|
||||||
| `conflict_error` | Operation violates uniqueness or relationship-write policy | no |
|
|
||||||
| `external_provider_error` | Provider API failure, rate limit, or execution problem | yes |
|
|
||||||
| `infrastructure_transient_error` | Temporary DB, file-system, or network instability | yes |
|
|
||||||
| `infrastructure_persistent_error` | Persistent configuration, credential, or database availability failure | no |
|
|
||||||
| `internal_unexpected_error` | Uncaught exception or logic defect | no |
|
|
||||||
|
|
||||||
## Async Batch and Page-Level Error Behavior
|
|
||||||
|
|
||||||
In multi-page `asyncio` processing:
|
|
||||||
|
|
||||||
1. Exceptions from individual page calls are trapped within the page task wrapper.
|
|
||||||
2. Failed page detail is written to `JobSource.error_detail` and the page state becomes `failed`.
|
|
||||||
3. Aggregate job status is derived from page outcomes:
|
|
||||||
- all pages succeed -> `completed`
|
|
||||||
- some succeed and some fail -> `partial_success`
|
|
||||||
- all fail -> `failed`
|
|
||||||
4. Successful pages remain valid even when sister pages fail.
|
|
||||||
|
|
||||||
## Relationship and Classification Conflict Behavior
|
|
||||||
|
|
||||||
When relationship or document-type writes fail policy checks:
|
|
||||||
|
|
||||||
1. Reject the full write operation.
|
|
||||||
2. Return structured conflict detail including target identifiers and the violated rule.
|
|
||||||
3. Preserve existing persisted relationships unchanged.
|
|
||||||
|
|
||||||
## API Error Response Contract
|
|
||||||
|
|
||||||
API error responses return a structured envelope:
|
|
||||||
|
|
||||||
^^^json
|
|
||||||
{
|
|
||||||
"error_id": "err_uuid_12345",
|
|
||||||
"category": "conflict_error",
|
|
||||||
"message": "Relationship write conflicts with existing links.",
|
|
||||||
"suggestion": "Adjust the requested relationship links and retry.",
|
|
||||||
"details": {
|
|
||||||
"document_id": "...",
|
|
||||||
"person_id": "...",
|
|
||||||
"attempted_role": "recipient",
|
|
||||||
"operation": "add_link",
|
|
||||||
"conflict_reason": "duplicate document-person-role link"
|
|
||||||
},
|
|
||||||
"timestamp": "2026-08-10T15:00:00Z"
|
|
||||||
}
|
|
||||||
^^^
|
|
||||||
|
|
||||||
HTTP status mappings:
|
|
||||||
|
|
||||||
- `validation_error`, `user_input_error` -> `400`
|
|
||||||
- `not_found_error` -> `404`
|
|
||||||
- `conflict_error` -> `409`
|
|
||||||
- `external_provider_error` -> `502` or `503`
|
|
||||||
- `infrastructure_transient_error` -> `503`
|
|
||||||
- `infrastructure_persistent_error`, `internal_unexpected_error` -> `500`
|
|
||||||
|
|
||||||
## UI Error Presentation Rules
|
|
||||||
|
|
||||||
- Display concise failure summaries with the next action the operator can take.
|
|
||||||
- Keep form state in context when feasible.
|
|
||||||
- Distinguish validation issues, conflict issues, provider failures, and infrastructure failures.
|
|
||||||
- For bulk relationship updates, identify the specific role or person that caused a conflict.
|
|
||||||
|
|
||||||
## Logging and Audit Expectations
|
|
||||||
|
|
||||||
- Log worker failures with correlation IDs and provider context.
|
|
||||||
- Log relationship and classification conflicts with machine-readable detail.
|
|
||||||
- Log persisted provider errors and page-level execution failures.
|
|
||||||
|
|
||||||
## Retry Guidance
|
|
||||||
|
|
||||||
- Do not auto-retry validation or conflict failures.
|
|
||||||
- Permit user-driven retry after the input or selection changes.
|
|
||||||
- Allow bounded retry for transient provider or infrastructure failures when the operation is idempotent.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [Error Handling Invariant](../invariant/error_handling.md)
|
|
||||||
- [System Overview](index_v4.md)
|
|
||||||
- [System Requirements](requirements_v4.md)
|
|
||||||
- [Data Model](schema_v4.md)
|
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# Implementation Plan (Version 4)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Implement the Version 4 project definition from the current repository state while preserving existing data by default.
|
|
||||||
|
|
||||||
## Migration Policy
|
|
||||||
|
|
||||||
- Database changes are non-destructive by default.
|
|
||||||
- Exception: the legacy `document_type` text field may be replaced by a `document_type_id` reference without migrating existing text values.
|
|
||||||
- Exception: `document_person` links may be recreated manually.
|
|
||||||
|
|
||||||
## Current Project Impact
|
|
||||||
|
|
||||||
- `src/transcription/db/models.py` requires full schema alignment with the V4 core documents.
|
|
||||||
- `src/transcription/services/documents.py` requires set-based document-person sync and document-type resolution.
|
|
||||||
- API modules require additive role-aware relationship behavior and document-type selection behavior.
|
|
||||||
- UI pages require grouped role displays, multi-role editing, and registry-backed document-type selection.
|
|
||||||
- Existing tests require updates for role enforcement, document-type selection, and regression safety.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### 1. Finalize the Transition Documents
|
|
||||||
|
|
||||||
- Confirm the reset scope.
|
|
||||||
- Confirm the database exception policy.
|
|
||||||
- Keep core V4 documents as the only authoritative product definition.
|
|
||||||
|
|
||||||
### 2. Align the Persistence Layer
|
|
||||||
|
|
||||||
- Update SQLModel definitions to match the final V4 schema.
|
|
||||||
- Add `person_role` and `document_type` support.
|
|
||||||
- Replace legacy document-type storage with `document_type_id`.
|
|
||||||
- Apply the accepted manual exception strategy for `document_type` and `document_person` data.
|
|
||||||
- Preserve all other data structures non-destructively.
|
|
||||||
|
|
||||||
### 3. Update Services and Write Semantics
|
|
||||||
|
|
||||||
- Organize service ownership around Documents, Sources, Jobs, and People.
|
|
||||||
- Centralize Source extension and MIME policy in the Sources service.
|
|
||||||
- Treat upload as an interface action and remove it from domain service naming where compatibility permits.
|
|
||||||
- Implement set-based synchronization for document-person updates.
|
|
||||||
- Implement deterministic uniqueness and relationship-write conflict checks.
|
|
||||||
- Remove suggestion-related service behavior.
|
|
||||||
- Add document-type resolution and validation by UUID.
|
|
||||||
|
|
||||||
### 4. Update API Contracts
|
|
||||||
|
|
||||||
- Keep API evolution additive.
|
|
||||||
- Add role-aware relationship retrieval and write behavior.
|
|
||||||
- Add document-type catalog retrieval and UUID-based selection for document writes.
|
|
||||||
- Remove suggestion-related API surfaces from the V4 target state.
|
|
||||||
|
|
||||||
### 5. Update UI Workflows
|
|
||||||
|
|
||||||
- Replace single-person link editing with grouped multi-role editing.
|
|
||||||
- Render grouped role links on document and person detail views.
|
|
||||||
- Replace free-text document type entry with registry-backed selection.
|
|
||||||
- Preserve clear validation and conflict messaging.
|
|
||||||
|
|
||||||
### 6. Verification and Hardening
|
|
||||||
|
|
||||||
- Add or update service tests for many-per-role behavior, uniqueness conflict handling, and set-based sync correctness.
|
|
||||||
- Add API tests for relationship behavior and document-type selection.
|
|
||||||
- Add UI tests or walkthrough coverage for grouped roles and type selection.
|
|
||||||
- Add regression coverage for delete and cleanup semantics.
|
|
||||||
- Enforce backup-first test execution for AI-run unit tests: backup `./data` before tests, then always prompt for restore after successful tests.
|
|
||||||
- Keep restore confirmation-gated by default so code and test outcomes can be reviewed before data is reverted.
|
|
||||||
|
|
||||||
## Done When
|
|
||||||
|
|
||||||
- Core V4 documents and code paths agree on the final project definition.
|
|
||||||
- Relationship-role writes are deterministic and non-destructive.
|
|
||||||
- Relationship-write conflict rules are enforced consistently.
|
|
||||||
- Document type selection is registry-backed.
|
|
||||||
- The accepted manual exceptions for `document_type` and `document_person` are completed.
|
|
||||||
- The focused test coverage passes.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Suggested/asserted relationship state.
|
|
||||||
- Suggestion review or extraction workflows.
|
|
||||||
- Global person entity-resolution engine.
|
|
||||||
- Automated semantic document-type classification.
|
|
||||||
|
|
||||||
## Delivery Order Recommendation
|
|
||||||
|
|
||||||
1. Freeze scope boundary and implementation plan.
|
|
||||||
2. Freeze core V4 documents.
|
|
||||||
3. Align persistence models.
|
|
||||||
4. Align services and API behavior.
|
|
||||||
5. Align UI behavior.
|
|
||||||
6. Run focused verification and regression checks.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [V4 Scope Boundary](scope_boundary_v4.md)
|
|
||||||
- [System Overview](index_v4.md)
|
|
||||||
- [System Requirements](requirements_v4.md)
|
|
||||||
- [Data Model](schema_v4.md)
|
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Document Transcription System Overview (Version 4)
|
|
||||||
|
|
||||||
Version 4 is the architecture baseline for the personal-scale application used to transcribe, organize, and preserve historical documents, source images, and related people records.
|
|
||||||
|
|
||||||
## Recommended Reading Order
|
|
||||||
|
|
||||||
1. [System Architecture](architecture_v4.md) for capabilities, technical stack, runtime structure, workflows, and component ownership.
|
|
||||||
2. [System Requirements](requirements_v4.md) for the verifiable V4 contract.
|
|
||||||
3. [Data Model](schema_v4.md) for entities, relationships, constraints, and persistence rules.
|
|
||||||
4. [Error Handling Policy](error_handling_v4.md) for the V4 taxonomy and boundary contracts.
|
|
||||||
|
|
||||||
## Cross-Version Invariants
|
|
||||||
|
|
||||||
- [Historical Document Transcription Design Intent](../invariant/intent.md)
|
|
||||||
- [Transcription Methodology](../invariant/transcription_methodology.md)
|
|
||||||
- [Error Handling](../invariant/error_handling.md)
|
|
||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
|
||||||
- [UI Style Guide](../invariant/ui_style_guide.md)
|
|
||||||
|
|
||||||
## V4 Transition Documents
|
|
||||||
|
|
||||||
- [Scope Boundary](scope_boundary_v4.md)
|
|
||||||
- [Implementation Plan](implementation_plan_v4.md)
|
|
||||||
|
|
||||||
## Incremental Revisions
|
|
||||||
|
|
||||||
- [V4.1 Scope](../ver4.1/scope_boundary_v4_1.md) and [Implementation Plan](../ver4.1/implementation_plan_v4_1.md)
|
|
||||||
- [V4.2 Evidence and Provenance Scope](../ver4.2/scope_boundary_v4_2.md) and [Implementation Plan](../ver4.2/implementation_plan_v4_2.md)
|
|
||||||
- [V4.3 Settings Scope](../ver4.3/scope_boundary_v4_3.md) and [Implementation Plan](../ver4.3/implementation_plan_v4_3.md)
|
|
||||||
- [V4.4 Semantic Registries, Linked People, and Printing Scope](../ver4.4/scope_boundary_v4_4.md) and [Implementation Plan](../ver4.4/implementation_plan_v4_4.md)
|
|
||||||
- [V4.5 Transcription Input Normalization and Quality Scope](../ver4.5/scope_boundary_v4_5.md) and [Implementation Plan](../ver4.5/implementation_plan_v4_5.md)
|
|
||||||
- [V4.6 Architecture Conformance and Reliability Scope](../ver4.6/scope_boundary_v4_6.md) and [Implementation Plan](../ver4.6/implementation_plan_v4_6.md)
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# Document Transcription System Requirements (Version 4)
|
|
||||||
|
|
||||||
This document defines the baseline requirements for the document transcription system.
|
|
||||||
|
|
||||||
## Requirements Model
|
|
||||||
|
|
||||||
| ID | Category | Requirement | Verify Method |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| REQ-0 | System | Provide end-to-end multi-page document transcription with persistent, inspectable async job states. | demonstration |
|
|
||||||
| REQ-1 | Functional | Allow users to upload one or more images as ordered `Source` pages under a `Document`. | test |
|
|
||||||
| REQ-2 | Functional | Process page transcription asynchronously using an `asyncio` worker pool bounded by rate limits. | test |
|
|
||||||
| REQ-3 | Functional | Persist submission-time request provenance and accurately labeled page-level SDK evidence; V4.2 adds exact OpenRouter-boundary transport evidence for new attempts. | test |
|
|
||||||
| REQ-4 | Functional | Support job states `queued`, `processing`, `completed`, `partial_success`, and `failed`, plus page states `pending`, `transcribed`, and `failed`. | inspection |
|
|
||||||
| REQ-5 | Functional | Allow users to manage historical `Person` records and link each Person to a Document once with exactly one role. | test |
|
|
||||||
| REQ-6 | Functional | Support an extensible role taxonomy for document-person relationships. | inspection |
|
|
||||||
| REQ-7 | Policy Constraint | Enforce deterministic relationship-role writes with uniqueness on `(document_id, person_id)` and explicit conflict responses for duplicate Person links. | test |
|
|
||||||
| REQ-8 | Functional | Use set-based synchronization for document-person mutations so updates add and remove only the intended links. | test |
|
|
||||||
| REQ-9 | Functional | Maintain selected machine output and exact attempt provenance on `Source` while permitting independent human edits on `Source.revised_text`. | test |
|
|
||||||
| REQ-10 | Functional | Support a UUID-identified `DocumentType` taxonomy with unique user-facing labels and active/inactive lifecycle control. | test |
|
|
||||||
| REQ-11 | Data Constraint | Store `Document` type as a controlled reference to `DocumentType`. | test |
|
|
||||||
| REQ-12 | Interface | Render multi-page transcriptions sequentially by `page_number` with document, people, and document-type metadata. | demonstration |
|
|
||||||
| REQ-13 | Interface | Document create/edit UI must provide one staged Linked People table and select active registry entries by UUID and label. | demonstration |
|
|
||||||
| REQ-14 | API Constraint | Expose additive, role-aware retrieval and write behavior for document-person links and UUID-based selection for document types. | test |
|
|
||||||
| REQ-15 | Data Constraint | Calculate and store cryptographic file hashes (SHA-256) and file sizes for uploaded source images. | test |
|
|
||||||
| REQ-16 | Data Constraint | Preserve a portable relational model across supported backends using SQLModel, SQLAlchemy, SQLite, and PostgreSQL. | inspection |
|
|
||||||
| REQ-17 | Reliability | Ensure delete and update flows for documents, people, and relationship links remain deterministic and safe. | test |
|
|
||||||
| REQ-18 | Operations Constraint | Keep canonical development, testing, restore, and recovery workflows OS-independent; for AI-run unit tests, require a pre-test backup of `./data` and an always-shown post-success confirmation prompt before any restore action. | inspection |
|
|
||||||
| REQ-19 | Quality | Provide automated coverage for async transcription workflows, relationship-role enforcement, document-type selection, and regression behavior. | test |
|
|
||||||
| REQ-20 | Data Constraint | Permit hidden immutable semantic keys only on protected built-in Document Types and Person Roles while retaining UUID as relationship identity. | test |
|
|
||||||
| REQ-21 | Reliability | Persist Document fields and their complete Linked People set atomically. | test |
|
|
||||||
| REQ-22 | Interface | Provide safe browser-native Facsimile and Text-only print views from persisted Document Detail. | demonstration |
|
|
||||||
| REQ-23 | Security | Escape stored print text and serve Source images through record-validated application routes without disclosing local paths. | test |
|
|
||||||
| REQ-24 | Functional | Print current human-preferred Source text, semantic Author metadata, deterministic Source order, and oldest-to-newest Job metadata. | test |
|
|
||||||
| REQ-25 | Quality | Physically apply recognized raster orientation metadata to provider-input derivatives without changing original Source bytes. | test |
|
|
||||||
| REQ-26 | Quality | Persist deterministic, non-mutating output warnings without automatic paid retries. | test |
|
|
||||||
| REQ-27 | Functional | Create one-Source retranscription Jobs from a configured model allowlist and preserve later successes as candidates until explicit promotion. | test |
|
|
||||||
|
|
||||||
## Clarifying Constraints
|
|
||||||
|
|
||||||
1. `DocumentType.id` and `PersonRole.id` are their public and relationship identities; labels are unique ignoring case and surrounding whitespace.
|
|
||||||
2. Nullable `semantic_key` values identify protected application built-ins, remain internal, and never change.
|
|
||||||
3. Relationship-write policy and conflict handling must be consistent across UI, API, services, and persistence.
|
|
||||||
4. One Person may appear only once per Document and every link has exactly one role.
|
|
||||||
5. Relationship conflicts must fail deterministically without partial Document or link mutation.
|
|
||||||
6. Source page reordering and server-generated PDF files remain outside this revision.
|
|
||||||
|
|
||||||
## Element Satisfaction Mapping
|
|
||||||
|
|
||||||
- UI (NiceGUI): Satisfies REQ-0, REQ-1, REQ-5, REQ-9, REQ-12, REQ-13, REQ-22, REQ-24.
|
|
||||||
- API (FastAPI): Satisfies REQ-1, REQ-4, REQ-5, REQ-7, REQ-8, REQ-14, REQ-23.
|
|
||||||
- Worker (`asyncio`): Satisfies REQ-2, REQ-3, REQ-4.
|
|
||||||
- Persistence (SQLModel / SQLAlchemy): Satisfies REQ-3, REQ-7, REQ-9, REQ-10, REQ-11, REQ-15, REQ-16, REQ-17, REQ-20, REQ-21.
|
|
||||||
- Test Suite: Verifies all test-marked requirements and satisfies REQ-19.
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [System Overview](index_v4.md)
|
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
- [Data Model](schema_v4.md)
|
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
# Database Schema (Version 4)
|
|
||||||
|
|
||||||
This document defines the relational schema for the document transcription system.
|
|
||||||
|
|
||||||
## Entity Relationship Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
DOCUMENT_TYPE {
|
|
||||||
UUID id PK
|
|
||||||
TEXT semantic_key UK
|
|
||||||
TEXT label
|
|
||||||
TEXT normalized_label
|
|
||||||
BOOLEAN is_active
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
PERSON_ROLE {
|
|
||||||
UUID id PK
|
|
||||||
TEXT semantic_key UK
|
|
||||||
TEXT label
|
|
||||||
TEXT normalized_label
|
|
||||||
BOOLEAN is_active
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
PERSON {
|
|
||||||
UUID id PK
|
|
||||||
TEXT full_name
|
|
||||||
TEXT display_name
|
|
||||||
TEXT maiden_name
|
|
||||||
DATE birth_date
|
|
||||||
TEXT birth_date_raw
|
|
||||||
TEXT birth_place
|
|
||||||
DATE death_date
|
|
||||||
TEXT death_date_raw
|
|
||||||
TEXT death_place
|
|
||||||
TEXT biography
|
|
||||||
TEXT portrait_path
|
|
||||||
JSONB metadata
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
DOCUMENT {
|
|
||||||
UUID id PK
|
|
||||||
UUID document_type_id FK
|
|
||||||
TEXT name
|
|
||||||
DATE document_date
|
|
||||||
TEXT document_date_raw
|
|
||||||
TEXT location_created
|
|
||||||
TEXT notes
|
|
||||||
TEXT archive_identifier
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
DOCUMENT_PERSON {
|
|
||||||
UUID id PK
|
|
||||||
UUID document_id FK
|
|
||||||
UUID person_id FK
|
|
||||||
UUID role_id FK
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
JOB {
|
|
||||||
UUID id PK
|
|
||||||
UUID document_id FK
|
|
||||||
VARCHAR status
|
|
||||||
INTEGER retry_count
|
|
||||||
VARCHAR purpose
|
|
||||||
TEXT provider
|
|
||||||
TEXT model
|
|
||||||
TEXT prompt_name
|
|
||||||
TEXT prompt_hash
|
|
||||||
TEXT system_prompt
|
|
||||||
TEXT user_prompt
|
|
||||||
FLOAT temperature
|
|
||||||
FLOAT top_p
|
|
||||||
TIMESTAMPTZ date_created
|
|
||||||
TIMESTAMPTZ date_updated
|
|
||||||
}
|
|
||||||
|
|
||||||
SOURCE {
|
|
||||||
UUID id PK
|
|
||||||
UUID document_id FK
|
|
||||||
INTEGER page_number
|
|
||||||
TEXT upload_name
|
|
||||||
TEXT filename
|
|
||||||
TEXT file_path
|
|
||||||
TEXT file_hash
|
|
||||||
BIGINT file_size_bytes
|
|
||||||
TEXT raw_transcription
|
|
||||||
UUID preferred_execution_attempt_id FK
|
|
||||||
TEXT revised_text
|
|
||||||
TIMESTAMPTZ date_uploaded
|
|
||||||
TIMESTAMPTZ date_revised
|
|
||||||
}
|
|
||||||
|
|
||||||
JOB_SOURCE {
|
|
||||||
UUID id PK
|
|
||||||
UUID job_id FK
|
|
||||||
UUID source_id FK
|
|
||||||
VARCHAR status
|
|
||||||
TEXT raw_transcription
|
|
||||||
JSONB ai_metadata
|
|
||||||
JSONB raw_api_response
|
|
||||||
TEXT error_detail
|
|
||||||
TIMESTAMPTZ executed_at
|
|
||||||
}
|
|
||||||
|
|
||||||
EXECUTION_ATTEMPT {
|
|
||||||
UUID id PK
|
|
||||||
UUID job_source_id FK
|
|
||||||
UUID job_id FK
|
|
||||||
UUID source_id FK
|
|
||||||
INTEGER attempt_number
|
|
||||||
VARCHAR status
|
|
||||||
JSONB request_manifest
|
|
||||||
TEXT request_manifest_sha256
|
|
||||||
INTEGER transport_status_code
|
|
||||||
BINARY transport_body
|
|
||||||
JSONB transport_safe_headers
|
|
||||||
JSONB sdk_response_snapshot
|
|
||||||
JSONB normalized_metadata
|
|
||||||
JSONB software_context
|
|
||||||
TEXT raw_transcription
|
|
||||||
TEXT failure_phase
|
|
||||||
TIMESTAMPTZ started_at
|
|
||||||
TIMESTAMPTZ finished_at
|
|
||||||
INTEGER duration_ms
|
|
||||||
}
|
|
||||||
|
|
||||||
PROCESSING_ARTIFACT {
|
|
||||||
UUID id PK
|
|
||||||
UUID source_id FK
|
|
||||||
UUID execution_attempt_id FK
|
|
||||||
TEXT artifact_type
|
|
||||||
TEXT media_type
|
|
||||||
TEXT schema_name
|
|
||||||
TEXT schema_version
|
|
||||||
TEXT producer
|
|
||||||
TEXT producer_version
|
|
||||||
JSONB inline_payload
|
|
||||||
TEXT external_reference
|
|
||||||
TEXT payload_sha256
|
|
||||||
BIGINT byte_size
|
|
||||||
JSONB coordinate_metadata
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
DOCUMENT_TYPE ||--o{ DOCUMENT : classifies
|
|
||||||
DOCUMENT ||--o{ DOCUMENT_PERSON : has_people
|
|
||||||
PERSON ||--o{ DOCUMENT_PERSON : appears_in
|
|
||||||
PERSON_ROLE ||--o{ DOCUMENT_PERSON : labels
|
|
||||||
DOCUMENT ||--o{ JOB : has_jobs
|
|
||||||
DOCUMENT ||--o{ SOURCE : contains_pages
|
|
||||||
JOB ||--o{ JOB_SOURCE : executes
|
|
||||||
SOURCE ||--o{ JOB_SOURCE : processed_in
|
|
||||||
JOB_SOURCE ||--o{ EXECUTION_ATTEMPT : projects
|
|
||||||
SOURCE ||--o{ PROCESSING_ARTIFACT : derives
|
|
||||||
EXECUTION_ATTEMPT ||--o{ PROCESSING_ARTIFACT : produces
|
|
||||||
```
|
|
||||||
|
|
||||||
## Domain Invariants and Provenance Rules
|
|
||||||
|
|
||||||
### Page-Level Execution and AI Outputs
|
|
||||||
|
|
||||||
- Every single page execution by an AI model produces a dedicated `JOB_SOURCE` record.
|
|
||||||
- Every `JOB` stores the frozen prompt identifier, prompt text, and hyperparameters used at submission time.
|
|
||||||
- `JOB_SOURCE.raw_api_response` is a compatibility projection containing an SDK-serialized OpenRouter response
|
|
||||||
snapshot. It is neither the exact HTTP body nor the native upstream-provider response.
|
|
||||||
- Every new provider call creates an immutable `EXECUTION_ATTEMPT` containing the frozen request manifest,
|
|
||||||
exact OpenRouter-boundary response bytes when received, safe transport metadata, SDK snapshot, normalized
|
|
||||||
metadata, timing, and outcome.
|
|
||||||
- `EXECUTION_ATTEMPT(job_id, source_id, attempt_number)` is unique; retries increment the persisted attempt number.
|
|
||||||
- Historical `JOB_SOURCE` rows without an `EXECUTION_ATTEMPT` remain SDK snapshots and are explicitly labeled as
|
|
||||||
lacking transport evidence.
|
|
||||||
- `SOURCE.raw_transcription` caches the explicitly selected preferred machine output for that page.
|
|
||||||
- `SOURCE.preferred_execution_attempt_id` records exact successful-attempt provenance. Legacy projections may remain
|
|
||||||
null until a new successful result is selected.
|
|
||||||
|
|
||||||
### Generic Processing Artifacts
|
|
||||||
|
|
||||||
- `PROCESSING_ARTIFACT` stores provider-neutral versioned derived outputs.
|
|
||||||
- Exactly one of `inline_payload` and `external_reference` is populated.
|
|
||||||
- Externally stored artifacts use application-managed relative references and are verified by SHA-256 and byte size.
|
|
||||||
- Coordinate metadata declares units, origin, dimensions, and transformations when geometry is present.
|
|
||||||
- Orientation-normalized binary model inputs and JSON quality-warning results use distinct versioned artifact types
|
|
||||||
and are attached to the exact consuming `EXECUTION_ATTEMPT`.
|
|
||||||
|
|
||||||
### Image Storage and Integrity
|
|
||||||
|
|
||||||
- Binary images are stored on disk; `SOURCE.file_path` stores the persisted path.
|
|
||||||
- `SOURCE.file_hash` stores a SHA-256 digest.
|
|
||||||
- `SOURCE.file_size_bytes` stores the original file size.
|
|
||||||
|
|
||||||
### Page Ordering and Revisions
|
|
||||||
|
|
||||||
- `SOURCE.page_number` dictates page ordering within a document.
|
|
||||||
- `SOURCE.raw_transcription` changes only through first-success selection or explicit candidate promotion.
|
|
||||||
- `SOURCE.revised_text` stores human edits and is the preferred display value when present.
|
|
||||||
|
|
||||||
### Semantic Registry Governance
|
|
||||||
|
|
||||||
- `DOCUMENT_TYPE.id` and `PERSON_ROLE.id` are the only relationship and public API identities.
|
|
||||||
- Nullable unique `semantic_key` values identify application-defined built-ins and are immutable after creation.
|
|
||||||
- Semantic keys are internal and are never accepted from Settings or public relationship APIs.
|
|
||||||
- A non-null semantic key marks a protected built-in; built-ins may be relabeled or disabled but not deleted.
|
|
||||||
- Custom entries have null semantic keys and may be deleted only when unreferenced.
|
|
||||||
- Labels are mutable display text and are unique after trimming and case normalization.
|
|
||||||
- Inactive entries remain valid for historical rows but are excluded from new-assignment selectors.
|
|
||||||
|
|
||||||
### Document-Person Role Governance
|
|
||||||
|
|
||||||
- Documents support zero or one relationship for each Person.
|
|
||||||
- Relationship roles are defined by `PERSON_ROLE` rather than hardcoded columns.
|
|
||||||
- `DOCUMENT_PERSON.role_id` is required.
|
|
||||||
- `DOCUMENT_PERSON` must be unique for `(document_id, person_id)`.
|
|
||||||
- Complete link sets and Document fields are validated and persisted in one atomic transaction.
|
|
||||||
- Existing inactive roles may remain unchanged; new or changed assignments require active roles.
|
|
||||||
|
|
||||||
### Document Type Governance
|
|
||||||
|
|
||||||
- Every document type is defined by `DOCUMENT_TYPE`.
|
|
||||||
- `DOCUMENT_TYPE.id` is the relationship identity; hidden semantic keys identify protected built-in meaning.
|
|
||||||
- `DOCUMENT_TYPE.label` is mutable display text and is unique after trimming and case normalization.
|
|
||||||
- `DOCUMENT_TYPE.normalized_label` stores the normalized uniqueness key.
|
|
||||||
- Inactive types remain valid for historical rows but should be excluded from default selection UIs.
|
|
||||||
|
|
||||||
## Constraint Summary
|
|
||||||
|
|
||||||
- `DOCUMENT_TYPE.normalized_label` is unique.
|
|
||||||
- `DOCUMENT_TYPE.semantic_key` is nullable and unique.
|
|
||||||
- `PERSON_ROLE.normalized_label` is unique.
|
|
||||||
- `PERSON_ROLE.semantic_key` is nullable and unique.
|
|
||||||
- `DOCUMENT_PERSON(document_id, person_id)` is unique.
|
|
||||||
|
|
||||||
## Indexing Guidance
|
|
||||||
|
|
||||||
- `document(document_type_id)`
|
|
||||||
- `document_person(document_id)`
|
|
||||||
- `document_person(person_id)`
|
|
||||||
- `document_person(role_id)`
|
|
||||||
- `source(document_id, page_number)`
|
|
||||||
- `job(document_id, status)`
|
|
||||||
- `job_source(job_id)`
|
|
||||||
- `job_source(source_id)`
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [System Overview](index_v4.md)
|
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
- [System Requirements](requirements_v4.md)
|
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# V4 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the scope for the transition from the current repository state to the Version 4 project definition.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Define what this revision includes, what it intentionally excludes, and what migration rules govern the transition work.
|
|
||||||
|
|
||||||
## In Scope
|
|
||||||
|
|
||||||
### 1. Relationship Model
|
|
||||||
|
|
||||||
- Extensible role taxonomy for document-person relationships.
|
|
||||||
- Many-to-many document-person links with many people per role.
|
|
||||||
- Set-based add/remove synchronization for document-person updates.
|
|
||||||
|
|
||||||
### 2. Document Type Governance
|
|
||||||
|
|
||||||
- Registry-driven `DocumentType` model with UUID identity, unique labels, and controlled selection.
|
|
||||||
- Minimal rollout for the current corpus with no alias helper table.
|
|
||||||
|
|
||||||
### 3. UI and API Behavior
|
|
||||||
|
|
||||||
- Grouped role links on document and person views.
|
|
||||||
- Multi-role relationship editing on document create/edit flows.
|
|
||||||
- Role-aware API retrieval and write behavior.
|
|
||||||
- Additive API evolution with explicit deprecations.
|
|
||||||
|
|
||||||
### 4. Verification
|
|
||||||
|
|
||||||
- Tests for many-per-role behavior.
|
|
||||||
- Tests for set-based relationship mutation behavior.
|
|
||||||
- Tests for document and person delete/link cleanup regressions.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Suggested versus asserted relationship states.
|
|
||||||
- Suggestion storage, review, acceptance, or rejection workflows.
|
|
||||||
- Automatic relationship extraction or recommendation features.
|
|
||||||
- Full entity resolution or identity merge across all people.
|
|
||||||
- Automated semantic document type classification.
|
|
||||||
- Redesign of the core transcription execution model.
|
|
||||||
|
|
||||||
## Locked Design Decisions
|
|
||||||
|
|
||||||
### A. Role Extensibility Mechanism
|
|
||||||
|
|
||||||
- Use registry tables for relationship roles.
|
|
||||||
|
|
||||||
### B. API Compatibility Strategy
|
|
||||||
|
|
||||||
- Use additive API evolution.
|
|
||||||
- In development mode, the current revision is authoritative.
|
|
||||||
- Deprecations should be explicit and short-lived.
|
|
||||||
|
|
||||||
### C. Document Type Rollout Strategy
|
|
||||||
|
|
||||||
- Use a minimal registry rollout for the current corpus.
|
|
||||||
- Do not introduce a `document_type_alias` helper table.
|
|
||||||
|
|
||||||
### D. Database Change Policy
|
|
||||||
|
|
||||||
- Future schema changes are non-destructive by default.
|
|
||||||
- Exception: `document_type` text may be replaced by `document_type_id` without migrating the legacy text values.
|
|
||||||
- Exception: `document_person` links may be recreated manually.
|
|
||||||
|
|
||||||
## Compatibility and Rollout
|
|
||||||
|
|
||||||
- Preserve existing repository behavior where unaffected by the V4 scope.
|
|
||||||
- Treat scope boundary and implementation plan as the only transition documents.
|
|
||||||
- Treat core V4 documents as the authoritative project definition once rewritten.
|
|
||||||
|
|
||||||
## Exit Criteria for Scope Freeze
|
|
||||||
|
|
||||||
V4 scope is considered frozen when:
|
|
||||||
|
|
||||||
- Relationship model and document-type governance are approved.
|
|
||||||
- Relationship model and document-type governance are approved.
|
|
||||||
- Additive API change list and deprecation schedule are approved.
|
|
||||||
- Migration exceptions are explicitly acknowledged.
|
|
||||||
|
|
||||||
## Core V4 Documents
|
|
||||||
|
|
||||||
1. `docs/ver4/index_v4.md`
|
|
||||||
2. `docs/ver4/requirements_v4.md`
|
|
||||||
3. `docs/ver4/schema_v4.md`
|
|
||||||
4. `docs/ver4/architecture_v4.md`
|
|
||||||
5. `docs/ver4/error_handling_v4.md`
|
|
||||||
6. `docs/ver4/implementation_plan_v4.md`
|
|
||||||
@@ -5,11 +5,9 @@ This directory stores transcription prompts as individual Markdown artifacts.
|
|||||||
## Conventions
|
## Conventions
|
||||||
- Keep one prompt per file.
|
- Keep one prompt per file.
|
||||||
- Use stable, descriptive snake_case file names.
|
- Use stable, descriptive snake_case file names.
|
||||||
- Store prompt files directly in this directory; nested paths are rejected.
|
|
||||||
- Prefer incremental edits to a single prompt per change for clean history.
|
- Prefer incremental edits to a single prompt per change for clean history.
|
||||||
- Keep prompts human-readable and policy-focused.
|
- Keep prompts human-readable and policy-focused.
|
||||||
- Do not store secrets in prompt files.
|
- Do not store secrets in prompt files.
|
||||||
- Runtime jobs snapshot prompt text, SHA-256 provenance, and sampling configuration.
|
|
||||||
|
|
||||||
## Current Prompt
|
## Current Prompt
|
||||||
- `transcribe_document.md`: baseline verbatim transcription policy for historical documents.
|
- `transcribe_document.md`: baseline verbatim transcription policy for historical documents.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
You are an assistant that may call tools.
|
||||||
|
|
||||||
|
Tool safety rules:
|
||||||
|
1) Tool arguments MUST be strict JSON matching the schema exactly.
|
||||||
|
2) Never place disallowed, sensitive, explicit, or policy-violating text directly into tool arguments.
|
||||||
|
3) If user content may be unsafe, first produce a brief neutral summary and pass only that summary.
|
||||||
|
4) Prefer IDs, enums, booleans, and short fields over raw free-form text.
|
||||||
|
5) Keep all string arguments <= 300 chars unless schema says otherwise.
|
||||||
|
6) If you cannot safely provide valid tool args, do not call the tool; respond with "NO_TOOL_CALL" and explain briefly.
|
||||||
|
7) Never include markdown/code fences in tool arguments.
|
||||||
@@ -6,15 +6,9 @@ Do not summarize. Do not paraphrase. Do not modernize style.
|
|||||||
|
|
||||||
## Output Contract
|
## Output Contract
|
||||||
- Return only the transcription text.
|
- Return only the transcription text.
|
||||||
- Begin with exactly one applicable body marker:
|
|
||||||
- `[document body handwritten]`
|
|
||||||
- `[document body typewritten]`
|
|
||||||
- `[document body typeset]`
|
|
||||||
- `[document body mixed]`
|
|
||||||
- Preserve original wording, punctuation, and meaningful structure.
|
- Preserve original wording, punctuation, and meaningful structure.
|
||||||
- Keep line/section flow readable while preserving intent and document organization.
|
- Keep line/section flow readable while preserving intent and document organization.
|
||||||
- Never invent missing content.
|
- Never invent missing content.
|
||||||
- Use ordinary plain-text characters rather than HTML entities.
|
|
||||||
|
|
||||||
## Rules for Ambiguous or Damaged Text
|
## Rules for Ambiguous or Damaged Text
|
||||||
|
|
||||||
@@ -52,31 +46,6 @@ Do not summarize. Do not paraphrase. Do not modernize style.
|
|||||||
- Signal location before the note text.
|
- Signal location before the note text.
|
||||||
- Example form: `[written in left margin: ...]`
|
- Example form: `[written in left margin: ...]`
|
||||||
|
|
||||||
### Document body medium
|
|
||||||
- Use `[document body handwritten]` when the main body is written by hand.
|
|
||||||
- Use `[document body typewritten]` for mechanically typewritten pages. Uneven impressions,
|
|
||||||
monospaced characters, worn type, and other typewriter defects are not handwriting.
|
|
||||||
- Use `[document body typeset]` for printed pages composed with movable type or comparable
|
|
||||||
typesetting.
|
|
||||||
- Use `[document body mixed]` when substantial body content uses more than one medium, such
|
|
||||||
as a completed printed form.
|
|
||||||
- Preserve printed and handwritten text together in their original reading context.
|
|
||||||
- On mixed documents, leave printed labels and instructions unmarked and wrap only actual
|
|
||||||
handwritten entries in `[handwritten: ...]`.
|
|
||||||
- Mark handwritten signatures as `[handwritten signature: ...]`.
|
|
||||||
- If the main body is entirely handwritten, use its one body marker rather than wrapping
|
|
||||||
each line in `[handwritten: ...]`.
|
|
||||||
- Mark later notes or uncertain additions as `[handwritten annotation: ...]`.
|
|
||||||
- When authorship is unclear, use `[handwritten annotation, author uncertain: ...]`.
|
|
||||||
- Do not infer authorship, writing date, or whether different handwriting belongs to different people unless explicitly evident.
|
|
||||||
|
|
||||||
### Structured layouts
|
|
||||||
- Preserve tables of contents as associated title, dotted-leader, and page-reference rows.
|
|
||||||
- Preserve tables and forms in reading order while keeping labels associated with their values.
|
|
||||||
- Preserve columns in their evident reading order; do not interleave unrelated rows.
|
|
||||||
- Preserve captions with the visual element they describe.
|
|
||||||
- Preserve marginalia with its location marker and page numbers in their evident position.
|
|
||||||
|
|
||||||
### Line-break hyphenation
|
### Line-break hyphenation
|
||||||
- Rejoin words split across line breaks when they are clearly one word.
|
- Rejoin words split across line breaks when they are clearly one word.
|
||||||
- Remove only line-break hyphens used for wrapping.
|
- Remove only line-break hyphens used for wrapping.
|
||||||
@@ -101,5 +70,3 @@ Before finalizing, ensure:
|
|||||||
2. Uncertain/illegible areas are explicitly marked.
|
2. Uncertain/illegible areas are explicitly marked.
|
||||||
3. Crossed-out and inserted text are preserved with required tags.
|
3. Crossed-out and inserted text are preserved with required tags.
|
||||||
4. Structure/ordering is preserved as faithfully as possible.
|
4. Structure/ordering is preserved as faithfully as possible.
|
||||||
5. Exactly one document-body marker appears, and localized handwriting markers are used only where applicable.
|
|
||||||
6. Tables, forms, columns, captions, marginalia, dotted leaders, and page references retain their associations.
|
|
||||||
|
|||||||
+1
-18
@@ -12,39 +12,22 @@ description = "Historical document transcription system"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiosqlite>=0.21.0",
|
|
||||||
"asyncpg>=0.31.0",
|
|
||||||
"fastapi>=0.138.0",
|
"fastapi>=0.138.0",
|
||||||
"nicegui==3.13.0",
|
"nicegui==3.13.0",
|
||||||
"openrouter>=0.7.0",
|
"openrouter>=0.7.0",
|
||||||
"pillow>=10.0.0",
|
|
||||||
"psycopg2-binary>=2.9.12",
|
|
||||||
"pydantic>=2.13.4",
|
"pydantic>=2.13.4",
|
||||||
"pydantic-settings>=2.9.1",
|
"pydantic-settings>=2.9.1",
|
||||||
"sqlmodel>=0.0.25",
|
"sqlmodel>=0.0.25",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"pytest>=8.0",
|
||||||
"pytest-asyncio>=0.25",
|
"pytest-asyncio>=0.25",
|
||||||
"httpx2>=2.5.0",
|
|
||||||
"ipykernel>=7.3.0",
|
|
||||||
"ipywidgets>=8.1.8",
|
|
||||||
"pre-commit>=4.6.0",
|
|
||||||
"rich>=15.0.0",
|
|
||||||
"ruff>=0.15.20",
|
|
||||||
"ty>=0.0.54",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--strict-markers -q"
|
addopts = "--strict-markers -q"
|
||||||
asyncio_mode = "strict"
|
|
||||||
asyncio_default_fixture_loop_scope = "function"
|
|
||||||
filterwarnings = [
|
|
||||||
"error:coroutine .* was never awaited:RuntimeWarning",
|
|
||||||
]
|
|
||||||
markers = [
|
markers = [
|
||||||
"unit: pure logic tests with no external dependencies",
|
"unit: pure logic tests with no external dependencies",
|
||||||
"integration: tests that touch framework or database contracts",
|
"integration: tests that touch framework or database contracts",
|
||||||
|
|||||||
Binary file not shown.
@@ -1,62 +0,0 @@
|
|||||||
line-length = 120
|
|
||||||
indent-width = 4
|
|
||||||
target-version = "py313"
|
|
||||||
|
|
||||||
exclude = [
|
|
||||||
".venv",
|
|
||||||
".devenv",
|
|
||||||
".git",
|
|
||||||
".vscode",
|
|
||||||
"build",
|
|
||||||
"site",
|
|
||||||
"__pycache__",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
preview = true
|
|
||||||
|
|
||||||
extend-select = [
|
|
||||||
"ARG", # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg
|
|
||||||
"B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
|
|
||||||
"C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
|
|
||||||
"DOC102", # https://docs.astral.sh/ruff/rules/docstring-extraneous-parameter/
|
|
||||||
"DOC202", # https://docs.astral.sh/ruff/rules/docstring-extraneous-returns/
|
|
||||||
"DOC403", # https://docs.astral.sh/ruff/rules/docstring-extraneous-yields/
|
|
||||||
"DOC502", # https://docs.astral.sh/ruff/rules/docstring-extraneous-exception/
|
|
||||||
"E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
|
||||||
"F", # https://docs.astral.sh/ruff/rules/#pyflakes-f
|
|
||||||
"FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb
|
|
||||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
|
||||||
"N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n
|
|
||||||
"PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd
|
|
||||||
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
|
||||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
|
||||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
|
||||||
"PLR0202", # https://docs.astral.sh/ruff/rules/no-classmethod-decorator/
|
|
||||||
"PLR0203", # https://docs.astral.sh/ruff/rules/no-staticmethod-decorator/
|
|
||||||
"PLR0206", # https://docs.astral.sh/ruff/rules/property-with-parameters/
|
|
||||||
"PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/
|
|
||||||
"PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/
|
|
||||||
"TRY002",
|
|
||||||
]
|
|
||||||
extend-fixable = ["ALL"]
|
|
||||||
ignore = [
|
|
||||||
"UP046",
|
|
||||||
"UP047",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.extend-per-file-ignores]
|
|
||||||
"*.ipynb" = [
|
|
||||||
"F401", # unused imports
|
|
||||||
"F841", # unused local variable
|
|
||||||
"F821", # undefined name in exploratory notebook cells
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.isort]
|
|
||||||
force-single-line = true
|
|
||||||
|
|
||||||
[format]
|
|
||||||
quote-style = "double"
|
|
||||||
indent-style = "space"
|
|
||||||
skip-magic-trailing-comma = false
|
|
||||||
line-ending = "auto"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
from .app import create_app
|
|
||||||
from .config import parse_cli_settings
|
|
||||||
|
|
||||||
|
|
||||||
def create_cli_app() -> FastAPI:
|
|
||||||
"""Create an app from CLI settings for Uvicorn's reload process."""
|
|
||||||
return create_app(settings=parse_cli_settings())
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
settings = parse_cli_settings()
|
|
||||||
application = "transcription.__main__:create_cli_app" if settings.reload else create_app(settings=settings)
|
|
||||||
uvicorn.run(
|
|
||||||
application,
|
|
||||||
factory=settings.reload,
|
|
||||||
host=settings.host,
|
|
||||||
port=settings.port,
|
|
||||||
log_level=settings.log_level,
|
|
||||||
reload=settings.reload,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -34,6 +34,12 @@ def _status_for(error: AppError) -> int:
|
|||||||
def register_error_handlers(app: FastAPI) -> None:
|
def register_error_handlers(app: FastAPI) -> None:
|
||||||
"""Register API exception handlers on the app."""
|
"""Register API exception handlers on the app."""
|
||||||
|
|
||||||
|
@app.exception_handler(AccessDeniedError)
|
||||||
|
async def access_denied_handler(_request: Request, exc: AccessDeniedError) -> JSONResponse:
|
||||||
|
envelope = build_error_envelope(exc)
|
||||||
|
headers = {"WWW-Authenticate": "Basic"} if exc.should_challenge else None
|
||||||
|
return JSONResponse(status_code=401, content=envelope.__dict__, headers=headers)
|
||||||
|
|
||||||
@app.exception_handler(AppError)
|
@app.exception_handler(AppError)
|
||||||
async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse:
|
async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse:
|
||||||
envelope = build_error_envelope(exc)
|
envelope = build_error_envelope(exc)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"""Functional API routes for jobs, revisions, search, and export."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from transcription.services.library import accept_revision
|
||||||
|
from transcription.services.library import add_revision
|
||||||
|
from transcription.services.library import export_transcripts
|
||||||
|
from transcription.services.library import get_job_detail
|
||||||
|
from transcription.services.library import list_jobs
|
||||||
|
from transcription.services.library import list_revisions
|
||||||
|
from transcription.services.library import search_accepted_transcripts
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api", tags=["transcription"])
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRevisionRequest(BaseModel):
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
source: str = "user"
|
||||||
|
accepted: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/jobs")
|
||||||
|
def get_jobs() -> list[dict[str, str]]:
|
||||||
|
jobs = list_jobs()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(job.id),
|
||||||
|
"document_id": str(job.document_id),
|
||||||
|
"status": job.status.value,
|
||||||
|
"created_at": job.created_at.isoformat(),
|
||||||
|
"updated_at": job.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
for job in jobs
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/jobs/{job_id}")
|
||||||
|
def get_job(job_id: UUID) -> dict[str, object | None]:
|
||||||
|
detail = get_job_detail(job_id=job_id)
|
||||||
|
return {
|
||||||
|
"job": {
|
||||||
|
"id": str(detail.job.id),
|
||||||
|
"document_id": str(detail.job.document_id),
|
||||||
|
"status": detail.job.status.value,
|
||||||
|
"created_at": detail.job.created_at.isoformat(),
|
||||||
|
"updated_at": detail.job.updated_at.isoformat(),
|
||||||
|
},
|
||||||
|
"document": (
|
||||||
|
{
|
||||||
|
"id": str(detail.document.id),
|
||||||
|
"filename": detail.document.filename,
|
||||||
|
"file_path": detail.document.file_path,
|
||||||
|
}
|
||||||
|
if detail.document is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"transcript": (
|
||||||
|
{
|
||||||
|
"id": str(detail.transcript.id),
|
||||||
|
"text": detail.transcript.text,
|
||||||
|
"error_detail": detail.transcript.error_detail,
|
||||||
|
"created_at": detail.transcript.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
if detail.transcript is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"accepted_revision": (
|
||||||
|
{
|
||||||
|
"id": str(detail.accepted_revision.id),
|
||||||
|
"revision_number": detail.accepted_revision.revision_number,
|
||||||
|
"text": detail.accepted_revision.text,
|
||||||
|
"source": detail.accepted_revision.source,
|
||||||
|
"created_at": detail.accepted_revision.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
if detail.accepted_revision is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/jobs/{job_id}/revisions")
|
||||||
|
def get_job_revisions(job_id: UUID) -> list[dict[str, object]]:
|
||||||
|
revisions = list_revisions(job_id=job_id)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(revision.id),
|
||||||
|
"job_id": str(revision.job_id),
|
||||||
|
"revision_number": revision.revision_number,
|
||||||
|
"text": revision.text,
|
||||||
|
"source": revision.source,
|
||||||
|
"accepted": revision.accepted,
|
||||||
|
"created_at": revision.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for revision in revisions
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jobs/{job_id}/revisions")
|
||||||
|
def create_job_revision(job_id: UUID, payload: CreateRevisionRequest) -> dict[str, object]:
|
||||||
|
revision = add_revision(
|
||||||
|
job_id=job_id,
|
||||||
|
text=payload.text,
|
||||||
|
source=payload.source,
|
||||||
|
accepted=payload.accepted,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": str(revision.id),
|
||||||
|
"job_id": str(revision.job_id),
|
||||||
|
"revision_number": revision.revision_number,
|
||||||
|
"text": revision.text,
|
||||||
|
"source": revision.source,
|
||||||
|
"accepted": revision.accepted,
|
||||||
|
"created_at": revision.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/revisions/{revision_id}/accept")
|
||||||
|
def accept_job_revision(revision_id: UUID) -> dict[str, object]:
|
||||||
|
revision = accept_revision(revision_id=revision_id)
|
||||||
|
return {
|
||||||
|
"id": str(revision.id),
|
||||||
|
"job_id": str(revision.job_id),
|
||||||
|
"revision_number": revision.revision_number,
|
||||||
|
"text": revision.text,
|
||||||
|
"source": revision.source,
|
||||||
|
"accepted": revision.accepted,
|
||||||
|
"created_at": revision.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/search")
|
||||||
|
def search(query: str) -> list[dict[str, object]]:
|
||||||
|
results = search_accepted_transcripts(query=query)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"revision_id": str(revision.id),
|
||||||
|
"job_id": str(revision.job_id),
|
||||||
|
"revision_number": revision.revision_number,
|
||||||
|
"text": revision.text,
|
||||||
|
"source": revision.source,
|
||||||
|
"accepted": revision.accepted,
|
||||||
|
"created_at": revision.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for revision in results
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export")
|
||||||
|
def export(accepted_only: bool = True) -> dict[str, object]:
|
||||||
|
records = export_transcripts(accepted_only=accepted_only)
|
||||||
|
return {
|
||||||
|
"count": len(records),
|
||||||
|
"accepted_only": accepted_only,
|
||||||
|
"records": records,
|
||||||
|
}
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
"""Additive V4 API routes for relationship and classification registries."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Annotated
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi import Request
|
|
||||||
from fastapi import Response
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from pydantic import ConfigDict
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
from transcription.db.models import Document
|
|
||||||
from transcription.db.models import DocumentPerson
|
|
||||||
from transcription.db.models import DocumentType
|
|
||||||
from transcription.db.models import PersonRole
|
|
||||||
from transcription.services import DocumentService
|
|
||||||
from transcription.services import PeopleService
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v4", tags=["v4-documents"])
|
|
||||||
|
|
||||||
|
|
||||||
class ApiModel(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid", frozen=True, str_strip_whitespace=True)
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeRead(ApiModel):
|
|
||||||
id: UUID
|
|
||||||
label: str
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
|
|
||||||
class PersonRoleRead(ApiModel):
|
|
||||||
id: UUID
|
|
||||||
label: str
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeWriteRequest(ApiModel):
|
|
||||||
document_type_id: UUID
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeWriteResponse(ApiModel):
|
|
||||||
document_id: UUID
|
|
||||||
document_type_id: UUID
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPersonWriteRequest(ApiModel):
|
|
||||||
person_id: UUID
|
|
||||||
role_id: UUID
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPersonRoleUpdateRequest(ApiModel):
|
|
||||||
role_id: UUID
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPersonRead(ApiModel):
|
|
||||||
id: UUID
|
|
||||||
document_id: UUID
|
|
||||||
person_id: UUID
|
|
||||||
role_id: UUID
|
|
||||||
role_label: str | None = None
|
|
||||||
person_name: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentPeopleResponse(ApiModel):
|
|
||||||
document_id: UUID
|
|
||||||
links: list[DocumentPersonRead] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def _document_type_to_read(item: DocumentType) -> DocumentTypeRead:
|
|
||||||
return DocumentTypeRead(
|
|
||||||
id=item.id,
|
|
||||||
label=item.label,
|
|
||||||
is_active=item.is_active,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _person_role_to_read(item: PersonRole) -> PersonRoleRead:
|
|
||||||
return PersonRoleRead(
|
|
||||||
id=item.id,
|
|
||||||
label=item.label,
|
|
||||||
is_active=item.is_active,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _document_person_to_read(item: DocumentPerson) -> DocumentPersonRead:
|
|
||||||
person_name = item.person.full_name if item.person is not None else None
|
|
||||||
return DocumentPersonRead(
|
|
||||||
id=item.id,
|
|
||||||
document_id=item.document_id,
|
|
||||||
person_id=item.person_id,
|
|
||||||
role_id=item.role_id,
|
|
||||||
role_label=item.role_ref.label if item.role_ref is not None else None,
|
|
||||||
person_name=person_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _document_to_type_response(item: Document) -> DocumentTypeWriteResponse:
|
|
||||||
if item.document_type_id is None:
|
|
||||||
raise ValueError("Document Type assignment did not persist")
|
|
||||||
return DocumentTypeWriteResponse(
|
|
||||||
document_id=item.id,
|
|
||||||
document_type_id=item.document_type_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_document_service(request: Request) -> DocumentService:
|
|
||||||
"""Resolve the document service from app lifespan state when available."""
|
|
||||||
services = getattr(request.app.state, "services", None)
|
|
||||||
if services is not None:
|
|
||||||
return services.documents
|
|
||||||
return DocumentService()
|
|
||||||
|
|
||||||
|
|
||||||
def get_people_service(request: Request) -> PeopleService:
|
|
||||||
"""Resolve the People service from app lifespan state when available."""
|
|
||||||
services = getattr(request.app.state, "services", None)
|
|
||||||
if services is not None:
|
|
||||||
return services.people
|
|
||||||
return PeopleService()
|
|
||||||
|
|
||||||
|
|
||||||
DocumentServiceDependency = Annotated[DocumentService, Depends(get_document_service)]
|
|
||||||
PeopleServiceDependency = Annotated[PeopleService, Depends(get_people_service)]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/document-types", response_model=list[DocumentTypeRead])
|
|
||||||
async def list_document_types(
|
|
||||||
service: DocumentServiceDependency,
|
|
||||||
active_only: bool = True,
|
|
||||||
) -> list[DocumentTypeRead]:
|
|
||||||
items = await service.list_document_types(active_only=active_only)
|
|
||||||
return [_document_type_to_read(item) for item in items]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/person-roles", response_model=list[PersonRoleRead])
|
|
||||||
async def list_person_roles(
|
|
||||||
service: PeopleServiceDependency,
|
|
||||||
active_only: bool = True,
|
|
||||||
) -> list[PersonRoleRead]:
|
|
||||||
items = await service.list_person_roles(active_only=active_only)
|
|
||||||
return [_person_role_to_read(item) for item in items]
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/documents/{document_id}/type", response_model=DocumentTypeWriteResponse)
|
|
||||||
async def set_document_type(
|
|
||||||
document_id: UUID,
|
|
||||||
payload: DocumentTypeWriteRequest,
|
|
||||||
service: DocumentServiceDependency,
|
|
||||||
) -> DocumentTypeWriteResponse:
|
|
||||||
document = await service.set_document_type(
|
|
||||||
document_id=document_id,
|
|
||||||
document_type_id=payload.document_type_id,
|
|
||||||
)
|
|
||||||
return _document_to_type_response(document)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/documents/{document_id}/people", response_model=DocumentPeopleResponse)
|
|
||||||
async def list_document_people(
|
|
||||||
document_id: UUID,
|
|
||||||
service: PeopleServiceDependency,
|
|
||||||
) -> DocumentPeopleResponse:
|
|
||||||
links = await service.list_document_people(document_id=document_id)
|
|
||||||
return DocumentPeopleResponse(document_id=document_id, links=[_document_person_to_read(item) for item in links])
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/documents/{document_id}/people", response_model=DocumentPersonRead)
|
|
||||||
async def add_document_person_link(
|
|
||||||
document_id: UUID,
|
|
||||||
payload: DocumentPersonWriteRequest,
|
|
||||||
service: PeopleServiceDependency,
|
|
||||||
) -> DocumentPersonRead:
|
|
||||||
link = await service.add_document_person_link(
|
|
||||||
document_id=document_id,
|
|
||||||
person_id=payload.person_id,
|
|
||||||
role_id=payload.role_id,
|
|
||||||
)
|
|
||||||
return _document_person_to_read(link)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/document-people/{document_person_id}", response_model=DocumentPersonRead)
|
|
||||||
async def set_document_person_role(
|
|
||||||
document_person_id: UUID,
|
|
||||||
payload: DocumentPersonRoleUpdateRequest,
|
|
||||||
service: PeopleServiceDependency,
|
|
||||||
) -> DocumentPersonRead:
|
|
||||||
link = await service.set_document_person_role(
|
|
||||||
document_person_id=document_person_id,
|
|
||||||
role_id=payload.role_id,
|
|
||||||
)
|
|
||||||
return _document_person_to_read(link)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/document-people/{document_person_id}", status_code=204)
|
|
||||||
async def delete_document_person_link(
|
|
||||||
document_person_id: UUID,
|
|
||||||
service: PeopleServiceDependency,
|
|
||||||
) -> Response:
|
|
||||||
await service.remove_document_person_link(document_person_id=document_person_id)
|
|
||||||
return Response(status_code=204)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""Safe media route for V4.4 Document print previews."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.responses import FileResponse
|
|
||||||
|
|
||||||
from transcription.services.source_media import SOURCE_MIME_TYPES
|
|
||||||
from transcription.services.sources import SourceService
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v4", tags=["v4-print"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_source_service(request: Request) -> SourceService:
|
|
||||||
services = getattr(request.app.state, "services", None)
|
|
||||||
if services is not None:
|
|
||||||
return services.sources
|
|
||||||
return SourceService()
|
|
||||||
|
|
||||||
|
|
||||||
SourceServiceDependency = Annotated[SourceService, Depends(get_source_service)]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/documents/{document_id}/sources/{source_id}/media", response_class=FileResponse)
|
|
||||||
async def read_document_source_media(
|
|
||||||
document_id: UUID,
|
|
||||||
source_id: UUID,
|
|
||||||
service: SourceServiceDependency,
|
|
||||||
) -> FileResponse:
|
|
||||||
"""Serve one validated Source through record identifiers, never a supplied path."""
|
|
||||||
source = await service.read_source(source_id)
|
|
||||||
if source.document_id != document_id:
|
|
||||||
raise HTTPException(status_code=404, detail="Source not found for Document")
|
|
||||||
|
|
||||||
path = Path(source.file_path).resolve()
|
|
||||||
upload_root = service.settings.upload_dir.resolve()
|
|
||||||
try:
|
|
||||||
path.relative_to(upload_root)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(status_code=404, detail="Source media is outside managed storage") from exc
|
|
||||||
if not path.is_file():
|
|
||||||
raise HTTPException(status_code=404, detail="Source media is unavailable")
|
|
||||||
|
|
||||||
media_type = SOURCE_MIME_TYPES.get(path.suffix.lower())
|
|
||||||
if media_type is None:
|
|
||||||
raise HTTPException(status_code=415, detail="Unsupported Source media type")
|
|
||||||
return FileResponse(path, media_type=media_type)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user