lllin000_PaperForge/paperforge/core/state.py
Research Assistant 05629bce82 refactor: v2.1 contract hardening — PFResult unification, ErrorCode expansion, adapter cleanup, SyncService orchestration
Core contract:
- ErrorCode: 8→26 with _missing_() graceful degradation for unknown codes
- PFResult: +warnings/next_actions, PFError: +suggestions
- OcrStatus: 4→9 granular states (NONE/QUEUED/BLOCKED/NO_PDF/DONE_INCOMPLETE restored 1:1)
- field_registry.yaml: +owner/deprecated/replacement/enum values/default

Adapter cleanup:
- new core/io.py (read_json/write_json), core/date_utils.py (extract_year)
- new adapters/collections.py (build_collection_lookup)
- adapters/bbt.py: cut worker dependency, _private→public+alias
- worker/_utils.py + _domain.py: re-export from core instead of duplicate defs

Command unification:
- All 6 commands (sync/status/ocr/deep/repair/dashboard) → PFResult
- cli.py --json dest unified to 'json' (was json_output on status/doctor)
- commands/sync.py → SyncService (no more int/dict/PFResult mixing)

Service hardening:
- SyncService.run() orchestrates full sync lifecycle (select→index→clean)
- cleanup loops (orphaned records, flat notes) migrated from worker
- worker/sync.py: freeze line + services→worker one-way dependency
- plugin/main.js: formal-library.json fallback deprecation warning

Verification: 181/181 tests, 0 new lint errors
2026-05-09 22:20:07 +08:00

131 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""State machine enums and allowed transitions for PaperForge lifecycle."""
from __future__ import annotations
from enum import Enum
from typing import ClassVar
class OcrStatus(str, Enum):
"""OCR processing state for a paper.
States are granular so that plugin UI can display targeted next actions:
none OCR not yet initiated
pending do_ocr=true, not yet queued
queued in queue, waiting for worker
processing currently being OCR'd by PaddleOCR
done full OCR completed successfully
done_incomplete OCR completed but some pages/images missing
failed API returned error or task failed
blocked can't proceed (missing PDF, invalid key, etc.)
nopdf no PDF file available to OCR
"""
NONE = "none"
PENDING = "pending"
QUEUED = "queued"
PROCESSING = "processing"
DONE = "done"
DONE_INCOMPLETE = "done_incomplete"
FAILED = "failed"
BLOCKED = "blocked"
NO_PDF = "nopdf"
@classmethod
def from_legacy(cls, value: str) -> OcrStatus:
"""Map legacy OCR state strings to canonical enum (1:1, no compression)."""
mapping = {
"none": cls.NONE,
"pending": cls.PENDING,
"queued": cls.QUEUED,
"running": cls.PROCESSING,
"processing": cls.PROCESSING,
"done": cls.DONE,
"done_incomplete": cls.DONE_INCOMPLETE,
"failed": cls.FAILED,
"error": cls.FAILED,
"blocked": cls.BLOCKED,
"nopdf": cls.NO_PDF,
}
return mapping.get(value.strip().lower(), cls.PENDING)
def __str__(self) -> str:
return self.value
class PdfStatus(str, Enum):
"""PDF attachment health state."""
HEALTHY = "healthy"
BROKEN = "broken"
MISSING = "missing"
def __str__(self) -> str:
return self.value
class Lifecycle(str, Enum):
"""Derived lifecycle stage summarizing the paper's overall progress."""
PDF_READY = "pdf_ready"
OCR_READY = "ocr_ready"
ANALYZE_READY = "analyze_ready"
DEEP_READ_DONE = "deep_read_done"
ERROR_STATE = "error_state"
def __str__(self) -> str:
return self.value
class ALLOWED_TRANSITIONS:
"""Legal state migration tables.
Workers MUST validate transitions via check() before writing state changes.
"""
OCR_STATUS: ClassVar[dict[OcrStatus, list[OcrStatus]]] = {
OcrStatus.NONE: [OcrStatus.PENDING, OcrStatus.QUEUED, OcrStatus.NO_PDF],
OcrStatus.PENDING: [OcrStatus.QUEUED, OcrStatus.PROCESSING, OcrStatus.NO_PDF, OcrStatus.BLOCKED],
OcrStatus.QUEUED: [OcrStatus.PROCESSING, OcrStatus.FAILED],
OcrStatus.PROCESSING: [OcrStatus.DONE, OcrStatus.DONE_INCOMPLETE, OcrStatus.FAILED],
OcrStatus.DONE: [OcrStatus.PENDING], # re-run
OcrStatus.DONE_INCOMPLETE: [OcrStatus.PENDING], # re-run full or retry partial
OcrStatus.FAILED: [OcrStatus.PENDING], # retry
OcrStatus.BLOCKED: [OcrStatus.PENDING], # fix preconditions then retry
OcrStatus.NO_PDF: [OcrStatus.PENDING], # add PDF then retry
}
DEEP_READING_STATUS: ClassVar[dict[str, list[str]]] = {
"pending": ["done"],
"done": ["pending"], # re-run with --force
}
LIFECYCLE: ClassVar[dict[Lifecycle, list[Lifecycle]]] = {
Lifecycle.PDF_READY: [Lifecycle.OCR_READY, Lifecycle.ERROR_STATE],
Lifecycle.OCR_READY: [Lifecycle.ANALYZE_READY, Lifecycle.ERROR_STATE],
Lifecycle.ANALYZE_READY: [Lifecycle.DEEP_READ_DONE, Lifecycle.ERROR_STATE],
Lifecycle.DEEP_READ_DONE: [Lifecycle.PDF_READY, Lifecycle.ERROR_STATE], # re-run
Lifecycle.ERROR_STATE: [Lifecycle.PDF_READY], # recover
}
@staticmethod
def check_ocr(current: OcrStatus, target: OcrStatus) -> tuple[bool, str]:
"""Validate OCR status transition."""
allowed = ALLOWED_TRANSITIONS.OCR_STATUS.get(current, [])
if target in allowed:
return True, ""
return False, (
f"Illegal OCR transition: {current.value} -> {target.value}. "
f"Allowed from '{current.value}': {[s.value for s in allowed]}"
)
@staticmethod
def check_lifecycle(current: Lifecycle, target: Lifecycle) -> tuple[bool, str]:
"""Validate lifecycle transition."""
allowed = ALLOWED_TRANSITIONS.LIFECYCLE.get(current, [])
if target in allowed:
return True, ""
return False, (
f"Illegal lifecycle transition: {current.value} -> {target.value}. "
f"Allowed from '{current.value}': {[s.value for s in allowed]}"
)