Commit graph

97 commits

Author SHA1 Message Date
Pier-Jean Malandrino
995e891d9b feat: orchestrated ingestion pipeline Docling → embedding → OpenSearch
Closes #72
2026-04-10 21:12:11 +02:00
Pier-Jean Malandrino
a21daa24da feat: add embedding microservice and EmbeddingService port
Closes #71
2026-04-10 20:53:24 +02:00
Pier-Jean Malandrino
9cffb2a9a7 feat(infra): add OpenSearch adapter implementing VectorStore port
Closes #70
2026-04-10 20:48:50 +02:00
Pier-Jean Malandrino
a111a5009f feat(domain): add VectorStore port and SearchResult value object
Closes #69
2026-04-10 20:40:17 +02:00
Pier-Jean Malandrino
b968ea230e feat(architecture): define vector index metadata schema
Closes #68
2026-04-10 20:35:03 +02:00
Pier-Jean Malandrino
80b0c44e8c feat(chunking): soft-delete chunk with confirmation dialog
Closes #90
2026-04-10 20:28:24 +02:00
Pier-Jean Malandrino
c74a3277b8 feat(chunking): inline chunk text editing
Closes #89
2026-04-10 19:37:29 +02:00
Pier-Jean Malandrino
d8937ad916 fix: remove dead store and no-effect await flagged by CodeQL
- local_converter.py: remove redundant `_default_converter = None` in
  except block of `_ensure_default_converter` (variable was already None,
  re-raised immediately — dead store)
- test_analysis_service.py: replace bare `await task` with
  `await asyncio.gather(task)` to satisfy static analysis
2026-04-10 14:37:51 +02:00
Pier-Jean Malandrino
6d0c4dd192
Merge pull request #144 from scub-france/fix/decoupling-audit
fix(decoupling): eliminate cross-feature imports & typed health endpoint
2026-04-10 14:04:33 +02:00
Pier-Jean Malandrino
3d199cb783 fix(clean-code): English mode strings, SRP extraction, rename getter
- Replace French mode strings (configurer/verifier/preparer) with English
  equivalents (configure/verify/prepare) in StudioPage.vue and tests
- Extract _build_conversion_options, _run_conversion, _finalize_analysis
  from _run_analysis_inner to respect Single Responsibility Principle
- Rename _get_default_converter to _ensure_default_converter to reflect
  its lazy-init side effect

Closes #136, closes #137, closes #138
2026-04-10 13:45:13 +02:00
Pier-Jean Malandrino
a82db25e59
Merge pull request #135 from scub-france/fix/ddd-audit
fix(domain): guard clauses & frozen value objects
2026-04-10 13:42:54 +02:00
Pier-Jean Malandrino
3a09c18c59 fix(decoupling): eliminate cross-feature imports and add typed health endpoint
Frontend decoupling:
- Create shared/appConfig.ts as reactive bridge (locale, maxFileSizeMb,
  maxPageCount) eliminating shared→features and feature→feature imports
- Give history feature its own Pinia store and API layer (was re-export
  of analysis store)
- Give chunking feature its own Pinia store and API layer (was importing
  from analysis)
- ChunkPanel receives analysis data via props instead of cross-feature
  store import
- document/store reads maxFileSizeMb from shared config instead of
  importing feature-flags store
- shared/i18n reads locale from shared config instead of importing
  settings store

Backend:
- Add HealthResponse Pydantic schema for /api/health endpoint

Closes #140, closes #141, closes #142, closes #143
2026-04-10 13:11:29 +02:00
Pier-Jean Malandrino
f9a1c56309 fix(domain): add guard clauses and frozen value objects
Add state-machine guard clauses to AnalysisJob transition methods
(mark_running, mark_completed, mark_failed, update_progress) to prevent
invalid status transitions. Make all domain value objects immutable with
frozen=True. Add 11 tests covering guard clause behavior.

Closes #132, closes #133
2026-04-10 12:04:44 +02:00
Pier-Jean Malandrino
9907b7f040 fix(arch): inject repositories and extract domain logic
- Add DocumentRepository and AnalysisRepository protocols in domain/ports.py (#128)
- Refactor persistence repos from module functions to SqliteDocumentRepository
  and SqliteAnalysisRepository classes
- Inject repos into AnalysisService and new DocumentService class via
  constructor, removing direct imports of persistence and infra.settings (#129)
- Move _merge_results, _classify_error, _extract_html_body to domain/services.py (#130)
- Update main.py composition root to build and wire all dependencies
- Switch api/documents.py to Depends pattern matching api/analyses.py
- Update all tests to use injected mocks instead of module-level patches

Closes #128, closes #129, closes #130
2026-04-10 11:44:33 +02:00
Pier-Jean Malandrino
d810fda333 fix: preserve batch progress on analysis completion
Re-read the job from DB before mark_completed so that
progress_current/progress_total written during batched conversion
are not overwritten by the stale in-memory object.

Add regression unit test and e2e assertion on final progress values.
2026-04-09 15:51:36 +02:00
Pier-Jean Malandrino
cfc5bb5c35 feat: add E2E API tests with Karate V2
Set up a full E2E test suite (39 scenarios) using Karate against
the real API stack. Hybrid architecture: domain-based features +
cross-domain workflows, with data-driven testing and callable helpers.

Structure:
- e2e/pom.xml: Maven + karate-core 1.5
- 3 helpers (upload, analyze+poll, cleanup)
- 3 JSON schemas (health, document, analysis)
- 12 feature files across health, documents, analyses, workflows
- Tags: @smoke (2), @regression (35), @e2e (2)
- generate-test-data.py: fpdf2-based PDF generation (no binaries)

Also adds:
- RATE_LIMIT_RPM env var to make rate limiter configurable (0=disabled)
- CI job e2e with needs: [backend, frontend]
- e2e/ in .dockerignore

Closes #119
2026-04-08 13:47:03 +02:00
Pier-Jean Malandrino
704ba550d4 feat: make document max size configurable via MAX_FILE_SIZE_MB env var
Replace hardcoded 5 MB upload limit with a configurable setting.
Backend exposes the value via /api/health, frontend reads it
dynamically for validation and UI messages.

Closes #48
2026-04-08 10:27:38 +02:00
Pier-Jean Malandrino
459a720e77 fix: add pypdfium2 to requirements.txt for CI
pypdfium2 is imported in analysis_service for PDF page counting
(batch feature #56) but was missing from requirements.txt, causing
CI collection errors on all 4 test modules.
2026-04-08 10:14:39 +02:00
Pier-Jean Malandrino
fc866ce229 feat: batch large documents with page_range and progress reporting
Use Docling's native page_range parameter to split large PDFs into
sequential batches, preventing memory exhaustion and timeouts.
Progress is reported via existing polling mechanism.

Closes #56
2026-04-07 17:54:40 +02:00
Pier-Jean Malandrino
af10c200f0 fix: cancel running conversion when analysis job is deleted (#64)
Track background tasks per job ID. On delete, cancel the task to release
the converter lock instead of letting phantom jobs run to completion.
2026-04-07 17:07:55 +02:00
Pier-Jean Malandrino
302b24cc4c fix: route _on_task_done errors through _classify_error (#63)
Unhandled task exceptions now produce user-friendly messages instead of
raw Python tracebacks in the error_message field.
2026-04-07 17:06:16 +02:00
Pier-Jean Malandrino
8ea0cebc16 fix: validate timeout cascade ordering in Settings (#62)
Enforce document_timeout < lock_timeout < conversion_timeout at startup.
Prevents inverted timeout configurations that cause unpredictable behavior.
2026-04-07 17:04:09 +02:00
Pier-Jean Malandrino
1657bce1f0 fix: make converter lock timeout configurable via LOCK_TIMEOUT env var (#61)
Replace hardcoded _LOCK_TIMEOUT=300 with settings.lock_timeout,
readable from LOCK_TIMEOUT environment variable.
2026-04-07 17:01:10 +02:00
Pier-Jean Malandrino
2c254382c8 fix: add __post_init__ validation to Settings dataclass (#65)
Reject invalid configuration at startup: negative timeouts, zero
concurrency, bad table mode. Reports all errors at once.
2026-04-07 16:59:37 +02:00
Pier-Jean Malandrino
f04e5369ef style: use single import style for infra.local_converter in tests
Use `import infra.local_converter as lc_mod` consistently instead of
mixing `import` and `from ... import` for the same module.

Addresses CodeQL review comment on PR #58.
2026-04-07 15:39:02 +02:00
Pier-Jean Malandrino
6177452de1 test: add robustness tests for pipeline failure scenarios
25 tests covering all backend robustness fixes:
- TestClassifyError (9): user-friendly error message mapping
- TestDefaultTableMode (4): settings.default_table_mode injection
- TestDocumentTimeout (2): document_timeout wired into pipeline
- TestConverterLockTimeout (3): lock timeout + release guarantees
- TestConvertSyncLimits (4): max_num_pages/max_file_size forwarding
- TestGetDefaultConverterReset (3): converter singleton reset on failure

Ref #57
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
c281b6c551 fix: classify pipeline errors into user-friendly messages (M1)
Raw PyTorch/Docling stack traces are no longer shown to the user.
Common failures (missing compiler, OOM, lock contention, corrupted
document) are mapped to actionable messages. Unknown errors are
truncated to 200 chars.

Ref #57 (M1)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
f89dc51661 fix: reset _default_converter on init failure (H5)
If the lazy-init of the default converter fails (e.g. model download
error), the singleton was left as None but subsequent calls would not
retry. Now the failed state is cleared so the next request retries.

Ref #57 (H5)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
f58b563a13 fix: make default table_mode configurable via DEFAULT_TABLE_MODE (H1)
TableFormerMode.ACCURATE is very expensive on CPU (~3-5x slower).
The default can now be set to "fast" on resource-constrained
environments (HF Spaces) via the DEFAULT_TABLE_MODE env var.

User-specified table_mode in the request still takes precedence.

Ref #57 (H1)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
6327b13614 fix: pass max_num_pages and max_file_size to Docling convert (C3)
Defense-in-depth: even if upload validation passes, Docling itself
now enforces page count and file size limits. Configurable via
MAX_PAGE_COUNT and MAX_FILE_SIZE env vars (0 = unlimited).

Ref #57 (C3)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
c0fb128718 fix: replace _converter_lock with timeout-based acquisition (C2)
A frozen conversion holding the lock indefinitely blocks all subsequent
jobs. Using lock.acquire(timeout=300) fails fast with a clear error
instead of waiting forever.

Ref #57 (C2)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
4c76101142 fix: add document_timeout to PdfPipelineOptions (C1)
Docling's native document_timeout is the only mechanism that can
interrupt processing inside a blocked thread (OCR, table extraction).
Without it, asyncio.wait_for cannot stop a frozen conversion.

Configurable via DOCUMENT_TIMEOUT env var (default: 120s).

Closes #57 (C1)
2026-04-07 15:32:38 +02:00
Pier-Jean Malandrino
05ed49b893 Add issue templates, GitHub stars badge, fix health endpoint type hint 2026-04-07 11:57:29 +02:00
Pier-Jean Malandrino
09145206d2 fix: limit upload to 5 MB / 20 pages max and increase conversion timeout
Prevents PyTorch/Docling pipeline crashes on HF Spaces CPU by:
- Reducing max file size from 50 MB to 5 MB
- Adding configurable MAX_PAGE_COUNT setting (env var, default unlimited)
- Increasing conversion timeout from 600s to 900s
- Adding frontend upload validation with explicit error messages
- Exposing maxPageCount via /api/health for dynamic UI hints
2026-04-07 10:56:16 +02:00
Pier-Jean Malandrino
ee57b04f84 Add disclaimer banner for shared deployments (HuggingFace)
Feature-flagged via DEPLOYMENT_MODE env var and /api/health endpoint.
Disabled by default (self-hosted), enabled when deploymentMode=huggingface.
2026-04-04 21:06:38 +02:00
Pier-Jean Malandrino
1c82a80389 Guard all docling-dependent tests for lightweight CI
local_converter imports docling at module level, so any test that
touches it crashes when docling is not installed. Skip those tests
with importorskip / skipif so the CI (which only has docling-core)
passes cleanly.
2026-04-03 15:46:29 +02:00
Pier-Jean Malandrino
d8f87c34bc Skip pipeline options tests when docling is not installed
The docling library (torch, transformers) is too heavy for lightweight
CI environments that only install docling-core. Use importorskip so
these tests are cleanly skipped instead of crashing collection.
2026-04-03 15:42:14 +02:00
Pier-Jean Malandrino
fefd406dc8 Align tests with schema refactoring (AliasChoices, status codes)
Adapt test expectations to external changes: upload returns 200,
ValueError yields 400, and schemas now accept both snake_case and
camelCase via AliasChoices.
2026-04-03 14:21:30 +02:00
Pier-Jean Malandrino
503b7e8a40 Fix import ordering violations (ruff I001)
Reorder first-party imports in local_converter and local_chunker to
satisfy isort rules: domain imports before infra imports.
2026-04-03 14:05:38 +02:00
Pier-Jean Malandrino
a9299e2735 Add document_service tests for upload, preview, and deletion
Cover PDF validation, file size limits, preview generation, page
counting, file deletion with path traversal protection, and the
not-found case — all previously untested code paths.
2026-04-03 13:57:05 +02:00
Pier-Jean Malandrino
d089bb8670 Add in-memory rate limiting middleware
Lightweight sliding-window per-IP rate limiter (100 req/min default)
with no external dependency. Health endpoint is excluded. Returns 429
with Retry-After header when exceeded. Sufficient for single-process
SQLite deployments; document the Redis upgrade path for scale.
2026-04-03 13:56:05 +02:00
Pier-Jean Malandrino
c82fd66ffc Add docstrings to all public methods
Domain model state transitions, repository CRUD operations, and service
methods now have descriptive docstrings to lower the contribution
barrier for an open-source project.
2026-04-03 13:54:52 +02:00
Pier-Jean Malandrino
8ee470f1aa Add database ping to health check endpoint
Health endpoint now verifies SQLite connectivity and reports a
"degraded" status when the database is unreachable, instead of
blindly returning "ok".
2026-04-03 13:53:01 +02:00
Pier-Jean Malandrino
360ea7ea8f Stream upload reads in 64 KB chunks
Read uploaded files in fixed-size chunks instead of a single file.read()
to reduce peak memory pressure. Also enforces the size limit during
streaming so oversized payloads are rejected before fully buffered.
2026-04-03 13:52:32 +02:00
Pier-Jean Malandrino
c2e91262c6 Limit concurrent analyses with asyncio.Semaphore
Unbounded asyncio.create_task calls could exhaust CPU and memory on
modest hardware. Add a configurable semaphore (MAX_CONCURRENT_ANALYSES,
default 3) so excess jobs queue instead of running all at once.
2026-04-03 13:51:53 +02:00
Pier-Jean Malandrino
7bd7d0f793 Validate page bounds on preview endpoint
Reject requests where page exceeds the document page count with a clear
400 error, instead of letting pdf2image fail with an opaque exception.
2026-04-03 13:50:32 +02:00
Pier-Jean Malandrino
661568f2cb Add return type annotations and use 201 for upload
All endpoint functions, lifespan context manager, and get_connection now
have explicit return types. Upload endpoint returns 201 Created instead
of 200 to follow REST conventions.
2026-04-03 13:50:07 +02:00
Pier-Jean Malandrino
69ce1df5a1 Narrow exception handlers for better diagnostics
Replace broad `except Exception` with specific types (FileNotFoundError,
PermissionError, OSError) so errors are properly categorized in logs.
Users reporting bugs will get actionable messages instead of generic ones.
2026-04-03 13:48:33 +02:00
Pier-Jean Malandrino
c50106c185 Centralize config through Settings singleton
UPLOAD_DIR and DB_PATH were read directly from os.environ, bypassing
the Settings dataclass. This caused an inconsistency where overriding
Settings had no effect on these values. Now all modules import from
infra.settings.settings.
2026-04-03 13:47:53 +02:00
Pier-Jean Malandrino
ab6d42aecd Move bbox.py from domain/ to infra/ and unify coordinate logic
domain/ must be pure with no external dependencies. bbox.py imports
docling_core and belongs in infra/. Also refactor ServeConverter to
use the canonical to_topleft_list via BoundingBox instead of
duplicated manual coordinate conversion. Move docling-core to base
requirements since it is now needed in both modes.
2026-04-03 13:17:26 +02:00