generated from john/python-template
This commit is contained in:
@@ -17,9 +17,17 @@ applyTo: 'src/transcription/services/*.py'
|
||||
- **A service module must not import another service module.** This is enforced by
|
||||
[test_service_boundaries](../../tests/test_service_boundaries.py). Shared types go in a
|
||||
neutral module that defines no service class (see [errors](../../src/transcription/services/errors.py)).
|
||||
- Not every module in this package is a service. Helper modules that define no `*Service`
|
||||
class (`base`, `errors`, `normalization`, `prompts`, `quality`, `media_storage`,
|
||||
`source_media`) are free-function modules and are exempt from the service rules below.
|
||||
- Not every module in this package is a service. Modules fall into three kinds:
|
||||
- **Aggregate services** own models and define a `*Service` class: `documents.py`, `sources.py`,
|
||||
`jobs.py`, `people.py`, `photos.py`, `maintenance.py`, and `evidence.py` (read/projection only,
|
||||
owns nothing).
|
||||
- **Orchestration modules** define no service class and compose writes across aggregates:
|
||||
`store.py`, `workflows.py`. They are the sanctioned place to create or delete rows owned by more
|
||||
than one service — see [Service Composition](#service-composition).
|
||||
- **Shared infrastructure and free-function helpers** are exempt from the service rules below:
|
||||
`base.py` (`ServiceBase`), `registry.py` (`RegistryService`, a generic base for lookup tables —
|
||||
not an aggregate owner itself), `unit_of_work.py`, `errors.py`, `normalization.py`, `prompts.py`,
|
||||
`quality.py`, `media_storage.py`, `source_media.py`. `__init__.py` exposes `ServiceBundle`.
|
||||
- Cross-cutting error behavior must follow
|
||||
[error-handling instructions](./error-handling.instructions.md).
|
||||
|
||||
@@ -30,11 +38,37 @@ is the only service that may **create or delete** its rows.
|
||||
|
||||
| Model | Owner |
|
||||
| --- | --- |
|
||||
| `Document`, `DocumentType` | `DocumentService` |
|
||||
| `Document`, `DocumentType`, `DocumentTag` | `DocumentService` |
|
||||
| `Source`, `JobSource` | `SourceService` |
|
||||
| `Job` | `JobService` |
|
||||
| `Person`, `PersonRole`, `DocumentPerson` | `PeopleService` |
|
||||
| `Person`, `PersonRole`, `DocumentPerson`, `PersonTag` | `PeopleService` |
|
||||
| `Photo` | `PhotosService` |
|
||||
| `MaintenanceRun` | `MaintenanceService` |
|
||||
| `ExecutionAttempt` | `SourceService` |
|
||||
| `Tag` | shared — see below |
|
||||
|
||||
Keep this table complete: every table in `src/transcription/db/models.py` appears exactly once,
|
||||
except `Tag`. When you add a model, add its owner here in the same change.
|
||||
|
||||
### `Tag` is deliberately shared
|
||||
|
||||
`Tag` is one table reached through two `RegistryService[Tag]` facades that differ only in the
|
||||
reference model they count usage through: `TagRegistry` (`documents.py`, via `DocumentTag`) and
|
||||
`PersonTagRegistry` (`people.py`, via `PersonTag`). Both create and delete `Tag` rows through the
|
||||
generic registry. This is the single sanctioned exception to one-owner-per-model — do not "fix" it by
|
||||
assigning `Tag` to one service, because the other facade would then be creating rows it does not own.
|
||||
Any change to `Tag` semantics, labels, or normalization must be validated against **both** facades
|
||||
and the junction table each one counts.
|
||||
|
||||
`DocumentTag` and `PersonTag` follow the junction rule below: each is created and deleted only by the
|
||||
service on its own side.
|
||||
|
||||
### Registries
|
||||
|
||||
`DocumentTypeRegistry`, `TagRegistry`, `PersonRoleRegistry`, and `PersonTagRegistry` are
|
||||
`RegistryService` subclasses, not independent services. A registry belongs to the aggregate service
|
||||
whose module declares it and shares that service's ownership. Registry CRUD uses `<operation>_entry`
|
||||
naming (see [CRUD Methods](#crud-methods)).
|
||||
|
||||
### Junction tables
|
||||
|
||||
@@ -50,8 +84,14 @@ lifecycle owner. The service on the other side may read through the junction (vi
|
||||
Two consequences follow, and both are deliberate:
|
||||
|
||||
- **Cascade deletion is not a violation.** A service deleting the aggregate root it owns
|
||||
may delete junction rows referencing that root, because they cannot outlive it
|
||||
(`JobService.delete_job_with_guardrails`).
|
||||
may delete rows referencing that root which cannot outlive it
|
||||
(`JobService.delete_job_with_guardrails` deletes the job's `job_source` rows).
|
||||
- **Evidence deletion is an explicit workflow, not a runtime path.** `JobService.delete_job_and_evidence`
|
||||
deletes `ExecutionAttempt` rows owned by `SourceService`. That is sanctioned because it is the
|
||||
named retention workflow that `delete_job_with_guardrails` refuses to perform implicitly — that
|
||||
method *blocks* deletion when attempts exist. Append-only means runtime code never rewrites or
|
||||
removes history to represent a new outcome; it does not forbid a deliberate, operator-invoked
|
||||
retention operation. Do not add a second path that deletes attempts.
|
||||
- **Ownership governs creation and deletion, not every state transition.** `job_source` is
|
||||
both a link and the transcription work queue. `JobService.cancel_job` and
|
||||
`resubmit_failed_sources` transition `job_source.status` across a whole job, because that
|
||||
|
||||
Reference in New Issue
Block a user