Initial import NDC_1C

This commit is contained in:
2026-03-26 10:38:25 +03:00
commit a162d77ef7
2943 changed files with 3615871 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# API Contract
Base URL: `http://localhost:8787`
## POST `/api/normalize`
Core request fields:
- `promptVersion` (e.g. `normalizer_v2_0_2`)
- `schemaVersion` (e.g. `v2_0_2`)
- `userQuestion`
- model transport fields (`apiKey`, `model`, `baseUrl`, `temperature`, `maxOutputTokens`)
For v2.0.2, backend returns:
- `schema_version: "v2_0_2"`
- normalized payload with `normalized_query_v2_0_2`
- deterministic `route_hint_summary`
Schema selection:
- `promptVersion=normalizer_v2_0_2` or `schemaVersion=v2_0_2` -> `normalized_query_v2_0_2`
- `promptVersion=normalizer_v2_0_1` or `schemaVersion=v2_0_1` -> `normalized_query_v2_0_1`
- `promptVersion=normalizer_v2` or `schemaVersion=v2` -> `normalized_query_v2`
- otherwise -> `normalized_query_v1`
## POST `/api/eval/run`
Supports v2 family (`v2`, `v2_0_1`, `v2_0_2`) with inline batch via `rawQuestions`.
Assistant Stage 1 eval target is additive and enabled only when `eval_target=assistant_stage1`.
Legacy normalizer eval remains default when `eval_target` is omitted.
Example:
```json
{
"mode": "single-pass-strict",
"rawQuestions": "вопрос 1; вопрос 2; вопрос 3",
"useMock": false,
"normalizeConfig": {
"promptVersion": "normalizer_v2_0_2",
"schemaVersion": "v2_0_2",
"model": "gpt-4o-mini",
"temperature": 0,
"maxOutputTokens": 900
}
}
```
Assistant Stage 1 example:
```json
{
"eval_target": "assistant_stage1",
"mode": "single-pass-strict",
"useMock": true,
"caseSetFile": "assistant_stage1_canonical_v0_1.json",
"compare_with_report_file": "assistant-stage1-baseline.json",
"normalizeConfig": {
"promptVersion": "normalizer_v2_0_2"
}
}
```
v2.0.2 eval metrics include:
- `schema_validation_pass_rate`
- `scope_detection_accuracy`
- `route_resolution_accuracy`
- `no_route_precision`
- `false_no_route_rate`
- `execution_state_consistency_rate`
- `clarification_precision`
- `clarification_recall`
## Presets and History
- `GET /api/presets`
- `POST /api/presets/save`
- `GET /api/history`
- `GET /api/history/:trace_id`
+33
View File
@@ -0,0 +1,33 @@
# Prompt System
Промпты лежат в корне проекта в каталоге `prompts/`.
## Supported Versions
- `normalizer_v1`
- `normalizer_v1_1`
- `normalizer_v1_1_1`
- `normalizer_v1_1_2`
- `normalizer_v1_1_2_1`
- `normalizer_v2`
- `normalizer_v2_0_1`
- `normalizer_v2_0_2`
## Main Files
- `prompts/system/default.txt`
- `prompts/domain/normalizer_domain_v1_1.txt`
- `prompts/developer/normalizer_v2_0_2.txt`
- `prompts/fewshot/normalizer_v2_0_2.txt`
## v2.0.2 Notes
- Целевая схема: `normalized_query_v2_0_2`.
- Требует fragment-level поля:
- `execution_readiness`
- `route_status`
- `no_route_reason`
- Добавляет дисциплину: routable in-scope fragments не должны оставаться в `no_route`.
## Prompt Manager
`backend/src/services/promptBuilder.ts`:
- Загружает builtin presets для всех версий, включая `normalizer_v2_0_2`.
- Подставляет version-specific `developer/domain/fewshot`.
- По умолчанию использует `DEFAULT_PROMPT_VERSION` из backend config.
+38
View File
@@ -0,0 +1,38 @@
# Schema Contracts
## Supported Schemas
- `normalized_query_v1`
- file: `backend/src/schemas/normalized_query_v1.json`
- `normalized_query_v2`
- file: `backend/src/schemas/normalized_query_v2.json`
- `normalized_query_v2_0_1`
- file: `backend/src/schemas/normalized_query_v2_0_1.json`
- `normalized_query_v2_0_2`
- file: `backend/src/schemas/normalized_query_v2_0_2.json`
Root aliases in `/schemas`:
- `schemas/normalized_query_v2.json`
- `schemas/normalized_query_v2_0_1.json`
- `schemas/normalized_query_v2_0_2.json`
## v2.0.2 Additions
Fragment-level required fields:
- `execution_readiness`
- `route_status`
- `no_route_reason`
Enums:
- `execution_readiness`: `executable | executable_with_soft_assumptions | needs_clarification | no_route`
- `route_status`: `routed | no_route`
- `no_route_reason`: `out_of_scope | insufficient_specificity | missing_mapping | unsupported_fragment_type`
Consistency rules in schema:
- If `route_status=no_route` then `no_route_reason` must be non-null enum value.
- If `route_status=routed` then `no_route_reason` must be `null`.
## Validation API
Backend validates via AJV:
- `validateNormalized(payload, "v1")`
- `validateNormalized(payload, "v2")`
- `validateNormalized(payload, "v2_0_1")`
- `validateNormalized(payload, "v2_0_2")`
@@ -0,0 +1,45 @@
# Assistant Mode Flow
## End-to-End
1. User opens `Assistant` mode in GUI.
2. User enters message and clicks `Send`.
3. Frontend starts status ticker:
- `Razbirayu zapros`
- `Proveryayu kontur`
- `Opredelyayu marshrut`
- `Ishchu dannye`
- `Sobirayu otvet`
4. Backend stores user message in session history.
5. Backend runs normalizer (`normalizer_v2_0_2`) with provided prompt/config.
6. Backend derives deterministic route summary from normalized payload.
7. Backend builds retrieval plan (current stage: stubbed/diagnostic).
8. Backend composes human-readable assistant answer via `answer_composer`.
9. Backend stores assistant reply in session history.
10. Backend returns reply + debug payload + updated conversation snapshot.
11. Frontend updates chat timeline and keeps debug expandable per assistant message.
## Fallback Behavior
- `out_of_scope`: polite contour boundary response.
- `clarification`: asks for concrete period/account/document/counterparty.
- `partial`: reports available in-scope part and marks unavailable part.
- `none`: reports routed fragments and planned execution path.
## Debug Drawer Data
Each assistant reply can expose:
- `trace_id`
- normalized fragments
- route summary and fallback type
- retrieval plan payload
- full normalized object snapshot
## Session Memory Rules
- scope: current backend process memory only
- key: `session_id`
- message cap: bounded in-memory list per session
- persistence: not durable across backend restart
## Why This Stage Exists
This mode creates a usable operator loop before deeper field-hardening:
- gather real dialog traces
- identify where clarification/no-route appears in practice
- evaluate answer quality and UX with real user input
@@ -0,0 +1,83 @@
# Assistant Mode Spec
## Goal
Add a second UI mode (`Assistant`) on top of the existing decomposition pipeline, without removing current decomposition/debug capabilities.
## Scope Delivered
- Keep existing `Decomposition` mode unchanged.
- Add `Assistant` chat mode in frontend.
- Add backend endpoint `POST /api/assistant/message`.
- Add session-scoped chat memory (in-memory store).
- Add `answer_composer` layer for human-readable output.
- Add debug payload per assistant reply (expandable in UI).
- Keep one shared backend normalization/routing pipeline.
## Backend Contract
### Endpoint
`POST /api/assistant/message`
### Request
- `session_id` (optional)
- `user_message` (required)
- connection settings: `apiKey`, `model`, `baseUrl`, `temperature`, `maxOutputTokens`
- prompt settings: `promptVersion`, `systemPrompt`, `developerPrompt`, `domainPrompt`, `fewShotExamples`
- optional `context` (`period_hint`, `business_context`)
- `useMock` (optional)
### Response
- `ok`
- `session_id`
- `assistant_reply`
- `conversation_item` (assistant message)
- `debug`:
- `trace_id`
- `fragments`
- `fallback_type`
- `route_summary`
- `retrieval`
- `normalized`
- `conversation` (session items snapshot)
### Session Endpoint
`GET /api/assistant/session/:session_id`
Returns current in-memory session transcript.
## Internal Pipeline
`user_message -> normalizer_v2_0_2 -> deterministic route summary -> retrieval plan -> answer_composer -> assistant reply`
Notes:
- retrieval layer is currently sandbox/stubbed in this stage;
- answer is human-readable and not raw JSON;
- debug JSON is still available per message.
## Logging Fields
Each assistant message writes structured log entry with:
- `session_id`
- `message_id`
- `user_message`
- `normalizer_output`
- `resolved_execution_state`
- `routes`
- `fallback_type`
- `retrieval_payloads`
- `assistant_reply`
- `trace_id`
## Frontend Behavior
### Mode Switch
Two explicit modes:
- `Assistant`
- `Decomposition`
### Assistant UI
- chat feed (user/assistant messages)
- message input + send
- pipeline status text while processing
- session reset button
- optional debug section per assistant message (`details`)
### Decomposition UI
All existing panels stay as-is: normalize/eval/history/runtime/debug tabs.
@@ -0,0 +1,146 @@
# Assistant Mode vNext Spec
## 1. Цель
Перевести Assistant Mode из planner/debug shell в рабочий factual-режим:
- принять вопрос пользователя;
- нормализовать и декомпозировать;
- выбрать маршрут выполнения;
- выполнить route-specific retrieval;
- нормализовать retrieval results в единый контракт;
- собрать один человекочитаемый ответ на русском;
- отдать debug отдельно, через раскрываемый слой.
## 2. Реализованный контур
Текущий контур в backend:
`User message -> NormalizerService -> Route plan -> AssistantDataLayer (route executor) -> normalizeRetrievalResult -> composeAssistantAnswer -> Assistant API response`
Ключевые файлы:
- `backend/src/services/assistantService.ts`
- `backend/src/services/assistantDataLayer.ts`
- `backend/src/services/retrievalResultNormalizer.ts`
- `backend/src/services/answerComposer.ts`
- `backend/src/routes/assistant.ts`
## 3. API контракты
Endpoint: `POST /api/assistant/message`
Request (поддерживаются оба поля `user_message` и `message`):
```json
{
"session_id": "asst-...",
"mode": "assistant",
"message": "Покажи риски по НДС за июнь 2020",
"user_message": "Покажи риски по НДС за июнь 2020",
"promptVersion": "normalizer_v2_0_2",
"context": {
"period_hint": "2020-06",
"business_context": "buh_test"
},
"useMock": true
}
```
Response:
```json
{
"ok": true,
"session_id": "asst-...",
"assistant_reply": "Проверка выполнена...",
"reply_type": "factual",
"conversation_item": {},
"debug": {
"trace_id": "...",
"fragments": [],
"routes": [],
"retrieval_status": [],
"retrieval_results": []
},
"conversation": []
}
```
## 4. Reply policy
Реализованы user-facing типы:
- `factual`
- `empty`
- `partial`
- `clarification`
- `out_of_scope`
- `error`
Технические маркеры (`fallback_type`, route names, trace details) остаются в debug payload и не выводятся как основной ответ.
## 5. Debug payload
Debug отделён от user reply и содержит:
- `trace_id`
- `route_summary`
- `fragments`
- `routes`
- `retrieval_status`
- `retrieval_results`
- `normalized`
## 6. UI поведение
Frontend Assistant panel:
- показывает нормальный текст ответа;
- показывает техразбор только внутри `details`-блока `Показать технический разбор`;
- использует русские loading-состояния:
- `Разбираю запрос`
- `Ищу данные`
- `Собираю ответ`
Ключевые файлы:
- `frontend/src/components/AssistantPanel.tsx`
- `frontend/src/App.tsx`
- `frontend/src/state/types.ts`
## 7. Логирование
В `assistant_loop` логируются:
- `session_id`, `message_id`, `user_message`
- `normalizer_output`
- `execution_plan`
- `retrieval_calls`
- `retrieval_results_raw`
- `retrieval_results_normalized`
- `assistant_reply`
- `reply_type`
- `trace_id`
Дополнительно введён session-level лог (один файл на `session_id`):
- каталог: `data/assistant_sessions`
- формат: `assistant_session_log_v1`
- модель записи: один JSON-файл `<session_id>.json`, который обновляется при каждом сообщении в рамках этой сессии.
- внутри `turns[]` каждый закрытый контур хранит человекочитаемый блок:
- `Вопрос`
- `Понято как`
- `Декомпозиция`
- `Ответ`
- ниже в `technical_json` остаётся полный технический JSON по этому же контуру.
## 8. Минимальная приёмка этапа
Этап считается выполненным:
1. Assistant возвращает русскоязычный пользовательский ответ, не route plan.
2. Debug остаётся доступным отдельно.
3. Работает factual retrieval loop через route executors.
4. Отображаются `out_of_scope / clarification / partial / empty / error`.
5. Decomposition-режим не сломан.
@@ -0,0 +1,36 @@
# Clarification Policy (v2.0.1)
## Purpose
Prevent over-triggering clarification for in-scope operational accounting questions.
## Execution Readiness Levels
- `executable`: enough information to run safely.
- `executable_with_soft_assumptions`: route is clear, missing details can be covered by safe context assumptions.
- `needs_clarification`: missing information blocks reliable routing/execution.
## When Clarification Is Not Required
- In-scope query with recognizable accounting area and problem type.
- Route can be selected deterministically.
- Colloquial accounting language still maps to scan/review/anomaly/rule-check intent.
- Period is missing but active period exists in session context.
## When Clarification Is Required
- Domain/scope unclear.
- Accounting area/object cannot be identified.
- Routing cannot be selected reliably.
- Critical period-dependent task and period cannot be inferred from session context.
- Conflicting mixed tasks that cannot be decomposed safely.
## Soft Assumptions
Allowed markers:
- `period_from_session_context`
- `company_scope_defaulted`
- `problem_scan_mode_enabled`
These assumptions allow execution without forcing clarification when risk is acceptable.
@@ -0,0 +1,43 @@
# Domain Scope Policy
## Цель
Формализовать, какие запросы допускаются в бухгалтерский контур компании, а какие нет.
## In-Scope
`domain_relevance = in_scope` только если запрос относится к данным текущего предприятия и учетной онтологии:
- документы, проводки, оплаты, взаиморасчеты;
- остатки, хвосты, сальдо, аномалии;
- контроль учетных правил в контуре предприятия;
- риски закрытия периода в контексте конкретной базы.
`business_scope = company_specific_accounting`.
## Out-of-Scope
`domain_relevance = out_of_scope` если запрос:
- про абстрактную бухгалтерию “вообще”;
- про законы/ФСБУ/НК РФ без привязки к данным предприятия;
- оффтоп/бытовой чат;
- не связан с сущностями доступного учетного контура.
`business_scope`:
- `generic_accounting` для общетеоретических бух-запросов;
- `offtopic` для нерелевантного контента.
## Unclear
`domain_relevance = unclear`, если есть сигнал бухгалтерской темы, но не хватает контекста для уверенного доступа к данным.
`business_scope = unclear`.
## Обязательное правило выполнения
Фрагменты `out_of_scope`:
- не отправляются в 1С/retrieval/analytics pipeline;
- всегда ведут к fallback-поведеню.
Фрагменты `unclear`:
- допускаются к уточнению;
- не должны насильно эскалироваться в deep-route без дополнительных данных.
+40
View File
@@ -0,0 +1,40 @@
# Fallback Policy
## Типы fallback
### 1) Out-of-scope fallback
Условие:
- сообщение не имеет валидных in-scope фрагментов.
Шаблон:
> Я работаю только с данными и бухгалтерским контуром текущей компании.
> Запрос вне доступной предметной области.
### 2) Clarification fallback
Условие:
- in-scope есть, но для исполнения не хватает критичного контекста (период, объект, участок учета).
Шаблон:
> Могу проверить это в контуре компании, но нужно уточнить период, документ, счет или участок учета.
### 3) Partial fallback
Условие:
- смешанное сообщение: часть in-scope, часть out-of-scope.
Шаблон:
> Обработаю только ту часть запроса, которая относится к данным компании.
> Остальное выходит за пределы доступного контура.
## Тональность
- профессионально и спокойно;
- без канцелярита;
- без грубости и оценочных формулировок;
- без имитации “полного ответа”, если контур не позволяет.
## Техническое правило
Fallback выбирается после fragment-level domain gating, до исполнения маршрутов.
@@ -0,0 +1,82 @@
# Final Answer Composer Spec
## 1. Назначение
`answerComposer` преобразует нормализованные retrieval results в один user-facing ответ на русском языке.
Реализация:
- `backend/src/services/answerComposer.ts`
Вход:
- `userMessage`
- `routeSummary`
- `retrievalResults[]` (уже в unified schema)
Выход:
- `assistant_reply` (текст для пользователя)
- `reply_type`
- `fallback_type`
## 2. Приоритеты ответа
Порядок разрешения:
1. `out_of_scope` fallback.
2. `clarification` fallback (если factual результатов нет).
3. `error`, если есть только ошибки retrieval.
4. `empty`, если запрос валиден, но найдено 0 результатов.
5. `partial`, если есть полезные данные, но покрытие неполное.
6. `factual`, если есть корректные результаты без fallback-конфликта.
## 3. Типы ответов
### `factual`
- данные найдены;
- даётся краткий итог + компактная сводка.
### `empty`
- корректный запрос, но выдача пустая.
### `partial`
- часть вопроса обработана;
- часть недоступна/пустая/ошибочная.
### `clarification`
- нужно уточнение периода/документа/счёта/контрагента.
### `out_of_scope`
- запрос не относится к учётному контуру компании.
### `error`
- техническая ошибка retrieval.
## 4. Контентные правила
User-facing текст:
- только русский;
- без route names;
- без `trace`, `fallback_type`, `planned routes`;
- без служебного planner/debug языка.
Технические детали живут только в `debug` payload.
## 5. Форматирование factual-ответа
Composer умеет кратко форматировать несколько типов retrieval:
- `chain`: акцент на цепочки контрагент/документы/операции.
- `ranking`: топ/ранжирование.
- `list`: список записей (в т.ч. риск-объекты).
- `object/summary`: компактный факт-блок.
Если результатов много, выдаётся только верхняя часть (top-N), остальное остаётся в debug payload.
@@ -0,0 +1,44 @@
# Fragment Execution Policy
## Назначение
Определяет, как система исполняет multi-intent сообщение после `normalized_query_v2`.
## Pipeline
1. Получить decomposition (`fragments`, `discarded_fragments`).
2. Отфильтровать `out_of_scope` фрагменты.
3. Оставшиеся `in_scope` прогнать через deterministic routing.
4. Сгруппировать результаты в единый ответ.
## Группировка фрагментов
Рекомендуемая стратегия:
- `live_mcp_drilldown` — отдельно (точечные задачи);
- `hybrid_store_plus_live` — отдельно (цепочки и причинность);
- `batch_refresh_then_store` — отдельно (обзор/топ/срез);
- `store_feature_risk` и `store_canonical` можно агрегировать в один блок.
## Execution Planner Rules
- Не сводить насильно много задач к одному intent.
- Не терять валидные in-scope задачи из-за соседнего шума.
- При mixed-message обязательно возвращать partial fallback для out-of-scope части.
- Если все in-scope фрагменты требуют уточнения — clarification fallback до выполнения.
## Evidence Safety
Если флаг `asks_for_evidence=true`:
- ответ должен содержать ссылку на подтверждающий источник/объект после исполнения.
Если `asks_for_exact_object_trace=true`:
- приоритет у точечного route `live_mcp_drilldown`.
## Наблюдаемость
Минимум логирования:
- число фрагментов;
- число discarded;
- count in_scope/out_of_scope;
- route decisions по fragment_id;
- выбранный fallback type.
@@ -0,0 +1,37 @@
# Known Limits Before Field Eval
## Current Stage Limits
- Retrieval layer is stubbed in assistant mode:
- system returns execution plan and routing trace,
- not a full factual extraction from 1C/OData/MCP yet.
- Session memory is in-memory only:
- resets on backend restart,
- no durable storage for long dialog continuity.
- No production-grade orchestration:
- no multi-agent planner,
- no long-horizon tool chain.
- Clarification policy is deterministic baseline:
- adequate for sandbox,
- will need tuning from real field traces.
## What Is Intentionally Deferred
- Large conversation memory (100+ turns).
- Automatic re-labeling and synthetic auto-eval expansion.
- Full data retrieval hardening for every route.
- Deep route fallback optimization beyond current deterministic policy.
- Production SLO/SLA, auth, tenancy, and governance controls.
## Risks to Track
- User may interpret planned route as final factual answer.
- Partial fallback wording can still be too technical for non-debug users.
- Out-of-scope vs clarification boundary may drift on ambiguous prompts.
- Without field replay set (30-40 real questions), optimization remains speculative.
## Exit Criteria for Next Hardening Step
- Collect real assistant-mode traces from live operators.
- Build labeled set from real interactions (not synthetic-only).
- Run targeted policy eval for:
- clarification precision/recall,
- false no-route rate,
- partial fallback quality.
- Connect at least one route to factual retrieval and validate answer-grounding.
@@ -0,0 +1,54 @@
# Known Limits: Current Routes
## 1. Источник данных пока snapshot-based
Текущие route executors работают на экспортированных JSON из `docs/ARCH/2020экспорт`, а не на прямом online-query в 1С.
Следствие:
- ответы factual относительно snapshot-среза;
- realtime-актуальность зависит от обновления экспорта.
## 2. Ограниченная семантика маршрутов
Маршруты покрывают базовые сценарии (chain/risk/ranking/canonical/drilldown), но без полного доменного покрытия бухгалтерского контура.
Следствие:
- часть сложных бухгалтерских формулировок уйдёт в `clarification` или `partial`;
- не все аналитики и субконто-паттерны интерпретируются детерминированно.
## 3. No-route и уточнения
Для фрагментов `no_route` retrieval пропускается и возвращается skipped-result.
Следствие:
- пользователь получает корректный fallback-ответ;
- но фактических данных по такому фрагменту не будет до уточнения.
## 4. Точечный drilldown требует идентификатор
`live_mcp_drilldown` в текущем контуре ожидает GUID-сигнал в тексте.
Следствие:
- без GUID маршрут вернёт `empty`;
- это штатное поведение, не ошибка pipeline.
## 5. Формат user-facing ответа остаётся компактным
Composer отдаёт короткий итог и top-N фрагменты.
Следствие:
- полный массив фактов доступен через debug payload;
- пользовательский пузырь не предназначен для полного аналитического досье.
## 6. Что нужно для следующего этапа
Рекомендуемое усиление:
1. Подключить live-data executor поверх MCP/FoxyLink вместо snapshot-only.
2. Расширить route coverage по бухгалтерским кейсам (субконто/проводки/объяснение сальдо).
3. Добавить интеграционные тесты factual retrieval на реальном контуре.
@@ -0,0 +1,17 @@
# Forensic Audit v1.1 (NQ-004 / NQ-008 / NQ-009)
Источник baseline: `data/eval_cases/eval-YxrhL2dCcH.report.json` и trace-файлы `h5BLdC1oBO0wY6`, `iel5ScdccVZ4zT`, `1C9MATbKvo5FnF`.
Новые API-вызовы на forensic этап: `0`.
| case_id | raw_question | expected.intent_class | actual.intent_class | expected.route_hint | actual.route_hint | expected.requires | actual.requires | какие признаки модель не увидела | какие признаки модель увидела лишние | предполагаемая причина ошибки | какая минимальная правка должна это исправить |
|---|---|---|---|---|---|---|---|---|---|---|---|
| NQ-004 | По 97 счету проверь, где возможна ошибка дат начала и окончания списания. | rule_based_account_control | anomaly_probe | store_feature_risk | store_feature_risk | `{cross=false, causal=false}` | `{cross=false, causal=true}` | Что это rule-based контроль по учетному правилу 97, а не поиск аномалий | Ложный causal (`needs_causal_chain=true`) и смещение в anomaly_probe | Лексика "ошибка" сработала как anomaly trigger без приоритета rule-based контроля | В developer v1.1 добавить приоритет: "ошибка дат/правил по счету" -> `rule_based_account_control`; запрет поднимать causal без явной цепочки |
| NQ-008 | Покажи по банку документ №TRX-88 и связанную проводку по 51. | drilldown_explain | cross_entity | live_mcp_drilldown | live_mcp_drilldown | `{cross=false, causal=false}` | `{cross=true, causal=false}` | Что это точечный object trace с фокусом на конкретный документ | Лишний `needs_cross_entity_join=true`, из-за чего intent ушел в cross_entity | Слово "связанную" переоценено как cross-entity, хотя запрос точечный | В developer v1.1 закрепить правило: при точном doc/ref приоритет у `drilldown_explain`, cross_entity только если запрошен массовый разбор |
| NQ-009 | Где у нас пахнет ручной ошибкой по июню? | ambiguous_human_query | anomaly_probe | batch_refresh_then_store | store_feature_risk | `{cross=false, causal=false}` | `{cross=false, causal=false}` | Что это широкий human-style запрос про периодный обзор без точного объекта | Лишний уход в risk-route (`store_feature_risk`) как будто это только anomaly probe | Комбинация "ручная ошибка" + короткая формулировка классифицирована как чистый risk-bucket | В developer/domain v1.1 добавить приоритеты: широкие human-form формулировки по периоду -> `batch_refresh_then_store`; `ambiguous_human_query` только контролируемый fallback |
## Итог forensic
- Основной baseline-дефект был в границах `intent_class`, а не в schema.
- У `NQ-004` и `NQ-008` route был верный, но intent смещался в соседний класс.
- У `NQ-009` одновременно сломались и intent, и route из-за перегруза risk-лексики.
- Минимальные правки действительно лежат в prompt/few-shot таксономии и приоритетах, без переписывания бизнес-логики backend.
@@ -0,0 +1,71 @@
# Normalizer v1.1.1 Patch Notes
## Что изменено
Сделан точечный `v1.1.1` patch поверх `v1.1` без пересборки архитектуры и без изменения schema:
- добавлен новый preset `normalizer_v1_1_1` в prompt manager;
- добавлен developer prompt:
- `prompts/developer/normalizer_v1_1_1.txt`
- добавлен few-shot prompt:
- `prompts/fewshot/normalizer_fewshot_v1_1_1.txt`
- domain prompt намеренно не переписывался (используется `normalizer_domain_v1_1.txt`);
- добавлены micro-eval артефакты:
- `reports/normalizer_v1_1_1_micro_eval.json`
- `reports/normalizer_v1_1_1_micro_eval.md`
## Какие 3 паттерна лечили
1. `period_close_risk` vs `heavy_analytical`
- закреплен приоритет `period_close_risk` для лексики предзакрытия/последнего дня/сдачи отчетности.
2. Точечный drilldown и лишний `needs_cross_entity_join`
- закреплено правило exact object trace:
- `needs_exact_object_trace = true`
- `needs_runtime_truth = true`
- `needs_cross_entity_join = false` (если нет массового multi-entity анализа).
3. `anomaly_probe` и лишняя batch escalation
- закреплено правило: если есть риск/аномалия без рейтинга и company-wide aggregation, route остается `store_feature_risk`.
## Почему изменения безопасны
- изменения локализованы только в developer/few-shot слоях `v1.1.1`;
- сильные зоны `cross_entity`, causal chain и schema не переписывались;
- schema и transport-контракт не изменены;
- количество новых few-shot ограничено тремя целевыми примерами.
## Бюджет вызовов и ретраи
Текущий micro-run в этой среде выполнен в `use_mock=true`, так как `OPENAI_API_KEY` в shell не задан.
- внешние API-вызовы: `0`
- ретраи: `0`
- лимит этапа (`<=5`) не превышен.
## Что улучшилось
По micro-eval на целевых 5 кейсах:
- `schema_validation_pass_rate = 100`
- `intent_class_accuracy = 100`
- `route_hint_accuracy = 100`
- `causal_flag_accuracy = 100`
- `high_confidence_error_rate = 0`
Покрытые кейсы:
- `NQ-008`
- `V11-DD-005`
- `V11-OT-003`
- `V11-OT-004`
- `V11-OT-005`
## Что осталось как есть
- core логика `v1.1` по `cross_entity`/`heavy_analytical`/`rule_based_account_control` не расширялась;
- не добавлялся большой eval sweep;
- не трогались schema, parser и orchestration-контракты.
## Важно для финальной приемки
Micro-run зафиксирован корректно, но он mock-based (`use_mock=true`).
Для production-приемки `v1.1.1` нужно один реальный strict micro-run на тех же 5 кейсах с `use_mock=false` и OpenAI API key.
@@ -0,0 +1,68 @@
# Normalizer v1.1.2.1 Patch Notes
## Задача этапа
Сохранить стабильную prompt-логику `v1.1.2` и добавить новый 30-case набор
из живых формулировок бух-ревью (`TZ_LLM_Normalizer_v1.1.2.1.md`) для отдельного контрольного прогона.
Ключевой принцип этапа:
- не ломать работающий taxonomy/route baseline;
- расширить покрытие на реальный язык бухгалтера.
## Что изменено
1. Добавлена версия prompt preset `normalizer_v1_1_2_1`.
2. Добавлены новые prompt-файлы:
- `prompts/developer/normalizer_v1_1_2_1.txt`
- `prompts/fewshot/normalizer_fewshot_v1_1_2_1.txt`
3. Добавлен новый eval dataset:
- `eval_cases/normalizer_eval_v1_1_2_1_30cases.json`
4. GUI переведен по умолчанию на:
- `promptVersion: normalizer_v1_1_2_1`
- `caseSetFile: normalizer_eval_v1_1_2_1_30cases.json`
5. В `EvalService` добавлена автозапись артефактов для strict-run:
- `reports/normalizer_v1_1_2_1_eval.json`
- `reports/normalizer_v1_1_2_1_eval.md`
6. Обновлены документы `docs/PROMPTS.md` и `docs/API.md`.
## Охват 30-case набора
В набор включены 7 предметных блоков:
- поставщики/покупатели/взаиморасчеты;
- реализация/неоплата/90+62;
- банк/выписки/51;
- товары/склад/41;
- материалы/10;
- РБП/97;
- ОС/01-02.
## Совместимость и риск
Изменения в `v1.1.2.1` сделаны как controlled extension:
- schema не менялась;
- базовые route-policy и boundary-policy из `v1.1.2` сохранены;
- добавлено только покрытие новых формулировок и новый eval-pack.
## Артефакты этапа
- `prompts/developer/normalizer_v1_1_2_1.txt`
- `prompts/fewshot/normalizer_fewshot_v1_1_2_1.txt`
- `eval_cases/normalizer_eval_v1_1_2_1_30cases.json`
- `reports/normalizer_v1_1_2_1_eval.json` (после strict-run)
- `reports/normalizer_v1_1_2_1_eval.md` (после strict-run)
## Результат контрольного прогона
Strict-run выполнен на `normalizer_eval_v1_1_2_1_30cases.json` в режиме `use_mock=true`.
- run_id: `eval-qfxRc9_xyJ`
- cases_total: `30`
- schema_validation_pass_rate: `100`
- intent_class_accuracy: `83.33`
- route_hint_accuracy: `100`
- causal_flag_accuracy: `93.33`
- high_confidence_error_rate: `0`
Комментарий:
- mock-режим использует внутренние эвристики и не отражает финальное LLM-качество 1:1;
- для production-приемки нужен повтор этого же strict-run в `use_mock=false`.
@@ -0,0 +1,81 @@
# Normalizer v1.1.2 Patch Notes
## Проблема v1.1.1
На `v1.1.1` был локальный перекос taxonomy на границе:
- `heavy_analytical``period_close_risk`.
Симптом:
- вопросы обзорного/рейтингового типа в контексте закрытия периода местами уезжали в `period_close_risk`;
- часть пограничных кейсов получала излишне уверенную оценку.
Root cause:
- правило для `period_close_risk` в `v1.1.1` было слишком жестким;
- был сильный positive-trigger на close-context без симметричного противовеса для heavy-overview.
## Что изменено
### 1) Developer prompt (новый файл)
- Добавлен `prompts/developer/normalizer_v1_1_2.txt`.
- Переписан boundary-блок:
- `period_close_risk` только если core вопроса про риск срыва/дестабилизации close-процесса.
- `heavy_analytical` имеет приоритет, если цель вопроса: ranking/top/overview/summary/company-wide/prioritized analytical review, даже в close-контексте.
### 2) Few-shot (новый файл)
- Добавлен `prompts/fewshot/normalizer_fewshot_v1_1_2.txt`.
- Сохранен anchor-пример для `period_close_risk`.
- Добавлены 2 симметричных heavy-counterexamples:
- "Сделай рейтинг самых рисковых хвостов..."
- "Дай обзорный риск-срез перед сдачей отчетности..."
### 3) Confidence guard
- Добавлено явное правило в `developer v1.1.2`:
- на boundary `heavy_analytical`/`period_close_risk` не ставить `confidence.overall=high` без однозначного close-failure core.
## Что не меняли (безопасность patch)
- schema не менялась;
- route rules не переписывались;
- cross-entity / drilldown / anomaly core-патчи не пересобирались;
- domain prompt не расширяли массово.
Это intentional surgical patch только на одном taxonomy-boundary.
## Изменения в коде
- добавлена версия `normalizer_v1_1_2` в prompt manager и типы версий;
- добавлен micro-eval авто-артефакт для кейсов:
- `NQ-002, NQ-007, V11-HA-004, V11-OT-003, V11-OT-005`
- файлы: `reports/normalizer_v1_1_2_micro_eval.json|md`;
- UI и preset save по умолчанию переведены на `normalizer_v1_1_2`.
## Бюджет API
В этой среде `OPENAI_API_KEY` не задан, поэтому micro-run выполнен в `use_mock=true`.
- внешние API-вызовы: `0`
- ретраи: `0`
Лимит этапа `<= 5` не превышен.
## Результаты micro-run
Отчет: `reports/normalizer_v1_1_2_micro_eval.json`
- `NQ-002` -> `heavy_analytical` (ok)
- `NQ-007` -> `heavy_analytical` (ok)
- `V11-HA-004` -> `heavy_analytical` (ok)
- `V11-OT-003` -> `period_close_risk` (ok)
- `V11-OT-005` -> `period_close_risk` (ok)
Итог метрик micro-run:
- schema_validation_pass_rate = 100
- intent_class_accuracy = 100
- route_hint_accuracy = 100
- causal_flag_accuracy = 100
- high_confidence_error_rate = 0
## Что осталось на будущее
- Сделать один production micro-run (`use_mock=false`) на тех же 5 кейсах для финальной приемки по фактическому API-поведению модели.
- После подтверждения можно закрывать `v1.1.2` и переходить к следующей пачке новых кейсов.
@@ -0,0 +1,69 @@
# Changelog: LLM Normalizer v1.1
## Что изменено в prompt-слое
- Добавлен preset versioning: `normalizer_v1` и `normalizer_v1_1`.
- Добавлены отдельные файлы:
- `prompts/developer/normalizer_v1_1.txt`
- `prompts/domain/normalizer_domain_v1_1.txt`
- `prompts/fewshot/normalizer_fewshot_v1_1.txt`
- В `developer` усилены приоритеты между классами:
- `cross_entity` vs `anomaly_probe`
- `cross_entity` vs `rule_based_account_control`
- `drilldown_explain` для точечного object trace
- контроль fallback в `ambiguous_human_query`
- Добавлена confidence-policy v1.1: при ambiguity/сложном вопросе/high-uncertainty запрещено ставить `high` без оснований.
## Какие linguistic patterns добавлены
- "не сходится", "не видно", "не собралось", "повисло", "что пошло криво".
- "разложи по документам/оплатам/закрывающим".
- "чем подтверждается", "где ошибка в цепочке".
- Отдельно выделены паттерны точечного drilldown: ref, номер документа, конкретная строка проводки.
## Какие few-shot кейсы добавлены
В `normalizer_fewshot_v1_1.txt` добавлено 7 коротких примеров, покрывающих:
- `cross_entity` vs `anomaly_probe`
- `cross_entity` vs `rule_based_account_control`
- `cross_entity` (массовый explain) vs `drilldown_explain` (точечный trace)
- causal language + risk words
- ambiguous human wording
- rule-based контроль без causal chain
- heavy overview без точечного explain
## Какие кейсы были проблемными в baseline
- `NQ-004`: intent ушел в `anomaly_probe` вместо `rule_based_account_control`.
- `NQ-008`: intent ушел в `cross_entity` вместо `drilldown_explain`.
- `NQ-009`: intent и route ушли в risk bucket вместо широкого human-style обзорного маршрута.
Подробный forensic: `docs/normalizer_forensic_audit_v1_1.md`.
## Сколько API-вызовов потрачено
- Forensic этап: `0` новых внешних вызовов (использованы существующие traces).
- Финальный контрольный run: `0` внешних вызовов, так как запуск выполнен в `useMock=true` режиме.
- Итого этап: `0` внешних API-вызовов.
## Итоговые метрики до/после
Baseline (из `eval-YxrhL2dCcH.report.json`):
- schema_validation_pass_rate: `100`
- intent_class_accuracy: `72.73`
- route_hint_accuracy: `90.91`
- causal_flag_accuracy: `81.82`
- high_confidence_error_rate: `9.09`
v1.1 strict run (`reports/normalizer_eval_v1_1_run.json`, mock):
- schema_validation_pass_rate: `100`
- intent_class_accuracy: `83.33`
- route_hint_accuracy: `100`
- causal_flag_accuracy: `100`
- high_confidence_error_rate: `0`
## Что осталось проблемным после тюнинга
- В mock-run остались mismatch по классам `anomaly_probe`, `ambiguous_human_query`, `period_close_risk`.
- Это ожидаемо для текущей mock-эвристики (она route-driven и не отражает полноценно LLM taxonomy).
- Следующий шаг для честной приемки: один реальный `single-pass-strict` запуск на тех же 30 кейсах с OpenAI API key и фиксация финальных production-метрик.
@@ -0,0 +1,45 @@
# Normalizer v2.0.1 Spec
## Goal
`v2.0.1` keeps decomposition-first architecture but reduces unnecessary clarification on one-step in-scope accounting queries.
Core target:
- keep schema/scope stability;
- keep deterministic routing in code;
- lower false clarification behavior.
## Key Contract Changes
Schema: `normalized_query_v2_0_1`
Fragment-level fields added:
- `execution_readiness`: `executable | executable_with_soft_assumptions | needs_clarification`
- `clarification_reason`: `string | null`
- `soft_assumption_used`: `period_from_session_context | company_scope_defaulted | problem_scan_mode_enabled` (array)
## Readiness Policy
Policy is applied in code (post-check), not only in prompt:
`decide_fragment_execution_policy(fragment, session_context)`
Rules:
1. `out_of_scope` or `unclear` -> `needs_clarification`.
2. In-scope but business area/route cannot be identified -> `needs_clarification`.
3. In-scope and operationally clear -> `executable` or `executable_with_soft_assumptions`.
4. Missing period does not force clarification if session context provides active period.
5. Scan/review/anomaly/rule-check colloquial requests are executable if accounting area is understandable.
## Global Clarification Rule
`global_notes.needs_clarification = true` only when **all** in-scope fragments are blocked by clarification.
If at least one in-scope fragment is executable, clarification is not global fallback.
## Routing Compatibility
Routing remains deterministic (`routeHintAdapter`), but:
- fragments with `execution_readiness=needs_clarification` get `no_route`;
- soft assumptions do not block routing.
+65
View File
@@ -0,0 +1,65 @@
# Normalizer v2 Spec
## Назначение
`Normalizer v2` переводит слой нормализации с single-intent модели на decomposition-first:
`raw message -> fragments + scope + flags -> deterministic routing in code`
LLM в v2 не выдает финальное route-решение как источник истины.
LLM возвращает структурированную семантику, а маршрут выбирается правилами в коде.
## Вход
- Сырое сообщение пользователя (в т.ч. длинное, multi-intent, шумное).
## Выход
- JSON по схеме `normalized_query_v2`.
- Ключевые части:
- `message_in_scope`, `scope_confidence`
- `fragments[]`
- `discarded_fragments[]`
- `global_notes`
## Fragment Contract
Каждый фрагмент содержит:
- domain gating (`domain_relevance`, `business_scope`);
- semantic hints (`entity_hints`, `account_hints`, `document_hints`, `register_hints`);
- `time_scope`;
- route-critical flags;
- `candidate_labels` (multi-label, без жесткого single-intent);
- `confidence`.
## Deterministic Routing
Код применяет правила к `flags`:
1. `asks_for_exact_object_trace=true` -> `live_mcp_drilldown`
2. `asks_for_ranking_or_top=true` или `asks_for_period_summary=true` -> `batch_refresh_then_store`
3. `has_multi_entity_scope=true` и `asks_for_chain_explanation=true` -> `hybrid_store_plus_live`
4. `asks_for_rule_check=true` и нет causal-chain -> `store_feature_risk`
5. `asks_for_anomaly_scan=true` без heavy/causal -> `store_feature_risk`
6. fragment `out_of_scope` -> `no_route`
7. остальное in-scope -> `store_canonical`
## Совместимость
- `v1` и `v2` поддерживаются параллельно.
- Выбор схемы:
- `promptVersion=normalizer_v2` (или `schemaVersion=v2`) -> `normalized_query_v2`
- иначе -> `normalized_query_v1`
## UI-диагностика v2
Добавлены диагностические представления:
- Fragment View
- Scope View
- Flags View
- Route Simulation
## Ограничения этапа
- Полноценный quality-eval v2 отдельно планируется (см. `reports/v2_pilot_eval_plan.md`).
- Старый eval runner метрик `intent/route` ориентирован на v1 и не является основным приемочным контуром для v2.
@@ -0,0 +1,116 @@
# Route Executor Contracts
## 1. Общий контракт
Каждый route executor возвращает результат в unified-формате:
```json
{
"fragment_id": "F1",
"route": "store_feature_risk",
"status": "ok | empty | partial | error",
"result_type": "list | summary | object | chain | ranking",
"items": [],
"summary": {},
"evidence": [],
"errors": []
}
```
Реализация:
- raw execution: `backend/src/services/assistantDataLayer.ts`
- normalizer: `backend/src/services/retrievalResultNormalizer.ts`
## 2. Вход executor’ов
Вызов выполняется через:
`executeRoute(route: string, fragmentText: string)`
Входы:
- `route`: выбранный маршрут из route planner.
- `fragmentText`: текст фрагмента после нормализации/декомпозиции.
Источник данных текущего MVP:
- snapshot-пакет `docs/ARCH/2020экспорт/*.json`
## 3. Route: `hybrid_store_plus_live`
Назначение:
- causal/cross-entity цепочки;
- связь документов с контрагентами;
- поиск разрывов связности.
Result:
- `result_type = "chain"`
- `items`: агрегаты по контрагенту (`operations_count`, `document_refs_count`, `relation_types`, `samples`)
- `summary`: `checked_records`, `matched_counterparties`, `route_focus`
## 4. Route: `store_feature_risk`
Назначение:
- anomaly/rule-check;
- риск-признаки в проблемных записях.
Result:
- `result_type = "list"`
- `items`: записи с `risk_score`, `reasons`, техническими индикаторами
- `summary`: `checked_records`, `risky_records`, `average_risk_score`
## 5. Route: `batch_refresh_then_store`
Назначение:
- обзорные и ranking-задачи;
- top/приоритизация проверки.
Result:
- `result_type = "ranking"`
- `items`: `rank`, `entity`, `records_count`
- `summary`: `checked_records`, `ranked_entities`
## 6. Route: `store_canonical`
Назначение:
- канонический factual path по документам.
Result:
- `result_type = "list"`
- `items`: документные записи (`source_entity`, `source_id`, `period`, `counterparty_id`, `recorder`)
- `summary`: `checked_records`, `returned_records`
## 7. Route: `live_mcp_drilldown`
Назначение:
- точечный drilldown по GUID/объекту.
Result:
- `result_type = "object"`
- `items`: найденные совпадения по GUID
- `summary`: `query_guids`, `matched_records`
## 8. Ошибки и no-route
Если fragment получает `route=no_route`, backend не вызывает executor, а формирует skipped-result:
- `status = "empty"`
- `result_type = "summary"`
- `summary.skipped = true`
- `summary.no_route_reason`
Если executor падает:
- `status = "error"`
- `errors[]` содержит текст ошибки.
@@ -0,0 +1,79 @@
# v2.0.2 Execution State Machine
## Goal
Synchronize fragment-level states so there are no gray zones between:
- domain scope
- execution readiness
- routing
- fallback behavior
## Canonical Fragment Contract (v2.0.2)
- `execution_readiness`: `executable | executable_with_soft_assumptions | needs_clarification | no_route`
- `route_status`: `routed | no_route`
- `no_route_reason`: `out_of_scope | insufficient_specificity | missing_mapping | unsupported_fragment_type | null`
## Resolver Layer
`resolveFragmentExecutionStateV202(fragment, session_context)` runs after raw LLM output and before schema validation.
Resolver responsibilities:
1. Normalize readiness.
2. Normalize route status.
3. Set explicit no-route reason.
4. Enforce deterministic no-route guard.
## State Rules
### Rule A: out-of-scope
- Condition: `domain_relevance=out_of_scope`
- Result:
- `execution_readiness=no_route`
- `route_status=no_route`
- `no_route_reason=out_of_scope`
### Rule B: insufficient specificity
- Condition: readiness policy says clarification is required
- Result:
- `execution_readiness=needs_clarification`
- `route_status=no_route`
- `no_route_reason=insufficient_specificity`
### Rule C: missing mapping
- Condition: in-scope fragment cannot be mapped by deterministic route selector
- Result:
- `execution_readiness=no_route`
- `route_status=no_route`
- `no_route_reason=missing_mapping`
### Rule D: routable fragment
- Condition: in-scope, not clarification-blocked, route-selectable
- Result:
- `execution_readiness=executable | executable_with_soft_assumptions`
- `route_status=routed`
- `no_route_reason=null`
## Global Clarification Rule
`global_notes.needs_clarification=true` only when all in-scope fragments are clarification-blocked.
## Deterministic Routing Consistency Checks
Decision is consistent when:
- `route=no_route` -> readiness is not executable, and `no_route_reason` is present.
- `route!=no_route` -> readiness is not clarification/no_route, and `no_route_reason` is null.
This consistency is now measured in eval as:
- `execution_state_consistency_rate`
## Fallback Alignment
- `out_of_scope`: no in-scope fragments
- `clarification`: in-scope exists, but zero routable fragments due clarification
- `partial`: mix of routed and no-route fragments
- `none`: all in-scope fragments routed
## Trace Completeness Guard
For each normalization run, service now validates trace has:
- raw model output
- parsed normalized payload
- fragment execution state fields (for v2.0.1/v2.0.2)
- deterministic route decisions per fragment
Missing elements are logged as system trace-completeness errors.
@@ -0,0 +1,42 @@
# v2.0.2 No-Route Audit
## Source Run
- run_id: `eval-baY1nPi1rI`
- no_route fragments in run: `6`
## Extracted No-Route Cases
| case_id | trace_id | domain_relevance | execution_readiness | old reason | classification |
|---|---|---|---|---|---|
| BQ-011 | HNAXW_JV6E_hp- | in_scope | needs_clarification | critical_period_missing | legit_no_route |
| BQ-015 | mIR3Cru_dmM5PQ | in_scope | needs_clarification | critical_period_missing | legit_no_route |
| BQ-017 | vuvha8oq0Y67kU | in_scope | needs_clarification | critical_period_missing | legit_no_route |
| BQ-018 | dtTCTe9sMiGv2F | in_scope | needs_clarification | critical_period_missing | legit_no_route |
| BQ-020 | 2VO4fwiW6_quNT | in_scope | needs_clarification | critical_period_missing | legit_no_route |
| BQ-026 | dtXqHsYutlsp6q | out_of_scope | needs_clarification | fragment_out_of_scope | legit_no_route |
## Audit Result
- `legit_no_route`: 6
- `missing_mapping`: 0
Conclusion: historical no-route mass was mostly clarification/out-of-scope, not route-map holes.
## v2.0.2 Policy Mapping
Historical no-route reasons were normalized to explicit enum:
- out-of-scope cases -> `no_route_reason=out_of_scope`
- clarification/underspecified in-scope cases -> `no_route_reason=insufficient_specificity`
For real route-map gaps, v2.0.2 now reserves:
- `no_route_reason=missing_mapping`
- `execution_readiness=no_route`
## Deterministic Guard Added
If fragment is:
- `domain_relevance=in_scope`
- not clarification-blocked
- and route-selectable by deterministic policy
then `route_status=no_route` is forbidden and fragment is forced to routed state.
This prevents silent unresolved fragments.
@@ -0,0 +1,42 @@
# v2.0.2 Schema Forensic
## Source Run
- run_id: `eval-baY1nPi1rI`
- timestamp: `2026-03-23T18:59:54.829Z`
- prompt_version: `normalizer_v2_0_1`
- schema_validation_pass_rate: `96.15%`
- failed case: `BQ-001`
- trace_id: `SatgafwxwDR9BU`
## BQ-001 Failure Reconstruction
1. Raw question was a long multi-intent pre-close analytical request.
2. Model response arrived with:
- `status: incomplete`
- `incomplete_details.reason: max_output_tokens`
3. The JSON body in `output_text` was truncated mid-fragment.
4. Parser error after retry:
- `JSON_PARSE_ERROR_AFTER_RETRY: Unterminated string in JSON at position 3506`
5. `request_count_for_case = 2`, but retry used the same output budget, so truncation repeated.
## Root Cause
Primary failure was not taxonomy misclassification.
It was transport-level truncation due insufficient `max_output_tokens` for long structured output.
## Systemic Fix Implemented in v2.0.2
1. Added adaptive retry output budget logic in normalizer service:
- Detects `status=incomplete` + `reason=max_output_tokens`.
- Escalates retry budget (`computeRetryMaxOutputTokens`) instead of repeating the same limit.
- Hard cap: `2400` output tokens.
2. Kept strict JSON schema validation unchanged (no relaxation).
3. Added `normalized_query_v2_0_2` contract with explicit execution/route fields to reduce ambiguous partial outputs.
## Why This Is Not a Case-Specific Patch
- Trigger condition is generic (`max_output_tokens` truncation), not tied to `BQ-001`.
- Applies to any long, multi-fragment normalization.
- Preserves strict schema discipline while improving completion reliability.
## Follow-up Validation
- Run `single-pass-strict` on v2.0.2 labeled eval set.
- Confirm:
- `schema_validation_pass_rate = 100`
- no parser failures caused by truncation in trace logs.