diff --git a/docs/orchestration/address_truth_harness_phase57_metadata_movement_all_time_after_retrieval.json b/docs/orchestration/address_truth_harness_phase57_metadata_movement_all_time_after_retrieval.json new file mode 100644 index 0000000..883aa06 --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase57_metadata_movement_all_time_after_retrieval.json @@ -0,0 +1,78 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase57_metadata_movement_all_time_after_retrieval", + "domain": "address_phase57_metadata_movement_all_time_after_retrieval", + "title": "Phase 57 metadata movement all-time follow-up after bounded retrieval", + "description": "Targeted AGENT replay for Big Block F where a metadata-born movement loop reaches bounded retrieval and then survives a short all-time follow-up without losing organization or resetting the movement 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_all_time_followup_keeps_same_movement_loop", + "title": "All-time follow-up clears the previous period but keeps the same movement lane and organization", + "question": "а теперь за все время?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)все время|доступное время|весь период", + "(?i)ндс|движени|регистр|операц|платеж|поступлен|списан|строк" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .*организац", + "(?i)уточните .*контур", + "(?i)за 2020", + "(?i)документ", + "(?i)не найден контрагент" + ], + "criticality": "critical", + "semantic_tags": ["all_time_followup", "movement_lane_continuity", "period_cleared"] + } + ] +} diff --git a/docs/orchestration/address_truth_harness_phase58_metadata_document_to_movement_pivot.json b/docs/orchestration/address_truth_harness_phase58_metadata_document_to_movement_pivot.json new file mode 100644 index 0000000..b83d37a --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase58_metadata_document_to_movement_pivot.json @@ -0,0 +1,76 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase58_metadata_document_to_movement_pivot", + "domain": "address_phase58_metadata_document_to_movement_pivot", + "title": "Phase 58 metadata document retrieval to movement pivot", + "description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval and then pivots into movement evidence on a short follow-up without losing organization or resetting the scoped proof path.", + "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_document_lane_with_org_keeps_only_period_gap", + "title": "Document 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": ["document_lane_after_clarification", "inline_organization_clarification"] + }, + { + "step_id": "step_04_period_clarification_executes_same_document_loop", + "title": "Period clarification executes the same bounded document loop", + "question": "за 2020 год", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк" + ], + "criticality": "critical", + "semantic_tags": ["document_lane_execution", "bounded_retrieval"] + }, + { + "step_id": "step_05_movement_pivot_keeps_same_scope", + "title": "Short movement 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": ["movement_pivot_after_document_retrieval", "scope_reuse", "same_proof_path_family_shift"] + } + ] +} diff --git a/docs/orchestration/address_truth_harness_phase59_metadata_document_all_time_after_retrieval.json b/docs/orchestration/address_truth_harness_phase59_metadata_document_all_time_after_retrieval.json new file mode 100644 index 0000000..c28c95c --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase59_metadata_document_all_time_after_retrieval.json @@ -0,0 +1,78 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase59_metadata_document_all_time_after_retrieval", + "domain": "address_phase59_metadata_document_all_time_after_retrieval", + "title": "Phase 59 metadata document all-time follow-up after bounded retrieval", + "description": "Targeted AGENT replay for Big Block F where a metadata-born document loop reaches bounded retrieval and then survives a short all-time follow-up without losing organization or resetting the 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_document_choice_with_org_keeps_only_period_gap", + "title": "Document 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": ["document_lane_after_clarification", "inline_organization_clarification"] + }, + { + "step_id": "step_04_period_clarification_executes_same_document_loop", + "title": "Period clarification executes the same bounded document loop", + "question": "за 2020 год", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк" + ], + "criticality": "critical", + "semantic_tags": ["document_lane_execution", "bounded_retrieval"] + }, + { + "step_id": "step_05_all_time_followup_keeps_same_document_loop", + "title": "All-time follow-up clears the previous period but keeps the same document lane and organization", + "question": "а теперь за все время?", + "allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)все время|доступное время|весь период", + "(?i)документ|счет|сч[её]т[- ]?фактур|накладн|акт|строк" + ], + "forbidden_answer_patterns": [ + "(?i)уточните .*организац", + "(?i)уточните .*контур", + "(?i)за 2020", + "(?i)движени|регистр", + "(?i)не найден контрагент" + ], + "criticality": "critical", + "semantic_tags": ["all_time_followup", "document_lane_continuity", "period_cleared"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index 6df44a0..18a9aab 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -148,11 +148,20 @@ function explicitOrganizationScope(pilot) { const normalized = value.trim(); return normalized.length > 0 ? normalized : null; } +function hasAllTimeScope(pilot) { + return (dryRunHasAxis(pilot, "all_time_scope") || + pilot.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") || + pilot.dry_run.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected")); +} function documentOrMovementScopeRu(pilot) { const entity = firstEntityCandidate(pilot); const period = explicitDateScope(pilot); const entityPart = entity ? ` по контрагенту ${entity}` : ""; - const periodPart = period ? ` за ${period}` : " в проверенном окне"; + const periodPart = period + ? ` за ${period}` + : hasAllTimeScope(pilot) + ? " за все доступное время" + : " в проверенном окне"; return `${entityPart}${periodPart}`; } function isMovementLaneClarification(pilot) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 0de6b84..b6d933e 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -200,11 +200,23 @@ function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionCon return normalized.length > 0 ? normalized : null; } +function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { + return ( + dryRunHasAxis(pilot, "all_time_scope") || + pilot.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") || + pilot.dry_run.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") + ); +} + function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string { const entity = firstEntityCandidate(pilot); const period = explicitDateScope(pilot); const entityPart = entity ? ` по контрагенту ${entity}` : ""; - const periodPart = period ? ` за ${period}` : " в проверенном окне"; + const periodPart = period + ? ` за ${period}` + : hasAllTimeScope(pilot) + ? " за все доступное время" + : " в проверенном окне"; return `${entityPart}${periodPart}`; } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index ffe494d..27a0dc0 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -170,6 +170,51 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe."); }); + it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => { + const planner = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: [], + business_fact_family: "movement_evidence", + action_family: "list_movements", + aggregation_need: null, + time_scope_need: "all_time_scope", + comparison_need: null, + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: ["collect_scoped_movements", "probe_coverage"], + forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], + reason_codes: ["data_need_graph_built", "data_need_graph_all_time_scope_hint"] + }, + turnMeaning: { + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: [], + explicit_organization_scope: "ООО Альтернатива Плюс", + unsupported_but_understood_family: "movement_evidence" + } + }); + const pilot = await executeAssistantMcpDiscoveryPilot( + planner, + buildCustomQueryDeps({ + fetched_rows: 100, + matched_rows: 0, + rows: [], + raw_rows: [{ Period: "2020-06-30T00:00:00", Organization: "ООО Альтернатива Плюс", Registrar: "Move1" }] + }) + ); + + const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); + + expect(draft.answer_mode).toBe("bounded_inference_only"); + expect(draft.headline).toContain("движени"); + expect(draft.headline).toContain("все доступное время"); + expect(draft.headline).not.toContain("за 2020"); + expect(draft.inference_lines.join("\n")).not.toContain("за 2020"); + }); + it("keeps bounded-only movement answers tied to the resolved entity and checked period", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: {