753 lines
46 KiB
JavaScript
753 lines
46 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||
function normalizeReasonCode(value) {
|
||
const normalized = value
|
||
.trim()
|
||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||
.replace(/^_+|_+$/g, "")
|
||
.toLowerCase();
|
||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||
}
|
||
function pushReason(target, value) {
|
||
const normalized = normalizeReasonCode(value);
|
||
if (normalized && !target.includes(normalized)) {
|
||
target.push(normalized);
|
||
}
|
||
}
|
||
function uniqueStrings(values) {
|
||
const result = [];
|
||
for (const value of values) {
|
||
const text = String(value ?? "").trim();
|
||
if (text && !result.includes(text)) {
|
||
result.push(text);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
function formatNamedChoiceList(values) {
|
||
return uniqueStrings(values)
|
||
.slice(0, 6)
|
||
.map((value, index) => `${index + 1}. ${value}`)
|
||
.join("; ");
|
||
}
|
||
function isInternalMechanicsLine(value) {
|
||
const text = value.toLowerCase();
|
||
return (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("needs more scope before execution") ||
|
||
text.includes("mcp_execution_performed"));
|
||
}
|
||
function userFacingUnknowns(values) {
|
||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||
}
|
||
function rankedValueFlowUnknownLines(pilot) {
|
||
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) {
|
||
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
|
||
}
|
||
function modeFor(pilot) {
|
||
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) {
|
||
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 isDocumentPilot(pilot) {
|
||
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
|
||
}
|
||
function isMovementPilot(pilot) {
|
||
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
|
||
}
|
||
function isMetadataPilot(pilot) {
|
||
return pilot.pilot_scope === "metadata_inspection_v1";
|
||
}
|
||
function isCatalogDrilldownPilot(pilot) {
|
||
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) {
|
||
return pilot.pilot_scope === "entity_resolution_search_v1";
|
||
}
|
||
function isMetadataLaneChoiceClarification(pilot) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
const entity = firstEntityCandidate(pilot);
|
||
const period = explicitDateScope(pilot);
|
||
const entityPart = entity ? ` по контрагенту ${entity}` : "";
|
||
const periodPart = period
|
||
? ` за ${period}`
|
||
: hasAllTimeScope(pilot)
|
||
? " за все доступное время"
|
||
: " в проверенном окне";
|
||
return `${entityPart}${periodPart}`;
|
||
}
|
||
function isMovementLaneClarification(pilot) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
const entity = firstEntityCandidate(pilot);
|
||
return entity ? ` по "${entity}"` : "";
|
||
}
|
||
function dryRunHasAxis(pilot, axis) {
|
||
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
|
||
}
|
||
function dryRunMissingAxis(pilot, axis) {
|
||
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) {
|
||
const values = pilot.evidence.query_plan.clarification_gaps;
|
||
return Array.isArray(values) ? uniqueStrings(values) : [];
|
||
}
|
||
function clarificationGapMissing(pilot, axis) {
|
||
const gaps = queryPlanClarificationGaps(pilot);
|
||
if (gaps.length > 0) {
|
||
return gaps.includes(axis);
|
||
}
|
||
return dryRunMissingAxis(pilot, axis);
|
||
}
|
||
function clarificationNeedRu(pilot) {
|
||
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, laneLabel) {
|
||
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) {
|
||
if (routeFamily === "document_evidence") {
|
||
return "контур документов";
|
||
}
|
||
if (routeFamily === "movement_evidence") {
|
||
return "контур движений/регистров";
|
||
}
|
||
if (routeFamily === "catalog_drilldown") {
|
||
return "контур справочников и связанных объектов";
|
||
}
|
||
return null;
|
||
}
|
||
function headlineFor(mode, pilot) {
|
||
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 (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С удалось углубиться в контур справочников и связанных объектов; это уже не общий обзор схемы, а следующий безопасный catalog drilldown.";
|
||
}
|
||
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
|
||
if (pilot.derived_metadata_surface.ambiguity_detected) {
|
||
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
|
||
}
|
||
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 "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
|
||
}
|
||
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, pilot) {
|
||
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 === "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 до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
|
||
}
|
||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||
if (surface.selected_entity_set && routeLabel) {
|
||
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
|
||
}
|
||
}
|
||
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
|
||
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
|
||
}
|
||
if (mode === "blocked") {
|
||
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
|
||
}
|
||
return null;
|
||
}
|
||
function buildMustNotClaim(pilot) {
|
||
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 (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 (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 = [
|
||
"янв",
|
||
"фев",
|
||
"мар",
|
||
"апр",
|
||
"май",
|
||
"июн",
|
||
"июл",
|
||
"авг",
|
||
"сен",
|
||
"окт",
|
||
"ноя",
|
||
"дек"
|
||
];
|
||
function monthLabelRu(monthBucket) {
|
||
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) {
|
||
if (netDirection === "net_incoming") {
|
||
return "нетто в нашу сторону";
|
||
}
|
||
if (netDirection === "net_outgoing") {
|
||
return "нетто исходящее";
|
||
}
|
||
return "нетто нулевое";
|
||
}
|
||
function derivedActivityInferenceLine(pilot) {
|
||
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) {
|
||
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 selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
|
||
const selectedObjects = surface.selected_surface_objects.length > 0
|
||
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
|
||
: "";
|
||
const fields = surface.available_fields.length > 0
|
||
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
|
||
: "";
|
||
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
|
||
}
|
||
function derivedMetadataInferenceLine(pilot) {
|
||
const surface = pilot.derived_metadata_surface;
|
||
if (!surface) {
|
||
return null;
|
||
}
|
||
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
|
||
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
|
||
}
|
||
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
|
||
if (!surface.selected_entity_set || !routeLabel) {
|
||
return null;
|
||
}
|
||
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
|
||
}
|
||
function derivedEntityResolutionConfirmedLine(pilot) {
|
||
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) {
|
||
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) {
|
||
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) {
|
||
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}` : " в проверенном окне";
|
||
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) {
|
||
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) {
|
||
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, latest) {
|
||
if (first && latest) {
|
||
return ` первая дата ${first}, последняя ${latest}`;
|
||
}
|
||
return " даты движения не выделены";
|
||
}
|
||
function derivedBidirectionalValueFlowConfirmedLine(pilot) {
|
||
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) {
|
||
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 buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||
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");
|
||
}
|
||
if (pilot.evidence.inferred_facts.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_bounded_inference");
|
||
}
|
||
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
|
||
derivedMetadataInferenceLine(pilot) ??
|
||
derivedRankedValueFlowInferenceLine(pilot) ??
|
||
derivedEntityResolutionInferenceLine(pilot);
|
||
const inferenceLines = derivedInferenceLine
|
||
? [derivedInferenceLine]
|
||
: pilot.evidence.inferred_facts;
|
||
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);
|
||
if (monthlyConfirmedLines.length > 0) {
|
||
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
|
||
}
|
||
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
|
||
? [derivedValueLine]
|
||
: derivedValueLine
|
||
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
|
||
: derivedEntityResolutionLine
|
||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||
: derivedMetadataLine
|
||
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
|
||
: pilot.evidence.confirmed_facts;
|
||
return {
|
||
schema_version: exports.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: rankedValueFlowUnknownLines(pilot),
|
||
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)
|
||
};
|
||
}
|