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[]; user_actionable_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; additionalMissingAxes?: string[] | null; } 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)); } 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 { 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 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({ 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, user_actionable_missing_axes: actionableAxisGaps, 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 }; }