diff --git a/docs/accounting-assistant/accounting-assistant/03_execution/ACCEPTANCE_CHECKLIST_STAGE_03.md b/docs/accounting-assistant/accounting-assistant/03_execution/ACCEPTANCE_CHECKLIST_STAGE_03.md new file mode 100644 index 0000000..d4592dd --- /dev/null +++ b/docs/accounting-assistant/accounting-assistant/03_execution/ACCEPTANCE_CHECKLIST_STAGE_03.md @@ -0,0 +1,687 @@ +# ACCEPTANCE_CHECKLIST_STAGE_03 + +## Назначение документа + +Этот документ используется для приёмки реализации Stage 3. +Его задача — не проверить “что-то поменялось”, а убедиться, что: + +- lifecycle formalization реально внедрена в runtime; +- текущий scope не расползся; +- lifecycle-модели не остались формальными таблицами; +- problem units, ranking и answer реально используют lifecycle; +- заложена корректная база для Stage 4. + +Документ обязателен для: +- Codex; +- разработчика; +- ручного review; +- финальной фиксации результата по Stage 3. + +--- + +## Статус документа + +- Статус: чеклист приёмки Stage 3 +- Язык: русский +- Режим использования: обязателен при завершении каждой волны и при финальной приёмке Stage 3 +- При конфликте по scope приоритет имеет `STAGE_03_TASK_CARD.md` +- При конфликте по архитектурным ограничениям приоритет имеет `ARCHITECTURE_GUARDRAILS.md` +- При конфликте по platform logic приоритет имеет `TZ_Platform_Core_Accounting_Assistant_Mode.md` + +--- + +## Правила оценки + +Для каждого пункта допускаются только следующие статусы: + +- `PASS` — выполнено полностью +- `PARTIAL` — выполнено частично, требуется доработка +- `FAIL` — не выполнено +- `N/A` — не применимо, только если это действительно обосновано + +Для каждого пункта должен быть указан комментарий: +- что проверялось; +- где это реализовано; +- чем подтверждается; +- какие ограничения остались. + +--- + +## Общая логика приёмки + +Stage 3 считается принятым только если одновременно соблюдены условия: + +1. Закрыт именно Stage 3, а не “произвольный улучшенный вариант”. +2. Текущий рабочий контур не разрушен. +3. Есть формальные lifecycle-модели по целевым доменам. +4. Есть рабочий lifecycle runtime. +5. Problem units реально обогащаются lifecycle-полями. +6. Ranking использует lifecycle-дефекты по смыслу вопроса. +7. Ответы объясняют проблему через state/transition logic. +8. Есть benchmark/eval и before/after evidence. +9. Нет скрытого выезда в Stage 4–6. +10. Изменения совместимы с platform core. + +Если хотя бы один из этих пунктов провален, Stage 3 не считается завершённым. + +--- + +# Блок A. Scope discipline + +## A1. Реализован именно Stage 3 +Статус: +Комментарий: + +Проверка: +- реализованы lifecycle formalization изменения; +- не добавлена скрытая логика следующих этапов; +- улучшения соответствуют текущему scope. + +Критерии PASS: +- все ключевые изменения относятся к Stage 3; +- нет “заодно реализованных” future-stage подсистем. + +--- + +## A2. Нет скрытого выезда в Stage 4 +Статус: +Комментарий: + +Проверка: +- не внедрён полноценный ontology/graph runtime; +- нет graph-first core path; +- graph не стал обязательной зависимостью answer flow. + +Критерии PASS: +- максимум заложена совместимость; +- полноценный Stage 4 runtime не реализован. + +--- + +## A3. Нет скрытого выезда в Stage 5 +Статус: +Комментарий: + +Проверка: +- не внедрён full investigation engine; +- нет полноценного branching case-runtime; +- нет сложного bounded investigation orchestration. + +Критерии PASS: +- Stage 5 логика не реализована как текущий core-runtime. + +--- + +## A4. Нет скрытого выезда в Stage 6 +Статус: +Комментарий: + +Проверка: +- не внедрён live verification core path; +- нет full product mode split `direct / investigation / audit`; +- нет полноценного trust-state live contour. + +Критерии PASS: +- Stage 6 логика не реализована как текущий рабочий слой. + +--- + +## A5. Не выполнен большой ненужный рефактор +Статус: +Комментарий: + +Проверка: +- не переписан transport layer без необходимости; +- не переписан endpoint layer без необходимости; +- не переписан base routing без необходимости; +- не переписан assistant loop ради архитектурной красоты. + +Критерии PASS: +- изменения локальны и обоснованы; +- рабочий контур сохранён. + +--- + +# Блок B. Lifecycle models + +## B1. Есть lifecycle_domain registry по целевым доменам +Статус: +Комментарий: + +Проверка: +- покрыты `bank_settlement`, `customer_settlement`, `deferred_expense`, `fixed_asset`, `vat_flow`, `period_close`; +- для доменов определены поддерживаемые объекты. + +Критерии PASS: +- registry существует и используется runtime. + +--- + +## B2. Состояния описаны формально и операционно +Статус: +Комментарий: + +Проверка: +- у состояний есть `entry_conditions` и `exit_conditions`; +- у состояний есть business смысл; +- состояния не абстрактны и распознаваемы по данным. + +Критерии PASS: +- state-модель применима к реальным retrieval данным. + +--- + +## B3. Переходы описаны с evidence requirements +Статус: +Комментарий: + +Проверка: +- есть `required_evidence` и `forbidden_conditions`; +- transition logic пригодна для runtime resolution. + +Критерии PASS: +- переходы можно проверить автоматически. + +--- + +## B4. Есть lifecycle defect catalog +Статус: +Комментарий: + +Проверка: +- покрыты базовые defect classes; +- у дефектов есть severity/business meaning; +- дефекты привязаны к evidence requirements. + +Критерии PASS: +- дефекты классифицируются по данным, а не вручную. + +--- + +## B5. Соблюдено правило масштаба lifecycle_object +Статус: +Комментарий: + +Проверка: +- lifecycle применяется к объекту правильного масштаба; +- не используется слишком грубая сущность уровня “контрагент в целом”. + +Критерии PASS: +- resolution имеет предметный объект и контекст. + +--- + +# Блок C. Lifecycle runtime + +## C1. Реализован и используется `LifecycleRegistry` +Статус: +Комментарий: + +Проверка: +- registry является source of truth; +- runtime читает определения из registry. + +Критерии PASS: +- нет дублирующей логики “по месту”. + +--- + +## C2. Реализован и используется `LifecycleResolver` +Статус: +Комментарий: + +Проверка: +- resolver вычисляет `current` и `expected` state; +- resolver определяет missing/invalid transitions; +- resolver отдаёт confidence и limitations. + +Критерии PASS: +- resolution работает на runtime-данных. + +--- + +## C3. Реализован `LifecycleDefectClassifier` +Статус: +Комментарий: + +Проверка: +- classifier переводит несоответствия в defect types; +- классификация воспроизводима и тестируема. + +Критерии PASS: +- defect typing не остаётся свободной интерпретацией. + +--- + +## C4. Реализован `LifecycleEnricher` +Статус: +Комментарий: + +Проверка: +- enrichment происходит на runtime-пути; +- enrichment добавляет lifecycle поля в problem units. + +Критерии PASS: +- enriched unit не является “мёртвой” сущностью. + +--- + +## C5. Runtime устойчив к неполным данным +Статус: +Комментарий: + +Проверка: +- есть честная обработка ограниченного evidence; +- нет ложной уверенности при слабой опоре. + +Критерии PASS: +- uncertainty/limitations явно фиксируются. + +--- + +# Блок D. Integration with problem units + +## D1. Обновлён `problem_unit_schema` +Статус: +Комментарий: + +Проверка: +- присутствуют lifecycle-поля Stage 3; +- схема совместима с существующим Stage 2 контуром. + +Критерии PASS: +- problem unit содержит lifecycle-смысл. + +--- + +## D2. Lifecycle enrichment реально участвует в runtime +Статус: +Комментарий: + +Проверка: +- enrichment выполняется на рабочем пути; +- lifecycle-данные доходят до answer слоя. + +Критерии PASS: +- lifecycle не ограничен логами или служебным payload. + +--- + +## D3. Механика дефекта видна в unit +Статус: +Комментарий: + +Проверка: +- в unit видны state/transition mismatch; +- `missing_transition`, `invalid_transition`, `lifecycle_defect_type` доступны downstream. + +Критерии PASS: +- problem unit объясняет “что сломано” и “на какой стадии сломано”. + +--- + +# Блок E. Ranking integration + +## E1. Lifecycle factors добавлены в ranking policy +Статус: +Комментарий: + +Проверка: +- есть lifecycle severity, stale duration, period impact и связанные факторы; +- ranking учитывает lifecycle confidence. + +Критерии PASS: +- ranking использует lifecycle-сигналы явно. + +--- + +## E2. Ranking не сводится к сумме/объёму +Статус: +Комментарий: + +Проверка: +- для risk/stale/lifecycle-запросов lifecycle weight выше magnitude-only сигналов. + +Критерии PASS: +- порядок выдачи меняется по смыслу вопроса. + +--- + +## E3. Есть evidence улучшения ranking +Статус: +Комментарий: + +Проверка: +- есть before/after примеры; +- видно снижение entity-heavy leakage. + +Критерии PASS: +- изменение ranking подтверждено измеримо. + +--- + +# Блок F. Answer quality + +## F1. Ответы объясняют lifecycle-логику +Статус: +Комментарий: + +Проверка: +- ответ показывает текущую стадию и ожидаемую стадию; +- ответ показывает проблемный или отсутствующий переход. + +Критерии PASS: +- есть state/transition reasoning, а не общие labels. + +--- + +## F2. Появилась предметная business-интерпретация +Статус: +Комментарий: + +Проверка: +- объяснено бухгалтерское значение дефекта; +- указано влияние на период/расчёты/вычет/амортизацию при релевантности. + +Критерии PASS: +- ответ операбелен для бухгалтера. + +--- + +## F3. Есть связь вывода с evidence +Статус: +Комментарий: + +Проверка: +- в объяснении присутствуют документы/проводки/регистры как опора; +- нет оторванных от evidence выводов. + +Критерии PASS: +- claim и evidence связаны прозрачно. + +--- + +## F4. Generic lifecycle labels не доминируют +Статус: +Комментарий: + +Проверка: +- ответы не ограничиваются “broken_lifecycle”, “неполно подтверждено” и подобными формулировками. + +Критерии PASS: +- ответы стали stage-aware и механизмно объяснимыми. + +--- + +# Блок G. Eval / quality + +## G1. Есть benchmark suite по covered domains +Статус: +Комментарий: + +Проверка: +- benchmark покрывает 51/60, 97, ОС, НДС, period close; +- кейсы воспроизводимы. + +Критерии PASS: +- benchmark можно использовать для повторной проверки. + +--- + +## G2. Добавлены lifecycle resolution tests +Статус: +Комментарий: + +Проверка: +- тестируется определение current/expected states; +- тестируются missing/invalid transitions. + +Критерии PASS: +- critical resolution логика покрыта тестами. + +--- + +## G3. Добавлены defect classification tests +Статус: +Комментарий: + +Проверка: +- тестируется классификация основных defect types. + +Критерии PASS: +- defect classifier проверяется автоматически. + +--- + +## G4. Добавлены lifecycle-aware explanation tests +Статус: +Комментарий: + +Проверка: +- тестируется шаблон и содержание lifecycle-объяснения; +- проверяется отсутствие ухода в generic labels при успешном resolution. + +Критерии PASS: +- answer слой закреплён тестами. + +--- + +## G5. Подготовлен before/after eval report +Статус: +Комментарий: + +Проверка: +- есть baseline; +- есть результаты после внедрения Stage 3; +- видно, что именно улучшилось и где остались ограничения. + +Критерии PASS: +- улучшения подтверждены измеримо. + +--- + +# Блок H. Observability / compatibility + +## H1. Lifecycle-решения диагностируемы +Статус: +Комментарий: + +Проверка: +- можно увидеть, как было разрешено состояние; +- можно увидеть причину классификации дефекта. + +Критерии PASS: +- есть минимальная наблюдаемость новых решений. + +--- + +## H2. Новые контракты описаны явно +Статус: +Комментарий: + +Проверка: +- lifecycle contracts формализованы; +- поля и назначение понятны; +- source of truth определён. + +Критерии PASS: +- нет неявной архитектуры “между строк”. + +--- + +## H3. Изменения не создают тупик для Stage 4–6 +Статус: +Комментарий: + +Проверка: +- решения Stage 3 не блокируют graph/investigation/live развитие; +- нет временных схем, выдаваемых за целевые. + +Критерии PASS: +- путь к следующим этапам открыт. + +--- + +## H4. Обратная совместимость и миграции понятны +Статус: +Комментарий: + +Проверка: +- если появились новые контракты/хранилища/схемы, описано как они инициализируются; +- понятно, нужен ли migration step. + +Критерии PASS: +- внедрение повторяемо и сопровождаемо. + +--- + +# Блок I. Documentation completeness + +## I1. Созданы спецификационные артефакты Stage 3 +Статус: +Комментарий: + +Проверка: +- есть domain/state/transition/defect спецификации; +- есть lifecycle object mapping спецификация. + +Критерии PASS: +- спецификационные артефакты существуют и актуальны. + +--- + +## I2. Созданы runtime-артефакты Stage 3 +Статус: +Комментарий: + +Проверка: +- реализованы и задокументированы `LifecycleRegistry`, `LifecycleResolver`, `LifecycleDefectClassifier`, `LifecycleEnricher`. + +Критерии PASS: +- runtime-артефакты доступны и применяются. + +--- + +## I3. Есть acceptance mapping +Статус: +Комментарий: + +Проверка: +- для каждого ключевого изменения указано, какой критерий Stage 3 оно закрывает. + +Критерии PASS: +- изменения привязаны к acceptance criteria. + +--- + +## I4. Есть список сознательно не реализованного +Статус: +Комментарий: + +Проверка: +- явно перечислено, что не делалось сейчас; +- причины отложенных вещей зафиксированы; +- нет скрытого scope drift. + +Критерии PASS: +- границы текущего этапа прозрачны. + +--- + +# Блок J. Финальное решение по этапу + +## J1. Stage 3 можно считать принятым +Статус: +Комментарий: + +Критерии PASS: +- блоки A–I не содержат критических FAIL; +- PARTIAL не влияют на core acceptance; +- lifecycle formalization реально работает в runtime. + +--- + +## J2. Stage 3 нельзя считать принятым +Статус: +Комментарий: + +Ставится `PASS`, если выполнено хотя бы одно из условий: +- lifecycle-модели остались только в документации; +- lifecycle не участвует в problem units/ranking/answer; +- дефекты не классифицируются автоматически; +- ответы остаются generic; +- benchmark/eval отсутствует; +- был скрытый выезд в Stage 4–6; +- рабочий контур сломан. + +--- + +# Итоговая сводка по приёмке + +## Общий итог +- Результат: `PASS / PARTIAL / FAIL` +- Дата проверки: +- Проверял: +- Версия / ветка / commit: +- Связанные документы: + +--- + +## Ключевые сильные стороны +1. +2. +3. + +--- + +## Ключевые недочёты +1. +2. +3. + +--- + +## Что обязательно исправить до приёмки +1. +2. +3. + +--- + +## Что допустимо перенести в следующий этап +1. +2. +3. + +--- + +## Явно подтверждено как non-scope текущего этапа +1. +2. +3. + +--- + +## Финальное решение +- `Принять Stage 3` +- `Принять Stage 3 условно` +- `Вернуть на доработку` + +Комментарий: + +--- + +# Короткая практическая формула + +Stage 3 считается успешным не тогда, когда: + +- появились новые lifecycle labels; +- ответы стали длиннее; +- тесты стали зелёными. + +Stage 3 считается успешным тогда, когда одновременно: + +- lifecycle-модель реально работает на runtime-данных; +- problem units, ranking и answer используют lifecycle-логику; +- пользователь получает объяснение, какая стадия нарушена и почему; +- ценность улучшений подтверждена benchmark/eval. diff --git a/docs/accounting-assistant/accounting-assistant/03_execution/ARCHITECTURE_GUARDRAILS.md b/docs/accounting-assistant/accounting-assistant/03_execution/ARCHITECTURE_GUARDRAILS.md index e794062..0285d78 100644 --- a/docs/accounting-assistant/accounting-assistant/03_execution/ARCHITECTURE_GUARDRAILS.md +++ b/docs/accounting-assistant/accounting-assistant/03_execution/ARCHITECTURE_GUARDRAILS.md @@ -1,4 +1,4 @@ -ARCHITECTURE_GUARDRAILS.md +ARCHITECTURE_GUARDRAILS.md # ARCHITECTURE_GUARDRAILS @@ -9,14 +9,14 @@ ARCHITECTURE_GUARDRAILS.md Документ нужен, чтобы: - не допустить расползания scope; -- не дать текущей реализации преждевременно превратиться в Stage 2–6; +- не дать текущей реализации преждевременно превратиться в Stage 4–6; - не допустить появления скрытых костылей под видом “улучшения архитектуры”; - удержать изменения в рамках текущего этапа; - сохранить совместимость с будущим развитием системы. Документ не заменяет: - `CODEX_MASTER_BRIEF.md` -- `STAGE_01_TASK_CARD.md` +- `STAGE_03_TASK_CARD.md` - `TZ_Platform_Core_Accounting_Assistant_Mode.md` - этапные ТЗ @@ -29,7 +29,7 @@ ARCHITECTURE_GUARDRAILS.md - Статус: обязательный архитектурный ограничитель - Язык: русский - Режим использования: обязателен к применению до любых кодовых изменений -- При конфликте с текущим scope приоритет имеет `STAGE_01_TASK_CARD.md` +- При конфликте с текущим scope приоритет имеет `STAGE_03_TASK_CARD.md` - При конфликте по платформенным ограничениям приоритет имеет `TZ_Platform_Core_Accounting_Assistant_Mode.md` --- @@ -121,7 +121,7 @@ ARCHITECTURE_GUARDRAILS.md - уже действующий assistant loop. Разрешены только точечные изменения, если они: -- прямо обязательны для Stage 1; +- прямо обязательны для Stage 3; - не могут быть внесены более локально. --- @@ -138,7 +138,7 @@ ARCHITECTURE_GUARDRAILS.md --- -### 3. Не внедрять преждевременно Stage 2–6 +### 3. Не внедрять преждевременно Stage 4–6 До наступления соответствующих этапов запрещено внедрять как core-runtime: @@ -313,7 +313,7 @@ Evidence должно иметь хотя бы минимально явную Если да — выбирается более локальный вариант. ### Вопрос 3 -Это не тянет Stage 2–6 раньше времени? +Это не тянет Stage 4–6 раньше времени? Если тянет — изменение откладывается или упрощается. @@ -495,7 +495,7 @@ Evidence должно иметь хотя бы минимально явную 1. Проверить соответствие текущему scope 2. Проверить соответствие platform core ТЗ -3. Проверить, не тянет ли изменение Stage 2–6 +3. Проверить, не тянет ли изменение Stage 4–6 4. Проверить, можно ли сделать локальнее 5. Зафиксировать риски 6. Только после этого принимать решение @@ -530,4 +530,4 @@ Evidence должно иметь хотя бы минимально явную **не сделать видимость зрелой системы, а реально уменьшить structural debt и подготовить прочную основу для следующих шагов.** -Любое изменение, которое противоречит этому принципу, должно считаться ошибочным, даже если оно выглядит “умным”, “масштабируемым” или “красивым”. \ No newline at end of file +Любое изменение, которое противоречит этому принципу, должно считаться ошибочным, даже если оно выглядит “умным”, “масштабируемым” или “красивым”. diff --git a/docs/accounting-assistant/accounting-assistant/03_execution/CODEX_MASTER_BRIEF.md b/docs/accounting-assistant/accounting-assistant/03_execution/CODEX_MASTER_BRIEF.md index f7c5ebf..a25816c 100644 --- a/docs/accounting-assistant/accounting-assistant/03_execution/CODEX_MASTER_BRIEF.md +++ b/docs/accounting-assistant/accounting-assistant/03_execution/CODEX_MASTER_BRIEF.md @@ -1,6 +1,4 @@ -CODEX_MASTER_BRIEF.md - -# CODEX_MASTER_BRIEF +# CODEX_MASTER_BRIEF ## Назначение документа @@ -22,14 +20,14 @@ CODEX_MASTER_BRIEF.md - Статус: основной управляющий бриф для Codex - Язык: русский - Режим использования: обязателен к прочтению перед любыми изменениями в коде -- При конфликте с рабочим scope текущей итерации приоритет имеет `STAGE_01_TASK_CARD.md` +- При конфликте с рабочим scope текущей итерации приоритет имеет `STAGE_03_TASK_CARD.md` - При конфликте по архитектурным ограничениям приоритет имеет `TZ_Platform_Core_Accounting_Assistant_Mode.md` --- ## Контекст проекта -Разрабатывается бухгалтерский ассистент, который уже находится в рабочем состоянии на уровне функционального MVP+ и способен: +Разрабатывается бухгалтерский ассистент, который находится в рабочем состоянии на уровне функционального MVP+ и способен: - принимать пользовательские вопросы; - обращаться к имеющимся контурам данных; @@ -38,34 +36,30 @@ CODEX_MASTER_BRIEF.md - формировать объяснение; - возвращать ответ пользователю. -При этом текущая система ещё не является полноценным investigation copilot. -Основные текущие ограничения: +При этом текущая система ещё не является полноценным accountant-grade investigation copilot. +На текущем переходе считаем этапы 1 и 2 выполненными и переходим к **Stage 3 / Lifecycle Formalization**. -- snapshot-only truth contour; -- слабая формализация investigation state; -- entity-heavy retrieval; -- недостаточная структурность evidence; -- неполный accountant-facing eval; -- ограниченная управляемость broad / generic query handling; -- отсутствие полноценного bounded investigation runtime; -- отсутствие formal live verification trust model. +Основные текущие ограничения, которые Stage 3 должен закрыть: -Проект развивается по поэтапной схеме. -На текущей итерации реализуется только **Stage 1 / Foundation Hardening**. -Этапы 2–6 задают forward-compatibility constraints, но не являются scope текущей реализации. +- lifecycle-семантика остаётся частично эвристической; +- отсутствует формализованная модель допустимых состояний/переходов по ключевым доменам; +- problem units недостаточно насыщены temporal и stage-based смыслом; +- ranking по ряду классов вопросов всё ещё тяготеет к frequency/sum/entity сигналам; +- ответы местами остаются на уровне generic lifecycle labels. --- ## Цель работы Codex на текущей итерации -Codex должен помочь реализовать **только Stage 1**, не разрушая текущий работающий контур и не подтягивая prematurely решения из следующих этапов. +Codex должен помочь реализовать **только Stage 3**, не разрушая текущий рабочий контур и не подтягивая prematurely решения из следующих этапов. Текущая цель: -- усилить существующий assistant mode; -- сделать архитектурно корректную базу для следующих этапов; -- убрать наиболее опасные structural gaps; -- не превращать текущий этап в скрытую реализацию Stage 2–6. +- ввести формальную lifecycle-модель по целевым доменам Stage 3; +- внедрить lifecycle runtime-компоненты и их использование в рабочем пути; +- интегрировать lifecycle в problem units, ranking и answer synthesis; +- подтвердить полезность через domain-eval и before/after проверку; +- не превращать текущий этап в скрытую реализацию Stage 4–6. --- @@ -74,7 +68,7 @@ Codex должен помочь реализовать **только Stage 1**, При чтении и интерпретации материалов использовать следующий порядок приоритета. ### 1. Текущий рабочий scope -- `03_execution/STAGE_01_TASK_CARD.md` +- `03_execution/STAGE_03_TASK_CARD.md` Это главный документ по тому, что делать прямо сейчас. @@ -90,12 +84,17 @@ Codex должен помочь реализовать **только Stage 1**, - security; - live bridge policy. -### 3. Детальное ТЗ первого этапа -- `02_stages/stage-01-foundation-hardening.md` +### 3. Детальное ТЗ третьего этапа +- `02_stages/TZ_Stage_3_Lifecycle_Formalization_Assistant_Mode.md` -Этот документ определяет содержимое Stage 1. +Этот документ определяет содержимое Stage 3. -### 4. Текущий статус и общая логика развития +### 4. Зависимости Stage 3 +- `02_stages/TZ_Stage_2_Retrieval_Unit_Shift_Assistant_Mode.md` + +Stage 3 опирается на problem-centric слой Stage 2 и не должен его ломать. + +### 5. Текущий статус и общая логика развития - `00_context/Assistant_Mode_GLOBAL_STATUS_2026-03-24.md` - `00_context/Assistant_Mode_GLOBAL_STATUS_Appendix_2026-03-24.md` - `00_context/ROADMAP_endToReal.md` @@ -104,15 +103,13 @@ Codex должен помочь реализовать **только Stage 1**, Эти документы нужны для понимания: - что уже сделано; - где реальные потолки системы; -- почему Stage 1 выполняется именно сейчас; -- как Stage 1 стыкуется с дальнейшими этапами. +- почему сейчас выполняется Stage 3; +- как Stage 3 стыкуется с дальнейшими этапами. -### 5. Этапы 2–6 -- `02_stages/stage-02-...` -- `02_stages/stage-03-...` -- `02_stages/stage-04-...` -- `02_stages/stage-05-...` -- `02_stages/stage-06-...` +### 6. Этапы 4–6 +- `02_stages/TZ_Stage_4_...` +- `02_stages/TZ_Stage_5_...` +- `02_stages/TZ_Stage_6_...` Эти документы используются только как: - ограничители будущей совместимости; @@ -125,18 +122,18 @@ Codex должен помочь реализовать **только Stage 1**, ## Scope текущей итерации -Разрешено делать только то, что относится к Stage 1 и необходимо для его корректной реализации. +Разрешено делать только то, что относится к Stage 3 и необходимо для его корректной реализации. К текущему scope относятся: -- усиление foundation layer без переписывания всей системы; -- минимально необходимая формализация `investigation_state`; -- усиление answer policy; -- усиление broad-query / generic-query handling; -- более структурное представление evidence; -- accountant-facing metrics; -- baseline benchmark/eval harness; -- подготовка базы для следующих этапов без преждевременной реализации этих этапов. +- формализация lifecycle-доменов и lifecycle-сущностей Stage 3; +- описание states/transitions/defects с привязкой к доступным evidence; +- реализация runtime-слоя (`LifecycleRegistry`, `LifecycleResolver`, `LifecycleDefectClassifier`, `LifecycleEnricher`); +- обновление `problem_unit_schema` lifecycle-полями; +- интеграция lifecycle-факторов в ranking policy; +- интеграция lifecycle-логики в answer policy; +- lifecycle-aware тесты и benchmark контур по ключевым доменам; +- before/after eval отчёт по продуктовой ценности Stage 3. --- @@ -144,13 +141,12 @@ Codex должен помочь реализовать **только Stage 1**, На этой итерации нельзя фактически реализовывать как core-runtime следующие слои: -- полноценный problem unit architecture из Stage 2; -- полноценный lifecycle engine из Stage 3; - полноразмерный ontology / graph runtime из Stage 4; -- investigation engine в полном виде из Stage 5; +- полноценный investigation orchestrator из Stage 5; - live verification runtime core и full product mode split из Stage 6; - переезд на новую полную сервисную архитектуру; - переписывание ассистента вокруг новых abstraction layers без крайней необходимости; +- домены, которые не поддерживаются текущими данными/evidence mapping; - большие инфраструктурные переделки ради “красоты”. --- @@ -158,20 +154,20 @@ Codex должен помочь реализовать **только Stage 1**, ## Главный принцип текущей работы **Не строить целевую систему раньше времени.** -Нужно не “сразу сделать правильно всё”, а “сделать Stage 1 так, чтобы он был структурно корректен, совместим с будущими этапами и не создал новые архитектурные долги”. +Нужно сделать Stage 3 так, чтобы lifecycle-модели были не формальными таблицами, а реально работающим runtime-слоем и базой для следующих этапов. --- ## Жёсткие архитектурные ограничения ### 1. Нельзя ломать текущий рабочий контур без прямой причины -Если существующий transport / endpoint / base routing / normalizer pipeline работает, он должен сохраняться, если только изменение не является обязательным условием Stage 1. +Если существующий transport / endpoint / base routing / normalizer pipeline работает, он должен сохраняться, если только изменение не является обязательным условием Stage 3. ### 2. Нельзя подменять архитектурные изменения промптами -Проблемы state, evidence structure, eval, traceability, narrowing и boundedness не должны решаться только промптами или “умной формулировкой ответа”. +Проблемы lifecycle-state, transition logic, defect classification, ranking integration и answer grounding не должны решаться только промптами или “умной формулировкой ответа”. -### 3. Нельзя преждевременно тащить Stage 2–6 в кодовую базу -Если какое-либо изменение фактически реализует future-stage runtime, оно должно быть отклонено или отложено, если не доказана его необходимость для Stage 1. +### 3. Нельзя преждевременно тащить Stage 4–6 в кодовую базу +Если какое-либо изменение фактически реализует future-stage runtime, оно должно быть отклонено или отложено, если не доказана его необходимость для Stage 3. ### 4. Нельзя делать большие рефакторы ради абстрактной чистоты Разрешены только те изменения, которые: @@ -179,23 +175,15 @@ Codex должен помочь реализовать **только Stage 1**, - повышают устойчивость текущего слоя; - не разрушают траекторию дальнейшего развития. -### 5. Все новые сущности должны быть future-compatible -Любые новые: -- типы, -- storage contracts, -- runtime state contracts, -- evidence models, -- metric payloads, -- trace structures +### 5. Каждый lifecycle-элемент обязан иметь полный контур реализации +Для каждого lifecycle-элемента должны существовать: +- spec-level описание; +- runtime-level вычисление; +- retrieval/ranking-level использование; +- answer-level интерпретация. -должны проектироваться так, чтобы не конфликтовать со следующими этапами. - -### 6. Нельзя маскировать structural gaps perceived-quality улучшениями -Недопустимо заменять структурное решение: -- более длинным ответом, -- более “умным” summarization, -- более агрессивной промптовой маршрутизацией, -- косметическим улучшением вывода. +### 6. Нельзя вводить состояния и дефекты без evidence mapping +Если состояние/переход/дефект нельзя определить по реально доступным данным, его нельзя вводить как runtime-элемент Stage 3. --- @@ -207,14 +195,14 @@ Codex должен помочь реализовать **только Stage 1**, Сначала изучить: - текущий статус; - platform core ТЗ; -- Stage 1; +- Stage 3; +- зависимость от Stage 2; - roadmap; - контекст следующих этапов. ### Шаг B. Анализ текущего кода До внесения изменений определить: -- какие части системы уже существуют; -- какие из требований Stage 1 уже частично реализованы; +- какие части lifecycle already/partially реализованы; - где находятся реальные точки расширения; - какие элементы являются хрупкими; - какие изменения потребуют новых contracts; @@ -233,7 +221,7 @@ Codex должен помочь реализовать **только Stage 1**, Только после плана переходить к реализации. Изменения должны вноситься малыми порциями, чтобы можно было проверить: -- не вышел ли scope за Stage 1; +- не вышел ли scope за Stage 3; - не сломан ли текущий контур; - не появились ли premature abstractions. @@ -255,20 +243,20 @@ Codex должен помочь реализовать **только Stage 1**, ### 1. Summary текущего состояния Краткое описание того, как текущая реализация устроена по коду. -### 2. Gap analysis относительно Stage 1 -Перечень того, чего не хватает для соответствия Stage 1. +### 2. Gap analysis относительно Stage 3 +Перечень того, чего не хватает для соответствия Stage 3. ### 3. Предлагаемый file-level plan Какие файлы нужно менять, создавать или расширять. ### 4. Предлагаемые contracts / types / schemas -Какие сущности и интерфейсы появятся. +Какие lifecycle-сущности и интерфейсы появятся. ### 5. Test plan Какие тесты будут добавлены или обновлены. ### 6. Acceptance mapping -Какие критерии Stage 1 покрываются какими изменениями. +Какие критерии Stage 3 покрываются какими изменениями. ### 7. Explicit non-scope Что сознательно не будет делаться сейчас. @@ -284,7 +272,7 @@ Codex должен помочь реализовать **только Stage 1**, 1. Что было проанализировано 2. Что обнаружено 3. Что предлагается изменить -4. Почему это соответствует Stage 1 +4. Почему это соответствует Stage 3 5. Что не входит в текущий scope 6. Какие файлы затрагиваются 7. Какие риски есть @@ -295,7 +283,7 @@ Codex должен помочь реализовать **только Stage 1**, - это локальное изменение или системное; - ломает ли оно обратную совместимость; - требует ли миграции; -- влияет ли на transport / routing / state / answer composition; +- влияет ли на transport / routing / problem assembly / ranking / answer composition; - как это соотносится с будущими этапами. --- @@ -307,21 +295,22 @@ Codex должен помочь реализовать **только Stage 1**, ### 2. Явные contracts Всё, что касается: -- state, -- evidence, -- traceability, -- metrics, -- runtime decisions +- lifecycle states/transitions/defects; +- lifecycle resolution; +- enrichment contracts; +- ranking factors; +- answer interpretation; +- quality metrics должно оформляться через явные контракты, а не “как получится по месту”. ### 3. Контролируемая расширяемость Расширяемость допустима, но только в той мере, в которой она: -- реально нужна Stage 1; +- реально нужна Stage 3; - не заставляет внедрять всю будущую архитектуру заранее. ### 4. Наблюдаемость изменений -Если добавляется новая логика, нужно продумать: +Если добавляется новая lifecycle-логика, нужно продумать: - как она тестируется; - как она логируется; - как проверяется её корректность; @@ -340,15 +329,13 @@ Codex должен помочь реализовать **только Stage 1**, Следующие действия считаются ошибочными: -- попытка “сразу построить конечную архитектуру”; -- внедрение лишних сервисов без необходимости; -- скрытая реализация future-stage логики под видом Stage 1; -- замена structural fixes косметикой; +- “красивые lifecycle-таблицы” без рабочего resolver; +- lifecycle-поля в логах без влияния на ranking/answer; +- ответы вида “broken_lifecycle” без state/transition логики; +- скрытая реализация Stage 4–6 под видом Stage 3; - создание новых абстракций без runtime-пользы; - переписывание рабочего контура ради абстрактной чистоты; -- смешивание temporary workaround и target architecture без явной маркировки; - неявное изменение scope; -- неконтролируемая генерация “умных” helper layers; - перенос ответственности за структурный пробел в prompt layer. --- @@ -357,14 +344,12 @@ Codex должен помочь реализовать **только Stage 1**, Если в процессе работы появляется одно или несколько из следующих явлений, нужно остановиться и пересобрать plan: -- предлагается большой platform refactor для реализации Stage 1; -- предлагается новая архитектура вместо усиления текущей; -- в код начинают подтягиваться сущности из Stages 4–6 как обязательные; -- вводятся новые сервисы, не дающие прямой пользы на текущем шаге; -- “для удобства” переписывается base loop; -- проблема объясняется как решаемая чисто промптом; -- предлагается сложный orchestrator без прямой необходимости; -- формируется новый data model слой без связи с acceptance criteria Stage 1. +- предлагается graph runtime как обязательный путь Stage 3; +- предлагается full investigation orchestration для “удобства”; +- lifecycle-модели проектируются без data/evidence mapping; +- ranking и answer не получают lifecycle-интеграцию; +- для Stage 3 предлагается большой platform refactor; +- формируется новый data model слой без связи с acceptance criteria Stage 3. --- @@ -372,24 +357,24 @@ Codex должен помочь реализовать **только Stage 1**, Текущая волна считается завершённой только если выполнены одновременно все условия: -1. Реализован scope Stage 1, а не произвольный “улучшенный вариант”. +1. Реализован scope Stage 3, а не произвольный “улучшенный вариант”. 2. Текущий рабочий контур не разрушен. -3. Новые state / evidence / metrics contracts описаны явно. +3. Новые lifecycle contracts описаны явно. 4. Есть тесты и/или проверяемые критерии для внесённых изменений. -5. Нет скрытого уезда в Stage 2–6. +5. Нет скрытого уезда в Stage 4–6. 6. Изменения совместимы с platform core ТЗ. 7. Зафиксировано, что сознательно осталось за пределами текущего этапа. --- -## Практическая цель первой итерации +## Практическая цель текущей итерации -Первая итерация должна дать не “идеальную новую систему”, а следующий результат: +Текущая итерация должна дать следующий результат: -- структурно усиленный assistant mode; -- минимальную, но реальную формализацию foundation gaps; -- снижение зависимости от неявной логики и ad hoc поведения; -- более стабильную базу для перехода к следующим этапам. +- lifecycle-aware problem reasoning вместо generic lifecycle labels; +- stage/transition-aware ranking на covered-доменах; +- более прикладные ответы по сценариям 51/60, 97, ОС, НДС и period close; +- рабочий lifecycle runtime-контур, пригодный для дальнейшего развития. --- @@ -409,6 +394,6 @@ Codex должен помочь реализовать **только Stage 1**, Главный вопрос перед любым изменением: -**Это действительно необходимо для Stage 1, или это попытка преждевременно реализовать следующий этап?** +**Это действительно необходимо для Stage 3, или это попытка преждевременно реализовать Stage 4–6?** -Если ответ неочевиден, изменение откладывается и выносится на отдельное согласование. \ No newline at end of file +Если ответ неочевиден, изменение откладывается и выносится на отдельное согласование. diff --git a/docs/accounting-assistant/accounting-assistant/03_execution/STAGE_03_TASK_CARD.md b/docs/accounting-assistant/accounting-assistant/03_execution/STAGE_03_TASK_CARD.md new file mode 100644 index 0000000..171abf7 --- /dev/null +++ b/docs/accounting-assistant/accounting-assistant/03_execution/STAGE_03_TASK_CARD.md @@ -0,0 +1,446 @@ +# STAGE_03_TASK_CARD + +## Назначение документа + +Этот документ фиксирует **рабочий scope третьей итерации реализации** для Codex и разработчика. +Документ не заменяет Stage 3 ТЗ и не заменяет platform core ТЗ. +Его задача — перевести третий этап в **практический implementation scope**, который можно брать в работу без расползания в следующие этапы. + +Документ должен использоваться как основной рабочий ориентир при реализации Stage 3. + +--- + +## Статус документа + +- Статус: рабочая карта реализации Stage 3 +- Язык: русский +- Режим использования: обязателен к прочтению перед любыми изменениями по Stage 3 +- При конфликте по архитектурным ограничениям приоритет имеет `TZ_Platform_Core_Accounting_Assistant_Mode.md` +- При конфликте по общему режиму работы Codex приоритет имеет `CODEX_MASTER_BRIEF.md` + +--- + +## Контекст + +Текущий бухгалтерский ассистент уже работает на уровне функционального MVP+ и после Stage 2 умеет поднимать problem-centric units. +При этом система ещё не умеет стабильно объяснять проблему как нарушение ожидаемого жизненного цикла объекта. + +На текущем этапе нужно **не перестроить ассистента целиком**, а **ввести рабочий lifecycle knowledge layer**, чтобы: + +- формально описать состояния и переходы по целевым доменам; +- вычислять current/expected state на runtime-данных; +- классифицировать lifecycle-дефекты; +- обогащать problem units lifecycle-смыслом; +- улучшить ranking и ответы по логике стадии/перехода; +- не тащить prematurely графовое ядро и full investigation engine. + +--- + +## Цель Stage 3 + +Stage 3 должен дать **lifecycle-aware reasoning слой**, не ломая текущий рабочий контур. + +Практический результат этапа: + +- formal lifecycle models для целевых доменов; +- `LifecycleRegistry` как единый source of truth по lifecycle; +- `LifecycleResolver` и `LifecycleDefectClassifier` в runtime; +- `LifecycleEnricher` для problem units; +- lifecycle-aware ranking factors; +- lifecycle-aware answer policy; +- benchmark/eval контур с before/after evidence по ключевым сценариям. + +--- + +## Scope текущей реализации + +В рамках Stage 3 разрешено реализовывать только то, что необходимо для lifecycle formalization. + +### В scope входят + +1. **Формализация lifecycle-доменов** + - фиксация domain registry; + - описание lifecycle objects нужного масштаба; + - описание states/transitions/defects с business-смыслом; + - обязательный evidence mapping для каждого элемента. + +2. **Runtime-слой lifecycle-разрешения** + - компонент `LifecycleRegistry`; + - компонент `LifecycleResolver`; + - компонент `LifecycleDefectClassifier`; + - компонент `LifecycleEnricher`. + +3. **Интеграция lifecycle в problem units** + - расширение `problem_unit_schema` lifecycle-полями; + - обогащение unit на runtime-пути; + - поддержка confidence и snapshot limitations. + +4. **Интеграция lifecycle в ranking** + - добавление lifecycle-based факторов; + - усиление веса stale/period-impact/cross-branch дефектов по релевантным вопросам; + - снижение зависимости ranking только от суммы/объёма. + +5. **Интеграция lifecycle в answer layer** + - переход на объяснение через current state, expected state и transition logic; + - явная связь вывода с evidence; + - пользовательский next step по месту lifecycle-разрыва. + +6. **Quality контур Stage 3** + - lifecycle resolution tests; + - defect classification tests; + - lifecycle-aware explanation tests; + - benchmark suite по covered domains; + - before/after eval report. + +--- + +## Что не входит в scope + +Следующие вещи **не должны** реализовываться в рамках Stage 3 как core-runtime. + +### Не делать сейчас + +- полный ontology graph runtime из Stage 4; +- полный rule engine по всем типам учёта; +- full investigation orchestrator из Stage 5; +- live verification core runtime из Stage 6; +- full product mode split `direct / investigation / audit`; +- универсальную state machine для всей 1С; +- большой platform refactor ради будущего масштаба; +- замену structural lifecycle-решений косметическими prompt-улучшениями. + +--- + +## Обязательные результаты этапа + +По завершении Stage 3 в системе должны появиться следующие результаты. + +### 1. Рабочие lifecycle-модели по целевым доменам +Должны существовать formal-модели как минимум по доменам Stage 3: + +- `bank_settlement`; +- `customer_settlement`; +- `deferred_expense`; +- `fixed_asset`; +- `vat_flow`; +- `period_close`. + +### 2. Рабочий lifecycle runtime +Должен существовать runtime-контур, который: + +- определяет `current_lifecycle_state`; +- определяет `expected_lifecycle_state`; +- находит `missing_transition` и `invalid_transition`; +- классифицирует lifecycle-дефект; +- отдаёт `lifecycle_confidence` и `snapshot_limitations`. + +### 3. Lifecycle-enriched problem units +Problem units должны стать lifecycle-aware и включать: + +- lifecycle domain; +- lifecycle object; +- stage/transition интерпретацию; +- defect-type; +- business lifecycle interpretation. + +### 4. Lifecycle-aware ranking +Ranking должен учитывать lifecycle severity и не сводиться к entities/sum/count. + +### 5. Lifecycle-aware ответы +Ответы должны объяснять проблему через логику стадии и перехода, а не через generic labels. + +### 6. Измеримость ценности +Должны быть тесты, benchmark и before/after отчёт, подтверждающие улучшение на ключевых сценариях. + +--- + +## Рабочие deliverables от Codex + +Codex должен вернуть не только код, но и набор артефактов. + +### Обязательные deliverables + +1. **Gap analysis по Stage 3** + - чего не хватает в текущем коде; + - что уже есть частично; + - где точки внедрения. + +2. **Implementation plan** + - какие компоненты меняются; + - какие файлы меняются; + - какие сущности появляются; + - что остаётся нетронутым. + +3. **Новые или обновлённые contracts / types / schemas** + - для lifecycle models; + - для lifecycle resolution; + - для enriched problem units; + - для ranking и answer policy; + - для eval/metrics. + +4. **Кодовые изменения** + - малыми контролируемыми порциями; + - без скрытого выезда в Stage 4–6. + +5. **Test / eval changes** + - unit / integration / regression checks; + - benchmark updates; + - before/after проверка ценности. + +6. **Итоговый отчёт по волне** + - что сделано; + - что не сделано сознательно; + - какие риски остались; + - что подготовлено для Stage 4. + +--- + +## Ожидаемые сущности Stage 3 + +Ниже — минимальный набор сущностей, который должен быть введён или формализован. + +### 1. LifecycleDomain +Примерный состав: +- `domain_code` +- `domain_label` +- `supported_object_types` +- `required_evidence_signals` + +### 2. LifecycleObject +Примерный состав: +- `lifecycle_object_id` +- `lifecycle_object_type` +- `lifecycle_domain` +- `source_entities` +- `period_context` +- `account_context` +- `document_context` + +### 3. LifecycleState +Примерный состав: +- `state_code` +- `state_label` +- `state_class` +- `entry_conditions` +- `exit_conditions` +- `is_terminal` +- `is_problematic` +- `business_meaning` + +### 4. LifecycleTransition +Примерный состав: +- `from_state` +- `to_state` +- `transition_type` +- `required_evidence` +- `optional_evidence` +- `forbidden_conditions` +- `business_meaning` + +### 5. LifecycleDefect +Примерный состав: +- `defect_code` +- `defect_class` +- `severity_hint` +- `business_meaning` +- `evidence_requirements` +- `period_impact_potential` + +### 6. LifecycleResolution +Примерный состав: +- `lifecycle_object_id` +- `resolved_current_state` +- `resolved_expected_state` +- `resolved_previous_states` +- `missing_transitions` +- `invalid_transitions` +- `detected_defects` +- `state_confidence` +- `resolution_evidence` +- `snapshot_limitations` + +### 7. LifecycleEnrichedProblemUnit +Примерный состав: +- `problem_unit_id` +- `problem_unit_type` +- `lifecycle_domain` +- `current_lifecycle_state` +- `expected_lifecycle_state` +- `lifecycle_defect_type` +- `missing_transition` +- `invalid_transition` +- `stale_duration` +- `lifecycle_confidence` +- `business_lifecycle_interpretation` + +--- + +## Жёсткие implementation-ограничения + +### 1. Не трогать без необходимости +Без прямой нужды не переписывать: +- transport layer; +- endpoint layer; +- base routing; +- normalizer pipeline; +- рабочий retrieval flow. + +### 2. Не внедрять будущие этапы скрыто +Если предлагаемое изменение: +- требует полноразмерного graph runtime; +- требует full investigation orchestration; +- требует live verification core path, + +то оно не относится к Stage 3 и должно быть отложено. + +### 3. Не моделировать абстрактные состояния +Каждое состояние, переход и дефект должны быть распознаваемы по доступным данным. + +### 4. Не отделять lifecycle от runtime +Lifecycle считается внедрённым только если используется в resolver, ranking и answer layer. + +### 5. Не подменять lifecycle ценность красивым текстом +Смысл Stage 3 — в структурном reasoning, а не в более длинной формулировке ответа. + +--- + +## Порядок работы по Stage 3 + +### Шаг 1. Прочитать материалы +Обязательно прочитать: +- `CODEX_MASTER_BRIEF.md` +- `TZ_Platform_Core_Accounting_Assistant_Mode.md` +- `TZ_Stage_3_Lifecycle_Formalization_Assistant_Mode.md` +- `TZ_Stage_2_Retrieval_Unit_Shift_Assistant_Mode.md` +- status documents +- roadmap + +### Шаг 2. Сделать code-level mapping +Нужно определить: +- где находится текущий lifecycle/signal extraction слой; +- где собирается problem unit; +- где формируется ranking; +- где формируется финальный answer; +- где безопасно подключать lifecycle runtime; +- где подключать benchmark/eval. + +### Шаг 3. Подготовить plan без кода +До начала реализации Codex должен выдать: +- gap analysis; +- file-level plan; +- список новых контрактов; +- список новых тестов; +- список non-scope. + +### Шаг 4. Реализовывать малыми волнами +Рекомендуемая последовательность: + +#### Волна 1 +- спецификация lifecycle domains/states/transitions/defects; +- контракт registry. + +#### Волна 2 +- внедрение `LifecycleRegistry` и `LifecycleResolver`. + +#### Волна 3 +- внедрение `LifecycleDefectClassifier` и `LifecycleEnricher`. + +#### Волна 4 +- интеграция lifecycle в `problem_unit_schema` и ranking. + +#### Волна 5 +- интеграция lifecycle в answer layer. + +#### Волна 6 +- benchmark, before/after eval, acceptance mapping. + +--- + +## Acceptance criteria + +Stage 3 считается закрытым только если выполнены все критерии ниже. + +### A. По моделям +- для каждого целевого домена есть formal lifecycle model; +- states/transitions/defects описаны и привязаны к данным; +- lifecycle-модели не висят отдельно от runtime. + +### B. По runtime +- реализован lifecycle resolver; +- problem units реально обогащаются lifecycle-полями; +- defects вычисляются автоматически по retrieval data. + +### C. По ranking +- lifecycle severity влияет на ranking; +- stale и period-impact defects поднимаются выше при релевантных вопросах; +- ranking больше не сводится к объёму и сумме. + +### D. По ответу +- ответы по covered domains объясняют проблему через state/transition logic; +- generic lifecycle labels уходят на второй план; +- пользователю понятно, какая стадия нарушена. + +### E. По ценности +- на сценариях 51/60, 97, ОС, НДС и period close ответы становятся глубже; +- система различает `stalled`, `misclosed`, `not yet completed`, `contradicted`. + +### F. По защите от формализма +- для каждого lifecycle domain есть working examples; +- есть benchmark cases; +- есть связка `spec -> runtime -> retrieval -> answer`. + +--- + +## Что Codex обязан явно указать в конце работы + +В финальном отчёте по Stage 3 обязательно должны быть разделы: + +1. Что было сделано +2. Какие файлы изменены +3. Какие новые сущности введены +4. Какие тесты добавлены +5. Какие acceptance criteria закрыты +6. Что сознательно НЕ реализовано +7. Какие риски и ограничения остались +8. Что подготовлено для Stage 4 + +--- + +## Красные флаги + +Если в ходе работы появляется одно из следующего, реализацию нужно остановить и пересобрать plan: + +- lifecycle описан только на уровне документации; +- resolver не влияет на problem unit, ranking и answer; +- ответы остаются на generic labels; +- состояние нельзя определить по реальным данным; +- предлагается ранний graph-first runtime; +- предлагается full investigation engine; +- ради Stage 3 предлагается большой рефактор transport/routing. + +--- + +## Definition of Done + +Stage 3 завершён, если одновременно соблюдены все условия: + +- lifecycle formalization реализована как рабочий runtime-слой; +- problem units стали lifecycle-aware; +- ranking учитывает lifecycle-дефекты по смыслу вопроса; +- ответы строятся на state/transition логике; +- есть benchmark и before/after подтверждение ценности; +- текущий рабочий контур не разрушен; +- нет premature implementation из Stage 4–6. + +--- + +## Короткая практическая формула этапа + +**Stage 3 = переход от problem-centric retrieval к lifecycle-aware accounting reasoning.** + +Нужно получить не просто новые labels, а рабочую способность системы объяснять: + +- где объект находится сейчас; +- куда он должен был перейти; +- какой переход не произошёл или произошёл неверно; +- почему это бухгалтерски важно именно в контексте периода и домена. diff --git a/docs/snapshots/toolkit/proxy_stdout.log b/docs/snapshots/toolkit/proxy_stdout.log index 74e8476..9ed2138 100644 --- a/docs/snapshots/toolkit/proxy_stdout.log +++ b/docs/snapshots/toolkit/proxy_stdout.log @@ -184596,3 +184596,40 @@ INFO: 127.0.0.1:55719 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Conte INFO: 127.0.0.1:55722 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content INFO: 127.0.0.1:55725 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content INFO: 127.0.0.1:55727 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55729 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55732 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55733 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55734 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55735 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55737 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55739 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55740 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55741 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55742 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55743 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55745 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55753 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55755 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55756 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55757 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55758 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55759 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55764 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55766 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55767 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55768 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55771 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55774 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55776 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55777 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55780 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55782 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55784 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55785 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55787 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55788 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55789 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55790 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55791 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55792 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content +INFO: 127.0.0.1:55793 - "GET /1c/poll?channel=default HTTP/1.1" 204 No Content diff --git a/llm_normalizer/backend/dist/config.js b/llm_normalizer/backend/dist/config.js index dffe520..c9e533a 100644 --- a/llm_normalizer/backend/dist/config.js +++ b/llm_normalizer/backend/dist/config.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; +exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; const path_1 = __importDefault(require("path")); exports.BACKEND_ROOT = path_1.default.resolve(__dirname, ".."); exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, ".."); @@ -34,6 +34,8 @@ exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = toBooleanFlag(process.env.FEATURE_A exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, false); exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1, false); exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_STAGE2_EVAL_V1, false); +exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1, false); +exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, false); exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data"); exports.TRACES_DIR = path_1.default.resolve(exports.DATA_DIR, "traces"); exports.PRESETS_DIR = path_1.default.resolve(exports.DATA_DIR, "presets"); diff --git a/llm_normalizer/backend/dist/services/answerComposer.js b/llm_normalizer/backend/dist/services/answerComposer.js index 2958b2b..9da853d 100644 --- a/llm_normalizer/backend/dist/services/answerComposer.js +++ b/llm_normalizer/backend/dist/services/answerComposer.js @@ -282,6 +282,59 @@ function formatAffectedScope(unit) { } return scopeParts.join("; "); } +function formatLifecycleScope(unit) { + if (!unit.lifecycle_domain) { + return null; + } + const parts = [`domain=${unit.lifecycle_domain}`]; + if (unit.current_lifecycle_state) { + parts.push(`current=${unit.current_lifecycle_state}`); + } + if (unit.expected_lifecycle_state) { + parts.push(`expected=${unit.expected_lifecycle_state}`); + } + if (unit.lifecycle_defect_type) { + parts.push(`defect=${unit.lifecycle_defect_type}`); + } + if (unit.missing_transition) { + parts.push(`missing_transition=${unit.missing_transition}`); + } + if (unit.invalid_transition) { + parts.push(`invalid_transition=${unit.invalid_transition}`); + } + if (unit.stale_duration) { + parts.push(`stale_duration=${unit.stale_duration}`); + } + return parts.join(", "); +} +function rankProblemUnitsForAnswer(units, lifecycleAnswerEnabled) { + if (!lifecycleAnswerEnabled) { + return units.slice().sort((left, right) => { + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) + return severityDiff; + return right.confidence.score - left.confidence.score; + }); + } + return units.slice().sort((left, right) => { + const lifecycleRankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0); + if (lifecycleRankDiff !== 0) + return lifecycleRankDiff; + const lifecycleConfidenceDiff = (right.lifecycle_confidence?.score ?? 0) - (left.lifecycle_confidence?.score ?? 0); + if (lifecycleConfidenceDiff !== 0) + return lifecycleConfidenceDiff; + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) + return severityDiff; + return right.confidence.score - left.confidence.score; + }); +} +function hasLifecycleResolution(units) { + return units.some((unit) => Boolean(unit.lifecycle_domain) && + Boolean(unit.current_lifecycle_state) && + Boolean(unit.expected_lifecycle_state) && + Boolean(unit.lifecycle_defect_type)); +} function buildProblemCentricActions(input) { const actions = []; const unitTypes = new Set(input.units.map((item) => item.problem_unit_type)); @@ -300,6 +353,17 @@ function buildProblemCentricActions(input) { if (unitTypes.has("lifecycle_anomaly_node")) { actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии."); } + for (const unit of input.units) { + if (unit.lifecycle_defect_type === "stale_active_state") { + actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии."); + } + if (unit.lifecycle_defect_type === "misclosed_state") { + actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути."); + } + if (unit.lifecycle_defect_type === "cross_branch_state_conflict") { + actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния."); + } + } if (input.mode === "clarification_required") { if (input.missingAnchors.period) { actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура."); @@ -657,6 +721,12 @@ function buildDirectAnswer(input) { return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса."; } function buildProblemCentricAnswerSummary(input) { + if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) { + if (input.mode === "clarification_required") { + return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей."; + } + return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`; + } if (input.mode === "clarification_required") { return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса."; } @@ -673,10 +743,23 @@ function buildProblemCentricDirectAnswer(input) { ? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен." : input.weakUnits ? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме." - : "Выделены ключевые проблемные зоны и их влияние на учетный контур."; + : input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units) + ? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода." + : "Выделены ключевые проблемные зоны и их влияние на учетный контур."; const unitLines = input.units.map((unit) => { const scope = formatAffectedScope(unit); - return `- ${unit.title}: ${unit.business_defect_class}; ${scope}; severity=${unit.severity.grade}, confidence=${unit.confidence.grade}.`; + const lifecycleScope = input.lifecycleAnswerEnabled ? formatLifecycleScope(unit) : null; + const lifecycleInterpretation = input.lifecycleAnswerEnabled ? unit.business_lifecycle_interpretation : null; + const segments = [ + `${unit.title}: ${unit.business_defect_class}`, + scope, + lifecycleScope, + lifecycleInterpretation, + `severity=${unit.severity.grade}`, + `confidence=${unit.confidence.grade}`, + unit.lifecycle_confidence ? `lifecycle_confidence=${unit.lifecycle_confidence.grade}` : null + ].filter((item) => Boolean(item)); + return `- ${segments.join("; ")}.`; }); if (unitLines.length === 0) { return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`; @@ -685,6 +768,7 @@ function buildProblemCentricDirectAnswer(input) { } function buildProblemCentricAnswerStructure(input) { const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low"); + const lifecycleEnriched = input.lifecycleAnswerEnabled && hasLifecycleResolution(input.selectedUnits); const unitMechanismNotes = uniqueStrings(input.selectedUnits .map((item) => item.mechanism_summary) .filter((item) => typeof item === "string" && item.trim().length > 0), 6); @@ -724,12 +808,14 @@ function buildProblemCentricAnswerStructure(input) { answer_summary: buildProblemCentricAnswerSummary({ mode: input.mode, weakUnits, - summary: input.problemSummary + summary: input.problemSummary, + lifecycleEnriched }), direct_answer: buildProblemCentricDirectAnswer({ mode: input.mode, units: input.selectedUnits, - weakUnits + weakUnits, + lifecycleAnswerEnabled: input.lifecycleAnswerEnabled }), mechanism_block: { status: mechanismStatus, @@ -836,10 +922,11 @@ function composeAssistantAnswerV11(input) { const mechanismNotes = uniqueStrings(evidenceItems .map((item) => item.mechanism_note) .filter((item) => typeof item === "string" && item.trim().length > 0), 6); + const lifecycleAnswerEnabled = Boolean(input.enableLifecycleAnswerV1); const problemUnits = flattenProblemUnits(input.retrievalResults); const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults); const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type)); - const selectedProblemUnits = problemHeavyUnits.slice(0, 4); + const selectedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled).slice(0, 4); const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults); const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems); const lowConfidenceSignals = evidenceItems.filter((item) => item.confidence === "low").length; @@ -903,8 +990,10 @@ function composeAssistantAnswerV11(input) { groundingCheck: input.groundingCheck, retrievalResults: input.retrievalResults, missingAnchors, - coverageReport: input.coverageReport + coverageReport: input.coverageReport, + lifecycleAnswerEnabled }); + const lifecycleModeActive = lifecycleAnswerEnabled && hasLifecycleResolution(selectedProblemUnits); return { assistant_reply: renderPolicyReply(problemCentricStructure), fallback_type: decision.fallback_type, @@ -912,7 +1001,7 @@ function composeAssistantAnswerV11(input) { answer_structure_v11: problemCentricStructure, problem_centric_answer_applied: true, problem_units_used_count: selectedProblemUnits.length, - problem_answer_mode: "stage2_problem_centric_v1", + problem_answer_mode: lifecycleModeActive ? "stage3_lifecycle_aware_v1" : "stage2_problem_centric_v1", problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id) }; } diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index b328e6b..7dce7a1 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -968,7 +968,8 @@ class AssistantService { coverageReport: coverageEvaluation.coverage, groundingCheck, enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11, - enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 + enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, + enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 }); const answerStructureV11 = config_1.FEATURE_ASSISTANT_CONTRACTS_V11 ? config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11 diff --git a/llm_normalizer/backend/dist/services/lifecycleRuntime.js b/llm_normalizer/backend/dist/services/lifecycleRuntime.js new file mode 100644 index 0000000..6fa7a7d --- /dev/null +++ b/llm_normalizer/backend/dist/services/lifecycleRuntime.js @@ -0,0 +1,824 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LifecycleRegistry = void 0; +exports.classifyLifecycleDefect = classifyLifecycleDefect; +exports.resolveLifecycle = resolveLifecycle; +exports.enrichProblemUnitLifecycle = enrichProblemUnitLifecycle; +exports.rankLifecycleProblemUnits = rankLifecycleProblemUnits; +const stage3Lifecycle_1 = require("../types/stage3Lifecycle"); +function clampUnitScore(value) { + if (!Number.isFinite(value)) { + return 0; + } + if (value <= 0) + return 0; + if (value >= 1) + return 1; + return Number(value.toFixed(2)); +} +function lifecycleConfidenceGrade(score) { + if (score >= 0.75) + return "high"; + if (score >= 0.45) + return "medium"; + return "low"; +} +function uniqueStrings(values, limit = 16) { + return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit); +} +function includesAny(source, patterns) { + return patterns.some((pattern) => pattern.test(source)); +} +function hasToken(values, pattern) { + return values.some((value) => pattern.test(value)); +} +function defaultExpectedState(domain) { + if (domain === "bank_settlement") + return "settlement_closed"; + if (domain === "customer_settlement") + return "receivable_closed"; + if (domain === "deferred_expense") + return "fully_written_off"; + if (domain === "fixed_asset") + return "depreciation_active"; + if (domain === "vat_flow") + return "vat_deducted"; + return "close_completed"; +} +const LIFECYCLE_DOMAIN_MODELS = { + bank_settlement: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "bank_settlement", + lifecycle_object_types: ["payment_settlement_link"], + states: [ + { + state_code: "initiated_payment", + state_label: "Платеж инициирован", + state_class: "initial", + entry_conditions: ["payment_order_created"], + exit_conditions: ["bank_recorded"], + is_terminal: false, + is_problematic: false, + business_meaning: "Есть инициирование платежа." + }, + { + state_code: "bank_recorded", + state_label: "Платеж отражен банком", + state_class: "active", + entry_conditions: ["bank_statement_recorded"], + exit_conditions: ["settlement_linked"], + is_terminal: false, + is_problematic: false, + business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие." + }, + { + state_code: "settlement_closed", + state_label: "Расчет закрыт", + state_class: "terminal", + entry_conditions: ["payment_to_settlement_linked"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Платеж доведен до расчетного результата." + }, + { + state_code: "stale_unlinked_payment", + state_label: "Платеж завис без закрытия", + state_class: "problematic", + entry_conditions: ["bank_recorded", "missing_link"], + exit_conditions: ["settlement_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена." + }, + { + state_code: "misclosed_payment", + state_label: "Платеж закрыт некорректно", + state_class: "problematic", + entry_conditions: ["wrong_document_type_or_posting_mismatch"], + exit_conditions: ["settlement_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Формальное закрытие есть, но путь закрытия неверный." + } + ], + transitions: [ + { + from_state: "initiated_payment", + to_state: "bank_recorded", + transition_type: "expected", + required_evidence: ["bank_statement_recorded"], + optional_evidence: ["payment_order"], + forbidden_conditions: [], + business_meaning: "Платеж должен появиться во выписке." + }, + { + from_state: "bank_recorded", + to_state: "settlement_closed", + transition_type: "expected", + required_evidence: ["payment_to_settlement_link"], + optional_evidence: ["document_to_posting"], + forbidden_conditions: ["wrong_document_type"], + business_meaning: "После выписки должен закрываться расчет." + } + ], + defects: [] + }, + customer_settlement: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "customer_settlement", + lifecycle_object_types: ["receivable_chain"], + states: [ + { + state_code: "invoice_issued", + state_label: "Реализация отражена", + state_class: "initial", + entry_conditions: ["realization_document_exists"], + exit_conditions: ["payment_recorded"], + is_terminal: false, + is_problematic: false, + business_meaning: "Возникла дебиторская позиция." + }, + { + state_code: "payment_recorded", + state_label: "Оплата отражена", + state_class: "active", + entry_conditions: ["payment_document_exists"], + exit_conditions: ["receivable_closed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Оплата есть, ожидается корректное закрытие." + }, + { + state_code: "receivable_closed", + state_label: "Дебиторка закрыта", + state_class: "terminal", + entry_conditions: ["closing_document_linked"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Дебиторская позиция закрыта корректно." + }, + { + state_code: "stale_receivable", + state_label: "Дебиторка зависла", + state_class: "problematic", + entry_conditions: ["unresolved_settlement"], + exit_conditions: ["receivable_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Позиция остается незавершенной дольше ожидаемого." + } + ], + transitions: [ + { + from_state: "invoice_issued", + to_state: "payment_recorded", + transition_type: "expected", + required_evidence: ["payment_document_exists"], + optional_evidence: [], + forbidden_conditions: [], + business_meaning: "После реализации ожидается оплата/зачет." + }, + { + from_state: "payment_recorded", + to_state: "receivable_closed", + transition_type: "expected", + required_evidence: ["closing_document_linked"], + optional_evidence: ["register_movement_exists"], + forbidden_conditions: ["cross_branch_inconsistency"], + business_meaning: "Оплата должна завершаться корректным закрытием расчета." + } + ], + defects: [] + }, + deferred_expense: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "deferred_expense", + lifecycle_object_types: ["deferred_expense_item"], + states: [ + { + state_code: "recognized", + state_label: "РБП признан", + state_class: "initial", + entry_conditions: ["deferred_expense_created"], + exit_conditions: ["writeoff_started"], + is_terminal: false, + is_problematic: false, + business_meaning: "РБП поставлен на учет." + }, + { + state_code: "partially_written_off", + state_label: "Частичное списание", + state_class: "active", + entry_conditions: ["partial_writeoff_exists"], + exit_conditions: ["fully_written_off"], + is_terminal: false, + is_problematic: false, + business_meaning: "Списание идет по графику." + }, + { + state_code: "fully_written_off", + state_label: "РБП полностью списан", + state_class: "terminal", + entry_conditions: ["full_writeoff_exists"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "РБП завершил lifecycle." + }, + { + state_code: "overdue_writeoff", + state_label: "Просроченное списание", + state_class: "problematic", + entry_conditions: ["period_boundary", "missing_link"], + exit_conditions: ["fully_written_off"], + is_terminal: false, + is_problematic: true, + business_meaning: "РБП живет дольше допустимого окна." + } + ], + transitions: [], + defects: [] + }, + fixed_asset: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "fixed_asset", + lifecycle_object_types: ["fixed_asset_card"], + states: [ + { + state_code: "capitalized", + state_label: "Капвложения отражены", + state_class: "initial", + entry_conditions: ["capitalization_document_exists"], + exit_conditions: ["accepted_for_accounting"], + is_terminal: false, + is_problematic: false, + business_meaning: "Объект зафиксирован как вложение." + }, + { + state_code: "accepted_for_accounting", + state_label: "Принят к учету", + state_class: "active", + entry_conditions: ["acceptance_document_exists"], + exit_conditions: ["depreciation_active"], + is_terminal: false, + is_problematic: false, + business_meaning: "Объект переведен в основной контур учета." + }, + { + state_code: "depreciation_active", + state_label: "Амортизация активна", + state_class: "active", + entry_conditions: ["depreciation_register_movement"], + exit_conditions: ["disposed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Жизненный цикл ОС идет штатно." + }, + { + state_code: "contradictory_asset_state", + state_label: "Противоречивый статус ОС", + state_class: "problematic", + entry_conditions: ["posting_mismatch_or_wrong_path"], + exit_conditions: ["depreciation_active"], + is_terminal: false, + is_problematic: true, + business_meaning: "Статус ОС формально есть, но смыслово противоречив." + }, + { + state_code: "disposed", + state_label: "Выбыл", + state_class: "terminal", + entry_conditions: ["disposal_document_exists"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Жизненный цикл ОС завершен." + } + ], + transitions: [], + defects: [] + }, + vat_flow: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "vat_flow", + lifecycle_object_types: ["vat_document_chain"], + states: [ + { + state_code: "vat_registered", + state_label: "НДС отражен документно", + state_class: "initial", + entry_conditions: ["invoice_registered"], + exit_conditions: ["vat_reflected"], + is_terminal: false, + is_problematic: false, + business_meaning: "Сформирован первичный документный слой НДС." + }, + { + state_code: "vat_reflected", + state_label: "НДС отражен в учете", + state_class: "active", + entry_conditions: ["vat_register_movement"], + exit_conditions: ["vat_deducted"], + is_terminal: false, + is_problematic: false, + business_meaning: "НДС проходит штатную стадию отражения." + }, + { + state_code: "vat_deducted", + state_label: "НДС принят к вычету", + state_class: "terminal", + entry_conditions: ["deduction_confirmed"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "НДС-цепочка завершена корректно." + }, + { + state_code: "vat_conflict", + state_label: "Конфликт НДС-цепочки", + state_class: "problematic", + entry_conditions: ["cross_branch_inconsistency"], + exit_conditions: ["vat_reflected"], + is_terminal: false, + is_problematic: true, + business_meaning: "Бухгалтерская и налоговая ветки расходятся." + } + ], + transitions: [], + defects: [] + }, + period_close: { + schema_version: stage3Lifecycle_1.LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "period_close", + lifecycle_object_types: ["period_close_blocker"], + states: [ + { + state_code: "preclose_checks", + state_label: "Предзакрытие", + state_class: "active", + entry_conditions: ["period_scope_detected"], + exit_conditions: ["close_ready"], + is_terminal: false, + is_problematic: false, + business_meaning: "Идет проверка готовности периода." + }, + { + state_code: "close_ready", + state_label: "Готов к закрытию", + state_class: "active", + entry_conditions: ["no_blockers_detected"], + exit_conditions: ["close_completed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Период может быть закрыт." + }, + { + state_code: "close_completed", + state_label: "Закрытие завершено", + state_class: "terminal", + entry_conditions: ["close_operation_done"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Период закрыт." + }, + { + state_code: "close_blocked", + state_label: "Закрытие заблокировано", + state_class: "problematic", + entry_conditions: ["period_close_risk_or_stale_state"], + exit_conditions: ["close_ready"], + is_terminal: false, + is_problematic: true, + business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие." + }, + { + state_code: "close_contradicted", + state_label: "Закрыт формально, но с противоречием", + state_class: "problematic", + entry_conditions: ["misclosed_or_cross_branch_conflict"], + exit_conditions: ["close_completed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Формальное закрытие не согласовано с фактическими ветками." + } + ], + transitions: [], + defects: [] + } +}; +const SHARED_DEFECTS = [ + { + defect_code: "missing_expected_transition", + defect_class: "path", + severity_hint: "medium", + business_meaning: "Ожидаемый переход не произошел.", + evidence_requirements: ["expected_state", "missing_transition_signal"], + period_impact_potential: "indirect" + }, + { + defect_code: "invalid_transition", + defect_class: "path", + severity_hint: "high", + business_meaning: "Переход произошел по некорректному пути.", + evidence_requirements: ["invalid_transition_signal"], + period_impact_potential: "indirect" + }, + { + defect_code: "stale_active_state", + defect_class: "timing", + severity_hint: "high", + business_meaning: "Объект завис в активном состоянии.", + evidence_requirements: ["stale_marker", "missing_transition_signal"], + period_impact_potential: "direct" + }, + { + defect_code: "contradictory_state", + defect_class: "consistency", + severity_hint: "high", + business_meaning: "Статусы объекта противоречат друг другу.", + evidence_requirements: ["contradiction_signal"], + period_impact_potential: "direct" + }, + { + defect_code: "premature_terminal_state", + defect_class: "closure", + severity_hint: "medium", + business_meaning: "Терминальное состояние наступило преждевременно.", + evidence_requirements: ["terminal_state", "missing_required_previous_state"], + period_impact_potential: "indirect" + }, + { + defect_code: "misclosed_state", + defect_class: "closure", + severity_hint: "high", + business_meaning: "Контур формально закрыт, но закрыт неверно.", + evidence_requirements: ["wrong_closure_path"], + period_impact_potential: "direct" + }, + { + defect_code: "orphan_intermediate_state", + defect_class: "path", + severity_hint: "medium", + business_meaning: "Промежуточная стадия осталась без корректного продолжения.", + evidence_requirements: ["intermediate_state_without_next"], + period_impact_potential: "indirect" + }, + { + defect_code: "cross_branch_state_conflict", + defect_class: "consistency", + severity_hint: "high", + business_meaning: "Состояния соседних веток учета противоречат друг другу.", + evidence_requirements: ["cross_branch_conflict_signal"], + period_impact_potential: "direct" + } +]; +for (const domain of stage3Lifecycle_1.STAGE3_LIFECYCLE_DOMAINS) { + LIFECYCLE_DOMAIN_MODELS[domain].defects = SHARED_DEFECTS; +} +class LifecycleRegistryImpl { + models; + constructor(models) { + this.models = models; + } + listDomains() { + return stage3Lifecycle_1.STAGE3_LIFECYCLE_DOMAINS.slice(); + } + getDomain(domain) { + return this.models[domain]; + } +} +exports.LifecycleRegistry = new LifecycleRegistryImpl(LIFECYCLE_DOMAIN_MODELS); +function inferLifecycleDomain(input) { + const unitTokens = [ + input.unit.problem_unit_type, + input.unit.business_defect_class, + input.unit.mechanism_summary, + input.unit.failed_expected_edge ?? "", + input.unit.expected_state ?? "", + input.unit.actual_state ?? "", + ...input.unit.affected_accounts, + ...input.unit.affected_entities, + ...input.unit.affected_documents, + ...input.unit.affected_counterparties, + ...input.candidates.flatMap((item) => item.anomaly_patterns), + ...input.candidates.flatMap((item) => item.relation_pattern_hits) + ] + .join(" ") + .toLowerCase(); + if (includesAny(unitTokens, [/\bnds\b/, /\bvat\b/, /\btax\b/, /cross[_\s-]?branch/, /\b19\b/, /\b68\b/])) { + return "vat_flow"; + } + if (includesAny(unitTokens, [/\bperiod\b/, /\bclose\b/, /закрыт/, /reporting/]) || input.unit.problem_unit_type === "period_risk_cluster") { + return "period_close"; + } + if (includesAny(unitTokens, [/deferred/, /writeoff/, /рбп/, /\b97\b/])) { + return "deferred_expense"; + } + if (includesAny(unitTokens, [/fixed[_\s-]?asset/, /амортиз/, /ос\b/, /\b01\b/, /\b02\b/, /\b08\b/])) { + return "fixed_asset"; + } + if (includesAny(unitTokens, [/buyer/, /customer/, /дебитор/, /\b62\b/])) { + return "customer_settlement"; + } + return "bank_settlement"; +} +function inferCurrentState(domain, input) { + const explicitActual = input.unit.actual_state?.trim(); + if (explicitActual) { + return explicitActual; + } + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).map((item) => item.toLowerCase()); + const relations = input.candidates.flatMap((item) => item.relation_pattern_hits).map((item) => item.toLowerCase()); + const hasStale = hasToken(anomalies, /(no_continuation|stale|tail|missing_link|broken_lifecycle|partially_linked)/); + const hasInvalid = hasToken(anomalies, /(posting_mismatch|wrong_document_type|cross_domain_inconsistency|misclose|cross_branch)/); + if (domain === "bank_settlement") { + if (hasInvalid) + return "misclosed_payment"; + if (hasStale) + return "stale_unlinked_payment"; + if (hasToken(relations, /payment_to_settlement/)) + return "bank_recorded"; + return "initiated_payment"; + } + if (domain === "customer_settlement") { + if (hasStale) + return "stale_receivable"; + if (hasToken(relations, /payment|settlement/)) + return "payment_recorded"; + return "invoice_issued"; + } + if (domain === "deferred_expense") { + if (hasStale) + return "overdue_writeoff"; + if (hasToken(relations, /writeoff|partial/)) + return "partially_written_off"; + return "recognized"; + } + if (domain === "fixed_asset") { + if (hasInvalid) + return "contradictory_asset_state"; + if (hasToken(relations, /depreciation|amort/)) + return "depreciation_active"; + if (hasToken(relations, /accept|учет/)) + return "accepted_for_accounting"; + return "capitalized"; + } + if (domain === "vat_flow") { + if (hasInvalid || hasToken(anomalies, /cross_branch|inconsistency/)) + return "vat_conflict"; + if (hasToken(relations, /invoice_to_vat|vat/)) + return "vat_reflected"; + return "vat_registered"; + } + if (hasInvalid) + return "close_contradicted"; + if (hasStale || input.unit.period_impact?.impact_class === "close_risk") + return "close_blocked"; + return "preclose_checks"; +} +function inferExpectedState(domain, input) { + const explicitExpected = input.unit.expected_state?.trim(); + if (explicitExpected) { + return explicitExpected; + } + return defaultExpectedState(domain); +} +function inferMissingTransition(input) { + if (typeof input.unit.failed_expected_edge === "string" && input.unit.failed_expected_edge.trim().length > 0) { + return input.unit.failed_expected_edge.trim(); + } + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (/(missing_link|no_continuation|broken_lifecycle|tail|unresolved)/.test(anomalies)) { + return "expected_transition_not_observed"; + } + return null; +} +function inferInvalidTransition(input) { + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (/(cross_branch|cross_domain_inconsistency)/.test(anomalies)) { + return "cross_branch_conflict_transition"; + } + if (/(wrong_document_type|posting_mismatch|misclose)/.test(anomalies)) { + return "invalid_document_or_posting_transition"; + } + return null; +} +function classifyLifecycleDefect(input) { + const current = input.currentState.toLowerCase(); + if (input.invalidTransition?.includes("cross_branch")) { + return "cross_branch_state_conflict"; + } + if (input.invalidTransition) { + if (current.includes("misclosed") || input.domain === "period_close") { + return "misclosed_state"; + } + return "invalid_transition"; + } + if (input.missingTransition) { + if (current.includes("stale") || current.includes("overdue") || input.periodCloseSensitive) { + return "stale_active_state"; + } + return "missing_expected_transition"; + } + if (current.includes("contradict")) { + return "contradictory_state"; + } + if (current.includes("closed") && !input.expectedState.toLowerCase().includes("closed")) { + return "premature_terminal_state"; + } + if (input.currentState !== input.expectedState && !input.currentState.toLowerCase().includes("closed")) { + return "orphan_intermediate_state"; + } + return null; +} +function resolutionConfidence(unitConfidence, input) { + let score = unitConfidence.score; + if (input.hasExplicitStates) + score += 0.1; + if (input.hasDefectSignal) + score += 0.08; + if (input.candidateCount >= 2) + score += 0.05; + if (input.hasSnapshotLimitations) + score -= 0.12; + const normalized = clampUnitScore(score); + return { + score: normalized, + grade: lifecycleConfidenceGrade(normalized) + }; +} +function staleDurationHint(domain, defect, input) { + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (defect !== "stale_active_state") { + return undefined; + } + if (/(period_boundary|period|close_risk)/.test(anomalies) || domain === "period_close") { + return "period_boundary_exceeded"; + } + return "unknown_snapshot_window"; +} +function lifecycleInterpretation(input) { + const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`; + if (input.defect === "stale_active_state") { + return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`; + } + if (input.defect === "misclosed_state") { + return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`; + } + if (input.defect === "cross_branch_state_conflict") { + return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`; + } + if (input.defect === "missing_expected_transition") { + return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`; + } + if (input.defect === "invalid_transition") { + return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`; + } + return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`; +} +function resolveLifecycle(input) { + const lifecycle_domain = inferLifecycleDomain(input); + const currentState = inferCurrentState(lifecycle_domain, input); + const expectedState = inferExpectedState(lifecycle_domain, input); + const missingTransition = inferMissingTransition(input); + const invalidTransition = inferInvalidTransition(input); + const defect = classifyLifecycleDefect({ + domain: lifecycle_domain, + currentState, + expectedState, + missingTransition, + invalidTransition, + periodCloseSensitive: input.unit.period_impact?.impact_class === "close_risk" + }); + const evidenceIds = uniqueStrings(input.unit.evidence_pack, 8); + const limitations = uniqueStrings([ + ...input.unit.snapshot_limitations, + ...(input.candidates.some((item) => item.confidence_hint === "low") ? ["low_confidence_candidates_present"] : []), + ...(input.unit.actual_state ? [] : ["actual_state_inferred"]), + ...(input.unit.expected_state ? [] : ["expected_state_inferred"]) + ], 8); + const confidence = resolutionConfidence(input.unit.confidence, { + hasExplicitStates: Boolean(input.unit.actual_state || input.unit.expected_state), + hasDefectSignal: Boolean(defect || missingTransition || invalidTransition), + candidateCount: input.candidates.length, + hasSnapshotLimitations: limitations.length > 0 + }); + return { + lifecycle_object_id: `lcobj-${input.unit.problem_unit_id}`, + lifecycle_domain, + resolved_current_state: currentState, + resolved_expected_state: expectedState, + resolved_previous_states: [], + missing_transitions: missingTransition ? [missingTransition] : [], + invalid_transitions: invalidTransition ? [invalidTransition] : [], + detected_defects: defect ? [defect] : [], + state_confidence: confidence, + resolution_evidence: evidenceIds, + snapshot_limitations: limitations + }; +} +function lifecycleRanking(defect, input) { + let score = input.unit.severity.score; + const basis = ["base_problem_severity"]; + if (defect === "cross_branch_state_conflict") { + score += 0.55; + basis.push("cross_branch_conflict_weight"); + } + else if (defect === "misclosed_state") { + score += 0.45; + basis.push("misclosed_state_weight"); + } + else if (defect === "stale_active_state") { + score += 0.35; + basis.push("stale_duration_weight"); + } + else if (defect === "invalid_transition") { + score += 0.3; + basis.push("invalid_transition_weight"); + } + else if (defect === "missing_expected_transition") { + score += 0.25; + basis.push("missing_transition_weight"); + } + if (input.staleDuration) { + score += 0.15; + basis.push("stale_duration_present"); + } + if (input.unit.period_impact?.impact_class === "close_risk") { + score += 0.22; + basis.push("period_close_impact"); + } + if (input.resolution.state_confidence.grade === "high") { + score += 0.08; + basis.push("state_confidence_weight"); + } + return { + lifecycle_ranking_score: Number(score.toFixed(2)), + lifecycle_ranking_basis: basis + }; +} +function enrichProblemUnitLifecycle(input) { + const resolution = resolveLifecycle(input); + const defect = resolution.detected_defects[0] ?? null; + const staleDuration = staleDurationHint(resolution.lifecycle_domain, defect, input); + const ranking = lifecycleRanking(defect, { + unit: input.unit, + resolution, + staleDuration + }); + return { + ...input.unit, + lifecycle_domain: resolution.lifecycle_domain, + lifecycle_object_id: resolution.lifecycle_object_id, + current_lifecycle_state: resolution.resolved_current_state, + expected_lifecycle_state: resolution.resolved_expected_state, + ...(resolution.missing_transitions.length > 0 + ? { + missing_transition: resolution.missing_transitions[0] + } + : {}), + ...(resolution.invalid_transitions.length > 0 + ? { + invalid_transition: resolution.invalid_transitions[0] + } + : {}), + ...(defect + ? { + lifecycle_defect_type: defect + } + : {}), + ...(staleDuration + ? { + stale_duration: staleDuration + } + : {}), + lifecycle_confidence: resolution.state_confidence, + business_lifecycle_interpretation: lifecycleInterpretation({ + domain: resolution.lifecycle_domain, + currentState: resolution.resolved_current_state, + expectedState: resolution.resolved_expected_state, + defect, + missingTransition: resolution.missing_transitions[0] ?? null, + invalidTransition: resolution.invalid_transitions[0] ?? null + }), + lifecycle_resolution: resolution, + lifecycle_ranking_score: ranking.lifecycle_ranking_score, + lifecycle_ranking_basis: ranking.lifecycle_ranking_basis + }; +} +function rankLifecycleProblemUnits(units) { + return units + .slice() + .sort((left, right) => { + const rankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0); + if (rankDiff !== 0) + return rankDiff; + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) + return severityDiff; + return right.confidence.score - left.confidence.score; + }); +} diff --git a/llm_normalizer/backend/dist/services/problemUnitAssembler.js b/llm_normalizer/backend/dist/services/problemUnitAssembler.js index a77d17f..7481bc1 100644 --- a/llm_normalizer/backend/dist/services/problemUnitAssembler.js +++ b/llm_normalizer/backend/dist/services/problemUnitAssembler.js @@ -8,6 +8,8 @@ exports.buildProblemUnit = buildProblemUnit; exports.collapseDuplicates = collapseDuplicates; exports.assembleProblemUnits = assembleProblemUnits; const stage2ProblemUnits_1 = require("../types/stage2ProblemUnits"); +const config_1 = require("../config"); +const lifecycleRuntime_1 = require("./lifecycleRuntime"); function toObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; @@ -382,6 +384,17 @@ function collapseSignature(unit) { const backlink = unit.entity_backlinks[0] ? `${unit.entity_backlinks[0].entity}|${unit.entity_backlinks[0].id}` : "none"; return [unit.problem_unit_type, unit.business_defect_class, unit.failed_expected_edge ?? "none", backlink].join("|"); } +function preferredLifecycleSource(left, right) { + const leftRank = left.lifecycle_ranking_score ?? 0; + const rightRank = right.lifecycle_ranking_score ?? 0; + if (rightRank > leftRank) { + return right; + } + if (leftRank > rightRank) { + return left; + } + return right.confidence.score > left.confidence.score ? right : left; +} function collapseDuplicates(units) { const bySignature = new Map(); let duplicateCollapses = 0; @@ -393,6 +406,7 @@ function collapseDuplicates(units) { continue; } duplicateCollapses += 1; + const preferredLifecycle = preferredLifecycleSource(existing, unit); bySignature.set(signature, { ...existing, evidence_pack: uniqueStrings([...existing.evidence_pack, ...unit.evidence_pack]), @@ -408,7 +422,72 @@ function collapseDuplicates(units) { affected_contracts: uniqueStrings([...existing.affected_contracts, ...unit.affected_contracts]), snapshot_limitations: uniqueStrings([...existing.snapshot_limitations, ...unit.snapshot_limitations]), severity: unit.severity.score > existing.severity.score ? unit.severity : existing.severity, - confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence + confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence, + ...(preferredLifecycle.lifecycle_domain + ? { + lifecycle_domain: preferredLifecycle.lifecycle_domain + } + : {}), + ...(preferredLifecycle.lifecycle_object_id + ? { + lifecycle_object_id: preferredLifecycle.lifecycle_object_id + } + : {}), + ...(preferredLifecycle.current_lifecycle_state + ? { + current_lifecycle_state: preferredLifecycle.current_lifecycle_state + } + : {}), + ...(preferredLifecycle.expected_lifecycle_state + ? { + expected_lifecycle_state: preferredLifecycle.expected_lifecycle_state + } + : {}), + ...(preferredLifecycle.missing_transition + ? { + missing_transition: preferredLifecycle.missing_transition + } + : {}), + ...(preferredLifecycle.invalid_transition + ? { + invalid_transition: preferredLifecycle.invalid_transition + } + : {}), + ...(preferredLifecycle.lifecycle_defect_type + ? { + lifecycle_defect_type: preferredLifecycle.lifecycle_defect_type + } + : {}), + ...(preferredLifecycle.stale_duration + ? { + stale_duration: preferredLifecycle.stale_duration + } + : {}), + ...(preferredLifecycle.lifecycle_confidence + ? { + lifecycle_confidence: preferredLifecycle.lifecycle_confidence + } + : {}), + ...(preferredLifecycle.business_lifecycle_interpretation + ? { + business_lifecycle_interpretation: preferredLifecycle.business_lifecycle_interpretation + } + : {}), + ...(preferredLifecycle.lifecycle_resolution + ? { + lifecycle_resolution: preferredLifecycle.lifecycle_resolution + } + : {}), + ...(preferredLifecycle.lifecycle_ranking_score !== undefined + ? { + lifecycle_ranking_score: preferredLifecycle.lifecycle_ranking_score + } + : {}), + ...(preferredLifecycle.lifecycle_ranking_basis + ? { + lifecycle_ranking_basis: preferredLifecycle.lifecycle_ranking_basis + } + : {}) }); } return { @@ -429,10 +508,20 @@ function buildSummary(units, duplicateCollapses) { medium: 0, high: 0 }; + const lifecycleDomainDistribution = {}; + const lifecycleDefectDistribution = {}; + let lifecycleEnrichedUnits = 0; for (const unit of units) { typeDistribution[unit.problem_unit_type] = (typeDistribution[unit.problem_unit_type] ?? 0) + 1; severityDistribution[unit.severity.grade] += 1; confidenceDistribution[unit.confidence.grade] += 1; + if (unit.lifecycle_domain) { + lifecycleEnrichedUnits += 1; + lifecycleDomainDistribution[unit.lifecycle_domain] = (lifecycleDomainDistribution[unit.lifecycle_domain] ?? 0) + 1; + } + if (unit.lifecycle_defect_type) { + lifecycleDefectDistribution[unit.lifecycle_defect_type] = (lifecycleDefectDistribution[unit.lifecycle_defect_type] ?? 0) + 1; + } } return { schema_version: stage2ProblemUnits_1.PROBLEM_UNIT_SUMMARY_SCHEMA_VERSION, @@ -442,7 +531,10 @@ function buildSummary(units, duplicateCollapses) { type_distribution: typeDistribution, severity_distribution: severityDistribution, confidence_distribution: confidenceDistribution, - primary_unit_type: units[0]?.problem_unit_type ?? null + primary_unit_type: units[0]?.problem_unit_type ?? null, + lifecycle_enriched_units: lifecycleEnrichedUnits, + lifecycle_domain_distribution: lifecycleDomainDistribution, + lifecycle_defect_distribution: lifecycleDefectDistribution }; } function assembleProblemUnits(input) { @@ -457,11 +549,23 @@ function assembleProblemUnits(input) { risk_factors: uniqueStrings(input.risk_factors ?? []) }); const clusters = clusterCandidateEvidence(candidates); - const units = clusters.map((cluster, index) => buildProblemUnit(cluster, index)); + const units = clusters.map((cluster, index) => { + const baseUnit = buildProblemUnit(cluster, index); + if (!config_1.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1) { + return baseUnit; + } + return (0, lifecycleRuntime_1.enrichProblemUnitLifecycle)({ + unit: baseUnit, + candidates: cluster.candidates + }); + }); const collapsed = collapseDuplicates(units); + const rankedUnits = config_1.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 + ? (0, lifecycleRuntime_1.rankLifecycleProblemUnits)(collapsed.problem_units) + : collapsed.problem_units; return { candidate_evidence: candidates, - problem_units: collapsed.problem_units, - problem_unit_summary: buildSummary(collapsed.problem_units, collapsed.duplicate_collapses) + problem_units: rankedUnits, + problem_unit_summary: buildSummary(rankedUnits, collapsed.duplicate_collapses) }; } diff --git a/llm_normalizer/backend/dist/services/retrievalResultNormalizer.js b/llm_normalizer/backend/dist/services/retrievalResultNormalizer.js index ecaa8e7..6d58d48 100644 --- a/llm_normalizer/backend/dist/services/retrievalResultNormalizer.js +++ b/llm_normalizer/backend/dist/services/retrievalResultNormalizer.js @@ -69,7 +69,10 @@ function mergeSummaryWithProblemUnitMeta(summary, input) { problem_unit_types: input.unitTypes, problem_unit_duplicate_collapses: input.duplicateCollapses, problem_unit_severity_distribution: input.severityDistribution, - problem_unit_confidence_distribution: input.confidenceDistribution + problem_unit_confidence_distribution: input.confidenceDistribution, + lifecycle_enriched_units: input.lifecycleEnrichedUnits, + problem_unit_lifecycle_domain_distribution: input.lifecycleDomainDistribution, + problem_unit_lifecycle_defect_distribution: input.lifecycleDefectDistribution }; } function normalizeConfidence(value) { @@ -411,7 +414,10 @@ function normalizeRetrievalResult(fragmentId, requirementIds, route, raw) { unitTypes: assembled.problem_unit_summary.unit_types, duplicateCollapses: assembled.problem_unit_summary.duplicate_collapses, severityDistribution: assembled.problem_unit_summary.severity_distribution, - confidenceDistribution: assembled.problem_unit_summary.confidence_distribution + confidenceDistribution: assembled.problem_unit_summary.confidence_distribution, + lifecycleEnrichedUnits: assembled.problem_unit_summary.lifecycle_enriched_units ?? 0, + lifecycleDomainDistribution: (assembled.problem_unit_summary.lifecycle_domain_distribution ?? {}), + lifecycleDefectDistribution: (assembled.problem_unit_summary.lifecycle_defect_distribution ?? {}) }); return { ...baseResult, diff --git a/llm_normalizer/backend/dist/types/stage3Lifecycle.js b/llm_normalizer/backend/dist/types/stage3Lifecycle.js new file mode 100644 index 0000000..c46e9a9 --- /dev/null +++ b/llm_normalizer/backend/dist/types/stage3Lifecycle.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LIFECYCLE_DEFECT_TYPES = exports.STAGE3_LIFECYCLE_DOMAINS = exports.LIFECYCLE_MODEL_SCHEMA_VERSION = void 0; +exports.LIFECYCLE_MODEL_SCHEMA_VERSION = "lifecycle_model_v0_1"; +exports.STAGE3_LIFECYCLE_DOMAINS = [ + "bank_settlement", + "customer_settlement", + "deferred_expense", + "fixed_asset", + "vat_flow", + "period_close" +]; +exports.LIFECYCLE_DEFECT_TYPES = [ + "missing_expected_transition", + "invalid_transition", + "stale_active_state", + "contradictory_state", + "premature_terminal_state", + "misclosed_state", + "orphan_intermediate_state", + "cross_branch_state_conflict" +]; diff --git a/llm_normalizer/backend/src/config.ts b/llm_normalizer/backend/src/config.ts index f6a01d5..8739cbe 100644 --- a/llm_normalizer/backend/src/config.ts +++ b/llm_normalizer/backend/src/config.ts @@ -67,6 +67,14 @@ export const FEATURE_ASSISTANT_STAGE2_EVAL_V1 = toBooleanFlag( process.env.FEATURE_ASSISTANT_STAGE2_EVAL_V1, false ); +export const FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = toBooleanFlag( + process.env.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1, + false +); +export const FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = toBooleanFlag( + process.env.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, + false +); export const DATA_DIR = process.env.DATA_DIR ?? path.resolve(MODULE_ROOT, "data"); export const TRACES_DIR = path.resolve(DATA_DIR, "traces"); diff --git a/llm_normalizer/backend/src/services/answerComposer.ts b/llm_normalizer/backend/src/services/answerComposer.ts index 86a338c..3fd0907 100644 --- a/llm_normalizer/backend/src/services/answerComposer.ts +++ b/llm_normalizer/backend/src/services/answerComposer.ts @@ -10,7 +10,7 @@ import type { RouteHintSummary } from "../types/normalizer"; import type { AnswerStructureV11, EvidenceConfidence, EvidenceItem, EvidenceLimitationReasonCode } from "../types/stage1Contracts"; import type { ProblemUnit, ProblemUnitSummary, ProblemUnitType } from "../types/stage2ProblemUnits"; -type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1"; +type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1" | "stage3_lifecycle_aware_v1"; interface ComposeAnswerInput { userMessage: string; @@ -21,6 +21,7 @@ interface ComposeAnswerInput { groundingCheck: AnswerGroundingCheck; enableAnswerPolicyV11?: boolean; enableProblemCentricAnswerV1?: boolean; + enableLifecycleAnswerV1?: boolean; } interface ComposeAnswerOutput { @@ -382,6 +383,61 @@ function formatAffectedScope(unit: ProblemUnit): string { return scopeParts.join("; "); } +function formatLifecycleScope(unit: ProblemUnit): string | null { + if (!unit.lifecycle_domain) { + return null; + } + const parts: string[] = [`domain=${unit.lifecycle_domain}`]; + if (unit.current_lifecycle_state) { + parts.push(`current=${unit.current_lifecycle_state}`); + } + if (unit.expected_lifecycle_state) { + parts.push(`expected=${unit.expected_lifecycle_state}`); + } + if (unit.lifecycle_defect_type) { + parts.push(`defect=${unit.lifecycle_defect_type}`); + } + if (unit.missing_transition) { + parts.push(`missing_transition=${unit.missing_transition}`); + } + if (unit.invalid_transition) { + parts.push(`invalid_transition=${unit.invalid_transition}`); + } + if (unit.stale_duration) { + parts.push(`stale_duration=${unit.stale_duration}`); + } + return parts.join(", "); +} + +function rankProblemUnitsForAnswer(units: ProblemUnit[], lifecycleAnswerEnabled: boolean): ProblemUnit[] { + if (!lifecycleAnswerEnabled) { + return units.slice().sort((left, right) => { + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) return severityDiff; + return right.confidence.score - left.confidence.score; + }); + } + return units.slice().sort((left, right) => { + const lifecycleRankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0); + if (lifecycleRankDiff !== 0) return lifecycleRankDiff; + const lifecycleConfidenceDiff = (right.lifecycle_confidence?.score ?? 0) - (left.lifecycle_confidence?.score ?? 0); + if (lifecycleConfidenceDiff !== 0) return lifecycleConfidenceDiff; + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) return severityDiff; + return right.confidence.score - left.confidence.score; + }); +} + +function hasLifecycleResolution(units: ProblemUnit[]): boolean { + return units.some( + (unit) => + Boolean(unit.lifecycle_domain) && + Boolean(unit.current_lifecycle_state) && + Boolean(unit.expected_lifecycle_state) && + Boolean(unit.lifecycle_defect_type) + ); +} + function buildProblemCentricActions(input: { units: ProblemUnit[]; mode: PolicyMode; @@ -406,6 +462,17 @@ function buildProblemCentricActions(input: { if (unitTypes.has("lifecycle_anomaly_node")) { actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии."); } + for (const unit of input.units) { + if (unit.lifecycle_defect_type === "stale_active_state") { + actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии."); + } + if (unit.lifecycle_defect_type === "misclosed_state") { + actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути."); + } + if (unit.lifecycle_defect_type === "cross_branch_state_conflict") { + actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния."); + } + } if (input.mode === "clarification_required") { if (input.missingAnchors.period) { @@ -825,7 +892,14 @@ function buildProblemCentricAnswerSummary(input: { mode: PolicyMode; weakUnits: boolean; summary: ProblemUnitSummary | null; + lifecycleEnriched: boolean; }): string { + if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) { + if (input.mode === "clarification_required") { + return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей."; + } + return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`; + } if (input.mode === "clarification_required") { return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса."; } @@ -842,17 +916,31 @@ function buildProblemCentricDirectAnswer(input: { mode: PolicyMode; units: ProblemUnit[]; weakUnits: boolean; + lifecycleAnswerEnabled: boolean; }): string { const lead = input.mode === "clarification_required" ? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен." : input.weakUnits ? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме." - : "Выделены ключевые проблемные зоны и их влияние на учетный контур."; + : input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units) + ? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода." + : "Выделены ключевые проблемные зоны и их влияние на учетный контур."; const unitLines = input.units.map((unit) => { const scope = formatAffectedScope(unit); - return `- ${unit.title}: ${unit.business_defect_class}; ${scope}; severity=${unit.severity.grade}, confidence=${unit.confidence.grade}.`; + const lifecycleScope = input.lifecycleAnswerEnabled ? formatLifecycleScope(unit) : null; + const lifecycleInterpretation = input.lifecycleAnswerEnabled ? unit.business_lifecycle_interpretation : null; + const segments = [ + `${unit.title}: ${unit.business_defect_class}`, + scope, + lifecycleScope, + lifecycleInterpretation, + `severity=${unit.severity.grade}`, + `confidence=${unit.confidence.grade}`, + unit.lifecycle_confidence ? `lifecycle_confidence=${unit.lifecycle_confidence.grade}` : null + ].filter((item): item is string => Boolean(item)); + return `- ${segments.join("; ")}.`; }); if (unitLines.length === 0) { @@ -873,8 +961,10 @@ function buildProblemCentricAnswerStructure(input: { retrievalResults: UnifiedRetrievalResult[]; missingAnchors: MissingAnchors; coverageReport: RequirementCoverageReport; + lifecycleAnswerEnabled: boolean; }): AnswerStructureV11 { const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low"); + const lifecycleEnriched = input.lifecycleAnswerEnabled && hasLifecycleResolution(input.selectedUnits); const unitMechanismNotes = uniqueStrings( input.selectedUnits .map((item) => item.mechanism_summary) @@ -932,12 +1022,14 @@ function buildProblemCentricAnswerStructure(input: { answer_summary: buildProblemCentricAnswerSummary({ mode: input.mode, weakUnits, - summary: input.problemSummary + summary: input.problemSummary, + lifecycleEnriched }), direct_answer: buildProblemCentricDirectAnswer({ mode: input.mode, units: input.selectedUnits, - weakUnits + weakUnits, + lifecycleAnswerEnabled: input.lifecycleAnswerEnabled }), mechanism_block: { status: mechanismStatus, @@ -1059,10 +1151,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp .filter((item): item is string => typeof item === "string" && item.trim().length > 0), 6 ); + const lifecycleAnswerEnabled = Boolean(input.enableLifecycleAnswerV1); const problemUnits = flattenProblemUnits(input.retrievalResults); const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults); const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type)); - const selectedProblemUnits = problemHeavyUnits.slice(0, 4); + const selectedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled).slice(0, 4); const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults); const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems); const lowConfidenceSignals = evidenceItems.filter((item) => item.confidence === "low").length; @@ -1138,9 +1231,12 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp groundingCheck: input.groundingCheck, retrievalResults: input.retrievalResults, missingAnchors, - coverageReport: input.coverageReport + coverageReport: input.coverageReport, + lifecycleAnswerEnabled }); + const lifecycleModeActive = lifecycleAnswerEnabled && hasLifecycleResolution(selectedProblemUnits); + return { assistant_reply: renderPolicyReply(problemCentricStructure), fallback_type: decision.fallback_type, @@ -1148,7 +1244,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp answer_structure_v11: problemCentricStructure, problem_centric_answer_applied: true, problem_units_used_count: selectedProblemUnits.length, - problem_answer_mode: "stage2_problem_centric_v1", + problem_answer_mode: lifecycleModeActive ? "stage3_lifecycle_aware_v1" : "stage2_problem_centric_v1", problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id) }; } diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 1982976..613a5fd 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -19,6 +19,7 @@ import { FEATURE_ASSISTANT_CONTRACTS_V11, FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1, FEATURE_ASSISTANT_INVESTIGATION_STATE_V1, + FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1, FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1, FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 @@ -1203,7 +1204,8 @@ export class AssistantService { coverageReport: coverageEvaluation.coverage, groundingCheck, enableAnswerPolicyV11: FEATURE_ASSISTANT_ANSWER_POLICY_V11, - enableProblemCentricAnswerV1: FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 + enableProblemCentricAnswerV1: FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1, + enableLifecycleAnswerV1: FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 }); const answerStructureV11 = FEATURE_ASSISTANT_CONTRACTS_V11 diff --git a/llm_normalizer/backend/src/services/lifecycleRuntime.ts b/llm_normalizer/backend/src/services/lifecycleRuntime.ts new file mode 100644 index 0000000..7aea315 --- /dev/null +++ b/llm_normalizer/backend/src/services/lifecycleRuntime.ts @@ -0,0 +1,865 @@ +import type { CandidateEvidenceItem, ProblemConfidence, ProblemUnit, ProblemUnitType } from "../types/stage2ProblemUnits"; +import { + LIFECYCLE_MODEL_SCHEMA_VERSION, + STAGE3_LIFECYCLE_DOMAINS, + type LifecycleConfidence, + type LifecycleDefectDefinition, + type LifecycleDefectType, + type LifecycleDomain, + type LifecycleDomainModel, + type LifecycleResolution +} from "../types/stage3Lifecycle"; + +interface LifecycleResolverInput { + unit: ProblemUnit; + candidates: CandidateEvidenceItem[]; +} + +interface LifecycleRankingResult { + lifecycle_ranking_score: number; + lifecycle_ranking_basis: string[]; +} + +function clampUnitScore(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + if (value <= 0) return 0; + if (value >= 1) return 1; + return Number(value.toFixed(2)); +} + +function lifecycleConfidenceGrade(score: number): LifecycleConfidence["grade"] { + if (score >= 0.75) return "high"; + if (score >= 0.45) return "medium"; + return "low"; +} + +function uniqueStrings(values: string[], limit = 16): string[] { + return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit); +} + +function includesAny(source: string, patterns: RegExp[]): boolean { + return patterns.some((pattern) => pattern.test(source)); +} + +function hasToken(values: string[], pattern: RegExp): boolean { + return values.some((value) => pattern.test(value)); +} + +function defaultExpectedState(domain: LifecycleDomain): string { + if (domain === "bank_settlement") return "settlement_closed"; + if (domain === "customer_settlement") return "receivable_closed"; + if (domain === "deferred_expense") return "fully_written_off"; + if (domain === "fixed_asset") return "depreciation_active"; + if (domain === "vat_flow") return "vat_deducted"; + return "close_completed"; +} + +const LIFECYCLE_DOMAIN_MODELS: Record = { + bank_settlement: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "bank_settlement", + lifecycle_object_types: ["payment_settlement_link"], + states: [ + { + state_code: "initiated_payment", + state_label: "Платеж инициирован", + state_class: "initial", + entry_conditions: ["payment_order_created"], + exit_conditions: ["bank_recorded"], + is_terminal: false, + is_problematic: false, + business_meaning: "Есть инициирование платежа." + }, + { + state_code: "bank_recorded", + state_label: "Платеж отражен банком", + state_class: "active", + entry_conditions: ["bank_statement_recorded"], + exit_conditions: ["settlement_linked"], + is_terminal: false, + is_problematic: false, + business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие." + }, + { + state_code: "settlement_closed", + state_label: "Расчет закрыт", + state_class: "terminal", + entry_conditions: ["payment_to_settlement_linked"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Платеж доведен до расчетного результата." + }, + { + state_code: "stale_unlinked_payment", + state_label: "Платеж завис без закрытия", + state_class: "problematic", + entry_conditions: ["bank_recorded", "missing_link"], + exit_conditions: ["settlement_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена." + }, + { + state_code: "misclosed_payment", + state_label: "Платеж закрыт некорректно", + state_class: "problematic", + entry_conditions: ["wrong_document_type_or_posting_mismatch"], + exit_conditions: ["settlement_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Формальное закрытие есть, но путь закрытия неверный." + } + ], + transitions: [ + { + from_state: "initiated_payment", + to_state: "bank_recorded", + transition_type: "expected", + required_evidence: ["bank_statement_recorded"], + optional_evidence: ["payment_order"], + forbidden_conditions: [], + business_meaning: "Платеж должен появиться во выписке." + }, + { + from_state: "bank_recorded", + to_state: "settlement_closed", + transition_type: "expected", + required_evidence: ["payment_to_settlement_link"], + optional_evidence: ["document_to_posting"], + forbidden_conditions: ["wrong_document_type"], + business_meaning: "После выписки должен закрываться расчет." + } + ], + defects: [] + }, + customer_settlement: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "customer_settlement", + lifecycle_object_types: ["receivable_chain"], + states: [ + { + state_code: "invoice_issued", + state_label: "Реализация отражена", + state_class: "initial", + entry_conditions: ["realization_document_exists"], + exit_conditions: ["payment_recorded"], + is_terminal: false, + is_problematic: false, + business_meaning: "Возникла дебиторская позиция." + }, + { + state_code: "payment_recorded", + state_label: "Оплата отражена", + state_class: "active", + entry_conditions: ["payment_document_exists"], + exit_conditions: ["receivable_closed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Оплата есть, ожидается корректное закрытие." + }, + { + state_code: "receivable_closed", + state_label: "Дебиторка закрыта", + state_class: "terminal", + entry_conditions: ["closing_document_linked"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Дебиторская позиция закрыта корректно." + }, + { + state_code: "stale_receivable", + state_label: "Дебиторка зависла", + state_class: "problematic", + entry_conditions: ["unresolved_settlement"], + exit_conditions: ["receivable_closed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Позиция остается незавершенной дольше ожидаемого." + } + ], + transitions: [ + { + from_state: "invoice_issued", + to_state: "payment_recorded", + transition_type: "expected", + required_evidence: ["payment_document_exists"], + optional_evidence: [], + forbidden_conditions: [], + business_meaning: "После реализации ожидается оплата/зачет." + }, + { + from_state: "payment_recorded", + to_state: "receivable_closed", + transition_type: "expected", + required_evidence: ["closing_document_linked"], + optional_evidence: ["register_movement_exists"], + forbidden_conditions: ["cross_branch_inconsistency"], + business_meaning: "Оплата должна завершаться корректным закрытием расчета." + } + ], + defects: [] + }, + deferred_expense: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "deferred_expense", + lifecycle_object_types: ["deferred_expense_item"], + states: [ + { + state_code: "recognized", + state_label: "РБП признан", + state_class: "initial", + entry_conditions: ["deferred_expense_created"], + exit_conditions: ["writeoff_started"], + is_terminal: false, + is_problematic: false, + business_meaning: "РБП поставлен на учет." + }, + { + state_code: "partially_written_off", + state_label: "Частичное списание", + state_class: "active", + entry_conditions: ["partial_writeoff_exists"], + exit_conditions: ["fully_written_off"], + is_terminal: false, + is_problematic: false, + business_meaning: "Списание идет по графику." + }, + { + state_code: "fully_written_off", + state_label: "РБП полностью списан", + state_class: "terminal", + entry_conditions: ["full_writeoff_exists"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "РБП завершил lifecycle." + }, + { + state_code: "overdue_writeoff", + state_label: "Просроченное списание", + state_class: "problematic", + entry_conditions: ["period_boundary", "missing_link"], + exit_conditions: ["fully_written_off"], + is_terminal: false, + is_problematic: true, + business_meaning: "РБП живет дольше допустимого окна." + } + ], + transitions: [], + defects: [] + }, + fixed_asset: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "fixed_asset", + lifecycle_object_types: ["fixed_asset_card"], + states: [ + { + state_code: "capitalized", + state_label: "Капвложения отражены", + state_class: "initial", + entry_conditions: ["capitalization_document_exists"], + exit_conditions: ["accepted_for_accounting"], + is_terminal: false, + is_problematic: false, + business_meaning: "Объект зафиксирован как вложение." + }, + { + state_code: "accepted_for_accounting", + state_label: "Принят к учету", + state_class: "active", + entry_conditions: ["acceptance_document_exists"], + exit_conditions: ["depreciation_active"], + is_terminal: false, + is_problematic: false, + business_meaning: "Объект переведен в основной контур учета." + }, + { + state_code: "depreciation_active", + state_label: "Амортизация активна", + state_class: "active", + entry_conditions: ["depreciation_register_movement"], + exit_conditions: ["disposed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Жизненный цикл ОС идет штатно." + }, + { + state_code: "contradictory_asset_state", + state_label: "Противоречивый статус ОС", + state_class: "problematic", + entry_conditions: ["posting_mismatch_or_wrong_path"], + exit_conditions: ["depreciation_active"], + is_terminal: false, + is_problematic: true, + business_meaning: "Статус ОС формально есть, но смыслово противоречив." + }, + { + state_code: "disposed", + state_label: "Выбыл", + state_class: "terminal", + entry_conditions: ["disposal_document_exists"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Жизненный цикл ОС завершен." + } + ], + transitions: [], + defects: [] + }, + vat_flow: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "vat_flow", + lifecycle_object_types: ["vat_document_chain"], + states: [ + { + state_code: "vat_registered", + state_label: "НДС отражен документно", + state_class: "initial", + entry_conditions: ["invoice_registered"], + exit_conditions: ["vat_reflected"], + is_terminal: false, + is_problematic: false, + business_meaning: "Сформирован первичный документный слой НДС." + }, + { + state_code: "vat_reflected", + state_label: "НДС отражен в учете", + state_class: "active", + entry_conditions: ["vat_register_movement"], + exit_conditions: ["vat_deducted"], + is_terminal: false, + is_problematic: false, + business_meaning: "НДС проходит штатную стадию отражения." + }, + { + state_code: "vat_deducted", + state_label: "НДС принят к вычету", + state_class: "terminal", + entry_conditions: ["deduction_confirmed"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "НДС-цепочка завершена корректно." + }, + { + state_code: "vat_conflict", + state_label: "Конфликт НДС-цепочки", + state_class: "problematic", + entry_conditions: ["cross_branch_inconsistency"], + exit_conditions: ["vat_reflected"], + is_terminal: false, + is_problematic: true, + business_meaning: "Бухгалтерская и налоговая ветки расходятся." + } + ], + transitions: [], + defects: [] + }, + period_close: { + schema_version: LIFECYCLE_MODEL_SCHEMA_VERSION, + lifecycle_domain: "period_close", + lifecycle_object_types: ["period_close_blocker"], + states: [ + { + state_code: "preclose_checks", + state_label: "Предзакрытие", + state_class: "active", + entry_conditions: ["period_scope_detected"], + exit_conditions: ["close_ready"], + is_terminal: false, + is_problematic: false, + business_meaning: "Идет проверка готовности периода." + }, + { + state_code: "close_ready", + state_label: "Готов к закрытию", + state_class: "active", + entry_conditions: ["no_blockers_detected"], + exit_conditions: ["close_completed"], + is_terminal: false, + is_problematic: false, + business_meaning: "Период может быть закрыт." + }, + { + state_code: "close_completed", + state_label: "Закрытие завершено", + state_class: "terminal", + entry_conditions: ["close_operation_done"], + exit_conditions: [], + is_terminal: true, + is_problematic: false, + business_meaning: "Период закрыт." + }, + { + state_code: "close_blocked", + state_label: "Закрытие заблокировано", + state_class: "problematic", + entry_conditions: ["period_close_risk_or_stale_state"], + exit_conditions: ["close_ready"], + is_terminal: false, + is_problematic: true, + business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие." + }, + { + state_code: "close_contradicted", + state_label: "Закрыт формально, но с противоречием", + state_class: "problematic", + entry_conditions: ["misclosed_or_cross_branch_conflict"], + exit_conditions: ["close_completed"], + is_terminal: false, + is_problematic: true, + business_meaning: "Формальное закрытие не согласовано с фактическими ветками." + } + ], + transitions: [], + defects: [] + } +}; + +const SHARED_DEFECTS: LifecycleDefectDefinition[] = [ + { + defect_code: "missing_expected_transition", + defect_class: "path", + severity_hint: "medium", + business_meaning: "Ожидаемый переход не произошел.", + evidence_requirements: ["expected_state", "missing_transition_signal"], + period_impact_potential: "indirect" + }, + { + defect_code: "invalid_transition", + defect_class: "path", + severity_hint: "high", + business_meaning: "Переход произошел по некорректному пути.", + evidence_requirements: ["invalid_transition_signal"], + period_impact_potential: "indirect" + }, + { + defect_code: "stale_active_state", + defect_class: "timing", + severity_hint: "high", + business_meaning: "Объект завис в активном состоянии.", + evidence_requirements: ["stale_marker", "missing_transition_signal"], + period_impact_potential: "direct" + }, + { + defect_code: "contradictory_state", + defect_class: "consistency", + severity_hint: "high", + business_meaning: "Статусы объекта противоречат друг другу.", + evidence_requirements: ["contradiction_signal"], + period_impact_potential: "direct" + }, + { + defect_code: "premature_terminal_state", + defect_class: "closure", + severity_hint: "medium", + business_meaning: "Терминальное состояние наступило преждевременно.", + evidence_requirements: ["terminal_state", "missing_required_previous_state"], + period_impact_potential: "indirect" + }, + { + defect_code: "misclosed_state", + defect_class: "closure", + severity_hint: "high", + business_meaning: "Контур формально закрыт, но закрыт неверно.", + evidence_requirements: ["wrong_closure_path"], + period_impact_potential: "direct" + }, + { + defect_code: "orphan_intermediate_state", + defect_class: "path", + severity_hint: "medium", + business_meaning: "Промежуточная стадия осталась без корректного продолжения.", + evidence_requirements: ["intermediate_state_without_next"], + period_impact_potential: "indirect" + }, + { + defect_code: "cross_branch_state_conflict", + defect_class: "consistency", + severity_hint: "high", + business_meaning: "Состояния соседних веток учета противоречат друг другу.", + evidence_requirements: ["cross_branch_conflict_signal"], + period_impact_potential: "direct" + } +]; + +for (const domain of STAGE3_LIFECYCLE_DOMAINS) { + LIFECYCLE_DOMAIN_MODELS[domain].defects = SHARED_DEFECTS; +} + +class LifecycleRegistryImpl { + constructor(private readonly models: Record) {} + + public listDomains(): LifecycleDomain[] { + return STAGE3_LIFECYCLE_DOMAINS.slice(); + } + + public getDomain(domain: LifecycleDomain): LifecycleDomainModel { + return this.models[domain]; + } +} + +export const LifecycleRegistry = new LifecycleRegistryImpl(LIFECYCLE_DOMAIN_MODELS); + +function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain { + const unitTokens = [ + input.unit.problem_unit_type, + input.unit.business_defect_class, + input.unit.mechanism_summary, + input.unit.failed_expected_edge ?? "", + input.unit.expected_state ?? "", + input.unit.actual_state ?? "", + ...input.unit.affected_accounts, + ...input.unit.affected_entities, + ...input.unit.affected_documents, + ...input.unit.affected_counterparties, + ...input.candidates.flatMap((item) => item.anomaly_patterns), + ...input.candidates.flatMap((item) => item.relation_pattern_hits) + ] + .join(" ") + .toLowerCase(); + + if (includesAny(unitTokens, [/\bnds\b/, /\bvat\b/, /\btax\b/, /cross[_\s-]?branch/, /\b19\b/, /\b68\b/])) { + return "vat_flow"; + } + if (includesAny(unitTokens, [/\bperiod\b/, /\bclose\b/, /закрыт/, /reporting/]) || input.unit.problem_unit_type === "period_risk_cluster") { + return "period_close"; + } + if (includesAny(unitTokens, [/deferred/, /writeoff/, /рбп/, /\b97\b/])) { + return "deferred_expense"; + } + if (includesAny(unitTokens, [/fixed[_\s-]?asset/, /амортиз/, /ос\b/, /\b01\b/, /\b02\b/, /\b08\b/])) { + return "fixed_asset"; + } + if (includesAny(unitTokens, [/buyer/, /customer/, /дебитор/, /\b62\b/])) { + return "customer_settlement"; + } + return "bank_settlement"; +} + +function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInput): string { + const explicitActual = input.unit.actual_state?.trim(); + if (explicitActual) { + return explicitActual; + } + + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).map((item) => item.toLowerCase()); + const relations = input.candidates.flatMap((item) => item.relation_pattern_hits).map((item) => item.toLowerCase()); + + const hasStale = hasToken(anomalies, /(no_continuation|stale|tail|missing_link|broken_lifecycle|partially_linked)/); + const hasInvalid = hasToken(anomalies, /(posting_mismatch|wrong_document_type|cross_domain_inconsistency|misclose|cross_branch)/); + + if (domain === "bank_settlement") { + if (hasInvalid) return "misclosed_payment"; + if (hasStale) return "stale_unlinked_payment"; + if (hasToken(relations, /payment_to_settlement/)) return "bank_recorded"; + return "initiated_payment"; + } + if (domain === "customer_settlement") { + if (hasStale) return "stale_receivable"; + if (hasToken(relations, /payment|settlement/)) return "payment_recorded"; + return "invoice_issued"; + } + if (domain === "deferred_expense") { + if (hasStale) return "overdue_writeoff"; + if (hasToken(relations, /writeoff|partial/)) return "partially_written_off"; + return "recognized"; + } + if (domain === "fixed_asset") { + if (hasInvalid) return "contradictory_asset_state"; + if (hasToken(relations, /depreciation|amort/)) return "depreciation_active"; + if (hasToken(relations, /accept|учет/)) return "accepted_for_accounting"; + return "capitalized"; + } + if (domain === "vat_flow") { + if (hasInvalid || hasToken(anomalies, /cross_branch|inconsistency/)) return "vat_conflict"; + if (hasToken(relations, /invoice_to_vat|vat/)) return "vat_reflected"; + return "vat_registered"; + } + + if (hasInvalid) return "close_contradicted"; + if (hasStale || input.unit.period_impact?.impact_class === "close_risk") return "close_blocked"; + return "preclose_checks"; +} + +function inferExpectedState(domain: LifecycleDomain, input: LifecycleResolverInput): string { + const explicitExpected = input.unit.expected_state?.trim(); + if (explicitExpected) { + return explicitExpected; + } + return defaultExpectedState(domain); +} + +function inferMissingTransition(input: LifecycleResolverInput): string | null { + if (typeof input.unit.failed_expected_edge === "string" && input.unit.failed_expected_edge.trim().length > 0) { + return input.unit.failed_expected_edge.trim(); + } + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (/(missing_link|no_continuation|broken_lifecycle|tail|unresolved)/.test(anomalies)) { + return "expected_transition_not_observed"; + } + return null; +} + +function inferInvalidTransition(input: LifecycleResolverInput): string | null { + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (/(cross_branch|cross_domain_inconsistency)/.test(anomalies)) { + return "cross_branch_conflict_transition"; + } + if (/(wrong_document_type|posting_mismatch|misclose)/.test(anomalies)) { + return "invalid_document_or_posting_transition"; + } + return null; +} + +export function classifyLifecycleDefect(input: { + domain: LifecycleDomain; + currentState: string; + expectedState: string; + missingTransition: string | null; + invalidTransition: string | null; + periodCloseSensitive: boolean; +}): LifecycleDefectType | null { + const current = input.currentState.toLowerCase(); + if (input.invalidTransition?.includes("cross_branch")) { + return "cross_branch_state_conflict"; + } + if (input.invalidTransition) { + if (current.includes("misclosed") || input.domain === "period_close") { + return "misclosed_state"; + } + return "invalid_transition"; + } + if (input.missingTransition) { + if (current.includes("stale") || current.includes("overdue") || input.periodCloseSensitive) { + return "stale_active_state"; + } + return "missing_expected_transition"; + } + if (current.includes("contradict")) { + return "contradictory_state"; + } + if (current.includes("closed") && !input.expectedState.toLowerCase().includes("closed")) { + return "premature_terminal_state"; + } + if (input.currentState !== input.expectedState && !input.currentState.toLowerCase().includes("closed")) { + return "orphan_intermediate_state"; + } + return null; +} + +function resolutionConfidence(unitConfidence: ProblemConfidence, input: { + hasExplicitStates: boolean; + hasDefectSignal: boolean; + candidateCount: number; + hasSnapshotLimitations: boolean; +}): LifecycleConfidence { + let score = unitConfidence.score; + if (input.hasExplicitStates) score += 0.1; + if (input.hasDefectSignal) score += 0.08; + if (input.candidateCount >= 2) score += 0.05; + if (input.hasSnapshotLimitations) score -= 0.12; + const normalized = clampUnitScore(score); + return { + score: normalized, + grade: lifecycleConfidenceGrade(normalized) + }; +} + +function staleDurationHint(domain: LifecycleDomain, defect: LifecycleDefectType | null, input: LifecycleResolverInput): string | undefined { + const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase(); + if (defect !== "stale_active_state") { + return undefined; + } + if (/(period_boundary|period|close_risk)/.test(anomalies) || domain === "period_close") { + return "period_boundary_exceeded"; + } + return "unknown_snapshot_window"; +} + +function lifecycleInterpretation(input: { + domain: LifecycleDomain; + currentState: string; + expectedState: string; + defect: LifecycleDefectType | null; + missingTransition: string | null; + invalidTransition: string | null; +}): string { + const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`; + if (input.defect === "stale_active_state") { + return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`; + } + if (input.defect === "misclosed_state") { + return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`; + } + if (input.defect === "cross_branch_state_conflict") { + return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`; + } + if (input.defect === "missing_expected_transition") { + return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`; + } + if (input.defect === "invalid_transition") { + return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`; + } + return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`; +} + +export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution { + const lifecycle_domain = inferLifecycleDomain(input); + const currentState = inferCurrentState(lifecycle_domain, input); + const expectedState = inferExpectedState(lifecycle_domain, input); + const missingTransition = inferMissingTransition(input); + const invalidTransition = inferInvalidTransition(input); + const defect = classifyLifecycleDefect({ + domain: lifecycle_domain, + currentState, + expectedState, + missingTransition, + invalidTransition, + periodCloseSensitive: input.unit.period_impact?.impact_class === "close_risk" + }); + const evidenceIds = uniqueStrings(input.unit.evidence_pack, 8); + const limitations = uniqueStrings( + [ + ...input.unit.snapshot_limitations, + ...(input.candidates.some((item) => item.confidence_hint === "low") ? ["low_confidence_candidates_present"] : []), + ...(input.unit.actual_state ? [] : ["actual_state_inferred"]), + ...(input.unit.expected_state ? [] : ["expected_state_inferred"]) + ], + 8 + ); + + const confidence = resolutionConfidence(input.unit.confidence, { + hasExplicitStates: Boolean(input.unit.actual_state || input.unit.expected_state), + hasDefectSignal: Boolean(defect || missingTransition || invalidTransition), + candidateCount: input.candidates.length, + hasSnapshotLimitations: limitations.length > 0 + }); + + return { + lifecycle_object_id: `lcobj-${input.unit.problem_unit_id}`, + lifecycle_domain, + resolved_current_state: currentState, + resolved_expected_state: expectedState, + resolved_previous_states: [], + missing_transitions: missingTransition ? [missingTransition] : [], + invalid_transitions: invalidTransition ? [invalidTransition] : [], + detected_defects: defect ? [defect] : [], + state_confidence: confidence, + resolution_evidence: evidenceIds, + snapshot_limitations: limitations + }; +} + +function lifecycleRanking(defect: LifecycleDefectType | null, input: { + unit: ProblemUnit; + resolution: LifecycleResolution; + staleDuration?: string; +}): LifecycleRankingResult { + let score = input.unit.severity.score; + const basis: string[] = ["base_problem_severity"]; + + if (defect === "cross_branch_state_conflict") { + score += 0.55; + basis.push("cross_branch_conflict_weight"); + } else if (defect === "misclosed_state") { + score += 0.45; + basis.push("misclosed_state_weight"); + } else if (defect === "stale_active_state") { + score += 0.35; + basis.push("stale_duration_weight"); + } else if (defect === "invalid_transition") { + score += 0.3; + basis.push("invalid_transition_weight"); + } else if (defect === "missing_expected_transition") { + score += 0.25; + basis.push("missing_transition_weight"); + } + + if (input.staleDuration) { + score += 0.15; + basis.push("stale_duration_present"); + } + if (input.unit.period_impact?.impact_class === "close_risk") { + score += 0.22; + basis.push("period_close_impact"); + } + if (input.resolution.state_confidence.grade === "high") { + score += 0.08; + basis.push("state_confidence_weight"); + } + + return { + lifecycle_ranking_score: Number(score.toFixed(2)), + lifecycle_ranking_basis: basis + }; +} + +export function enrichProblemUnitLifecycle(input: LifecycleResolverInput): ProblemUnit { + const resolution = resolveLifecycle(input); + const defect = resolution.detected_defects[0] ?? null; + const staleDuration = staleDurationHint(resolution.lifecycle_domain, defect, input); + const ranking = lifecycleRanking(defect, { + unit: input.unit, + resolution, + staleDuration + }); + + return { + ...input.unit, + lifecycle_domain: resolution.lifecycle_domain, + lifecycle_object_id: resolution.lifecycle_object_id, + current_lifecycle_state: resolution.resolved_current_state, + expected_lifecycle_state: resolution.resolved_expected_state, + ...(resolution.missing_transitions.length > 0 + ? { + missing_transition: resolution.missing_transitions[0] + } + : {}), + ...(resolution.invalid_transitions.length > 0 + ? { + invalid_transition: resolution.invalid_transitions[0] + } + : {}), + ...(defect + ? { + lifecycle_defect_type: defect + } + : {}), + ...(staleDuration + ? { + stale_duration: staleDuration + } + : {}), + lifecycle_confidence: resolution.state_confidence, + business_lifecycle_interpretation: lifecycleInterpretation({ + domain: resolution.lifecycle_domain, + currentState: resolution.resolved_current_state, + expectedState: resolution.resolved_expected_state, + defect, + missingTransition: resolution.missing_transitions[0] ?? null, + invalidTransition: resolution.invalid_transitions[0] ?? null + }), + lifecycle_resolution: resolution, + lifecycle_ranking_score: ranking.lifecycle_ranking_score, + lifecycle_ranking_basis: ranking.lifecycle_ranking_basis + }; +} + +export function rankLifecycleProblemUnits(units: ProblemUnit[]): ProblemUnit[] { + return units + .slice() + .sort((left, right) => { + const rankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0); + if (rankDiff !== 0) return rankDiff; + const severityDiff = right.severity.score - left.severity.score; + if (severityDiff !== 0) return severityDiff; + return right.confidence.score - left.confidence.score; + }); +} + diff --git a/llm_normalizer/backend/src/services/problemUnitAssembler.ts b/llm_normalizer/backend/src/services/problemUnitAssembler.ts index 476a4fc..b90b158 100644 --- a/llm_normalizer/backend/src/services/problemUnitAssembler.ts +++ b/llm_normalizer/backend/src/services/problemUnitAssembler.ts @@ -13,6 +13,8 @@ import { PROBLEM_UNIT_SCHEMA_VERSION, PROBLEM_UNIT_SUMMARY_SCHEMA_VERSION } from "../types/stage2ProblemUnits"; +import { FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 } from "../config"; +import { enrichProblemUnitLifecycle, rankLifecycleProblemUnits } from "./lifecycleRuntime"; type RetrievalResultType = "list" | "summary" | "object" | "chain" | "ranking"; @@ -486,6 +488,18 @@ function collapseSignature(unit: ProblemUnit): string { return [unit.problem_unit_type, unit.business_defect_class, unit.failed_expected_edge ?? "none", backlink].join("|"); } +function preferredLifecycleSource(left: ProblemUnit, right: ProblemUnit): ProblemUnit { + const leftRank = left.lifecycle_ranking_score ?? 0; + const rightRank = right.lifecycle_ranking_score ?? 0; + if (rightRank > leftRank) { + return right; + } + if (leftRank > rightRank) { + return left; + } + return right.confidence.score > left.confidence.score ? right : left; +} + export function collapseDuplicates(units: ProblemUnit[]): { problem_units: ProblemUnit[]; duplicate_collapses: number; @@ -502,6 +516,7 @@ export function collapseDuplicates(units: ProblemUnit[]): { } duplicateCollapses += 1; + const preferredLifecycle = preferredLifecycleSource(existing, unit); bySignature.set(signature, { ...existing, evidence_pack: uniqueStrings([...existing.evidence_pack, ...unit.evidence_pack]), @@ -521,7 +536,72 @@ export function collapseDuplicates(units: ProblemUnit[]): { affected_contracts: uniqueStrings([...existing.affected_contracts, ...unit.affected_contracts]), snapshot_limitations: uniqueStrings([...existing.snapshot_limitations, ...unit.snapshot_limitations]), severity: unit.severity.score > existing.severity.score ? unit.severity : existing.severity, - confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence + confidence: unit.confidence.score > existing.confidence.score ? unit.confidence : existing.confidence, + ...(preferredLifecycle.lifecycle_domain + ? { + lifecycle_domain: preferredLifecycle.lifecycle_domain + } + : {}), + ...(preferredLifecycle.lifecycle_object_id + ? { + lifecycle_object_id: preferredLifecycle.lifecycle_object_id + } + : {}), + ...(preferredLifecycle.current_lifecycle_state + ? { + current_lifecycle_state: preferredLifecycle.current_lifecycle_state + } + : {}), + ...(preferredLifecycle.expected_lifecycle_state + ? { + expected_lifecycle_state: preferredLifecycle.expected_lifecycle_state + } + : {}), + ...(preferredLifecycle.missing_transition + ? { + missing_transition: preferredLifecycle.missing_transition + } + : {}), + ...(preferredLifecycle.invalid_transition + ? { + invalid_transition: preferredLifecycle.invalid_transition + } + : {}), + ...(preferredLifecycle.lifecycle_defect_type + ? { + lifecycle_defect_type: preferredLifecycle.lifecycle_defect_type + } + : {}), + ...(preferredLifecycle.stale_duration + ? { + stale_duration: preferredLifecycle.stale_duration + } + : {}), + ...(preferredLifecycle.lifecycle_confidence + ? { + lifecycle_confidence: preferredLifecycle.lifecycle_confidence + } + : {}), + ...(preferredLifecycle.business_lifecycle_interpretation + ? { + business_lifecycle_interpretation: preferredLifecycle.business_lifecycle_interpretation + } + : {}), + ...(preferredLifecycle.lifecycle_resolution + ? { + lifecycle_resolution: preferredLifecycle.lifecycle_resolution + } + : {}), + ...(preferredLifecycle.lifecycle_ranking_score !== undefined + ? { + lifecycle_ranking_score: preferredLifecycle.lifecycle_ranking_score + } + : {}), + ...(preferredLifecycle.lifecycle_ranking_basis + ? { + lifecycle_ranking_basis: preferredLifecycle.lifecycle_ranking_basis + } + : {}) }); } @@ -544,11 +624,21 @@ function buildSummary(units: ProblemUnit[], duplicateCollapses: number): Problem medium: 0, high: 0 }; + const lifecycleDomainDistribution: NonNullable = {}; + const lifecycleDefectDistribution: NonNullable = {}; + let lifecycleEnrichedUnits = 0; for (const unit of units) { typeDistribution[unit.problem_unit_type] = (typeDistribution[unit.problem_unit_type] ?? 0) + 1; severityDistribution[unit.severity.grade] += 1; confidenceDistribution[unit.confidence.grade] += 1; + if (unit.lifecycle_domain) { + lifecycleEnrichedUnits += 1; + lifecycleDomainDistribution[unit.lifecycle_domain] = (lifecycleDomainDistribution[unit.lifecycle_domain] ?? 0) + 1; + } + if (unit.lifecycle_defect_type) { + lifecycleDefectDistribution[unit.lifecycle_defect_type] = (lifecycleDefectDistribution[unit.lifecycle_defect_type] ?? 0) + 1; + } } return { @@ -559,7 +649,10 @@ function buildSummary(units: ProblemUnit[], duplicateCollapses: number): Problem type_distribution: typeDistribution, severity_distribution: severityDistribution, confidence_distribution: confidenceDistribution, - primary_unit_type: units[0]?.problem_unit_type ?? null + primary_unit_type: units[0]?.problem_unit_type ?? null, + lifecycle_enriched_units: lifecycleEnrichedUnits, + lifecycle_domain_distribution: lifecycleDomainDistribution, + lifecycle_defect_distribution: lifecycleDefectDistribution }; } @@ -580,13 +673,24 @@ export function assembleProblemUnits(input: AssembleProblemUnitsInput): { risk_factors: uniqueStrings(input.risk_factors ?? []) }); const clusters = clusterCandidateEvidence(candidates); - const units = clusters.map((cluster, index) => buildProblemUnit(cluster, index)); + const units = clusters.map((cluster, index) => { + const baseUnit = buildProblemUnit(cluster, index); + if (!FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1) { + return baseUnit; + } + return enrichProblemUnitLifecycle({ + unit: baseUnit, + candidates: cluster.candidates + }); + }); const collapsed = collapseDuplicates(units); + const rankedUnits = FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 + ? rankLifecycleProblemUnits(collapsed.problem_units) + : collapsed.problem_units; return { candidate_evidence: candidates, - problem_units: collapsed.problem_units, - problem_unit_summary: buildSummary(collapsed.problem_units, collapsed.duplicate_collapses) + problem_units: rankedUnits, + problem_unit_summary: buildSummary(rankedUnits, collapsed.duplicate_collapses) }; } - diff --git a/llm_normalizer/backend/src/services/retrievalResultNormalizer.ts b/llm_normalizer/backend/src/services/retrievalResultNormalizer.ts index fefbcd4..93f6253 100644 --- a/llm_normalizer/backend/src/services/retrievalResultNormalizer.ts +++ b/llm_normalizer/backend/src/services/retrievalResultNormalizer.ts @@ -104,6 +104,9 @@ function mergeSummaryWithProblemUnitMeta( duplicateCollapses: number; severityDistribution: Record; confidenceDistribution: Record; + lifecycleEnrichedUnits: number; + lifecycleDomainDistribution: Record; + lifecycleDefectDistribution: Record; } ): Record { return { @@ -114,7 +117,10 @@ function mergeSummaryWithProblemUnitMeta( problem_unit_types: input.unitTypes, problem_unit_duplicate_collapses: input.duplicateCollapses, problem_unit_severity_distribution: input.severityDistribution, - problem_unit_confidence_distribution: input.confidenceDistribution + problem_unit_confidence_distribution: input.confidenceDistribution, + lifecycle_enriched_units: input.lifecycleEnrichedUnits, + problem_unit_lifecycle_domain_distribution: input.lifecycleDomainDistribution, + problem_unit_lifecycle_defect_distribution: input.lifecycleDefectDistribution }; } @@ -526,7 +532,10 @@ export function normalizeRetrievalResult( unitTypes: assembled.problem_unit_summary.unit_types, duplicateCollapses: assembled.problem_unit_summary.duplicate_collapses, severityDistribution: assembled.problem_unit_summary.severity_distribution, - confidenceDistribution: assembled.problem_unit_summary.confidence_distribution + confidenceDistribution: assembled.problem_unit_summary.confidence_distribution, + lifecycleEnrichedUnits: assembled.problem_unit_summary.lifecycle_enriched_units ?? 0, + lifecycleDomainDistribution: (assembled.problem_unit_summary.lifecycle_domain_distribution ?? {}) as Record, + lifecycleDefectDistribution: (assembled.problem_unit_summary.lifecycle_defect_distribution ?? {}) as Record }); return { diff --git a/llm_normalizer/backend/src/types/assistant.ts b/llm_normalizer/backend/src/types/assistant.ts index a3dd1e3..61c49f5 100644 --- a/llm_normalizer/backend/src/types/assistant.ts +++ b/llm_normalizer/backend/src/types/assistant.ts @@ -21,7 +21,10 @@ export type AssistantReplyType = export type RetrievalResultStatus = "ok" | "empty" | "partial" | "error"; export type RetrievalResultType = "list" | "summary" | "object" | "chain" | "ranking"; export type RetrievalConfidence = "high" | "medium" | "low"; -export type AssistantProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1"; +export type AssistantProblemAnswerMode = + | "stage1_policy_v11" + | "stage2_problem_centric_v1" + | "stage3_lifecycle_aware_v1"; export interface AssistantRequirement { requirement_id: string; diff --git a/llm_normalizer/backend/src/types/stage2ProblemUnits.ts b/llm_normalizer/backend/src/types/stage2ProblemUnits.ts index 20101b5..e0b1b64 100644 --- a/llm_normalizer/backend/src/types/stage2ProblemUnits.ts +++ b/llm_normalizer/backend/src/types/stage2ProblemUnits.ts @@ -1,5 +1,6 @@ import type { InvestigationState } from "./stage1Contracts"; import type { EvidenceSourceRef } from "./stage1Contracts"; +import type { LifecycleConfidence, LifecycleDefectType, LifecycleDomain, LifecycleResolution } from "./stage3Lifecycle"; export const CANDIDATE_EVIDENCE_SCHEMA_VERSION = "candidate_evidence_v0_1" as const; export const PROBLEM_UNIT_SCHEMA_VERSION = "problem_unit_v0_1" as const; @@ -75,6 +76,19 @@ export interface ProblemUnit { evidence_pack: string[]; entity_backlinks: ProblemUnitEntityBacklink[]; snapshot_limitations: string[]; + lifecycle_domain?: LifecycleDomain; + lifecycle_object_id?: string; + current_lifecycle_state?: string; + expected_lifecycle_state?: string; + missing_transition?: string; + invalid_transition?: string; + lifecycle_defect_type?: LifecycleDefectType; + stale_duration?: string; + lifecycle_confidence?: LifecycleConfidence; + business_lifecycle_interpretation?: string; + lifecycle_resolution?: LifecycleResolution; + lifecycle_ranking_score?: number; + lifecycle_ranking_basis?: string[]; } export interface ProblemUnitSummary { @@ -86,6 +100,9 @@ export interface ProblemUnitSummary { severity_distribution: Record; confidence_distribution: Record; primary_unit_type: ProblemUnitType | null; + lifecycle_enriched_units?: number; + lifecycle_domain_distribution?: Partial>; + lifecycle_defect_distribution?: Partial>; } export interface InvestigationProblemUnitState { diff --git a/llm_normalizer/backend/src/types/stage3Lifecycle.ts b/llm_normalizer/backend/src/types/stage3Lifecycle.ts new file mode 100644 index 0000000..2d678a3 --- /dev/null +++ b/llm_normalizer/backend/src/types/stage3Lifecycle.ts @@ -0,0 +1,90 @@ +export const LIFECYCLE_MODEL_SCHEMA_VERSION = "lifecycle_model_v0_1" as const; + +export const STAGE3_LIFECYCLE_DOMAINS = [ + "bank_settlement", + "customer_settlement", + "deferred_expense", + "fixed_asset", + "vat_flow", + "period_close" +] as const; + +export type LifecycleDomain = (typeof STAGE3_LIFECYCLE_DOMAINS)[number]; + +export type LifecycleStateClass = "initial" | "active" | "terminal" | "problematic"; + +export type LifecycleTransitionType = "expected" | "optional" | "forbidden"; + +export const LIFECYCLE_DEFECT_TYPES = [ + "missing_expected_transition", + "invalid_transition", + "stale_active_state", + "contradictory_state", + "premature_terminal_state", + "misclosed_state", + "orphan_intermediate_state", + "cross_branch_state_conflict" +] as const; + +export type LifecycleDefectType = (typeof LIFECYCLE_DEFECT_TYPES)[number]; + +export interface LifecycleStateDefinition { + state_code: string; + state_label: string; + state_class: LifecycleStateClass; + entry_conditions: string[]; + exit_conditions: string[]; + is_terminal: boolean; + is_problematic: boolean; + business_meaning: string; +} + +export interface LifecycleTransitionDefinition { + from_state: string; + to_state: string; + transition_type: LifecycleTransitionType; + required_evidence: string[]; + optional_evidence: string[]; + forbidden_conditions: string[]; + business_meaning: string; +} + +export interface LifecycleDefectDefinition { + defect_code: LifecycleDefectType; + defect_class: "timing" | "path" | "consistency" | "closure"; + severity_hint: "low" | "medium" | "high"; + business_meaning: string; + evidence_requirements: string[]; + period_impact_potential: "none" | "indirect" | "direct"; +} + +export interface LifecycleDomainModel { + schema_version: typeof LIFECYCLE_MODEL_SCHEMA_VERSION; + lifecycle_domain: LifecycleDomain; + lifecycle_object_types: string[]; + states: LifecycleStateDefinition[]; + transitions: LifecycleTransitionDefinition[]; + defects: LifecycleDefectDefinition[]; +} + +export type LifecycleConfidenceGrade = "low" | "medium" | "high"; + +export interface LifecycleConfidence { + score: number; + grade: LifecycleConfidenceGrade; +} + +export interface LifecycleResolution { + lifecycle_object_id: string; + lifecycle_domain: LifecycleDomain; + resolved_current_state: string; + resolved_expected_state: string; + resolved_previous_states: string[]; + missing_transitions: string[]; + invalid_transitions: string[]; + detected_defects: LifecycleDefectType[]; + state_confidence: LifecycleConfidence; + resolution_evidence: string[]; + snapshot_limitations: string[]; +} + diff --git a/llm_normalizer/backend/tests/assistantLifecycleAwareAnswerV1.test.ts b/llm_normalizer/backend/tests/assistantLifecycleAwareAnswerV1.test.ts new file mode 100644 index 0000000..283fa34 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantLifecycleAwareAnswerV1.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, it } from "vitest"; +import { composeAssistantAnswer } from "../src/services/answerComposer"; +import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant"; +import type { ProblemUnit, ProblemUnitSummary } from "../src/types/stage2ProblemUnits"; + +function buildRouteSummary() { + return { + mode: "deterministic_v2" as const, + message_in_scope: true, + scope_confidence: "high" as const, + planner: { + total_fragments: 1, + in_scope_fragments: 1, + out_of_scope_fragments: 0, + discarded_fragments: 0, + contains_multiple_tasks: false + }, + decisions: [], + fallback: { + type: "none" as const, + message: null + } + }; +} + +function buildCoverage(partial = false): RequirementCoverageReport { + return { + requirements_total: 1, + requirements_covered: partial ? 0 : 1, + requirements_uncovered: partial ? ["R1"] : [], + requirements_partially_covered: partial ? ["R1"] : [], + clarification_needed_for: [], + out_of_scope_requirements: [] + }; +} + +function buildGrounding(status: AnswerGroundingCheck["status"]): AnswerGroundingCheck { + return { + status, + route_subject_match: true, + missing_requirements: status === "partial" ? ["R1"] : [], + reasons: status === "partial" ? ["Coverage is partial for lifecycle-focused analysis."] : [], + why_included_summary: ["synthetic-test"], + selection_reason_summary: ["synthetic-test"] + }; +} + +function buildLifecycleProblemUnit(): ProblemUnit { + return { + schema_version: "problem_unit_v0_1", + problem_unit_id: "pu-lc-1", + problem_unit_type: "lifecycle_anomaly_node", + title: "Lifecycle anomaly node detected", + mechanism_summary: "Mechanism candidate: expected transition is missing.", + business_defect_class: "missing_expected_transition", + severity: { + score: 0.84, + grade: "high" + }, + confidence: { + score: 0.68, + grade: "medium" + }, + affected_entities: ["Document:DOC-1"], + affected_documents: ["Document:DOC-1"], + affected_postings: [], + affected_accounts: ["51", "60"], + affected_counterparties: ["Counterparty:CP-1"], + affected_contracts: ["Contract:CTR-1"], + expected_state: "settlement_closed", + actual_state: "stale_unlinked_payment", + failed_expected_edge: "payment_to_settlement", + period_impact: { + is_period_sensitive: true, + impact_class: "close_risk" + }, + evidence_pack: ["cand-1"], + entity_backlinks: [{ entity: "Document", id: "DOC-1" }], + snapshot_limitations: [], + lifecycle_domain: "bank_settlement", + lifecycle_object_id: "lcobj-pu-lc-1", + current_lifecycle_state: "stale_unlinked_payment", + expected_lifecycle_state: "settlement_closed", + missing_transition: "payment_to_settlement", + lifecycle_defect_type: "stale_active_state", + stale_duration: "period_boundary_exceeded", + lifecycle_confidence: { + score: 0.79, + grade: "high" + }, + business_lifecycle_interpretation: + "Текущая стадия: stale_unlinked_payment; ожидаемая стадия: settlement_closed. Объект завис во времени и не дошел до ожидаемого перехода.", + lifecycle_ranking_score: 1.41, + lifecycle_ranking_basis: ["base_problem_severity", "stale_duration_weight", "period_close_impact"] + }; +} + +function buildSummary(units: ProblemUnit[]): ProblemUnitSummary { + const unitTypes = Array.from(new Set(units.map((item) => item.problem_unit_type))); + return { + schema_version: "problem_unit_summary_v0_1", + units_total: units.length, + duplicate_collapses: 0, + unit_types: unitTypes, + type_distribution: { + lifecycle_anomaly_node: units.length + }, + severity_distribution: { + low: 0, + medium: 0, + high: units.length + }, + confidence_distribution: { + low: 0, + medium: units.length, + high: 0 + }, + primary_unit_type: unitTypes[0] ?? null, + lifecycle_enriched_units: units.length, + lifecycle_domain_distribution: { + bank_settlement: units.length + }, + lifecycle_defect_distribution: { + stale_active_state: units.length + } + }; +} + +function buildRetrievalResult(problemUnits: ProblemUnit[]): UnifiedRetrievalResult { + return { + fragment_id: "F1", + requirement_ids: ["R1"], + route: "hybrid_store_plus_live", + status: "ok", + result_type: "chain", + items: [ + { + counterparty_id: "CP-1", + operations_count: 5, + document_refs_count: 3 + } + ], + raw_entities: [], + candidate_evidence: [], + problem_units: problemUnits, + problem_unit_summary: buildSummary(problemUnits), + summary: { + broad_query_detected: true, + broad_result_flag: true, + minimum_evidence_failed: false, + degraded_to: "partial", + narrowing_strength: "weak" + }, + evidence: [ + { + evidence_id: "ev-1", + claim_ref: "requirement:R1", + source_type: "retrieval_item", + source_ref: { + schema_version: "evidence_source_ref_v1", + namespace: "snapshot_2020", + entity: "Document", + id: "DOC-1", + period: "2020-06", + canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06" + }, + pointer: { + fragment_id: "F1", + route: "hybrid_store_plus_live", + source: { + namespace: "snapshot_2020", + entity: "Document", + id: "DOC-1", + period: "2020-06" + }, + locator: { + field_path: "risk_score", + item_index: 0 + } + }, + evidence_kind: "mechanism_link", + mechanism_note: "failed_edge=payment_to_settlement", + confidence: "medium", + limitation: null, + payload: { + risk_score: 5 + } + } + ], + why_included: ["synthetic-test"], + selection_reason: ["synthetic-test"], + risk_factors: ["broken_chain"], + business_interpretation: ["synthetic-test"], + confidence: "medium", + limitations: [], + errors: [] + }; +} + +describe("assistant lifecycle-aware answer mode v1", () => { + it("promotes stage3 lifecycle mode when lifecycle answer flag is enabled", () => { + const units = [buildLifecycleProblemUnit()]; + const output = composeAssistantAnswer({ + userMessage: "Проверь, где зависли платежи по 51/60 и какой переход не завершился.", + routeSummary: buildRouteSummary(), + retrievalResults: [buildRetrievalResult(units)], + requirements: [ + { + requirement_id: "R1", + source_fragment_id: "F1", + requirement_text: "Проверить lifecycle-переход", + subject_tokens: ["chain", "account_51", "account_60"], + status: "covered", + route: "hybrid_store_plus_live" + } + ], + coverageReport: buildCoverage(true), + groundingCheck: buildGrounding("partial"), + enableAnswerPolicyV11: true, + enableProblemCentricAnswerV1: true, + enableLifecycleAnswerV1: true + }); + + expect(output.problem_centric_answer_applied).toBe(true); + expect(output.problem_answer_mode).toBe("stage3_lifecycle_aware_v1"); + expect(output.answer_structure_v11?.answer_summary).toMatch(/lifecycle|Lifecycle/i); + expect(output.answer_structure_v11?.direct_answer).toContain("current=stale_unlinked_payment"); + expect(output.answer_structure_v11?.direct_answer).toContain("expected=settlement_closed"); + expect(output.answer_structure_v11?.direct_answer).toContain("defect=stale_active_state"); + }); +}); + diff --git a/llm_normalizer/backend/tests/lifecycleRuntime.test.ts b/llm_normalizer/backend/tests/lifecycleRuntime.test.ts new file mode 100644 index 0000000..f68333c --- /dev/null +++ b/llm_normalizer/backend/tests/lifecycleRuntime.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import type { CandidateEvidenceItem, ProblemUnit } from "../src/types/stage2ProblemUnits"; +import { enrichProblemUnitLifecycle, rankLifecycleProblemUnits, resolveLifecycle } from "../src/services/lifecycleRuntime"; + +function buildBaseProblemUnit(input: { + id: string; + type: ProblemUnit["problem_unit_type"]; + expectedState?: string; + actualState?: string; + failedEdge?: string; + impactCloseRisk?: boolean; + severity?: number; + confidence?: number; +}): ProblemUnit { + const severityScore = input.severity ?? 0.62; + const confidenceScore = input.confidence ?? 0.58; + const severityGrade: ProblemUnit["severity"]["grade"] = severityScore >= 0.7 ? "high" : severityScore >= 0.4 ? "medium" : "low"; + const confidenceGrade: ProblemUnit["confidence"]["grade"] = + confidenceScore >= 0.75 ? "high" : confidenceScore >= 0.45 ? "medium" : "low"; + return { + schema_version: "problem_unit_v0_1", + problem_unit_id: input.id, + problem_unit_type: input.type, + title: "Synthetic unit", + mechanism_summary: "Synthetic mechanism", + business_defect_class: "broken_lifecycle", + severity: { + score: Number(severityScore.toFixed(2)), + grade: severityGrade + }, + confidence: { + score: Number(confidenceScore.toFixed(2)), + grade: confidenceGrade + }, + affected_entities: ["Document:DOC-1"], + affected_documents: ["Document:DOC-1"], + affected_postings: [], + affected_accounts: ["51", "60"], + affected_counterparties: [], + affected_contracts: [], + ...(input.expectedState + ? { + expected_state: input.expectedState + } + : {}), + ...(input.actualState + ? { + actual_state: input.actualState + } + : {}), + ...(input.failedEdge + ? { + failed_expected_edge: input.failedEdge + } + : {}), + ...(input.impactCloseRisk + ? { + period_impact: { + is_period_sensitive: true, + impact_class: "close_risk" as const + } + } + : {}), + evidence_pack: ["cand-1"], + entity_backlinks: [{ entity: "Document", id: "DOC-1" }], + snapshot_limitations: [] + }; +} + +function buildCandidate(input: { + id: string; + anomalies?: string[]; + relations?: string[]; + confidence?: "high" | "medium" | "low"; +}): CandidateEvidenceItem { + return { + schema_version: "candidate_evidence_v0_1", + candidate_id: input.id, + route: "hybrid_store_plus_live", + source_ref: { + schema_version: "evidence_source_ref_v1", + namespace: "snapshot_2020", + entity: "Document", + id: "DOC-1", + period: "2020-06", + canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06" + }, + relation_pattern_hits: input.relations ?? [], + anomaly_patterns: input.anomalies ?? [], + entity_backlinks: [{ entity: "Document", id: "DOC-1" }], + confidence_hint: input.confidence ?? "medium" + }; +} + +describe("lifecycle runtime stage3", () => { + it("resolves stale missing transition for bank settlement chains", () => { + const unit = buildBaseProblemUnit({ + id: "pu-bank-1", + type: "broken_chain_segment", + expectedState: "settlement_closed", + failedEdge: "payment_to_settlement" + }); + const candidates = [ + buildCandidate({ + id: "cand-1", + relations: ["payment_to_settlement", "statement_to_document"], + anomalies: ["broken_lifecycle", "missing_link", "no_continuation"] + }) + ]; + + const resolution = resolveLifecycle({ unit, candidates }); + expect(resolution.lifecycle_domain).toBe("bank_settlement"); + expect(resolution.missing_transitions.length).toBeGreaterThan(0); + expect(resolution.detected_defects[0]).toBe("stale_active_state"); + }); + + it("classifies cross-branch conflicts for VAT flow", () => { + const unit = buildBaseProblemUnit({ + id: "pu-vat-1", + type: "cross_branch_inconsistency_cluster" + }); + const candidates = [ + buildCandidate({ + id: "cand-1", + anomalies: ["cross_branch_inconsistency", "vat_chain_conflict"], + relations: ["invoice_to_vat"] + }) + ]; + + const enriched = enrichProblemUnitLifecycle({ unit, candidates }); + expect(enriched.lifecycle_domain).toBe("vat_flow"); + expect(enriched.lifecycle_defect_type).toBe("cross_branch_state_conflict"); + expect(typeof enriched.business_lifecycle_interpretation).toBe("string"); + }); + + it("sorts lifecycle-ranked units by lifecycle severity and ranking score", () => { + const stale = enrichProblemUnitLifecycle({ + unit: buildBaseProblemUnit({ + id: "pu-stale", + type: "period_risk_cluster", + impactCloseRisk: true, + expectedState: "close_completed" + }), + candidates: [buildCandidate({ id: "cand-stale", anomalies: ["period_close_risk", "missing_link", "no_continuation"] })] + }); + + const generic = enrichProblemUnitLifecycle({ + unit: buildBaseProblemUnit({ + id: "pu-generic", + type: "document_conflict", + severity: 0.4, + confidence: 0.45 + }), + candidates: [buildCandidate({ id: "cand-generic", anomalies: ["anomaly_signal"] })] + }); + + const ranked = rankLifecycleProblemUnits([generic, stale]); + expect(ranked[0].problem_unit_id).toBe("pu-stale"); + expect((ranked[0].lifecycle_ranking_score ?? 0) >= (ranked[1].lifecycle_ranking_score ?? 0)).toBe(true); + }); +}); + diff --git a/llm_normalizer/backend/tests/retrievalLifecycleRuntimeRollout.test.ts b/llm_normalizer/backend/tests/retrievalLifecycleRuntimeRollout.test.ts new file mode 100644 index 0000000..d77e48d --- /dev/null +++ b/llm_normalizer/backend/tests/retrievalLifecycleRuntimeRollout.test.ts @@ -0,0 +1,116 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const PROBLEM_UNITS_FLAG = "FEATURE_ASSISTANT_PROBLEM_UNITS_V1"; +const LIFECYCLE_RUNTIME_FLAG = "FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1"; +const ORIGINAL_PROBLEM_UNITS_FLAG = process.env[PROBLEM_UNITS_FLAG]; +const ORIGINAL_LIFECYCLE_RUNTIME_FLAG = process.env[LIFECYCLE_RUNTIME_FLAG]; + +function restoreFlags(): void { + if (ORIGINAL_PROBLEM_UNITS_FLAG === undefined) { + delete process.env[PROBLEM_UNITS_FLAG]; + } else { + process.env[PROBLEM_UNITS_FLAG] = ORIGINAL_PROBLEM_UNITS_FLAG; + } + + if (ORIGINAL_LIFECYCLE_RUNTIME_FLAG === undefined) { + delete process.env[LIFECYCLE_RUNTIME_FLAG]; + } else { + process.env[LIFECYCLE_RUNTIME_FLAG] = ORIGINAL_LIFECYCLE_RUNTIME_FLAG; + } +} + +async function normalizeWithFlags(input: { + problemUnitsFlag: "0" | "1"; + lifecycleRuntimeFlag: "0" | "1"; +}) { + process.env[PROBLEM_UNITS_FLAG] = input.problemUnitsFlag; + process.env[LIFECYCLE_RUNTIME_FLAG] = input.lifecycleRuntimeFlag; + vi.resetModules(); + const { normalizeRetrievalResult } = await import("../src/services/retrievalResultNormalizer"); + return normalizeRetrievalResult("F1", ["R1"], "hybrid_store_plus_live", { + status: "ok", + result_type: "list", + items: [ + { + source_entity: "Document", + source_id: "DOC-1", + risk_score: 4 + } + ], + summary: { + broad_query_detected: false + }, + evidence: [ + { + evidence_id: "ev-1", + claim_ref: "requirement:R1", + source_type: "retrieval_item", + pointer: { + fragment_id: "F1", + route: "hybrid_store_plus_live", + source: { + namespace: "snapshot_2020", + entity: "Document", + id: "DOC-1", + period: "2020-06" + }, + locator: { + field_path: "risk_score", + item_index: 0 + } + }, + failed_expected_edge: "payment_to_settlement", + anomaly_patterns: ["period_close_risk", "broken_lifecycle", "missing_link", "no_continuation"], + confidence: "medium" + } + ], + why_included: ["test"], + selection_reason: ["test"], + risk_factors: ["test"], + business_interpretation: ["test"], + confidence: "medium", + limitations: [], + errors: [] + }); +} + +describe.sequential("retrieval lifecycle runtime rollout", () => { + afterEach(() => { + restoreFlags(); + vi.resetModules(); + }); + + it("keeps Stage 2 payload when lifecycle runtime flag is OFF", async () => { + const result = await normalizeWithFlags({ + problemUnitsFlag: "1", + lifecycleRuntimeFlag: "0" + }); + expect(Array.isArray(result.problem_units)).toBe(true); + const first = result.problem_units?.[0]; + expect(first?.lifecycle_domain).toBeUndefined(); + expect(first?.lifecycle_defect_type).toBeUndefined(); + expect(result.summary.lifecycle_enriched_units).toBe(0); + }); + + it("adds lifecycle fields to problem units when lifecycle runtime flag is ON", async () => { + const result = await normalizeWithFlags({ + problemUnitsFlag: "1", + lifecycleRuntimeFlag: "1" + }); + + expect(Array.isArray(result.problem_units)).toBe(true); + expect(result.problem_units?.length).toBeGreaterThan(0); + const first = result.problem_units?.[0]; + expect(first?.lifecycle_domain).toBeTruthy(); + expect(first?.current_lifecycle_state).toBeTruthy(); + expect(first?.expected_lifecycle_state).toBeTruthy(); + expect(first?.lifecycle_defect_type).toBeTruthy(); + expect(typeof first?.business_lifecycle_interpretation).toBe("string"); + + expect(result.problem_unit_summary?.lifecycle_enriched_units).toBeGreaterThan(0); + expect(result.summary.lifecycle_enriched_units).toBeGreaterThan(0); + expect(result.summary.problem_unit_lifecycle_domain_distribution).toBeTruthy(); + expect(result.summary.problem_unit_lifecycle_defect_distribution).toBeTruthy(); + }); +}); + diff --git a/llm_normalizer/data/eval_cases/eval-0M5PNZp9FY.report.json b/llm_normalizer/data/eval_cases/eval-0M5PNZp9FY.report.json new file mode 100644 index 0000000..10f788a --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-0M5PNZp9FY.report.json @@ -0,0 +1,135 @@ +{ + "run_id": "eval-0M5PNZp9FY", + "timestamp": "2026-03-26T12:41:47.018Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 3 + }, + "cases_total": 3, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 33.33, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 33.33, + "routed_fragment_rate": 33.33, + "no_route_fragment_rate": 66.67, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 3, + "checks_passed": 3 + }, + "route_distribution": { + "store_feature_risk": 1, + "no_route": 2 + }, + "fallback_distribution": { + "none": 1, + "out_of_scope": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь хвосты по поставщикам и разложи цепочку", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "JOdfhbRQuPyASw", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Как вообще по ФСБУ", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 1, + "unclear_fragments": 0, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "out_of_scope", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "2ynm4eijBPvh5R", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-003", + "raw_question": "Покажи топ рисков за июнь 2020", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 0, + "unclear_fragments": 1, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "insufficient_specificity", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "2ooQZ_w1Ek5E9S", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-CcP-swdiJD.report.json b/llm_normalizer/data/eval_cases/eval-CcP-swdiJD.report.json new file mode 100644 index 0000000..eab3392 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-CcP-swdiJD.report.json @@ -0,0 +1,135 @@ +{ + "run_id": "eval-CcP-swdiJD", + "timestamp": "2026-03-26T12:40:31.062Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 3 + }, + "cases_total": 3, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 33.33, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 33.33, + "routed_fragment_rate": 33.33, + "no_route_fragment_rate": 66.67, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 3, + "checks_passed": 3 + }, + "route_distribution": { + "store_feature_risk": 1, + "no_route": 2 + }, + "fallback_distribution": { + "none": 1, + "out_of_scope": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь хвосты по поставщикам и разложи цепочку", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "IHevFSGCO0dpRm", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Как вообще по ФСБУ", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 1, + "unclear_fragments": 0, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "out_of_scope", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "XMpXdBJSLHT1HO", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-003", + "raw_question": "Покажи топ рисков за июнь 2020", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 0, + "unclear_fragments": 1, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "insufficient_specificity", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "DEbkFEJ6DCCgZY", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-EDvNkoALBS.report.json b/llm_normalizer/data/eval_cases/eval-EDvNkoALBS.report.json new file mode 100644 index 0000000..3066a42 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-EDvNkoALBS.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-EDvNkoALBS", + "timestamp": "2026-03-26T12:41:09.015Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "Kmd3qLt9xwLLDl", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по НДС и по закрытию", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "CZwGn0tv9Drnrr", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-FRq1sdepEh.report.json b/llm_normalizer/data/eval_cases/eval-FRq1sdepEh.report.json new file mode 100644 index 0000000..18afc02 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-FRq1sdepEh.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-FRq1sdepEh", + "timestamp": "2026-03-26T12:41:47.585Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "c4ZViCUU5BgSZA", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по НДС и по закрытию", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "hPtH-BgauVnlzg", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-Y6lFA96ywu.report.json b/llm_normalizer/data/eval_cases/eval-Y6lFA96ywu.report.json new file mode 100644 index 0000000..d7b959d --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-Y6lFA96ywu.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-Y6lFA96ywu", + "timestamp": "2026-03-26T12:40:32.436Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "YAs0TDXGr0ZVNq", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по НДС и по закрытию", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "aPDtpysxCNUiwy", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-YfhJafdzmT.report.json b/llm_normalizer/data/eval_cases/eval-YfhJafdzmT.report.json new file mode 100644 index 0000000..3a3a758 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-YfhJafdzmT.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-YfhJafdzmT", + "timestamp": "2026-03-26T12:41:09.003Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "1ueLJSRaJedoxg", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по счету 97", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "f_76vMb-8Q45n2", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-iJqHDv7Xnw.report.json b/llm_normalizer/data/eval_cases/eval-iJqHDv7Xnw.report.json new file mode 100644 index 0000000..f4df464 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-iJqHDv7Xnw.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-iJqHDv7Xnw", + "timestamp": "2026-03-26T12:40:32.418Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "ja-YiQxiELJhva", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по счету 97", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "GHPHZ1GIZLnraN", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-nARkOiG8ly.report.json b/llm_normalizer/data/eval_cases/eval-nARkOiG8ly.report.json new file mode 100644 index 0000000..6d4ddfd --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-nARkOiG8ly.report.json @@ -0,0 +1,135 @@ +{ + "run_id": "eval-nARkOiG8ly", + "timestamp": "2026-03-26T12:41:06.518Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 3 + }, + "cases_total": 3, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 33.33, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 33.33, + "routed_fragment_rate": 33.33, + "no_route_fragment_rate": 66.67, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 3, + "checks_passed": 3 + }, + "route_distribution": { + "store_feature_risk": 1, + "no_route": 2 + }, + "fallback_distribution": { + "none": 1, + "out_of_scope": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь хвосты по поставщикам и разложи цепочку", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "GgUFOuZwDAOjx5", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Как вообще по ФСБУ", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 1, + "unclear_fragments": 0, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "out_of_scope", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "CPZBol6f1zT-Cv", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-003", + "raw_question": "Покажи топ рисков за июнь 2020", + "validation_passed": true, + "message_in_scope": false, + "scope_confidence": "low", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 0, + "out_of_scope_fragments": 0, + "unclear_fragments": 1, + "fallback_type": "out_of_scope", + "predicted_route_status": "no_route", + "expected_route_status": null, + "predicted_no_route_reason": "insufficient_specificity", + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 0, + "trace_id": "MnZHNkwlt1HeY1", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/data/eval_cases/eval-ny0OTiGoC1.report.json b/llm_normalizer/data/eval_cases/eval-ny0OTiGoC1.report.json new file mode 100644 index 0000000..224c621 --- /dev/null +++ b/llm_normalizer/data/eval_cases/eval-ny0OTiGoC1.report.json @@ -0,0 +1,111 @@ +{ + "run_id": "eval-ny0OTiGoC1", + "timestamp": "2026-03-26T12:41:47.583Z", + "mode": "single-pass-strict", + "use_mock": true, + "prompt_version": "normalizer_v2_0_2", + "schema_version": "v2_0_2", + "dataset": { + "source": "inline_raw_questions", + "file": null, + "raw_questions_count": 2 + }, + "cases_total": 2, + "metrics": { + "schema_validation_pass_rate": 100, + "scope_detection_accuracy": null, + "scope_in_scope_rate": 100, + "multi_intent_detected_rate": 0, + "clarification_required_rate": 0, + "avg_fragments_per_message": 1, + "out_of_scope_fragment_rate": 0, + "routed_fragment_rate": 100, + "no_route_fragment_rate": 0, + "route_resolution_accuracy": null, + "no_route_precision": null, + "false_no_route_rate": null, + "execution_state_consistency_rate": 100, + "executable_with_soft_assumptions_rate": 100, + "soft_assumption_used_fragment_rate": 100, + "clarification_precision": null, + "clarification_recall": null, + "false_clarification_rate": null + }, + "budget": { + "requests_total": 0, + "retries_used": 0 + }, + "clarification_eval": { + "labeled_cases": 0, + "true_positive": 0, + "false_positive": 0, + "false_negative": 0 + }, + "route_eval": { + "labeled_cases": 0, + "correct_cases": 0, + "expected_routed_cases": 0, + "no_route_true_positive": 0, + "no_route_false_positive": 0 + }, + "scope_eval": { + "labeled_cases": 0, + "correct_cases": 0 + }, + "execution_state_eval": { + "checks_total": 2, + "checks_passed": 2 + }, + "route_distribution": { + "store_feature_risk": 2 + }, + "fallback_distribution": { + "none": 2 + }, + "results": [ + { + "case_id": "BQ-001", + "raw_question": "Проверь счет 60 за июнь 2020", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "6BsWdYOwlVkrW7", + "request_count_for_case": 0 + }, + { + "case_id": "BQ-002", + "raw_question": "Покажи риски по счету 97", + "validation_passed": true, + "message_in_scope": true, + "scope_confidence": "high", + "contains_multiple_tasks": false, + "fragments_total": 1, + "in_scope_fragments": 1, + "out_of_scope_fragments": 0, + "unclear_fragments": 0, + "fallback_type": "none", + "predicted_route_status": "routed", + "expected_route_status": null, + "predicted_no_route_reason": null, + "expected_no_route_reason": null, + "predicted_clarification_required": false, + "expected_clarification_required": null, + "executable_with_soft_assumptions_fragments": 1, + "trace_id": "miQ7qO3z2rd5bw", + "request_count_for_case": 0 + } + ] +} \ No newline at end of file diff --git a/llm_normalizer/docs/runs/2026-03-26_15-41-01_Wave_1_Stage_3_Kickoff.zip b/llm_normalizer/docs/runs/2026-03-26_15-41-01_Wave_1_Stage_3_Kickoff.zip new file mode 100644 index 0000000..70bbb9a Binary files /dev/null and b/llm_normalizer/docs/runs/2026-03-26_15-41-01_Wave_1_Stage_3_Kickoff.zip differ diff --git a/llm_normalizer/docs/runs/runs.zip b/llm_normalizer/docs/runs/runs.zip deleted file mode 100644 index 6785635..0000000 Binary files a/llm_normalizer/docs/runs/runs.zip and /dev/null differ diff --git a/llm_normalizer/docs/API.md b/llm_normalizer/docs/stage0-1/API.md similarity index 100% rename from llm_normalizer/docs/API.md rename to llm_normalizer/docs/stage0-1/API.md diff --git a/llm_normalizer/docs/PROMPTS.md b/llm_normalizer/docs/stage0-1/PROMPTS.md similarity index 100% rename from llm_normalizer/docs/PROMPTS.md rename to llm_normalizer/docs/stage0-1/PROMPTS.md diff --git a/llm_normalizer/docs/SCHEMA.md b/llm_normalizer/docs/stage0-1/SCHEMA.md similarity index 100% rename from llm_normalizer/docs/SCHEMA.md rename to llm_normalizer/docs/stage0-1/SCHEMA.md diff --git a/llm_normalizer/docs/assistant_mode_flow.md b/llm_normalizer/docs/stage0-1/assistant_mode_flow.md similarity index 100% rename from llm_normalizer/docs/assistant_mode_flow.md rename to llm_normalizer/docs/stage0-1/assistant_mode_flow.md diff --git a/llm_normalizer/docs/assistant_mode_spec.md b/llm_normalizer/docs/stage0-1/assistant_mode_spec.md similarity index 100% rename from llm_normalizer/docs/assistant_mode_spec.md rename to llm_normalizer/docs/stage0-1/assistant_mode_spec.md diff --git a/llm_normalizer/docs/assistant_mode_vnext_spec.md b/llm_normalizer/docs/stage0-1/assistant_mode_vnext_spec.md similarity index 100% rename from llm_normalizer/docs/assistant_mode_vnext_spec.md rename to llm_normalizer/docs/stage0-1/assistant_mode_vnext_spec.md diff --git a/llm_normalizer/docs/clarification_policy.md b/llm_normalizer/docs/stage0-1/clarification_policy.md similarity index 100% rename from llm_normalizer/docs/clarification_policy.md rename to llm_normalizer/docs/stage0-1/clarification_policy.md diff --git a/llm_normalizer/docs/domain_scope_policy.md b/llm_normalizer/docs/stage0-1/domain_scope_policy.md similarity index 100% rename from llm_normalizer/docs/domain_scope_policy.md rename to llm_normalizer/docs/stage0-1/domain_scope_policy.md diff --git a/llm_normalizer/docs/fallback_policy.md b/llm_normalizer/docs/stage0-1/fallback_policy.md similarity index 100% rename from llm_normalizer/docs/fallback_policy.md rename to llm_normalizer/docs/stage0-1/fallback_policy.md diff --git a/llm_normalizer/docs/final_answer_composer_spec.md b/llm_normalizer/docs/stage0-1/final_answer_composer_spec.md similarity index 100% rename from llm_normalizer/docs/final_answer_composer_spec.md rename to llm_normalizer/docs/stage0-1/final_answer_composer_spec.md diff --git a/llm_normalizer/docs/fragment_execution_policy.md b/llm_normalizer/docs/stage0-1/fragment_execution_policy.md similarity index 100% rename from llm_normalizer/docs/fragment_execution_policy.md rename to llm_normalizer/docs/stage0-1/fragment_execution_policy.md diff --git a/llm_normalizer/docs/known_limits_before_field_eval.md b/llm_normalizer/docs/stage0-1/known_limits_before_field_eval.md similarity index 100% rename from llm_normalizer/docs/known_limits_before_field_eval.md rename to llm_normalizer/docs/stage0-1/known_limits_before_field_eval.md diff --git a/llm_normalizer/docs/known_limits_current_routes.md b/llm_normalizer/docs/stage0-1/known_limits_current_routes.md similarity index 100% rename from llm_normalizer/docs/known_limits_current_routes.md rename to llm_normalizer/docs/stage0-1/known_limits_current_routes.md diff --git a/llm_normalizer/docs/normalizer_forensic_audit_v1_1.md b/llm_normalizer/docs/stage0-1/normalizer_forensic_audit_v1_1.md similarity index 100% rename from llm_normalizer/docs/normalizer_forensic_audit_v1_1.md rename to llm_normalizer/docs/stage0-1/normalizer_forensic_audit_v1_1.md diff --git a/llm_normalizer/docs/normalizer_v1_1_1_patch_notes.md b/llm_normalizer/docs/stage0-1/normalizer_v1_1_1_patch_notes.md similarity index 100% rename from llm_normalizer/docs/normalizer_v1_1_1_patch_notes.md rename to llm_normalizer/docs/stage0-1/normalizer_v1_1_1_patch_notes.md diff --git a/llm_normalizer/docs/normalizer_v1_1_2_1_patch_notes.md b/llm_normalizer/docs/stage0-1/normalizer_v1_1_2_1_patch_notes.md similarity index 100% rename from llm_normalizer/docs/normalizer_v1_1_2_1_patch_notes.md rename to llm_normalizer/docs/stage0-1/normalizer_v1_1_2_1_patch_notes.md diff --git a/llm_normalizer/docs/normalizer_v1_1_2_patch_notes.md b/llm_normalizer/docs/stage0-1/normalizer_v1_1_2_patch_notes.md similarity index 100% rename from llm_normalizer/docs/normalizer_v1_1_2_patch_notes.md rename to llm_normalizer/docs/stage0-1/normalizer_v1_1_2_patch_notes.md diff --git a/llm_normalizer/docs/normalizer_v1_1_changes.md b/llm_normalizer/docs/stage0-1/normalizer_v1_1_changes.md similarity index 100% rename from llm_normalizer/docs/normalizer_v1_1_changes.md rename to llm_normalizer/docs/stage0-1/normalizer_v1_1_changes.md diff --git a/llm_normalizer/docs/normalizer_v2_0_1_spec.md b/llm_normalizer/docs/stage0-1/normalizer_v2_0_1_spec.md similarity index 100% rename from llm_normalizer/docs/normalizer_v2_0_1_spec.md rename to llm_normalizer/docs/stage0-1/normalizer_v2_0_1_spec.md diff --git a/llm_normalizer/docs/normalizer_v2_spec.md b/llm_normalizer/docs/stage0-1/normalizer_v2_spec.md similarity index 100% rename from llm_normalizer/docs/normalizer_v2_spec.md rename to llm_normalizer/docs/stage0-1/normalizer_v2_spec.md diff --git a/llm_normalizer/docs/route_executor_contracts.md b/llm_normalizer/docs/stage0-1/route_executor_contracts.md similarity index 100% rename from llm_normalizer/docs/route_executor_contracts.md rename to llm_normalizer/docs/stage0-1/route_executor_contracts.md diff --git a/llm_normalizer/docs/v2_0_2_execution_state_machine.md b/llm_normalizer/docs/stage0-1/v2_0_2_execution_state_machine.md similarity index 100% rename from llm_normalizer/docs/v2_0_2_execution_state_machine.md rename to llm_normalizer/docs/stage0-1/v2_0_2_execution_state_machine.md diff --git a/llm_normalizer/docs/v2_0_2_no_route_audit.md b/llm_normalizer/docs/stage0-1/v2_0_2_no_route_audit.md similarity index 100% rename from llm_normalizer/docs/v2_0_2_no_route_audit.md rename to llm_normalizer/docs/stage0-1/v2_0_2_no_route_audit.md diff --git a/llm_normalizer/docs/v2_0_2_schema_forensic.md b/llm_normalizer/docs/stage0-1/v2_0_2_schema_forensic.md similarity index 100% rename from llm_normalizer/docs/v2_0_2_schema_forensic.md rename to llm_normalizer/docs/stage0-1/v2_0_2_schema_forensic.md