Очистить бизнес-язык ответов phase98
This commit is contained in:
+59
-43
@@ -86,7 +86,7 @@ function userFacingLines(values) {
|
||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
}
|
||||
function sanitizeUserFacingMechanics(value) {
|
||||
return String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
@@ -102,6 +102,28 @@ function sanitizeUserFacingMechanics(value) {
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
const replacements = [
|
||||
[/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"],
|
||||
[/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"],
|
||||
[/\bvendor-risk route\b/giu, "проверка поставщицкого риска"],
|
||||
[/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"],
|
||||
[/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"],
|
||||
[/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\boperating-flow proxy\b/giu, "денежный операционный показатель"],
|
||||
[/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"],
|
||||
[/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"],
|
||||
[/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"],
|
||||
[/\bproxy-сигналы\b/giu, "косвенные признаки"],
|
||||
[/\bproxy\b/giu, "косвенный показатель"],
|
||||
[/\bsales-to-stock\b/giu, "отношение продаж к остатку"],
|
||||
[/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"],
|
||||
[/\bP&L\b/gu, "полный отчет о прибылях и убытках"]
|
||||
];
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
text = text.replace(pattern, replacement);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function localizeLine(value) {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
@@ -385,6 +407,10 @@ function businessOverviewCoverageLimitLine(overview) {
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
: null;
|
||||
}
|
||||
function joinBusinessReplyLines(lines) {
|
||||
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
function businessOverviewYearRowsLine(overview) {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const values = years
|
||||
@@ -577,8 +603,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
if (fallbackNextStep) {
|
||||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
function compactComparable(value) {
|
||||
return String(value ?? "")
|
||||
@@ -748,8 +773,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
: "сумма не распознана";
|
||||
lines.push(`Коротко: по бухгалтерскому маршруту 90/91/99 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`);
|
||||
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
@@ -770,8 +794,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (debtDueDateBoundary) {
|
||||
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
|
||||
@@ -786,36 +809,35 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
|
||||
}
|
||||
else if (status === "no_payment_terms_configured") {
|
||||
lines.push(`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
|
||||
lines.push(rowsWithAmount !== null
|
||||
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
|
||||
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
|
||||
}
|
||||
else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`);
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута.");
|
||||
lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты.");
|
||||
lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (vendorRiskBoundary) {
|
||||
const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality);
|
||||
@@ -839,7 +861,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
|
||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
||||
if (status === "financial_institution_leads_outgoing_cash") {
|
||||
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
lines.push(financialHintText);
|
||||
@@ -849,19 +871,18 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
}
|
||||
}
|
||||
else if (status === "reviewed_procurement_concentration") {
|
||||
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
||||
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
||||
}
|
||||
else {
|
||||
lines.push(`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
|
||||
}
|
||||
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
|
||||
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
|
||||
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
|
||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||
: "";
|
||||
lines.push(`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
lines.push(`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
@@ -870,26 +891,24 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
lines.push(`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`);
|
||||
lines.push("Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.");
|
||||
lines.push("Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
lines.push("Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
|
||||
lines.push(reserveBasis
|
||||
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
|
||||
if (inventoryQualityEvents) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
@@ -900,9 +919,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
@@ -912,8 +930,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (rankingNeed) {
|
||||
const incomingLeader = strongestIncomingYear(overview);
|
||||
@@ -1005,8 +1022,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
function statusFrom(entryPoint) {
|
||||
if (!entryPoint || entryPoint.entry_status === "skipped_not_applicable") {
|
||||
|
||||
@@ -120,7 +120,7 @@ function userFacingLines(values: string[]): string[] {
|
||||
}
|
||||
|
||||
function sanitizeUserFacingMechanics(value: string): string {
|
||||
return String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
@@ -136,6 +136,28 @@ function sanitizeUserFacingMechanics(value: string): string {
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
const replacements: Array<[RegExp, string]> = [
|
||||
[/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"],
|
||||
[/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"],
|
||||
[/\bvendor-risk route\b/giu, "проверка поставщицкого риска"],
|
||||
[/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"],
|
||||
[/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"],
|
||||
[/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\boperating-flow proxy\b/giu, "денежный операционный показатель"],
|
||||
[/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"],
|
||||
[/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"],
|
||||
[/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"],
|
||||
[/\bproxy-сигналы\b/giu, "косвенные признаки"],
|
||||
[/\bproxy\b/giu, "косвенный показатель"],
|
||||
[/\bsales-to-stock\b/giu, "отношение продаж к остатку"],
|
||||
[/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"],
|
||||
[/\bP&L\b/gu, "полный отчет о прибылях и убытках"]
|
||||
];
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
text = text.replace(pattern, replacement);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function localizeLine(value: string): string {
|
||||
@@ -454,6 +476,11 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
||||
: null;
|
||||
}
|
||||
|
||||
function joinBusinessReplyLines(lines: string[]): string | null {
|
||||
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
function businessOverviewYearRowsLine(overview: Record<string, unknown>): string | null {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const values = years
|
||||
@@ -670,8 +697,7 @@ function buildCompactBidirectionalValueFlowReply(
|
||||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||
}
|
||||
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
function compactComparable(value: string | null): string {
|
||||
@@ -883,8 +909,7 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(
|
||||
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
@@ -909,8 +934,7 @@ function buildCompactBusinessOverviewReply(
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (debtDueDateBoundary) {
|
||||
@@ -928,14 +952,15 @@ function buildCompactBusinessOverviewReply(
|
||||
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(
|
||||
`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
|
||||
);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
|
||||
} else if (status === "no_payment_terms_configured") {
|
||||
lines.push(
|
||||
`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
|
||||
);
|
||||
lines.push(
|
||||
rowsWithAmount !== null
|
||||
@@ -944,31 +969,29 @@ function buildCompactBusinessOverviewReply(
|
||||
);
|
||||
} else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(
|
||||
`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
|
||||
);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
|
||||
}
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
|
||||
);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута."
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты."
|
||||
);
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging."
|
||||
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (vendorRiskBoundary) {
|
||||
@@ -996,7 +1019,7 @@ function buildCompactBusinessOverviewReply(
|
||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
||||
if (status === "financial_institution_leads_outgoing_cash") {
|
||||
lines.push(
|
||||
`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||
);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
@@ -1009,11 +1032,11 @@ function buildCompactBusinessOverviewReply(
|
||||
}
|
||||
} else if (status === "reviewed_procurement_concentration") {
|
||||
lines.push(
|
||||
`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
||||
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
|
||||
);
|
||||
}
|
||||
const contractText =
|
||||
@@ -1023,10 +1046,9 @@ function buildCompactBusinessOverviewReply(
|
||||
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
|
||||
: "";
|
||||
lines.push(
|
||||
`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
|
||||
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
@@ -1035,7 +1057,9 @@ function buildCompactBusinessOverviewReply(
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
|
||||
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
lines.push(
|
||||
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
|
||||
);
|
||||
@@ -1043,27 +1067,23 @@ function buildCompactBusinessOverviewReply(
|
||||
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
|
||||
);
|
||||
lines.push(
|
||||
"Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
|
||||
"Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
reserveBasis
|
||||
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
|
||||
);
|
||||
if (inventoryQualityEvents) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
@@ -1075,10 +1095,9 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва."
|
||||
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва."
|
||||
);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
@@ -1095,8 +1114,7 @@ function buildCompactBusinessOverviewReply(
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (rankingNeed) {
|
||||
@@ -1205,8 +1223,7 @@ function buildCompactBusinessOverviewReply(
|
||||
lines.push(limitLine);
|
||||
}
|
||||
lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода.");
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): AssistantMcpDiscoveryResponseCandidateStatus {
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("нельзя точно подтвердить чистую прибыль");
|
||||
expect(candidate.reply_text).toContain("P&L");
|
||||
expect(candidate.reply_text).toContain("полный отчет о прибылях и убытках");
|
||||
expect(candidate.reply_text).toContain("себестоимости");
|
||||
expect(candidate.reply_text).not.toContain("47 628 853");
|
||||
});
|
||||
@@ -208,7 +208,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("риск зависимости");
|
||||
expect(candidate.reply_text).toContain("outgoing cash concentration proxy");
|
||||
expect(candidate.reply_text).toContain("сигнал концентрации исходящих денег");
|
||||
expect(candidate.reply_text).toContain("банк/финансовая организация");
|
||||
expect(candidate.reply_text).toContain("не доказанная зависимость от обычного поставщика");
|
||||
expect(candidate.reply_text).not.toContain("крупнейший подтвержденный поставщик/получатель исходящих платежей: СБЕРБАНК");
|
||||
@@ -296,11 +296,12 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("procurement-concentration route");
|
||||
expect(candidate.reply_text).toContain("проверка концентрации закупок/исходящих платежей");
|
||||
expect(candidate.reply_text).toContain("банк/финансовая организация");
|
||||
expect(candidate.reply_text).toContain("Поставщик А");
|
||||
expect(candidate.reply_text).toContain("надежность поставщика");
|
||||
expect(candidate.reply_text).not.toContain("outgoing cash concentration proxy");
|
||||
expect(candidate.reply_text).not.toContain("procurement-concentration route");
|
||||
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
|
||||
});
|
||||
|
||||
@@ -464,7 +465,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("12 474 036,91 руб");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший источник входящих денег: ГКУ УКРиС");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший получатель исходящих денег: ООО Поставщик");
|
||||
expect(candidate.reply_text).toContain("денежный operating-flow proxy");
|
||||
expect(candidate.reply_text).toContain("денежный операционный показатель");
|
||||
expect(candidate.reply_text).not.toContain("Что можно сказать только как вывод:");
|
||||
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user