Этап 4 / Волна 11: объектный трейс по бизнес-якорям, доменное заземление, cleanup утечки дебага
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
import type { RouteHintSummary } from "../types/normalizer";
|
||||
import type { AnswerStructureV11, EvidenceConfidence, EvidenceItem, EvidenceLimitationReasonCode } from "../types/stage1Contracts";
|
||||
import type { ProblemUnit, ProblemUnitSummary, ProblemUnitType } from "../types/stage2ProblemUnits";
|
||||
import type { QuestionTypeClass } from "./questionTypeResolver";
|
||||
import type { CompanyAnchorSet } from "./companyAnchorResolver";
|
||||
|
||||
type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1" | "stage3_lifecycle_aware_v1";
|
||||
|
||||
@@ -20,6 +22,8 @@ interface ComposeAnswerInput {
|
||||
coverageReport: RequirementCoverageReport;
|
||||
groundingCheck: AnswerGroundingCheck;
|
||||
focusDomainHint?: string | null;
|
||||
questionTypeHint?: QuestionTypeClass | null;
|
||||
companyAnchors?: CompanyAnchorSet | null;
|
||||
enableAnswerPolicyV11?: boolean;
|
||||
enableProblemCentricAnswerV1?: boolean;
|
||||
enableLifecycleAnswerV1?: boolean;
|
||||
@@ -47,6 +51,123 @@ function uniqueStrings(values: string[], limit = 6): string[] {
|
||||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
||||
}
|
||||
|
||||
interface CompanyAnchorUsage {
|
||||
present: string[];
|
||||
used: string[];
|
||||
unused: string[];
|
||||
}
|
||||
|
||||
interface AnswerRenderContext {
|
||||
questionType: QuestionTypeClass;
|
||||
focusDomain: P0NarrativeDomain;
|
||||
anchors: CompanyAnchorUsage;
|
||||
}
|
||||
|
||||
function withUniquePush(target: string[], value: string): void {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (!target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAnchorForMatch(value: string): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}.:/-]+/gu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function collectCompanyAnchorTokens(anchors: CompanyAnchorSet | null | undefined): string[] {
|
||||
if (!anchors) {
|
||||
return [];
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
for (const item of anchors.contract_numbers ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.document_numbers ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.dates ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.amounts ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.accounts ?? []) withUniquePush(tokens, `\u0441\u0447\u0435\u0442 ${item}`);
|
||||
for (const item of anchors.accounts ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.periods ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.document_types ?? []) withUniquePush(tokens, item);
|
||||
for (const item of anchors.all ?? []) withUniquePush(tokens, item);
|
||||
return uniqueStrings(tokens, 48);
|
||||
}
|
||||
|
||||
function collectRetrievalCorpus(results: UnifiedRetrievalResult[]): string {
|
||||
const chunks: string[] = [];
|
||||
for (const result of results) {
|
||||
chunks.push(JSON.stringify(result.summary ?? {}));
|
||||
for (const item of result.items.slice(0, 10)) {
|
||||
chunks.push(JSON.stringify(item));
|
||||
}
|
||||
for (const evidence of result.evidence.slice(0, 16)) {
|
||||
chunks.push(JSON.stringify(evidence));
|
||||
}
|
||||
chunks.push(...result.why_included.slice(0, 16));
|
||||
chunks.push(...result.selection_reason.slice(0, 16));
|
||||
chunks.push(...result.business_interpretation.slice(0, 16));
|
||||
}
|
||||
return chunks.join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function isAnchorMatchedInCorpus(anchor: string, corpus: string): boolean {
|
||||
const normalized = normalizeAnchorForMatch(anchor);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized.length < 3) {
|
||||
return false;
|
||||
}
|
||||
if (corpus.includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
const withoutPrefix = normalized
|
||||
.replace(/^(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|document|account|period|doc_type)\s*[:№#]?\s*/iu, "")
|
||||
.trim();
|
||||
if (withoutPrefix.length >= 3 && corpus.includes(withoutPrefix)) {
|
||||
return true;
|
||||
}
|
||||
if (/^\d+(?:[.,]\d{2})?$/.test(withoutPrefix)) {
|
||||
const normalizedAmount = withoutPrefix.replace(",", ".");
|
||||
return corpus.includes(withoutPrefix) || corpus.includes(normalizedAmount);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function evaluateCompanyAnchorUsage(
|
||||
anchors: CompanyAnchorSet | null | undefined,
|
||||
retrievalResults: UnifiedRetrievalResult[]
|
||||
): CompanyAnchorUsage {
|
||||
const present = collectCompanyAnchorTokens(anchors);
|
||||
if (present.length === 0) {
|
||||
return {
|
||||
present: [],
|
||||
used: [],
|
||||
unused: []
|
||||
};
|
||||
}
|
||||
const corpus = normalizeAnchorForMatch(collectRetrievalCorpus(retrievalResults));
|
||||
const used: string[] = [];
|
||||
const unused: string[] = [];
|
||||
for (const anchor of present) {
|
||||
if (isAnchorMatchedInCorpus(anchor, corpus)) {
|
||||
withUniquePush(used, anchor);
|
||||
} else {
|
||||
withUniquePush(unused, anchor);
|
||||
}
|
||||
}
|
||||
return {
|
||||
present: uniqueStrings(present, 24),
|
||||
used: uniqueStrings(used, 12),
|
||||
unused: uniqueStrings(unused, 12)
|
||||
};
|
||||
}
|
||||
|
||||
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
||||
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
||||
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
||||
@@ -1129,6 +1250,12 @@ function isProblemUnitAlignedWithNarrativeDomain(unit: ProblemUnit, domain: P0Na
|
||||
}
|
||||
|
||||
if (domain === "vat_document_register_book") {
|
||||
const foreignVatDomain = ["period_close", "deferred_expense", "fixed_asset", "bank_settlement", "customer_settlement"].includes(
|
||||
String(unit.lifecycle_domain ?? "")
|
||||
);
|
||||
if (foreignVatDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||
return false;
|
||||
}
|
||||
if (unit.lifecycle_domain === "vat_flow") {
|
||||
return true;
|
||||
}
|
||||
@@ -1139,6 +1266,12 @@ function isProblemUnitAlignedWithNarrativeDomain(unit: ProblemUnit, domain: P0Na
|
||||
}
|
||||
|
||||
if (domain === "month_close_costs_20_44") {
|
||||
const foreignMonthCloseDomain = ["vat_flow", "bank_settlement", "customer_settlement", "fixed_asset"].includes(
|
||||
String(unit.lifecycle_domain ?? "")
|
||||
);
|
||||
if (foreignMonthCloseDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
unit.lifecycle_domain === "period_close" ||
|
||||
unit.lifecycle_domain === "deferred_expense" ||
|
||||
@@ -1775,12 +1908,178 @@ function mapDefectTokenToNarrative(value: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const KNOWN_ACCOUNT_PREFIXES = new Set<string>([
|
||||
"01",
|
||||
"02",
|
||||
"07",
|
||||
"08",
|
||||
"10",
|
||||
"13",
|
||||
"19",
|
||||
"20",
|
||||
"21",
|
||||
"23",
|
||||
"25",
|
||||
"26",
|
||||
"28",
|
||||
"29",
|
||||
"41",
|
||||
"43",
|
||||
"44",
|
||||
"45",
|
||||
"50",
|
||||
"51",
|
||||
"52",
|
||||
"55",
|
||||
"57",
|
||||
"58",
|
||||
"60",
|
||||
"62",
|
||||
"66",
|
||||
"67",
|
||||
"68",
|
||||
"69",
|
||||
"70",
|
||||
"71",
|
||||
"73",
|
||||
"76",
|
||||
"90",
|
||||
"91",
|
||||
"94",
|
||||
"96",
|
||||
"97"
|
||||
]);
|
||||
|
||||
function collectDateLikeSpansForNarrative(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [
|
||||
/\b20\d{2}[./-](?:0[1-9]|1[0-2])(?:[./-](?:0[1-9]|[12]\d|3[01]))?\b/g,
|
||||
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
|
||||
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function collectAmountLikeSpansForNarrative(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const pattern = /\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function intersectsNarrativeSpan(
|
||||
start: number,
|
||||
end: number,
|
||||
spans: Array<{ start: number; end: number }>
|
||||
): boolean {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
|
||||
function hasAccountContextMarker(text: string, start: number, end: number): boolean {
|
||||
const left = text.slice(Math.max(0, start - 24), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 24));
|
||||
return /(?:счет|сч\.?|account|schet|по\s+60|по\s+62|по\s+19|по\s+68|по\s+20|по\s+25|по\s+26|по\s+44|расчет|ндс|закрыти|рбп|амортиз|settlement|vat|close)/iu.test(
|
||||
`${left} ${right}`
|
||||
);
|
||||
}
|
||||
|
||||
function toKnownAccountToken(value: string): string | null {
|
||||
const token = String(value ?? "").trim();
|
||||
const prefix = token.match(/^(\d{2})/)?.[1];
|
||||
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function extractAccountNumbers(values: string[]): string[] {
|
||||
const numbers = values.flatMap((value) => {
|
||||
const matches = String(value ?? "").match(/\b\d{2}(?:\.\d{1,2})?\b/g);
|
||||
return matches ?? [];
|
||||
});
|
||||
return uniqueStrings(numbers, 12);
|
||||
const tokens: string[] = [];
|
||||
for (const value of values) {
|
||||
const raw = String(value ?? "");
|
||||
const matches = raw.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||
for (const match of matches) {
|
||||
const account = toKnownAccountToken(match);
|
||||
if (account) {
|
||||
tokens.push(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueStrings(tokens, 16);
|
||||
}
|
||||
|
||||
function extractAccountNumbersFromNarrativeText(value: string): string[] {
|
||||
const text = String(value ?? "").toLowerCase();
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
const dateSpans = collectDateLikeSpansForNarrative(text);
|
||||
const amountSpans = collectAmountLikeSpansForNarrative(text);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans];
|
||||
|
||||
const contextualPattern =
|
||||
/(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*([0-9./,\sиand]{2,96})/giu;
|
||||
let contextualMatch: RegExpExecArray | null = null;
|
||||
while ((contextualMatch = contextualPattern.exec(text)) !== null) {
|
||||
const chunk = String(contextualMatch[1] ?? "");
|
||||
const chunkTokens = chunk.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||
for (const token of chunkTokens) {
|
||||
const account = toKnownAccountToken(token);
|
||||
if (account) {
|
||||
result.push(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accountPairPattern = /\b(\d{2}(?:\.\d{1,2})?)\s*\/\s*(\d{2}(?:\.\d{1,2})?)\b/g;
|
||||
let pairMatch: RegExpExecArray | null = null;
|
||||
while ((pairMatch = accountPairPattern.exec(text)) !== null) {
|
||||
const left = toKnownAccountToken(String(pairMatch[1] ?? ""));
|
||||
const right = toKnownAccountToken(String(pairMatch[2] ?? ""));
|
||||
if (left) {
|
||||
result.push(left);
|
||||
}
|
||||
if (right) {
|
||||
result.push(right);
|
||||
}
|
||||
}
|
||||
|
||||
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
||||
let explicitMatch: RegExpExecArray | null = null;
|
||||
while ((explicitMatch = explicitPattern.exec(text)) !== null) {
|
||||
const token = String(explicitMatch[0] ?? "");
|
||||
const account = toKnownAccountToken(token);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const start = explicitMatch.index;
|
||||
const end = start + token.length;
|
||||
if (intersectsNarrativeSpan(start, end, blockedSpans)) {
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountContextMarker(text, start, end)) {
|
||||
continue;
|
||||
}
|
||||
result.push(account);
|
||||
}
|
||||
|
||||
return uniqueStrings(result, 16);
|
||||
}
|
||||
|
||||
function inferP0NarrativeDomain(units: ProblemUnit[]): P0NarrativeDomain {
|
||||
@@ -1914,8 +2213,8 @@ function collectSemanticProfileScopes(results: UnifiedRetrievalResult[]): { acco
|
||||
};
|
||||
}
|
||||
|
||||
interface SettlementEvidenceGrounding {
|
||||
has_settlement_primary: boolean;
|
||||
interface P0DomainEvidenceGrounding {
|
||||
has_primary: boolean;
|
||||
has_foreign_primary: boolean;
|
||||
foreign_primary_domains: string[];
|
||||
blocked: boolean;
|
||||
@@ -1935,10 +2234,28 @@ function isSettlementDomainToken(value: string): boolean {
|
||||
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
|
||||
}
|
||||
|
||||
function isVatDomainToken(value: string): boolean {
|
||||
return /(?:vat_flow|vat|nds|taxes?|purchase_book|sales_book|invoice|book_entry|register)/i.test(String(value ?? ""));
|
||||
}
|
||||
|
||||
function isMonthCloseDomainToken(value: string): boolean {
|
||||
return /(?:period_close|month_close|close_operation|cost_close|cost_allocation|deferred_expense)/i.test(String(value ?? ""));
|
||||
}
|
||||
|
||||
function isForeignToSettlementDomainToken(value: string): boolean {
|
||||
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
|
||||
}
|
||||
|
||||
function isForeignToVatDomainToken(value: string): boolean {
|
||||
return /(?:bank_settlement|customer_settlement|settlements?|period_close|deferred_expense|fixed_asset|fixed_assets|month_close)/i.test(
|
||||
String(value ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function isForeignToMonthCloseDomainToken(value: string): boolean {
|
||||
return /(?:bank_settlement|customer_settlement|settlements?|vat_flow|vat|fixed_asset|fixed_assets)/i.test(String(value ?? ""));
|
||||
}
|
||||
|
||||
function collectResultAccounts(result: UnifiedRetrievalResult): string[] {
|
||||
const accounts: string[] = [];
|
||||
const semanticProfile = summaryValue(result, "semantic_profile");
|
||||
@@ -1985,46 +2302,111 @@ function isSubstantiveResult(result: UnifiedRetrievalResult): boolean {
|
||||
return result.items.length > 0 || result.evidence.length > 0;
|
||||
}
|
||||
|
||||
function evaluateSettlementEvidenceGrounding(results: UnifiedRetrievalResult[]): SettlementEvidenceGrounding {
|
||||
const substantive = results.filter((item) => isSubstantiveResult(item));
|
||||
if (substantive.length === 0) {
|
||||
function evaluateP0DomainEvidenceGrounding(
|
||||
results: UnifiedRetrievalResult[],
|
||||
focusDomain: P0NarrativeDomain
|
||||
): P0DomainEvidenceGrounding {
|
||||
if (!focusDomain) {
|
||||
return {
|
||||
has_settlement_primary: false,
|
||||
has_primary: false,
|
||||
has_foreign_primary: false,
|
||||
foreign_primary_domains: [],
|
||||
blocked: false
|
||||
};
|
||||
}
|
||||
|
||||
const classify = (result: UnifiedRetrievalResult): { settlement: boolean; foreignDomains: string[] } => {
|
||||
const substantive = results.filter((item) => isSubstantiveResult(item));
|
||||
if (substantive.length === 0) {
|
||||
return {
|
||||
has_primary: false,
|
||||
has_foreign_primary: false,
|
||||
foreign_primary_domains: [],
|
||||
blocked: false
|
||||
};
|
||||
}
|
||||
|
||||
const classify = (result: UnifiedRetrievalResult): { inDomain: boolean; foreignDomains: string[] } => {
|
||||
const accounts = collectResultAccounts(result);
|
||||
const domains = collectResultDomains(result);
|
||||
const relations = collectResultRelations(result);
|
||||
const settlement =
|
||||
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
||||
domains.some((item) => isSettlementDomainToken(item)) ||
|
||||
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents/.test(item));
|
||||
const foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
||||
let inDomain = false;
|
||||
let foreignDomains: string[] = [];
|
||||
|
||||
if (focusDomain === "settlements_60_62") {
|
||||
inDomain =
|
||||
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
||||
domains.some((item) => isSettlementDomainToken(item)) ||
|
||||
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents|linked_to_settlement|settlement_closed/.test(item));
|
||||
foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
||||
} else if (focusDomain === "vat_document_register_book") {
|
||||
inDomain =
|
||||
accounts.some((item) => isVatAccountToken(item)) ||
|
||||
domains.some((item) => isVatDomainToken(item)) ||
|
||||
relations.some((item) =>
|
||||
/invoice_to_vat|source_doc_present|invoice_linked|book_entry_generated|deduction_posted|register_to_book|vat_/i.test(item)
|
||||
);
|
||||
foreignDomains = domains.filter((item) => isForeignToVatDomainToken(item));
|
||||
} else if (focusDomain === "month_close_costs_20_44") {
|
||||
inDomain =
|
||||
accounts.some((item) => isCloseCostsAccountToken(item)) ||
|
||||
domains.some((item) => isMonthCloseDomainToken(item)) ||
|
||||
relations.some((item) =>
|
||||
/costs_accumulated|allocation_rules_resolved|close_operation_runs|residuals_zero|close_operation|period_close|allocation|writeoff/i.test(
|
||||
item
|
||||
)
|
||||
);
|
||||
foreignDomains = domains.filter((item) => isForeignToMonthCloseDomainToken(item));
|
||||
}
|
||||
|
||||
return {
|
||||
settlement,
|
||||
inDomain,
|
||||
foreignDomains: uniqueStrings(foreignDomains, 8)
|
||||
};
|
||||
};
|
||||
|
||||
const top = substantive[0];
|
||||
const topClass = classify(top);
|
||||
const hasAnySettlement = substantive.some((item) => classify(item).settlement);
|
||||
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.settlement;
|
||||
const blocked = hasForeignPrimary && !hasAnySettlement && !hasControlledCrossDomainHandoffInResult(top);
|
||||
const hasAnyPrimary = substantive.some((item) => classify(item).inDomain);
|
||||
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.inDomain;
|
||||
const blocked = hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top);
|
||||
|
||||
return {
|
||||
has_settlement_primary: hasAnySettlement,
|
||||
has_primary: hasAnyPrimary,
|
||||
has_foreign_primary: hasForeignPrimary,
|
||||
foreign_primary_domains: topClass.foreignDomains,
|
||||
blocked
|
||||
};
|
||||
}
|
||||
|
||||
function hasStrongNarrativeDomainSignalInText(userMessage: string, domain: P0NarrativeDomain): boolean {
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
const text = String(userMessage ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||
if (domain === "settlements_60_62") {
|
||||
return (
|
||||
accountTokens.some((item) => isSettlementAccountToken(item)) ||
|
||||
/(60\.0[12]|62\.0[12]|долг|аванс|зач[её]т|взаимозач|расч[её]т)/i.test(text)
|
||||
);
|
||||
}
|
||||
if (domain === "vat_document_register_book") {
|
||||
return (
|
||||
accountTokens.some((item) => isVatAccountToken(item)) ||
|
||||
/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text)
|
||||
);
|
||||
}
|
||||
if (domain === "month_close_costs_20_44") {
|
||||
return (
|
||||
accountTokens.some((item) => isCloseCostsAccountToken(item)) ||
|
||||
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
|
||||
text
|
||||
)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function inferP0FocusNarrativeDomain(
|
||||
userMessage: string,
|
||||
results: UnifiedRetrievalResult[],
|
||||
@@ -2032,20 +2414,30 @@ function inferP0FocusNarrativeDomain(
|
||||
focusDomainHint?: string | null
|
||||
): P0NarrativeDomain {
|
||||
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
|
||||
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
||||
const strongFromMessage = Boolean(fromMessage && hasStrongNarrativeDomainSignalInText(userMessage, fromMessage));
|
||||
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
||||
if (fromHint && fromMessage && fromHint !== fromMessage) {
|
||||
return strongFromMessage ? fromMessage : fromHint;
|
||||
}
|
||||
if (fromHint) {
|
||||
return fromHint;
|
||||
}
|
||||
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
||||
if (fromDomainGuard && fromMessage && fromDomainGuard !== fromMessage) {
|
||||
return strongFromMessage ? fromMessage : fromDomainGuard;
|
||||
}
|
||||
if (fromDomainGuard) {
|
||||
return fromDomainGuard;
|
||||
}
|
||||
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
||||
if (strongFromMessage) {
|
||||
return fromMessage;
|
||||
}
|
||||
if (fromMessage) {
|
||||
return fromMessage;
|
||||
}
|
||||
|
||||
const semanticScopes = collectSemanticProfileScopes(results);
|
||||
const messageAccounts = extractAccountNumbers([userMessage]);
|
||||
const messageAccounts = extractAccountNumbersFromNarrativeText(userMessage);
|
||||
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some(
|
||||
(item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item)
|
||||
);
|
||||
@@ -2224,14 +2616,22 @@ function buildDirectAnswer(input: {
|
||||
mode: PolicyMode;
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
policySignals: PolicySignals;
|
||||
focusDomain: P0NarrativeDomain;
|
||||
}): string {
|
||||
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
|
||||
const domainAnchor = domainNarrativeAnchor(input.focusDomain);
|
||||
const topFactDomain = topFact ? inferNarrativeDomainFromText(topFact) : null;
|
||||
const topFactAligned = Boolean(topFact) && (!input.focusDomain || topFactDomain === input.focusDomain);
|
||||
const preferredFact = topFactAligned ? topFact : null;
|
||||
if (input.mode === "focused_grounded") {
|
||||
return topFact ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
||||
return preferredFact ?? domainAnchor ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
if (topFact) {
|
||||
return `${topFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||
if (preferredFact) {
|
||||
return `${preferredFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||
}
|
||||
if (domainAnchor) {
|
||||
return `${domainAnchor.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||
}
|
||||
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
|
||||
}
|
||||
@@ -2338,11 +2738,23 @@ function buildProblemCentricAnswerStructure(input: {
|
||||
6
|
||||
);
|
||||
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
|
||||
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, input.evidenceItems);
|
||||
const hasCriticalEvidenceLimitation =
|
||||
input.limitationReasonCodes.includes("weak_source_mapping") ||
|
||||
input.limitationReasonCodes.includes("insufficient_detail");
|
||||
const confidenceLimited =
|
||||
input.mode !== "focused_grounded" ||
|
||||
weakUnits ||
|
||||
input.domainLockMiss ||
|
||||
input.limitationReasonCodes.includes("missing_mechanism") ||
|
||||
input.limitationReasonCodes.includes("heuristic_inference") ||
|
||||
hasCriticalEvidenceLimitation ||
|
||||
aggregateEvidenceConfidence === "low";
|
||||
|
||||
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
||||
unitMechanismNotes.length === 0
|
||||
? "unresolved"
|
||||
: weakUnits || input.limitationReasonCodes.includes("missing_mechanism")
|
||||
: confidenceLimited
|
||||
? "limited"
|
||||
: "grounded";
|
||||
|
||||
@@ -2453,21 +2865,50 @@ function limitationReasonToUserText(code: EvidenceLimitationReasonCode): string
|
||||
|
||||
function inferNarrativeDomainFromText(value: string): P0NarrativeDomain {
|
||||
const text = String(value ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountNumbers([text]);
|
||||
const hasSettlementLexicalSignal = /(оплат|долг|аванс|взаимозач|зачет|зачёт|поставщ|покупат|не\s+сход)/i.test(text);
|
||||
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||
|
||||
if (accountTokens.some((token) => isSettlementAccountToken(token)) || hasSettlementLexicalSignal) {
|
||||
return "settlements_60_62";
|
||||
let settlementScore = 0;
|
||||
let vatScore = 0;
|
||||
let monthCloseScore = 0;
|
||||
|
||||
if (accountTokens.some((token) => isSettlementAccountToken(token))) {
|
||||
settlementScore += 3;
|
||||
}
|
||||
if (accountTokens.some((token) => isVatAccountToken(token)) || /(ндс|счет[-\s]?фактур|регистр|книг)/i.test(text)) {
|
||||
return "vat_document_register_book";
|
||||
if (accountTokens.some((token) => isVatAccountToken(token))) {
|
||||
vatScore += 3;
|
||||
}
|
||||
if (accountTokens.some((token) => isCloseCostsAccountToken(token))) {
|
||||
monthCloseScore += 3;
|
||||
}
|
||||
|
||||
if (/(долг|аванс|взаимозач|зачет|зачёт|62\.01|62\.02|60\.01|60\.02|не\s+сход)/i.test(text)) {
|
||||
settlementScore += 2;
|
||||
}
|
||||
if (/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text)) {
|
||||
vatScore += 3;
|
||||
}
|
||||
if (
|
||||
accountTokens.some((token) => isCloseCostsAccountToken(token)) ||
|
||||
/(закрыти[ея]\s+месяц|затрат|распределени|списан)/i.test(text)
|
||||
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
|
||||
text
|
||||
)
|
||||
) {
|
||||
monthCloseScore += 3;
|
||||
}
|
||||
|
||||
const maxScore = Math.max(settlementScore, vatScore, monthCloseScore);
|
||||
if (maxScore <= 0) {
|
||||
return null;
|
||||
}
|
||||
// Tie-break prioritizes explicit VAT and month-close lexical markers over broad settlement wording.
|
||||
if (vatScore === maxScore) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (monthCloseScore === maxScore) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (settlementScore === maxScore) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2578,6 +3019,11 @@ function buildEvidenceSectionLines(structure: AnswerStructureV11): string[] {
|
||||
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
|
||||
? structure.evidence_block.claim_evidence_links.length
|
||||
: 0;
|
||||
const reliabilityLimited =
|
||||
structure.mechanism_block.status !== "grounded" ||
|
||||
structure.uncertainty_block.limitations.length > 0 ||
|
||||
structure.uncertainty_block.open_uncertainties.length > 0 ||
|
||||
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
|
||||
const lines: string[] = [];
|
||||
const coverageSplitLines = buildCoverageSplitLines(structure);
|
||||
|
||||
@@ -2593,7 +3039,7 @@ function buildEvidenceSectionLines(structure: AnswerStructureV11): string[] {
|
||||
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
|
||||
lines.push("Опора частичная: часть требований покрыта не полностью.");
|
||||
} else if (evidenceCount > 0) {
|
||||
lines.push("Опора достаточна для первичного вывода.");
|
||||
lines.push(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
@@ -2678,6 +3124,8 @@ function humanizeLimitationToken(value: string): string | null {
|
||||
if (normalized === "missing_anchor:account") return "Счет или группа счетов не указаны.";
|
||||
if (normalized === "missing_anchor:document_or_object") return "Не указан документ или объект для трассировки.";
|
||||
if (normalized === "missing_anchor:counterparty") return "Не указан контрагент или договор.";
|
||||
if (normalized === "primary_domain_evidence_not_confirmed")
|
||||
return "Целевой механизм активного домена подтвержден частично; вывод ограничен.";
|
||||
if (normalized === "settlement_primary_evidence_not_confirmed")
|
||||
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
|
||||
if (normalized.includes("snapshot")) return "Вывод сделан по snapshot и может не включать часть цепочки.";
|
||||
@@ -2733,22 +3181,188 @@ function buildLimitationsSectionLines(structure: AnswerStructureV11): string[] {
|
||||
return ["Существенных ограничений в текущем срезе не выявлено."];
|
||||
}
|
||||
|
||||
function renderPolicyReply(structure: AnswerStructureV11): string {
|
||||
function domainNameForQuestionType(domain: P0NarrativeDomain): string {
|
||||
if (domain === "settlements_60_62") return "\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0443\u0440\u0430";
|
||||
if (domain === "vat_document_register_book") return "\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u041d\u0414\u0421";
|
||||
if (domain === "month_close_costs_20_44")
|
||||
return "\u043a\u043e\u043d\u0442\u0443\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043c\u0435\u0441\u044f\u0446\u0430";
|
||||
return "\u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0443\u0447\u0430\u0441\u0442\u043a\u0430";
|
||||
}
|
||||
|
||||
function buildQuestionTypeShortLine(context: AnswerRenderContext): string | null {
|
||||
const domainName = domainNameForQuestionType(context.focusDomain);
|
||||
if (context.questionType === "where_break_is") {
|
||||
return `\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0440\u044b\u0432 \u0432\u043d\u0443\u0442\u0440\u0438 ${domainName}.`;
|
||||
}
|
||||
if (context.questionType === "prove_or_guess") {
|
||||
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0432\u0435\u0441\u0442\u0438 \u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0443.";
|
||||
}
|
||||
if (context.questionType === "what_is_it_grounded_on") {
|
||||
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c.";
|
||||
}
|
||||
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438.";
|
||||
}
|
||||
if (context.questionType === "what_to_check_first") {
|
||||
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0434\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildQuestionTypeBrokenLine(context: AnswerRenderContext): string | null {
|
||||
if (context.questionType !== "where_break_is") {
|
||||
return null;
|
||||
}
|
||||
if (context.focusDomain === "settlements_60_62") {
|
||||
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u043e\u043f\u043b\u0430\u0442\u044b \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0443 \u0440\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0443 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f.";
|
||||
}
|
||||
if (context.focusDomain === "vat_document_register_book") {
|
||||
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u0441\u0432\u044f\u0437\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430, \u0441\u0447\u0435\u0442\u0430-\u0444\u0430\u043a\u0442\u0443\u0440\u044b \u0438 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043d\u0438\u0433\u0438.";
|
||||
}
|
||||
if (context.focusDomain === "month_close_costs_20_44") {
|
||||
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043e\u0442 \u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442 \u043a \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044e/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e.";
|
||||
}
|
||||
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e; \u043d\u0443\u0436\u043d\u0430 \u0442\u043e\u0447\u0435\u0447\u043d\u0430\u044f \u0441\u0432\u0435\u0440\u043a\u0430.";
|
||||
}
|
||||
|
||||
function buildQuestionTypeWhyLine(context: AnswerRenderContext): string | null {
|
||||
if (context.questionType === "prove_or_guess") {
|
||||
return "\u0417\u0434\u0435\u0441\u044c \u0447\u0435\u0441\u0442\u043d\u043e \u0440\u0430\u0437\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0447\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u043e\u0439.";
|
||||
}
|
||||
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||
return "\u0426\u0435\u043f\u043e\u0447\u043a\u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u043e\u043f\u043e\u0440\u0435.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildQuestionTypeEvidenceLine(context: AnswerRenderContext): string | null {
|
||||
if (context.questionType === "what_is_it_grounded_on") {
|
||||
return "\u0412 \u044d\u0442\u043e\u043c \u043e\u0442\u0432\u0435\u0442\u0435 \u0432 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430.";
|
||||
}
|
||||
if (context.questionType === "prove_or_guess") {
|
||||
return "\u0421\u0438\u043b\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043e\u0446\u0435\u043d\u0435\u043d\u0430 \u043f\u043e \u043f\u0440\u044f\u043c\u043e\u0439 \u043e\u043f\u043e\u0440\u0435, \u0430 \u043d\u0435 \u043f\u043e \u0434\u043e\u0433\u0430\u0434\u043a\u0430\u043c.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatAnchorList(anchors: string[], prefix: string): string | null {
|
||||
if (anchors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return `${prefix}: ${anchors.join(", ")}.`;
|
||||
}
|
||||
|
||||
function buildQuestionTypeCheckLine(context: AnswerRenderContext): string | null {
|
||||
if (context.questionType === "what_to_check_first") {
|
||||
return "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u043f\u0443\u043d\u043a\u0442\u0430 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043a\u043e\u043a\u0430.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildQuestionTypeLimitationLine(context: AnswerRenderContext): string | null {
|
||||
if (context.questionType === "prove_or_guess") {
|
||||
return "\u0414\u043b\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u00ab\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043b\u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0430\u00bb \u0432\u0441\u0435 \u043d\u0435\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0434\u0435\u043b\u0435\u043d\u044b \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f.";
|
||||
}
|
||||
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||
return "\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u00abcomplete/incomplete\u00bb \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043f\u043e\u043b\u043d\u043e\u0442\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0441\u0440\u0435\u0437\u0435.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyQuestionTypeAndAnchorPolicy(input: {
|
||||
shortLine: string;
|
||||
brokenLines: string[];
|
||||
whyLines: string[];
|
||||
evidenceLines: string[];
|
||||
checkLines: string[];
|
||||
limitationLines: string[];
|
||||
context: AnswerRenderContext;
|
||||
}): {
|
||||
shortLine: string;
|
||||
brokenLines: string[];
|
||||
whyLines: string[];
|
||||
evidenceLines: string[];
|
||||
checkLines: string[];
|
||||
limitationLines: string[];
|
||||
} {
|
||||
const nextShort = buildQuestionTypeShortLine(input.context) ?? input.shortLine;
|
||||
const nextBroken = dedupeNarrativeLines(
|
||||
[buildQuestionTypeBrokenLine(input.context), ...input.brokenLines].filter((item): item is string => Boolean(item)),
|
||||
4
|
||||
);
|
||||
const nextWhy = dedupeNarrativeLines(
|
||||
[buildQuestionTypeWhyLine(input.context), ...input.whyLines].filter((item): item is string => Boolean(item)),
|
||||
4
|
||||
);
|
||||
const anchorUsedLine = formatAnchorList(
|
||||
input.context.anchors.used,
|
||||
"\u0412 \u043e\u043f\u043e\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u044f\u043a\u043e\u0440\u044f \u0432\u043e\u043f\u0440\u043e\u0441\u0430"
|
||||
);
|
||||
const anchorUnusedLine = formatAnchorList(
|
||||
input.context.anchors.unused,
|
||||
"\u042f\u043a\u043e\u0440\u044f \u0438\u0437 \u0432\u043e\u043f\u0440\u043e\u0441\u0430 \u0431\u0435\u0437 \u043f\u0440\u044f\u043c\u043e\u0433\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f"
|
||||
);
|
||||
const nextEvidence = dedupeNarrativeLines(
|
||||
[buildQuestionTypeEvidenceLine(input.context), ...input.evidenceLines, anchorUsedLine].filter(
|
||||
(item): item is string => Boolean(item)
|
||||
),
|
||||
7
|
||||
);
|
||||
const nextChecks = dedupeNarrativeLines(
|
||||
[buildQuestionTypeCheckLine(input.context), ...input.checkLines].filter((item): item is string => Boolean(item)),
|
||||
5
|
||||
);
|
||||
const nextLimitations = dedupeNarrativeLines(
|
||||
[buildQuestionTypeLimitationLine(input.context), anchorUnusedLine, ...input.limitationLines].filter(
|
||||
(item): item is string => Boolean(item)
|
||||
),
|
||||
6
|
||||
);
|
||||
|
||||
return {
|
||||
shortLine: ensureSentence(nextShort),
|
||||
brokenLines: nextBroken,
|
||||
whyLines: nextWhy,
|
||||
evidenceLines: nextEvidence,
|
||||
checkLines: nextChecks,
|
||||
limitationLines: nextLimitations
|
||||
};
|
||||
}
|
||||
|
||||
function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRenderContext): string {
|
||||
const shortLine = ensureSentence(buildShortSectionLine(structure));
|
||||
const brokenLines = buildBrokenSectionLines(structure);
|
||||
const whyLines = buildWhySectionLines(structure);
|
||||
const evidenceLines = buildEvidenceSectionLines(structure);
|
||||
const checkLines = buildChecksSectionLines(structure);
|
||||
const limitationLines = buildLimitationsSectionLines(structure);
|
||||
const enriched = context
|
||||
? applyQuestionTypeAndAnchorPolicy({
|
||||
shortLine,
|
||||
brokenLines,
|
||||
whyLines,
|
||||
evidenceLines,
|
||||
checkLines,
|
||||
limitationLines,
|
||||
context
|
||||
})
|
||||
: {
|
||||
shortLine,
|
||||
brokenLines,
|
||||
whyLines,
|
||||
evidenceLines,
|
||||
checkLines,
|
||||
limitationLines
|
||||
};
|
||||
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
`Коротко: ${shortLine}`,
|
||||
`Что сломано:\n${formatList(brokenLines)}`,
|
||||
`Почему это похоже на проблему:\n${formatList(whyLines)}`,
|
||||
`На чем это основано:\n${formatList(evidenceLines)}`,
|
||||
`Что проверить первым:\n${formatList(checkLines)}`,
|
||||
`Ограничения:\n${formatList(limitationLines)}`
|
||||
`Коротко: ${enriched.shortLine}`,
|
||||
`Что сломано:\n${formatList(enriched.brokenLines)}`,
|
||||
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`,
|
||||
`На чем это основано:\n${formatList(enriched.evidenceLines)}`,
|
||||
`Что проверить первым:\n${formatList(enriched.checkLines)}`,
|
||||
`Ограничения:\n${formatList(enriched.limitationLines)}`
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
@@ -2757,6 +3371,8 @@ function renderPolicyReply(structure: AnswerStructureV11): string {
|
||||
|
||||
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||
const questionType: QuestionTypeClass = input.questionTypeHint ?? "unknown";
|
||||
const anchorUsage = evaluateCompanyAnchorUsage(input.companyAnchors, input.retrievalResults);
|
||||
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
|
||||
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
||||
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
||||
@@ -2786,15 +3402,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
problemHeavyUnits,
|
||||
input.focusDomainHint
|
||||
);
|
||||
const settlementGrounding = focusNarrativeDomain === "settlements_60_62"
|
||||
? evaluateSettlementEvidenceGrounding(input.retrievalResults)
|
||||
: {
|
||||
has_settlement_primary: false,
|
||||
has_foreign_primary: false,
|
||||
foreign_primary_domains: [],
|
||||
blocked: false
|
||||
};
|
||||
const settlementGroundingBlocked = focusNarrativeDomain === "settlements_60_62" && settlementGrounding.blocked;
|
||||
const focusDomainGrounding = evaluateP0DomainEvidenceGrounding(input.retrievalResults, focusNarrativeDomain);
|
||||
const focusDomainGroundingBlocked = Boolean(focusNarrativeDomain && focusDomainGrounding.blocked);
|
||||
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
|
||||
const domainAlignedProblemUnits =
|
||||
focusNarrativeDomain === null
|
||||
@@ -2805,7 +3414,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
rankedProblemUnits.length > 0 &&
|
||||
domainAlignedProblemUnits.length === 0
|
||||
);
|
||||
const domainLockMiss = domainLockMissBase || settlementGroundingBlocked;
|
||||
const domainLockMiss = domainLockMissBase || focusDomainGroundingBlocked;
|
||||
const selectedProblemUnits = (
|
||||
focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits
|
||||
).slice(0, 4);
|
||||
@@ -2853,7 +3462,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
policySignals
|
||||
});
|
||||
const guardedDecision: PolicyDecision =
|
||||
settlementGroundingBlocked &&
|
||||
focusDomainGroundingBlocked &&
|
||||
decision.mode !== "out_of_scope" &&
|
||||
decision.mode !== "route_mismatch" &&
|
||||
decision.mode !== "backend_error"
|
||||
@@ -2870,7 +3479,9 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
policySignals.minimum_evidence_failed ||
|
||||
limitationReasonCodes.includes("missing_mechanism") ||
|
||||
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||
limitationReasonCodes.includes("insufficient_detail") ||
|
||||
aggregateEvidenceConfidence === "low" ||
|
||||
domainLockMiss ||
|
||||
lowConfidenceConcentration;
|
||||
const hardBlockedMode =
|
||||
guardedDecision.mode === "out_of_scope" ||
|
||||
@@ -2907,7 +3518,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
|
||||
|
||||
return {
|
||||
assistant_reply: renderPolicyReply(problemCentricStructure),
|
||||
assistant_reply: renderPolicyReply(problemCentricStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage
|
||||
}),
|
||||
fallback_type: guardedDecision.fallback_type,
|
||||
reply_type: guardedDecision.reply_type,
|
||||
answer_structure_v11: problemCentricStructure,
|
||||
@@ -2937,11 +3552,12 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
|
||||
...extractLimitations(input.retrievalResults),
|
||||
...input.groundingCheck.reasons,
|
||||
...(settlementGroundingBlocked
|
||||
...(focusDomainGroundingBlocked
|
||||
? ["Целевой механизм активного домена подтвержден частично; часть первичной опоры пришла из смежного контура."]
|
||||
: []),
|
||||
...(anchorUsage.unused.length > 0
|
||||
? [
|
||||
`Primary settlement evidence is not confirmed; foreign domains dominate: ${
|
||||
settlementGrounding.foreign_primary_domains.join(", ") || "unknown"
|
||||
}.`
|
||||
`Часть якорей запроса пока не подтверждена в опоре: ${anchorUsage.unused.slice(0, 5).join(", ")}.`
|
||||
]
|
||||
: []),
|
||||
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
|
||||
@@ -2958,15 +3574,24 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
|
||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
|
||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
|
||||
...(settlementGroundingBlocked ? ["settlement_primary_evidence_not_confirmed"] : [])
|
||||
...(focusDomainGroundingBlocked ? ["primary_domain_evidence_not_confirmed"] : [])
|
||||
],
|
||||
8
|
||||
);
|
||||
|
||||
const confidenceLimited =
|
||||
guardedDecision.mode !== "focused_grounded" ||
|
||||
limitationReasonCodes.includes("missing_mechanism") ||
|
||||
limitationReasonCodes.includes("heuristic_inference") ||
|
||||
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||
limitationReasonCodes.includes("insufficient_detail") ||
|
||||
aggregateEvidenceConfidence === "low" ||
|
||||
focusDomainGroundingBlocked;
|
||||
|
||||
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
||||
mechanismNotes.length === 0
|
||||
? "unresolved"
|
||||
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
||||
: confidenceLimited
|
||||
? "limited"
|
||||
: "grounded";
|
||||
|
||||
@@ -2976,7 +3601,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
direct_answer: buildDirectAnswer({
|
||||
mode: guardedDecision.mode,
|
||||
retrievalResults: input.retrievalResults,
|
||||
policySignals
|
||||
policySignals,
|
||||
focusDomain: focusNarrativeDomain
|
||||
}),
|
||||
mechanism_block: {
|
||||
status: mechanismStatus,
|
||||
@@ -3011,7 +3637,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||
};
|
||||
|
||||
return {
|
||||
assistant_reply: renderPolicyReply(answerStructure),
|
||||
assistant_reply: renderPolicyReply(answerStructure, {
|
||||
questionType,
|
||||
focusDomain: focusNarrativeDomain,
|
||||
anchors: anchorUsage
|
||||
}),
|
||||
fallback_type: guardedDecision.fallback_type,
|
||||
reply_type: guardedDecision.reply_type,
|
||||
answer_structure_v11: answerStructure,
|
||||
|
||||
Reference in New Issue
Block a user