Этап 4 / Волна 11: объектный трейс по бизнес-якорям, доменное заземление, cleanup утечки дебага

This commit is contained in:
2026-03-28 13:12:09 +03:00
parent 553a5c407a
commit 8b84f5e989
34 changed files with 5517 additions and 173 deletions
@@ -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,