Stage 3: улучшена логика жизненного цикла и очищены ответы ассистента
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type {
|
||||
import type {
|
||||
AssistantFallbackType,
|
||||
AssistantReplyType,
|
||||
AnswerGroundingCheck,
|
||||
@@ -50,21 +50,96 @@ const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]
|
||||
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
||||
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
||||
const RAW_REF_TOKEN_PATTERN = /\b(?:source_ref|canonical_ref|entity_id|fragment_id|guid|uuid)\b/gi;
|
||||
const SYNTHETIC_PLACEHOLDER_PATTERN = /\bunknown_entity(?::[^\s,;]+)?\b/gi;
|
||||
const SYNTHETIC_FALLBACK_MARKER_PATTERN = /\b(?:unknown_source|unknown_record)\b/gi;
|
||||
const SYNTHETIC_ROUTE_TOKEN_PATTERN = /\bbatch_refresh_then_store:[^\s,;]+/gi;
|
||||
const CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/u;
|
||||
const LATIN_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/u;
|
||||
const SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
|
||||
const PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\p{L}\p{N}_-]+[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
|
||||
const MOJIBAKE_SINGLE_MARKER_PATTERN = /^[\u0420\u0421\u00D0\u00D1]$/u;
|
||||
const MOJIBAKE_MARKER_CHAR_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/u;
|
||||
const CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/gu;
|
||||
const LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/g;
|
||||
const MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/gu;
|
||||
|
||||
function normalizeToken(value: string): string {
|
||||
return value.replace(/^[^\p{L}\p{N}_-]+|[^\p{L}\p{N}_-]+$/gu, "");
|
||||
}
|
||||
|
||||
function isLikelyMojibakeToken(value: string): boolean {
|
||||
const token = normalizeToken(String(value ?? ""));
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
if (MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)) {
|
||||
return true;
|
||||
}
|
||||
if (SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
|
||||
return true;
|
||||
}
|
||||
if (token.length <= 8 && PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
|
||||
return true;
|
||||
}
|
||||
return CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(token) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(token);
|
||||
}
|
||||
|
||||
function countMojibakeTokens(value: string): number {
|
||||
return String(value ?? "")
|
||||
.split(/[\s,.;:!?()[\]{}"']+/g)
|
||||
.filter((token) => token.length > 0)
|
||||
.filter((token) => isLikelyMojibakeToken(token)).length;
|
||||
}
|
||||
|
||||
function countMojibakeSingleMarkers(value: string): number {
|
||||
return String(value ?? "")
|
||||
.split(/[\s,.;:!?()[\]{}"']+/g)
|
||||
.filter((token) => token.length > 0)
|
||||
.map((token) => normalizeToken(token))
|
||||
.filter((token) => MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)).length;
|
||||
}
|
||||
|
||||
function stripMojibakeFragments(value: string): string {
|
||||
const removedByToken = String(value ?? "")
|
||||
.split(/(\s+)/g)
|
||||
.map((part) => {
|
||||
if (/^\s+$/u.test(part)) {
|
||||
return part;
|
||||
}
|
||||
return isLikelyMojibakeToken(part) ? "" : part;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return removedByToken
|
||||
.replace(CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
|
||||
.replace(LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
|
||||
.replace(MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN, "")
|
||||
.replace(/\s+([,.;:!?])/g, "$1")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function looksLikeMojibake(value: string): boolean {
|
||||
const text = String(value ?? "");
|
||||
if (!text.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:Р.|С.){5,}/u.test(text)) {
|
||||
const tokenHits = countMojibakeTokens(text);
|
||||
const singleMarkers = countMojibakeSingleMarkers(text);
|
||||
if (tokenHits >= 2 || (tokenHits >= 1 && singleMarkers >= 1) || singleMarkers >= 3) {
|
||||
return true;
|
||||
}
|
||||
if (/[ЃѓЂђЌќЎў]/u.test(text)) {
|
||||
if (MOJIBAKE_MARKER_CHAR_PATTERN.test(text)) {
|
||||
return true;
|
||||
}
|
||||
if (CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(text) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(text)) {
|
||||
return true;
|
||||
}
|
||||
if (/\uFFFD/u.test(text)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function looksLikeTechnicalIdentifier(value: string): boolean {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) {
|
||||
@@ -99,15 +174,33 @@ function scrubRawTechnicalRefs(value: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeUserFacingReply(value: string): string {
|
||||
return scrubRawTechnicalRefs(value)
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
function stripSyntheticPlaceholders(value: string): string {
|
||||
return String(value ?? "")
|
||||
.replace(SYNTHETIC_PLACEHOLDER_PATTERN, "")
|
||||
.replace(SYNTHETIC_FALLBACK_MARKER_PATTERN, "")
|
||||
.replace(SYNTHETIC_ROUTE_TOKEN_PATTERN, "")
|
||||
.replace(/[;,:]\s*[;,:]+/g, "; ")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeUserFacingReply(value: string): string {
|
||||
const normalized = scrubRawTechnicalRefs(value).replace(/[ \t]+\n/g, "\n");
|
||||
const cleanedLines = normalized
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => stripSyntheticPlaceholders(line))
|
||||
.map((line) => stripMojibakeFragments(line))
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.filter((line) => !looksLikeMojibake(line));
|
||||
const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
||||
return cleaned || "Available data requires clarification for a reliable user-facing answer.";
|
||||
}
|
||||
|
||||
function sanitizeUserText(value: string): string | null {
|
||||
const normalized = scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim());
|
||||
const normalized = stripMojibakeFragments(
|
||||
stripSyntheticPlaceholders(scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim()))
|
||||
);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
@@ -238,13 +331,13 @@ function buildFallbackWhyIncluded(results: UnifiedRetrievalResult[]): string[] {
|
||||
const checkedRecords = summaryNumber(result, "checked_records");
|
||||
|
||||
if (routeFocus) {
|
||||
lines.push(`Проверка выполнена по профилю ${routeFocus}.`);
|
||||
lines.push(`Проверка выполнена по профилю ${routeFocus}.`);
|
||||
}
|
||||
if (sourceRecords !== null && filteredRecords !== null && filteredRecords < sourceRecords) {
|
||||
lines.push(`Применено сужение выборки: ${filteredRecords} из ${sourceRecords} записей.`);
|
||||
lines.push(`Применено сужение выборки: ${filteredRecords} из ${sourceRecords} записей.`);
|
||||
}
|
||||
if (checkedRecords !== null) {
|
||||
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
|
||||
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,19 +348,19 @@ function buildFallbackSelectionReasons(results: UnifiedRetrievalResult[]): strin
|
||||
const lines: string[] = [];
|
||||
for (const result of results.slice(0, 2)) {
|
||||
if (summaryBoolean(result, "semantic_narrowing_applied")) {
|
||||
lines.push("Отбор выполнен по семантическому сужению предметной области.");
|
||||
lines.push("Отбор выполнен по семантическому сужению предметной области.");
|
||||
}
|
||||
const rankingBasis = summaryStringArray(result, "ranking_basis");
|
||||
if (rankingBasis.length > 0) {
|
||||
lines.push(`Ранжирование основано на: ${rankingBasis.join(", ")}.`);
|
||||
lines.push(`Ранжирование основано на: ${rankingBasis.join(", ")}.`);
|
||||
}
|
||||
if (summaryBoolean(result, "broad_guard_applied")) {
|
||||
lines.push("Применен broad-query guard для контроля ложной точности.");
|
||||
lines.push("Применен broad-query guard для контроля ложной точности.");
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
lines.push("Отбор выполнен по совпадению предметных сигналов и доступной evidence-опоры.");
|
||||
lines.push("Отбор выполнен по совпадению предметных сигналов и доступной evidence-опоры.");
|
||||
}
|
||||
|
||||
return sanitizeUserLines(lines, 4);
|
||||
@@ -276,16 +369,16 @@ function buildFallbackSelectionReasons(results: UnifiedRetrievalResult[]): strin
|
||||
function suggestNextStep(requirements: AssistantRequirement[], coverage: RequirementCoverageReport): string[] {
|
||||
const next: string[] = [];
|
||||
if (coverage.clarification_needed_for.length > 0) {
|
||||
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
|
||||
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
|
||||
}
|
||||
if (coverage.requirements_uncovered.length > 0) {
|
||||
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
|
||||
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
|
||||
}
|
||||
if (coverage.out_of_scope_requirements.length > 0) {
|
||||
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
|
||||
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
|
||||
}
|
||||
if (next.length === 0 && requirements.length > 0) {
|
||||
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
|
||||
next.push("Следующим шагом можно открыть технический разбор и углубить проверку по выбранным объектам.");
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -364,21 +457,25 @@ function selectProblemUnitSummary(results: UnifiedRetrievalResult[]): ProblemUni
|
||||
}
|
||||
|
||||
function formatAffectedScope(unit: ProblemUnit): string {
|
||||
const accountScope = sanitizeUserLines(unit.affected_accounts, 2);
|
||||
const counterpartyScope = sanitizeUserLines(unit.affected_counterparties, 2);
|
||||
const documentScope = sanitizeUserLines(unit.affected_documents, 2);
|
||||
const entityScope = sanitizeUserLines(unit.affected_entities, 2);
|
||||
const scopeParts: string[] = [];
|
||||
if (unit.affected_accounts.length > 0) {
|
||||
scopeParts.push(`счета: ${unit.affected_accounts.slice(0, 2).join(", ")}`);
|
||||
if (accountScope.length > 0) {
|
||||
scopeParts.push(`accounts: ${accountScope.join(", ")}`);
|
||||
}
|
||||
if (unit.affected_counterparties.length > 0) {
|
||||
scopeParts.push(`контрагенты: ${unit.affected_counterparties.slice(0, 2).join(", ")}`);
|
||||
if (counterpartyScope.length > 0) {
|
||||
scopeParts.push(`counterparties: ${counterpartyScope.join(", ")}`);
|
||||
}
|
||||
if (unit.affected_documents.length > 0) {
|
||||
scopeParts.push(`документы: ${unit.affected_documents.slice(0, 2).join(", ")}`);
|
||||
if (documentScope.length > 0) {
|
||||
scopeParts.push(`documents: ${documentScope.join(", ")}`);
|
||||
}
|
||||
if (scopeParts.length === 0 && unit.affected_entities.length > 0) {
|
||||
scopeParts.push(`объекты: ${unit.affected_entities.slice(0, 2).join(", ")}`);
|
||||
if (scopeParts.length === 0 && entityScope.length > 0) {
|
||||
scopeParts.push(`entities: ${entityScope.join(", ")}`);
|
||||
}
|
||||
if (scopeParts.length === 0) {
|
||||
return "затронутый контур требует уточнения";
|
||||
return "affected scope requires clarification";
|
||||
}
|
||||
return scopeParts.join("; ");
|
||||
}
|
||||
@@ -448,49 +545,49 @@ function buildProblemCentricActions(input: {
|
||||
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
|
||||
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
|
||||
}
|
||||
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
|
||||
}
|
||||
if (unitTypes.has("lifecycle_anomaly_node")) {
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
|
||||
}
|
||||
for (const unit of input.units) {
|
||||
if (unit.lifecycle_defect_type === "stale_active_state") {
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "misclosed_state") {
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
|
||||
}
|
||||
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.mode === "clarification_required") {
|
||||
if (input.missingAnchors.period) {
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(actions, 6);
|
||||
@@ -510,28 +607,28 @@ 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-06), в котором нужно проверить проблемный кластер.");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
|
||||
}
|
||||
if (unitTypes.has("broken_chain_segment")) {
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
|
||||
}
|
||||
if (unitTypes.has("period_risk_cluster")) {
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
|
||||
}
|
||||
if (unitTypes.has("unresolved_settlement_cluster")) {
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(questions, 6);
|
||||
@@ -644,10 +741,10 @@ function limitationReasonToText(code: EvidenceLimitationReasonCode): string {
|
||||
function detectMissingAnchors(userMessage: string): MissingAnchors {
|
||||
const lower = String(userMessage ?? "").toLowerCase();
|
||||
const hasPeriod = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/.test(lower);
|
||||
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b\d{2}(?:\.\d{2})?\b)/i.test(lower);
|
||||
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b\d{2}(?:\.\d{2})?\b)/i.test(lower);
|
||||
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\bid\b|\bref\b|dokument|doc)/i.test(lower);
|
||||
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel)/i.test(lower);
|
||||
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
|
||||
|
||||
return {
|
||||
period: !hasPeriod,
|
||||
@@ -671,22 +768,22 @@ function buildClarificationQuestions(input: {
|
||||
}
|
||||
|
||||
if (input.missingAnchors.period) {
|
||||
questions.push("Уточните период проверки (например, 2020-06).");
|
||||
questions.push("Уточните период проверки (например, 2020-06).");
|
||||
}
|
||||
if (input.missingAnchors.account) {
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
|
||||
}
|
||||
if (input.missingAnchors.documentOrObject) {
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
|
||||
}
|
||||
if (input.missingAnchors.counterparty) {
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
questions.push("Укажите контрагента или группу контрагентов.");
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
|
||||
}
|
||||
if (input.coverageReport.clarification_needed_for.length > 0) {
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(questions, 6);
|
||||
@@ -701,31 +798,31 @@ function buildRecommendedActions(input: {
|
||||
}): string[] {
|
||||
const actions: string[] = [];
|
||||
if (input.mode === "focused_grounded") {
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
|
||||
}
|
||||
if (input.coverageReport.requirements_uncovered.length > 0) {
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
|
||||
}
|
||||
if (input.coverageReport.requirements_partially_covered.length > 0) {
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
|
||||
}
|
||||
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("snapshot_only")) {
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
|
||||
}
|
||||
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
|
||||
}
|
||||
if (input.sourceRefs.length > 0) {
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
|
||||
}
|
||||
|
||||
return uniqueStrings(actions, 6);
|
||||
@@ -842,14 +939,14 @@ function buildPolicyDecision(input: {
|
||||
}
|
||||
|
||||
function buildAnswerSummary(mode: PolicyMode): string {
|
||||
if (mode === "focused_grounded") return "Сформирован прямой ответ на основе подтвержденной опоры.";
|
||||
if (mode === "broad_partial") return "Вывод ограничен: есть частичная опора, но не полный coverage.";
|
||||
if (mode === "clarification_required") return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
|
||||
if (mode === "out_of_scope") return "Запрос вне доступного учетного контура.";
|
||||
if (mode === "route_mismatch") return "Результат маршрута не совпал с предметом вопроса.";
|
||||
if (mode === "empty") return "В текущем срезе данных релевантные записи не обнаружены.";
|
||||
if (mode === "no_grounded") return "Недостаточно опоры для обоснованного ответа.";
|
||||
return "Не удалось собрать обоснованный ответ по текущему запросу.";
|
||||
if (mode === "focused_grounded") return "Сформирован прямой ответ на основе подтвержденной опоры.";
|
||||
if (mode === "broad_partial") return "Вывод ограничен: есть частичная опора, но не полный coverage.";
|
||||
if (mode === "clarification_required") return "Нужны уточнения: без сужения strong factual вывод ненадежен.";
|
||||
if (mode === "out_of_scope") return "Запрос вне доступного учетного контура.";
|
||||
if (mode === "route_mismatch") return "Результат маршрута не совпал с предметом вопроса.";
|
||||
if (mode === "empty") return "В текущем срезе данных релевантные записи не обнаружены.";
|
||||
if (mode === "no_grounded") return "Недостаточно опоры для обоснованного ответа.";
|
||||
return "Не удалось собрать обоснованный ответ по текущему запросу.";
|
||||
}
|
||||
|
||||
function buildDirectAnswer(input: {
|
||||
@@ -859,33 +956,33 @@ function buildDirectAnswer(input: {
|
||||
}): string {
|
||||
const topFact = firstMeaningfulFact(input.retrievalResults);
|
||||
if (input.mode === "focused_grounded") {
|
||||
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
|
||||
return topFact ?? "Подтвержденный результат получен; можно продолжать предметную проверку без деградации.";
|
||||
}
|
||||
if (input.mode === "broad_partial") {
|
||||
if (topFact) {
|
||||
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
|
||||
return `Доступен ограниченный подтвержденный фрагмент: ${topFact}`;
|
||||
}
|
||||
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
|
||||
return "Есть только ограниченная опора; вывод дан в частичном режиме без ложной точности.";
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
|
||||
return "Текущий запрос слишком широкий или недоопределен; надежный factual вывод пока невозможен.";
|
||||
}
|
||||
if (input.mode === "out_of_scope") {
|
||||
return "Могу отвечать только в пределах данных доступного учетного контура.";
|
||||
return "Могу отвечать только в пределах данных доступного учетного контура.";
|
||||
}
|
||||
if (input.mode === "route_mismatch") {
|
||||
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
|
||||
return "Предмет результата не совпал с предметом вопроса; требуется уточнение фокуса.";
|
||||
}
|
||||
if (input.mode === "empty") {
|
||||
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
|
||||
return "В текущем срезе данных проблемные записи по заданному условию не найдены.";
|
||||
}
|
||||
if (input.mode === "no_grounded") {
|
||||
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
|
||||
return "Недостаточно подтвержденной опоры для ответа в требуемой точности.";
|
||||
}
|
||||
if (input.policySignals.minimum_evidence_failed) {
|
||||
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
|
||||
return "Маршрут отработал, но минимальная evidence-опора не пройдена.";
|
||||
}
|
||||
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
|
||||
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
|
||||
}
|
||||
|
||||
function buildProblemCentricAnswerSummary(input: {
|
||||
@@ -896,20 +993,20 @@ function buildProblemCentricAnswerSummary(input: {
|
||||
}): string {
|
||||
if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) {
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей.";
|
||||
return "Выявлены lifecycle-дефекты, но для надежного вывода требуется уточнение предметных якорей.";
|
||||
}
|
||||
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
|
||||
return `Сформирован lifecycle-aware problem срез: выделено ${input.summary.lifecycle_enriched_units} lifecycle-узлов с приоритетом по дефектам перехода.`;
|
||||
}
|
||||
if (input.mode === "clarification_required") {
|
||||
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
|
||||
return "Выявлены проблемные кластеры, но для надежного вывода требуется предметное уточнение фокуса.";
|
||||
}
|
||||
if (input.weakUnits) {
|
||||
return "Сформирован problem-centric срез с ограниченной опорой; вывод предварительный и требует до-проверки.";
|
||||
return "Сформирован problem-centric срез с ограниченной опорой; вывод предварительный и требует до-проверки.";
|
||||
}
|
||||
if (input.summary?.units_total && input.summary.units_total > 1) {
|
||||
return `Сформирован problem-centric срез: выделено ${input.summary.units_total} проблемных кластера с приоритетами.`;
|
||||
return `Сформирован problem-centric срез: выделено ${input.summary.units_total} проблемных кластера с приоритетами.`;
|
||||
}
|
||||
return "Сформирован problem-centric срез: выделен ключевой проблемный кластер и затронутый контур.";
|
||||
return "Сформирован problem-centric срез: выделен ключевой проблемный кластер и затронутый контур.";
|
||||
}
|
||||
|
||||
function buildProblemCentricDirectAnswer(input: {
|
||||
@@ -920,19 +1017,24 @@ function buildProblemCentricDirectAnswer(input: {
|
||||
}): string {
|
||||
const lead =
|
||||
input.mode === "clarification_required"
|
||||
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
|
||||
? "Обнаружены проблемные зоны, но без уточнения якорей сильный factual-вывод преждевременен."
|
||||
: input.weakUnits
|
||||
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
|
||||
? "Выделены проблемные зоны с ограниченной надежностью; вывод дан в ограниченном режиме."
|
||||
: input.lifecycleAnswerEnabled && hasLifecycleResolution(input.units)
|
||||
? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода."
|
||||
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
|
||||
? "Выделены lifecycle-проблемы: определены текущие/ожидаемые стадии и тип нарушения перехода."
|
||||
: "Выделены ключевые проблемные зоны и их влияние на учетный контур.";
|
||||
|
||||
const unitLines = input.units.map((unit) => {
|
||||
const scope = formatAffectedScope(unit);
|
||||
const lifecycleScope = input.lifecycleAnswerEnabled ? formatLifecycleScope(unit) : null;
|
||||
const lifecycleInterpretation = input.lifecycleAnswerEnabled ? unit.business_lifecycle_interpretation : null;
|
||||
const lifecycleInterpretation =
|
||||
input.lifecycleAnswerEnabled && unit.business_lifecycle_interpretation
|
||||
? sanitizeUserText(unit.business_lifecycle_interpretation)
|
||||
: null;
|
||||
const title = sanitizeUserText(unit.title) ?? "Problem cluster detected";
|
||||
const defect = sanitizeUserText(unit.business_defect_class) ?? "detected_issue";
|
||||
const segments = [
|
||||
`${unit.title}: ${unit.business_defect_class}`,
|
||||
`${title}: ${defect}`,
|
||||
scope,
|
||||
lifecycleScope,
|
||||
lifecycleInterpretation,
|
||||
@@ -944,10 +1046,10 @@ function buildProblemCentricDirectAnswer(input: {
|
||||
});
|
||||
|
||||
if (unitLines.length === 0) {
|
||||
return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`;
|
||||
return `${lead}\nПроблемные кластеры не удалось детализировать в текущем срезе.`;
|
||||
}
|
||||
|
||||
return [lead, "Проблемные кластеры:", ...unitLines].join("\n");
|
||||
return [lead, "Проблемные кластеры:", ...unitLines].join("\n");
|
||||
}
|
||||
|
||||
function buildProblemCentricAnswerStructure(input: {
|
||||
@@ -1358,21 +1460,23 @@ function composeExplainableAnswer(input: ComposeAnswerInput, scopeLabel: "full"
|
||||
|
||||
const lead =
|
||||
scopeLabel === "full"
|
||||
? "Итог: запрос обработан по предмету, найденные объекты подтверждены данными контура."
|
||||
: "Итог: запрос обработан частично, ниже подтвержденная часть и ограничения.";
|
||||
? "Ртог: запрос обработан РїРѕ предмету, найденные объекты подтверждены данными контура."
|
||||
: "Ртог: запрос обработан частично, РЅРёР¶Рµ подтвержденная часть Рё ограничения.";
|
||||
|
||||
return [
|
||||
lead,
|
||||
facts.length > 0 ? "Подтвержденные результаты:\n" + formatList(facts) : "",
|
||||
whyIncluded.length > 0 ? "Почему это попало в ответ:\n" + formatList(whyIncluded) : "",
|
||||
selectionReasons.length > 0 ? "Основание отбора:\n" + formatList(selectionReasons) : "",
|
||||
riskFactors.length > 0 ? "Подтверждающие признаки:\n" + formatList(riskFactors) : "",
|
||||
interpretation.length > 0 ? "Практический смысл:\n" + formatList(interpretation) : "",
|
||||
limitations.length > 0 ? "Ограничения:\n" + formatList(limitations) : "",
|
||||
nextSteps.length > 0 ? "Что проверить дальше:\n" + formatList(nextSteps) : ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
return sanitizeUserFacingReply(
|
||||
[
|
||||
lead,
|
||||
facts.length > 0 ? "Подтвержденные результаты:\n" + formatList(facts) : "",
|
||||
whyIncluded.length > 0 ? "Почему это попало в ответ:\n" + formatList(whyIncluded) : "",
|
||||
selectionReasons.length > 0 ? "Основание отбора:\n" + formatList(selectionReasons) : "",
|
||||
riskFactors.length > 0 ? "Подтверждающие признаки:\n" + formatList(riskFactors) : "",
|
||||
interpretation.length > 0 ? "Практический смысл:\n" + formatList(interpretation) : "",
|
||||
limitations.length > 0 ? "Ограничения:\n" + formatList(limitations) : "",
|
||||
nextSteps.length > 0 ? "Что проверить дальше:\n" + formatList(nextSteps) : ""
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswerOutput {
|
||||
@@ -1385,6 +1489,8 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
||||
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
||||
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
|
||||
const legacyEvidenceItems = flattenEvidence(input.retrievalResults);
|
||||
const legacyLimitationReasonCodes = collectLimitationReasonCodes(legacyEvidenceItems);
|
||||
const hasBroadMinimumEvidenceSignal = input.retrievalResults.some(
|
||||
(item) => summaryBoolean(item, "broad_guard_applied") && summaryBoolean(item, "minimum_evidence_failed")
|
||||
);
|
||||
@@ -1398,7 +1504,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
if (fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
|
||||
return {
|
||||
assistant_reply:
|
||||
"Я могу отвечать только по данным вашей учетной базы. Этот запрос выходит за рамки доступного контура.",
|
||||
"РЇ РјРѕРіСѓ отвечать только РїРѕ данным вашей учетной базы. Ртот запрос выходит Р·Р° рамки доступного контура.",
|
||||
fallback_type: "out_of_scope",
|
||||
reply_type: "out_of_scope"
|
||||
};
|
||||
@@ -1407,8 +1513,8 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
if (input.groundingCheck.status === "route_mismatch_blocked") {
|
||||
return {
|
||||
assistant_reply: [
|
||||
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
|
||||
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
|
||||
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
|
||||
"Уточните формулировку (например, нужный счет/участок учета), и я выполню повторный проход."
|
||||
].join("\n\n"),
|
||||
fallback_type: "partial",
|
||||
reply_type: "route_mismatch_blocked"
|
||||
@@ -1418,7 +1524,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
if (input.groundingCheck.status === "no_grounded_answer" && okResults.length === 0 && !hasBroadMinimumEvidenceSignal) {
|
||||
return {
|
||||
assistant_reply:
|
||||
"Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
|
||||
"Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "no_grounded_answer"
|
||||
};
|
||||
@@ -1427,7 +1533,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
if (hasBroadClarificationSignal && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply:
|
||||
"Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
|
||||
"Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
|
||||
fallback_type: "clarification",
|
||||
reply_type: "clarification_required"
|
||||
};
|
||||
@@ -1435,7 +1541,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
|
||||
if (fallbackType === "clarification" && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
|
||||
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы закрыть все части вопроса корректно.",
|
||||
fallback_type: "clarification",
|
||||
reply_type: "clarification_required"
|
||||
};
|
||||
@@ -1443,7 +1549,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
|
||||
if (errorResults.length > 0 && okResults.length === 0 && partialResults.length === 0) {
|
||||
return {
|
||||
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
|
||||
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "backend_error"
|
||||
};
|
||||
@@ -1459,7 +1565,7 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
|
||||
if (okResults.length === 0 && partialResults.length === 0 && emptyResults.length > 0) {
|
||||
return {
|
||||
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
|
||||
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
|
||||
fallback_type: fallbackType,
|
||||
reply_type: "empty_but_valid"
|
||||
};
|
||||
@@ -1471,7 +1577,9 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
input.coverageReport.clarification_needed_for.length > 0 ||
|
||||
input.coverageReport.out_of_scope_requirements.length > 0 ||
|
||||
input.groundingCheck.status === "partial" ||
|
||||
errorResults.length > 0;
|
||||
errorResults.length > 0 ||
|
||||
legacyLimitationReasonCodes.includes("weak_source_mapping") ||
|
||||
legacyLimitationReasonCodes.includes("missing_mechanism");
|
||||
|
||||
if (okResults.length > 0 && hasPartialCoverage) {
|
||||
return {
|
||||
@@ -1490,9 +1598,10 @@ export function composeAssistantAnswer(input: ComposeAnswerInput): ComposeAnswer
|
||||
}
|
||||
|
||||
return {
|
||||
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
|
||||
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
|
||||
fallback_type: "unknown",
|
||||
reply_type: "backend_error"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1125,9 +1125,9 @@ export class AssistantDataLayer {
|
||||
} else if (route === "store_feature_risk") {
|
||||
result = this.executeRisk(fragmentText, data);
|
||||
} else if (route === "batch_refresh_then_store") {
|
||||
result = this.executeBatch(data);
|
||||
result = this.executeBatch(fragmentText, data);
|
||||
} else if (route === "store_canonical") {
|
||||
result = this.executeCanonical(data);
|
||||
result = this.executeCanonical(fragmentText, data);
|
||||
} else if (route === "live_mcp_drilldown") {
|
||||
result = this.executeDrilldown(fragmentText, data);
|
||||
}
|
||||
@@ -1437,7 +1437,9 @@ export class AssistantDataLayer {
|
||||
};
|
||||
}
|
||||
|
||||
private executeRisk(_fragmentText: string, data: DatasetBundle): RawRetrievalResult {
|
||||
private executeRisk(fragmentText: string, data: DatasetBundle): RawRetrievalResult {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const profileRiskFactors = semanticProfile.anomaly_patterns;
|
||||
const records = [...data.problemCases, ...data.ndsRegisters];
|
||||
const scored = records
|
||||
.map((record) => {
|
||||
@@ -1491,12 +1493,15 @@ export class AssistantDataLayer {
|
||||
items: [],
|
||||
summary: {
|
||||
checked_records: records.length,
|
||||
risky_records: 0
|
||||
risky_records: 0,
|
||||
query_subject: semanticProfile.query_subject,
|
||||
semantic_profile: semanticProfile,
|
||||
ranking_basis: semanticProfile.ranking_basis
|
||||
},
|
||||
evidence: [],
|
||||
why_included: [],
|
||||
selection_reason: ["Риск-оценка выполнялась по техническим признакам, но записи выше порога не найдены."],
|
||||
risk_factors: [],
|
||||
risk_factors: profileRiskFactors,
|
||||
business_interpretation: ["По текущему срезу явные риск-признаки не обнаружены."],
|
||||
confidence: "medium",
|
||||
limitations: ["Оценка основана на snapshot-данных и эвристическом risk score."],
|
||||
@@ -1505,6 +1510,13 @@ export class AssistantDataLayer {
|
||||
}
|
||||
|
||||
const averageScore = items.reduce((acc, item) => acc + item.risk_score, 0) / items.length;
|
||||
const normalizedRiskFactors = uniqueStrings([
|
||||
...profileRiskFactors,
|
||||
"unknown_link_count",
|
||||
"zero_guid_values",
|
||||
"navigation_links",
|
||||
"missing_counterparty_link"
|
||||
]);
|
||||
return {
|
||||
status: "ok",
|
||||
result_type: "list",
|
||||
@@ -1512,7 +1524,10 @@ export class AssistantDataLayer {
|
||||
summary: {
|
||||
checked_records: records.length,
|
||||
risky_records: items.length,
|
||||
average_risk_score: Number(averageScore.toFixed(2))
|
||||
average_risk_score: Number(averageScore.toFixed(2)),
|
||||
query_subject: semanticProfile.query_subject,
|
||||
semantic_profile: semanticProfile,
|
||||
ranking_basis: semanticProfile.ranking_basis
|
||||
},
|
||||
evidence: items.slice(0, 10).map((item) => ({
|
||||
source_entity: item.source_entity,
|
||||
@@ -1521,14 +1536,10 @@ export class AssistantDataLayer {
|
||||
})),
|
||||
why_included: ["В ответ включены записи с risk_score >= 2."],
|
||||
selection_reason: [
|
||||
"score растет при unknown links, zero GUID, навигационных ссылках и отсутствии явного контрагента."
|
||||
],
|
||||
risk_factors: [
|
||||
"unknown_link_count",
|
||||
"zero_guid_values",
|
||||
"navigation_links",
|
||||
"missing_counterparty_link"
|
||||
"score растет при unknown links, zero GUID, навигационных ссылках и отсутствии явного контрагента.",
|
||||
`Semantic profile subject: ${semanticProfile.query_subject}.`
|
||||
],
|
||||
risk_factors: normalizedRiskFactors,
|
||||
business_interpretation: ["Рти записи требуют первичной бухгалтерской проверки как потенциальные аномалии."],
|
||||
confidence: "high",
|
||||
limitations: ["Риск-факторы определяются эвристикой, а не полным набором бизнес-правил 1С."],
|
||||
@@ -1536,7 +1547,8 @@ export class AssistantDataLayer {
|
||||
};
|
||||
}
|
||||
|
||||
private executeBatch(data: DatasetBundle): RawRetrievalResult {
|
||||
private executeBatch(fragmentText: string, data: DatasetBundle): RawRetrievalResult {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const source = [...data.problemCases, ...data.keyFields, ...data.docs];
|
||||
const byEntity = new Map<string, number>();
|
||||
for (const record of source) {
|
||||
@@ -1558,7 +1570,10 @@ export class AssistantDataLayer {
|
||||
items,
|
||||
summary: {
|
||||
checked_records: source.length,
|
||||
ranked_entities: items.length
|
||||
ranked_entities: items.length,
|
||||
query_subject: semanticProfile.query_subject,
|
||||
semantic_profile: semanticProfile,
|
||||
ranking_basis: semanticProfile.ranking_basis
|
||||
},
|
||||
evidence: items.slice(0, 5).map((item) => ({
|
||||
entity: item.entity,
|
||||
@@ -1566,9 +1581,9 @@ export class AssistantDataLayer {
|
||||
})),
|
||||
why_included: items.length > 0 ? ["Показаны сущности с максимальным количеством записей."] : [],
|
||||
selection_reason: ["Ранжирование выполнено по records_count по убыванию."],
|
||||
risk_factors: ["Высокий объем записей по сущности повышает приоритет проверки."],
|
||||
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
|
||||
business_interpretation: [
|
||||
"Сущности в топе ранга чаще дают наибольший вклад в проблемный объем и требуют приоритетного аудита."
|
||||
"Top entities by volume highlight where lifecycle-focused review should start first."
|
||||
],
|
||||
confidence: "medium",
|
||||
limitations: ["Ранжирование по объему не всегда эквивалентно бизнес-риску."],
|
||||
@@ -1576,8 +1591,11 @@ export class AssistantDataLayer {
|
||||
};
|
||||
}
|
||||
|
||||
private executeCanonical(data: DatasetBundle): RawRetrievalResult {
|
||||
const items = data.docs
|
||||
private executeCanonical(fragmentText: string, data: DatasetBundle): RawRetrievalResult {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const useVatSource = semanticProfile.domain_scope.includes("vat") || semanticProfile.domain_scope.includes("taxes");
|
||||
const sourceRecords = useVatSource ? [...data.ndsRegisters, ...data.keyFields] : data.docs;
|
||||
const items = sourceRecords
|
||||
.map((record) => {
|
||||
const period = extractDate(record);
|
||||
return {
|
||||
@@ -1599,8 +1617,11 @@ export class AssistantDataLayer {
|
||||
result_type: "list",
|
||||
items,
|
||||
summary: {
|
||||
checked_records: data.docs.length,
|
||||
returned_records: items.length
|
||||
checked_records: sourceRecords.length,
|
||||
returned_records: items.length,
|
||||
query_subject: semanticProfile.query_subject,
|
||||
semantic_profile: semanticProfile,
|
||||
ranking_basis: semanticProfile.ranking_basis
|
||||
},
|
||||
evidence: items.slice(0, 6).map((item) => ({
|
||||
source_entity: item.source_entity,
|
||||
@@ -1608,8 +1629,11 @@ export class AssistantDataLayer {
|
||||
period: item.period
|
||||
})),
|
||||
why_included: items.length > 0 ? ["Показаны последние по дате записи канонического документного слоя."] : [],
|
||||
selection_reason: ["Отбор по максимальной дате документа в пределах snapshot."],
|
||||
risk_factors: [],
|
||||
selection_reason: [
|
||||
"Отбор по максимальной дате документа в пределах snapshot.",
|
||||
`Semantic profile subject: ${semanticProfile.query_subject}.`
|
||||
],
|
||||
risk_factors: semanticProfile.anomaly_patterns,
|
||||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||||
confidence: "high",
|
||||
limitations: ["Рто read-only snapshot, Р° РЅРµ онлайн-состояние 1РЎ."],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CandidateEvidenceItem, ProblemConfidence, ProblemUnit, ProblemUnitType } from "../types/stage2ProblemUnits";
|
||||
import type { CandidateEvidenceItem, ProblemConfidence, ProblemUnit, ProblemUnitType } from "../types/stage2ProblemUnits";
|
||||
import {
|
||||
LIFECYCLE_MODEL_SCHEMA_VERSION,
|
||||
STAGE3_LIFECYCLE_DOMAINS,
|
||||
@@ -47,13 +47,99 @@ function hasToken(values: string[], pattern: RegExp): boolean {
|
||||
return values.some((value) => pattern.test(value));
|
||||
}
|
||||
|
||||
function defaultExpectedState(domain: LifecycleDomain): string {
|
||||
if (domain === "bank_settlement") return "settlement_closed";
|
||||
if (domain === "customer_settlement") return "receivable_closed";
|
||||
if (domain === "deferred_expense") return "fully_written_off";
|
||||
if (domain === "fixed_asset") return "depreciation_active";
|
||||
if (domain === "vat_flow") return "vat_deducted";
|
||||
return "close_completed";
|
||||
function normalizeStateToken(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveStateCode(model: LifecycleDomainModel, stateCode: string | null | undefined): string | null {
|
||||
if (!stateCode || typeof stateCode !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeStateToken(stateCode);
|
||||
const matched = model.states.find((state) => normalizeStateToken(state.state_code) === normalized);
|
||||
return matched?.state_code ?? null;
|
||||
}
|
||||
|
||||
function defaultInitialState(model: LifecycleDomainModel): string {
|
||||
const initial = model.states.find((state) => state.state_class === "initial");
|
||||
if (initial) {
|
||||
return initial.state_code;
|
||||
}
|
||||
return model.states[0]?.state_code ?? "unknown_state";
|
||||
}
|
||||
|
||||
function defaultExpectedState(model: LifecycleDomainModel): string {
|
||||
const terminal = model.states.find((state) => state.is_terminal || state.state_class === "terminal");
|
||||
if (terminal) {
|
||||
return terminal.state_code;
|
||||
}
|
||||
const active = model.states.find((state) => state.state_class === "active");
|
||||
if (active) {
|
||||
return active.state_code;
|
||||
}
|
||||
return defaultInitialState(model);
|
||||
}
|
||||
|
||||
function expectedTransitionAdjacency(model: LifecycleDomainModel): Map<string, string[]> {
|
||||
const graph = new Map<string, string[]>();
|
||||
for (const transition of model.transitions) {
|
||||
if (transition.transition_type !== "expected") {
|
||||
continue;
|
||||
}
|
||||
const from = transition.from_state;
|
||||
const to = transition.to_state;
|
||||
const current = graph.get(from) ?? [];
|
||||
if (!current.includes(to)) {
|
||||
current.push(to);
|
||||
}
|
||||
graph.set(from, current);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
function shortestExpectedPath(model: LifecycleDomainModel, fromState: string, toState: string): string[] | null {
|
||||
if (fromState === toState) {
|
||||
return [fromState];
|
||||
}
|
||||
const graph = expectedTransitionAdjacency(model);
|
||||
const queue: string[][] = [[fromState]];
|
||||
const visited = new Set<string>([fromState]);
|
||||
while (queue.length > 0) {
|
||||
const path = queue.shift();
|
||||
if (!path) {
|
||||
continue;
|
||||
}
|
||||
const tail = path[path.length - 1];
|
||||
const nextStates = graph.get(tail) ?? [];
|
||||
for (const nextState of nextStates) {
|
||||
if (visited.has(nextState)) {
|
||||
continue;
|
||||
}
|
||||
const nextPath = [...path, nextState];
|
||||
if (nextState === toState) {
|
||||
return nextPath;
|
||||
}
|
||||
visited.add(nextState);
|
||||
queue.push(nextPath);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function transitionEdgeLabel(fromState: string, toState: string): string {
|
||||
return `${fromState}->${toState}`;
|
||||
}
|
||||
|
||||
function resolvePreviousStates(model: LifecycleDomainModel, currentState: string): string[] {
|
||||
const initialState = defaultInitialState(model);
|
||||
if (initialState === currentState) {
|
||||
return [];
|
||||
}
|
||||
const path = shortestExpectedPath(model, initialState, currentState);
|
||||
if (!path || path.length <= 1) {
|
||||
return [];
|
||||
}
|
||||
return path.slice(0, -1);
|
||||
}
|
||||
|
||||
const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
@@ -64,53 +150,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "initiated_payment",
|
||||
state_label: "Платеж инициирован",
|
||||
state_label: "Платеж инициирован",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["payment_order_created"],
|
||||
exit_conditions: ["bank_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
business_meaning: "Есть инициирование платежа."
|
||||
},
|
||||
{
|
||||
state_code: "bank_recorded",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_label: "Платеж отражен банком",
|
||||
state_class: "active",
|
||||
entry_conditions: ["bank_statement_recorded"],
|
||||
exit_conditions: ["settlement_linked"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "settlement_closed",
|
||||
state_label: "Расчет закрыт",
|
||||
state_label: "Расчет закрыт",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["payment_to_settlement_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
business_meaning: "Платеж доведен до расчетного результата."
|
||||
},
|
||||
{
|
||||
state_code: "stale_unlinked_payment",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_label: "Платеж завис без закрытия",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["bank_recorded", "missing_link"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
|
||||
},
|
||||
{
|
||||
state_code: "misclosed_payment",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_label: "Платеж закрыт некорректно",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["wrong_document_type_or_posting_mismatch"],
|
||||
exit_conditions: ["settlement_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -121,7 +207,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["bank_statement_recorded"],
|
||||
optional_evidence: ["payment_order"],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
business_meaning: "Платеж должен появиться во выписке."
|
||||
},
|
||||
{
|
||||
from_state: "bank_recorded",
|
||||
@@ -130,7 +216,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["payment_to_settlement_link"],
|
||||
optional_evidence: ["document_to_posting"],
|
||||
forbidden_conditions: ["wrong_document_type"],
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
business_meaning: "После выписки должен закрываться расчет."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -142,43 +228,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "invoice_issued",
|
||||
state_label: "Реализация отражена",
|
||||
state_label: "Реализация отражена",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["realization_document_exists"],
|
||||
exit_conditions: ["payment_recorded"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
business_meaning: "Возникла дебиторская позиция."
|
||||
},
|
||||
{
|
||||
state_code: "payment_recorded",
|
||||
state_label: "Оплата отражена",
|
||||
state_label: "Оплата отражена",
|
||||
state_class: "active",
|
||||
entry_conditions: ["payment_document_exists"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
business_meaning: "Оплата есть, ожидается корректное закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "receivable_closed",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_label: "Дебиторка закрыта",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["closing_document_linked"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
business_meaning: "Дебиторская позиция закрыта корректно."
|
||||
},
|
||||
{
|
||||
state_code: "stale_receivable",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_label: "Дебиторка зависла",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["unresolved_settlement"],
|
||||
exit_conditions: ["receivable_closed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
business_meaning: "Позиция остается незавершенной дольше ожидаемого."
|
||||
}
|
||||
],
|
||||
transitions: [
|
||||
@@ -189,7 +275,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["payment_document_exists"],
|
||||
optional_evidence: [],
|
||||
forbidden_conditions: [],
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
business_meaning: "После реализации ожидается оплата/зачет."
|
||||
},
|
||||
{
|
||||
from_state: "payment_recorded",
|
||||
@@ -198,7 +284,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
required_evidence: ["closing_document_linked"],
|
||||
optional_evidence: ["register_movement_exists"],
|
||||
forbidden_conditions: ["cross_branch_inconsistency"],
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
business_meaning: "Оплата должна завершаться корректным закрытием расчета."
|
||||
}
|
||||
],
|
||||
defects: []
|
||||
@@ -210,43 +296,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "recognized",
|
||||
state_label: "РБП признан",
|
||||
state_label: "РБП признан",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["deferred_expense_created"],
|
||||
exit_conditions: ["writeoff_started"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
business_meaning: "РБП поставлен на учет."
|
||||
},
|
||||
{
|
||||
state_code: "partially_written_off",
|
||||
state_label: "Частичное списание",
|
||||
state_label: "Частичное списание",
|
||||
state_class: "active",
|
||||
entry_conditions: ["partial_writeoff_exists"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Списание идет по графику."
|
||||
business_meaning: "Списание идет по графику."
|
||||
},
|
||||
{
|
||||
state_code: "fully_written_off",
|
||||
state_label: "РБП полностью списан",
|
||||
state_label: "РБП полностью списан",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["full_writeoff_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
business_meaning: "РБП завершил lifecycle."
|
||||
},
|
||||
{
|
||||
state_code: "overdue_writeoff",
|
||||
state_label: "Просроченное списание",
|
||||
state_label: "Просроченное списание",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_boundary", "missing_link"],
|
||||
exit_conditions: ["fully_written_off"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
business_meaning: "РБП живет дольше допустимого окна."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -259,53 +345,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "capitalized",
|
||||
state_label: "Капвложения отражены",
|
||||
state_label: "Капвложения отражены",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["capitalization_document_exists"],
|
||||
exit_conditions: ["accepted_for_accounting"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
business_meaning: "Объект зафиксирован как вложение."
|
||||
},
|
||||
{
|
||||
state_code: "accepted_for_accounting",
|
||||
state_label: "Принят к учету",
|
||||
state_label: "Принят к учету",
|
||||
state_class: "active",
|
||||
entry_conditions: ["acceptance_document_exists"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
business_meaning: "Объект переведен в основной контур учета."
|
||||
},
|
||||
{
|
||||
state_code: "depreciation_active",
|
||||
state_label: "Амортизация активна",
|
||||
state_label: "Амортизация активна",
|
||||
state_class: "active",
|
||||
entry_conditions: ["depreciation_register_movement"],
|
||||
exit_conditions: ["disposed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
business_meaning: "Жизненный цикл ОС идет штатно."
|
||||
},
|
||||
{
|
||||
state_code: "contradictory_asset_state",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_label: "Противоречивый статус ОС",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["posting_mismatch_or_wrong_path"],
|
||||
exit_conditions: ["depreciation_active"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
business_meaning: "Статус ОС формально есть, но смыслово противоречив."
|
||||
},
|
||||
{
|
||||
state_code: "disposed",
|
||||
state_label: "Выбыл",
|
||||
state_label: "Выбыл",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["disposal_document_exists"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
business_meaning: "Жизненный цикл ОС завершен."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -318,43 +404,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "vat_registered",
|
||||
state_label: "НДС отражен документно",
|
||||
state_label: "НДС отражен документно",
|
||||
state_class: "initial",
|
||||
entry_conditions: ["invoice_registered"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
business_meaning: "Сформирован первичный документный слой НДС."
|
||||
},
|
||||
{
|
||||
state_code: "vat_reflected",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_label: "НДС отражен в учете",
|
||||
state_class: "active",
|
||||
entry_conditions: ["vat_register_movement"],
|
||||
exit_conditions: ["vat_deducted"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
business_meaning: "НДС проходит штатную стадию отражения."
|
||||
},
|
||||
{
|
||||
state_code: "vat_deducted",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_label: "НДС принят к вычету",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["deduction_confirmed"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
business_meaning: "НДС-цепочка завершена корректно."
|
||||
},
|
||||
{
|
||||
state_code: "vat_conflict",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_label: "Конфликт НДС-цепочки",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["cross_branch_inconsistency"],
|
||||
exit_conditions: ["vat_reflected"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
business_meaning: "Бухгалтерская и налоговая ветки расходятся."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -367,53 +453,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
|
||||
states: [
|
||||
{
|
||||
state_code: "preclose_checks",
|
||||
state_label: "Предзакрытие",
|
||||
state_label: "Предзакрытие",
|
||||
state_class: "active",
|
||||
entry_conditions: ["period_scope_detected"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Идет проверка готовности периода."
|
||||
business_meaning: "Рдет проверка готовности периода."
|
||||
},
|
||||
{
|
||||
state_code: "close_ready",
|
||||
state_label: "Готов к закрытию",
|
||||
state_label: "Готов к закрытию",
|
||||
state_class: "active",
|
||||
entry_conditions: ["no_blockers_detected"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период может быть закрыт."
|
||||
business_meaning: "Период может быть закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_completed",
|
||||
state_label: "Закрытие завершено",
|
||||
state_label: "Закрытие завершено",
|
||||
state_class: "terminal",
|
||||
entry_conditions: ["close_operation_done"],
|
||||
exit_conditions: [],
|
||||
is_terminal: true,
|
||||
is_problematic: false,
|
||||
business_meaning: "Период закрыт."
|
||||
business_meaning: "Период закрыт."
|
||||
},
|
||||
{
|
||||
state_code: "close_blocked",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_label: "Закрытие заблокировано",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["period_close_risk_or_stale_state"],
|
||||
exit_conditions: ["close_ready"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
|
||||
},
|
||||
{
|
||||
state_code: "close_contradicted",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_label: "Закрыт формально, но с противоречием",
|
||||
state_class: "problematic",
|
||||
entry_conditions: ["misclosed_or_cross_branch_conflict"],
|
||||
exit_conditions: ["close_completed"],
|
||||
is_terminal: false,
|
||||
is_problematic: true,
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
|
||||
}
|
||||
],
|
||||
transitions: [],
|
||||
@@ -426,7 +512,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "missing_expected_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
business_meaning: "Ожидаемый переход не произошел.",
|
||||
evidence_requirements: ["expected_state", "missing_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -434,7 +520,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "invalid_transition",
|
||||
defect_class: "path",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
business_meaning: "Переход произошел по некорректному пути.",
|
||||
evidence_requirements: ["invalid_transition_signal"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -442,7 +528,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "stale_active_state",
|
||||
defect_class: "timing",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
business_meaning: "Объект завис в активном состоянии.",
|
||||
evidence_requirements: ["stale_marker", "missing_transition_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -450,7 +536,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "contradictory_state",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
business_meaning: "Статусы объекта противоречат друг другу.",
|
||||
evidence_requirements: ["contradiction_signal"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -458,7 +544,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "premature_terminal_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
business_meaning: "Терминальное состояние наступило преждевременно.",
|
||||
evidence_requirements: ["terminal_state", "missing_required_previous_state"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -466,7 +552,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "misclosed_state",
|
||||
defect_class: "closure",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
business_meaning: "Контур формально закрыт, но закрыт неверно.",
|
||||
evidence_requirements: ["wrong_closure_path"],
|
||||
period_impact_potential: "direct"
|
||||
},
|
||||
@@ -474,7 +560,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "orphan_intermediate_state",
|
||||
defect_class: "path",
|
||||
severity_hint: "medium",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
|
||||
evidence_requirements: ["intermediate_state_without_next"],
|
||||
period_impact_potential: "indirect"
|
||||
},
|
||||
@@ -482,7 +568,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
|
||||
defect_code: "cross_branch_state_conflict",
|
||||
defect_class: "consistency",
|
||||
severity_hint: "high",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
business_meaning: "Состояния соседних веток учета противоречат друг другу.",
|
||||
evidence_requirements: ["cross_branch_conflict_signal"],
|
||||
period_impact_potential: "direct"
|
||||
}
|
||||
@@ -502,6 +588,23 @@ class LifecycleRegistryImpl {
|
||||
public getDomain(domain: LifecycleDomain): LifecycleDomainModel {
|
||||
return this.models[domain];
|
||||
}
|
||||
|
||||
public hasState(domain: LifecycleDomain, stateCode: string | null | undefined): boolean {
|
||||
const model = this.getDomain(domain);
|
||||
return Boolean(resolveStateCode(model, stateCode));
|
||||
}
|
||||
|
||||
public resolveDefaultExpectedState(domain: LifecycleDomain): string {
|
||||
return defaultExpectedState(this.getDomain(domain));
|
||||
}
|
||||
|
||||
public resolveInitialState(domain: LifecycleDomain): string {
|
||||
return defaultInitialState(this.getDomain(domain));
|
||||
}
|
||||
|
||||
public findExpectedPath(domain: LifecycleDomain, fromState: string, toState: string): string[] | null {
|
||||
return shortestExpectedPath(this.getDomain(domain), fromState, toState);
|
||||
}
|
||||
}
|
||||
|
||||
export const LifecycleRegistry = new LifecycleRegistryImpl(LIFECYCLE_DOMAIN_MODELS);
|
||||
@@ -524,30 +627,88 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
if (includesAny(unitTokens, [/\bnds\b/, /\bvat\b/, /\btax\b/, /cross[_\s-]?branch/, /\b19\b/, /\b68\b/])) {
|
||||
return "vat_flow";
|
||||
}
|
||||
if (includesAny(unitTokens, [/\bperiod\b/, /\bclose\b/, /закрыт/, /reporting/]) || input.unit.problem_unit_type === "period_risk_cluster") {
|
||||
return "period_close";
|
||||
}
|
||||
if (includesAny(unitTokens, [/deferred/, /writeoff/, /рбп/, /\b97\b/])) {
|
||||
const hasVatMarkers = includesAny(unitTokens, [
|
||||
/domain_hint:vat_flow/,
|
||||
/\binvoice_to_vat\b/,
|
||||
/\bvat_chain_conflict\b/,
|
||||
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
|
||||
/(^|[^a-z0-9])vat([^a-z0-9]|$)/,
|
||||
/(^|[^a-z0-9])tax(?:es)?([^a-z0-9]|$)/,
|
||||
/\baccount[_:\s-]?(19|68)\b/
|
||||
]);
|
||||
const hasDeferredMarkers = includesAny(unitTokens, [
|
||||
/domain_hint:deferred_expense/,
|
||||
/\bdeferred(?:_expense)?\b/,
|
||||
/\bdeferred_expense_to_writeoff\b/,
|
||||
/\bwriteoff\b/,
|
||||
/\bpartially_written_off\b/,
|
||||
/\bfully_written_off\b/,
|
||||
/\baccount[_:\s-]?97\b/
|
||||
]);
|
||||
const hasFixedAssetMarkers = includesAny(unitTokens, [
|
||||
/domain_hint:fixed_asset/,
|
||||
/\bfixed[_\s-]?asset(?:s)?\b/,
|
||||
/\basset_card_to_depreciation\b/,
|
||||
/\bdepreciation(?:_active)?\b/,
|
||||
/\baccepted_for_accounting\b/,
|
||||
/\bcapitalized\b/,
|
||||
/\baccount[_:\s-]?(01|02|08)\b/
|
||||
]);
|
||||
const hasPeriodCloseMarkers = includesAny(unitTokens, [
|
||||
/domain_hint:period_close/,
|
||||
/\bperiod[_\s-]?close\b/,
|
||||
/\bperiod_close_risk\b/,
|
||||
/\bclose[_\s-]?risk\b/,
|
||||
/\bclosure[_\s-]?risk\b/,
|
||||
/\bpreclose\b/,
|
||||
/\bmonth[_\s-]?close\b/,
|
||||
/\bperiod_risk\b/
|
||||
]);
|
||||
|
||||
if (hasDeferredMarkers) {
|
||||
return "deferred_expense";
|
||||
}
|
||||
if (includesAny(unitTokens, [/fixed[_\s-]?asset/, /амортиз/, /ос\b/, /\b01\b/, /\b02\b/, /\b08\b/])) {
|
||||
if (hasFixedAssetMarkers) {
|
||||
return "fixed_asset";
|
||||
}
|
||||
if (includesAny(unitTokens, [/buyer/, /customer/, /дебитор/, /\b62\b/])) {
|
||||
if (hasVatMarkers) {
|
||||
return "vat_flow";
|
||||
}
|
||||
|
||||
if (
|
||||
hasPeriodCloseMarkers ||
|
||||
input.unit.problem_unit_type === "period_risk_cluster" ||
|
||||
input.unit.period_impact?.impact_class === "close_risk"
|
||||
) {
|
||||
return "period_close";
|
||||
}
|
||||
if (includesAny(unitTokens, [/buyer/, /customer/, /\b62\b/])) {
|
||||
return "customer_settlement";
|
||||
}
|
||||
if (
|
||||
includesAny(unitTokens, [
|
||||
/domain_hint:bank_settlement/,
|
||||
/\bpayment_to_settlement\b/,
|
||||
/\bstatement_to_document\b/,
|
||||
/\bbank_recorded\b/,
|
||||
/\binitiated_payment\b/,
|
||||
/\bsettlement(?:_closed)?\b/
|
||||
]) ||
|
||||
input.unit.problem_unit_type === "unresolved_settlement_cluster" ||
|
||||
input.unit.problem_unit_type === "broken_chain_segment"
|
||||
) {
|
||||
return "bank_settlement";
|
||||
}
|
||||
if (input.unit.problem_unit_type === "cross_branch_inconsistency_cluster") {
|
||||
return "vat_flow";
|
||||
}
|
||||
if (input.unit.problem_unit_type === "lifecycle_anomaly_node") {
|
||||
return "deferred_expense";
|
||||
}
|
||||
return "bank_settlement";
|
||||
}
|
||||
|
||||
function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInput): string {
|
||||
const explicitActual = input.unit.actual_state?.trim();
|
||||
if (explicitActual) {
|
||||
return explicitActual;
|
||||
}
|
||||
|
||||
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).map((item) => item.toLowerCase());
|
||||
const relations = input.candidates.flatMap((item) => item.relation_pattern_hits).map((item) => item.toLowerCase());
|
||||
|
||||
@@ -573,7 +734,7 @@ function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInpu
|
||||
if (domain === "fixed_asset") {
|
||||
if (hasInvalid) return "contradictory_asset_state";
|
||||
if (hasToken(relations, /depreciation|amort/)) return "depreciation_active";
|
||||
if (hasToken(relations, /accept|учет/)) return "accepted_for_accounting";
|
||||
if (hasToken(relations, /accept|account/)) return "accepted_for_accounting";
|
||||
return "capitalized";
|
||||
}
|
||||
if (domain === "vat_flow") {
|
||||
@@ -587,27 +748,51 @@ function inferCurrentState(domain: LifecycleDomain, input: LifecycleResolverInpu
|
||||
return "preclose_checks";
|
||||
}
|
||||
|
||||
function inferExpectedState(domain: LifecycleDomain, input: LifecycleResolverInput): string {
|
||||
function inferExpectedState(domain: LifecycleDomain, input: LifecycleResolverInput, model: LifecycleDomainModel): string {
|
||||
const explicitExpected = input.unit.expected_state?.trim();
|
||||
if (explicitExpected) {
|
||||
return explicitExpected;
|
||||
}
|
||||
return defaultExpectedState(domain);
|
||||
return defaultExpectedState(model);
|
||||
}
|
||||
|
||||
function inferMissingTransition(input: LifecycleResolverInput): string | null {
|
||||
function inferMissingTransition(
|
||||
input: LifecycleResolverInput,
|
||||
model: LifecycleDomainModel,
|
||||
currentState: string,
|
||||
expectedState: string
|
||||
): string | null {
|
||||
if (typeof input.unit.failed_expected_edge === "string" && input.unit.failed_expected_edge.trim().length > 0) {
|
||||
return input.unit.failed_expected_edge.trim();
|
||||
}
|
||||
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
|
||||
if (/(missing_link|no_continuation|broken_lifecycle|tail|unresolved)/.test(anomalies)) {
|
||||
return "expected_transition_not_observed";
|
||||
if (!/(missing_link|no_continuation|broken_lifecycle|tail|unresolved)/.test(anomalies)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
if (currentState !== expectedState) {
|
||||
const path = shortestExpectedPath(model, currentState, expectedState);
|
||||
if (path && path.length >= 2) {
|
||||
return transitionEdgeLabel(path[0], path[1]);
|
||||
}
|
||||
}
|
||||
const directExpected = model.transitions.find(
|
||||
(transition) => transition.transition_type === "expected" && transition.from_state === currentState
|
||||
);
|
||||
if (directExpected) {
|
||||
return transitionEdgeLabel(directExpected.from_state, directExpected.to_state);
|
||||
}
|
||||
return "expected_transition_not_observed";
|
||||
}
|
||||
|
||||
function inferInvalidTransition(input: LifecycleResolverInput): string | null {
|
||||
function inferInvalidTransition(input: LifecycleResolverInput, model: LifecycleDomainModel): string | null {
|
||||
const anomalies = input.candidates.flatMap((item) => item.anomaly_patterns).join(" ").toLowerCase();
|
||||
for (const transition of model.transitions) {
|
||||
for (const forbiddenCondition of transition.forbidden_conditions) {
|
||||
if (anomalies.includes(forbiddenCondition.toLowerCase())) {
|
||||
return `${transitionEdgeLabel(transition.from_state, transition.to_state)}:forbidden:${forbiddenCondition}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (/(cross_branch|cross_domain_inconsistency)/.test(anomalies)) {
|
||||
return "cross_branch_conflict_transition";
|
||||
}
|
||||
@@ -653,6 +838,14 @@ export function classifyLifecycleDefect(input: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function registryBackedDefect(domain: LifecycleDomain, defect: LifecycleDefectType | null): LifecycleDefectType | null {
|
||||
if (!defect) {
|
||||
return null;
|
||||
}
|
||||
const model = LifecycleRegistry.getDomain(domain);
|
||||
return model.defects.some((definition) => definition.defect_code === defect) ? defect : null;
|
||||
}
|
||||
|
||||
function resolutionConfidence(unitConfidence: ProblemConfidence, input: {
|
||||
hasExplicitStates: boolean;
|
||||
hasDefectSignal: boolean;
|
||||
@@ -690,32 +883,47 @@ function lifecycleInterpretation(input: {
|
||||
missingTransition: string | null;
|
||||
invalidTransition: string | null;
|
||||
}): string {
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
|
||||
if (input.defect === "stale_active_state") {
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
|
||||
}
|
||||
if (input.defect === "misclosed_state") {
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
|
||||
}
|
||||
if (input.defect === "cross_branch_state_conflict") {
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
|
||||
}
|
||||
if (input.defect === "missing_expected_transition") {
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
|
||||
}
|
||||
if (input.defect === "invalid_transition") {
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
|
||||
}
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
|
||||
}
|
||||
|
||||
export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution {
|
||||
const lifecycle_domain = inferLifecycleDomain(input);
|
||||
const currentState = inferCurrentState(lifecycle_domain, input);
|
||||
const expectedState = inferExpectedState(lifecycle_domain, input);
|
||||
const missingTransition = inferMissingTransition(input);
|
||||
const invalidTransition = inferInvalidTransition(input);
|
||||
const defect = classifyLifecycleDefect({
|
||||
const model = LifecycleRegistry.getDomain(lifecycle_domain);
|
||||
|
||||
const inferredCurrentState = inferCurrentState(lifecycle_domain, input);
|
||||
const inferredExpectedState = inferExpectedState(lifecycle_domain, input, model);
|
||||
|
||||
const explicitActualState = input.unit.actual_state?.trim() ?? null;
|
||||
const explicitExpectedState = input.unit.expected_state?.trim() ?? null;
|
||||
|
||||
const explicitCurrentState = resolveStateCode(model, explicitActualState);
|
||||
const explicitExpectedResolved = resolveStateCode(model, explicitExpectedState);
|
||||
|
||||
const inferredCurrentResolved = resolveStateCode(model, inferredCurrentState);
|
||||
const inferredExpectedResolved = resolveStateCode(model, inferredExpectedState);
|
||||
|
||||
const currentState = explicitCurrentState ?? inferredCurrentResolved ?? defaultInitialState(model);
|
||||
const expectedState = explicitExpectedResolved ?? inferredExpectedResolved ?? defaultExpectedState(model);
|
||||
|
||||
const missingTransition = inferMissingTransition(input, model, currentState, expectedState);
|
||||
const invalidTransition = inferInvalidTransition(input, model);
|
||||
const detectedDefect = classifyLifecycleDefect({
|
||||
domain: lifecycle_domain,
|
||||
currentState,
|
||||
expectedState,
|
||||
@@ -723,19 +931,23 @@ export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolu
|
||||
invalidTransition,
|
||||
periodCloseSensitive: input.unit.period_impact?.impact_class === "close_risk"
|
||||
});
|
||||
const defect = registryBackedDefect(lifecycle_domain, detectedDefect);
|
||||
const evidenceIds = uniqueStrings(input.unit.evidence_pack, 8);
|
||||
const previousStates = resolvePreviousStates(model, currentState);
|
||||
const limitations = uniqueStrings(
|
||||
[
|
||||
...input.unit.snapshot_limitations,
|
||||
...(input.candidates.some((item) => item.confidence_hint === "low") ? ["low_confidence_candidates_present"] : []),
|
||||
...(input.unit.actual_state ? [] : ["actual_state_inferred"]),
|
||||
...(input.unit.expected_state ? [] : ["expected_state_inferred"])
|
||||
...(explicitActualState && !explicitCurrentState ? ["actual_state_not_in_registry_normalized"] : []),
|
||||
...(explicitExpectedState && !explicitExpectedResolved ? ["expected_state_not_in_registry_normalized"] : []),
|
||||
...(explicitCurrentState ? [] : ["actual_state_inferred"]),
|
||||
...(explicitExpectedResolved ? [] : ["expected_state_inferred"])
|
||||
],
|
||||
8
|
||||
);
|
||||
|
||||
const confidence = resolutionConfidence(input.unit.confidence, {
|
||||
hasExplicitStates: Boolean(input.unit.actual_state || input.unit.expected_state),
|
||||
hasExplicitStates: Boolean(explicitCurrentState || explicitExpectedResolved),
|
||||
hasDefectSignal: Boolean(defect || missingTransition || invalidTransition),
|
||||
candidateCount: input.candidates.length,
|
||||
hasSnapshotLimitations: limitations.length > 0
|
||||
@@ -746,7 +958,7 @@ export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolu
|
||||
lifecycle_domain,
|
||||
resolved_current_state: currentState,
|
||||
resolved_expected_state: expectedState,
|
||||
resolved_previous_states: [],
|
||||
resolved_previous_states: previousStates,
|
||||
missing_transitions: missingTransition ? [missingTransition] : [],
|
||||
invalid_transitions: invalidTransition ? [invalidTransition] : [],
|
||||
detected_defects: defect ? [defect] : [],
|
||||
|
||||
@@ -100,7 +100,7 @@ function extractAccounts(text: string): string[] {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const explicitAccounts = new Set<string>();
|
||||
const contextualPattern =
|
||||
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
|
||||
/(?:\bсч(?:е|ё)т(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextual: RegExpExecArray | null = null;
|
||||
while ((contextual = contextualPattern.exec(lower)) !== null) {
|
||||
if (contextual[1]) {
|
||||
@@ -322,13 +322,15 @@ function buildFragmentV2(rawText: string, index: number): NormalizedFragmentV2 |
|
||||
}
|
||||
|
||||
const inScopeTokens =
|
||||
/(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|ндс|амортиз|расходы будущих периодов|рбп|ос|контрагент|оплат|банк|выписк|склад|товар|материал)/i.test(
|
||||
/(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|сч(?:е|ё)т|ндс|амортиз|расходы будущих периодов|рбп|ос|контрагент|оплат|банк|выписк|склад|товар|материал|списани|жизненн|цикл|переход|lifecycle|writeoff|deferred)/i.test(
|
||||
lower
|
||||
);
|
||||
const translitInScopeTokens =
|
||||
/\b(?:schet|scheta|schetu|schetom|postavsh|kontragent|dokument|doc|oplata|oplati|platezh|vypisk|provodk|realiz|postuplen|nds|os|saldo|hvost|tail|anomali|risk|zakryt)\b/i.test(
|
||||
/\b(?:schet|scheta|schetu|schetom|postavsh|kontragent|dokument|doc|oplata|oplati|platezh|vypisk|provodk|realiz|postuplen|nds|os|saldo|hvost|tail|anomali|risk|zakryt|lifecycle|state|transition|writeoff|deferred|periodclose)\b/i.test(
|
||||
lower
|
||||
);
|
||||
const lifecycleInScopeTokens =
|
||||
/(lifecycle|жизненн(?:ого|ый)?\s+цикл|стади|переход|списани|writeoff|deferred|period\s*close)/i.test(lower);
|
||||
const genericAccountingTokens = /(фсбу|налогов(ый|ого)|нк рф|закон|форма отчетности|как правильно в бухгалтерии)/i.test(lower);
|
||||
const offTopicTokens = /(погода|анекдот|музык|фильм|игр[аы]|рецепт|курс валют в мире)/i.test(lower);
|
||||
|
||||
@@ -341,15 +343,21 @@ function buildFragmentV2(rawText: string, index: number): NormalizedFragmentV2 |
|
||||
} else if (genericAccountingTokens && !inScopeTokens && !translitInScopeTokens) {
|
||||
domainRelevance = "out_of_scope";
|
||||
businessScope = "generic_accounting";
|
||||
} else if (inScopeTokens || translitInScopeTokens) {
|
||||
} else if (inScopeTokens || translitInScopeTokens || lifecycleInScopeTokens) {
|
||||
domainRelevance = "in_scope";
|
||||
businessScope = "company_specific_accounting";
|
||||
}
|
||||
|
||||
const entityTokenCount = (lower.match(/(документ|оплат|проводк|контрагент|договор|реализац|поступлен|выписк|закрыт|взаиморасчет|склад|товар|материал)/g) ?? [])
|
||||
const entityTokenCount = (
|
||||
lower.match(
|
||||
/(документ|оплат|проводк|контрагент|договор|реализац|поступлен|выписк|закрыт|взаиморасчет|склад|товар|материал|поставщ|покупат|списани|жизненн|цикл)/g
|
||||
) ?? []
|
||||
)
|
||||
.length;
|
||||
const translitEntityTokenCount = (
|
||||
lower.match(/\b(?:dokument|oplata|platezh|provodk|kontragent|realiz|postuplen|vypisk|zakryt|schet|sklad|tovar|material)\b/g) ?? []
|
||||
lower.match(
|
||||
/\b(?:dokument|oplata|platezh|provodk|kontragent|postavsh|pokupat|realiz|postuplen|vypisk|zakryt|schet|sklad|tovar|material)\b/g
|
||||
) ?? []
|
||||
).length;
|
||||
const entityTokenCountTotal = entityTokenCount + translitEntityTokenCount;
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ export function simulateDeterministicRouting(normalized: V2Family): RouteHintSum
|
||||
const decisions = normalized.fragments.map((fragment) => decideRouteForFragment(fragment));
|
||||
const inScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope").length;
|
||||
const outOfScopeCount = decisions.filter((item) => item.domain_relevance === "out_of_scope").length;
|
||||
const unclearCount = decisions.filter((item) => item.domain_relevance === "unclear").length;
|
||||
const routedInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.route !== "no_route").length;
|
||||
const clarificationInScopeCount = decisions.filter(
|
||||
(item) => item.domain_relevance === "in_scope" && item.execution_readiness === "needs_clarification"
|
||||
@@ -245,7 +246,7 @@ export function simulateDeterministicRouting(normalized: V2Family): RouteHintSum
|
||||
|
||||
let fallbackType: RouteHintSummaryV2["fallback"]["type"] = "none";
|
||||
if (!normalized.message_in_scope || inScopeCount === 0) {
|
||||
fallbackType = "out_of_scope";
|
||||
fallbackType = outOfScopeCount > 0 && unclearCount === 0 ? "out_of_scope" : "clarification";
|
||||
} else if (routedInScopeCount === 0 && clarificationInScopeCount > 0) {
|
||||
fallbackType = "clarification";
|
||||
} else if (routedInScopeCount === 0 && noRouteInScopeCount > 0) {
|
||||
|
||||
Reference in New Issue
Block a user