diff --git a/.gitignore b/.gitignore index 73cb158..9a7d7e5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ graphify-out/ # domain-case loop artifacts artifacts/domain_runs/* !artifacts/domain_runs/.gitkeep +artifacts/runtime_logs/*.log diff --git a/docs/ARCH/11 - architecture_turnaround/20 - planner_autonomy_consolidation_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/20 - planner_autonomy_consolidation_2026-05-01.md index 6a68d85..99d6faf 100644 --- a/docs/ARCH/11 - architecture_turnaround/20 - planner_autonomy_consolidation_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/20 - planner_autonomy_consolidation_2026-05-01.md @@ -102,6 +102,14 @@ The runtime answer boundary still makes unsupported or unconfirmed inventory sta - `must_not_claim` forbids presenting inventory planning as executed stock, supplier, purchase, or sale evidence; - technical unsupported-pilot limitation text is filtered out of user-facing lines, while existing bounded unknowns for lifecycle/value-flow remain intact. +The next local scoring step broadened metadata-surface autonomy without adding a new hard domain route: + +- if a confirmed metadata surface is unambiguous and only exposes `Document.*`, `Register.*`, or `Catalog.*` objects, the planner can infer the next reviewed lane even when upstream has not yet filled `downstream_route_family`; +- inferred document surfaces instantiate `document_evidence`; +- inferred register/movement surfaces instantiate `movement_evidence`; +- inferred catalog surfaces instantiate `catalog_drilldown`; +- mixed or ambiguous surfaces still do not guess and continue through clarification / explicit data-need scoring. + ## Why This Matters This reduces the pressure to add one hard route per user wording. @@ -166,16 +174,26 @@ Latest validation after the inventory exact-runtime bridge: - `npm.cmd run build`: passed - graphify rebuild: `5930 nodes`, `12884 edges`, `135 communities` +Latest validation after unambiguous metadata-surface lane inference: + +- targeted planner tests: passed, `36 passed` +- full MCP-discovery suite: passed, `281 passed`, `9 skipped` +- `npm.cmd run build`: passed +- graphify rebuild: `5937 nodes`, `12899 edges`, `138 communities` +- live inventory full-pack attempt: `inventory_stock_exact_bridge_live_20260501_after_runtime_bridge`, status `partial` +- live attempt interpretation: route/intent/recipe/capability selection matched, but MCP execution failed with `MCP fetch failed: This operation was aborted`; direct proxy `get_metadata` also timed out while `/health` reported `active_sessions_count=0` and pending commands, so this is an infrastructure/polling-session blocker rather than accepted semantic evidence. + ## Next Step -The next safe step is to validate the inventory exact-runtime bridge with live replay and then continue into broader reviewed scoring. +The next safe step is to re-run live replay once the 1C side is actively polling the proxy, then continue into broader reviewed scoring. Recommended order: -1. rerun the inventory canary and a mixed cross-stage canary against live 1C/MCP once the proxy is available; -2. broaden catalog scoring beyond explicit document/movement lane choice into unfamiliar 1C asks; -3. grow primitive descriptors only where live replay shows a real evidence gap; -4. keep phase19, phase21, phase22, value-flow, metadata ambiguity, and inventory-stock canaries as regression gates. +1. reconnect or restart the 1C toolkit polling side, then rerun the inventory canary against live 1C/MCP; +2. rerun a mixed cross-stage canary after the inventory canary is semantically clean; +3. continue broadening catalog scoring into unfamiliar 1C asks where metadata surface and data-need graph can pick reviewed lanes; +4. grow primitive descriptors only where live replay shows a real evidence gap; +5. keep phase19, phase21, phase22, value-flow, metadata ambiguity, and inventory-stock canaries as regression gates. The key rule remains: diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index 8ab8578..5552952 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -79,6 +79,7 @@ It now documents a turnaround that is already operational in code, already mater - inventory stock snapshot, supplier overlap, purchase provenance, and sale trace are now reviewed catalog chain templates; generic free-form inventory execution remains forbidden, and evidence must pass through reviewed exact recipe bridges; - runtime bridge and answer adapter now keep unsupported inventory route templates behind an explicit user-facing boundary instead of letting template planning look like confirmed stock/supplier/purchase/sale evidence; - inventory catalog templates now bridge through existing exact inventory recipes (`41.01` scoped stock, supplier overlap, purchase provenance, and sale trace) inside the bounded MCP discovery pilot, while missing selected-item anchors still clarify instead of guessing; + - unambiguous metadata surfaces can now infer the next reviewed lane from `Document.*`, `Register.*`, or `Catalog.*` objects even before upstream labels `downstream_route_family`, while mixed surfaces still do not guess; - live map sync: [20 - planner_autonomy_consolidation_2026-05-01.md](./20%20-%20planner_autonomy_consolidation_2026-05-01.md) Current honest status: @@ -90,8 +91,8 @@ Current honest status: - open-world bounded-autonomy readiness: `~85%` - Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice - active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions -- Planner Autonomy Consolidation progress: `~74%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, and exact inventory recipe bridging validated locally, but live replay for the new bridge and broader unfamiliar 1C asks still pending -- graph snapshot after latest rebuild: `5930 nodes`, `12884 edges`, `135 communities` +- Planner Autonomy Consolidation progress: `~78%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, and unambiguous metadata-surface lane inference validated locally, but live replay for the new bridge is currently blocked by missing active 1C polling and broader unfamiliar 1C asks still need replay-backed growth +- graph snapshot after latest rebuild: `5937 nodes`, `12899 edges`, `138 communities` - current breakpoint: - the validated hot paths are no longer structurally broken; - flagship continuity collapse is no longer the primary risk; @@ -134,6 +135,8 @@ Latest live proof now includes: - inventory template lift accepted locally: catalog/data-need/planner/turn-input slice passed `139/139` with `6` skipped; full MCP-discovery slice passed `276/276` with `9` skipped; build passed; graphify stayed at `5912 nodes`, `12833 edges`, `138 communities` - inventory runtime-boundary hardening accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `68/68` with `1` skipped; full MCP-discovery slice passed `277/277` with `9` skipped; build passed; graphify rebuilt to `5913 nodes`, `12837 edges`, `138 communities` - inventory exact-runtime bridge accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `70/70` with `1` skipped; full MCP-discovery slice passed `279/279` with `9` skipped; build passed; graphify rebuilt to `5930 nodes`, `12884 edges`, `135 communities` +- unambiguous metadata-surface lane inference accepted locally: planner slice passed `36/36`; full MCP-discovery slice passed `281/281` with `9` skipped; build passed; graphify rebuilt to `5937 nodes`, `12899 edges`, `138 communities` +- live inventory exact-bridge rerun `inventory_stock_exact_bridge_live_20260501_after_runtime_bridge` is recorded as infrastructure-blocked, not accepted: route/intent/recipe/capability matched, but MCP calls aborted and direct `get_metadata` timed out while proxy health showed `active_sessions_count=0` with pending commands Current architectural reading: diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 24bbd6e..8c9dab5 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -123,6 +123,59 @@ function mergeCatalogPrimitivesWithFallback(catalogPrimitives, fallbackPrimitive } return result; } +function normalizeMetadataSurfaceToken(value) { + return String(value ?? "").trim().toLowerCase().replace(/[\s_.-]+/g, ""); +} +function metadataSurfaceValueSuggestsDocument(value) { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("document") || token.includes("invoice") || token.includes("waybill") || token.includes("act"); +} +function metadataSurfaceValueSuggestsMovement(value) { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("register") || token.includes("movement") || token.includes("operation") || token.includes("bank"); +} +function metadataSurfaceValueSuggestsCatalog(value) { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("catalog") || token.includes("directory"); +} +function routeFamilyFromMetadataSurfaceRef(surface) { + if (!surface || surface.ambiguity_detected) { + return null; + } + if (surface.downstream_route_family) { + return surface.downstream_route_family; + } + const values = [surface.selected_entity_set ?? "", ...surface.selected_surface_objects]; + const documentHit = values.some(metadataSurfaceValueSuggestsDocument); + const movementHit = values.some(metadataSurfaceValueSuggestsMovement); + const catalogHit = values.some(metadataSurfaceValueSuggestsCatalog); + const hitCount = [documentHit, movementHit, catalogHit].filter(Boolean).length; + if (hitCount !== 1) { + return null; + } + if (documentHit) { + return "document_evidence"; + } + if (movementHit) { + return "movement_evidence"; + } + return "catalog_drilldown"; +} +function inferredRouteFamilyFromMetadataSurfaceRef(surface) { + return Boolean(surface && !surface.downstream_route_family && routeFamilyFromMetadataSurfaceRef(surface)); +} +function preferredPrimitiveForRouteFamily(routeFamily) { + if (routeFamily === "document_evidence") { + return "query_documents"; + } + if (routeFamily === "movement_evidence") { + return "query_movements"; + } + if (routeFamily === "catalog_drilldown") { + return "drilldown_related_objects"; + } + return null; +} function preferredPrimitiveFromMetadataSurface(surface) { if (surface?.ambiguity_detected) { return null; @@ -131,16 +184,7 @@ function preferredPrimitiveFromMetadataSurface(surface) { if (recommendedPrimitive) { return recommendedPrimitive; } - if (surface?.downstream_route_family === "document_evidence") { - return "query_documents"; - } - if (surface?.downstream_route_family === "movement_evidence") { - return "query_movements"; - } - if (surface?.downstream_route_family === "catalog_drilldown") { - return "drilldown_related_objects"; - } - return null; + return preferredPrimitiveForRouteFamily(routeFamilyFromMetadataSurfaceRef(surface)); } function preferredPrimitiveFromExplicitDataNeedGraph(graph) { const factFamily = lower(graph?.business_fact_family); @@ -289,7 +333,8 @@ function budgetOverrideFor(input, recipe) { } function routeFamilyFromThinMetadataSurfaceInput(input) { const surface = input.metadataSurface ?? null; - if (!surface || surface.ambiguity_detected || !surface.downstream_route_family || !surface.recommended_next_primitive) { + const surfaceRouteFamily = routeFamilyFromMetadataSurfaceRef(surface); + if (!surface || surface.ambiguity_detected || !surfaceRouteFamily) { return null; } const meaning = input.turnMeaning ?? null; @@ -305,20 +350,16 @@ function routeFamilyFromThinMetadataSurfaceInput(input) { return null; } if (graphFactFamily === "document_evidence" || includesAny(combined, ["document", "documents", "list_documents"])) { - return surface.downstream_route_family === "document_evidence" ? "document_evidence" : null; + return surfaceRouteFamily === "document_evidence" ? "document_evidence" : null; } if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) { - return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null; + return surfaceRouteFamily === "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; + return surfaceRouteFamily === "catalog_drilldown" ? "catalog_drilldown" : null; } if (!graphFactFamily && !domain && !action) { - if (surface.downstream_route_family === "document_evidence" || - surface.downstream_route_family === "movement_evidence" || - surface.downstream_route_family === "catalog_drilldown") { - return surface.downstream_route_family; - } + return surfaceRouteFamily; } return null; } @@ -370,7 +411,12 @@ function recipeFor(input) { primitives: primitiveSelection.primitives, reason: "planner_selected_document_from_confirmed_metadata_surface_ref", chainSummary: "Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } if (thinSurfaceRouteFamily === "movement_evidence") { @@ -389,7 +435,12 @@ function recipeFor(input) { primitives: primitiveSelection.primitives, reason: "planner_selected_movement_from_confirmed_metadata_surface_ref", chainSummary: "Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } if (thinSurfaceRouteFamily === "catalog_drilldown") { @@ -408,7 +459,12 @@ function recipeFor(input) { primitives: primitiveSelection.primitives, reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref", 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.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } if (graphFactFamily === "value_flow") { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index 8ae179f..779c7b9 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -249,6 +249,73 @@ function mergeCatalogPrimitivesWithFallback( return result; } +function normalizeMetadataSurfaceToken(value: unknown): string { + return String(value ?? "").trim().toLowerCase().replace(/[\s_.-]+/g, ""); +} + +function metadataSurfaceValueSuggestsDocument(value: unknown): boolean { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("document") || token.includes("invoice") || token.includes("waybill") || token.includes("act"); +} + +function metadataSurfaceValueSuggestsMovement(value: unknown): boolean { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("register") || token.includes("movement") || token.includes("operation") || token.includes("bank"); +} + +function metadataSurfaceValueSuggestsCatalog(value: unknown): boolean { + const token = normalizeMetadataSurfaceToken(value); + return token.includes("catalog") || token.includes("directory"); +} + +function routeFamilyFromMetadataSurfaceRef( + surface: AssistantMcpDiscoveryMetadataSurfaceRef | null | undefined +): AssistantMcpDiscoveryMetadataRouteFamily | null { + if (!surface || surface.ambiguity_detected) { + return null; + } + if (surface.downstream_route_family) { + return surface.downstream_route_family; + } + + const values = [surface.selected_entity_set ?? "", ...surface.selected_surface_objects]; + const documentHit = values.some(metadataSurfaceValueSuggestsDocument); + const movementHit = values.some(metadataSurfaceValueSuggestsMovement); + const catalogHit = values.some(metadataSurfaceValueSuggestsCatalog); + const hitCount = [documentHit, movementHit, catalogHit].filter(Boolean).length; + if (hitCount !== 1) { + return null; + } + if (documentHit) { + return "document_evidence"; + } + if (movementHit) { + return "movement_evidence"; + } + return "catalog_drilldown"; +} + +function inferredRouteFamilyFromMetadataSurfaceRef( + surface: AssistantMcpDiscoveryMetadataSurfaceRef | null | undefined +): boolean { + return Boolean(surface && !surface.downstream_route_family && routeFamilyFromMetadataSurfaceRef(surface)); +} + +function preferredPrimitiveForRouteFamily( + routeFamily: AssistantMcpDiscoveryMetadataRouteFamily | null +): AssistantMcpDiscoveryPrimitive | null { + if (routeFamily === "document_evidence") { + return "query_documents"; + } + if (routeFamily === "movement_evidence") { + return "query_movements"; + } + if (routeFamily === "catalog_drilldown") { + return "drilldown_related_objects"; + } + return null; +} + function preferredPrimitiveFromMetadataSurface( surface: AssistantMcpDiscoveryMetadataSurfaceRef | null | undefined ): AssistantMcpDiscoveryPrimitive | null { @@ -259,16 +326,7 @@ function preferredPrimitiveFromMetadataSurface( if (recommendedPrimitive) { return recommendedPrimitive; } - if (surface?.downstream_route_family === "document_evidence") { - return "query_documents"; - } - if (surface?.downstream_route_family === "movement_evidence") { - return "query_movements"; - } - if (surface?.downstream_route_family === "catalog_drilldown") { - return "drilldown_related_objects"; - } - return null; + return preferredPrimitiveForRouteFamily(routeFamilyFromMetadataSurfaceRef(surface)); } function preferredPrimitiveFromExplicitDataNeedGraph( @@ -466,7 +524,8 @@ function routeFamilyFromThinMetadataSurfaceInput( input: AssistantMcpDiscoveryPlannerInput ): AssistantMcpDiscoveryMetadataRouteFamily | null { const surface = input.metadataSurface ?? null; - if (!surface || surface.ambiguity_detected || !surface.downstream_route_family || !surface.recommended_next_primitive) { + const surfaceRouteFamily = routeFamilyFromMetadataSurfaceRef(surface); + if (!surface || surface.ambiguity_detected || !surfaceRouteFamily) { return null; } const meaning = input.turnMeaning ?? null; @@ -483,22 +542,16 @@ function routeFamilyFromThinMetadataSurfaceInput( return null; } if (graphFactFamily === "document_evidence" || includesAny(combined, ["document", "documents", "list_documents"])) { - return surface.downstream_route_family === "document_evidence" ? "document_evidence" : null; + return surfaceRouteFamily === "document_evidence" ? "document_evidence" : null; } if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) { - return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null; + return surfaceRouteFamily === "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; + return surfaceRouteFamily === "catalog_drilldown" ? "catalog_drilldown" : null; } if (!graphFactFamily && !domain && !action) { - if ( - surface.downstream_route_family === "document_evidence" || - surface.downstream_route_family === "movement_evidence" || - surface.downstream_route_family === "catalog_drilldown" - ) { - return surface.downstream_route_family; - } + return surfaceRouteFamily; } return null; } @@ -555,7 +608,12 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { reason: "planner_selected_document_from_confirmed_metadata_surface_ref", chainSummary: "Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } if (thinSurfaceRouteFamily === "movement_evidence") { @@ -575,7 +633,12 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { reason: "planner_selected_movement_from_confirmed_metadata_surface_ref", chainSummary: "Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } if (thinSurfaceRouteFamily === "catalog_drilldown") { @@ -595,7 +658,12 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref", 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.", - extraReasons: primitiveSelection.reasonCodes + extraReasons: [ + ...primitiveSelection.reasonCodes, + ...(inferredRouteFamilyFromMetadataSurfaceRef(input.metadataSurface) + ? ["planner_inferred_next_lane_from_unambiguous_metadata_surface"] + : []) + ] }); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index f02042b..fb36c79 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -218,6 +218,31 @@ describe("assistant MCP discovery planner", () => { expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search"); }); + it("infers document evidence from an unambiguous document metadata surface even before a downstream lane is labeled", () => { + const result = planAssistantMcpDiscovery({ + metadataSurface: { + selected_entity_set: "Document", + selected_surface_objects: ["Document.InvoiceIssued"], + downstream_route_family: null, + route_family_selection_basis: null, + recommended_next_primitive: null, + ambiguity_detected: false, + ambiguity_entity_sets: [] + }, + turnMeaning: { + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.selected_chain_id).toBe("document_evidence"); + expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]); + expect(result.proposed_primitives).not.toContain("query_movements"); + expect(result.reason_codes).toContain("planner_inferred_next_lane_from_unambiguous_metadata_surface"); + expect(result.reason_codes).toContain("planner_surface_aware_next_lane_from_confirmed_metadata_objects"); + }); + it("builds a movement discovery plan without aggregating value-flow totals", () => { const result = planAssistantMcpDiscovery({ dataNeedGraph: { @@ -325,6 +350,31 @@ describe("assistant MCP discovery planner", () => { expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search"); }); + it("infers movement evidence from an unambiguous register metadata surface even before a downstream lane is labeled", () => { + const result = planAssistantMcpDiscovery({ + metadataSurface: { + selected_entity_set: "AccumulationRegister", + selected_surface_objects: ["AccumulationRegister.BankOperations"], + downstream_route_family: null, + route_family_selection_basis: null, + recommended_next_primitive: null, + ambiguity_detected: false, + ambiguity_entity_sets: [] + }, + turnMeaning: { + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.selected_chain_id).toBe("movement_evidence"); + expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]); + expect(result.proposed_primitives).not.toContain("query_documents"); + expect(result.reason_codes).toContain("planner_inferred_next_lane_from_unambiguous_metadata_surface"); + expect(result.reason_codes).toContain("planner_surface_aware_next_lane_from_confirmed_metadata_objects"); + }); + it("can select catalog drilldown directly from a confirmed catalog metadata surface when the follow-up itself is thin", () => { const result = planAssistantMcpDiscovery({ metadataSurface: {