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