# ADR: Extract OCR / Markdown / Search into a shared `teamhub_core` engine Status: accepted (staged) · 2026-06-15 · scope: `legacy-knowledge-indexer` + `TeamHUB/Engines` ## Контекст Платформа (08_DECISIONS D10, CONVENTIONS «Shared engines», plans/LegacyHUB.md §4) требует вынести проверенный пайплайн LegacyHUB (OCR → Markdown → hybrid search) в переиспользуемое **stateless** ядро, чтобы QMS-Hub и MailHUB потребляли его, а не дублировали. LegacyHUB остаётся эталоном и первым потребителем. ## Решение Ядро `teamhub_core` живёт в `C:\Users\manag\TeamHUB\Engines\engines/teamhub_core/` (НЕ в этом репозитории, НЕ в `TeamHUB-Platform`) и потребляется как pinned versioned dependency / git submodule (D10). Извлечение делается поэтапно, чтобы не дестабилизировать работающий эталон; на каждом шаге тесты зелёные. ### Границы ядра (stateless) | Пакет ядра | Источник в LegacyHUB | Содержимое | |---|---|---| | `ingest` | `app/ingestion/{ocr,docling_extractor,chunker,normalizer,quality,table_processor,figure_processor}.py` | OCRmyPDF, Docling, chunker, quality-flags, нормализация (сохранение ГОСТ/ID) | | `search` | `app/indexing/{embeddings,reranker,hybrid_search,opensearch_client,qdrant_client}.py` | BGE-M3, reranker, `run_search`=RRF(BM25,dense)+rerank, bootstrap индексов | | `storage` | `app/storage/{minio_client,artifacts}.py` | put/get + retry, `ensure_artifact` | | `contracts` | search/citation Pydantic-типы | контракт поиска/цитирования | | `obs` | `app/common/json_logger.py` | JSON-логи с маскированием (уже app-agnostic) | Персистентность (`app/db/*`, Alembic), эмиссия событий, имена бакетов/индексов/ коллекций и REST-роуты ОСТАЮТСЯ в модуле. ## Что уже сделано для расцепления (этот репозиторий) - **Имена параметризованы.** `opensearch_index_chunks`, `qdrant_collection_chunks`, все `minio_bucket_*` читаются из `Settings` (env-aliases). Дефолты приведены к платформенному стандарту (`teamhub-legacyhub-*`); потребитель (QMS/MailHUB) переопределяет их через env, не трогая код. Жёстко вшитых `legacyhub-*` имён в логике больше нет. - **`run_search` не зависит от БД.** `app/indexing/hybrid_search.py` импортирует только контракты (`app/api/schemas`), `Settings` и клиентов индексации — слой поиска уже stateless по отношению к Postgres/ORM. - **`obs`-ядро готово.** `app/common/json_logger.py` написан без app-specific импортов и переносится в ядро как есть. - **Плейтменеджмент секретов вынесен из дефолтов** (см. `_enforce_secret_policy`). ## Остаточные шаги (staged, координируются с QMS/MailHUB) 1. Создать `Engines/engines/teamhub_core/` с README (public API/CLI, inputs/outputs, profile/config model, команда проверки, release notes) и версионным тегом (`teamhub-core-v0.1.0`). 2. Перенести `contracts` (типы поиска/цитирования) из `app/api/schemas` в `teamhub_core.contracts`; `app/api/schemas` ре-экспортирует их для совместимости. 3. Перенести `search`/`ingest`/`storage`/`obs` в ядро; `run_search` принимает имена индекса/коллекции/бакетов аргументами/конфигом, а не глобальным `Settings` модуля. 4. Переключить LegacyHUB на ядро как pinned submodule; `app/indexing`, `app/ingestion` становятся тонкими адаптерами. Временная vendored-копия — только с задокументированным сроком удаления. 5. Контракт-тесты ядра единым сьютом (chunker/quality/hybrid/hashing/duplicates). 6. Подключить QMS-Hub (свои бакеты `teamhub-qms-*`, индекс `qms_chunks`, своя PG) и MailHUB (поиск по письмам/вложениям) как вторых потребителей. ## Последствия - Физический вынос затрагивает три репозитория (LegacyHUB, Engines, потребители) и выполняется отдельной скоординированной работой; в этом проходе закрыта **готовность к расцеплению** (параметризация имён, stateless `run_search`, app-agnostic obs-core) и зафиксирован план. - Риск регрессий минимизируется поэтапностью и зелёным тест-сьютом на каждом шаге. См. также `docs/ADR-layers.md` (маппинг слоёв) и `docs/dispatch-contract.md`.