Этап 4 / Волна 16: смысловая изоляция РБП и ОС, фиксы лайв-ответов и добивка экспорта

This commit is contained in:
2026-03-28 21:11:11 +03:00
parent 6123fafd5b
commit 7eb1410501
35 changed files with 4120 additions and 155 deletions
@@ -24,6 +24,7 @@ interface ComposeAnswerInput {
focusDomainHint?: string | null;
questionTypeHint?: QuestionTypeClass | null;
companyAnchors?: CompanyAnchorSet | null;
normalizationPeriodExplicit?: boolean;
enableAnswerPolicyV11?: boolean;
enableProblemCentricAnswerV1?: boolean;
enableLifecycleAnswerV1?: boolean;
@@ -61,6 +62,7 @@ interface AnswerRenderContext {
questionType: QuestionTypeClass;
focusDomain: P0NarrativeDomain;
anchors: CompanyAnchorUsage;
userMessage?: string;
}
function withUniquePush(target: string[], value: string): void {
@@ -259,6 +261,10 @@ const HUMAN_SIGNAL_MAP: Record<string, string> = {
amount_independent_risk: "Проблема не выглядит случайной суммовой погрешностью.",
wrong_document_type: "Есть признак неверного типа закрывающего документа.",
fixed_asset_card_mismatch: "Есть несоответствие между карточкой ОС, документом движения и начислением.",
contradictory_asset_state: "Состояние объекта ОС выглядит противоречивым по текущей опоре.",
disposed: "Есть признак выбытия объекта ОС в цепочке состояния.",
invalid_document_or_posting_transition: "Переход состояния ОС не подтвержден документами и проводками.",
asset_card_to_depreciation: "Переход от карточки ОС к начислению амортизации подтвержден не полностью.",
supplier_tail_analysis: "Есть признаки незавершенного расчетного контура по поставщикам.",
cross_entity_breakage: "Есть разрыв между связанными объектами в одной цепочке.",
deferred_expense_to_writeoff: "Ожидаемая цепочка списания РБП выглядит незавершенной.",
@@ -674,8 +680,13 @@ function stripSyntheticPlaceholders(value: string): string {
}
function sanitizeUserFacingReply(value: string): string {
const withoutDebugBlocks = String(value ?? "")
const raw = String(value ?? "");
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b/i);
const preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
const withoutDebugBlocks = preCut
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/###\s*technical_breakdown_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.replace(/```json[\s\S]*?```/gi, "");
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
const cleanedLines = normalized
@@ -1384,7 +1395,7 @@ function buildProblemCentricActions(input: {
}
if (input.missingAnchors.period && input.mode !== "clarification_required") {
actions.push("Уточните период проверки (например, 2020-06), чтобы подтвердить незавершенное списание без лишнего шума.");
actions.push("Уточните период проверки (например, июль 2020), чтобы подтвердить незавершенное списание без лишнего шума.");
}
if (input.mode === "clarification_required") {
@@ -1423,7 +1434,7 @@ function buildProblemCentricClarifications(input: {
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
if (input.missingAnchors.period) {
questions.push("Уточните период (например, 2020-06), в котором нужно проверить проблемный кластер.");
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
@@ -1564,6 +1575,15 @@ function asRecordObject(value: unknown): Record<string, unknown> | null {
const EXPLICIT_PERIOD_ANCHOR_PATTERN =
/(?:\b20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?(?:[-./](?:0?[1-9]|[12]\d|3[01]))?\b|\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b|\b(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]|июл[ьяе]|август[ае]?|сентябр[ьяе]|октябр[ьяе]|ноябр[ьяе]|декабр[ьяе]|january|february|march|april|may|june|july|august|september|october|november|december)\b)/i;
function hasPeriodAnchorInCompanyAnchors(anchors: CompanyAnchorSet | null | undefined): boolean {
if (!anchors) {
return false;
}
const dates = Array.isArray(anchors.dates) ? anchors.dates : [];
const periods = Array.isArray(anchors.periods) ? anchors.periods : [];
return dates.some((item) => String(item ?? "").trim().length > 0) || periods.some((item) => String(item ?? "").trim().length > 0);
}
function hasPeriodAnchorInRetrieval(results: UnifiedRetrievalResult[]): boolean {
for (const result of results) {
const summary = asRecordObject(result.summary);
@@ -1606,9 +1626,20 @@ function hasAccountAnchorInRetrieval(results: UnifiedRetrievalResult[]): boolean
return false;
}
function detectMissingAnchors(userMessage: string, retrievalResults: UnifiedRetrievalResult[] = []): MissingAnchors {
function detectMissingAnchors(
userMessage: string,
retrievalResults: UnifiedRetrievalResult[] = [],
options?: {
normalizationPeriodExplicit?: boolean;
companyAnchors?: CompanyAnchorSet | null;
}
): MissingAnchors {
const lower = String(userMessage ?? "").toLowerCase();
const hasPeriod = EXPLICIT_PERIOD_ANCHOR_PATTERN.test(lower) || hasPeriodAnchorInRetrieval(retrievalResults);
const hasPeriod =
EXPLICIT_PERIOD_ANCHOR_PATTERN.test(lower) ||
hasPeriodAnchorInRetrieval(retrievalResults) ||
Boolean(options?.normalizationPeriodExplicit) ||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
const hasAccount =
/(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(
lower
@@ -1640,7 +1671,7 @@ function buildClarificationQuestions(input: {
}
if (input.missingAnchors.period) {
questions.push("Уточните период проверки (например, 2020-06).");
questions.push("Уточните период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
@@ -2106,8 +2137,8 @@ function inferP0NarrativeDomain(units: ProblemUnit[]): P0NarrativeDomain {
}
if (
hasCloseAccount ||
units.some((unit) => ["period_close", "deferred_expense", "fixed_asset"].includes(String(unit.lifecycle_domain ?? ""))) ||
units.some((unit) => unit.problem_unit_type === "period_risk_cluster" || unit.problem_unit_type === "lifecycle_anomaly_node")
units.some((unit) => ["period_close", "deferred_expense"].includes(String(unit.lifecycle_domain ?? ""))) ||
units.some((unit) => unit.problem_unit_type === "period_risk_cluster")
) {
return "month_close_costs_20_44";
}
@@ -2158,8 +2189,7 @@ function p0NarrativeDomainFromHint(value: string | null | undefined): P0Narrativ
if (
normalized.includes("month_close_costs_20_44") ||
normalized.includes("period_close") ||
normalized.includes("deferred_expense") ||
normalized.includes("fixed_asset")
normalized.includes("deferred_expense")
) {
return "month_close_costs_20_44";
}
@@ -2370,7 +2400,31 @@ function evaluateP0DomainEvidenceGrounding(
const topClass = classify(top);
const hasAnyPrimary = substantive.some((item) => classify(item).inDomain);
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.inDomain;
const blocked = hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top);
const topAccounts = collectResultAccounts(top);
const topDomains = collectResultDomains(top);
const topRelations = collectResultRelations(top);
const vatPrimarySignals =
topAccounts.filter((item) => isVatAccountToken(item)).length +
topDomains.filter((item) => isVatDomainToken(item)).length +
topRelations.filter((item) =>
/invoice_to_vat|source_doc_present|invoice_linked|register_to_book|book_entry_generated|deduction_posted|vat_/i.test(item)
).length;
const vatForeignSignals =
topAccounts.filter((item) => isSettlementAccountToken(item) || isCloseCostsAccountToken(item)).length +
topDomains.filter((item) => isForeignToVatDomainToken(item)).length +
topRelations.filter((item) =>
/payment_to_settlement|statement_to_document|deferred_expense_to_writeoff|close_operation|allocation|period_close|fixed_asset/i.test(
item
)
).length;
const vatContaminatedPrimary =
focusDomain === "vat_document_register_book" &&
topClass.inDomain &&
topClass.foreignDomains.length > 0 &&
vatForeignSignals > Math.max(1, vatPrimarySignals) &&
!hasControlledCrossDomainHandoffInResult(top);
const blocked =
(hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top)) || vatContaminatedPrimary;
return {
has_primary: hasAnyPrimary,
@@ -2403,7 +2457,7 @@ function hasStrongNarrativeDomainSignalInText(userMessage: string, domain: P0Nar
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(
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
text
)
);
@@ -2411,6 +2465,23 @@ function hasStrongNarrativeDomainSignalInText(userMessage: string, domain: P0Nar
return false;
}
function hasFixedAssetAmortizationSignalInText(userMessage: string): boolean {
const text = String(userMessage ?? "").toLowerCase();
const explicitFixedAssetAccountMention =
/(?:сч(?:е|ё)т(?:а|у|ом|ов)?\s*(?:№|#|:)?\s*0[12](?:\.\d{1,2})?|\b0[12]\s*\/\s*0[12]\b)/iu.test(text);
return (
explicitFixedAssetAccountMention ||
/(основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(text)
);
}
function hasExplicitMonthCloseSignalInText(userMessage: string): boolean {
const text = String(userMessage ?? "").toLowerCase();
return /(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
text
);
}
function inferP0FocusNarrativeDomain(
userMessage: string,
results: UnifiedRetrievalResult[],
@@ -2421,12 +2492,16 @@ function inferP0FocusNarrativeDomain(
const fromMessage = inferNarrativeDomainFromText(userMessage);
const strongFromMessage = Boolean(fromMessage && hasStrongNarrativeDomainSignalInText(userMessage, fromMessage));
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
const fixedAssetOnlySignal = hasFixedAssetAmortizationSignalInText(userMessage) && !hasExplicitMonthCloseSignalInText(userMessage);
if (fromHint && fromMessage && fromHint !== fromMessage) {
return strongFromMessage ? fromMessage : fromHint;
}
if (fromHint) {
return fromHint;
}
if (fromDomainGuard === "month_close_costs_20_44" && fixedAssetOnlySignal) {
return null;
}
if (fromDomainGuard && fromMessage && fromDomainGuard !== fromMessage) {
return strongFromMessage ? fromMessage : fromDomainGuard;
}
@@ -2787,6 +2862,7 @@ function buildProblemCentricAnswerStructure(input: {
const openUncertainties = uniqueStrings(
[
...input.groundingCheck.missing_requirements,
...(input.domainLockMiss ? ["primary_domain_evidence_not_confirmed"] : []),
...(input.missingAnchors.period ? ["missing_anchor:period"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.account ? ["missing_anchor:account"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.documentOrObject
@@ -2870,6 +2946,8 @@ function limitationReasonToUserText(code: EvidenceLimitationReasonCode): string
function inferNarrativeDomainFromText(value: string): P0NarrativeDomain {
const text = String(value ?? "").toLowerCase();
const accountTokens = extractAccountNumbersFromNarrativeText(text);
const fixedAssetSignal = hasFixedAssetAmortizationSignalInText(text);
const explicitMonthCloseSignal = hasExplicitMonthCloseSignalInText(text);
let settlementScore = 0;
let vatScore = 0;
@@ -2898,14 +2976,14 @@ function inferNarrativeDomainFromText(value: string): P0NarrativeDomain {
) {
vatScore += 3;
}
if (
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
text
)
) {
if (explicitMonthCloseSignal) {
monthCloseScore += 3;
}
if (fixedAssetSignal && !explicitMonthCloseSignal && settlementScore === 0 && vatScore === 0) {
return null;
}
const maxScore = Math.max(settlementScore, vatScore, monthCloseScore);
if (maxScore <= 0) {
return null;
@@ -2960,9 +3038,50 @@ function buildShortSectionLine(structure: AnswerStructureV11): string {
return incomplete ? "Проблема подтверждается частично на текущей опоре." : "Проблема подтверждена на текущей опоре.";
}
function humanizeCompositeDirectAnswer(value: string): string | null {
const raw = String(value ?? "").trim();
if (!raw) {
return null;
}
const tokenPattern = /\b[a-z][a-z0-9_:-]{2,}\b/gi;
const tokenMappings = uniqueStrings(
Array.from(raw.matchAll(tokenPattern))
.map((match) => humanizeTechnicalToken(String(match?.[0] ?? "")))
.filter((item): item is string => Boolean(item))
.map((item) => ensureSentence(item)),
4
);
const residualRaw = raw
.replace(tokenPattern, " ")
.replace(/[()]/g, " ")
.replace(/\s*[;:]\s*/g, " ")
.replace(/\s{2,}/g, " ")
.trim();
const residualText = sanitizeUserText(residualRaw);
const lines: string[] = [...tokenMappings];
if (residualText && !hasUserFacingLeakage(residualText)) {
lines.push(ensureSentence(residualText));
}
const compact = dedupeNarrativeLines(lines, 3);
if (compact.length === 0) {
return null;
}
return compact.join(" ");
}
function buildBrokenSectionLines(structure: AnswerStructureV11): string[] {
const direct = sanitizeUserText(structure.direct_answer);
if (direct) {
if (/\b[a-z]+_[a-z0-9_:-]+\b/i.test(direct)) {
const compositeHumanized = humanizeCompositeDirectAnswer(direct);
if (compositeHumanized) {
return [compositeHumanized];
}
}
const mapped = mapDefectTokenToNarrative(direct) ?? humanizeTechnicalToken(direct);
if (mapped) {
return [ensureSentence(mapped)];
@@ -2975,7 +3094,7 @@ function buildBrokenSectionLines(structure: AnswerStructureV11): string[] {
return ["Есть признаки нарушения в связанной цепочке документов и проводок."];
}
function buildWhySectionLines(structure: AnswerStructureV11): string[] {
function buildWhySectionLines(structure: AnswerStructureV11, context?: AnswerRenderContext): string[] {
const noteLines = dedupeNarrativeLines(
structure.mechanism_block.mechanism_notes
.map((item) => sanitizeSupportLine(item))
@@ -2984,11 +3103,31 @@ function buildWhySectionLines(structure: AnswerStructureV11): string[] {
4
);
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(sanitizeUserText(structure.direct_answer) ?? "");
const mechanismCorpus = `${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(
" "
)}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetSignal =
fixedAssetContextSignal ||
((context?.focusDomain ?? null) !== "settlements_60_62" && hasFixedAssetSignalInStructure(structure, context));
const rbpSignal = hasRbpContextSignal(context) || hasRbpSignalInText(mechanismCorpus);
const lines: string[] = [...noteLines];
if (structure.mechanism_block.status === "grounded") {
lines.push("Признак проблемы повторяется в связанных документах и проводках.");
} else if (structure.mechanism_block.status === "limited") {
lines.push("Часть ожидаемой цепочки подтверждена, но ключевой переход закрытия не подтвержден.");
if (domain === "vat_document_register_book") {
lines.push("Часть НДС-цепочки подтверждена, но один или несколько переходов документ -> счет-фактура -> регистр -> книга не подтверждены.");
} else if (fixedAssetSignal) {
lines.push("По ОС часть переходов к начислению амортизации подтверждена не полностью, поэтому есть риск пропуска отдельных объектов.");
} else if (rbpSignal) {
lines.push("По РБП часть списаний к концу периода подтверждена не полностью, поэтому остаток может сохраняться дольше ожидаемого.");
} else if (domain === "month_close_costs_20_44") {
lines.push("Часть шагов закрытия периода подтверждена, но ключевой переход распределения/закрытия не подтвержден.");
} else {
lines.push("Часть ожидаемой цепочки подтверждена, но ключевой переход не подтвержден.");
}
} else {
lines.push("Сигнал проблемы есть, но механизм подтвержден не полностью.");
}
@@ -3044,7 +3183,8 @@ function buildCoverageSplitLines(
function buildEvidenceSectionLines(
structure: AnswerStructureV11,
questionType: QuestionTypeClass = "unknown"
questionType: QuestionTypeClass = "unknown",
context?: AnswerRenderContext
): string[] {
const evidenceCount = Array.isArray(structure.evidence_block.evidence_ids) ? structure.evidence_block.evidence_ids.length : 0;
const sourceCount = Array.isArray(structure.evidence_block.source_refs) ? structure.evidence_block.source_refs.length : 0;
@@ -3058,13 +3198,38 @@ function buildEvidenceSectionLines(
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
const lines: string[] = [];
const coverageSplitLines = buildCoverageSplitLines(structure, questionType);
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(sanitizeUserText(structure.direct_answer) ?? "");
const evidenceCorpus = `${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(
" "
)}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetSignal =
fixedAssetContextSignal ||
((context?.focusDomain ?? null) !== "settlements_60_62" && hasFixedAssetSignalInStructure(structure, context));
const rbpSignal = hasRbpContextSignal(context) || hasRbpSignalInText(evidenceCorpus);
if (questionType === "what_is_it_grounded_on") {
lines.push("Основание вывода перечислено по подтвержденным документам, регистрам и проводкам.");
if (domain === "vat_document_register_book") {
lines.push("Основание собрано по НДС-цепочке: документ, счет-фактура, регистр НДС и запись книги.");
} else if (fixedAssetSignal) {
lines.push("Основание собрано по ОС: карточка объекта, параметры амортизации, начисление и движения по 01/02.");
} else if (rbpSignal) {
lines.push("Основание собрано по РБП: объект списания, документ списания и остаток на конец периода.");
} else {
lines.push("Основание вывода перечислено по подтвержденным документам, регистрам и проводкам.");
}
} else if (questionType === "prove_or_guess") {
lines.push("Основание разделено на подтвержденную часть и зону гипотез.");
} else if (questionType === "which_chains_are_complete_vs_incomplete") {
lines.push("Опора собрана так, чтобы разделить цепочки на полные и неполные.");
if (domain === "vat_document_register_book") {
lines.push("Опора собрана по звеньям НДС-цепочки, чтобы разделить полные и неполные переходы.");
} else if (rbpSignal) {
lines.push("Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.");
} else if (fixedAssetSignal) {
lines.push("Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.");
} else {
lines.push("Опора собрана так, чтобы разделить цепочки на полные и неполные.");
}
}
if (evidenceCount > 0) {
@@ -3076,10 +3241,20 @@ function buildEvidenceSectionLines(
if (claimLinks > 0) {
lines.push("Есть связка между основным выводом и подтверждающими записями.");
}
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
lines.push("Опора частичная: часть требований покрыта не полностью.");
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited" || reliabilityLimited) {
if (domain === "vat_document_register_book") {
lines.push("Опора частичная: по НДС-цепочке не подтверждены одно или несколько звеньев.");
} else if (fixedAssetSignal) {
lines.push("Опора частичная: не по всем объектам ОС подтверждено попадание в начисление амортизации.");
} else if (rbpSignal) {
lines.push("Опора частичная: не по всем объектам РБП подтверждено списание к концу периода.");
} else if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
lines.push("Опора частичная: часть требований покрыта не полностью.");
} else if (evidenceCount > 0) {
lines.push("Опора есть, но достаточна только для предварительного вывода.");
}
} else if (evidenceCount > 0) {
lines.push(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
lines.push("Опора достаточна для первичного вывода.");
}
if (lines.length === 0) {
@@ -3110,6 +3285,143 @@ function buildDefaultChecksByDomain(domain: P0NarrativeDomain): string[] {
return ["Проверьте связку документов и проводок по проблемному участку в указанном периоде."];
}
function hasFixedAssetAnchorContext(context?: AnswerRenderContext): boolean {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used].join(" ").toLowerCase();
return /(?:doc_type:amortization|account:0[12]|амортиз|основн|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|fixed\s*asset|depreciat)/i.test(
corpus
);
}
function hasFixedAssetContextSignal(context?: AnswerRenderContext): boolean {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used, context.userMessage ?? ""].join(" ").toLowerCase();
return (
hasFixedAssetAnchorContext(context) ||
hasFixedAssetAmortizationSignalInText(corpus) ||
/(?:\bос\b|основн(?:ые|ых)?\s+средств|амортиз|сч(?:е|ё)т\s*0[12])/i.test(corpus)
);
}
function hasRbpAnchorContext(context?: AnswerRenderContext): boolean {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used].join(" ").toLowerCase();
return /(?:\brbp(?:[_\s-]?writeoff)?\b|рбп|deferred[_\s-]?expense(?:[_\s-]?to[_\s-]?writeoff)?|doc_type:(?:deferred|rbp_writeoff)|счет\s*97|account:97)/i.test(
corpus
);
}
function hasRbpContextSignal(context?: AnswerRenderContext): boolean {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used, context.userMessage ?? ""].join(" ");
return hasRbpAnchorContext(context) || hasRbpSignalInText(corpus);
}
function hasRbpSignalInText(value: string): boolean {
const text = String(value ?? "").toLowerCase();
return /(?:\brbp(?:[_\s-]?writeoff)?\b|рбп|deferred[_\s-]?expense(?:[_\s-]?to[_\s-]?writeoff)?|счет\s*97|списани[ея]\s+рбп|остат(ок|ки)\s+рбп)/i.test(
text
);
}
function hasFixedAssetSignalInStructure(structure: AnswerStructureV11, context?: AnswerRenderContext): boolean {
const corpus = [
structure.direct_answer,
...structure.mechanism_block.mechanism_notes,
...structure.evidence_block.mechanism_notes,
...(structure.evidence_block.source_refs ?? []),
...(structure.evidence_block.evidence_ids ?? []),
...(context?.anchors.present ?? []),
...(context?.anchors.used ?? [])
]
.filter(Boolean)
.join(" ");
if (hasFixedAssetAnchorContext(context) || hasFixedAssetAmortizationSignalInText(corpus)) {
return true;
}
return /(?:asset_card_to_depreciation|fixed_asset|fixed_assets|амортиз|основн(?:ые|ых)?\s+средств|сч(?:е|ё)т\s*0[12]|\b0[12](?:\.\d{2})?\b)/i.test(
corpus
);
}
function buildFixedAssetChecksByQuestionType(questionType: QuestionTypeClass): string[] {
if (questionType === "what_to_check_first") {
return [
"Проверьте по каждому объекту ОС карточку и параметр амортизации (способ, срок, дата начала начисления).",
"Сверьте ввод в эксплуатацию и попадание объекта в набор начисления амортизации за нужный период.",
"Подтвердите начисление по объектам проводками и регистром амортизации."
];
}
if (questionType === "prove_or_guess") {
return [
"Разделите доказанные и предположительные участки по цепочке ОС: принятие -> ввод -> начисление амортизации.",
"Проверьте, какие объекты отсутствуют в наборе начисления или имеют некорректные параметры амортизации."
];
}
if (questionType === "where_break_is") {
return [
"Локализуйте разрыв в цепочке ОС: карточка объекта -> ввод в эксплуатацию -> начисление амортизации.",
"Сверьте, на каком шаге пропадает подтверждение по конкретным объектам."
];
}
if (questionType === "what_is_it_grounded_on") {
return [
"Перечислите основание: карточка ОС, документ ввода в эксплуатацию, запись регистра амортизации, проводки по начислению."
];
}
return [
"Проверьте ОС-контур: объект ОС -> ввод в эксплуатацию -> начисление амортизации по счетам 01/02.",
"Сверьте параметр амортизации и наличие начисления по каждому объекту ОС в периоде."
];
}
function buildRbpChecksByQuestionType(questionType: QuestionTypeClass): string[] {
if (questionType === "what_to_check_first") {
return [
"Проверьте список объектов РБП, которые должны были списаться к концу периода.",
"Сверьте документ списания РБП и движение по счету 97 по каждому объекту.",
"Проверьте остаток РБП после списания и причину, если часть суммы остается активной."
];
}
if (questionType === "prove_or_guess") {
return [
"Разделите по РБП доказанное и гипотезу: где списание подтверждено, а где есть только косвенные признаки.",
"Проверьте, для каких объектов РБП нет подтверждения списания на конец периода."
];
}
if (questionType === "where_break_is") {
return [
"Локализуйте разрыв в РБП-цепочке: объект РБП -> документ списания -> движение по счету 97.",
"Проверьте, на каком шаге исчезает подтверждение списания."
];
}
if (questionType === "what_is_it_grounded_on") {
return [
"Перечислите основание по РБП: объект, документ списания, движение по счету 97, остаток на конец периода."
];
}
if (questionType === "which_chains_are_complete_vs_incomplete") {
return [
"Разделите РБП-цепочки на: списание подтверждено, подтверждено частично, не подтверждено.",
"Проверьте, где к концу периода остается РБП без подтвержденного списания."
];
}
return [
"Проверьте РБП-контур: объект РБП -> документ списания -> движение по счету 97.",
"Сверьте остаток РБП на конец периода и причину, если часть суммы не списана."
];
}
function buildQuestionTypeDomainChecks(questionType: QuestionTypeClass, domain: P0NarrativeDomain): string[] {
if (questionType === "what_to_check_first") {
if (domain === "settlements_60_62") {
@@ -3238,7 +3550,23 @@ function buildChecksSectionLines(structure: AnswerStructureV11, context?: Answer
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(broken);
const questionType = context?.questionType ?? "unknown";
const domainFallback = buildQuestionTypeDomainChecks(questionType, domain);
const effectiveQuestionType: QuestionTypeClass = questionType === "unknown" ? "what_to_check_first" : questionType;
const fixedAssetMechanismSignal = hasFixedAssetAmortizationSignalInText(
`${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(" ")}`
);
const domainAndEvidenceCorpus = `${broken} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(
" "
)}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetCase =
fixedAssetContextSignal ||
(domain !== "settlements_60_62" && (hasFixedAssetSignalInStructure(structure, context) || fixedAssetMechanismSignal));
const rbpCase = hasRbpContextSignal(context) || hasRbpSignalInText(domainAndEvidenceCorpus);
const domainFallback = fixedAssetCase
? buildFixedAssetChecksByQuestionType(effectiveQuestionType)
: rbpCase
? buildRbpChecksByQuestionType(effectiveQuestionType)
: buildQuestionTypeDomainChecks(questionType, domain);
const hasMissingPeriod = structure.uncertainty_block.open_uncertainties.some((item) =>
/missing_anchor:period/i.test(String(item ?? ""))
);
@@ -3267,16 +3595,21 @@ function buildChecksSectionLines(structure: AnswerStructureV11, context?: Answer
}
}
}
const filteredLines =
fixedAssetCase || rbpCase
? lines.filter((item) => !/проверьте связку документов и проводок по проблемному участку/i.test(item))
: lines;
if (hasMissingPeriod) {
if (questionType === "what_to_check_first") {
lines.push("Уточните период, если он не зафиксирован в исходной формулировке вопроса.");
} else if (domain === "settlements_60_62" && lines.length > 0) {
lines.push("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
filteredLines.push("Уточните период, если он не зафиксирован в исходной формулировке вопроса.");
} else if (domain === "settlements_60_62" && filteredLines.length > 0) {
filteredLines.push("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
} else {
lines.unshift("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
filteredLines.unshift("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
}
}
return dedupeNarrativeLines(lines, questionType === "what_to_check_first" ? 3 : 5);
return dedupeNarrativeLines(filteredLines, questionType === "what_to_check_first" ? 3 : 5);
}
function humanizeLimitationToken(value: string): string | null {
@@ -3366,6 +3699,15 @@ function buildQuestionTypeShortLine(context: AnswerRenderContext): string | null
return "\u0412\u044b\u0432\u043e\u0434 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d \u043d\u0430 \u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0443.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Ниже перечислены основания вывода по РБП: списание, остаток и подтверждение на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Ниже перечислены основания вывода по ОС/амортизации по данным учета.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Ниже перечислены основания вывода по НДС-цепочке по данным учета.";
}
return "\u041d\u0438\u0436\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u044b \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c \u0443\u0447\u0435\u0442\u0430.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
@@ -3384,8 +3726,14 @@ function buildQuestionTypeShortLine(context: AnswerRenderContext): string | null
if (context.focusDomain === "month_close_costs_20_44") {
return "Наиболее вероятная причина: цепочка распределения затрат и закрытия месяца подтверждена не полностью.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Наиболее вероятная причина: по ОС часть переходов от параметров амортизации к начислению подтверждена не полностью.";
}
return "Наиболее вероятный механизм проблемы подтвержден частично и требует первичной проверки.";
}
if (context.questionType === "unknown" && hasFixedAssetAnchorContext(context)) {
return "Риск неполного начисления амортизации подтвержден частично и требует проверки по объектам ОС.";
}
return null;
}
@@ -3416,6 +3764,15 @@ function buildQuestionTypeWhyLine(context: AnswerRenderContext): string | null {
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.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Фокус ответа по РБП: подтверждение списания и остатка на конец периода, а не общий close-narrative.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Фокус ответа по ОС: подтверждение попадания объектов в начисление амортизации.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Фокус ответа по НДС: подтверждение переходов между документом, счетом-фактурой, регистром и книгой.";
}
return "\u0424\u043e\u043a\u0443\u0441 \u043e\u0442\u0432\u0435\u0442\u0430 \u0441\u043c\u0435\u0449\u0435\u043d \u0432 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438, \u0430 \u043d\u0435 \u0432 \u043e\u0431\u0449\u0438\u0439 narrative.";
}
return null;
@@ -3423,12 +3780,30 @@ function buildQuestionTypeWhyLine(context: AnswerRenderContext): string | null {
function buildQuestionTypeEvidenceLine(context: AnswerRenderContext): string | null {
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Опора перечислена по РБП-объектам, документам списания и остаткам на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Опора перечислена по ОС-объектам, параметрам амортизации и движениям начисления.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Опора перечислена по НДС-звеньям: документ, счет-фактура, регистр и книга.";
}
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.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
if (context.focusDomain === "vat_document_register_book") {
return "Опора собрана по НДС-звеньям, чтобы разделить полные и неполные переходы.";
}
if (hasRbpContextSignal(context)) {
return "Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.";
}
return "\u041e\u043f\u043e\u0440\u0430 \u0441\u043e\u0431\u0440\u0430\u043d\u0430 \u0442\u0430\u043a, \u0447\u0442\u043e\u0431\u044b \u0447\u0435\u0441\u0442\u043d\u043e \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0435 \u0438 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438.";
}
return null;
@@ -3452,9 +3827,27 @@ function buildQuestionTypeCheckLine(context: AnswerRenderContext): string | null
return "\u041f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435\u043c \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435 \u0444\u0430\u043a\u0442\u044b \u043e\u0442 \u0433\u0438\u043f\u043e\u0442\u0435\u0437.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Сначала перечислите по РБП: объект, документ списания и остаток после списания на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Сначала перечислите по ОС: объект, параметры амортизации и подтверждение начисления за период.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Сначала перечислите по НДС: документ, счет-фактуру, запись регистра и запись книги.";
}
return "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0438\u0442\u0435 \u043e\u043f\u043e\u0440\u043d\u044b\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b, \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0449\u0438\u0435 \u043f\u0440\u043e\u0432\u043e\u0434\u043a\u0438.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
if (context.focusDomain === "vat_document_register_book") {
return "Сначала разложите НДС-цепочку по шагам: документ -> счет-фактура -> регистр -> книга.";
}
if (hasRbpContextSignal(context)) {
return "Сначала разложите РБП-цепочку на подтвержденное списание, частичное и неподтвержденное.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Сначала разложите ОС-цепочку на подтвержденное начисление, частичное и неподтвержденное.";
}
return "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0440\u0430\u0437\u043b\u043e\u0436\u0438\u0442\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u043d\u0430 \u043f\u043e\u043b\u043d\u044b\u0435, \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e \u043f\u043e\u043b\u043d\u044b\u0435 \u0438 \u043d\u0435\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435.";
}
return null;
@@ -3539,15 +3932,152 @@ function applyQuestionTypeAndAnchorPolicy(input: {
};
}
type DomainWordingMode = "neutral" | "rbp" | "fa_amortization";
const RBP_WORDING_PATTERN =
/(?:\bрбп\b|deferred[_\s-]?expense|сч(?:е|ё)т\s*97|объект\w*\s+рбп|списани[ея]\s+рбп|остат(?:ок|ки)\s+рбп|документ\s+списани[яе])/iu;
const FA_WORDING_PATTERN =
/(?:\bос\b|основн(?:ые|ых)?\s+средств|амортиз|сч(?:е|ё)т\s*0[12]|01\/02|карточк\w*\s+ос|объект\w*\s+ос|ввод\w*\s+в\s+эксплуатац|fixed\s*asset|depreciat)/iu;
function hasRbpWordingPhrase(value: string): boolean {
return RBP_WORDING_PATTERN.test(String(value ?? ""));
}
function hasFaWordingPhrase(value: string): boolean {
return FA_WORDING_PATTERN.test(String(value ?? ""));
}
function resolveDomainWordingMode(structure: AnswerStructureV11, context?: AnswerRenderContext): DomainWordingMode {
if (!context) {
return "neutral";
}
const userMessage = String(context.userMessage ?? "");
const explicitRbpFromMessage = hasRbpSignalInText(userMessage);
const explicitFaFromMessage = hasFixedAssetAmortizationSignalInText(userMessage);
if (explicitRbpFromMessage && !explicitFaFromMessage) {
return "rbp";
}
if (explicitFaFromMessage && !explicitRbpFromMessage) {
return "fa_amortization";
}
const anchorRbp = hasRbpAnchorContext(context);
const anchorFa = hasFixedAssetAnchorContext(context);
if (anchorRbp && !anchorFa) {
return "rbp";
}
if (anchorFa && !anchorRbp) {
return "fa_amortization";
}
const structureCorpus = [
structure.direct_answer,
...structure.mechanism_block.mechanism_notes,
...structure.evidence_block.mechanism_notes,
...(structure.evidence_block.source_refs ?? []),
...(context.anchors.present ?? []),
...(context.anchors.used ?? [])
]
.filter(Boolean)
.join(" ");
const structureRbp = hasRbpSignalInText(structureCorpus);
const structureFa = hasFixedAssetAmortizationSignalInText(structureCorpus);
const rbpScore = [explicitRbpFromMessage, anchorRbp, structureRbp].filter(Boolean).length;
const faScore = [explicitFaFromMessage, anchorFa, structureFa].filter(Boolean).length;
if (rbpScore > faScore) {
return "rbp";
}
if (faScore > rbpScore) {
return "fa_amortization";
}
return "neutral";
}
function enforceDomainWordingIsolation(
payload: {
shortLine: string;
brokenLines: string[];
whyLines: string[];
evidenceLines: string[];
checkLines: string[];
limitationLines: string[];
},
structure: AnswerStructureV11,
context?: AnswerRenderContext
): {
shortLine: string;
brokenLines: string[];
whyLines: string[];
evidenceLines: string[];
checkLines: string[];
limitationLines: string[];
} {
const mode = resolveDomainWordingMode(structure, context);
if (mode === "neutral" || !context) {
return payload;
}
const effectiveQuestionType: QuestionTypeClass = context.questionType === "unknown" ? "what_to_check_first" : context.questionType;
const isForbidden = mode === "rbp" ? hasFaWordingPhrase : hasRbpWordingPhrase;
const filterLines = (lines: string[]): string[] => lines.filter((line) => !isForbidden(line));
const shortFallback =
mode === "rbp"
? "Признаки по РБП подтверждены частично и требуют проверки списания к концу периода."
: "Риск неполного начисления амортизации по объектам ОС подтвержден частично.";
const whyFallback =
mode === "rbp"
? ["По РБП часть списаний к концу периода подтверждена не полностью, поэтому остаток может сохраняться дольше ожидаемого."]
: ["По ОС часть переходов к начислению амортизации подтверждена не полностью, поэтому есть риск пропуска отдельных объектов."];
const evidenceFallback =
mode === "rbp"
? ["Основание собрано по РБП: объект списания, документ списания и остаток на конец периода."]
: ["Основание собрано по ОС: карточка объекта, параметры амортизации, начисление и движения по 01/02."];
const checkFallback =
mode === "rbp"
? buildRbpChecksByQuestionType(effectiveQuestionType).slice(0, 2)
: buildFixedAssetChecksByQuestionType(effectiveQuestionType).slice(0, 2);
const filteredShort = isForbidden(payload.shortLine) ? shortFallback : payload.shortLine;
const filteredBroken = dedupeNarrativeLines(filterLines(payload.brokenLines), 4);
const filteredWhy = dedupeNarrativeLines(
[...filterLines(payload.whyLines), ...(filterLines(payload.whyLines).length === 0 ? whyFallback : [])],
4
);
const filteredEvidence = dedupeNarrativeLines(
[...filterLines(payload.evidenceLines), ...(filterLines(payload.evidenceLines).length === 0 ? evidenceFallback : [])],
7
);
const filteredChecks = dedupeNarrativeLines(
[...filterLines(payload.checkLines), ...(filterLines(payload.checkLines).length === 0 ? checkFallback : [])],
effectiveQuestionType === "what_to_check_first" ? 3 : 5
);
const filteredLimitations = dedupeNarrativeLines(filterLines(payload.limitationLines), 6);
return {
shortLine: ensureSentence(filteredShort),
brokenLines: filteredBroken.length > 0 ? filteredBroken : payload.brokenLines,
whyLines: filteredWhy.length > 0 ? filteredWhy : whyFallback,
evidenceLines: filteredEvidence.length > 0 ? filteredEvidence : evidenceFallback,
checkLines: filteredChecks.length > 0 ? filteredChecks : checkFallback,
limitationLines: filteredLimitations.length > 0 ? filteredLimitations : payload.limitationLines
};
}
function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRenderContext): string {
const questionType = context?.questionType ?? "unknown";
const shortLine = ensureSentence(buildShortSectionLine(structure));
const brokenLines = buildBrokenSectionLines(structure);
const whyLines = buildWhySectionLines(structure);
const evidenceLines = buildEvidenceSectionLines(structure, questionType);
const whyLines = buildWhySectionLines(structure, context);
const evidenceLines = buildEvidenceSectionLines(structure, questionType, context);
const checkLines = buildChecksSectionLines(structure, context);
const limitationLines = buildLimitationsSectionLines(structure);
const enriched = context
const enrichedBase = context
? applyQuestionTypeAndAnchorPolicy({
shortLine,
brokenLines,
@@ -3565,6 +4095,7 @@ function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRender
checkLines,
limitationLines
};
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
return sanitizeUserFacingReply(
[
@@ -3684,7 +4215,10 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
}
: decision;
const missingAnchors = detectMissingAnchors(input.userMessage, input.retrievalResults);
const missingAnchors = detectMissingAnchors(input.userMessage, input.retrievalResults, {
normalizationPeriodExplicit: Boolean(input.normalizationPeriodExplicit),
companyAnchors: input.companyAnchors ?? null
});
const hasProblemWeakSignal =
policySignals.narrowing_strength !== "strong" ||
policySignals.minimum_evidence_failed ||
@@ -3732,7 +4266,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
assistant_reply: renderPolicyReply(problemCentricStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage
anchors: anchorUsage,
userMessage: input.userMessage
}),
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
@@ -3851,7 +4386,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
assistant_reply: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage
anchors: anchorUsage,
userMessage: input.userMessage
}),
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
@@ -3908,6 +4444,10 @@ function composeExplainableAnswer(input: ComposeAnswerInput, scopeLabel: "full"
);
}
export function sanitizeAssistantReplyForUserFacing(value: string): string {
return sanitizeUserFacingReply(value);
}
export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswerOutput {
if (input.enableAnswerPolicyV11) {
return composeAssistantAnswerV11(input);