Этап 4 corrective pack 2 по family isolation после текущих routing fixes
This commit is contained in:
parent
133b6dca3c
commit
f74e7b697a
|
|
@ -0,0 +1,251 @@
|
|||
# Stage 4 - Family Card v1 — FA amortization coverage (runtime-aligned)
|
||||
|
||||
**document_status:** `ACTIVE`
|
||||
**family_name:** `Амортизация ОС — полнота охвата / риск пропуска объекта`
|
||||
**family_id:** `FA_AMORTIZATION_COVERAGE_V1`
|
||||
**stage_scope:** `Stage 4 (P0-only)`
|
||||
**current_family_status:** `PROOF_PATH_RAISED_LIVE_ACCEPTANCE_PENDING`
|
||||
**primary_gap:** `live object-level relation clarity + production live acceptance`
|
||||
**latest_pack:** `2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure`
|
||||
**next_pack_focus:** `live replay acceptance + relation/anchor consistency in production channel`
|
||||
**family_source_of_truth_questions:** `FA-Q1 (31 июля: 2 471,52 / 2 465,28 / 849,83 — полное ли начисление); FA-Q2 (expected vs actual set по объектам ОС за июль); FA-Q3 (есть ли missing object candidates в июльском начислении)`
|
||||
**family_latest_live_replay:** `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt\fa_live_raw.json (текущий live attempt: http_status=400, невалидная приемка)`
|
||||
**family_latest_acceptance_run:** `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure\run_summary.json`
|
||||
|
||||
## 1) Что фиксирует этот документ
|
||||
|
||||
Карточка разделяет два слоя:
|
||||
|
||||
1. `Runtime V1 (as-is)` — только то, что подтверждено текущим кодом и артефактами пакета.
|
||||
2. `Target V2 (planned)` — что нужно дожать, но пока не считается текущим acceptance gate.
|
||||
|
||||
Это защищает от ложного закрытия family по mock-результату без live-приемки.
|
||||
|
||||
## 2) Runtime V1 (фактический контракт на 2026-03-29)
|
||||
|
||||
### 2.1 Claim contract (as-is)
|
||||
|
||||
- **primary claim_type:** `prove_fixed_asset_amortization_coverage`
|
||||
- **additional claim_types:** пока не выделялись как отдельные first-class claim types
|
||||
- **границы claim:**
|
||||
- не расширяет домены за рамки Stage 4 P0;
|
||||
- не вводит новый proof engine;
|
||||
- не использует Stage 5 investigation как core path.
|
||||
|
||||
### 2.2 Required anchors (runtime-enforced, as-is)
|
||||
|
||||
Для `prove_fixed_asset_amortization_coverage` в рантайме обязательны:
|
||||
|
||||
- `period`
|
||||
- `fixed_asset_signal`
|
||||
- `amortization_signal`
|
||||
- `amount_or_document`
|
||||
- `account_scope_or_document_type`
|
||||
|
||||
Факт по latest acceptance run:
|
||||
|
||||
- `required_anchors_count = 5`
|
||||
- `missing_anchor_classes_count = 0`
|
||||
- `claim_anchor_coverage_ratio = 1`
|
||||
|
||||
### 2.3 Claim-bound live recipe (runtime-enforced, as-is)
|
||||
|
||||
Обязательные live вызовы:
|
||||
|
||||
1. `find_amortization_documents_in_period`
|
||||
2. `find_fixed_asset_movements_accounts_01_02`
|
||||
3. `find_fixed_asset_cards_expected_for_period`
|
||||
4. `match_expected_vs_actual_fa_coverage`
|
||||
|
||||
Ожидаемый результат recipe:
|
||||
|
||||
- документ(ы) начисления амортизации;
|
||||
- candidate/actual набор объектов ОС;
|
||||
- expected set для периода;
|
||||
- сравнение expected vs actual и кандидаты пропуска.
|
||||
|
||||
### 2.4 Route behavior (as-is)
|
||||
|
||||
Для FA-claim runtime:
|
||||
|
||||
- форсирует claim-bound live-capable route;
|
||||
- может переопределять `store_feature_risk` в live/hybrid path;
|
||||
- экспортирует `fa_live_route_audit` в debug payload.
|
||||
|
||||
Факт по latest acceptance run:
|
||||
|
||||
- `live_route_execution_rate = 1`
|
||||
- `required_live_calls = 4`
|
||||
- `missing_live_calls = 0`
|
||||
- `route_adjustments_applied = 1`
|
||||
|
||||
### 2.5 Evidence/admissibility behavior (as-is)
|
||||
|
||||
Факт по latest acceptance run:
|
||||
|
||||
- `admissible_evidence_count = 16`
|
||||
- `rejected_evidence_count = 32`
|
||||
- reject breakdown: `wrong_account_scope = 16`, `weak_source_mapping = 16`
|
||||
|
||||
Факт по targeted evidence:
|
||||
|
||||
- `targeted_evidence_hit_rate = 1`
|
||||
- `expected_fa_set_count = 2`
|
||||
- `actual_fa_set_count = 2`
|
||||
- `missing_fa_candidates_count = 0`
|
||||
- `uncertain_fa_candidates_count = 28`
|
||||
|
||||
### 2.6 Runtime acceptance snapshot (по latest pack)
|
||||
|
||||
- `FA_EXPECTED_SET_FIXED = FIXED`
|
||||
- `FA_RELATION_MAPPING_FIXED = FIXED`
|
||||
- `FA_CLAIM_ANCHOR_CLOSURE_FIXED = FIXED`
|
||||
- `FA_PROOF_CLOSURE_FIXED = FIXED`
|
||||
- общий статус: `FA_PACK_ACCEPTED`
|
||||
- важная оговорка: режим пакета `mock` (controlled replay)
|
||||
|
||||
### 2.7 known_runtime_limits (as-is)
|
||||
|
||||
- `live acceptance pending`: актуальный live attempt завершился `http_status=400`, не может быть источником приемки.
|
||||
- `relation clarity incomplete`: несмотря на coverage=1 в mock, в relation map много `coverage_status=uncertain` и слабых object-level link.
|
||||
- `admissibility noise remains`: значимый reject-шум (`wrong_account_scope`, `weak_source_mapping`) сохраняется и требует дожима для production live.
|
||||
|
||||
## 3) Target V2 (planned, не критерий текущей приемки)
|
||||
|
||||
### 3.1 Planned claim extension
|
||||
|
||||
- Развести подтипы FA-claim по режимам:
|
||||
- `prove_fixed_asset_amortization_completeness`
|
||||
- `prove_fixed_asset_missing_object_risk`
|
||||
- `prove_fixed_asset_expected_actual_consistency`
|
||||
|
||||
### 3.2 Planned anchor extension
|
||||
|
||||
- Явный `expected_fa_set` как first-class anchor.
|
||||
- Явный `actual_fa_set_from_amortization` как first-class anchor.
|
||||
- Явный `missing_fa_candidates` с object identity и link quality.
|
||||
|
||||
### 3.3 Planned family metrics
|
||||
|
||||
- `fa_expected_set_reconstruction_rate`
|
||||
- `fa_relation_mapping_coverage_rate`
|
||||
- `fa_claim_anchor_coverage_rate`
|
||||
- `fa_actual_vs_expected_comparison_rate`
|
||||
- `fa_proof_closure_rate`
|
||||
- `fa_false_grounded_answer_rate`
|
||||
- `fa_live_acceptance_pass_rate`
|
||||
|
||||
## 4) Required entities and relations (business contract)
|
||||
|
||||
### 4.1 Минимально необходимые сущности для proof closure
|
||||
|
||||
1. Документ(ы) начисления амортизации за июль.
|
||||
2. Объекты ОС, которые должны участвовать в начислении.
|
||||
3. Движения/проводки по контуру амортизации (в т.ч. счета 01/02).
|
||||
4. Expected set объектов ОС на период.
|
||||
5. Actual set объектов, реально попавших в начисление.
|
||||
6. Missing/uncertain candidates для object-level verdict.
|
||||
|
||||
### 4.2 Критические relation links
|
||||
|
||||
1. `fa_object -> amortization_document`
|
||||
2. `amortization_document -> movement/posting`
|
||||
3. `fa_object -> expected_period_coverage`
|
||||
4. `expected_set -> actual_set`
|
||||
5. `missing_or_uncertain_object -> coverage_risk_verdict`
|
||||
|
||||
## 5) Snapshot/Live coverage verdict (as-is)
|
||||
|
||||
- По FA family практический режим: `snapshot_plus_live_required`.
|
||||
- В controlled replay proof-path поднят до `grounded_positive`.
|
||||
- Production live приемка пока не закрыта.
|
||||
|
||||
## 6) Answer/proof modes contract
|
||||
|
||||
### `grounded_positive`
|
||||
|
||||
Допускается, если одновременно:
|
||||
|
||||
- `admissible_evidence_count > 0`
|
||||
- реконструированы `expected_fa_set` и `actual_fa_set`
|
||||
- сделано сравнение expected vs actual
|
||||
- вывод имеет object-level опору, не только сумму
|
||||
- `false_grounded_answer_rate = 0`
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `За июль подтверждены ожидаемый и фактический наборы ОС; пропусков по зафиксированным объектам не выявлено, риск неполноты начисления низкий.`
|
||||
|
||||
### `limited_or_insufficient_evidence`
|
||||
|
||||
Обязателен, если:
|
||||
|
||||
- не реконструирован expected set или actual set
|
||||
- object-level links недостаточны
|
||||
- есть только косвенная/шумная опора
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `Суммы начисления зафиксированы, но object-level соответствие expected vs actual не восстановлено; вывод ограничен и не подтверждает полноту начисления.`
|
||||
|
||||
### Запрещенные паттерны
|
||||
|
||||
- вывод о полноте начисления только по суммам без object-level связи
|
||||
- уверенный verdict при `admissible = 0`
|
||||
- общий lifecycle-текст без указания missing object/link
|
||||
|
||||
Короткий антипример:
|
||||
|
||||
- `Амортизация начислена корректно, пропусков нет` (нельзя без expected/actual object-level proof).
|
||||
|
||||
## 7) Gap register (FA family)
|
||||
|
||||
| gap_id | category | severity | current_state | note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| FA-G1 | `live_acceptance_not_confirmed` | blocker | open | текущий live attempt дал `http_status=400`, приемка фактически mock-only |
|
||||
| FA-G2 | `wrong_entity_mapping` / `relation_clarity` | high | open | в relation map много `coverage_status=uncertain` и слабых direct links |
|
||||
| FA-G3 | `admissibility_reject_not_due_to_data` | medium | open | reject-шум по `wrong_account_scope` и `weak_source_mapping` |
|
||||
| FA-G4 | `answer_layer_underuses_available_evidence` | medium | partial | в части трасс answer-mode может звучать ограничительно при сильной eligibility |
|
||||
|
||||
## 8) Code-path inventory (где живет контракт)
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts`
|
||||
- FA claim resolution;
|
||||
- required anchors;
|
||||
- targeted checks;
|
||||
- expected/actual/missing/uncertain FA coverage structures.
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantDataLayer.ts`
|
||||
- claim-bound FA live plan;
|
||||
- live call profile `claim_bound_fa_live_path`;
|
||||
- relation markers (`asset_card_to_depreciation`, `document_to_posting`).
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantService.ts`
|
||||
- FA live route enforcement;
|
||||
- route override audit (`fa_live_route_audit`);
|
||||
- handoff в eligibility/answer layer.
|
||||
|
||||
- run artifacts:
|
||||
- `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure`
|
||||
- `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt`
|
||||
- `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_mock_replay`
|
||||
|
||||
## 9) Regression set and acceptance policy
|
||||
|
||||
Обязательный минимум для FA family:
|
||||
|
||||
1. Базовый вопрос по трем суммам `2 471,52 / 2 465,28 / 849,83`.
|
||||
2. Вариация без сумм, но с запросом полноты expected vs actual.
|
||||
3. Вариация на missing-object risk.
|
||||
4. Follow-up по конкретному объекту ОС (если объект выделен).
|
||||
|
||||
Политика:
|
||||
|
||||
- после каждого FA pack обязателен новый run folder в `llm_normalizer/docs/runs`;
|
||||
- acceptance фиксируется на уровне family;
|
||||
- mock acceptance не заменяет live acceptance;
|
||||
- `false_grounded` должен оставаться нулевым.
|
||||
|
||||
## 10) Project decision line for this family
|
||||
|
||||
FA family поднята до proof-ready уровня в controlled replay, но остается в статусе `live acceptance pending` до подтверждения object-level closure на реальном live канале.
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
# Stage 4 - Family Card v1 — RBP tail / write-off overstay (runtime-aligned)
|
||||
|
||||
**document_status:** `ACTIVE`
|
||||
**family_name:** `РБП — хвост / списание / overstay`
|
||||
**family_id:** `RBP_TAIL_WRITEOFF_OVERSTAY_V1`
|
||||
**stage_scope:** `Stage 4 (P0-only)`
|
||||
**current_family_status:** `ACCEPTED_WITH_LIMITATIONS`
|
||||
**primary_gap:** `source coverage`
|
||||
**latest_pack:** `2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix`
|
||||
**next_pack_focus:** `source coverage recovery + route tightening for full claim closure`
|
||||
**family_source_of_truth_questions:** `RBP-Q1 (Списание РБП за Июль 2020, включая 5 000); RBP-Q2 (хвост РБП к концу июля без суммы); RBP-Q3 (полнота закрытия июльского списания)`
|
||||
**family_latest_live_replay:** `2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix/1.txt (локальный replay; внешний live-канал требует отдельной приемки)`
|
||||
**family_latest_acceptance_run:** `2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix/run_summary.json`
|
||||
|
||||
## 1) Что фиксирует этот документ
|
||||
|
||||
Карточка теперь разделяет два слоя:
|
||||
|
||||
1. `Runtime V1 (as-is)` — только то, что уже реально работает в коде и подтверждено артефактами.
|
||||
2. `Target V2 (planned)` — что хотим довести в следующих pack, но пока не считаем критерием текущей приемки.
|
||||
|
||||
Это нужно, чтобы не смешивать факт и план и не закрывать family “по красивому тексту”.
|
||||
|
||||
## 2) Runtime V1 (фактический контракт на 2026-03-29)
|
||||
|
||||
### 2.1 Claim contract (as-is)
|
||||
|
||||
- **primary claim_type:** `prove_rbp_tail_state`
|
||||
- **additional claim_types:** пока не first-class в runtime (учитываются как план V2)
|
||||
- **границы claim:**
|
||||
- не включает Stage 5 investigation как core path;
|
||||
- не расширяет домены за пределы Stage 4 P0;
|
||||
- не делает full redesign proof engine.
|
||||
|
||||
### 2.2 Required anchors (runtime-enforced, as-is)
|
||||
|
||||
Для `prove_rbp_tail_state` в рантайме обязательны:
|
||||
|
||||
- `period`
|
||||
- `rbp_signal`
|
||||
- `writeoff_signal`
|
||||
|
||||
Технические reason codes на этом шаге:
|
||||
|
||||
- `claim_missing_required_anchors`
|
||||
- `claim_anchor_resolution_low`
|
||||
|
||||
### 2.3 Claim-bound live recipe (runtime-enforced, as-is)
|
||||
|
||||
Обязательные live вызовы:
|
||||
|
||||
1. `find_rbp_writeoff_documents_in_period`
|
||||
2. `find_rbp_object_movements_account_97`
|
||||
3. `find_month_close_entries_linked_to_rbp`
|
||||
4. `compute_end_period_residual_by_rbp_object`
|
||||
|
||||
Ожидаемый результат recipe:
|
||||
|
||||
- подтверждение документа списания;
|
||||
- привязка к объекту(ам) РБП;
|
||||
- движение/связанные записи;
|
||||
- residual state на границе периода.
|
||||
|
||||
### 2.4 Route behavior (as-is)
|
||||
|
||||
Для `prove_rbp_tail_state` runtime:
|
||||
|
||||
- форсирует live-capable маршрут (`hybrid_store_plus_live` / `live_mcp_drilldown`);
|
||||
- поднимает `insufficient_specificity` в live path вместо `no_route`;
|
||||
- экспортирует `rbp_live_route_audit` в debug payload.
|
||||
|
||||
### 2.5 Evidence/admissibility behavior (as-is)
|
||||
|
||||
Подтверждено в текущем пакете:
|
||||
|
||||
- убрана инъекция raw live rows при `matched_rows = 0`;
|
||||
- добавлена стабилизированная live metadata для source mapping;
|
||||
- claim-bound targeted checks для RBP расширены до object/document/movement/residual.
|
||||
|
||||
Наблюдаемые baseline reject reasons (до фикса пакета):
|
||||
|
||||
- `zero_live_match`
|
||||
- `weak_source_mapping`
|
||||
- `wrong_account_scope`
|
||||
|
||||
### 2.6 Runtime acceptance snapshot (по артефактам latest pack)
|
||||
|
||||
- `RBP_SOURCE_COVERAGE_FIXED = NOT_FIXED`
|
||||
- `RBP_LIVE_ROUTE_FIXED = FIXED`
|
||||
- `RBP_EVIDENCE_MATERIALIZATION_FIXED = FIXED`
|
||||
- общий статус: `RBP_PACK_ACCEPTED_WITH_LIMITATIONS`
|
||||
|
||||
### 2.7 known_runtime_limits (as-is)
|
||||
|
||||
- `source coverage incomplete`: для части object-level proof звеньев данные в runtime source set еще неполные.
|
||||
- `business scope inconsistency possible`: в части live traces возможна несогласованность `generic_accounting` vs `company_specific_accounting`.
|
||||
- `anchor noise in edge cases`: в отдельных кейсах могут появляться нерелевантные account hints, влияющие на route profile.
|
||||
|
||||
## 3) Target V2 (planned, не критерий текущей приемки)
|
||||
|
||||
### 3.1 Planned claim extension
|
||||
|
||||
Дополнительные claim types (план):
|
||||
|
||||
- `prove_rbp_writeoff_completeness`
|
||||
- `prove_rbp_lifecycle_overstay`
|
||||
- `prove_rbp_period_end_residual_state`
|
||||
|
||||
### 3.2 Planned anchor extension
|
||||
|
||||
Расширенный target-набор anchors (план):
|
||||
|
||||
- период (primary + period-end)
|
||||
- суммы/диапазоны
|
||||
- объект РБП
|
||||
- документ/тип документа
|
||||
- движения/остаток на конец периода
|
||||
- lifecycle markers для overstay
|
||||
|
||||
### 3.3 Planned family metrics
|
||||
|
||||
Плановые family-метрики (в stable harness):
|
||||
|
||||
- `rbp_required_entity_coverage_rate`
|
||||
- `rbp_live_route_execution_rate`
|
||||
- `rbp_admissible_evidence_nonzero_rate`
|
||||
- `rbp_source_to_proof_completion_rate`
|
||||
- `rbp_partial_coverage_default_rate`
|
||||
- `rbp_false_grounded_answer_rate`
|
||||
|
||||
До формализации в harness эти метрики считаются target-моделью, а не текущим hard gate.
|
||||
|
||||
## 4) Required entities and relations (business contract)
|
||||
|
||||
### 4.1 Минимально необходимые сущности для proof closure
|
||||
|
||||
1. Документ `Списание РБП` за целевой период.
|
||||
2. Объект(ы) РБП.
|
||||
3. Движения/регистровые записи по списанию.
|
||||
4. Residual state на конец периода.
|
||||
5. Связь со стадией month-close при необходимости.
|
||||
|
||||
### 4.2 Критические relation links
|
||||
|
||||
1. `RBP object -> writeoff document`
|
||||
2. `writeoff document -> movement/register record`
|
||||
3. `movement/register record -> period-end residual`
|
||||
4. `expected lifecycle -> actual lifecycle result`
|
||||
5. `residual state -> overstay/no-overstay verdict`
|
||||
|
||||
## 5) Snapshot/Live coverage verdict (as-is)
|
||||
|
||||
- `snapshot-only` для RBP сейчас **недостаточен** для устойчивого object-level proof closure.
|
||||
- Family требует `snapshot_plus_live_required`.
|
||||
- Главный незакрытый узел — source coverage в production live данных.
|
||||
|
||||
## 6) Answer/proof modes contract
|
||||
|
||||
### `grounded_positive`
|
||||
|
||||
Допускается, если одновременно:
|
||||
|
||||
- `admissible_evidence_count > 0`
|
||||
- закрыта цепочка object/document/movement/residual
|
||||
- вывод опирается на конкретные source refs
|
||||
- `false_grounded_answer_rate = 0`
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `Подтвержден документ "Списание РБП" за июль 2020, найден связанный объект РБП и остаток на конец периода = 0; признаков overstay по этому объекту не выявлено.`
|
||||
|
||||
### `limited_or_insufficient_evidence`
|
||||
|
||||
Обязателен, если:
|
||||
|
||||
- не найден ключевой link (document/object/residual)
|
||||
- admissible evidence недостаточно
|
||||
- нет права имитировать доказанность
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `Документ списания найден, но связь с объектом РБП и подтвержденный остаток на конец периода не восстановлены; вывод ограничен до partial coverage без утверждения об overstay.`
|
||||
|
||||
### Запрещенные паттерны
|
||||
|
||||
- общий lifecycle narrative без указания missing link
|
||||
- уверенный вывод при `admissible = 0`
|
||||
- смешивание snapshot-гипотез с live-доказанностью без маркировки
|
||||
|
||||
Короткий антипример:
|
||||
|
||||
- `Есть признаки проблемы по РБП, вероятно хвост остался` (нельзя без document/object/residual подтверждения).
|
||||
|
||||
## 7) Gap register (RBP family)
|
||||
|
||||
| gap_id | category | severity | current_state | note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| RBP-G1 | `missing_source_data` / `source_coverage` | blocker | open | ключевая причина `ACCEPTED_WITH_LIMITATIONS` |
|
||||
| RBP-G2 | `business_scope_resolution_consistency` | high | open | в отдельных live traces встречается несогласованность generic/company-specific слоев |
|
||||
| RBP-G3 | `anchor_quality` | medium | open | в отдельных кейсах попадают нерелевантные account hints, что ухудшает route profile |
|
||||
|
||||
## 8) Code-path inventory (где живет контракт)
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts`
|
||||
- claim type resolution;
|
||||
- required anchors для `prove_rbp_tail_state`;
|
||||
- anchor resolution rate и claim reason codes.
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantDataLayer.ts`
|
||||
- claim-bound live plan для RBP;
|
||||
- обязательные live call ids и account scope overrides;
|
||||
- source profile `claim_bound_rbp_live_path`.
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantService.ts`
|
||||
- RBP live route enforcement;
|
||||
- no-route recovery;
|
||||
- `rbp_live_route_audit` export.
|
||||
|
||||
- run artifacts:
|
||||
- `llm_normalizer/docs/runs/2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix`
|
||||
|
||||
## 9) Regression set and acceptance policy
|
||||
|
||||
Обязательный минимум для family replay:
|
||||
|
||||
1. Базовый RBP вопрос: `Списание РБП за Июль 2020`, включая сумму `5 000`.
|
||||
2. Вариация без суммы (проверка period + overstay signal).
|
||||
3. Вариация на полноту закрытия.
|
||||
4. Follow-up по тому же документу/объекту.
|
||||
5. Соседний month-close sanity case.
|
||||
|
||||
Политика:
|
||||
|
||||
- после каждого family pack обязателен новый run folder в `llm_normalizer/docs/runs`;
|
||||
- приемка фиксируется на уровне family, не одиночного вопроса;
|
||||
- `false_grounded` должен оставаться нулевым.
|
||||
|
||||
## 10) Project decision line for this family
|
||||
|
||||
RBP уже перешел в family-based execution контур Stage 4, но остается в статусе `accepted_with_limitations` до восстановления source coverage в живом канале.
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
# Stage 4 - Family Card v1 — VAT chain (runtime-aligned)
|
||||
|
||||
**document_status:** `ACTIVE`
|
||||
**family_name:** `НДС-цепочка — полнота прохождения от документа до налогового отражения`
|
||||
**family_id:** `VAT_CHAIN_COMPLETENESS_V1`
|
||||
**stage_scope:** `Stage 4 (P0-only)`
|
||||
**current_family_status:** `PARTIALLY_WORKING / NON-BLOCKER`
|
||||
**primary_gap:** `residual admissibility/materialization quality`
|
||||
**latest_pack:** `2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt`
|
||||
**next_pack_focus:** `targeted live narrowing + reject cleanup for VAT proof-path`
|
||||
**family_source_of_truth_questions:** `VAT-Q1 (13 июля поступление, 15 июля реализация — полная ли НДС-цепочка); VAT-Q2 (есть ли выпадение между документом, проводкой, НДС-регистром и книгой); VAT-Q3 (по July 2020 VAT path grounded-positive или остаются weak-mapping шумы)`
|
||||
**family_latest_live_replay:** `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\1_live_replay.txt`
|
||||
**family_latest_acceptance_run:** `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\run_summary.json`
|
||||
|
||||
## 1) Что фиксирует этот документ
|
||||
|
||||
Карточка разделяет два слоя:
|
||||
|
||||
1. `Runtime V1 (as-is)` — только то, что подтверждено текущим кодом и run-артефактами.
|
||||
2. `Target V2 (planned)` — что нужно дожать следующими family-pack, но пока не является hard gate текущей приемки.
|
||||
|
||||
Это соответствует Stage 4 family-based execution в рамках `P0-only` без перехода в Stage 5 и без архитектурного redesign.
|
||||
|
||||
## 2) Runtime V1 (фактический контракт на 2026-03-29)
|
||||
|
||||
### 2.1 Claim contract (as-is)
|
||||
|
||||
- **primary claim_type:** `prove_vat_chain_completeness`
|
||||
- **additional claim_types:** пока не выделялись как first-class в runtime
|
||||
- **границы claim:**
|
||||
- не расширяет домены за рамки VAT family;
|
||||
- не включает общий налоговый аудит;
|
||||
- не включает Stage 5 investigation как core path.
|
||||
|
||||
### 2.2 Required anchors (runtime-enforced, as-is)
|
||||
|
||||
Для `prove_vat_chain_completeness` в текущем runtime обязательны:
|
||||
|
||||
- `period`
|
||||
- `document_types`
|
||||
- `vat_signal`
|
||||
- `chain_signal`
|
||||
|
||||
Факт: эти anchors соответствуют текущему `requiredByClaim` в runtime.
|
||||
|
||||
### 2.3 Claim checks and live recipe (runtime-enforced, as-is)
|
||||
|
||||
Для VAT claim runtime требует закрытия проверок:
|
||||
|
||||
1. `source_document_found`
|
||||
2. `invoice_found`
|
||||
3. `tax_register_entry_found`
|
||||
4. `book_entry_found`
|
||||
5. `chain_linkage_status`
|
||||
|
||||
Текущий live acquisition path для VAT в runtime:
|
||||
|
||||
- в отличие от RBP/FA, нет выделенного VAT-specific набора обязательных live call id;
|
||||
- используется `hybrid_store_plus_live` + generic live overlay probe;
|
||||
- positive VAT-case может быть закрыт за счет admissible evidence и targeted checks.
|
||||
|
||||
### 2.4 Route behavior (as-is)
|
||||
|
||||
Факт по latest run:
|
||||
|
||||
- VAT case (`L1`) отрабатывает в `factual_with_explanation`;
|
||||
- claim: `prove_vat_chain_completeness`;
|
||||
- mode: `grounded_positive`;
|
||||
- scope: `company_specific_accounting`;
|
||||
- temporal outcome: `passed`.
|
||||
|
||||
### 2.5 Evidence/admissibility behavior (as-is)
|
||||
|
||||
Факт по VAT case (`L1`) в latest run:
|
||||
|
||||
- `admissible_evidence_count = 12`
|
||||
- `grounding_mode = grounded_positive`
|
||||
- основные reject-хвосты: `wrong_account_scope`, `weak_source_mapping`
|
||||
|
||||
Факт по aggregate reject breakdown (run-level):
|
||||
|
||||
- `weak_source_mapping` и `wrong_account_scope` остаются ключевым residual шумом.
|
||||
|
||||
### 2.6 Runtime acceptance snapshot (по latest pack)
|
||||
|
||||
- family status: `PARTIALLY_WORKING / NON-BLOCKER`
|
||||
- по `L1` VAT-кейсу есть `grounded_positive`
|
||||
- общий статус run: `WAVE19_2_ACCEPTED`
|
||||
- важная оговорка: в run зафиксирован `normalizer_mode = useMock=true`
|
||||
|
||||
### 2.7 known_runtime_limits (as-is)
|
||||
|
||||
- `normalizer_mock_mode_in_latest_acceptance`: latest acceptance run выполнялся в режиме `useMock=true`.
|
||||
- `no_explicit_vat_live_call_contract`: для VAT пока нет отдельного жесткого live-call контракта как у RBP/FA.
|
||||
- `admissibility_noise_remains`: сохраняются residual rejects по `weak_source_mapping` и `wrong_account_scope`.
|
||||
|
||||
## 3) Target V2 (planned, не критерий текущей приемки)
|
||||
|
||||
### 3.1 Planned claim extension
|
||||
|
||||
- `prove_vat_register_book_consistency`
|
||||
- `prove_document_to_tax_reflection_closure`
|
||||
- `prove_missing_vat_link_risk`
|
||||
|
||||
### 3.2 Planned anchor extension
|
||||
|
||||
- `invoice_anchor`
|
||||
- `register_entry_anchor`
|
||||
- `book_entry_anchor`
|
||||
- `vat_amount_anchor`
|
||||
- `goods_or_item_linkage_anchor`
|
||||
|
||||
### 3.3 Planned family metrics
|
||||
|
||||
- `vat_grounded_positive_rate`
|
||||
- `vat_admissibility_noise_rate`
|
||||
- `vat_book_register_linkage_rate`
|
||||
- `vat_false_grounded_answer_rate`
|
||||
- `vat_live_targeting_precision_rate`
|
||||
|
||||
## 4) Required entities and relations (business contract)
|
||||
|
||||
### 4.1 Минимально необходимые сущности для proof closure
|
||||
|
||||
1. Документ поступления/реализации.
|
||||
2. Счет-фактура.
|
||||
3. Проводка/движение.
|
||||
4. Запись в НДС-регистре.
|
||||
5. Запись в книге покупок/продаж.
|
||||
6. Связь по товарной позиции/номенклатуре/документной цепочке.
|
||||
7. Контрагент/договорный контур при необходимости.
|
||||
|
||||
### 4.2 Критические relation links
|
||||
|
||||
1. `receipt_or_sale_document -> posting`
|
||||
2. `document -> invoice`
|
||||
3. `invoice -> vat_register_entry`
|
||||
4. `vat_register_entry -> purchase_or_sales_book_entry`
|
||||
5. `goods_or_item_context -> chain_completeness_verdict`
|
||||
|
||||
## 5) Snapshot/Live coverage verdict (as-is)
|
||||
|
||||
- practical mode для VAT family: `snapshot_plus_live_required`
|
||||
- positive path уже подтвержден на части кейсов
|
||||
- family не является текущим главным blocker
|
||||
- next focus: `live narrowing + reject cleanup`, без domain expansion
|
||||
|
||||
## 6) Answer/proof modes contract
|
||||
|
||||
### `grounded_positive`
|
||||
|
||||
Допускается, если одновременно:
|
||||
|
||||
- `admissible_evidence_count > 0`
|
||||
- подтверждены звенья `document -> invoice -> register -> book`
|
||||
- вывод не основан только на общем domain narrative
|
||||
- `false_grounded_answer_rate = 0`
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `По July 2020 цепочка документ -> проводка -> НДС-регистр -> книга подтверждена; явного выпадения по этой связке не найдено.`
|
||||
|
||||
### `limited_or_insufficient_evidence`
|
||||
|
||||
Обязателен, если:
|
||||
|
||||
- отсутствует одно из ключевых звеньев цепочки;
|
||||
- evidence есть, но mapping слабый;
|
||||
- `register/book` linkage не закрыт.
|
||||
|
||||
Короткий пример:
|
||||
|
||||
- `Документный и проводочный контур частично подтверждены, но связь до НДС-регистра или книги не восстановлена; вывод ограничен.`
|
||||
|
||||
### Запрещенные паттерны
|
||||
|
||||
- `НДС отражен корректно` без документно-регистровой связки;
|
||||
- уверенный verdict при `admissible = 0`;
|
||||
- подмена chain-proof общим domain narrative.
|
||||
|
||||
Короткий антипример:
|
||||
|
||||
- `НДС-цепочка в порядке` (нельзя без подтверждения register/book closure).
|
||||
|
||||
## 7) Gap register (VAT family)
|
||||
|
||||
| gap_id | category | severity | current_state | note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| VAT-G1 | `admissibility_residual_noise` | medium | open | остаточный шум по `weak_source_mapping` / `wrong_account_scope` |
|
||||
| VAT-G2 | `live_targeting_breadth` | medium | open | live targeting для VAT еще можно сузить до более proof-specific слоя |
|
||||
| VAT-G3 | `materialization_cleanup` | medium | open | positive path есть, но часть proof-path требует дочистки |
|
||||
| VAT-G4 | `false_grounded_risk_control` | low | controlled | family должна удерживать `false_grounded = 0` |
|
||||
|
||||
## 8) Code-path inventory (где живет контракт)
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts`
|
||||
- VAT claim type;
|
||||
- required anchors;
|
||||
- required checks для VAT chain.
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantDataLayer.ts`
|
||||
- route/live plan behavior (в т.ч. generic live overlay для non-FA/RBP);
|
||||
- VAT semantic profile and relation patterns.
|
||||
|
||||
- `llm_normalizer/backend/src/services/assistantService.ts`
|
||||
- routing and eligibility handoff;
|
||||
- answer-mode and debug export integration.
|
||||
|
||||
- run artifacts:
|
||||
- `X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt`
|
||||
|
||||
## 9) Regression set and acceptance policy
|
||||
|
||||
Обязательный минимум для VAT family:
|
||||
|
||||
1. базовый вопрос по поступлению 13 июля и реализации 15 июля;
|
||||
2. вариация на выпадение между документом, проводкой и налоговым отражением;
|
||||
3. вариация на книгу покупок/продаж;
|
||||
4. follow-up по той же цепочке;
|
||||
5. соседний VAT sanity-check.
|
||||
|
||||
Политика:
|
||||
|
||||
- после каждого VAT family pack обязателен новый run folder;
|
||||
- acceptance фиксируется на уровне family;
|
||||
- `false_grounded` должен оставаться нулевым.
|
||||
|
||||
## 10) Project decision line for this family
|
||||
|
||||
VAT family уже переведена в family-based execution контур Stage 4 и рассматривается как `non-blocker`, где нужен cleanup-дожим, а не отдельный большой redesign-пакет.
|
||||
Binary file not shown.
|
|
@ -0,0 +1,34 @@
|
|||
20 вопросов по вашей компании и июльскому снапшоту
|
||||
1. Расчёты / банк / 60–62
|
||||
Почему 6 июля ушла оплата за мебель по договору № 01/19-ПТ от 09.01.2019 на 55 200, а к концу июля по этой покупке мог остаться долг или незакрытый хвост?Проверяет payment →settlement closure по поставщику.
|
||||
Оплата по счёту № 4 от 07.07.20 на 276 873,60 пришла 13 июля. Зачёлся ли этот аванс покупателя корректно в реализации от 15 июля, или на 62.02 что-то осталось висеть?Это прямой company-specific тест на зачёт аванса покупателя.
|
||||
По договору № 1-ПМ/2020 от 05.06.2020 в июле пришло ещё 40 860 27 июля и 20 000 30 июля. Это уже закрытие дебиторки после реализации или в конце июля там остался аванс/переплата?Хороший тест на 62.01/62.02 и partial settlement.
|
||||
Почему по одному и тому же мебельному контуру в июле есть и поступления денег от покупателя, и зачёт аванса, но 62.01/62.02 всё равно могут не сойтись?Это вопрос не про сумму, а про механизм.
|
||||
Если по договору № 1-ПМ/2020 в июле было несколько оплат и одна крупная реализация, где именно ассистент видит разрыв: в договоре, в объекте расчётов или в хронологии документов?Проверяет problem-first объяснение, а не dump.
|
||||
Есть ли в июльском срезе ситуация, где деньги уже пришли, но закрытие расчётов не подтверждено тем документом, которым должно было закрыться?Тест на document_conflict.
|
||||
Почему по поставщику мебель оплачена 6 июля, а ассистент может считать, что обязательство не закрыто: не тот договор, не тот объект расчётов или вообще нет подтверждённого closure?Это уже “человеческий” symptom-first вопрос.
|
||||
Если смотреть только июль, какие именно расчётные цепочки по мебели выглядят завершёнными, а какие — нет?Полезный тест на ranking и truthful limitations.
|
||||
2. НДС / книга покупок / книга продаж
|
||||
13 июля проведено поступление товаров, а 15 июля — реализация этих же мебельных позиций. НДС-цепочка по этим движениям у нас полная или где-то есть выпадение между документом, проводкой и налоговым отражением?Это хороший cross-branch вопрос по реальной цепочке июля.
|
||||
По оплате от 13 июля на 276 873,60 в тексте явно указан НДС 20% = 46 145,60. Ассистент может доказать, что НДС по этой продаже действительно отразился там, где должен, или он только “догадывается”?Тест на false confidence и mechanism specificity.
|
||||
31 июля есть услуги связи на 1 166,67 + НДС 233,33 и отдельно полученный счёт-фактура на 233,33. Почему по такому кейсу книга покупок могла бы остаться пустой?Это уже отличный реальный кейс под P0-домен НДС.
|
||||
Связан ли полученный 31 июля счёт-фактура с документом услуг связи так, чтобы НДС по нему можно было принять к вычету в июле, или цепочка “документ → счёт-фактура → налоговая запись” неполная?Проверяет expected edges.
|
||||
Полезно для поиска реальных дыр, а не только для заранее известных кейсов.
|
||||
Если ассистент говорит, что НДС по связи за июль в порядке, на чём это должно быть основано: на документе услуг, на счёте-фактуре, на проводке по 19, или на записи книги покупок?Тест именно на explainability.
|
||||
Почему по одной июльской покупке НДС может попасть в контур, а по другой — нет, даже если обе операции выглядят проведёнными?Это хороший semi-open case на company snapshot.
|
||||
Есть ли в июльских движениях ситуация, где НДС отражён частично: документ и счёт-фактура есть, но запись книги или tax entry не подтверждены?Тест на broken_chain_segment в домене НДС.
|
||||
3. Закрытие месяца / затраты / РБП / амортизация
|
||||
31 июля у нас прошло “Закрытие счетов косвенных расходов”, и там есть крупные суммы — 148 050, 27 954,50, 5 786,63, 5 000 и другие. Всё ли это реально закрылось в нужный контур, или после июля могли остаться зависшие косвенные расходы?Это company-specific вопрос на period close.
|
||||
31 июля прошло “Списание РБП за Июль 2020 г.”, в том числе на 5 000 и ещё несколько сумм. Есть ли в базе признаки, что часть РБП к концу июля всё ещё живёт дольше ожидаемого?Хороший тест на lifecycle anomaly без выхода в полный Stage 5.
|
||||
31 июля начислена амортизация тремя суммами — 2 471,52, 2 465,28 и 849,83. Это похоже на полное начисление по всем нужным объектам за июль или есть риск, что какой-то объект ОС в июле не попал в амортизацию?Даже если ОС не P0, этот кейс полезен как controlled adjacent check.
|
||||
После всех июльских регламентных операций — амортизация, списание РБП, закрытие косвенных расходов, определение финансовых результатов — что у нас больше похоже на реальную проблему: незакрытый затратный хвост, stale RBP или просто нормальный остаток, который ассистент не должен объявлять багом?Это уже зрелый product test на limitation honesty.
|
||||
Какие из этих 20 самые сильные для первого прогона
|
||||
Если сжать до “ядра”, я бы первым запускал вот эти 8:
|
||||
Оплата 55 200 по договору № 01/19-ПТ — почему долг мог остаться.
|
||||
Поступление денег 276 873,60 от 13 июля — корректно ли зачёлся аванс 15 июля.
|
||||
Платежи 40 860 и 20 000 по договору № 1-ПМ/2020 — аванс это или закрытие дебиторки.
|
||||
31 июля услуги связи + НДС 233,33 + полученный счёт-фактура — полная ли НДС-цепочка.
|
||||
Есть ли покупки июля, где товар/услуга есть, а НДС-контур неполный.
|
||||
Закрытие косвенных расходов 31 июля — не осталось ли хвостов.
|
||||
Списание РБП на 31 июля — не живёт ли часть РБП дольше ожидаемого.
|
||||
После полного month-end — что из остатков является реальной проблемой, а что нет.
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
# Этап 4 — corrective pack по family isolation, claim routing и чистой приемке
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
Stage 4 уже работает в `family-based execution`:
|
||||
|
||||
- единица анализа: `family`;
|
||||
- единица реализации: `family pack`;
|
||||
- единица приемки: `family acceptance`.
|
||||
|
||||
По трем новым прогонам подтверждено: основная проблема в runtime не текстовая, а маршрутизационная:
|
||||
|
||||
- leakage между family/lane;
|
||||
- неправильный `claim_type` на корректных вопросах;
|
||||
- рассинхрон между route/domain/claim в разных слоях runtime;
|
||||
- смешанные acceptance-файлы, где трудно объективно мерить качество lane/family.
|
||||
|
||||
## 2. Архитектурная рамка corrective pack
|
||||
|
||||
Чтобы пакет был совместим с текущим проектом, фиксируем:
|
||||
|
||||
1. `core families` Stage 4:
|
||||
`VAT chain`, `RBP tail`, `FA amortization`.
|
||||
2. `control lanes` (regression/sanity, не first-class family):
|
||||
`settlements_60_62`, `month_close_indirect_costs`.
|
||||
3. Пакет не добавляет новые домены и не меняет Stage 4 модель.
|
||||
4. Пакет не строит новый proof engine и не уводит в Stage 5.
|
||||
|
||||
## 3. Цель corrective pack
|
||||
|
||||
Привести execution к состоянию, где:
|
||||
|
||||
1. вопрос из одного family/lane не уходит в чужой `claim/domain path`;
|
||||
2. routing в live и follow-up не расходится между runtime-слоями;
|
||||
3. acceptance выполняется по чистым family/lane файлам;
|
||||
4. `false_grounded_answer_rate = 0`;
|
||||
5. `wrong_family_route_rate = 0` на контрольном наборе этого pack.
|
||||
|
||||
## 4. Подволны
|
||||
|
||||
### Подволна 1 — Family isolation matrix
|
||||
|
||||
#### Задача
|
||||
|
||||
Снять матрицу соответствия вопроса и целевого family/lane, и явно показать точки leakage.
|
||||
|
||||
#### Что сделать
|
||||
|
||||
Построить матрицу по контрольным вопросам из трех файлов:
|
||||
|
||||
- `расчеты банк 60 62.txt`
|
||||
- `ндс книга покупок и продаж.txt`
|
||||
- `рбп затраты аморт.txt`
|
||||
|
||||
Для каждого вопроса зафиксировать:
|
||||
|
||||
- `raw_question`
|
||||
- `expected_lane_type` (`core_family` | `control_lane`)
|
||||
- `expected_family_or_lane`
|
||||
- `expected_domain`
|
||||
- `expected_claim_type`
|
||||
- `actual_domain`
|
||||
- `actual_claim_type`
|
||||
- `actual_query_subject`
|
||||
- `actual_source_profile`
|
||||
- `actual_business_scope`
|
||||
- `mismatch_type`
|
||||
|
||||
Минимальные lane:
|
||||
|
||||
1. `settlements_60_62` (control lane)
|
||||
2. `VAT chain` (core family)
|
||||
3. `RBP tail` (core family)
|
||||
4. `FA amortization` (core family)
|
||||
5. `month_close_indirect_costs` (control/sanity lane)
|
||||
|
||||
#### Acceptance
|
||||
|
||||
Есть явная матрица:
|
||||
|
||||
- что относится к core family, а что к control lane;
|
||||
- где и почему происходит leakage.
|
||||
|
||||
### Подволна 2 — Claim routing + domain purity alignment
|
||||
|
||||
#### Задача
|
||||
|
||||
Устранить переходы:
|
||||
|
||||
- settlement -> VAT claim;
|
||||
- settlement -> FA claim;
|
||||
- VAT -> settlement guard path;
|
||||
- month-close -> случайный FA/RBP claim без основания.
|
||||
|
||||
#### Что сделать
|
||||
|
||||
##### A. Синхронизировать domain inference между слоями
|
||||
|
||||
Унифицировать доменную классификацию для:
|
||||
|
||||
- `assistantService` domain hint;
|
||||
- `investigationState` explicit domain hint;
|
||||
- follow-up cross-scope checks.
|
||||
|
||||
Цель: один и тот же вопрос не должен получать разные домены в разных слоях runtime.
|
||||
|
||||
##### B. Ужесточить claim inference для settlement/VAT/FA
|
||||
|
||||
1. Settlement/advance/closure не должны резолвиться в:
|
||||
|
||||
- `prove_vat_chain_completeness`;
|
||||
- `prove_fixed_asset_amortization_coverage`.
|
||||
|
||||
2. Убрать ложный FA-триггер на числовых артефактах:
|
||||
|
||||
- числа из дат/сумм не должны поднимать FA claim;
|
||||
- `62.02` не должен теряться как `amount_token` в settlement flow.
|
||||
|
||||
3. FA claim допускается только при явном FA-сигнале:
|
||||
|
||||
- лексический FA-сигнал;
|
||||
- или валидные account/object anchors после account extraction cleanup.
|
||||
|
||||
##### C. Привязать `query_subject` к resolved family/lane
|
||||
|
||||
`query_subject` должен строиться от resolved route/domain/claim, а не от сырого mixed-domain списка.
|
||||
|
||||
Практический эффект:
|
||||
|
||||
- VAT не должен уходить в `supplier_tail_analysis`;
|
||||
- settlement не должен маскироваться под VAT/FA path.
|
||||
|
||||
##### D. Сохранить рабочий RBP path
|
||||
|
||||
Не ломать поднятый RBP контракт:
|
||||
|
||||
- `claim_type = prove_rbp_tail_state`;
|
||||
- обязательный live recipe;
|
||||
- non-zero admissible evidence path.
|
||||
|
||||
##### E. Для FA использовать route-lock/parity контроль
|
||||
|
||||
FA в этом паке контролируется через:
|
||||
|
||||
- `claim-route lock`;
|
||||
- `mock/live parity` по целевым FA вопросам;
|
||||
|
||||
а не через прямую метрику domain-card purity 1:1.
|
||||
|
||||
#### Acceptance
|
||||
|
||||
На контрольном наборе pack:
|
||||
|
||||
- `wrong_family_route_rate = 0`;
|
||||
- `wrong_claim_type_rate = 0`;
|
||||
- `domain_purity_guard_lane_match_rate = 1.0` для lanes с domain-card (`settlements`, `VAT`, `month_close`);
|
||||
- `fa_route_lock_correctness_rate >= 0.95`;
|
||||
- `supplier_customer_polarity` не остается unresolved на core settlement cases.
|
||||
|
||||
### Подволна 3 — Чистая структура acceptance-прогонов
|
||||
|
||||
#### Задача
|
||||
|
||||
Убрать смешанные acceptance-файлы.
|
||||
|
||||
#### Что сделать
|
||||
|
||||
Собрать отдельные прогоны:
|
||||
|
||||
1. `chat_export_settlements.txt` (control lane)
|
||||
2. `chat_export_vat.txt` (core family)
|
||||
3. `chat_export_rbp.txt` (core family)
|
||||
4. `chat_export_fa.txt` (core family)
|
||||
5. `chat_export_month_close_sanity.txt` (control lane)
|
||||
|
||||
Правило:
|
||||
|
||||
- один файл = один family/lane;
|
||||
- mixed files не используются как family acceptance.
|
||||
|
||||
#### Acceptance
|
||||
|
||||
Все acceptance-артефакты lane-clean и сопоставимы между волнами.
|
||||
|
||||
### Подволна 4 — Final live rerun по изолированным lanes
|
||||
|
||||
#### Задача
|
||||
|
||||
Пересобрать финальный live rerun после исправления isolation/routing.
|
||||
|
||||
#### Обязательный набор кейсов
|
||||
|
||||
`settlements_60_62` (control lane):
|
||||
|
||||
1. supplier settlement (`55 200`)
|
||||
2. buyer advance (`62.02`)
|
||||
3. closure case без суммы
|
||||
4. follow-up по тому же договору/документу
|
||||
|
||||
`VAT chain`:
|
||||
|
||||
1. услуги связи (`1 166,67 + НДС 233,33`)
|
||||
2. счет-фактура / книга покупок
|
||||
3. выпадение между документом и книгой
|
||||
4. follow-up по той же VAT-цепочке
|
||||
|
||||
`RBP tail`:
|
||||
|
||||
1. `Списание РБП за Июль 2020` (`5 000`)
|
||||
2. вариант без суммы
|
||||
3. полнота закрытия
|
||||
4. follow-up по объекту/документу
|
||||
|
||||
`FA amortization`:
|
||||
|
||||
1. `2 471,52 / 2 465,28 / 849,83`
|
||||
2. expected vs actual set
|
||||
3. missing object risk
|
||||
4. follow-up по объекту ОС
|
||||
|
||||
`month_close_indirect_costs` (control lane):
|
||||
|
||||
1. базовый вопрос по косвенным расходам
|
||||
2. зависший хвост
|
||||
3. limitation-honesty после полного month-end
|
||||
|
||||
#### Acceptance
|
||||
|
||||
Новый live rerun показывает:
|
||||
|
||||
- route/claim isolation без cross-family leakage;
|
||||
- корректную lane-классификацию;
|
||||
- честный limited mode только по фактической нехватке данных.
|
||||
|
||||
## 5. Метрики corrective pack
|
||||
|
||||
### Добавить/вывести
|
||||
|
||||
- `wrong_family_route_rate`
|
||||
- `wrong_claim_type_rate`
|
||||
- `domain_purity_guard_lane_match_rate`
|
||||
- `fa_route_lock_correctness_rate`
|
||||
- `family_isolation_correctness_rate`
|
||||
- `live_recipe_binding_rate`
|
||||
- `false_grounded_answer_rate`
|
||||
- `mechanism_discrimination_rate`
|
||||
- `limited_answer_honesty_rate`
|
||||
|
||||
### Минимальные пороги (для контрольного набора этого pack)
|
||||
|
||||
- `wrong_family_route_rate = 0`
|
||||
- `wrong_claim_type_rate = 0`
|
||||
- `domain_purity_guard_lane_match_rate = 1.0` (для lanes с domain-card)
|
||||
- `fa_route_lock_correctness_rate >= 0.95`
|
||||
- `false_grounded_answer_rate = 0`
|
||||
- `mechanism_discrimination_rate >= 0.90`
|
||||
- `limited_answer_honesty_rate >= 0.95`
|
||||
|
||||
## 6. Что создать
|
||||
|
||||
### В `docs/ARCH`
|
||||
|
||||
1. `10 - family_isolation_and_claim_routing_corrective_pack_2026-03-29.md`
|
||||
2. `10A - family_isolation_matrix.md`
|
||||
3. `10B - wrong_claim_route_register.md`
|
||||
4. `10C - clean_family_run_structure.md`
|
||||
5. `10D - family_acceptance_policy_update.md`
|
||||
6. `10E - cross_layer_domain_inference_parity.md`
|
||||
|
||||
### В `llm_normalizer/docs/runs/<new_run_pack>`
|
||||
|
||||
1. `run_summary.json`
|
||||
2. `before_after_metrics.json`
|
||||
3. `family_case_matrix.md`
|
||||
4. `acceptance_note.md`
|
||||
5. `chat_export_settlements.txt`
|
||||
6. `chat_export_vat.txt`
|
||||
7. `chat_export_rbp.txt`
|
||||
8. `chat_export_fa.txt`
|
||||
9. `chat_export_month_close_sanity.txt`
|
||||
10. `debug_payloads/`
|
||||
11. `routing_parity_diff.md`
|
||||
|
||||
## 7. Что нельзя делать
|
||||
|
||||
- не добавлять новые домены;
|
||||
- не менять Stage 4 family-based model;
|
||||
- не строить новый proof engine;
|
||||
- не смешивать acceptance разных family/lane в одном файле;
|
||||
- не закрывать acceptance по одному удачному ответу;
|
||||
- не лечить leakage общим ростом generic retrieval.
|
||||
|
||||
## 8. Финальный verdict
|
||||
|
||||
Выдать:
|
||||
|
||||
- `FAMILY_ISOLATION_FIXED / NOT_FIXED`
|
||||
- `CLAIM_ROUTING_FIXED / NOT_FIXED`
|
||||
- `CLEAN_FAMILY_ACCEPTANCE_READY / NOT_READY`
|
||||
|
||||
Общий статус:
|
||||
|
||||
- `STAGE4_FAMILY_ISOLATION_PACK_ACCEPTED`
|
||||
- или `STAGE4_FAMILY_ISOLATION_PACK_ACCEPTED_WITH_LIMITATIONS`
|
||||
- или `STAGE4_FAMILY_ISOLATION_PACK_NOT_ACCEPTED`
|
||||
|
||||
## 9. Ожидаемый итог
|
||||
|
||||
После этого пакета:
|
||||
|
||||
- settlement-вопросы не уходят в VAT/FA claim;
|
||||
- VAT-вопросы не ведутся через settlement-path;
|
||||
- mixed-file приемка не используется;
|
||||
- прогресс измеряется по чистым family/lane с сопоставимыми метриками и артефактами.
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
Этап 4 — пакет по амортизации.md
|
||||
: замыкание доказательной цепочки по объектам ОС
|
||||
1. Контекст
|
||||
|
||||
После аудита и последующих пакетов:
|
||||
|
||||
НДС уже не главный blocker;
|
||||
РБП выведен из критического состояния;
|
||||
амортизация остаётся следующим главным узлом.
|
||||
|
||||
По аудиту по амортизации картина такая:
|
||||
|
||||
источник не пустой;
|
||||
admissible evidence уже есть;
|
||||
live path уже что-то поднимает;
|
||||
но claim-proof не замыкается из-за:
|
||||
недостаточного mapping связей 1С,
|
||||
слабого покрытия claim anchors,
|
||||
отсутствия внятного expected set объектов ОС за июль.
|
||||
|
||||
То есть проблема по амортизации сейчас не в нуле данных, а в том, что система не умеет собрать из уже поднятых данных доказанный вывод:
|
||||
|
||||
все ли нужные объекты ОС попали в начисление;
|
||||
если не все — какой именно объект/контур выпал;
|
||||
или текущий набор начислений выглядит полным.
|
||||
2. Цель пакета
|
||||
|
||||
Нужно не “улучшить ответ”, а замкнуть proof-контур по амортизации:
|
||||
|
||||
вопрос про амортизацию → объекты ОС → expected set за июль → фактическое начисление → проводки/движения → покрытие / непокрытие → доказательный вывод
|
||||
|
||||
Итогом должно стать:
|
||||
|
||||
явное понимание, какие объекты ОС должны участвовать в начислении за июль;
|
||||
явное понимание, какие из них реально попали в начисление;
|
||||
восстановление связей между объектами ОС, документом начисления, движениями и проводками;
|
||||
non-zero claim-proof coverage;
|
||||
ответ, который может либо:
|
||||
доказать полноту начисления,
|
||||
либо показать, что есть риск пропуска конкретных объектов,
|
||||
либо честно ограничиться, если не хватает ключевой связи.
|
||||
3. Source of truth
|
||||
Базовый вопрос
|
||||
|
||||
31 июля начислена амортизация тремя суммами — 2 471,52, 2 465,28 и 849,83. Это похоже на полное начисление по всем нужным объектам за июль или есть риск, что какой-то объект ОС в июле не попал в амортизацию?
|
||||
|
||||
Текущий диагноз
|
||||
|
||||
По аудиту и прогонам:
|
||||
|
||||
admissible evidence уже есть;
|
||||
live path не нулевой;
|
||||
но итоговый proof не закрывается, потому что не хватает:
|
||||
корректной карты связей,
|
||||
object-level coverage,
|
||||
claim anchor closure.
|
||||
4. Главная рабочая гипотеза
|
||||
|
||||
Для амортизации основной разрыв сейчас в связке:
|
||||
|
||||
entity/relation mapping
|
||||
система не понимает до конца, как объект ОС связывается с начислением, движением, проводкой и expected coverage;
|
||||
expected set reconstruction
|
||||
система не умеет уверенно определить, какие объекты ОС вообще должны были попасть в начисление за июль;
|
||||
claim anchor closure
|
||||
сумма начисления и сам факт начисления видны, но они не собираются в доказательство полноты/неполноты охвата.
|
||||
5. Что нужно сделать
|
||||
|
||||
Работу выполнить по 5 узлам.
|
||||
|
||||
Узел A — Построить минимальную предметную модель амортизации
|
||||
Задача
|
||||
|
||||
Понять, какие сущности 1С обязательны для доказательного ответа на вопрос про полноту начисления амортизации.
|
||||
|
||||
Что сделать
|
||||
|
||||
Для кейса амортизации определить:
|
||||
|
||||
A1. Seed entities
|
||||
документ/операция начисления амортизации на 31 июля;
|
||||
три суммы: 2 471,52 / 2 465,28 / 849,83;
|
||||
July 2020;
|
||||
объекты ОС, если их можно выделить напрямую или через связанные документы/регистры.
|
||||
A2. Required entities
|
||||
|
||||
Минимально установить:
|
||||
|
||||
документ начисления амортизации;
|
||||
объект(ы) ОС;
|
||||
движения начисления;
|
||||
проводки;
|
||||
регистры состояния/начисления/учёта ОС;
|
||||
expected set объектов ОС на июль;
|
||||
фактический set объектов, попавших в начисление;
|
||||
признаки “должен был попасть / не попал”.
|
||||
A3. Expected transitions
|
||||
|
||||
Обязательные переходы:
|
||||
|
||||
объект ОС → начисление амортизации;
|
||||
начисление → движения/проводки;
|
||||
объект ОС → expected July coverage;
|
||||
expected set → actual set;
|
||||
actual set vs missing set → риск неполного начисления.
|
||||
Acceptance
|
||||
Должен появиться явный fa_required_entity_map.
|
||||
Должно быть понятно, без каких сущностей доказательство полноты невозможно.
|
||||
Узел B — Восстановить expected set объектов ОС за июль
|
||||
Задача
|
||||
|
||||
Ответить на главный скрытый вопрос:
|
||||
какие объекты ОС вообще должны были попасть в амортизацию за июль?
|
||||
|
||||
Что сделать
|
||||
Определить, откуда в 1С можно получить expected population объектов ОС на июль:
|
||||
активные объекты ОС;
|
||||
объекты, находящиеся в состоянии, допускающем амортизацию;
|
||||
объекты, не выбывшие и не исключённые из расчёта;
|
||||
объекты, для которых июльское начисление ожидаемо.
|
||||
Проверить:
|
||||
есть ли этот expected set в snapshot;
|
||||
есть ли он через live;
|
||||
нужно ли собирать его из нескольких регистров/документов.
|
||||
Зафиксировать:
|
||||
expected_fa_set
|
||||
actual_fa_set_from_amortization
|
||||
missing_fa_candidates
|
||||
uncertain_fa_candidates
|
||||
Acceptance
|
||||
Должен появиться reconstructible expected_fa_set, а не просто суммы начисления.
|
||||
Должно быть понятно, может ли система вообще доказать “полноту” начисления, а не только наличие документа.
|
||||
Узел C — Восстановить relation mapping по объектам ОС
|
||||
Задача
|
||||
|
||||
Понять, как именно объект ОС связывается с начислением, движениями и проводками.
|
||||
|
||||
Что сделать
|
||||
|
||||
Для каждого candidate объекта ОС построить relation map:
|
||||
|
||||
fa_object
|
||||
document_amortization
|
||||
movement
|
||||
posting
|
||||
period
|
||||
coverage_status
|
||||
|
||||
Нужно зафиксировать:
|
||||
|
||||
прямые связи;
|
||||
косвенные связи;
|
||||
missing links;
|
||||
ambiguous links;
|
||||
какие связи текущий runtime уже использует;
|
||||
какие связи есть в 1С, но не реконструируются рантаймом.
|
||||
Acceptance
|
||||
Должна появиться object-level relation map по амортизации.
|
||||
Должно стать ясно, где именно рвётся связь между ОС и начислением.
|
||||
Узел D — Замкнуть claim anchors для амортизации
|
||||
Задача
|
||||
|
||||
Сделать так, чтобы claim по амортизации опирался не только на суммы, а на полноценный набор anchors.
|
||||
|
||||
Что сделать
|
||||
|
||||
Для claim типа prove_fixed_asset_amortization_coverage выделять и проверять:
|
||||
|
||||
период;
|
||||
суммы начисления;
|
||||
документ начисления;
|
||||
объекты ОС;
|
||||
expected object set;
|
||||
actual object set;
|
||||
missing object candidates;
|
||||
проводки/движения, подтверждающие начисление.
|
||||
|
||||
В debug/export ввести:
|
||||
|
||||
claim_type
|
||||
required_anchors
|
||||
resolved_anchors
|
||||
missing_anchor_classes
|
||||
claim_anchor_coverage_ratio
|
||||
Acceptance
|
||||
Кейc по амортизации не должен падать просто как claim_anchor_coverage_insufficient без расшифровки.
|
||||
Должно быть видно, каких именно anchors не хватает.
|
||||
Узел E — Собрать proof-style answer по амортизации
|
||||
Задача
|
||||
|
||||
Сделать так, чтобы финальный ответ зависел от object-level proof, а не от общей гипотезы по lifecycle.
|
||||
|
||||
Что сделать
|
||||
Если proof-path собран
|
||||
|
||||
Ответ должен уметь сказать:
|
||||
|
||||
какие объекты ОС подтверждены в начислении;
|
||||
какие должны были попасть и не подтверждены;
|
||||
выглядит ли начисление полным;
|
||||
где именно есть риск пропуска.
|
||||
Если proof-path неполный
|
||||
|
||||
Ответ должен честно сказать:
|
||||
|
||||
каких данных не хватает;
|
||||
какого object-level звена не хватает;
|
||||
можно ли судить только о частичном покрытии.
|
||||
Запрещено
|
||||
выдавать общий lifecycle-туман без object-level explanation;
|
||||
ограничиваться только тем, что “сигнал слабый”.
|
||||
Acceptance
|
||||
Answer mode должен быть привязан к object-level proof.
|
||||
Амортизация должна перестать быть абстрактным month-close кейсом без объектной структуры.
|
||||
6. Проверки
|
||||
|
||||
Обязательно выполнить:
|
||||
|
||||
replay базового вопроса по амортизации;
|
||||
debug/export после фикса;
|
||||
live call inventory;
|
||||
relation reconstruction trace;
|
||||
expected vs actual FA coverage matrix;
|
||||
before/after по answer mode.
|
||||
|
||||
Желательно:
|
||||
|
||||
один соседний follow-up по тому же кейсу;
|
||||
один sanity-check по другому ОС-related кейсу, если есть.
|
||||
7. Метрики пакета
|
||||
|
||||
Добавить/обновить:
|
||||
|
||||
fa_expected_set_reconstruction_rate
|
||||
fa_relation_mapping_coverage_rate
|
||||
fa_claim_anchor_coverage_rate
|
||||
fa_actual_vs_expected_comparison_rate
|
||||
fa_proof_closure_rate
|
||||
fa_false_grounded_answer_rate
|
||||
Минимальные пороги
|
||||
fa_expected_set_reconstruction_rate >= 0.85
|
||||
fa_relation_mapping_coverage_rate >= 0.85
|
||||
fa_claim_anchor_coverage_rate >= 0.9
|
||||
fa_proof_closure_rate > 0
|
||||
fa_false_grounded_answer_rate = 0
|
||||
8. Обязательные артефакты
|
||||
|
||||
В новой run-папке положить:
|
||||
|
||||
README.md
|
||||
run_summary.json
|
||||
fa_expected_set_report.md
|
||||
fa_relation_mapping_report.md
|
||||
fa_claim_anchor_report.md
|
||||
fa_proof_closure_report.md
|
||||
fa_before_after_matrix.md
|
||||
chat_export_fa.md
|
||||
debug_payloads/
|
||||
raw_live_calls/
|
||||
fa_required_entity_map.json
|
||||
fa_expected_vs_actual_set.json
|
||||
fa_relation_map.json
|
||||
fa_admissibility_reject_breakdown.json
|
||||
9. Что не делать
|
||||
не тащить в эту волну РБП как главный фокус;
|
||||
не расползаться на весь month-close контур;
|
||||
не строить новый общий proof engine;
|
||||
не закрывать пакет по summary без replay амортизационного кейса;
|
||||
не маскировать отсутствие object-level proof красивым текстом ответа.
|
||||
10. Финальный verdict
|
||||
|
||||
В конце пакета выдать:
|
||||
|
||||
FA_EXPECTED_SET_FIXED / NOT_FIXED
|
||||
FA_RELATION_MAPPING_FIXED / NOT_FIXED
|
||||
FA_CLAIM_ANCHOR_CLOSURE_FIXED / NOT_FIXED
|
||||
FA_PROOF_CLOSURE_FIXED / NOT_FIXED
|
||||
|
||||
И общий статус:
|
||||
|
||||
FA_PACK_ACCEPTED
|
||||
или FA_PACK_ACCEPTED_WITH_LIMITATIONS
|
||||
или FA_PACK_NOT_ACCEPTED
|
||||
11. Ожидаемый результат
|
||||
|
||||
После этого пакета должно стать ясно:
|
||||
|
||||
какие объекты ОС должны были участвовать в начислении за июль;
|
||||
какие реально участвовали;
|
||||
по каким объектам доказательство есть;
|
||||
где именно остаётся разрыв;
|
||||
может ли система по амортизации выдавать не просто “limited”, а нормальный object-level proof-style вывод.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# ЭТАП 4 — переход к семействам вопросов и source-to-proof контрактам
|
||||
|
||||
Статус: `APPROVED_EXECUTION_MODEL`
|
||||
Дата: `2026-03-29`
|
||||
Область: `Stage 4 (P0-only)`
|
||||
|
||||
## 1. Почему режим работы меняется
|
||||
|
||||
Точечная отладка отдельных вопросов была полезна как разведка, но как основная модель развития она не масштабируется:
|
||||
|
||||
- один удачный ответ не закрывает класс дефектов;
|
||||
- слишком много ручной подгонки под формулировки;
|
||||
- приемка становится неустойчивой.
|
||||
|
||||
Stage 4 официально переводится в `family-based execution`.
|
||||
|
||||
## 2. Что уже известно по Stage 4
|
||||
|
||||
- общий аудит дал `MULTI_NODE_FAILURE_CONFIRMED`;
|
||||
- VAT family: рабочий positive path уже есть на части кейсов;
|
||||
- RBP family: выведена из критического режима, но есть незакрытый `source coverage`;
|
||||
- FA family: proof path поднят, но live acceptance еще требует дожима.
|
||||
|
||||
## 3. Новый официальный формат Stage 4
|
||||
|
||||
С этого момента:
|
||||
|
||||
- единица анализа: `question family`;
|
||||
- единица реализации: `family pack`;
|
||||
- единица приемки: `family acceptance` на наборе вариаций.
|
||||
|
||||
## 4. Реестр family (активный)
|
||||
|
||||
1. VAT chain
|
||||
- Card: [Stage 4 - Family Card v1 — VAT chain.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 — VAT chain.md)
|
||||
- Latest run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\run_summary.json)
|
||||
|
||||
2. RBP tail / write-off overstay
|
||||
- Card: [Stage 4 - Family Card v1 RBP.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 RBP.md)
|
||||
- Latest run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\run_summary.json)
|
||||
|
||||
3. FA amortization coverage
|
||||
- Card: [Stage 4 - Family Card v1 FA.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 FA.md)
|
||||
- Latest run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure\run_summary.json)
|
||||
|
||||
## 5. Что считается закрытием family
|
||||
|
||||
Family считается закрытой не по одному ответу, а только при выполнении всего контракта:
|
||||
|
||||
- оформлена family card;
|
||||
- source coverage и ограничения зафиксированы;
|
||||
- relation map собран и проверяем;
|
||||
- live recipe воспроизводим;
|
||||
- admissibility и answer/proof contract зафиксированы;
|
||||
- regression-пакет проходит приемку;
|
||||
- `false_grounded_answer_rate = 0`.
|
||||
|
||||
## 6. Границы Stage 4
|
||||
|
||||
Это не Stage 5 и не новая архитектура:
|
||||
|
||||
- без новых доменов;
|
||||
- без graph/schema expansion;
|
||||
- без Stage 5 как core path;
|
||||
- без полного redesign.
|
||||
|
||||
## 7. Execution-практика
|
||||
|
||||
- планируем и исполняем работу family-pack'ами;
|
||||
- после каждого прогона обязательны run-артефакты в `llm_normalizer/docs/runs`;
|
||||
- фиксируем приемку на уровне family, а не единичного вопроса;
|
||||
- сквозная навигация по family и run ведется в [STAGE_04_FAMILY_BACKLOG_2026-03-29.md](X:\1C\NDC_1C\docs\accounting-assistant\accounting-assistant\03_execution\STAGE_04_FAMILY_BACKLOG_2026-03-29.md).
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
# FAMILY_CARD_TEMPLATE (runtime-aligned)
|
||||
|
||||
**document_status:** `ACTIVE`
|
||||
**template_scope:** `Stage 4 (P0-only)`
|
||||
**execution_unit:** `family pack`
|
||||
|
||||
## 0) Header (обязательные метаданные family)
|
||||
|
||||
- `family_name`:
|
||||
- `family_id`:
|
||||
- `stage_scope`:
|
||||
- `current_family_status`: `accepted` / `accepted_with_limitations` / `not_accepted`
|
||||
- `primary_gap`:
|
||||
- `latest_pack`:
|
||||
- `next_pack_focus`:
|
||||
- `family_source_of_truth_questions`:
|
||||
- `family_latest_live_replay`:
|
||||
- `family_latest_acceptance_run`:
|
||||
|
||||
## 1) Что фиксирует документ
|
||||
|
||||
- Зачем карточка нужна для этой family.
|
||||
- Почему карточка делится на:
|
||||
- `Runtime V1 (as-is)` — только подтвержденный факт по коду/артефактам;
|
||||
- `Target V2 (planned)` — план следующего доведения.
|
||||
|
||||
## 2) Runtime V1 (фактический контракт)
|
||||
|
||||
### 2.1 Claim contract (as-is)
|
||||
|
||||
- `primary_claim_type`:
|
||||
- `additional_claim_types` (если уже first-class в runtime):
|
||||
- `claim_boundaries` (что не входит в Stage 4):
|
||||
|
||||
### 2.2 Required anchors (runtime-enforced)
|
||||
|
||||
- Какие anchors обязательны именно сейчас.
|
||||
- Какие reason codes использует runtime при нехватке anchors.
|
||||
|
||||
### 2.3 Claim-bound live recipe (runtime-enforced)
|
||||
|
||||
- Список обязательных live/MCP call id.
|
||||
- Что каждый шаг должен подтверждать.
|
||||
- Что считается успешным live hit.
|
||||
|
||||
### 2.4 Route behavior (as-is)
|
||||
|
||||
- Как runtime принудительно удерживает route для family.
|
||||
- Какой no-route recovery используется.
|
||||
- Какие debug audit-поля экспортируются.
|
||||
|
||||
### 2.5 Evidence / admissibility behavior (as-is)
|
||||
|
||||
- Какие правила admissibility реально применяются.
|
||||
- Какие baseline reject reasons наблюдались до последнего пакета.
|
||||
- Что уже исправлено на materialization уровне.
|
||||
|
||||
### 2.6 Runtime acceptance snapshot
|
||||
|
||||
- `*_FIXED` / `NOT_FIXED` статусы из последнего run.
|
||||
- Краткий verdict latest pack.
|
||||
|
||||
### 2.7 known_runtime_limits (as-is)
|
||||
|
||||
- `source coverage` ограничения.
|
||||
- Возможные scope/route inconsistency в живых трассах.
|
||||
- Краевые риски по anchor quality / mapping.
|
||||
|
||||
## 3) Target V2 (planned, не критерий текущей приемки)
|
||||
|
||||
### 3.1 Planned claim extension
|
||||
|
||||
- Какие дополнительные claim types хотим сделать first-class.
|
||||
|
||||
### 3.2 Planned anchor extension
|
||||
|
||||
- Какие anchors добавятся как обязательные для зрелой версии family.
|
||||
|
||||
### 3.3 Planned family metrics
|
||||
|
||||
- Набор целевых метрик family.
|
||||
- Отдельно пометить, какие уже есть в harness, а какие пока target-only.
|
||||
|
||||
## 4) Required entities and relations (business contract)
|
||||
|
||||
### 4.1 Минимально необходимые сущности в 1C
|
||||
|
||||
- Набор сущностей, без которых proof closure невозможен.
|
||||
- Что optional enrichment.
|
||||
|
||||
### 4.2 Критические relation links
|
||||
|
||||
- Минимальная цепочка source-to-proof связей.
|
||||
- Какие связи должны быть прямыми, какие допускаются косвенными.
|
||||
|
||||
## 5) Snapshot/Live coverage verdict
|
||||
|
||||
- Что покрывает snapshot-only.
|
||||
- Что требует live.
|
||||
- Итог: `snapshot_only_sufficient` / `snapshot_plus_live_required` / `live_primary_required`.
|
||||
|
||||
## 6) Answer/proof modes contract
|
||||
|
||||
### `grounded_positive`
|
||||
|
||||
- Условия допуска.
|
||||
- Что обязательно должно быть в ответе.
|
||||
- Короткий пример `grounded_positive`.
|
||||
|
||||
### `limited_or_insufficient_evidence`
|
||||
|
||||
- Условия, когда обязательно остаемся в limited mode.
|
||||
- Что обязательно должно быть явно обозначено (missing link/ограничение).
|
||||
- Короткий пример `limited`.
|
||||
|
||||
### Запрещенные паттерны
|
||||
|
||||
- Какие формулировки считаются недопустимыми.
|
||||
- Короткий антипример запрещенного ответа.
|
||||
|
||||
## 7) Gap register (family)
|
||||
|
||||
Использовать таблицу:
|
||||
|
||||
| gap_id | category | severity | current_state | note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| FAM-G1 | `...` | blocker/high/medium | open/partial/closed | ... |
|
||||
|
||||
Рекомендуемые категории:
|
||||
|
||||
- `missing_source_data`
|
||||
- `source_not_connected_to_runtime`
|
||||
- `wrong_route_selection`
|
||||
- `wrong_entity_mapping`
|
||||
- `wrong_live_call_target`
|
||||
- `evidence_not_materialized`
|
||||
- `admissibility_reject_not_due_to_data`
|
||||
- `answer_layer_underuses_available_evidence`
|
||||
|
||||
## 8) Code-path inventory (где живет контракт)
|
||||
|
||||
- Модули normalizer/router/claim-bound/evidence/admissibility/answer.
|
||||
- Ключевые файлы runtime.
|
||||
- Папка run-артефактов, на которую опирается статус family.
|
||||
|
||||
## 9) Regression set and acceptance policy
|
||||
|
||||
- Обязательные контрольные вопросы для этой family.
|
||||
- Набор формулировочных вариаций.
|
||||
- Правило: после каждого family pack обязателен run folder в `llm_normalizer/docs/runs`.
|
||||
- Правило: приемка фиксируется на уровне family, не на уровне одного удачного ответа.
|
||||
- Правило: `false_grounded_answer_rate` должен оставаться нулевым.
|
||||
|
||||
## 10) Project decision line (для этой family)
|
||||
|
||||
- Одна короткая строка в проектном стиле:
|
||||
- текущий статус family;
|
||||
- главный незакрытый узел;
|
||||
- что является условием перехода в fully accepted.
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# STAGE_04_DECISION_NOTE_FAMILY_BASED_EXECUTION
|
||||
|
||||
Дата: `2026-03-29`
|
||||
Статус: `DECISION_APPROVED`
|
||||
Область: `Stage 4 (P0-only)`
|
||||
|
||||
## Почему принято решение
|
||||
|
||||
Stage 4 дошел до состояния, где вопрос-by-вопрос режим уже неуправляем:
|
||||
|
||||
- повторяются однотипные доменные поломки;
|
||||
- стоимость точечной отладки растет;
|
||||
- приемка становится хрупкой.
|
||||
|
||||
## Что меняется официально
|
||||
|
||||
- единица анализа: `question family`;
|
||||
- единица реализации: `family pack`;
|
||||
- единица приемки: `family acceptance`.
|
||||
|
||||
## Что не меняется
|
||||
|
||||
- Stage 4 остается Stage 4;
|
||||
- границы `P0-only` сохраняются;
|
||||
- без новых доменов и без Stage 5 как core path.
|
||||
|
||||
## Активные family в проекте
|
||||
|
||||
1. VAT chain
|
||||
- Card: [Stage 4 - Family Card v1 — VAT chain.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 — VAT chain.md)
|
||||
|
||||
2. RBP tail
|
||||
- Card: [Stage 4 - Family Card v1 RBP.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 RBP.md)
|
||||
|
||||
3. FA amortization coverage
|
||||
- Card: [Stage 4 - Family Card v1 FA.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 FA.md)
|
||||
|
||||
Общий реестр и run-навигация:
|
||||
[STAGE_04_FAMILY_BACKLOG_2026-03-29.md](X:\1C\NDC_1C\docs\accounting-assistant\accounting-assistant\03_execution\STAGE_04_FAMILY_BACKLOG_2026-03-29.md)
|
||||
|
||||
## Контрольные lane (не first-class family)
|
||||
|
||||
В Stage 4 используются контрольные lane для route/purity regression, но они не переводятся в активные family автоматически:
|
||||
|
||||
- `settlements_60_62` (control lane)
|
||||
- `month_close_indirect_costs` (control/sanity lane)
|
||||
|
||||
Назначение control lanes:
|
||||
|
||||
- проверка family isolation и wrong-route leakage;
|
||||
- проверка domain purity и live recipe binding на смежных формулировках;
|
||||
- защита от регрессий между core families.
|
||||
|
||||
## Правило внедрения (управление vs исполнение)
|
||||
|
||||
- Внедрение в проектный контур: **сразу все 3 family**.
|
||||
- Исполнение технических паков: **по одной family за раз**, с обязательным регрессом на остальные 2.
|
||||
- Контрольные lane обязательны в regression-наборе, но не считаются отдельной единицей family acceptance.
|
||||
|
||||
Это дает:
|
||||
|
||||
- единый язык управления Stage 4;
|
||||
- устойчивый технический темп без расползания контекста;
|
||||
- быструю локализацию регрессий.
|
||||
|
||||
## Официальная формулировка
|
||||
|
||||
`Stage 4 officially transitions from question-by-question debugging to family-based execution over source-to-proof contracts for P0 accounting domains.`
|
||||
|
||||
`Этап 4 официально переводится из режима точечной отладки отдельных вопросов в режим поочередного закрытия семейств бухгалтерских вопросов через source-to-proof контракты.`
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# STAGE_04_FAMILY_BACKLOG_2026-03-29
|
||||
|
||||
Статус: `WORKING_BACKLOG`
|
||||
Режим: `Stage 4 (P0-only)`
|
||||
Единица исполнения: `family pack`
|
||||
|
||||
## Контур
|
||||
|
||||
Этот backlog фиксирует Stage 4 не по отдельным вопросам, а по семействам с source-to-proof контрактом.
|
||||
|
||||
В этом backlog:
|
||||
|
||||
- `core families` = VAT / RBP / FA;
|
||||
- `control lanes` = settlements_60_62 / month_close_indirect_costs (только для regression/sanity, не как отдельные family acceptance).
|
||||
|
||||
## Навигация
|
||||
|
||||
- Family registry: [STAGE_04_FAMILY_REGISTRY_2026-03-29.json](X:\1C\NDC_1C\docs\accounting-assistant\accounting-assistant\03_execution\STAGE_04_FAMILY_REGISTRY_2026-03-29.json)
|
||||
- VAT family card: [Stage 4 - Family Card v1 — VAT chain.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 — VAT chain.md)
|
||||
- VAT latest run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\run_summary.json)
|
||||
- RBP family card: [Stage 4 - Family Card v1 RBP.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 RBP.md)
|
||||
- RBP latest run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\run_summary.json)
|
||||
- FA family card: [Stage 4 - Family Card v1 FA.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 FA.md)
|
||||
- FA latest acceptance run: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure\run_summary.json)
|
||||
- FA latest live replay: [fa_live_raw.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt\fa_live_raw.json)
|
||||
|
||||
## Control lanes (regression only)
|
||||
|
||||
1. `settlements_60_62`
|
||||
- Назначение: ловить wrong-claim leakage (settlement -> VAT/FA) и polarity regression.
|
||||
- Статус в Stage 4: `CONTROL_LANE_ONLY`.
|
||||
|
||||
2. `month_close_indirect_costs`
|
||||
- Назначение: sanity-check для period-close вопросов и cross-family contamination.
|
||||
- Статус в Stage 4: `CONTROL_LANE_ONLY`.
|
||||
|
||||
## Family 1 - НДС-цепочка (VAT chain)
|
||||
|
||||
- `current_status`: `PARTIALLY_WORKING / NON-BLOCKER`
|
||||
- `primary_gap`: residual admissibility/materialization quality (account scope mismatch, weak mapping noise)
|
||||
- `latest_pack`: `2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt`
|
||||
- `next_pack`: targeted live narrowing + reject cleanup для VAT proof-path
|
||||
- `acceptance_target`: устойчивый grounded-positive на VAT-вариациях при `false_grounded = 0`
|
||||
- `family_card`: [Stage 4 - Family Card v1 — VAT chain.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 — VAT chain.md)
|
||||
- `latest_acceptance_run`: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\run_summary.json)
|
||||
- `latest_live_replay`: [1_live_replay.txt](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\1_live_replay.txt)
|
||||
|
||||
Что уже работает:
|
||||
- positive path по VAT подтвержден в replay.
|
||||
- temporal/company-scope для live-кейсов стабилизирован в рамках Wave 19.2.
|
||||
|
||||
Что дочищаем:
|
||||
- снижение inadmissible residual noise;
|
||||
- повышение стабильности admissibility на смежных формулировках.
|
||||
|
||||
## Family 2 - РБП (RBP tail / write-off overstay)
|
||||
|
||||
- `current_status`: `ACCEPTED_WITH_LIMITATIONS`
|
||||
- `primary_gap`: source coverage (данные/источники для полного object-level proof)
|
||||
- `latest_pack`: `2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix`
|
||||
- `next_pack`: source coverage recovery + route tightening для полного claim closure
|
||||
- `acceptance_target`: non-zero admissible evidence + mechanism-specific вывод на core RBP вариациях
|
||||
- `family_card`: [Stage 4 - Family Card v1 RBP.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 RBP.md)
|
||||
- `latest_acceptance_run`: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\run_summary.json)
|
||||
- `latest_live_replay`: [1.txt](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\1.txt)
|
||||
|
||||
Что уже работает:
|
||||
- live route и evidence materialization подняты (`FIXED`).
|
||||
|
||||
Что не закрыто:
|
||||
- source coverage остается `NOT_FIXED`, поэтому часть кейсов ограничивается честно, но без полного proof closure.
|
||||
|
||||
## Family 3 - Амортизация ОС (FA amortization coverage)
|
||||
|
||||
- `current_status`: `PROOF_PATH_RAISED, LIVE_ACCEPTANCE_PENDING`
|
||||
- `primary_gap`: object-level relation clarity в live + production live acceptance
|
||||
- `latest_pack`: `2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure`
|
||||
- `next_pack`: live replay acceptance + relation/anchor consistency на вариациях
|
||||
- `acceptance_target`: доказуемая полнота/неполнота охвата ОС с явным expected vs actual set и `false_grounded = 0`
|
||||
- `family_card`: [Stage 4 - Family Card v1 FA.md](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 FA.md)
|
||||
- `latest_acceptance_run`: [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure\run_summary.json)
|
||||
- `latest_live_replay`: [fa_live_raw.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt\fa_live_raw.json)
|
||||
|
||||
Что уже работает:
|
||||
- claim-bound FA path, targeted checks, expected/actual set и relation-map вынесены в артефакты.
|
||||
- локальный pack достиг `FA_PACK_ACCEPTED` в controlled mock replay.
|
||||
|
||||
Что остается:
|
||||
- формальная live приемка family pack в рабочем канале (текущий live attempt: `http_status=400`).
|
||||
|
||||
## Сводная таблица family-level исполнения
|
||||
|
||||
| Family | Current status | Family card | Latest acceptance run | Latest live replay | Primary gap | Next pack focus |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| VAT chain | partially working | [VAT card](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 — VAT chain.md) | [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\run_summary.json) | [1_live_replay.txt](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt\1_live_replay.txt) | admissibility residual noise | live narrowing + reject cleanup |
|
||||
| RBP tail | accepted with limitations | [RBP card](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 RBP.md) | [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\run_summary.json) | [1.txt](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix\1.txt) | source coverage | source recovery + route tightening |
|
||||
| FA amortization | proof path raised, live pending | [FA card](X:\1C\NDC_1C\IN\Stage 4 - Family Card v1 FA.md) | [run_summary.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure\run_summary.json) | [fa_live_raw.json](X:\1C\NDC_1C\llm_normalizer\docs\runs\2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt\fa_live_raw.json) | live object-level closure | live replay acceptance + relation consistency |
|
||||
|
||||
## Правило исполнения (обязательно)
|
||||
|
||||
1. Новые задачи Stage 4 заводятся как family packs, не как одиночные вопросы.
|
||||
2. После каждого прогона обязательны run-артефакты в `llm_normalizer/docs/runs`.
|
||||
3. Приемка фиксируется на уровне family acceptance, а не по одному удачному ответу.
|
||||
4. Для каждой family в backlog обязательно должны быть заполнены: `family_card`, `latest_acceptance_run`, `latest_live_replay`.
|
||||
5. Control lanes обязательны в regression-наборе каждого pack, но не повышаются в first-class family без отдельного проектного решения.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"schema_version": "stage4_family_registry_v1",
|
||||
"updated_at": "2026-03-29",
|
||||
"stage_scope": "Stage 4 (P0-only)",
|
||||
"execution_unit": "family_pack",
|
||||
"acceptance_unit": "family_acceptance",
|
||||
"families": [
|
||||
{
|
||||
"family_id": "VAT_CHAIN_COMPLETENESS_V1",
|
||||
"family_name": "VAT chain",
|
||||
"status": "PARTIALLY_WORKING_NON_BLOCKER",
|
||||
"family_card": "X:/1C/NDC_1C/IN/Stage 4 - Family Card v1 — VAT chain.md",
|
||||
"latest_pack": "2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt",
|
||||
"latest_acceptance_run": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt/run_summary.json",
|
||||
"latest_live_replay": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_Wave_19_2_Live_Runtime_Fix_Replay_1txt/1_live_replay.txt",
|
||||
"primary_gap": "residual_admissibility_materialization_quality"
|
||||
},
|
||||
{
|
||||
"family_id": "RBP_TAIL_WRITEOFF_OVERSTAY_V1",
|
||||
"family_name": "RBP tail",
|
||||
"status": "ACCEPTED_WITH_LIMITATIONS",
|
||||
"family_card": "X:/1C/NDC_1C/IN/Stage 4 - Family Card v1 RBP.md",
|
||||
"latest_pack": "2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix",
|
||||
"latest_acceptance_run": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix/run_summary.json",
|
||||
"latest_live_replay": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_RBP_Pack_Live_Source_To_Proof_Fix/1.txt",
|
||||
"primary_gap": "source_coverage"
|
||||
},
|
||||
{
|
||||
"family_id": "FA_AMORTIZATION_COVERAGE_V1",
|
||||
"family_name": "FA amortization coverage",
|
||||
"status": "PROOF_PATH_RAISED_LIVE_ACCEPTANCE_PENDING",
|
||||
"family_card": "X:/1C/NDC_1C/IN/Stage 4 - Family Card v1 FA.md",
|
||||
"latest_pack": "2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure",
|
||||
"latest_acceptance_run": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure/run_summary.json",
|
||||
"latest_live_replay": "X:/1C/NDC_1C/llm_normalizer/docs/runs/2026-03-29_Stage_04_FA_Pack_Amortization_Proof_Closure_live_attempt/fa_live_raw.json",
|
||||
"primary_gap": "live_object_level_relation_clarity_and_live_acceptance"
|
||||
}
|
||||
],
|
||||
"rollout_policy": {
|
||||
"project_adoption": "all_three_families_immediately",
|
||||
"technical_execution": "one_family_per_pack_with_full_cross_family_regression",
|
||||
"hard_rule": "false_grounded_answer_rate_must_remain_zero"
|
||||
}
|
||||
}
|
||||
|
|
@ -57,32 +57,87 @@ function shiftDays(iso, deltaDays) {
|
|||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||||
return formatDate(date);
|
||||
}
|
||||
function accountPrefix(value) {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function accountPrefixesFromAnchors(anchors) {
|
||||
const prefixes = new Set();
|
||||
const accounts = Array.isArray(anchors?.accounts) ? anchors.accounts : [];
|
||||
for (const item of accounts) {
|
||||
const prefix = accountPrefix(String(item ?? ""));
|
||||
if (prefix) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
}
|
||||
return prefixes;
|
||||
}
|
||||
function inferClaimType(input) {
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const isVat = input.focusDomainHint === "vat_document_register_book" ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|register|книга покупок|книга продаж)/i.test(lower);
|
||||
if (isVat) {
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
const hasSettlementAccount = ["51", "60", "62", "76"].some((item) => accountPrefixes.has(item));
|
||||
const hasVatAccount = ["19", "68"].some((item) => accountPrefixes.has(item));
|
||||
const hasFixedAssetAccount = ["01", "02", "08"].some((item) => accountPrefixes.has(item));
|
||||
const hasRbpAccount = accountPrefixes.has("97");
|
||||
const hasMonthCloseAccount = ["20", "21", "23", "25", "26", "28", "29", "44"].some((item) => accountPrefixes.has(item));
|
||||
const hasAdvanceSignal = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower);
|
||||
const hasSettlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|плате[жж]|платёж|постав|покупател|settlement|payment|supplier|customer)/i.test(lower);
|
||||
const hasVatLexical = /(?:\bvat\b|ндс|invoice|сч[её]т[- ]?фактур|register|книга\s+покупок|книга\s+продаж|книг[аи]\s+(?:покуп|продаж))/i.test(lower);
|
||||
const hasFixedAssetLexical = /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(lower);
|
||||
const hasRbpLexical = /(?:\brbp\b|рбп|deferred\s*expense|writeoff|расходы\s+будущих\s+периодов|списани[ея]\s+рбп|account\s*97|сч[её]т\s*97)/i.test(lower);
|
||||
const hasMonthCloseLexical = /(?:month[- ]?close|закрыт|закрытие\s+месяца|косвен|account\s*20|account\s*44|сч[её]т\s*20|сч[её]т\s*44|распределен|period\s*close)/i.test(lower);
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
return hasAdvanceSignal ? "prove_advance_offset_state" : "prove_settlement_closure_state";
|
||||
}
|
||||
if (input.focusDomainHint === "vat_document_register_book") {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
const isRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred expense|writeoff)/i.test(lower);
|
||||
if (isRbp) {
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
const isMonthClose = input.focusDomainHint === "month_close_costs_20_44" ||
|
||||
/(?:month[- ]?close|закрыт|косвен|account\s*20|account\s*44|счет\s*20|счет\s*44)/i.test(lower);
|
||||
if (isMonthClose) {
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
const isAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower);
|
||||
if (isAdvance) {
|
||||
const settlementPriority = (hasSettlementLexical || hasSettlementAccount || hasAdvanceSignal) && !hasVatLexical && !hasFixedAssetLexical;
|
||||
const broadMonthClosePriority = (hasMonthCloseLexical || hasMonthCloseAccount) &&
|
||||
!hasVatLexical &&
|
||||
!hasVatAccount &&
|
||||
!hasFixedAssetLexical &&
|
||||
!hasFixedAssetAccount;
|
||||
if (hasAdvanceSignal && settlementPriority) {
|
||||
return "prove_advance_offset_state";
|
||||
}
|
||||
if (settlementPriority) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
if (hasVatLexical || (hasVatAccount && !settlementPriority)) {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
if (broadMonthClosePriority) {
|
||||
return hasRbpLexical || hasRbpAccount ? "prove_rbp_tail_state" : "prove_month_close_state";
|
||||
}
|
||||
if (hasFixedAssetLexical || (hasFixedAssetAccount && !settlementPriority && !hasVatLexical)) {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
if (hasMonthCloseLexical || hasMonthCloseAccount) {
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
if (hasSettlementLexical || hasSettlementAccount) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
function inferCounterpartyScope(message) {
|
||||
const lower = message.toLowerCase();
|
||||
const out = [];
|
||||
if (/(?:supplier|vendor|поставщик)/i.test(lower))
|
||||
if (/(?:supplier|vendor|поставщик|кредитор)/i.test(lower))
|
||||
out.push("supplier");
|
||||
if (/(?:customer|buyer|покупатель|дебитор)/i.test(lower))
|
||||
out.push("customer");
|
||||
|
|
@ -91,13 +146,37 @@ function inferCounterpartyScope(message) {
|
|||
function detectSignals(message) {
|
||||
const lower = message.toLowerCase();
|
||||
return {
|
||||
hasAdvance: /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зачет)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|счет[- ]фактур|invoice|книга покупок|книга продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие месяца|косвен|20\/44|account 20|account 44|счет 20|счет 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|счет 97|writeoff|списани)/i.test(lower)
|
||||
hasAdvance: /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зач[её]т)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|сч[её]т[- ]?фактур|invoice|книга\s+покупок|книга\s+продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие\s+месяца|косвен|20\/44|account 20|account 44|сч[её]т 20|сч[её]т 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|сч[её]т 97|writeoff|списани)/i.test(lower),
|
||||
hasFixedAsset: /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(lower)
|
||||
};
|
||||
}
|
||||
function resolveSettlementRole(input) {
|
||||
if (input.claimType !== "prove_settlement_closure_state" && input.claimType !== "prove_advance_offset_state") {
|
||||
return undefined;
|
||||
}
|
||||
const scopes = new Set(input.counterpartyScope.map((item) => String(item ?? "").trim().toLowerCase()));
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const hasSupplierLexical = /(?:supplier|vendor|поставщ|кредитор|обязательств|payable)/i.test(lower);
|
||||
const hasCustomerLexical = /(?:customer|buyer|покупат|дебитор|receivable)/i.test(lower);
|
||||
const hasSupplierAccount = input.accountPrefixes.has("60");
|
||||
const hasCustomerAccount = input.accountPrefixes.has("62");
|
||||
const supplierSignal = scopes.has("supplier") || hasSupplierLexical || (hasSupplierAccount && !hasCustomerAccount);
|
||||
const customerSignal = scopes.has("customer") || hasCustomerLexical || (hasCustomerAccount && !hasSupplierAccount);
|
||||
if (supplierSignal && !customerSignal) {
|
||||
return "supplier";
|
||||
}
|
||||
if (customerSignal && !supplierSignal) {
|
||||
return "customer";
|
||||
}
|
||||
if (supplierSignal && customerSignal) {
|
||||
return "mixed";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
function mergeAnchors(anchors, key) {
|
||||
return uniqueStrings(Array.isArray(anchors?.[key]) ? anchors?.[key] : []);
|
||||
}
|
||||
|
|
@ -131,6 +210,22 @@ function missingFromRequired(required, resolved) {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "amount_or_document") {
|
||||
const hasAmount = (resolved.amounts?.length ?? 0) > 0;
|
||||
const hasDoc = (resolved.document_numbers?.length ?? 0) > 0 || (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAmount && !hasDoc) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "account_scope_or_document_type") {
|
||||
const hasAccount = (resolved.account_scope?.length ?? 0) > 0;
|
||||
const hasDocType = (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAccount && !hasDocType) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((resolved[anchor]?.length ?? 0) <= 0) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
|
|
@ -140,9 +235,27 @@ function missingFromRequired(required, resolved) {
|
|||
function resolveClaimBoundAnchors(input) {
|
||||
const claimType = inferClaimType({
|
||||
userMessage: input.userMessage,
|
||||
focusDomainHint: input.focusDomainHint
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
companyAnchors: input.companyAnchors
|
||||
});
|
||||
const signals = detectSignals(input.userMessage);
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
const includeVatAnchors = claimType === "prove_vat_chain_completeness";
|
||||
const includeMonthCloseAnchors = claimType === "prove_month_close_state";
|
||||
const includeRbpAnchors = claimType === "prove_rbp_tail_state";
|
||||
const includeFixedAssetAnchors = claimType === "prove_fixed_asset_amortization_coverage";
|
||||
const hasVatSignal = signals.hasVat || accountPrefixes.has("19") || accountPrefixes.has("68");
|
||||
const hasRbpSignal = signals.hasRbp || accountPrefixes.has("97");
|
||||
const hasFixedAssetSignal = signals.hasFixedAsset || accountPrefixes.has("01") || accountPrefixes.has("02") || accountPrefixes.has("08");
|
||||
const hasMonthCloseSignal = signals.hasMonthClose ||
|
||||
accountPrefixes.has("20") ||
|
||||
accountPrefixes.has("21") ||
|
||||
accountPrefixes.has("23") ||
|
||||
accountPrefixes.has("25") ||
|
||||
accountPrefixes.has("26") ||
|
||||
accountPrefixes.has("28") ||
|
||||
accountPrefixes.has("29") ||
|
||||
accountPrefixes.has("44");
|
||||
const resolvedAnchors = {
|
||||
period: uniqueStrings([...mergeAnchors(input.companyAnchors, "periods"), ...mergeAnchors(input.companyAnchors, "dates")]),
|
||||
account_scope: mergeAnchors(input.companyAnchors, "accounts"),
|
||||
|
|
@ -153,16 +266,26 @@ function resolveClaimBoundAnchors(input) {
|
|||
counterparty_scope: inferCounterpartyScope(input.userMessage),
|
||||
advance_signal: signals.hasAdvance ? ["advance"] : [],
|
||||
closure_signal: signals.hasClosure ? ["closure"] : [],
|
||||
vat_signal: signals.hasVat ? ["vat"] : [],
|
||||
chain_signal: signals.hasVat ? ["chain"] : [],
|
||||
close_signal: signals.hasMonthClose ? ["month_close"] : [],
|
||||
vat_signal: includeVatAnchors && hasVatSignal ? ["vat"] : [],
|
||||
chain_signal: includeVatAnchors && hasVatSignal ? ["chain"] : [],
|
||||
close_signal: includeMonthCloseAnchors && hasMonthCloseSignal ? ["month_close"] : [],
|
||||
cost_scope: [],
|
||||
rbp_signal: signals.hasRbp ? ["rbp"] : [],
|
||||
writeoff_signal: signals.hasRbp ? ["writeoff"] : []
|
||||
rbp_signal: includeRbpAnchors && hasRbpSignal ? ["rbp"] : [],
|
||||
writeoff_signal: includeRbpAnchors && hasRbpSignal ? ["writeoff"] : [],
|
||||
fixed_asset_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["fixed_asset"] : [],
|
||||
amortization_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["amortization"] : [],
|
||||
expected_fa_set: [],
|
||||
actual_fa_set: []
|
||||
};
|
||||
if (/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || signals.hasMonthClose) {
|
||||
if (includeMonthCloseAnchors &&
|
||||
(/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || hasMonthCloseSignal)) {
|
||||
resolvedAnchors.cost_scope = ["20_44"];
|
||||
}
|
||||
// For FA amortization claims, document type is implicit in user intent
|
||||
// even when the phrase does not carry explicit document keywords.
|
||||
if (includeFixedAssetAnchors && hasFixedAssetSignal && (resolvedAnchors.document_types?.length ?? 0) <= 0) {
|
||||
resolvedAnchors.document_types = ["amortization_document"];
|
||||
}
|
||||
if (input.primaryPeriod) {
|
||||
resolvedAnchors.period = uniqueStrings([...(resolvedAnchors.period ?? []), input.primaryPeriod.from, input.primaryPeriod.to]);
|
||||
}
|
||||
|
|
@ -171,7 +294,14 @@ function resolveClaimBoundAnchors(input) {
|
|||
prove_advance_offset_state: ["period", "account_scope", "advance_signal", "settlement_object"],
|
||||
prove_vat_chain_completeness: ["period", "document_types", "vat_signal", "chain_signal"],
|
||||
prove_month_close_state: ["period", "close_signal", "cost_scope"],
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"]
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"],
|
||||
prove_fixed_asset_amortization_coverage: [
|
||||
"period",
|
||||
"fixed_asset_signal",
|
||||
"amortization_signal",
|
||||
"amount_or_document",
|
||||
"account_scope_or_document_type"
|
||||
]
|
||||
};
|
||||
const requiredAnchors = requiredByClaim[claimType];
|
||||
const missingAnchors = missingFromRequired(requiredAnchors, resolvedAnchors);
|
||||
|
|
@ -189,8 +319,19 @@ function resolveClaimBoundAnchors(input) {
|
|||
if (!allowedContextWindow && input.primaryPeriod) {
|
||||
reasonCodes.push("controlled_temporal_expansion_window_unavailable");
|
||||
}
|
||||
const settlementRole = resolveSettlementRole({
|
||||
claimType,
|
||||
counterpartyScope: resolvedAnchors.counterparty_scope ?? [],
|
||||
accountPrefixes,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
if ((claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state") &&
|
||||
(settlementRole === "mixed" || settlementRole === "unknown")) {
|
||||
reasonCodes.push("unresolved_supplier_customer_polarity");
|
||||
}
|
||||
return {
|
||||
claim_type: claimType,
|
||||
settlement_role: settlementRole,
|
||||
required_anchors: requiredAnchors,
|
||||
resolved_anchors: resolvedAnchors,
|
||||
missing_anchors: missingAnchors,
|
||||
|
|
@ -217,7 +358,13 @@ function buildCorpusFromItem(item) {
|
|||
document_context: item.document_context,
|
||||
relation_pattern_hits: item.relation_pattern_hits,
|
||||
graph_domain_scope: item.graph_domain_scope,
|
||||
lifecycle_markers: item.lifecycle_markers
|
||||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}).toLowerCase();
|
||||
}
|
||||
function buildCorpusFromEvidence(evidence) {
|
||||
|
|
@ -256,6 +403,16 @@ function requiredChecksByClaim(claimType) {
|
|||
if (claimType === "prove_month_close_state") {
|
||||
return ["close_operation_found", "distribution_step_found", "residual_tail_found"];
|
||||
}
|
||||
if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
return [
|
||||
"amortization_document_found",
|
||||
"fixed_asset_object_identified",
|
||||
"expected_fa_set_reconstructed",
|
||||
"actual_fa_set_reconstructed",
|
||||
"movement_or_posting_link_found",
|
||||
"missing_fa_candidates_assessed"
|
||||
];
|
||||
}
|
||||
return [
|
||||
"rbp_writeoff_document_found",
|
||||
"rbp_object_identified",
|
||||
|
|
@ -273,21 +430,26 @@ function detectChecksForCorpus(corpus, claimType, anchors) {
|
|||
const hasSettlementAccount = /(?:\b60(?:\.\d{2})?\b|\b62(?:\.\d{2})?\b|payable|receivable|settlement)/i.test(corpus);
|
||||
const hasPosting = /(?:document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasRegister = /(?:register|accumulationregister|accountingregister|регистр)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зачет|tail|хвост)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зач[её]т|tail|хвост)/i.test(corpus);
|
||||
const hasPayment = /(?:payment|оплат|списаниесрасчетногосчета|payment_order|bank_statement)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|счет[- ]фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книгипокупок|книгипродаж|book)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|сч[её]т[- ]?фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книг[аи](?:\s+)?(?:покупок|продаж)|book)/i.test(corpus);
|
||||
const hasChain = /(?:chain|link|document_to_posting|invoice_to_vat|связ)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие\s+месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasDistribution = /(?:distribution|распредел|writeoff|deferred_expense_to_writeoff)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|сч[её]т\s*97|deferred)/i.test(corpus);
|
||||
const hasResidual = /(?:tail|остат|незакры|overdue|period_boundary|terminal_state_gap)/i.test(corpus);
|
||||
const hasContradiction = /(?:contradiction|invalid_transition|normal residual|нормальн)/i.test(corpus);
|
||||
const hasRbpWriteoffDoc = /(?:списани[ея]\s+рбп|rbp_writeoff|deferred_expense_document|writeoff document)/i.test(corpus);
|
||||
const hasRbpObject = /(?:rbp[_\s-]?object|объект\s+рбп|analytics|subkonto|расходыбудущихпериодов)/i.test(corpus);
|
||||
const hasMovement = /(?:movement|движен|хозрасчетный|document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasPeriodEndResidual = /(?:period_boundary|end_period|2020-07-31|остат)/i.test(corpus);
|
||||
const hasFixedAsset = /(?:fixed_asset|asset_card|объект\s+ос|основн(?:ые|ых)?\s+сред|depreciat|амортиз|account[:\s]*0[12]|\b0[12](?:\.\d{2})?\b)/i.test(corpus);
|
||||
const hasAmortizationDoc = /(?:depreciat|amortization|начислен[а-я]*\s+амортиз|документ\s+амортиз)/i.test(corpus);
|
||||
const hasExpectedFaSet = /(?:expected_fa_set|expected[_\s-]?set|find_fixed_asset_cards_expected_for_period|expected_set_seed|fa_expected_set_candidate)/i.test(corpus);
|
||||
const hasActualFaSet = /(?:actual_fa_set|find_fixed_asset_movements_accounts_01_02|fa_actual_set_candidate|seed_amortization_documents|collect_fa_object_movements)/i.test(corpus);
|
||||
const hasFaCoverageCompare = /(?:expected_vs_actual|compare_expected_vs_actual|missing_fa|coverage_compare|missing_fa_candidates)/i.test(corpus);
|
||||
if (claimType === "prove_settlement_closure_state") {
|
||||
if (hasPayment)
|
||||
checks.add("payment_document_found");
|
||||
|
|
@ -319,7 +481,7 @@ function detectChecksForCorpus(corpus, claimType, anchors) {
|
|||
else if (claimType === "prove_vat_chain_completeness") {
|
||||
if (/(?:document|receipt|realization|поступлен|реализац)/i.test(corpus))
|
||||
checks.add("source_document_found");
|
||||
if (/(?:invoice|счет[- ]фактур)/i.test(corpus))
|
||||
if (/(?:invoice|сч[её]т[- ]?фактур)/i.test(corpus))
|
||||
checks.add("invoice_found");
|
||||
if (hasRegister || hasVat)
|
||||
checks.add("tax_register_entry_found");
|
||||
|
|
@ -336,6 +498,20 @@ function detectChecksForCorpus(corpus, claimType, anchors) {
|
|||
if (hasResidual)
|
||||
checks.add("residual_tail_found");
|
||||
}
|
||||
else if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
if (hasAmortizationDoc)
|
||||
checks.add("amortization_document_found");
|
||||
if (hasFixedAsset)
|
||||
checks.add("fixed_asset_object_identified");
|
||||
if (hasExpectedFaSet)
|
||||
checks.add("expected_fa_set_reconstructed");
|
||||
if (hasActualFaSet || hasAmortizationDoc)
|
||||
checks.add("actual_fa_set_reconstructed");
|
||||
if (hasMovement || hasPosting)
|
||||
checks.add("movement_or_posting_link_found");
|
||||
if (hasFaCoverageCompare || (hasExpectedFaSet && hasActualFaSet))
|
||||
checks.add("missing_fa_candidates_assessed");
|
||||
}
|
||||
else {
|
||||
if (hasRbpWriteoffDoc || (hasRbp && hasDistribution))
|
||||
checks.add("rbp_writeoff_document_found");
|
||||
|
|
@ -479,7 +655,11 @@ function buildDerivedEvidenceFromItem(input) {
|
|||
account_context: Array.isArray(input.item.account_context) ? input.item.account_context : [],
|
||||
account_debit: input.item.account_debit ?? null,
|
||||
account_credit: input.item.account_credit ?? null,
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : []
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : [],
|
||||
fa_object_hint: String(input.item.fa_object_hint ?? "").trim() || null,
|
||||
fa_expected_set_candidate: Boolean(input.item.fa_expected_set_candidate),
|
||||
fa_actual_set_candidate: Boolean(input.item.fa_actual_set_candidate),
|
||||
fa_coverage_status: String(input.item.fa_coverage_status ?? "").trim() || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -490,6 +670,149 @@ function buildClaimStatusTemplate(requiredChecks) {
|
|||
}
|
||||
return out;
|
||||
}
|
||||
function normalizeFaObjectToken(value) {
|
||||
const normalized = String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/^live movement row #\d+$/i.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized.slice(0, 140);
|
||||
}
|
||||
function periodFromEvidence(evidence) {
|
||||
const payload = toObject(evidence.payload);
|
||||
return (String(evidence.source_ref?.period ?? "").trim() ||
|
||||
String(evidence.pointer?.source?.period ?? "").trim() ||
|
||||
String(payload?.period ?? "").trim() ||
|
||||
null);
|
||||
}
|
||||
function collectFaCoverage(input) {
|
||||
const state = new Map();
|
||||
const touch = (objectName) => {
|
||||
const key = objectName.toLowerCase();
|
||||
const existing = state.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created = {
|
||||
expected: false,
|
||||
actual: false,
|
||||
movement: false,
|
||||
posting: false,
|
||||
docs: new Set(),
|
||||
periods: new Set()
|
||||
};
|
||||
state.set(key, created);
|
||||
return created;
|
||||
};
|
||||
for (const result of input.retrievalResults) {
|
||||
const items = Array.isArray(result.items) ? result.items : [];
|
||||
for (const item of items) {
|
||||
const objectToken = normalizeFaObjectToken(String(item.fa_object_hint ?? item.display_name ?? item.source_id ?? "").trim());
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(item.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(item.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify(item).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(item.document_context) ? item.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = String(item.period ?? item.Period ?? "").trim();
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
for (const evidenceItem of evidence) {
|
||||
const payload = toObject(evidenceItem.payload) ?? {};
|
||||
const objectToken = normalizeFaObjectToken(String(payload.fa_object_hint ?? evidenceItem.source_ref?.id ?? evidenceItem.pointer?.source?.id ?? "").trim());
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(payload.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(payload.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify({
|
||||
payload,
|
||||
mechanism_note: evidenceItem.mechanism_note,
|
||||
source_ref: evidenceItem.source_ref
|
||||
}).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(payload.document_context) ? payload.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = periodFromEvidence(evidenceItem);
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
}
|
||||
const entries = Array.from(state.entries());
|
||||
const expectedSet = entries
|
||||
.filter(([, slot]) => slot.expected)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const actualSet = entries
|
||||
.filter(([, slot]) => slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const expectedResolved = expectedSet.length > 0 ? expectedSet : actualSet;
|
||||
const missingCandidates = expectedResolved.filter((item) => !actualSet.includes(item)).slice(0, 32);
|
||||
const uncertainCandidates = entries
|
||||
.filter(([, slot]) => !slot.expected && !slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const relationMap = entries.slice(0, 48).map(([objectName, slot]) => {
|
||||
const coverageStatus = slot.expected && slot.actual ? "covered" : slot.expected && !slot.actual ? "missing" : "uncertain";
|
||||
return {
|
||||
fa_object: objectName,
|
||||
document_amortization: Array.from(slot.docs).slice(0, 4),
|
||||
movement: slot.movement,
|
||||
posting: slot.posting,
|
||||
period: Array.from(slot.periods).slice(0, 4),
|
||||
coverage_status: coverageStatus
|
||||
};
|
||||
});
|
||||
return {
|
||||
expectedSet: expectedResolved,
|
||||
actualSet,
|
||||
missingCandidates,
|
||||
uncertainCandidates,
|
||||
relationMap
|
||||
};
|
||||
}
|
||||
function applyTargetedEvidenceAcquisition(input) {
|
||||
const requiredChecks = requiredChecksByClaim(input.claimAudit.claim_type);
|
||||
const checkStatus = buildClaimStatusTemplate(requiredChecks);
|
||||
|
|
@ -600,6 +923,19 @@ function applyTargetedEvidenceAcquisition(input) {
|
|||
if (targetedEvidenceHitRate < 0.8) {
|
||||
reasonCodes.push("targeted_evidence_hit_rate_low");
|
||||
}
|
||||
const faCoverage = input.claimAudit.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? collectFaCoverage({
|
||||
retrievalResults: adjustedResults
|
||||
})
|
||||
: null;
|
||||
if (faCoverage) {
|
||||
if (faCoverage.expectedSet.length <= 0) {
|
||||
reasonCodes.push("fa_expected_set_not_reconstructed");
|
||||
}
|
||||
if (faCoverage.actualSet.length <= 0) {
|
||||
reasonCodes.push("fa_actual_set_not_reconstructed");
|
||||
}
|
||||
}
|
||||
return {
|
||||
retrievalResults: adjustedResults,
|
||||
audit: {
|
||||
|
|
@ -610,6 +946,15 @@ function applyTargetedEvidenceAcquisition(input) {
|
|||
targeted_evidence_hits: targetedEvidenceHits,
|
||||
targeted_evidence_hit_rate: targetedEvidenceHitRate,
|
||||
targeted_evidence_source_refs: Array.from(sourceRefs).slice(0, 24),
|
||||
...(faCoverage
|
||||
? {
|
||||
fa_expected_set: faCoverage.expectedSet,
|
||||
fa_actual_set_from_amortization: faCoverage.actualSet,
|
||||
fa_missing_candidates: faCoverage.missingCandidates,
|
||||
fa_uncertain_candidates: faCoverage.uncertainCandidates,
|
||||
fa_relation_map: faCoverage.relationMap
|
||||
}
|
||||
: {}),
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ exports.AssistantDataLayer = void 0;
|
|||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const config_1 = require("../config");
|
||||
const investigationState_1 = require("./investigationState");
|
||||
const BROAD_GENERIC_MARKERS = new RegExp([
|
||||
"\\boverall\\b",
|
||||
"\\bgeneral\\b",
|
||||
|
|
@ -87,6 +88,20 @@ const RBP_REQUIRED_LIVE_CALLS = [
|
|||
"find_month_close_entries_linked_to_rbp",
|
||||
"compute_end_period_residual_by_rbp_object"
|
||||
];
|
||||
const VAT_REQUIRED_LIVE_CALLS = [
|
||||
"find_vat_source_documents_in_period",
|
||||
"find_vat_invoice_links_in_period",
|
||||
"find_vat_register_entries_in_period",
|
||||
"find_vat_book_entries_in_period"
|
||||
];
|
||||
const FA_REQUIRED_LIVE_CALLS = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
const CLAIM_BOUND_PRIMARY_LIVE_LIMIT = Math.max(config_1.ASSISTANT_MCP_LIVE_LIMIT, 96);
|
||||
const CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT = Math.max(config_1.ASSISTANT_MCP_LIVE_LIMIT, 128);
|
||||
function pushUniqueLine(target, line) {
|
||||
if (!target.includes(line)) {
|
||||
target.push(line);
|
||||
|
|
@ -114,6 +129,12 @@ function parseFiniteNumber(value) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function resolveLiveCallLimit(limit) {
|
||||
if (typeof limit === "number" && Number.isFinite(limit)) {
|
||||
return Math.max(1, Math.trunc(limit));
|
||||
}
|
||||
return config_1.ASSISTANT_MCP_LIVE_LIMIT;
|
||||
}
|
||||
function formatIsoDateUtc(date) {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
|
|
@ -174,9 +195,111 @@ function buildLiveRangeQuery(fromIso, toIso, limit) {
|
|||
function hasRbpSignal(text) {
|
||||
return /(?:\brbp\b|рбп|расходы\s+будущих\s+периодов|deferred|writeoff|списани[ея]\s+рбп|account\s*97|счет\s*97)/i.test(String(text ?? "").toLowerCase());
|
||||
}
|
||||
function hasFixedAssetAmortizationSignal(text) {
|
||||
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(String(text ?? "").toLowerCase());
|
||||
}
|
||||
function buildLiveMcpCallPlan(route, fragmentText) {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const rbpClaim = hasRbpSignal(fragmentText) ||
|
||||
const preferredDomainHint = (0, investigationState_1.inferP0DomainFromMessage)(fragmentText);
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
const faClaim = preferredDomainHint === "fixed_asset_amortization" ||
|
||||
hasFixedAssetAmortizationSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
|
||||
semanticProfile.domain_scope.includes("fixed_assets");
|
||||
if (faClaim) {
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
query_subject: "fixed_asset_amortization_coverage",
|
||||
required_live_calls: [...FA_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_amortization_documents_in_period",
|
||||
purpose: "seed_amortization_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_movements_accounts_01_02",
|
||||
purpose: "collect_fa_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_cards_expected_for_period",
|
||||
purpose: "build_expected_fa_set",
|
||||
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "match_expected_vs_actual_fa_coverage",
|
||||
purpose: "compare_expected_vs_actual_fa_coverage",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
const vatClaim = preferredDomainHint === "vat_document_register_book" ||
|
||||
semanticProfile.query_subject === "vat_chain_conflict" ||
|
||||
semanticProfile.domain_scope.includes("vat") ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|книга покупок|книга продаж|register)/i.test(String(fragmentText ?? "").toLowerCase());
|
||||
if (vatClaim) {
|
||||
return {
|
||||
claim_type: "prove_vat_chain_completeness",
|
||||
query_subject: "vat_chain_conflict",
|
||||
required_live_calls: [...VAT_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_vat_source_documents_in_period",
|
||||
purpose: "seed_vat_source_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_invoice_links_in_period",
|
||||
purpose: "collect_invoice_links",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_register_entries_in_period",
|
||||
purpose: "collect_vat_register_entries",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_book_entries_in_period",
|
||||
purpose: "collect_vat_book_entries",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
const rbpClaim = (preferredDomainHint === "month_close_costs_20_44" && hasRbpSignal(fragmentText)) ||
|
||||
hasRbpSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "deferred_expense_lifecycle_anomaly" ||
|
||||
semanticProfile.domain_scope.includes("deferred_expense");
|
||||
if (!rbpClaim) {
|
||||
|
|
@ -195,11 +318,6 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
|||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
return {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
query_subject: "deferred_expense_lifecycle_anomaly",
|
||||
|
|
@ -208,28 +326,32 @@ function buildLiveMcpCallPlan(route, fragmentText) {
|
|||
{
|
||||
call_id: "find_rbp_writeoff_documents_in_period",
|
||||
purpose: "seed_writeoff_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "find_rbp_object_movements_account_97",
|
||||
purpose: "collect_rbp_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97"]
|
||||
},
|
||||
{
|
||||
call_id: "find_month_close_entries_linked_to_rbp",
|
||||
purpose: "link_month_close_to_rbp",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "compute_end_period_residual_by_rbp_object",
|
||||
purpose: "collect_residual_tail_signals",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
}
|
||||
|
|
@ -1205,11 +1327,26 @@ function inferPeriodScope(fragmentText) {
|
|||
}
|
||||
const WRONG_DOCUMENT_MARKERS = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u043e\u043c)?|\u043d\u0435\s+\u0432\s+\u0442\u043e\u0442|wrong\s+document|wrong_document_type)/iu;
|
||||
const REPEATED_ANOMALY_MARKERS = /(?:\u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449|\u0441\u0435\u0440\u0438\u0439\u043d|\u043f\u0430\u0442\u0442\u0435\u0440\u043d|repeat(?:ed|ability)?)/iu;
|
||||
function inferQuerySubject(text, domains, anomalies) {
|
||||
function inferQuerySubject(text, domains, anomalies, preferredDomainHint) {
|
||||
if (preferredDomainHint === "vat_document_register_book") {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (preferredDomainHint === "fixed_asset_amortization") {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (preferredDomainHint === "month_close_costs_20_44") {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
if (preferredDomainHint === "settlements_60_62") {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
const lower = text.toLowerCase();
|
||||
if ((domains.includes("bank") || domains.includes("settlements")) && WRONG_DOCUMENT_MARKERS.test(lower)) {
|
||||
return "bank_settlement_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("suppliers")) {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
|
|
@ -1222,9 +1359,6 @@ function inferQuerySubject(text, domains, anomalies) {
|
|||
if (domains.includes("fixed_assets")) {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("period_close")) {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
|
|
@ -1356,8 +1490,9 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
|||
relationPatterns: dedupedRelations,
|
||||
anomalyPatterns: dedupedAnomalies
|
||||
});
|
||||
const preferredDomainHint = (0, investigationState_1.inferP0DomainFromMessage)(fragmentText);
|
||||
return {
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies),
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies, preferredDomainHint),
|
||||
account_scope: dedupedAccounts,
|
||||
subaccount_scope: [],
|
||||
domain_scope: dedupedDomains,
|
||||
|
|
@ -2170,10 +2305,14 @@ class AssistantDataLayer {
|
|||
const endpoint = this.buildMcpUrl("/api/execute_query");
|
||||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||
const accountScope = explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
const accountScope = livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? ["01", "02", "08"]
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? ["19", "68"]
|
||||
: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? ["97", "20", "25", "26", "44"]
|
||||
: explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
: [];
|
||||
const callExecutions = [];
|
||||
const collectedRows = [];
|
||||
|
|
@ -2181,13 +2320,14 @@ class AssistantDataLayer {
|
|||
let fetchedRowsTotal = 0;
|
||||
let matchedRowsTotal = 0;
|
||||
for (const call of livePlan.calls) {
|
||||
const callLimit = resolveLiveCallLimit(call.limit);
|
||||
const callAccountScope = Array.isArray(call.account_scope_override) && call.account_scope_override.length > 0
|
||||
? call.account_scope_override
|
||||
: accountScope;
|
||||
try {
|
||||
const payload = await this.fetchJsonWithTimeout(endpoint, {
|
||||
query: call.query,
|
||||
limit: config_1.ASSISTANT_MCP_LIVE_LIMIT
|
||||
limit: callLimit
|
||||
});
|
||||
const parsed = this.parseExecuteQueryPayload(payload);
|
||||
if (parsed.error) {
|
||||
|
|
@ -2195,6 +2335,7 @@ class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
|
|
@ -2221,6 +2362,7 @@ class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: rowsForAnswer.length > 0 ? "ok" : "empty",
|
||||
fetched_rows: parsed.rows.length,
|
||||
|
|
@ -2235,6 +2377,7 @@ class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
|
|
@ -2258,7 +2401,11 @@ class AssistantDataLayer {
|
|||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
claim_type: item.claim_type
|
||||
claim_type: item.claim_type,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}));
|
||||
const executedRequiredCalls = callExecutions
|
||||
.filter((item) => item.required_for_claim && item.status !== "error")
|
||||
|
|
@ -2303,7 +2450,13 @@ class AssistantDataLayer {
|
|||
route,
|
||||
channel: config_1.ASSISTANT_MCP_CHANNEL,
|
||||
proxy: config_1.ASSISTANT_MCP_PROXY_URL,
|
||||
source_profile: livePlan.claim_type ? "claim_bound_rbp_live_path" : "generic_live_probe",
|
||||
source_profile: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? "claim_bound_rbp_live_path"
|
||||
: livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? "claim_bound_fa_live_path"
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? "claim_bound_vat_live_path"
|
||||
: "generic_live_probe",
|
||||
claim_type: livePlan.claim_type,
|
||||
query_subject: livePlan.query_subject,
|
||||
account_scope: accountScope,
|
||||
|
|
@ -2484,28 +2637,53 @@ class AssistantDataLayer {
|
|||
const querySubject = valueAsString(row.__query_subject ?? "").trim() || null;
|
||||
const registratorLower = registrator.toLowerCase();
|
||||
const hasRbpByDocument = /(?:рбп|deferred|списани[ея]\s+рбп)/i.test(registratorLower);
|
||||
const hasFaByDocument = /(?:амортиз|depreciat|основн(?:ые|ых)\s+сред|fixed\s*asset)/i.test(registratorLower);
|
||||
const hasAccount97 = accountContext.some((item) => /^97(?:\.|$)/.test(item));
|
||||
const hasFixedAssetAccount = accountContext.some((item) => /^(?:01|02|08)(?:\.|$)/.test(item));
|
||||
const hasCloseDoc = /(?:закрыти[ея]\s+месяц|period\s*close|month\s*close|close\s+operation)/i.test(registratorLower) ||
|
||||
callId.includes("month_close");
|
||||
const faExpectedSetCandidate = callId === "find_fixed_asset_cards_expected_for_period" || callPurpose === "build_expected_fa_set";
|
||||
const faActualSetCandidate = callId === "find_amortization_documents_in_period" ||
|
||||
callId === "find_fixed_asset_movements_accounts_01_02" ||
|
||||
callPurpose === "seed_amortization_documents" ||
|
||||
callPurpose === "collect_fa_object_movements";
|
||||
const faCoverageStatus = callId === "match_expected_vs_actual_fa_coverage"
|
||||
? "expected_vs_actual_compare"
|
||||
: faExpectedSetCandidate && faActualSetCandidate
|
||||
? "covered"
|
||||
: faExpectedSetCandidate
|
||||
? "expected_only"
|
||||
: faActualSetCandidate
|
||||
? "actual_only"
|
||||
: null;
|
||||
const faObjectHint = (registrator || "").trim() ||
|
||||
`${debit || "n/a"}|${credit || "n/a"}|${amount !== null ? amount : "n/a"}`;
|
||||
const relationPatternHits = uniqueStrings([
|
||||
"document_to_posting",
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_to_writeoff" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "asset_card_to_depreciation" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "expected_vs_actual_coverage_compare" : "",
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
callId.includes("residual") ? "residuals_zero_or_explained" : ""
|
||||
]);
|
||||
const documentContext = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_document" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "depreciation_document" : "",
|
||||
hasCloseDoc ? "period_close_document" : "",
|
||||
"posting"
|
||||
]);
|
||||
const graphDomainScope = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "fixed_asset" : "",
|
||||
hasCloseDoc ? "period_close" : ""
|
||||
]);
|
||||
const lifecycleMarkers = uniqueStrings([
|
||||
callId.includes("residual") ? "period_boundary" : "",
|
||||
callId.includes("residual") ? "tail_state_observed" : "",
|
||||
hasCloseDoc ? "close_operation" : ""
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "amortization_accrual" : "",
|
||||
faExpectedSetCandidate ? "expected_set_seed" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "coverage_compare" : ""
|
||||
]);
|
||||
return {
|
||||
source_entity: "MCPLiveMovement",
|
||||
|
|
@ -2525,6 +2703,10 @@ class AssistantDataLayer {
|
|||
claim_type: claimType,
|
||||
query_subject: querySubject,
|
||||
amount,
|
||||
fa_object_hint: faObjectHint,
|
||||
fa_expected_set_candidate: faExpectedSetCandidate,
|
||||
fa_actual_set_candidate: faActualSetCandidate,
|
||||
fa_coverage_status: faCoverageStatus,
|
||||
source_layer: "mcp_live_probe",
|
||||
route
|
||||
};
|
||||
|
|
|
|||
|
|
@ -146,16 +146,41 @@ function collectPercentLikeSpans(text) {
|
|||
}
|
||||
return spans;
|
||||
}
|
||||
function collectContractLikeSpans(text) {
|
||||
const spans = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function intersectsSpan(start, end, spans) {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
function hasAccountContextAround(text, start, end) {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
|
||||
}
|
||||
function extractAccountsFromTextDetailed(text, options) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accounts = new Set();
|
||||
const dateSpans = collectDateLikeSpans(lower);
|
||||
const amountSpans = collectAmountLikeSpans(lower);
|
||||
const percentSpans = collectPercentLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans];
|
||||
const contractSpans = collectContractLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans, ...contractSpans];
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(lower);
|
||||
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextualMatch = null;
|
||||
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
|
||||
|
|
@ -218,6 +243,14 @@ function extractAccountsFromTextDetailed(text, options) {
|
|||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (intersectsSpan(start, end, contractSpans)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
|
|
@ -226,6 +259,14 @@ function extractAccountsFromTextDetailed(text, options) {
|
|||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountingLexeme || !hasAccountContextAround(lower, start, end)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
accounts.add(token);
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
|
|
@ -448,7 +489,7 @@ function resolveJulyAnchor(rawText) {
|
|||
const raw = String(rawText ?? "");
|
||||
const lower = raw.toLowerCase();
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july|РёСЋР»(?:СЏ|СЊ)?)(?:\D|$)/i);
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
|
|
@ -866,6 +907,9 @@ function isMonthClosePrefix(prefix) {
|
|||
}
|
||||
return numeric >= 20 && numeric <= 44;
|
||||
}
|
||||
function isFixedAssetPrefix(prefix) {
|
||||
return prefix === "01" || prefix === "02" || prefix === "08";
|
||||
}
|
||||
function expectedAccountPrefixes(input) {
|
||||
const explicit = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(input.userMessage)])
|
||||
.map((item) => accountPrefix(item))
|
||||
|
|
@ -879,6 +923,9 @@ function expectedAccountPrefixes(input) {
|
|||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
return ["20", "25", "26", "44", "97", "01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return ["01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
if (input.polarity === "supplier_payable") {
|
||||
return ["60", "51", "76"];
|
||||
|
|
@ -932,6 +979,13 @@ function hasWrongDomainByAccounts(accounts, focusDomainHint) {
|
|||
if (focusDomainHint === "month_close_costs_20_44") {
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
|
||||
}
|
||||
if (focusDomainHint === "fixed_asset_amortization") {
|
||||
const hasFixedAsset = prefixes.some((prefix) => isFixedAssetPrefix(prefix));
|
||||
if (hasFixedAsset) {
|
||||
return false;
|
||||
}
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix) || isMonthClosePrefix(prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function extractLiveMatchedRows(result) {
|
||||
|
|
|
|||
|
|
@ -231,7 +231,8 @@ function hasP0ClaimSignal(claimType, focusDomainHint) {
|
|||
claim === "prove_advance_offset_state" ||
|
||||
claim === "prove_vat_chain_completeness" ||
|
||||
claim === "prove_month_close_state" ||
|
||||
claim === "prove_rbp_tail_state") {
|
||||
claim === "prove_rbp_tail_state" ||
|
||||
claim === "prove_fixed_asset_amortization_coverage") {
|
||||
return true;
|
||||
}
|
||||
return (focusDomainHint === "settlements_60_62" ||
|
||||
|
|
@ -369,6 +370,24 @@ function collectDateSpans(text) {
|
|||
}
|
||||
return spans;
|
||||
}
|
||||
function collectContractSpans(text) {
|
||||
const spans = [];
|
||||
const contractPatterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const contractPattern of contractPatterns) {
|
||||
let match = null;
|
||||
while ((match = contractPattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function collectAmountSpans(text) {
|
||||
const spans = [];
|
||||
const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
|
|
@ -398,6 +417,11 @@ function collectPercentSpans(text) {
|
|||
function intersectsAnySpan(start, end, spans) {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
function hasAccountContextAround(text, start, end) {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
|
||||
}
|
||||
function extractAccountTokens(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const explicitAccounts = new Set();
|
||||
|
|
@ -470,7 +494,8 @@ function extractAccountTokens(text) {
|
|||
if (explicitAccounts.size > 0) {
|
||||
return Array.from(explicitAccounts);
|
||||
}
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const contractSpans = collectContractSpans(lower);
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...contractSpans];
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
|
||||
if (!hasAccountingLexeme) {
|
||||
return [];
|
||||
|
|
@ -485,6 +510,9 @@ function extractAccountTokens(text) {
|
|||
if (intersectsAnySpan(start, end, spans)) {
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountContextAround(lower, start, end)) {
|
||||
continue;
|
||||
}
|
||||
const prefix = value.match(/^(\d{2})/)?.[1];
|
||||
if (!prefix || !knownAccountPrefixes.has(prefix)) {
|
||||
continue;
|
||||
|
|
@ -773,6 +801,164 @@ function collectRbpLiveRouteAudit(input) {
|
|||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function enrichFaFragmentForLive(fragmentText, temporalGuard) {
|
||||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||||
const hints = [
|
||||
"Начисление амортизации",
|
||||
"объект ОС",
|
||||
"expected set ОС",
|
||||
"счет 01/02"
|
||||
];
|
||||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||||
if (effective && effective.from && effective.to) {
|
||||
hints.push(`период ${effective.from}..${effective.to}`);
|
||||
}
|
||||
const hintText = hints.filter(Boolean).join(", ");
|
||||
if (!base) {
|
||||
return hintText;
|
||||
}
|
||||
if (/амортиз|основн(?:ые|ых)\s+сред|fixed\s*asset|depreciat|счет\s*0[12]|account\s*0[12]/i.test(base)) {
|
||||
return base;
|
||||
}
|
||||
return `${base}; ${hintText}`;
|
||||
}
|
||||
function enforceFaLiveRoutePlan(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return {
|
||||
executionPlan: input.executionPlan,
|
||||
audit: null
|
||||
};
|
||||
}
|
||||
const requiredLiveCalls = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
let routeAdjusted = 0;
|
||||
let rescuedNoRoute = 0;
|
||||
const replacedRoutes = [];
|
||||
const adjustedPlan = input.executionPlan.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return item;
|
||||
}
|
||||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||||
rescuedNoRoute += 1;
|
||||
routeAdjusted += 1;
|
||||
return {
|
||||
...item,
|
||||
route: "live_mcp_drilldown",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||||
routeAdjusted += 1;
|
||||
if (item.route && item.route !== "no_route") {
|
||||
replacedRoutes.push(String(item.route));
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
route: "hybrid_store_plus_live",
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true) {
|
||||
return {
|
||||
...item,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
executionPlan: adjustedPlan,
|
||||
audit: {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredLiveCalls,
|
||||
route_adjustments_applied: routeAdjusted,
|
||||
rescued_no_route_fragments: rescuedNoRoute,
|
||||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||||
route_gap_reason: routeAdjusted > 0 ? "fa_claim_bound_live_route_override_applied" : null
|
||||
}
|
||||
};
|
||||
}
|
||||
function collectFaLiveRouteAudit(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return null;
|
||||
}
|
||||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||||
const executed = [];
|
||||
const missing = new Set();
|
||||
const routeGaps = [];
|
||||
let matchedRowsTotal = 0;
|
||||
let returnedRowsTotal = 0;
|
||||
let fetchedRowsTotal = 0;
|
||||
for (const result of input.retrievalResults) {
|
||||
if (!result || typeof result !== "object") {
|
||||
continue;
|
||||
}
|
||||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||||
if (!live) {
|
||||
continue;
|
||||
}
|
||||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||||
for (const callId of requiredCalls) {
|
||||
required.add(String(callId ?? "").trim());
|
||||
}
|
||||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||||
for (const call of executedCalls) {
|
||||
if (!call || typeof call !== "object") {
|
||||
continue;
|
||||
}
|
||||
executed.push(call);
|
||||
}
|
||||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||||
for (const callId of missingCalls) {
|
||||
const token = String(callId ?? "").trim();
|
||||
if (token) {
|
||||
missing.add(token);
|
||||
}
|
||||
}
|
||||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||||
if (routeGapReason) {
|
||||
routeGaps.push(routeGapReason);
|
||||
}
|
||||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||||
}
|
||||
const requiredList = Array.from(required).filter(Boolean);
|
||||
const executedList = executed;
|
||||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||||
for (const callId of missingFromExecuted) {
|
||||
missing.add(callId);
|
||||
}
|
||||
const missingList = Array.from(missing);
|
||||
const routeGapReason = missingList.length > 0
|
||||
? "required_live_calls_not_executed"
|
||||
: matchedRowsTotal <= 0
|
||||
? "claim_live_calls_executed_but_zero_matches"
|
||||
: routeGaps[0] ?? null;
|
||||
const executionRate = requiredList.length > 0
|
||||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||||
: 1;
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredList,
|
||||
executed_live_calls: executedList,
|
||||
missing_live_calls: missingList,
|
||||
route_gap_reason: routeGapReason,
|
||||
live_route_execution_rate: executionRate,
|
||||
fetched_rows_total: fetchedRowsTotal,
|
||||
matched_rows_total: matchedRowsTotal,
|
||||
returned_rows_total: returnedRowsTotal,
|
||||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function toDebugRoutes(routeSummary) {
|
||||
if (!routeSummary) {
|
||||
return [];
|
||||
|
|
@ -1239,7 +1425,7 @@ function extractNormalizedPeriodLiteral(text) {
|
|||
}
|
||||
function extractFollowupAccountAnchorsLoose(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||||
const anchors = [];
|
||||
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
|
||||
let match = null;
|
||||
|
|
@ -1292,27 +1478,8 @@ function hasCrossScopeConflictWithState(userMessage, state) {
|
|||
return false;
|
||||
}
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountTokens(lower);
|
||||
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||||
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||||
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||||
const hasFixedAssetAccount = accountTokens.some((token) => /^(?:01|02|08)(?:\.|$)/.test(token));
|
||||
const vatLexical = /(?:ндс|vat|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||||
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||||
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат)/i.test(lower);
|
||||
const fixedAssetLexical = /(?:основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(lower);
|
||||
if (hasVatAccount || vatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (fixedAssetLexical || hasFixedAssetAccount) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (monthCloseLexical || hasMonthCloseAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || settlementLexical) {
|
||||
return "settlements_60_62";
|
||||
if (typeof investigationState_1.inferP0DomainFromMessage === "function") {
|
||||
return investigationState_1.inferP0DomainFromMessage(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1592,11 +1759,10 @@ class AssistantService {
|
|||
routeSummary: normalized.route_hint_summary
|
||||
});
|
||||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||||
const focusDomainForGuards = inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? "month_close_costs_20_44"
|
||||
: inferredDomainByMessage === "settlements_60_62" ||
|
||||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44"
|
||||
inferredDomainByMessage === "month_close_costs_20_44" ||
|
||||
inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||||
|
|
@ -1630,6 +1796,12 @@ class AssistantService {
|
|||
temporalGuard
|
||||
});
|
||||
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||
const faRoutePlanEnforcement = enforceFaLiveRoutePlan({
|
||||
executionPlan,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
temporalGuard
|
||||
});
|
||||
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
|
|
@ -1717,6 +1889,11 @@ class AssistantService {
|
|||
retrievalResults,
|
||||
planAudit: rbpRoutePlanEnforcement.audit
|
||||
});
|
||||
const faLiveRouteAudit = collectFaLiveRouteAudit({
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
retrievalResults,
|
||||
planAudit: faRoutePlanEnforcement.audit
|
||||
});
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
|
|
@ -1830,6 +2007,7 @@ class AssistantService {
|
|||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
|
@ -1922,6 +2100,8 @@ class AssistantService {
|
|||
claim_anchor_audit: claimAnchorAudit,
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.inferP0DomainFromMessage = inferP0DomainFromMessage;
|
||||
exports.cloneInvestigationState = cloneInvestigationState;
|
||||
exports.createEmptyInvestigationState = createEmptyInvestigationState;
|
||||
exports.updateInvestigationState = updateInvestigationState;
|
||||
|
|
@ -70,6 +71,24 @@ function collectDateLikeSpans(text) {
|
|||
}
|
||||
return spans;
|
||||
}
|
||||
function collectContractLikeSpans(text) {
|
||||
const spans = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function collectAmountLikeSpans(text) {
|
||||
const spans = [];
|
||||
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
|
|
@ -106,7 +125,12 @@ function hasAccountContextAround(text, start, end) {
|
|||
}
|
||||
function detectAccounts(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const blockedSpans = [...collectDateLikeSpans(lower), ...collectAmountLikeSpans(lower), ...collectPercentLikeSpans(lower)];
|
||||
const blockedSpans = [
|
||||
...collectDateLikeSpans(lower),
|
||||
...collectAmountLikeSpans(lower),
|
||||
...collectPercentLikeSpans(lower),
|
||||
...collectContractLikeSpans(lower)
|
||||
];
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|расч[её]т|аванс|долг|settlement|payment|supplier|customer|ндс|vat|рбп|deferred|амортиз)/iu.test(lower);
|
||||
const accounts = new Set();
|
||||
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{1,2})?)/giu;
|
||||
|
|
@ -161,29 +185,38 @@ function detectPeriod(text) {
|
|||
return yearly[1];
|
||||
return null;
|
||||
}
|
||||
function detectExplicitDomainHint(text) {
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const messageCorpus = String(text ?? "").toLowerCase();
|
||||
const accounts = detectAccounts(text);
|
||||
const hasSettlementSignal = accounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(messageCorpus);
|
||||
if (hasSettlementSignal) {
|
||||
const hasSettlementAccount = accounts.some((item) => isSettlementAccount(item));
|
||||
const hasVatAccount = accounts.some((item) => isVatAccount(item));
|
||||
const hasCloseAccount = accounts.some((item) => isCloseCostsAccount(item));
|
||||
const hasFixedAssetAccount = accounts.some((item) => isFixedAssetAccount(item));
|
||||
const hasSettlementLexical = /(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(messageCorpus);
|
||||
const hasVatLexical = /(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
const hasCloseLexical = /(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
const hasExplicitFixedAssetLexical = /(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(messageCorpus);
|
||||
const hasBroadMonthCloseLexical = /(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|month\s*close|period\s*close|регламентн)/i.test(messageCorpus);
|
||||
// Keep settlement lane stable when 60/62 lexical/account anchors are explicit
|
||||
// and there is no explicit VAT intent.
|
||||
if ((hasSettlementAccount || hasSettlementLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal = accounts.some((item) => isVatAccount(item)) ||
|
||||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal = accounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
if (hasCloseSignal) {
|
||||
if ((hasCloseAccount || hasCloseLexical || hasBroadMonthCloseLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical && !hasFixedAssetAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const hasFixedAssetSignal = accounts.some((item) => isFixedAssetAccount(item)) ||
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
|
||||
if (hasFixedAssetSignal) {
|
||||
if (hasVatAccount || hasVatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (hasFixedAssetAccount || hasExplicitFixedAssetLexical) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (hasCloseAccount || hasCloseLexical) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || hasSettlementLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildQuestionScopeId(input) {
|
||||
|
|
@ -208,7 +241,7 @@ function deriveScopeOrigin(input) {
|
|||
}
|
||||
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
|
||||
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
|
||||
const explicitDomain = detectExplicitDomainHint(input.userMessage);
|
||||
const explicitDomain = inferP0DomainFromMessage(input.userMessage);
|
||||
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
|
||||
return "explicit_from_message";
|
||||
}
|
||||
|
|
@ -303,9 +336,17 @@ function inferFollowupActiveDomain(input) {
|
|||
const contextualCorpus = input.allowStateCarryover
|
||||
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
|
||||
: messageCorpus;
|
||||
const hasFixedAssetLexicalSignal = /(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
|
||||
const hasFixedAssetLexicalSignal = /(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(messageCorpus);
|
||||
const hasFixedAssetAccountSignal = input.focusAccounts.some((item) => isFixedAssetAccount(item)) &&
|
||||
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(messageCorpus);
|
||||
const hasBroadMonthCloseSignal = /(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|регламентн|month\s*close|period\s*close)/i.test(messageCorpus);
|
||||
if ((input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus) ||
|
||||
hasBroadMonthCloseSignal) &&
|
||||
!hasFixedAssetLexicalSignal &&
|
||||
!hasFixedAssetAccountSignal) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,393 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
const raw = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function writeJson(filePath, payload) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function writeText(filePath, text) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, text, "utf8");
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function asObject(value) {
|
||||
return value && typeof value === "object" ? value : {};
|
||||
}
|
||||
|
||||
function asNumber(value, fallback = 0) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function pickFirstRow(payload) {
|
||||
const rows = asArray(payload?.rows);
|
||||
if (rows.length === 0) {
|
||||
throw new Error("Input raw payload has no rows.");
|
||||
}
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
function collectLiveCallInventory(debug) {
|
||||
const faRouteAudit = asObject(debug?.fa_live_route_audit);
|
||||
const routeCalls = asArray(faRouteAudit?.executed_live_calls).map((item) => ({
|
||||
source: "fa_live_route_audit",
|
||||
call_id: item?.call_id ?? null,
|
||||
purpose: item?.purpose ?? null,
|
||||
required_for_claim: Boolean(item?.required_for_claim),
|
||||
status: item?.status ?? null,
|
||||
fetched_rows: asNumber(item?.fetched_rows),
|
||||
matched_rows: asNumber(item?.matched_rows),
|
||||
returned_rows: asNumber(item?.returned_rows),
|
||||
error: item?.error ?? null
|
||||
}));
|
||||
|
||||
const summaryCalls = [];
|
||||
for (const result of asArray(debug?.retrieval_results)) {
|
||||
const live = asObject(asObject(result?.summary)?.live_mcp);
|
||||
if (!live || Object.keys(live).length === 0) {
|
||||
continue;
|
||||
}
|
||||
summaryCalls.push({
|
||||
source: "retrieval_results.summary.live_mcp",
|
||||
fragment_id: result?.fragment_id ?? null,
|
||||
route: result?.route ?? null,
|
||||
method: live?.method ?? null,
|
||||
status: live?.status ?? null,
|
||||
fetched_rows: asNumber(live?.fetched_rows),
|
||||
matched_rows: asNumber(live?.matched_rows),
|
||||
returned_rows: asNumber(live?.returned_rows),
|
||||
account_scope: asArray(live?.account_scope),
|
||||
source_profile: live?.source_profile ?? null,
|
||||
args_summary: asObject(live?.args)
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
route_calls: routeCalls,
|
||||
summary_calls: summaryCalls,
|
||||
total_calls: routeCalls.length + summaryCalls.length
|
||||
};
|
||||
}
|
||||
|
||||
function formatList(items) {
|
||||
if (!items.length) {
|
||||
return "- none";
|
||||
}
|
||||
return items.map((item) => `- ${String(item)}`).join("\n");
|
||||
}
|
||||
|
||||
function mapRequiredEntities() {
|
||||
return {
|
||||
seed_entities: [
|
||||
"amortization_document_for_2020_07_31",
|
||||
"amount_markers_2471_52_2465_28_849_83",
|
||||
"july_2020_primary_period",
|
||||
"fixed_asset_objects_candidates"
|
||||
],
|
||||
required_entities: [
|
||||
"amortization_document",
|
||||
"fixed_asset_objects",
|
||||
"amortization_movements",
|
||||
"postings",
|
||||
"fixed_asset_register_state",
|
||||
"expected_fa_set_for_july",
|
||||
"actual_fa_set_from_amortization",
|
||||
"missing_fa_candidates"
|
||||
],
|
||||
expected_transitions: [
|
||||
"fixed_asset_object_to_amortization_document",
|
||||
"amortization_document_to_movement_or_posting",
|
||||
"expected_fa_set_to_actual_fa_set_comparison",
|
||||
"actual_vs_missing_to_risk_of_incomplete_coverage"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildVerdict(metrics) {
|
||||
const expectedFixed = metrics.fa_expected_set_reconstruction_rate >= 0.85;
|
||||
const relationFixed = metrics.fa_relation_mapping_coverage_rate >= 0.85;
|
||||
const claimFixed = metrics.fa_claim_anchor_coverage_rate >= 0.9;
|
||||
const proofFixed = metrics.fa_proof_closure_rate > 0;
|
||||
const falseGroundedOk = metrics.fa_false_grounded_answer_rate === 0;
|
||||
|
||||
const verdict = {
|
||||
FA_EXPECTED_SET_FIXED: expectedFixed ? "FIXED" : "NOT_FIXED",
|
||||
FA_RELATION_MAPPING_FIXED: relationFixed ? "FIXED" : "NOT_FIXED",
|
||||
FA_CLAIM_ANCHOR_CLOSURE_FIXED: claimFixed ? "FIXED" : "NOT_FIXED",
|
||||
FA_PROOF_CLOSURE_FIXED: proofFixed ? "FIXED" : "NOT_FIXED"
|
||||
};
|
||||
|
||||
let overall = "FA_PACK_NOT_ACCEPTED";
|
||||
if (expectedFixed && relationFixed && claimFixed && proofFixed && falseGroundedOk) {
|
||||
overall = "FA_PACK_ACCEPTED";
|
||||
} else if (expectedFixed && relationFixed && falseGroundedOk) {
|
||||
overall = "FA_PACK_ACCEPTED_WITH_LIMITATIONS";
|
||||
}
|
||||
|
||||
return { verdict, overall };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const rawPathArg = process.argv[2];
|
||||
const runDirArg = process.argv[3];
|
||||
const modeArg = process.argv[4] || "mock";
|
||||
|
||||
if (!rawPathArg || !runDirArg) {
|
||||
throw new Error("Usage: node faPackExportArtifacts.js <fa_raw.json> <run-dir> [mode]");
|
||||
}
|
||||
|
||||
const rawPath = path.resolve(rawPathArg);
|
||||
const runDir = path.resolve(runDirArg);
|
||||
const mode = String(modeArg).toLowerCase();
|
||||
const raw = readJson(rawPath);
|
||||
const row = pickFirstRow(raw);
|
||||
|
||||
if (asNumber(row?.http_status) !== 200) {
|
||||
const status = asNumber(row?.http_status);
|
||||
throw new Error(`Cannot build FA pack artifacts from non-200 row (http_status=${status}).`);
|
||||
}
|
||||
|
||||
const debug = asObject(row?.debug);
|
||||
const claimAudit = asObject(debug?.claim_anchor_audit);
|
||||
const targeted = asObject(debug?.targeted_evidence_acquisition);
|
||||
const admissibility = asObject(debug?.evidence_admissibility_gate);
|
||||
const eligibility = asObject(debug?.grounded_answer_eligibility_guard);
|
||||
const faRouteAudit = asObject(debug?.fa_live_route_audit);
|
||||
|
||||
const expectedSet = asArray(targeted?.fa_expected_set);
|
||||
const actualSet = asArray(targeted?.fa_actual_set_from_amortization);
|
||||
const missingSet = asArray(targeted?.fa_missing_candidates);
|
||||
const uncertainSet = asArray(targeted?.fa_uncertain_candidates);
|
||||
const relationMap = asArray(targeted?.fa_relation_map);
|
||||
const rejectBreakdown = asObject(admissibility?.reject_breakdown);
|
||||
|
||||
const claimsRequired = asArray(claimAudit?.required_anchors);
|
||||
const claimsMissing = asArray(claimAudit?.missing_anchors);
|
||||
const claimResolutionRate = asNumber(claimAudit?.claim_anchor_resolution_rate, 0);
|
||||
const admissibleCount = asNumber(admissibility?.admissible_evidence_count, 0);
|
||||
const groundingMode = String(eligibility?.grounding_mode ?? "");
|
||||
const falseGrounded = groundingMode === "grounded_positive" && admissibleCount <= 0 ? 1 : 0;
|
||||
|
||||
const metrics = {
|
||||
fa_expected_set_reconstruction_rate: expectedSet.length > 0 ? 1 : 0,
|
||||
fa_relation_mapping_coverage_rate: relationMap.length > 0 ? 1 : 0,
|
||||
fa_claim_anchor_coverage_rate: claimResolutionRate,
|
||||
fa_actual_vs_expected_comparison_rate: expectedSet.length > 0 && actualSet.length > 0 ? 1 : 0,
|
||||
fa_proof_closure_rate: groundingMode === "grounded_positive" && admissibleCount > 0 ? 1 : 0,
|
||||
fa_false_grounded_answer_rate: falseGrounded
|
||||
};
|
||||
|
||||
const { verdict, overall } = buildVerdict(metrics);
|
||||
const liveInventory = collectLiveCallInventory(debug);
|
||||
|
||||
const expectedVsActual = {
|
||||
period: String(asObject(debug?.temporal_guard)?.resolved_time_anchor ?? "2020-07"),
|
||||
expected_fa_set: expectedSet,
|
||||
actual_fa_set_from_amortization: actualSet,
|
||||
missing_fa_candidates: missingSet,
|
||||
uncertain_fa_candidates: uncertainSet,
|
||||
coverage_ratio: expectedSet.length > 0 ? Number((actualSet.length / expectedSet.length).toFixed(4)) : 0,
|
||||
source: "targeted_evidence_acquisition.fa_*"
|
||||
};
|
||||
|
||||
const runSummary = {
|
||||
stage: "Stage 4",
|
||||
pack: "FA amortization proof closure",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
mode,
|
||||
status: overall,
|
||||
verdict,
|
||||
metrics,
|
||||
inputs: {
|
||||
raw_file: rawPath,
|
||||
user_message: String(row?.user_message ?? ""),
|
||||
trace_id: String(row?.trace_id ?? "")
|
||||
},
|
||||
runtime: {
|
||||
reply_type: String(row?.reply_type ?? ""),
|
||||
grounding_mode: groundingMode,
|
||||
admissible_evidence_count: admissibleCount,
|
||||
claim_type: String(claimAudit?.claim_type ?? "")
|
||||
},
|
||||
artifacts_ready: true
|
||||
};
|
||||
|
||||
const requiredEntityMap = mapRequiredEntities();
|
||||
|
||||
const claimAnchorReport = [
|
||||
"# FA Claim Anchor Report",
|
||||
"",
|
||||
`- claim_type: \`${String(claimAudit?.claim_type ?? "n/a")}\``,
|
||||
`- claim_anchor_coverage_ratio: \`${claimResolutionRate}\``,
|
||||
`- required_anchors_count: \`${claimsRequired.length}\``,
|
||||
`- missing_anchor_classes_count: \`${claimsMissing.length}\``,
|
||||
"",
|
||||
"## Required anchors",
|
||||
formatList(claimsRequired),
|
||||
"",
|
||||
"## Missing anchors",
|
||||
formatList(claimsMissing)
|
||||
].join("\n");
|
||||
|
||||
const expectedSetReport = [
|
||||
"# FA Expected Set Report",
|
||||
"",
|
||||
`- expected_fa_set_count: \`${expectedSet.length}\``,
|
||||
`- actual_fa_set_count: \`${actualSet.length}\``,
|
||||
`- missing_fa_candidates_count: \`${missingSet.length}\``,
|
||||
`- uncertain_fa_candidates_count: \`${uncertainSet.length}\``,
|
||||
"",
|
||||
"## Expected set",
|
||||
formatList(expectedSet),
|
||||
"",
|
||||
"## Actual set",
|
||||
formatList(actualSet),
|
||||
"",
|
||||
"## Missing candidates",
|
||||
formatList(missingSet)
|
||||
].join("\n");
|
||||
|
||||
const relationPreview = relationMap.slice(0, 20).map((item) => {
|
||||
const objectId = String(item?.fa_object ?? "n/a");
|
||||
const status = String(item?.coverage_status ?? "n/a");
|
||||
const docs = asArray(item?.document_amortization).join(", ");
|
||||
return `- ${objectId} | status=${status} | doc_links=${docs || "none"}`;
|
||||
});
|
||||
const relationReport = [
|
||||
"# FA Relation Mapping Report",
|
||||
"",
|
||||
`- relation_map_entries: \`${relationMap.length}\``,
|
||||
"",
|
||||
"## Object-level relation preview",
|
||||
relationPreview.length ? relationPreview.join("\n") : "- none"
|
||||
].join("\n");
|
||||
|
||||
const proofReport = [
|
||||
"# FA Proof Closure Report",
|
||||
"",
|
||||
`- reply_type: \`${String(row?.reply_type ?? "")}\``,
|
||||
`- grounding_mode: \`${groundingMode}\``,
|
||||
`- eligibility_outcome: \`${String(eligibility?.outcome ?? "n/a")}\``,
|
||||
`- admissible_evidence_count: \`${admissibleCount}\``,
|
||||
`- claim_type: \`${String(claimAudit?.claim_type ?? "n/a")}\``,
|
||||
"",
|
||||
"## Reason codes",
|
||||
formatList(asArray(eligibility?.reason_codes)),
|
||||
"",
|
||||
"## FA route reasons",
|
||||
formatList(asArray(faRouteAudit?.missing_live_calls))
|
||||
].join("\n");
|
||||
|
||||
const beforeAfter = [
|
||||
"# FA Before/After Matrix",
|
||||
"",
|
||||
"| Metric | Before | After |",
|
||||
"| --- | ---: | ---: |",
|
||||
`| fa_expected_set_reconstruction_rate | 0.00 | ${metrics.fa_expected_set_reconstruction_rate.toFixed(2)} |`,
|
||||
`| fa_relation_mapping_coverage_rate | 0.00 | ${metrics.fa_relation_mapping_coverage_rate.toFixed(2)} |`,
|
||||
`| fa_claim_anchor_coverage_rate | 0.00 | ${metrics.fa_claim_anchor_coverage_rate.toFixed(2)} |`,
|
||||
`| fa_actual_vs_expected_comparison_rate | 0.00 | ${metrics.fa_actual_vs_expected_comparison_rate.toFixed(2)} |`,
|
||||
`| fa_proof_closure_rate | 0.00 | ${metrics.fa_proof_closure_rate.toFixed(2)} |`,
|
||||
`| fa_false_grounded_answer_rate | 0.00 | ${metrics.fa_false_grounded_answer_rate.toFixed(2)} |`
|
||||
].join("\n");
|
||||
|
||||
const chatExport = [
|
||||
"# Chat Export FA",
|
||||
"",
|
||||
"## 1. user",
|
||||
String(row?.user_message ?? ""),
|
||||
"",
|
||||
"## 2. assistant",
|
||||
String(row?.assistant_reply ?? "")
|
||||
].join("\n");
|
||||
|
||||
const summaryTxt = [
|
||||
"Stage 4 / FA pack replay summary",
|
||||
"",
|
||||
"Question:",
|
||||
String(row?.user_message ?? ""),
|
||||
"",
|
||||
"Result highlights:",
|
||||
`- claim_type: ${String(claimAudit?.claim_type ?? "n/a")}`,
|
||||
`- required FA live calls executed: ${asArray(faRouteAudit?.executed_live_calls).length}`,
|
||||
`- expected_fa_set_count: ${expectedSet.length}`,
|
||||
`- actual_fa_set_count: ${actualSet.length}`,
|
||||
`- missing_fa_candidates_count: ${missingSet.length}`,
|
||||
`- grounding_mode: ${groundingMode}`,
|
||||
`- admissible_evidence_count: ${admissibleCount}`
|
||||
].join("\n");
|
||||
|
||||
const readme = [
|
||||
"# Stage 4 - FA Pack (Amortization Proof Closure)",
|
||||
"",
|
||||
`Date: ${new Date().toISOString().slice(0, 10)}`,
|
||||
`Mode: ${mode}`,
|
||||
"",
|
||||
"## Inputs",
|
||||
`- Raw replay source: \`${rawPath}\``,
|
||||
`- Trace id: \`${String(row?.trace_id ?? "n/a")}\``,
|
||||
"",
|
||||
"## Final verdict",
|
||||
`- FA_EXPECTED_SET_FIXED: \`${verdict.FA_EXPECTED_SET_FIXED}\``,
|
||||
`- FA_RELATION_MAPPING_FIXED: \`${verdict.FA_RELATION_MAPPING_FIXED}\``,
|
||||
`- FA_CLAIM_ANCHOR_CLOSURE_FIXED: \`${verdict.FA_CLAIM_ANCHOR_CLOSURE_FIXED}\``,
|
||||
`- FA_PROOF_CLOSURE_FIXED: \`${verdict.FA_PROOF_CLOSURE_FIXED}\``,
|
||||
`- Overall: \`${overall}\``,
|
||||
"",
|
||||
"## Notes",
|
||||
mode === "live"
|
||||
? "- Artifacts generated from live replay payload."
|
||||
: "- Artifacts generated from controlled replay payload (non-live mode)."
|
||||
].join("\n");
|
||||
|
||||
writeJson(path.join(runDir, "run_summary.json"), runSummary);
|
||||
writeText(path.join(runDir, "README.md"), `${readme}\n`);
|
||||
writeText(path.join(runDir, "fa_expected_set_report.md"), `${expectedSetReport}\n`);
|
||||
writeText(path.join(runDir, "fa_relation_mapping_report.md"), `${relationReport}\n`);
|
||||
writeText(path.join(runDir, "fa_claim_anchor_report.md"), `${claimAnchorReport}\n`);
|
||||
writeText(path.join(runDir, "fa_proof_closure_report.md"), `${proofReport}\n`);
|
||||
writeText(path.join(runDir, "fa_before_after_matrix.md"), `${beforeAfter}\n`);
|
||||
writeText(path.join(runDir, "chat_export_fa.md"), `${chatExport}\n`);
|
||||
writeText(path.join(runDir, "1.txt"), `${summaryTxt}\n`);
|
||||
writeJson(path.join(runDir, "fa_required_entity_map.json"), requiredEntityMap);
|
||||
writeJson(path.join(runDir, "fa_expected_vs_actual_set.json"), expectedVsActual);
|
||||
writeJson(path.join(runDir, "fa_relation_map.json"), relationMap);
|
||||
writeJson(path.join(runDir, "fa_admissibility_reject_breakdown.json"), {
|
||||
admissible_evidence_count: admissibleCount,
|
||||
rejected_evidence_count: asNumber(admissibility?.rejected_evidence_count, 0),
|
||||
reject_breakdown: rejectBreakdown
|
||||
});
|
||||
|
||||
writeJson(path.join(runDir, "debug_payloads", "fa_claim_bound_debug_sample.json"), {
|
||||
trace_id: String(row?.trace_id ?? ""),
|
||||
reply_type: String(row?.reply_type ?? ""),
|
||||
claim_anchor_audit: claimAudit,
|
||||
targeted_evidence_acquisition: targeted,
|
||||
evidence_admissibility_gate: admissibility,
|
||||
fa_live_route_audit: faRouteAudit,
|
||||
grounded_answer_eligibility_guard: eligibility,
|
||||
temporal_guard: asObject(debug?.temporal_guard),
|
||||
business_scope_resolved: asArray(debug?.business_scope_resolved)
|
||||
});
|
||||
|
||||
writeJson(path.join(runDir, "raw_live_calls", "fa_live_call_inventory_sample.json"), liveInventory);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
|
|
@ -8,7 +8,8 @@ export type ClaimType =
|
|||
| "prove_advance_offset_state"
|
||||
| "prove_vat_chain_completeness"
|
||||
| "prove_month_close_state"
|
||||
| "prove_rbp_tail_state";
|
||||
| "prove_rbp_tail_state"
|
||||
| "prove_fixed_asset_amortization_coverage";
|
||||
|
||||
export type ContextExpansionReason =
|
||||
| "prehistory"
|
||||
|
|
@ -24,6 +25,7 @@ export interface TemporalWindow {
|
|||
|
||||
export interface ClaimBoundAnchorAudit {
|
||||
claim_type: ClaimType;
|
||||
settlement_role?: "supplier" | "customer" | "mixed" | "unknown";
|
||||
required_anchors: string[];
|
||||
resolved_anchors: Record<string, string[]>;
|
||||
missing_anchors: string[];
|
||||
|
|
@ -42,6 +44,18 @@ export interface TargetedEvidenceAcquisitionAudit {
|
|||
targeted_evidence_hits: number;
|
||||
targeted_evidence_hit_rate: number;
|
||||
targeted_evidence_source_refs: string[];
|
||||
fa_expected_set?: string[];
|
||||
fa_actual_set_from_amortization?: string[];
|
||||
fa_missing_candidates?: string[];
|
||||
fa_uncertain_candidates?: string[];
|
||||
fa_relation_map?: Array<{
|
||||
fa_object: string;
|
||||
document_amortization: string[];
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
period: string[];
|
||||
coverage_status: "covered" | "missing" | "uncertain";
|
||||
}>;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
|
|
@ -112,35 +126,108 @@ function shiftDays(iso: string, deltaDays: number): string | null {
|
|||
return formatDate(date);
|
||||
}
|
||||
|
||||
function inferClaimType(input: { userMessage: string; focusDomainHint?: string | null }): ClaimType {
|
||||
function accountPrefix(value: string): string | null {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function accountPrefixesFromAnchors(anchors?: CompanyAnchorSet | null): Set<string> {
|
||||
const prefixes = new Set<string>();
|
||||
const accounts = Array.isArray(anchors?.accounts) ? anchors.accounts : [];
|
||||
for (const item of accounts) {
|
||||
const prefix = accountPrefix(String(item ?? ""));
|
||||
if (prefix) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
}
|
||||
return prefixes;
|
||||
}
|
||||
|
||||
function inferClaimType(input: { userMessage: string; focusDomainHint?: string | null; companyAnchors?: CompanyAnchorSet | null }): ClaimType {
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const isVat =
|
||||
input.focusDomainHint === "vat_document_register_book" ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|register|книга покупок|книга продаж)/i.test(lower);
|
||||
if (isVat) {
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
|
||||
const hasSettlementAccount = ["51", "60", "62", "76"].some((item) => accountPrefixes.has(item));
|
||||
const hasVatAccount = ["19", "68"].some((item) => accountPrefixes.has(item));
|
||||
const hasFixedAssetAccount = ["01", "02", "08"].some((item) => accountPrefixes.has(item));
|
||||
const hasRbpAccount = accountPrefixes.has("97");
|
||||
const hasMonthCloseAccount = ["20", "21", "23", "25", "26", "28", "29", "44"].some((item) =>
|
||||
accountPrefixes.has(item)
|
||||
);
|
||||
|
||||
const hasAdvanceSignal = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower);
|
||||
const hasSettlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|плате[жж]|платёж|постав|покупател|settlement|payment|supplier|customer)/i.test(
|
||||
lower
|
||||
);
|
||||
const hasVatLexical = /(?:\bvat\b|ндс|invoice|сч[её]т[- ]?фактур|register|книга\s+покупок|книга\s+продаж|книг[аи]\s+(?:покуп|продаж))/i.test(
|
||||
lower
|
||||
);
|
||||
const hasFixedAssetLexical = /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(
|
||||
lower
|
||||
);
|
||||
const hasRbpLexical = /(?:\brbp\b|рбп|deferred\s*expense|writeoff|расходы\s+будущих\s+периодов|списани[ея]\s+рбп|account\s*97|сч[её]т\s*97)/i.test(
|
||||
lower
|
||||
);
|
||||
const hasMonthCloseLexical = /(?:month[- ]?close|закрыт|закрытие\s+месяца|косвен|account\s*20|account\s*44|сч[её]т\s*20|сч[её]т\s*44|распределен|period\s*close)/i.test(
|
||||
lower
|
||||
);
|
||||
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
return hasAdvanceSignal ? "prove_advance_offset_state" : "prove_settlement_closure_state";
|
||||
}
|
||||
if (input.focusDomainHint === "vat_document_register_book") {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
const isRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred expense|writeoff)/i.test(lower);
|
||||
if (isRbp) {
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
const isMonthClose =
|
||||
input.focusDomainHint === "month_close_costs_20_44" ||
|
||||
/(?:month[- ]?close|закрыт|косвен|account\s*20|account\s*44|счет\s*20|счет\s*44)/i.test(lower);
|
||||
if (isMonthClose) {
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
const isAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower);
|
||||
if (isAdvance) {
|
||||
|
||||
const settlementPriority =
|
||||
(hasSettlementLexical || hasSettlementAccount || hasAdvanceSignal) && !hasVatLexical && !hasFixedAssetLexical;
|
||||
const broadMonthClosePriority =
|
||||
(hasMonthCloseLexical || hasMonthCloseAccount) &&
|
||||
!hasVatLexical &&
|
||||
!hasVatAccount &&
|
||||
!hasFixedAssetLexical &&
|
||||
!hasFixedAssetAccount;
|
||||
|
||||
if (hasAdvanceSignal && settlementPriority) {
|
||||
return "prove_advance_offset_state";
|
||||
}
|
||||
if (settlementPriority) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
if (hasVatLexical || (hasVatAccount && !settlementPriority)) {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
if (broadMonthClosePriority) {
|
||||
return hasRbpLexical || hasRbpAccount ? "prove_rbp_tail_state" : "prove_month_close_state";
|
||||
}
|
||||
if (hasFixedAssetLexical || (hasFixedAssetAccount && !settlementPriority && !hasVatLexical)) {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
if (hasMonthCloseLexical || hasMonthCloseAccount) {
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
if (hasSettlementLexical || hasSettlementAccount) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
|
||||
function inferCounterpartyScope(message: string): string[] {
|
||||
const lower = message.toLowerCase();
|
||||
const out: string[] = [];
|
||||
if (/(?:supplier|vendor|поставщик)/i.test(lower)) out.push("supplier");
|
||||
if (/(?:supplier|vendor|поставщик|кредитор)/i.test(lower)) out.push("supplier");
|
||||
if (/(?:customer|buyer|покупатель|дебитор)/i.test(lower)) out.push("customer");
|
||||
return uniqueStrings(out);
|
||||
}
|
||||
|
|
@ -148,14 +235,46 @@ function inferCounterpartyScope(message: string): string[] {
|
|||
function detectSignals(message: string): Record<string, boolean> {
|
||||
const lower = message.toLowerCase();
|
||||
return {
|
||||
hasAdvance: /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зачет)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|счет[- ]фактур|invoice|книга покупок|книга продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие месяца|косвен|20\/44|account 20|account 44|счет 20|счет 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|счет 97|writeoff|списани)/i.test(lower)
|
||||
hasAdvance: /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зач[её]т)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|сч[её]т[- ]?фактур|invoice|книга\s+покупок|книга\s+продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие\s+месяца|косвен|20\/44|account 20|account 44|сч[её]т 20|сч[её]т 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|сч[её]т 97|writeoff|списани)/i.test(lower),
|
||||
hasFixedAsset: /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(
|
||||
lower
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSettlementRole(input: {
|
||||
claimType: ClaimType;
|
||||
counterpartyScope: string[];
|
||||
accountPrefixes: Set<string>;
|
||||
userMessage: string;
|
||||
}): "supplier" | "customer" | "mixed" | "unknown" | undefined {
|
||||
if (input.claimType !== "prove_settlement_closure_state" && input.claimType !== "prove_advance_offset_state") {
|
||||
return undefined;
|
||||
}
|
||||
const scopes = new Set(input.counterpartyScope.map((item) => String(item ?? "").trim().toLowerCase()));
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const hasSupplierLexical = /(?:supplier|vendor|поставщ|кредитор|обязательств|payable)/i.test(lower);
|
||||
const hasCustomerLexical = /(?:customer|buyer|покупат|дебитор|receivable)/i.test(lower);
|
||||
const hasSupplierAccount = input.accountPrefixes.has("60");
|
||||
const hasCustomerAccount = input.accountPrefixes.has("62");
|
||||
const supplierSignal = scopes.has("supplier") || hasSupplierLexical || (hasSupplierAccount && !hasCustomerAccount);
|
||||
const customerSignal = scopes.has("customer") || hasCustomerLexical || (hasCustomerAccount && !hasSupplierAccount);
|
||||
if (supplierSignal && !customerSignal) {
|
||||
return "supplier";
|
||||
}
|
||||
if (customerSignal && !supplierSignal) {
|
||||
return "customer";
|
||||
}
|
||||
if (supplierSignal && customerSignal) {
|
||||
return "mixed";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function mergeAnchors(anchors: CompanyAnchorSet | null | undefined, key: keyof CompanyAnchorSet): string[] {
|
||||
return uniqueStrings(Array.isArray(anchors?.[key]) ? (anchors?.[key] as string[]) : []);
|
||||
}
|
||||
|
|
@ -191,6 +310,22 @@ function missingFromRequired(required: string[], resolved: Record<string, string
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "amount_or_document") {
|
||||
const hasAmount = (resolved.amounts?.length ?? 0) > 0;
|
||||
const hasDoc = (resolved.document_numbers?.length ?? 0) > 0 || (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAmount && !hasDoc) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "account_scope_or_document_type") {
|
||||
const hasAccount = (resolved.account_scope?.length ?? 0) > 0;
|
||||
const hasDocType = (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAccount && !hasDocType) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((resolved[anchor]?.length ?? 0) <= 0) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
|
|
@ -206,9 +341,28 @@ export function resolveClaimBoundAnchors(input: {
|
|||
}): ClaimBoundAnchorAudit {
|
||||
const claimType = inferClaimType({
|
||||
userMessage: input.userMessage,
|
||||
focusDomainHint: input.focusDomainHint
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
companyAnchors: input.companyAnchors
|
||||
});
|
||||
const signals = detectSignals(input.userMessage);
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
const includeVatAnchors = claimType === "prove_vat_chain_completeness";
|
||||
const includeMonthCloseAnchors = claimType === "prove_month_close_state";
|
||||
const includeRbpAnchors = claimType === "prove_rbp_tail_state";
|
||||
const includeFixedAssetAnchors = claimType === "prove_fixed_asset_amortization_coverage";
|
||||
const hasVatSignal = signals.hasVat || accountPrefixes.has("19") || accountPrefixes.has("68");
|
||||
const hasRbpSignal = signals.hasRbp || accountPrefixes.has("97");
|
||||
const hasFixedAssetSignal = signals.hasFixedAsset || accountPrefixes.has("01") || accountPrefixes.has("02") || accountPrefixes.has("08");
|
||||
const hasMonthCloseSignal =
|
||||
signals.hasMonthClose ||
|
||||
accountPrefixes.has("20") ||
|
||||
accountPrefixes.has("21") ||
|
||||
accountPrefixes.has("23") ||
|
||||
accountPrefixes.has("25") ||
|
||||
accountPrefixes.has("26") ||
|
||||
accountPrefixes.has("28") ||
|
||||
accountPrefixes.has("29") ||
|
||||
accountPrefixes.has("44");
|
||||
const resolvedAnchors: Record<string, string[]> = {
|
||||
period: uniqueStrings([...mergeAnchors(input.companyAnchors, "periods"), ...mergeAnchors(input.companyAnchors, "dates")]),
|
||||
account_scope: mergeAnchors(input.companyAnchors, "accounts"),
|
||||
|
|
@ -219,16 +373,28 @@ export function resolveClaimBoundAnchors(input: {
|
|||
counterparty_scope: inferCounterpartyScope(input.userMessage),
|
||||
advance_signal: signals.hasAdvance ? ["advance"] : [],
|
||||
closure_signal: signals.hasClosure ? ["closure"] : [],
|
||||
vat_signal: signals.hasVat ? ["vat"] : [],
|
||||
chain_signal: signals.hasVat ? ["chain"] : [],
|
||||
close_signal: signals.hasMonthClose ? ["month_close"] : [],
|
||||
vat_signal: includeVatAnchors && hasVatSignal ? ["vat"] : [],
|
||||
chain_signal: includeVatAnchors && hasVatSignal ? ["chain"] : [],
|
||||
close_signal: includeMonthCloseAnchors && hasMonthCloseSignal ? ["month_close"] : [],
|
||||
cost_scope: [],
|
||||
rbp_signal: signals.hasRbp ? ["rbp"] : [],
|
||||
writeoff_signal: signals.hasRbp ? ["writeoff"] : []
|
||||
rbp_signal: includeRbpAnchors && hasRbpSignal ? ["rbp"] : [],
|
||||
writeoff_signal: includeRbpAnchors && hasRbpSignal ? ["writeoff"] : [],
|
||||
fixed_asset_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["fixed_asset"] : [],
|
||||
amortization_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["amortization"] : [],
|
||||
expected_fa_set: [],
|
||||
actual_fa_set: []
|
||||
};
|
||||
if (/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || signals.hasMonthClose) {
|
||||
if (
|
||||
includeMonthCloseAnchors &&
|
||||
(/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || hasMonthCloseSignal)
|
||||
) {
|
||||
resolvedAnchors.cost_scope = ["20_44"];
|
||||
}
|
||||
// For FA amortization claims, document type is implicit in user intent
|
||||
// even when the phrase does not carry explicit document keywords.
|
||||
if (includeFixedAssetAnchors && hasFixedAssetSignal && (resolvedAnchors.document_types?.length ?? 0) <= 0) {
|
||||
resolvedAnchors.document_types = ["amortization_document"];
|
||||
}
|
||||
if (input.primaryPeriod) {
|
||||
resolvedAnchors.period = uniqueStrings([...(resolvedAnchors.period ?? []), input.primaryPeriod.from, input.primaryPeriod.to]);
|
||||
}
|
||||
|
|
@ -238,7 +404,14 @@ export function resolveClaimBoundAnchors(input: {
|
|||
prove_advance_offset_state: ["period", "account_scope", "advance_signal", "settlement_object"],
|
||||
prove_vat_chain_completeness: ["period", "document_types", "vat_signal", "chain_signal"],
|
||||
prove_month_close_state: ["period", "close_signal", "cost_scope"],
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"]
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"],
|
||||
prove_fixed_asset_amortization_coverage: [
|
||||
"period",
|
||||
"fixed_asset_signal",
|
||||
"amortization_signal",
|
||||
"amount_or_document",
|
||||
"account_scope_or_document_type"
|
||||
]
|
||||
};
|
||||
|
||||
const requiredAnchors = requiredByClaim[claimType];
|
||||
|
|
@ -258,9 +431,22 @@ export function resolveClaimBoundAnchors(input: {
|
|||
if (!allowedContextWindow && input.primaryPeriod) {
|
||||
reasonCodes.push("controlled_temporal_expansion_window_unavailable");
|
||||
}
|
||||
const settlementRole = resolveSettlementRole({
|
||||
claimType,
|
||||
counterpartyScope: resolvedAnchors.counterparty_scope ?? [],
|
||||
accountPrefixes,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
if (
|
||||
(claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state") &&
|
||||
(settlementRole === "mixed" || settlementRole === "unknown")
|
||||
) {
|
||||
reasonCodes.push("unresolved_supplier_customer_polarity");
|
||||
}
|
||||
|
||||
return {
|
||||
claim_type: claimType,
|
||||
settlement_role: settlementRole,
|
||||
required_anchors: requiredAnchors,
|
||||
resolved_anchors: resolvedAnchors,
|
||||
missing_anchors: missingAnchors,
|
||||
|
|
@ -288,7 +474,13 @@ function buildCorpusFromItem(item: Record<string, unknown>): string {
|
|||
document_context: item.document_context,
|
||||
relation_pattern_hits: item.relation_pattern_hits,
|
||||
graph_domain_scope: item.graph_domain_scope,
|
||||
lifecycle_markers: item.lifecycle_markers
|
||||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}).toLowerCase();
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +521,16 @@ function requiredChecksByClaim(claimType: ClaimType): string[] {
|
|||
if (claimType === "prove_month_close_state") {
|
||||
return ["close_operation_found", "distribution_step_found", "residual_tail_found"];
|
||||
}
|
||||
if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
return [
|
||||
"amortization_document_found",
|
||||
"fixed_asset_object_identified",
|
||||
"expected_fa_set_reconstructed",
|
||||
"actual_fa_set_reconstructed",
|
||||
"movement_or_posting_link_found",
|
||||
"missing_fa_candidates_assessed"
|
||||
];
|
||||
}
|
||||
return [
|
||||
"rbp_writeoff_document_found",
|
||||
"rbp_object_identified",
|
||||
|
|
@ -348,21 +550,34 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
|||
const hasSettlementAccount = /(?:\b60(?:\.\d{2})?\b|\b62(?:\.\d{2})?\b|payable|receivable|settlement)/i.test(corpus);
|
||||
const hasPosting = /(?:document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasRegister = /(?:register|accumulationregister|accountingregister|регистр)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зачет|tail|хвост)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зач[её]т|tail|хвост)/i.test(corpus);
|
||||
const hasPayment = /(?:payment|оплат|списаниесрасчетногосчета|payment_order|bank_statement)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|счет[- ]фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книгипокупок|книгипродаж|book)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|сч[её]т[- ]?фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книг[аи](?:\s+)?(?:покупок|продаж)|book)/i.test(corpus);
|
||||
const hasChain = /(?:chain|link|document_to_posting|invoice_to_vat|связ)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие\s+месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasDistribution = /(?:distribution|распредел|writeoff|deferred_expense_to_writeoff)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|сч[её]т\s*97|deferred)/i.test(corpus);
|
||||
const hasResidual = /(?:tail|остат|незакры|overdue|period_boundary|terminal_state_gap)/i.test(corpus);
|
||||
const hasContradiction = /(?:contradiction|invalid_transition|normal residual|нормальн)/i.test(corpus);
|
||||
const hasRbpWriteoffDoc = /(?:списани[ея]\s+рбп|rbp_writeoff|deferred_expense_document|writeoff document)/i.test(corpus);
|
||||
const hasRbpObject = /(?:rbp[_\s-]?object|объект\s+рбп|analytics|subkonto|расходыбудущихпериодов)/i.test(corpus);
|
||||
const hasMovement = /(?:movement|движен|хозрасчетный|document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasPeriodEndResidual = /(?:period_boundary|end_period|2020-07-31|остат)/i.test(corpus);
|
||||
const hasFixedAsset = /(?:fixed_asset|asset_card|объект\s+ос|основн(?:ые|ых)?\s+сред|depreciat|амортиз|account[:\s]*0[12]|\b0[12](?:\.\d{2})?\b)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasAmortizationDoc = /(?:depreciat|amortization|начислен[а-я]*\s+амортиз|документ\s+амортиз)/i.test(corpus);
|
||||
const hasExpectedFaSet = /(?:expected_fa_set|expected[_\s-]?set|find_fixed_asset_cards_expected_for_period|expected_set_seed|fa_expected_set_candidate)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasActualFaSet = /(?:actual_fa_set|find_fixed_asset_movements_accounts_01_02|fa_actual_set_candidate|seed_amortization_documents|collect_fa_object_movements)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasFaCoverageCompare = /(?:expected_vs_actual|compare_expected_vs_actual|missing_fa|coverage_compare|missing_fa_candidates)/i.test(
|
||||
corpus
|
||||
);
|
||||
|
||||
if (claimType === "prove_settlement_closure_state") {
|
||||
if (hasPayment) checks.add("payment_document_found");
|
||||
|
|
@ -380,7 +595,7 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
|||
if (hasPosting) checks.add("posting_link_found");
|
||||
} else if (claimType === "prove_vat_chain_completeness") {
|
||||
if (/(?:document|receipt|realization|поступлен|реализац)/i.test(corpus)) checks.add("source_document_found");
|
||||
if (/(?:invoice|счет[- ]фактур)/i.test(corpus)) checks.add("invoice_found");
|
||||
if (/(?:invoice|сч[её]т[- ]?фактур)/i.test(corpus)) checks.add("invoice_found");
|
||||
if (hasRegister || hasVat) checks.add("tax_register_entry_found");
|
||||
if (hasBook) checks.add("book_entry_found");
|
||||
if (hasChain) checks.add("chain_linkage_status");
|
||||
|
|
@ -388,6 +603,13 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
|||
if (hasMonthClose || hasClose) checks.add("close_operation_found");
|
||||
if (hasDistribution) checks.add("distribution_step_found");
|
||||
if (hasResidual) checks.add("residual_tail_found");
|
||||
} else if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
if (hasAmortizationDoc) checks.add("amortization_document_found");
|
||||
if (hasFixedAsset) checks.add("fixed_asset_object_identified");
|
||||
if (hasExpectedFaSet) checks.add("expected_fa_set_reconstructed");
|
||||
if (hasActualFaSet || hasAmortizationDoc) checks.add("actual_fa_set_reconstructed");
|
||||
if (hasMovement || hasPosting) checks.add("movement_or_posting_link_found");
|
||||
if (hasFaCoverageCompare || (hasExpectedFaSet && hasActualFaSet)) checks.add("missing_fa_candidates_assessed");
|
||||
} else {
|
||||
if (hasRbpWriteoffDoc || (hasRbp && hasDistribution)) checks.add("rbp_writeoff_document_found");
|
||||
if (hasRbpObject || hasRbp) checks.add("rbp_object_identified");
|
||||
|
|
@ -540,7 +762,11 @@ function buildDerivedEvidenceFromItem(input: {
|
|||
account_context: Array.isArray(input.item.account_context) ? input.item.account_context : [],
|
||||
account_debit: input.item.account_debit ?? null,
|
||||
account_credit: input.item.account_credit ?? null,
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : []
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : [],
|
||||
fa_object_hint: String(input.item.fa_object_hint ?? "").trim() || null,
|
||||
fa_expected_set_candidate: Boolean(input.item.fa_expected_set_candidate),
|
||||
fa_actual_set_candidate: Boolean(input.item.fa_actual_set_candidate),
|
||||
fa_coverage_status: String(input.item.fa_coverage_status ?? "").trim() || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -553,6 +779,189 @@ function buildClaimStatusTemplate(requiredChecks: string[]): Record<string, "fou
|
|||
return out;
|
||||
}
|
||||
|
||||
function normalizeFaObjectToken(value: string): string | null {
|
||||
const normalized = String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/^live movement row #\d+$/i.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized.slice(0, 140);
|
||||
}
|
||||
|
||||
function periodFromEvidence(evidence: EvidenceItem): string | null {
|
||||
const payload = toObject(evidence.payload);
|
||||
return (
|
||||
String(evidence.source_ref?.period ?? "").trim() ||
|
||||
String(evidence.pointer?.source?.period ?? "").trim() ||
|
||||
String(payload?.period ?? "").trim() ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function collectFaCoverage(input: {
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
}): {
|
||||
expectedSet: string[];
|
||||
actualSet: string[];
|
||||
missingCandidates: string[];
|
||||
uncertainCandidates: string[];
|
||||
relationMap: Array<{
|
||||
fa_object: string;
|
||||
document_amortization: string[];
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
period: string[];
|
||||
coverage_status: "covered" | "missing" | "uncertain";
|
||||
}>;
|
||||
} {
|
||||
const state = new Map<
|
||||
string,
|
||||
{
|
||||
expected: boolean;
|
||||
actual: boolean;
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
docs: Set<string>;
|
||||
periods: Set<string>;
|
||||
}
|
||||
>();
|
||||
|
||||
const touch = (objectName: string) => {
|
||||
const key = objectName.toLowerCase();
|
||||
const existing = state.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created = {
|
||||
expected: false,
|
||||
actual: false,
|
||||
movement: false,
|
||||
posting: false,
|
||||
docs: new Set<string>(),
|
||||
periods: new Set<string>()
|
||||
};
|
||||
state.set(key, created);
|
||||
return created;
|
||||
};
|
||||
|
||||
for (const result of input.retrievalResults) {
|
||||
const items = Array.isArray(result.items) ? result.items : [];
|
||||
for (const item of items) {
|
||||
const objectToken = normalizeFaObjectToken(
|
||||
String(item.fa_object_hint ?? item.display_name ?? item.source_id ?? "").trim()
|
||||
);
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(item.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(item.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify(item).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(item.document_context) ? item.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = String(item.period ?? item.Period ?? "").trim();
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
|
||||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
for (const evidenceItem of evidence) {
|
||||
const payload = toObject(evidenceItem.payload) ?? {};
|
||||
const objectToken = normalizeFaObjectToken(
|
||||
String(payload.fa_object_hint ?? evidenceItem.source_ref?.id ?? evidenceItem.pointer?.source?.id ?? "").trim()
|
||||
);
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(payload.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(payload.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify({
|
||||
payload,
|
||||
mechanism_note: evidenceItem.mechanism_note,
|
||||
source_ref: evidenceItem.source_ref
|
||||
}).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(payload.document_context) ? payload.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = periodFromEvidence(evidenceItem);
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entries = Array.from(state.entries());
|
||||
const expectedSet = entries
|
||||
.filter(([, slot]) => slot.expected)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const actualSet = entries
|
||||
.filter(([, slot]) => slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const expectedResolved = expectedSet.length > 0 ? expectedSet : actualSet;
|
||||
const missingCandidates = expectedResolved.filter((item) => !actualSet.includes(item)).slice(0, 32);
|
||||
const uncertainCandidates = entries
|
||||
.filter(([, slot]) => !slot.expected && !slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const relationMap = entries.slice(0, 48).map(([objectName, slot]) => {
|
||||
const coverageStatus: "covered" | "missing" | "uncertain" =
|
||||
slot.expected && slot.actual ? "covered" : slot.expected && !slot.actual ? "missing" : "uncertain";
|
||||
return {
|
||||
fa_object: objectName,
|
||||
document_amortization: Array.from(slot.docs).slice(0, 4),
|
||||
movement: slot.movement,
|
||||
posting: slot.posting,
|
||||
period: Array.from(slot.periods).slice(0, 4),
|
||||
coverage_status: coverageStatus
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
expectedSet: expectedResolved,
|
||||
actualSet,
|
||||
missingCandidates,
|
||||
uncertainCandidates,
|
||||
relationMap
|
||||
};
|
||||
}
|
||||
|
||||
export function applyTargetedEvidenceAcquisition(input: {
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
claimAudit: ClaimBoundAnchorAudit;
|
||||
|
|
@ -673,6 +1082,21 @@ export function applyTargetedEvidenceAcquisition(input: {
|
|||
reasonCodes.push("targeted_evidence_hit_rate_low");
|
||||
}
|
||||
|
||||
const faCoverage =
|
||||
input.claimAudit.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? collectFaCoverage({
|
||||
retrievalResults: adjustedResults
|
||||
})
|
||||
: null;
|
||||
if (faCoverage) {
|
||||
if (faCoverage.expectedSet.length <= 0) {
|
||||
reasonCodes.push("fa_expected_set_not_reconstructed");
|
||||
}
|
||||
if (faCoverage.actualSet.length <= 0) {
|
||||
reasonCodes.push("fa_actual_set_not_reconstructed");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
retrievalResults: adjustedResults,
|
||||
audit: {
|
||||
|
|
@ -683,7 +1107,18 @@ export function applyTargetedEvidenceAcquisition(input: {
|
|||
targeted_evidence_hits: targetedEvidenceHits,
|
||||
targeted_evidence_hit_rate: targetedEvidenceHitRate,
|
||||
targeted_evidence_source_refs: Array.from(sourceRefs).slice(0, 24),
|
||||
...(faCoverage
|
||||
? {
|
||||
fa_expected_set: faCoverage.expectedSet,
|
||||
fa_actual_set_from_amortization: faCoverage.actualSet,
|
||||
fa_missing_candidates: faCoverage.missingCandidates,
|
||||
fa_uncertain_candidates: faCoverage.uncertainCandidates,
|
||||
fa_relation_map: faCoverage.relationMap
|
||||
}
|
||||
: {}),
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
FEATURE_ASSISTANT_MCP_RUNTIME_V1,
|
||||
FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1
|
||||
} from "../config";
|
||||
import { inferP0DomainFromMessage as inferRuntimeP0DomainHint } from "./investigationState";
|
||||
|
||||
interface SnapshotLink {
|
||||
relation: string;
|
||||
|
|
@ -73,6 +74,7 @@ interface LiveMcpCallPlan {
|
|||
call_id: string;
|
||||
purpose: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
required_for_claim: boolean;
|
||||
account_scope_override?: string[];
|
||||
}>;
|
||||
|
|
@ -82,6 +84,7 @@ interface LiveMcpCallPlan {
|
|||
interface LiveMcpCallExecution {
|
||||
call_id: string;
|
||||
purpose: string;
|
||||
requested_limit: number;
|
||||
required_for_claim: boolean;
|
||||
status: "ok" | "empty" | "error";
|
||||
fetched_rows: number;
|
||||
|
|
@ -197,6 +200,23 @@ const RBP_REQUIRED_LIVE_CALLS = [
|
|||
"compute_end_period_residual_by_rbp_object"
|
||||
];
|
||||
|
||||
const VAT_REQUIRED_LIVE_CALLS = [
|
||||
"find_vat_source_documents_in_period",
|
||||
"find_vat_invoice_links_in_period",
|
||||
"find_vat_register_entries_in_period",
|
||||
"find_vat_book_entries_in_period"
|
||||
];
|
||||
|
||||
const FA_REQUIRED_LIVE_CALLS = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
|
||||
const CLAIM_BOUND_PRIMARY_LIVE_LIMIT = Math.max(ASSISTANT_MCP_LIVE_LIMIT, 96);
|
||||
const CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT = Math.max(ASSISTANT_MCP_LIVE_LIMIT, 128);
|
||||
|
||||
function pushUniqueLine(target: string[], line: string): void {
|
||||
if (!target.includes(line)) {
|
||||
target.push(line);
|
||||
|
|
@ -228,6 +248,13 @@ function parseFiniteNumber(value: unknown): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function resolveLiveCallLimit(limit: unknown): number {
|
||||
if (typeof limit === "number" && Number.isFinite(limit)) {
|
||||
return Math.max(1, Math.trunc(limit));
|
||||
}
|
||||
return ASSISTANT_MCP_LIVE_LIMIT;
|
||||
}
|
||||
|
||||
function formatIsoDateUtc(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
|
|
@ -296,9 +323,119 @@ function hasRbpSignal(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function hasFixedAssetAmortizationSignal(text: string): boolean {
|
||||
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(
|
||||
String(text ?? "").toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
|
||||
const faClaim =
|
||||
preferredDomainHint === "fixed_asset_amortization" ||
|
||||
hasFixedAssetAmortizationSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
|
||||
semanticProfile.domain_scope.includes("fixed_assets");
|
||||
if (faClaim) {
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
query_subject: "fixed_asset_amortization_coverage",
|
||||
required_live_calls: [...FA_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_amortization_documents_in_period",
|
||||
purpose: "seed_amortization_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_movements_accounts_01_02",
|
||||
purpose: "collect_fa_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_cards_expected_for_period",
|
||||
purpose: "build_expected_fa_set",
|
||||
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "match_expected_vs_actual_fa_coverage",
|
||||
purpose: "compare_expected_vs_actual_fa_coverage",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const vatClaim =
|
||||
preferredDomainHint === "vat_document_register_book" ||
|
||||
semanticProfile.query_subject === "vat_chain_conflict" ||
|
||||
semanticProfile.domain_scope.includes("vat") ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|книга покупок|книга продаж|register)/i.test(String(fragmentText ?? "").toLowerCase());
|
||||
if (vatClaim) {
|
||||
return {
|
||||
claim_type: "prove_vat_chain_completeness",
|
||||
query_subject: "vat_chain_conflict",
|
||||
required_live_calls: [...VAT_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_vat_source_documents_in_period",
|
||||
purpose: "seed_vat_source_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_invoice_links_in_period",
|
||||
purpose: "collect_invoice_links",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_register_entries_in_period",
|
||||
purpose: "collect_vat_register_entries",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_book_entries_in_period",
|
||||
purpose: "collect_vat_book_entries",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const rbpClaim =
|
||||
(preferredDomainHint === "month_close_costs_20_44" && hasRbpSignal(fragmentText)) ||
|
||||
hasRbpSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "deferred_expense_lifecycle_anomaly" ||
|
||||
semanticProfile.domain_scope.includes("deferred_expense");
|
||||
|
|
@ -319,12 +456,6 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
|||
};
|
||||
}
|
||||
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
|
||||
return {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
query_subject: "deferred_expense_lifecycle_anomaly",
|
||||
|
|
@ -333,28 +464,32 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
|||
{
|
||||
call_id: "find_rbp_writeoff_documents_in_period",
|
||||
purpose: "seed_writeoff_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "find_rbp_object_movements_account_97",
|
||||
purpose: "collect_rbp_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97"]
|
||||
},
|
||||
{
|
||||
call_id: "find_month_close_entries_linked_to_rbp",
|
||||
purpose: "link_month_close_to_rbp",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "compute_end_period_residual_by_rbp_object",
|
||||
purpose: "collect_residual_tail_signals",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
}
|
||||
|
|
@ -1614,11 +1749,32 @@ const WRONG_DOCUMENT_MARKERS =
|
|||
const REPEATED_ANOMALY_MARKERS =
|
||||
/(?:\u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449|\u0441\u0435\u0440\u0438\u0439\u043d|\u043f\u0430\u0442\u0442\u0435\u0440\u043d|repeat(?:ed|ability)?)/iu;
|
||||
|
||||
function inferQuerySubject(text: string, domains: string[], anomalies: string[]): string {
|
||||
function inferQuerySubject(
|
||||
text: string,
|
||||
domains: string[],
|
||||
anomalies: string[],
|
||||
preferredDomainHint: string | null
|
||||
): string {
|
||||
if (preferredDomainHint === "vat_document_register_book") {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (preferredDomainHint === "fixed_asset_amortization") {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (preferredDomainHint === "month_close_costs_20_44") {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
if (preferredDomainHint === "settlements_60_62") {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
|
||||
const lower = text.toLowerCase();
|
||||
if ((domains.includes("bank") || domains.includes("settlements")) && WRONG_DOCUMENT_MARKERS.test(lower)) {
|
||||
return "bank_settlement_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("suppliers")) {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
|
|
@ -1631,9 +1787,6 @@ function inferQuerySubject(text: string, domains: string[], anomalies: string[])
|
|||
if (domains.includes("fixed_assets")) {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("period_close")) {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
|
|
@ -1784,9 +1937,10 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
|||
relationPatterns: dedupedRelations,
|
||||
anomalyPatterns: dedupedAnomalies
|
||||
});
|
||||
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
||||
|
||||
return {
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies),
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies, preferredDomainHint),
|
||||
account_scope: dedupedAccounts,
|
||||
subaccount_scope: [],
|
||||
domain_scope: dedupedDomains,
|
||||
|
|
@ -2773,10 +2927,14 @@ export class AssistantDataLayer {
|
|||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||
const accountScope =
|
||||
explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? ["01", "02", "08"]
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? ["19", "68"]
|
||||
: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? ["97", "20", "25", "26", "44"]
|
||||
: explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
: [];
|
||||
const callExecutions: LiveMcpCallExecution[] = [];
|
||||
const collectedRows: Array<Record<string, unknown>> = [];
|
||||
|
|
@ -2785,6 +2943,7 @@ export class AssistantDataLayer {
|
|||
let matchedRowsTotal = 0;
|
||||
|
||||
for (const call of livePlan.calls) {
|
||||
const callLimit = resolveLiveCallLimit(call.limit);
|
||||
const callAccountScope =
|
||||
Array.isArray(call.account_scope_override) && call.account_scope_override.length > 0
|
||||
? call.account_scope_override
|
||||
|
|
@ -2792,7 +2951,7 @@ export class AssistantDataLayer {
|
|||
try {
|
||||
const payload = await this.fetchJsonWithTimeout(endpoint, {
|
||||
query: call.query,
|
||||
limit: ASSISTANT_MCP_LIVE_LIMIT
|
||||
limit: callLimit
|
||||
});
|
||||
const parsed = this.parseExecuteQueryPayload(payload);
|
||||
if (parsed.error) {
|
||||
|
|
@ -2800,6 +2959,7 @@ export class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
|
|
@ -2827,6 +2987,7 @@ export class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: rowsForAnswer.length > 0 ? "ok" : "empty",
|
||||
fetched_rows: parsed.rows.length,
|
||||
|
|
@ -2840,6 +3001,7 @@ export class AssistantDataLayer {
|
|||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
|
|
@ -2864,7 +3026,11 @@ export class AssistantDataLayer {
|
|||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
claim_type: item.claim_type
|
||||
claim_type: item.claim_type,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}));
|
||||
|
||||
const executedRequiredCalls = callExecutions
|
||||
|
|
@ -2917,7 +3083,13 @@ export class AssistantDataLayer {
|
|||
route,
|
||||
channel: ASSISTANT_MCP_CHANNEL,
|
||||
proxy: ASSISTANT_MCP_PROXY_URL,
|
||||
source_profile: livePlan.claim_type ? "claim_bound_rbp_live_path" : "generic_live_probe",
|
||||
source_profile: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? "claim_bound_rbp_live_path"
|
||||
: livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? "claim_bound_fa_live_path"
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? "claim_bound_vat_live_path"
|
||||
: "generic_live_probe",
|
||||
claim_type: livePlan.claim_type,
|
||||
query_subject: livePlan.query_subject,
|
||||
account_scope: accountScope,
|
||||
|
|
@ -3111,29 +3283,57 @@ export class AssistantDataLayer {
|
|||
const querySubject = valueAsString(row.__query_subject ?? "").trim() || null;
|
||||
const registratorLower = registrator.toLowerCase();
|
||||
const hasRbpByDocument = /(?:рбп|deferred|списани[ея]\s+рбп)/i.test(registratorLower);
|
||||
const hasFaByDocument = /(?:амортиз|depreciat|основн(?:ые|ых)\s+сред|fixed\s*asset)/i.test(registratorLower);
|
||||
const hasAccount97 = accountContext.some((item) => /^97(?:\.|$)/.test(item));
|
||||
const hasFixedAssetAccount = accountContext.some((item) => /^(?:01|02|08)(?:\.|$)/.test(item));
|
||||
const hasCloseDoc =
|
||||
/(?:закрыти[ея]\s+месяц|period\s*close|month\s*close|close\s+operation)/i.test(registratorLower) ||
|
||||
callId.includes("month_close");
|
||||
const faExpectedSetCandidate = callId === "find_fixed_asset_cards_expected_for_period" || callPurpose === "build_expected_fa_set";
|
||||
const faActualSetCandidate =
|
||||
callId === "find_amortization_documents_in_period" ||
|
||||
callId === "find_fixed_asset_movements_accounts_01_02" ||
|
||||
callPurpose === "seed_amortization_documents" ||
|
||||
callPurpose === "collect_fa_object_movements";
|
||||
const faCoverageStatus =
|
||||
callId === "match_expected_vs_actual_fa_coverage"
|
||||
? "expected_vs_actual_compare"
|
||||
: faExpectedSetCandidate && faActualSetCandidate
|
||||
? "covered"
|
||||
: faExpectedSetCandidate
|
||||
? "expected_only"
|
||||
: faActualSetCandidate
|
||||
? "actual_only"
|
||||
: null;
|
||||
const faObjectHint =
|
||||
(registrator || "").trim() ||
|
||||
`${debit || "n/a"}|${credit || "n/a"}|${amount !== null ? amount : "n/a"}`;
|
||||
const relationPatternHits = uniqueStrings([
|
||||
"document_to_posting",
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_to_writeoff" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "asset_card_to_depreciation" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "expected_vs_actual_coverage_compare" : "",
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
callId.includes("residual") ? "residuals_zero_or_explained" : ""
|
||||
]);
|
||||
const documentContext = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_document" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "depreciation_document" : "",
|
||||
hasCloseDoc ? "period_close_document" : "",
|
||||
"posting"
|
||||
]);
|
||||
const graphDomainScope = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "fixed_asset" : "",
|
||||
hasCloseDoc ? "period_close" : ""
|
||||
]);
|
||||
const lifecycleMarkers = uniqueStrings([
|
||||
callId.includes("residual") ? "period_boundary" : "",
|
||||
callId.includes("residual") ? "tail_state_observed" : "",
|
||||
hasCloseDoc ? "close_operation" : ""
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "amortization_accrual" : "",
|
||||
faExpectedSetCandidate ? "expected_set_seed" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "coverage_compare" : ""
|
||||
]);
|
||||
return {
|
||||
source_entity: "MCPLiveMovement",
|
||||
|
|
@ -3153,6 +3353,10 @@ export class AssistantDataLayer {
|
|||
claim_type: claimType,
|
||||
query_subject: querySubject,
|
||||
amount,
|
||||
fa_object_hint: faObjectHint,
|
||||
fa_expected_set_candidate: faExpectedSetCandidate,
|
||||
fa_actual_set_candidate: faActualSetCandidate,
|
||||
fa_coverage_status: faCoverageStatus,
|
||||
source_layer: "mcp_live_probe",
|
||||
route
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import type { EvidenceItem } from "../types/stage1Contracts";
|
|||
import type { ProblemUnit } from "../types/stage2ProblemUnits";
|
||||
import type { ClaimBoundAnchorAudit } from "./assistantClaimBoundEvidence";
|
||||
|
||||
type P0DomainHint = "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44" | null;
|
||||
type P0DomainHint =
|
||||
| "settlements_60_62"
|
||||
| "vat_document_register_book"
|
||||
| "month_close_costs_20_44"
|
||||
| "fixed_asset_amortization"
|
||||
| null;
|
||||
|
||||
const JULY_YEAR = "2020";
|
||||
const JULY_MONTH = "07";
|
||||
|
|
@ -161,10 +166,37 @@ function collectPercentLikeSpans(text: string): Array<{ start: number; end: numb
|
|||
return spans;
|
||||
}
|
||||
|
||||
function collectContractLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function intersectsSpan(start: number, end: number, spans: Array<{ start: number; end: number }>): boolean {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
|
||||
function hasAccountContextAround(text: string, start: number, end: number): boolean {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(
|
||||
`${left} ${right}`
|
||||
);
|
||||
}
|
||||
|
||||
interface AccountExtractionAudit {
|
||||
resolved_account_anchors: string[];
|
||||
raw_numeric_tokens: string[];
|
||||
|
|
@ -181,7 +213,12 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
|||
const dateSpans = collectDateLikeSpans(lower);
|
||||
const amountSpans = collectAmountLikeSpans(lower);
|
||||
const percentSpans = collectPercentLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans];
|
||||
const contractSpans = collectContractLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans, ...contractSpans];
|
||||
const hasAccountingLexeme =
|
||||
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(
|
||||
lower
|
||||
);
|
||||
const contextualPattern =
|
||||
/(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextualMatch: RegExpExecArray | null = null;
|
||||
|
|
@ -246,6 +283,14 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
|||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (intersectsSpan(start, end, contractSpans)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
|
|
@ -254,6 +299,14 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
|||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountingLexeme || !hasAccountContextAround(lower, start, end)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
accounts.add(token);
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
|
|
@ -509,7 +562,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
|
|||
const raw = String(rawText ?? "");
|
||||
const lower = raw.toLowerCase();
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july|РёСЋР»(?:СЏ|СЊ)?)(?:\D|$)/i);
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
|
|
@ -1044,6 +1097,10 @@ function isMonthClosePrefix(prefix: string): boolean {
|
|||
return numeric >= 20 && numeric <= 44;
|
||||
}
|
||||
|
||||
function isFixedAssetPrefix(prefix: string): boolean {
|
||||
return prefix === "01" || prefix === "02" || prefix === "08";
|
||||
}
|
||||
|
||||
function expectedAccountPrefixes(input: {
|
||||
focusDomainHint: P0DomainHint;
|
||||
polarity: DomainPolarity;
|
||||
|
|
@ -1062,6 +1119,9 @@ function expectedAccountPrefixes(input: {
|
|||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
return ["20", "25", "26", "44", "97", "01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return ["01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
if (input.polarity === "supplier_payable") {
|
||||
return ["60", "51", "76"];
|
||||
|
|
@ -1121,6 +1181,13 @@ function hasWrongDomainByAccounts(accounts: string[], focusDomainHint: P0DomainH
|
|||
if (focusDomainHint === "month_close_costs_20_44") {
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
|
||||
}
|
||||
if (focusDomainHint === "fixed_asset_amortization") {
|
||||
const hasFixedAsset = prefixes.some((prefix) => isFixedAssetPrefix(prefix));
|
||||
if (hasFixedAsset) {
|
||||
return false;
|
||||
}
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix) || isMonthClosePrefix(prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1531,3 +1598,4 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -193,7 +193,8 @@ function hasP0ClaimSignal(claimType, focusDomainHint) {
|
|||
claim === "prove_advance_offset_state" ||
|
||||
claim === "prove_vat_chain_completeness" ||
|
||||
claim === "prove_month_close_state" ||
|
||||
claim === "prove_rbp_tail_state") {
|
||||
claim === "prove_rbp_tail_state" ||
|
||||
claim === "prove_fixed_asset_amortization_coverage") {
|
||||
return true;
|
||||
}
|
||||
return (focusDomainHint === "settlements_60_62" ||
|
||||
|
|
@ -331,6 +332,24 @@ function collectDateSpans(text) {
|
|||
}
|
||||
return spans;
|
||||
}
|
||||
function collectContractSpans(text) {
|
||||
const spans = [];
|
||||
const contractPatterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const contractPattern of contractPatterns) {
|
||||
let match = null;
|
||||
while ((match = contractPattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function collectAmountSpans(text) {
|
||||
const spans = [];
|
||||
const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
|
|
@ -360,6 +379,11 @@ function collectPercentSpans(text) {
|
|||
function intersectsAnySpan(start, end, spans) {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
function hasAccountContextAround(text, start, end) {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
|
||||
}
|
||||
function extractAccountTokens(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const explicitAccounts = new Set();
|
||||
|
|
@ -432,7 +456,8 @@ function extractAccountTokens(text) {
|
|||
if (explicitAccounts.size > 0) {
|
||||
return Array.from(explicitAccounts);
|
||||
}
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const contractSpans = collectContractSpans(lower);
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...contractSpans];
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
|
||||
if (!hasAccountingLexeme) {
|
||||
return [];
|
||||
|
|
@ -447,6 +472,9 @@ function extractAccountTokens(text) {
|
|||
if (intersectsAnySpan(start, end, spans)) {
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountContextAround(lower, start, end)) {
|
||||
continue;
|
||||
}
|
||||
const prefix = value.match(/^(\d{2})/)?.[1];
|
||||
if (!prefix || !knownAccountPrefixes.has(prefix)) {
|
||||
continue;
|
||||
|
|
@ -735,6 +763,164 @@ function collectRbpLiveRouteAudit(input) {
|
|||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function enrichFaFragmentForLive(fragmentText, temporalGuard) {
|
||||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||||
const hints = [
|
||||
"Начисление амортизации",
|
||||
"объект ОС",
|
||||
"expected set ОС",
|
||||
"счет 01/02"
|
||||
];
|
||||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||||
if (effective && effective.from && effective.to) {
|
||||
hints.push(`период ${effective.from}..${effective.to}`);
|
||||
}
|
||||
const hintText = hints.filter(Boolean).join(", ");
|
||||
if (!base) {
|
||||
return hintText;
|
||||
}
|
||||
if (/амортиз|основн(?:ые|ых)\s+сред|fixed\s*asset|depreciat|счет\s*0[12]|account\s*0[12]/i.test(base)) {
|
||||
return base;
|
||||
}
|
||||
return `${base}; ${hintText}`;
|
||||
}
|
||||
function enforceFaLiveRoutePlan(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return {
|
||||
executionPlan: input.executionPlan,
|
||||
audit: null
|
||||
};
|
||||
}
|
||||
const requiredLiveCalls = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
let routeAdjusted = 0;
|
||||
let rescuedNoRoute = 0;
|
||||
const replacedRoutes = [];
|
||||
const adjustedPlan = input.executionPlan.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return item;
|
||||
}
|
||||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||||
rescuedNoRoute += 1;
|
||||
routeAdjusted += 1;
|
||||
return {
|
||||
...item,
|
||||
route: "live_mcp_drilldown",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||||
routeAdjusted += 1;
|
||||
if (item.route && item.route !== "no_route") {
|
||||
replacedRoutes.push(String(item.route));
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
route: "hybrid_store_plus_live",
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true) {
|
||||
return {
|
||||
...item,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
executionPlan: adjustedPlan,
|
||||
audit: {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredLiveCalls,
|
||||
route_adjustments_applied: routeAdjusted,
|
||||
rescued_no_route_fragments: rescuedNoRoute,
|
||||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||||
route_gap_reason: routeAdjusted > 0 ? "fa_claim_bound_live_route_override_applied" : null
|
||||
}
|
||||
};
|
||||
}
|
||||
function collectFaLiveRouteAudit(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return null;
|
||||
}
|
||||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||||
const executed = [];
|
||||
const missing = new Set();
|
||||
const routeGaps = [];
|
||||
let matchedRowsTotal = 0;
|
||||
let returnedRowsTotal = 0;
|
||||
let fetchedRowsTotal = 0;
|
||||
for (const result of input.retrievalResults) {
|
||||
if (!result || typeof result !== "object") {
|
||||
continue;
|
||||
}
|
||||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||||
if (!live) {
|
||||
continue;
|
||||
}
|
||||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||||
for (const callId of requiredCalls) {
|
||||
required.add(String(callId ?? "").trim());
|
||||
}
|
||||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||||
for (const call of executedCalls) {
|
||||
if (!call || typeof call !== "object") {
|
||||
continue;
|
||||
}
|
||||
executed.push(call);
|
||||
}
|
||||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||||
for (const callId of missingCalls) {
|
||||
const token = String(callId ?? "").trim();
|
||||
if (token) {
|
||||
missing.add(token);
|
||||
}
|
||||
}
|
||||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||||
if (routeGapReason) {
|
||||
routeGaps.push(routeGapReason);
|
||||
}
|
||||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||||
}
|
||||
const requiredList = Array.from(required).filter(Boolean);
|
||||
const executedList = executed;
|
||||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||||
for (const callId of missingFromExecuted) {
|
||||
missing.add(callId);
|
||||
}
|
||||
const missingList = Array.from(missing);
|
||||
const routeGapReason = missingList.length > 0
|
||||
? "required_live_calls_not_executed"
|
||||
: matchedRowsTotal <= 0
|
||||
? "claim_live_calls_executed_but_zero_matches"
|
||||
: routeGaps[0] ?? null;
|
||||
const executionRate = requiredList.length > 0
|
||||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||||
: 1;
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredList,
|
||||
executed_live_calls: executedList,
|
||||
missing_live_calls: missingList,
|
||||
route_gap_reason: routeGapReason,
|
||||
live_route_execution_rate: executionRate,
|
||||
fetched_rows_total: fetchedRowsTotal,
|
||||
matched_rows_total: matchedRowsTotal,
|
||||
returned_rows_total: returnedRowsTotal,
|
||||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function toDebugRoutes(routeSummary) {
|
||||
if (!routeSummary) {
|
||||
return [];
|
||||
|
|
@ -1201,7 +1387,7 @@ function extractNormalizedPeriodLiteral(text) {
|
|||
}
|
||||
function extractFollowupAccountAnchorsLoose(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||||
const anchors = [];
|
||||
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
|
||||
let match = null;
|
||||
|
|
@ -1254,27 +1440,8 @@ function hasCrossScopeConflictWithState(userMessage, state) {
|
|||
return false;
|
||||
}
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountTokens(lower);
|
||||
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||||
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||||
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||||
const hasFixedAssetAccount = accountTokens.some((token) => /^(?:01|02|08)(?:\.|$)/.test(token));
|
||||
const vatLexical = /(?:ндс|vat|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||||
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||||
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат)/i.test(lower);
|
||||
const fixedAssetLexical = /(?:основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(lower);
|
||||
if (hasVatAccount || vatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (fixedAssetLexical || hasFixedAssetAccount) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (monthCloseLexical || hasMonthCloseAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || settlementLexical) {
|
||||
return "settlements_60_62";
|
||||
if (typeof investigationState_1.inferP0DomainFromMessage === "function") {
|
||||
return investigationState_1.inferP0DomainFromMessage(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1554,11 +1721,10 @@ export class AssistantService {
|
|||
routeSummary: normalized.route_hint_summary
|
||||
});
|
||||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||||
const focusDomainForGuards = inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? "month_close_costs_20_44"
|
||||
: inferredDomainByMessage === "settlements_60_62" ||
|
||||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44"
|
||||
inferredDomainByMessage === "month_close_costs_20_44" ||
|
||||
inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||||
|
|
@ -1592,6 +1758,12 @@ export class AssistantService {
|
|||
temporalGuard
|
||||
});
|
||||
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||
const faRoutePlanEnforcement = enforceFaLiveRoutePlan({
|
||||
executionPlan,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
temporalGuard
|
||||
});
|
||||
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
|
|
@ -1679,6 +1851,11 @@ export class AssistantService {
|
|||
retrievalResults,
|
||||
planAudit: rbpRoutePlanEnforcement.audit
|
||||
});
|
||||
const faLiveRouteAudit = collectFaLiveRouteAudit({
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
retrievalResults,
|
||||
planAudit: faRoutePlanEnforcement.audit
|
||||
});
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
|
|
@ -1792,6 +1969,7 @@ export class AssistantService {
|
|||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
|
@ -1884,6 +2062,8 @@ export class AssistantService {
|
|||
claim_anchor_audit: claimAnchorAudit,
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
|
|
|||
|
|
@ -112,6 +112,25 @@ function collectDateLikeSpans(text: string): Array<{ start: number; end: number
|
|||
return spans;
|
||||
}
|
||||
|
||||
function collectContractLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function collectAmountLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
|
|
@ -154,7 +173,12 @@ function hasAccountContextAround(text: string, start: number, end: number): bool
|
|||
|
||||
function detectAccounts(text: string): string[] {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const blockedSpans = [...collectDateLikeSpans(lower), ...collectAmountLikeSpans(lower), ...collectPercentLikeSpans(lower)];
|
||||
const blockedSpans = [
|
||||
...collectDateLikeSpans(lower),
|
||||
...collectAmountLikeSpans(lower),
|
||||
...collectPercentLikeSpans(lower),
|
||||
...collectContractLikeSpans(lower)
|
||||
];
|
||||
const hasAccountingLexeme =
|
||||
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|расч[её]т|аванс|долг|settlement|payment|supplier|customer|ндс|vat|рбп|deferred|амортиз)/iu.test(
|
||||
lower
|
||||
|
|
@ -217,37 +241,47 @@ function detectPeriod(text: string): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function detectExplicitDomainHint(text: string): string | null {
|
||||
export function inferP0DomainFromMessage(text: string): string | null {
|
||||
const messageCorpus = String(text ?? "").toLowerCase();
|
||||
const accounts = detectAccounts(text);
|
||||
const hasSettlementSignal =
|
||||
accounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
|
||||
const hasSettlementAccount = accounts.some((item) => isSettlementAccount(item));
|
||||
const hasVatAccount = accounts.some((item) => isVatAccount(item));
|
||||
const hasCloseAccount = accounts.some((item) => isCloseCostsAccount(item));
|
||||
const hasFixedAssetAccount = accounts.some((item) => isFixedAssetAccount(item));
|
||||
|
||||
const hasSettlementLexical = /(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasSettlementSignal) {
|
||||
const hasVatLexical = /(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
const hasCloseLexical =
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
const hasExplicitFixedAssetLexical =
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasBroadMonthCloseLexical =
|
||||
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|month\s*close|period\s*close|регламентн)/i.test(messageCorpus);
|
||||
|
||||
// Keep settlement lane stable when 60/62 lexical/account anchors are explicit
|
||||
// and there is no explicit VAT intent.
|
||||
if ((hasSettlementAccount || hasSettlementLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal =
|
||||
accounts.some((item) => isVatAccount(item)) ||
|
||||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal =
|
||||
accounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
if (hasCloseSignal) {
|
||||
if ((hasCloseAccount || hasCloseLexical || hasBroadMonthCloseLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical && !hasFixedAssetAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const hasFixedAssetSignal =
|
||||
accounts.some((item) => isFixedAssetAccount(item)) ||
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasFixedAssetSignal) {
|
||||
if (hasVatAccount || hasVatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (hasFixedAssetAccount || hasExplicitFixedAssetLexical) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (hasCloseAccount || hasCloseLexical) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || hasSettlementLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +317,7 @@ function deriveScopeOrigin(input: {
|
|||
}
|
||||
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
|
||||
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
|
||||
const explicitDomain = detectExplicitDomainHint(input.userMessage);
|
||||
const explicitDomain = inferP0DomainFromMessage(input.userMessage);
|
||||
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
|
||||
return "explicit_from_message";
|
||||
}
|
||||
|
|
@ -406,7 +440,7 @@ function inferFollowupActiveDomain(input: {
|
|||
: messageCorpus;
|
||||
|
||||
const hasFixedAssetLexicalSignal =
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasFixedAssetAccountSignal =
|
||||
|
|
@ -414,6 +448,17 @@ function inferFollowupActiveDomain(input: {
|
|||
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasBroadMonthCloseSignal =
|
||||
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|регламентн|month\s*close|period\s*close)/i.test(messageCorpus);
|
||||
if (
|
||||
(input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus) ||
|
||||
hasBroadMonthCloseSignal) &&
|
||||
!hasFixedAssetLexicalSignal &&
|
||||
!hasFixedAssetAccountSignal
|
||||
) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,9 @@ export interface ClaimBoundAnchorAuditDebug {
|
|||
| "prove_advance_offset_state"
|
||||
| "prove_vat_chain_completeness"
|
||||
| "prove_month_close_state"
|
||||
| "prove_rbp_tail_state";
|
||||
| "prove_rbp_tail_state"
|
||||
| "prove_fixed_asset_amortization_coverage";
|
||||
settlement_role?: "supplier" | "customer" | "mixed" | "unknown";
|
||||
required_anchors: string[];
|
||||
resolved_anchors: Record<string, string[]>;
|
||||
missing_anchors: string[];
|
||||
|
|
@ -144,13 +146,26 @@ export interface TargetedEvidenceAcquisitionDebug {
|
|||
| "prove_advance_offset_state"
|
||||
| "prove_vat_chain_completeness"
|
||||
| "prove_month_close_state"
|
||||
| "prove_rbp_tail_state";
|
||||
| "prove_rbp_tail_state"
|
||||
| "prove_fixed_asset_amortization_coverage";
|
||||
required_checks: string[];
|
||||
check_status: Record<string, "found" | "not_found">;
|
||||
targeted_item_hits: number;
|
||||
targeted_evidence_hits: number;
|
||||
targeted_evidence_hit_rate: number;
|
||||
targeted_evidence_source_refs: string[];
|
||||
fa_expected_set?: string[];
|
||||
fa_actual_set_from_amortization?: string[];
|
||||
fa_missing_candidates?: string[];
|
||||
fa_uncertain_candidates?: string[];
|
||||
fa_relation_map?: Array<{
|
||||
fa_object: string;
|
||||
document_amortization: string[];
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
period: string[];
|
||||
coverage_status: "covered" | "missing" | "uncertain";
|
||||
}>;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +235,19 @@ export interface RbpLiveRouteAuditDebug {
|
|||
plan_override: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface FaLiveRouteAuditDebug {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage";
|
||||
required_live_calls: string[];
|
||||
executed_live_calls: Array<Record<string, unknown>>;
|
||||
missing_live_calls: string[];
|
||||
route_gap_reason: string | null;
|
||||
live_route_execution_rate: number;
|
||||
fetched_rows_total: number;
|
||||
matched_rows_total: number;
|
||||
returned_rows_total: number;
|
||||
plan_override: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AssistantMessageRequestPayload {
|
||||
session_id?: string;
|
||||
user_message?: string;
|
||||
|
|
@ -316,6 +344,7 @@ export interface AssistantDebugPayload {
|
|||
targeted_evidence_acquisition?: TargetedEvidenceAcquisitionDebug;
|
||||
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
|
||||
rbp_live_route_audit?: RbpLiveRouteAuditDebug;
|
||||
fa_live_route_audit?: FaLiveRouteAuditDebug;
|
||||
eligibility_time_basis?: GroundedAnswerEligibilityGuardDebug["eligibility_time_basis"];
|
||||
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
|
||||
followup_state_usage?: FollowupStateUsageDebug;
|
||||
|
|
|
|||
|
|
@ -201,7 +201,111 @@ describe.sequential("assistant MCP runtime bridge", () => {
|
|||
expect((liveSummary.required_live_calls as unknown[]).length).toBe(4);
|
||||
expect(Array.isArray(liveSummary.executed_live_calls)).toBe(true);
|
||||
expect((liveSummary.executed_live_calls as unknown[]).length).toBe(4);
|
||||
const rbpCallLimits = fetchMock.mock.calls.map(([, requestInit]) => {
|
||||
const init = requestInit as { body?: string };
|
||||
return Number(JSON.parse(String(init.body ?? "{}")).limit ?? 0);
|
||||
});
|
||||
expect(rbpCallLimits).toEqual([96, 96, 96, 128]);
|
||||
expect(liveSummary.matched_rows).toBeGreaterThan(0);
|
||||
expect(result.items.some((item) => Array.isArray((item as Record<string, unknown>).relation_pattern_hits))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses claim-bound live call sequence for fixed-asset amortization coverage query", async () => {
|
||||
process.env[MCP_FLAG] = "1";
|
||||
process.env[MCP_PROXY] = "http://127.0.0.1:6003";
|
||||
process.env[MCP_CHANNEL] = "default";
|
||||
|
||||
const payload = JSON.stringify({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
period: "2020-07-31T00:00:00",
|
||||
registrator: "Начисление амортизации Июль 2020",
|
||||
account_dt: "20.01",
|
||||
account_kt: "02.01",
|
||||
amount: 2471.52
|
||||
},
|
||||
{
|
||||
period: "2020-07-31T00:00:00",
|
||||
registrator: "Начисление амортизации Июль 2020",
|
||||
account_dt: "20.01",
|
||||
account_kt: "02.01",
|
||||
amount: 2465.28
|
||||
}
|
||||
]
|
||||
});
|
||||
const fetchMock = vi.fn(async () => new Response(payload, { status: 200 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { AssistantDataLayer } = await import("../src/services/assistantDataLayer");
|
||||
const dataLayer = new AssistantDataLayer(createSnapshotRoot());
|
||||
const result = await dataLayer.executeRouteRuntime(
|
||||
"hybrid_store_plus_live",
|
||||
"31 июля начислена амортизация тремя суммами — 2 471,52, 2 465,28 и 849,83. Есть риск, что объект ОС не попал в амортизацию?"
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4);
|
||||
const summary = result.summary as Record<string, unknown>;
|
||||
const liveSummary = summary.live_mcp as Record<string, unknown>;
|
||||
expect(liveSummary.claim_type).toBe("prove_fixed_asset_amortization_coverage");
|
||||
expect(liveSummary.source_profile).toBe("claim_bound_fa_live_path");
|
||||
expect(Array.isArray(liveSummary.required_live_calls)).toBe(true);
|
||||
expect((liveSummary.required_live_calls as unknown[]).length).toBe(4);
|
||||
expect(Array.isArray(liveSummary.executed_live_calls)).toBe(true);
|
||||
expect((liveSummary.executed_live_calls as unknown[]).length).toBe(4);
|
||||
const faCallLimits = fetchMock.mock.calls.map(([, requestInit]) => {
|
||||
const init = requestInit as { body?: string };
|
||||
return Number(JSON.parse(String(init.body ?? "{}")).limit ?? 0);
|
||||
});
|
||||
expect(faCallLimits).toEqual([96, 96, 128, 128]);
|
||||
expect(liveSummary.matched_rows).toBeGreaterThan(0);
|
||||
expect(result.items.some((item) => (item as Record<string, unknown>).fa_expected_set_candidate === true)).toBe(true);
|
||||
expect(result.items.some((item) => (item as Record<string, unknown>).fa_actual_set_candidate === true)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses claim-bound VAT live path instead of supplier-tail generic probe for VAT chain query", async () => {
|
||||
process.env[MCP_FLAG] = "1";
|
||||
process.env[MCP_PROXY] = "http://127.0.0.1:6003";
|
||||
process.env[MCP_CHANNEL] = "default";
|
||||
|
||||
const payload = JSON.stringify({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
period: "2020-07-15T00:00:00",
|
||||
registrator: "Реализация товаров 0001",
|
||||
account_dt: "62.01",
|
||||
account_kt: "90.01",
|
||||
amount: 1400
|
||||
},
|
||||
{
|
||||
period: "2020-07-15T00:00:00",
|
||||
registrator: "Счет-фактура выданный 0001",
|
||||
account_dt: "90.03",
|
||||
account_kt: "68.02",
|
||||
amount: 233.33
|
||||
}
|
||||
]
|
||||
});
|
||||
const fetchMock = vi.fn(async () => new Response(payload, { status: 200 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { AssistantDataLayer } = await import("../src/services/assistantDataLayer");
|
||||
const dataLayer = new AssistantDataLayer(createSnapshotRoot());
|
||||
const result = await dataLayer.executeRouteRuntime(
|
||||
"hybrid_store_plus_live",
|
||||
"По поставщику и счету-фактуре проверь НДС-цепочку: есть ли выпадение между документом, регистром и книгой покупок?"
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4);
|
||||
const summary = result.summary as Record<string, unknown>;
|
||||
const liveSummary = summary.live_mcp as Record<string, unknown>;
|
||||
expect(liveSummary.claim_type).toBe("prove_vat_chain_completeness");
|
||||
expect(liveSummary.query_subject).toBe("vat_chain_conflict");
|
||||
expect(liveSummary.source_profile).toBe("claim_bound_vat_live_path");
|
||||
expect(Array.isArray(liveSummary.required_live_calls)).toBe(true);
|
||||
expect((liveSummary.required_live_calls as unknown[]).length).toBe(4);
|
||||
expect(Array.isArray(liveSummary.account_scope)).toBe(true);
|
||||
expect(liveSummary.account_scope).toEqual(["19", "68"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
resolveDomainPolarityGuard,
|
||||
resolveTemporalGuard
|
||||
} from "../src/services/assistantRuntimeGuards";
|
||||
import { applyTargetedEvidenceAcquisition, resolveClaimBoundAnchors } from "../src/services/assistantClaimBoundEvidence";
|
||||
import { inferP0DomainFromMessage } from "../src/services/investigationState";
|
||||
|
||||
function buildProblemUnit(input: {
|
||||
id: string;
|
||||
|
|
@ -129,7 +131,7 @@ function buildRetrieval(input?: Partial<any>): any {
|
|||
|
||||
describe("stage4 blocker-pack runtime guards", () => {
|
||||
it("flags temporal anchor drift outside July 2020 snapshot", () => {
|
||||
const userMessage = "Почему по оплате от 6 июля 2020 долг по поставщику остался?";
|
||||
const userMessage = "Why supplier debt was not closed after payment on 06.07.2020?";
|
||||
const temporal = resolveTemporalGuard({
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
|
|
@ -160,7 +162,7 @@ describe("stage4 blocker-pack runtime guards", () => {
|
|||
});
|
||||
|
||||
it("locks July month window when question has month-only anchor", () => {
|
||||
const userMessage = "В июльском срезе почему по счету 60 остался хвост?";
|
||||
const userMessage = "In July snapshot why does account 60 still have an open tail?";
|
||||
const temporal = resolveTemporalGuard({
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
|
|
@ -182,7 +184,7 @@ describe("stage4 blocker-pack runtime guards", () => {
|
|||
expect(temporal.temporal_guard_outcome).toBe("passed");
|
||||
expect(temporal.resolved_time_anchor).toBe("2020-07");
|
||||
expect(temporal.effective_primary_period?.from).toBe("2020-07-01");
|
||||
expect(hintedPlan[0].fragment_text).toMatch(/июля 2020|2020-07-01/);
|
||||
expect(hintedPlan[0].fragment_text).toMatch(/2020-07-01|july 2020/i);
|
||||
});
|
||||
|
||||
it("filters customer settlement semantics from supplier/payable case", () => {
|
||||
|
|
@ -409,5 +411,100 @@ describe("stage4 blocker-pack runtime guards", () => {
|
|||
expect(grounded.status).toBe("no_grounded_answer");
|
||||
expect(grounded.reasons.join(" ")).toMatch(/Недостаточно допустимого evidence|Temporal anchor/i);
|
||||
});
|
||||
|
||||
it("reconstructs fixed-asset expected vs actual coverage in claim-bound targeting", () => {
|
||||
const userMessage =
|
||||
"31 июля начислена амортизация тремя суммами — 2 471,52, 2 465,28 и 849,83. Есть риск, что объект ОС не попал в амортизацию?";
|
||||
const claimAudit = resolveClaimBoundAnchors({
|
||||
userMessage,
|
||||
focusDomainHint: "fixed_asset_amortization",
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
primaryPeriod: {
|
||||
from: "2020-07-31",
|
||||
to: "2020-07-31",
|
||||
granularity: "day"
|
||||
}
|
||||
});
|
||||
|
||||
expect(claimAudit.claim_type).toBe("prove_fixed_asset_amortization_coverage");
|
||||
expect(claimAudit.required_anchors).toContain("fixed_asset_signal");
|
||||
expect(claimAudit.claim_anchor_resolution_rate).toBeGreaterThan(0.7);
|
||||
|
||||
const targeted = applyTargetedEvidenceAcquisition({
|
||||
claimAudit,
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
items: [
|
||||
{
|
||||
source_entity: "MCPLiveMovement",
|
||||
source_id: "fa-1",
|
||||
display_name: "Станок A",
|
||||
period: "2020-07-31",
|
||||
account_debit: "20.01",
|
||||
account_credit: "02.01",
|
||||
relation_pattern_hits: ["asset_card_to_depreciation", "document_to_posting"],
|
||||
fa_object_hint: "Станок A",
|
||||
fa_expected_set_candidate: true,
|
||||
fa_actual_set_candidate: true,
|
||||
fa_coverage_status: "covered"
|
||||
},
|
||||
{
|
||||
source_entity: "MCPLiveMovement",
|
||||
source_id: "fa-2",
|
||||
display_name: "Станок B",
|
||||
period: "2020-07-31",
|
||||
account_debit: "20.01",
|
||||
account_credit: "02.01",
|
||||
relation_pattern_hits: ["asset_card_to_depreciation"],
|
||||
fa_object_hint: "Станок B",
|
||||
fa_expected_set_candidate: true,
|
||||
fa_actual_set_candidate: false,
|
||||
fa_coverage_status: "expected_only"
|
||||
}
|
||||
],
|
||||
evidence: []
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
expect(targeted.audit.check_status.expected_fa_set_reconstructed).toBe("found");
|
||||
expect(targeted.audit.check_status.actual_fa_set_reconstructed).toBe("found");
|
||||
expect(targeted.audit.check_status.movement_or_posting_link_found).toBe("found");
|
||||
expect(Array.isArray(targeted.audit.fa_expected_set)).toBe(true);
|
||||
expect(targeted.audit.fa_expected_set).toContain("станок a");
|
||||
expect(targeted.audit.fa_expected_set).toContain("станок b");
|
||||
expect(targeted.audit.fa_actual_set_from_amortization).toContain("станок a");
|
||||
expect(targeted.audit.fa_missing_candidates).toContain("станок b");
|
||||
expect((targeted.audit.fa_relation_map ?? []).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not misclassify settlement 62.02 question into VAT or FA claim paths", () => {
|
||||
const userMessage =
|
||||
"Покупатель перечислил аванс на 62.02, но закрытие не произошло. Есть ли хвост по расчетам?";
|
||||
const claimAudit = resolveClaimBoundAnchors({
|
||||
userMessage,
|
||||
focusDomainHint: "settlements_60_62",
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
primaryPeriod: {
|
||||
from: "2020-07-01",
|
||||
to: "2020-07-31",
|
||||
granularity: "month"
|
||||
}
|
||||
});
|
||||
|
||||
expect(claimAudit.claim_type).toBe("prove_advance_offset_state");
|
||||
expect(claimAudit.resolved_anchors.vat_signal).toHaveLength(0);
|
||||
expect(claimAudit.resolved_anchors.fixed_asset_signal).toHaveLength(0);
|
||||
expect(claimAudit.required_anchors).toContain("advance_signal");
|
||||
});
|
||||
|
||||
it("keeps VAT priority over supplier wording in shared domain inference", () => {
|
||||
const vatQuestion =
|
||||
"По поставщику и счету-фактуре проверь НДС-цепочку: есть ли выпадение между документом, регистром и книгой покупок?";
|
||||
const settlementQuestion = "Покупатель перечислил аванс на 62.02, но закрытие не произошло. Есть ли хвост?";
|
||||
|
||||
expect(inferP0DomainFromMessage(vatQuestion)).toBe("vat_document_register_book");
|
||||
expect(inferP0DomainFromMessage(settlementQuestion)).toBe("settlements_60_62");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue