NODEDC_1C/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswer...

1612 lines
91 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
if (overview.inventory_staleness_risk_proxy) {
return "резервы/списания/ликвидационная стоимость склада";
}
if (overview.inventory_turnover_proxy) {
return "FIFO-оборачиваемость/подтвержденная складская ликвидность";
}
if (overview.inventory_position) {
return "полноценная складская ликвидность";
}
return "склад";
}
function businessOverviewNextStepLine(overview: BusinessOverview): string {
const missing = new Set(overview.missing_signal_families);
const checks: string[] = [];
if (missing.has("accounting_profit_margin") || missing.has("profit_or_clean_margin")) {
checks.push("чистую прибыль через себестоимость продаж, расходы и закрытие периода");
} else if (missing.has("profit_margin") || missing.has("profit_or_margin")) {
checks.push("прибыль/маржу по проверенному финрезультату");
}
if (missing.has("tax_position")) {
checks.push("НДС/налоговую позицию за явный период");
}
if (missing.has("debt_due_date_aging_quality")) {
checks.push("договорные сроки оплаты и due-date aging");
} else if (missing.has("debt_open_settlement_quality")) {
checks.push("качество открытых расчетов");
} else if (missing.has("debt_position")) {
checks.push("долговой срез на дату");
}
if (missing.has("inventory_reserve_liquidation_quality")) {
checks.push("резервы, списания, неликвидность и ликвидационную стоимость склада");
} else if (missing.has("inventory_liquidity_quality")) {
checks.push("FIFO-оборачиваемость и подтвержденную складскую ликвидность");
} else if (missing.has("inventory_turnover_quality")) {
checks.push("скорость продаж и оборачиваемость склада");
} else if (missing.has("inventory_position")) {
checks.push("складской остаток на дату");
}
const target = checks.length > 0
? checks.join("; ")
: "оставшиеся непроверенные области по выбранному контуру";
return `Следующий шаг для полного бизнес-аудита: отдельно проверить ${target}, не смешивая эти будущие проверки с уже подтвержденным обзором.`;
}
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.yearly_breakdown?.length) {
families.push("годовая operating-flow динамика");
}
if (overview.activity_period) {
families.push("активность");
}
if (overview.document_activity_profile) {
families.push("профиль типов документов и разделов учета");
}
if (overview.counterparty_profile) {
families.push("профиль контрагентской базы");
}
if (overview.contract_usage_profile) {
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.debt_staleness_risk_proxy) {
families.push("staleness risk proxy открытых расчетов");
}
if (overview.inventory_position) {
families.push("складской срез на дату");
}
if (overview.inventory_turnover_proxy) {
families.push("оборотный proxy склада");
}
if (overview.inventory_staleness_risk_proxy) {
families.push("staleness risk proxy склада");
}
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
if (!overview.tax_position) {
unknownFamilies.push("НДС");
}
if (!overview.debt_position) {
unknownFamilies.push("долговой срез");
}
unknownFamilies.push(
overview.debt_staleness_risk_proxy
? "договорные сроки оплаты/due-date просрочка"
: overview.debt_open_settlement_quality
? "due-date просрочка"
: "качество открытых расчетов"
);
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
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 pilot.derived_business_overview
? businessOverviewNextStepLine(pilot.derived_business_overview)
: "Если нужен уже управленческий вывод, следующим шагом стоит отдельно проверить прибыль/маржу, долги, НДС и складскую ликвидность, а затем собрать полный бизнес-аудит.";
}
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 yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
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 present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity.");
claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness.");
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 business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
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 present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
if (pilot.derived_business_overview?.missing_proof_families?.length) {
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
}
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 yearCountHumanRu(count: number): string {
const abs = Math.abs(count) % 100;
const last = abs % 10;
const noun =
abs >= 11 && abs <= 14
? "лет"
: last === 1
? "год"
: last >= 2 && last <= 4
? "года"
: "лет";
return `${count} ${noun}`;
}
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 inventoryStalenessRiskBandRu(
riskBand: NonNullable<BusinessOverview["inventory_staleness_risk_proxy"]>["risk_band"]
): string {
if (riskBand === "high") {
return "высокая зона внимания";
}
if (riskBand === "elevated") {
return "повышенная зона внимания";
}
if (riskBand === "watch") {
return "зона наблюдения";
}
return "низкий видимый риск";
}
function debtStalenessRiskBandRu(
riskBand: NonNullable<BusinessOverview["debt_staleness_risk_proxy"]>["risk_band"]
): string {
if (riskBand === "high") {
return "высокая зона внимания";
}
if (riskBand === "elevated") {
return "повышенная зона внимания";
}
if (riskBand === "watch") {
return "зона наблюдения";
}
return "низкий видимый риск";
}
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}.`);
}
const supplierLeader = overview.top_suppliers?.[0];
if (supplierLeader) {
lines.push(
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
);
}
if (overview.yearly_breakdown?.length) {
lines.push(
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
);
}
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.document_activity_profile) {
const profile = overview.document_activity_profile;
const topDocument = profile.top_document_types[0];
const topSection = profile.top_account_sections[0];
const parts: string[] = [];
if (topDocument) {
const shareText = topDocument.share_pct === null ? "" : ` (${topDocument.share_pct}%)`;
parts.push(`ведущий тип документов ${topDocument.document_type}${topDocument.count} документов${shareText}`);
}
if (topSection) {
const shareText = topSection.share_pct === null ? "" : ` (${topSection.share_pct}%)`;
parts.push(`ведущий раздел учета ${topSection.account_section}${topSection.operation_count} операций${shareText}`);
}
if (parts.length > 0) {
lines.push(
`Профиль операционной активности${organization}${period}: ${parts.join("; ")}. Это activity mix по найденным строкам 1С, а не аудит качества учета или полноты процессов.`
);
}
}
if (overview.counterparty_profile) {
const profile = overview.counterparty_profile;
const totalText = profile.total_counterparties === null
? `${profile.active_counterparties} активных контрагентов`
: `${profile.total_counterparties} контрагентов в базе, ${profile.active_counterparties} активных по документальной активности`;
lines.push(
`Профиль контрагентской базы${organization}${period}: ${totalText}; заказчики ${profile.customer_only_count}, поставщики ${profile.supplier_only_count}, смешанная роль ${profile.mixed_role_count}. Это не CRM-аудит, не юридическая проверка контрагентов и не оценка качества клиентской базы.`
);
}
if (overview.contract_usage_profile) {
const profile = overview.contract_usage_profile;
const totalText = profile.total_contracts === null
? `${profile.used_contracts} договоров с подтвержденной связью с операциями`
: `${profile.used_contracts} из ${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`;
const unusedText = profile.unused_contracts === null ? "" : `, неиспользуемых ${profile.unused_contracts}`;
lines.push(
`Договорной профиль${organization}${period}: ${totalText}${unusedText}. Это не contract-risk аудит, не проверка актуальности условий и не доказательство качества договорной базы.`
);
}
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.debt_staleness_risk_proxy) {
const proxy = overview.debt_staleness_risk_proxy;
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
lines.push(
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
);
}
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}.`
);
}
}
if (overview.inventory_turnover_proxy) {
const proxy = overview.inventory_turnover_proxy;
const ratioText = proxy.sales_to_stock_amount_ratio === null
? "не рассчитано"
: `${proxy.sales_to_stock_amount_ratio}x`;
const stockShareText = proxy.stock_to_sales_revenue_pct === null
? "не рассчитана"
: `${proxy.stock_to_sales_revenue_pct}%`;
lines.push(
`Оборотный proxy склада за ${proxy.period_scope}: продажи ${proxy.sales_revenue_human_ru}, остаток на ${proxy.as_of_date} ${proxy.inventory_amount_human_ru}, sales-to-stock ratio ${ratioText}, остаток к продажам ${stockShareText}. Это не полноценная складская ликвидность, не FIFO-оборачиваемость и не анализ устаревания.`
);
}
if (overview.inventory_staleness_risk_proxy) {
const proxy = overview.inventory_staleness_risk_proxy;
lines.push(
`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`
);
}
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 businessOverviewSupplierConcentrationLine(overview: BusinessOverview): string | null {
const leader = overview.top_suppliers?.[0];
if (!leader || overview.outgoing_supplier_payout.total_amount <= 0) {
return null;
}
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
return share
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
}
function businessOverviewYearlyOperatingLine(overview: BusinessOverview): string | null {
const years = overview.yearly_breakdown ?? [];
if (years.length === 0) {
return null;
}
const strongestIncomingYear = [...years]
.filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
const strongestNetYear = [...years]
.filter((bucket) => bucket.net_amount !== 0)
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
if (!strongestIncomingYear && !strongestNetYear) {
return null;
}
const parts: string[] = [];
if (strongestIncomingYear) {
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${strongestIncomingYear.incoming_total_amount_human_ru}`);
}
if (strongestNetYear) {
const netText = strongestNetYear.net_direction === "net_outgoing"
? `нетто исходящее ${strongestNetYear.net_amount_human_ru}`
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
}
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
}
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.debt_staleness_risk_proxy) {
signals.push(
`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`
);
}
if (overview.document_activity_profile) {
const topDocument = overview.document_activity_profile.top_document_types[0];
const topSection = overview.document_activity_profile.top_account_sections[0];
const parts = [
topDocument ? `ведущий тип документов ${topDocument.document_type}` : null,
topSection ? `ведущий раздел учета ${topSection.account_section}` : null
].filter((item): item is string => Boolean(item));
if (parts.length > 0) {
signals.push(`операционный activity mix: ${parts.join(", ")}`);
}
}
if (overview.counterparty_profile) {
const totalText = overview.counterparty_profile.total_counterparties === null
? `${overview.counterparty_profile.active_counterparties} активных контрагентов`
: `${overview.counterparty_profile.total_counterparties} контрагентов, активных ${overview.counterparty_profile.active_counterparties}`;
signals.push(`контрагентская база: ${totalText}`);
}
if (overview.contract_usage_profile) {
const profile = overview.contract_usage_profile;
const totalText = profile.total_contracts === null
? `${profile.used_contracts} используемых договоров`
: `${profile.used_contracts}/${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`;
signals.push(`договорной профиль: ${totalText}`);
}
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} дн.`);
}
}
if (overview.inventory_turnover_proxy) {
const ratioText = overview.inventory_turnover_proxy.sales_to_stock_amount_ratio === null
? "sales-to-stock не рассчитан"
: `sales-to-stock ${overview.inventory_turnover_proxy.sales_to_stock_amount_ratio}x`;
signals.push(`оборотный proxy склада: ${ratioText}`);
}
if (overview.inventory_staleness_risk_proxy) {
signals.push(
`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_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 hasTaxDebtInventorySignals = Boolean(
overview.tax_position ||
overview.trading_margin_proxy ||
overview.debt_position ||
overview.debt_open_settlement_quality ||
overview.debt_staleness_risk_proxy ||
overview.inventory_position ||
overview.inventory_turnover_proxy ||
overview.inventory_staleness_risk_proxy
);
const hasOperationalProfileSignal = Boolean(
overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile
);
const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal;
if (!hasCash && !hasExtraSignals) {
return null;
}
const cashTone =
overview.net_direction === "net_incoming"
? "операционно входящий поток сильнее исходящего"
: overview.net_direction === "net_outgoing"
? "операционно исходящий поток сильнее входящего, это зона внимания к расходам/закупкам"
: "операционный поток выглядит сбалансированным";
const evidenceTone = hasTaxDebtInventorySignals
? "часть налоговых, долговых или складских контуров уже отдельно проверена"
: hasOperationalProfileSignal
? "операционные профили по документам, контрагентам или договорам уже отдельно проверены"
: "налоги, долги и склад еще не дают проверенного управленческого контекста";
return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`;
}
function derivedBusinessOverviewInferenceLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const overview = pilot.derived_business_overview;
if (!overview) {
return [];
}
return [
businessOverviewCashSynthesisLine(overview),
businessOverviewCustomerConcentrationLine(overview),
businessOverviewSupplierConcentrationLine(overview),
businessOverviewYearlyOperatingLine(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?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}
if (pilot.derived_business_overview?.document_activity_profile) {
pushReason(reasonCodes, "answer_contains_business_overview_document_activity_profile");
}
if (pilot.derived_business_overview?.counterparty_profile) {
pushReason(reasonCodes, "answer_contains_business_overview_counterparty_profile");
}
if (pilot.derived_business_overview?.contract_usage_profile) {
pushReason(reasonCodes, "answer_contains_business_overview_contract_usage_profile");
}
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?.debt_staleness_risk_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
}
if (pilot.derived_business_overview?.inventory_position) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
}
if (pilot.derived_business_overview?.inventory_turnover_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_turnover_proxy");
}
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
}
if (pilot.derived_business_overview?.missing_proof_families?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
}
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)
};
}