diff --git a/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md index 8ed2828..13db863 100644 --- a/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md @@ -25,15 +25,15 @@ This snapshot is based on: Latest graph rebuild: -- `5228 nodes` -- `11338 edges` -- `133 communities` +- `5251 nodes` +- `11337 edges` +- `136 communities` Most relevant current god nodes for turnaround `11`: 1. `resolveAddressIntent()` -2. `ChannelRegistry` -3. `composeFactualReply()` +2. `composeFactualReplyBody()` +3. `ChannelRegistry` 4. `CanonicalStore` 5. `compactWhitespace()` @@ -42,7 +42,7 @@ The relevant conclusion is not that every god node is part of turnaround `11`. The relevant conclusion is: - `resolveAddressIntent()` remains the main unresolved domain-intent concentration point; -- `composeFactualReply()` remains the main unresolved answer-shaping concentration point; +- `composeFactualReplyBody()` now carries the remaining answer-shaping concentration after packaging extraction; - `assistantService` still appears as a large coordinator-heavy community rather than a thin shell. ## What Is Already Real In Code @@ -126,7 +126,7 @@ This is enough to build targeted semantic packs that are not single-domain toy s ## Honest Phase Status -Estimated overall turnaround completion: `~87%` +Estimated overall turnaround completion: `~88%` ### Phase 0. Shared Baseline @@ -177,17 +177,19 @@ Remaining debt: ### Phase 4. Coverage / Evidence / Truth Gate Isolation -Status: `84%` +Status: `86%` Reason: - explicit truth and coverage/evidence contracts exist; - answer policy reads those contracts rather than rebuilding verdicts blindly from raw rows. +- reply-packaging mechanics are now explicitly split into `address_runtime/replyPackaging.ts` instead of staying fully in `composeStage.ts`. Remaining debt: -- `composeFactualReply()` is still a major concentration point; -- humanized blocked/limited semantics are not yet fully separated from final packaging logic across all paths. +- `composeFactualReplyBody()` is still a major concentration point; +- humanized blocked/limited semantics are not yet fully separated from answer semantics across all paths; +- `composeStage.ts` still remains too large even after packaging extraction. ### Phase 5. AssistantService Extraction @@ -244,6 +246,7 @@ Compared with the pre-turnaround baseline, the system is now materially better i - meta questions and memory recap are no longer purely incidental side effects of route logic; - organization data-scope probing is no longer owned only by coordinator-local helper bodies; - debug payload assembly is now further isolated from top-level turn coordination; +- reply formatting and reply-type classification now have an explicit owner outside `composeStage.ts`; - architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers. ## What Still Remains The Main Architectural Debt @@ -258,9 +261,9 @@ Intent resolution remains one of the most connected business nodes in the graph. This means capability and contour growth still concentrate pressure there. -### 3. `composeFactualReply()` is still too central +### 3. `composeFactualReplyBody()` is still too central -Truth contracts are now explicit, but final answer-shaping still retains too much architecture weight. +Truth contracts are now explicit, and reply packaging has started moving into its own owner, but final answer-shaping still retains too much architecture weight. This is the main remaining reason why user-facing humanization and limitation semantics are not completely isolated yet. @@ -281,7 +284,7 @@ But not every business family has reached the same contract maturity. The next honest architecture slice should be: 1. continue reducing `assistantService.ts` to a thinner coordinator; -2. isolate answer-shaping semantics further away from `composeFactualReply()`; +2. continue isolating answer semantics further away from `composeFactualReply()` now that reply packaging has its own owner seam; 3. keep extending AGENT packs with mixed business + meta + interruption patterns instead of single-family smoke tests; 4. keep using scenario acceptance as the main sign-off rather than unit-test green status alone. diff --git a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js index f3de3fa..f1de795 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -4,6 +4,7 @@ exports.contractCandidatesFromRows = contractCandidatesFromRows; exports.composeFactualReply = composeFactualReply; exports.inferReplyType = inferReplyType; const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher"); +const replyPackaging_1 = require("./replyPackaging"); function uniqueStrings(values) { return Array.from(new Set(values .map((item) => item.trim()) @@ -2106,8 +2107,13 @@ function buildOpenContractRiskAggregate(rows) { }); } function composeFactualReply(intent, rows, options = {}) { - const applyNumericEmphasis = (line) => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line); - const joinLines = (lines) => lines.map(applyNumericEmphasis).join("\n"); + return (0, replyPackaging_1.finalizeComposeReplyResult)(composeFactualReplyBody(intent, rows, options), { + emphasizeNumbers: options.emphasizeNumbers, + emphasizeNumericTokens + }); +} +function composeFactualReplyBody(intent, rows, options = {}) { + const joinLines = replyPackaging_1.joinComposeReplyLines; if (intent === "document_type_and_account_section_profile") { const rowsByMarker = new Map(); for (const row of rows) { @@ -4370,8 +4376,5 @@ function composeFactualReply(intent, rows, options = {}) { }; } function inferReplyType(responseType) { - if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { - return "factual"; - } - return "partial_coverage"; + return (0, replyPackaging_1.inferAddressReplyType)(responseType); } diff --git a/llm_normalizer/backend/dist/services/address_runtime/replyPackaging.js b/llm_normalizer/backend/dist/services/address_runtime/replyPackaging.js new file mode 100644 index 0000000..1a19741 --- /dev/null +++ b/llm_normalizer/backend/dist/services/address_runtime/replyPackaging.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.joinComposeReplyLines = joinComposeReplyLines; +exports.finalizeComposeReplyResult = finalizeComposeReplyResult; +exports.inferAddressReplyType = inferAddressReplyType; +function joinComposeReplyLines(lines) { + return lines.join("\n"); +} +function finalizeComposeReplyResult(result, options = {}) { + if (!options.emphasizeNumbers || typeof options.emphasizeNumericTokens !== "function") { + return result; + } + return { + ...result, + text: String(result.text ?? "") + .split("\n") + .map((line) => options.emphasizeNumericTokens(line)) + .join("\n") + }; +} +function inferAddressReplyType(responseType) { + if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { + return "factual"; + } + return "partial_coverage"; +} diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index eda5b03..ae26025 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -5,6 +5,16 @@ import type { AddressResultMode } from "../../types/addressQuery"; import { normalizeOrganizationScopeValue } from "../assistantOrganizationMatcher"; +import { + finalizeComposeReplyResult, + inferAddressReplyType, + joinComposeReplyLines, + type ComposeFactualReplyOptions as ComposeFactualReplyOptionsBase, + type ComposeReplyResult, + type ComposeReplySemantics +} from "./replyPackaging"; + +export type { ComposeFactualReplyOptions, ComposeReplySemantics } from "./replyPackaging"; export interface ComposeStageRow { period: string | null; @@ -39,26 +49,7 @@ export interface VatDirectSourceProbeSummary { errors: string[]; } -interface ComposeFactualReplyOptions { - userMessage?: string; - itemHint?: string; - counterpartyHint?: string; - organizationHint?: string; - accountHint?: string; - periodFrom?: string; - periodTo?: string; - asOfDate?: string; - requestedResultMode?: AddressResultMode; - vatDirectSourceProbe?: VatDirectSourceProbeSummary | null; - emphasizeNumbers?: boolean; - useRubCurrency?: boolean; -} - -export interface ComposeReplySemantics { - result_mode?: AddressResultMode; - evidence_strength?: AddressEvidenceStrength; - balance_confirmed?: boolean; -} +type ComposeFactualReplyOptions = ComposeFactualReplyOptionsBase; type PeriodProfileFocus = | "full_profile" @@ -2725,9 +2716,19 @@ export function composeFactualReply( intent: AddressIntent, rows: ComposeStageRow[], options: ComposeFactualReplyOptions = {} -): { responseType: AddressResponseType; text: string; semantics?: ComposeReplySemantics } { - const applyNumericEmphasis = (line: string): string => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line); - const joinLines = (lines: string[]): string => lines.map(applyNumericEmphasis).join("\n"); +) { + return finalizeComposeReplyResult(composeFactualReplyBody(intent, rows, options), { + emphasizeNumbers: options.emphasizeNumbers, + emphasizeNumericTokens + }); +} + +function composeFactualReplyBody( + intent: AddressIntent, + rows: ComposeStageRow[], + options: ComposeFactualReplyOptions = {} +): ComposeReplyResult { + const joinLines = joinComposeReplyLines; if (intent === "document_type_and_account_section_profile") { const rowsByMarker = new Map(); @@ -5496,8 +5497,5 @@ export function composeFactualReply( } export function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" { - if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { - return "factual"; - } - return "partial_coverage"; + return inferAddressReplyType(responseType); } diff --git a/llm_normalizer/backend/src/services/address_runtime/replyPackaging.ts b/llm_normalizer/backend/src/services/address_runtime/replyPackaging.ts new file mode 100644 index 0000000..1b1beae --- /dev/null +++ b/llm_normalizer/backend/src/services/address_runtime/replyPackaging.ts @@ -0,0 +1,61 @@ +import type { AddressEvidenceStrength, AddressResponseType, AddressResultMode } from "../../types/addressQuery"; + +export interface ComposeFactualReplyOptions { + userMessage?: string; + itemHint?: string; + counterpartyHint?: string; + organizationHint?: string; + accountHint?: string; + periodFrom?: string; + periodTo?: string; + asOfDate?: string; + requestedResultMode?: AddressResultMode; + vatDirectSourceProbe?: TVatDirectSourceProbe | null; + emphasizeNumbers?: boolean; + useRubCurrency?: boolean; +} + +export interface ComposeReplySemantics { + result_mode?: AddressResultMode; + evidence_strength?: AddressEvidenceStrength; + balance_confirmed?: boolean; +} + +export interface ComposeReplyResult { + responseType: AddressResponseType; + text: string; + semantics?: ComposeReplySemantics; +} + +export interface FinalizeComposeReplyOptions { + emphasizeNumbers?: boolean; + emphasizeNumericTokens?: (line: string) => string; +} + +export function joinComposeReplyLines(lines: string[]): string { + return lines.join("\n"); +} + +export function finalizeComposeReplyResult( + result: ComposeReplyResult, + options: FinalizeComposeReplyOptions = {} +): ComposeReplyResult { + if (!options.emphasizeNumbers || typeof options.emphasizeNumericTokens !== "function") { + return result; + } + + return { + ...result, + text: String(result.text ?? "") + .split("\n") + .map((line) => options.emphasizeNumericTokens!(line)) + .join("\n") + }; +} + +export function inferAddressReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" { + if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { + return "factual"; + } + return "partial_coverage"; +} diff --git a/llm_normalizer/backend/tests/replyPackaging.test.ts b/llm_normalizer/backend/tests/replyPackaging.test.ts new file mode 100644 index 0000000..9e85823 --- /dev/null +++ b/llm_normalizer/backend/tests/replyPackaging.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; + +import { + finalizeComposeReplyResult, + inferAddressReplyType, + joinComposeReplyLines +} from "../src/services/address_runtime/replyPackaging"; + +describe("replyPackaging", () => { + it("joins reply lines without changing their order", () => { + expect(joinComposeReplyLines(["Первая строка", "Вторая строка"])).toBe("Первая строка\nВторая строка"); + }); + + it("applies numeric emphasis line-by-line only when requested", () => { + const result = finalizeComposeReplyResult( + { + responseType: "FACTUAL_SUMMARY", + text: "Сумма 1500\nОстаток 22", + semantics: { result_mode: "aggregate" } + }, + { + emphasizeNumbers: true, + emphasizeNumericTokens: (line) => line.replace(/\d+/g, (match) => `**${match}**`) + } + ); + + expect(result.text).toBe("Сумма **1500**\nОстаток **22**"); + expect(result.semantics).toEqual({ result_mode: "aggregate" }); + }); + + it("keeps reply text unchanged when emphasis is disabled", () => { + const result = finalizeComposeReplyResult( + { + responseType: "FACTUAL_SUMMARY", + text: "Сумма 1500" + }, + { + emphasizeNumbers: false, + emphasizeNumericTokens: () => "не должно примениться" + } + ); + + expect(result.text).toBe("Сумма 1500"); + }); + + it("maps factual response types away from partial coverage", () => { + expect(inferAddressReplyType("FACTUAL_LIST")).toBe("factual"); + expect(inferAddressReplyType("FACTUAL_SUMMARY")).toBe("factual"); + expect(inferAddressReplyType("SOFT_REFUSAL")).toBe("partial_coverage"); + }); +});