1293 lines
72 KiB
TypeScript
1293 lines
72 KiB
TypeScript
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||
|
||
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
||
"assistant_mcp_discovery_answer_draft_v1" as const;
|
||
|
||
export type AssistantMcpDiscoveryAnswerMode =
|
||
| "confirmed_with_bounded_inference"
|
||
| "bounded_inference_only"
|
||
| "checked_sources_only"
|
||
| "needs_clarification"
|
||
| "blocked";
|
||
|
||
export interface AssistantMcpDiscoveryAnswerDraftContract {
|
||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION;
|
||
policy_owner: "assistantMcpDiscoveryAnswerAdapter";
|
||
answer_mode: AssistantMcpDiscoveryAnswerMode;
|
||
headline: string;
|
||
confirmed_lines: string[];
|
||
inference_lines: string[];
|
||
unknown_lines: string[];
|
||
limitation_lines: string[];
|
||
next_step_line: string | null;
|
||
internal_mechanics_allowed: false;
|
||
must_not_claim: string[];
|
||
reason_codes: string[];
|
||
}
|
||
|
||
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
|
||
|
||
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: string[]): string[] {
|
||
const result: string[] = [];
|
||
for (const value of values) {
|
||
const text = String(value ?? "").trim();
|
||
if (text && !result.includes(text)) {
|
||
result.push(text);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function formatNamedChoiceList(values: string[]): string {
|
||
return uniqueStrings(values)
|
||
.slice(0, 6)
|
||
.map((value, index) => `${index + 1}. ${value}`)
|
||
.join("; ");
|
||
}
|
||
|
||
function isInternalMechanicsLine(value: string): boolean {
|
||
const text = value.toLowerCase();
|
||
return (
|
||
text.includes("mcp fetch failed") ||
|
||
text.includes("this operation was aborted") ||
|
||
text.includes("entity-resolution") ||
|
||
text.includes("could not continue") ||
|
||
text.includes("checked catalog search step") ||
|
||
text.includes("primitive") ||
|
||
text.includes("query_documents") ||
|
||
text.includes("query_movements") ||
|
||
text.includes("resolve_entity_reference") ||
|
||
text.includes("probe_coverage") ||
|
||
text.includes("explain_evidence_basis") ||
|
||
text.includes("pilot_only_executes") ||
|
||
text.includes("pilot_") ||
|
||
text.includes("runtime_") ||
|
||
text.includes("planner_") ||
|
||
text.includes("catalog_") ||
|
||
text.includes("scope is not implemented yet") ||
|
||
text.includes("needs more scope before execution") ||
|
||
text.includes("mcp_execution_performed")
|
||
|| text.includes("confirmed 1c metadata surface")
|
||
|| text.includes("metadata surface family scores")
|
||
|| text.includes("available metadata object sets")
|
||
|| text.includes("selected metadata")
|
||
);
|
||
}
|
||
|
||
function isMcpTransportFailureLine(value: string): boolean {
|
||
const text = value.toLowerCase();
|
||
return (
|
||
text.includes("mcp fetch failed") ||
|
||
text.includes("this operation was aborted") ||
|
||
text.includes("operation was aborted")
|
||
);
|
||
}
|
||
|
||
function userFacingUnknowns(values: string[]): string[] {
|
||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||
}
|
||
|
||
function rankedValueFlowUnknownLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
if (!pilot.derived_ranked_value_flow) {
|
||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||
}
|
||
const ranking = pilot.derived_ranked_value_flow;
|
||
const period = ranking.period_scope ? `периода ${ranking.period_scope}` : "проверенного окна";
|
||
return [`Полный рейтинг контрагентов вне ${period} этим поиском не подтвержден.`];
|
||
}
|
||
|
||
function userFacingLimitations(values: string[]): string[] {
|
||
const result: string[] = [];
|
||
for (const value of uniqueStrings(values)) {
|
||
if (isMcpTransportFailureLine(value)) {
|
||
const line = "Доступ к 1С во время проверки оборвался; подтвержденные строки не получены.";
|
||
if (!result.includes(line)) {
|
||
result.push(line);
|
||
}
|
||
continue;
|
||
}
|
||
if (!isInternalMechanicsLine(value)) {
|
||
result.push(value);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantMcpDiscoveryAnswerMode {
|
||
if (pilot.pilot_status === "blocked") {
|
||
return "blocked";
|
||
}
|
||
if (pilot.pilot_status === "skipped_needs_clarification") {
|
||
return "needs_clarification";
|
||
}
|
||
if (
|
||
pilot.pilot_scope === "entity_resolution_search_v1" &&
|
||
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
|
||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")
|
||
) {
|
||
return "needs_clarification";
|
||
}
|
||
if (pilot.evidence.answer_permission === "confirmed_answer") {
|
||
return "confirmed_with_bounded_inference";
|
||
}
|
||
if (pilot.evidence.answer_permission === "bounded_inference") {
|
||
return "bounded_inference_only";
|
||
}
|
||
return "checked_sources_only";
|
||
}
|
||
|
||
function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" ||
|
||
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
|
||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1"
|
||
);
|
||
}
|
||
|
||
function isBusinessOverviewPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "business_overview_route_template_v1";
|
||
}
|
||
|
||
function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
|
||
}
|
||
|
||
function isMovementPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
|
||
}
|
||
|
||
function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "metadata_inspection_v1";
|
||
}
|
||
|
||
function isInventoryTemplatePilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "inventory_route_template_v1";
|
||
}
|
||
|
||
function isCatalogDrilldownPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
isMetadataPilot(pilot) &&
|
||
(pilot.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
|
||
pilot.reason_codes.includes("pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref"))
|
||
);
|
||
}
|
||
|
||
function isEntityResolutionPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return pilot.pilot_scope === "entity_resolution_search_v1";
|
||
}
|
||
|
||
function isMetadataLaneChoiceClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
|
||
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph")
|
||
);
|
||
}
|
||
|
||
function askedActionFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||
if (typeof action !== "string") {
|
||
return null;
|
||
}
|
||
const normalized = action.trim().toLowerCase();
|
||
return normalized.length > 0 ? normalized : null;
|
||
}
|
||
|
||
function unsupportedFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||
if (typeof unsupported !== "string") {
|
||
return null;
|
||
}
|
||
const normalized = unsupported.trim().toLowerCase();
|
||
return normalized.length > 0 ? normalized : null;
|
||
}
|
||
|
||
function firstEntityCandidate(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
|
||
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
|
||
: [];
|
||
for (const value of values) {
|
||
const text = String(value ?? "").trim();
|
||
if (text) {
|
||
return text;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_date_scope;
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const normalized = value.trim();
|
||
return normalized.length > 0 ? normalized : null;
|
||
}
|
||
|
||
function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const normalized = value.trim();
|
||
return normalized.length > 0 ? normalized : null;
|
||
}
|
||
|
||
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
dryRunHasAxis(pilot, "all_time_scope") ||
|
||
pilot.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected") ||
|
||
pilot.dry_run.reason_codes.includes("mcp_discovery_all_time_scope_signal_detected")
|
||
);
|
||
}
|
||
|
||
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||
const entity = firstEntityCandidate(pilot);
|
||
const period = explicitDateScope(pilot);
|
||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||
const periodPart = period
|
||
? ` за ${period}`
|
||
: hasAllTimeScope(pilot)
|
||
? " за все доступное время"
|
||
: " в проверенном окне";
|
||
return `${entityPart}${periodPart}`;
|
||
}
|
||
|
||
function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
isMovementPilot(pilot) ||
|
||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
|
||
askedActionFamily(pilot) === "list_movements" ||
|
||
unsupportedFamily(pilot) === "movement_evidence"
|
||
);
|
||
}
|
||
|
||
function isRankedValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph")
|
||
);
|
||
}
|
||
|
||
function isBidirectionalValueFlowComparisonClarification(
|
||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||
): boolean {
|
||
return (
|
||
pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph")
|
||
);
|
||
}
|
||
|
||
function isOpenScopeValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph")
|
||
);
|
||
}
|
||
|
||
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||
return (
|
||
isDocumentPilot(pilot) ||
|
||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
|
||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
|
||
askedActionFamily(pilot) === "list_documents" ||
|
||
unsupportedFamily(pilot) === "document_evidence"
|
||
);
|
||
}
|
||
|
||
function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||
const entity = firstEntityCandidate(pilot);
|
||
return entity ? ` по "${entity}"` : "";
|
||
}
|
||
|
||
function dryRunHasAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
|
||
}
|
||
|
||
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||
if (dryRunHasAxis(pilot, axis)) {
|
||
return false;
|
||
}
|
||
return pilot.dry_run.execution_steps.some((step) =>
|
||
step.missing_axis_options.some((option) => option.includes(axis))
|
||
);
|
||
}
|
||
|
||
function queryPlanClarificationGaps(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const values = pilot.evidence.query_plan.clarification_gaps;
|
||
return Array.isArray(values) ? uniqueStrings(values) : [];
|
||
}
|
||
|
||
function clarificationGapMissing(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
|
||
const gaps = queryPlanClarificationGaps(pilot);
|
||
if (gaps.length > 0) {
|
||
return gaps.includes(axis);
|
||
}
|
||
return dryRunMissingAxis(pilot, axis);
|
||
}
|
||
|
||
function clarificationNeedRu(
|
||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||
): { subject: string; verb: string } {
|
||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||
const organizationScopedOpenTotal =
|
||
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||
return {
|
||
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
|
||
verb: "\u043d\u0443\u0436\u043d\u043e"
|
||
};
|
||
}
|
||
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
|
||
const hasAccount = dryRunHasAxis(pilot, "account");
|
||
const needsOrganization = !hasCounterparty && !hasAccount && clarificationGapMissing(pilot, "organization");
|
||
if (needsPeriod && needsOrganization) {
|
||
return { subject: "проверяемый период и организацию", verb: "нужно" };
|
||
}
|
||
if (needsPeriod) {
|
||
return { subject: "проверяемый период", verb: "нужен" };
|
||
}
|
||
if (needsOrganization) {
|
||
return { subject: "организацию", verb: "нужно" };
|
||
}
|
||
return { subject: "контекст проверки", verb: "нужно" };
|
||
}
|
||
|
||
function clarificationNextStepLine(
|
||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||
laneLabel: string
|
||
): string {
|
||
const organizationScopedOpenTotal =
|
||
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
|
||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
|
||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
|
||
const needsPeriod = clarificationGapMissing(pilot, "period");
|
||
const needsOrganization = clarificationGapMissing(pilot, "organization");
|
||
const scopeSuffix = laneScopeSuffix(pilot);
|
||
if (organizationScopedOpenTotal && !needsPeriod) {
|
||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||
}
|
||
if (needsPeriod && needsOrganization) {
|
||
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||
}
|
||
if (needsPeriod) {
|
||
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||
}
|
||
if (needsOrganization) {
|
||
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||
}
|
||
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
|
||
}
|
||
|
||
function metadataRouteFamilyLabelRu(
|
||
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
|
||
): string | null {
|
||
if (routeFamily === "document_evidence") {
|
||
return "контур документов";
|
||
}
|
||
if (routeFamily === "movement_evidence") {
|
||
return "контур движений/регистров";
|
||
}
|
||
if (routeFamily === "catalog_drilldown") {
|
||
return "контур справочников и связанных объектов";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||
const askedMonthlyBreakdown =
|
||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
|
||
}
|
||
if (isInventoryTemplatePilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return "По exact inventory runtime в 1С найдены подтвержденные строки; ответ ограничен проверенным складским/товарным срезом.";
|
||
}
|
||
if (isInventoryTemplatePilot(pilot) && mode === "checked_sources_only") {
|
||
if (pilot.mcp_execution_performed) {
|
||
return "Exact inventory runtime был проверен, но подтвержденный складской/товарный факт в найденных строках не получен.";
|
||
}
|
||
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
|
||
}
|
||
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||
const overview = pilot.derived_business_overview;
|
||
const families: string[] = [];
|
||
if (
|
||
overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||
overview.outgoing_supplier_payout.rows_with_amount > 0
|
||
) {
|
||
families.push("денежный поток");
|
||
}
|
||
if (overview.activity_period) {
|
||
families.push("активность");
|
||
}
|
||
if (overview.tax_position) {
|
||
families.push("НДС-позиция");
|
||
}
|
||
if (overview.trading_margin_proxy) {
|
||
families.push("торговый margin proxy");
|
||
}
|
||
if (overview.debt_position) {
|
||
families.push("долговой срез на дату");
|
||
}
|
||
if (overview.debt_open_settlement_quality) {
|
||
families.push("качество открытых расчетов");
|
||
if (overview.debt_open_settlement_quality.age_signal) {
|
||
families.push("возрастной сигнал открытых расчетов");
|
||
}
|
||
}
|
||
if (overview.inventory_position) {
|
||
families.push("складской срез на дату");
|
||
}
|
||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||
if (!overview.tax_position) {
|
||
unknownFamilies.push("НДС");
|
||
}
|
||
if (!overview.debt_position) {
|
||
unknownFamilies.push("долговой срез");
|
||
}
|
||
unknownFamilies.push(overview.debt_open_settlement_quality ? "due-date просрочка" : "качество открытых расчетов");
|
||
unknownFamilies.push(overview.inventory_position ? "полноценная складская ликвидность" : "склад");
|
||
return `По данным 1С собран ограниченный бизнес-обзор: ${families.join(", ")} подтверждены найденными строками; ${unknownFamilies.join(", ")} остаются отдельными непроверенными областями.`;
|
||
}
|
||
if (isBusinessOverviewPilot(pilot) && mode === "checked_sources_only") {
|
||
return "Бизнес-обзор был запущен, но подтвержденные денежные или activity-сигналы в найденных строках не получены.";
|
||
}
|
||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
|
||
}
|
||
if (
|
||
isEntityResolutionPilot(pilot) &&
|
||
mode === "checked_sources_only" &&
|
||
pilot.derived_entity_resolution?.resolution_status === "not_found"
|
||
) {
|
||
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
|
||
}
|
||
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
return "По данным 1С можно построить ограниченный рейтинг по контрагентам на подтвержденных строках денежных движений.";
|
||
}
|
||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||
}
|
||
if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return "По схеме 1С удалось углубиться в контур справочников и связанных объектов; это следующий безопасный шаг по проверенной схеме, а не бизнес-обороты.";
|
||
}
|
||
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
|
||
if (pilot.derived_metadata_surface.ambiguity_detected) {
|
||
return "По схеме 1С найдены несколько конкурирующих контуров; перед следующим шагом нужно явно выбрать нужный тип данных.";
|
||
}
|
||
if (pilot.derived_metadata_surface.downstream_route_family) {
|
||
return "По схеме 1С найдены подходящие объекты; можно безопасно выбрать следующий контур проверки.";
|
||
}
|
||
return "По схеме 1С найдены доступные объекты для дальнейшего безопасного поиска.";
|
||
}
|
||
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
|
||
}
|
||
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
return "По данным 1С найдены строки входящих денежных поступлений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
|
||
}
|
||
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
|
||
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
|
||
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.";
|
||
}
|
||
if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return `По документам${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||
}
|
||
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
|
||
}
|
||
if (mode === "confirmed_with_bounded_inference") {
|
||
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
|
||
}
|
||
if (isDocumentPilot(pilot) && mode === "bounded_inference_only") {
|
||
return `По документам${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||
}
|
||
if (isMovementPilot(pilot) && mode === "bounded_inference_only") {
|
||
return `По движениям${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
|
||
}
|
||
if (mode === "bounded_inference_only") {
|
||
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
|
||
}
|
||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||
return "По проверенной схеме 1С видно несколько возможных контуров, и без явного выбора дальше идти нельзя.";
|
||
}
|
||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||
const need = clarificationNeedRu(pilot);
|
||
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||
}
|
||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||
const need = clarificationNeedRu(pilot);
|
||
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
|
||
}
|
||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||
const need = clarificationNeedRu(pilot);
|
||
return `Могу сравнить входящий и исходящий денежный поток, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||
}
|
||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||
const need = clarificationNeedRu(pilot);
|
||
return `Могу посчитать рейтинг по денежному потоку между контрагентами, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||
}
|
||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||
const need = clarificationNeedRu(pilot);
|
||
return `Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
|
||
}
|
||
if (mode === "needs_clarification") {
|
||
return "Нужно уточнить контекст перед поиском в 1С.";
|
||
}
|
||
if (mode === "blocked") {
|
||
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
|
||
}
|
||
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
|
||
}
|
||
|
||
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
|
||
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
|
||
if (ambiguityCandidates.length > 0) {
|
||
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(
|
||
ambiguityCandidates
|
||
)}. Можно ответить названием или номером варианта.`;
|
||
}
|
||
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
|
||
}
|
||
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
|
||
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
|
||
}
|
||
if (
|
||
isEntityResolutionPilot(pilot) &&
|
||
mode === "checked_sources_only" &&
|
||
pilot.derived_entity_resolution?.resolution_status === "not_found"
|
||
) {
|
||
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
|
||
}
|
||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
|
||
}
|
||
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
|
||
return clarificationNextStepLine(pilot, "движениям/регистрам");
|
||
}
|
||
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
|
||
return clarificationNextStepLine(pilot, "документам");
|
||
}
|
||
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
|
||
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
|
||
}
|
||
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
|
||
return clarificationNextStepLine(pilot, "рейтингу контрагентов по денежному потоку");
|
||
}
|
||
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
|
||
return clarificationNextStepLine(pilot, "денежному потоку");
|
||
}
|
||
if (mode === "needs_clarification") {
|
||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||
}
|
||
if (mode === "checked_sources_only" && isInventoryTemplatePilot(pilot)) {
|
||
if (pilot.mcp_execution_performed) {
|
||
return "Можно уточнить дату, организацию, склад, поставщика или позицию и повторить exact inventory проверку.";
|
||
}
|
||
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
|
||
}
|
||
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
|
||
return "Если нужен уже управленческий вывод, следующим шагом стоит отдельно проверить прибыль/маржу, долги, НДС и складскую ликвидность, а затем собрать полный бизнес-аудит.";
|
||
}
|
||
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
|
||
const surface = pilot.derived_metadata_surface;
|
||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||
return `Следующим шагом лучше выбрать один контур схемы: ${surface.ambiguity_entity_sets.join(", ")}.`;
|
||
}
|
||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||
if (surface.selected_entity_set && routeLabel) {
|
||
return `Следующим шагом могу пойти в ${routeLabel} по типу «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
|
||
}
|
||
}
|
||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||
return "Можно повторить проверку после восстановления доступа к 1С или сузить вопрос до конкретного контрагента/периода.";
|
||
}
|
||
if (mode === "blocked") {
|
||
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const claims = [
|
||
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
|
||
"Do not claim rows were checked when mcp_execution_performed=false."
|
||
];
|
||
if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
|
||
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
|
||
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
|
||
}
|
||
if (isValueFlowPilot(pilot)) {
|
||
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
|
||
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||
}
|
||
if (isBusinessOverviewPilot(pilot)) {
|
||
claims.push("Do not present business overview cash-flow spread as profit or margin.");
|
||
claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
|
||
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
|
||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||
claims.push("Do not expose business_overview_route_template_v1 or MCP primitive names in the user answer.");
|
||
}
|
||
if (pilot.derived_ranked_value_flow) {
|
||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||
}
|
||
if (isDocumentPilot(pilot)) {
|
||
claims.push("Do not claim full document history outside the checked period.");
|
||
claims.push("Do not present the confirmed document rows as a complete document universe.");
|
||
}
|
||
if (isMovementPilot(pilot)) {
|
||
claims.push("Do not claim full movement history outside the checked period.");
|
||
claims.push("Do not present the confirmed movement rows as a complete movement universe.");
|
||
}
|
||
if (isMetadataPilot(pilot)) {
|
||
claims.push("Do not present metadata surface as confirmed business data rows.");
|
||
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
|
||
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
|
||
}
|
||
if (isEntityResolutionPilot(pilot)) {
|
||
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
|
||
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
|
||
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
|
||
}
|
||
if (isInventoryTemplatePilot(pilot)) {
|
||
if (!pilot.mcp_execution_performed) {
|
||
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
|
||
}
|
||
claims.push("Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.");
|
||
claims.push("Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope.");
|
||
}
|
||
if (pilot.evidence.confirmed_facts.length === 0) {
|
||
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
|
||
}
|
||
return claims;
|
||
}
|
||
|
||
const RU_MONTH_LABELS_SHORT = [
|
||
"янв",
|
||
"фев",
|
||
"мар",
|
||
"апр",
|
||
"май",
|
||
"июн",
|
||
"июл",
|
||
"авг",
|
||
"сен",
|
||
"окт",
|
||
"ноя",
|
||
"дек"
|
||
] as const;
|
||
|
||
function monthLabelRu(monthBucket: string): string {
|
||
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
|
||
if (!match) {
|
||
return monthBucket;
|
||
}
|
||
const monthIndex = Number(match[2]) - 1;
|
||
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
|
||
return `${label} ${match[1]}`;
|
||
}
|
||
|
||
function netLabelRu(netDirection: "net_incoming" | "net_outgoing" | "balanced"): string {
|
||
if (netDirection === "net_incoming") {
|
||
return "нетто в нашу сторону";
|
||
}
|
||
if (netDirection === "net_outgoing") {
|
||
return "нетто исходящее";
|
||
}
|
||
return "нетто нулевое";
|
||
}
|
||
|
||
function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const period = pilot.derived_activity_period;
|
||
if (!period) {
|
||
return null;
|
||
}
|
||
return [
|
||
`По подтвержденным строкам активности в 1С период взаимодействия можно оценить примерно как ${period.duration_human_ru}.`,
|
||
`Первая найденная активность: ${period.first_activity_date}; последняя найденная активность: ${period.latest_activity_date}.`,
|
||
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
|
||
].join(" ");
|
||
}
|
||
|
||
function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const surface = pilot.derived_metadata_surface;
|
||
if (!surface) {
|
||
return null;
|
||
}
|
||
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
|
||
const entitySets =
|
||
surface.available_entity_sets.length > 0
|
||
? ` Тип объектов: ${surface.available_entity_sets.join(", ")}.`
|
||
: "";
|
||
const objects =
|
||
surface.matched_objects.length > 0
|
||
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
|
||
: "";
|
||
const selectedObjects =
|
||
surface.selected_surface_objects.length > 0
|
||
? ` Для следующего шага подходят: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
|
||
: "";
|
||
const fields =
|
||
surface.available_fields.length > 0
|
||
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
|
||
: "";
|
||
return `В схеме 1С${scope} найдены подтвержденные объекты: ${surface.matched_rows}.${entitySets}${objects}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
|
||
}
|
||
|
||
function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const surface = pilot.derived_metadata_surface;
|
||
if (!surface) {
|
||
return null;
|
||
}
|
||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||
return `По проверенной схеме видно несколько возможных контуров: ${surface.ambiguity_entity_sets.join(", ")}. Следующий шаг пока нельзя выбрать без явного сужения.`;
|
||
}
|
||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||
if (!surface.selected_entity_set || !routeLabel) {
|
||
return null;
|
||
}
|
||
return `Следующий проверяемый шаг можно вести в ${routeLabel} через тип «${surface.selected_entity_set}». Это пока выбор контура по схеме 1С, а не уже полученные бизнес-строки.`;
|
||
}
|
||
|
||
function derivedEntityResolutionConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const resolution = pilot.derived_entity_resolution;
|
||
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
|
||
return null;
|
||
}
|
||
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
|
||
const confidence =
|
||
resolution.confidence === "high"
|
||
? " Точность совпадения выглядит высокой."
|
||
: resolution.confidence === "medium"
|
||
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
|
||
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
|
||
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
|
||
}
|
||
|
||
function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const resolution = pilot.derived_entity_resolution;
|
||
if (!resolution) {
|
||
return null;
|
||
}
|
||
if (resolution.resolution_status === "resolved") {
|
||
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
|
||
}
|
||
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
|
||
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(
|
||
resolution.ambiguity_candidates
|
||
)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const ranking = pilot.derived_ranked_value_flow;
|
||
if (!ranking) {
|
||
return null;
|
||
}
|
||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||
return `Рейтинг по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
|
||
}
|
||
|
||
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const ranking = pilot.derived_ranked_value_flow;
|
||
if (!ranking || ranking.ranked_values.length <= 0) {
|
||
return null;
|
||
}
|
||
const leader = ranking.ranked_values[0];
|
||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||
if (ranking.ranked_values.length === 1) {
|
||
const singleLead =
|
||
ranking.value_flow_direction === "outgoing_supplier_payout"
|
||
? "В проверенных исходящих платежах найден один контрагент"
|
||
: "В проверенных входящих поступлениях найден один контрагент";
|
||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
|
||
}
|
||
const directionLead =
|
||
ranking.ranking_need === "bottom_asc"
|
||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||
? "Меньше всего заплатили контрагенту"
|
||
: "Меньше всего денег принёс контрагент"
|
||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||
? "Больше всего заплатили контрагенту"
|
||
: "Больше всего денег принёс контрагент";
|
||
const tail = ranking.ranked_values
|
||
.slice(1, 3)
|
||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||
.join("; ");
|
||
const trail = tail ? ` Следом: ${tail}.` : "";
|
||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||
: "";
|
||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||
}
|
||
|
||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const flow = pilot.derived_value_flow;
|
||
if (!flow) {
|
||
return null;
|
||
}
|
||
const organizationScope = explicitOrganizationScope(pilot);
|
||
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
|
||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||
const movementLabel =
|
||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||
? "исходящих платежей/списаний"
|
||
: "входящих денежных поступлений";
|
||
const totalLabel =
|
||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||
? "сумма исходящих платежей/списаний составляет"
|
||
: "сумма входящих денежных поступлений составляет";
|
||
const caveat =
|
||
flow.value_flow_direction === "outgoing_supplier_payout"
|
||
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
|
||
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
|
||
const dates =
|
||
flow.first_movement_date && flow.latest_movement_date
|
||
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
|
||
: "";
|
||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||
? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью."
|
||
: "";
|
||
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
|
||
}
|
||
|
||
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const flow = pilot.derived_value_flow;
|
||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||
return [];
|
||
}
|
||
return flow.monthly_breakdown.map((bucket) => {
|
||
const monthLabel = monthLabelRu(bucket.month_bucket);
|
||
if (flow.value_flow_direction === "outgoing_supplier_payout") {
|
||
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||
}
|
||
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
|
||
});
|
||
}
|
||
|
||
function sideDateRange(first: string | null, latest: string | null): string {
|
||
if (first && latest) {
|
||
return ` первая дата ${first}, последняя ${latest}`;
|
||
}
|
||
return " даты движения не выделены";
|
||
}
|
||
|
||
function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||
const flow = pilot.derived_bidirectional_value_flow;
|
||
if (!flow) {
|
||
return null;
|
||
}
|
||
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
|
||
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
|
||
const incoming = flow.incoming_customer_revenue;
|
||
const outgoing = flow.outgoing_supplier_payout;
|
||
const netLabel =
|
||
flow.net_direction === "net_incoming"
|
||
? "нетто в нашу сторону"
|
||
: flow.net_direction === "net_outgoing"
|
||
? "нетто исходящий"
|
||
: "нетто нулевое";
|
||
const limitCaveat = flow.coverage_limited_by_probe_limit
|
||
? " Лимит строк проверки достигнут хотя бы по одной стороне; полный запрошенный период может быть покрыт не полностью."
|
||
: "";
|
||
return [
|
||
`По найденным строкам 1С${counterparty}${period}: получили ${incoming.total_amount_human_ru} по входящим движениям, заплатили ${outgoing.total_amount_human_ru} по исходящим платежам/списаниям.`,
|
||
`Расчетное ${netLabel}: ${flow.net_amount_human_ru}`,
|
||
`Входящие строки с суммой: ${incoming.rows_with_amount} из ${incoming.rows_matched};${sideDateRange(incoming.first_movement_date, incoming.latest_movement_date)}.`,
|
||
`Исходящие строки с суммой: ${outgoing.rows_with_amount} из ${outgoing.rows_matched};${sideDateRange(outgoing.first_movement_date, outgoing.latest_movement_date)}.`,
|
||
`${limitCaveat} Это расчет по найденным строкам 1С, а не подтверждение полного сальдо вне проверенного окна.`
|
||
]
|
||
.join(" ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const flow = pilot.derived_bidirectional_value_flow;
|
||
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
|
||
return [];
|
||
}
|
||
return flow.monthly_breakdown.map(
|
||
(bucket) =>
|
||
`Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`
|
||
);
|
||
}
|
||
|
||
function businessOverviewNetDirectionRu(direction: "net_incoming" | "net_outgoing" | "balanced"): string {
|
||
if (direction === "net_incoming") {
|
||
return "операционный денежный поток в проверенном срезе больше входящий, чем исходящий";
|
||
}
|
||
if (direction === "net_outgoing") {
|
||
return "операционный денежный поток в проверенном срезе больше исходящий, чем входящий";
|
||
}
|
||
return "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы";
|
||
}
|
||
|
||
function amountHumanRu(value: number): string {
|
||
const rounded = Math.round(Math.abs(value) * 100) / 100;
|
||
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(rounded)} руб.`;
|
||
}
|
||
|
||
function percentOfTotal(part: number, total: number): number | null {
|
||
if (!Number.isFinite(part) || !Number.isFinite(total) || total <= 0) {
|
||
return null;
|
||
}
|
||
return Math.round((part / total) * 10_000) / 100;
|
||
}
|
||
|
||
function percentText(part: number, total: number): string | null {
|
||
const pct = percentOfTotal(part, total);
|
||
return pct === null ? null : `${pct}%`;
|
||
}
|
||
|
||
function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const overview = pilot.derived_business_overview;
|
||
if (!overview) {
|
||
return [];
|
||
}
|
||
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
|
||
const period = overview.period_scope ? ` за ${overview.period_scope}` : " за все доступное проверенное окно";
|
||
const lines: string[] = [];
|
||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
||
lines.push(
|
||
`Входящие поступления${organization}${period}: ${overview.incoming_customer_revenue.total_amount_human_ru} по ${overview.incoming_customer_revenue.rows_with_amount} строкам с суммой.`
|
||
);
|
||
}
|
||
if (overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||
lines.push(
|
||
`Исходящие платежи/списания${organization}${period}: ${overview.outgoing_supplier_payout.total_amount_human_ru} по ${overview.outgoing_supplier_payout.rows_with_amount} строкам с суммой.`
|
||
);
|
||
}
|
||
const leader = overview.top_customers[0];
|
||
if (leader) {
|
||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||
}
|
||
if (overview.activity_period) {
|
||
lines.push(
|
||
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
|
||
);
|
||
}
|
||
if (overview.tax_position) {
|
||
const taxDirection =
|
||
overview.tax_position.net_vat_direction === "vat_to_pay"
|
||
? "к уплате"
|
||
: overview.tax_position.net_vat_direction === "vat_to_recover_or_offset"
|
||
? "к вычету/зачету"
|
||
: "сбалансирован";
|
||
lines.push(
|
||
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
|
||
);
|
||
}
|
||
if (overview.trading_margin_proxy) {
|
||
const proxy = overview.trading_margin_proxy;
|
||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||
lines.push(
|
||
`Торговый margin proxy за ${proxy.period_scope}: выручка продаж ${proxy.sales_revenue_human_ru}, закупочный документный след ${proxy.purchase_cost_proxy_human_ru}, валовый спред proxy ${proxy.gross_spread_proxy_human_ru}, маржинальность к выручке ${marginText}. Это не чистая прибыль и не бухгалтерский финрезультат.`
|
||
);
|
||
}
|
||
if (overview.debt_position) {
|
||
const debtDirection =
|
||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||
? "в пользу дебиторки"
|
||
: overview.debt_position.net_debt_position_direction === "net_payable"
|
||
? "в сторону кредиторки"
|
||
: "сбалансировано";
|
||
lines.push(
|
||
`Долговой срез на ${overview.debt_position.as_of_date}: дебиторка ${overview.debt_position.receivables.total_amount_human_ru}, кредиторка ${overview.debt_position.payables.total_amount_human_ru}, нетто ${debtDirection} ${overview.debt_position.net_debt_position_amount_human_ru}.`
|
||
);
|
||
}
|
||
if (overview.debt_open_settlement_quality) {
|
||
const quality = overview.debt_open_settlement_quality;
|
||
const topContract = quality.top_contracts[0];
|
||
const topContractText = topContract
|
||
? ` Крупнейший открытый договор: ${topContract.contract}${topContract.counterparty ? ` / ${topContract.counterparty}` : ""} — ${topContract.total_amount_human_ru}${topContract.share_of_gross_open_amount_pct === null ? "" : ` (${topContract.share_of_gross_open_amount_pct}%)`}.`
|
||
: "";
|
||
lines.push(
|
||
`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`
|
||
);
|
||
if (quality.age_signal?.oldest_start_date) {
|
||
const ageText = quality.age_signal.max_age_days === null
|
||
? ""
|
||
: `, максимальный возраст сигнала ${quality.age_signal.max_age_days} дн.`;
|
||
lines.push(
|
||
`Возрастной сигнал открытых расчетов: самая ранняя найденная дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не просрочка и не due-date анализ.`
|
||
);
|
||
}
|
||
}
|
||
if (overview.inventory_position) {
|
||
const leader = overview.inventory_position.top_items[0];
|
||
const leaderText = leader
|
||
? ` Крупнейшая подтвержденная позиция: ${leader.item} — ${leader.total_amount_human_ru}.`
|
||
: "";
|
||
lines.push(
|
||
`Складской срез на ${overview.inventory_position.as_of_date}: остаток ${overview.inventory_position.total_amount_human_ru} по ${overview.inventory_position.rows_with_amount} строкам с суммой и ${overview.inventory_position.rows_with_quantity} строкам с количеством.${leaderText}`
|
||
);
|
||
if (overview.inventory_position.aging_signal?.oldest_purchase_date) {
|
||
const ageText = overview.inventory_position.aging_signal.max_age_days === null
|
||
? ""
|
||
: `, максимальный возраст сигнала ${overview.inventory_position.aging_signal.max_age_days} дн.`;
|
||
lines.push(
|
||
`Возрастной сигнал склада: самая ранняя найденная дата закупки ${overview.inventory_position.aging_signal.oldest_purchase_date}${ageText}.`
|
||
);
|
||
}
|
||
}
|
||
return lines;
|
||
}
|
||
|
||
function businessOverviewCashSynthesisLine(overview: BusinessOverview): string | null {
|
||
const incoming = overview.incoming_customer_revenue;
|
||
const outgoing = overview.outgoing_supplier_payout;
|
||
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
|
||
return null;
|
||
}
|
||
const checkedOperationalScale = Math.abs(incoming.total_amount) + Math.abs(outgoing.total_amount);
|
||
return [
|
||
`Аналитический вывод по оборотам: проверенный операционный размах ${amountHumanRu(checkedOperationalScale)}; входящий поток ${incoming.total_amount_human_ru}, исходящий ${outgoing.total_amount_human_ru}.`,
|
||
`${businessOverviewNetDirectionRu(overview.net_direction)}; расчетное нетто ${overview.net_amount_human_ru}.`
|
||
].join(" ");
|
||
}
|
||
|
||
function businessOverviewCustomerConcentrationLine(overview: BusinessOverview): string | null {
|
||
const leader = overview.top_customers[0];
|
||
if (!leader || overview.incoming_customer_revenue.total_amount <= 0) {
|
||
return null;
|
||
}
|
||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
||
return share
|
||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||
}
|
||
|
||
function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null {
|
||
const signals: string[] = [];
|
||
if (overview.tax_position) {
|
||
const taxDirection =
|
||
overview.tax_position.net_vat_direction === "vat_to_pay"
|
||
? `НДС к уплате ${overview.tax_position.net_vat_amount_human_ru}`
|
||
: overview.tax_position.net_vat_direction === "vat_to_recover_or_offset"
|
||
? `НДС к вычету/зачету ${overview.tax_position.net_vat_amount_human_ru}`
|
||
: "НДС-позиция сбалансирована";
|
||
signals.push(taxDirection);
|
||
}
|
||
if (overview.trading_margin_proxy) {
|
||
const marginText = overview.trading_margin_proxy.margin_to_revenue_pct === null
|
||
? "маржинальность не рассчитана"
|
||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||
}
|
||
if (overview.debt_position) {
|
||
const debtDirection =
|
||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||
: overview.debt_position.net_debt_position_direction === "net_payable"
|
||
? `кредиторка больше дебиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||
: "дебиторка и кредиторка сбалансированы";
|
||
signals.push(debtDirection);
|
||
}
|
||
if (overview.debt_open_settlement_quality?.concentration_top_contract_pct !== null && overview.debt_open_settlement_quality?.top_contracts[0]) {
|
||
const topContract = overview.debt_open_settlement_quality.top_contracts[0];
|
||
signals.push(`крупнейший открытый договор держит ${overview.debt_open_settlement_quality.concentration_top_contract_pct}% открытых остатков (${topContract.total_amount_human_ru})`);
|
||
}
|
||
if (overview.debt_open_settlement_quality?.age_signal?.max_age_days !== null && overview.debt_open_settlement_quality?.age_signal?.max_age_days !== undefined) {
|
||
signals.push(`самый старый договорный возрастной сигнал ${overview.debt_open_settlement_quality.age_signal.max_age_days} дн.`);
|
||
}
|
||
if (overview.inventory_position) {
|
||
signals.push(`складской остаток на дату ${overview.inventory_position.total_amount_human_ru}`);
|
||
if (overview.inventory_position.aging_signal?.max_age_days !== null && overview.inventory_position.aging_signal?.max_age_days !== undefined) {
|
||
signals.push(`самый старый складской purchase-date сигнал ${overview.inventory_position.aging_signal.max_age_days} дн.`);
|
||
}
|
||
}
|
||
return signals.length > 0
|
||
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
|
||
: null;
|
||
}
|
||
|
||
function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): string | null {
|
||
const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0;
|
||
const hasExtraSignals = Boolean(
|
||
overview.tax_position ||
|
||
overview.trading_margin_proxy ||
|
||
overview.debt_position ||
|
||
overview.debt_open_settlement_quality ||
|
||
overview.inventory_position
|
||
);
|
||
if (!hasCash && !hasExtraSignals) {
|
||
return null;
|
||
}
|
||
const cashTone =
|
||
overview.net_direction === "net_incoming"
|
||
? "операционно входящий поток сильнее исходящего"
|
||
: overview.net_direction === "net_outgoing"
|
||
? "операционно исходящий поток сильнее входящего, это зона внимания к расходам/закупкам"
|
||
: "операционный поток выглядит сбалансированным";
|
||
const evidenceTone = hasExtraSignals
|
||
? "часть налоговых, долговых или складских контуров уже отдельно проверена"
|
||
: "налоги, долги и склад еще не дают проверенного управленческого контекста";
|
||
return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`;
|
||
}
|
||
|
||
function derivedBusinessOverviewInferenceLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
const overview = pilot.derived_business_overview;
|
||
if (!overview) {
|
||
return [];
|
||
}
|
||
return [
|
||
businessOverviewCashSynthesisLine(overview),
|
||
businessOverviewCustomerConcentrationLine(overview),
|
||
businessOverviewRiskSynthesisLine(overview),
|
||
businessOverviewExecutiveVerdictLine(overview),
|
||
"Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость."
|
||
].filter((line): line is string => Boolean(line));
|
||
}
|
||
|
||
function businessOverviewUnknownLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
|
||
if (!pilot.derived_business_overview) {
|
||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||
}
|
||
return userFacingUnknowns(pilot.evidence.unknown_facts);
|
||
}
|
||
|
||
export function buildAssistantMcpDiscoveryAnswerDraft(
|
||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||
): AssistantMcpDiscoveryAnswerDraftContract {
|
||
const mode = modeFor(pilot);
|
||
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
|
||
pushReason(reasonCodes, `answer_mode_${mode}`);
|
||
if (pilot.evidence.unknown_facts.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
|
||
}
|
||
const businessOverviewInferenceLines = derivedBusinessOverviewInferenceLines(pilot);
|
||
const derivedInferenceLine =
|
||
derivedActivityInferenceLine(pilot) ??
|
||
derivedMetadataInferenceLine(pilot) ??
|
||
derivedRankedValueFlowInferenceLine(pilot) ??
|
||
derivedEntityResolutionInferenceLine(pilot);
|
||
const inferenceLines = businessOverviewInferenceLines.length > 0
|
||
? businessOverviewInferenceLines
|
||
: derivedInferenceLine
|
||
? [derivedInferenceLine]
|
||
: pilot.evidence.inferred_facts;
|
||
if (inferenceLines.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||
}
|
||
if (businessOverviewInferenceLines.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_analyst_synthesis");
|
||
}
|
||
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
|
||
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
|
||
const derivedValueLine =
|
||
derivedBidirectionalValueFlowConfirmedLine(pilot) ??
|
||
derivedRankedValueFlowConfirmedLine(pilot) ??
|
||
derivedValueFlowConfirmedLine(pilot);
|
||
const monthlyConfirmedLines =
|
||
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
|
||
? derivedBidirectionalValueFlowMonthlyLines(pilot)
|
||
: derivedValueFlowMonthlyLines(pilot);
|
||
const businessOverviewLines = derivedBusinessOverviewConfirmedLines(pilot);
|
||
if (monthlyConfirmedLines.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||
}
|
||
if (businessOverviewLines.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview");
|
||
}
|
||
if (pilot.derived_business_overview?.tax_position) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||
}
|
||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||
}
|
||
if (pilot.derived_business_overview?.debt_position) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
|
||
}
|
||
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
|
||
if (pilot.derived_business_overview.debt_open_settlement_quality.age_signal) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_debt_age_signal");
|
||
}
|
||
}
|
||
if (pilot.derived_business_overview?.inventory_position) {
|
||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||
}
|
||
const confirmedLines = businessOverviewLines.length > 0
|
||
? businessOverviewLines
|
||
: pilot.derived_ranked_value_flow && derivedValueLine
|
||
? [derivedValueLine]
|
||
: derivedValueLine
|
||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||
: derivedEntityResolutionLine
|
||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||
: derivedMetadataLine
|
||
? [derivedMetadataLine]
|
||
: pilot.evidence.confirmed_facts;
|
||
const unknownLines = pilot.derived_business_overview
|
||
? businessOverviewUnknownLines(pilot)
|
||
: pilot.derived_metadata_surface
|
||
? pilot.derived_metadata_surface.available_fields.length > 0
|
||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||
: rankedValueFlowUnknownLines(pilot);
|
||
|
||
return {
|
||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||
answer_mode: mode,
|
||
headline: headlineFor(mode, pilot),
|
||
confirmed_lines: uniqueStrings(confirmedLines),
|
||
inference_lines: uniqueStrings(inferenceLines),
|
||
unknown_lines: unknownLines,
|
||
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
|
||
next_step_line: nextStepFor(mode, pilot),
|
||
internal_mechanics_allowed: false,
|
||
must_not_claim: buildMustNotClaim(pilot),
|
||
reason_codes: uniqueStrings(reasonCodes)
|
||
};
|
||
}
|