generated from john/python-template
gpt-5.3-codex review phase 1 - Flatten the documentation
Quality Gate / gate (push) Successful in 33s
Quality Gate / gate (push) Successful in 33s
This commit is contained in:
@@ -32,7 +32,7 @@ is the only service that may **create or delete** its rows.
|
|||||||
| `Source`, `JobSource` | `SourceService` |
|
| `Source`, `JobSource` | `SourceService` |
|
||||||
| `Job` | `JobService` |
|
| `Job` | `JobService` |
|
||||||
| `Person`, `PersonRole`, `DocumentPerson` | `PeopleService` |
|
| `Person`, `PersonRole`, `DocumentPerson` | `PeopleService` |
|
||||||
| `ExecutionAttempt` | `EvidenceService` |
|
| `ExecutionAttempt` | `SourceService` |
|
||||||
|
|
||||||
### Junction tables
|
### Junction tables
|
||||||
|
|
||||||
@@ -56,10 +56,8 @@ Two consequences follow, and both are deliberate:
|
|||||||
transition is a Job lifecycle event, not a per-page outcome. They create and delete
|
transition is a Job lifecycle event, not a per-page outcome. They create and delete
|
||||||
nothing.
|
nothing.
|
||||||
|
|
||||||
`EvidenceService.promote_machine_attempt` writes two fields on `Source`
|
`EvidenceService` is read-focused and projection-focused. It may coordinate selection
|
||||||
(`preferred_execution_attempt_id`, `raw_transcription`). This is allowed on the same
|
flows, but append-only attempt creation remains in `SourceService` write paths.
|
||||||
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
|
If a new operation cannot be expressed within one owner, it belongs in an orchestration
|
||||||
module, not in a cross-service import.
|
module, not in a cross-service import.
|
||||||
@@ -86,7 +84,8 @@ module, not in a cross-service import.
|
|||||||
- Where a service exposes create/read/update/delete for its root model, define them at the
|
- 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.
|
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
|
- 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.
|
`SourceService` workflow-facing methods, so `EvidenceService` deliberately exposes reads and
|
||||||
|
no create or delete.
|
||||||
Do not add unused CRUD methods to satisfy symmetry.
|
Do not add unused CRUD methods to satisfy symmetry.
|
||||||
- `RegistryService` is generic across small lookup models and uses `<operation>_entry`
|
- `RegistryService` is generic across small lookup models and uses `<operation>_entry`
|
||||||
naming instead.
|
naming instead.
|
||||||
@@ -132,6 +131,13 @@ Separation of concerns:
|
|||||||
- Services should expose session-aware write helpers (flush on caller-owned session) so orchestration controls commit boundaries.
|
- 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.
|
- Backoff/sleep behavior must run outside transactional scopes.
|
||||||
|
|
||||||
|
## V4 Contract Alignment
|
||||||
|
|
||||||
|
- Treat `docs/ver4/` as the active architecture and requirements baseline.
|
||||||
|
- `Job.status` success path is `TRANSCRIBED`; `COMPLETED` is legacy-compatible and must not be used for new success transitions.
|
||||||
|
- `JobSource.status` is queue/projection state only (`PENDING`, `TRANSCRIBED`, `FAILED`, `CANCELLED`).
|
||||||
|
- Source ingest may normalize media before persistence; persisted bytes/hash are canonical for processing and provenance.
|
||||||
|
|
||||||
# Service Composition
|
# Service Composition
|
||||||
|
|
||||||
A service method may read across models it does not own, using eager loads from its own
|
A service method may read across models it does not own, using eager loads from its own
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Pages may depend on application services and framework-provided dependencies. Co
|
|||||||
- Keep app-wide navigation and layout primitives in `components/app_shell.py`.
|
- 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 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.
|
- Keep exception normalization and user-facing error display in `components/error_presenter.py`; preserve `AppError` details and operation identifiers at page/component boundaries.
|
||||||
|
- Use `components/media_urls.py` for media URL generation; do not hand-build upload/static paths in page code.
|
||||||
|
|
||||||
## CSS Assets
|
## CSS Assets
|
||||||
|
|
||||||
@@ -51,3 +52,9 @@ Pages may depend on application services and framework-provided dependencies. Co
|
|||||||
- Limit component state to ephemeral interaction state such as loading flags, form values, dialogs, and expansion state.
|
- 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.
|
- 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.
|
- Keep filesystem, network, provider, and worker orchestration behind application services or dedicated adapters. UI code may trigger those operations but must not implement them.
|
||||||
|
|
||||||
|
## V4 Contract Alignment
|
||||||
|
|
||||||
|
- Treat `docs/ver4/` as the active baseline and `docs/ver4/history.md` as historical reference only.
|
||||||
|
- Use status vocabulary exactly as modeled (`queued`, `processing`, `transcribed`, `partial_success`, `failed`; and `pending`, `transcribed`, `failed`, `cancelled`).
|
||||||
|
- Print/export media flows must use record-validated routes from API modules; direct local filesystem paths are prohibited.
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ Perform thorough, evidence-based code reviews for Python projects. Every finding
|
|||||||
4. **Prioritize Hot Paths:** Focus deeply on request handling, database sessions, background workers, and external API calls.
|
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.
|
5. **Enforce Read-Only Safety:** Do not modify code unless explicitly instructed.
|
||||||
|
|
||||||
|
## Repo-Specific Deterministic Checks (Transcription)
|
||||||
|
|
||||||
|
When reviewing this repository, always include explicit pass/fail checks for:
|
||||||
|
|
||||||
|
1. **Service boundary rule:** no service-to-service imports (`tests/test_service_boundaries.py`).
|
||||||
|
2. **UI boundary rule:** pages/components do not perform persistence access (`tests/test_ui_boundaries.py`).
|
||||||
|
3. **Status vocabulary conformance:** `JobStatus`/`JobSourceStatus` usage matches current enums in `src/transcription/db/models.py`.
|
||||||
|
4. **Evidence ownership conformance:** append-only attempt history is preserved and projection writes are not mistaken for history mutation (`src/transcription/services/sources.py`, `src/transcription/services/evidence.py`).
|
||||||
|
5. **Canonical V4 authority:** findings must resolve against `docs/ver4/*` first, and treat `docs/ver4/history.md` plus `docs/ver4.x/*` as historical context.
|
||||||
|
|
||||||
## Core Review Areas
|
## Core Review Areas
|
||||||
|
|
||||||
### 1. Python Best Practices (3.12+)
|
### 1. Python Best Practices (3.12+)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The application exists to preserve historical source material and produce useful
|
|||||||
|
|
||||||
The application distinguishes five kinds of information:
|
The application distinguishes five kinds of information:
|
||||||
|
|
||||||
1. **Source evidence**: the original uploaded media and the facts needed to identify and verify it.
|
1. **Source evidence**: the canonical stored media used for processing 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.
|
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.
|
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.
|
4. **Normalized data**: selected fields extracted for search, display, accounting, and workflow behavior.
|
||||||
@@ -20,12 +20,12 @@ Normalized data and derived artifacts never replace source or transport evidence
|
|||||||
|
|
||||||
## 3. Core Invariants
|
## 3. Core Invariants
|
||||||
|
|
||||||
### 3.1 Original Source Preservation
|
### 3.1 Canonical Source Preservation
|
||||||
|
|
||||||
1. The original uploaded bytes are the primary evidence and must be preserved without transformation.
|
1. Each source must have one canonical stored byte stream used for processing and provenance.
|
||||||
2. Each source must have a cryptographic content digest, byte size, and stable identity.
|
2. Canonical storage may apply deterministic ingest normalization before persistence.
|
||||||
3. Processing may use transformed derivatives, but those derivatives must not overwrite the original.
|
3. Canonical stored bytes must have a cryptographic content digest, byte size, and stable identity.
|
||||||
4. A derivative used for processing must record its relationship to the original, its transformation, and its own digest.
|
4. Post-ingest processing derivatives must not overwrite canonical stored bytes.
|
||||||
5. Moving or renaming a stored file must not change its evidence identity.
|
5. Moving or renaming a stored file must not change its evidence identity.
|
||||||
|
|
||||||
### 3.2 Append-Only Processing History
|
### 3.2 Append-Only Processing History
|
||||||
@@ -45,7 +45,7 @@ Each execution must preserve enough information to understand what the applicati
|
|||||||
3. Prompt asset name and content digest when a prompt asset is used.
|
3. Prompt asset name and content digest when a prompt asset is used.
|
||||||
4. Every explicitly supplied generation or processing parameter.
|
4. Every explicitly supplied generation or processing parameter.
|
||||||
5. Whether an optional parameter was explicitly set or omitted.
|
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.
|
6. Canonical source digest (and derivative digests when used), media type, dimensions or page geometry when known, and page identity.
|
||||||
7. A secret-safe representation of the request structure.
|
7. A secret-safe representation of the request structure.
|
||||||
8. Application, provider-adapter, and client-library versions sufficient to interpret the execution.
|
8. Application, provider-adapter, and client-library versions sufficient to interpret the execution.
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Provenance supports explanation, comparison, and best-effort reproduction; it do
|
|||||||
|
|
||||||
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.
|
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.
|
Likewise, preserving a general vision-model response does not create OCR coordinates that were never returned. Future coordinate extraction remains possible because canonical source evidence is preserved and can be processed again by a suitable system.
|
||||||
|
|
||||||
## 5. Model Evaluation Policy
|
## 5. Model Evaluation Policy
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ Benchmark material containing family records remains private application data un
|
|||||||
|
|
||||||
## 6. Ownership and Change Policy
|
## 6. Ownership and Change Policy
|
||||||
|
|
||||||
1. Versioned architecture, schema, scope, and implementation documents define how a release satisfies this invariant.
|
1. Canonical V4 architecture, schema, requirements, and error-policy documents define how current behavior satisfies this invariant.
|
||||||
2. Provider adapters own the capture of provider-boundary evidence.
|
2. Provider adapters own the capture of provider-boundary evidence.
|
||||||
3. Services own validation, persistence, retention, and export behavior.
|
3. Services own validation, persistence, retention, and export behavior.
|
||||||
4. UI pages may inspect evidence through service contracts but do not define evidence semantics.
|
4. UI pages may inspect evidence through service contracts but do not define evidence semantics.
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ When documents disagree, use this order:
|
|||||||
4. Durable failure behavior: [Error Handling invariant](../invariant/error_handling.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).
|
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).
|
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.
|
7. Historical context only: [V4 revision history](../ver4/history.md).
|
||||||
8. Implementation truth: current code and tests.
|
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.
|
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.
|
||||||
@@ -57,4 +57,4 @@ Each page contract contains:
|
|||||||
|
|
||||||
## Current Baseline
|
## Current Baseline
|
||||||
|
|
||||||
These contracts describe the completed V4 through V4.5 behavior.
|
These contracts describe the current flattened V4 baseline.
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ The application root and `/ui` redirect to `/ui/homepage`.
|
|||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- Homepage storage is fixed under the repository/application `data` directory rather than a configured application-data root.
|
- Homepage storage location is configured by application settings and must remain writable in the active runtime environment.
|
||||||
- Uploading an image is immediate and is not rolled back by Cancel.
|
- Uploading an image is immediate and is not rolled back by Cancel.
|
||||||
- The editor does not currently delete or select among previously uploaded images.
|
- The editor does not currently delete or select among previously uploaded images.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Jobs manages transcription processing runs. A Job belongs to one Document, links
|
|||||||
|
|
||||||
## Cancel Behavior
|
## Cancel Behavior
|
||||||
|
|
||||||
- The confirmation explains that processing stops and remaining non-transcribed Sources become failed.
|
- The confirmation explains that processing stops and remaining pending Sources become cancelled.
|
||||||
- The service decides whether the current state permits cancellation.
|
- The service decides whether the current state permits cancellation.
|
||||||
- Success updates the Job, notifies the worker, and returns to Job Detail.
|
- Success updates the Job, notifies the worker, and returns to Job Detail.
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# Implementation Plan (version 4.0)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Implement the version 4.0 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,89 +0,0 @@
|
|||||||
# V4.0 Scope Boundary
|
|
||||||
|
|
||||||
This document defines the scope for the transition from the current repository state to the Version 4.0 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`
|
|
||||||
@@ -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.0/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.0/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`
|
|
||||||
@@ -6,7 +6,7 @@ V4.8 is the first release since V4.5 to add **new user-facing behavior**. V4.6 w
|
|||||||
|
|
||||||
## Dependency on V4.7
|
## 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).
|
**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 archived V4.7 scope boundary at `docs-v4x-archive:docs/ver4.7/scope_boundary_v4_7.md`.
|
||||||
|
|
||||||
## Candidate Features
|
## Candidate Features
|
||||||
|
|
||||||
@@ -103,8 +103,8 @@ Item 6 is not recommended.
|
|||||||
|
|
||||||
## Related Local References
|
## Related Local References
|
||||||
|
|
||||||
- [V4.7 Scope Boundary](../ver4.7/scope_boundary_v4_7.md) - the blocking dependency for item 5
|
- Archived V4.7 scope boundary: `docs-v4x-archive:docs/ver4.7/scope_boundary_v4_7.md` (blocking dependency for item 5)
|
||||||
- [V4.6 Scope Boundary](../ver4.6/scope_boundary_v4_6.md)
|
- Archived V4.6 scope boundary: `docs-v4x-archive:docs/ver4.6/scope_boundary_v4_6.md`
|
||||||
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
- [Architecture & Code Review Report](../architecture_code_review_2026-08-17.md)
|
||||||
- `.github/instructions/ui.instructions.md`
|
- `.github/instructions/ui.instructions.md`
|
||||||
- `src/transcription/ui/homepage_store.py` - existing multi-image storage
|
- `src/transcription/ui/homepage_store.py` - existing multi-image storage
|
||||||
|
|||||||
+80
-160
@@ -1,75 +1,36 @@
|
|||||||
# System Architecture (Version 4)
|
# System Architecture (Version 4)
|
||||||
|
|
||||||
This document describes the production architecture of the document transcription system.
|
This document defines the current V4 architecture baseline.
|
||||||
|
|
||||||
## Architecture Objectives
|
## Architecture Objectives
|
||||||
|
|
||||||
- Preserve original source material, per-execution machine output, and separate human revision.
|
- Preserve durable archival records for Documents, Sources, People, and processing runs.
|
||||||
- Support batching one or more images into ordered multi-page documents.
|
- Execute page transcription asynchronously with bounded worker behavior.
|
||||||
- Capture submission-time prompt provenance and a per-page OpenRouter SDK response snapshot.
|
- Preserve append-only machine-attempt evidence with request/response provenance.
|
||||||
- Execute page transcription concurrently with bounded `asyncio` workers.
|
- Keep UI, API, service, persistence, and provider boundaries explicit and testable.
|
||||||
- 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
|
## Technical Stack
|
||||||
|
|
||||||
- **Runtime:** Python 3.12 or later.
|
- **Runtime:** Python 3.12+
|
||||||
- **Web application:** FastAPI and NiceGUI.
|
- **Web application:** FastAPI + NiceGUI
|
||||||
- **Persistence:** SQLModel and SQLAlchemy, with SQLite and PostgreSQL support.
|
- **Persistence:** SQLModel / SQLAlchemy (SQLite-first, PostgreSQL-compatible model design)
|
||||||
- **Validation and settings:** Pydantic V2 and pydantic-settings.
|
- **Validation and settings:** Pydantic V2 + pydantic-settings
|
||||||
- **Concurrency:** Python `asyncio` workers.
|
- **Concurrency:** asyncio worker loop
|
||||||
- **Vision integration:** OpenRouter through the application's provider adapter.
|
- **Provider integration:** OpenRouter adapter behind provider interface
|
||||||
- **Testing and quality:** pytest, pytest-asyncio, Ruff, and ty.
|
- **Quality and tests:** Ruff, ty, pytest, pytest-asyncio
|
||||||
|
|
||||||
## Runtime Topology
|
## Runtime Topology
|
||||||
|
|
||||||
The runtime operates as an asynchronous Python application:
|
```mermaid
|
||||||
|
|
||||||
- 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
|
flowchart LR
|
||||||
U[Browser User] --> A[FastAPI + NiceGUI App]
|
U[Browser User] --> A[FastAPI + NiceGUI App]
|
||||||
A --> W[Asyncio Worker Engine]
|
A --> W[Asyncio Worker]
|
||||||
A --> DB[(Relational DB)]
|
A --> DB[(SQLite/PostgreSQL Model)]
|
||||||
W --> P[Vision Provider APIs]
|
W --> P[Provider Adapter]
|
||||||
W --> DB
|
W --> DB
|
||||||
^^^
|
```
|
||||||
|
|
||||||
## Lifecycle Ownership
|
## Layered Boundaries
|
||||||
|
|
||||||
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
|
### Interface Layer
|
||||||
|
|
||||||
@@ -78,136 +39,95 @@ Application lifespan owns runtime setup and teardown:
|
|||||||
|
|
||||||
Responsibilities:
|
Responsibilities:
|
||||||
|
|
||||||
- Render document, source, person, job, and classification views.
|
- Route registration, page orchestration, presentation adapters.
|
||||||
- Accept user input for uploads, editing, linking, and revisions.
|
- Structured user messaging through shared error presenter.
|
||||||
- Present structured validation and conflict feedback.
|
- No direct persistence access from pages/components.
|
||||||
|
|
||||||
### Application and Async Worker Layer
|
### Service and Orchestration 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/documents.py`
|
||||||
- `src/transcription/services/sources.py`
|
|
||||||
- `src/transcription/services/jobs.py`
|
|
||||||
- `src/transcription/services/people.py`
|
- `src/transcription/services/people.py`
|
||||||
|
- `src/transcription/services/jobs.py`
|
||||||
|
- `src/transcription/services/sources.py`
|
||||||
|
- `src/transcription/services/evidence.py`
|
||||||
|
- `src/transcription/services/store.py`
|
||||||
- `src/transcription/services/workflows.py`
|
- `src/transcription/services/workflows.py`
|
||||||
|
|
||||||
Responsibilities:
|
Responsibilities:
|
||||||
|
|
||||||
- Keep one primary service boundary per aggregate: Documents, Sources, Jobs, and People.
|
- Aggregate ownership and invariants.
|
||||||
- Documents own document records and the document-type registry.
|
- Transaction-aware write helpers.
|
||||||
- Sources own source records, revisions, source media formats, MIME resolution, and page execution evidence.
|
- Cross-service workflows in orchestration modules (`store.py`, `workflows.py`).
|
||||||
- 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
|
### Persistence Layer
|
||||||
|
|
||||||
- `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/db/**`
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- SQLModel definitions, async session/engine runtime, registry bootstrap.
|
||||||
|
- Loader helpers that enforce explicit eager loading with `lazy="raise"` relationships.
|
||||||
|
|
||||||
|
### Provider Layer
|
||||||
|
|
||||||
- `src/transcription/providers/**`
|
- `src/transcription/providers/**`
|
||||||
|
|
||||||
Responsibilities:
|
Responsibilities:
|
||||||
|
|
||||||
- Provide async database sessions and engine configuration.
|
- Provider API encapsulation.
|
||||||
- Provide provider adapters for vision model execution.
|
- Request manifest and transport evidence capture.
|
||||||
|
- Normalized transcription result contract.
|
||||||
|
|
||||||
## Core Workflows
|
## Core Domain Model
|
||||||
|
|
||||||
### 1. Multi-Page Transcription
|
- `Document` owns archival metadata and links to `Source`, `Job`, and `DocumentPerson`.
|
||||||
|
- `Source` is a document page/file record with selected machine projection and human revision.
|
||||||
|
- `Job` is an aggregate processing run with status and frozen prompt/runtime settings.
|
||||||
|
- `JobSource` is queue/membership state for one `(job, source)` pair.
|
||||||
|
- `ExecutionAttempt` is append-only evidence for each provider call.
|
||||||
|
- `DocumentType` and `PersonRole` are UUID-backed registries with optional protected `semantic_key`.
|
||||||
|
|
||||||
1. User uploads one or more images for a `Document`.
|
## Processing and Evidence Workflow
|
||||||
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 creates/updates Document metadata and linked People atomically through workflow orchestration.
|
||||||
|
2. User creates a Job by uploading one or more Source files or by retranscribing an existing Source.
|
||||||
|
3. Source files are validated and stored; orientation normalization may be applied at ingest, and stored bytes become the canonical processing bytes.
|
||||||
|
4. Worker claims queued Job, transitions to `processing`, and processes pending pages in deterministic order.
|
||||||
|
5. Each provider call writes one immutable `ExecutionAttempt` with:
|
||||||
|
- request manifest + hash
|
||||||
|
- transport evidence (when response exists)
|
||||||
|
- SDK snapshot and normalized metadata
|
||||||
|
- outcome, timing, and error details when applicable
|
||||||
|
6. `JobSource` status is updated as queue/projection state; `Source.raw_transcription` is set on first successful attempt and can be explicitly re-pointed by candidate promotion.
|
||||||
|
7. Job terminal status resolves to `transcribed`, `partial_success`, or `failed`.
|
||||||
|
|
||||||
1. User opens Document Create or Edit.
|
## Status Semantics
|
||||||
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
|
- **Job statuses:** `queued`, `processing`, `transcribed`, `completed`, `partial_success`, `failed`
|
||||||
|
- Operational success path currently resolves to `transcribed`.
|
||||||
|
- `completed` remains a recognized legacy-compatible status value.
|
||||||
|
- **JobSource statuses:** `pending`, `transcribed`, `failed`, `cancelled`
|
||||||
|
|
||||||
1. User selects a registry-backed document type for a document.
|
## Security and Path Handling Boundaries
|
||||||
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
|
- Print media delivery uses record-validated API route:
|
||||||
|
- `src/transcription/api/v4_print.py`
|
||||||
|
- General UI media links resolve through:
|
||||||
|
- `src/transcription/ui/components/media_urls.py`
|
||||||
|
- Local filesystem paths must never be accepted from user input as trusted media routes.
|
||||||
|
|
||||||
1. User opens Print from persisted Document Detail.
|
## Concurrency and Reliability Principles
|
||||||
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
|
- Worker loop reuses service bundle/provider resources for pooled calls.
|
||||||
|
- Provider-call timeout is explicit and bounded.
|
||||||
|
- Non-retriable worker-loop faults are surfaced and stop loop spin.
|
||||||
|
- Per-page outcomes are durably persisted before processing next page.
|
||||||
|
|
||||||
- `JobSource.raw_transcription` preserves page output for its Job execution.
|
## Related References
|
||||||
- `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)
|
- [System Requirements](requirements_v4.md)
|
||||||
- [Data Model](schema_v4.md)
|
- [Data Model](schema_v4.md)
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
- [Error Handling Policy](error_handling_v4.md)
|
||||||
- [Error Handling Invariant](../invariant/error_handling.md)
|
- [V4 Revision History](history.md)
|
||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
- [Error Handling invariant](../invariant/error_handling.md)
|
||||||
|
- [AI evidence invariant](../invariant/ai_evidence_and_provenance.md)
|
||||||
|
|||||||
@@ -1,115 +1,58 @@
|
|||||||
# Error Handling Policy (Version 4)
|
# 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).
|
This policy defines active V4 error taxonomy, translation boundaries, and retry semantics.
|
||||||
|
|
||||||
## Invariant Alignment
|
## Error Categories
|
||||||
|
|
||||||
Version 4 implements the invariant through:
|
| Category | Meaning | Typical Origin | User Treatment |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `validation` | Input payload/selection is invalid | UI form parsing, service validators | Inline correction guidance |
|
||||||
|
| `not_found` | Target record is missing | ID lookup in service layer | Non-blocking warning or redirect |
|
||||||
|
| `conflict` | State prevents requested action | lifecycle transitions, duplicate semantic keys | Explain required precondition |
|
||||||
|
| `external` | Provider/network dependency failure | OpenRouter/provider adapter | Retry path and evidence retained |
|
||||||
|
| `timeout` | Provider call exceeded configured bound | worker/provider client timeout | Retry path and bounded messaging |
|
||||||
|
| `internal` | Unexpected local failure | unhandled service/runtime faults | Safe generic message + diagnostics capture |
|
||||||
|
|
||||||
- The shared error taxonomy below.
|
## Translation Boundaries
|
||||||
- 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
|
- **Provider layer:** raise provider-scoped exceptions with provider context; do not emit UI text.
|
||||||
|
- **Service layer:** map raw exceptions into domain-aware categories and preserve causal chain.
|
||||||
|
- **UI/API layer:** convert category to user-safe message with contextual action guidance.
|
||||||
|
|
||||||
This policy governs error behavior across:
|
## Job and Page Failure Semantics
|
||||||
|
|
||||||
- NiceGUI pages
|
### Page-Level (`JobSource`)
|
||||||
- FastAPI routes
|
|
||||||
- Service-layer orchestration
|
|
||||||
- `asyncio` worker tasks
|
|
||||||
- Database interactions
|
|
||||||
- Provider adapters
|
|
||||||
|
|
||||||
## Error Taxonomy
|
- `pending` -> `transcribed` when attempt succeeds.
|
||||||
|
- `pending` -> `failed` when attempt fails terminally.
|
||||||
|
- `pending` -> `cancelled` on job cancellation before processing.
|
||||||
|
|
||||||
| Category | Definition | Retriable |
|
### Job-Level (`Job`)
|
||||||
| --- | --- | --- |
|
|
||||||
| `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
|
- `transcribed` when all pages transcribe successfully.
|
||||||
|
- `partial_success` when mixed success/failure outcomes exist.
|
||||||
|
- `failed` when no page transcribes successfully.
|
||||||
|
|
||||||
In multi-page `asyncio` processing:
|
## Retry and Retranscription Rules
|
||||||
|
|
||||||
1. Exceptions from individual page calls are trapped within the page task wrapper.
|
1. Failed/cancelled pages may be re-queued through retranscription workflows.
|
||||||
2. Failed page detail is written to `JobSource.error_detail` and the page state becomes `failed`.
|
2. Retry attempts must append new `ExecutionAttempt` rows; prior evidence remains immutable.
|
||||||
3. Aggregate job status is derived from page outcomes:
|
3. Selecting a better candidate must update projection pointers, not mutate historical attempt rows.
|
||||||
- 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
|
## Logging and Diagnostics Rules
|
||||||
|
|
||||||
When relationship or document-type writes fail policy checks:
|
1. Persist sufficient attempt error metadata (`error_category`, `error_message`, transport evidence) for post-hoc analysis.
|
||||||
|
2. Avoid leaking stack traces or local paths into user-facing message envelopes.
|
||||||
|
3. Preserve causal exception chains for internal diagnostics.
|
||||||
|
|
||||||
1. Reject the full write operation.
|
## UI Messaging Contract
|
||||||
2. Return structured conflict detail including target identifiers and the violated rule.
|
|
||||||
3. Preserve existing persisted relationships unchanged.
|
|
||||||
|
|
||||||
## API Error Response Contract
|
- User-visible errors must be actionable, bounded, and category-consistent.
|
||||||
|
- Multi-page jobs must show partial outcomes instead of collapsing into a single opaque failure.
|
||||||
|
- Recovery actions (`retry`, `retranscribe`, `edit input`) must be offered where available.
|
||||||
|
|
||||||
API error responses return a structured envelope:
|
## Cross-Reference
|
||||||
|
|
||||||
^^^json
|
- [Error Handling invariant](../invariant/error_handling.md)
|
||||||
{
|
|
||||||
"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)
|
- [System Requirements](requirements_v4.md)
|
||||||
- [Data Model](schema_v4.md)
|
- [Data Model](schema_v4.md)
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# V4 Revision History (Archived)
|
||||||
|
|
||||||
|
This index tracks superseded V4.x documents as historical context.
|
||||||
|
These revisions were intentionally removed from the active working tree to prevent accidental reuse by tools and AI agents.
|
||||||
|
They are preserved at immutable git tag `docs-v4x-archive`.
|
||||||
|
|
||||||
|
## Archive Map
|
||||||
|
|
||||||
|
| Revision | Archived Paths at `docs-v4x-archive` |
|
||||||
|
| :--- | :--- |
|
||||||
|
| V4.0 | `docs/ver4.0/scope_boundary_v4.md`, `docs/ver4.0/implementation_plan_v4.md` |
|
||||||
|
| V4.1 | `docs/ver4.1/scope_boundary_v4_1.md`, `docs/ver4.1/implementation_plan_v4_1.md` |
|
||||||
|
| V4.2 | `docs/ver4.2/scope_boundary_v4_2.md`, `docs/ver4.2/implementation_plan_v4_2.md` |
|
||||||
|
| V4.3 | `docs/ver4.3/scope_boundary_v4_3.md`, `docs/ver4.3/implementation_plan_v4_3.md` |
|
||||||
|
| V4.4 | `docs/ver4.4/scope_boundary_v4_4.md`, `docs/ver4.4/implementation_plan_v4_4.md` |
|
||||||
|
| V4.5 | `docs/ver4.5/scope_boundary_v4_5.md`, `docs/ver4.5/implementation_plan_v4_5.md` |
|
||||||
|
| V4.6 | `docs/ver4.6/scope_boundary_v4_6.md`, `docs/ver4.6/implementation_plan_v4_6.md`, `docs/ver4.6/review_log_v4_6.md` |
|
||||||
|
| V4.7 | `docs/ver4.7/scope_boundary_v4_7.md`, `docs/ver4.7/implementation_plan_v4_7.md`, `docs/ver4.7/review_log_v4_7.md` |
|
||||||
|
| V4.8 | `../ver4.8/feature_backlog_v4_8.md` |
|
||||||
|
|
||||||
|
To inspect archived content locally:
|
||||||
|
|
||||||
|
`git show docs-v4x-archive:docs/ver4.6/scope_boundary_v4_6.md`
|
||||||
|
|
||||||
|
## Canonical Contract Reminder
|
||||||
|
|
||||||
|
Use `docs/ver4/` for current-state requirements, architecture, schema, and error policy:
|
||||||
|
|
||||||
|
- `index_v4.md`
|
||||||
|
- `architecture_v4.md`
|
||||||
|
- `requirements_v4.md`
|
||||||
|
- `schema_v4.md`
|
||||||
|
- `error_handling_v4.md`
|
||||||
+10
-18
@@ -1,13 +1,14 @@
|
|||||||
# Document Transcription System Overview (Version 4)
|
# 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.
|
This directory is the single source of truth for current V4 behavior and architecture.
|
||||||
|
|
||||||
## Recommended Reading Order
|
## Canonical Reading Order
|
||||||
|
|
||||||
1. [System Architecture](architecture_v4.md) for capabilities, technical stack, runtime structure, workflows, and component ownership.
|
1. [System Architecture](architecture_v4.md) for runtime topology, boundaries, and lifecycle ownership.
|
||||||
2. [System Requirements](requirements_v4.md) for the verifiable V4 contract.
|
2. [System Requirements](requirements_v4.md) for verifiable current-state requirements.
|
||||||
3. [Data Model](schema_v4.md) for entities, relationships, constraints, and persistence rules.
|
3. [Data Model](schema_v4.md) for entities, constraints, and evidence persistence rules.
|
||||||
4. [Error Handling Policy](error_handling_v4.md) for the V4 taxonomy and boundary contracts.
|
4. [Error Handling Policy](error_handling_v4.md) for category, translation, and retry behavior.
|
||||||
|
5. [V4 Revision History](history.md) for superseded scope and implementation documents.
|
||||||
|
|
||||||
## Cross-Version Invariants
|
## Cross-Version Invariants
|
||||||
|
|
||||||
@@ -17,16 +18,7 @@ Version 4 is the architecture baseline for the personal-scale application used t
|
|||||||
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
- [Digital Evidence and AI Processing Provenance](../invariant/ai_evidence_and_provenance.md)
|
||||||
- [UI Style Guide](../invariant/ui_style_guide.md)
|
- [UI Style Guide](../invariant/ui_style_guide.md)
|
||||||
|
|
||||||
## V4 Transition Documents
|
## Baseline Statement
|
||||||
|
|
||||||
- [Scope Boundary](scope_boundary_v4.md)
|
The current V4 baseline includes behavior delivered through V4.7 architectural cleanup.
|
||||||
- [Implementation Plan](implementation_plan_v4.md)
|
Versioned V4.x scope and implementation documents are retained as historical records only at git tag `docs-v4x-archive` and do not define active contracts.
|
||||||
|
|
||||||
## 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 +1,61 @@
|
|||||||
# Document Transcription System Requirements (Version 4)
|
# System Requirements (Version 4)
|
||||||
|
|
||||||
This document defines the baseline requirements for the document transcription system.
|
These requirements define the active V4 contract and align to current implementation.
|
||||||
|
|
||||||
## Requirements Model
|
## Functional Requirements
|
||||||
|
|
||||||
| ID | Category | Requirement | Verify Method |
|
### Domain and Record Management
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 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
|
- **REQ-4-001 Document Registry:** The system must create and update `Document` records with title, type, language, comments, date metadata, and optional location.
|
||||||
|
- **REQ-4-002 Source Registry:** The system must create and update `Source` records linked to exactly one `Document`.
|
||||||
|
- **REQ-4-003 People Registry:** The system must create and update `Person` records and support many-to-many links to `Document` with role and confidence.
|
||||||
|
- **REQ-4-004 Registry Semantics:** Document types and person roles must support optional immutable semantic keys and hard-delete only when unreferenced.
|
||||||
|
|
||||||
1. `DocumentType.id` and `PersonRole.id` are their public and relationship identities; labels are unique ignoring case and surrounding whitespace.
|
### Job and Workflow Behavior
|
||||||
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
|
- **REQ-4-010 Job Creation:** The system must create `Job` records from uploaded sources and from retranscription of existing sources.
|
||||||
|
- **REQ-4-011 Prompt Snapshotting:** Job creation must persist effective prompt and runtime settings as immutable per-job snapshots.
|
||||||
|
- **REQ-4-012 Queue Membership:** Each `(job, source)` pair must be represented by one `JobSource` row.
|
||||||
|
- **REQ-4-013 Job Status Lifecycle:** `Job.status` must use one of `queued`, `processing`, `transcribed`, `completed`, `partial_success`, `failed`.
|
||||||
|
- **REQ-4-014 JobSource Status Lifecycle:** `JobSource.status` must use one of `pending`, `transcribed`, `failed`, `cancelled`.
|
||||||
|
- **REQ-4-015 Terminal Job Resolution:** Job terminal status must derive from page outcomes as `transcribed`, `partial_success`, or `failed`.
|
||||||
|
- **REQ-4-016 Cancellation Semantics:** Job cancellation must set remaining `pending` page entries to `cancelled`.
|
||||||
|
|
||||||
- UI (NiceGUI): Satisfies REQ-0, REQ-1, REQ-5, REQ-9, REQ-12, REQ-13, REQ-22, REQ-24.
|
### Transcription and Evidence
|
||||||
- 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
|
- **REQ-4-020 Attempt Evidence:** Each provider call must emit one append-only `ExecutionAttempt` record.
|
||||||
|
- **REQ-4-021 Attempt Payload:** `ExecutionAttempt` must retain request manifest/hash, outcome, timing, model/provider fields, and error details when present.
|
||||||
|
- **REQ-4-022 Transport Evidence:** Provider response evidence must be attached to the attempt when a response is available.
|
||||||
|
- **REQ-4-023 Source Projection Rule:** `Source.raw_transcription` is a projection chosen from attempt outcomes and can be repointed by explicit promotion.
|
||||||
|
- **REQ-4-024 Candidate Visibility:** UI must expose candidate attempts with metadata needed for comparative review and selection.
|
||||||
|
|
||||||
- [System Overview](index_v4.md)
|
### Media and Access
|
||||||
- [System Architecture](architecture_v4.md)
|
|
||||||
- [Data Model](schema_v4.md)
|
- **REQ-4-030 Ingest Canonicalization:** Stored source bytes may be normalized at ingest (for example orientation correction); stored bytes are the canonical processing source.
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
- **REQ-4-031 Path Safety:** Client-facing media URLs must be generated from controlled application paths only.
|
||||||
|
- **REQ-4-032 Print Media Validation:** Print/export source media must be served through record-validated API routes.
|
||||||
|
|
||||||
|
### Error and UX Contracts
|
||||||
|
|
||||||
|
- **REQ-4-040 Error Envelope:** Service/API errors must map to structured, user-safe error categories and messages.
|
||||||
|
- **REQ-4-041 Partial Failure Visibility:** Mixed page outcomes must be visible at job and page level.
|
||||||
|
- **REQ-4-042 Retry Support:** Failed and cancelled pages must support targeted retranscription without requiring full document recreation.
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
- **REQ-4-100 Boundary Integrity:** UI pages/components must not access persistence directly and must call service APIs.
|
||||||
|
- **REQ-4-101 Service Ownership:** Aggregate writes must occur in owning service/workflow modules, not in UI handlers.
|
||||||
|
- **REQ-4-102 Deterministic Loading:** ORM relationship reads in service/UI code must use explicit eager loading compatible with `lazy="raise"`.
|
||||||
|
- **REQ-4-103 Async Safety:** Long-running provider calls must not block UI event handlers directly.
|
||||||
|
- **REQ-4-104 Evidence Durability:** Attempt evidence must survive process restart once the transaction commits.
|
||||||
|
- **REQ-4-105 Test Guardrails:** Architecture boundary tests must remain in place for services and UI boundaries.
|
||||||
|
|
||||||
|
## Traceability Notes
|
||||||
|
|
||||||
|
- Source of truth for status enums:
|
||||||
|
- `src/transcription/db/models.py`
|
||||||
|
- Source of truth for workflow transitions:
|
||||||
|
- `src/transcription/services/workflows.py`
|
||||||
|
- `src/transcription/services/jobs.py`
|
||||||
|
- Source of truth for attempt evidence writes:
|
||||||
|
- `src/transcription/services/sources.py`
|
||||||
|
|||||||
+97
-225
@@ -1,258 +1,130 @@
|
|||||||
# Database Schema (Version 4)
|
# Data Model and Persistence Schema (Version 4)
|
||||||
|
|
||||||
This document defines the relational schema for the document transcription system.
|
This schema reflects the current V4 persistence contract.
|
||||||
|
|
||||||
## Entity Relationship Diagram
|
## Entity Relationship Overview
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
DOCUMENT_TYPE {
|
Document ||--o{ Source : has
|
||||||
UUID id PK
|
Document ||--o{ Job : has
|
||||||
TEXT semantic_key UK
|
Document ||--o{ DocumentPerson : links
|
||||||
TEXT label
|
Person ||--o{ DocumentPerson : links
|
||||||
TEXT normalized_label
|
Job ||--o{ JobSource : includes
|
||||||
BOOLEAN is_active
|
Source ||--o{ JobSource : participates
|
||||||
TIMESTAMPTZ created_at
|
Source ||--o{ ExecutionAttempt : records
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
|
Document {
|
||||||
|
uuid id PK
|
||||||
|
string title
|
||||||
|
uuid type_id FK
|
||||||
|
string language
|
||||||
|
datetime doc_date
|
||||||
|
string date_note
|
||||||
|
string comments
|
||||||
|
string location
|
||||||
|
datetime created_at
|
||||||
|
datetime updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
PERSON_ROLE {
|
Source {
|
||||||
UUID id PK
|
uuid id PK
|
||||||
TEXT semantic_key UK
|
uuid document_id FK
|
||||||
TEXT label
|
string original_name
|
||||||
TEXT normalized_label
|
string media_type
|
||||||
BOOLEAN is_active
|
string storage_path
|
||||||
TIMESTAMPTZ created_at
|
int file_size
|
||||||
TIMESTAMPTZ updated_at
|
string file_hash
|
||||||
|
string raw_transcription
|
||||||
|
datetime created_at
|
||||||
|
datetime updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
PERSON {
|
Job {
|
||||||
UUID id PK
|
uuid id PK
|
||||||
TEXT full_name
|
uuid document_id FK
|
||||||
TEXT display_name
|
enum status
|
||||||
TEXT maiden_name
|
string prompt
|
||||||
DATE birth_date
|
json model_settings_json
|
||||||
TEXT birth_date_raw
|
datetime created_at
|
||||||
TEXT birth_place
|
datetime updated_at
|
||||||
DATE death_date
|
|
||||||
TEXT death_date_raw
|
|
||||||
TEXT death_place
|
|
||||||
TEXT biography
|
|
||||||
TEXT portrait_path
|
|
||||||
JSONB metadata
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DOCUMENT {
|
JobSource {
|
||||||
UUID id PK
|
uuid job_id FK
|
||||||
UUID document_type_id FK
|
uuid source_id FK
|
||||||
TEXT name
|
enum status
|
||||||
DATE document_date
|
uuid selected_attempt_id FK
|
||||||
TEXT document_date_raw
|
string error_message
|
||||||
TEXT location_created
|
datetime created_at
|
||||||
TEXT notes
|
datetime updated_at
|
||||||
TEXT archive_identifier
|
|
||||||
TIMESTAMPTZ created_at
|
|
||||||
TIMESTAMPTZ updated_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DOCUMENT_PERSON {
|
ExecutionAttempt {
|
||||||
UUID id PK
|
uuid id PK
|
||||||
UUID document_id FK
|
uuid source_id FK
|
||||||
UUID person_id FK
|
uuid job_id FK
|
||||||
UUID role_id FK
|
enum outcome
|
||||||
TIMESTAMPTZ created_at
|
string provider_name
|
||||||
TIMESTAMPTZ updated_at
|
string provider_model
|
||||||
|
string request_manifest_hash
|
||||||
|
json request_manifest_json
|
||||||
|
json transport_evidence_json
|
||||||
|
string transcript_text
|
||||||
|
string error_category
|
||||||
|
string error_message
|
||||||
|
float duration_seconds
|
||||||
|
datetime started_at
|
||||||
|
datetime completed_at
|
||||||
|
datetime created_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
|
## Authoritative Enumerations
|
||||||
|
|
||||||
### Page-Level Execution and AI Outputs
|
### JobStatus
|
||||||
|
|
||||||
- Every single page execution by an AI model produces a dedicated `JOB_SOURCE` record.
|
- `queued`
|
||||||
- Every `JOB` stores the frozen prompt identifier, prompt text, and hyperparameters used at submission time.
|
- `processing`
|
||||||
- `JOB_SOURCE.raw_api_response` is a compatibility projection containing an SDK-serialized OpenRouter response
|
- `transcribed`
|
||||||
snapshot. It is neither the exact HTTP body nor the native upstream-provider response.
|
- `completed` (legacy-compatible)
|
||||||
- Every new provider call creates an immutable `EXECUTION_ATTEMPT` containing the frozen request manifest,
|
- `partial_success`
|
||||||
exact OpenRouter-boundary response bytes when received, safe transport metadata, SDK snapshot, normalized
|
- `failed`
|
||||||
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
|
### JobSourceStatus
|
||||||
|
|
||||||
- `PROCESSING_ARTIFACT` stores provider-neutral versioned derived outputs.
|
- `pending`
|
||||||
- Exactly one of `inline_payload` and `external_reference` is populated.
|
- `transcribed`
|
||||||
- Externally stored artifacts use application-managed relative references and are verified by SHA-256 and byte size.
|
- `failed`
|
||||||
- Coordinate metadata declares units, origin, dimensions, and transformations when geometry is present.
|
- `cancelled`
|
||||||
- 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
|
## Aggregate Ownership
|
||||||
|
|
||||||
- Binary images are stored on disk; `SOURCE.file_path` stores the persisted path.
|
- `Document` aggregate: `Document`, linked `Source`, linked `DocumentPerson`.
|
||||||
- `SOURCE.file_hash` stores a SHA-256 digest.
|
- `Job` aggregate: `Job`, `JobSource` rows, selected-attempt pointers.
|
||||||
- `SOURCE.file_size_bytes` stores the original file size.
|
- Evidence aggregate: append-only `ExecutionAttempt` rows keyed by `source_id` + `job_id`.
|
||||||
|
|
||||||
### Page Ordering and Revisions
|
## Persistence Invariants
|
||||||
|
|
||||||
- `SOURCE.page_number` dictates page ordering within a document.
|
1. `ExecutionAttempt` rows are immutable after creation, except explicit support fields reserved for compatibility migrations.
|
||||||
- `SOURCE.raw_transcription` changes only through first-success selection or explicit candidate promotion.
|
2. `JobSource.status` is queue/projection state; it does not duplicate full attempt payload.
|
||||||
- `SOURCE.revised_text` stores human edits and is the preferred display value when present.
|
3. `Source.raw_transcription` is a projection, not the complete evidence record.
|
||||||
|
4. `Job` terminal status is derived from `JobSource` outcomes.
|
||||||
|
5. Registry semantic keys, when present, are immutable once created.
|
||||||
|
|
||||||
### Semantic Registry Governance
|
## Media Storage Semantics
|
||||||
|
|
||||||
- `DOCUMENT_TYPE.id` and `PERSON_ROLE.id` are the only relationship and public API identities.
|
1. `Source.storage_path` references canonical stored bytes used by processing.
|
||||||
- Nullable unique `semantic_key` values identify application-defined built-ins and are immutable after creation.
|
2. Canonical stored bytes may reflect ingest-time normalization.
|
||||||
- Semantic keys are internal and are never accepted from Settings or public relationship APIs.
|
3. File hash and size fields describe canonical stored bytes.
|
||||||
- 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
|
## Query and Loading Requirements
|
||||||
|
|
||||||
- Documents support zero or one relationship for each Person.
|
- Relationship access from service/UI layers must use explicit eager loading patterns compatible with `lazy="raise"`.
|
||||||
- Relationship roles are defined by `PERSON_ROLE` rather than hardcoded columns.
|
- Candidate-attempt views should select latest/selected attempts explicitly; do not rely on implicit lazy traversal.
|
||||||
- `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
|
## Cross-Reference
|
||||||
|
|
||||||
- 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 Architecture](architecture_v4.md)
|
||||||
- [System Requirements](requirements_v4.md)
|
- [System Requirements](requirements_v4.md)
|
||||||
- [Error Handling Policy](error_handling_v4.md)
|
- [Error Handling Policy](error_handling_v4.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user