ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры

This commit is contained in:
dctouch 2026-04-10 19:22:40 +03:00
parent 19dbaff741
commit 80d108e506
116 changed files with 13950 additions and 2040 deletions

View File

@ -14,7 +14,7 @@ export const designConfig = {
scrollbarThumbHoverRgb: "30, 50, 30"
},
layout: {
modeColumnWidthPx: 440,
modeColumnWidthPx: 406,
modeToggleWidthPx: 188
}
} as const;

867
docs/TECH/1CLLMARCH-FACT.md Normal file
View File

@ -0,0 +1,867 @@
# 1CLLMARCH Fact Check And Stabilization Plan
Updated at: 2026-04-10
Source baseline: `docs/TECH/1CLLMARCH.md`
## 1. Purpose
This document fixes the current factual state of the codebase against `1CLLMARCH` and records a production-focused stabilization plan that preserves:
1. existing MCP routes;
2. manual routing and guard logic;
3. GUI manual markup + autorun operational loop.
## 2. Executive Reality Check
Current state is **not MVP** and also **not stable production**.
It is an advanced prototype with strong observability and eval tooling, but with architectural coupling and quality bottlenecks.
Approximate readiness against target architecture: **55/100**.
## 3. Verified Facts (Code + Runtime)
1. Strong engineering layers already exist:
- requirement extraction, coverage, grounding checks;
- rich debug payload and session trace;
- investigation state/followup continuity;
- manual annotation schema and post-analysis queues;
- async eval run + live history APIs.
2. Main production blockers:
- orchestration monolith and coupling in `assistantService.ts`;
- deterministic/template-heavy final answer construction;
- large lexical heuristic surface for intent routing;
- temporal policy inconsistency (date handling differs by lane/path);
- hidden fallback behavior around historical snapshot assumptions.
3. Quality metrics from latest Stage1 runs confirm weak user value:
- low retrieval differentiation;
- high generic explanation rate;
- low accountant actionability;
- zero mechanism specificity.
## 4. Gap Vs `1CLLMARCH`
### 4.1 Already aligned (partially or strongly)
1. Structured normalization contracts and validator loops.
2. Coverage and grounding artifacts (with different naming than target).
3. Conversation state persistence.
4. Operational diagnostics and report generation.
### 4.2 Not aligned enough for stable prod
1. Role separation (Interpreter/Planner/Critic/Answer) is incomplete and strongly coupled.
2. Final user answer quality is constrained by deterministic template synthesis.
3. Unified contract for analysis date/time scope is missing across all lanes.
4. Result classes from target (`FULLY_ANSWERED`, `BLOCKED_BY_*`, etc.) are not normalized as one contract.
5. Reason-code taxonomy exists but is fragmented across modules.
## 5. Stabilization Plan (No Route Breakage)
## Stage 1 (P0): Unified Analysis Context + Temporal Hardening
Goal:
1. Introduce unified `analysis_context` contract.
2. Propagate it through eval -> assistant -> runtime lanes.
3. Remove hidden hardcoded period fallback in live MCP plan generation.
4. Keep backward compatibility (`period_hint` still supported).
Acceptance:
1. If analysis date is set, runtime uses it explicitly in both deep/address paths.
2. If analysis date is absent, runtime no longer silently injects fixed historical date in live-plan fallback.
3. Existing APIs and manual workflows remain operational.
Implemented in current pass:
1. Added unified `analysis_context` contract to request context (`as_of_date`, `period_from`, `period_to`, `snapshot_mode`, `source`).
2. Added compatibility bridge: legacy `period_hint` is still accepted and normalized into `analysis_context`.
3. Propagated analysis context through eval flows into assistant runtime.
4. Applied analysis context in temporal guard with explicit precedence over implicit snapshot lock.
5. Removed hidden hardcoded live-plan fallback period by switching to:
- explicit analysis period/date when provided;
- query-derived period when present;
- generic live probe when period is absent.
Validation:
1. `npm run build` passed.
2. Targeted runtime tests passed:
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Status: **Completed**
## Stage 2 (P1): Orchestrator Decomposition (Behavior-Preserving Refactor)
Goal:
1. Split monolith into explicit modules:
- QueryFrame builder
- Execution planner
- Evidence assembler
- Coverage critic
- Answer package builder
2. Preserve current behavior under compatibility adapter.
Acceptance:
1. No MCP route regressions.
2. Existing tests and autorun loop remain green.
Implemented in current pass (Phase 2.1):
1. Added new orchestration contract module:
- `assistant_query_frame_v1`
- `assistant_execution_plan_v1`
- `assistant_evidence_bundle_v1`
- `assistant_coverage_contract_v1`
- outcome classifier (`FULLY_ANSWERED`, `PARTIALLY_ANSWERED`, `BLOCKED_*`, `MISROUTED`, `FAILED_TO_BIND_ENTITIES`)
2. Integrated contracts into deep-lane runtime without route/answer behavior changes:
- debug payload now includes `assistant_outcome_class_v1`;
- debug payload and event logs now include `assistant_orchestration_contracts_v1`.
3. Added unit regression tests:
- `assistantOrchestrationContracts.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted tests passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.2):
1. Added runtime orchestration adapter for the deep lane:
- `assistantOrchestrationRuntimeAdapter.ts`
- unified pipeline call for `requirements -> coverage -> grounding` with stable interfaces.
2. Integrated adapter into `assistantService` main deep-lane flow (behavior-preserving):
- existing extraction/coverage/grounding logic preserved;
- execution now routed through one orchestration boundary.
3. Added adapter unit tests:
- `assistantOrchestrationRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted tests passed:
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.3):
1. Extracted answer package builder (`answer_structure_v11`) from `assistantService` into dedicated module:
- `assistantAnswerPackageBuilder.ts`
2. Rewired deep-lane answer structure assembly to use the new module without contract changes.
3. Added focused unit tests for answer package behavior:
- `assistantAnswerPackageBuilder.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 test pack passed:
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.4):
1. Extracted coverage/grounding pipeline into dedicated module:
- `assistantCoverageGrounding.ts`
- moved requirement extraction, coverage resolution, and grounding checks under one reusable boundary.
2. Rewired `assistantService` to use extracted coverage/grounding module via compatibility wrappers (behavior-preserving).
3. Added focused regression tests:
- `assistantCoverageGrounding.test.ts`
4. Stabilized deterministic Stage 2 regression environment for followup continuity checks:
- fixed accidental false-positive token in grounding test fixture;
- fixed env isolation in `assistantWave10SettlementCorrectiveRegression.test.ts` by explicitly controlling `FEATURE_ASSISTANT_ADDRESS_QUERY_V1`.
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.5):
1. Extracted query-planning helpers from `assistantService` into dedicated module:
- `assistantQueryPlanning.ts`
- moved `fragmentTextById`, execution-plan builder, and debug-routes builder under one reusable boundary.
2. Rewired `assistantService` to use extracted query-planning module (behavior-preserving wrapper integration).
3. Added focused unit tests for query-planning behavior:
- `assistantQueryPlanning.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.6):
1. Extracted evidence-bundle assembly from `assistantService` into dedicated module:
- `assistantEvidenceBundleAssembler.ts`
- centralized:
- `assistant_evidence_bundle_v1` contract assembly;
- debug `retrieval_status` projection from normalized retrieval results.
2. Rewired `assistantService` to use assembler output for both:
- `assistant_orchestration_contracts_v1.evidence_bundle`;
- debug payload `retrieval_status`.
3. Added focused unit tests:
- `assistantEvidenceBundleAssembler.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.7):
1. Extracted deep-lane debug payload assembly from `assistantService` into dedicated module:
- `assistantDebugPayloadAssembler.ts`
2. Rewired `assistantService` to build `debug` via assembler (behavior-preserving):
- keeps all existing fields, guard audits, orchestration contracts and optional sections.
3. Added focused unit tests:
- `assistantDebugPayloadAssembler.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantDebugPayloadAssembler.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.8):
1. Extracted deep-lane processed log payload assembly from `assistantService` into dedicated module:
- `assistantMessageLogAssembler.ts`
2. Rewired `assistantService` to build `assistant_message_processed.details` via assembler (behavior-preserving).
3. Added focused unit tests:
- `assistantMessageLogAssembler.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantMessageLogAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.9):
1. Extracted orchestration-contract bundle assembly from `assistantService` into dedicated module:
- `assistantContractsBundleAssembler.ts`
2. Rewired `assistantService` to consume bundled contracts/outcome class from assembler (behavior-preserving):
- `query_frame`, `execution_plan`, `evidence_bundle`, `coverage`, `outcome_class`.
3. Added focused unit tests:
- `assistantContractsBundleAssembler.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantContractsBundleAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.10):
1. Extracted deep response envelope assembly from `assistantService` into dedicated module:
- `assistantDeepResponseAssembler.ts`
- centralized:
- safe final assistant text cleanup (debug tail stripping);
- `answer_structure_v11` selection/building policy;
- assistant conversation item construction.
2. Rewired `assistantService` to consume deep response assembler (behavior-preserving).
3. Added focused unit tests:
- `assistantDeepResponseAssembler.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 + safety regressions passed:
- `assistantDeepResponseAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantOrchestrationContracts.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.11):
1. Added top-level deep turn packager to compose extracted Stage 2 modules behind one boundary:
- `assistantDeepTurnPackaging.ts`
- centralizes orchestration of:
- evidence bundle assembly;
- contracts/outcome class assembly;
- deep answer artifacts;
- debug payload;
- assistant conversation item;
- processed log details.
2. Rewired deep-lane `assistantService` flow to use `assembleAssistantDeepTurnPackaging(...)` (behavior-preserving):
- replaced duplicated per-artifact assembly block with single orchestrator call;
- preserved investigation-state update and existing response/event contract.
3. Added focused unit test:
- `assistantDeepTurnPackaging.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.12):
1. Extracted deep-turn input normalization/defaulting from `assistantService` into dedicated module:
- `assistantDeepTurnInputBuilder.ts`
- centralized normalization of:
- `followupStateUsage` default (`null` when absent);
- composition defaults (`problem_*` fields, `answer_structure_v11`);
- `problem_unit_ids_used` array normalization.
2. Rewired `assistantService` to build deep-turn input via:
- `buildAssistantDeepTurnPackagingInput(...)`
- followed by existing `assembleAssistantDeepTurnPackaging(...)` call (behavior-preserving).
3. Added focused unit tests:
- `assistantDeepTurnInputBuilder.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.13):
1. Extracted investigation-state update/persist runtime block from `assistantService` into dedicated adapter:
- `assistantInvestigationStateRuntimeAdapter.ts`
- introduced:
- `buildAssistantInvestigationStateSnapshot(...)`
- `persistAssistantInvestigationStateSnapshot(...)`
2. Rewired `assistantService` to consume new adapter (behavior-preserving):
- same `investigation_state` snapshot semantics;
- same `FEATURE_ASSISTANT_INVESTIGATION_STATE_V1` gating;
- same session persistence call path.
3. Added focused unit tests:
- `assistantInvestigationStateRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.14):
1. Extracted deep-lane assistant post-turn commit/persist/log block from `assistantService` into dedicated adapter:
- `assistantTurnCommitRuntimeAdapter.ts`
- introduced:
- `commitAssistantTurnAndLog(...)`
2. Rewired `assistantService` deep-lane to use new adapter (behavior-preserving):
- same append-to-session semantics;
- same session persistence behavior when session exists;
- same `assistant_message_processed` log envelope and event type.
3. Added focused unit tests:
- `assistantTurnCommitRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.15):
1. Extracted deep-lane pre-packaging context assembly from `assistantService` into dedicated module:
- `assistantDeepTurnPrePackagingContext.ts`
- centralized:
- dropped intent segments extraction;
- analysis context projection for contracts;
- debug route projection;
- resolved execution state projection;
- safe assistant reply base sanitization.
2. Rewired `assistantService` deep-lane to consume `buildAssistantDeepTurnPrePackagingContext(...)` (behavior-preserving).
3. Added focused unit tests:
- `assistantDeepTurnPrePackagingContext.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.16):
1. Extracted deep-lane success response envelope assembly from `assistantService` into dedicated builder:
- `assistantDeepTurnResponseBuilder.ts`
- introduced:
- `buildAssistantDeepTurnSuccessResponse(...)`
2. Rewired `assistantService` deep-lane to return response via new builder (behavior-preserving).
3. Added focused unit tests:
- `assistantDeepTurnResponseBuilder.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.17):
1. Extracted deep-lane composition assembly from `assistantService` into dedicated runtime adapter:
- `assistantDeepTurnCompositionRuntimeAdapter.ts`
- introduced:
- `buildAssistantDeepTurnComposition(...)`
2. Centralized composition-time derivations (behavior-preserving):
- `focusDomainHint` from followup usage + investigation state;
- `questionTypeClass` via `resolveQuestionType(...)`;
- period presence signals from company anchors/normalization payload;
- downstream `composeAssistantAnswer(...)` call wiring.
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.18):
1. Extracted deep-lane guard runtime pipeline from `assistantService` into dedicated adapter:
- `assistantDeepTurnGuardRuntimeAdapter.ts`
- introduced:
- `applyAssistantDeepTurnRetrievalGuards(...)`
- `applyAssistantDeepTurnGroundingEligibility(...)`
2. Centralized and isolated runtime sequence (behavior-preserving):
- polarity guard on retrieval results;
- targeted evidence acquisition;
- evidence admissibility gate;
- grounded-answer eligibility evaluation + grounding status overlay.
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.19):
1. Extracted deep-lane retrieval execution loop from `assistantService` into dedicated runtime adapter:
- `assistantDeepTurnRetrievalRuntimeAdapter.ts`
- introduced:
- `executeAssistantDeepTurnRetrievalPlan(...)`
2. Centralized retrieval runtime behavior (behavior-preserving):
- skipped/no-route call record generation;
- sequential route execution with temporal hint propagation;
- raw result capture;
- failed-route fallback normalization with stable error envelope.
3. Rewired `assistantService` deep-lane to consume retrieval adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.20):
1. Extracted deep-lane execution-plan assembly/enforcement from `assistantService` into dedicated runtime adapter:
- `assistantDeepTurnPlanRuntimeAdapter.ts`
- introduced:
- `buildAssistantDeepTurnExecutionPlan(...)`
2. Centralized planning runtime sequence (behavior-preserving):
- requirement extraction by fragment;
- execution plan build from route summary;
- RBP route-plan enforcement;
- FA route-plan enforcement;
- temporal hint overlay;
- polarity hint overlay.
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.21):
1. Extracted deep-lane pre-guard runtime context assembly from `assistantService` into dedicated adapter:
- `assistantDeepTurnContextRuntimeAdapter.ts`
- introduced:
- `buildAssistantDeepTurnRuntimeContext(...)`
2. Centralized context runtime sequence (behavior-preserving):
- company anchors resolution;
- initial business-scope alignment;
- P0 domain inference + domain whitelist gating for guard focus;
- temporal guard resolution with runtime analysis context;
- domain polarity guard resolution;
- claim-bound anchors resolution;
- live business-scope resolution with followup flag;
- normalized live temporal hint projection.
3. Rewired `assistantService` deep-lane to consume context adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.22):
1. Extracted deep-lane post-retrieval grounding runtime orchestration from `assistantService` into dedicated adapter:
- `assistantDeepTurnGroundingRuntimeAdapter.ts`
- introduced:
- `runAssistantDeepTurnGroundingRuntime(...)`
2. Centralized grounding/runtime sequence (behavior-preserving):
- RBP live-route audit projection;
- FA live-route audit projection;
- coverage+grounding pipeline execution;
- grounded-answer eligibility overlay on base grounding check.
3. Rewired `assistantService` deep-lane to consume adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Implemented in current pass (Phase 2.23):
1. Extracted deep-lane packaging orchestration block from `assistantService` into dedicated runtime adapter:
- `assistantDeepTurnPackagingRuntimeAdapter.ts`
- introduced:
- `runAssistantDeepTurnPackagingRuntime(...)`
2. Centralized packaging/runtime sequence (behavior-preserving):
- pre-packaging context assembly;
- investigation-state snapshot build/persist;
- deep-turn packaging input assembly;
- deep-turn packaging output projection (`safeAssistantReply`, debug payload, assistant item, processed log details).
3. Rewired `assistantService` deep-lane to consume packaging runtime adapter output (behavior-preserving).
4. Added focused unit tests:
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted Stage 2 assembler/adapter pack passed:
- `assistantOrchestrationContracts.test.ts`
- `assistantOrchestrationRuntimeAdapter.test.ts`
- `assistantAnswerPackageBuilder.test.ts`
- `assistantCoverageGrounding.test.ts`
- `assistantQueryPlanning.test.ts`
- `assistantEvidenceBundleAssembler.test.ts`
- `assistantDebugPayloadAssembler.test.ts`
- `assistantMessageLogAssembler.test.ts`
- `assistantContractsBundleAssembler.test.ts`
- `assistantDeepResponseAssembler.test.ts`
- `assistantDeepTurnPackaging.test.ts`
- `assistantDeepTurnInputBuilder.test.ts`
- `assistantInvestigationStateRuntimeAdapter.test.ts`
- `assistantTurnCommitRuntimeAdapter.test.ts`
- `assistantDeepTurnPrePackagingContext.test.ts`
- `assistantDeepTurnResponseBuilder.test.ts`
- `assistantDeepTurnCompositionRuntimeAdapter.test.ts`
- `assistantDeepTurnGuardRuntimeAdapter.test.ts`
- `assistantDeepTurnRetrievalRuntimeAdapter.test.ts`
- `assistantDeepTurnPlanRuntimeAdapter.test.ts`
- `assistantDeepTurnContextRuntimeAdapter.test.ts`
- `assistantDeepTurnGroundingRuntimeAdapter.test.ts`
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
3. Additional safety regressions passed:
- `assistantWave10SettlementCorrectiveRegression.test.ts`
- `assistantMcpRuntimeBridge.test.ts`
- `assistantAddressFollowupContext.test.ts`
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 completed)**
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
Goal:
1. Use LLM for semantic extraction/decomposition in strict schema.
2. Keep deterministic guardrails as verifier, not primary “brain”.
3. Reduce dictionary overfitting and false route drifts.
Status: Planned
## Stage 4 (P2): Human-Centric Answer Layer
Goal:
1. Move final user response to contract-driven answer package with:
- direct answer;
- what was checked;
- what was found;
- what remains unproven;
- best next step.
2. Keep claim-to-evidence binding strict.
Status: Planned
## Stage 5 (P3): Quality Loop Driven By GUI Markup
Goal:
1. Drive backlog from `manual_case_decision` queues.
2. Build targeted regression packs from real failed comments.
3. Track trend by reason-code clusters.
Status: Planned
## 6. Non-Negotiable Constraints
1. Do not break MCP route interfaces.
2. Do not remove manual logic without compatible replacement.
3. Preserve UTF-8 (without BOM) for all source/doc files.
4. Keep manual markup and autorun API contract stable.

1351
docs/TECH/1CLLMARCH.md Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/TECH/1CLLMARCH.zip Normal file

Binary file not shown.

View File

@ -797,6 +797,7 @@ function buildRunSummary(run) {
llm_provider: llmProvider,
model,
use_mock: toBooleanSafe(run.report.use_mock),
analysis_date: toStringSafe(run.report.analysis_date),
prompt_version: toStringSafe(run.report.prompt_version),
schema_version: toStringSafe(run.report.schema_version),
suite_id: toStringSafe(run.report.suite_id),

View File

@ -90,7 +90,32 @@ function normalizeCaseIds(value) {
.filter((item) => item.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
function normalizeAnalysisDate(value) {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return undefined;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return undefined;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return undefined;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function buildEvalPayloadFromBody(body) {
const analysisDate = normalizeAnalysisDate(body.analysis_date) ??
normalizeAnalysisDate(body.analysisDate);
return {
normalizeConfig: (body.normalizeConfig ?? {}),
caseIds: normalizeCaseIds(body.caseIds),
@ -103,7 +128,8 @@ function buildEvalPayloadFromBody(body) {
? body.compare_with_report_file
: typeof body.comparisonBaselineReportFile === "string"
? body.comparisonBaselineReportFile
: undefined
: undefined,
analysisDate
};
}
function resolveReadablePath(inputPath) {
@ -245,6 +271,7 @@ function snapshotJob(job) {
eval_target: job.eval_target,
run_id: job.run_id,
case_set_file: job.case_set_file,
analysis_date: job.analysis_date,
total_cases: job.total_cases,
completed_cases: job.completed_cases,
error: job.error,
@ -258,7 +285,8 @@ function snapshotJob(job) {
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
? Number(toRecord(job.report.metrics)?.score_index)
: null,
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
}
: null
};
@ -310,6 +338,7 @@ function buildEvalRouter(services) {
eval_target: payload.evalTarget,
run_id: runId,
case_set_file: runtimeCaseSetFile,
analysis_date: payload.analysisDate ?? null,
total_cases: caseSeeds.length,
completed_cases: 0,
cases: caseSeeds.map((item) => ({

View File

@ -427,8 +427,20 @@ function extractLooseByAnchorValue(text) {
}
const lowered = token.toLowerCase();
const stopWords = new Set([
"какой",
"какая",
"какие",
"каких",
"каким",
"какими",
"каком",
"кто",
"что",
"мы",
"видим",
"контрагенту",
"контрагента",
"контрагентам",
"контре",
"компании",
"компанию",
@ -436,10 +448,14 @@ function extractLooseByAnchorValue(text) {
"организацию",
"поставщику",
"поставщика",
"поставщикам",
"клиенту",
"клиента",
"клиентам",
"покупателю",
"покупателя",
"покупателям",
"заказчикам",
"партнеру",
"партнера",
"договору",
@ -552,6 +568,9 @@ function isLikelyCounterpartyToken(rawToken) {
"какая",
"какое",
"каких",
"каким",
"какими",
"каком",
"какому",
"какую",
"кто",
@ -566,6 +585,8 @@ function isLikelyCounterpartyToken(rawToken) {
"чья",
"чей",
"чью",
"мы",
"видим",
"самый",
"самая",
"самое",
@ -620,10 +641,23 @@ function isLikelyCounterpartyToken(rawToken) {
"контрагент",
"контрагенту",
"контрагента",
"контрагентам",
"компания",
"компании",
"организация",
"организации",
"поставщикам",
"клиентам",
"покупателям",
"заказчикам",
"аванс",
"авансы",
"проблемный",
"проблемные",
"проблемным",
"закрытия",
"закрыть",
"закрыты",
"год",
"года",
"г",
@ -727,7 +761,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
if (tokens.length === 0) {
return true;
}
const questionCue = /(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
const questionCue = /(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/[?]/u.test(String(rawValue ?? ""));
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);

View File

@ -604,6 +604,10 @@ function hasLifecycleSegmentationSignal(text) {
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
}
function hasCounterpartyActivityLifecycleSignal(text) {
const hasPaymentRiskLexeme = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(text);
if (hasPaymentRiskLexeme) {
return false;
}
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
@ -678,6 +682,7 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
const asksMajorShare = /(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(text);
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
@ -702,6 +707,9 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
if (asksCounterpartySource && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
return true;
}
@ -801,6 +809,58 @@ function hasOpenContractsListSignal(text) {
}
return true;
}
function hasSupplierTailRiskSignal(text) {
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
}
function hasReceivablesLatencyRiskSignal(text) {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
const hasBetweenShipmentAndPayment = /между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
return true;
}
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
}
function hasSettlementGapSignal(text) {
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
const hasNoDocumentForClosing = /(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoDocumentForClosingReversed = /(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoPayments = /(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
const hasUnclosedAdvanceGap = hasAdvance &&
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed);
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
}
function hasReconciliationMismatchSignal(text) {
const hasCounterparty = /(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
const hasMismatchLexeme = /(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
return (hasCounterparty &&
hasReconciliationLexeme &&
hasMismatchLexeme &&
hasBalanceLexeme &&
(hasLookupVerb || hasInterrogativeLookup));
}
function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
@ -1115,6 +1175,34 @@ function resolveAddressIntent(userMessage) {
reasons: ["payables_signal_detected"]
};
}
if (hasSettlementGapSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["settlement_gap_signal_detected"]
};
}
if (hasReconciliationMismatchSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["reconciliation_mismatch_signal_detected"]
};
}
if (hasReceivablesLatencyRiskSignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_payment_lag_signal_detected"]
};
}
if (hasSupplierTailRiskSignal(text)) {
return {
intent: "list_payables_counterparties",
confidence: "medium",
reasons: ["supplier_tail_risk_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
@ -1137,7 +1225,7 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))) {
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(text)) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",

View File

@ -86,6 +86,33 @@ function parseFiniteNumber(value) {
}
return null;
}
function normalizeAnalysisDateHint(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
if (!isoPrefix) {
return null;
}
const year = Number(isoPrefix[1]);
const month = Number(isoPrefix[2]);
const day = Number(isoPrefix[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
}
function valueAsString(value) {
if (value === null || value === undefined) {
return "";
@ -665,6 +692,50 @@ function runtimeReadinessForLimitedCategory(category) {
}
return "UNKNOWN";
}
function normalizeLimitedReason(reason) {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/address\s*v1/giu, "текущий адресный режим"],
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
[/live\s+recipe/giu, "текущий сценарий выборки"],
[/materialized\s+live-строках/giu, "доступном срезе данных"],
[/live-выборке/giu, "выборке данных"],
[/live-данных/giu, "данных"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\blookup\b/giu, "поиск"],
[/\bintent\b/giu, "сценария"],
[/\brecipe\b/giu, "шаблон выборки"],
[/\byakor\b/giu, "ориентир"],
[/\banchor\b/giu, "ориентир"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function normalizeLimitedNextStep(nextStep) {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
[/\bintent\b/giu, "сценарий"],
[/\blookup\b/giu, "поиск"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function rowHasNonEmptyField(row, keys) {
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
}
@ -766,20 +837,27 @@ function toLegacyMcpStatus(status) {
}
function composeLimitedReply(category, reason, nextStep) {
const heading = category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine = category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
`Причина: ${reason}.`
reasonLine
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
}
return lines.join("\n");
}
@ -842,7 +920,22 @@ class AddressQueryService {
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
const { mode, shape, intent, filters } = decompose;
const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) {
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
if (!hasTemporalFilter) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: analysisDate
};
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
baseReasons.push("as_of_date_from_analysis_context");
}
}
const composeOptionsFromFilters = (filterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@ -863,8 +956,8 @@ class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
@ -903,8 +996,8 @@ class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});

View File

@ -1172,7 +1172,7 @@ function composeFactualReply(intent, rows, options = {}) {
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
@ -1188,6 +1188,34 @@ function composeFactualReply(intent, rows, options = {}) {
text: lines.join("\n")
};
}
if (intent === "list_payables_counterparties") {
const lines = [
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк для ручной проверки."]
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_receivables_counterparties") {
const lines = [
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
@ -1279,12 +1307,7 @@ function composeFactualReply(intent, rows, options = {}) {
text: lines.join("\n")
};
}
const title = intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")

View File

@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantAnswerStructureV11 = buildAssistantAnswerStructureV11;
const config_1 = require("../config");
const stage1Contracts_1 = require("../types/stage1Contracts");
const EVIDENCE_LIMITATION_REASON_CODE_SET = new Set([
"snapshot_only",
"heuristic_inference",
"missing_mechanism",
"weak_source_mapping",
"insufficient_detail",
"unknown"
]);
function summarizeUnique(values, limit = 6) {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function isEvidenceLimitationReasonCode(value) {
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value);
}
function firstNonEmptyLine(text) {
const line = String(text ?? "")
.split("\n")
.map((item) => item.trim())
.find((item) => item.length > 0);
return (line ?? String(text ?? "")).slice(0, 220);
}
function buildClaimEvidenceLinks(retrievalResults) {
const byClaim = new Map();
for (const result of retrievalResults) {
for (const evidence of result.evidence) {
const claimRef = String(evidence.claim_ref ?? "").trim();
if (!claimRef) {
continue;
}
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claimRef, evidenceIds]) => ({
claim_ref: claimRef,
evidence_ids: summarizeUnique(evidenceIds, 10)
}));
}
function buildAssistantAnswerStructureV11(input) {
const evidenceIds = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)), 10);
const mechanismNotes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
.map((evidence) => evidence.mechanism_note)
.filter((note) => typeof note === "string" && note.trim().length > 0)), 6);
const sourceRefs = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
.map((evidence) => evidence.source_ref?.canonical_ref)
.filter((value) => typeof value === "string" && value.trim().length > 0)), 8);
const limitationReasonCodes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.flatMap((evidence) => {
const code = evidence.limitation?.reason_code;
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
})), 8).filter(isEvidenceLimitationReasonCode);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const limitations = summarizeUnique([...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons], 8);
const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`);
const recommendedActions = summarizeUnique([
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`)
], 6);
const mechanismStatus = mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const enableEvidenceEnrichment = input.options?.enableEvidenceEnrichment ?? config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
return {
schema_version: stage1Contracts_1.ANSWER_STRUCTURE_SCHEMA_VERSION,
answer_summary: firstNonEmptyLine(input.assistantReply),
direct_answer: input.assistantReply,
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: evidenceIds,
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
coverage_note: input.coverageReport.requirements_total === input.coverageReport.requirements_covered
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(enableEvidenceEnrichment && claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: input.groundingCheck.missing_requirements,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
}

View File

@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantContractsBundleV1 = assembleAssistantContractsBundleV1;
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
function assembleAssistantContractsBundleV1(input) {
const queryFrameContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantQueryFrameContractV1)({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContext
});
const executionPlanContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantExecutionPlanContractV1)({
executionPlan: input.executionPlan,
requirements: input.requirements
});
const outcomeClassV1 = (0, assistantOrchestrationContracts_1.classifyAssistantOutcomeClassV1)({
replyType: input.replyType,
coverageReport: input.coverageReport,
grounding: input.grounding,
retrievalResults: input.retrievalResults
});
const coverageContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantCoverageContractV1)({
coverageReport: input.coverageReport,
grounding: input.grounding,
outcomeClass: outcomeClassV1
});
return {
queryFrameContractV1,
executionPlanContractV1,
outcomeClassV1,
coverageContractV1,
assistantOrchestrationContractsV1: {
query_frame: queryFrameContractV1,
execution_plan: executionPlanContractV1,
evidence_bundle: input.evidenceBundleContractV1,
coverage: coverageContractV1
}
};
}

View File

@ -0,0 +1,344 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractRequirementsForRoute = extractRequirementsForRoute;
exports.evaluateCoverageForRequirements = evaluateCoverageForRequirements;
exports.checkGroundingForRequirements = checkGroundingForRequirements;
function summarizeUnique(values, limit = 6) {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
const SUBJECT_TOKEN_RULES = {
nds: {
critical: true,
patterns: [
"vat",
"accumulationregister",
"ндс",
"книгипокупок",
"книгипродаж",
"налогнадобавленнуюстоимость"
]
},
os: {
critical: true,
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
},
saldo: {
critical: true,
patterns: ["balance", "saldo", "сальдо", "остат"]
},
counterparty: {
critical: false,
patterns: [
"counterparty",
"supplier",
"buyer",
"counterparty_id",
"journal_counterparty",
"document_has_counterparty",
"контрагент",
"поставщик",
"покупател"
],
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
},
document: {
critical: false,
patterns: [
"document",
"recorder",
"journal",
"document_refs_count",
"recorded_by_document",
"journal_refers_to_document",
"документ"
],
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
},
anomaly: {
critical: false,
patterns: [
"risk",
"risk_score",
"unknown_link_count",
"zero_guid",
"navigation_links",
"missing_counterparty_link",
"аномал",
"риск"
],
routes: ["store_feature_risk", "batch_refresh_then_store"]
},
chain: {
critical: false,
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
routes: ["hybrid_store_plus_live"]
}
};
function hasRegexMatch(corpus, pattern) {
try {
return pattern.test(corpus);
}
catch {
return false;
}
}
function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
if (token.startsWith("account_")) {
const account = token.slice("account_".length).trim();
if (!account) {
return { matched: false, critical: true };
}
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
}
const rule = SUBJECT_TOKEN_RULES[token];
if (rule) {
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
return { matched: byPattern || byRoute, critical: rule.critical };
}
return { matched: corpus.includes(token), critical: false };
}
function evidenceCountForRequirement(requirementId, result) {
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
if (evidence.length === 0) {
return 0;
}
const tagged = evidence.filter((item) => {
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
}).length;
if (tagged > 0) {
return tagged;
}
if (Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId) {
return evidence.length;
}
return 0;
}
function hasSubstantiveCoverageForRequirement(requirementId, result) {
const evidenceCount = evidenceCountForRequirement(requirementId, result);
if (evidenceCount > 0) {
return true;
}
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
if (Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId) {
return true;
}
}
return false;
}
function extractRequirementsForRoute(input) {
const byFragment = new Map();
const requirements = [];
const pushRequirement = (item) => {
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
requirements.push({
requirement_id: item.requirement_id,
source_fragment_id: item.source_fragment_id,
requirement_text: item.requirement_text,
subject_tokens: subjectTokens,
status: item.status,
route: item.route
});
if (item.source_fragment_id) {
const current = byFragment.get(item.source_fragment_id) ?? [];
current.push(item.requirement_id);
byFragment.set(item.source_fragment_id, current);
}
};
if (!input.routeSummary) {
pushRequirement({
requirement_id: "R1",
source_fragment_id: null,
requirement_text: input.userMessage,
status: "clarification_needed",
route: null
});
return { requirements, byFragment };
}
if (input.routeSummary.mode === "legacy_v1") {
pushRequirement({
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: input.userMessage,
status: "covered",
route: input.routeSummary.route_hint
});
return { requirements, byFragment };
}
input.routeSummary.decisions.forEach((decision, index) => {
const requirementId = `R${index + 1}`;
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
let status = "covered";
if (decision.route === "no_route") {
if (decision.no_route_reason === "out_of_scope") {
status = "out_of_scope";
}
else if (decision.no_route_reason === "insufficient_specificity") {
status = "clarification_needed";
}
else {
status = "uncovered";
}
}
pushRequirement({
requirement_id: requirementId,
source_fragment_id: decision.fragment_id,
requirement_text: text,
status,
route: decision.route === "no_route" ? null : decision.route
});
});
return { requirements, byFragment };
}
function evaluateCoverageForRequirements(requirements, retrievalResults) {
const statusByRequirement = new Map();
for (const result of retrievalResults) {
for (const requirementId of result.requirement_ids) {
const list = statusByRequirement.get(requirementId) ?? [];
list.push({
status: result.status,
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
});
statusByRequirement.set(requirementId, list);
}
}
const resolvedRequirements = requirements.map((requirement) => {
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
return requirement;
}
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
if (states.length === 0) {
return { ...requirement, status: "uncovered" };
}
const hasAnySubstantive = states.some((item) => item.substantive);
if (!hasAnySubstantive) {
return { ...requirement, status: "uncovered" };
}
const hasOk = states.some((item) => item.status === "ok");
const hasPartial = states.some((item) => item.status === "partial");
const hasEmpty = states.some((item) => item.status === "empty");
const hasError = states.some((item) => item.status === "error");
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
return { ...requirement, status: "covered" };
}
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
return { ...requirement, status: "partially_covered" };
}
return { ...requirement, status: "uncovered" };
});
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
const requirementsUncovered = resolvedRequirements
.filter((item) => item.status === "uncovered")
.map((item) => item.requirement_id);
const requirementsPartiallyCovered = resolvedRequirements
.filter((item) => item.status === "partially_covered")
.map((item) => item.requirement_id);
const clarificationNeededFor = resolvedRequirements
.filter((item) => item.status === "clarification_needed")
.map((item) => item.requirement_id);
const outOfScopeRequirements = resolvedRequirements
.filter((item) => item.status === "out_of_scope")
.map((item) => item.requirement_id);
return {
requirements: resolvedRequirements,
coverage: {
requirements_total: resolvedRequirements.length,
requirements_covered: requirementsCovered,
requirements_uncovered: requirementsUncovered,
requirements_partially_covered: requirementsPartiallyCovered,
clarification_needed_for: clarificationNeededFor,
out_of_scope_requirements: outOfScopeRequirements
}
};
}
function checkGroundingForRequirements(input) {
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
const subjectTokens = input.extractSubjectTokens(input.userMessage);
const executedRoutes = new Set(input.retrievalResults
.filter((item) => item.status !== "error")
.map((item) => item.route)
.filter(Boolean));
const retrievalCorpus = JSON.stringify(input.retrievalResults.map((item) => ({
route: item.route,
result_type: item.result_type,
summary: item.summary,
items: item.items,
evidence: item.evidence,
why_included: item.why_included,
selection_reason: item.selection_reason,
risk_factors: item.risk_factors,
business_interpretation: item.business_interpretation
}))).toLowerCase();
const missingSubjectTokens = [];
const missingCriticalTokens = [];
for (const token of subjectTokens) {
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
if (!match.matched) {
missingSubjectTokens.push(token);
if (match.critical) {
missingCriticalTokens.push(token);
}
}
}
const onlyAccountCriticalMissing = missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
const accountOnlyMismatchRecoverable = hasMaterialResults &&
input.coverage.requirements_covered > 0 &&
onlyAccountCriticalMissing &&
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
let status = "grounded";
const reasons = [];
if (!routeSubjectMatch) {
status = "route_mismatch_blocked";
reasons.push(`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`);
}
else if (accountOnlyMismatchRecoverable) {
status = "partial";
reasons.push(`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`);
}
else if (input.coverage.requirements_covered === 0) {
status = "no_grounded_answer";
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
}
else if (input.coverage.requirements_uncovered.length > 0 ||
input.coverage.requirements_partially_covered.length > 0 ||
input.coverage.clarification_needed_for.length > 0 ||
input.coverage.out_of_scope_requirements.length > 0) {
status = "partial";
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
}
if (whyIncludedSummary.length === 0) {
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
}
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
}
const missingRequirements = [
...input.coverage.requirements_uncovered,
...input.coverage.requirements_partially_covered,
...input.coverage.clarification_needed_for,
...input.coverage.out_of_scope_requirements
];
return {
status,
route_subject_match: routeSubjectMatch,
missing_requirements: missingRequirements,
reasons,
why_included_summary: whyIncludedSummary,
selection_reason_summary: selectionReasonSummary
};
}

View File

@ -141,6 +141,29 @@ function formatIsoDateUtc(date) {
const day = String(date.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function normalizeIsoDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function monthEndFromIso(isoDate) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
@ -198,14 +221,25 @@ function hasRbpSignal(text) {
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) {
function buildLiveMcpCallPlan(route, fragmentText, temporalHint) {
const semanticProfile = buildSemanticRetrievalProfile(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 hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
const primaryTo = periodScope.to ??
hintedPeriodTo ??
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
const buildPrimaryQuery = (limit) => primaryFrom && primaryTo
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
const buildCarryQuery = (limit) => carryFrom && carryTo
? buildLiveRangeQuery(carryFrom, carryTo, limit)
: buildPrimaryQuery(limit);
const faClaim = preferredDomainHint === "fixed_asset_amortization" ||
hasFixedAssetAmortizationSignal(fragmentText) ||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
@ -219,7 +253,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_amortization_documents_in_period",
purpose: "seed_amortization_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -227,7 +261,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_fixed_asset_movements_accounts_01_02",
purpose: "collect_fa_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -235,7 +269,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_fixed_asset_cards_expected_for_period",
purpose: "build_expected_fa_set",
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -243,7 +277,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
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),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -265,7 +299,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_source_documents_in_period",
purpose: "seed_vat_source_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -273,7 +307,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_invoice_links_in_period",
purpose: "collect_invoice_links",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -281,7 +315,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_register_entries_in_period",
purpose: "collect_vat_register_entries",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -289,7 +323,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_book_entries_in_period",
purpose: "collect_vat_book_entries",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -326,7 +360,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_rbp_writeoff_documents_in_period",
purpose: "seed_writeoff_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@ -334,7 +368,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_rbp_object_movements_account_97",
purpose: "collect_rbp_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97"]
@ -342,7 +376,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_month_close_entries_linked_to_rbp",
purpose: "link_month_close_to_rbp",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@ -350,7 +384,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "compute_end_period_residual_by_rbp_object",
purpose: "collect_residual_tail_signals",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(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"]
@ -1410,7 +1444,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
if (/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
hasFixedAssetAccountScope) {
pushMany(domainScope, ["fixed_assets"]);
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
@ -2243,7 +2277,7 @@ class AssistantDataLayer {
}
return enforceBroadQueryGuards(route, fragmentText, result);
}
async executeRouteRuntime(route, fragmentText) {
async executeRouteRuntime(route, fragmentText, options) {
const base = this.executeRoute(route, fragmentText);
if (!config_1.FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
return base;
@ -2251,7 +2285,7 @@ class AssistantDataLayer {
if (route !== "hybrid_store_plus_live" && route !== "live_mcp_drilldown") {
return base;
}
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
return this.mergeWithLiveOverlay(base, liveOverlay);
}
cloneRawResult(base) {
@ -2301,9 +2335,9 @@ class AssistantDataLayer {
}
return merged;
}
async fetchLiveMcpOverlay(route, fragmentText) {
async fetchLiveMcpOverlay(route, fragmentText, temporalHint) {
const endpoint = this.buildMcpUrl("/api/execute_query");
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
const accountScope = livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
? ["01", "02", "08"]

View File

@ -0,0 +1,92 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
function toAnalysisContext(input) {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function buildDeepAnalysisDebugPayload(input) {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
trace_id: input.traceId,
prompt_version: input.promptVersion,
schema_version: input.schemaVersion,
fallback_type: input.fallbackType,
route_summary: input.routeSummary,
fragments: input.fragments,
requirements_extracted: input.requirementsExtracted,
coverage_report: input.coverageReport,
routes: input.routes,
retrieval_status: input.retrievalStatus,
retrieval_results: input.retrievalResults,
answer_grounding_check: input.groundingCheck,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
normalized: input.normalizedPayload
};
}

View File

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnswerArtifacts = buildDeepAnswerArtifacts;
exports.buildAssistantConversationItem = buildAssistantConversationItem;
const assistantAnswerPackageBuilder_1 = require("./assistantAnswerPackageBuilder");
function stripTechnicalTail(text) {
return String(text ?? "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.trim();
}
function buildDeepAnswerArtifacts(input) {
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
const answerStructureV11 = input.featureContractsV11
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
? input.compositionAnswerStructureV11
: (0, assistantAnswerPackageBuilder_1.buildAssistantAnswerStructureV11)({
assistantReply: safeAssistantReply,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
})
: null;
return {
safeAssistantReply,
answerStructureV11
};
}
function buildAssistantConversationItem(input) {
return {
message_id: input.messageId,
session_id: input.sessionId,
role: "assistant",
text: input.text,
reply_type: input.replyType,
created_at: new Date().toISOString(),
trace_id: input.traceId,
debug: input.debug
};
}

View File

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnComposition = buildAssistantDeepTurnComposition;
const questionTypeResolver_1 = require("./questionTypeResolver");
const answerComposer_1 = require("./answerComposer");
function buildAssistantDeepTurnComposition(input) {
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? questionTypeResolver_1.resolveQuestionType;
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? answerComposer_1.composeAssistantAnswer;
const followupApplied = Boolean(input.followupUsage?.applied);
const focusDomainHint = followupApplied
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
: null;
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
const companyAnchorSet = input.companyAnchors;
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
const composition = composeAssistantAnswerSafe({
userMessage: input.userMessage,
routeSummary: input.routeSummary,
retrievalResults: input.retrievalResults,
requirements: input.requirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
focusDomainHint,
questionTypeHint: questionTypeClass,
companyAnchors: input.companyAnchors,
normalizationPeriodExplicit,
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
});
return {
focusDomainHint,
questionTypeClass,
hasPeriodInCompanyAnchors,
normalizationPeriodExplicit,
composition
};
}

View File

@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnRuntimeContext = buildAssistantDeepTurnRuntimeContext;
const KNOWN_P0_DOMAINS = new Set([
"settlements_60_62",
"vat_document_register_book",
"month_close_costs_20_44",
"fixed_asset_amortization"
]);
function toAnalysisContext(runtimeAnalysisContext) {
if (!runtimeAnalysisContext.active) {
return null;
}
return {
as_of_date: runtimeAnalysisContext.as_of_date,
period_from: runtimeAnalysisContext.period_from,
period_to: runtimeAnalysisContext.period_to,
source: runtimeAnalysisContext.source
};
}
function buildAssistantDeepTurnRuntimeContext(input) {
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
userMessage: input.userMessage,
companyAnchors,
normalized: input.normalizedPayload,
routeSummary: input.routeSummary
});
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
const focusDomainForGuards = inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
const temporalGuard = input.resolveTemporalGuard({
userMessage: input.userMessage,
normalized: input.normalizedPayload,
companyAnchors,
analysisContext
});
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const claimAnchorAudit = input.resolveClaimBoundAnchors({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
});
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
current: initialBusinessScopeResolution,
temporalGuard,
claimType: claimAnchorAudit.claim_type,
focusDomainHint: focusDomainForGuards,
userMessage: input.userMessage,
companyAnchors,
followupApplied: Boolean(input.followupUsage?.applied)
});
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
return {
companyAnchors,
initialBusinessScopeResolution,
inferredDomainByMessage,
focusDomainForGuards,
temporalGuard,
domainPolarityGuardInitial,
claimAnchorAudit,
businessScopeResolution,
resolvedRouteSummary,
liveTemporalHint
};
}

View File

@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnGroundingRuntime = runAssistantDeepTurnGroundingRuntime;
const assistantOrchestrationRuntimeAdapter_1 = require("./assistantOrchestrationRuntimeAdapter");
const assistantDeepTurnGuardRuntimeAdapter_1 = require("./assistantDeepTurnGuardRuntimeAdapter");
function runAssistantDeepTurnGroundingRuntime(input) {
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? assistantOrchestrationRuntimeAdapter_1.runAssistantCoverageGroundingPipeline;
const applyGroundingEligibilitySafe = input.applyGroundingEligibilityFn ??
((payload) => (0, assistantDeepTurnGuardRuntimeAdapter_1.applyAssistantDeepTurnGroundingEligibility)(payload));
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.rbpPlanAudit
});
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.faPlanAudit
});
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
routeSummary: input.routeSummary,
normalized: input.normalizedPayload,
userMessage: input.userMessage,
retrievalResults: input.retrievalResults,
requirementExtraction: input.requirementExtraction,
extractRequirements: input.extractRequirements,
evaluateCoverage: input.evaluateCoverage,
checkGrounding: input.checkGrounding
});
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
groundingCheckBase,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
evidenceAudit: input.evidenceAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
return {
rbpLiveRouteAudit,
faLiveRouteAudit,
coverageEvaluation,
groundingCheckBase,
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
groundingCheck: groundingEligibilityRuntime.groundingCheck
};
}

View File

@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyAssistantDeepTurnRetrievalGuards = applyAssistantDeepTurnRetrievalGuards;
exports.applyAssistantDeepTurnGroundingEligibility = applyAssistantDeepTurnGroundingEligibility;
const assistantClaimBoundEvidence_1 = require("./assistantClaimBoundEvidence");
const assistantRuntimeGuards_1 = require("./assistantRuntimeGuards");
function applyAssistantDeepTurnRetrievalGuards(input) {
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults;
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition;
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate;
const polarityGuardResult = applyDomainPolarityGuardSafe({
retrievalResults: input.retrievalResults,
guard: input.domainPolarityGuardInitial
});
const targetedEvidenceResult = applyTargetedEvidenceSafe({
retrievalResults: polarityGuardResult.retrievalResults,
claimAudit: input.claimAnchorAudit
});
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
retrievalResults: targetedEvidenceResult.retrievalResults,
temporal: input.temporalGuard,
focusDomainHint: input.focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
return {
retrievalResults: evidenceGateResult.retrievalResults,
polarityGuardResult,
targetedEvidenceResult,
evidenceGateResult
};
}
function applyAssistantDeepTurnGroundingEligibility(input) {
const evaluateGroundedAnswerEligibilitySafe = input.evaluateGroundedAnswerEligibilityFn ?? assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility;
const applyEligibilityToGroundingCheckSafe = input.applyEligibilityToGroundingCheckFn ??
((groundingCheck, eligibility) => (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheck, eligibility));
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
temporal: input.temporalGuard,
polarity: input.polarityAudit,
evidence: input.evidenceAudit,
claimAnchors: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
return {
groundedAnswerEligibilityGuard,
groundingCheck
};
}

View File

@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnPackagingInput = buildAssistantDeepTurnPackagingInput;
function buildAssistantDeepTurnPackagingInput(args) {
return {
...args,
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
followupStateUsage: args.followupStateUsage ?? null,
composition: {
reply_type: args.composition.reply_type,
fallback_type: args.composition.fallback_type,
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
}
};
}

View File

@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantDeepTurnPackaging = assembleAssistantDeepTurnPackaging;
const assistantEvidenceBundleAssembler_1 = require("./assistantEvidenceBundleAssembler");
const assistantContractsBundleAssembler_1 = require("./assistantContractsBundleAssembler");
const assistantDeepResponseAssembler_1 = require("./assistantDeepResponseAssembler");
const assistantDebugPayloadAssembler_1 = require("./assistantDebugPayloadAssembler");
const assistantMessageLogAssembler_1 = require("./assistantMessageLogAssembler");
function assembleAssistantDeepTurnPackaging(input) {
const normalizedPayload = (input.normalized.normalized ?? null);
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? normalizedPayload?.["fragments"] : [];
const evidenceBundleAssembly = (0, assistantEvidenceBundleAssembler_1.assembleAssistantEvidenceBundle)({
retrievalCalls: input.retrievalCalls,
retrievalResults: input.retrievalResults
});
const contractsBundleV1 = (0, assistantContractsBundleAssembler_1.assembleAssistantContractsBundleV1)({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContextForContract,
executionPlan: input.executionPlan,
requirements: input.requirementExtractionRequirements,
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
replyType: input.composition.reply_type,
coverageReport: input.coverageReport,
grounding: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const deepAnswerArtifacts = (0, assistantDeepResponseAssembler_1.buildDeepAnswerArtifacts)({
safeAssistantReplyBase: input.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const debug = (0, assistantDebugPayloadAssembler_1.buildDeepAnalysisDebugPayload)({
traceId: input.normalized.trace_id,
promptVersion: input.normalized.prompt_version,
schemaVersion: input.normalized.schema_version,
fallbackType: input.composition.fallback_type,
routeSummary: input.routeSummary,
fragments: normalizedFragments,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
routes: input.routesForDebug,
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
retrievalResults: input.retrievalResults,
groundingCheck: input.groundingCheck,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
},
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
normalizedPayload: normalizedPayload
});
const assistantItem = (0, assistantDeepResponseAssembler_1.buildAssistantConversationItem)({
messageId: input.messageId,
sessionId: input.sessionId,
text: deepAnswerArtifacts.safeAssistantReply,
replyType: input.composition.reply_type,
traceId: input.normalized.trace_id,
debug: debug
});
const deepAnalysisLogDetails = (0, assistantMessageLogAssembler_1.buildDeepAnalysisProcessedLogDetails)({
sessionId: input.sessionId,
messageId: input.messageId,
userMessage: input.userMessage,
normalizerOutput: input.normalized.normalized,
executionPlan: input.executionPlan,
resolvedExecutionState: input.resolvedExecutionState,
routes: input.routesForDebug,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResultsNormalized: input.retrievalResults,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
replyType: input.composition.reply_type,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
fallback_type: input.composition.fallback_type
},
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
assistantReply: deepAnswerArtifacts.safeAssistantReply,
traceId: input.normalized.trace_id
});
return {
evidenceBundleAssembly,
contractsBundleV1,
deepAnswerArtifacts,
debug,
assistantItem,
deepAnalysisLogDetails
};
}

View File

@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnPackagingRuntime = runAssistantDeepTurnPackagingRuntime;
const nanoid_1 = require("nanoid");
const assistantDeepTurnInputBuilder_1 = require("./assistantDeepTurnInputBuilder");
const assistantDeepTurnPackaging_1 = require("./assistantDeepTurnPackaging");
const assistantDeepTurnPrePackagingContext_1 = require("./assistantDeepTurnPrePackagingContext");
const assistantInvestigationStateRuntimeAdapter_1 = require("./assistantInvestigationStateRuntimeAdapter");
function runAssistantDeepTurnPackagingRuntime(input) {
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? assistantDeepTurnPrePackagingContext_1.buildAssistantDeepTurnPrePackagingContext;
const buildInvestigationStateSnapshotSafe = input.buildInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.buildAssistantInvestigationStateSnapshot;
const persistInvestigationStateSnapshotSafe = input.persistInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.persistAssistantInvestigationStateSnapshot;
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? assistantDeepTurnInputBuilder_1.buildAssistantDeepTurnPackagingInput;
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assistantDeepTurnPackaging_1.assembleAssistantDeepTurnPackaging;
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
normalizedPayload: input.normalized.normalized,
routeSummary: input.routeSummary,
runtimeAnalysisContext: input.runtimeAnalysisContext,
assistantReply: input.composition.assistant_reply,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
sanitizeReply: input.sanitizeReply
});
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
previousState: input.previousInvestigationState,
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.composition.reply_type,
followupApplied: input.followupApplied
});
persistInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
sessionId: input.sessionId,
snapshot: investigationStateSnapshot,
persist: input.persistInvestigationState
});
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${(0, nanoid_1.nanoid)(10)}`;
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
sessionId: input.sessionId,
messageId,
userMessage: input.userMessage,
normalized: input.normalized,
normalizedQuestion: input.normalizedQuestion,
routeSummary: input.routeSummary,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
executionPlan: input.executionPlan,
requirementExtractionRequirements: input.requirementExtractionRequirements,
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResults: input.retrievalResults,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
composition: {
reply_type: input.composition.reply_type,
fallback_type: input.composition.fallback_type,
answer_structure_v11: input.composition.answer_structure_v11,
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
problem_units_used_count: input.composition.problem_units_used_count,
problem_answer_mode: input.composition.problem_answer_mode,
problem_unit_ids_used: input.composition.problem_unit_ids_used
},
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
investigationStateSnapshot,
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
});
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
return {
messageId,
investigationStateSnapshot,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
debug: deepTurnPackaging.debug,
assistantItem: deepTurnPackaging.assistantItem,
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
};
}

View File

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnExecutionPlan = buildAssistantDeepTurnExecutionPlan;
function buildAssistantDeepTurnExecutionPlan(input) {
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
let executionPlan = input.toExecutionPlan(input.routeSummary, input.normalizedPayload, input.userMessage, requirementExtraction.byFragment);
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = rbpRoutePlanEnforcement.executionPlan;
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = faRoutePlanEnforcement.executionPlan;
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
return {
requirementExtraction,
executionPlan,
rbpRoutePlanEnforcement,
faRoutePlanEnforcement
};
}

View File

@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnPrePackagingContext = buildAssistantDeepTurnPrePackagingContext;
function buildAssistantDeepTurnPrePackagingContext(input) {
return {
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
analysisContextForContract: input.runtimeAnalysisContext.active
? {
as_of_date: input.runtimeAnalysisContext.as_of_date,
period_from: input.runtimeAnalysisContext.period_from,
period_to: input.runtimeAnalysisContext.period_to,
source: input.runtimeAnalysisContext.source,
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
}
: null,
routesForDebug: input.buildDebugRoutes(input.routeSummary),
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
};
}

View File

@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnSuccessResponse = buildAssistantDeepTurnSuccessResponse;
function buildAssistantDeepTurnSuccessResponse(input) {
return {
ok: true,
session_id: input.sessionId,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.conversationItem,
debug: input.debug,
conversation: input.conversation
};
}

View File

@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeAssistantDeepTurnRetrievalPlan = executeAssistantDeepTurnRetrievalPlan;
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
function buildRouteExecutorErrorRawResult(route, message) {
return {
status: "error",
result_type: "summary",
items: [],
summary: {
route
},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Route executor failed."],
errors: [message]
};
}
async function executeAssistantDeepTurnRetrievalPlan(input) {
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? retrievalResultNormalizer_1.normalizeRetrievalResult;
const retrievalCalls = [];
const retrievalResultsRaw = [];
const retrievalResults = [];
for (const planItem of input.executionPlan) {
if (!planItem.should_execute) {
retrievalCalls.push({
fragment_id: planItem.fragment_id,
requirement_ids: planItem.requirement_ids,
route: planItem.route,
status: "skipped",
query_text: planItem.fragment_text,
reason: input.mapNoRouteReason(planItem.no_route_reason)
});
retrievalResults.push(input.buildSkippedResult(planItem));
continue;
}
retrievalCalls.push({
fragment_id: planItem.fragment_id,
requirement_ids: planItem.requirement_ids,
route: planItem.route,
status: "executed",
query_text: planItem.fragment_text,
reason: null
});
try {
const raw = await input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
temporalHint: input.liveTemporalHint
});
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: raw
});
retrievalResults.push(normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw));
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
retrievalCalls[retrievalCalls.length - 1].status = "failed";
retrievalCalls[retrievalCalls.length - 1].reason = message;
const rawError = buildRouteExecutorErrorRawResult(planItem.route, message);
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: rawError
});
retrievalResults.push(normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
}
}
return {
retrievalCalls,
retrievalResultsRaw,
retrievalResults
};
}

View File

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantEvidenceBundle = assembleAssistantEvidenceBundle;
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
function buildRetrievalStatus(retrievalResults) {
return retrievalResults.map((item) => ({
fragment_id: item.fragment_id,
requirement_ids: item.requirement_ids,
route: item.route,
status: item.status,
result_type: item.result_type
}));
}
function assembleAssistantEvidenceBundle(input) {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
return {
evidenceBundleContractV1: (0, assistantOrchestrationContracts_1.buildAssistantEvidenceBundleContractV1)({
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
retrievalResults
}),
retrievalStatus: buildRetrievalStatus(retrievalResults)
};
}

View File

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantInvestigationStateSnapshot = buildAssistantInvestigationStateSnapshot;
exports.persistAssistantInvestigationStateSnapshot = persistAssistantInvestigationStateSnapshot;
const investigationState_1 = require("./investigationState");
function buildAssistantInvestigationStateSnapshot(input) {
if (!input.featureEnabled || !input.previousState) {
return null;
}
return (0, investigationState_1.updateInvestigationState)({
previous: input.previousState,
timestamp: input.timestamp,
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.requirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.replyType,
followupApplied: input.followupApplied
});
}
function persistAssistantInvestigationStateSnapshot(input) {
if (!input.featureEnabled || !input.snapshot) {
return false;
}
input.persist(input.sessionId, input.snapshot);
return true;
}

View File

@ -0,0 +1,102 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnalysisProcessedLogDetails = buildDeepAnalysisProcessedLogDetails;
function toAnalysisContext(input) {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function resolveCoverageStatus(coverageReport) {
return coverageReport.requirements_total === coverageReport.requirements_covered &&
coverageReport.requirements_uncovered.length === 0 &&
coverageReport.requirements_partially_covered.length === 0
? "full"
: "partial_or_limited";
}
function buildDeepAnalysisProcessedLogDetails(input) {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
session_id: input.sessionId,
message_id: input.messageId,
user_message: input.userMessage,
normalizer_output: input.normalizerOutput,
execution_plan: input.executionPlan,
resolved_execution_state: input.resolvedExecutionState,
routes: input.routes,
retrieval_calls: input.retrievalCalls,
retrieval_results_raw: input.retrievalResultsRaw,
retrieval_results_normalized: input.retrievalResultsNormalized,
requirements_extracted: input.requirementsExtracted,
requirements_total: input.coverageReport.requirements_total,
requirements_covered: input.coverageReport.requirements_covered,
requirements_uncovered: input.coverageReport.requirements_uncovered,
coverage_status: resolveCoverageStatus(input.coverageReport),
answer_grounding_status: input.groundingCheck.status,
reply_semantic_type: input.replyType,
why_included_summary: input.groundingCheck.why_included_summary,
selection_reason_summary: input.groundingCheck.selection_reason_summary,
route_subject_match: input.groundingCheck.route_subject_match,
clarification_target: input.coverageReport.clarification_needed_for,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
fallback_type: input.compositionDebug.fallback_type,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
trace_id: input.traceId
};
}

View File

@ -0,0 +1,148 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.classifyAssistantOutcomeClassV1 = classifyAssistantOutcomeClassV1;
exports.buildAssistantQueryFrameContractV1 = buildAssistantQueryFrameContractV1;
exports.buildAssistantExecutionPlanContractV1 = buildAssistantExecutionPlanContractV1;
exports.buildAssistantEvidenceBundleContractV1 = buildAssistantEvidenceBundleContractV1;
exports.buildAssistantCoverageContractV1 = buildAssistantCoverageContractV1;
function normalizeSnapshotMode(value) {
const token = String(value ?? "").trim();
if (token === "force_snapshot" || token === "force_live") {
return token;
}
return "auto";
}
function extractFragmentsTotal(normalized) {
if (!normalized || typeof normalized !== "object") {
return 0;
}
const source = normalized;
const fragments = source.fragments;
return Array.isArray(fragments) ? fragments.length : 0;
}
function collectEvidenceTotals(retrievalResults) {
let evidenceTotal = 0;
const sourceRefs = new Set();
let limitationTotal = 0;
let errorTotal = 0;
for (const result of retrievalResults) {
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
for (const evidence of result.evidence ?? []) {
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
if (ref) {
sourceRefs.add(ref);
}
}
}
return {
evidence_total: evidenceTotal,
source_refs_total: sourceRefs.size,
limitation_total: limitationTotal,
error_total: errorTotal
};
}
function classifyAssistantOutcomeClassV1(input) {
const replyType = input.replyType;
const grounding = input.grounding;
const coverage = input.coverageReport;
const hasOnlyErrors = input.retrievalResults.length > 0 &&
input.retrievalResults.every((item) => item.status === "error");
if (replyType === "backend_error" || hasOnlyErrors) {
return "BLOCKED_BY_TOOLING";
}
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
return "MISROUTED";
}
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
return "BLOCKED_BY_AMBIGUITY";
}
if (replyType === "out_of_scope") {
return "BLOCKED_BY_AMBIGUITY";
}
const fullCoverage = coverage.requirements_total > 0 &&
coverage.requirements_total === coverage.requirements_covered &&
coverage.requirements_uncovered.length === 0 &&
coverage.requirements_partially_covered.length === 0 &&
coverage.clarification_needed_for.length === 0 &&
coverage.out_of_scope_requirements.length === 0;
if (fullCoverage && grounding.status === "grounded") {
return "FULLY_ANSWERED";
}
const hasAnyCoverage = coverage.requirements_covered > 0 ||
coverage.requirements_partially_covered.length > 0 ||
grounding.status === "partial";
if (hasAnyCoverage) {
return "PARTIALLY_ANSWERED";
}
const missingRequirementSignal = grounding.missing_requirements.length > 0 ||
coverage.requirements_uncovered.length > 0 ||
coverage.requirements_total > 0;
const possibleBindingFailure = replyType === "no_grounded_answer" &&
missingRequirementSignal &&
grounding.route_subject_match;
if (possibleBindingFailure) {
return "FAILED_TO_BIND_ENTITIES";
}
return "BLOCKED_BY_MISSING_DATA";
}
function buildAssistantQueryFrameContractV1(input) {
const analysis = input.analysisContext
? {
as_of_date: input.analysisContext.as_of_date ?? null,
period_from: input.analysisContext.period_from ?? null,
period_to: input.analysisContext.period_to ?? null,
source: input.analysisContext.source ?? null,
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
}
: null;
return {
schema_version: "assistant_query_frame_v1",
original_user_question: String(input.userMessage ?? ""),
normalized_question: String(input.normalizedQuestion ?? ""),
route_summary_mode: input.routeSummary?.mode ?? "none",
fragments_total: extractFragmentsTotal(input.normalized),
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
analysis_context: analysis
};
}
function buildAssistantExecutionPlanContractV1(input) {
return {
schema_version: "assistant_execution_plan_v1",
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
fragment_id: String(item.fragment_id ?? ""),
route: String(item.route ?? ""),
should_execute: Boolean(item.should_execute),
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
no_route_reason: item.no_route_reason ?? null,
clarification_reason: item.clarification_reason ?? null
})),
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
};
}
function buildAssistantEvidenceBundleContractV1(input) {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
const breakdown = {
ok: retrievalResults.filter((item) => item.status === "ok").length,
partial: retrievalResults.filter((item) => item.status === "partial").length,
empty: retrievalResults.filter((item) => item.status === "empty").length,
error: retrievalResults.filter((item) => item.status === "error").length
};
const totals = collectEvidenceTotals(retrievalResults);
return {
schema_version: "assistant_evidence_bundle_v1",
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
retrieval_results_total: retrievalResults.length,
retrieval_status_breakdown: breakdown,
...totals
};
}
function buildAssistantCoverageContractV1(input) {
return {
schema_version: "assistant_coverage_contract_v1",
coverage_report: input.coverageReport,
grounding: input.grounding,
outcome_class: input.outcomeClass
};
}

View File

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantCoverageGroundingPipeline = runAssistantCoverageGroundingPipeline;
function runAssistantCoverageGroundingPipeline(input) {
const requirementExtraction = input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
const groundingCheckBase = input.checkGrounding(input.userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, input.retrievalResults);
return {
requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}

View File

@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildFragmentTextById = buildFragmentTextById;
exports.buildExecutionPlanFromRoute = buildExecutionPlanFromRoute;
exports.buildDebugRoutesFromRoute = buildDebugRoutesFromRoute;
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function enrichFragmentTextWithHints(fragment, text) {
const baseText = String(text ?? "").trim();
const accountHints = Array.isArray(fragment.account_hints)
? Array.from(new Set(fragment.account_hints.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0)))
: [];
if (accountHints.length === 0) {
return baseText;
}
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
if (hasAccountInText) {
return baseText;
}
return `${baseText}, по счету ${accountHints.join(", ")}`;
}
function buildFragmentTextById(fragments) {
const result = new Map();
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
if (!fragmentId) {
continue;
}
const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
"";
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
}
return result;
}
function buildExecutionPlanFromRoute(input) {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
route: input.routeSummary.route_hint,
should_execute: true,
fragment_text: input.userMessage,
no_route_reason: null,
clarification_reason: null
}
];
}
return input.routeSummary.decisions.map((decision) => {
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
if (decision.route === "no_route") {
return {
fragment_id: decision.fragment_id,
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
route: "no_route",
should_execute: false,
fragment_text: text,
no_route_reason: decision.no_route_reason ?? null,
clarification_reason: decision.clarification_reason ?? null
};
}
return {
fragment_id: decision.fragment_id,
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
route: decision.route,
should_execute: true,
fragment_text: text,
no_route_reason: null,
clarification_reason: decision.clarification_reason ?? null
};
});
}
function buildDebugRoutesFromRoute(input) {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
route: input.routeSummary.route_hint,
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
confidence: input.routeSummary.confidence,
intent_class: input.routeSummary.intent_class
}
];
}
return input.routeSummary.decisions.map((decision) => ({
fragment_id: decision.fragment_id,
route: decision.route,
reason: decision.reason,
route_status: decision.route_status ?? null,
no_route_reason: decision.no_route_reason ?? null,
clarification_reason: decision.clarification_reason ?? null,
execution_readiness: decision.execution_readiness ?? null
}));
}

View File

@ -584,7 +584,78 @@ function toTemporalGuardInput(window, fallback) {
const value = String(fallback ?? "").trim();
return value || null;
}
function normalizeIsoDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function normalizeTemporalWindow(input) {
const asOfDate = normalizeIsoDate(input.asOfDate);
if (asOfDate) {
return {
from: asOfDate,
to: asOfDate,
granularity: "day"
};
}
const from = normalizeIsoDate(input.periodFrom);
const to = normalizeIsoDate(input.periodTo);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: from === to ? "day" : "month"
};
}
function resolveTemporalGuard(input) {
const analysisWindow = normalizeTemporalWindow({
asOfDate: input.analysisContext?.as_of_date,
periodFrom: input.analysisContext?.period_from,
periodTo: input.analysisContext?.period_to
});
if (analysisWindow) {
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
return {
raw_time_anchor: analysisWindow.from,
raw_time_scope: guardInput,
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
resolved_primary_period: analysisWindow,
effective_primary_period: analysisWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: "aligned",
temporal_resolution_source: source,
temporal_guard_basis: "raw_time_scope_unlocked",
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
allowed_context_window: null,
controlled_temporal_expansion_enabled: false,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: false,
reason_codes: ["analysis_context_applied"]
};
}
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
@ -654,9 +725,14 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
return executionPlan;
}
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
const periodLabel = primaryWindow
? `${primaryWindow.from}..${primaryWindow.to}`
: temporal.resolved_time_anchor
? temporal.resolved_time_anchor
: "active_period";
const hint = primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
@ -1319,15 +1395,15 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
? "no_grounded_answer"
: "partial";
const reasonMap = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.commitAssistantTurnAndLog = commitAssistantTurnAndLog;
function commitAssistantTurnAndLog(input) {
input.appendItem(input.sessionId, input.assistantItem);
const currentSession = input.getSession(input.sessionId);
if (currentSession) {
input.persistSession(currentSession);
}
const conversation = input.cloneConversation(currentSession?.items ?? []);
input.logEvent({
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId: input.sessionId,
eventType: input.eventType,
details: input.logDetails
});
return {
currentSession,
conversation
};
}

View File

@ -169,6 +169,29 @@ function parseRawQuestions(rawQuestions) {
.filter(Boolean);
return byLine.length > 0 ? byLine : [text];
}
function normalizeAnalysisDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function executionReadinessOf(fragment) {
return "execution_readiness" in fragment ? fragment.execution_readiness : "executable";
}
@ -759,6 +782,13 @@ class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: payload.analysisDate ?? undefined,
analysis_context: payload.analysisDate
? {
as_of_date: payload.analysisDate,
source: "eval_analysis_date"
}
: undefined,
eval_label: runId,
case_id: item.case_id,
eval_mode: payload.mode
@ -1553,6 +1583,7 @@ class EvalService {
const suite = parseAssistantSuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
const diagnostics = [];
let requestsTotal = 0;
@ -1579,6 +1610,15 @@ class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
}));
turnResponses.push(response);
@ -1820,6 +1860,7 @@ class EvalService {
eval_target: "assistant_stage1",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@ -1887,6 +1928,7 @@ class EvalService {
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${(0, nanoid_1.nanoid)(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
const diagnostics = [];
let requestsTotal = 0;
@ -1915,6 +1957,15 @@ class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
}));
turnResponses.push(response);
@ -2090,6 +2141,7 @@ class EvalService {
eval_target: "assistant_stage2",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@ -2172,6 +2224,7 @@ class EvalService {
async run(payload) {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
if (evalTarget === "assistant_stage1") {
return this.runAssistantStage1({
normalizeConfig: payload.normalizeConfig,
@ -2180,6 +2233,7 @@ class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@ -2191,6 +2245,7 @@ class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@ -2231,6 +2286,7 @@ class EvalService {
return this.runV2({
...payload,
mode,
analysisDate: analysisDate ?? undefined,
cases: filtered
});
}
@ -2256,6 +2312,13 @@ class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: analysisDate ?? undefined,
analysis_context: analysisDate
? {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
: undefined,
expected_route: item.expected.route_hint,
eval_label: runId,
case_id: item.case_id,
@ -2366,6 +2429,7 @@ class EvalService {
timestamp: new Date().toISOString(),
mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
dataset: {
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",

View File

@ -99,7 +99,7 @@ function resolveQuestionType(input) {
if (bestType !== "unknown") {
return bestType;
}
if (/[?пјџ]/u.test(text)) {
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
return "why_breaks";
}
return "unknown";

View File

@ -93,6 +93,7 @@ interface RunSummary {
llm_provider: string | null;
model: string | null;
use_mock: boolean | null;
analysis_date: string | null;
prompt_version: string | null;
schema_version: string | null;
suite_id: string | null;
@ -1012,6 +1013,7 @@ function buildRunSummary(run: IndexedRun): RunSummary {
llm_provider: llmProvider,
model,
use_mock: toBooleanSafe(run.report.use_mock),
analysis_date: toStringSafe(run.report.analysis_date),
prompt_version: toStringSafe(run.report.prompt_version),
schema_version: toStringSafe(run.report.schema_version),
suite_id: toStringSafe(run.report.suite_id),

View File

@ -35,6 +35,7 @@ interface EvalAsyncJob {
eval_target: EvalTarget;
run_id: string;
case_set_file: string | null;
analysis_date: string | null;
total_cases: number;
completed_cases: number;
cases: EvalAsyncCaseInfo[];
@ -131,6 +132,32 @@ function normalizeCaseIds(value: unknown): string[] | undefined {
return normalized.length > 0 ? normalized : undefined;
}
function normalizeAnalysisDate(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return undefined;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return undefined;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return undefined;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function buildEvalPayloadFromBody(body: Record<string, unknown>): {
normalizeConfig: Omit<NormalizeRequestPayload, "userQuestion" | "context">;
caseIds?: string[];
@ -140,7 +167,11 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
rawQuestions?: string;
evalTarget: EvalTarget;
compareWithReportFile?: string;
analysisDate?: string;
} {
const analysisDate =
normalizeAnalysisDate(body.analysis_date) ??
normalizeAnalysisDate(body.analysisDate);
return {
normalizeConfig: (body.normalizeConfig ?? {}) as Omit<NormalizeRequestPayload, "userQuestion" | "context">,
caseIds: normalizeCaseIds(body.caseIds),
@ -154,7 +185,8 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
? body.compare_with_report_file
: typeof body.comparisonBaselineReportFile === "string"
? body.comparisonBaselineReportFile
: undefined
: undefined,
analysisDate
};
}
@ -300,6 +332,7 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
eval_target: job.eval_target,
run_id: job.run_id,
case_set_file: job.case_set_file,
analysis_date: job.analysis_date,
total_cases: job.total_cases,
completed_cases: job.completed_cases,
error: job.error,
@ -314,7 +347,8 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
? Number(toRecord(job.report.metrics)?.score_index)
: null,
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
}
: null
};
@ -377,6 +411,7 @@ export function buildEvalRouter(services: AppServices): Router {
eval_target: payload.evalTarget,
run_id: runId,
case_set_file: runtimeCaseSetFile,
analysis_date: payload.analysisDate ?? null,
total_cases: caseSeeds.length,
completed_cases: 0,
cases: caseSeeds.map((item) => ({

View File

@ -490,8 +490,20 @@ function extractLooseByAnchorValue(text: string): string | undefined {
}
const lowered = token.toLowerCase();
const stopWords = new Set([
"какой",
"какая",
"какие",
"каких",
"каким",
"какими",
"каком",
"кто",
"что",
"мы",
"видим",
"контрагенту",
"контрагента",
"контрагентам",
"контре",
"компании",
"компанию",
@ -499,10 +511,14 @@ function extractLooseByAnchorValue(text: string): string | undefined {
"организацию",
"поставщику",
"поставщика",
"поставщикам",
"клиенту",
"клиента",
"клиентам",
"покупателю",
"покупателя",
"покупателям",
"заказчикам",
"партнеру",
"партнера",
"договору",
@ -618,6 +634,9 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"какая",
"какое",
"каких",
"каким",
"какими",
"каком",
"какому",
"какую",
"кто",
@ -632,6 +651,8 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"чья",
"чей",
"чью",
"мы",
"видим",
"самый",
"самая",
"самое",
@ -686,10 +707,23 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"контрагент",
"контрагенту",
"контрагента",
"контрагентам",
"компания",
"компании",
"организация",
"организации",
"поставщикам",
"клиентам",
"покупателям",
"заказчикам",
"аванс",
"авансы",
"проблемный",
"проблемные",
"проблемным",
"закрытия",
"закрыть",
"закрыты",
"год",
"года",
"г",
@ -795,7 +829,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
return true;
}
const questionCue =
/(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/[?]/u.test(String(rawValue ?? ""));
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);

View File

@ -667,6 +667,13 @@ function hasLifecycleSegmentationSignal(text: string): boolean {
}
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
const hasPaymentRiskLexeme =
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(
text
);
if (hasPaymentRiskLexeme) {
return false;
}
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
@ -768,6 +775,10 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
);
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
const asksMajorShare =
/(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(
text
);
const asksValue =
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
text
@ -797,6 +808,9 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
if (asksCounterpartySource && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
return true;
}
@ -920,6 +934,71 @@ function hasOpenContractsListSignal(text: string): boolean {
return true;
}
function hasSupplierTailRiskSignal(text: string): boolean {
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
}
function hasReceivablesLatencyRiskSignal(text: string): boolean {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
const hasBetweenShipmentAndPayment =
/между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
return true;
}
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
}
function hasSettlementGapSignal(text: string): boolean {
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
const hasNoDocumentForClosing =
/(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoDocumentForClosingReversed =
/(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoPayments =
/(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
const hasUnclosedAdvanceGap =
hasAdvance &&
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed);
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
}
function hasReconciliationMismatchSignal(text: string): boolean {
const hasCounterparty =
/(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
const hasMismatchLexeme =
/(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
return (
hasCounterparty &&
hasReconciliationLexeme &&
hasMismatchLexeme &&
hasBalanceLexeme &&
(hasLookupVerb || hasInterrogativeLookup)
);
}
function isLikelyCounterpartyToken(rawToken: string): boolean {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
@ -1273,6 +1352,38 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasSettlementGapSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["settlement_gap_signal_detected"]
};
}
if (hasReconciliationMismatchSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["reconciliation_mismatch_signal_detected"]
};
}
if (hasReceivablesLatencyRiskSignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_payment_lag_signal_detected"]
};
}
if (hasSupplierTailRiskSignal(text)) {
return {
intent: "list_payables_counterparties",
confidence: "medium",
reasons: ["supplier_tail_risk_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
@ -1299,7 +1410,9 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
if (
hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(
text
)
) {
return {
intent: "open_items_by_counterparty_or_contract",

View File

@ -30,6 +30,7 @@ interface NormalizedAddressRow {
interface AddressTryHandleOptions {
followupContext?: AddressFollowupContext | null;
analysisDateHint?: string | null;
}
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
@ -121,6 +122,36 @@ function parseFiniteNumber(value: unknown): number | null {
return null;
}
function normalizeAnalysisDateHint(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
if (!isoPrefix) {
return null;
}
const year = Number(isoPrefix[1]);
const month = Number(isoPrefix[2]);
const day = Number(isoPrefix[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
}
function valueAsString(value: unknown): string {
if (value === null || value === undefined) {
return "";
@ -788,6 +819,58 @@ function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCatego
return "UNKNOWN";
}
function normalizeLimitedReason(reason: string): string {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements: Array<[RegExp, string]> = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/address\s*v1/giu, "текущий адресный режим"],
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
[/live\s+recipe/giu, "текущий сценарий выборки"],
[/materialized\s+live-строках/giu, "доступном срезе данных"],
[/live-выборке/giu, "выборке данных"],
[/live-данных/giu, "данных"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\blookup\b/giu, "поиск"],
[/\bintent\b/giu, "сценария"],
[/\brecipe\b/giu, "шаблон выборки"],
[/\byakor\b/giu, "ориентир"],
[/\banchor\b/giu, "ориентир"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function normalizeLimitedNextStep(nextStep: string): string {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements: Array<[RegExp, string]> = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
[/\bintent\b/giu, "сценарий"],
[/\blookup\b/giu, "поиск"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
interface RowStageDiagnostics {
rawRowKeysSample: string[];
materializationDropReason:
@ -945,20 +1028,28 @@ function toLegacyMcpStatus(
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
const heading =
category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine =
category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
`Причина: ${reason}.`
reasonLine
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
}
return lines.join("\n");
}
@ -1057,7 +1148,24 @@ export class AddressQueryService {
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
const { mode, shape, intent, filters } = decompose;
const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) {
const hasTemporalFilter = Boolean(
(typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
);
if (!hasTemporalFilter) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: analysisDate
};
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
baseReasons.push("as_of_date_from_analysis_context");
}
}
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@ -1079,8 +1187,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
@ -1123,8 +1231,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});

View File

@ -1509,7 +1509,7 @@ export function composeFactualReply(
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
@ -1525,6 +1525,36 @@ export function composeFactualReply(
};
}
if (intent === "list_payables_counterparties") {
const lines = [
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк для ручной проверки."]
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_receivables_counterparties") {
const lines = [
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
@ -1628,14 +1658,7 @@ export function composeFactualReply(
};
}
const title =
intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")

View File

@ -0,0 +1,159 @@
import { FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 } from "../config";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import {
ANSWER_STRUCTURE_SCHEMA_VERSION,
type AnswerStructureV11,
type EvidenceLimitationReasonCode
} from "../types/stage1Contracts";
export interface BuildAssistantAnswerStructureV11Input {
assistantReply: string;
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
options?: {
enableEvidenceEnrichment?: boolean;
};
}
const EVIDENCE_LIMITATION_REASON_CODE_SET: ReadonlySet<EvidenceLimitationReasonCode> = new Set([
"snapshot_only",
"heuristic_inference",
"missing_mechanism",
"weak_source_mapping",
"insufficient_detail",
"unknown"
]);
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function isEvidenceLimitationReasonCode(value: string): value is EvidenceLimitationReasonCode {
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value as EvidenceLimitationReasonCode);
}
function firstNonEmptyLine(text: string): string {
const line = String(text ?? "")
.split("\n")
.map((item) => item.trim())
.find((item) => item.length > 0);
return (line ?? String(text ?? "")).slice(0, 220);
}
function buildClaimEvidenceLinks(
retrievalResults: UnifiedRetrievalResult[]
): NonNullable<AnswerStructureV11["evidence_block"]["claim_evidence_links"]> {
const byClaim = new Map<string, string[]>();
for (const result of retrievalResults) {
for (const evidence of result.evidence) {
const claimRef = String(evidence.claim_ref ?? "").trim();
if (!claimRef) {
continue;
}
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claimRef, evidenceIds]) => ({
claim_ref: claimRef,
evidence_ids: summarizeUnique(evidenceIds, 10)
}));
}
export function buildAssistantAnswerStructureV11(input: BuildAssistantAnswerStructureV11Input): AnswerStructureV11 {
const evidenceIds = summarizeUnique(
input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)),
10
);
const mechanismNotes = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence
.map((evidence) => evidence.mechanism_note)
.filter((note): note is string => typeof note === "string" && note.trim().length > 0)
),
6
);
const sourceRefs = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence
.map((evidence) => evidence.source_ref?.canonical_ref)
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
),
8
);
const limitationReasonCodes: EvidenceLimitationReasonCode[] = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence.flatMap((evidence) => {
const code = evidence.limitation?.reason_code;
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
})
),
8
).filter(isEvidenceLimitationReasonCode);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const limitations = summarizeUnique(
[...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons],
8
);
const clarificationQuestions = input.coverageReport.clarification_needed_for.map(
(item) => `Уточните требование ${item}.`
);
const recommendedActions = summarizeUnique(
[
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
...input.coverageReport.requirements_partially_covered.map(
(item) => `Доуточнить частично покрытое требование ${item}.`
)
],
6
);
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const enableEvidenceEnrichment =
input.options?.enableEvidenceEnrichment ?? FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
return {
schema_version: ANSWER_STRUCTURE_SCHEMA_VERSION,
answer_summary: firstNonEmptyLine(input.assistantReply),
direct_answer: input.assistantReply,
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: evidenceIds,
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
coverage_note:
input.coverageReport.requirements_total === input.coverageReport.requirements_covered
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(enableEvidenceEnrichment && claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: input.groundingCheck.missing_requirements,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
}

View File

@ -0,0 +1,97 @@
import type {
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
import {
buildAssistantCoverageContractV1,
buildAssistantExecutionPlanContractV1,
buildAssistantQueryFrameContractV1,
classifyAssistantOutcomeClassV1,
type AssistantCoverageContractV1,
type AssistantEvidenceBundleContractV1,
type AssistantExecutionPlanContractV1,
type AssistantOutcomeClassV1,
type AssistantQueryFrameContractV1
} from "./assistantOrchestrationContracts";
export interface AssistantContractsBundleV1 {
queryFrameContractV1: AssistantQueryFrameContractV1;
executionPlanContractV1: AssistantExecutionPlanContractV1;
outcomeClassV1: AssistantOutcomeClassV1;
coverageContractV1: AssistantCoverageContractV1;
assistantOrchestrationContractsV1: {
query_frame: AssistantQueryFrameContractV1;
execution_plan: AssistantExecutionPlanContractV1;
evidence_bundle: AssistantEvidenceBundleContractV1;
coverage: AssistantCoverageContractV1;
};
}
export function assembleAssistantContractsBundleV1(input: {
userMessage: string;
normalizedQuestion: string;
normalized: NormalizedPayload | null;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContext: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirements: AssistantRequirement[];
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
replyType: AssistantReplyType;
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantContractsBundleV1 {
const queryFrameContractV1 = buildAssistantQueryFrameContractV1({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContext
});
const executionPlanContractV1 = buildAssistantExecutionPlanContractV1({
executionPlan: input.executionPlan,
requirements: input.requirements
});
const outcomeClassV1 = classifyAssistantOutcomeClassV1({
replyType: input.replyType,
coverageReport: input.coverageReport,
grounding: input.grounding,
retrievalResults: input.retrievalResults
});
const coverageContractV1 = buildAssistantCoverageContractV1({
coverageReport: input.coverageReport,
grounding: input.grounding,
outcomeClass: outcomeClassV1
});
return {
queryFrameContractV1,
executionPlanContractV1,
outcomeClassV1,
coverageContractV1,
assistantOrchestrationContractsV1: {
query_frame: queryFrameContractV1,
execution_plan: executionPlanContractV1,
evidence_bundle: input.evidenceBundleContractV1,
coverage: coverageContractV1
}
};
}

View File

@ -0,0 +1,421 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { RouteHintSummary } from "../types/normalizer";
interface SubjectTokenRule {
critical: boolean;
patterns: string[];
routes?: string[];
}
export interface AssistantRequirementExtractionResult {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
const SUBJECT_TOKEN_RULES: Record<string, SubjectTokenRule> = {
nds: {
critical: true,
patterns: [
"vat",
"accumulationregister",
"ндс",
"книгипокупок",
"книгипродаж",
"налогнадобавленнуюстоимость"
]
},
os: {
critical: true,
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
},
saldo: {
critical: true,
patterns: ["balance", "saldo", "сальдо", "остат"]
},
counterparty: {
critical: false,
patterns: [
"counterparty",
"supplier",
"buyer",
"counterparty_id",
"journal_counterparty",
"document_has_counterparty",
"контрагент",
"поставщик",
"покупател"
],
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
},
document: {
critical: false,
patterns: [
"document",
"recorder",
"journal",
"document_refs_count",
"recorded_by_document",
"journal_refers_to_document",
"документ"
],
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
},
anomaly: {
critical: false,
patterns: [
"risk",
"risk_score",
"unknown_link_count",
"zero_guid",
"navigation_links",
"missing_counterparty_link",
"аномал",
"риск"
],
routes: ["store_feature_risk", "batch_refresh_then_store"]
},
chain: {
critical: false,
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
routes: ["hybrid_store_plus_live"]
}
};
function hasRegexMatch(corpus: string, pattern: RegExp): boolean {
try {
return pattern.test(corpus);
} catch {
return false;
}
}
function evaluateSubjectTokenMatch(
token: string,
corpus: string,
executedRoutes: Set<string>
): {
matched: boolean;
critical: boolean;
} {
if (token.startsWith("account_")) {
const account = token.slice("account_".length).trim();
if (!account) {
return { matched: false, critical: true };
}
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
}
const rule = SUBJECT_TOKEN_RULES[token];
if (rule) {
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
return { matched: byPattern || byRoute, critical: rule.critical };
}
return { matched: corpus.includes(token), critical: false };
}
function evidenceCountForRequirement(requirementId: string, result: UnifiedRetrievalResult): number {
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
if (evidence.length === 0) {
return 0;
}
const tagged = evidence.filter((item) => {
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
}).length;
if (tagged > 0) {
return tagged;
}
if (
Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId
) {
return evidence.length;
}
return 0;
}
function hasSubstantiveCoverageForRequirement(requirementId: string, result: UnifiedRetrievalResult): boolean {
const evidenceCount = evidenceCountForRequirement(requirementId, result);
if (evidenceCount > 0) {
return true;
}
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
if (
Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId
) {
return true;
}
}
return false;
}
export function extractRequirementsForRoute(input: {
routeSummary: RouteHintSummary | null;
userMessage: string;
fragmentTextById: Map<string, string>;
extractSubjectTokens: (text: string) => string[];
}): AssistantRequirementExtractionResult {
const byFragment = new Map<string, string[]>();
const requirements: AssistantRequirement[] = [];
const pushRequirement = (item: {
requirement_id: string;
source_fragment_id: string | null;
requirement_text: string;
status: AssistantRequirement["status"];
route: string | null;
}): void => {
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
requirements.push({
requirement_id: item.requirement_id,
source_fragment_id: item.source_fragment_id,
requirement_text: item.requirement_text,
subject_tokens: subjectTokens,
status: item.status,
route: item.route
});
if (item.source_fragment_id) {
const current = byFragment.get(item.source_fragment_id) ?? [];
current.push(item.requirement_id);
byFragment.set(item.source_fragment_id, current);
}
};
if (!input.routeSummary) {
pushRequirement({
requirement_id: "R1",
source_fragment_id: null,
requirement_text: input.userMessage,
status: "clarification_needed",
route: null
});
return { requirements, byFragment };
}
if (input.routeSummary.mode === "legacy_v1") {
pushRequirement({
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: input.userMessage,
status: "covered",
route: input.routeSummary.route_hint
});
return { requirements, byFragment };
}
input.routeSummary.decisions.forEach((decision, index) => {
const requirementId = `R${index + 1}`;
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
let status: AssistantRequirement["status"] = "covered";
if (decision.route === "no_route") {
if (decision.no_route_reason === "out_of_scope") {
status = "out_of_scope";
} else if (decision.no_route_reason === "insufficient_specificity") {
status = "clarification_needed";
} else {
status = "uncovered";
}
}
pushRequirement({
requirement_id: requirementId,
source_fragment_id: decision.fragment_id,
requirement_text: text,
status,
route: decision.route === "no_route" ? null : decision.route
});
});
return { requirements, byFragment };
}
export function evaluateCoverageForRequirements(
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
): {
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
} {
const statusByRequirement = new Map<string, Array<{ status: UnifiedRetrievalResult["status"]; substantive: boolean }>>();
for (const result of retrievalResults) {
for (const requirementId of result.requirement_ids) {
const list = statusByRequirement.get(requirementId) ?? [];
list.push({
status: result.status,
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
});
statusByRequirement.set(requirementId, list);
}
}
const resolvedRequirements = requirements.map((requirement) => {
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
return requirement;
}
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
if (states.length === 0) {
return { ...requirement, status: "uncovered" as const };
}
const hasAnySubstantive = states.some((item) => item.substantive);
if (!hasAnySubstantive) {
return { ...requirement, status: "uncovered" as const };
}
const hasOk = states.some((item) => item.status === "ok");
const hasPartial = states.some((item) => item.status === "partial");
const hasEmpty = states.some((item) => item.status === "empty");
const hasError = states.some((item) => item.status === "error");
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
return { ...requirement, status: "covered" as const };
}
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
return { ...requirement, status: "partially_covered" as const };
}
return { ...requirement, status: "uncovered" as const };
});
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
const requirementsUncovered = resolvedRequirements
.filter((item) => item.status === "uncovered")
.map((item) => item.requirement_id);
const requirementsPartiallyCovered = resolvedRequirements
.filter((item) => item.status === "partially_covered")
.map((item) => item.requirement_id);
const clarificationNeededFor = resolvedRequirements
.filter((item) => item.status === "clarification_needed")
.map((item) => item.requirement_id);
const outOfScopeRequirements = resolvedRequirements
.filter((item) => item.status === "out_of_scope")
.map((item) => item.requirement_id);
return {
requirements: resolvedRequirements,
coverage: {
requirements_total: resolvedRequirements.length,
requirements_covered: requirementsCovered,
requirements_uncovered: requirementsUncovered,
requirements_partially_covered: requirementsPartiallyCovered,
clarification_needed_for: clarificationNeededFor,
out_of_scope_requirements: outOfScopeRequirements
}
};
}
export function checkGroundingForRequirements(input: {
userMessage: string;
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
retrievalResults: UnifiedRetrievalResult[];
extractSubjectTokens: (text: string) => string[];
}): AnswerGroundingCheck {
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
const subjectTokens = input.extractSubjectTokens(input.userMessage);
const executedRoutes = new Set(
input.retrievalResults
.filter((item) => item.status !== "error")
.map((item) => item.route)
.filter(Boolean)
);
const retrievalCorpus = JSON.stringify(
input.retrievalResults.map((item) => ({
route: item.route,
result_type: item.result_type,
summary: item.summary,
items: item.items,
evidence: item.evidence,
why_included: item.why_included,
selection_reason: item.selection_reason,
risk_factors: item.risk_factors,
business_interpretation: item.business_interpretation
}))
).toLowerCase();
const missingSubjectTokens: string[] = [];
const missingCriticalTokens: string[] = [];
for (const token of subjectTokens) {
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
if (!match.matched) {
missingSubjectTokens.push(token);
if (match.critical) {
missingCriticalTokens.push(token);
}
}
}
const onlyAccountCriticalMissing =
missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
const accountOnlyMismatchRecoverable =
hasMaterialResults &&
input.coverage.requirements_covered > 0 &&
onlyAccountCriticalMissing &&
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
const routeSubjectMatch =
!hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
let status: AnswerGroundingCheck["status"] = "grounded";
const reasons: string[] = [];
if (!routeSubjectMatch) {
status = "route_mismatch_blocked";
reasons.push(
`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`
);
} else if (accountOnlyMismatchRecoverable) {
status = "partial";
reasons.push(
`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`
);
} else if (input.coverage.requirements_covered === 0) {
status = "no_grounded_answer";
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
} else if (
input.coverage.requirements_uncovered.length > 0 ||
input.coverage.requirements_partially_covered.length > 0 ||
input.coverage.clarification_needed_for.length > 0 ||
input.coverage.out_of_scope_requirements.length > 0
) {
status = "partial";
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
}
if (whyIncludedSummary.length === 0) {
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
}
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
}
const missingRequirements = [
...input.coverage.requirements_uncovered,
...input.coverage.requirements_partially_covered,
...input.coverage.clarification_needed_for,
...input.coverage.out_of_scope_requirements
];
return {
status,
route_subject_match: routeSubjectMatch,
missing_requirements: missingRequirements,
reasons,
why_included_summary: whyIncludedSummary,
selection_reason_summary: selectionReasonSummary
};
}

View File

@ -93,6 +93,13 @@ interface LiveMcpCallExecution {
error?: string | null;
}
interface LiveTemporalHint {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
}
type BroadnessLevel = "low" | "medium" | "high";
interface BroadQueryAssessment {
@ -262,6 +269,32 @@ function formatIsoDateUtc(date: Date): string {
return `${year}-${month}-${day}`;
}
function normalizeIsoDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function monthEndFromIso(isoDate: string): string | null {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
@ -329,14 +362,28 @@ function hasFixedAssetAmortizationSignal(text: string): boolean {
);
}
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
function buildLiveMcpCallPlan(route: string, fragmentText: string, temporalHint?: LiveTemporalHint | null): 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 hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
const primaryTo =
periodScope.to ??
hintedPeriodTo ??
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
const buildPrimaryQuery = (limit: number): string =>
primaryFrom && primaryTo
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
const buildCarryQuery = (limit: number): string =>
carryFrom && carryTo
? buildLiveRangeQuery(carryFrom, carryTo, limit)
: buildPrimaryQuery(limit);
const faClaim =
preferredDomainHint === "fixed_asset_amortization" ||
@ -352,7 +399,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_amortization_documents_in_period",
purpose: "seed_amortization_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -360,7 +407,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_fixed_asset_movements_accounts_01_02",
purpose: "collect_fa_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -368,7 +415,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_fixed_asset_cards_expected_for_period",
purpose: "build_expected_fa_set",
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -376,7 +423,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
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),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@ -400,7 +447,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_source_documents_in_period",
purpose: "seed_vat_source_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -408,7 +455,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_invoice_links_in_period",
purpose: "collect_invoice_links",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -416,7 +463,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_register_entries_in_period",
purpose: "collect_vat_register_entries",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -424,7 +471,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_book_entries_in_period",
purpose: "collect_vat_book_entries",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@ -464,7 +511,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_rbp_writeoff_documents_in_period",
purpose: "seed_writeoff_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@ -472,7 +519,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_rbp_object_movements_account_97",
purpose: "collect_rbp_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97"]
@ -480,7 +527,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_month_close_entries_linked_to_rbp",
purpose: "link_month_close_to_rbp",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@ -488,7 +535,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "compute_end_period_residual_by_rbp_object",
purpose: "collect_residual_tail_signals",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(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"]
@ -1849,7 +1896,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (
/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
lower
) ||
hasFixedAssetAccountScope
@ -2855,7 +2902,13 @@ export class AssistantDataLayer {
return enforceBroadQueryGuards(route, fragmentText, result);
}
public async executeRouteRuntime(route: string, fragmentText: string): Promise<RawRetrievalResult> {
public async executeRouteRuntime(
route: string,
fragmentText: string,
options?: {
temporalHint?: LiveTemporalHint | null;
}
): Promise<RawRetrievalResult> {
const base = this.executeRoute(route, fragmentText);
if (!FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
return base;
@ -2864,7 +2917,7 @@ export class AssistantDataLayer {
return base;
}
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
return this.mergeWithLiveOverlay(base, liveOverlay);
}
@ -2922,9 +2975,13 @@ export class AssistantDataLayer {
return merged;
}
private async fetchLiveMcpOverlay(route: string, fragmentText: string): Promise<LiveMcpOverlay> {
private async fetchLiveMcpOverlay(
route: string,
fragmentText: string,
temporalHint?: LiveTemporalHint | null
): Promise<LiveMcpOverlay> {
const endpoint = this.buildMcpUrl("/api/execute_query");
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
const accountScope =
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"

View File

@ -0,0 +1,160 @@
import type { AssistantDebugPayload } from "../types/assistant";
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
export interface DeepAnalysisDebugPayloadInput {
traceId: string;
promptVersion: string;
schemaVersion: string;
fallbackType: unknown;
routeSummary: unknown;
fragments: unknown[];
requirementsExtracted: unknown[];
coverageReport: unknown;
routes: Array<Record<string, unknown>>;
retrievalStatus: RetrievalStatusItem[];
retrievalResults: unknown[];
groundingCheck: unknown;
droppedIntentSegments: string[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
compositionDebug: {
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
};
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
outcomeClassV1: unknown;
assistantOrchestrationContractsV1: unknown;
answerStructureV11: unknown;
investigationStateSnapshot: unknown;
normalizedPayload: unknown;
}
function toAnalysisContext(input: DeepAnalysisDebugPayloadInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInput): Record<string, unknown> {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
trace_id: input.traceId,
prompt_version: input.promptVersion,
schema_version: input.schemaVersion,
fallback_type: input.fallbackType,
route_summary: input.routeSummary,
fragments: input.fragments,
requirements_extracted: input.requirementsExtracted,
coverage_report: input.coverageReport,
routes: input.routes,
retrieval_status: input.retrievalStatus,
retrieval_results: input.retrievalResults,
answer_grounding_check: input.groundingCheck,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
normalized: input.normalizedPayload
};
}

View File

@ -0,0 +1,68 @@
import { buildAssistantAnswerStructureV11 } from "./assistantAnswerPackageBuilder";
import type {
AssistantConversationItem,
AssistantDebugPayload,
AssistantReplyType,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
export interface DeepAnswerArtifacts {
safeAssistantReply: string;
answerStructureV11: AnswerStructureV11 | null;
}
function stripTechnicalTail(text: string): string {
return String(text ?? "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.trim();
}
export function buildDeepAnswerArtifacts(input: {
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
compositionAnswerStructureV11: AnswerStructureV11 | null | undefined;
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): DeepAnswerArtifacts {
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
const answerStructureV11 = input.featureContractsV11
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
? input.compositionAnswerStructureV11
: buildAssistantAnswerStructureV11({
assistantReply: safeAssistantReply,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
})
: null;
return {
safeAssistantReply,
answerStructureV11
};
}
export function buildAssistantConversationItem(input: {
messageId: string;
sessionId: string;
text: string;
replyType: AssistantReplyType;
traceId: string | null;
debug: AssistantDebugPayload;
}): AssistantConversationItem {
return {
message_id: input.messageId,
session_id: input.sessionId,
role: "assistant",
text: input.text,
reply_type: input.replyType,
created_at: new Date().toISOString(),
trace_id: input.traceId,
debug: input.debug
};
}

View File

@ -0,0 +1,77 @@
import type { AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import type { QuestionTypeClass } from "./questionTypeResolver";
import { resolveQuestionType } from "./questionTypeResolver";
import { composeAssistantAnswer } from "./answerComposer";
export interface BuildAssistantDeepTurnCompositionInput {
userMessage: string;
routeSummary: RouteHintSummary | null;
retrievalResults: UnifiedRetrievalResult[];
requirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
followupUsage: unknown | null | undefined;
investigationState: InvestigationStateWithProblemUnits | null | undefined;
companyAnchors: unknown;
normalizedPayload: NormalizeResponsePayload["normalized"];
featureAnswerPolicyV11: boolean;
featureProblemCentricAnswerV1: boolean;
featureLifecycleAnswerV1: boolean;
hasExplicitPeriodAnchor: (normalizedPayload: NormalizeResponsePayload["normalized"]) => boolean;
resolveQuestionTypeFn?: (input: string) => QuestionTypeClass;
composeAssistantAnswerFn?: typeof composeAssistantAnswer;
}
export interface AssistantDeepTurnCompositionOutput {
focusDomainHint: string | null;
questionTypeClass: QuestionTypeClass;
hasPeriodInCompanyAnchors: boolean;
normalizationPeriodExplicit: boolean;
composition: ReturnType<typeof composeAssistantAnswer>;
}
export function buildAssistantDeepTurnComposition(
input: BuildAssistantDeepTurnCompositionInput
): AssistantDeepTurnCompositionOutput {
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? resolveQuestionType;
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? composeAssistantAnswer;
const followupApplied = Boolean((input.followupUsage as { applied?: unknown } | null)?.applied);
const focusDomainHint = followupApplied
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
: null;
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
const companyAnchorSet = input.companyAnchors as {
dates?: unknown[];
periods?: unknown[];
} | null;
const hasPeriodInCompanyAnchors =
(Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
const composition = composeAssistantAnswerSafe({
userMessage: input.userMessage,
routeSummary: input.routeSummary,
retrievalResults: input.retrievalResults,
requirements: input.requirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
focusDomainHint,
questionTypeHint: questionTypeClass,
companyAnchors: input.companyAnchors as any,
normalizationPeriodExplicit,
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
});
return {
focusDomainHint,
questionTypeClass,
hasPeriodInCompanyAnchors,
normalizationPeriodExplicit,
composition
};
}

View File

@ -0,0 +1,169 @@
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
const KNOWN_P0_DOMAINS = new Set([
"settlements_60_62",
"vat_document_register_book",
"month_close_costs_20_44",
"fixed_asset_amortization"
]);
function toAnalysisContext(
runtimeAnalysisContext: BuildAssistantDeepTurnRuntimeContextInput["runtimeAnalysisContext"]
): Record<string, string | null> | null {
if (!runtimeAnalysisContext.active) {
return null;
}
return {
as_of_date: runtimeAnalysisContext.as_of_date,
period_from: runtimeAnalysisContext.period_from,
period_to: runtimeAnalysisContext.period_to,
source: runtimeAnalysisContext.source
};
}
export interface BuildAssistantDeepTurnRuntimeContextInput {
userMessage: string;
normalizedPayload: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
};
followupUsage: unknown | null | undefined;
resolveCompanyAnchors: (userMessage: string) => unknown;
resolveBusinessScopeAlignment: (input: {
userMessage: string;
companyAnchors: unknown;
normalized: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
}) => {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
inferP0DomainFromMessage: (userMessage: string) => string | null;
resolveTemporalGuard: (input: {
userMessage: string;
normalized: NormalizeResponsePayload["normalized"];
companyAnchors: unknown;
analysisContext: Record<string, string | null> | null;
}) => {
effective_primary_period?: unknown;
primary_period_window?: unknown;
[key: string]: unknown;
};
resolveDomainPolarityGuard: (input: {
userMessage: string;
companyAnchors: unknown;
focusDomainHint: string | null;
}) => unknown;
resolveClaimBoundAnchors: (input: {
userMessage: string;
companyAnchors: unknown;
focusDomainHint: string | null;
primaryPeriod: unknown;
}) => {
claim_type: string;
[key: string]: unknown;
};
resolveBusinessScopeFromLiveContext: (input: {
current: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
temporalGuard: unknown;
claimType: string;
focusDomainHint: string | null;
userMessage: string;
companyAnchors: unknown;
followupApplied: boolean;
}) => {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
}
export interface BuildAssistantDeepTurnRuntimeContextOutput {
companyAnchors: unknown;
initialBusinessScopeResolution: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
inferredDomainByMessage: string | null;
focusDomainForGuards: string | null;
temporalGuard: {
effective_primary_period?: unknown;
primary_period_window?: unknown;
[key: string]: unknown;
};
domainPolarityGuardInitial: unknown;
claimAnchorAudit: {
claim_type: string;
[key: string]: unknown;
};
businessScopeResolution: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
resolvedRouteSummary: RouteHintSummary | null;
liveTemporalHint: Record<string, string | null> | null;
}
export function buildAssistantDeepTurnRuntimeContext(
input: BuildAssistantDeepTurnRuntimeContextInput
): BuildAssistantDeepTurnRuntimeContextOutput {
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
userMessage: input.userMessage,
companyAnchors,
normalized: input.normalizedPayload,
routeSummary: input.routeSummary
});
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
const focusDomainForGuards =
inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
const temporalGuard = input.resolveTemporalGuard({
userMessage: input.userMessage,
normalized: input.normalizedPayload,
companyAnchors,
analysisContext
});
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const claimAnchorAudit = input.resolveClaimBoundAnchors({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
});
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
current: initialBusinessScopeResolution,
temporalGuard,
claimType: claimAnchorAudit.claim_type,
focusDomainHint: focusDomainForGuards,
userMessage: input.userMessage,
companyAnchors,
followupApplied: Boolean((input.followupUsage as { applied?: unknown } | null)?.applied)
});
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
return {
companyAnchors,
initialBusinessScopeResolution,
inferredDomainByMessage,
focusDomainForGuards,
temporalGuard,
domainPolarityGuardInitial,
claimAnchorAudit,
businessScopeResolution,
resolvedRouteSummary,
liveTemporalHint
};
}

View File

@ -0,0 +1,122 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AssistantRequirementExtractionResult, AssistantCoverageEvaluationResult } from "./assistantOrchestrationRuntimeAdapter";
import { runAssistantCoverageGroundingPipeline } from "./assistantOrchestrationRuntimeAdapter";
import type { AssistantDeepTurnGroundingEligibilityOutput } from "./assistantDeepTurnGuardRuntimeAdapter";
import { applyAssistantDeepTurnGroundingEligibility } from "./assistantDeepTurnGuardRuntimeAdapter";
export interface AssistantDeepTurnGroundingRuntimeInput {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
rbpPlanAudit: unknown;
faPlanAudit: unknown;
routeSummary: RouteHintSummary | null;
normalizedPayload: NormalizeResponsePayload["normalized"] | null | undefined;
userMessage: string;
requirementExtraction: AssistantRequirementExtractionResult;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalized: NormalizeResponsePayload["normalized"] | null | undefined,
userMessage: string
) => AssistantRequirementExtractionResult;
evaluateCoverage: (
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
) => AssistantCoverageEvaluationResult;
checkGrounding: (
userMessage: string,
requirements: AssistantRequirement[],
coverage: RequirementCoverageReport,
retrievalResults: UnifiedRetrievalResult[]
) => AnswerGroundingCheck;
temporalGuard: unknown;
polarityAudit: unknown;
evidenceAudit: unknown;
claimAnchorAudit: unknown;
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
collectRbpLiveRouteAudit: (input: {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
planAudit: unknown;
}) => unknown;
collectFaLiveRouteAudit: (input: {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
planAudit: unknown;
}) => unknown;
runCoverageGroundingPipelineFn?: typeof runAssistantCoverageGroundingPipeline;
applyGroundingEligibilityFn?: (input: {
groundingCheckBase: AnswerGroundingCheck;
temporalGuard: unknown;
polarityAudit: unknown;
evidenceAudit: unknown;
claimAnchorAudit?: unknown;
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
}) => AssistantDeepTurnGroundingEligibilityOutput<AnswerGroundingCheck>;
}
export interface AssistantDeepTurnGroundingRuntimeOutput {
rbpLiveRouteAudit: unknown;
faLiveRouteAudit: unknown;
coverageEvaluation: AssistantCoverageEvaluationResult;
groundingCheckBase: AnswerGroundingCheck;
groundedAnswerEligibilityGuard: unknown;
groundingCheck: AnswerGroundingCheck;
}
export function runAssistantDeepTurnGroundingRuntime(
input: AssistantDeepTurnGroundingRuntimeInput
): AssistantDeepTurnGroundingRuntimeOutput {
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? runAssistantCoverageGroundingPipeline;
const applyGroundingEligibilitySafe =
input.applyGroundingEligibilityFn ??
((payload) => applyAssistantDeepTurnGroundingEligibility(payload as any) as any);
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.rbpPlanAudit
});
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.faPlanAudit
});
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
routeSummary: input.routeSummary,
normalized: input.normalizedPayload,
userMessage: input.userMessage,
retrievalResults: input.retrievalResults,
requirementExtraction: input.requirementExtraction,
extractRequirements: input.extractRequirements,
evaluateCoverage: input.evaluateCoverage,
checkGrounding: input.checkGrounding
});
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
groundingCheckBase,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
evidenceAudit: input.evidenceAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
return {
rbpLiveRouteAudit,
faLiveRouteAudit,
coverageEvaluation,
groundingCheckBase,
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
groundingCheck: groundingEligibilityRuntime.groundingCheck
};
}

View File

@ -0,0 +1,112 @@
import type { UnifiedRetrievalResult } from "../types/assistant";
import { applyTargetedEvidenceAcquisition } from "./assistantClaimBoundEvidence";
import {
applyDomainPolarityGuardToRetrievalResults,
applyEvidenceAdmissibilityGate,
applyEligibilityToGroundingCheck,
evaluateGroundedAnswerEligibility
} from "./assistantRuntimeGuards";
type GroundingCheckLike = {
status: string;
reasons: string[];
};
type ApplyEligibilityToGroundingCheckFn = <T extends GroundingCheckLike>(
groundingCheck: T,
eligibility: ReturnType<typeof evaluateGroundedAnswerEligibility>
) => T;
export interface AssistantDeepTurnRetrievalGuardPipelineInput {
retrievalResults: UnifiedRetrievalResult[];
domainPolarityGuardInitial: Parameters<typeof applyDomainPolarityGuardToRetrievalResults>[0]["guard"];
claimAnchorAudit: Parameters<typeof applyTargetedEvidenceAcquisition>[0]["claimAudit"];
temporalGuard: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["temporal"];
focusDomainForGuards: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["focusDomainHint"];
companyAnchors?: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["companyAnchors"];
userMessage: string;
applyDomainPolarityGuardFn?: typeof applyDomainPolarityGuardToRetrievalResults;
applyTargetedEvidenceFn?: typeof applyTargetedEvidenceAcquisition;
applyEvidenceAdmissibilityGateFn?: typeof applyEvidenceAdmissibilityGate;
}
export interface AssistantDeepTurnRetrievalGuardPipelineOutput {
retrievalResults: UnifiedRetrievalResult[];
polarityGuardResult: ReturnType<typeof applyDomainPolarityGuardToRetrievalResults>;
targetedEvidenceResult: ReturnType<typeof applyTargetedEvidenceAcquisition>;
evidenceGateResult: ReturnType<typeof applyEvidenceAdmissibilityGate>;
}
export function applyAssistantDeepTurnRetrievalGuards(
input: AssistantDeepTurnRetrievalGuardPipelineInput
): AssistantDeepTurnRetrievalGuardPipelineOutput {
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? applyDomainPolarityGuardToRetrievalResults;
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? applyTargetedEvidenceAcquisition;
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? applyEvidenceAdmissibilityGate;
const polarityGuardResult = applyDomainPolarityGuardSafe({
retrievalResults: input.retrievalResults,
guard: input.domainPolarityGuardInitial
});
const targetedEvidenceResult = applyTargetedEvidenceSafe({
retrievalResults: polarityGuardResult.retrievalResults,
claimAudit: input.claimAnchorAudit
});
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
retrievalResults: targetedEvidenceResult.retrievalResults,
temporal: input.temporalGuard,
focusDomainHint: input.focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
return {
retrievalResults: evidenceGateResult.retrievalResults,
polarityGuardResult,
targetedEvidenceResult,
evidenceGateResult
};
}
export interface AssistantDeepTurnGroundingEligibilityInput<T extends GroundingCheckLike> {
groundingCheckBase: T;
temporalGuard: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["temporal"];
polarityAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["polarity"];
evidenceAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["evidence"];
claimAnchorAudit?: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["claimAnchors"];
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
evaluateGroundedAnswerEligibilityFn?: typeof evaluateGroundedAnswerEligibility;
applyEligibilityToGroundingCheckFn?: ApplyEligibilityToGroundingCheckFn;
}
export interface AssistantDeepTurnGroundingEligibilityOutput<T extends GroundingCheckLike> {
groundedAnswerEligibilityGuard: ReturnType<typeof evaluateGroundedAnswerEligibility>;
groundingCheck: T;
}
export function applyAssistantDeepTurnGroundingEligibility<T extends GroundingCheckLike>(
input: AssistantDeepTurnGroundingEligibilityInput<T>
): AssistantDeepTurnGroundingEligibilityOutput<T> {
const evaluateGroundedAnswerEligibilitySafe =
input.evaluateGroundedAnswerEligibilityFn ?? evaluateGroundedAnswerEligibility;
const applyEligibilityToGroundingCheckSafe =
input.applyEligibilityToGroundingCheckFn ??
((groundingCheck, eligibility) => applyEligibilityToGroundingCheck(groundingCheck, eligibility));
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
temporal: input.temporalGuard,
polarity: input.polarityAudit,
evidence: input.evidenceAudit,
claimAnchors: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
return {
groundedAnswerEligibilityGuard,
groundingCheck
};
}

View File

@ -0,0 +1,112 @@
import type { AssistantReplyType, AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
import type { AssistantDeepTurnPackagingInput } from "./assistantDeepTurnPackaging";
export interface AssistantDeepTurnInputBuilderArgs {
sessionId: string;
messageId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContextForContract: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage?: unknown;
composition: {
reply_type: AssistantReplyType;
fallback_type: unknown;
answer_structure_v11?: AnswerStructureV11 | null;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: unknown;
};
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
investigationStateSnapshot: unknown;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
}
export function buildAssistantDeepTurnPackagingInput(args: AssistantDeepTurnInputBuilderArgs): AssistantDeepTurnPackagingInput {
return {
...args,
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
followupStateUsage: args.followupStateUsage ?? null,
composition: {
reply_type: args.composition.reply_type,
fallback_type: args.composition.fallback_type,
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
}
};
}

View File

@ -0,0 +1,250 @@
import type {
AssistantConversationItem,
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
import {
assembleAssistantEvidenceBundle,
type AssistantEvidenceBundleAssembly
} from "./assistantEvidenceBundleAssembler";
import { assembleAssistantContractsBundleV1, type AssistantContractsBundleV1 } from "./assistantContractsBundleAssembler";
import { buildDeepAnswerArtifacts, buildAssistantConversationItem, type DeepAnswerArtifacts } from "./assistantDeepResponseAssembler";
import { buildDeepAnalysisDebugPayload } from "./assistantDebugPayloadAssembler";
import { buildDeepAnalysisProcessedLogDetails } from "./assistantMessageLogAssembler";
export interface AssistantDeepTurnPackagingInput {
sessionId: string;
messageId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContextForContract: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
composition: {
reply_type: AssistantReplyType;
fallback_type: unknown;
answer_structure_v11?: AnswerStructureV11 | null;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
};
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
investigationStateSnapshot: unknown;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
}
export interface AssistantDeepTurnPackagingOutput {
evidenceBundleAssembly: AssistantEvidenceBundleAssembly;
contractsBundleV1: AssistantContractsBundleV1;
deepAnswerArtifacts: DeepAnswerArtifacts;
debug: Record<string, unknown>;
assistantItem: AssistantConversationItem;
deepAnalysisLogDetails: Record<string, unknown>;
}
export function assembleAssistantDeepTurnPackaging(input: AssistantDeepTurnPackagingInput): AssistantDeepTurnPackagingOutput {
const normalizedPayload = (input.normalized.normalized ?? null) as Record<string, unknown> | null;
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? (normalizedPayload?.["fragments"] as unknown[]) : [];
const evidenceBundleAssembly = assembleAssistantEvidenceBundle({
retrievalCalls: input.retrievalCalls,
retrievalResults: input.retrievalResults
});
const contractsBundleV1 = assembleAssistantContractsBundleV1({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContextForContract,
executionPlan: input.executionPlan,
requirements: input.requirementExtractionRequirements,
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
replyType: input.composition.reply_type,
coverageReport: input.coverageReport,
grounding: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const deepAnswerArtifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: input.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const debug = buildDeepAnalysisDebugPayload({
traceId: input.normalized.trace_id,
promptVersion: input.normalized.prompt_version,
schemaVersion: input.normalized.schema_version,
fallbackType: input.composition.fallback_type,
routeSummary: input.routeSummary,
fragments: normalizedFragments,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
routes: input.routesForDebug,
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
retrievalResults: input.retrievalResults,
groundingCheck: input.groundingCheck,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
},
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
normalizedPayload: normalizedPayload
});
const assistantItem = buildAssistantConversationItem({
messageId: input.messageId,
sessionId: input.sessionId,
text: deepAnswerArtifacts.safeAssistantReply,
replyType: input.composition.reply_type,
traceId: input.normalized.trace_id,
debug: debug as any
});
const deepAnalysisLogDetails = buildDeepAnalysisProcessedLogDetails({
sessionId: input.sessionId,
messageId: input.messageId,
userMessage: input.userMessage,
normalizerOutput: input.normalized.normalized,
executionPlan: input.executionPlan,
resolvedExecutionState: input.resolvedExecutionState,
routes: input.routesForDebug,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResultsNormalized: input.retrievalResults,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
replyType: input.composition.reply_type,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
fallback_type: input.composition.fallback_type as string
},
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
assistantReply: deepAnswerArtifacts.safeAssistantReply,
traceId: input.normalized.trace_id
});
return {
evidenceBundleAssembly,
contractsBundleV1,
deepAnswerArtifacts,
debug,
assistantItem,
deepAnalysisLogDetails
};
}

View File

@ -0,0 +1,212 @@
import { nanoid } from "nanoid";
import type {
AssistantConversationItem,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import type { AssistantDeepTurnInputBuilderArgs } from "./assistantDeepTurnInputBuilder";
import { buildAssistantDeepTurnPackagingInput } from "./assistantDeepTurnInputBuilder";
import { assembleAssistantDeepTurnPackaging } from "./assistantDeepTurnPackaging";
import type {
AssistantAnalysisContextForContract,
AssistantRuntimeAnalysisContextForPrePackaging
} from "./assistantDeepTurnPrePackagingContext";
import { buildAssistantDeepTurnPrePackagingContext } from "./assistantDeepTurnPrePackagingContext";
import {
buildAssistantInvestigationStateSnapshot,
persistAssistantInvestigationStateSnapshot
} from "./assistantInvestigationStateRuntimeAdapter";
type AssistantDeepTurnCompositionForPackaging = AssistantDeepTurnInputBuilderArgs["composition"] & {
assistant_reply: string;
};
export interface AssistantDeepTurnPackagingRuntimeInput {
featureInvestigationStateV1: boolean;
sessionId: string;
questionId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
executionPlan: AssistantDeepTurnInputBuilderArgs["executionPlan"];
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: AssistantDeepTurnInputBuilderArgs["runtimeAnalysisContext"];
businessScopeResolution: AssistantDeepTurnInputBuilderArgs["businessScopeResolution"];
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage?: unknown;
followupApplied: boolean;
composition: AssistantDeepTurnCompositionForPackaging;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
previousInvestigationState: InvestigationStateWithProblemUnits | null | undefined;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
sanitizeReply: (value: string, fallback?: string) => string;
persistInvestigationState: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
nowIso?: () => string;
messageIdFactory?: () => string;
buildPrePackagingContextFn?: typeof buildAssistantDeepTurnPrePackagingContext;
buildInvestigationStateSnapshotFn?: typeof buildAssistantInvestigationStateSnapshot;
persistInvestigationStateSnapshotFn?: typeof persistAssistantInvestigationStateSnapshot;
buildDeepTurnPackagingInputFn?: typeof buildAssistantDeepTurnPackagingInput;
assembleDeepTurnPackagingFn?: typeof assembleAssistantDeepTurnPackaging;
}
export interface AssistantDeepTurnPackagingRuntimeOutput {
messageId: string;
investigationStateSnapshot: InvestigationStateWithProblemUnits | null;
droppedIntentSegments: string[];
analysisContextForContract: AssistantAnalysisContextForContract | null;
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
safeAssistantReplyBase: string;
safeAssistantReply: string;
debug: Record<string, unknown>;
assistantItem: AssistantConversationItem;
deepAnalysisLogDetails: Record<string, unknown>;
}
export function runAssistantDeepTurnPackagingRuntime(
input: AssistantDeepTurnPackagingRuntimeInput
): AssistantDeepTurnPackagingRuntimeOutput {
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? buildAssistantDeepTurnPrePackagingContext;
const buildInvestigationStateSnapshotSafe =
input.buildInvestigationStateSnapshotFn ?? buildAssistantInvestigationStateSnapshot;
const persistInvestigationStateSnapshotSafe =
input.persistInvestigationStateSnapshotFn ?? persistAssistantInvestigationStateSnapshot;
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? buildAssistantDeepTurnPackagingInput;
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assembleAssistantDeepTurnPackaging;
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
normalizedPayload: input.normalized.normalized,
routeSummary: input.routeSummary,
runtimeAnalysisContext: input.runtimeAnalysisContext as AssistantRuntimeAnalysisContextForPrePackaging,
assistantReply: input.composition.assistant_reply,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
sanitizeReply: input.sanitizeReply
});
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
previousState: input.previousInvestigationState,
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.composition.reply_type,
followupApplied: input.followupApplied
});
persistInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
sessionId: input.sessionId,
snapshot: investigationStateSnapshot,
persist: input.persistInvestigationState
});
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${nanoid(10)}`;
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
sessionId: input.sessionId,
messageId,
userMessage: input.userMessage,
normalized: input.normalized,
normalizedQuestion: input.normalizedQuestion,
routeSummary: input.routeSummary,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
executionPlan: input.executionPlan,
requirementExtractionRequirements: input.requirementExtractionRequirements,
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResults: input.retrievalResults,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
composition: {
reply_type: input.composition.reply_type,
fallback_type: input.composition.fallback_type,
answer_structure_v11: input.composition.answer_structure_v11,
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
problem_units_used_count: input.composition.problem_units_used_count,
problem_answer_mode: input.composition.problem_answer_mode,
problem_unit_ids_used: input.composition.problem_unit_ids_used
},
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
investigationStateSnapshot,
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
});
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
return {
messageId,
investigationStateSnapshot,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
debug: deepTurnPackaging.debug,
assistantItem: deepTurnPackaging.assistantItem,
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
};
}

View File

@ -0,0 +1,91 @@
import type { AssistantRequirement } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
export interface AssistantRequirementExtractionLike {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
export interface AssistantPlanEnforcementAuditLike {
executionPlan: AssistantExecutionPlanItem[];
audit: unknown;
}
export interface BuildAssistantDeepTurnExecutionPlanInput {
routeSummary: RouteHintSummary | null;
normalizedPayload: NormalizeResponsePayload["normalized"];
userMessage: string;
claimType: string;
temporalGuard: unknown;
domainPolarityGuardInitial: unknown;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalizedPayload: NormalizeResponsePayload["normalized"],
userMessage: string
) => AssistantRequirementExtractionLike;
toExecutionPlan: (
routeSummary: RouteHintSummary | null,
normalizedPayload: NormalizeResponsePayload["normalized"],
userMessage: string,
requirementByFragment: Map<string, string[]>
) => AssistantExecutionPlanItem[];
enforceRbpLiveRoutePlan: (input: {
executionPlan: AssistantExecutionPlanItem[];
claimType: string;
temporalGuard: unknown;
}) => AssistantPlanEnforcementAuditLike;
enforceFaLiveRoutePlan: (input: {
executionPlan: AssistantExecutionPlanItem[];
claimType: string;
temporalGuard: unknown;
}) => AssistantPlanEnforcementAuditLike;
applyTemporalHintToExecutionPlan: (
executionPlan: AssistantExecutionPlanItem[],
temporalGuard: unknown
) => AssistantExecutionPlanItem[];
applyPolarityHintToExecutionPlan: (
executionPlan: AssistantExecutionPlanItem[],
domainPolarityGuardInitial: unknown
) => AssistantExecutionPlanItem[];
}
export interface BuildAssistantDeepTurnExecutionPlanOutput {
requirementExtraction: AssistantRequirementExtractionLike;
executionPlan: AssistantExecutionPlanItem[];
rbpRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
faRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
}
export function buildAssistantDeepTurnExecutionPlan(
input: BuildAssistantDeepTurnExecutionPlanInput
): BuildAssistantDeepTurnExecutionPlanOutput {
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
let executionPlan = input.toExecutionPlan(
input.routeSummary,
input.normalizedPayload,
input.userMessage,
requirementExtraction.byFragment
);
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = rbpRoutePlanEnforcement.executionPlan;
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = faRoutePlanEnforcement.executionPlan;
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
return {
requirementExtraction,
executionPlan,
rbpRoutePlanEnforcement,
faRoutePlanEnforcement
};
}

View File

@ -0,0 +1,57 @@
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
export interface AssistantRuntimeAnalysisContextForPrePackaging {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface AssistantAnalysisContextForContract {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface BuildAssistantDeepTurnPrePackagingContextInput {
normalizedPayload: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
runtimeAnalysisContext: AssistantRuntimeAnalysisContextForPrePackaging;
assistantReply: string;
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
sanitizeReply: (value: string, fallback?: string) => string;
}
export interface AssistantDeepTurnPrePackagingContext {
droppedIntentSegments: string[];
analysisContextForContract: AssistantAnalysisContextForContract | null;
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
safeAssistantReplyBase: string;
}
export function buildAssistantDeepTurnPrePackagingContext(
input: BuildAssistantDeepTurnPrePackagingContextInput
): AssistantDeepTurnPrePackagingContext {
return {
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
analysisContextForContract: input.runtimeAnalysisContext.active
? {
as_of_date: input.runtimeAnalysisContext.as_of_date,
period_from: input.runtimeAnalysisContext.period_from,
period_to: input.runtimeAnalysisContext.period_to,
source: input.runtimeAnalysisContext.source,
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
}
: null,
routesForDebug: input.buildDebugRoutes(input.routeSummary),
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
};
}

View File

@ -0,0 +1,29 @@
import type {
AssistantConversationItem,
AssistantDebugPayload,
AssistantMessageResponsePayload,
AssistantReplyType
} from "../types/assistant";
export interface BuildAssistantDeepTurnSuccessResponseInput {
sessionId: string;
assistantReply: string;
replyType: AssistantReplyType;
conversationItem: AssistantConversationItem;
debug: AssistantDebugPayload | Record<string, unknown>;
conversation: AssistantConversationItem[];
}
export function buildAssistantDeepTurnSuccessResponse(
input: BuildAssistantDeepTurnSuccessResponseInput
): AssistantMessageResponsePayload {
return {
ok: true,
session_id: input.sessionId,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.conversationItem,
debug: input.debug as AssistantDebugPayload,
conversation: input.conversation
};
}

View File

@ -0,0 +1,131 @@
import type { UnifiedRetrievalResult } from "../types/assistant";
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
import { normalizeRetrievalResult } from "./retrievalResultNormalizer";
export interface AssistantLiveTemporalHint {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
}
export interface AssistantRetrievalCallRecord {
fragment_id: string;
requirement_ids: string[];
route: string;
status: "skipped" | "executed" | "failed";
query_text: string;
reason: string | null;
}
export interface AssistantRetrievalRawResultRecord {
fragment_id: string;
route: string;
raw_result: unknown;
}
export interface AssistantDeepTurnRetrievalExecutionInput {
executionPlan: AssistantExecutionPlanItem[];
liveTemporalHint: AssistantLiveTemporalHint | null;
executeRouteRuntime: (
route: string,
fragmentText: string,
options: {
temporalHint: AssistantLiveTemporalHint | null;
}
) => Promise<unknown>;
mapNoRouteReason: (reason: string | null) => string;
buildSkippedResult: (item: AssistantExecutionPlanItem) => UnifiedRetrievalResult;
normalizeRetrievalResultFn?: typeof normalizeRetrievalResult;
}
export interface AssistantDeepTurnRetrievalExecutionOutput {
retrievalCalls: AssistantRetrievalCallRecord[];
retrievalResultsRaw: AssistantRetrievalRawResultRecord[];
retrievalResults: UnifiedRetrievalResult[];
}
function buildRouteExecutorErrorRawResult(route: string, message: string): Record<string, unknown> {
return {
status: "error",
result_type: "summary",
items: [],
summary: {
route
},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Route executor failed."],
errors: [message]
};
}
export async function executeAssistantDeepTurnRetrievalPlan(
input: AssistantDeepTurnRetrievalExecutionInput
): Promise<AssistantDeepTurnRetrievalExecutionOutput> {
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? normalizeRetrievalResult;
const retrievalCalls: AssistantRetrievalCallRecord[] = [];
const retrievalResultsRaw: AssistantRetrievalRawResultRecord[] = [];
const retrievalResults: UnifiedRetrievalResult[] = [];
for (const planItem of input.executionPlan) {
if (!planItem.should_execute) {
retrievalCalls.push({
fragment_id: planItem.fragment_id,
requirement_ids: planItem.requirement_ids,
route: planItem.route,
status: "skipped",
query_text: planItem.fragment_text,
reason: input.mapNoRouteReason(planItem.no_route_reason)
});
retrievalResults.push(input.buildSkippedResult(planItem));
continue;
}
retrievalCalls.push({
fragment_id: planItem.fragment_id,
requirement_ids: planItem.requirement_ids,
route: planItem.route,
status: "executed",
query_text: planItem.fragment_text,
reason: null
});
try {
const raw = await input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
temporalHint: input.liveTemporalHint
});
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: raw
});
retrievalResults.push(
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw as any)
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
retrievalCalls[retrievalCalls.length - 1].status = "failed";
retrievalCalls[retrievalCalls.length - 1].reason = message;
const rawError = buildRouteExecutorErrorRawResult(planItem.route, message);
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: rawError
});
retrievalResults.push(
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError as any)
);
}
}
return {
retrievalCalls,
retrievalResultsRaw,
retrievalResults
};
}

View File

@ -0,0 +1,36 @@
import type { AssistantDebugPayload, UnifiedRetrievalResult } from "../types/assistant";
import {
buildAssistantEvidenceBundleContractV1,
type AssistantEvidenceBundleContractV1
} from "./assistantOrchestrationContracts";
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
export interface AssistantEvidenceBundleAssembly {
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
retrievalStatus: RetrievalStatusItem[];
}
function buildRetrievalStatus(retrievalResults: UnifiedRetrievalResult[]): RetrievalStatusItem[] {
return retrievalResults.map((item) => ({
fragment_id: item.fragment_id,
requirement_ids: item.requirement_ids,
route: item.route,
status: item.status,
result_type: item.result_type
}));
}
export function assembleAssistantEvidenceBundle(input: {
retrievalCalls: Array<Record<string, unknown>>;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantEvidenceBundleAssembly {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
return {
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
retrievalResults
}),
retrievalStatus: buildRetrievalStatus(retrievalResults)
};
}

View File

@ -0,0 +1,55 @@
import type { AssistantReplyType, AssistantRequirement, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import { updateInvestigationState } from "./investigationState";
export interface BuildAssistantInvestigationStateSnapshotInput {
featureEnabled: boolean;
previousState: InvestigationStateWithProblemUnits | null | undefined;
timestamp: string;
questionId: string;
userMessage: string;
routeSummary: RouteHintSummary | null;
requirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
retrievalResults: UnifiedRetrievalResult[];
replyType: AssistantReplyType;
followupApplied: boolean;
}
export function buildAssistantInvestigationStateSnapshot(
input: BuildAssistantInvestigationStateSnapshotInput
): InvestigationStateWithProblemUnits | null {
if (!input.featureEnabled || !input.previousState) {
return null;
}
return updateInvestigationState({
previous: input.previousState,
timestamp: input.timestamp,
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.requirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.replyType,
followupApplied: input.followupApplied
});
}
export interface PersistAssistantInvestigationStateSnapshotInput {
featureEnabled: boolean;
sessionId: string;
snapshot: InvestigationStateWithProblemUnits | null;
persist: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
}
export function persistAssistantInvestigationStateSnapshot(
input: PersistAssistantInvestigationStateSnapshotInput
): boolean {
if (!input.featureEnabled || !input.snapshot) {
return false;
}
input.persist(input.sessionId, input.snapshot);
return true;
}

View File

@ -0,0 +1,159 @@
import type { AnswerGroundingCheck, RequirementCoverageReport } from "../types/assistant";
export interface DeepAnalysisMessageLogDetailsInput {
sessionId: string;
messageId: string;
userMessage: string;
normalizerOutput: unknown;
executionPlan: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
routes: Array<Record<string, unknown>>;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResultsNormalized: unknown[];
requirementsExtracted: unknown[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
replyType: string;
droppedIntentSegments: string[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
compositionDebug: {
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
fallback_type?: string;
};
outcomeClassV1: unknown;
assistantOrchestrationContractsV1: unknown;
answerStructureV11: unknown;
investigationStateSnapshot: unknown;
assistantReply: string;
traceId: string;
}
function toAnalysisContext(input: DeepAnalysisMessageLogDetailsInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function resolveCoverageStatus(coverageReport: RequirementCoverageReport): "full" | "partial_or_limited" {
return coverageReport.requirements_total === coverageReport.requirements_covered &&
coverageReport.requirements_uncovered.length === 0 &&
coverageReport.requirements_partially_covered.length === 0
? "full"
: "partial_or_limited";
}
export function buildDeepAnalysisProcessedLogDetails(input: DeepAnalysisMessageLogDetailsInput): Record<string, unknown> {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
session_id: input.sessionId,
message_id: input.messageId,
user_message: input.userMessage,
normalizer_output: input.normalizerOutput,
execution_plan: input.executionPlan,
resolved_execution_state: input.resolvedExecutionState,
routes: input.routes,
retrieval_calls: input.retrievalCalls,
retrieval_results_raw: input.retrievalResultsRaw,
retrieval_results_normalized: input.retrievalResultsNormalized,
requirements_extracted: input.requirementsExtracted,
requirements_total: input.coverageReport.requirements_total,
requirements_covered: input.coverageReport.requirements_covered,
requirements_uncovered: input.coverageReport.requirements_uncovered,
coverage_status: resolveCoverageStatus(input.coverageReport),
answer_grounding_status: input.groundingCheck.status,
reply_semantic_type: input.replyType,
why_included_summary: input.groundingCheck.why_included_summary,
selection_reason_summary: input.groundingCheck.selection_reason_summary,
route_subject_match: input.groundingCheck.route_subject_match,
clarification_target: input.coverageReport.clarification_needed_for,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
fallback_type: input.compositionDebug.fallback_type,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
trace_id: input.traceId
};
}

View File

@ -0,0 +1,275 @@
import type {
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
export type AssistantOutcomeClassV1 =
| "FULLY_ANSWERED"
| "PARTIALLY_ANSWERED"
| "BLOCKED_BY_AMBIGUITY"
| "BLOCKED_BY_MISSING_DATA"
| "BLOCKED_BY_TOOLING"
| "MISROUTED"
| "FAILED_TO_BIND_ENTITIES";
export interface AssistantAnalysisContextContractV1 {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface AssistantQueryFrameContractV1 {
schema_version: "assistant_query_frame_v1";
original_user_question: string;
normalized_question: string;
route_summary_mode: RouteHintSummary["mode"] | "none";
fragments_total: number;
dropped_intent_segments: string[];
analysis_context: AssistantAnalysisContextContractV1 | null;
}
export interface AssistantExecutionPlanStepContractV1 {
fragment_id: string;
route: string;
should_execute: boolean;
requirement_ids: string[];
no_route_reason: string | null;
clarification_reason: string | null;
}
export interface AssistantExecutionPlanContractV1 {
schema_version: "assistant_execution_plan_v1";
steps: AssistantExecutionPlanStepContractV1[];
requirements_total: number;
}
export interface AssistantEvidenceBundleContractV1 {
schema_version: "assistant_evidence_bundle_v1";
retrieval_calls_total: number;
retrieval_results_total: number;
retrieval_status_breakdown: {
ok: number;
partial: number;
empty: number;
error: number;
};
evidence_total: number;
source_refs_total: number;
limitation_total: number;
error_total: number;
}
export interface AssistantCoverageContractV1 {
schema_version: "assistant_coverage_contract_v1";
coverage_report: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
outcome_class: AssistantOutcomeClassV1;
}
function normalizeSnapshotMode(value: unknown): "auto" | "force_snapshot" | "force_live" {
const token = String(value ?? "").trim();
if (token === "force_snapshot" || token === "force_live") {
return token;
}
return "auto";
}
function extractFragmentsTotal(normalized: NormalizedPayload | null | undefined): number {
if (!normalized || typeof normalized !== "object") {
return 0;
}
const source = normalized as unknown as { fragments?: unknown };
const fragments = source.fragments;
return Array.isArray(fragments) ? fragments.length : 0;
}
function collectEvidenceTotals(retrievalResults: UnifiedRetrievalResult[]): {
evidence_total: number;
source_refs_total: number;
limitation_total: number;
error_total: number;
} {
let evidenceTotal = 0;
const sourceRefs = new Set<string>();
let limitationTotal = 0;
let errorTotal = 0;
for (const result of retrievalResults) {
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
for (const evidence of result.evidence ?? []) {
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
if (ref) {
sourceRefs.add(ref);
}
}
}
return {
evidence_total: evidenceTotal,
source_refs_total: sourceRefs.size,
limitation_total: limitationTotal,
error_total: errorTotal
};
}
export function classifyAssistantOutcomeClassV1(input: {
replyType: AssistantReplyType;
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantOutcomeClassV1 {
const replyType = input.replyType;
const grounding = input.grounding;
const coverage = input.coverageReport;
const hasOnlyErrors =
input.retrievalResults.length > 0 &&
input.retrievalResults.every((item) => item.status === "error");
if (replyType === "backend_error" || hasOnlyErrors) {
return "BLOCKED_BY_TOOLING";
}
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
return "MISROUTED";
}
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
return "BLOCKED_BY_AMBIGUITY";
}
if (replyType === "out_of_scope") {
return "BLOCKED_BY_AMBIGUITY";
}
const fullCoverage =
coverage.requirements_total > 0 &&
coverage.requirements_total === coverage.requirements_covered &&
coverage.requirements_uncovered.length === 0 &&
coverage.requirements_partially_covered.length === 0 &&
coverage.clarification_needed_for.length === 0 &&
coverage.out_of_scope_requirements.length === 0;
if (fullCoverage && grounding.status === "grounded") {
return "FULLY_ANSWERED";
}
const hasAnyCoverage =
coverage.requirements_covered > 0 ||
coverage.requirements_partially_covered.length > 0 ||
grounding.status === "partial";
if (hasAnyCoverage) {
return "PARTIALLY_ANSWERED";
}
const missingRequirementSignal =
grounding.missing_requirements.length > 0 ||
coverage.requirements_uncovered.length > 0 ||
coverage.requirements_total > 0;
const possibleBindingFailure =
replyType === "no_grounded_answer" &&
missingRequirementSignal &&
grounding.route_subject_match;
if (possibleBindingFailure) {
return "FAILED_TO_BIND_ENTITIES";
}
return "BLOCKED_BY_MISSING_DATA";
}
export function buildAssistantQueryFrameContractV1(input: {
userMessage: string;
normalizedQuestion: string;
normalized: NormalizedPayload | null | undefined;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContext?: {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
snapshot_mode?: string | null;
} | null;
}): AssistantQueryFrameContractV1 {
const analysis = input.analysisContext
? {
as_of_date: input.analysisContext.as_of_date ?? null,
period_from: input.analysisContext.period_from ?? null,
period_to: input.analysisContext.period_to ?? null,
source: input.analysisContext.source ?? null,
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
}
: null;
return {
schema_version: "assistant_query_frame_v1",
original_user_question: String(input.userMessage ?? ""),
normalized_question: String(input.normalizedQuestion ?? ""),
route_summary_mode: input.routeSummary?.mode ?? "none",
fragments_total: extractFragmentsTotal(input.normalized),
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
analysis_context: analysis
};
}
export function buildAssistantExecutionPlanContractV1(input: {
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirements: AssistantRequirement[];
}): AssistantExecutionPlanContractV1 {
return {
schema_version: "assistant_execution_plan_v1",
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
fragment_id: String(item.fragment_id ?? ""),
route: String(item.route ?? ""),
should_execute: Boolean(item.should_execute),
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
no_route_reason: item.no_route_reason ?? null,
clarification_reason: item.clarification_reason ?? null
})),
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
};
}
export function buildAssistantEvidenceBundleContractV1(input: {
retrievalCalls: Array<Record<string, unknown>>;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantEvidenceBundleContractV1 {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
const breakdown = {
ok: retrievalResults.filter((item) => item.status === "ok").length,
partial: retrievalResults.filter((item) => item.status === "partial").length,
empty: retrievalResults.filter((item) => item.status === "empty").length,
error: retrievalResults.filter((item) => item.status === "error").length
};
const totals = collectEvidenceTotals(retrievalResults);
return {
schema_version: "assistant_evidence_bundle_v1",
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
retrieval_results_total: retrievalResults.length,
retrieval_status_breakdown: breakdown,
...totals
};
}
export function buildAssistantCoverageContractV1(input: {
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
outcomeClass: AssistantOutcomeClassV1;
}): AssistantCoverageContractV1 {
return {
schema_version: "assistant_coverage_contract_v1",
coverage_report: input.coverageReport,
grounding: input.grounding,
outcome_class: input.outcomeClass
};
}

View File

@ -0,0 +1,66 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
export interface AssistantRequirementExtractionResult {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
export interface AssistantCoverageEvaluationResult {
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
}
export interface AssistantCoverageGroundingPipelineInput {
routeSummary: RouteHintSummary | null;
normalized: NormalizedPayload | null | undefined;
userMessage: string;
retrievalResults: UnifiedRetrievalResult[];
requirementExtraction?: AssistantRequirementExtractionResult;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalized: NormalizedPayload | null | undefined,
userMessage: string
) => AssistantRequirementExtractionResult;
evaluateCoverage: (
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
) => AssistantCoverageEvaluationResult;
checkGrounding: (
userMessage: string,
requirements: AssistantRequirement[],
coverage: RequirementCoverageReport,
retrievalResults: UnifiedRetrievalResult[]
) => AnswerGroundingCheck;
}
export interface AssistantCoverageGroundingPipelineOutput {
requirementExtraction: AssistantRequirementExtractionResult;
coverageEvaluation: AssistantCoverageEvaluationResult;
groundingCheckBase: AnswerGroundingCheck;
}
export function runAssistantCoverageGroundingPipeline(
input: AssistantCoverageGroundingPipelineInput
): AssistantCoverageGroundingPipelineOutput {
const requirementExtraction =
input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
const groundingCheckBase = input.checkGrounding(
input.userMessage,
coverageEvaluation.requirements,
coverageEvaluation.coverage,
input.retrievalResults
);
return {
requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}

View File

@ -0,0 +1,133 @@
import type { RouteHintSummary } from "../types/normalizer";
interface FragmentLike {
fragment_id?: string;
raw_fragment_text?: string;
normalized_fragment_text?: string;
account_hints?: unknown;
}
export interface AssistantExecutionPlanItem {
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
fragment_text: string;
no_route_reason: string | null;
clarification_reason: string | null;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function enrichFragmentTextWithHints(fragment: FragmentLike, text: string): string {
const baseText = String(text ?? "").trim();
const accountHints = Array.isArray(fragment.account_hints)
? Array.from(new Set(fragment.account_hints.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0)))
: [];
if (accountHints.length === 0) {
return baseText;
}
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
if (hasAccountInText) {
return baseText;
}
return `${baseText}, по счету ${accountHints.join(", ")}`;
}
export function buildFragmentTextById(fragments: FragmentLike[]): Map<string, string> {
const result = new Map<string, string>();
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item as FragmentLike;
const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
if (!fragmentId) {
continue;
}
const text =
(typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
"";
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
}
return result;
}
export function buildExecutionPlanFromRoute(input: {
routeSummary: RouteHintSummary | null;
userMessage: string;
fragmentTextById: Map<string, string>;
requirementByFragment: Map<string, string[]>;
}): AssistantExecutionPlanItem[] {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
route: input.routeSummary.route_hint,
should_execute: true,
fragment_text: input.userMessage,
no_route_reason: null,
clarification_reason: null
}
];
}
return input.routeSummary.decisions.map((decision) => {
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
if (decision.route === "no_route") {
return {
fragment_id: decision.fragment_id,
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
route: "no_route",
should_execute: false,
fragment_text: text,
no_route_reason: decision.no_route_reason ?? null,
clarification_reason: decision.clarification_reason ?? null
};
}
return {
fragment_id: decision.fragment_id,
requirement_ids: input.requirementByFragment.get(decision.fragment_id) ?? [],
route: decision.route,
should_execute: true,
fragment_text: text,
no_route_reason: null,
clarification_reason: decision.clarification_reason ?? null
};
});
}
export function buildDebugRoutesFromRoute(input: {
routeSummary: RouteHintSummary | null;
resolveLegacyRouteReason: (route: string) => string;
}): Array<Record<string, unknown>> {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
route: input.routeSummary.route_hint,
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
confidence: input.routeSummary.confidence,
intent_class: input.routeSummary.intent_class
}
];
}
return input.routeSummary.decisions.map((decision) => ({
fragment_id: decision.fragment_id,
route: decision.route,
reason: decision.reason,
route_status: decision.route_status ?? null,
no_route_reason: decision.no_route_reason ?? null,
clarification_reason: decision.clarification_reason ?? null,
execution_readiness: decision.execution_readiness ?? null
}));
}

View File

@ -683,11 +683,97 @@ function toTemporalGuardInput(window: TemporalWindow | null, fallback: string |
return value || null;
}
function normalizeIsoDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function normalizeTemporalWindow(input: {
asOfDate?: unknown;
periodFrom?: unknown;
periodTo?: unknown;
}): TemporalWindow | null {
const asOfDate = normalizeIsoDate(input.asOfDate);
if (asOfDate) {
return {
from: asOfDate,
to: asOfDate,
granularity: "day"
};
}
const from = normalizeIsoDate(input.periodFrom);
const to = normalizeIsoDate(input.periodTo);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: from === to ? "day" : "month"
};
}
export function resolveTemporalGuard(input: {
userMessage: string;
normalized: NormalizedPayload | null | undefined;
companyAnchors?: CompanyAnchorSet | null;
analysisContext?: {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
} | null;
}): TemporalGuardAudit {
const analysisWindow = normalizeTemporalWindow({
asOfDate: input.analysisContext?.as_of_date,
periodFrom: input.analysisContext?.period_from,
periodTo: input.analysisContext?.period_to
});
if (analysisWindow) {
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
return {
raw_time_anchor: analysisWindow.from,
raw_time_scope: guardInput,
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
resolved_primary_period: analysisWindow,
effective_primary_period: analysisWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: "aligned",
temporal_resolution_source: source,
temporal_guard_basis: "raw_time_scope_unlocked",
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
allowed_context_window: null,
controlled_temporal_expansion_enabled: false,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: false,
reason_codes: ["analysis_context_applied"]
};
}
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
@ -762,10 +848,15 @@ export function applyTemporalHintToExecutionPlan<
return executionPlan;
}
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
const periodLabel = primaryWindow
? `${primaryWindow.from}..${primaryWindow.to}`
: temporal.resolved_time_anchor
? temporal.resolved_time_anchor
: "active_period";
const hint =
primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
@ -1590,15 +1681,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
? "no_grounded_answer"
: "partial";
const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant";
export interface CommitAssistantTurnAndLogInput {
sessionId: string;
assistantItem: AssistantConversationItem;
eventType: string;
logDetails: Record<string, unknown>;
appendItem: (sessionId: string, item: AssistantConversationItem) => void;
getSession: (sessionId: string) => AssistantSessionState | null;
persistSession: (session: AssistantSessionState) => void;
cloneConversation: (items: AssistantConversationItem[]) => AssistantConversationItem[];
logEvent: (payload: {
timestamp: string;
level: "info";
service: "assistant_loop";
message: "assistant_message_processed";
sessionId: string;
eventType: string;
details: Record<string, unknown>;
}) => void;
nowIso?: () => string;
}
export interface CommitAssistantTurnAndLogOutput {
currentSession: AssistantSessionState | null;
conversation: AssistantConversationItem[];
}
export function commitAssistantTurnAndLog(input: CommitAssistantTurnAndLogInput): CommitAssistantTurnAndLogOutput {
input.appendItem(input.sessionId, input.assistantItem);
const currentSession = input.getSession(input.sessionId);
if (currentSession) {
input.persistSession(currentSession);
}
const conversation = input.cloneConversation(currentSession?.items ?? []);
input.logEvent({
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId: input.sessionId,
eventType: input.eventType,
details: input.logDetails
});
return {
currentSession,
conversation
};
}

View File

@ -264,6 +264,32 @@ function parseRawQuestions(rawQuestions: string): string[] {
return byLine.length > 0 ? byLine : [text];
}
function normalizeAnalysisDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
type V2FamilyFragment =
| NormalizedQueryV2["fragments"][number]
| NormalizedQueryV2_0_1["fragments"][number]
@ -936,6 +962,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
rawQuestions?: string;
analysisDate?: string;
cases: EvalInputCase[];
}): Promise<Record<string, unknown>> {
const runId = `eval-${nanoid(10)}`;
@ -976,6 +1003,13 @@ export class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: payload.analysisDate ?? undefined,
analysis_context: payload.analysisDate
? {
as_of_date: payload.analysisDate,
source: "eval_analysis_date"
}
: undefined,
eval_label: runId,
case_id: item.case_id,
eval_mode: payload.mode
@ -1876,6 +1910,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
@ -1889,6 +1924,7 @@ export class EvalService {
const suite = parseAssistantSuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${nanoid(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
const diagnostics: AssistantCaseDiagnostics[] = [];
let requestsTotal = 0;
@ -1917,6 +1953,15 @@ export class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
@ -2153,6 +2198,7 @@ export class EvalService {
eval_target: "assistant_stage1",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@ -2225,6 +2271,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_STAGE2_EVAL_V1) {
@ -2238,6 +2285,7 @@ export class EvalService {
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${nanoid(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
const diagnostics: AssistantStage2CaseDiagnostics[] = [];
let requestsTotal = 0;
@ -2269,6 +2317,15 @@ export class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
@ -2446,6 +2503,7 @@ export class EvalService {
eval_target: "assistant_stage2",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@ -2552,10 +2610,12 @@ export class EvalService {
rawQuestions?: string;
evalTarget?: EvalTarget;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
if (evalTarget === "assistant_stage1") {
return this.runAssistantStage1({
@ -2565,6 +2625,7 @@ export class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@ -2577,6 +2638,7 @@ export class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@ -2622,6 +2684,7 @@ export class EvalService {
return this.runV2({
...payload,
mode,
analysisDate: analysisDate ?? undefined,
cases: filtered
});
}
@ -2651,6 +2714,13 @@ export class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: analysisDate ?? undefined,
analysis_context: analysisDate
? {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
: undefined,
expected_route: item.expected.route_hint as NormalizeRequestPayload["context"] extends infer C
? C extends { expected_route?: infer R }
? R
@ -2779,6 +2849,7 @@ export class EvalService {
timestamp: new Date().toISOString(),
mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
dataset: {
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",

View File

@ -125,7 +125,7 @@ export function resolveQuestionType(input: string): QuestionTypeClass {
return bestType;
}
if (/[?пјџ]/u.test(text)) {
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
return "why_breaks";
}

View File

@ -235,6 +235,14 @@ export interface RouteHintSummaryV2 {
export type RouteHintSummary = RouteHintSummaryV1 | RouteHintSummaryV2;
export type NormalizedPayload = NormalizedQueryV1 | NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2;
export interface AnalysisContextV1 {
as_of_date?: string;
period_from?: string;
period_to?: string;
snapshot_mode?: "auto" | "force_snapshot" | "force_live";
source?: string;
}
export interface NormalizeRequestPayload {
llmProvider?: LlmProvider;
apiKey?: string;
@ -250,6 +258,7 @@ export interface NormalizeRequestPayload {
userQuestion: string;
context?: {
period_hint?: string;
analysis_context?: AnalysisContextV1;
business_context?: string;
expected_route?: RouteHint;
eval_label?: string;

View File

@ -1654,6 +1654,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves major-share revenue wording into customer revenue intent", () => {
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from highest inflow slang wording", () => {
const result = resolveAddressIntent("какие приходы самые высокие за все время");
expect(result.intent).toBe("customer_revenue_and_payments");
@ -1725,6 +1730,74 @@ describe("address intent resolver expansion (M2.3a)", () => {
const result = resolveAddressIntent("покажи документы по этому же договору");
expect(result.intent).toBe("list_documents_by_contract");
});
it("routes supplier tail-risk wording into payables intent", () => {
const result = resolveAddressIntent(
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
);
expect(result.intent).toBe("list_payables_counterparties");
});
it("keeps out-of-scope supplier control wording as unknown intent", () => {
const result = resolveAddressIntent(
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
);
expect(result.intent).toBe("unknown");
});
it("routes long shipment-to-payment lag wording into receivables intent", () => {
const result = resolveAddressIntent(
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
);
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes non-paying counterparties month-risk wording into receivables intent", () => {
const result = resolveAddressIntent(
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
);
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes reconciliation mismatch wording into open contracts intent", () => {
const result = resolveAddressIntent(
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes reconciliation mismatch wording without explicit lookup verb into open contracts intent", () => {
const result = resolveAddressIntent(
"По каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes payments-without-closing-docs wording into open contracts intent", () => {
const result = resolveAddressIntent(
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes documents-without-payments wording into open contracts intent", () => {
const result = resolveAddressIntent(
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes stale advances without closing docs wording into open contracts intent", () => {
const result = resolveAddressIntent(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes buyers with open debt wording into open-items intent", () => {
const result = resolveAddressIntent("по каким покупателям у нас есть открытые задолженности на конец месяца?");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
});
});
describe("address filter extraction for balance drilldown", () => {
@ -1810,6 +1883,14 @@ describe("address filter extraction for balance drilldown", () => {
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
});
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
const extracted = extractAddressFilters(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
"list_open_contracts"
);
expect(extracted.extracted_filters.counterparty).toBeUndefined();
});
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
const extracted = extractAddressFilters(
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
@ -2250,6 +2331,98 @@ describe("address filter extraction for balance drilldown", () => {
});
describe("address query limited taxonomy and stage diagnostics", () => {
it("injects as_of_date from analysis context when user message has no explicit period", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", {
analysisDateHint: "2020-07-31"
});
expect(result?.handled).toBe(true);
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-07-31");
expect(Array.isArray(result?.debug.reasons)).toBe(true);
expect(result?.debug.reasons).toContain("as_of_date_from_analysis_context");
});
it("returns soft out-of-scope reply without technical jargon for unsupported supplier-control wording", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.detected_intent).toBe("unknown");
expect(result?.debug.limited_reason_category).toBe("unsupported");
const reply = String(result?.reply_text ?? "");
expect(reply.toLowerCase()).toContain("вне поддерживаемого контура");
expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu);
});
it("routes supplier tail-risk wording without forcing missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_payables_counterparties");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes shipment-to-payment lag wording into receivables lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes payments-without-closing-docs wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes documents-without-payments wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("За какие годы в базе есть данные?");

View File

@ -0,0 +1,186 @@
import { describe, expect, it } from "vitest";
import { buildAssistantAnswerStructureV11 } from "../src/services/assistantAnswerPackageBuilder";
function buildRetrieval(input?: Partial<any>): any {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant answer package builder v11", () => {
it("builds baseline answer structure with unresolved mechanism", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Первая строка\nВторая строка",
coverageReport: {
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "partial",
route_subject_match: true,
missing_requirements: ["R2"],
reasons: ["limited coverage"],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [buildRetrieval()]
});
expect(structure.schema_version).toBe("answer_structure_v1_1");
expect(structure.answer_summary).toBe("Первая строка");
expect(structure.mechanism_block.status).toBe("unresolved");
expect(structure.evidence_block.coverage_note).toBe("coverage_partial_or_limited");
expect(structure.uncertainty_block.open_uncertainties).toEqual(["R2"]);
});
it("adds claim-evidence links when enrichment is explicitly enabled", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Ответ",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [
buildRetrieval({
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "trace confirmed",
confidence: "high",
limitation: null,
payload: {}
}
]
})
],
options: {
enableEvidenceEnrichment: true
}
});
expect(Array.isArray(structure.evidence_block.claim_evidence_links)).toBe(true);
expect(structure.evidence_block.claim_evidence_links?.[0]?.claim_ref).toBe("requirement:R1");
expect(structure.evidence_block.claim_evidence_links?.[0]?.evidence_ids).toContain("ev-1");
});
it("omits claim-evidence links when enrichment is disabled", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Ответ",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [
buildRetrieval({
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "trace confirmed",
confidence: "high",
limitation: null,
payload: {}
}
]
})
],
options: {
enableEvidenceEnrichment: false
}
});
expect(structure.evidence_block.claim_evidence_links).toBeUndefined();
});
});

View File

@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import type { AssistantRequirement, UnifiedRetrievalResult } from "../src/types/assistant";
import { buildAssistantEvidenceBundleContractV1 } from "../src/services/assistantOrchestrationContracts";
import { assembleAssistantContractsBundleV1 } from "../src/services/assistantContractsBundleAssembler";
function buildRequirement(): AssistantRequirement {
return {
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
};
}
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant contracts bundle assembler", () => {
it("assembles query/execution/coverage contracts with outcome class", () => {
const retrievalResults = [buildRetrieval({ status: "ok" })];
const bundle = assembleAssistantContractsBundleV1({
userMessage: "проверь хвосты по 60.01",
normalizedQuestion: "проверь хвосты по 60.01",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь хвосты по 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
} as any,
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
},
droppedIntentSegments: [],
analysisContext: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
},
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
}
],
requirements: [buildRequirement()],
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResults
}),
replyType: "factual_with_explanation",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
grounding: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults
});
expect(bundle.queryFrameContractV1.schema_version).toBe("assistant_query_frame_v1");
expect(bundle.executionPlanContractV1.schema_version).toBe("assistant_execution_plan_v1");
expect(bundle.coverageContractV1.schema_version).toBe("assistant_coverage_contract_v1");
expect(bundle.outcomeClassV1).toBe("FULLY_ANSWERED");
expect(bundle.assistantOrchestrationContractsV1.evidence_bundle.schema_version).toBe("assistant_evidence_bundle_v1");
});
});

View File

@ -0,0 +1,199 @@
import { describe, expect, it } from "vitest";
import {
checkGroundingForRequirements,
evaluateCoverageForRequirements,
extractRequirementsForRoute
} from "../src/services/assistantCoverageGrounding";
describe("assistant coverage-grounding module", () => {
it("extracts requirements from deterministic route summary", () => {
const extracted = extractRequirementsForRoute({
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
route: "no_route",
no_route_reason: "insufficient_specificity",
reason: "missing anchor"
},
{
fragment_id: "F2",
route: "hybrid_store_plus_live",
reason: "ok route"
}
],
fallback: {
type: "none",
message: null
}
} as any,
userMessage: "base question",
fragmentTextById: new Map([
["F1", "need more details"],
["F2", "check account 60"]
]),
extractSubjectTokens: (text) => (text.includes("60") ? ["account_60"] : ["counterparty"])
});
expect(extracted.requirements).toHaveLength(2);
expect(extracted.requirements[0].status).toBe("clarification_needed");
expect(extracted.requirements[0].route).toBeNull();
expect(extracted.requirements[1].status).toBe("covered");
expect(extracted.requirements[1].route).toBe("hybrid_store_plus_live");
expect(extracted.byFragment.get("F2")).toEqual(["R2"]);
});
it("evaluates coverage from retrieval outcomes", () => {
const requirements = [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: ["account_60"],
status: "covered" as const,
route: "hybrid_store_plus_live"
},
{
requirement_id: "R2",
source_fragment_id: "F2",
requirement_text: "req2",
subject_tokens: ["counterparty"],
status: "covered" as const,
route: "store_feature_risk"
}
];
const retrievalResults = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "ok",
confidence: "high",
limitation: null,
payload: {}
}
],
why_included: ["why"],
selection_reason: ["sel"],
risk_factors: [],
business_interpretation: [],
confidence: "high",
limitations: [],
errors: []
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_feature_risk",
status: "empty",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: [],
errors: []
}
] as any;
const evaluation = evaluateCoverageForRequirements(requirements as any, retrievalResults);
expect(evaluation.coverage.requirements_total).toBe(2);
expect(evaluation.coverage.requirements_covered).toBe(1);
expect(evaluation.coverage.requirements_uncovered).toContain("R2");
expect(evaluation.requirements.find((item) => item.requirement_id === "R1")?.status).toBe("covered");
});
it("produces route mismatch grounding when critical subject token is absent", () => {
const grounded = checkGroundingForRequirements({
userMessage: "Проверь НДС цепочку",
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "vat chain",
subject_tokens: ["nds"],
status: "covered",
route: "hybrid_store_plus_live"
}
] as any,
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: { note: "no tax markers" },
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
}
] as any,
extractSubjectTokens: () => ["nds"]
});
expect(grounded.status).toBe("route_mismatch_blocked");
expect(grounded.route_subject_match).toBe(false);
expect(grounded.reasons.some((item) => item.includes("Ключевые ориентиры вопроса"))).toBe(true);
});
});

View File

@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import { buildDeepAnalysisDebugPayload } from "../src/services/assistantDebugPayloadAssembler";
function baseInput() {
return {
traceId: "trace-1",
promptVersion: "normalizer_v2_0_2",
schemaVersion: "normalized_query_v2_0_2",
fallbackType: "none",
routeSummary: { mode: "deterministic_v2" },
fragments: [{ fragment_id: "F1" }],
requirementsExtracted: [{ requirement_id: "R1", status: "covered" }],
coverageReport: { requirements_total: 1, requirements_covered: 1 },
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
retrievalStatus: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary"
}
],
retrievalResults: [{ fragment_id: "F1", status: "ok" }],
groundingCheck: { status: "grounded" },
droppedIntentSegments: [],
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: {
raw_time_anchor: "2020-07",
raw_time_scope: "month",
resolved_time_anchor: "2020-07",
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
temporal_guard_input: "2020-07",
temporal_alignment_status: "aligned",
temporal_resolution_source: "analysis_context",
temporal_guard_basis: "analysis_context",
temporal_guard_applied: true,
temporal_guard_outcome: "pass"
},
polarityAudit: {
raw_numeric_tokens: ["60.01"],
classified_numeric_tokens: [{ token: "60.01" }],
rejected_as_non_accounts: [],
resolved_account_anchors: ["60.01"]
},
claimAnchorAudit: {
settlement_role: "supplier",
settlement_role_resolution_reason: ["account_60_detected"],
polarity_resolution_status: "resolved"
},
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
followupStateUsage: null,
compositionDebug: {
problem_centric_answer_applied: true,
problem_units_used_count: 2,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1", "pu-2"]
},
addressRuntimeMetaForDeep: {
attempted: true,
applied: true,
reason: "ok",
provider: "openai",
fallbackRuleHit: null,
toolGateDecision: "run_address_lane",
toolGateReason: "detected",
predecomposeContract: { schema_version: "x" },
orchestrationContract: { schema_version: "y" }
},
outcomeClassV1: "FULLY_ANSWERED",
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
answerStructureV11: { schema_version: "answer_structure_v1_1" },
investigationStateSnapshot: { status: "active" },
normalizedPayload: { schema_version: "normalized_query_v2_0_2" }
};
}
describe("assistant debug payload assembler", () => {
it("builds deep debug payload with analysis context and optional sections", () => {
const payload = buildDeepAnalysisDebugPayload(baseInput());
expect(payload.trace_id).toBe("trace-1");
expect(payload.analysis_context_applied).toBe(true);
expect(payload.analysis_context).toMatchObject({
as_of_date: "2020-07-31",
source: "eval_analysis_date"
});
expect(payload.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
expect(payload.address_llm_predecompose_applied).toBe(true);
expect(payload.assistant_outcome_class_v1).toBe("FULLY_ANSWERED");
});
it("omits optional fields when they are not provided", () => {
const input = baseInput();
input.runtimeAnalysisContext.active = false;
input.followupStateUsage = null;
input.compositionDebug.problem_unit_ids_used = [];
input.rbpLiveRouteAudit = null;
input.faLiveRouteAudit = null;
input.addressRuntimeMetaForDeep = null;
const payload = buildDeepAnalysisDebugPayload(input);
expect(payload.analysis_context).toBeNull();
expect(Object.prototype.hasOwnProperty.call(payload, "followup_state_usage")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(payload, "problem_unit_ids_used")).toBe(false);
expect(payload.address_llm_predecompose_applied).toBe(false);
expect(payload.address_llm_predecompose_contract).toBeNull();
});
});

View File

@ -0,0 +1,131 @@
import { describe, expect, it } from "vitest";
import { buildAssistantConversationItem, buildDeepAnswerArtifacts } from "../src/services/assistantDeepResponseAssembler";
describe("assistant deep response assembler", () => {
it("strips technical tail and builds fallback answer structure when missing in composition", () => {
const artifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
featureContractsV11: true,
featureAnswerPolicyV11: true,
compositionAnswerStructureV11: null,
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: []
});
expect(artifacts.safeAssistantReply).toBe("Короткий ответ");
expect(artifacts.answerStructureV11?.schema_version).toBe("answer_structure_v1_1");
});
it("uses provided composition answer structure and creates assistant conversation item", () => {
const provided = {
schema_version: "answer_structure_v1_1",
answer_summary: "sum",
direct_answer: "direct",
mechanism_block: {
status: "grounded" as const,
mechanism_notes: [],
limitation_reason_codes: []
},
evidence_block: {
evidence_ids: [],
mechanism_notes: [],
coverage_note: "ok"
},
uncertainty_block: {
open_uncertainties: [],
limitations: []
},
next_step_block: {
recommended_actions: [],
clarification_questions: []
}
};
const artifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: "Готово",
featureContractsV11: true,
featureAnswerPolicyV11: true,
compositionAnswerStructureV11: provided as any,
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: []
});
expect(artifacts.answerStructureV11).toEqual(provided);
const item = buildAssistantConversationItem({
messageId: "msg-1",
sessionId: "asst-1",
text: artifacts.safeAssistantReply,
replyType: "factual",
traceId: "trace-1",
debug: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
fallback_type: "none",
route_summary: null,
fragments: [],
requirements_extracted: [],
coverage_report: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
routes: [],
retrieval_status: [],
retrieval_results: [],
answer_grounding_check: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
dropped_intent_segments: [],
answer_structure_v11: null,
investigation_state_snapshot: null,
normalized: null
} as any
});
expect(item.message_id).toBe("msg-1");
expect(item.session_id).toBe("asst-1");
expect(item.reply_type).toBe("factual");
expect(item.text).toBe("Готово");
expect(typeof item.created_at).toBe("string");
});
});

View File

@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnComposition } from "../src/services/assistantDeepTurnCompositionRuntimeAdapter";
describe("assistant deep turn composition runtime adapter", () => {
it("uses followup domain hint and company-anchor period signal", () => {
let capturedInput: Record<string, unknown> | null = null;
const output = buildAssistantDeepTurnComposition({
userMessage: "проверь хвосты по 60.01",
routeSummary: null,
retrievalResults: [],
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
followupUsage: { applied: true },
investigationState: {
schema_version: "investigation_state_v1",
session_id: "asst-1",
status: "active",
turn_index: 1,
updated_at: "2026-04-10T10:00:00.000Z",
question_id: "msg-1",
question_scope_id: null,
scope_origin: null,
focus: {
domain: "settlements_60_62",
period: null,
primary_accounts: [],
active_query_subject: null
},
narrowing_status: "unknown",
evidence_refs: [],
open_uncertainties: [],
last_answer_mode: null,
followup_context: null,
query_mode_hint: "direct_answer"
} as any,
companyAnchors: {
periods: ["2020-07"],
dates: []
},
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
featureAnswerPolicyV11: true,
featureProblemCentricAnswerV1: true,
featureLifecycleAnswerV1: true,
hasExplicitPeriodAnchor: () => false,
resolveQuestionTypeFn: () => "factual_lookup",
composeAssistantAnswerFn: ((input: Record<string, unknown>) => {
capturedInput = input;
return {
assistant_reply: "ok",
fallback_type: "none",
reply_type: "factual"
};
}) as any
});
expect(output.focusDomainHint).toBe("settlements_60_62");
expect(output.questionTypeClass).toBe("factual_lookup");
expect(output.hasPeriodInCompanyAnchors).toBe(true);
expect(output.normalizationPeriodExplicit).toBe(true);
expect(output.composition.reply_type).toBe("factual");
expect(capturedInput?.focusDomainHint).toBe("settlements_60_62");
expect(capturedInput?.normalizationPeriodExplicit).toBe(true);
});
it("falls back to explicit period from normalized payload when anchors are absent", () => {
const output = buildAssistantDeepTurnComposition({
userMessage: "проверь закрытие",
routeSummary: null,
retrievalResults: [],
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
followupUsage: { applied: false },
investigationState: null,
companyAnchors: {
periods: [],
dates: []
},
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
featureAnswerPolicyV11: true,
featureProblemCentricAnswerV1: true,
featureLifecycleAnswerV1: true,
hasExplicitPeriodAnchor: () => true,
resolveQuestionTypeFn: () => "verification",
composeAssistantAnswerFn: (() => ({
assistant_reply: "ok",
fallback_type: "none",
reply_type: "factual"
})) as any
});
expect(output.focusDomainHint).toBeNull();
expect(output.questionTypeClass).toBe("verification");
expect(output.hasPeriodInCompanyAnchors).toBe(false);
expect(output.normalizationPeriodExplicit).toBe(true);
});
});

View File

@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnRuntimeContext } from "../src/services/assistantDeepTurnContextRuntimeAdapter";
describe("assistant deep turn context runtime adapter", () => {
it("assembles context in deterministic order and propagates followup flag", () => {
const callOrder: string[] = [];
const companyAnchors = { accounts: ["60.01"] };
const temporalGuard = {
effective_primary_period: { from: "2020-07-01", to: "2020-07-31" },
primary_period_window: { from: "2020-07-01", to: "2020-07-31" }
};
const businessScope = { route_summary_resolved: { mode: "deterministic_v2", decisions: [] as any[] } } as any;
const output = buildAssistantDeepTurnRuntimeContext({
userMessage: "почему не закрыт 60.01",
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
},
followupUsage: { applied: true },
resolveCompanyAnchors: () => {
callOrder.push("anchors");
return companyAnchors;
},
resolveBusinessScopeAlignment: () => {
callOrder.push("scope_align");
return businessScope;
},
inferP0DomainFromMessage: () => {
callOrder.push("infer_domain");
return "settlements_60_62";
},
resolveTemporalGuard: (input) => {
callOrder.push("temporal");
expect(input.analysisContext).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
return temporalGuard as any;
},
resolveDomainPolarityGuard: (input) => {
callOrder.push("polarity");
expect(input.focusDomainHint).toBe("settlements_60_62");
return { polarity: "supplier_payable" };
},
resolveClaimBoundAnchors: (input) => {
callOrder.push("claim");
expect(input.primaryPeriod).toEqual(temporalGuard.effective_primary_period);
return { claim_type: "prove_settlement_closure_state" } as any;
},
resolveBusinessScopeFromLiveContext: (input) => {
callOrder.push("scope_live");
expect(input.followupApplied).toBe(true);
return {
...businessScope,
live_scope_used: true
} as any;
}
});
expect(callOrder).toEqual(["anchors", "scope_align", "infer_domain", "temporal", "polarity", "claim", "scope_live"]);
expect(output.companyAnchors).toBe(companyAnchors);
expect(output.focusDomainForGuards).toBe("settlements_60_62");
expect(output.claimAnchorAudit.claim_type).toBe("prove_settlement_closure_state");
expect(output.liveTemporalHint).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
});
it("drops unknown inferred domain and disables live temporal hint when context is inactive", () => {
const output = buildAssistantDeepTurnRuntimeContext({
userMessage: "какой-нибудь вопрос",
normalizedPayload: null as any,
routeSummary: null,
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null
},
followupUsage: null,
resolveCompanyAnchors: () => ({}),
resolveBusinessScopeAlignment: () => ({ route_summary_resolved: null }),
inferP0DomainFromMessage: () => "unknown_domain",
resolveTemporalGuard: () => ({ primary_period_window: null }),
resolveDomainPolarityGuard: () => ({ polarity: "not_applicable" }),
resolveClaimBoundAnchors: () => ({ claim_type: "unknown" } as any),
resolveBusinessScopeFromLiveContext: () => ({ route_summary_resolved: null })
});
expect(output.focusDomainForGuards).toBeNull();
expect(output.resolvedRouteSummary).toBeNull();
expect(output.liveTemporalHint).toBeNull();
});
});

View File

@ -0,0 +1,142 @@
import { describe, expect, it } from "vitest";
import { runAssistantDeepTurnGroundingRuntime } from "../src/services/assistantDeepTurnGroundingRuntimeAdapter";
describe("assistant deep turn grounding runtime adapter", () => {
it("runs audits, coverage-grounding pipeline and eligibility overlay in stable order", () => {
const callOrder: string[] = [];
const retrievalResults = [{ fragment_id: "F1" }] as any[];
const coverageEvaluation = {
requirements: [{ requirement_id: "R1" }],
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
} as any;
const groundingCheckBase = {
status: "grounded_positive",
reasons: [],
route_subject_match: true,
missing_requirements: [],
why_included_summary: [],
selection_reason_summary: []
} as any;
const output = runAssistantDeepTurnGroundingRuntime({
claimType: "prove_settlement_closure_state",
retrievalResults,
rbpPlanAudit: { rbp: true },
faPlanAudit: { fa: true },
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
userMessage: "check",
requirementExtraction: {
requirements: [{ requirement_id: "R1" }] as any,
byFragment: new Map([["F1", ["R1"]]])
} as any,
extractRequirements: (() => {
throw new Error("should not be called when requirementExtraction is provided");
}) as any,
evaluateCoverage: (() => {
throw new Error("should not be called directly from adapter");
}) as any,
checkGrounding: (() => {
throw new Error("should not be called directly from adapter");
}) as any,
temporalGuard: { temporal_guard_outcome: "passed" } as any,
polarityAudit: { outcome: "passed" } as any,
evidenceAudit: { admissible_evidence_count: 2 } as any,
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
targetedEvidenceHitRate: 0.5,
businessScopeResolved: ["company_specific_accounting"],
collectRbpLiveRouteAudit: (input) => {
callOrder.push("rbp_audit");
expect(input.planAudit).toEqual({ rbp: true });
return { rbp_live: 1 };
},
collectFaLiveRouteAudit: (input) => {
callOrder.push("fa_audit");
expect(input.planAudit).toEqual({ fa: true });
return { fa_live: 1 };
},
runCoverageGroundingPipelineFn: ((input: Record<string, unknown>) => {
callOrder.push("coverage_pipeline");
expect(input.retrievalResults).toBe(retrievalResults);
return {
requirementExtraction: input.requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}) as any,
applyGroundingEligibilityFn: ((input: Record<string, unknown>) => {
callOrder.push("eligibility");
expect(input.groundingCheckBase).toBe(groundingCheckBase);
return {
groundedAnswerEligibilityGuard: {
eligible: true
},
groundingCheck: groundingCheckBase
};
}) as any
});
expect(callOrder).toEqual(["rbp_audit", "fa_audit", "coverage_pipeline", "eligibility"]);
expect(output.rbpLiveRouteAudit).toEqual({ rbp_live: 1 });
expect(output.faLiveRouteAudit).toEqual({ fa_live: 1 });
expect(output.coverageEvaluation).toBe(coverageEvaluation);
expect(output.groundedAnswerEligibilityGuard).toEqual({ eligible: true });
expect(output.groundingCheck).toBe(groundingCheckBase);
});
it("threads default pipeline output through without custom hooks", () => {
const output = runAssistantDeepTurnGroundingRuntime({
claimType: "unknown",
retrievalResults: [],
rbpPlanAudit: null,
faPlanAudit: null,
routeSummary: null,
normalizedPayload: null as any,
userMessage: "q",
requirementExtraction: {
requirements: [],
byFragment: new Map()
} as any,
extractRequirements: () => ({
requirements: [],
byFragment: new Map()
}) as any,
evaluateCoverage: () => ({
requirements: [],
coverage: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
}),
checkGrounding: () =>
({
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
}) as any,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
polarityAudit: { applied: false, outcome: "not_applicable" } as any,
evidenceAudit: { admissible_evidence_count: 0 } as any,
claimAnchorAudit: null,
collectRbpLiveRouteAudit: () => null,
collectFaLiveRouteAudit: () => null
});
expect(output.coverageEvaluation.requirements).toEqual([]);
expect(output.groundingCheck.status).toBe("no_grounded_answer");
});
});

View File

@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import {
applyAssistantDeepTurnGroundingEligibility,
applyAssistantDeepTurnRetrievalGuards
} from "../src/services/assistantDeepTurnGuardRuntimeAdapter";
describe("assistant deep turn guard runtime adapter", () => {
it("runs retrieval guards in expected order and threads outputs", () => {
const callOrder: string[] = [];
const seedResults = [{ fragment_id: "F1" }] as any[];
const afterPolarity = [{ fragment_id: "P1" }] as any[];
const afterTargeted = [{ fragment_id: "T1" }] as any[];
const afterGate = [{ fragment_id: "G1" }] as any[];
const output = applyAssistantDeepTurnRetrievalGuards({
retrievalResults: seedResults as any,
domainPolarityGuardInitial: { applied: true, polarity: "supplier_payable" } as any,
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
focusDomainForGuards: "settlements_60_62" as any,
companyAnchors: { accounts: ["60.01"] } as any,
userMessage: "check settlements",
applyDomainPolarityGuardFn: ((input: Record<string, unknown>) => {
callOrder.push("polarity");
expect(input.retrievalResults).toBe(seedResults);
return {
retrievalResults: afterPolarity,
audit: {
applied: true,
polarity: "supplier_payable",
outcome: "passed",
reason_codes: []
}
};
}) as any,
applyTargetedEvidenceFn: ((input: Record<string, unknown>) => {
callOrder.push("targeted");
expect(input.retrievalResults).toBe(afterPolarity);
return {
retrievalResults: afterTargeted,
audit: {
targeted_evidence_hit_rate: 0.5,
reason_codes: []
}
};
}) as any,
applyEvidenceAdmissibilityGateFn: ((input: Record<string, unknown>) => {
callOrder.push("gate");
expect(input.retrievalResults).toBe(afterTargeted);
expect(input.polarity).toBe("supplier_payable");
expect(input.userMessage).toBe("check settlements");
return {
retrievalResults: afterGate,
audit: {
admissible_evidence_count: 2,
reason_codes: []
}
};
}) as any
});
expect(callOrder).toEqual(["polarity", "targeted", "gate"]);
expect(output.retrievalResults).toBe(afterGate);
expect(output.polarityGuardResult.retrievalResults).toBe(afterPolarity);
expect(output.targetedEvidenceResult.retrievalResults).toBe(afterTargeted);
expect(output.evidenceGateResult.retrievalResults).toBe(afterGate);
});
it("evaluates grounding eligibility and applies status overlay", () => {
const callOrder: string[] = [];
const groundingCheckBase = {
status: "grounded_positive",
reasons: ["base"],
route_subject_match: true
};
const output = applyAssistantDeepTurnGroundingEligibility({
groundingCheckBase,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
polarityAudit: { applied: true, outcome: "passed", polarity: "supplier_payable" } as any,
evidenceAudit: { admissible_evidence_count: 0 } as any,
claimAnchorAudit: { claim_anchor_resolution_rate: 1, missing_anchors: [], required_anchors: [] } as any,
targetedEvidenceHitRate: 0,
businessScopeResolved: ["company_specific_accounting"],
evaluateGroundedAnswerEligibilityFn: ((input: Record<string, unknown>) => {
callOrder.push("eligibility");
expect(input.targetedEvidenceHitRate).toBe(0);
return {
eligible: false,
temporal_passed: true,
eligibility_time_basis: "none",
business_scope_passed: true,
polarity_passed: true,
claim_anchors_passed: true,
claim_anchor_resolution_rate: 1,
missing_required_anchors: 0,
admissible_evidence_count: 0,
critical_contradiction: false,
outcome: "limited_or_insufficient_evidence",
grounding_mode: "limited_or_insufficient_evidence",
reason_codes: ["admissible_evidence_count_zero"]
};
}) as any,
applyEligibilityToGroundingCheckFn: ((check: Record<string, unknown>, eligibility: Record<string, unknown>) => {
callOrder.push("overlay");
expect(check.status).toBe("grounded_positive");
expect(eligibility.eligible).toBe(false);
return {
...check,
status: "no_grounded_answer",
reasons: ["base", "not_enough_evidence"]
};
}) as any
});
expect(callOrder).toEqual(["eligibility", "overlay"]);
expect(output.groundedAnswerEligibilityGuard.eligible).toBe(false);
expect(output.groundingCheck.status).toBe("no_grounded_answer");
expect(output.groundingCheck.reasons).toEqual(["base", "not_enough_evidence"]);
});
});

View File

@ -0,0 +1,140 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnPackagingInput, type AssistantDeepTurnInputBuilderArgs } from "../src/services/assistantDeepTurnInputBuilder";
function baseArgs(): AssistantDeepTurnInputBuilderArgs {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь 60.01",
normalized: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
}
},
normalizedQuestion: "проверь 60.01",
routeSummary: null,
droppedIntentSegments: [],
analysisContextForContract: null,
executionPlan: [],
requirementExtractionRequirements: [],
coverageEvaluationRequirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: [],
routesForDebug: [],
resolvedExecutionState: {},
questionTypeClass: "factual_lookup",
companyAnchors: {},
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null,
snapshot_mode: "auto"
},
businessScopeResolution: {},
temporalGuard: {},
polarityAudit: {},
claimAnchorAudit: {},
targetedEvidenceAudit: null,
evidenceAdmissibilityGateAudit: null,
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: {},
followupStateUsage: undefined,
composition: {
reply_type: "factual",
fallback_type: "none"
},
safeAssistantReplyBase: "ok",
featureContractsV11: true,
featureAnswerPolicyV11: true,
investigationStateSnapshot: null,
addressRuntimeMetaForDeep: null
};
}
describe("assistant deep turn input builder", () => {
it("applies stable defaults for optional composition and followup fields", () => {
const built = buildAssistantDeepTurnPackagingInput(baseArgs());
expect(built.followupStateUsage).toBeNull();
expect(built.composition.answer_structure_v11).toBeNull();
expect(built.composition.problem_centric_answer_applied).toBe(false);
expect(built.composition.problem_units_used_count).toBe(0);
expect(built.composition.problem_answer_mode).toBe("stage1_policy_v11");
expect(built.composition.problem_unit_ids_used).toEqual([]);
});
it("preserves explicit composition fields and normalizes unit ids array", () => {
const args = baseArgs();
args.followupStateUsage = { applied: true };
args.composition.answer_structure_v11 = {
schema_version: "answer_structure_v1_1",
answer_summary: "sum",
direct_answer: "direct",
mechanism_block: {
status: "grounded",
mechanism_notes: [],
limitation_reason_codes: []
},
evidence_block: {
evidence_ids: [],
source_refs: [],
mechanism_notes: [],
coverage_note: "ok"
},
uncertainty_block: {
open_uncertainties: [],
limitations: []
},
next_step_block: {
recommended_actions: [],
clarification_questions: []
}
} as any;
args.composition.problem_centric_answer_applied = true;
args.composition.problem_units_used_count = 3;
args.composition.problem_answer_mode = "stage3_lifecycle_aware_v1";
args.composition.problem_unit_ids_used = ["pu-1", "pu-2"];
const built = buildAssistantDeepTurnPackagingInput(args);
expect(built.followupStateUsage).toEqual({ applied: true });
expect(built.composition.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
expect(built.composition.problem_centric_answer_applied).toBe(true);
expect(built.composition.problem_units_used_count).toBe(3);
expect(built.composition.problem_answer_mode).toBe("stage3_lifecycle_aware_v1");
expect(built.composition.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
});
});

View File

@ -0,0 +1,179 @@
import { describe, expect, it } from "vitest";
import { assembleAssistantDeepTurnPackaging } from "../src/services/assistantDeepTurnPackaging";
function buildRetrieval() {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
};
}
function baseInput() {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь хвосты по 60.01",
normalized: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь хвосты по 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
}
},
normalizedQuestion: "проверь хвосты по 60.01",
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
},
droppedIntentSegments: [],
analysisContextForContract: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
}
],
requirementExtractionRequirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить хвосты 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageEvaluationRequirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить хвосты 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResultsRaw: [buildRetrieval()],
retrievalResults: [buildRetrieval()],
routesForDebug: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
resolvedExecutionState: { executable: 1 },
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: { temporal_guard_applied: true },
polarityAudit: { resolved_account_anchors: ["60.01"] },
claimAnchorAudit: { settlement_role: "supplier" },
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligible: true },
followupStateUsage: null,
composition: {
reply_type: "factual" as const,
fallback_type: "none",
answer_structure_v11: null,
problem_centric_answer_applied: true,
problem_units_used_count: 1,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1"]
},
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
featureContractsV11: true,
featureAnswerPolicyV11: true,
investigationStateSnapshot: { status: "active" },
addressRuntimeMetaForDeep: null
};
}
describe("assistant deep turn packaging", () => {
it("assembles deep artifacts, debug payload and processed log in one call", () => {
const input = baseInput();
const output = assembleAssistantDeepTurnPackaging(input as any);
expect(output.deepAnswerArtifacts.safeAssistantReply).toBe("Короткий ответ");
expect(output.contractsBundleV1.outcomeClassV1).toBe("FULLY_ANSWERED");
expect(output.debug.trace_id).toBe("trace-1");
expect(output.assistantItem.message_id).toBe("msg-1");
expect(output.assistantItem.text).toBe("Короткий ответ");
expect(output.deepAnalysisLogDetails.session_id).toBe("asst-1");
expect(output.deepAnalysisLogDetails.message_id).toBe("msg-1");
});
});

View File

@ -0,0 +1,184 @@
import { describe, expect, it } from "vitest";
import { runAssistantDeepTurnPackagingRuntime } from "../src/services/assistantDeepTurnPackagingRuntimeAdapter";
function buildBaseInput() {
return {
featureInvestigationStateV1: true,
sessionId: "asst-1",
questionId: "msg-user-1",
userMessage: "проверь кейс",
normalized: {
trace_id: "trace-1",
prompt_version: "p1",
schema_version: "s1",
normalized: { schema_version: "normalized_query_v2_0_2" } as any
},
normalizedQuestion: "проверь кейс",
routeSummary: null,
executionPlan: [],
requirementExtractionRequirements: [],
coverageEvaluationRequirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: [],
questionTypeClass: "factual_lookup",
companyAnchors: {},
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null,
snapshot_mode: "auto" as const
},
businessScopeResolution: {},
temporalGuard: {},
polarityAudit: {},
claimAnchorAudit: {},
targetedEvidenceAudit: {},
evidenceAdmissibilityGateAudit: {},
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: {},
followupStateUsage: null,
followupApplied: false,
composition: {
assistant_reply: "raw-reply",
reply_type: "factual" as const,
fallback_type: "none"
},
featureContractsV11: true,
featureAnswerPolicyV11: true,
previousInvestigationState: null,
addressRuntimeMetaForDeep: null,
extractDroppedIntentSegments: () => [],
buildDebugRoutes: () => [],
extractExecutionState: () => null,
sanitizeReply: (value: string) => value,
persistInvestigationState: () => {},
messageIdFactory: () => "msg-fixed"
};
}
describe("assistant deep turn packaging runtime adapter", () => {
it("executes pre-packaging, snapshot, persist, input-build and assembly in stable order", () => {
const callOrder: string[] = [];
let persistedByCallback = 0;
const output = runAssistantDeepTurnPackagingRuntime({
...buildBaseInput(),
persistInvestigationState: () => {
persistedByCallback += 1;
},
buildPrePackagingContextFn: (() => {
callOrder.push("pre");
return {
droppedIntentSegments: ["F2_dropped"],
analysisContextForContract: null,
routesForDebug: [{ fragment_id: "F1" }],
resolvedExecutionState: [{ fragment_id: "F1", execution_readiness: "ready" }],
safeAssistantReplyBase: "safe-base"
};
}) as any,
buildInvestigationStateSnapshotFn: (() => {
callOrder.push("snapshot");
return { schema_version: "investigation_state_v1" };
}) as any,
persistInvestigationStateSnapshotFn: ((input: Record<string, unknown>) => {
callOrder.push("persist");
(input.persist as Function)(input.sessionId, input.snapshot);
return true;
}) as any,
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => {
callOrder.push("input");
expect(input.messageId).toBe("msg-fixed");
expect(input.droppedIntentSegments).toEqual(["F2_dropped"]);
expect(input.safeAssistantReplyBase).toBe("safe-base");
return input;
}) as any,
assembleDeepTurnPackagingFn: (() => {
callOrder.push("assemble");
return {
deepAnswerArtifacts: {
safeAssistantReply: "assistant-safe"
},
debug: { ok: true },
assistantItem: {
message_id: "msg-fixed",
session_id: "asst-1",
role: "assistant",
text: "assistant-safe",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: { stage: "deep_analysis" }
};
}) as any
});
expect(callOrder).toEqual(["pre", "snapshot", "persist", "input", "assemble"]);
expect(persistedByCallback).toBe(1);
expect(output.messageId).toBe("msg-fixed");
expect(output.safeAssistantReply).toBe("assistant-safe");
expect(output.debug).toEqual({ ok: true });
expect(output.deepAnalysisLogDetails).toEqual({ stage: "deep_analysis" });
});
it("does not persist investigation snapshot when feature is disabled", () => {
let persistedByCallback = 0;
const output = runAssistantDeepTurnPackagingRuntime({
...buildBaseInput(),
featureInvestigationStateV1: false,
persistInvestigationState: () => {
persistedByCallback += 1;
},
buildPrePackagingContextFn: (() => ({
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: null,
safeAssistantReplyBase: "safe-base"
})) as any,
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => input) as any,
assembleDeepTurnPackagingFn: (() => ({
deepAnswerArtifacts: {
safeAssistantReply: "assistant-safe"
},
debug: {},
assistantItem: {
message_id: "msg-fixed",
session_id: "asst-1",
role: "assistant",
text: "assistant-safe",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
})) as any
});
expect(output.investigationStateSnapshot).toBeNull();
expect(persistedByCallback).toBe(0);
});
});

View File

@ -0,0 +1,96 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnExecutionPlan } from "../src/services/assistantDeepTurnPlanRuntimeAdapter";
describe("assistant deep turn plan runtime adapter", () => {
it("builds execution plan through extraction, enforcement and guard hints in stable order", () => {
const callOrder: string[] = [];
const byFragment = new Map<string, string[]>([["F1", ["R1"]]]);
const planInitial = [{ fragment_id: "F1", route: "store_canonical" }] as any[];
const planAfterRbp = [{ fragment_id: "F1", route: "hybrid_store_plus_live" }] as any[];
const planAfterFa = [{ fragment_id: "F1", route: "hybrid_store_plus_live", fa: true }] as any[];
const planAfterTemporal = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true }] as any[];
const planAfterPolarity = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true, polarity: true }] as any[];
const output = buildAssistantDeepTurnExecutionPlan({
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
userMessage: "check tails",
claimType: "prove_settlement_closure_state",
temporalGuard: { temporal_guard_outcome: "passed" } as any,
domainPolarityGuardInitial: { polarity: "supplier_payable" } as any,
extractRequirements: () => {
callOrder.push("extract");
return {
requirements: [{ requirement_id: "R1" }] as any[],
byFragment
};
},
toExecutionPlan: (_routeSummary, _normalizedPayload, _userMessage, requirementByFragment) => {
callOrder.push("plan");
expect(requirementByFragment).toBe(byFragment);
return planInitial as any;
},
enforceRbpLiveRoutePlan: ({ executionPlan }) => {
callOrder.push("rbp");
expect(executionPlan).toBe(planInitial);
return {
executionPlan: planAfterRbp as any,
audit: { rbp: true }
};
},
enforceFaLiveRoutePlan: ({ executionPlan }) => {
callOrder.push("fa");
expect(executionPlan).toBe(planAfterRbp);
return {
executionPlan: planAfterFa as any,
audit: { fa: true }
};
},
applyTemporalHintToExecutionPlan: (executionPlan) => {
callOrder.push("temporal");
expect(executionPlan).toBe(planAfterFa);
return planAfterTemporal as any;
},
applyPolarityHintToExecutionPlan: (executionPlan) => {
callOrder.push("polarity");
expect(executionPlan).toBe(planAfterTemporal);
return planAfterPolarity as any;
}
});
expect(callOrder).toEqual(["extract", "plan", "rbp", "fa", "temporal", "polarity"]);
expect(output.requirementExtraction.byFragment).toBe(byFragment);
expect(output.executionPlan).toBe(planAfterPolarity);
expect(output.rbpRoutePlanEnforcement.audit).toEqual({ rbp: true });
expect(output.faRoutePlanEnforcement.audit).toEqual({ fa: true });
});
it("preserves empty execution plan end-to-end", () => {
const output = buildAssistantDeepTurnExecutionPlan({
routeSummary: null,
normalizedPayload: null as any,
userMessage: "noop",
claimType: "unknown",
temporalGuard: null,
domainPolarityGuardInitial: null,
extractRequirements: () => ({
requirements: [],
byFragment: new Map()
}),
toExecutionPlan: () => [],
enforceRbpLiveRoutePlan: ({ executionPlan }) => ({
executionPlan,
audit: { rbp: false }
}),
enforceFaLiveRoutePlan: ({ executionPlan }) => ({
executionPlan,
audit: { fa: false }
}),
applyTemporalHintToExecutionPlan: (executionPlan) => executionPlan,
applyPolarityHintToExecutionPlan: (executionPlan) => executionPlan
});
expect(output.executionPlan).toEqual([]);
expect(output.requirementExtraction.requirements).toEqual([]);
});
});

View File

@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnPrePackagingContext } from "../src/services/assistantDeepTurnPrePackagingContext";
describe("assistant deep turn pre-packaging context", () => {
it("builds all pre-packaging fields with active analysis context", () => {
const output = buildAssistantDeepTurnPrePackagingContext({
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: null,
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
},
assistantReply: "raw",
extractDroppedIntentSegments: () => ["segment_1"],
buildDebugRoutes: () => [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
extractExecutionState: () => ({ executable: 1 }),
sanitizeReply: (value, fallback) => `${value}::${fallback}`
});
expect(output.droppedIntentSegments).toEqual(["segment_1"]);
expect(output.analysisContextForContract).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
});
expect(output.routesForDebug).toEqual([{ fragment_id: "F1", route: "hybrid_store_plus_live" }]);
expect(output.resolvedExecutionState).toEqual({ executable: 1 });
expect(output.safeAssistantReplyBase).toContain("Нужны уточнения для надежного ответа.");
});
it("returns null analysis context when runtime context is inactive", () => {
const output = buildAssistantDeepTurnPrePackagingContext({
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: null,
runtimeAnalysisContext: {
active: false,
as_of_date: "2020-07-31",
period_from: "2020-07-01",
period_to: "2020-07-31",
source: "eval_analysis_date",
snapshot_mode: "auto"
},
assistantReply: "ok",
extractDroppedIntentSegments: () => [],
buildDebugRoutes: () => [],
extractExecutionState: () => null,
sanitizeReply: (value) => value
});
expect(output.analysisContextForContract).toBeNull();
expect(output.routesForDebug).toEqual([]);
expect(output.safeAssistantReplyBase).toBe("ok");
});
});

View File

@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnSuccessResponse } from "../src/services/assistantDeepTurnResponseBuilder";
import type { AssistantConversationItem } from "../src/types/assistant";
function buildAssistantItem(): AssistantConversationItem {
return {
message_id: "msg-1",
session_id: "asst-1",
role: "assistant",
text: "ok",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
};
}
describe("assistant deep turn response builder", () => {
it("builds canonical assistant message response envelope", () => {
const assistantItem = buildAssistantItem();
const response = buildAssistantDeepTurnSuccessResponse({
sessionId: "asst-1",
assistantReply: "ответ",
replyType: "factual",
conversationItem: assistantItem,
debug: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
fallback_type: "none",
route_summary: null,
fragments: [],
requirements_extracted: [],
coverage_report: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
routes: [],
retrieval_status: [],
retrieval_results: [],
answer_grounding_check: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
dropped_intent_segments: [],
answer_structure_v11: null,
investigation_state_snapshot: null,
normalized: null
} as any,
conversation: [assistantItem]
});
expect(response.ok).toBe(true);
expect(response.session_id).toBe("asst-1");
expect(response.assistant_reply).toBe("ответ");
expect(response.reply_type).toBe("factual");
expect(response.conversation_item.message_id).toBe("msg-1");
expect(response.conversation).toEqual([assistantItem]);
expect(response.debug.trace_id).toBe("trace-1");
});
});

View File

@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";
import type { AssistantExecutionPlanItem } from "../src/services/assistantQueryPlanning";
import { executeAssistantDeepTurnRetrievalPlan } from "../src/services/assistantDeepTurnRetrievalRuntimeAdapter";
describe("assistant deep turn retrieval runtime adapter", () => {
it("handles skipped, executed and failed plan items with stable call records", async () => {
const executionPlan: AssistantExecutionPlanItem[] = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "no_route",
should_execute: false,
fragment_text: "clarify period",
no_route_reason: "insufficient_specificity",
clarification_reason: "domain_or_scope_unclear"
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
should_execute: true,
fragment_text: "show balances",
no_route_reason: null,
clarification_reason: null
},
{
fragment_id: "F3",
requirement_ids: ["R3"],
route: "live_mcp_drilldown",
should_execute: true,
fragment_text: "tail check",
no_route_reason: null,
clarification_reason: null
}
];
const normalizeCalls: Array<{ fragmentId: string; route: string; rawStatus: string | null }> = [];
const output = await executeAssistantDeepTurnRetrievalPlan({
executionPlan,
liveTemporalHint: null,
executeRouteRuntime: async (route) => {
if (route === "live_mcp_drilldown") {
throw new Error("route failed");
}
return {
status: "ok",
result_type: "summary",
items: [],
summary: { route },
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "high",
limitations: [],
errors: []
};
},
mapNoRouteReason: (reason) => (reason === "insufficient_specificity" ? "Needs clarification." : "No-route decision."),
buildSkippedResult: () =>
({
fragment_id: "F1",
route: "no_route",
status: "partial"
}) as any,
normalizeRetrievalResultFn: ((fragmentId: string, _requirementIds: string[], route: string, raw: Record<string, unknown>) => {
normalizeCalls.push({
fragmentId,
route,
rawStatus: typeof raw.status === "string" ? raw.status : null
});
return {
fragment_id: fragmentId,
route,
status: raw.status
} as any;
}) as any
});
expect(output.retrievalCalls).toEqual([
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "no_route",
status: "skipped",
query_text: "clarify period",
reason: "Needs clarification."
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "executed",
query_text: "show balances",
reason: null
},
{
fragment_id: "F3",
requirement_ids: ["R3"],
route: "live_mcp_drilldown",
status: "failed",
query_text: "tail check",
reason: "route failed"
}
]);
expect(output.retrievalResultsRaw).toHaveLength(2);
expect((output.retrievalResultsRaw[0].raw_result as Record<string, unknown>).status).toBe("ok");
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).status).toBe("error");
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).limitations).toEqual(["Route executor failed."]);
expect(output.retrievalResults).toHaveLength(3);
expect(output.retrievalResults[0].status).toBe("partial");
expect(normalizeCalls).toEqual([
{ fragmentId: "F2", route: "store_canonical", rawStatus: "ok" },
{ fragmentId: "F3", route: "live_mcp_drilldown", rawStatus: "error" }
]);
});
it("passes live temporal hint into route runtime execution", async () => {
const executionPlan: AssistantExecutionPlanItem[] = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
fragment_text: "check as of date",
no_route_reason: null,
clarification_reason: null
}
];
let capturedTemporalHint: Record<string, unknown> | null = null;
await executeAssistantDeepTurnRetrievalPlan({
executionPlan,
liveTemporalHint: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
},
executeRouteRuntime: async (_route, _fragmentText, options) => {
capturedTemporalHint = options.temporalHint as unknown as Record<string, unknown>;
return { status: "ok" };
},
mapNoRouteReason: () => "No-route decision.",
buildSkippedResult: (() => ({ status: "partial" })) as any,
normalizeRetrievalResultFn: (() => ({ status: "ok" })) as any
});
expect(capturedTemporalHint).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
});
});

View File

@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
import { assembleAssistantEvidenceBundle } from "../src/services/assistantEvidenceBundleAssembler";
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant evidence bundle assembler", () => {
it("builds evidence contract and retrieval status from the same retrieval set", () => {
const assembled = assembleAssistantEvidenceBundle({
retrievalCalls: [{ route: "hybrid_store_plus_live" }, { route: "store_canonical" }],
retrievalResults: [
buildRetrieval({
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "signal",
confidence: "medium",
limitation: null,
payload: {}
}
]
}),
buildRetrieval({
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "error",
result_type: "list",
errors: ["timeout"]
})
]
});
expect(assembled.evidenceBundleContractV1.retrieval_calls_total).toBe(2);
expect(assembled.evidenceBundleContractV1.retrieval_results_total).toBe(2);
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.ok).toBe(1);
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.error).toBe(1);
expect(assembled.evidenceBundleContractV1.evidence_total).toBe(1);
expect(assembled.evidenceBundleContractV1.source_refs_total).toBe(1);
expect(assembled.retrievalStatus).toEqual([
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary"
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "error",
result_type: "list"
}
]);
});
});

View File

@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";
import type { RouteHintSummary } from "../src/types/normalizer";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
import { createEmptyInvestigationState } from "../src/services/investigationState";
import {
buildAssistantInvestigationStateSnapshot,
persistAssistantInvestigationStateSnapshot
} from "../src/services/assistantInvestigationStateRuntimeAdapter";
function buildRouteSummary(): RouteHintSummary {
return {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
candidate_labels: ["anomaly_probe"],
decision_flags: {
has_multi_entity_scope: false,
asks_for_chain_explanation: false,
asks_for_ranking_or_top: false,
asks_for_period_summary: false,
asks_for_rule_check: true,
asks_for_anomaly_scan: true,
asks_for_exact_object_trace: false,
asks_for_evidence: true,
mentions_period_close_context: false
},
route: "store_feature_risk",
reason: "test-route"
}
],
fallback: {
type: "none",
message: null
}
};
}
function buildRetrievalResult(): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
};
}
describe("assistant investigation state runtime adapter", () => {
it("returns null and skips persist when feature disabled", () => {
const snapshot = buildAssistantInvestigationStateSnapshot({
featureEnabled: false,
previousState: createEmptyInvestigationState("asst-1", "2026-04-10T10:00:00.000Z"),
timestamp: "2026-04-10T10:01:00.000Z",
questionId: "msg-1",
userMessage: "проверь 60.01",
routeSummary: buildRouteSummary(),
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [],
replyType: "factual",
followupApplied: false
});
expect(snapshot).toBeNull();
let persistCalled = false;
const persisted = persistAssistantInvestigationStateSnapshot({
featureEnabled: false,
sessionId: "asst-1",
snapshot: null,
persist: () => {
persistCalled = true;
}
});
expect(persisted).toBe(false);
expect(persistCalled).toBe(false);
});
it("builds snapshot and persists it when feature enabled", () => {
const previous = createEmptyInvestigationState("asst-2", "2026-04-10T10:00:00.000Z");
const snapshot = buildAssistantInvestigationStateSnapshot({
featureEnabled: true,
previousState: previous,
timestamp: "2026-04-10T10:01:00.000Z",
questionId: "msg-2",
userMessage: "проверь счет 60.01 за 2020-07",
routeSummary: buildRouteSummary(),
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить счет 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "store_feature_risk"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [buildRetrievalResult()],
replyType: "factual",
followupApplied: false
});
expect(snapshot).not.toBeNull();
expect(snapshot?.turn_index).toBe(1);
expect(snapshot?.question_id).toBe("msg-2");
let persistedSessionId: string | null = null;
let persistedQuestionId: string | null = null;
const persisted = persistAssistantInvestigationStateSnapshot({
featureEnabled: true,
sessionId: "asst-2",
snapshot: snapshot,
persist: (sessionId, state) => {
persistedSessionId = sessionId;
persistedQuestionId = state.question_id;
}
});
expect(persisted).toBe(true);
expect(persistedSessionId).toBe("asst-2");
expect(persistedQuestionId).toBe("msg-2");
});
});

View File

@ -209,6 +209,33 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage:
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?",
effectiveAddressUserMessage:
"Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "high",
intent: "unknown",
intent_confidence: "low"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
});
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",

View File

@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import { buildDeepAnalysisProcessedLogDetails } from "../src/services/assistantMessageLogAssembler";
function baseInput() {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь 60.01",
normalizerOutput: { schema_version: "normalized_query_v2_0_2" },
executionPlan: [{ fragment_id: "F1", route: "hybrid_store_plus_live", should_execute: true }],
resolvedExecutionState: { executable: 1 },
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResultsRaw: [{ status: "ok" }],
retrievalResultsNormalized: [{ status: "ok" }],
requirementsExtracted: [{ requirement_id: "R1" }],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["signal"],
selection_reason_summary: ["ranked"]
},
replyType: "factual",
droppedIntentSegments: [],
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: {
raw_time_anchor: "2020-07",
raw_time_scope: "month",
resolved_time_anchor: "2020-07",
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
temporal_guard_input: "2020-07",
temporal_alignment_status: "aligned",
temporal_resolution_source: "analysis_context",
temporal_guard_basis: "analysis_context",
temporal_guard_applied: true,
temporal_guard_outcome: "pass"
},
polarityAudit: {
raw_numeric_tokens: ["60.01"],
classified_numeric_tokens: [{ token: "60.01" }],
rejected_as_non_accounts: [],
resolved_account_anchors: ["60.01"]
},
claimAnchorAudit: {
settlement_role: "supplier",
settlement_role_resolution_reason: ["account_60_detected"],
polarity_resolution_status: "resolved"
},
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
followupStateUsage: null,
compositionDebug: {
problem_centric_answer_applied: true,
problem_units_used_count: 1,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1"],
fallback_type: "none"
},
outcomeClassV1: "FULLY_ANSWERED",
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
answerStructureV11: { schema_version: "answer_structure_v1_1" },
investigationStateSnapshot: { status: "active" },
assistantReply: "ok",
traceId: "trace-1"
};
}
describe("assistant message log assembler", () => {
it("builds deep analysis log details and resolves full coverage status", () => {
const details = buildDeepAnalysisProcessedLogDetails(baseInput());
expect(details.session_id).toBe("asst-1");
expect(details.coverage_status).toBe("full");
expect(details.analysis_context).toMatchObject({
as_of_date: "2020-07-31"
});
expect(details.problem_unit_ids_used).toEqual(["pu-1"]);
expect(details.reply_type).toBe("factual");
});
it("marks partial coverage and omits optional sections when empty", () => {
const input = baseInput();
input.coverageReport.requirements_covered = 0;
input.coverageReport.requirements_uncovered = ["R1"];
input.runtimeAnalysisContext.active = false;
input.followupStateUsage = null;
input.compositionDebug.problem_unit_ids_used = [];
const details = buildDeepAnalysisProcessedLogDetails(input);
expect(details.coverage_status).toBe("partial_or_limited");
expect(details.analysis_context).toBeNull();
expect(Object.prototype.hasOwnProperty.call(details, "followup_state_usage")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(details, "problem_unit_ids_used")).toBe(false);
});
});

View File

@ -0,0 +1,304 @@
import { describe, expect, it } from "vitest";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
import type { RouteHintSummary } from "../src/types/normalizer";
import {
buildAssistantCoverageContractV1,
buildAssistantEvidenceBundleContractV1,
buildAssistantExecutionPlanContractV1,
buildAssistantQueryFrameContractV1,
classifyAssistantOutcomeClassV1
} from "../src/services/assistantOrchestrationContracts";
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
return {
requirements_total: 1,
requirements_covered: 0,
requirements_uncovered: ["R1"],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: [],
...input
};
}
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
return {
status: "no_grounded_answer",
route_subject_match: true,
missing_requirements: ["R1"],
reasons: [],
why_included_summary: [],
selection_reason_summary: [],
...input
};
}
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
function buildRouteSummary(): RouteHintSummary {
return {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
};
}
describe("assistant orchestration contracts v1", () => {
it("builds query frame and execution plan contracts with normalized analysis context", () => {
const queryFrame = buildAssistantQueryFrameContractV1({
userMessage: "Покажи хвосты по счету 60",
normalizedQuestion: "Покажи хвосты по счету 60",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "Покажи хвосты по счету 60",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }, { fragment_id: "F2" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
} as any,
routeSummary: buildRouteSummary(),
droppedIntentSegments: ["лишний сегмент"],
analysisContext: {
as_of_date: "2020-07-31",
source: "eval_analysis_date",
snapshot_mode: "unexpected_mode"
}
});
const executionPlan = buildAssistantExecutionPlanContractV1({
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "no_route",
should_execute: false,
no_route_reason: "insufficient_specificity",
clarification_reason: "need_period"
}
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
},
{
requirement_id: "R2",
source_fragment_id: "F2",
requirement_text: "req2",
subject_tokens: [],
status: "clarification_needed",
route: null
}
]
});
expect(queryFrame.schema_version).toBe("assistant_query_frame_v1");
expect(queryFrame.route_summary_mode).toBe("deterministic_v2");
expect(queryFrame.fragments_total).toBe(2);
expect(queryFrame.analysis_context?.as_of_date).toBe("2020-07-31");
expect(queryFrame.analysis_context?.snapshot_mode).toBe("auto");
expect(executionPlan.schema_version).toBe("assistant_execution_plan_v1");
expect(executionPlan.steps).toHaveLength(2);
expect(executionPlan.requirements_total).toBe(2);
});
it("classifies fully answered and misrouted outcomes", () => {
const fullyAnswered = classifyAssistantOutcomeClassV1({
replyType: "factual_with_explanation",
coverageReport: buildCoverage({
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}),
grounding: buildGrounding({
status: "grounded",
missing_requirements: []
}),
retrievalResults: [buildRetrieval({ status: "ok" })]
});
const misrouted = classifyAssistantOutcomeClassV1({
replyType: "route_mismatch_blocked",
coverageReport: buildCoverage(),
grounding: buildGrounding({
status: "route_mismatch_blocked"
}),
retrievalResults: [buildRetrieval({ status: "partial" })]
});
expect(fullyAnswered).toBe("FULLY_ANSWERED");
expect(misrouted).toBe("MISROUTED");
});
it("classifies tooling and entity-binding failures", () => {
const toolingBlocked = classifyAssistantOutcomeClassV1({
replyType: "factual",
coverageReport: buildCoverage(),
grounding: buildGrounding(),
retrievalResults: [buildRetrieval({ status: "error" }), buildRetrieval({ status: "error" })]
});
const entityBindingFailure = classifyAssistantOutcomeClassV1({
replyType: "no_grounded_answer",
coverageReport: buildCoverage({
requirements_total: 1,
requirements_covered: 0,
requirements_uncovered: ["R1"]
}),
grounding: buildGrounding({
status: "no_grounded_answer",
route_subject_match: true,
missing_requirements: ["R1"]
}),
retrievalResults: [buildRetrieval({ status: "empty" })]
});
expect(toolingBlocked).toBe("BLOCKED_BY_TOOLING");
expect(entityBindingFailure).toBe("FAILED_TO_BIND_ENTITIES");
});
it("builds evidence bundle and coverage contracts", () => {
const evidenceBundle = buildAssistantEvidenceBundleContractV1({
retrievalCalls: [{ id: 1 }, { id: 2 }, { id: 3 }],
retrievalResults: [
buildRetrieval({
status: "ok",
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "signal",
confidence: "medium",
limitation: null,
payload: {}
}
],
limitations: ["needs_extra_period"]
}),
buildRetrieval({
status: "partial",
evidence: [],
errors: ["timeout"]
}),
buildRetrieval({
status: "error",
evidence: [],
errors: ["mcp_unavailable"]
})
]
});
const outcomeClass = classifyAssistantOutcomeClassV1({
replyType: "partial_coverage",
coverageReport: buildCoverage({
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"]
}),
grounding: buildGrounding({
status: "partial",
route_subject_match: true,
missing_requirements: ["R2"]
}),
retrievalResults: [buildRetrieval({ status: "ok" }), buildRetrieval({ status: "partial" })]
});
const coverageContract = buildAssistantCoverageContractV1({
coverageReport: buildCoverage({
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"]
}),
grounding: buildGrounding({
status: "partial",
missing_requirements: ["R2"]
}),
outcomeClass
});
expect(evidenceBundle.retrieval_calls_total).toBe(3);
expect(evidenceBundle.retrieval_results_total).toBe(3);
expect(evidenceBundle.retrieval_status_breakdown.ok).toBe(1);
expect(evidenceBundle.retrieval_status_breakdown.partial).toBe(1);
expect(evidenceBundle.retrieval_status_breakdown.error).toBe(1);
expect(evidenceBundle.evidence_total).toBe(1);
expect(evidenceBundle.source_refs_total).toBe(1);
expect(evidenceBundle.error_total).toBe(2);
expect(coverageContract.outcome_class).toBe("PARTIALLY_ANSWERED");
});
});

View File

@ -0,0 +1,104 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantCoverageGroundingPipeline } from "../src/services/assistantOrchestrationRuntimeAdapter";
describe("assistant orchestration runtime adapter", () => {
it("runs requirement -> coverage -> grounding pipeline in order", () => {
const requirementExtraction = {
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req",
subject_tokens: ["account_60"],
status: "covered" as const,
route: "hybrid_store_plus_live"
}
],
byFragment: new Map<string, string[]>([["F1", ["R1"]]])
};
const coverageEvaluation = {
requirements: requirementExtraction.requirements,
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
};
const groundingCheck = {
status: "grounded" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["why"],
selection_reason_summary: ["selection"]
};
const extractRequirements = vi.fn(() => requirementExtraction);
const evaluateCoverage = vi.fn(() => coverageEvaluation);
const checkGrounding = vi.fn(() => groundingCheck);
const output = runAssistantCoverageGroundingPipeline({
routeSummary: null,
normalized: null,
userMessage: "test",
retrievalResults: [],
extractRequirements,
evaluateCoverage,
checkGrounding
});
expect(extractRequirements).toHaveBeenCalledTimes(1);
expect(evaluateCoverage).toHaveBeenCalledTimes(1);
expect(checkGrounding).toHaveBeenCalledTimes(1);
expect(output.requirementExtraction).toBe(requirementExtraction);
expect(output.coverageEvaluation).toBe(coverageEvaluation);
expect(output.groundingCheckBase).toBe(groundingCheck);
});
it("reuses precomputed requirement extraction when provided", () => {
const precomputed = {
requirements: [],
byFragment: new Map<string, string[]>()
};
const extractRequirements = vi.fn(() => {
throw new Error("extractRequirements should not be called");
});
const evaluateCoverage = vi.fn(() => ({
requirements: [],
coverage: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
}));
const checkGrounding = vi.fn(() => ({
status: "no_grounded_answer" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
}));
const output = runAssistantCoverageGroundingPipeline({
routeSummary: null,
normalized: null,
userMessage: "test",
retrievalResults: [],
requirementExtraction: precomputed,
extractRequirements,
evaluateCoverage,
checkGrounding
});
expect(extractRequirements).not.toHaveBeenCalled();
expect(evaluateCoverage).toHaveBeenCalledWith(precomputed.requirements, []);
expect(output.requirementExtraction).toBe(precomputed);
});
});

View File

@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import {
buildDebugRoutesFromRoute,
buildExecutionPlanFromRoute,
buildFragmentTextById
} from "../src/services/assistantQueryPlanning";
describe("assistant query planning module", () => {
it("builds fragment text map with account hints enrichment", () => {
const map = buildFragmentTextById([
{
fragment_id: "F1",
raw_fragment_text: "проверить хвосты",
normalized_fragment_text: "",
account_hints: ["60.01", "60.02"]
},
{
fragment_id: "F2",
raw_fragment_text: "проверить 60.01 по поставщику",
normalized_fragment_text: "",
account_hints: ["60.01"]
}
]);
expect(map.get("F1")).toBe("проверить хвосты, по счету 60.01, 60.02");
expect(map.get("F2")).toBe("проверить 60.01 по поставщику");
});
it("builds deterministic execution plan from route summary", () => {
const executionPlan = buildExecutionPlanFromRoute({
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
route: "no_route",
no_route_reason: "insufficient_specificity",
clarification_reason: "missing anchor",
reason: "needs clarification"
},
{
fragment_id: "F2",
route: "hybrid_store_plus_live",
reason: "route selected"
}
],
fallback: {
type: "none",
message: null
}
} as any,
userMessage: "base question",
fragmentTextById: new Map([
["F1", "уточни период"],
["F2", "проверь по 60.01"]
]),
requirementByFragment: new Map([
["F1", ["R1"]],
["F2", ["R2"]]
])
});
expect(executionPlan).toHaveLength(2);
expect(executionPlan[0]).toMatchObject({
fragment_id: "F1",
route: "no_route",
should_execute: false,
no_route_reason: "insufficient_specificity",
clarification_reason: "missing anchor"
});
expect(executionPlan[1]).toMatchObject({
fragment_id: "F2",
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null
});
});
it("builds legacy debug routes via resolver", () => {
const routes = buildDebugRoutesFromRoute({
routeSummary: {
mode: "legacy_v1",
intent_class: "partner_reconciliation",
route_hint: "store_canonical",
confidence: "medium"
} as any,
resolveLegacyRouteReason: (route) => `legacy:${route}`
});
expect(routes).toHaveLength(1);
expect(routes[0]).toMatchObject({
fragment_id: "F1",
route: "store_canonical",
reason: "legacy:store_canonical",
confidence: "medium"
});
});
});

View File

@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import type { AssistantConversationItem, AssistantSessionState } from "../src/types/assistant";
import { commitAssistantTurnAndLog } from "../src/services/assistantTurnCommitRuntimeAdapter";
function buildAssistantItem(): AssistantConversationItem {
return {
message_id: "msg-1",
session_id: "asst-1",
role: "assistant",
text: "ok",
reply_type: "factual",
created_at: "2026-04-10T10:01:00.000Z",
trace_id: "trace-1",
debug: null
};
}
describe("assistant turn commit runtime adapter", () => {
it("appends item, persists existing session, clones conversation and logs event", () => {
const assistantItem = buildAssistantItem();
const storedSession: AssistantSessionState = {
session_id: "asst-1",
updated_at: "2026-04-10T10:01:00.000Z",
items: [assistantItem],
investigation_state: null
};
const calls = {
append: 0,
persist: 0,
log: 0
};
let loggedPayload: Record<string, unknown> | null = null;
const result = commitAssistantTurnAndLog({
sessionId: "asst-1",
assistantItem,
eventType: "assistant_message",
logDetails: { some: "details" },
appendItem: () => {
calls.append += 1;
},
getSession: () => storedSession,
persistSession: () => {
calls.persist += 1;
},
cloneConversation: (items) => items.map((item) => ({ ...item, debug: item.debug ? { ...item.debug } : null })),
logEvent: (payload) => {
calls.log += 1;
loggedPayload = payload as unknown as Record<string, unknown>;
},
nowIso: () => "2026-04-10T10:02:00.000Z"
});
expect(calls.append).toBe(1);
expect(calls.persist).toBe(1);
expect(calls.log).toBe(1);
expect(result.currentSession?.session_id).toBe("asst-1");
expect(result.conversation).toEqual([assistantItem]);
expect(result.conversation).not.toBe(storedSession.items);
expect(loggedPayload?.["sessionId"]).toBe("asst-1");
expect(loggedPayload?.["eventType"]).toBe("assistant_message");
expect(loggedPayload?.["message"]).toBe("assistant_message_processed");
expect(loggedPayload?.["timestamp"]).toBe("2026-04-10T10:02:00.000Z");
});
it("skips persist when session is missing and still logs with empty conversation", () => {
const assistantItem = buildAssistantItem();
let persistCalled = false;
let logCalled = false;
const result = commitAssistantTurnAndLog({
sessionId: "asst-missing",
assistantItem,
eventType: "assistant_message",
logDetails: { x: 1 },
appendItem: () => {},
getSession: () => null,
persistSession: () => {
persistCalled = true;
},
cloneConversation: (items) => items.map((item) => ({ ...item })),
logEvent: () => {
logCalled = true;
}
});
expect(persistCalled).toBe(false);
expect(logCalled).toBe(true);
expect(result.currentSession).toBeNull();
expect(result.conversation).toEqual([]);
});
});

View File

@ -11,7 +11,8 @@ const FLAG_KEYS = [
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1"
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
"FEATURE_ASSISTANT_ADDRESS_QUERY_V1"
] as const;
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
@ -623,6 +624,7 @@ describe("wave10 settlement corrective regression", () => {
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = "0";
vi.resetModules();
const { createApp } = await import("../src/server");

View File

@ -31,4 +31,9 @@ describe("questionTypeResolver", () => {
expect(resolveQuestionType("Почему не сходится 62.01/62.02?"))
.toBe("why_breaks");
});
it("keeps generic non-why questions as unknown", () => {
expect(resolveQuestionType("Какие реализации стоит проверить заранее, чтобы не испортить отчетность за месяц?"))
.toBe("unknown");
});
});

Some files were not shown because too many files have changed in this diff Show More