Ниже даю ТЗ прямо текстом, без файлов.

# ТЗ: 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”.


