diff --git a/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js b/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js index 924a05f..573e69c 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js +++ b/llm_normalizer/backend/dist/services/assistantMcpCatalogIndex.js @@ -296,6 +296,8 @@ function tagSetFromMetadataSurfaceInput(input) { } if (routeFamily === "catalog_drilldown" || recommendedPrimitive === "drilldown_related_objects") { tags.add("drilldown"); + tags.add("metadata"); + tags.add("surface_inspection"); } for (const value of surfaceValues) { if (metadataSurfaceSuggestsDocument(value)) { @@ -306,6 +308,8 @@ function tagSetFromMetadataSurfaceInput(input) { } if (metadataSurfaceSuggestsCatalog(value)) { tags.add("drilldown"); + tags.add("metadata"); + tags.add("surface_inspection"); } } const requiredAxisSet = toStringSet(input.required_axes ?? []); @@ -328,6 +332,15 @@ function factFamiliesFromMetadataSurfaceInput(input) { if (routeFamily === "movement_evidence") { families.add("movement_evidence"); } + if (routeFamily === "catalog_drilldown") { + families.add("schema_surface"); + } + for (const value of [input.selected_entity_set ?? "", ...(input.selected_surface_objects ?? [])]) { + if (metadataSurfaceSuggestsCatalog(value)) { + families.add("schema_surface"); + break; + } + } return families; } function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(input) { @@ -443,6 +456,12 @@ function searchAssistantMcpCatalogPrimitivesByMetadataSurface(input) { } const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of); const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0; + if (contract.primitive_id === "probe_coverage" && !desiredTags.has("coverage")) { + continue; + } + if (contract.primitive_id === "drilldown_related_objects" && !hasCompatibleAxisGroup && axisOverlap <= 0) { + continue; + } let score = 0; if (primitiveRecommended) { score += 6; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index c615964..b0e5188 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -342,7 +342,8 @@ function isValueFlowPilotEligible(planner) { } function isMetadataPilotEligible(planner) { if (planner.selected_chain_id === "metadata_inspection" || - planner.selected_chain_id === "metadata_lane_clarification") { + planner.selected_chain_id === "metadata_lane_clarification" || + planner.selected_chain_id === "catalog_drilldown") { return true; } const meaning = planner.discovery_plan.turn_meaning_ref; @@ -380,6 +381,23 @@ function metadataScopeForPlanner(planner) { if (entityCandidate) { return entityCandidate; } + if (planner.selected_chain_id === "catalog_drilldown") { + const surface = planner.metadata_surface_ref; + const scopeCandidate = [ + ...(surface?.selected_surface_objects ?? []), + surface?.selected_entity_set ?? "" + ] + .map((value) => toNonEmptyString(value)) + .filter((value) => Boolean(value)) + .map((value) => { + const parts = value.split(".").map((item) => item.trim()).filter((item) => item.length > 0); + return parts.length > 0 ? parts[parts.length - 1] ?? value : value; + }) + .find((value) => value.length > 0); + if (scopeCandidate) { + return scopeCandidate; + } + } const meaning = planner.discovery_plan.turn_meaning_ref; const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}` .toLowerCase() @@ -396,6 +414,12 @@ function metadataScopeForPlanner(planner) { return null; } function metadataTypesForPlanner(planner) { + if (planner.selected_chain_id === "catalog_drilldown") { + const selectedEntitySet = toNonEmptyString(planner.metadata_surface_ref?.selected_entity_set); + if (selectedEntitySet) { + return [selectedEntitySet]; + } + } const meaning = planner.discovery_plan.turn_meaning_ref; const action = String(meaning?.asked_action_family ?? "").toLowerCase(); if (action === "inspect_registers") { @@ -1773,6 +1797,7 @@ function buildEmptyEvidence(planner, dryRun, probeResults, reason) { } function pilotScopeForPlanner(planner) { switch (planner.selected_chain_id) { + case "catalog_drilldown": case "metadata_lane_clarification": case "metadata_inspection": return "metadata_inspection_v1"; @@ -1903,6 +1928,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { let metadataResult = null; const metadataScope = metadataScopeForPlanner(planner); const requestedMetaTypes = metadataTypesForPlanner(planner); + if (planner.selected_chain_id === "catalog_drilldown" && metadataScope) { + pushReason(reasonCodes, "pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref"); + } for (const step of dryRun.execution_steps) { if (step.primitive_id !== "inspect_1c_metadata") { skippedPrimitives.push(step.primitive_id); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 113e372..5069567 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -246,8 +246,13 @@ function routeFamilyFromThinMetadataSurfaceInput(input) { if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) { return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null; } + if (graphFactFamily === "schema_surface" || includesAny(combined, ["catalog", "directory", "inspect_catalog"])) { + return surface.downstream_route_family === "catalog_drilldown" ? "catalog_drilldown" : null; + } if (!graphFactFamily && !domain && !action) { - if (surface.downstream_route_family === "document_evidence" || surface.downstream_route_family === "movement_evidence") { + if (surface.downstream_route_family === "document_evidence" || + surface.downstream_route_family === "movement_evidence" || + surface.downstream_route_family === "catalog_drilldown") { return surface.downstream_route_family; } } @@ -321,6 +326,25 @@ function recipeFor(input) { extraReasons: primitiveSelection.reasonCodes }; } + if (thinSurfaceRouteFamily === "catalog_drilldown") { + pushUnique(axes, "metadata_scope"); + const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ + dataNeedGraph, + fallbackPrimitives: ["inspect_1c_metadata"], + requiredAxes: axes, + metadataSurface: input.metadataSurface, + actionFamily: action + }); + return { + semanticDataNeed: "catalog drilldown metadata evidence", + chainId: "catalog_drilldown", + chainSummary: "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.", + primitives: primitiveSelection.primitives, + axes, + reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref", + extraReasons: primitiveSelection.reasonCodes + }; + } if (graphFactFamily === "value_flow") { if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) { pushUnique(axes, "amount"); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index d33f624..941f316 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -788,6 +788,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !metadataDocumentHintSignal && !metadataMovementHintSignal && hasMetadataDownstreamContinuationSignal(rawText)); + const metadataGroundedCatalogLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "catalog_drilldown" && + !followupSeed.metadataAmbiguityDetected && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText)); const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) && @@ -838,34 +847,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && !metadataAmbiguityLaneClarificationApplicable && !metadataGroundedDocumentLaneApplicable && - !metadataGroundedMovementLaneApplicable; + !metadataGroundedMovementLaneApplicable && + !metadataGroundedCatalogLaneContinuationApplicable; const seededDomain = metadataAmbiguityLaneClarificationApplicable ? "metadata" : metadataGroundedDocumentLaneApplicable ? "documents" : metadataGroundedMovementLaneApplicable ? "movements" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.domain - : null; + : metadataGroundedCatalogLaneContinuationApplicable + ? "metadata" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.domain + : null; const seededAction = metadataAmbiguityLaneClarificationApplicable ? "resolve_next_lane" : metadataGroundedDocumentLaneApplicable ? "list_documents" : metadataGroundedMovementLaneApplicable ? "list_movements" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.action - : null; + : metadataGroundedCatalogLaneContinuationApplicable + ? "inspect_catalog" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.action + : null; const seededUnsupported = metadataAmbiguityLaneClarificationApplicable ? "metadata_lane_choice_clarification" : metadataGroundedDocumentLaneApplicable ? "document_evidence" : metadataGroundedMovementLaneApplicable ? "movement_evidence" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.unsupported - : null; + : metadataGroundedCatalogLaneContinuationApplicable + ? "schema_surface" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.unsupported + : null; const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const bidirectionalValueFlowSignal = !lifecycleSignal && (rawBidirectionalValueFlowSignal || seededAction === "net_value_flow"); @@ -1180,6 +1196,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } + if (metadataGroundedCatalogLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_catalog_continuation"); + } if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts b/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts index 0c3e4b2..93a2822 100644 --- a/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts +++ b/llm_normalizer/backend/src/services/assistantMcpCatalogIndex.ts @@ -381,6 +381,8 @@ function tagSetFromMetadataSurfaceInput(input: AssistantMcpCatalogMetadataSurfac } if (routeFamily === "catalog_drilldown" || recommendedPrimitive === "drilldown_related_objects") { tags.add("drilldown"); + tags.add("metadata"); + tags.add("surface_inspection"); } for (const value of surfaceValues) { if (metadataSurfaceSuggestsDocument(value)) { @@ -391,6 +393,8 @@ function tagSetFromMetadataSurfaceInput(input: AssistantMcpCatalogMetadataSurfac } if (metadataSurfaceSuggestsCatalog(value)) { tags.add("drilldown"); + tags.add("metadata"); + tags.add("surface_inspection"); } } const requiredAxisSet = toStringSet(input.required_axes ?? []); @@ -416,6 +420,15 @@ function factFamiliesFromMetadataSurfaceInput(input: AssistantMcpCatalogMetadata if (routeFamily === "movement_evidence") { families.add("movement_evidence"); } + if (routeFamily === "catalog_drilldown") { + families.add("schema_surface"); + } + for (const value of [input.selected_entity_set ?? "", ...(input.selected_surface_objects ?? [])]) { + if (metadataSurfaceSuggestsCatalog(value)) { + families.add("schema_surface"); + break; + } + } return families; } @@ -552,6 +565,12 @@ export function searchAssistantMcpCatalogPrimitivesByMetadataSurface( } const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of); const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0; + if (contract.primitive_id === "probe_coverage" && !desiredTags.has("coverage")) { + continue; + } + if (contract.primitive_id === "drilldown_related_objects" && !hasCompatibleAxisGroup && axisOverlap <= 0) { + continue; + } let score = 0; if (primitiveRecommended) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index 3e4434e..3f29e32 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -613,7 +613,8 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract) function isMetadataPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean { if ( planner.selected_chain_id === "metadata_inspection" || - planner.selected_chain_id === "metadata_lane_clarification" + planner.selected_chain_id === "metadata_lane_clarification" || + planner.selected_chain_id === "catalog_drilldown" ) { return true; } @@ -658,6 +659,23 @@ function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): if (entityCandidate) { return entityCandidate; } + if (planner.selected_chain_id === "catalog_drilldown") { + const surface = planner.metadata_surface_ref; + const scopeCandidate = [ + ...(surface?.selected_surface_objects ?? []), + surface?.selected_entity_set ?? "" + ] + .map((value) => toNonEmptyString(value)) + .filter((value): value is string => Boolean(value)) + .map((value) => { + const parts = value.split(".").map((item) => item.trim()).filter((item) => item.length > 0); + return parts.length > 0 ? parts[parts.length - 1] ?? value : value; + }) + .find((value) => value.length > 0); + if (scopeCandidate) { + return scopeCandidate; + } + } const meaning = planner.discovery_plan.turn_meaning_ref; const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}` .toLowerCase() @@ -675,6 +693,12 @@ function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): } function metadataTypesForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] { + if (planner.selected_chain_id === "catalog_drilldown") { + const selectedEntitySet = toNonEmptyString(planner.metadata_surface_ref?.selected_entity_set); + if (selectedEntitySet) { + return [selectedEntitySet]; + } + } const meaning = planner.discovery_plan.turn_meaning_ref; const action = String(meaning?.asked_action_family ?? "").toLowerCase(); if (action === "inspect_registers") { @@ -2381,6 +2405,7 @@ function buildEmptyEvidence( function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryPilotScope { switch (planner.selected_chain_id) { + case "catalog_drilldown": case "metadata_lane_clarification": case "metadata_inspection": return "metadata_inspection_v1"; @@ -2524,6 +2549,9 @@ export async function executeAssistantMcpDiscoveryPilot( let metadataResult: AddressMcpMetadataRowsResult | null = null; const metadataScope = metadataScopeForPlanner(planner); const requestedMetaTypes = metadataTypesForPlanner(planner); + if (planner.selected_chain_id === "catalog_drilldown" && metadataScope) { + pushReason(reasonCodes, "pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref"); + } for (const step of dryRun.execution_steps) { if (step.primitive_id !== "inspect_1c_metadata") { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index 0b82e73..21524e1 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -37,6 +37,7 @@ export interface AssistantMcpDiscoveryMetadataSurfaceRef { export type AssistantMcpDiscoveryChainId = | "metadata_inspection" + | "catalog_drilldown" | "metadata_lane_clarification" | "value_flow" | "value_flow_comparison" @@ -382,8 +383,15 @@ function routeFamilyFromThinMetadataSurfaceInput( if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) { return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null; } + if (graphFactFamily === "schema_surface" || includesAny(combined, ["catalog", "directory", "inspect_catalog"])) { + return surface.downstream_route_family === "catalog_drilldown" ? "catalog_drilldown" : null; + } if (!graphFactFamily && !domain && !action) { - if (surface.downstream_route_family === "document_evidence" || surface.downstream_route_family === "movement_evidence") { + if ( + surface.downstream_route_family === "document_evidence" || + surface.downstream_route_family === "movement_evidence" || + surface.downstream_route_family === "catalog_drilldown" + ) { return surface.downstream_route_family; } } @@ -463,6 +471,26 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { extraReasons: primitiveSelection.reasonCodes }; } + if (thinSurfaceRouteFamily === "catalog_drilldown") { + pushUnique(axes, "metadata_scope"); + const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ + dataNeedGraph, + fallbackPrimitives: ["inspect_1c_metadata"], + requiredAxes: axes, + metadataSurface: input.metadataSurface, + actionFamily: action + }); + return { + semanticDataNeed: "catalog drilldown metadata evidence", + chainId: "catalog_drilldown", + chainSummary: + "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.", + primitives: primitiveSelection.primitives, + axes, + reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref", + extraReasons: primitiveSelection.reasonCodes + }; + } if (graphFactFamily === "value_flow") { if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index d64c06f..b7a38fa 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -1080,6 +1080,17 @@ export function buildAssistantMcpDiscoveryTurnInput( !metadataMovementHintSignal && hasMetadataDownstreamContinuationSignal(rawText) ); + const metadataGroundedCatalogLaneContinuationApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "catalog_drilldown" && + !followupSeed.metadataAmbiguityDetected && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !metadataDocumentHintSignal && + !metadataMovementHintSignal && + hasMetadataDownstreamContinuationSignal(rawText) + ); const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean( followupSeed.pilotScope === "metadata_inspection_v1" && followupSeed.metadataAmbiguityDetected && @@ -1139,13 +1150,16 @@ export function buildAssistantMcpDiscoveryTurnInput( metadataFollowupSeedApplicable && !metadataAmbiguityLaneClarificationApplicable && !metadataGroundedDocumentLaneApplicable && - !metadataGroundedMovementLaneApplicable; + !metadataGroundedMovementLaneApplicable && + !metadataGroundedCatalogLaneContinuationApplicable; const seededDomain = metadataAmbiguityLaneClarificationApplicable ? "metadata" : metadataGroundedDocumentLaneApplicable ? "documents" : metadataGroundedMovementLaneApplicable ? "movements" + : metadataGroundedCatalogLaneContinuationApplicable + ? "metadata" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.domain : null; @@ -1155,6 +1169,8 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "list_documents" : metadataGroundedMovementLaneApplicable ? "list_movements" + : metadataGroundedCatalogLaneContinuationApplicable + ? "inspect_catalog" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.action : null; @@ -1164,6 +1180,8 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "document_evidence" : metadataGroundedMovementLaneApplicable ? "movement_evidence" + : metadataGroundedCatalogLaneContinuationApplicable + ? "schema_surface" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.unsupported : null; @@ -1505,6 +1523,9 @@ export function buildAssistantMcpDiscoveryTurnInput( if (metadataGroundedLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); } + if (metadataGroundedCatalogLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_catalog_continuation"); + } if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane"); } diff --git a/llm_normalizer/backend/tests/assistantMcpCatalogIndex.test.ts b/llm_normalizer/backend/tests/assistantMcpCatalogIndex.test.ts index adfec43..54dfd45 100644 --- a/llm_normalizer/backend/tests/assistantMcpCatalogIndex.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpCatalogIndex.test.ts @@ -95,6 +95,18 @@ describe("assistant MCP catalog index", () => { expect(primitives).toEqual(["query_movements", "resolve_entity_reference", "probe_coverage"]); }); + it("can search reviewed primitives directly from a confirmed catalog metadata surface without inventing an unsupported drilldown primitive", () => { + const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({ + downstream_route_family: "catalog_drilldown", + selected_entity_set: "Catalog", + selected_surface_objects: ["Catalog.Counterparties"], + recommended_next_primitive: "drilldown_related_objects", + required_axes: ["metadata_scope"] + }); + + expect(primitives).toEqual(["inspect_1c_metadata"]); + }); + it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => { const plan = buildAssistantMcpDiscoveryPlan({ semanticDataNeed: "counterparty turnover evidence", diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index 0020731..c454fdf 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -249,6 +249,57 @@ describe("assistant MCP discovery pilot executor", () => { }); }); + it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => { + const planner = planAssistantMcpDiscovery({ + metadataSurface: { + selected_entity_set: "Catalog", + selected_surface_objects: ["Catalog.Counterparties"], + downstream_route_family: "catalog_drilldown", + route_family_selection_basis: "selected_entity_set", + recommended_next_primitive: "drilldown_related_objects", + ambiguity_detected: false, + ambiguity_entity_sets: [] + }, + turnMeaning: { + asked_domain_family: "metadata", + asked_action_family: "inspect_catalog", + unsupported_but_understood_family: "schema_surface" + } + }); + const deps = buildMetadataDeps([ + { + FullName: "Catalog.Counterparties", + MetaType: "Catalog", + attributes: [{ Name: "Description" }] + }, + { + FullName: "Catalog.Contracts", + MetaType: "Catalog", + attributes: [{ Name: "Owner" }] + } + ]); + + const result = await executeAssistantMcpDiscoveryPilot(planner, deps); + + expect(result.pilot_status).toBe("executed"); + expect(result.pilot_scope).toBe("metadata_inspection_v1"); + expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]); + expect(result.derived_metadata_surface).toMatchObject({ + metadata_scope: "Counterparties", + requested_meta_types: ["Catalog"], + available_entity_sets: ["Catalog"], + selected_entity_set: "Catalog", + downstream_route_family: "catalog_drilldown", + recommended_next_primitive: "drilldown_related_objects" + }); + expect(result.reason_codes).toContain("pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref"); + expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1); + expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({ + meta_type: ["Catalog"], + name_mask: "Counterparties" + }); + }); + it("executes the full entity-resolution chain through the checked counterparty catalog slice", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index e9b7c37..b00884a 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -323,6 +323,32 @@ describe("assistant MCP discovery planner", () => { expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search"); }); + it("can select catalog drilldown directly from a confirmed catalog metadata surface when the follow-up itself is thin", () => { + const result = planAssistantMcpDiscovery({ + metadataSurface: { + selected_entity_set: "Catalog", + selected_surface_objects: ["Catalog.Counterparties"], + downstream_route_family: "catalog_drilldown", + route_family_selection_basis: "selected_entity_set", + recommended_next_primitive: "drilldown_related_objects", + ambiguity_detected: false, + ambiguity_entity_sets: [] + }, + turnMeaning: { + asked_domain_family: "metadata", + asked_action_family: "inspect_catalog", + unsupported_but_understood_family: "schema_surface" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + 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.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"); + }); + it("does not force a lane from ambiguous metadata surface even when decomposition hints mention both documents and movements", () => { const result = planAssistantMcpDiscovery({ dataNeedGraph: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 761b72a..fddd698 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -1148,6 +1148,38 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane"); }); + it("continues from a confirmed catalog metadata surface into inspect_catalog on a generic downstream follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "continue with data", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_route_family: "catalog_drilldown", + previous_discovery_metadata_selected_surface_objects: ["Catalog.Counterparties"], + previous_discovery_metadata_recommended_next_primitive: "drilldown_related_objects", + previous_discovery_metadata_ambiguity_detected: false + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("1C metadata evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "metadata", + asked_action_family: "inspect_catalog" + }); + expect(result.metadata_surface_ref).toMatchObject({ + selected_entity_set: null, + selected_surface_objects: ["Catalog.Counterparties"], + downstream_route_family: "catalog_drilldown", + route_family_selection_basis: null, + recommended_next_primitive: "drilldown_related_objects", + ambiguity_detected: false, + ambiguity_entity_sets: [] + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_catalog_continuation"); + expect(result.reason_codes).toContain("mcp_discovery_metadata_surface_ref_from_followup_context"); + }); + it("requires an explicit lane choice on a generic downstream follow-up when metadata ambiguity stays mixed", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "давай дальше",