АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия + доп кля + конфиг дизапйна

This commit is contained in:
2026-04-09 16:32:19 +03:00
parent edfa09c9af
commit 99288c195d
40 changed files with 5174 additions and 853 deletions
+19
View File
@@ -134,9 +134,28 @@ export const TRACES_DIR = path.resolve(DATA_DIR, "traces");
export const PRESETS_DIR = path.resolve(DATA_DIR, "presets");
export const EVAL_CASES_DIR = path.resolve(DATA_DIR, "eval_cases");
export const ASSISTANT_SESSIONS_DIR = path.resolve(DATA_DIR, "assistant_sessions");
export const AUTORUN_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "autorun_annotations");
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
export const AUTORUN_GENERATOR_HISTORY_FILE = path.resolve(AUTORUN_GENERATOR_DIR, "history.json");
export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts");
export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports");
export const EVAL_DATASETS_DIR = path.resolve(MODULE_ROOT, "eval_cases");
export const SCHEMAS_DIR = path.resolve(BACKEND_ROOT, "src", "schemas");
export const ARCH_EXPORT_2020_DIR = path.resolve(MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт");
export const ASSISTANT_CANON_FILE = path.resolve(MODULE_ROOT, "..", "docs", "TECH", "assistant_canon.md");
export const ASSISTANT_CAPABILITIES_REGISTRY_FILE = path.resolve(
MODULE_ROOT,
"..",
"docs",
"TECH",
"capabilities_registry.json"
);
export const MANUAL_CASE_DECISION_SCHEMA_FILE = path.resolve(
MODULE_ROOT,
"..",
"docs",
"TECH",
"manual_case_decision_schema.json"
);
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1,7 +1,17 @@
import "dotenv/config";
import cors from "cors";
import express from "express";
import { PORT, PRESETS_DIR, TRACES_DIR, EVAL_CASES_DIR, REPORTS_DIR, TIMEZONE, ASSISTANT_SESSIONS_DIR } from "./config";
import {
PORT,
PRESETS_DIR,
TRACES_DIR,
EVAL_CASES_DIR,
REPORTS_DIR,
TIMEZONE,
ASSISTANT_SESSIONS_DIR,
AUTORUN_ANNOTATIONS_DIR,
AUTORUN_GENERATOR_DIR
} from "./config";
import { buildAccountingAgentRouter } from "./routes/accountingAgent";
import { buildAssistantRouter } from "./routes/assistant";
import { buildAutoRunsRouter } from "./routes/autoRuns";
@@ -27,6 +37,8 @@ export function createApp(): express.Express {
ensureDir(EVAL_CASES_DIR);
ensureDir(REPORTS_DIR);
ensureDir(ASSISTANT_SESSIONS_DIR);
ensureDir(AUTORUN_ANNOTATIONS_DIR);
ensureDir(AUTORUN_GENERATOR_DIR);
const app = express();
app.use(cors());
@@ -1368,29 +1368,29 @@ function buildProblemCentricActions(input: {
}
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("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
}
}
@@ -1400,21 +1400,21 @@ function buildProblemCentricActions(input: {
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);
@@ -1437,25 +1437,25 @@ function buildProblemCentricClarifications(input: {
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
}
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);
@@ -1641,13 +1641,13 @@ function detectMissingAnchors(
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(
/(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(
lower
) || hasAccountAnchorInRetrieval(retrievalResults);
const hasDocumentOrObject =
/(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
/(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return {
period: !hasPeriod,
@@ -1674,19 +1674,19 @@ function buildClarificationQuestions(input: {
questions.push("Уточните период проверки (например, июль 2020).");
}
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);
@@ -1701,31 +1701,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);
@@ -0,0 +1,38 @@
import fs from "fs";
import { ASSISTANT_CANON_FILE } from "../config";
const FALLBACK_CANON = [
"Не выдумывай возможности.",
"Не обещай настройку 1С и админ-действия.",
"Не показывай внутренние технические термины пользователю.",
"Говори по-человечески и предлагай ближайший полезный поддерживаемый шаг."
].join(" ");
let cache: { mtimeMs: number; excerpt: string } | null = null;
function stripMarkdown(input: string): string {
return input
.replace(/^#{1,6}\s+/gm, "")
.replace(/[`*_>\-\[\]\(\)]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export function loadAssistantCanonExcerpt(maxChars = 900): string {
try {
const mtimeMs = fs.existsSync(ASSISTANT_CANON_FILE) ? fs.statSync(ASSISTANT_CANON_FILE).mtimeMs : -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.excerpt;
}
if (!fs.existsSync(ASSISTANT_CANON_FILE)) {
return FALLBACK_CANON;
}
const raw = fs.readFileSync(ASSISTANT_CANON_FILE, "utf-8");
const normalized = stripMarkdown(raw);
const excerpt = normalized.length > maxChars ? `${normalized.slice(0, maxChars).trim()}...` : normalized;
cache = { mtimeMs, excerpt: excerpt || FALLBACK_CANON };
return cache.excerpt;
} catch {
return cache?.excerpt ?? FALLBACK_CANON;
}
}
@@ -731,8 +731,8 @@ const P0_DOMAIN_CARDS: P0DomainCard[] = [
/закрыт[а-яё]*\s+период/i,
/close\s+operation/i,
/allocation/i,
/закр/i,
/перио/i,
/закр/i,
/перио/i,
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
@@ -756,14 +756,14 @@ function parseDateCandidate(value: unknown): number | null {
function extractDate(record: SnapshotRecord): string | null {
const attrs = record.attributes ?? {};
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
for (const key of directKeys) {
if (attrs[key] !== undefined && attrs[key] !== null) {
return String(attrs[key]);
}
}
for (const [key, value] of Object.entries(attrs)) {
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
return value;
}
}
@@ -795,7 +795,7 @@ function countNavigationLinks(record: SnapshotRecord): number {
function findCounterpartyLinks(record: SnapshotRecord): SnapshotLink[] {
return record.links.filter(
(link) =>
link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field)
link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field)
);
}
@@ -1723,14 +1723,14 @@ function inferPeriodScope(fragmentText: string): SemanticPeriodScope {
granularity: "year"
};
}
if (/квартал|quarter/i.test(fragmentText)) {
if (/квартал|quarter/i.test(fragmentText)) {
return {
from: null,
to: null,
granularity: "quarter"
};
}
if (/месяц|month|период/i.test(fragmentText)) {
if (/месяц|month|период/i.test(fragmentText)) {
return {
from: null,
to: null,
@@ -1808,7 +1808,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
const rankingBasis: string[] = ["closure_risk", "repeatability", "financial_impact"];
const explanationFocus: string[] = ["why_selected", "where_chain_breaks", "what_business_risk"];
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
pushMany(domainScope, ["bank", "settlements"]);
pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
@@ -1823,22 +1823,22 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
/(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(
lower
) ||
(/закр/i.test(lower) && /перио/i.test(lower));
(/закр/i.test(lower) && /перио/i.test(lower));
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["suppliers", "settlements"]);
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
}
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["customers", "settlements"]);
pushMany(documentTypes, ["sales_document", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
}
if (
/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(
/ндс|vat|книга\s+покупок|книга\s+продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(
lower
) ||
hasVatAccountScope
@@ -1849,7 +1849,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (
/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
lower
) ||
hasFixedAssetAccountScope
@@ -1860,7 +1860,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
}
if (
/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
/рбп|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
hasDeferredExpenseAccountScope
) {
pushMany(domainScope, ["deferred_expense", "period_close"]);
@@ -1868,17 +1868,17 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(entityTypes, ["document", "posting"]);
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
}
if (/цепоч|разрыв|СЃРІСЏР·|документ.*РїСЂРѕРІРѕРґ|РіРґРµ рвет|Р¶РёРІСѓС‚ отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
if (/цепоч|разрыв|связ|документ.*провод|где рвет|живут отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]);
}
if (/аномал|СЂРёСЃРє|С…РІРѕСЃС‚|РїРѕРґРѕР·СЂ|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
if (/аномал|риск|хвост|подозр|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]);
}
if (WRONG_DOCUMENT_MARKERS.test(lower)) {
pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]);
}
if (/Р¶РёРІСѓС‚ отдельно|РЅРµ СЃРІСЏР·|без СЃРІСЏР·Рё|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
if (/живут отдельно|не связ|без связи|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
}
if (REPEATED_ANOMALY_MARKERS.test(lower)) {
@@ -1890,10 +1890,10 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
pushMany(documentTypes, ["period_close_document"]);
}
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) {
if (/не\s+в\s+платеже|not payment/i.test(lower)) {
pushMany(excludedInterpretations, ["simple_payment_delay"]);
}
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) {
if (/не\s+по\s+сумм|не\s+сумм|не\s+сумма|not by amount/i.test(lower)) {
pushMany(excludedInterpretations, ["amount_only_anomaly"]);
pushMany(rankingBasis, ["amount_independent_risk"]);
}
@@ -2288,16 +2288,16 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
accounts.push(token.split(".")[0]);
}
for (const key of Object.keys(record.attributes ?? {})) {
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
accounts.push("51");
}
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
accounts.push("60");
}
if (/счетучетандс/i.test(key)) {
if (/счетучетандс/i.test(key)) {
accounts.push("19");
}
if (/субконтодт/i.test(key)) {
if (/субконтодт/i.test(key)) {
accounts.push("60");
}
}
@@ -2306,28 +2306,28 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
function inferDocumentTypesFromRecord(record: SnapshotRecord, corpus: string): string[] {
const items: string[] = [];
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
pushMany(items, ["bank_statement", "payment_order"]);
}
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
items.push("supplier_receipt");
}
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
items.push("sales_document");
}
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
pushMany(items, ["invoice", "vat_document"]);
}
if (/корректировк|ручн|manual/i.test(corpus)) {
if (/корректировк|ручн|manual/i.test(corpus)) {
items.push("manual_operation");
}
if (/закрытие|регламент/i.test(corpus)) {
if (/закрытие|регламент/i.test(corpus)) {
items.push("period_close_document");
}
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
pushMany(items, ["fixed_asset_card", "depreciation_document"]);
}
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
items.push("deferred_expense_document");
}
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
@@ -2356,11 +2356,11 @@ function inferDomainsFromRecord(corpus: string, documentTypes: string[], record:
if (documentTypes.some((item) => item === "deferred_expense_document")) {
pushMany(domains, ["deferred_expense", "period_close"]);
}
if (/закрытие|регламент|period close/i.test(corpus)) {
if (/закрытие|регламент|period close/i.test(corpus)) {
domains.push("period_close");
}
const hasSettlementLexicalAnchor =
/(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(
/(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(
corpus
);
const hasSettlementDocAnchor = documentTypes.some(
@@ -2386,13 +2386,13 @@ function inferEntityTypes(record: SnapshotRecord): string[] {
entities.push("counterparty");
}
const corpus = collectTextFromRecord(record);
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) {
if (/договор|contract/i.test(corpus)) {
entities.push("contract");
}
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
entities.push("fixed_asset");
}
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
entities.push("tax_entry");
}
return uniqueStrings(entities);
@@ -2406,25 +2406,25 @@ function inferRelationPatterns(record: SnapshotRecord, corpus: string): string[]
if (hasDocLinks) {
patterns.push("document_to_posting");
}
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
patterns.push("payment_to_settlement");
}
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
patterns.push("statement_to_document");
}
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
patterns.push("asset_card_to_depreciation");
}
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
patterns.push("deferred_expense_to_writeoff");
}
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
patterns.push("invoice_to_vat");
}
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) {
if (/договор|contract/i.test(corpus) && hasDocLinks) {
patterns.push("contract_to_documents");
}
if (/склад|товар|материал|receipt/i.test(corpus)) {
if (/склад|товар|материал|receipt/i.test(corpus)) {
patterns.push("receipt_to_stock_movement");
}
return uniqueStrings(patterns);
@@ -2466,7 +2466,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
anomalies.push("posting_mismatch");
}
if (/ручн|manual|корректировк/.test(corpus)) {
if (/ручн|manual|корректировк/.test(corpus)) {
anomalies.push("manual_intervention_suspicion");
}
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
@@ -2478,7 +2478,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
anomalies.push("silent_orphan");
}
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
if (!hasAmountSignal && anomalies.length > 0) {
anomalies.push("amount_independent_risk");
}
@@ -2530,15 +2530,15 @@ function evaluateExcludedInterpretations(
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (!hasStructural) {
reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
}
}
if (interpretationSet.has("amount_only_anomaly")) {
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (hasAmountSignal && !hasStructural) {
reasons.push("Исключено как amount-only аномалия без структурных признаков.");
reasons.push("Исключено как amount-only аномалия без структурных признаков.");
}
}
@@ -2611,22 +2611,22 @@ function evaluateRecordAgainstProfile(record: SnapshotRecord, profile: SemanticR
const matchReasons: string[] = [];
if (accountMatch && profile.account_scope.length > 0) {
matchReasons.push("Совпал account_scope.");
matchReasons.push("Совпал account_scope.");
}
if (domainMatch && profile.domain_scope.length > 0) {
matchReasons.push("Совпал domain_scope.");
matchReasons.push("Совпал domain_scope.");
}
if (documentMatch && profile.document_types.length > 0) {
matchReasons.push("Совпал document_types.");
matchReasons.push("Совпал document_types.");
}
if (relationMatch && profile.relation_patterns.length > 0) {
matchReasons.push("Совпали relation_patterns.");
matchReasons.push("Совпали relation_patterns.");
}
if (anomalyMatch && profile.anomaly_patterns.length > 0) {
matchReasons.push("Совпали anomaly_patterns.");
matchReasons.push("Совпали anomaly_patterns.");
}
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
matchReasons.push("Совпал lifecycle_stage_filters.");
matchReasons.push("Совпал lifecycle_stage_filters.");
}
if (graphTraversal.domain_match) {
matchReasons.push("Graph traversal domain matched.");
@@ -2818,7 +2818,7 @@ export class AssistantDataLayer {
business_interpretation: [],
confidence: "low",
limitations: ["Snapshot data files could not be loaded."],
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
};
}
let result: RawRetrievalResult | null = null;
@@ -3718,8 +3718,8 @@ export class AssistantDataLayer {
: "low",
business_interpretation:
group.risk_factors.size > 0
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
relation_types: Array.from(group.relations.entries())
.sort((left, right) => right[1] - left[1])
.map((item) => item[0]),
@@ -3768,24 +3768,24 @@ export class AssistantDataLayer {
evidence: [],
why_included: [],
selection_reason: [
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
guidFilter.length > 0
? "GUID-фильтрация включена."
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
? "GUID-фильтрация включена."
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
`Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
],
risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: [
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
"По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
"Для точечного drilldown добавьте GUID или уточните период/контрагента."
],
confidence: "medium",
limitations: [
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
"Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
"Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся."
],
errors: []
@@ -3833,18 +3833,18 @@ export class AssistantDataLayer {
},
evidence: evidence.slice(0, 12),
why_included: [
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
semanticProfile.account_scope.length > 0
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
`Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.`
],
selection_reason: [
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
"GUID-mode отключен: full scan без ограничителей не использовался.",
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
"GUID-mode отключен: full scan без ограничителей не использовался.",
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.",
`Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
@@ -3852,15 +3852,15 @@ export class AssistantDataLayer {
risk_factors:
aggregatedRiskFactors.length > 0
? aggregatedRiskFactors
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
business_interpretation: [
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
],
confidence: "high",
limitations: [
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
"Источник данных — snapshot 2020 (read-only), не live контур 1С.",
guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
"Источник данных — snapshot 2020 (read-only), не live контур 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся."
],
errors: []
@@ -4177,12 +4177,12 @@ export class AssistantDataLayer {
})),
why_included: items.length > 0
? [
"Показаны сущности с максимальным количеством записей.",
"Показаны сущности с максимальным количеством записей.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
]
: [],
selection_reason: [
"Ранжирование выполнено по records_count по убыванию.",
"Ранжирование выполнено по records_count по убыванию.",
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
],
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
@@ -4191,7 +4191,7 @@ export class AssistantDataLayer {
],
confidence: "medium",
limitations: [
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
],
errors: []
@@ -4354,7 +4354,7 @@ export class AssistantDataLayer {
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
],
risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
confidence: "high",
limitations: [
"Это read-only snapshot, а не онлайн-состояние 1С.",
@@ -4385,11 +4385,11 @@ export class AssistantDataLayer {
},
evidence: [],
why_included: [],
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
risk_factors: [],
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
confidence: "low",
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
errors: []
};
}
@@ -4440,14 +4440,14 @@ export class AssistantDataLayer {
evidence: matches.slice(0, 10),
why_included:
matches.length > 0
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
: [],
selection_reason: [
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
],
risk_factors: [],
business_interpretation: [
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
],
confidence: matches.length > 0 ? "medium" : "low",
limitations: [
@@ -4478,12 +4478,12 @@ export class AssistantDataLayer {
matched_records: matches.length
},
evidence: matches.slice(0, 10),
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
risk_factors: [],
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
confidence: matches.length > 0 ? "high" : "medium",
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
errors: []
};
}
@@ -431,7 +431,7 @@ function parseDateLike(raw: string): string | null {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
}
const rusMonthYear = value.match(
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
);
if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
@@ -530,7 +530,7 @@ function normalizedAnchorFromFragments(normalized: NormalizedPayload | null | un
source: `normalized_time_scope:${type || "unknown"}`
};
}
if (/(?:июл|july|РёСЋР»)/i.test(value)) {
if (/(?:июл|july|июл)/i.test(value)) {
return {
value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}`
@@ -564,7 +564,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
const monthByNamed = /(?:июл|july|июл)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return {
@@ -771,7 +771,7 @@ export function applyTemporalHintToExecutionPlan<
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|РёСЋР»|july/i.test(text)) {
if (/2020-07|июл|июл|july/i.test(text)) {
return item;
}
return {
@@ -819,7 +819,7 @@ export function resolveDomainPolarityGuard(input: {
prefixes.has("62") ||
prefixes.has("51") ||
prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower);
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|хвост)/i.test(lower);
if (!settlementSignal) {
return {
applied: false,
@@ -839,13 +839,13 @@ export function resolveDomainPolarityGuard(input: {
};
}
const supplierScore =
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) +
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0);
(/(?:сч[её]т\s*60|по\s*60|счет\s*60)/i.test(lower) ? 1 : 0);
const customerScore =
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) +
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
(/(?:сч[её]т\s*62|по\s*62|счет\s*62)/i.test(lower) ? 1 : 0);
let polarity: DomainPolarity = "mixed_or_unresolved";
if (supplierScore > 0 || customerScore > 0) {
@@ -903,10 +903,10 @@ export function applyPolarityHintToExecutionPlan<
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) {
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60)/i.test(text)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) {
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62)/i.test(text)) {
return item;
}
return {
@@ -917,11 +917,11 @@ export function applyPolarityHintToExecutionPlan<
}
function containsReceivableSignal(value: string): boolean {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
}
function containsPayableSignal(value: string): boolean {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
}
function problemUnitCorpus(unit: ProblemUnit): string {
@@ -1590,15 +1590,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
? "no_grounded_answer"
: "partial";
const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
@@ -19,6 +19,8 @@ import * as addressFilterExtractor_1 from "./addressFilterExtractor";
import * as predecomposeContract_1 from "./address_runtime/predecomposeContract";
import * as openaiResponsesClient_1 from "./openaiResponsesClient";
import * as addressMcpClient_1 from "./addressMcpClient";
import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
import * as assistantCanon_1 from "./assistantCanon";
import iconv from "iconv-lite";
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
const dataScopeProbeCache = new Map();
@@ -3859,17 +3861,7 @@ function buildLivingChatPrompt(userMessage, conversationWindow) {
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
}
function buildAssistantCapabilityContractReply() {
return [
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею сейчас:",
"1. Находить документы, операции, договоры и остатки по контрагенту/договору/периоду.",
"2. Делать агрегаты по базе: активность, роли контрагентов, top-срезы по суммам и операциям.",
"3. Кратко объяснять результат и подсказывать следующий точный запрос.",
"Что не умею:",
"1. Не настраиваю 1С и не меняю конфигурацию.",
"2. Не создаю и не провожу документы в базе.",
"3. Не выполняю админские действия на сервере."
].join("\n");
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
}
function normalizeScopeLabel(value) {
const repaired = repairAddressMojibake(String(value ?? ""));
@@ -4971,6 +4963,7 @@ export class AssistantService {
else {
const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
@@ -4984,7 +4977,8 @@ export class AssistantService {
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
"Если пользователь спрашивает про возможности, отвечай только по этому контракту."
"Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
`Канон поведения: ${canonExcerpt}`
].join(" "),
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
userMessage: userPrompt,
@@ -0,0 +1,205 @@
import fs from "fs";
import { ASSISTANT_CAPABILITIES_REGISTRY_FILE } from "../config";
export type CapabilityMaturity = "production_ready" | "partial" | "planned" | "deprecated";
export interface CapabilityGroup {
group_code: string;
group_title: string;
description: string;
risk_level: "low" | "medium" | "high";
maturity_status: CapabilityMaturity;
supported_operations: string[];
unsupported_operations: string[];
required_entities: string[];
optional_entities: string[];
typical_queries: string[];
related_routes: string[];
safe_alternatives: string[];
one_c_hints: string[];
}
export interface CapabilityRegistry {
schema_version: string;
updated_at: string;
assistant_mode: "read_only" | "mixed";
groups: CapabilityGroup[];
}
const FALLBACK_REGISTRY: CapabilityRegistry = {
schema_version: "capabilities_registry_fallback_v1",
updated_at: "2026-04-09T00:00:00.000Z",
assistant_mode: "read_only",
groups: [
{
group_code: "vat",
group_title: "НДС",
description: "Срезы и расчеты НДС на базе данных 1С.",
risk_level: "high",
maturity_status: "partial",
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
unsupported_operations: ["submit_tax_declaration"],
required_entities: ["period", "organization"],
optional_entities: ["counterparty"],
typical_queries: ["Сколько НДС к уплате за период?"],
related_routes: [],
safe_alternatives: ["Показать движения по 68/19 за период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "counterparties",
group_title: "Контрагенты",
description: "Документы, операции, договоры и срезы по контрагентам.",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
unsupported_operations: ["edit_counterparty_card"],
required_entities: ["counterparty_scope_or_contract"],
optional_entities: ["period", "organization"],
typical_queries: ["Покажи документы по контрагенту"],
related_routes: [],
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
one_c_hints: ["Справочник.Контрагенты"]
},
{
group_code: "boundaries",
group_title: "Ограничения",
description: "Операции, которые ассистент не выполняет.",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
required_entities: [],
optional_entities: [],
typical_queries: ["Можешь настроить 1С?"],
related_routes: [],
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
one_c_hints: []
}
]
};
let cache: { mtimeMs: number; value: CapabilityRegistry } | null = null;
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function toStringSafe(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function readRegistryFromFile(): CapabilityRegistry | null {
if (!fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)) return null;
try {
const raw = fs.readFileSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
const parsed = JSON.parse(raw) as unknown;
const root = toRecord(parsed);
if (!root) return null;
const groups = toArray(root.groups)
.map((item) => toRecord(item))
.filter((item): item is Record<string, unknown> => item !== null)
.map((item) => ({
group_code: toStringSafe(item.group_code) ?? "unknown_group",
group_title: toStringSafe(item.group_title) ?? "Группа",
description: toStringSafe(item.description) ?? "",
risk_level: (toStringSafe(item.risk_level) as "low" | "medium" | "high" | null) ?? "medium",
maturity_status:
(toStringSafe(item.maturity_status) as CapabilityMaturity | null) ??
("partial" as CapabilityMaturity),
supported_operations: toArray(item.supported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
unsupported_operations: toArray(item.unsupported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
required_entities: toArray(item.required_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
optional_entities: toArray(item.optional_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
typical_queries: toArray(item.typical_queries)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
related_routes: toArray(item.related_routes)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
safe_alternatives: toArray(item.safe_alternatives)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
one_c_hints: toArray(item.one_c_hints)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null)
}));
if (groups.length === 0) return null;
return {
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
assistant_mode: (toStringSafe(root.assistant_mode) as "read_only" | "mixed" | null) ?? "read_only",
groups
};
} catch {
return null;
}
}
export function loadCapabilitiesRegistry(): CapabilityRegistry {
try {
const mtimeMs = fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)
? fs.statSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
: -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.value;
}
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
cache = { mtimeMs, value };
return value;
} catch {
return cache?.value ?? FALLBACK_REGISTRY;
}
}
export function buildCapabilityContractReplyFromRegistry(): string {
const registry = loadCapabilitiesRegistry();
const topGroups = registry.groups.slice(0, 6);
const groupLines = topGroups.map((group, index) => {
const ops = group.supported_operations.slice(0, 3).join(", ");
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
});
return [
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею по группам:",
...groupLines,
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
].join("\n");
}
export function resolveNearestCapabilityGroup(input: { domain?: string | null; queryClass?: string | null }): CapabilityGroup | null {
const registry = loadCapabilitiesRegistry();
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
if (!haystack.trim()) return null;
const scoring: Array<{ group: CapabilityGroup; score: number }> = registry.groups.map((group) => {
let score = 0;
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
if (bucket.includes(token)) score += 1;
}
return { group, score };
});
scoring.sort((a, b) => b.score - a.score);
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
}
@@ -150,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: [
@@ -207,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",
@@ -216,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: []
@@ -228,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: [
@@ -275,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",
@@ -284,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: []
@@ -296,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: [],
@@ -345,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: [],
@@ -404,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: [],
@@ -453,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: [],
@@ -512,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"
},
@@ -520,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"
},
@@ -528,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"
},
@@ -536,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"
},
@@ -544,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"
},
@@ -552,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"
},
@@ -560,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"
},
@@ -568,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"
}
@@ -905,23 +905,23 @@ 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 {
@@ -78,10 +78,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
},
normalizer_v1_1_2_1: {
id: "default-normalizer-v1_1_2_1",
name: "Стандартный пресет NDC v1.1.2.1",
name: "Стандартный пресет NDC v1.1.2.1",
promptVersion: "normalizer_v1_1_2_1",
schemaNotes:
"v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.",
"v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.",
files: {
system: path.join("system", "default.txt"),
developer: path.join("developer", "normalizer_v1_1_2_1.txt"),
@@ -91,10 +91,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
},
normalizer_v2: {
id: "default-normalizer-v2",
name: "Стандартный пресет NDC v2",
name: "Стандартный пресет NDC v2",
promptVersion: "normalizer_v2",
schemaNotes:
"v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
"v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
files: {
system: path.join("system", "default.txt"),
developer: path.join("developer", "normalizer_v2.txt"),
@@ -79,7 +79,7 @@ const AMBIGUITY_PATTERN =
/(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
const DOMAIN_LEXICAL_ANCHOR_PATTERN =
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С‚.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i;
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С‚.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
{