From d64d7f84ccfcf3891cdc04f9434876af303d8fdc Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 22 May 2026 18:48:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D1=8F=D0=B7=D0=B0=D1=82=D1=8C=20ev?= =?UTF-8?q?idence=20planner=20=D1=81=20route=20candidate=20handoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/assistantEvidencePlanner.js | 23 +++++- .../assistantMcpDiscoveryDebugAttachment.js | 3 + .../services/assistantMcpDiscoveryPlanner.js | 65 ++++++++++++++- .../assistantMcpDiscoveryRuntimeBridge.js | 20 +++-- .../src/services/assistantEvidencePlanner.ts | 29 ++++++- .../assistantMcpDiscoveryDebugAttachment.ts | 6 ++ .../services/assistantMcpDiscoveryPlanner.ts | 80 ++++++++++++++++++- .../assistantMcpDiscoveryRuntimeBridge.ts | 24 ++++-- ...sistantMcpDiscoveryDebugAttachment.test.ts | 9 +++ .../assistantMcpDiscoveryPlanner.test.ts | 1 + ...assistantMcpDiscoveryRuntimeBridge.test.ts | 12 +++ 11 files changed, 256 insertions(+), 16 deletions(-) diff --git a/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js index 5c75a5e..f5a70c9 100644 --- a/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js +++ b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js @@ -57,6 +57,24 @@ function providedAxesFromMeaning(meaning) { function missingAxes(requiredAxes, providedAxes) { return requiredAxes.filter((axis) => !providedAxes.includes(axis)); } +const USER_ACTIONABLE_AXIS_SET = new Set([ + "counterparty", + "business_entity", + "organization", + "period", + "as_of_date", + "item", + "supplier", + "buyer", + "warehouse", + "document", + "contract", + "metadata_scope", + "lane_family_choice" +]); +function userActionableMissingAxes(axes) { + return axes.filter((axis) => USER_ACTIONABLE_AXIS_SET.has(axis)); +} function coverageExpectationFor(graph) { if (graph?.proof_expectation === "bounded_inference") { return "bounded_inference"; @@ -100,7 +118,9 @@ function buildAssistantEvidencePlanner(input) { const requiredAxes = uniqueStrings(plan.required_axes); const providedAxes = providedAxesFromMeaning(turnMeaning); const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); - const axisGaps = missingAxes(requiredAxes, providedAxes); + const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter((axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis))); + const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]); + const actionableAxisGaps = userActionableMissingAxes(axisGaps); const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]); const coverageExpectation = coverageExpectationFor(graph); const answerMode = answerModeFor({ @@ -135,6 +155,7 @@ function buildAssistantEvidencePlanner(input) { required_axes: requiredAxes, provided_axes: providedAxes, missing_axes: axisGaps, + user_actionable_missing_axes: actionableAxisGaps, clarification_gaps: clarificationGaps }, primitive_plan: { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js index 5e139d4..69cfdd5 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js @@ -83,6 +83,9 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) { mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family), mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family), mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation), + mcp_discovery_route_candidate_evidence_plan_status: toNonEmptyString(routeCandidate?.evidence_plan_status), + mcp_discovery_route_candidate_evidence_answer_mode: toNonEmptyString(routeCandidate?.evidence_answer_mode), + mcp_discovery_route_candidate_evidence_expected_coverage: toNonEmptyString(routeCandidate?.evidence_expected_coverage), mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes), mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes), mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 82e2276..dd9e348 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -42,6 +42,16 @@ function pushAllUnique(target, values) { pushUnique(target, value); } } +function uniqueStringList(values) { + const result = []; + for (const value of values) { + const text = toNonEmptyString(value); + if (text) { + pushUnique(result, text); + } + } + return result; +} function pushCatalogChainTemplateAlignmentReasons(target, alignment) { if (alignment.alignment_status === "selected_matches_top") { pushReason(target, "planner_catalog_chain_template_alignment_evaluated"); @@ -1078,6 +1088,55 @@ function statusFrom(plan, review) { } return "ready_for_execution"; } +function providedAxesFromPlanTurnMeaning(plan) { + const result = []; + const meaning = plan.turn_meaning_ref; + if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { + pushUnique(result, "counterparty"); + pushUnique(result, "business_entity"); + } + if (toNonEmptyString(meaning?.explicit_organization_scope)) { + pushUnique(result, "organization"); + } + if (toNonEmptyString(meaning?.explicit_date_scope)) { + pushUnique(result, "period"); + } + if (toNonEmptyString(meaning?.asked_aggregation_axis)) { + pushUnique(result, "aggregate_axis"); + } + if (toNonEmptyString(meaning?.metadata_scope_hint)) { + pushUnique(result, "metadata_scope"); + } + return result; +} +function preferredClarificationAxesForRecipe(input) { + if (input.recipe.chainId === "value_flow_ranking" || + input.recipe.chainId === "value_flow_comparison" || + input.recipe.chainId === "business_overview") { + return ["organization"]; + } + if (input.dataNeedGraph?.business_fact_family === "value_flow" && !hasSubjectCandidates(input.dataNeedGraph)) { + return ["organization"]; + } + return []; +} +function missingAxesFromCatalogReview(review, input) { + const result = []; + for (const options of Object.values(review.missing_axes_by_primitive)) { + const candidates = options + .map((option) => uniqueStringList(option).filter((axis) => !input.providedAxes.includes(axis))) + .filter((option) => option.length > 0); + const preferredCandidate = candidates.find((option) => option.some((axis) => input.preferredAxes.includes(axis))); + const candidate = preferredCandidate ?? candidates.sort((left, right) => left.length - right.length)[0]; + if (!candidate) { + continue; + } + for (const axis of candidate) { + pushUnique(result, axis); + } + } + return result; +} function planAssistantMcpDiscovery(input) { const recipe = recipeFor(input); const budgetOverride = budgetOverrideFor(input, recipe); @@ -1145,7 +1204,11 @@ function planAssistantMcpDiscovery(input) { plannerStatus, semanticDataNeed, dataNeedGraph, - discoveryPlan: plan + discoveryPlan: plan, + additionalMissingAxes: missingAxesFromCatalogReview(adjustedReview, { + providedAxes: providedAxesFromPlanTurnMeaning(plan), + preferredAxes: preferredClarificationAxesForRecipe({ recipe, dataNeedGraph }) + }) }); return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js index 34af03b..77be4d9 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js @@ -185,8 +185,13 @@ function routeCandidateNextAction(status) { } function buildRouteCandidate(planner, pilot, bridgeStatus) { const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? []; - const providedAxes = flattenAxes(pilot, "provided_axes"); - const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"); + const evidenceAxes = planner.evidence_plan.evidence_axes; + const providedAxes = uniqueStrings([...evidenceAxes.provided_axes, ...flattenAxes(pilot, "provided_axes")]); + const missingAxes = plannerClarificationGaps.length > 0 + ? plannerClarificationGaps + : evidenceAxes.user_actionable_missing_axes.length > 0 + ? evidenceAxes.user_actionable_missing_axes + : flattenAxes(pilot, "missing_axis_options"); const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot); const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily); return { @@ -197,9 +202,12 @@ function buildRouteCandidate(planner, pilot, bridgeStatus) { selected_chain_summary: planner.selected_chain_summary, nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match, catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status, - business_fact_family: planner.data_need_graph?.business_fact_family ?? null, - action_family: planner.data_need_graph?.action_family ?? null, - proof_expectation: planner.data_need_graph?.proof_expectation ?? null, + business_fact_family: planner.evidence_plan.data_need.business_fact_family, + action_family: planner.evidence_plan.data_need.action_family, + proof_expectation: planner.evidence_plan.data_need.proof_expectation, + evidence_plan_status: planner.evidence_plan.planner_status, + evidence_answer_mode: planner.evidence_plan.answer_contract.answer_mode, + evidence_expected_coverage: planner.evidence_plan.coverage_gate.expected_coverage, required_axes: [...planner.required_axes], provided_axes: providedAxes, missing_axes: missingAxes, @@ -207,7 +215,7 @@ function buildRouteCandidate(planner, pilot, bridgeStatus) { enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily), recommended_next_action: routeCandidateNextAction(candidateStatus), forbidden_overclaim_flags: uniqueStrings([ - ...(planner.data_need_graph?.forbidden_overclaim_flags ?? []), + ...planner.evidence_plan.answer_contract.forbidden_overclaim_flags, ...(missingProofFamily ? [missingProofFamily.must_not_claim] : []) ]) }; diff --git a/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts index be30c16..264d8ca 100644 --- a/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts +++ b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts @@ -30,6 +30,7 @@ export interface AssistantEvidenceAxesContract { required_axes: string[]; provided_axes: string[]; missing_axes: string[]; + user_actionable_missing_axes: string[]; clarification_gaps: string[]; } @@ -74,6 +75,7 @@ export interface BuildAssistantEvidencePlannerInput { semanticDataNeed?: string | null; dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null; discoveryPlan: AssistantMcpDiscoveryPlanContract; + additionalMissingAxes?: string[] | null; } function toNonEmptyString(value: unknown): string | null { @@ -136,6 +138,26 @@ function missingAxes(requiredAxes: string[], providedAxes: string[]): string[] { return requiredAxes.filter((axis) => !providedAxes.includes(axis)); } +const USER_ACTIONABLE_AXIS_SET = new Set([ + "counterparty", + "business_entity", + "organization", + "period", + "as_of_date", + "item", + "supplier", + "buyer", + "warehouse", + "document", + "contract", + "metadata_scope", + "lane_family_choice" +]); + +function userActionableMissingAxes(axes: string[]): string[] { + return axes.filter((axis) => USER_ACTIONABLE_AXIS_SET.has(axis)); +} + function coverageExpectationFor( graph: AssistantMcpDiscoveryDataNeedGraphContract | null ): AssistantEvidenceCoverageExpectation { @@ -189,7 +211,11 @@ export function buildAssistantEvidencePlanner( const requiredAxes = uniqueStrings(plan.required_axes); const providedAxes = providedAxesFromMeaning(turnMeaning); const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); - const axisGaps = missingAxes(requiredAxes, providedAxes); + const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter( + (axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis)), + ); + const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]); + const actionableAxisGaps = userActionableMissingAxes(axisGaps); const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]); const coverageExpectation = coverageExpectationFor(graph); const answerMode = answerModeFor({ @@ -226,6 +252,7 @@ export function buildAssistantEvidencePlanner( required_axes: requiredAxes, provided_axes: providedAxes, missing_axes: axisGaps, + user_actionable_missing_axes: actionableAxisGaps, clarification_gaps: clarificationGaps }, primitive_plan: { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts index c0e862b..b9e19ef 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts @@ -23,6 +23,9 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields { mcp_discovery_route_candidate_fact_family: string | null; mcp_discovery_route_candidate_action_family: string | null; mcp_discovery_route_candidate_proof_expectation: string | null; + mcp_discovery_route_candidate_evidence_plan_status: string | null; + mcp_discovery_route_candidate_evidence_answer_mode: string | null; + mcp_discovery_route_candidate_evidence_expected_coverage: string | null; mcp_discovery_route_candidate_missing_axes: string[]; mcp_discovery_route_candidate_provided_axes: string[]; mcp_discovery_route_candidate_executable_now: boolean; @@ -135,6 +138,9 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields( mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family), mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family), mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation), + mcp_discovery_route_candidate_evidence_plan_status: toNonEmptyString(routeCandidate?.evidence_plan_status), + mcp_discovery_route_candidate_evidence_answer_mode: toNonEmptyString(routeCandidate?.evidence_answer_mode), + mcp_discovery_route_candidate_evidence_expected_coverage: toNonEmptyString(routeCandidate?.evidence_expected_coverage), mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes), mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes), mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index ab14d82..55d5908 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -158,6 +158,17 @@ function pushAllUnique(target: string[], values: string[]): void { } } +function uniqueStringList(values: unknown[]): string[] { + const result: string[] = []; + for (const value of values) { + const text = toNonEmptyString(value); + if (text) { + pushUnique(result, text); + } + } + return result; +} + function pushCatalogChainTemplateAlignmentReasons( target: string[], alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment @@ -1360,6 +1371,69 @@ function statusFrom( return "ready_for_execution"; } +function providedAxesFromPlanTurnMeaning(plan: AssistantMcpDiscoveryPlanContract): string[] { + const result: string[] = []; + const meaning = plan.turn_meaning_ref; + if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { + pushUnique(result, "counterparty"); + pushUnique(result, "business_entity"); + } + if (toNonEmptyString(meaning?.explicit_organization_scope)) { + pushUnique(result, "organization"); + } + if (toNonEmptyString(meaning?.explicit_date_scope)) { + pushUnique(result, "period"); + } + if (toNonEmptyString(meaning?.asked_aggregation_axis)) { + pushUnique(result, "aggregate_axis"); + } + if (toNonEmptyString(meaning?.metadata_scope_hint)) { + pushUnique(result, "metadata_scope"); + } + return result; +} + +function preferredClarificationAxesForRecipe(input: { + recipe: PlannerRecipe; + dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null; +}): string[] { + if ( + input.recipe.chainId === "value_flow_ranking" || + input.recipe.chainId === "value_flow_comparison" || + input.recipe.chainId === "business_overview" + ) { + return ["organization"]; + } + if (input.dataNeedGraph?.business_fact_family === "value_flow" && !hasSubjectCandidates(input.dataNeedGraph)) { + return ["organization"]; + } + return []; +} + +function missingAxesFromCatalogReview( + review: AssistantMcpCatalogPlanReview, + input: { + providedAxes: string[]; + preferredAxes: string[]; + } +): string[] { + const result: string[] = []; + for (const options of Object.values(review.missing_axes_by_primitive)) { + const candidates = options + .map((option) => uniqueStringList(option).filter((axis) => !input.providedAxes.includes(axis))) + .filter((option) => option.length > 0); + const preferredCandidate = candidates.find((option) => option.some((axis) => input.preferredAxes.includes(axis))); + const candidate = preferredCandidate ?? candidates.sort((left, right) => left.length - right.length)[0]; + if (!candidate) { + continue; + } + for (const axis of candidate) { + pushUnique(result, axis); + } + } + return result; +} + export function planAssistantMcpDiscovery( input: AssistantMcpDiscoveryPlannerInput ): AssistantMcpDiscoveryPlannerContract { @@ -1432,7 +1506,11 @@ export function planAssistantMcpDiscovery( plannerStatus, semanticDataNeed, dataNeedGraph, - discoveryPlan: plan + discoveryPlan: plan, + additionalMissingAxes: missingAxesFromCatalogReview(adjustedReview, { + providedAxes: providedAxesFromPlanTurnMeaning(plan), + preferredAxes: preferredClarificationAxesForRecipe({ recipe, dataNeedGraph }) + }) }); return { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts index c451ade..9761c11 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts @@ -79,6 +79,9 @@ export interface AssistantMcpRouteCandidateContract { business_fact_family: string | null; action_family: string | null; proof_expectation: string | null; + evidence_plan_status: string | null; + evidence_answer_mode: string | null; + evidence_expected_coverage: string | null; required_axes: string[]; provided_axes: string[]; missing_axes: string[]; @@ -326,8 +329,14 @@ function buildRouteCandidate( bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus ): AssistantMcpRouteCandidateContract { const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? []; - const providedAxes = flattenAxes(pilot, "provided_axes"); - const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"); + const evidenceAxes = planner.evidence_plan.evidence_axes; + const providedAxes = uniqueStrings([...evidenceAxes.provided_axes, ...flattenAxes(pilot, "provided_axes")]); + const missingAxes = + plannerClarificationGaps.length > 0 + ? plannerClarificationGaps + : evidenceAxes.user_actionable_missing_axes.length > 0 + ? evidenceAxes.user_actionable_missing_axes + : flattenAxes(pilot, "missing_axis_options"); const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot); const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily); return { @@ -338,9 +347,12 @@ function buildRouteCandidate( selected_chain_summary: planner.selected_chain_summary, nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match, catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status, - business_fact_family: planner.data_need_graph?.business_fact_family ?? null, - action_family: planner.data_need_graph?.action_family ?? null, - proof_expectation: planner.data_need_graph?.proof_expectation ?? null, + business_fact_family: planner.evidence_plan.data_need.business_fact_family, + action_family: planner.evidence_plan.data_need.action_family, + proof_expectation: planner.evidence_plan.data_need.proof_expectation, + evidence_plan_status: planner.evidence_plan.planner_status, + evidence_answer_mode: planner.evidence_plan.answer_contract.answer_mode, + evidence_expected_coverage: planner.evidence_plan.coverage_gate.expected_coverage, required_axes: [...planner.required_axes], provided_axes: providedAxes, missing_axes: missingAxes, @@ -348,7 +360,7 @@ function buildRouteCandidate( enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily), recommended_next_action: routeCandidateNextAction(candidateStatus), forbidden_overclaim_flags: uniqueStrings([ - ...(planner.data_need_graph?.forbidden_overclaim_flags ?? []), + ...planner.evidence_plan.answer_contract.forbidden_overclaim_flags, ...(missingProofFamily ? [missingProofFamily.must_not_claim] : []) ]) }; diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts index 7e35786..48cff29 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts @@ -52,6 +52,9 @@ function entryPointContract(overrides: Record = {}) { business_fact_family: "value_flow", action_family: "turnover", proof_expectation: "coverage_checked_fact", + evidence_plan_status: "ready_for_execution", + evidence_answer_mode: "confirmed_business_answer", + evidence_expected_coverage: "confirmed_coverage", required_axes: ["organization", "period"], provided_axes: ["organization", "period"], missing_axes: [], @@ -95,6 +98,9 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution"); expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow"); expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover"); + expect(debug.mcp_discovery_route_candidate_evidence_plan_status).toBe("ready_for_execution"); + expect(debug.mcp_discovery_route_candidate_evidence_answer_mode).toBe("confirmed_business_answer"); + expect(debug.mcp_discovery_route_candidate_evidence_expected_coverage).toBe("confirmed_coverage"); expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]); expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]); expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true); @@ -130,6 +136,9 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false); expect(debug.mcp_discovery_route_candidate_v1).toBeNull(); expect(debug.mcp_discovery_route_candidate_status).toBeNull(); + expect(debug.mcp_discovery_route_candidate_evidence_plan_status).toBeNull(); + expect(debug.mcp_discovery_route_candidate_evidence_answer_mode).toBeNull(); + expect(debug.mcp_discovery_route_candidate_evidence_expected_coverage).toBeNull(); expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]); expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]); expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index ab85c59..31a451c 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -74,6 +74,7 @@ describe("assistant MCP discovery planner", () => { ]); expect(result.evidence_plan.evidence_axes.provided_axes).toEqual(["counterparty", "business_entity", "period"]); expect(result.evidence_plan.evidence_axes.missing_axes).toEqual(["aggregate_axis", "amount", "coverage_target"]); + expect(result.evidence_plan.evidence_axes.user_actionable_missing_axes).toEqual([]); expect(result.data_need_graph?.business_fact_family).toBe("value_flow"); expect(result.catalog_chain_template_matches[0]).toBe("value_flow"); expect(result.catalog_chain_template_alignment).toMatchObject({ diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts index 000c628..d495595 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts @@ -163,6 +163,13 @@ describe("assistant MCP discovery runtime bridge", () => { }); expect(result.loop_state.pending_axes).toContain("organization"); expect(result.loop_state.provided_axes).toContain("aggregate_axis"); + expect(result.planner.evidence_plan.evidence_axes.user_actionable_missing_axes).toEqual(["organization"]); + expect(result.planner.evidence_plan.evidence_axes.missing_axes).toEqual([ + "organization", + "aggregate_axis", + "amount", + "coverage_target" + ]); expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking"); expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top"); expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true); @@ -174,10 +181,15 @@ describe("assistant MCP discovery runtime bridge", () => { catalog_alignment_status: "selected_matches_top", business_fact_family: "value_flow", action_family: "turnover", + evidence_plan_status: "needs_clarification", + evidence_answer_mode: "clarification_required", + evidence_expected_coverage: "confirmed_coverage", executable_now: false }); expect(result.route_candidate.missing_axes).toContain("organization"); expect(result.route_candidate.provided_axes).toContain("aggregate_axis"); + expect(result.route_candidate.missing_axes).not.toContain("amount"); + expect(result.route_candidate.missing_axes).not.toContain("coverage_target"); expect(result.route_candidate.recommended_next_action).toBe( "Ask the user for the missing scope axes before MCP execution." );