diff --git a/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js new file mode 100644 index 0000000..5c75a5e --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js @@ -0,0 +1,160 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = void 0; +exports.buildAssistantEvidencePlanner = buildAssistantEvidencePlanner; +exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = "assistant_evidence_planner_v1"; +function toNonEmptyString(value) { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} +function normalizeReasonCode(value) { + const normalized = value + .trim() + .replace(/[^\p{L}\p{N}_.:-]+/gu, "_") + .replace(/^_+|_+$/g, "") + .toLowerCase(); + return normalized.length > 0 ? normalized.slice(0, 120) : null; +} +function pushReason(target, value) { + const normalized = normalizeReasonCode(value); + if (normalized && !target.includes(normalized)) { + target.push(normalized); + } +} +function uniqueStrings(values) { + const result = []; + for (const value of values) { + const text = toNonEmptyString(value); + if (text && !result.includes(text)) { + result.push(text); + } + } + return result; +} +function providedAxesFromMeaning(meaning) { + const result = []; + if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { + result.push("counterparty"); + result.push("business_entity"); + } + if (toNonEmptyString(meaning?.explicit_organization_scope)) { + result.push("organization"); + } + if (toNonEmptyString(meaning?.explicit_date_scope)) { + result.push("period"); + } + if (toNonEmptyString(meaning?.asked_aggregation_axis)) { + result.push("aggregate_axis"); + } + if (toNonEmptyString(meaning?.metadata_scope_hint)) { + result.push("metadata_scope"); + } + return uniqueStrings(result); +} +function missingAxes(requiredAxes, providedAxes) { + return requiredAxes.filter((axis) => !providedAxes.includes(axis)); +} +function coverageExpectationFor(graph) { + if (graph?.proof_expectation === "bounded_inference") { + return "bounded_inference"; + } + if (graph?.proof_expectation === "clarification_required") { + return "clarification"; + } + return "confirmed_coverage"; +} +function answerModeFor(input) { + if (input.plannerStatus === "needs_clarification") { + return "clarification_required"; + } + if (input.plannerStatus === "blocked") { + return "checked_sources_only"; + } + if (input.coverageExpectation === "bounded_inference") { + return "bounded_business_inference"; + } + if (input.coverageExpectation === "clarification") { + return "clarification_required"; + } + return "confirmed_business_answer"; +} +function requiredUserLayersFor(answerMode) { + if (answerMode === "clarification_required") { + return ["clarifying_question", "why_needed", "available_calculation"]; + } + if (answerMode === "bounded_business_inference") { + return ["business_conclusion", "key_figures", "evidence_boundary", "next_step"]; + } + if (answerMode === "checked_sources_only") { + return ["checked_sources_boundary", "unknowns", "next_probe"]; + } + return ["direct_business_answer", "key_figures", "evidence_boundary", "next_step"]; +} +function buildAssistantEvidencePlanner(input) { + const graph = input.dataNeedGraph ?? null; + const plan = input.discoveryPlan; + const turnMeaning = plan.turn_meaning_ref; + const requiredAxes = uniqueStrings(plan.required_axes); + const providedAxes = providedAxesFromMeaning(turnMeaning); + const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); + const axisGaps = missingAxes(requiredAxes, providedAxes); + const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]); + const coverageExpectation = coverageExpectationFor(graph); + const answerMode = answerModeFor({ + plannerStatus: input.plannerStatus, + coverageExpectation + }); + const reasonCodes = uniqueStrings(plan.reason_codes); + pushReason(reasonCodes, "evidence_planner_contract_built"); + if (graph) { + pushReason(reasonCodes, "evidence_planner_consumed_data_need_graph"); + } + if (clarificationGaps.length > 0) { + pushReason(reasonCodes, "evidence_planner_has_missing_axes_or_gaps"); + } + return { + schema_version: exports.ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION, + policy_owner: "assistantEvidencePlanner", + planner_status: input.plannerStatus, + semantic_data_need: toNonEmptyString(input.semanticDataNeed), + selected_chain_id: input.selectedChainId, + data_need: { + business_fact_family: graph?.business_fact_family ?? null, + action_family: graph?.action_family ?? null, + aggregation_need: graph?.aggregation_need ?? null, + comparison_need: graph?.comparison_need ?? null, + ranking_need: graph?.ranking_need ?? null, + time_scope_need: graph?.time_scope_need ?? null, + proof_expectation: graph?.proof_expectation ?? null, + subject_candidates: uniqueStrings(graph?.subject_candidates ?? []) + }, + evidence_axes: { + required_axes: requiredAxes, + provided_axes: providedAxes, + missing_axes: axisGaps, + clarification_gaps: clarificationGaps + }, + primitive_plan: { + selected_chain_id: input.selectedChainId, + allowed_primitives: plan.allowed_primitives, + rejected_primitives: plan.rejected_primitives, + execution_budget: plan.execution_budget + }, + coverage_gate: { + requires_evidence_gate: true, + expected_coverage: coverageExpectation, + answer_permission_if_satisfied: answerMode, + answer_may_use_raw_model_claims: false + }, + answer_contract: { + answer_mode: answerMode, + required_user_layers: requiredUserLayersFor(answerMode), + forbidden_overclaim_flags: uniqueStrings(graph?.forbidden_overclaim_flags ?? []), + must_keep_internal_mechanics_hidden: true + }, + reason_codes: reasonCodes + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js index 26360e2..5e139d4 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js @@ -38,6 +38,10 @@ function isRouteCandidateContract(value) { return (record?.schema_version === "assistant_mcp_route_candidate_v1" && record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge"); } +function isEvidencePlannerContract(value) { + const record = toRecordObject(value); + return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner"; +} function resolveEntryPoint(input) { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { return input.entryPoint; @@ -51,6 +55,10 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) { const entryPoint = resolveEntryPoint(input); const bridge = toRecordObject(entryPoint?.bridge); const planner = toRecordObject(bridge?.planner); + const evidencePlan = isEvidencePlannerContract(planner?.evidence_plan) ? planner.evidence_plan : null; + const evidencePlanAxes = toRecordObject(evidencePlan?.evidence_axes); + const evidencePlanCoverageGate = toRecordObject(evidencePlan?.coverage_gate); + const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null; const answerDraft = toRecordObject(bridge?.answer_draft); @@ -61,6 +69,11 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) { mcp_discovery_hot_runtime_wired: false, mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status), mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id), + mcp_discovery_evidence_plan_v1: evidencePlan, + mcp_discovery_evidence_plan_status: toNonEmptyString(evidencePlan?.planner_status), + mcp_discovery_evidence_plan_answer_mode: toNonEmptyString(evidencePlanAnswerContract?.answer_mode), + mcp_discovery_evidence_plan_missing_axes: toStringArray(evidencePlanAxes?.missing_axes), + mcp_discovery_evidence_plan_expected_coverage: toNonEmptyString(evidencePlanCoverageGate?.expected_coverage), mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches), mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status), mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match), diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 2c7ac9e..82e2276 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -4,6 +4,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = void 0; exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery; const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy"); const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex"); +const assistantEvidencePlanner_1 = require("./assistantEvidencePlanner"); exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1"; const CHUNKED_COVERAGE_PROBE_BUDGET = 30; function toNonEmptyString(value) { @@ -1139,6 +1140,13 @@ function planAssistantMcpDiscovery(input) { else { pushReason(reasonCodes, "planner_needs_more_user_or_scope_context"); } + const evidencePlan = (0, assistantEvidencePlanner_1.buildAssistantEvidencePlanner)({ + selectedChainId: recipe.chainId, + plannerStatus, + semanticDataNeed, + dataNeedGraph, + discoveryPlan: plan + }); return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPlanner", @@ -1152,6 +1160,7 @@ function planAssistantMcpDiscovery(input) { catalog_chain_template_alignment: catalogChainTemplateAlignment, proposed_primitives: recipe.primitives, required_axes: recipe.axes, + evidence_plan: evidencePlan, discovery_plan: plan, catalog_review: adjustedReview, reason_codes: reasonCodes diff --git a/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts new file mode 100644 index 0000000..be30c16 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts @@ -0,0 +1,251 @@ +import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph"; +import type { + AssistantMcpDiscoveryPlanContract, + AssistantMcpDiscoveryPrimitive, + AssistantMcpDiscoveryTurnMeaningRef +} from "./assistantMcpDiscoveryPolicy"; + +export const ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION = "assistant_evidence_planner_v1" as const; + +export type AssistantEvidencePlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked"; +export type AssistantEvidenceCoverageExpectation = "confirmed_coverage" | "bounded_inference" | "clarification"; +export type AssistantEvidenceAnswerMode = + | "confirmed_business_answer" + | "bounded_business_inference" + | "clarification_required" + | "checked_sources_only"; + +export interface AssistantEvidenceDataNeedContract { + business_fact_family: string | null; + action_family: string | null; + aggregation_need: string | null; + comparison_need: string | null; + ranking_need: string | null; + time_scope_need: string | null; + proof_expectation: string | null; + subject_candidates: string[]; +} + +export interface AssistantEvidenceAxesContract { + required_axes: string[]; + provided_axes: string[]; + missing_axes: string[]; + clarification_gaps: string[]; +} + +export interface AssistantEvidencePrimitivePlanContract { + selected_chain_id: string; + allowed_primitives: AssistantMcpDiscoveryPrimitive[]; + rejected_primitives: string[]; + execution_budget: AssistantMcpDiscoveryPlanContract["execution_budget"]; +} + +export interface AssistantEvidenceCoverageGateContract { + requires_evidence_gate: true; + expected_coverage: AssistantEvidenceCoverageExpectation; + answer_permission_if_satisfied: AssistantEvidenceAnswerMode; + answer_may_use_raw_model_claims: false; +} + +export interface AssistantEvidenceAnswerContract { + answer_mode: AssistantEvidenceAnswerMode; + required_user_layers: string[]; + forbidden_overclaim_flags: string[]; + must_keep_internal_mechanics_hidden: true; +} + +export interface AssistantEvidencePlannerContract { + schema_version: typeof ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION; + policy_owner: "assistantEvidencePlanner"; + planner_status: AssistantEvidencePlannerStatus; + semantic_data_need: string | null; + selected_chain_id: string; + data_need: AssistantEvidenceDataNeedContract; + evidence_axes: AssistantEvidenceAxesContract; + primitive_plan: AssistantEvidencePrimitivePlanContract; + coverage_gate: AssistantEvidenceCoverageGateContract; + answer_contract: AssistantEvidenceAnswerContract; + reason_codes: string[]; +} + +export interface BuildAssistantEvidencePlannerInput { + selectedChainId: string; + plannerStatus: AssistantEvidencePlannerStatus; + semanticDataNeed?: string | null; + dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null; + discoveryPlan: AssistantMcpDiscoveryPlanContract; +} + +function toNonEmptyString(value: unknown): string | null { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} + +function normalizeReasonCode(value: string): string | null { + const normalized = value + .trim() + .replace(/[^\p{L}\p{N}_.:-]+/gu, "_") + .replace(/^_+|_+$/g, "") + .toLowerCase(); + return normalized.length > 0 ? normalized.slice(0, 120) : null; +} + +function pushReason(target: string[], value: string): void { + const normalized = normalizeReasonCode(value); + if (normalized && !target.includes(normalized)) { + target.push(normalized); + } +} + +function uniqueStrings(values: unknown[]): string[] { + const result: string[] = []; + for (const value of values) { + const text = toNonEmptyString(value); + if (text && !result.includes(text)) { + result.push(text); + } + } + return result; +} + +function providedAxesFromMeaning(meaning: AssistantMcpDiscoveryTurnMeaningRef | null): string[] { + const result: string[] = []; + if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { + result.push("counterparty"); + result.push("business_entity"); + } + if (toNonEmptyString(meaning?.explicit_organization_scope)) { + result.push("organization"); + } + if (toNonEmptyString(meaning?.explicit_date_scope)) { + result.push("period"); + } + if (toNonEmptyString(meaning?.asked_aggregation_axis)) { + result.push("aggregate_axis"); + } + if (toNonEmptyString(meaning?.metadata_scope_hint)) { + result.push("metadata_scope"); + } + return uniqueStrings(result); +} + +function missingAxes(requiredAxes: string[], providedAxes: string[]): string[] { + return requiredAxes.filter((axis) => !providedAxes.includes(axis)); +} + +function coverageExpectationFor( + graph: AssistantMcpDiscoveryDataNeedGraphContract | null +): AssistantEvidenceCoverageExpectation { + if (graph?.proof_expectation === "bounded_inference") { + return "bounded_inference"; + } + if (graph?.proof_expectation === "clarification_required") { + return "clarification"; + } + return "confirmed_coverage"; +} + +function answerModeFor(input: { + plannerStatus: AssistantEvidencePlannerStatus; + coverageExpectation: AssistantEvidenceCoverageExpectation; +}): AssistantEvidenceAnswerMode { + if (input.plannerStatus === "needs_clarification") { + return "clarification_required"; + } + if (input.plannerStatus === "blocked") { + return "checked_sources_only"; + } + if (input.coverageExpectation === "bounded_inference") { + return "bounded_business_inference"; + } + if (input.coverageExpectation === "clarification") { + return "clarification_required"; + } + return "confirmed_business_answer"; +} + +function requiredUserLayersFor(answerMode: AssistantEvidenceAnswerMode): string[] { + if (answerMode === "clarification_required") { + return ["clarifying_question", "why_needed", "available_calculation"]; + } + if (answerMode === "bounded_business_inference") { + return ["business_conclusion", "key_figures", "evidence_boundary", "next_step"]; + } + if (answerMode === "checked_sources_only") { + return ["checked_sources_boundary", "unknowns", "next_probe"]; + } + return ["direct_business_answer", "key_figures", "evidence_boundary", "next_step"]; +} + +export function buildAssistantEvidencePlanner( + input: BuildAssistantEvidencePlannerInput +): AssistantEvidencePlannerContract { + const graph = input.dataNeedGraph ?? null; + const plan = input.discoveryPlan; + const turnMeaning = plan.turn_meaning_ref; + const requiredAxes = uniqueStrings(plan.required_axes); + const providedAxes = providedAxesFromMeaning(turnMeaning); + const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); + const axisGaps = missingAxes(requiredAxes, providedAxes); + const clarificationGaps = uniqueStrings([...graphClarificationGaps, ...axisGaps]); + const coverageExpectation = coverageExpectationFor(graph); + const answerMode = answerModeFor({ + plannerStatus: input.plannerStatus, + coverageExpectation + }); + const reasonCodes = uniqueStrings(plan.reason_codes); + + pushReason(reasonCodes, "evidence_planner_contract_built"); + if (graph) { + pushReason(reasonCodes, "evidence_planner_consumed_data_need_graph"); + } + if (clarificationGaps.length > 0) { + pushReason(reasonCodes, "evidence_planner_has_missing_axes_or_gaps"); + } + + return { + schema_version: ASSISTANT_EVIDENCE_PLANNER_SCHEMA_VERSION, + policy_owner: "assistantEvidencePlanner", + planner_status: input.plannerStatus, + semantic_data_need: toNonEmptyString(input.semanticDataNeed), + selected_chain_id: input.selectedChainId, + data_need: { + business_fact_family: graph?.business_fact_family ?? null, + action_family: graph?.action_family ?? null, + aggregation_need: graph?.aggregation_need ?? null, + comparison_need: graph?.comparison_need ?? null, + ranking_need: graph?.ranking_need ?? null, + time_scope_need: graph?.time_scope_need ?? null, + proof_expectation: graph?.proof_expectation ?? null, + subject_candidates: uniqueStrings(graph?.subject_candidates ?? []) + }, + evidence_axes: { + required_axes: requiredAxes, + provided_axes: providedAxes, + missing_axes: axisGaps, + clarification_gaps: clarificationGaps + }, + primitive_plan: { + selected_chain_id: input.selectedChainId, + allowed_primitives: plan.allowed_primitives, + rejected_primitives: plan.rejected_primitives, + execution_budget: plan.execution_budget + }, + coverage_gate: { + requires_evidence_gate: true, + expected_coverage: coverageExpectation, + answer_permission_if_satisfied: answerMode, + answer_may_use_raw_model_claims: false + }, + answer_contract: { + answer_mode: answerMode, + required_user_layers: requiredUserLayersFor(answerMode), + forbidden_overclaim_flags: uniqueStrings(graph?.forbidden_overclaim_flags ?? []), + must_keep_internal_mechanics_hidden: true + }, + reason_codes: reasonCodes + }; +} diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts index c9d7dda..c0e862b 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts @@ -1,5 +1,6 @@ import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge"; +import type { AssistantEvidencePlannerContract } from "./assistantEvidencePlanner"; export interface AssistantMcpDiscoveryDebugAttachmentFields { assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null; @@ -8,6 +9,11 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields { mcp_discovery_hot_runtime_wired: false; mcp_discovery_bridge_status: string | null; mcp_discovery_selected_chain_id: string | null; + mcp_discovery_evidence_plan_v1: AssistantEvidencePlannerContract | null; + mcp_discovery_evidence_plan_status: string | null; + mcp_discovery_evidence_plan_answer_mode: string | null; + mcp_discovery_evidence_plan_missing_axes: string[]; + mcp_discovery_evidence_plan_expected_coverage: string | null; mcp_discovery_catalog_chain_template_matches: string[]; mcp_discovery_catalog_chain_alignment_status: string | null; mcp_discovery_catalog_chain_top_match: string | null; @@ -78,6 +84,11 @@ function isRouteCandidateContract(value: unknown): value is AssistantMcpRouteCan ); } +function isEvidencePlannerContract(value: unknown): value is AssistantEvidencePlannerContract { + const record = toRecordObject(value); + return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner"; +} + function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { return input.entryPoint; @@ -95,6 +106,10 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields( const entryPoint = resolveEntryPoint(input); const bridge = toRecordObject(entryPoint?.bridge); const planner = toRecordObject(bridge?.planner); + const evidencePlan = isEvidencePlannerContract(planner?.evidence_plan) ? planner.evidence_plan : null; + const evidencePlanAxes = toRecordObject(evidencePlan?.evidence_axes); + const evidencePlanCoverageGate = toRecordObject(evidencePlan?.coverage_gate); + const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null; const answerDraft = toRecordObject(bridge?.answer_draft); @@ -106,6 +121,11 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields( mcp_discovery_hot_runtime_wired: false, mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status), mcp_discovery_selected_chain_id: toNonEmptyString(planner?.selected_chain_id), + mcp_discovery_evidence_plan_v1: evidencePlan, + mcp_discovery_evidence_plan_status: toNonEmptyString(evidencePlan?.planner_status), + mcp_discovery_evidence_plan_answer_mode: toNonEmptyString(evidencePlanAnswerContract?.answer_mode), + mcp_discovery_evidence_plan_missing_axes: toStringArray(evidencePlanAxes?.missing_axes), + mcp_discovery_evidence_plan_expected_coverage: toNonEmptyString(evidencePlanCoverageGate?.expected_coverage), mcp_discovery_catalog_chain_template_matches: toStringArray(planner?.catalog_chain_template_matches), mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status), mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match), diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index b836120..ab14d82 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -15,6 +15,10 @@ import { type AssistantMcpCatalogPlanReview } from "./assistantMcpCatalogIndex"; import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph"; +import { + buildAssistantEvidencePlanner, + type AssistantEvidencePlannerContract +} from "./assistantEvidencePlanner"; export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const; @@ -91,6 +95,7 @@ export interface AssistantMcpDiscoveryPlannerContract { catalog_chain_template_alignment: AssistantMcpDiscoveryCatalogChainTemplateAlignment; proposed_primitives: AssistantMcpDiscoveryPrimitive[]; required_axes: string[]; + evidence_plan: AssistantEvidencePlannerContract; discovery_plan: AssistantMcpDiscoveryPlanContract; catalog_review: AssistantMcpCatalogPlanReview; reason_codes: string[]; @@ -1422,6 +1427,13 @@ export function planAssistantMcpDiscovery( } else { pushReason(reasonCodes, "planner_needs_more_user_or_scope_context"); } + const evidencePlan = buildAssistantEvidencePlanner({ + selectedChainId: recipe.chainId, + plannerStatus, + semanticDataNeed, + dataNeedGraph, + discoveryPlan: plan + }); return { schema_version: ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION, @@ -1436,6 +1448,7 @@ export function planAssistantMcpDiscovery( catalog_chain_template_alignment: catalogChainTemplateAlignment, proposed_primitives: recipe.primitives, required_axes: recipe.axes, + evidence_plan: evidencePlan, discovery_plan: plan, catalog_review: adjustedReview, reason_codes: reasonCodes diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts index da23d55..7e35786 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts @@ -16,6 +16,21 @@ function entryPointContract(overrides: Record = {}) { requires_user_clarification: false, planner: { selected_chain_id: "value_flow_ranking", + evidence_plan: { + schema_version: "assistant_evidence_planner_v1", + policy_owner: "assistantEvidencePlanner", + planner_status: "ready_for_execution", + selected_chain_id: "value_flow_ranking", + evidence_axes: { + missing_axes: [] + }, + coverage_gate: { + expected_coverage: "confirmed_coverage" + }, + answer_contract: { + answer_mode: "confirmed_business_answer" + } + }, catalog_chain_template_matches: ["value_flow_ranking", "value_flow"], catalog_chain_template_alignment: { alignment_status: "selected_matches_top", @@ -69,6 +84,10 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_hot_runtime_wired).toBe(false); expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready"); expect(debug.mcp_discovery_selected_chain_id).toBe("value_flow_ranking"); + expect(debug.mcp_discovery_evidence_plan_status).toBe("ready_for_execution"); + expect(debug.mcp_discovery_evidence_plan_answer_mode).toBe("confirmed_business_answer"); + expect(debug.mcp_discovery_evidence_plan_expected_coverage).toBe("confirmed_coverage"); + expect(debug.mcp_discovery_evidence_plan_missing_axes).toEqual([]); expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual(["value_flow_ranking", "value_flow"]); expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top"); expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking"); @@ -100,6 +119,11 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_hot_runtime_wired).toBe(false); expect(debug.mcp_discovery_bridge_status).toBeNull(); expect(debug.mcp_discovery_selected_chain_id).toBeNull(); + expect(debug.mcp_discovery_evidence_plan_v1).toBeNull(); + expect(debug.mcp_discovery_evidence_plan_status).toBeNull(); + expect(debug.mcp_discovery_evidence_plan_answer_mode).toBeNull(); + expect(debug.mcp_discovery_evidence_plan_expected_coverage).toBeNull(); + expect(debug.mcp_discovery_evidence_plan_missing_axes).toEqual([]); expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual([]); expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull(); expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull(); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index f4b7af2..ab85c59 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -46,6 +46,34 @@ describe("assistant MCP discovery planner", () => { expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]); expect(result.catalog_review.review_status).toBe("catalog_compatible"); expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false); + expect(result.evidence_plan).toMatchObject({ + schema_version: "assistant_evidence_planner_v1", + planner_status: "ready_for_execution", + selected_chain_id: "value_flow", + data_need: { + business_fact_family: "value_flow", + action_family: "turnover", + proof_expectation: "coverage_checked_fact" + }, + coverage_gate: { + expected_coverage: "confirmed_coverage", + answer_permission_if_satisfied: "confirmed_business_answer", + answer_may_use_raw_model_claims: false + }, + answer_contract: { + answer_mode: "confirmed_business_answer", + must_keep_internal_mechanics_hidden: true + } + }); + expect(result.evidence_plan.evidence_axes.required_axes).toEqual([ + "counterparty", + "period", + "aggregate_axis", + "amount", + "coverage_target" + ]); + 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.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({ @@ -284,6 +312,17 @@ describe("assistant MCP discovery planner", () => { selected_chain_matches_top: true }); expect(result.catalog_review.review_status).toBe("catalog_compatible"); + expect(result.evidence_plan.coverage_gate.expected_coverage).toBe("bounded_inference"); + expect(result.evidence_plan.answer_contract.answer_mode).toBe("bounded_business_inference"); + expect(result.evidence_plan.answer_contract.required_user_layers).toEqual([ + "business_conclusion", + "key_figures", + "evidence_boundary", + "next_step" + ]); + expect(result.evidence_plan.answer_contract.forbidden_overclaim_flags).toContain( + "no_unchecked_business_health_claim" + ); expect(result.reason_codes).toContain("planner_selected_business_overview_from_data_need_graph"); expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_business_overview"); });