From 4bb0187510bcbb8dfefcb0c8e73aacd163b6e13d Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 18 Apr 2026 15:17:38 +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=20profile=20exact-=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=8B=20business-first=20=D0=B8=20=D1=83?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D1=81=D0=BB=D1=83=D0=B6=D0=B5?= =?UTF-8?q?=D0=B1=D0=BD=D1=8B=D0=B9=20aggregate-=D0=BFreamble?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 4 ++ .../services/address_runtime/composeStage.js | 67 ++++++++++++++---- .../services/address_runtime/composeStage.ts | 70 +++++++++++++++---- .../tests/addressQueryRuntimeM23.test.ts | 8 ++- 4 files changed, 119 insertions(+), 30 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 f6d0600..fcdf4ef 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 @@ -245,6 +245,10 @@ Latest continuity-authority convergence evidence after the current route pass: - deterministic organization-fact boundary replies can now trigger from grounded continuity even when `sessionScope.selectedOrganization` and `sessionScope.activeOrganization` are both empty at runtime entry; - the chat layer now records whether it entered with grounded continuity and which organization came from that continuity snapshot, making future saved-session review less blind; - proactive organization offer logic is now explicitly blocked when grounded address continuity already exists, so the chat layer does not re-offer company selection on top of an already grounded business session; +- the first human-answer-shaping cleanup pass is now applied to heavy profile/aggregate exact answers: + - `period_coverage_profile` and `document_type_and_account_section_profile` now start with a direct business-first lead (`Коротко: ...`) instead of service-flavored intros like `профиль собран` / `строк агрегата`; + - the top block now states the business conclusion first and leaves ranked detail blocks below, which reduces the catalog-like feel without hiding the actual data; + - this is still only the first shaping seam: other long exact replies, especially VAT and some ranking families, still need the same treatment; - 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/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js index bb9303c..4c2fce9 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -2229,10 +2229,30 @@ function composeFactualReplyBody(intent, rows, options = {}) { const includeSections = focus === "full_profile" || focus === "sections_only" || focus === "sections_rare_only"; const includeDocTypesLowOnly = focus === "doc_types_rare_only"; const includeSectionsLowOnly = focus === "sections_rare_only"; - const lines = [ - "Профиль типов документов и разделов учета собран (movement-based aggregate).", - `Строк агрегата: ${rows.length}.` - ]; + const topDocType = docTypeRanking[0] ?? null; + const rareDocType = docTypeRankingLow[0] ?? null; + const topSection = sectionRanking[0] ?? null; + const rareSection = sectionRankingLow[0] ?? null; + const formatSectionTitle = (section) => { + const label = ACCOUNT_SECTION_LABELS[section]; + return label ? `${section} (${label})` : section; + }; + const directLeadParts = []; + if (includeDocTypesLowOnly && rareDocType) { + directLeadParts.push(`реже всего используется тип документа ${rareDocType.docType} (${rareDocType.count})`); + } + else if (includeDocTypes && topDocType) { + directLeadParts.push(`чаще всего используется тип документа ${topDocType.docType} (${topDocType.count})`); + } + if (includeSectionsLowOnly && rareSection) { + directLeadParts.push(`среди разделов учета минимальная активность у ${formatSectionTitle(rareSection.section)} (${rareSection.count})`); + } + else if (includeSections && topSection) { + directLeadParts.push(`среди разделов учета лидирует ${formatSectionTitle(topSection.section)} (${topSection.count})`); + } + const lines = directLeadParts.length > 0 + ? [`Коротко: ${directLeadParts.join("; ")}.`] + : ["Коротко: по доступному срезу профиль типов документов и разделов учета не подтвердился."]; if (includeDocTypes) { if (docTypeRanking.length > 0) { if (includeDocTypesLowOnly) { @@ -2263,8 +2283,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { if (includeSectionsLowOnly) { lines.push("Наименее заполненные разделы учета (по операциям Дт+Кт):"); lines.push(...sectionRankingLow.map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); const share = formatPercent(item.count, sectionTotal); return share ? `${index + 1}. ${sectionTitle}: ${item.count} (${share})` @@ -2274,8 +2293,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { else { lines.push("Наиболее заполненные разделы учета (по операциям Дт+Кт):"); lines.push(...sectionRanking.slice(0, 10).map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); const share = formatPercent(item.count, sectionTotal); return share ? `${index + 1}. ${sectionTitle}: ${item.count} (${share})` @@ -2283,8 +2301,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { })); lines.push("Разделы с минимальной активностью (среди использованных):"); lines.push(...sectionRankingLow.map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); return `${index + 1}. ${sectionTitle}: ${item.count}`; })); } @@ -2366,10 +2383,32 @@ function composeFactualReplyBody(intent, rows, options = {}) { operationalWindow.operationalTo !== null && operationalWindow.dataTo !== null && operationalWindow.operationalTo < operationalWindow.dataTo; - const lines = [ - "Профиль периодов базы собран (movement-based aggregate).", - `Строк агрегата: ${rows.length}.` - ]; + const directLeadParts = []; + if (includeCoverage) { + if (hasTailYears && + operationalWindow.operationalFrom !== null && + operationalWindow.operationalTo !== null) { + directLeadParts.push(`рабочий период с выраженной активностью: ${operationalWindow.operationalFrom}..${operationalWindow.operationalTo}`); + } + else if (minDate || maxDate) { + directLeadParts.push(`данные в базе подтверждены за период ${minDate ?? "н/д"} .. ${maxDate ?? "н/д"}`); + } + } + if (includeTopYear && topYearByDocs) { + directLeadParts.push(`самый активный год по документам — ${topYearByDocs.year} (${topYearByDocs.count})`); + } + if (includeBottomYear && bottomYearByDocs) { + directLeadParts.push(`самый спокойный год по документам — ${bottomYearByDocs.year} (${bottomYearByDocs.count})`); + } + if (includeTopMonth && topMonthByOps) { + directLeadParts.push(`самый активный месяц по операциям — ${topMonthByOps.month} (${topMonthByOps.count})`); + } + if (includeBottomMonth && bottomMonthByOps) { + directLeadParts.push(`самый спокойный месяц по операциям — ${bottomMonthByOps.month} (${bottomMonthByOps.count})`); + } + const lines = directLeadParts.length > 0 + ? [`Коротко: ${directLeadParts.join("; ")}.`] + : ["Коротко: по доступному срезу периодический профиль базы не подтвердился."]; if (includeCoverage) { if (hasTailYears && operationalWindow.operationalFrom !== null && diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index 05c2013..5ddafc6 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -2861,11 +2861,30 @@ function composeFactualReplyBody( focus === "full_profile" || focus === "sections_only" || focus === "sections_rare_only"; const includeDocTypesLowOnly = focus === "doc_types_rare_only"; const includeSectionsLowOnly = focus === "sections_rare_only"; + const topDocType = docTypeRanking[0] ?? null; + const rareDocType = docTypeRankingLow[0] ?? null; + const topSection = sectionRanking[0] ?? null; + const rareSection = sectionRankingLow[0] ?? null; + const formatSectionTitle = (section: string): string => { + const label = ACCOUNT_SECTION_LABELS[section]; + return label ? `${section} (${label})` : section; + }; - const lines: string[] = [ - "Профиль типов документов и разделов учета собран (movement-based aggregate).", - `Строк агрегата: ${rows.length}.` - ]; + const directLeadParts: string[] = []; + if (includeDocTypesLowOnly && rareDocType) { + directLeadParts.push(`реже всего используется тип документа ${rareDocType.docType} (${rareDocType.count})`); + } else if (includeDocTypes && topDocType) { + directLeadParts.push(`чаще всего используется тип документа ${topDocType.docType} (${topDocType.count})`); + } + if (includeSectionsLowOnly && rareSection) { + directLeadParts.push(`среди разделов учета минимальная активность у ${formatSectionTitle(rareSection.section)} (${rareSection.count})`); + } else if (includeSections && topSection) { + directLeadParts.push(`среди разделов учета лидирует ${formatSectionTitle(topSection.section)} (${topSection.count})`); + } + + const lines: string[] = directLeadParts.length > 0 + ? [`Коротко: ${directLeadParts.join("; ")}.`] + : ["Коротко: по доступному срезу профиль типов документов и разделов учета не подтвердился."]; if (includeDocTypes) { if (docTypeRanking.length > 0) { @@ -2901,8 +2920,7 @@ function composeFactualReplyBody( lines.push("Наименее заполненные разделы учета (по операциям Дт+Кт):"); lines.push( ...sectionRankingLow.map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); const share = formatPercent(item.count, sectionTotal); return share ? `${index + 1}. ${sectionTitle}: ${item.count} (${share})` @@ -2913,8 +2931,7 @@ function composeFactualReplyBody( lines.push("Наиболее заполненные разделы учета (по операциям Дт+Кт):"); lines.push( ...sectionRanking.slice(0, 10).map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); const share = formatPercent(item.count, sectionTotal); return share ? `${index + 1}. ${sectionTitle}: ${item.count} (${share})` @@ -2925,8 +2942,7 @@ function composeFactualReplyBody( lines.push("Разделы с минимальной активностью (среди использованных):"); lines.push( ...sectionRankingLow.map((item, index) => { - const label = ACCOUNT_SECTION_LABELS[item.section]; - const sectionTitle = label ? `${item.section} (${label})` : item.section; + const sectionTitle = formatSectionTitle(item.section); return `${index + 1}. ${sectionTitle}: ${item.count}`; }) ); @@ -3024,10 +3040,36 @@ function composeFactualReplyBody( operationalWindow.dataTo !== null && operationalWindow.operationalTo < operationalWindow.dataTo; - const lines: string[] = [ - "Профиль периодов базы собран (movement-based aggregate).", - `Строк агрегата: ${rows.length}.` - ]; + const directLeadParts: string[] = []; + if (includeCoverage) { + if ( + hasTailYears && + operationalWindow.operationalFrom !== null && + operationalWindow.operationalTo !== null + ) { + directLeadParts.push( + `рабочий период с выраженной активностью: ${operationalWindow.operationalFrom}..${operationalWindow.operationalTo}` + ); + } else if (minDate || maxDate) { + directLeadParts.push(`данные в базе подтверждены за период ${minDate ?? "н/д"} .. ${maxDate ?? "н/д"}`); + } + } + if (includeTopYear && topYearByDocs) { + directLeadParts.push(`самый активный год по документам — ${topYearByDocs.year} (${topYearByDocs.count})`); + } + if (includeBottomYear && bottomYearByDocs) { + directLeadParts.push(`самый спокойный год по документам — ${bottomYearByDocs.year} (${bottomYearByDocs.count})`); + } + if (includeTopMonth && topMonthByOps) { + directLeadParts.push(`самый активный месяц по операциям — ${topMonthByOps.month} (${topMonthByOps.count})`); + } + if (includeBottomMonth && bottomMonthByOps) { + directLeadParts.push(`самый спокойный месяц по операциям — ${bottomMonthByOps.month} (${bottomMonthByOps.count})`); + } + + const lines: string[] = directLeadParts.length > 0 + ? [`Коротко: ${directLeadParts.join("; ")}.`] + : ["Коротко: по доступному срезу периодический профиль базы не подтвердился."]; if (includeCoverage) { if ( diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 05b693f..20fa911 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -628,9 +628,11 @@ describe("address compose stage utf8 headers", () => { } ]); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); - expect(reply.text).toContain("Профиль периодов базы собран"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("Самый активный год по документам: 2019 (1004)."); expect(reply.text).toContain("Самый активный месяц по операциям: 2015-02 (1249)."); + expect(reply.text).not.toContain("Строк агрегата"); + expect(reply.text).not.toContain("movement-based aggregate"); }); it("renders document type + account section profile summary", () => { @@ -685,10 +687,12 @@ describe("address compose stage utf8 headers", () => { } ]); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); - expect(reply.text).toContain("Профиль типов документов и разделов учета собран"); + expect(reply.text).toContain("Коротко:"); expect(reply.text).toContain("Списание с расчетного счета: 2352"); expect(reply.text).toContain("90 (Продажи): 2973"); expect(reply.text).toContain("58 (Финансовые вложения): 2"); + expect(reply.text).not.toContain("Строк агрегата"); + expect(reply.text).not.toContain("movement-based aggregate"); }); it("returns focused answer for active year question (without month block)", () => {