Ниже даю ТЗ прямо текстом, без файлов. # ТЗ: Router / Orchestration Fix для бухгалтерского ассистента ## 1. Контекст этапа Ontology/mapping слой после semantic-v2 ремонта вытащен из красной зоны: `semantic_coverage_pct` вырос с `61.1917` до `94.9279`, `unknown_links` снизились с `1016` до `102`, `relation_types_total` вырос с `1` до `25`, `covered_entity_classes` — с `33` до `42`, `source_id_unknown` ушёл в `0`. Regression tests: `17 passed`. При этом orchestration/router слой остаётся узким местом. В benchmark по 35 вопросам: * `route_mismatch_count = 7` * `degraded_answers_count = 0` * `batch_route_count = 0` * `store_feature_risk` используется 19 раз * `live_mcp_drilldown` — 4 раза * `hybrid_store_plus_live` — 4 раза * `store_canonical` — 8 раз. Остаточные промахи распределены так: * `heavy_analytical = 4` * `cross_entity = 2` * `drilldown_explain = 1`. Основная confusion matrix: * `batch_refresh_then_store -> store_feature_risk: 4` * `hybrid_store_plus_live -> store_canonical: 2` * `live_mcp_drilldown -> hybrid_store_plus_live: 1`. Сама policy уже описывает правильную целевую логику: * exact object trace → `live_mcp_drilldown` * simple factual in loaded slice → `store_canonical` * trend/anomaly/risk → `store_feature_risk` * heavy whole-slice with freshness gap → `batch_refresh_then_store` * low confidence fallback → `hybrid_store_plus_live`. Проблема текущего этапа: policy на бумаге правильная, но runtime-router выбирает route слишком грубо и слишком рано downcast’ит сложные запросы в store-only ветки. --- ## 2. Цель этапа Довести router/orchestration слой до состояния, в котором: 1. heavy whole-slice запросы реально уходят в `batch_refresh_then_store`, а не в `store_feature_risk`; 2. cross-entity causal/join запросы не downcast’ятся в `store_canonical`; 3. точечный drilldown по конкретному объекту стабильно уходит в `live_mcp_drilldown`; 4. появляется прозрачный decision log по каждому route-решению; 5. `batch_refresh_then_store` перестаёт быть декларацией и начинает реально исполняться в runtime. --- ## 3. Scope работ В scope этапа входят: * refactor query classifier / route selector; * внедрение decision flags; * внедрение store sufficiency checker; * внедрение explicit route guards; * реализация runtime handoff для `batch_refresh_then_store`; * внедрение route explanation logging; * повторный benchmark-run на тех же 35 вопросах. В scope не входят: * новый overhaul ontology/mapping слоя; * новый redesign canonical model; * расширение snapshot domain beyond June-2020; * тяжёлая оптимизация latency сверх baseline этого этапа. Ontology уже починен до уровня, достаточного для данного этапа; оставшиеся unknown-поля точечные и не являются основным блокером для router-фазы. --- ## 4. Проблемы, которые надо исправить ### 4.1 Heavy analytical downcast Сейчас 4 heavy-вопроса, которые должны идти в `batch_refresh_then_store`, реально уходят в `store_feature_risk`. Это касается whole-slice heavy analytical запросов, включая: * `Полный риск-срез за июнь` * `Рейтинг риск-счетов` * `Рейтинг риск-контрагентов` * `Company anomaly summary`. Это противоречит policy, где для `heavy_analytical` задан приоритет `batch_refresh_then_store -> feature_store -> risk_store`. ### 4.2 Cross-entity downcast Два cross-entity вопроса ожидают `hybrid_store_plus_live`, но реально уходят в `store_canonical`: * `Свяжи документы покупателей и проводки` * `Свяжи контрагентов, договоры и проводки`. Это означает, что router недостаточно хорошо распознаёт chain-join / causal cross-entity shape и преждевременно считает, что canonical store уже достаточен. ### 4.3 Drilldown boundary bug Один drilldown вопрос ожидает `live_mcp_drilldown`, но реально идёт в `hybrid_store_plus_live`. Это означает, что граница между exact object trace и mixed live/store explain сейчас определена недостаточно жёстко. ### 4.4 Отсутствие реально работающего batch runtime `batch_route_count = 0`, хотя benchmark содержит heavy-вопросы, ожидающие `batch_refresh_then_store`. Это прямо подтверждает, что batch-маршрут не активируется как реальный runtime-path. --- ## 5. Требуемая архитектура решения ### 5.1 Query classifier v2 Нужно внедрить отдельный слой decision classification перед выбором маршрута. Требуемый интерфейс: ```python def classify_query_for_route( question_text: str, parsed_intent: dict, store_metadata: dict, ) -> RouteDecisionFlags: ... ``` Classifier обязан вычислять минимум такие flags: ```python @dataclass class RouteDecisionFlags: needs_exact_object_trace: bool needs_causal_chain: bool needs_cross_entity_join: bool needs_full_period_aggregation: bool needs_ranking: bool needs_anomaly_summary: bool needs_runtime_truth: bool freshness_sensitive: bool ambiguous_object_scope: bool store_sufficiency_confident: bool precomputed_aggregate_available: bool ``` Назначение: отделить semantic classification вопроса от конечного route selection. Сейчас они, по сути, схлопнуты, из-за чего router переоценивает store sufficiency и недооценивает heavy/cross-entity требования. Это видно по текущим mismatch’ам. --- ## 6. Правила классификации ### 6.1 `live_mcp_drilldown` Запрос должен классифицироваться в `live_mcp_drilldown`, если одновременно выполняется: * запрос относится к exact object trace, posting chain, source-of-record или object-level why/explain; * объект явно указан или легко нормализуется до конкретного документа/проводки/строки/регистра/субконто; * ответ требует runtime evidence, а не только store summary. Это согласуется с policy: “exact object trace or posting chain -> live_mcp_drilldown”. Триггеры: * “почему именно эта проводка” * “источник этой строки” * “цепочка документ -> проводки -> субконто” * “почему выбрано это субконто” * “документ по номеру и его ссылка”. ### 6.2 `hybrid_store_plus_live` Запрос должен классифицироваться в `hybrid_store_plus_live`, если: * требуется cross-entity join между 3+ сущностями; * требуется causal stitching по нескольким источникам; * store сам по себе не гарантирует достаточную explainability; * но запрос не является exact object trace в узком смысле. Триггеры: * документ ↔ проводки * контрагент ↔ договор ↔ проводки * регистр ↔ первичный документ * explain через движения без указания одного точечного source-of-record. ### 6.3 `store_canonical` Разрешён только для: * simple factual within loaded slice; * материализованных и однозначных canonical facts; * cross-entity вопросов без causal stitching и без runtime evidence need. Запрещён для: * chain joins; * “свяжи X и Y через Z”; * source-of-record explanations; * heavy full-period aggregations. ### 6.4 `store_feature_risk` Разрешён только для: * trend / anomaly / risk вопросов; * ambiguous fuzzy risk/tax questions; * heavy-вопросов только при явном наличии готового и свежего precomputed aggregate. Не должен быть default-веткой для heavy_analytical whole-slice вопросов. ### 6.5 `batch_refresh_then_store` Обязателен для heavy whole-slice вопросов, если: * нужен full-period aggregation; * нужен ranking; * нужен company-wide anomaly/risk summary; * нужна свежесть выше текущего store snapshot; * нет подтверждённого precomputed aggregate нужного уровня. --- ## 7. Store sufficiency checker Нужно реализовать отдельный модуль проверки достаточности store перед выбором маршрута. Требуемый интерфейс: ```python def check_store_sufficiency( question_shape: RouteDecisionFlags, store_metadata: dict, ) -> StoreSufficiencyResult: ... ``` Минимальная модель результата: ```python @dataclass class StoreSufficiencyResult: canonical_sufficient: bool feature_sufficient: bool risk_sufficient: bool freshness_ok: bool aggregate_level_ok: bool ranking_ready: bool explanation_ready: bool reason_codes: list[str] ``` Checker обязан учитывать: * покрытие периода; * наличие нужной группировки; * наличие precomputed ranking aggregate; * наличие risk/anomaly aggregate нужного уровня; * freshness snapshot; * достаточность store для explainability. Ключевая цель — убрать ложные решения вида: * “store_feature_risk и так хватит” * “store_canonical и так хватит” в тех кейсах, где store на самом деле не покрывает shape запроса. Именно это сейчас ломает heavy и cross-entity маршруты. --- ## 8. Explicit route guards После classifier и sufficiency-checker route должен выбираться не scoring’ом “в среднем”, а guard-правилами. Требуемая логика: ```python def choose_route(flags: RouteDecisionFlags, suff: StoreSufficiencyResult) -> str: if flags.needs_exact_object_trace: return "live_mcp_drilldown" if flags.needs_full_period_aggregation or flags.needs_ranking or flags.needs_anomaly_summary: if not (flags.precomputed_aggregate_available and suff.freshness_ok and suff.aggregate_level_ok): return "batch_refresh_then_store" if flags.needs_cross_entity_join and flags.needs_causal_chain: if not suff.explanation_ready: return "hybrid_store_plus_live" if suff.feature_sufficient and not flags.needs_runtime_truth and ( flags.needs_anomaly_summary or parsed_as_trend_or_risk ): return "store_feature_risk" if suff.canonical_sufficient and not flags.needs_causal_chain: return "store_canonical" return "hybrid_store_plus_live" ``` Смысл: * drilldown имеет жёсткий приоритет; * heavy whole-slice с ranking/summary и недостаточным aggregate обязан уйти в batch; * causal cross-entity не имеет права downcast’иться в `store_canonical`; * fallback остаётся `hybrid_store_plus_live`. --- ## 9. Runtime batch handoff Нужно реализовать реальный handoff для `batch_refresh_then_store`. Требуемый поток: 1. Router выбирает `batch_refresh_then_store`. 2. Создаётся orchestration job с типом `refresh_and_answer`. 3. Job: * проверяет актуальность snapshot/feature/risk stores; * при необходимости пересчитывает refresh/features/risk; * фиксирует run ids; * передаёт управление answer synthesis. 4. Answer synthesis читает уже обновлённые stores и формирует ответ. 5. Route/log сохраняют факт batch execution. Минимальный payload job: ```json { "job_type": "refresh_and_answer", "question_id": "Q28", "slice_window": "2020-06", "requested_outputs": ["feature_store", "risk_store"], "reason": ["needs_full_period_aggregation", "needs_ranking", "aggregate_not_sufficient"] } ``` Обязательное требование: `batch_refresh_then_store` должен стать исполняемым runtime-path, а не декларацией в policy. Сейчас это не так, что видно по `batch_route_count = 0`. --- ## 10. Route explanation logging Нужно добавить прозрачный лог принятия маршрута. Требуемый формат: ```python @dataclass class RouteDecisionLog: question_id: str question_text: str parsed_class: str decision_flags: dict sufficiency_snapshot: dict candidate_routes: list[str] rejected_routes: dict[str, str] chosen_route: str execution_mode: str batch_job_id: str | None ``` Пример лог-записи: ```json { "question_id": "Q30", "parsed_class": "heavy_analytical", "decision_flags": { "needs_full_period_aggregation": true, "needs_ranking": false, "needs_anomaly_summary": true, "needs_runtime_truth": false }, "sufficiency_snapshot": { "feature_sufficient": false, "risk_sufficient": false, "freshness_ok": false, "aggregate_level_ok": false }, "rejected_routes": { "store_feature_risk": "aggregate_not_sufficient", "store_canonical": "wrong_query_shape" }, "chosen_route": "batch_refresh_then_store" } ``` Это нужно для последующего дебага benchmark mismatch’ов. Иначе команда и дальше будет видеть только конечный маршрут, но не причину выбора. --- ## 11. Конкретные ошибки benchmark, которые должны быть закрыты ### 11.1 Heavy Нужно закрыть как минимум: * Q26 `Полный риск-срез за июнь` * Q27 `Рейтинг риск-счетов` * Q28 `Рейтинг риск-контрагентов` * Q30 `Company anomaly summary` Ожидаемый результат: эти вопросы должны идти в `batch_refresh_then_store`, а не в `store_feature_risk`. ### 11.2 Cross-entity Нужно закрыть: * Q11 `Свяжи документы покупателей и проводки` * Q12 `Свяжи контрагентов, договоры и проводки` Ожидаемый результат: `hybrid_store_plus_live`, а не `store_canonical`. ### 11.3 Drilldown Нужно закрыть 1 mismatch по границе `live_mcp_drilldown` / `hybrid_store_plus_live`. Ожидаемый результат: точечный object-trace вопрос не должен уходить в hybrid, если вопрос явно указывает на source-of-record / posting chain. --- ## 12. Предлагаемая структура модулей ### Вариант минимальной структуры `router/query_classifier.py` * `classify_query_for_route()` * правила выделения decision flags `router/store_sufficiency.py` * `check_store_sufficiency()` * оценка достаточности canonical / feature / risk stores `router/route_selector.py` * `choose_route()` * explicit route guards `orchestration/batch_runtime.py` * `enqueue_refresh_and_answer_job()` * `run_refresh_and_answer_job()` `router/decision_log.py` * `build_route_decision_log()` * сериализация route decision trace `tests/test_router_decision_flags.py` * флаги для benchmark-вопросов `tests/test_store_sufficiency.py` * sufficiency cases `tests/test_route_guards.py` * guard logic `tests/test_batch_runtime_handoff.py` * batch execution path --- ## 13. Требования к тестам Нужно добавить unit + integration тесты. ### Unit tests Покрыть: * correct flag extraction for heavy queries; * correct flag extraction for drilldown queries; * correct flag extraction for cross-entity causal queries; * store sufficiency false/true cases; * route guards for each route; * batch handoff payload building. ### Integration tests Покрыть: * heavy query without ready aggregate → `batch_refresh_then_store` * heavy query with ready fresh aggregate → `store_feature_risk` * cross-entity causal query → `hybrid_store_plus_live` * exact object trace → `live_mcp_drilldown` * simple factual in loaded slice → `store_canonical` Отдельно нужно прогнать benchmark subset по вопросам: * Q06–Q12 * Q26–Q30 потому что именно там сейчас сосредоточены остаточные mismatch’и. --- ## 14. Acceptance criteria Этап считается принятым, если после повторного benchmark-run достигнуты условия: * `route_mismatch_count <= 2` вместо текущих `7` * `heavy_analytical mismatches = 0` * `cross_entity mismatches = 0` * `drilldown_explain mismatches <= 1`, целевое — `0` * `batch_route_count > 0` * `degraded_answers_count = 0` * `adopt_with_improvements` сохраняется минимум, желательно с качественным улучшением рекомендаций * route decision logs сохраняются для всех 35 benchmark-вопросов. Латентность допускается немного выше baseline по heavy-вопросам, так как batch route по профилю дороже: у него baseline retrieval около `1240 ms` против `190 ms` у `store_feature_risk`. Но рост должен быть локализован только на heavy-path и не должен ломать обычные store-first сценарии. --- ## 15. Нефункциональные требования Нужно сохранить текущие плюсы системы: * `store-first retrieval policy`; * bounded context; * отсутствие деградированных ответов; * отсутствие uncapped heavy live scans. Запрещается: * заменять batch на тяжёлый live-scan; * насильно переводить все cross-entity вопросы в live; * ломать простые store-only factual ответы; * убирать fallback `hybrid_store_plus_live`. --- ## 16. Порядок реализации Сначала сделать classifier v2 и decision flags. Потом — store sufficiency checker. После этого — route guards. Затем — runtime handoff для batch. Потом — decision logging. И только после этого — повторный benchmark-run и точечная подстройка порогов по остаточным кейсам. Такой порядок минимизирует расползание архитектуры и даёт сразу диагностируемую систему. --- ## 17. Ожидаемый результат этапа После выполнения этого ТЗ система должна перейти из состояния: * “ontology уже хорошая, но router маршрутизирует местами по упрощённой логике” в состояние: * “ontology + router + orchestration согласованы между собой, а route choice воспроизводим, объясним и исполняется реально, включая batch”.