From 0c6440fc5ef2c0e338120f5983eec7178fb3c4b2 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 18 Apr 2026 15:32:05 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B0:=20=D0=90=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0:=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20counterparty=20=D0=B8=20contra?= =?UTF-8?q?ct=20ranking-=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=8B=20business-fi?= =?UTF-8?q?rst=20=D0=B1=D0=B5=D0=B7=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20preamble?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 6 +++- .../counterpartyAnalyticsReplyBuilders.js | 30 ++++++++----------- .../counterpartyAnalyticsReplyBuilders.ts | 30 ++++++++----------- .../tests/addressQueryRuntimeM23.test.ts | 9 ++++-- 4 files changed, 35 insertions(+), 40 deletions(-) diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 0b51efd..44121f0 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -252,7 +252,11 @@ Latest continuity-authority convergence evidence after the current route pass: - `vat_payable_forecast` and `vat_liability_confirmed_for_tax_period` now open with a business-first `Коротко: ...` lead, while the detailed calculation stays in the secondary block; - service-flavored top lines like `Собран прогноз...`, `Режим результата...`, and `Строк агрегата...` are removed from the first screen of the reply, which makes VAT answers read like user-facing guidance instead of an engine report; - VAT reply tests now explicitly protect this top-block shape, so future changes cannot silently reintroduce the same mechanical preamble; - - this is still not the end of shaping work: some ranking families and long evidence-heavy replies still need the same cleanup; +- the next human-answer-shaping cleanup pass is now applied to counterparty ranking/profile replies: + - `counterparty_activity_lifecycle`, `contract_usage_overview`, `customer_revenue_and_payments`, `supplier_payouts_profile`, and `contract_usage_and_value` now open with business-first wording instead of service-flavored `профиль собран / строк агрегата / строк источника`; + - ranking and contract replies now preserve user wording better in the visible heading layer, including `минимальный бюджет` phrasing for low-turnover active contracts; + - targeted ranking/profile tests now protect the new top-block shape, so these families are less likely to regress back into report-like wording during later route/domain work; + - this is still not the end of shaping work: some long evidence-heavy replies and residual catalog-style blocks still need the same cleanup; - this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/address_runtime/counterpartyAnalyticsReplyBuilders.js b/llm_normalizer/backend/dist/services/address_runtime/counterpartyAnalyticsReplyBuilders.js index be94f8e..0925f16 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/counterpartyAnalyticsReplyBuilders.js +++ b/llm_normalizer/backend/dist/services/address_runtime/counterpartyAnalyticsReplyBuilders.js @@ -304,14 +304,12 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) { } const lines = longevityQuestion ? [ - `Заказчиков с самым длинным горизонтом сотрудничества: ${counterparties.length}.`, - "Собран профиль длительности сотрудничества по годам и частоте активности.", - `Строк агрегата: ${rows.length}.` + `Коротко: заказчиков с самым длинным горизонтом сотрудничества — ${counterparties.length}.`, + `Оценка собрана по подтвержденной активности в 1С: ${rows.length} строк в выборке.` ] : [ - `Активные заказчики ${scopeLabel}: ${counterparties.length}.`, - "Собран профиль активности заказчиков по платежным документам.", - `Строк агрегата: ${rows.length}.` + `Коротко: активных заказчиков ${scopeLabel} — ${counterparties.length}.`, + `Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.` ]; if (counterparties.length === 0) { lines.push(longevityQuestion @@ -351,9 +349,8 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) { ? `Использованных договоров: ${usedContracts} из ${totalContracts}${usedShare ? ` (${usedShare})` : ""}.` : `Использованных договоров с подтвержденной связью с операциями: ${usedContracts}.`; const lines = [ - usageLead, - "Профиль договорной базы собран по справочнику и подтвержденным операциям.", - `Строк агрегата: ${rows.length}.` + `Коротко: ${usageLead.replace(/\.$/, "")}.`, + `Что видно по договорной базе: ${rows.length} строк в подтвержденной выборке.` ]; if (totalContracts > 0) { lines.push(`Всего договоров в базе: ${totalContracts}.`); @@ -454,10 +451,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) { .sort((a, b) => a.amount - b.amount || (a.period ?? "").localeCompare(b.period ?? "")); const lines = [ isSupplier - ? "Собран профиль выплат поставщикам по платежным документам." - : "Собран профиль поступлений от заказчиков по платежным документам.", - `Строк источника: ${rows.length}.`, - `Уникальных контрагентов: ${profileRows.length}.` + ? `Коротко: в выборке ${profileRows.length} поставщиков по ${rows.length} подтвержденным платежным строкам.` + : `Коротко: в выборке ${profileRows.length} заказчиков по ${rows.length} подтвержденным платежным строкам.`, + `Подтвержденный денежный поток в выборке: ${deps.formatMoneyRub(totalFlow)}.` ]; if (profileRows.length === 0) { lines.push("По выбранному окну данных платежные строки не найдены."); @@ -614,10 +610,8 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) { .filter((item) => item.docs > 0 && item.turnover > 0) .sort((a, b) => a.turnover - b.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract)); const lines = [ - `Активных договоров: ${contractRows.length}.`, - "Собран профиль договоров по обороту и подтвержденным операциям.", - `Строк источника: ${rows.length}.`, - `Договорных агрегатов: ${contractRows.length}.` + `Коротко: активных договоров в выборке — ${contractRows.length}.`, + `Подтвержденных операций по договорам: ${rows.length}.` ]; if (contractRows.length === 0) { lines.push("В выбранном окне не найдено операций, связанных с договорами."); @@ -631,7 +625,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) { } if (focus === "bottom_by_turnover_active") { const visible = rankedBottomActive.slice(0, limit); - lines.unshift(`Топ-${visible.length} активных договоров с минимальным оборотом:`); + lines.unshift(`Топ-${visible.length} активных договоров с минимальным бюджетом:`); lines.push(...visible.map((item, index) => `${index + 1}. ${item.contract} | оборот: ${deps.formatMoneyRub(item.turnover)} | операций: ${item.docs} | последняя активность: ${formatOptionalDate(item.lastPeriod, deps.formatDateRu)}`)); return (0, replyContracts_1.buildFactualListReply)(lines); } diff --git a/llm_normalizer/backend/src/services/address_runtime/counterpartyAnalyticsReplyBuilders.ts b/llm_normalizer/backend/src/services/address_runtime/counterpartyAnalyticsReplyBuilders.ts index a72f4f3..e000452 100644 --- a/llm_normalizer/backend/src/services/address_runtime/counterpartyAnalyticsReplyBuilders.ts +++ b/llm_normalizer/backend/src/services/address_runtime/counterpartyAnalyticsReplyBuilders.ts @@ -419,14 +419,12 @@ export function composeCounterpartyAnalyticsReply( const lines: string[] = longevityQuestion ? [ - `Заказчиков с самым длинным горизонтом сотрудничества: ${counterparties.length}.`, - "Собран профиль длительности сотрудничества по годам и частоте активности.", - `Строк агрегата: ${rows.length}.` + `Коротко: заказчиков с самым длинным горизонтом сотрудничества — ${counterparties.length}.`, + `Оценка собрана по подтвержденной активности в 1С: ${rows.length} строк в выборке.` ] : [ - `Активные заказчики ${scopeLabel}: ${counterparties.length}.`, - "Собран профиль активности заказчиков по платежным документам.", - `Строк агрегата: ${rows.length}.` + `Коротко: активных заказчиков ${scopeLabel} — ${counterparties.length}.`, + `Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.` ]; if (counterparties.length === 0) { @@ -481,9 +479,8 @@ export function composeCounterpartyAnalyticsReply( : `Использованных договоров с подтвержденной связью с операциями: ${usedContracts}.`; const lines: string[] = [ - usageLead, - "Профиль договорной базы собран по справочнику и подтвержденным операциям.", - `Строк агрегата: ${rows.length}.` + `Коротко: ${usageLead.replace(/\.$/, "")}.`, + `Что видно по договорной базе: ${rows.length} строк в подтвержденной выборке.` ]; if (totalContracts > 0) { @@ -598,10 +595,9 @@ export function composeCounterpartyAnalyticsReply( const lines: string[] = [ isSupplier - ? "Собран профиль выплат поставщикам по платежным документам." - : "Собран профиль поступлений от заказчиков по платежным документам.", - `Строк источника: ${rows.length}.`, - `Уникальных контрагентов: ${profileRows.length}.` + ? `Коротко: в выборке ${profileRows.length} поставщиков по ${rows.length} подтвержденным платежным строкам.` + : `Коротко: в выборке ${profileRows.length} заказчиков по ${rows.length} подтвержденным платежным строкам.`, + `Подтвержденный денежный поток в выборке: ${deps.formatMoneyRub(totalFlow)}.` ]; if (profileRows.length === 0) { @@ -809,10 +805,8 @@ export function composeCounterpartyAnalyticsReply( .sort((a, b) => a.turnover - b.turnover || b.docs - a.docs || a.contract.localeCompare(b.contract)); const lines: string[] = [ - `Активных договоров: ${contractRows.length}.`, - "Собран профиль договоров по обороту и подтвержденным операциям.", - `Строк источника: ${rows.length}.`, - `Договорных агрегатов: ${contractRows.length}.` + `Коротко: активных договоров в выборке — ${contractRows.length}.`, + `Подтвержденных операций по договорам: ${rows.length}.` ]; if (contractRows.length === 0) { @@ -834,7 +828,7 @@ export function composeCounterpartyAnalyticsReply( if (focus === "bottom_by_turnover_active") { const visible = rankedBottomActive.slice(0, limit); - lines.unshift(`Топ-${visible.length} активных договоров с минимальным оборотом:`); + lines.unshift(`Топ-${visible.length} активных договоров с минимальным бюджетом:`); lines.push( ...visible.map( (item, index) => diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 1baf972..a96ce7e 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -1387,7 +1387,7 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); - expect(reply.text).toContain("Заказчиков с самым длинным горизонтом сотрудничества (по годам)"); + expect(reply.text).toContain("Коротко: заказчиков с самым длинным горизонтом сотрудничества"); expect(reply.text).toContain("Топ-"); expect(reply.text).toContain("лет в базе"); expect(reply.text).toContain("НОРТОН"); @@ -1448,9 +1448,9 @@ describe("address compose stage utf8 headers", () => { } ]); - expect(reply.text).toContain("Профиль договорной базы собран"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("Всего договоров в базе: 520."); - expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148."); + expect(reply.text).toContain("Использованных договоров"); expect(reply.text).toContain("Неиспользуемых договоров: 372."); }); @@ -1487,6 +1487,7 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("Топ-2 заказчиков по сумме поступлений:"); expect(reply.text).toContain("1. Клиент А | сумма: 800"); expect(reply.text).toContain("2. Клиент Б | сумма: 700"); @@ -1591,6 +1592,7 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("Топ-2 поставщиков по количеству исходящих платежных операций:"); expect(reply.text).toContain("1. Поставщик А | операций: 2"); }); @@ -1628,6 +1630,7 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("активных договоров с минимальным бюджетом"); expect(reply.text).toContain("1. Договор 02/20 | оборот: 100"); });