# V2RetrievalPolicyResolver Architecture

## 1. Роль компонента

`V2RetrievalPolicyResolver` это deterministic bridge между `V2IntentRouter` и docs-RAG retrieval.

Компонент работает поверх уже готового `V2RouteResult` и не делает повторную интерпретацию пользовательского текста:

- не вызывает LLM;
- не меняет `intent` и `subintent`;
- не ранжирует документы;
- не собирает evidence.

Его задача: собрать один `RetrievalPlan` с полями:

- `profile`
- `layers`
- `limit`
- `filters`

## 2. Зависимости

Актуальная реализация опирается на:

- `src/app/core/agent/processes/v2/retrieval/policy_resolver.py`
- `src/app/core/agent/processes/v2/anchor_signals.py`
- `src/app/core/agent/processes/v2/models.py`
- `src/app/core/rag/contracts/enums.py`
- `src/app/core/agent/processes/v2/retrieval/v2_rag_adapter.py`
- `src/app/core/rag/retrieval/session_retriever.py`
- `src/app/core/rag/persistence/repository.py`
- `src/app/core/rag/persistence/query_repository.py`
- `src/app/core/rag/persistence/retrieval_statement_builder.py`

## 3. Входной контракт

Resolver использует:

- `route.intent`
- `route.subintent`
- `route.anchors.entity_names`
- `route.anchors.file_names`
- `route.anchors.endpoint_paths`
- `route.anchors.target_doc_hints`
- `route.anchors.matched_aliases`
- `route.anchors.process_domain`
- `route.anchors.process_subdomain`

`route.target_terms` в текущей реализации profile/filter branching не влияет.

## 4. Верхнеуровневый branching

`resolve(route)` имеет три ветки:

1. `GENERAL_QA` -> `general_qa_grounded_summary`
2. `FIND_FILES` -> `file_lookup`
3. иначе -> docs summary branch

Инварианты:

- `GENERAL_QA` всегда остаётся general profile;
- `FIND_FILES` всегда остаётся `file_lookup`;
- resolver всегда возвращает один валидный `RetrievalPlan`.

## 5. Внутренняя декомпозиция

Текущая реализация разбита на два helper-класса.

### `_AnchorTermCollector`

Собирает термы для `prefer_like_patterns`.

Источники:

- basename из `target_doc_hints`
- `endpoint_paths`
- `file_names`
- `entity_names`
- `matched_aliases`
- `process_domain`
- `process_subdomain`

Все значения нормализуются в lower-case и превращаются в SQL-like patterns вида `"%term%"`.

Для `FIND_FILES` действует отдельное правило:

- если есть `target_doc_hints`, `prefer_like_patterns` строится только по basename hints;
- иначе используется общий набор collected terms.

### `_RouteFilterBuilder`

Собирает `filters` для трёх веток:

- `general_filters(route)`
- `summary_filters(route)`
- `find_files_filters(route)`

Дополнительно содержит path selection:

- `_summary_prefixes(route)`
- `_find_files_prefixes(route)`
- `_find_files_prefer_prefixes(route)`

## 6. Signal detection

Summary profile и часть path preferences зависят от `anchor_signal_types(route)`.

Сигналы вычисляются так:

- `FIND_FILES`
  - если `route.subintent == FIND_FILES`
- `API_ENDPOINT`
  - если есть `endpoint_paths`
  - или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются маркеры `"/api/"`, `"api"`, `"endpoint"`
- `ARCHITECTURE`
  - если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/architecture/"`, `"architecture"`, `"arch"`
- `LOGIC_FLOW`
  - если в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/logic/"`, `"logic"`, `"workflow"`, `"flow"`, `"process"`
- `DOMAIN_ENTITY`
  - если есть `entity_names`
  - или в `target_doc_hints` / `file_names` / `matched_aliases` встречаются `"/domains/"`, `"domain"`, `"entity"`, `"component"`

Важно:

- `process_domain` и `process_subdomain` сейчас **не участвуют** в signal detection;
- они влияют только на filters и `prefer_like_patterns`.

## 7. Summary profile selection

Метод `_summary_profile(route)` использует:

- `meaningful = anchor_signal_types(route) - {FIND_FILES}`

Правило:

- если meaningful signal не ровно один -> `docs_summary_generic`
- если ровно один:
  - `API_ENDPOINT` -> `docs_summary_api_endpoint`
  - `ARCHITECTURE` -> `docs_summary_architecture`
  - `LOGIC_FLOW` -> `docs_summary_logic_flow`
  - `DOMAIN_ENTITY` -> `docs_summary_domain_entity`

Следствие:

- конфликт API + architecture -> generic;
- API + entity -> generic;
- weak/no signals -> generic.

## 8. Profiles, layers, limits

### `general_qa_grounded_summary`

- condition: `route.intent == GENERAL_QA`
- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`

### `file_lookup`

- condition: `route.subintent == FIND_FILES`
- layers: `[D1_DOCUMENT_CATALOG, D3_ENTITY_CATALOG]`
- limit: `12`

### `docs_summary_api_endpoint`

- layers: `[D1_DOCUMENT_CATALOG, D2_FACT_INDEX, D0_DOC_CHUNKS]`
- limit: `8`

### `docs_summary_logic_flow`

- layers: `[D4_WORKFLOW_INDEX, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`

### `docs_summary_domain_entity`

- layers: `[D3_ENTITY_CATALOG, D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`

### `docs_summary_architecture`

- layers: `[D1_DOCUMENT_CATALOG, D5_RELATION_GRAPH, D0_DOC_CHUNKS]`
- limit: `8`

### `docs_summary_generic`

- layers: `[D1_DOCUMENT_CATALOG, D0_DOC_CHUNKS]`
- limit: `8`

## 9. Filters by branch

### General branch

`general_filters(route)` возвращает:

- `prefer_path_prefixes = ["docs/architecture/", "docs/"]`
- `prefer_like_patterns = ["%readme.md%", "%overview%"]`
- `target_doc_hints = list(route.anchors.target_doc_hints)`

Это обзорный, но не узкий plan: hard `path_prefixes` здесь нет.

### Summary branch

`summary_filters(route)` всегда включает:

- `target_doc_hints`
- `metadata.domain`, если есть `process_domain`
- `metadata.subdomain`, если есть `process_subdomain`
- `prefer_path_prefixes`
- `prefer_like_patterns`

Дополнительно:

- если есть `API_ENDPOINT` signal, добавляется hard `path_prefixes = ["docs/api/", "docs/"]`

`prefer_path_prefixes` для summary:

- API -> `["docs/api/", "docs/"]`
- ARCHITECTURE -> `["docs/architecture/", "docs/"]`
- LOGIC_FLOW -> `["docs/logic/", "docs/architecture/", "docs/"]`
- DOMAIN_ENTITY -> `["docs/domains/", "docs/", "docs/api/"]`
- empty signals -> `["docs/"]`

Если сигналов несколько, prefixes объединяются и dedupe-ятся с сохранением порядка.

### FIND_FILES branch

`find_files_filters(route)` всегда включает:

- `target_doc_hints`
- `metadata.domain`, если есть `process_domain`
- `metadata.subdomain`, если есть `process_subdomain`
- `path_prefixes`
- `prefer_path_prefixes`
- `prefer_like_patterns`

`path_prefixes` для `FIND_FILES` выбираются по приоритету:

1. директории из `target_doc_hints`
2. директории из `file_names`, если путь начинается с `docs/`
3. signal-based fallback:
   - API -> `["docs/api/", "docs/"]`
   - ARCHITECTURE -> `["docs/architecture/", "docs/"]`
   - LOGIC_FLOW -> `["docs/logic/", "docs/"]`
   - DOMAIN_ENTITY -> `["docs/domains/", "docs/"]`
4. default -> `["docs/"]`

`prefer_path_prefixes` для `FIND_FILES`:

- начинается с `path_prefixes`
- если есть `process_domain` или `process_subdomain`, дополнительно добавляет:
  - `"docs/domains/"`
  - `"docs/logic/"`

## 10. Hard и soft сигналы в текущей реализации

В терминах текущего кода:

Hard-ish / narrowing filters:

- `path_prefixes`
- `metadata.domain`
- `metadata.subdomain`

Soft preferences:

- `prefer_path_prefixes`
- `prefer_like_patterns`

Отдельно:

- `target_doc_hints` всегда сохраняются в `RetrievalPlan.filters`, но **не маппятся напрямую** в `RagRepository.retrieve(...)` как SQL hard filter.

То есть сейчас `target_doc_hints` это не прямой DB filter, а downstream anchor для других шагов пайплайна и для deterministic exact-doc seeding logic.

## 11. Интеграция с retrieval stack

Следующий слой после resolver теперь исполняет plan не напрямую в `V2Process`, а через `V2RagRetrievalAdapter`.

`V2RagRetrievalAdapter.fetch_rows(...)` использует `RetrievalPlan` так:

- читает `filters["target_doc_hints"]` из самого плана;
- делает exact-path seed через `retrieve_exact_files(...)`;
- для missing hints делает substring fallback через `retrieve_chunks_by_path_substrings(...)`;
- затем делает обычный semantic retrieve через `RagSessionRetriever.retrieve(...)`;
- объединяет exact / substring / semantic rows через dedupe merge.

Это важный сдвиг: execution strategy теперь зависит от **контракта `RetrievalPlan`**, а не от скрытой route-specific логики внутри `V2Process`.

`RagSessionRetriever._map_filters()` прокидывает в `RagRepository.retrieve(...)`:

- `path_prefixes`
- `exclude_path_prefixes`
- `exclude_like_patterns`
- `prefer_path_prefixes`
- `prefer_like_patterns`
- `prefer_non_tests`
- `metadata_domain` из `filters["metadata.domain"]`
- `metadata_subdomain` из `filters["metadata.subdomain"]`

`RetrievalStatementBuilder.build_retrieve(...)` добавляет SQL predicates:

- `lower(metadata_json->>'domain') = :metadata_domain`
- `lower(metadata_json->>'subdomain') = :metadata_subdomain`

Таким образом:

- `process_domain/process_subdomain` реально участвуют в retrieval query;
- `target_doc_hints` реально участвуют в retrieval execution strategy на уровне adapter;
- `V2RetrievalPolicyResolver` определяет plan contract, а следующий шаг исполняет этот contract более буквально.

## 12. Актуальные ограничения

- Логика полностью deterministic.
- `target_terms` сейчас не участвуют в branching resolver.
- `process_domain/process_subdomain` не влияют на summary profile selection.
- API signal добавляет `path_prefixes` даже в generic summary, если среди конфликтующих сигналов присутствует API.
- `target_doc_hints` не являются прямым SQL filter внутри обычного `retrieve`, но используются adapter-уровнем для exact-path / substring seeding до semantic retrieval.
