From 4c00d8c854732fcde17ddb93b0923cebcc7952b6 Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 1 May 2026 22:38:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20MCP=20planner=20=D1=81=20value-flow?= =?UTF-8?q?=20=D1=86=D0=B5=D0=BF=D0=BE=D1=87=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/assistantMcpCatalogIndex.js | 4 +- .../assistantMcpDiscoveryPilotExecutor.js | 7 ++ .../services/assistantMcpDiscoveryPlanner.js | 52 ++++++++++++-- .../assistantMcpDiscoveryResponsePolicy.js | 29 ++++++++ .../src/services/assistantMcpCatalogIndex.ts | 6 +- .../assistantMcpDiscoveryPilotExecutor.ts | 7 ++ .../services/assistantMcpDiscoveryPlanner.ts | 61 +++++++++++++++-- .../assistantMcpDiscoveryResponsePolicy.ts | 36 ++++++++++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 31 +++++++++ .../assistantMcpDiscoveryPlanner.test.ts | 68 +++++++++++++++++++ ...ssistantMcpDiscoveryResponsePolicy.test.ts | 37 ++++++++++ 11 files changed, 316 insertions(+), 22 deletions(-) diff --git a/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js b/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js index 7725cb0..35c9a61 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js +++ b/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js @@ -622,9 +622,7 @@ function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(input) { continue; } for (const contract of PRIMITIVE_CONTRACTS) { - if (contract.primitive_id === "aggregate_by_axis" && - normalizedCandidate === "aggregate_by_month" && - !allowAggregateByAxis) { + if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) { continue; } if (!contract.decomposition_hints.includes(normalizedCandidate)) { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index c5006c2..88d4055 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -97,6 +97,13 @@ function dateScopeToFilters(dateScope) { period_to: `${yearMatch[1]}-12-31` }; } + const rangeMatch = dateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/); + if (rangeMatch) { + return { + period_from: rangeMatch[1], + period_to: rangeMatch[2] + }; + } const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/); if (dateMatch) { const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 24ebdc5..6517fdb 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -217,6 +217,26 @@ function preferredPrimitiveFromExplicitDataNeedGraph(graph) { } return null; } +function catalogTemplateIdFromChainId(chainId) { + if (chainId === "metadata_lane_clarification") { + return null; + } + return chainId; +} +function promoteConfirmedMetadataSurfaceChainTemplate(input) { + const surfaceRouteFamily = routeFamilyFromMetadataSurfaceRef(input.metadataSurface); + const selectedChainId = input.selectedChainId ?? null; + if (!surfaceRouteFamily || !selectedChainId || selectedChainId !== surfaceRouteFamily) { + return input.matches; + } + if (input.matches[0] === selectedChainId || !input.matches.includes(selectedChainId)) { + return input.matches; + } + return [ + selectedChainId, + ...input.matches.filter((chainId) => chainId !== selectedChainId) + ]; +} function hasCarriedMetadataSurfaceScoringEvidence(surface) { return Boolean(surface && (toNonEmptyString(surface.selected_entity_set) || @@ -298,7 +318,7 @@ function selectPrimitivesFromGraphAndCatalog(input) { if (factAxisPrimitives.length > 0) { reasonCodes.push("planner_selected_catalog_primitives_from_fact_axis_search"); } - const chainTemplateMatches = input.dataNeedGraph + const rawChainTemplateMatches = input.dataNeedGraph ? (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogChainTemplatesByFactAxis)({ business_fact_family: input.dataNeedGraph.business_fact_family, action_family: input.actionFamily ?? input.dataNeedGraph.action_family, @@ -308,8 +328,16 @@ function selectPrimitivesFromGraphAndCatalog(input) { aggregation_need: input.dataNeedGraph.aggregation_need }) : []; + const chainTemplateMatches = promoteConfirmedMetadataSurfaceChainTemplate({ + matches: rawChainTemplateMatches, + metadataSurface: input.metadataSurface, + selectedChainId: input.selectedChainId + }); if (chainTemplateMatches.length > 0) { reasonCodes.push("planner_scored_catalog_chain_templates_from_fact_axis"); + if (rawChainTemplateMatches[0] !== chainTemplateMatches[0]) { + reasonCodes.push("planner_catalog_chain_template_promoted_by_confirmed_metadata_surface"); + } reasonCodes.push(`planner_catalog_chain_template_search_top_${chainTemplateMatches[0]}`); } const combinedCatalogPrimitives = []; @@ -353,8 +381,10 @@ function selectPrimitivesFromGraphAndCatalog(input) { function budgetOverrideFor(input, recipe) { const meaning = input.turnMeaning ?? null; const requestedAggregationAxis = aggregationAxis(meaning); - const isValueFlowRecipe = recipe.semanticDataNeed === "counterparty value-flow evidence" && - recipe.primitives.includes("query_movements"); + const isValueFlowRecipe = recipe.primitives.includes("query_movements") && + (recipe.semanticDataNeed === "counterparty value-flow evidence" || + recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" || + recipe.semanticDataNeed === "ranked value-flow evidence"); if (!isValueFlowRecipe) { return {}; } @@ -370,7 +400,7 @@ function catalogChainTemplateMatchesForContract(input, recipe) { if (!dataNeedGraph) { return []; } - return (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogChainTemplatesByFactAxis)({ + const matches = (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogChainTemplatesByFactAxis)({ business_fact_family: dataNeedGraph.business_fact_family, action_family: toNonEmptyString(input.turnMeaning?.asked_action_family) ?? dataNeedGraph.action_family, required_axes: recipe.axes, @@ -378,6 +408,11 @@ function catalogChainTemplateMatchesForContract(input, recipe) { ranking_need: dataNeedGraph.ranking_need, aggregation_need: dataNeedGraph.aggregation_need }); + return promoteConfirmedMetadataSurfaceChainTemplate({ + matches, + metadataSurface: input.metadataSurface, + selectedChainId: catalogTemplateIdFromChainId(recipe.chainId) + }); } function catalogChainTemplateAlignmentForContract(recipe, matches) { const selectedChainIsCatalogTemplate = recipe.chainId !== "metadata_lane_clarification"; @@ -472,7 +507,8 @@ function recipeFor(input) { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "document_evidence" }); return recipeFromCatalogChainTemplate({ chainId: "document_evidence", @@ -496,7 +532,8 @@ function recipeFor(input) { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "movement_evidence" }); return recipeFromCatalogChainTemplate({ chainId: "movement_evidence", @@ -520,7 +557,8 @@ function recipeFor(input) { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "catalog_drilldown" }); return recipeFromCatalogChainTemplate({ chainId: "catalog_drilldown", diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js index 964115b..b56923d 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js @@ -202,6 +202,28 @@ function readStringArray(value) { ? value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item)) : []; } +function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) { + if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { + return false; + } + if (!hasEffectivelyFactualAddressReply(input)) { + return false; + } + const turnMeaning = readDiscoveryTurnMeaning(entryPoint); + const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family); + const askedAction = toNonEmptyString(turnMeaning?.asked_action_family); + if (askedDomain !== "counterparty_value") { + return false; + } + const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); + if (askedAction === "payout") { + return detectedIntent !== "supplier_payouts_profile"; + } + if (askedAction === "net_value_flow") { + return true; + } + return false; +} function hasExactMatchedFactualAddressReply(input, entryPoint) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { return false; @@ -212,6 +234,9 @@ function hasExactMatchedFactualAddressReply(input, entryPoint) { if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) { return false; } + if (hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint)) { + return false; + } const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status); const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode); const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe); @@ -378,6 +403,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint); const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); + const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint); if (!entryPoint) { pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); } @@ -399,6 +425,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { if (semanticConflictWithDiscoveryTurnMeaning) { pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override"); } + if (valueFlowActionConflictWithDiscoveryTurnMeaning) { + pushReason(reasonCodes, "mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override"); + } if (openScopeValueFlowDiscoveryPriority) { pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts b/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts index 637410a..0b010b3 100644 --- a/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts +++ b/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts @@ -766,11 +766,7 @@ export function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates( } for (const contract of PRIMITIVE_CONTRACTS) { - if ( - contract.primitive_id === "aggregate_by_axis" && - normalizedCandidate === "aggregate_by_month" && - !allowAggregateByAxis - ) { + if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) { continue; } if (!contract.decomposition_hints.includes(normalizedCandidate)) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index 6f32b35..a92f7ef 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -326,6 +326,13 @@ function dateScopeToFilters(dateScope: string | null): Pick chainId !== selectedChainId) + ]; +} + function hasCarriedMetadataSurfaceScoringEvidence( surface: AssistantMcpDiscoveryMetadataSurfaceRef | null | undefined ): boolean { @@ -456,6 +484,7 @@ function selectPrimitivesFromGraphAndCatalog(input: { metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null; actionFamily?: string | null; allowAggregateByAxis?: boolean; + selectedChainId?: AssistantMcpCatalogChainTemplateId | null; }): { primitives: AssistantMcpDiscoveryPrimitive[]; reasonCodes: string[] } { const reasonCodes: string[] = []; const decompositionCandidates = input.dataNeedGraph?.decomposition_candidates ?? []; @@ -500,7 +529,7 @@ function selectPrimitivesFromGraphAndCatalog(input: { reasonCodes.push("planner_selected_catalog_primitives_from_fact_axis_search"); } - const chainTemplateMatches = input.dataNeedGraph + const rawChainTemplateMatches = input.dataNeedGraph ? searchAssistantMcpCatalogChainTemplatesByFactAxis({ business_fact_family: input.dataNeedGraph.business_fact_family, action_family: input.actionFamily ?? input.dataNeedGraph.action_family, @@ -510,8 +539,16 @@ function selectPrimitivesFromGraphAndCatalog(input: { aggregation_need: input.dataNeedGraph.aggregation_need }) : []; + const chainTemplateMatches = promoteConfirmedMetadataSurfaceChainTemplate({ + matches: rawChainTemplateMatches, + metadataSurface: input.metadataSurface, + selectedChainId: input.selectedChainId + }); if (chainTemplateMatches.length > 0) { reasonCodes.push("planner_scored_catalog_chain_templates_from_fact_axis"); + if (rawChainTemplateMatches[0] !== chainTemplateMatches[0]) { + reasonCodes.push("planner_catalog_chain_template_promoted_by_confirmed_metadata_surface"); + } reasonCodes.push(`planner_catalog_chain_template_search_top_${chainTemplateMatches[0]}`); } @@ -565,8 +602,10 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla const meaning = input.turnMeaning ?? null; const requestedAggregationAxis = aggregationAxis(meaning); const isValueFlowRecipe = - recipe.semanticDataNeed === "counterparty value-flow evidence" && - recipe.primitives.includes("query_movements"); + recipe.primitives.includes("query_movements") && + (recipe.semanticDataNeed === "counterparty value-flow evidence" || + recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" || + recipe.semanticDataNeed === "ranked value-flow evidence"); if (!isValueFlowRecipe) { return {}; } @@ -586,7 +625,7 @@ function catalogChainTemplateMatchesForContract( if (!dataNeedGraph) { return []; } - return searchAssistantMcpCatalogChainTemplatesByFactAxis({ + const matches = searchAssistantMcpCatalogChainTemplatesByFactAxis({ business_fact_family: dataNeedGraph.business_fact_family, action_family: toNonEmptyString(input.turnMeaning?.asked_action_family) ?? dataNeedGraph.action_family, required_axes: recipe.axes, @@ -594,6 +633,11 @@ function catalogChainTemplateMatchesForContract( ranking_need: dataNeedGraph.ranking_need, aggregation_need: dataNeedGraph.aggregation_need }); + return promoteConfirmedMetadataSurfaceChainTemplate({ + matches, + metadataSurface: input.metadataSurface, + selectedChainId: catalogTemplateIdFromChainId(recipe.chainId) + }); } function catalogChainTemplateAlignmentForContract( @@ -700,7 +744,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "document_evidence" }); return recipeFromCatalogChainTemplate({ chainId: "document_evidence", @@ -725,7 +770,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "movement_evidence" }); return recipeFromCatalogChainTemplate({ chainId: "movement_evidence", @@ -750,7 +796,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { fallbackPrimitives: template.fallback_primitives, requiredAxes: axes, metadataSurface: input.metadataSurface, - actionFamily: action + actionFamily: action, + selectedChainId: "catalog_drilldown" }); return recipeFromCatalogChainTemplate({ chainId: "catalog_drilldown", diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts index c4a4df3..6ccdbc2 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts @@ -298,6 +298,32 @@ function readStringArray(value: unknown): string[] { : []; } +function hasValueFlowActionConflictWithDiscoveryTurnMeaning( + input: ApplyAssistantMcpDiscoveryResponsePolicyInput, + entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null +): boolean { + if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { + return false; + } + if (!hasEffectivelyFactualAddressReply(input)) { + return false; + } + const turnMeaning = readDiscoveryTurnMeaning(entryPoint); + const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family); + const askedAction = toNonEmptyString(turnMeaning?.asked_action_family); + if (askedDomain !== "counterparty_value") { + return false; + } + const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); + if (askedAction === "payout") { + return detectedIntent !== "supplier_payouts_profile"; + } + if (askedAction === "net_value_flow") { + return true; + } + return false; +} + function hasExactMatchedFactualAddressReply( input: ApplyAssistantMcpDiscoveryResponsePolicyInput, entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null @@ -311,6 +337,9 @@ function hasExactMatchedFactualAddressReply( if (hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint)) { return false; } + if (hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint)) { + return false; + } const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status); const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode); const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe); @@ -522,6 +551,10 @@ export function applyAssistantMcpDiscoveryResponsePolicy( const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint); const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); + const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning( + input, + entryPoint + ); if (!entryPoint) { pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); @@ -544,6 +577,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy( if (semanticConflictWithDiscoveryTurnMeaning) { pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override"); } + if (valueFlowActionConflictWithDiscoveryTurnMeaning) { + pushReason(reasonCodes, "mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override"); + } if (openScopeValueFlowDiscoveryPriority) { pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index 761b36d..be343aa 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -829,6 +829,37 @@ describe("assistant MCP discovery pilot executor", () => { expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2); }); + it("preserves explicit date ranges when building bidirectional value-flow probes", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "net_value_flow", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020-01-01..2020-12-31", + unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" + } + }); + const deps = buildSequentialDeps([ + { + rows: [{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" }] + }, + { + rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Counterparty: "SVK" }] + } + ]); + + const result = await executeAssistantMcpDiscoveryPilot(planner, deps); + + expect(result.pilot_status).toBe("executed"); + expect(result.derived_bidirectional_value_flow?.period_scope).toBe("2020-01-01..2020-12-31"); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2); + for (const call of deps.executeAddressMcpQuery.mock.calls) { + const query = String(call[0]?.query ?? ""); + expect(query).toContain("2020, 1, 1"); + expect(query).toContain("2020, 12, 31"); + } + }); + it("derives monthly bidirectional value-flow breakdown when the turn explicitly asks by month", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index ea09638..ad2c6c3 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -209,6 +209,48 @@ describe("assistant MCP discovery planner", () => { } }); + it("keeps bidirectional value-flow comparison executable when checked totals are derived without aggregate_by_axis", () => { + const result = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: ["SVK"], + business_fact_family: "value_flow", + action_family: "net_value_flow", + aggregation_need: null, + time_scope_need: "explicit_period", + comparison_need: "incoming_vs_outgoing", + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: [ + "resolve_entity_reference", + "collect_incoming_movements", + "collect_outgoing_movements", + "aggregate_checked_amounts", + "probe_coverage" + ], + forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], + reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"] + }, + turnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "net_value_flow", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.selected_chain_id).toBe("value_flow_comparison"); + expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]); + expect(result.proposed_primitives).not.toContain("aggregate_by_axis"); + expect(result.required_axes).toEqual(["counterparty", "period", "amount", "coverage_target"]); + expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30); + expect(result.catalog_review.review_status).toBe("catalog_compatible"); + expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"); + }); + it("keeps a value-flow plan in clarification state when period axis is missing", () => { const result = planAssistantMcpDiscovery({ turnMeaning: { @@ -537,6 +579,23 @@ describe("assistant MCP discovery planner", () => { it("can select catalog drilldown directly from a confirmed catalog metadata surface when the follow-up itself is thin", () => { const result = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: ["counterparty"], + metadata_scope_hint: "counterparty", + business_fact_family: "schema_surface", + action_family: "inspect_catalog", + aggregation_need: null, + time_scope_need: null, + comparison_need: null, + ranking_need: null, + proof_expectation: "schema_surface", + clarification_gaps: [], + decomposition_candidates: ["inspect_metadata_surface"], + forbidden_overclaim_flags: ["no_raw_model_claims", "no_fake_schema_surface"], + reason_codes: ["data_need_graph_built", "data_need_graph_family_schema_surface"] + }, metadataSurface: { selected_entity_set: "Catalog", selected_surface_objects: ["Catalog.Counterparties"], @@ -557,8 +616,17 @@ describe("assistant MCP discovery planner", () => { expect(result.selected_chain_id).toBe("catalog_drilldown"); expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]); expect(result.required_axes).toEqual(["metadata_scope"]); + expect(result.catalog_chain_template_matches[0]).toBe("catalog_drilldown"); + expect(result.catalog_chain_template_alignment).toMatchObject({ + alignment_status: "selected_matches_top", + top_chain_template_match: "catalog_drilldown", + selected_chain_template_rank: 1, + selected_chain_matches_top: true + }); expect(result.reason_codes).toContain("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref"); expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search"); + expect(result.reason_codes).toContain("planner_catalog_chain_template_promoted_by_confirmed_metadata_surface"); + expect(result.reason_codes).toContain("planner_catalog_chain_template_search_top_catalog_drilldown"); }); it("scores an explicit document data-need over an ambiguous metadata surface without carrying movement primitives", () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts index 1f80eda..93f688a 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponsePolicy.test.ts @@ -135,6 +135,43 @@ describe("assistant MCP discovery response policy", () => { expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate"); }); + it("overrides exact inbound value-flow replies when the discovery turn meaning asks for payouts", () => { + const result = applyAssistantMcpDiscoveryResponsePolicy({ + currentReply: "Incoming turnover by SVK: 12 224 925.00 rub.", + currentReplySource: "address_query_runtime_v1", + currentReplyType: "factual", + addressRuntimeMeta: { + detected_intent: "customer_revenue_and_payments", + selected_recipe: "address_customer_revenue_and_payments_v1", + mcp_call_status: "matched_non_empty", + truth_mode: "confirmed", + capability_binding_status: "bound", + capability_binding_violations: [], + assistant_mcp_discovery_entry_point_v1: entryPoint({ + turn_input: { + adapter_status: "ready", + should_run_discovery: true, + turn_meaning_ref: { + asked_domain_family: "counterparty_value", + asked_action_family: "payout", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + } + }) + } + }); + + expect(result.applied).toBe(true); + expect(result.decision).toBe("apply_candidate"); + expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded"); + expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override"); + expect(result.reason_codes).toContain( + "mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override" + ); + expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply"); + }); + it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => { const result = applyAssistantMcpDiscoveryResponsePolicy({ currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",