docs(legibility): add 18 per-domain README.md files (DOC-6)
Backend (9): document, person, tag, user, geschichte, notification, ocr, audit, dashboard. Frontend (8): document, person, tag, user, geschichte, notification, ocr, shared. OCR service (1): ocr-service/README.md. Each README covers: what the domain owns, explicit non-ownership, public surface (verified by grep against the codebase), internal layout, and cross-domain dependencies. Closes #400 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# audit
|
||||
|
||||
Append-only event store for all domain mutations. Every write across the application produces an `audit_log` row. The activity feed and Family Pulse dashboard aggregate from this table.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Table: `audit_log` (append-only by convention — no UPDATE or DELETE in application code).
|
||||
Features: log mutations, query activity feed, query per-entity history.
|
||||
|
||||
**Admission criteria (why this is cross-cutting, not a Tier-1 domain):** consumed by 5+ domains; has no user-facing CRUD of its own; the data model is fixed (event log, not a business entity).
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
Nothing beyond the log table. `audit/` is an infrastructure layer, not a business domain.
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `logAfterCommit(event)` | document, person, user, ocr, geschichte | Record a mutation event after the DB transaction commits |
|
||||
|
||||
`logAfterCommit` is the only write-path. Query paths (`AuditLogQueryService`) are consumed by `dashboard/` and the activity feed route.
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `AuditService` — `logAfterCommit()` (write)
|
||||
- `AuditLogQueryService` — query by entity, by user, for the activity feed
|
||||
- `AuditLog` (entity) → table `audit_log`
|
||||
- `AuditLogRepository`
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
None. `audit/` is consumed by other domains; it does not call out to any of them.
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
No direct frontend counterpart. Audit data surfaces in the `activity/` and `conversation/` frontend domains via the dashboard API.
|
||||
@@ -0,0 +1,37 @@
|
||||
# dashboard
|
||||
|
||||
Stats aggregation for the admin dashboard and the Family Pulse widget. This is a derived domain — it has no tables of its own; all data is computed on-the-fly from Tier-1 domain data.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
No entities. Routes: `/api/dashboard/*`, `/api/stats/*`.
|
||||
Features: document counts, person counts, publication stats, weekly activity data, incomplete-document list, enrichment queue, Family Pulse widget data, admin statistics.
|
||||
|
||||
**Admission criteria (cross-cutting):** aggregates from 3+ domains; no owned entities.
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
None of the underlying data — it reads from `document/`, `person/`, `audit/`, `notification/`, `geschichte/`.
|
||||
|
||||
## Public surface
|
||||
|
||||
`dashboard/` is a leaf domain — no other domain calls its services. It is the aggregator, not the aggregated.
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `StatsController` — REST under `/api/stats`
|
||||
- `DashboardController` — REST under `/api/dashboard`
|
||||
- `StatsService` — aggregated counts (documents, persons, geschichten, incomplete, etc.)
|
||||
- `DashboardService` — activity feed composition, Family Pulse data
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `DocumentService.count()` / `findWeeklyStats()` / `findReadyToReadQueue()` / `findSegmentationQueue()` — document metrics
|
||||
- `PersonService.count()` / `findAll()` / `findAllFamilyMembers()` — person metrics
|
||||
- `AuditLogQueryService.findRecentActivity()` — activity feed events
|
||||
- `NotificationService.getNotifications()` — per-user notifications for the activity feed
|
||||
- `GeschichteService.list()` / `count()` — story metrics
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
Activity feed and Pulse widget are assembled in `frontend/src/lib/shared/dashboard/` and in the `aktivitaeten` route; no dedicated `dashboard/` lib folder.
|
||||
@@ -0,0 +1,50 @@
|
||||
# document
|
||||
|
||||
The archive's core concept. A `Document` represents one physical artefact (a letter, a postcard, a photo) stored in MinIO and described by metadata.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entities: `Document`, `DocumentVersion`, `TranscriptionBlock`, `DocumentAnnotation`, `DocumentComment`.
|
||||
Features: document CRUD, file upload/download, full-text search, bulk editing, transcription workflows, annotation canvas, threaded comments, thumbnail generation (PDFBox).
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- `Person` (sender / receivers) — referenced by ID, resolved via `PersonService`
|
||||
- `Tag` — referenced by ID; the join is on the document side but tags are owned by `tag/`
|
||||
- `AppUser` — comments reference `AppUser` IDs, but user management lives in `user/`
|
||||
- OCR processing — `ocr/` orchestrates jobs; `ocr-service/` executes them
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `getDocumentById(UUID)` | ocr, notification | Fetch a single document |
|
||||
| `getDocumentsByIds(List<UUID>)` | ocr | Bulk fetch for OCR job |
|
||||
| `findByOriginalFilename(String)` | importing | Deduplication during mass import |
|
||||
| `deleteTagCascading(UUID tagId)` | tag | Remove a tag from all documents before deleting it |
|
||||
| `findWeeklyStats()` | dashboard | Activity data for Family Pulse widget |
|
||||
| `count()` | dashboard | Total document count for stats |
|
||||
| `addTrainingLabel(...)` | ocr | Attach a confirmed sender label to a document |
|
||||
| `findSegmentationQueue()` / `findTranscriptionQueue()` / `findReadyToReadQueue()` | ocr | OCR pipeline queues |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `DocumentController` — REST under `/api/documents`
|
||||
- `DocumentService` — CRUD, search (JPA Specifications), bulk edit
|
||||
- `DocumentRepository` — includes bidirectional conversation-thread query
|
||||
- `DocumentSpecifications` — composable `Specification` predicates for search
|
||||
- `DocumentVersionService` / `DocumentVersionRepository` — append-only version history
|
||||
- `ThumbnailService` + `ThumbnailAsyncRunner` — PDFBox thumbnail generation (separate thread pool)
|
||||
- Sub-packages: `annotation/`, `comment/`, `transcription/`
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `PersonService.getById()` / `getAllById()` — resolve sender and receivers
|
||||
- `TagService.expandTagNamesToDescendantIdSets()` — tag filter expansion
|
||||
- `FileService.uploadFile()` / `downloadFile()` / `generatePresignedUrl()` — S3 I/O
|
||||
- `NotificationService.notifyMentions()` / `.notifyReply()` — comment mentions
|
||||
- `AuditService.logAfterCommit()` — every mutation is audited
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/document/README.md`
|
||||
@@ -0,0 +1,38 @@
|
||||
# geschichte
|
||||
|
||||
Family stories — curated narrative pieces that weave together persons, documents, and commentary into a publishable article. German: *Geschichte* (story / history).
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entity: `Geschichte`.
|
||||
Lifecycle: `DRAFT → PUBLISHED` (only published stories are visible to non-authors).
|
||||
Features: story CRUD, rich-text editing with person and document cross-references, publish/unpublish toggle, comment thread (shared component from `shared/discussion/`).
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- `Person` or `Document` records — stories reference them by ID. Deleting a Person or Document does not cascade to Geschichte.
|
||||
- Comment storage — shared comment infrastructure is in `document/comment/` (or `shared/discussion/` on the frontend).
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `getById(UUID)` | notification | Resolve story context in mention notifications |
|
||||
| `list(...)` | dashboard | Recent stories for the activity feed |
|
||||
| `count()` | dashboard | Published story count for stats |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `GeschichteController` — REST under `/api/geschichten`
|
||||
- `GeschichteService` — CRUD, publish lifecycle
|
||||
- `GeschichteRepository` — list by status, author
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `PersonService.getById()` / `getAllById()` — resolve person references in story body
|
||||
- `DocumentService.getDocumentsByIds()` — resolve document references in story body
|
||||
- `AuditService.logAfterCommit()` — story mutations are audited
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/geschichte/README.md`
|
||||
@@ -0,0 +1,40 @@
|
||||
# notification
|
||||
|
||||
In-app messages delivered in real time via SSE and persisted in the bell-icon dropdown. Notifications are created by other domains in response to events (comment mentions, replies).
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entity: `Notification`, `NotificationPreference`.
|
||||
Features: create and deliver notifications, unread count, mark-read, SSE real-time push, per-user delivery preferences.
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- `AppUser` (recipient) — owned by `user/`
|
||||
- `Document` or `Geschichte` (notification context) — referenced by ID only
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `notifyMentions(mentionedUserIds, contextUrl)` | document (comment) | Push mention notifications when a comment contains @mentions |
|
||||
| `notifyReply(parentComment, reply)` | document (comment) | Push reply notification to the thread author |
|
||||
| `countUnread(userId)` | user session | Unread badge count in the nav bar |
|
||||
| `getNotifications(userId)` | dashboard / activity | Notification list for bell dropdown |
|
||||
| `markRead(id)` / `markAllRead(userId)` | notification controller | User-driven read-state updates |
|
||||
| `updatePreferences(userId, dto)` | notification controller | Per-user delivery preferences |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `NotificationController` — REST under `/api/notifications`
|
||||
- `NotificationService` — create, query, mark-read
|
||||
- `SseEmitterRegistry` — runtime-stateful component that keeps one `SseEmitter` per connected user. On `notifyMentions()` / `notifyReply()`, the service writes to `SseEmitterRegistry` to push real-time events. SSE connections go **backend → browser directly**, not via the SvelteKit SSR layer.
|
||||
- `NotificationRepository` — persisted notification rows
|
||||
- `NotificationPreferenceRepository` — per-user preference rows
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- None inbound (this domain is a consumer, not a provider for business logic)
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/notification/README.md`
|
||||
@@ -0,0 +1,44 @@
|
||||
# ocr
|
||||
|
||||
OCR/HTR pipeline orchestration. This domain manages job lifecycle and result ingestion — it does **not** perform OCR. Actual text recognition runs in the Python `ocr-service/` container (port 8000, internal network only).
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entities: `OcrJob`, `OcrJobDocument`, `SenderModel`.
|
||||
Features: start OCR jobs, track job lifecycle (`PENDING → RUNNING → DONE / FAILED`), stream transcription blocks back into `document/transcription/`, sender-model training, segmentation training.
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- Document content — `Document` and `TranscriptionBlock` are owned by `document/`
|
||||
- File storage — presigned MinIO URLs are generated by `filestorage/FileService` and passed to the OCR service
|
||||
- OCR processing — the Python `ocr-service/` executes Surya (typewritten) and Kraken (Kurrent/Sütterlin HTR) and streams results back
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `startOcr(documentId, ...)` | document | Trigger an OCR job for a document |
|
||||
| `getJob(UUID)` | document | Fetch job status |
|
||||
| `getDocumentOcrStatus(UUID)` | document | Per-document OCR status summary |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `OcrController` — REST under `/api/ocr`
|
||||
- `OcrService` — job creation, presigned URL generation, result ingestion
|
||||
- `OcrBatchService` — batch job workflows
|
||||
- `OcrAsyncRunner` — `@Async` execution of OCR jobs
|
||||
- `OcrTrainingService` — calls `/train` and `/segtrain` on the Python service (protected by `X-Training-Token` header)
|
||||
- `OcrJobRepository` / `OcrJobDocumentRepository`
|
||||
- `SenderModelRepository` — trained sender-recognition models
|
||||
- `OcrClient` (interface) / `RestClientOcrClient` — HTTP client for the Python OCR service; mockable for tests
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `DocumentService.getDocumentById()` / `getDocumentsByIds()` — resolve target documents
|
||||
- `DocumentService.addTrainingLabel()` — attach confirmed sender labels after training
|
||||
- `FileService.generatePresignedUrl()` — generate MinIO presigned URLs passed to the OCR service (PDF bytes never flow through the backend)
|
||||
- `AuditService.logAfterCommit()` — OCR job events are audited
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/ocr/README.md`
|
||||
@@ -0,0 +1,45 @@
|
||||
# person
|
||||
|
||||
Historical individuals referenced by documents. A `Person` is a family member who appears as a sender or receiver in the archive — they are never login accounts.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entities: `Person`, `PersonNameAlias`, `PersonRelationship`.
|
||||
Features: person CRUD, name alias management, person merge (deduplication), family-member designation, relationship graph, person type classification (FAMILY, CORRESPONDENT, INSTITUTION).
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- `AppUser` — login accounts are in `user/`. A `Person` record has no login credentials. The separation is deliberate: a historical family member from 1905 is never a system user.
|
||||
- Document content — `Person` records are referenced by documents (as sender/receiver), not the other way around.
|
||||
- Relationship rendering — the Stammbaum view is derived by the frontend from `PersonRelationship` data.
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `getById(UUID)` | document, geschichte, ocr | Fetch one person by ID |
|
||||
| `getAllById(List<UUID>)` | document | Bulk fetch for sender/receiver resolution |
|
||||
| `findAll()` | document, dashboard | List all persons |
|
||||
| `findByName(String)` | document | Typeahead search |
|
||||
| `findOrCreateByAlias(String, PersonType)` | importing | Idempotent create during mass import |
|
||||
| `findAllFamilyMembers()` | dashboard | Family member list for stats |
|
||||
| `findCorrespondents()` | document | Correspondent list for conversation filter |
|
||||
| `count()` | dashboard | Total person count for stats |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `PersonController` — REST under `/api/persons`
|
||||
- `PersonService` — CRUD, merge, alias management, family-member designation
|
||||
- `PersonRepository` — sorted list, name search
|
||||
- `PersonNameAlias` / `PersonNameAliasRepository` — alternative name spellings
|
||||
- `PersonNameParser` / `PersonTypeClassifier` — name parsing utilities
|
||||
- `PersonSummaryDTO` — lightweight DTO for typeahead / list views
|
||||
- Sub-package: `relationship/` — `PersonRelationship`, `RelationshipService`, `RelationshipController`
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `AuditService.logAfterCommit()` — person mutations are audited
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/person/README.md`
|
||||
@@ -0,0 +1,35 @@
|
||||
# tag
|
||||
|
||||
Hierarchical document categories. Tags form a tree via a self-referencing `parent_id` column and are applied to documents for filtering and browse navigation.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entity: `Tag` (self-referencing `parent_id` tree).
|
||||
Features: tag CRUD, hierarchical deletion (cascade to descendants), tag typeahead, admin tag management (rename, reparent, merge).
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- Documents — the `document_tags` join table is on the document side. `Tag` does not hold document references.
|
||||
- Tag assignment — adding/removing a tag from a document is handled by `DocumentService`.
|
||||
|
||||
## Public surface (called from other domains)
|
||||
|
||||
| Method | Consumer | Purpose |
|
||||
|---|---|---|
|
||||
| `delete(UUID)` | document | Remove a tag; triggers `DocumentService.deleteTagCascading()` first |
|
||||
| `deleteWithDescendants(UUID)` | admin tag UI | Recursive subtree deletion |
|
||||
| `expandTagNamesToDescendantIdSets(List<String>)` | document | Expand tag filter to include descendant tags |
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `TagController` — REST under `/api/tags`
|
||||
- `TagService` — CRUD, hierarchy traversal, cascade-delete coordination
|
||||
- `TagRepository` — find-or-create by name (case-insensitive), subtree queries
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
None. Documents reference tags; tags do not reference documents or other domains.
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/tag/README.md`
|
||||
@@ -0,0 +1,35 @@
|
||||
# user
|
||||
|
||||
Login accounts and permission groups. An `AppUser` is a system user who can authenticate and act in the application — they are never a historical family member.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Entities: `AppUser`, `UserGroup`, password-reset tokens, invite tokens.
|
||||
Features: user CRUD, group CRUD, password change, password reset flow, invite links.
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
- `Person` records — historical family members. An `AppUser` is never linked to a `Person`. This separation is intentional: a person who digitized letters in 2024 is not the same entity as their great-grandmother who wrote them in 1912. See `docs/GLOSSARY.md`.
|
||||
- Permission enforcement — `security/` owns `@RequirePermission` and `PermissionAspect`. `user/` only manages which permissions are stored on `UserGroup`.
|
||||
|
||||
## Public surface
|
||||
|
||||
`UserService` methods are consumed primarily by the security infrastructure and the admin UI. No other business-logic domain calls `UserService` directly.
|
||||
|
||||
The Spring Security chain (via `CustomUserDetailsService` in `security/`) calls `AppUserRepository.findByUsername()` on every authenticated request.
|
||||
|
||||
## Internal layout
|
||||
|
||||
- `UserController` — REST under `/api/users` (current user, CRUD)
|
||||
- `AuthController` — password reset, invite flow
|
||||
- `UserService` — BCrypt-encoded passwords, group assignment
|
||||
- `AppUserRepository` — find by username (used by Spring Security)
|
||||
- `UserGroupRepository` — group and permission management
|
||||
|
||||
## Cross-domain dependencies
|
||||
|
||||
- `AuditService.logAfterCommit()` — user-management mutations are audited
|
||||
|
||||
## Frontend counterpart
|
||||
|
||||
`frontend/src/lib/user/README.md`
|
||||
Reference in New Issue
Block a user