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

This commit is contained in:
dctouch 2026-03-28 21:11:11 +03:00
parent 6123fafd5b
commit 7eb1410501
35 changed files with 4120 additions and 155 deletions

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeAssistantReplyForUserFacing = sanitizeAssistantReplyForUserFacing;
exports.composeAssistantAnswer = composeAssistantAnswer;
function fallbackFromSummary(routeSummary) {
if (!routeSummary || routeSummary.mode !== "deterministic_v2") {
@ -207,6 +208,10 @@ const HUMAN_SIGNAL_MAP = {
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: "Ожидаемая цепочка списания РБП выглядит незавершенной.",
@ -576,8 +581,13 @@ function stripSyntheticPlaceholders(value) {
.trim();
}
function sanitizeUserFacingReply(value) {
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
@ -1174,7 +1184,7 @@ function buildProblemCentricActions(input) {
}
}
if (input.missingAnchors.period && input.mode !== "clarification_required") {
actions.push("Уточните период проверки (например, 2020-06), чтобы подтвердить незавершенное списание без лишнего шума.");
actions.push("Уточните период проверки (например, июль 2020), чтобы подтвердить незавершенное списание без лишнего шума.");
}
if (input.mode === "clarification_required") {
if (input.missingAnchors.period) {
@ -1202,7 +1212,7 @@ function buildProblemCentricClarifications(input) {
const questions = [];
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), РіРґРµ РІС РѕР¶РёРґР°РµС‚Рµ дефект.");
@ -1338,6 +1348,14 @@ function asRecordObject(value) {
return value;
}
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) {
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) {
for (const result of results) {
const summary = asRecordObject(result.summary);
@ -1378,9 +1396,12 @@ function hasAccountAnchorInRetrieval(results) {
}
return false;
}
function detectMissingAnchors(userMessage, retrievalResults = []) {
function detectMissingAnchors(userMessage, retrievalResults = [], options) {
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) || hasAccountAnchorInRetrieval(retrievalResults);
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
@ -1400,7 +1421,7 @@ function buildClarificationQuestions(input) {
return questions;
}
if (input.missingAnchors.period) {
questions.push("Уточните период проверки (например, 2020-06).");
questions.push("Уточните период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
@ -1798,8 +1819,8 @@ function inferP0NarrativeDomain(units) {
return "vat_document_register_book";
}
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";
}
return null;
@ -1842,8 +1863,7 @@ function p0NarrativeDomainFromHint(value) {
}
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";
}
return null;
@ -2014,7 +2034,21 @@ function evaluateP0DomainEvidenceGrounding(results, focusDomain) {
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,
has_foreign_primary: hasForeignPrimary,
@ -2038,21 +2072,35 @@ function hasStrongNarrativeDomainSignalInText(userMessage, domain) {
}
if (domain === "month_close_costs_20_44") {
return (accountTokens.some((item) => isCloseCostsAccountToken(item)) ||
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text));
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text));
}
return false;
}
function hasFixedAssetAmortizationSignalInText(userMessage) {
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) {
const text = String(userMessage ?? "").toLowerCase();
return /(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text);
}
function inferP0FocusNarrativeDomain(userMessage, results, units, focusDomainHint) {
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
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;
}
@ -2333,6 +2381,7 @@ function buildProblemCentricAnswerStructure(input) {
], 10);
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
@ -2415,6 +2464,8 @@ function limitationReasonToUserText(code) {
function inferNarrativeDomainFromText(value) {
const text = String(value ?? "").toLowerCase();
const accountTokens = extractAccountNumbersFromNarrativeText(text);
const fixedAssetSignal = hasFixedAssetAmortizationSignalInText(text);
const explicitMonthCloseSignal = hasExplicitMonthCloseSignalInText(text);
let settlementScore = 0;
let vatScore = 0;
let monthCloseScore = 0;
@ -2436,9 +2487,12 @@ function inferNarrativeDomainFromText(value) {
if (/(ндс|vat|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|книг[аи]|регистр|вычет|налогов(?:ый|ого)?\s+эффект)/i.test(text)) {
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;
@ -2487,9 +2541,42 @@ function buildShortSectionLine(structure) {
}
return incomplete ? "Проблема подтверждается частично на текущей опоре." : "Проблема подтверждена на текущей опоре.";
}
function humanizeCompositeDirectAnswer(value) {
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) => 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 = [...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) {
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)];
@ -2501,17 +2588,37 @@ function buildBrokenSectionLines(structure) {
}
return ["Есть признаки нарушения в связанной цепочке документов и проводок."];
}
function buildWhySectionLines(structure) {
function buildWhySectionLines(structure, context) {
const noteLines = dedupeNarrativeLines(structure.mechanism_block.mechanism_notes
.map((item) => sanitizeSupportLine(item))
.filter((item) => Boolean(item))
.map((item) => mapDefectTokenToNarrative(item) ?? humanizeTechnicalToken(item) ?? item), 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 = [...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("Сигнал проблемы есть, но механизм подтвержден не полностью.");
@ -2554,7 +2661,7 @@ function buildCoverageSplitLines(structure, questionType = "unknown") {
}
return dedupeNarrativeLines(lines, 3);
}
function buildEvidenceSectionLines(structure, questionType = "unknown") {
function buildEvidenceSectionLines(structure, questionType = "unknown", context) {
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;
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
@ -2566,15 +2673,43 @@ function buildEvidenceSectionLines(structure, questionType = "unknown") {
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
const lines = [];
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") {
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") {
if (domain === "vat_document_register_book") {
lines.push("Опора собрана по звеньям НДС-цепочки, чтобы разделить полные и неполные переходы.");
}
else if (rbpSignal) {
lines.push("Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.");
}
else if (fixedAssetSignal) {
lines.push("Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.");
}
else {
lines.push("Опора собрана так, чтобы разделить цепочки на полные и неполные.");
}
}
if (evidenceCount > 0) {
lines.push(`Вывод опирается на ${evidenceCount} подтвержденных наблюдений в текущем срезе.`);
}
@ -2584,11 +2719,25 @@ function buildEvidenceSectionLines(structure, questionType = "unknown") {
if (claimLinks > 0) {
lines.push("Есть связка между основным выводом и подтверждающими записями.");
}
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
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(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
lines.push("Опора есть, но достаточна только для предварительного вывода.");
}
}
else if (evidenceCount > 0) {
lines.push("Опора достаточна для первичного вывода.");
}
if (lines.length === 0) {
lines.push("Использована доступная выборка документов и проводок в текущем snapshot.");
@ -2616,6 +2765,123 @@ function buildDefaultChecksByDomain(domain) {
}
return ["Проверьте связку документов и проводок по проблемному участку в указанном периоде."];
}
function hasFixedAssetAnchorContext(context) {
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) {
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) {
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) {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used, context.userMessage ?? ""].join(" ");
return hasRbpAnchorContext(context) || hasRbpSignalInText(corpus);
}
function hasRbpSignalInText(value) {
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, context) {
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) {
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) {
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, domain) {
if (questionType === "what_to_check_first") {
if (domain === "settlements_60_62") {
@ -2734,7 +3000,18 @@ function buildChecksSectionLines(structure, context) {
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(broken);
const questionType = context?.questionType ?? "unknown";
const domainFallback = buildQuestionTypeDomainChecks(questionType, domain);
const effectiveQuestionType = 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 ?? "")));
const lines = [];
if (questionType === "what_to_check_first") {
@ -2764,18 +3041,21 @@ function buildChecksSectionLines(structure, context) {
}
}
}
const filteredLines = fixedAssetCase || rbpCase
? lines.filter((item) => !/проверьте связку документов и проводок по проблемному участку/i.test(item))
: lines;
if (hasMissingPeriod) {
if (questionType === "what_to_check_first") {
lines.push("Уточните период, если он не зафиксирован в исходной формулировке вопроса.");
filteredLines.push("Уточните период, если он не зафиксирован в исходной формулировке вопроса.");
}
else if (domain === "settlements_60_62" && lines.length > 0) {
lines.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) {
const raw = String(value ?? "").trim();
@ -2877,6 +3157,15 @@ function buildQuestionTypeShortLine(context) {
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") {
@ -2895,8 +3184,14 @@ function buildQuestionTypeShortLine(context) {
if (context.focusDomain === "month_close_costs_20_44") {
return "Наиболее вероятная причина: цепочка распределения затрат и закрытия месяца подтверждена не полностью.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Наиболее вероятная причина: по ОС часть переходов от параметров амортизации к начислению подтверждена не полностью.";
}
return "Наиболее вероятный механизм проблемы подтвержден частично и требует первичной проверки.";
}
if (context.questionType === "unknown" && hasFixedAssetAnchorContext(context)) {
return "Риск неполного начисления амортизации подтвержден частично и требует проверки по объектам ОС.";
}
return null;
}
function buildQuestionTypeBrokenLine(context) {
@ -2925,18 +3220,45 @@ function buildQuestionTypeWhyLine(context) {
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;
}
function buildQuestionTypeEvidenceLine(context) {
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;
@ -2958,9 +3280,27 @@ function buildQuestionTypeCheckLine(context) {
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;
@ -3001,15 +3341,101 @@ function applyQuestionTypeAndAnchorPolicy(input) {
limitationLines: nextLimitations
};
}
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) {
return RBP_WORDING_PATTERN.test(String(value ?? ""));
}
function hasFaWordingPhrase(value) {
return FA_WORDING_PATTERN.test(String(value ?? ""));
}
function resolveDomainWordingMode(structure, context) {
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, structure, context) {
const mode = resolveDomainWordingMode(structure, context);
if (mode === "neutral" || !context) {
return payload;
}
const effectiveQuestionType = context.questionType === "unknown" ? "what_to_check_first" : context.questionType;
const isForbidden = mode === "rbp" ? hasFaWordingPhrase : hasRbpWordingPhrase;
const filterLines = (lines) => 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, context) {
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,
@ -3027,6 +3453,7 @@ function renderPolicyReply(structure, context) {
checkLines,
limitationLines
};
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
return sanitizeUserFacingReply([
`Коротко: ${enriched.shortLine}`,
`Что сломано:\n${formatList(enriched.brokenLines)}`,
@ -3117,7 +3544,10 @@ function composeAssistantAnswerV11(input) {
reply_type: "clarification_required"
}
: 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 ||
limitationReasonCodes.includes("missing_mechanism") ||
@ -3158,7 +3588,8 @@ function composeAssistantAnswerV11(input) {
assistant_reply: renderPolicyReply(problemCentricStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage
anchors: anchorUsage,
userMessage: input.userMessage
}),
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
@ -3262,7 +3693,8 @@ function composeAssistantAnswerV11(input) {
assistant_reply: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage
anchors: anchorUsage,
userMessage: input.userMessage
}),
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
@ -3309,6 +3741,9 @@ function composeExplainableAnswer(input, scopeLabel) {
.filter(Boolean)
.join("\n\n"));
}
function sanitizeAssistantReplyForUserFacing(value) {
return sanitizeUserFacingReply(value);
}
function composeAssistantAnswer(input) {
if (input.enableAnswerPolicyV11) {
return composeAssistantAnswerV11(input);

View File

@ -1242,11 +1242,17 @@ function cardResolutionScore(card, fragmentText, profile) {
return 0;
}
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, profile) {
const text = String(fragmentText ?? "");
@ -1256,6 +1262,19 @@ function hasStrongVatDomainSignal(fragmentText, profile) {
profile.domain_scope.some((domain) => domain === "vat" || domain === "taxes") ||
profile.relation_patterns.some((pattern) => ["invoice_to_vat", "register_to_book", "book_entry_generated", "deduction_posted"].includes(pattern)));
}
function hasStrongMonthCloseSignal(fragmentText, profile) {
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, profile) {
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) {
return profile.account_scope.some((account) => account === "51" || account === "60" || account === "62" || account === "76");
}
@ -1302,6 +1321,19 @@ function hasSettlementRecoverySignal(signals) {
const hasSettlementDocument = signals.document_types.some((item) => ["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document"].includes(item));
return hasSettlementAccount || hasSettlementDomain || hasSettlementRelation || hasSettlementDocument;
}
function isVatAllowedAccountContext(account) {
const normalized = String(account ?? "").trim();
return normalized === "19" || normalized === "68";
}
function isVatAllowedDocumentContext(documentType) {
return /(?:invoice|vat_document|purchase_book|sales_book|tax_entry|supplier_receipt|sales_document|register)/i.test(String(documentType ?? ""));
}
function isVatAllowedRelationPattern(pattern) {
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) {
return /(?:vat_flow)/i.test(String(domain ?? ""));
}
function collectSourceRecords(data, sources) {
const items = [];
for (const source of sources) {
@ -2423,6 +2455,9 @@ 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);
}
@ -2432,6 +2467,9 @@ class AssistantDataLayer {
!["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document", "manual_operation"].includes(item)) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedDocumentContext(item)) {
continue;
}
group.document_context.add(item);
}
for (const item of evaluation.signals.relation_patterns) {
@ -2439,6 +2477,9 @@ class AssistantDataLayer {
!["payment_to_settlement", "statement_to_document", "contract_to_documents", "document_to_posting"].includes(item)) {
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) {
@ -2457,6 +2498,9 @@ class AssistantDataLayer {
!["bank_settlement", "customer_settlement"].includes(domain)) {
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)) {
@ -2471,15 +2515,23 @@ class AssistantDataLayer {
const unknownLinks = Number(record.unknown_link_count ?? 0);
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,

View File

@ -79,6 +79,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) => {
@ -243,7 +267,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 [];
}
@ -432,9 +456,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: []
});
}
@ -681,28 +705,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,
@ -765,10 +789,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"
@ -811,7 +835,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;
@ -824,17 +849,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)
@ -878,12 +903,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";
}
@ -1056,12 +1086,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();
@ -1225,6 +1255,9 @@ 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,
@ -1235,15 +1268,21 @@ 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
@ -1304,7 +1343,7 @@ 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,
@ -1364,7 +1403,7 @@ 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
}
@ -1372,7 +1411,7 @@ 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,

View File

@ -91,6 +91,10 @@ function isVatAccount(value) {
const prefix = normalizeAccountPrefix(value);
return prefix === "19" || prefix === "68";
}
function isFixedAssetAccount(value) {
const prefix = normalizeAccountPrefix(value);
return prefix === "01" || prefix === "02" || prefix === "08";
}
function isCloseCostsAccount(value) {
const prefix = normalizeAccountPrefix(value);
if (!prefix) {
@ -100,22 +104,34 @@ function isCloseCostsAccount(value) {
return (account >= 20 && account <= 44) || prefix === "97";
}
function inferFollowupActiveDomain(input) {
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";
}
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;
@ -321,7 +337,7 @@ function updateInvestigationState(input) {
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
const activeDomain = inferFollowupActiveDomain({
userMessage: input.userMessage,
focusAccounts: mergedFocusAccounts,
focusAccounts: focusFromMessage,
routeSummary: input.routeSummary,
previous
});

View File

@ -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,14 +3198,39 @@ 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") {
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") {
if (domain === "vat_document_register_book") {
lines.push("Опора собрана по звеньям НДС-цепочки, чтобы разделить полные и неполные переходы.");
} else if (rbpSignal) {
lines.push("Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.");
} else if (fixedAssetSignal) {
lines.push("Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.");
} else {
lines.push("Опора собрана так, чтобы разделить цепочки на полные и неполные.");
}
}
if (evidenceCount > 0) {
lines.push(`Вывод опирается на ${evidenceCount} подтвержденных наблюдений в текущем срезе.`);
@ -3076,10 +3241,20 @@ function buildEvidenceSectionLines(
if (claimLinks > 0) {
lines.push("Есть связка между основным выводом и подтверждающими записями.");
}
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
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(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
lines.push("Опора есть, но достаточна только для предварительного вывода.");
}
} else if (evidenceCount > 0) {
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);

View File

@ -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,

View File

@ -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 {
}
}

View File

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

View File

@ -147,7 +147,7 @@ describe("assistant mode API", () => {
});
expect(response.status).toBe(200);
expect(["partial_coverage", "route_mismatch_blocked", "factual_with_explanation"]).toContain(
expect(["partial_coverage", "clarification_required", "route_mismatch_blocked", "factual_with_explanation"]).toContain(
String(response.body.reply_type)
);
expect(["partial", "grounded", "route_mismatch_blocked"]).toContain(

View File

@ -194,6 +194,43 @@ describe.sequential("assistant follow-up state binding", () => {
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
});
it("rebinds follow-up domain away from settlements on fixed-asset amortization query", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const sessionId = `asst-wave16-fa-domain-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Почему деньги ушли, а долг по 60.01/62.02 остался?"
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.followup_context?.active_domain).toBe("settlements_60_62");
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message:
"Полно ли начислена амортизация по объектам ОС за июль? Проверь по 01/02, нет ли пропущенных объектов."
});
expect(second.status).toBe(200);
const activeDomain = String(second.body.debug?.investigation_state_snapshot?.followup_context?.active_domain ?? "");
expect(activeDomain).not.toBe("settlements_60_62");
expect(activeDomain).toMatch(/fixed_asset_amortization|month_close_costs_20_44|no_route|hybrid_store_plus_live|fixed_asset/i);
const settlementActions = second.body.debug?.investigation_state_snapshot?.followup_context?.settlement_next_actions;
expect(Array.isArray(settlementActions) ? settlementActions.length : 0).toBe(0);
});
it("keeps UTF-8 follow-up period refinement in-scope with soft continuity hints", async () => {
const app = await createAppWithFlags({
state: "1",

View File

@ -0,0 +1,546 @@
import { describe, expect, it } from "vitest";
import { composeAssistantAnswer } from "../src/services/answerComposer";
import { resolveCompanyAnchors } from "../src/services/companyAnchorResolver";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
import type { ProblemUnit } from "../src/types/stage2ProblemUnits";
function buildRouteSummary() {
return {
mode: "deterministic_v2" as const,
message_in_scope: true,
scope_confidence: "high" as const,
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none" as const,
message: null
}
};
}
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
return {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: [],
...input
};
}
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
return {
status: "partial",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["wave16-live"],
selection_reason_summary: ["wave16-live"],
...input
};
}
function buildProblemUnit(input?: Partial<ProblemUnit>): ProblemUnit {
return {
schema_version: "problem_unit_v0_1",
problem_unit_id: input?.problem_unit_id ?? "pu-live-1",
problem_unit_type: input?.problem_unit_type ?? "cross_branch_inconsistency_cluster",
title: input?.title ?? "Live corrective test unit",
mechanism_summary: input?.mechanism_summary ?? "Mechanism candidate: invoice_to_vat.",
business_defect_class: input?.business_defect_class ?? "invoice_to_vat",
severity: input?.severity ?? {
score: 0.61,
grade: "medium"
},
confidence: input?.confidence ?? {
score: 0.58,
grade: "medium"
},
lifecycle_domain: input?.lifecycle_domain ?? "vat_flow",
affected_entities: input?.affected_entities ?? ["Document:DOC-1"],
affected_documents: input?.affected_documents ?? ["Document:DOC-1"],
affected_postings: input?.affected_postings ?? ["Posting:POST-1"],
affected_accounts: input?.affected_accounts ?? ["19"],
affected_counterparties: input?.affected_counterparties ?? ["Counterparty:CP-1"],
affected_contracts: input?.affected_contracts ?? ["Contract:CTR-1"],
failed_expected_edge: input?.failed_expected_edge ?? "invoice_to_vat",
period_impact: input?.period_impact ?? {
is_period_sensitive: true,
impact_class: "close_risk"
},
evidence_pack: input?.evidence_pack ?? ["ev-1"],
entity_backlinks: input?.entity_backlinks ?? [{ entity: "Document", id: "DOC-1" }],
snapshot_limitations: input?.snapshot_limitations ?? []
};
}
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "chain",
items: [
{
source_entity: "Document",
source_id: "DOC-1",
display_name: "Документ",
account_context: ["19"],
document_context: ["invoice", "vat_document"],
relation_pattern_hits: ["invoice_to_vat", "document_to_posting"],
graph_domain_scope: ["vat_flow"],
period: "2020-07"
}
],
summary: {
semantic_profile: {
account_scope: ["19"],
domain_scope: ["vat", "taxes"],
relation_patterns: ["invoice_to_vat", "document_to_posting"],
period_scope: {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
}
},
domain_purity_guard: {
domain_card_id: "vat_document_register_book"
},
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020_07",
entity: "document",
id: "DOC-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020_07|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020_07",
entity: "document",
id: "DOC-1",
period: "2020-07"
},
locator: {
field_path: "risk_score",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "invoice_to_vat",
confidence: "medium",
limitation: null,
payload: {
value: 1
}
}
],
candidate_evidence: [],
problem_units: [buildProblemUnit()],
problem_unit_summary: {
schema_version: "problem_unit_summary_v0_1",
units_total: 1,
duplicate_collapses: 0,
unit_types: ["cross_branch_inconsistency_cluster"],
type_distribution: {
cross_branch_inconsistency_cluster: 1
},
severity_distribution: {
low: 0,
medium: 1,
high: 0
},
confidence_distribution: {
low: 0,
medium: 1,
high: 0
},
primary_unit_type: "cross_branch_inconsistency_cluster"
},
why_included: ["wave16-live"],
selection_reason: ["wave16-live"],
risk_factors: ["cross_branch_inconsistency"],
business_interpretation: ["wave16-live"],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("wave16 live corrective pass regressions", () => {
it("removes leaked debug payload scaffolding from user-facing reply", () => {
const output = composeAssistantAnswer({
userMessage: "Проверь НДС цепочку в июле.",
routeSummary: buildRouteSummary(),
retrievalResults: [
buildRetrieval({
selection_reason: [
"### debug_payload_json\n```json\n{\"trace_id\":\"abc\",\"route_summary\":{\"mode\":\"x\"}}\n```"
]
})
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка НДС",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage(),
groundingCheck: buildGrounding({ status: "grounded" }),
focusDomainHint: "vat_document_register_book",
questionTypeHint: "why_breaks",
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.assistant_reply).not.toMatch(/debug_payload_json|technical_breakdown_json|trace_id|route_summary/i);
});
it("does not claim missing period when normalization already extracted explicit period", () => {
const output = composeAssistantAnswer({
userMessage: "Рошибка кодировки", // emulates noisy text from live channel
routeSummary: buildRouteSummary(),
retrievalResults: [
buildRetrieval({
summary: {
semantic_profile: {
account_scope: ["19"],
domain_scope: ["vat", "taxes"],
relation_patterns: ["invoice_to_vat"]
},
domain_purity_guard: {
domain_card_id: "vat_document_register_book"
},
broad_query_detected: true,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "weak"
}
})
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка периода",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage({
requirements_covered: 0,
requirements_partially_covered: ["R1"],
clarification_needed_for: ["R1"]
}),
groundingCheck: buildGrounding({
status: "partial",
reasons: ["Mechanism is unresolved for part of the evidence."]
}),
focusDomainHint: "vat_document_register_book",
questionTypeHint: "which_chains_are_complete_vs_incomplete",
normalizationPeriodExplicit: true,
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.assistant_reply).not.toMatch(/период в запросе не указан/i);
expect(output.assistant_reply).not.toMatch(/уточните период проверки/i);
});
it("blocks VAT primary synthesis when top evidence is cross-domain polluted", () => {
const polluted = buildRetrieval({
items: [
{
source_entity: "Document",
source_id: "DOC-1",
display_name: "Документ",
account_context: ["25", "20", "19"],
document_context: ["invoice", "vat_document", "deferred_expense_document"],
relation_pattern_hits: ["invoice_to_vat", "deferred_expense_to_writeoff"],
graph_domain_scope: ["vat_flow", "deferred_expense", "period_close", "bank_settlement", "fixed_asset"],
period: "2020-07"
}
],
summary: {
semantic_profile: {
account_scope: ["19"],
domain_scope: ["vat", "taxes"],
relation_patterns: ["invoice_to_vat", "deferred_expense_to_writeoff"],
period_scope: {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
}
},
domain_purity_guard: {
domain_card_id: "vat_document_register_book"
},
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
}
});
const output = composeAssistantAnswer({
userMessage: "Проверь НДС-цепочку: документ -> счет-фактура -> регистр -> книга.",
routeSummary: buildRouteSummary(),
retrievalResults: [polluted],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка НДС",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage(),
groundingCheck: buildGrounding({ status: "grounded" }),
focusDomainHint: "vat_document_register_book",
questionTypeHint: "why_breaks",
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.reply_type).toBe("clarification_required");
expect(output.answer_structure_v11?.uncertainty_block.open_uncertainties).toContain("primary_domain_evidence_not_confirmed");
});
it("uses VAT-specific partial-coverage wording instead of generic chain template", () => {
const output = composeAssistantAnswer({
userMessage:
"13 июля поступление, 15 июля реализация. НДС-цепочка по этим движениям полная или есть выпадение?",
routeSummary: buildRouteSummary(),
retrievalResults: [
buildRetrieval({
summary: {
semantic_profile: {
account_scope: ["19", "68"],
domain_scope: ["vat", "taxes"],
relation_patterns: ["invoice_to_vat", "document_to_posting"],
period_scope: {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
}
},
domain_purity_guard: {
domain_card_id: "vat_document_register_book"
},
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
}
})
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка полноты НДС-цепочки",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage({ requirements_covered: 0, requirements_partially_covered: ["R1"] }),
groundingCheck: buildGrounding({ status: "partial" }),
focusDomainHint: "vat_document_register_book",
questionTypeHint: "which_chains_are_complete_vs_incomplete",
normalizationPeriodExplicit: true,
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.assistant_reply).toMatch(/НДС-цепочк|НДС-звеньям|документ -> счет-фактура -> регистр -> книга/i);
expect(output.assistant_reply).not.toMatch(/ключевой переход закрытия/i);
});
it("renders RBP answer in RBP language with RBP-first checks", () => {
const output = composeAssistantAnswer({
userMessage:
"31 июля прошло Списание РБП за июль. Есть ли признаки, что часть РБП к концу июля живет дольше ожидаемого?",
routeSummary: buildRouteSummary(),
retrievalResults: [
buildRetrieval({
items: [
{
source_entity: "Document",
source_id: "DOC-RBP-1",
display_name: "Списание РБП",
account_context: ["97"],
document_context: ["deferred_expense_document"],
relation_pattern_hits: ["deferred_expense_to_writeoff", "document_to_posting", "asset_card_to_depreciation"],
graph_domain_scope: ["deferred_expense", "period_close", "fixed_asset"],
period: "2020-07"
}
],
summary: {
semantic_profile: {
account_scope: ["97", "01"],
domain_scope: ["deferred_expense", "period_close"],
relation_patterns: ["deferred_expense_to_writeoff", "document_to_posting", "asset_card_to_depreciation"],
period_scope: {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
}
},
domain_purity_guard: {
domain_card_id: "month_close_costs_20_44"
},
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
}
})
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка списания РБП",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage({ requirements_covered: 0, requirements_partially_covered: ["R1"] }),
groundingCheck: buildGrounding({ status: "partial" }),
questionTypeHint: "what_is_it_grounded_on",
companyAnchors: resolveCompanyAnchors(
"31 июля прошло Списание РБП за июль. Есть ли признаки, что часть РБП к концу июля живет дольше ожидаемого?"
),
normalizationPeriodExplicit: true,
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.assistant_reply).toMatch(/РБП|списани[ея]\s+РБП|счет\s*97/i);
expect(output.assistant_reply).toMatch(/документ списания|остаток/i);
expect(output.assistant_reply).not.toMatch(/амортиз|объект\w*\s+ОС|01\/02|сч[её]т\s*0[12]/i);
expect(output.assistant_reply).not.toMatch(/отдельн\w*\s+проверк\w*\s+расчетн\w*\s+связк/i);
});
it("does not collapse fixed-asset amortization question into month-close primary narrative", () => {
const unit = buildProblemUnit({
problem_unit_id: "pu-fa-1",
problem_unit_type: "lifecycle_anomaly_node",
lifecycle_domain: "fixed_asset",
affected_accounts: ["01", "02"],
mechanism_summary: "Mechanism candidate: asset_card_to_depreciation.",
business_defect_class: "asset_card_to_depreciation",
failed_expected_edge: "asset_card_to_depreciation"
});
const output = composeAssistantAnswer({
userMessage: "Полно ли начислена амортизация по всем объектам ОС за июль?",
routeSummary: buildRouteSummary(),
retrievalResults: [
buildRetrieval({
problem_units: [unit],
problem_unit_summary: {
schema_version: "problem_unit_summary_v0_1",
units_total: 1,
duplicate_collapses: 0,
unit_types: ["lifecycle_anomaly_node"],
type_distribution: {
lifecycle_anomaly_node: 1
},
severity_distribution: {
low: 0,
medium: 1,
high: 0
},
confidence_distribution: {
low: 0,
medium: 1,
high: 0
},
primary_unit_type: "lifecycle_anomaly_node"
},
summary: {
semantic_profile: {
account_scope: ["01", "02"],
domain_scope: ["fixed_assets"],
relation_patterns: ["asset_card_to_depreciation", "deferred_expense_to_writeoff"],
period_scope: {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
}
},
domain_purity_guard: {
domain_card_id: "month_close_costs_20_44"
},
broad_query_detected: false,
broad_result_flag: false,
minimum_evidence_failed: false,
narrowing_strength: "strong"
}
})
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "Проверка амортизации",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: buildCoverage({ requirements_covered: 0, requirements_partially_covered: ["R1"] }),
groundingCheck: buildGrounding({ status: "partial" }),
questionTypeHint: "why_breaks",
companyAnchors: resolveCompanyAnchors("Полно ли начислена амортизация по всем объектам ОС за июль?"),
normalizationPeriodExplicit: true,
enableAnswerPolicyV11: true,
enableProblemCentricAnswerV1: true,
enableLifecycleAnswerV1: true
});
expect(output.assistant_reply).not.toMatch(/цепочка распределения затрат и закрытия месяца/i);
expect(output.assistant_reply).toMatch(/карточк[аеи] ОС|амортизац/i);
expect(output.assistant_reply).not.toMatch(/contradictory_asset_state|invalid_document_or_posting_transition|\bdisposed\b/i);
expect(output.assistant_reply).not.toMatch(/Проверьте связку документов и проводок по проблемному участку/i);
expect(output.assistant_reply).toMatch(/объект\w*\s+ОС|параметр\w*\s+амортиз|01\/02|счет\w*\s*0[12]/i);
expect(output.assistant_reply).not.toMatch(/РБП|сч[её]т\s*97|документ\s+списани[яе]|остат(ок|ки)\s+РБП|списани[ея]\s+РБП/i);
});
});

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-0E46_gT0ds",
"timestamp": "2026-03-28T14:52:43.266Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "j4C5JaGsdTX31w",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "EiQ_YNd0Z7R8dD",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-0yLbBfl8QU",
"timestamp": "2026-03-28T14:16:11.152Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "zeDz4lWmOU-sXS",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "Igp7ZjWw22GURw",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-1yuAQTgSrA",
"timestamp": "2026-03-28T14:52:43.542Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "dFofCt0krmlNxt",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "yuuGwyY-66-Acz",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-3lDLPY889T",
"timestamp": "2026-03-28T14:17:31.550Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "QsfgBqSscHBfPL",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "lrWC9XpjJMN6AA",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-48cua6GzNX",
"timestamp": "2026-03-28T17:57:44.019Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "60LijB0t8hI6Rv",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "tpPYPTRrOcZlBe",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-LbmAxVpEYt",
"timestamp": "2026-03-28T14:17:29.773Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "THNkvQLVaISlq3",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "0kU7UfIyaxB3TA",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "ZCKIUbbI9qTMwd",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-LpNMUq6e64",
"timestamp": "2026-03-28T14:52:41.774Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "jjxZOJBxrjPdsX",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "bYY62dikNsLECZ",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "baezDULblEFYkL",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-Mk-Ep58vC0",
"timestamp": "2026-03-28T17:57:43.813Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "p_blMcrrCin_H_",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "esnlDt05yjx_03",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-N-nY96elpX",
"timestamp": "2026-03-28T14:23:36.202Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "7mDZkne0GBSe62",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "wAVEOezciqQrtm",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "t5shV55ZPr8wir",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-S-kDKKK4xO",
"timestamp": "2026-03-28T14:17:31.390Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "kl3P7qQGw4BRHD",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "yn8Ku_R5880RIB",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-UNNoKia3JQ",
"timestamp": "2026-03-28T17:57:42.190Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "MY7YLMfaYP3kVR",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "gpwnhjSvR3NClQ",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "WYxkGFarO1WeXH",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-X71VuszUM2",
"timestamp": "2026-03-28T14:47:45.410Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "QacgRS_Ur2ayEH",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "qtDYC8oP9w1v9B",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-ZBwXbFcCs9",
"timestamp": "2026-03-28T14:23:37.997Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "NMt3FqmnkEylaF",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "TNvDsELGZj_UDF",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-_XbcacYQdg",
"timestamp": "2026-03-28T14:16:09.590Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "84nqlo6CKGkRZm",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "Z-6QeOaELWJ40H",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "fq5Nw3FI86Ldc1",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-bNqkZdQS9g",
"timestamp": "2026-03-28T14:23:37.732Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "AnVPMdNwg-rf3T",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по счету 97",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "utvJRunZgTZrKu",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,137 @@
{
"run_id": "eval-c2aEXxAGeh",
"timestamp": "2026-03-28T14:47:43.841Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 3
},
"cases_total": 3,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 33.33,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 33.33,
"routed_fragment_rate": 66.67,
"no_route_fragment_rate": 33.33,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 66.67,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 3,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 1,
"no_route": 1,
"batch_refresh_then_store": 1
},
"fallback_distribution": {
"none": 1,
"out_of_scope": 1,
"clarification": 1
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "r6cJKlGb3sAo1h",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Как вообще по ФСБУ",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 1,
"unclear_fragments": 0,
"fallback_type": "out_of_scope",
"predicted_route_status": "no_route",
"expected_route_status": null,
"predicted_no_route_reason": "out_of_scope",
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "RwtT5cfOm8EQzm",
"request_count_for_case": 0
},
{
"case_id": "BQ-003",
"raw_question": "Покажи топ рисков за июнь 2020",
"validation_passed": true,
"message_in_scope": false,
"scope_confidence": "low",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 0,
"out_of_scope_fragments": 0,
"unclear_fragments": 1,
"fallback_type": "clarification",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 0,
"trace_id": "aTubcVPJxRPKKy",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-giqMXxi5Cr",
"timestamp": "2026-03-28T14:16:11.355Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "lqgRK3iNNLJBPj",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "8F-6dbOWrrZ8DS",
"request_count_for_case": 0
}
]
}

View File

@ -0,0 +1,111 @@
{
"run_id": "eval-rbxdQ0Yd-g",
"timestamp": "2026-03-28T14:47:45.752Z",
"mode": "single-pass-strict",
"use_mock": true,
"prompt_version": "normalizer_v2_0_2",
"schema_version": "v2_0_2",
"dataset": {
"source": "inline_raw_questions",
"file": null,
"raw_questions_count": 2
},
"cases_total": 2,
"metrics": {
"schema_validation_pass_rate": 100,
"scope_detection_accuracy": null,
"scope_in_scope_rate": 100,
"multi_intent_detected_rate": 0,
"clarification_required_rate": 0,
"avg_fragments_per_message": 1,
"out_of_scope_fragment_rate": 0,
"routed_fragment_rate": 100,
"no_route_fragment_rate": 0,
"route_resolution_accuracy": null,
"no_route_precision": null,
"false_no_route_rate": null,
"execution_state_consistency_rate": 100,
"executable_with_soft_assumptions_rate": 100,
"soft_assumption_used_fragment_rate": 100,
"clarification_precision": null,
"clarification_recall": null,
"false_clarification_rate": null
},
"budget": {
"requests_total": 0,
"retries_used": 0
},
"clarification_eval": {
"labeled_cases": 0,
"true_positive": 0,
"false_positive": 0,
"false_negative": 0
},
"route_eval": {
"labeled_cases": 0,
"correct_cases": 0,
"expected_routed_cases": 0,
"no_route_true_positive": 0,
"no_route_false_positive": 0
},
"scope_eval": {
"labeled_cases": 0,
"correct_cases": 0
},
"execution_state_eval": {
"checks_total": 2,
"checks_passed": 2
},
"route_distribution": {
"hybrid_store_plus_live": 2
},
"fallback_distribution": {
"none": 2
},
"results": [
{
"case_id": "BQ-001",
"raw_question": "Проверь счет 60 за июнь 2020",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "KRtUQA4OmuvZXo",
"request_count_for_case": 0
},
{
"case_id": "BQ-002",
"raw_question": "Покажи риски по НДС и по закрытию",
"validation_passed": true,
"message_in_scope": true,
"scope_confidence": "high",
"contains_multiple_tasks": false,
"fragments_total": 1,
"in_scope_fragments": 1,
"out_of_scope_fragments": 0,
"unclear_fragments": 0,
"fallback_type": "none",
"predicted_route_status": "routed",
"expected_route_status": null,
"predicted_no_route_reason": null,
"expected_no_route_reason": null,
"predicted_clarification_required": false,
"expected_clarification_required": null,
"executable_with_soft_assumptions_fragments": 1,
"trace_id": "6HH1OsFaNsR6hO",
"request_count_for_case": 0
}
]
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-HMlzOgoV.js"></script>
<script type="module" crossorigin src="/assets/index-PA_66ng-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
</head>
<body>

View File

@ -41,7 +41,11 @@ function stringifyDebug(value: unknown): string {
}
}
function buildConversationExport(sessionId: string, conversation: AssistantConversationItem[]): string {
function buildConversationExport(
sessionId: string,
conversation: AssistantConversationItem[],
includeDebugPayload = false
): string {
const lines: string[] = [];
lines.push("# Assistant conversation export");
lines.push(`session_id: ${sessionId || "n/a"}`);
@ -61,7 +65,7 @@ function buildConversationExport(sessionId: string, conversation: AssistantConve
lines.push(item.text || "(empty)");
lines.push("");
if (item.role === "assistant" && item.debug) {
if (includeDebugPayload && item.role === "assistant" && item.debug) {
lines.push("### debug_payload_json");
lines.push("```json");
lines.push(stringifyDebug(item.debug));
@ -143,7 +147,8 @@ export function AssistantPanel({
if (conversation.length === 0) {
return;
}
const exportText = buildConversationExport(sessionId, conversation);
// Copy full run context for diagnostics (including debug payload blocks).
const exportText = buildConversationExport(sessionId, conversation, true);
const copied = await copyTextToClipboard(exportText);
setCopyState(copied ? "success" : "error");