Этап 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);
@@ -1636,12 +1636,19 @@ function cardResolutionScore(card: P0DomainCard, fragmentText: string, profile:
}
const hasVatSoftAnchor = card.id === "vat_document_register_book" && hasStrongVatDomainSignal(fragmentText, profile);
const hasHardAnchor = accountMatches.length > 0 || markerHit || hasVatSoftAnchor;
const hasMonthCloseSignal = card.id === "month_close_costs_20_44" && hasStrongMonthCloseSignal(fragmentText, profile);
const fixedAssetOnlySignal =
card.id === "month_close_costs_20_44" && hasFixedAssetSignal(fragmentText, profile) && !hasMonthCloseSignal && accountMatches.length === 0;
if (fixedAssetOnlySignal) {
return 0;
}
const markerWeight = card.id === "month_close_costs_20_44" ? hasMonthCloseSignal : markerHit;
const hasHardAnchor = accountMatches.length > 0 || markerWeight || hasVatSoftAnchor;
if (!hasHardAnchor) {
return 0;
}
return accountMatches.length * 4 + domainMatches.length * 3 + (markerHit ? 2 : 0);
return accountMatches.length * 4 + domainMatches.length * 3 + (markerWeight ? 2 : 0);
}
function hasStrongVatDomainSignal(fragmentText: string, profile: SemanticRetrievalProfile): boolean {
@@ -1660,6 +1667,30 @@ function hasStrongVatDomainSignal(fragmentText: string, profile: SemanticRetriev
);
}
function hasStrongMonthCloseSignal(fragmentText: string, profile: SemanticRetrievalProfile): boolean {
const text = String(fragmentText ?? "");
const hasMonthCloseLexicalAnchor =
/(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|month\s*close|period\s*close|close\s+operation)/iu.test(
text
);
return (
hasMonthCloseLexicalAnchor ||
profile.account_scope.some((account) => CLOSE_COST_ACCOUNTS.includes(account)) ||
profile.domain_scope.some((domain) => domain === "period_close" || domain === "deferred_expense") ||
profile.relation_patterns.some((pattern) =>
["deferred_expense_to_writeoff", "close_operation", "allocation_rules_resolved", "residuals_zero_or_explained"].includes(pattern)
)
);
}
function hasFixedAssetSignal(fragmentText: string, profile: SemanticRetrievalProfile): boolean {
const text = String(fragmentText ?? "");
return (
/(?:основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/iu.test(text) ||
profile.account_scope.some((account) => account === "01" || account === "02")
);
}
function hasStrongSettlementAccountSignal(profile: SemanticRetrievalProfile): boolean {
return profile.account_scope.some((account) => account === "51" || account === "60" || account === "62" || account === "76");
}
@@ -1726,6 +1757,27 @@ function hasSettlementRecoverySignal(signals: RecordSemanticSignals): boolean {
return hasSettlementAccount || hasSettlementDomain || hasSettlementRelation || hasSettlementDocument;
}
function isVatAllowedAccountContext(account: string): boolean {
const normalized = String(account ?? "").trim();
return normalized === "19" || normalized === "68";
}
function isVatAllowedDocumentContext(documentType: string): boolean {
return /(?:invoice|vat_document|purchase_book|sales_book|tax_entry|supplier_receipt|sales_document|register)/i.test(
String(documentType ?? "")
);
}
function isVatAllowedRelationPattern(pattern: string): boolean {
return /(?:invoice_to_vat|register_to_book|book_entry_generated|deduction_posted|document_to_posting|contract_to_documents|source_doc_present|invoice_linked)/i.test(
String(pattern ?? "")
);
}
function isVatAllowedGraphDomain(domain: string): boolean {
return /(?:vat_flow)/i.test(String(domain ?? ""));
}
function collectSourceRecords(data: DatasetBundle, sources: DatasetSourceName[]): SourceScopedRecord[] {
const items: SourceScopedRecord[] = [];
for (const source of sources) {
@@ -2995,6 +3047,9 @@ export class AssistantDataLayer {
group.relations.set(relation, (group.relations.get(relation) ?? 0) + 1);
}
for (const account of evaluation.signals.account_context) {
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedAccountContext(account)) {
continue;
}
if (semanticProfile.account_scope.length === 0 || semanticProfile.account_scope.includes(account)) {
group.account_context.add(account);
}
@@ -3006,6 +3061,9 @@ export class AssistantDataLayer {
) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedDocumentContext(item)) {
continue;
}
group.document_context.add(item);
}
for (const item of evaluation.signals.relation_patterns) {
@@ -3015,6 +3073,9 @@ export class AssistantDataLayer {
) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedRelationPattern(item)) {
continue;
}
group.relation_pattern_hits.add(item);
}
for (const item of evaluation.signals.anomaly_patterns) {
@@ -3035,6 +3096,9 @@ export class AssistantDataLayer {
) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedGraphDomain(domain)) {
continue;
}
group.graph_domain_scope.add(domain);
}
for (const reason of evaluation.match_reasons.slice(0, 4)) {
@@ -3050,22 +3114,30 @@ export class AssistantDataLayer {
const sampleAccountContext =
domainCard?.id === "settlements_60_62"
? evaluation.signals.account_context.filter((item) => ["51", "60", "62", "76"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.account_context.filter((item) => isVatAllowedAccountContext(item))
: evaluation.signals.account_context;
const sampleDocumentContext =
domainCard?.id === "settlements_60_62"
? evaluation.signals.document_types.filter((item) =>
["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document", "manual_operation"].includes(item)
)
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.document_types.filter((item) => isVatAllowedDocumentContext(item))
: evaluation.signals.document_types;
const sampleRelationPatterns =
domainCard?.id === "settlements_60_62"
? evaluation.signals.relation_patterns.filter((item) =>
["payment_to_settlement", "statement_to_document", "contract_to_documents", "document_to_posting"].includes(item)
)
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.relation_patterns.filter((item) => isVatAllowedRelationPattern(item))
: evaluation.signals.relation_patterns;
const sampleGraphDomainScope =
domainCard?.id === "settlements_60_62"
? evaluation.graph_domain_scope.filter((item) => ["bank_settlement", "customer_settlement"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.graph_domain_scope.filter((item) => isVatAllowedGraphDomain(item))
: evaluation.graph_domain_scope;
group.samples.push({
source_entity: record.source_entity,
@@ -1,4 +1,4 @@
// @ts-nocheck
// @ts-nocheck
import * as nanoid_1 from "nanoid";
import * as stage1Contracts_1 from "../types/stage1Contracts";
import * as config_1 from "../config";
@@ -41,6 +41,30 @@ function extractFragments(normalized) {
const source = normalized;
return Array.isArray(source.fragments) ? source.fragments : [];
}
function hasExplicitPeriodAnchorFromNormalized(normalized) {
const fragments = extractFragments(normalized);
const explicitPeriodPattern = /(?:\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;
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item;
const timeScope = fragment.time_scope && typeof fragment.time_scope === "object" ? fragment.time_scope : null;
if (timeScope) {
const type = String(timeScope.type ?? "").trim().toLowerCase();
const value = String(timeScope.value ?? "").trim();
const confidence = String(timeScope.confidence ?? "").trim().toLowerCase();
if ((type === "explicit" || type === "range") && value.length > 0 && confidence !== "low") {
return true;
}
}
const rawText = `${typeof fragment.raw_fragment_text === "string" ? fragment.raw_fragment_text : ""} ${typeof fragment.normalized_fragment_text === "string" ? fragment.normalized_fragment_text : ""}`;
if (explicitPeriodPattern.test(rawText)) {
return true;
}
}
return false;
}
function extractExecutionState(normalized) {
const fragments = extractFragments(normalized);
return fragments.map((item) => {
@@ -205,7 +229,7 @@ function extractAccountTokens(text) {
return Array.from(explicitAccounts);
}
const spans = collectDateSpans(lower);
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment|СЃСРµС|СЃС\.?)/iu.test(lower);
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
if (!hasAccountingLexeme) {
return [];
}
@@ -394,9 +418,9 @@ function buildSkippedResult(item) {
why_included: [],
selection_reason: [mapNoRouteReason(item.no_route_reason)],
risk_factors: [],
business_interpretation: ["Данный фрагмент не был выполнен из-за no-route решения."],
business_interpretation: ["Данный фрагмент не был выполнен из-за no-route решения."],
confidence: "low",
limitations: ["Фрагмент требует уточнения или отсутствует поддерживаемый маршрут."],
limitations: ["Фрагмент требует уточнения или отсутствует поддерживаемый маршрут."],
errors: []
});
}
@@ -643,28 +667,28 @@ function checkGrounding(userMessage, requirements, coverage, retrievalResults) {
const reasons = [];
if (!routeSubjectMatch) {
status = "route_mismatch_blocked";
reasons.push(`Не подтверждены критичные предметные токены запроса: ${missingCriticalTokens.join(", ")}`);
reasons.push(`Не подтверждены критичные предметные токены запроса: ${missingCriticalTokens.join(", ")}`);
}
else if (accountOnlyMismatchRecoverable) {
status = "partial";
reasons.push(`Рчет-токены не подтверждены напрямую (${missingCriticalTokens.join(", ")}), но есть релевантная опора для ограниченного вывода.`);
reasons.push(`Счет-токены не подтверждены напрямую (${missingCriticalTokens.join(", ")}), но есть релевантная опора для ограниченного вывода.`);
}
else if (coverage.requirements_covered === 0) {
status = "no_grounded_answer";
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
}
else if (coverage.requirements_uncovered.length > 0 ||
coverage.requirements_partially_covered.length > 0 ||
coverage.clarification_needed_for.length > 0 ||
coverage.out_of_scope_requirements.length > 0) {
status = "partial";
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
}
if (whyIncludedSummary.length === 0) {
reasons.push("Нет explainable-сигналов why_included в результатах выборки.");
reasons.push("Нет explainable-сигналов why_included в результатах выборки.");
}
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
reasons.push(`Часть контекстных токенов не подтверждена напрямую: ${missingSubjectTokens.join(", ")}`);
reasons.push(`Часть контекстных токенов не подтверждена напрямую: ${missingSubjectTokens.join(", ")}`);
}
const missingRequirements = [
...coverage.requirements_uncovered,
@@ -727,10 +751,10 @@ function buildAnswerStructureV11(input) {
})), 8);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const limitations = summarizeUnique([...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons], 8);
const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`);
const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`);
const recommendedActions = summarizeUnique([
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`)
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`)
], 6);
const mechanismStatus = mechanismNotes.length === 0
? "unresolved"
@@ -773,7 +797,8 @@ const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "
const FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP = {
settlements_60_62: "hybrid_store_plus_live",
vat_document_register_book: "hybrid_store_plus_live",
month_close_costs_20_44: "hybrid_store_plus_live"
month_close_costs_20_44: "hybrid_store_plus_live",
fixed_asset_amortization: "hybrid_store_plus_live"
};
const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320;
const FOLLOWUP_SUBJECT_MAX = 160;
@@ -786,17 +811,17 @@ function hasAccountingSignal(text) {
if (/(?:^|[\s,;:])\d{2}(?:\.\d{2})?(?=$|[\s,.;:])/i.test(lower)) {
return true;
}
return /(РїСЂРѕРІРѕРґРє|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|РЅРґСЃ|амортиз|СЂР±Рї|РѕСЃ|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|счёт|ндс|амортиз|рбп|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|закрыти|период|postavshchik|kontragent|schet|schetu|period|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(lower);
return /(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|счёт|ндс|амортиз|рбп|ос|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|закрыти|период|postavshchik|kontragent|schet|schetu|period|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(lower);
}
function hasFollowupMarker(text) {
const compact = compactWhitespace(text.toLowerCase());
return /^(Рё|Р° еще|Р° ещё|еще|ещё|добав|уточн|продолж|также|и|а если|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|plus|also|dobav|utochn|prodolzh)/i.test(compact);
return /^(и|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|plus|also|dobav|utochn|prodolzh)/i.test(compact);
}
function hasReferentialPointer(text) {
return /(РїРѕ этому|РїРѕ тому|это Р¶Рµ|этой|этим|тому|по этому|по тому|это же|этой|этим|этому|из этого|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
return /(по этому|по тому|это же|этой|этим|этому|из этого|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
}
function hasSmallTalkSignal(text) {
return /(привет|как дела|спасибо|привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase());
return /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase());
}
function countTokens(text) {
return compactWhitespace(text)
@@ -840,12 +865,17 @@ function inferP0DomainFromMessage(text) {
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
const vatLexical = /(?:ндс|vat|счет[\s-]?фактур|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
const hasFixedAssetAccount = accountTokens.some((token) => /^(?:01|02|08)(?:\.|$)/.test(token));
const vatLexical = /(?:ндс|vat|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых результат)/i.test(lower);
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат)/i.test(lower);
const fixedAssetLexical = /(?:основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(lower);
if (hasVatAccount || vatLexical) {
return "vat_document_register_book";
}
if (fixedAssetLexical || hasFixedAssetAccount) {
return "fixed_asset_amortization";
}
if (monthCloseLexical || hasMonthCloseAccount) {
return "month_close_costs_20_44";
}
@@ -1018,12 +1048,12 @@ function buildFollowupStateBinding(input) {
const shouldAugmentQuestion = Boolean(subject) && (followupMarker || referentialPointer || !strongSignal);
let normalizedQuestion = userMessage;
if (shouldAugmentQuestion) {
const appendParts = [`Фокус текущего разбора: ${subject}`];
const appendParts = [`Фокус текущего разбора: ${subject}`];
if (input.investigationState.focus.primary_accounts.length > 0 && !/\b\d{2}(?:\.\d{2})?\b/.test(userMessage)) {
appendParts.push(`Счета фокуса: ${input.investigationState.focus.primary_accounts.join(", ")}`);
appendParts.push(`Счета фокуса: ${input.investigationState.focus.primary_accounts.join(", ")}`);
}
if (periodHintFromState && !hasPeriodLiteral(userMessage)) {
appendParts.push(`Период фокуса: ${periodHintFromState}`);
appendParts.push(`Период фокуса: ${periodHintFromState}`);
}
const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX);
normalizedQuestion = `${userMessage}\n${appendBlock}`.trim();
@@ -1187,6 +1217,9 @@ export class AssistantService {
: null;
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
const composition = (0, answerComposer_1.composeAssistantAnswer)({
userMessage,
routeSummary: normalized.route_hint_summary,
@@ -1197,15 +1230,21 @@ export class AssistantService {
focusDomainHint,
questionTypeHint: questionTypeClass,
companyAnchors,
normalizationPeriodExplicit,
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
});
const safeAssistantReplyBase = (0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(composition.assistant_reply);
const safeAssistantReply = String(safeAssistantReplyBase ?? "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.trim();
const answerStructureV11 = config_1.FEATURE_ASSISTANT_CONTRACTS_V11
? config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11
? composition.answer_structure_v11
: buildAnswerStructureV11({
assistantReply: composition.assistant_reply,
assistantReply: safeAssistantReply,
coverageReport: coverageEvaluation.coverage,
groundingCheck,
retrievalResults
@@ -1266,7 +1305,7 @@ export class AssistantService {
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
session_id: sessionId,
role: "assistant",
text: composition.assistant_reply,
text: safeAssistantReply,
reply_type: composition.reply_type,
created_at: new Date().toISOString(),
trace_id: normalized.trace_id,
@@ -1326,7 +1365,7 @@ export class AssistantService {
answer_structure_v11: answerStructureV11,
investigation_state_snapshot: investigationStateSnapshot,
fallback_type: composition.fallback_type,
assistant_reply: composition.assistant_reply,
assistant_reply: safeAssistantReply,
reply_type: composition.reply_type,
trace_id: normalized.trace_id
}
@@ -1334,7 +1373,7 @@ export class AssistantService {
return {
ok: true,
session_id: sessionId,
assistant_reply: composition.assistant_reply,
assistant_reply: safeAssistantReply,
reply_type: composition.reply_type,
conversation_item: assistantItem,
debug,
@@ -1343,3 +1382,4 @@ export class AssistantService {
}
}
@@ -1,4 +1,4 @@
import type {
import type {
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
@@ -146,6 +146,11 @@ function isVatAccount(value: string): boolean {
return prefix === "19" || prefix === "68";
}
function isFixedAssetAccount(value: string): boolean {
const prefix = normalizeAccountPrefix(value);
return prefix === "01" || prefix === "02" || prefix === "08";
}
function isCloseCostsAccount(value: string): boolean {
const prefix = normalizeAccountPrefix(value);
if (!prefix) {
@@ -161,11 +166,26 @@ function inferFollowupActiveDomain(input: {
routeSummary: RouteHintSummary | null;
previous: InvestigationStateWithProblemUnits;
}): string | null {
const corpus = `${input.userMessage} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
const contextualCorpus = `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
const hasFixedAssetLexicalSignal =
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
messageCorpus
);
const hasFixedAssetAccountSignal =
input.focusAccounts.some((item) => isFixedAssetAccount(item)) &&
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(
messageCorpus
);
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
return "fixed_asset_amortization";
}
const hasSettlementSignal =
input.focusAccounts.some((item) => isSettlementAccount(item)) ||
/(60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расчет|расч[её]т|зачет|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
corpus
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
messageCorpus
);
if (hasSettlementSignal) {
return "settlements_60_62";
@@ -173,18 +193,26 @@ function inferFollowupActiveDomain(input: {
const hasVatSignal =
input.focusAccounts.some((item) => isVatAccount(item)) ||
/(ндс|счет[\s-]?фактур|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(corpus);
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
if (hasVatSignal) {
return "vat_document_register_book";
}
const hasCloseSignal =
input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
/(закрыти|закрытие|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(corpus);
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus);
if (hasCloseSignal) {
return "month_close_costs_20_44";
}
if (
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
(input.previous.followup_context?.active_domain === "settlements_60_62" ||
input.previous.focus.domain === "settlements_60_62")
) {
return "settlements_60_62";
}
const routeDomain = deriveDomain(input.routeSummary);
if (routeDomain && routeDomain !== "no_route") {
return routeDomain;
@@ -450,7 +478,7 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
const activeDomain = inferFollowupActiveDomain({
userMessage: input.userMessage,
focusAccounts: mergedFocusAccounts,
focusAccounts: focusFromMessage,
routeSummary: input.routeSummary,
previous
});
@@ -498,3 +526,4 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
: {})
};
}