Очистить бизнес-язык ответов phase98

This commit is contained in:
2026-05-13 02:26:08 +03:00
parent 0028ef24a0
commit b1a5e4435d
11 changed files with 673 additions and 113 deletions
@@ -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("Складской срез");
});
@@ -1,4 +1,54 @@
[
{
"generation_id": "gen-ag05122315-f1e27c",
"created_at": "2026-05-12T23:15:48+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"count": 6,
"domain": "address_phase98_limit_honesty_business_language",
"questions": [
"По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"То есть просрочку доказать нельзя, коротко почему?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"А кто принес больше всего денег за 2020?",
"По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"А зависимость от одного поставщика за 2020 можно точно оценить?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260512231548_gen-ag05122315-f1e27c.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260512231548_gen-ag05122315-f1e27c.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase98_limit_honesty_business_language.json",
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
"semantic_tags": [
"business_language",
"customer_revenue_and_payments",
"debt_due_date_aging_quality",
"debug_leak_guard",
"financial_counterparty_flow_hint",
"followup_directness",
"inventory_reserve_liquidation_quality",
"limit_honesty",
"vat",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag05122250-4451a8",
"created_at": "2026-05-12T22:50:23+00:00",
@@ -0,0 +1,155 @@
{
"saved_at": "2026-05-12T23:15:48+00:00",
"generation_id": "gen-ag05122315-f1e27c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"agent_run": true,
"questions": [
"По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"То есть просрочку доказать нельзя, коротко почему?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"А кто принес больше всего денег за 2020?",
"По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"А зависимость от одного поставщика за 2020 можно точно оценить?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase98_limit_honesty_business_language.json",
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
"semantic_tags": [
"business_language",
"customer_revenue_and_payments",
"debt_due_date_aging_quality",
"debug_leak_guard",
"financial_counterparty_flow_hint",
"followup_directness",
"inventory_reserve_liquidation_quality",
"limit_honesty",
"vat",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "То есть просрочку доказать нельзя, коротко почему?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А кто принес больше всего денег за 2020?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А зависимость от одного поставщика за 2020 можно точно оценить?",
"created_at": "2026-05-12T23:15:48+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase98_limit_honesty_business_language.json",
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
"semantic_tags": [
"business_language",
"customer_revenue_and_payments",
"debt_due_date_aging_quality",
"debug_leak_guard",
"financial_counterparty_flow_hint",
"followup_directness",
"inventory_reserve_liquidation_quality",
"limit_honesty",
"vat",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase98_limit_honesty_business_language_live3",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}
@@ -0,0 +1,43 @@
{
"suite_id": "assistant_saved_session_gen-ag05122315-f1e27c",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T23:15:48+00:00",
"generation_id": "gen-ag05122315-f1e27c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"domain": "address_phase98_limit_honesty_business_language",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 98 limit honesty and business-language replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "То есть просрочку доказать нельзя, коротко почему?"
},
{
"user_message": "НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "А кто принес больше всего денег за 2020?"
},
{
"user_message": "По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
},
{
"user_message": "А зависимость от одного поставщика за 2020 можно точно оценить?"
}
]
}
]
}