АРЧ АП11 - Архитектура: вынести reply packaging из composeStage в отдельный owner

This commit is contained in:
dctouch 2026-04-17 17:21:26 +03:00
parent 18e4eba54e
commit c2bafcff9b
6 changed files with 188 additions and 46 deletions

View File

@ -25,15 +25,15 @@ This snapshot is based on:
Latest graph rebuild: Latest graph rebuild:
- `5228 nodes` - `5251 nodes`
- `11338 edges` - `11337 edges`
- `133 communities` - `136 communities`
Most relevant current god nodes for turnaround `11`: Most relevant current god nodes for turnaround `11`:
1. `resolveAddressIntent()` 1. `resolveAddressIntent()`
2. `ChannelRegistry` 2. `composeFactualReplyBody()`
3. `composeFactualReply()` 3. `ChannelRegistry`
4. `CanonicalStore` 4. `CanonicalStore`
5. `compactWhitespace()` 5. `compactWhitespace()`
@ -42,7 +42,7 @@ The relevant conclusion is not that every god node is part of turnaround `11`.
The relevant conclusion is: The relevant conclusion is:
- `resolveAddressIntent()` remains the main unresolved domain-intent concentration point; - `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. - `assistantService` still appears as a large coordinator-heavy community rather than a thin shell.
## What Is Already Real In Code ## 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 ## Honest Phase Status
Estimated overall turnaround completion: `~87%` Estimated overall turnaround completion: `~88%`
### Phase 0. Shared Baseline ### Phase 0. Shared Baseline
@ -177,17 +177,19 @@ Remaining debt:
### Phase 4. Coverage / Evidence / Truth Gate Isolation ### Phase 4. Coverage / Evidence / Truth Gate Isolation
Status: `84%` Status: `86%`
Reason: Reason:
- explicit truth and coverage/evidence contracts exist; - explicit truth and coverage/evidence contracts exist;
- answer policy reads those contracts rather than rebuilding verdicts blindly from raw rows. - 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: Remaining debt:
- `composeFactualReply()` is still a major concentration point; - `composeFactualReplyBody()` is still a major concentration point;
- humanized blocked/limited semantics are not yet fully separated from final packaging logic across all paths. - 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 ### 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; - 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; - 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; - 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. - 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 ## 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. 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. 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: The next honest architecture slice should be:
1. continue reducing `assistantService.ts` to a thinner coordinator; 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; 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. 4. keep using scenario acceptance as the main sign-off rather than unit-test green status alone.

View File

@ -4,6 +4,7 @@ exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply; exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType; exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher"); const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const replyPackaging_1 = require("./replyPackaging");
function uniqueStrings(values) { function uniqueStrings(values) {
return Array.from(new Set(values return Array.from(new Set(values
.map((item) => item.trim()) .map((item) => item.trim())
@ -2106,8 +2107,13 @@ function buildOpenContractRiskAggregate(rows) {
}); });
} }
function composeFactualReply(intent, rows, options = {}) { function composeFactualReply(intent, rows, options = {}) {
const applyNumericEmphasis = (line) => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line); return (0, replyPackaging_1.finalizeComposeReplyResult)(composeFactualReplyBody(intent, rows, options), {
const joinLines = (lines) => lines.map(applyNumericEmphasis).join("\n"); emphasizeNumbers: options.emphasizeNumbers,
emphasizeNumericTokens
});
}
function composeFactualReplyBody(intent, rows, options = {}) {
const joinLines = replyPackaging_1.joinComposeReplyLines;
if (intent === "document_type_and_account_section_profile") { if (intent === "document_type_and_account_section_profile") {
const rowsByMarker = new Map(); const rowsByMarker = new Map();
for (const row of rows) { for (const row of rows) {
@ -4370,8 +4376,5 @@ function composeFactualReply(intent, rows, options = {}) {
}; };
} }
function inferReplyType(responseType) { function inferReplyType(responseType) {
if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { return (0, replyPackaging_1.inferAddressReplyType)(responseType);
return "factual";
}
return "partial_coverage";
} }

View File

@ -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";
}

View File

@ -5,6 +5,16 @@ import type {
AddressResultMode AddressResultMode
} from "../../types/addressQuery"; } from "../../types/addressQuery";
import { normalizeOrganizationScopeValue } from "../assistantOrganizationMatcher"; 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 { export interface ComposeStageRow {
period: string | null; period: string | null;
@ -39,26 +49,7 @@ export interface VatDirectSourceProbeSummary {
errors: string[]; errors: string[];
} }
interface ComposeFactualReplyOptions { type ComposeFactualReplyOptions = ComposeFactualReplyOptionsBase<VatDirectSourceProbeSummary>;
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 PeriodProfileFocus = type PeriodProfileFocus =
| "full_profile" | "full_profile"
@ -2725,9 +2716,19 @@ export function composeFactualReply(
intent: AddressIntent, intent: AddressIntent,
rows: ComposeStageRow[], rows: ComposeStageRow[],
options: ComposeFactualReplyOptions = {} options: ComposeFactualReplyOptions = {}
): { responseType: AddressResponseType; text: string; semantics?: ComposeReplySemantics } { ) {
const applyNumericEmphasis = (line: string): string => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line); return finalizeComposeReplyResult(composeFactualReplyBody(intent, rows, options), {
const joinLines = (lines: string[]): string => lines.map(applyNumericEmphasis).join("\n"); emphasizeNumbers: options.emphasizeNumbers,
emphasizeNumericTokens
});
}
function composeFactualReplyBody(
intent: AddressIntent,
rows: ComposeStageRow[],
options: ComposeFactualReplyOptions = {}
): ComposeReplyResult {
const joinLines = joinComposeReplyLines;
if (intent === "document_type_and_account_section_profile") { if (intent === "document_type_and_account_section_profile") {
const rowsByMarker = new Map<string, ComposeStageRow[]>(); const rowsByMarker = new Map<string, ComposeStageRow[]>();
@ -5496,8 +5497,5 @@ export function composeFactualReply(
} }
export function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" { export function inferReplyType(responseType: AddressResponseType): "factual" | "partial_coverage" {
if (responseType === "FACTUAL_LIST" || responseType === "FACTUAL_SUMMARY") { return inferAddressReplyType(responseType);
return "factual";
}
return "partial_coverage";
} }

View File

@ -0,0 +1,61 @@
import type { AddressEvidenceStrength, AddressResponseType, AddressResultMode } from "../../types/addressQuery";
export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
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";
}

View File

@ -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");
});
});