docling-studio/document-parser/domain/events.py
Pier-Jean Malandrino c878ee5f3b feat(#202): introduce Document lifecycle state machine
Adds a first-class lifecycle state to every document, distinct from
AnalysisJob.status. The lifecycle describes the document as a whole and
is the foundation for the doc-centric pivot in 0.6.0.

Domain
- DocumentLifecycleState enum (Uploaded/Parsed/Chunked/Ingested/Stale/Failed)
- Document.lifecycle_state and lifecycle_state_at fields
- Document.transition_to() validates against a transition table
  (domain/lifecycle.py) and returns a DocumentLifecycleChanged event
- InvalidLifecycleTransitionError on disallowed transitions

Persistence
- ALTER TABLE documents to add the two columns (default 'Uploaded')
- New index idx_documents_lifecycle_state for filter perf
- _COLUMN_MIGRATIONS refactored to support multiple tables
- _POST_MIGRATION_DDL list for indexes on freshly-added columns
- SqliteDocumentRepository.update_lifecycle()

Services
- AnalysisService drives transitions on parse / chunk / re-chunk / fail
  via _transition_document(); idempotent and resilient (logs WARN and
  continues if a stale state is somehow encountered)

API
- DocumentResponse exposes lifecycleState + lifecycleStateAt
  (additive — existing 'status' field kept for backwards compat)

Frontend
- Document type extended with lifecycleState and lifecycleStateAt
- DocumentLifecycleState union literal mirroring the backend enum

Tests
- 24 new tests in test_lifecycle.py covering transitions, idempotency,
  invariant preservation, and event emission
- test_repos.py: round-trip + every-enum-value check + update_lifecycle
- test_chunking.py: rechunk path now mocks document_repo correctly

Refs #202
2026-04-29 15:34:25 +02:00

35 lines
1 KiB
Python

"""Domain events — frozen records that document state transitions.
Events are produced by domain operations (typically returned from a
mutation method on an aggregate). They are pure data — no event bus is
wired in 0.6.0; services can choose to log, persist, or publish them
later. Keeping them here keeps the domain layer free of any event-bus
infrastructure.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import datetime
from domain.value_objects import DocumentLifecycleState
@dataclass(frozen=True)
class DocumentLifecycleChanged:
"""A Document lifecycle transition occurred.
Attributes:
document_id: id of the document that transitioned.
previous: state the document was in before the transition.
current: state the document is in after the transition.
at: timestamp of the transition (UTC).
"""
document_id: str
previous: DocumentLifecycleState
current: DocumentLifecycleState
at: datetime