diff --git a/docs/orchestration/address_truth_harness_phase60_metadata_document_pivot_year_switch.json b/docs/orchestration/address_truth_harness_phase60_metadata_document_pivot_year_switch.json new file mode 100644 index 0000000..68bd40e --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase60_metadata_document_pivot_year_switch.json @@ -0,0 +1,94 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase60_metadata_document_pivot_year_switch", + "domain": "address_phase60_metadata_document_pivot_year_switch", + "title": "Phase 60 metadata document pivot year switch", + "description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval, pivots into document evidence, and then survives a short year-switch follow-up without losing organization or resetting the new document lane.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_metadata_ambiguity_surface", + "title": "Metadata ambiguity is surfaced honestly for VAT", + "question": "какие объекты 1С есть по НДС?", + "allowed_reply_types": ["partial_coverage", "factual_with_explanation"], + "required_answer_patterns_all": [ + "(?i)metadata|метадан", + "(?i)ндс", + "(?i)документ|регистр" + ], + "criticality": "critical", + "semantic_tags": ["metadata_surface", "mixed_ambiguity"] + }, + { + "step_id": "step_02_neutral_followup_requires_lane_choice", + "title": "Neutral follow-up still requires lane choice", + "question": "давай дальше", + "allowed_reply_types": ["clarification_required", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)документ", + "(?i)движени|регистр", + "(?i)уточн|выб(ери|рать)|какой контур" + ], + "criticality": "critical", + "semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"] + }, + { + "step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap", + "title": "Movement lane plus organization in one follow-up leaves only the period gap", + "question": "по движениям по ООО Альтернатива Плюс", + "allowed_reply_types": ["clarification_required", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)движени|регистр", + "(?i)период" + ], + "criticality": "critical", + "semantic_tags": ["movement_lane_after_clarification", "inline_organization_clarification"] + }, + { + "step_id": "step_04_period_clarification_executes_same_movement_loop", + "title": "Period clarification executes the same bounded movement loop", + "question": "за 2020 год", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк" + ], + "criticality": "critical", + "semantic_tags": ["movement_lane_execution", "bounded_retrieval"] + }, + { + "step_id": "step_05_document_pivot_keeps_same_scope", + "title": "Short document pivot reuses the same organization and period", + "question": "а теперь по документам?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .*организац", + "(?i)уточните .*период", + "(?i)уточните .*контур", + "(?i)не найден контрагент" + ], + "criticality": "critical", + "semantic_tags": ["document_pivot_after_retrieval", "scope_reuse"] + }, + { + "step_id": "step_06_year_switch_keeps_document_lane", + "title": "Short year-switch follow-up stays in the document lane after the pivot", + "question": "а теперь за 2021?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)2021", + "(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .*организац", + "(?i)уточните .*контур", + "(?i)движени|регистр", + "(?i)не найден контрагент" + ], + "criticality": "critical", + "semantic_tags": ["year_switch_followup", "document_lane_continuity", "post_pivot_continuity"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 3d64b86..9b1b2ab 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -475,6 +475,18 @@ function hasMetadataSignal(text) { function hasMetadataObjectHint(text) { return /(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|objects?|registers?|documents?|catalogs?|fields?)/iu.test(text); } +function hasSimpleDocumentLanePivotSignal(text) { + return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text); +} +function hasSimpleMovementLanePivotSignal(text) { + return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text); +} +function hasUtf8DocumentLanePivotSignal(text) { + return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text); +} +function hasUtf8MovementLanePivotSignal(text) { + return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text); +} function hasDocumentEvidenceFollowupSignal(text) { return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u0441\u0447(?:[еe]т|\u0435\u0442)[-\u2011 ]?\u0444\u0430\u043a\u0442\u0443\u0440(?:\u044b|\u0430)?|\u043d\u0430\u043a\u043b\u0430\u0434\u043d(?:\u044b\u0435|\u0430\u044f)?|\u0430\u043a\u0442(?:\u044b)?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|invoice(?:s)?|bill(?:s)?|waybill(?:s)?)|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text); } @@ -742,13 +754,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? resolveEntityResolutionAmbiguityChoice(rawEntitySourceText, followupSeed.entityResolutionAmbiguityCandidates) : null; const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate); - const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText); - const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText) || hasPronounMovementEvidenceFollowupSignal(rawText); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate); + const currentTurnDocumentLaneSignal = rawAction === "list_documents"; + const currentTurnMovementLaneSignal = rawAction === "list_movements"; + const metadataDocumentHintSignal = currentTurnDocumentLaneSignal || + hasUtf8DocumentLanePivotSignal(rawText) || + hasSimpleDocumentLanePivotSignal(rawText) || + hasDocumentEvidenceFollowupSignal(rawText) || + hasPronounDocumentEvidenceFollowupSignal(rawText); + const metadataMovementHintSignal = currentTurnMovementLaneSignal || + hasUtf8MovementLanePivotSignal(rawText) || + hasSimpleMovementLanePivotSignal(rawText) || + hasMovementEvidenceFollowupSignal(rawText) || + hasPronounMovementEvidenceFollowupSignal(rawText); const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope); const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope); const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); @@ -894,6 +916,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !rawValueFlowSignal && !rawMetadataSignal && metadataDocumentHintSignal); + const metadataScopedMovementEvidenceToDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_movement_evidence_query_movements_v1" && + followupSeed.subjectResolutionOptional && + !followupSeed.counterparty && + metadataLaneCarryoverAvailable && + !rawLifecycleSignal && + !rawValueFlowSignal && + metadataDocumentHintSignal); + const metadataScopedDocumentEvidenceToMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" && + followupSeed.subjectResolutionOptional && + !followupSeed.counterparty && + metadataLaneCarryoverAvailable && + !rawLifecycleSignal && + !rawValueFlowSignal && + metadataMovementHintSignal); const metadataGroundedLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && (followupSeed.metadataRouteFamily === "document_evidence" || followupSeed.metadataRouteFamily === "movement_evidence") && @@ -952,13 +988,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) { metadataLaneCarryoverAvailable && !rawLifecycleSignal && !rawValueFlowSignal && - !rawMetadataSignal && - followupSeed.domain === "documents" && - followupSeed.action === "list_documents") || + !currentTurnMovementLaneSignal && + (!rawMetadataSignal || currentTurnDocumentLaneSignal) && + (currentTurnDocumentLaneSignal || + (followupSeed.domain === "documents" && followupSeed.action === "list_documents"))) || entityResolutionGroundedDocumentFollowupApplicable || entityResolutionClarifiedDocumentFollowupApplicable || valueFlowGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable || + metadataScopedMovementEvidenceToDocumentFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || @@ -968,13 +1006,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) { metadataLaneCarryoverAvailable && !rawLifecycleSignal && !rawValueFlowSignal && - !rawMetadataSignal && - followupSeed.domain === "movements" && - followupSeed.action === "list_movements") || + !currentTurnDocumentLaneSignal && + (!rawMetadataSignal || currentTurnMovementLaneSignal) && + (currentTurnMovementLaneSignal || + (followupSeed.domain === "movements" && followupSeed.action === "list_movements"))) || entityResolutionGroundedMovementFollowupApplicable || entityResolutionClarifiedMovementFollowupApplicable || valueFlowGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable || + metadataScopedDocumentEvidenceToMovementFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || metadataAmbiguityCollapsedMovementLaneContinuationApplicable; const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && @@ -1379,6 +1419,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (movementEvidenceGroundedDocumentFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_movement_evidence_grounded_document_followup"); } + if (metadataScopedMovementEvidenceToDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_scoped_movement_to_document_followup"); + } + if (metadataScopedDocumentEvidenceToMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_scoped_document_to_movement_followup"); + } if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index bb8da2e..cbf3f86 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -678,6 +678,22 @@ function hasMetadataObjectHint(text: string): boolean { ); } +function hasSimpleDocumentLanePivotSignal(text: string): boolean { + return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text); +} + +function hasSimpleMovementLanePivotSignal(text: string): boolean { + return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text); +} + +function hasUtf8DocumentLanePivotSignal(text: string): boolean { + return /(?:^|\s)по\s+документ(?:ам|ы)?(?:\?|$|\s)/iu.test(text); +} + +function hasUtf8MovementLanePivotSignal(text: string): boolean { + return /(?:^|\s)по\s+движени(?:ям|я)?(?:\?|$|\s)/iu.test(text); +} + function hasDocumentEvidenceFollowupSignal(text: string): boolean { return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043a\u0430\u043a\u0438\u0435|\u0441\u043f\u0438\u0441\u043e\u043a|\u0434\u0430\u0439|\u0438\u0449\u0438)\s+(?:\u0441\u0447(?:[еe]т|\u0435\u0442)[-\u2011 ]?\u0444\u0430\u043a\u0442\u0443\u0440(?:\u044b|\u0430)?|\u043d\u0430\u043a\u043b\u0430\u0434\u043d(?:\u044b\u0435|\u0430\u044f)?|\u0430\u043a\u0442(?:\u044b)?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u0438|\u0438\u044e)|invoice(?:s)?|bill(?:s)?|waybill(?:s)?)|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test( text @@ -1020,16 +1036,25 @@ export function buildAssistantMcpDiscoveryTurnInput( : null; const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate); - const metadataDocumentHintSignal = - hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText); - const metadataMovementHintSignal = - hasMovementEvidenceFollowupSignal(rawText) || hasPronounMovementEvidenceFollowupSignal(rawText); - const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate); + const currentTurnDocumentLaneSignal = rawAction === "list_documents"; + const currentTurnMovementLaneSignal = rawAction === "list_movements"; + const metadataDocumentHintSignal = + currentTurnDocumentLaneSignal || + hasUtf8DocumentLanePivotSignal(rawText) || + hasSimpleDocumentLanePivotSignal(rawText) || + hasDocumentEvidenceFollowupSignal(rawText) || + hasPronounDocumentEvidenceFollowupSignal(rawText); + const metadataMovementHintSignal = + currentTurnMovementLaneSignal || + hasUtf8MovementLanePivotSignal(rawText) || + hasSimpleMovementLanePivotSignal(rawText) || + hasMovementEvidenceFollowupSignal(rawText) || + hasPronounMovementEvidenceFollowupSignal(rawText); const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope); const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope); const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); @@ -1221,6 +1246,24 @@ export function buildAssistantMcpDiscoveryTurnInput( !rawMetadataSignal && metadataDocumentHintSignal ); + const metadataScopedMovementEvidenceToDocumentFollowupApplicable = Boolean( + followupSeed.pilotScope === "counterparty_movement_evidence_query_movements_v1" && + followupSeed.subjectResolutionOptional && + !followupSeed.counterparty && + metadataLaneCarryoverAvailable && + !rawLifecycleSignal && + !rawValueFlowSignal && + metadataDocumentHintSignal + ); + const metadataScopedDocumentEvidenceToMovementFollowupApplicable = Boolean( + followupSeed.pilotScope === "counterparty_document_evidence_query_documents_v1" && + followupSeed.subjectResolutionOptional && + !followupSeed.counterparty && + metadataLaneCarryoverAvailable && + !rawLifecycleSignal && + !rawValueFlowSignal && + metadataMovementHintSignal + ); const metadataGroundedLaneContinuationApplicable = Boolean( followupSeed.pilotScope === "metadata_inspection_v1" && (followupSeed.metadataRouteFamily === "document_evidence" || @@ -1290,13 +1333,15 @@ export function buildAssistantMcpDiscoveryTurnInput( metadataLaneCarryoverAvailable && !rawLifecycleSignal && !rawValueFlowSignal && - !rawMetadataSignal && - followupSeed.domain === "documents" && - followupSeed.action === "list_documents") || + !currentTurnMovementLaneSignal && + (!rawMetadataSignal || currentTurnDocumentLaneSignal) && + (currentTurnDocumentLaneSignal || + (followupSeed.domain === "documents" && followupSeed.action === "list_documents"))) || entityResolutionGroundedDocumentFollowupApplicable || entityResolutionClarifiedDocumentFollowupApplicable || valueFlowGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable || + metadataScopedMovementEvidenceToDocumentFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || metadataAmbiguityCollapsedDocumentLaneContinuationApplicable; const metadataGroundedMovementLaneApplicable = @@ -1307,13 +1352,15 @@ export function buildAssistantMcpDiscoveryTurnInput( metadataLaneCarryoverAvailable && !rawLifecycleSignal && !rawValueFlowSignal && - !rawMetadataSignal && - followupSeed.domain === "movements" && - followupSeed.action === "list_movements") || + !currentTurnDocumentLaneSignal && + (!rawMetadataSignal || currentTurnMovementLaneSignal) && + (currentTurnMovementLaneSignal || + (followupSeed.domain === "movements" && followupSeed.action === "list_movements"))) || entityResolutionGroundedMovementFollowupApplicable || entityResolutionClarifiedMovementFollowupApplicable || valueFlowGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable || + metadataScopedDocumentEvidenceToMovementFollowupApplicable || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || metadataAmbiguityCollapsedMovementLaneContinuationApplicable; const effectiveMetadataFollowupSeedApplicable = @@ -1761,6 +1808,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (movementEvidenceGroundedDocumentFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_movement_evidence_grounded_document_followup"); } + if (metadataScopedMovementEvidenceToDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_scoped_movement_to_document_followup"); + } + if (metadataScopedDocumentEvidenceToMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_scoped_document_to_movement_followup"); + } if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 174b8dc..78f363f 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -2017,4 +2017,90 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]); }); + + it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => { + const orgName = + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c?", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "list_documents", + explicit_intent_candidate: "list_documents_by_counterparty" + }, + followupContext: { + previous_discovery_pilot_scope: "counterparty_movement_evidence_query_movements_v1", + previous_discovery_loop_status: "ready_for_next_hop", + previous_discovery_loop_selected_chain_id: "movement_evidence", + previous_discovery_loop_pending_axes: [], + previous_discovery_loop_asked_domain_family: "movements", + previous_discovery_loop_asked_action_family: "list_movements", + previous_discovery_loop_unsupported_family: "movement_evidence", + previous_discovery_loop_metadata_scope_hint: "\u041d\u0414\u0421", + previous_discovery_loop_subject_resolution_optional: true, + previous_discovery_entity_candidates: ["\u041d\u0414\u0421"], + previous_filters: { + organization: orgName, + period_from: "2020-01-01", + period_to: "2020-12-31" + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("document evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "documents", + asked_action_family: "list_documents", + metadata_scope_hint: "\u041d\u0414\u0421", + subject_resolution_optional: true, + explicit_organization_scope: orgName, + explicit_date_scope: "2020", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.reason_codes).toContain("mcp_discovery_metadata_scoped_movement_to_document_followup"); + }); + + it("keeps the metadata-scoped document lane after a year-switch follow-up", () => { + const orgName = + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0437\u0430 2021?", + followupContext: { + previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1", + previous_discovery_loop_status: "ready_for_next_hop", + previous_discovery_loop_selected_chain_id: "document_evidence", + previous_discovery_loop_pending_axes: [], + previous_discovery_loop_asked_domain_family: "documents", + previous_discovery_loop_asked_action_family: "list_documents", + previous_discovery_loop_unsupported_family: "document_evidence", + previous_discovery_loop_metadata_scope_hint: "\u041d\u0414\u0421", + previous_discovery_loop_subject_resolution_optional: true, + previous_discovery_entity_candidates: ["\u041d\u0414\u0421"], + previous_filters: { + organization: orgName, + period_from: "2020-01-01", + period_to: "2020-12-31" + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("document evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "documents", + asked_action_family: "list_documents", + metadata_scope_hint: "\u041d\u0414\u0421", + subject_resolution_optional: true, + explicit_organization_scope: orgName, + explicit_date_scope: "2021", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + }); });