docling-studio/document-parser/domain/exceptions.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

37 lines
1.1 KiB
Python

"""Domain-level exceptions.
Exceptions defined in this module are raised by domain operations and value
objects when an invariant is violated. They have no infrastructure
dependencies and are safe to import from any layer.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from domain.value_objects import DocumentLifecycleState
class DomainError(Exception):
"""Base class for domain-level errors. Catch this when wiring API
layers if you want a single hook for any invariant violation."""
class InvalidLifecycleTransitionError(DomainError):
"""Raised when a Document.transition_to() call asks for a (source,
target) pair that is not in the allowed transition table.
Carries `source` and `target` so callers can produce a useful error
message without re-discovering them.
"""
def __init__(
self,
*,
source: DocumentLifecycleState,
target: DocumentLifecycleState,
) -> None:
super().__init__(f"Invalid document lifecycle transition: {source.value} -> {target.value}")
self.source = source
self.target = target