NODEDC_1C/llm_normalizer/backend/dist/services/assistantDataLayer.js

3554 lines
175 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssistantDataLayer = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const BROAD_GENERIC_MARKERS = new RegExp([
"\\boverall\\b",
"\\bgeneral\\b",
"\\bgeneric\\b",
"\\bsummary\\b",
"\\btop\\b",
"\\ball\\s+risks?\\b",
"\\bshow\\s+all\\b",
"\\bwhat\\s+is\\s+wrong\\b",
"\\u0432\\s*\\u0446\\u0435\\u043b\\u043e\\u043c",
"\\u043e\\u0431\\u0449(?:\\u0430\\u044f|\\u0443\\u044e)?\\s+\\u043a\\u0430\\u0440\\u0442\\u0438\\u043d\\u0443?",
"\\u043e\\u0431\\u0437\\u043e\\u0440",
"\\u043f\\u043e\\u043a\\u0430\\u0436\\u0438\\s+\\u0432\\u0441\\u0435",
"\\u0432\\u0441\\u0435\\s+\\u0440\\u0438\\u0441\\u043a\\u0438",
"\\u043e\\u0431\\u0449\\u0438\\u0435\\s+\\u0440\\u0438\\u0441\\u043a\\u0438",
"\\u0442\\u043e\\u043f\\s+\\u0440\\u0438\\u0441\\u043a\\u043e\\u0432",
"\\u0433\\u0434\\u0435\\s+\\u043f\\u0440\\u043e\\u0431\\u043b\\u0435\\u043c\\u044b",
"\\u0447\\u0442\\u043e\\s+\\u043d\\u0435\\s+\\u0442\\u0430\\u043a"
].join("|"), "iu");
const ACCOUNT_SPECIFIC_MARKERS = /(?:\u0441\u0447\u0435\u0442(?:\u0430|\u0443|\u043e\u043c)?|account)\s*[:#]?\s*\d{2}(?:\.\d{2})?/iu;
const PERIOD_MARKERS = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/;
const ENTITY_SPECIFIC_MARKERS = /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|supplier|buyer|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|invoice|posting|register|guid|id[:=\s])/iu;
const EXACT_OBJECT_MARKERS = /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\s*(?:#|\u2116)|\bref\b|\bid\b|trx-\d+|inv-\d+)/iu;
const CONTRACT_MARKERS = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|\u043e\u043c|\u0435)?\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
const DOCUMENT_NUMBER_MARKERS = /(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:\u0430|\u044b))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u044f|\u0438\u0438)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
const AMOUNT_MARKERS = /\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/u;
const ROUTE_MIN_EVIDENCE_GATE = {
hybrid_store_plus_live: {
min_evidence_items: 3,
min_result_items: 2
},
store_feature_risk: {
min_evidence_items: 2,
min_result_items: 2
},
batch_refresh_then_store: {
min_evidence_items: 6,
min_result_items: 8
},
store_canonical: {
min_evidence_items: 2,
min_result_items: 3
},
live_mcp_drilldown: {
min_evidence_items: 1,
min_result_items: 1
}
};
const MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Движения.Период КАК Период,
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) КАК СчетДт,
ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) КАК СчетКт,
Движения.Сумма КАК Сумма
ИЗ
РегистрБухгалтерии.Хозрасчетный КАК Движения
УПОРЯДОЧИТЬ ПО
Движения.Период УБЫВ
`;
const MCP_LIVE_MOVEMENTS_BY_PERIOD_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Движения.Период КАК Период,
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) КАК СчетДт,
ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) КАК СчетКт,
Движения.Сумма КАК Сумма
ИЗ
РегистрБухгалтерии.Хозрасчетный КАК Движения
ГДЕ
Движения.Период МЕЖДУ __FROM_DATETIME__ И __TO_DATETIME__
УПОРЯДОЧИТЬ ПО
Движения.Период УБЫВ
`;
const RBP_REQUIRED_LIVE_CALLS = [
"find_rbp_writeoff_documents_in_period",
"find_rbp_object_movements_account_97",
"find_month_close_entries_linked_to_rbp",
"compute_end_period_residual_by_rbp_object"
];
function pushUniqueLine(target, line) {
if (!target.includes(line)) {
target.push(line);
}
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function valueAsString(value) {
if (value === null || value === undefined) {
return "";
}
return String(value);
}
function parseFiniteNumber(value) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const normalized = value.replace(",", ".").trim();
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function formatIsoDateUtc(date) {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function monthEndFromIso(isoDate) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
if (!Number.isFinite(year) || !Number.isFinite(month)) {
return null;
}
const end = new Date(Date.UTC(year, month, 0));
return formatIsoDateUtc(end);
}
function shiftIsoDate(isoDate, deltaDays) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
if (Number.isNaN(date.getTime())) {
return null;
}
date.setUTCDate(date.getUTCDate() + deltaDays);
return formatIsoDateUtc(date);
}
function toDateTimeExpr(isoDate, endOfDay) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const hour = endOfDay ? 23 : 0;
const minute = endOfDay ? 59 : 0;
const second = endOfDay ? 59 : 0;
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`;
}
function buildLiveRangeQuery(fromIso, toIso, limit) {
const fromExpr = toDateTimeExpr(fromIso, false);
const toExpr = toDateTimeExpr(toIso, true);
if (!fromExpr || !toExpr) {
return MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
}
return MCP_LIVE_MOVEMENTS_BY_PERIOD_QUERY_TEMPLATE.replace("__LIMIT__", String(limit))
.replace("__FROM_DATETIME__", fromExpr)
.replace("__TO_DATETIME__", toExpr);
}
function hasRbpSignal(text) {
return /(?:\brbp\b|рбп|расходы\s+будущих\s+периодов|deferred|writeoff|списани[ея]\s+рбп|account\s*97|счет\s*97)/i.test(String(text ?? "").toLowerCase());
}
function buildLiveMcpCallPlan(route, fragmentText) {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const rbpClaim = hasRbpSignal(fragmentText) ||
semanticProfile.query_subject === "deferred_expense_lifecycle_anomaly" ||
semanticProfile.domain_scope.includes("deferred_expense");
if (!rbpClaim) {
return {
claim_type: null,
query_subject: semanticProfile.query_subject,
required_live_calls: [],
calls: [
{
call_id: "generic_accounting_register_probe",
purpose: "live_overlay_probe",
query: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(config_1.ASSISTANT_MCP_LIVE_LIMIT)),
required_for_claim: false
}
],
route_gap_reason: null
};
}
const periodScope = inferPeriodScope(fragmentText);
const primaryFrom = periodScope.from ?? "2020-07-01";
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
return {
claim_type: "prove_rbp_tail_state",
query_subject: "deferred_expense_lifecycle_anomaly",
required_live_calls: [...RBP_REQUIRED_LIVE_CALLS],
calls: [
{
call_id: "find_rbp_writeoff_documents_in_period",
purpose: "seed_writeoff_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
},
{
call_id: "find_rbp_object_movements_account_97",
purpose: "collect_rbp_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
required_for_claim: true,
account_scope_override: ["97"]
},
{
call_id: "find_month_close_entries_linked_to_rbp",
purpose: "link_month_close_to_rbp",
query: buildLiveRangeQuery(primaryFrom, primaryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
},
{
call_id: "compute_end_period_residual_by_rbp_object",
purpose: "collect_residual_tail_signals",
query: buildLiveRangeQuery(carryFrom, carryTo, config_1.ASSISTANT_MCP_LIVE_LIMIT),
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
}
],
route_gap_reason: null
};
}
function detectBroadQuery(fragmentText, route) {
const text = String(fragmentText ?? "").trim();
const lower = text.toLowerCase();
const tokenCount = lower.split(/\s+/).filter(Boolean).length;
const hasGenericMarker = BROAD_GENERIC_MARKERS.test(lower);
const hasAccountAnchor = ACCOUNT_SPECIFIC_MARKERS.test(lower);
const hasPeriodAnchor = PERIOD_MARKERS.test(lower);
const hasEntityAnchor = ENTITY_SPECIFIC_MARKERS.test(lower);
const hasExactObjectAnchor = EXACT_OBJECT_MARKERS.test(lower);
const hasGuidAnchor = extractGuids(lower).length > 0;
const hasContractAnchor = CONTRACT_MARKERS.test(lower);
const hasDocumentNumberAnchor = DOCUMENT_NUMBER_MARKERS.test(lower);
const hasAmountAnchor = AMOUNT_MARKERS.test(lower);
let anchorScore = 0;
if (hasGuidAnchor)
anchorScore += 3;
if (hasAccountAnchor)
anchorScore += 2;
if (hasPeriodAnchor)
anchorScore += 1;
if (hasEntityAnchor)
anchorScore += 1;
if (hasExactObjectAnchor)
anchorScore += 1;
if (hasContractAnchor)
anchorScore += 2;
if (hasDocumentNumberAnchor)
anchorScore += 2;
if (hasAmountAnchor)
anchorScore += 1;
const weakAnchors = anchorScore <= 1;
const strongFocus = hasGuidAnchor ||
(hasAccountAnchor && hasPeriodAnchor) ||
(hasContractAnchor && hasDocumentNumberAnchor) ||
anchorScore >= 4;
const routeSensitiveBroad = route === "batch_refresh_then_store" || route === "hybrid_store_plus_live";
let broadnessLevel = "low";
if (hasGenericMarker && !strongFocus && (weakAnchors || routeSensitiveBroad)) {
broadnessLevel = "high";
}
else if (hasGenericMarker && !strongFocus) {
broadnessLevel = "medium";
}
return {
broad_query_detected: broadnessLevel !== "low",
broadness_level: broadnessLevel,
scope_confidence_hint: broadnessLevel === "high" ? "low" : broadnessLevel === "medium" ? "medium" : "high",
narrowing_strength: anchorScore >= 3 ? "strong" : anchorScore === 2 ? "medium" : "weak"
};
}
function enforceBroadQueryGuards(route, fragmentText, raw) {
if (!config_1.FEATURE_ASSISTANT_BROAD_GUARD_V1) {
return raw;
}
const assessed = detectBroadQuery(fragmentText, route);
const summary = {
...(raw.summary ?? {})
};
let antiGenericGuardApplied = false;
let minimumEvidenceFailed = false;
if (config_1.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 && assessed.broad_query_detected && route === "batch_refresh_then_store") {
antiGenericGuardApplied = true;
raw.items = raw.items.slice(0, 5);
pushUniqueLine(raw.selection_reason, "Anti-generic ranking guard applied for broad batch request.");
pushUniqueLine(raw.limitations, "Broad ranking output was tightened to avoid generic over-precision.");
if (raw.confidence === "high") {
raw.confidence = "medium";
}
}
if (config_1.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 && assessed.broad_query_detected && raw.status !== "error") {
const gate = ROUTE_MIN_EVIDENCE_GATE[route];
if (gate) {
const broadPenalty = assessed.broadness_level === "high" ? 1 : 0;
const requiredEvidence = gate.min_evidence_items + broadPenalty;
const requiredItems = gate.min_result_items + broadPenalty;
const evidenceCount = raw.evidence.length;
const resultItemsCount = raw.items.length;
const hasLimitedSupport = evidenceCount > 0 || resultItemsCount > 0;
minimumEvidenceFailed = evidenceCount < requiredEvidence || resultItemsCount < requiredItems;
if (minimumEvidenceFailed) {
if (hasLimitedSupport) {
raw.status = "partial";
pushUniqueLine(raw.limitations, "Broad query support is limited; output degraded to partial confidence.");
pushUniqueLine(raw.selection_reason, "Route-aware minimum evidence gate downgraded broad output to partial.");
summary.degraded_to = "partial";
if (raw.confidence === "high") {
raw.confidence = "medium";
}
}
else {
raw.status = "empty";
pushUniqueLine(raw.limitations, "Broad query lacks enough support for even a limited factual output.");
pushUniqueLine(raw.selection_reason, "Route-aware minimum evidence gate requires clarification before retrieval output.");
raw.confidence = "low";
summary.degraded_to = "clarification";
}
}
}
}
summary.broad_query_detected = assessed.broad_query_detected;
summary.broadness_level = assessed.broadness_level;
summary.scope_confidence_hint = assessed.scope_confidence_hint;
summary.narrowing_strength = assessed.narrowing_strength;
summary.broad_guard_applied = assessed.broad_query_detected;
summary.minimum_evidence_failed = minimumEvidenceFailed;
summary.anti_generic_guard_applied = antiGenericGuardApplied;
summary.broad_result_flag = assessed.broad_query_detected;
raw.summary = summary;
return raw;
}
const CLOSE_COST_ACCOUNTS = ["20", "21", "23", "25", "26", "28", "29", "44"];
const P0_DOMAIN_CARDS = [
{
id: "settlements_60_62",
title: "Settlements and bank flow (60-62)",
account_scope: ["51", "60", "62"],
domain_scope: ["bank", "settlements", "suppliers", "customers", "supplier_payments"],
allowed_entities: ["document", "counterparty", "contract", "posting"],
allowed_evidence_sources: {
risk: ["problemCases", "keyFields", "docs"],
canonical: ["docs", "keyFields"]
},
expected_edges: ["payment_to_settlement", "statement_to_document", "contract_to_documents", "document_to_posting"],
forbidden_cross_domain_leakage: ["vat", "taxes", "deferred_expense", "fixed_assets", "period_close"],
symptom_markers: [
/payment/i,
/settlement/i,
/\b60\b/,
/\b62\b/,
/\b51\b/,
/\u043e\u043f\u043b\u0430\u0442/i,
/\u0440\u0430\u0441\u0447\u0435\u0442/i,
/\u043d\u0435\s+\u0437\u0430\u043a\u0440/i,
/\u0445\u0432\u043e\u0441\u0442/i
]
},
{
id: "vat_document_register_book",
title: "VAT flow document -> register -> book",
account_scope: ["19", "68"],
domain_scope: ["vat", "taxes"],
allowed_entities: ["document", "tax_entry", "posting", "counterparty"],
allowed_evidence_sources: {
risk: ["ndsRegisters", "keyFields", "problemCases"],
canonical: ["ndsRegisters", "keyFields", "docs"]
},
expected_edges: ["invoice_to_vat", "document_to_posting", "contract_to_documents"],
forbidden_cross_domain_leakage: ["bank", "settlements", "suppliers", "customers", "deferred_expense", "fixed_assets", "period_close"],
symptom_markers: [
/\bvat\b/i,
/\u043d\u0434\u0441/i,
/\u0441\u0447[её]т(?:а|у|ом|е)?.?фактур/i,
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u043e\u043a\u0443\u043f/i,
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u0440\u043e\u0434\u0430\u0436/i,
/\u0432\u044b\u0447\u0435\u0442/i,
/\u043d\u0430\u043b\u043e\u0433\u043e\u0432(?:\u044b\u0439|\u043e\u0433\u043e)?\s+\u044d\u0444\u0444\u0435\u043a\u0442/i
]
},
{
id: "month_close_costs_20_44",
title: "Month close and costs flow (20-44)",
account_scope: CLOSE_COST_ACCOUNTS,
domain_scope: ["period_close", "deferred_expense"],
allowed_entities: ["document", "posting", "contract"],
allowed_evidence_sources: {
risk: ["problemCases", "docs", "journals", "keyFields"],
canonical: ["docs", "journals", "keyFields"]
},
expected_edges: ["document_to_posting", "deferred_expense_to_writeoff", "contract_to_documents"],
forbidden_cross_domain_leakage: ["vat", "taxes", "bank", "settlements", "suppliers", "customers", "fixed_assets"],
symptom_markers: [
/period\s*close/i,
/month\s*close/i,
/close\s+period/i,
/закрыт[а-яё]*\s+период/i,
/close\s+operation/i,
/allocation/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,
/\u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b/i,
/\u0440\u0431\u043f/i,
/\u0430\u043c\u043e\u0440\u0442\u0438\u0437/i
]
}
];
function parseDateCandidate(value) {
if (typeof value !== "string") {
return null;
}
const time = Date.parse(value);
if (Number.isNaN(time)) {
return null;
}
return time;
}
function extractDate(record) {
const attrs = record.attributes ?? {};
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()) {
return value;
}
}
return null;
}
function countZeroGuidValues(record) {
const attrs = record.attributes ?? {};
let count = 0;
for (const value of Object.values(attrs)) {
if (typeof value === "string" && value.trim() === "00000000-0000-0000-0000-000000000000") {
count += 1;
}
}
return count;
}
function countNavigationLinks(record) {
const attrs = record.attributes ?? {};
let count = 0;
for (const key of Object.keys(attrs)) {
if (key.includes("@navigationLinkUrl")) {
count += 1;
}
}
return count;
}
function findCounterpartyLinks(record) {
return record.links.filter((link) => link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field));
}
function extractGuids(text) {
const matches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? [];
return Array.from(new Set(matches.map((item) => item.toLowerCase())));
}
function hasGuidMatch(record, guid) {
const source = record.source_id.toLowerCase();
if (source.includes(guid)) {
return true;
}
for (const link of record.links) {
if (String(link.target_id ?? "").toLowerCase() === guid) {
return true;
}
}
for (const value of Object.values(record.attributes ?? {})) {
if (typeof value === "string" && value.toLowerCase().includes(guid)) {
return true;
}
}
return false;
}
function normalizeBusinessDateToken(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return null;
}
const dayMonthYear = raw.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2}|\d{4})$/);
if (!dayMonthYear) {
return null;
}
const day = dayMonthYear[1].padStart(2, "0");
const month = dayMonthYear[2].padStart(2, "0");
const yearRaw = dayMonthYear[3];
const year = yearRaw.length === 2 ? `20${yearRaw}` : yearRaw;
return `${year}-${month}-${day}`;
}
function extractBusinessAnchorsFromText(fragmentText) {
const text = String(fragmentText ?? "");
const lower = text.toLowerCase();
const documentNumbers = Array.from(new Set((text.match(/(?:№|#)\s*([a-zа-я0-9][a-zа-я0-9\-/.]{0,31})/giu) ?? [])
.map((item) => item.replace(/^(?:№|#)\s*/u, "").trim().toLowerCase())
.filter((item) => item.length > 0))).slice(0, 6);
const dateTokens = Array.from(new Set((text.match(/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g) ?? []).map((item) => item.trim()))).slice(0, 8);
const dateIso = Array.from(new Set(dateTokens.map((item) => normalizeBusinessDateToken(item)).filter((item) => Boolean(item))));
const amountValues = Array.from(new Set((text.match(/\b\d{1,3}(?:[ \u00A0]\d{3})*(?:[,.]\d{2})\b/g) ?? [])
.map((item) => parseFiniteNumber(item.replace(/\s|\u00A0/g, "")))
.filter((item) => item !== null))).slice(0, 6);
const accountScope = extractAccountScopeFromText(lower);
const periodScope = inferPeriodScope(lower);
const periodKeys = uniqueStrings([
...(periodScope.from ? [String(periodScope.from).slice(0, 7)] : []),
...(periodScope.to ? [String(periodScope.to).slice(0, 7)] : []),
...dateIso.map((item) => item.slice(0, 7))
].filter((item) => /^\d{4}-\d{2}$/.test(item))).slice(0, 6);
let score = 0;
if (documentNumbers.length > 0)
score += 2;
if (dateTokens.length > 0 || periodKeys.length > 0)
score += 1;
if (amountValues.length > 0)
score += 1;
if (accountScope.length > 0)
score += 1;
return {
document_numbers: documentNumbers,
date_tokens: dateTokens,
date_iso: dateIso,
amount_values: amountValues,
account_scope: accountScope,
period_keys: periodKeys,
sufficient: score >= 3 && (documentNumbers.length > 0 || (dateTokens.length > 0 && amountValues.length > 0))
};
}
function extractAmountSignalsFromRecord(record) {
const values = [];
for (const [key, rawValue] of Object.entries(record.attributes ?? {})) {
if (!/sum|amount|итого|сумм|оплат|долг/i.test(key)) {
continue;
}
const parsed = parseFiniteNumber(rawValue);
if (parsed !== null) {
values.push(parsed);
}
}
return uniqueStrings(values.map((item) => item.toFixed(2))).map((item) => Number(item));
}
function matchesAmountAnchor(recordAmounts, anchors) {
if (recordAmounts.length === 0 || anchors.length === 0) {
return false;
}
return anchors.some((anchor) => recordAmounts.some((value) => Math.abs(value - anchor) <= 0.05));
}
function scoreRecordForBusinessAnchorTrace(record, anchors) {
const matchedCategories = [];
const corpus = collectTextFromRecord(record).toLowerCase();
const recordPeriod = extractDate(record);
const recordPeriodIso = recordPeriod ? String(recordPeriod).slice(0, 10) : "";
const recordPeriodMonth = recordPeriod ? String(recordPeriod).slice(0, 7) : "";
let score = 0;
if (anchors.document_numbers.length > 0) {
const docMatch = anchors.document_numbers.some((item) => new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegExp(item)}($|[^\\p{L}\\p{N}])`, "iu").test(corpus));
if (docMatch) {
matchedCategories.push("document_number");
score += 3;
}
}
if (anchors.date_tokens.length > 0 || anchors.date_iso.length > 0) {
const rawDateHit = anchors.date_tokens.some((item) => corpus.includes(item.toLowerCase()));
const isoDateHit = anchors.date_iso.some((item) => recordPeriodIso.startsWith(item));
const monthHit = anchors.period_keys.some((item) => recordPeriodMonth.startsWith(item));
if (rawDateHit || isoDateHit || monthHit) {
matchedCategories.push("date_or_period");
score += 2;
}
}
if (anchors.amount_values.length > 0) {
const amountHit = matchesAmountAnchor(extractAmountSignalsFromRecord(record), anchors.amount_values);
if (amountHit) {
matchedCategories.push("amount");
score += 3;
}
}
if (anchors.account_scope.length > 0) {
const accounts = inferAccountsFromRecord(record, corpus);
if (intersects(anchors.account_scope, accounts)) {
matchedCategories.push("account_scope");
score += 2;
}
}
if (anchors.period_keys.length > 0 && !matchedCategories.includes("date_or_period")) {
if (anchors.period_keys.some((item) => recordPeriodMonth.startsWith(item))) {
matchedCategories.push("period");
score += 1;
}
}
return {
score,
matched_categories: matchedCategories
};
}
const ACCOUNT_PRESETS = {
"51": {
domains: ["bank", "settlements", "supplier_payments"],
documents: ["bank_statement", "payment_order", "settlement_document"],
entities: ["counterparty", "contract", "document", "posting"],
relations: ["payment_to_settlement", "statement_to_document", "document_to_posting"],
anomalies: ["missing_link", "posting_mismatch", "closure_risk"]
},
"60": {
domains: ["suppliers", "settlements", "supplier_payments"],
documents: ["supplier_receipt", "payment_order", "settlement_document"],
entities: ["counterparty", "contract", "document", "posting"],
relations: ["payment_to_settlement", "contract_to_documents", "document_to_posting"],
anomalies: ["missing_link", "broken_lifecycle", "closure_risk"]
},
"62": {
domains: ["customers", "settlements"],
documents: ["sales_document", "payment_order", "settlement_document"],
entities: ["counterparty", "contract", "document", "posting"],
relations: ["payment_to_settlement", "contract_to_documents", "document_to_posting"],
anomalies: ["missing_link", "broken_lifecycle", "closure_risk"]
},
"76": {
domains: ["other_settlements", "settlements"],
documents: ["manual_operation", "settlement_document"],
entities: ["counterparty", "contract", "document", "posting"],
relations: ["contract_to_documents", "document_to_posting"],
anomalies: ["silent_orphan", "manual_intervention_suspicion", "closure_risk"]
},
"97": {
domains: ["deferred_expense", "period_close"],
documents: ["deferred_expense_document", "manual_operation", "period_close_document"],
entities: ["document", "posting"],
relations: ["deferred_expense_to_writeoff", "document_to_posting"],
anomalies: ["broken_lifecycle", "missing_link", "closure_risk"]
},
"01": {
domains: ["fixed_assets"],
documents: ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"],
entities: ["fixed_asset", "document", "posting"],
relations: ["asset_card_to_depreciation", "document_to_posting"],
anomalies: ["broken_lifecycle", "missing_link", "posting_mismatch"]
},
"02": {
domains: ["fixed_assets"],
documents: ["depreciation_document", "fixed_asset_card"],
entities: ["fixed_asset", "document", "posting"],
relations: ["asset_card_to_depreciation", "document_to_posting"],
anomalies: ["broken_lifecycle", "posting_mismatch"]
},
"08": {
domains: ["fixed_assets"],
documents: ["fixed_asset_acceptance", "fixed_asset_card", "manual_operation"],
entities: ["fixed_asset", "document", "posting"],
relations: ["asset_card_to_depreciation", "document_to_posting"],
anomalies: ["missing_link", "broken_lifecycle", "posting_mismatch"]
},
"20": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"21": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"23": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"25": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"26": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"28": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"29": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"44": {
domains: ["period_close", "deferred_expense"],
documents: ["period_close_document", "deferred_expense_document"],
entities: ["document", "posting", "contract"],
relations: ["document_to_posting", "deferred_expense_to_writeoff"],
anomalies: ["closure_risk", "broken_lifecycle", "missing_link"]
},
"19": {
domains: ["vat"],
documents: ["invoice", "vat_document", "supplier_receipt"],
entities: ["document", "tax_entry", "counterparty"],
relations: ["invoice_to_vat", "document_to_posting"],
anomalies: ["missing_link", "cross_domain_inconsistency", "closure_risk"]
},
"68": {
domains: ["vat", "taxes"],
documents: ["invoice", "vat_document", "period_close_document"],
entities: ["document", "tax_entry", "posting"],
relations: ["invoice_to_vat", "document_to_posting"],
anomalies: ["missing_link", "cross_domain_inconsistency", "closure_risk"]
}
};
const GRAPH_DOMAIN_PRESETS = {
bank_settlement: {
relations: ["payment_to_settlement", "statement_to_document", "document_to_posting", "contract_to_documents"],
signals: ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"],
lifecycle_markers: ["closed", "reconciled", "partially_linked", "no_continuation", "period_boundary"]
},
customer_settlement: {
relations: ["contract_to_documents", "payment_to_settlement", "document_to_posting"],
signals: ["missing_link", "broken_lifecycle", "closure_risk"],
lifecycle_markers: ["closed", "reconciled", "partially_linked", "no_continuation", "period_boundary"]
},
deferred_expense: {
relations: ["deferred_expense_to_writeoff", "document_to_posting"],
signals: ["missing_link", "broken_lifecycle", "closure_risk", "amount_independent_risk"],
lifecycle_markers: ["closed", "partially_linked", "period_boundary", "no_continuation", "reconciled"]
},
fixed_asset: {
relations: ["asset_card_to_depreciation", "document_to_posting"],
signals: ["missing_link", "broken_lifecycle", "posting_mismatch", "closure_risk"],
lifecycle_markers: ["closed", "partially_linked", "period_boundary", "no_continuation", "reconciled"]
},
vat_flow: {
relations: ["invoice_to_vat", "document_to_posting", "contract_to_documents"],
signals: ["missing_link", "cross_domain_inconsistency", "posting_mismatch", "closure_risk"],
lifecycle_markers: ["closed", "partially_linked", "period_boundary", "reconciled", "no_continuation"]
},
period_close: {
relations: ["document_to_posting", "deferred_expense_to_writeoff", "invoice_to_vat", "asset_card_to_depreciation"],
signals: ["closure_risk", "broken_lifecycle", "cross_domain_inconsistency", "missing_link"],
lifecycle_markers: ["posted", "closed", "period_boundary", "partially_linked", "no_continuation"]
}
};
const ACCOUNT_GRAPH_DOMAIN_MAP = {
"51": "bank_settlement",
"60": "bank_settlement",
"62": "customer_settlement",
"76": "bank_settlement",
"97": "deferred_expense",
"01": "fixed_asset",
"02": "fixed_asset",
"08": "fixed_asset",
"19": "vat_flow",
"68": "vat_flow"
};
const GRAPH_INTENT_MARKERS = /(?:lifecycle|transition|state|expected|actual|terminal|closing|chain|break|conflict|cross[-_\s]?branch|\u0436\u0438\u0437\u043d\u0435\u043d\u043d|\u043f\u0435\u0440\u0435\u0445\u043e\u0434|\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u0444\u0430\u043a\u0442\u0438\u0447|\u0440\u0430\u0437\u0440\u044b\u0432|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|\u0432\u0435\u0442\u043a)/iu;
function uniqueStrings(items) {
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean)));
}
function pushMany(target, values) {
for (const value of values) {
target.push(value);
}
}
function toGraphDomain(domain) {
if (domain === "deferred_expense") {
return "deferred_expense";
}
if (domain === "fixed_assets") {
return "fixed_asset";
}
if (domain === "vat" || domain === "taxes") {
return "vat_flow";
}
if (domain === "period_close") {
return "period_close";
}
if (domain === "customers") {
return "customer_settlement";
}
if (domain === "bank" ||
domain === "settlements" ||
domain === "suppliers" ||
domain === "supplier_payments" ||
domain === "other_settlements") {
return "bank_settlement";
}
return null;
}
function toGraphDomainScope(domains, accounts) {
const resolved = [];
for (const domain of domains) {
const mapped = toGraphDomain(domain);
if (mapped) {
resolved.push(mapped);
}
}
for (const account of accounts) {
const mapped = ACCOUNT_GRAPH_DOMAIN_MAP[account];
if (mapped) {
resolved.push(mapped);
if (mapped === "deferred_expense") {
resolved.push("period_close");
}
}
}
return uniqueStrings(resolved);
}
function buildGraphTraversalProfile(input) {
const targetDomains = toGraphDomainScope(input.domainScope, input.accountScope);
const targetRelations = [...input.relationPatterns];
const targetSignals = [...input.anomalyPatterns];
const targetLifecycleMarkers = ["closed", "reconciled", "partially_linked", "period_boundary", "no_continuation"];
for (const domain of targetDomains) {
const preset = GRAPH_DOMAIN_PRESETS[domain];
if (!preset) {
continue;
}
pushMany(targetRelations, preset.relations);
pushMany(targetSignals, preset.signals);
pushMany(targetLifecycleMarkers, preset.lifecycle_markers);
}
const graphIntent = GRAPH_INTENT_MARKERS.test(input.text) ||
input.anomalyPatterns.some((item) => item === "broken_lifecycle" || item === "closure_risk" || item === "cross_domain_inconsistency");
const eligible = graphIntent || targetDomains.length > 0;
return {
runtime_enabled: config_1.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1,
eligible,
planner_mode: config_1.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 && eligible ? "typed_domain_path" : "semantic_only",
target_domains: targetDomains,
target_relations: uniqueStrings(targetRelations),
target_signals: uniqueStrings(targetSignals),
target_lifecycle_markers: uniqueStrings(targetLifecycleMarkers)
};
}
function inferGraphRuntimeSignals(signals) {
const runtimeSignals = [];
if (signals.anomaly_patterns.includes("missing_link") ||
signals.lifecycle_markers.includes("partially_linked") ||
signals.lifecycle_markers.includes("no_continuation")) {
runtimeSignals.push("missing_transition");
}
if (signals.anomaly_patterns.includes("posting_mismatch") || signals.anomaly_patterns.includes("cross_domain_inconsistency")) {
runtimeSignals.push("conflicting_transition");
}
if (signals.anomaly_patterns.includes("closure_risk") || signals.lifecycle_markers.includes("period_boundary")) {
runtimeSignals.push("terminal_state_gap");
}
if (signals.anomaly_patterns.includes("wrong_document_type")) {
runtimeSignals.push("wrong_closing_document_type");
}
return uniqueStrings(runtimeSignals);
}
function summarizeGraphTraversalRuntime(candidates, profile) {
const domainHits = new Map();
const signalCounts = new Map();
let matchedCandidates = 0;
let neighborBranchLiftedCandidates = 0;
let crossBranchConflictCandidates = 0;
let terminalGapCandidates = 0;
let multiHopCandidates = 0;
let maxRelationHops = 0;
const rankingShiftSignals = new Set();
for (const candidate of candidates) {
if (candidate.evaluation.graph_traversal_score > 0) {
matchedCandidates += 1;
}
const relationHopCount = candidate.evaluation.signals.relation_patterns.length;
maxRelationHops = Math.max(maxRelationHops, relationHopCount);
if (relationHopCount >= 2 && candidate.evaluation.graph_traversal_score > 0) {
multiHopCandidates += 1;
}
const hasTargetDomain = candidate.evaluation.graph_domain_scope.some((domain) => profile.graph_traversal.target_domains.includes(domain));
const hasNeighborDomain = candidate.evaluation.graph_domain_scope.some((domain) => !profile.graph_traversal.target_domains.includes(domain));
if (hasTargetDomain && hasNeighborDomain) {
neighborBranchLiftedCandidates += 1;
rankingShiftSignals.add("neighbor_branch_lifting");
}
for (const domain of candidate.evaluation.graph_domain_scope) {
if (!profile.graph_traversal.target_domains.includes(domain)) {
continue;
}
domainHits.set(domain, (domainHits.get(domain) ?? 0) + 1);
}
for (const signal of candidate.evaluation.graph_runtime_signals) {
signalCounts.set(signal, (signalCounts.get(signal) ?? 0) + 1);
}
if (candidate.evaluation.graph_runtime_signals.includes("conflicting_transition")) {
crossBranchConflictCandidates += 1;
rankingShiftSignals.add("cross_branch_conflict");
}
if (candidate.evaluation.graph_runtime_signals.includes("terminal_state_gap")) {
terminalGapCandidates += 1;
rankingShiftSignals.add("terminal_gap");
}
}
if (multiHopCandidates > 0) {
rankingShiftSignals.add("multi_hop_chain");
}
if (matchedCandidates > 0 && matchedCandidates < candidates.length) {
rankingShiftSignals.add("graph_selective_scoring");
}
return {
runtime_enabled: profile.graph_traversal.runtime_enabled,
graph_eligible: profile.graph_traversal.eligible,
planner_mode: profile.graph_traversal.planner_mode,
traversal_applied: profile.graph_traversal.runtime_enabled && profile.graph_traversal.eligible && candidates.length > 0,
target_domains: profile.graph_traversal.target_domains,
target_relations: profile.graph_traversal.target_relations,
target_signals: profile.graph_traversal.target_signals,
target_lifecycle_markers: profile.graph_traversal.target_lifecycle_markers,
evaluated_candidates: candidates.length,
matched_candidates: matchedCandidates,
domain_hits: Object.fromEntries(Array.from(domainHits.entries())),
signal_counts: Object.fromEntries(Array.from(signalCounts.entries())),
neighbor_branch_lifted_candidates: neighborBranchLiftedCandidates,
cross_branch_conflict_candidates: crossBranchConflictCandidates,
terminal_gap_candidates: terminalGapCandidates,
multi_hop_candidates: multiHopCandidates,
max_relation_hops: maxRelationHops,
ranking_shift_signals: Array.from(rankingShiftSignals)
};
}
function collectTextFromRecord(record) {
const parts = [record.source_entity, record.display_name, record.source_id];
for (const link of record.links) {
parts.push(link.relation, link.target_entity, link.target_id, link.source_field);
}
for (const [key, value] of Object.entries(record.attributes ?? {})) {
parts.push(key, String(value));
}
return parts.join(" ").toLowerCase();
}
const KNOWN_ACCOUNT_CODES = new Set([
"01",
"02",
"08",
"19",
"20",
"21",
"23",
"25",
"26",
"28",
"29",
"44",
"51",
"60",
"62",
"68",
"76",
"97"
]);
const ACCOUNT_CONTEXT_AROUND_MARKERS = /(?:счет|сч\.?|account|schet|оплат|расчет|расч[её]т|аванс|зачет|зач[её]т|ндс|период|закрыт|провод|постав|покуп|settlement|payment|vat|close|supplier|customer)/iu;
function collectDateLikeSpans(text) {
const spans = [];
const patterns = [
/\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b/g,
/\b20\d{2}(?:[./-](?:0[1-9]|1[0-2]))(?:[./-](?:0[1-9]|[12]\d|3[01]))?\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu,
/\b(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)\s+20\d{2}\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectAmountLikeSpans(text) {
const spans = [];
const patterns = [
/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g,
/\b\d+[.,]\d{2}\b/g
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectPercentLikeSpans(text) {
const spans = [];
const pattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g;
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function intersectsSpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
function hasAccountContextAround(text, start, end) {
const left = text.slice(Math.max(0, start - 28), start);
const right = text.slice(end, Math.min(text.length, end + 28));
return ACCOUNT_CONTEXT_AROUND_MARKERS.test(`${left} ${right}`);
}
function extractAccountScopeFromText(text) {
const lower = String(text ?? "").toLowerCase();
const blockedSpans = [
...collectDateLikeSpans(lower),
...collectAmountLikeSpans(lower),
...collectPercentLikeSpans(lower)
];
const accounts = [];
const pushAccount = (raw) => {
const prefix = String(raw ?? "").trim().match(/^(\d{2})/)?.[1];
if (!prefix) {
return;
}
if (!KNOWN_ACCOUNT_CODES.has(prefix)) {
return;
}
accounts.push(prefix);
};
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*([0-9./,\sиand]{2,96})/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const rawChunk = String(contextualMatch[1] ?? "");
const chunkAccounts = rawChunk.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
for (const account of chunkAccounts) {
pushAccount(account);
}
}
const settlementPairPattern = /\b(?:60|62)\.\d{1,2}\s*\/\s*(?:60|62)\.\d{1,2}\b/g;
let settlementPairMatch = null;
while ((settlementPairMatch = settlementPairPattern.exec(lower)) !== null) {
const pair = settlementPairMatch[0];
const pairAccounts = pair.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
for (const account of pairAccounts) {
pushAccount(account);
}
}
const closePairPattern = /\b(?:20|21|23|25|26|28|29|44)\s*[-/]\s*(?:20|21|23|25|26|28|29|44)\b/g;
let closePairMatch = null;
while ((closePairMatch = closePairPattern.exec(lower)) !== null) {
const pair = closePairMatch[0];
const pairAccounts = pair.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
for (const account of pairAccounts) {
pushAccount(account);
}
}
const suffixAnchorPattern = /\b(?:51|60|62|68|76|97)(?:\.\d{1,2})?(?:-(?:му|й|го|м|х))?\b/giu;
let suffixAnchorMatch = null;
while ((suffixAnchorMatch = suffixAnchorPattern.exec(lower)) !== null) {
const token = suffixAnchorMatch[0];
const start = suffixAnchorMatch.index;
const end = start + token.length;
if (intersectsSpan(start, end, blockedSpans)) {
continue;
}
pushAccount(token);
}
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
let explicitMatch = null;
const settlementLexicalAnchor = /(оплат|расчет|расч[её]т|аванс|долг|постав|покуп|settlement|payment|supplier|customer)/i.test(lower);
while ((explicitMatch = explicitPattern.exec(lower)) !== null) {
const token = explicitMatch[0];
const start = explicitMatch.index;
const end = start + token.length;
if (intersectsSpan(start, end, blockedSpans)) {
continue;
}
const prefix = token.match(/^(\d{2})/)?.[1];
if (!prefix || !KNOWN_ACCOUNT_CODES.has(prefix)) {
continue;
}
if (prefix === "60" || prefix === "62" || prefix === "51" || prefix === "76") {
if (settlementLexicalAnchor || hasAccountContextAround(lower, start, end)) {
accounts.push(prefix);
}
continue;
}
if (hasAccountContextAround(lower, start, end)) {
accounts.push(prefix);
}
}
return uniqueStrings(accounts);
}
function inferPeriodScope(fragmentText) {
const dayMonthYear = fragmentText.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?[1-9]|1[0-2])[./-](\d{2}|\d{4})\b/);
if (dayMonthYear) {
const yearRaw = dayMonthYear[3];
const year = yearRaw.length === 2 ? `20${yearRaw}` : yearRaw;
const month = dayMonthYear[2].padStart(2, "0");
const day = dayMonthYear[1].padStart(2, "0");
return {
from: `${year}-${month}-${day}`,
to: null,
granularity: "day"
};
}
const month = fragmentText.match(/\b(20\d{2})[-./](0[1-9]|1[0-2])\b/);
if (month) {
return {
from: `${month[1]}-${month[2]}-01`,
to: null,
granularity: "month"
};
}
const monthNameToNumber = [
{ pattern: /январ|january/i, month: "01" },
{ pattern: /феврал|february/i, month: "02" },
{ pattern: /март|march/i, month: "03" },
{ pattern: /апрел|april/i, month: "04" },
{ pattern: /\bмай\b|\bмая\b|may/i, month: "05" },
{ pattern: /июн|june/i, month: "06" },
{ pattern: /июл|july/i, month: "07" },
{ pattern: /август|august/i, month: "08" },
{ pattern: /сентябр|september/i, month: "09" },
{ pattern: /октябр|october/i, month: "10" },
{ pattern: /ноябр|november/i, month: "11" },
{ pattern: /декабр|december/i, month: "12" }
];
const yearForMonthName = fragmentText.match(/\b(20\d{2})\b/)?.[1] ?? null;
if (yearForMonthName) {
const monthByName = monthNameToNumber.find((item) => item.pattern.test(fragmentText));
if (monthByName) {
return {
from: `${yearForMonthName}-${monthByName.month}-01`,
to: null,
granularity: "month"
};
}
}
if (monthNameToNumber.some((item) => item.pattern.test(fragmentText))) {
return {
from: null,
to: null,
granularity: "month"
};
}
const year = fragmentText.match(/\b(20\d{2})\b/);
if (year) {
return {
from: `${year[1]}-01-01`,
to: `${year[1]}-12-31`,
granularity: "year"
};
}
if (/квартал|quarter/i.test(fragmentText)) {
return {
from: null,
to: null,
granularity: "quarter"
};
}
if (/месяц|month|период/i.test(fragmentText)) {
return {
from: null,
to: null,
granularity: "month"
};
}
return {
from: null,
to: null,
granularity: "unknown"
};
}
const WRONG_DOCUMENT_MARKERS = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u043e\u043c)?|\u043d\u0435\s+\u0432\s+\u0442\u043e\u0442|wrong\s+document|wrong_document_type)/iu;
const REPEATED_ANOMALY_MARKERS = /(?:\u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449|\u0441\u0435\u0440\u0438\u0439\u043d|\u043f\u0430\u0442\u0442\u0435\u0440\u043d|repeat(?:ed|ability)?)/iu;
function inferQuerySubject(text, domains, anomalies) {
const lower = text.toLowerCase();
if ((domains.includes("bank") || domains.includes("settlements")) && WRONG_DOCUMENT_MARKERS.test(lower)) {
return "bank_settlement_mismatch";
}
if (domains.includes("suppliers")) {
return "supplier_tail_analysis";
}
if (domains.includes("customers")) {
return "customer_closure_gap";
}
if (domains.includes("deferred_expense")) {
return "deferred_expense_lifecycle_anomaly";
}
if (domains.includes("fixed_assets")) {
return "fixed_asset_card_mismatch";
}
if (domains.includes("vat")) {
return "vat_chain_conflict";
}
if (domains.includes("period_close")) {
return "period_closure_risk";
}
if (anomalies.includes("posting_mismatch")) {
return "document_posting_conflict";
}
return "cross_entity_breakage";
}
function buildSemanticRetrievalProfile(fragmentText) {
const lower = fragmentText.toLowerCase();
const accountScope = extractAccountScopeFromText(lower);
const domainScope = [];
const documentTypes = [];
const entityTypes = [];
const relationPatterns = [];
const anomalyPatterns = [];
const excludedInterpretations = [];
const rankingBasis = ["closure_risk", "repeatability", "financial_impact"];
const explanationFocus = ["why_selected", "where_chain_breaks", "what_business_risk"];
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"]);
pushMany(relationPatterns, ["payment_to_settlement", "statement_to_document", "document_to_posting"]);
}
const hasSettlementAccountScope = accountScope.some((item) => item === "51" || item === "60" || item === "62" || item === "76");
const hasVatAccountScope = accountScope.some((item) => item === "19" || item === "68");
const hasFixedAssetAccountScope = accountScope.some((item) => item === "01" || item === "02" || item === "08");
const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97");
const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item));
const hasExplicitMonthCloseLexicalMarker = /(?:закрыти[ея]\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));
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) {
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(lower) ||
hasVatAccountScope) {
pushMany(domainScope, ["vat", "taxes"]);
pushMany(documentTypes, ["invoice", "vat_document"]);
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
hasFixedAssetAccountScope) {
pushMany(domainScope, ["fixed_assets"]);
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
}
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
hasDeferredExpenseAccountScope) {
pushMany(domainScope, ["deferred_expense", "period_close"]);
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
pushMany(entityTypes, ["document", "posting"]);
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
}
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)) {
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)) {
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
}
if (REPEATED_ANOMALY_MARKERS.test(lower)) {
pushMany(anomalyPatterns, ["repeated_anomaly"]);
pushMany(rankingBasis, ["repeatability"]);
}
if (hasExplicitMonthCloseLexicalMarker || hasMonthCloseCostsAccountScope || hasDeferredExpenseAccountScope) {
pushMany(domainScope, ["period_close"]);
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
pushMany(documentTypes, ["period_close_document"]);
}
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) {
pushMany(excludedInterpretations, ["simple_payment_delay"]);
}
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) {
pushMany(excludedInterpretations, ["amount_only_anomaly"]);
pushMany(rankingBasis, ["amount_independent_risk"]);
}
for (const account of accountScope) {
const preset = ACCOUNT_PRESETS[account];
if (!preset) {
continue;
}
pushMany(domainScope, preset.domains);
pushMany(documentTypes, preset.documents);
pushMany(entityTypes, preset.entities);
pushMany(relationPatterns, preset.relations);
pushMany(anomalyPatterns, preset.anomalies);
}
if (relationPatterns.length === 0) {
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
}
if (anomalyPatterns.length === 0) {
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle"]);
}
if (domainScope.length === 0) {
pushMany(domainScope, ["settlements"]);
}
if (documentTypes.length === 0) {
pushMany(documentTypes, ["settlement_document"]);
}
if (entityTypes.length === 0) {
pushMany(entityTypes, ["counterparty", "document", "posting"]);
}
const dedupedDomains = uniqueStrings(domainScope);
const dedupedAnomalies = uniqueStrings(anomalyPatterns);
const dedupedAccounts = uniqueStrings(accountScope);
const dedupedRelations = uniqueStrings(relationPatterns);
const graphTraversal = buildGraphTraversalProfile({
text: lower,
accountScope: dedupedAccounts,
domainScope: dedupedDomains,
relationPatterns: dedupedRelations,
anomalyPatterns: dedupedAnomalies
});
return {
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies),
account_scope: dedupedAccounts,
subaccount_scope: [],
domain_scope: dedupedDomains,
document_types: uniqueStrings(documentTypes),
entity_types: uniqueStrings(entityTypes),
period_scope: inferPeriodScope(lower),
relation_patterns: dedupedRelations,
lifecycle_stage_filters: ["created", "posted", "closed", "reconciled"],
anomaly_patterns: dedupedAnomalies,
ranking_basis: uniqueStrings(rankingBasis),
excluded_interpretations: uniqueStrings(excludedInterpretations),
explanation_focus: uniqueStrings(explanationFocus),
graph_traversal: graphTraversal
};
}
function hasSymptomMarker(fragmentText, card) {
return card.symptom_markers.some((marker) => marker.test(fragmentText));
}
function cardResolutionScore(card, fragmentText, profile) {
const accountMatches = profile.account_scope.filter((account) => card.account_scope.includes(account));
const domainMatches = profile.domain_scope.filter((domain) => card.domain_scope.includes(domain));
const markerHit = hasSymptomMarker(fragmentText, card);
const hasExplicitAccountScope = profile.account_scope.length > 0;
// If the user explicitly asked with account hints and this card does not intersect,
// the card must not activate (prevents false P0 gating on non-P0 accounts like 97).
if (hasExplicitAccountScope && accountMatches.length === 0) {
return 0;
}
const hasVatSoftAnchor = card.id === "vat_document_register_book" && hasStrongVatDomainSignal(fragmentText, profile);
const hasMonthCloseSignal = card.id === "month_close_costs_20_44" && hasStrongMonthCloseSignal(fragmentText, profile);
const fixedAssetOnlySignal = card.id === "month_close_costs_20_44" && hasFixedAssetSignal(fragmentText, profile) && !hasMonthCloseSignal && accountMatches.length === 0;
if (fixedAssetOnlySignal) {
return 0;
}
const markerWeight = card.id === "month_close_costs_20_44" ? hasMonthCloseSignal : markerHit;
const hasHardAnchor = accountMatches.length > 0 || markerWeight || hasVatSoftAnchor;
if (!hasHardAnchor) {
return 0;
}
return accountMatches.length * 4 + domainMatches.length * 3 + (markerWeight ? 2 : 0);
}
function hasStrongVatDomainSignal(fragmentText, profile) {
const text = String(fragmentText ?? "");
const hasVatLexicalAnchor = /(?:ндс|vat|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|книг[аи]\s+(?:покуп|продаж)|вычет|налогов(?:ый|ого)?\s+эффект)/iu.test(text);
return (hasVatLexicalAnchor ||
profile.account_scope.some((account) => account === "19" || account === "68") ||
profile.domain_scope.some((domain) => domain === "vat" || domain === "taxes") ||
profile.relation_patterns.some((pattern) => ["invoice_to_vat", "register_to_book", "book_entry_generated", "deduction_posted"].includes(pattern)));
}
function hasStrongMonthCloseSignal(fragmentText, profile) {
const text = String(fragmentText ?? "");
const hasMonthCloseLexicalAnchor = /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|month\s*close|period\s*close|close\s+operation)/iu.test(text);
return (hasMonthCloseLexicalAnchor ||
profile.account_scope.some((account) => CLOSE_COST_ACCOUNTS.includes(account)) ||
profile.domain_scope.some((domain) => domain === "period_close" || domain === "deferred_expense") ||
profile.relation_patterns.some((pattern) => ["deferred_expense_to_writeoff", "close_operation", "allocation_rules_resolved", "residuals_zero_or_explained"].includes(pattern)));
}
function hasFixedAssetSignal(fragmentText, profile) {
const text = String(fragmentText ?? "");
return (/(?:основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/iu.test(text) ||
profile.account_scope.some((account) => account === "01" || account === "02"));
}
function hasStrongSettlementAccountSignal(profile) {
return profile.account_scope.some((account) => account === "51" || account === "60" || account === "62" || account === "76");
}
function resolveP0DomainCard(fragmentText, profile) {
const resolved = P0_DOMAIN_CARDS.map((card) => ({
card,
score: cardResolutionScore(card, fragmentText, profile)
}))
.filter((item) => item.score > 0)
.sort((left, right) => right.score - left.score);
if (resolved.length === 0) {
return null;
}
const [first, second] = resolved;
if (second && second.score === first.score) {
const pair = new Set([first.card.id, second.card.id]);
const hasVatSettlementTie = pair.has("vat_document_register_book") && pair.has("settlements_60_62");
if (hasVatSettlementTie && hasStrongVatDomainSignal(fragmentText, profile) && !hasStrongSettlementAccountSignal(profile)) {
return resolved.find((item) => item.card.id === "vat_document_register_book") ?? null;
}
return null;
}
return first;
}
function isSettlementSymptomQuery(fragmentText, profile) {
const lower = String(fragmentText ?? "").toLowerCase();
const hasSettlementAccount = profile.account_scope.some((account) => account === "60" || account === "62");
const hasSettlementRelation = profile.relation_patterns.includes("payment_to_settlement") ||
profile.relation_patterns.includes("statement_to_document") ||
profile.relation_patterns.includes("contract_to_documents");
const hasSettlementSymptom = /(?:\b60(?:\.\d{2})?\b|\b62(?:\.\d{2})?\b|\b51(?:\.\d{2})?\b|не\s+сход|деньг[аи].*долг|аванс.*не\s+зач|не\s+закры|settlement|payment_to_settlement|\u0440\u0430\u0441\u0447\u0435\u0442|\u0437\u0430\u0447\u0435\u0442)/iu.test(lower);
return hasSettlementAccount && (hasSettlementRelation || hasSettlementSymptom);
}
function shouldUseStrictForbiddenDomainGate(card, profile, fragmentText) {
if (card.id !== "settlements_60_62") {
return false;
}
return isSettlementSymptomQuery(fragmentText, profile);
}
function hasSettlementRecoverySignal(signals) {
const hasSettlementAccount = signals.account_context.some((item) => ["51", "60", "62", "76"].includes(item));
const hasSettlementDomain = signals.domain_scope.some((item) => ["bank", "settlements", "suppliers", "customers", "supplier_payments", "other_settlements"].includes(item));
const hasSettlementRelation = signals.relation_patterns.some((item) => ["payment_to_settlement", "statement_to_document", "contract_to_documents"].includes(item));
const hasSettlementDocument = signals.document_types.some((item) => ["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document"].includes(item));
return hasSettlementAccount || hasSettlementDomain || hasSettlementRelation || hasSettlementDocument;
}
function isVatAllowedAccountContext(account) {
const normalized = String(account ?? "").trim();
return normalized === "19" || normalized === "68";
}
function isVatAllowedDocumentContext(documentType) {
return /(?:invoice|vat_document|purchase_book|sales_book|tax_entry|supplier_receipt|sales_document|register)/i.test(String(documentType ?? ""));
}
function isVatAllowedRelationPattern(pattern) {
return /(?:invoice_to_vat|register_to_book|book_entry_generated|deduction_posted|document_to_posting|contract_to_documents|source_doc_present|invoice_linked)/i.test(String(pattern ?? ""));
}
function isVatAllowedGraphDomain(domain) {
return /(?:vat_flow)/i.test(String(domain ?? ""));
}
function collectSourceRecords(data, sources) {
const items = [];
for (const source of sources) {
const records = data[source] ?? [];
for (const record of records) {
items.push({
source,
record
});
}
}
return items;
}
function evaluateDomainPurity(card, profile, signals, options) {
const strictForbidden = options?.strict_forbidden === true;
const accountScopeActive = profile.account_scope.length > 0;
const domainScopeActive = profile.domain_scope.length > 0;
const accountMatch = !accountScopeActive || intersects(profile.account_scope, signals.account_context);
const domainMatch = !domainScopeActive || intersects(profile.domain_scope, signals.domain_scope);
const entityMatch = intersects(card.allowed_entities, signals.entity_types);
const edgeMatch = intersects(card.expected_edges, signals.relation_patterns);
const cardAccountMatch = intersects(card.account_scope, signals.account_context);
const cardDomainMatch = intersects(card.domain_scope, signals.domain_scope);
const crossDomainOverlap = signals.domain_scope.filter((domain) => !card.domain_scope.includes(domain));
const hardForbiddenDomains = uniqueStrings(card.forbidden_cross_domain_leakage.filter((domain) => signals.domain_scope.includes(domain)));
const hasStrongTargetAnchor = cardAccountMatch || cardDomainMatch || edgeMatch;
const forbiddenDomains = strictForbidden ? hardForbiddenDomains : hasStrongTargetAnchor ? [] : hardForbiddenDomains;
const anchorMatch = cardAccountMatch || cardDomainMatch || edgeMatch;
const allowed = forbiddenDomains.length === 0 && accountMatch && domainMatch && anchorMatch && (entityMatch || edgeMatch || cardDomainMatch);
return {
allowed,
account_match: accountMatch && cardAccountMatch,
domain_match: domainMatch && cardDomainMatch,
entity_match: entityMatch,
edge_match: edgeMatch,
forbidden_domains: forbiddenDomains,
cross_domain_overlap: crossDomainOverlap
};
}
function applyDomainPuritySourceGate(input, card, profile, options) {
const accepted = [];
let rejectedTotal = 0;
let rejectedForbidden = 0;
for (const item of input) {
const signals = inferRecordSignals(item.record);
const purity = evaluateDomainPurity(card, profile, signals, options);
if (!purity.allowed) {
rejectedTotal += 1;
if (purity.forbidden_domains.length > 0) {
rejectedForbidden += 1;
}
continue;
}
accepted.push({
...item,
signals,
purity
});
}
return {
accepted,
rejected_total: rejectedTotal,
rejected_forbidden: rejectedForbidden
};
}
function enforceDomainPurityRanking(input, card, profile, options) {
const accepted = [];
let rejectedTotal = 0;
for (const item of input) {
const purity = evaluateDomainPurity(card, profile, item.signals, options);
if (!purity.allowed) {
rejectedTotal += 1;
continue;
}
accepted.push({
...item,
purity
});
}
return {
accepted,
rejected_total: rejectedTotal
};
}
function topThreePurityHolds(items) {
const topThree = items.slice(0, 3);
return topThree.every((item) => item.purity.allowed && item.purity.forbidden_domains.length === 0);
}
function topOnePurityHolds(items) {
const topOne = items[0];
if (!topOne) {
return true;
}
return topOne.purity.allowed && topOne.purity.forbidden_domains.length === 0;
}
function enforceDomainPurityPromotion(input, card, profile, options) {
const accepted = [];
let rejectedTotal = 0;
for (const item of input) {
const purity = evaluateDomainPurity(card, profile, item.signals, options);
if (!purity.allowed) {
rejectedTotal += 1;
continue;
}
accepted.push({
...item,
purity
});
}
return {
accepted,
rejected_total: rejectedTotal
};
}
function inferAccountsFromRecord(record, corpus) {
const accounts = [];
const accountTokens = corpus.match(/\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)\b/g) ?? [];
for (const token of accountTokens) {
accounts.push(token.split(".")[0]);
}
for (const key of Object.keys(record.attributes ?? {})) {
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
accounts.push("51");
}
if (/счетучетарасчетовсконтрагентом/i.test(key)) {
accounts.push("60");
}
if (/счетучетандс/i.test(key)) {
accounts.push("19");
}
if (/субконтодт/i.test(key)) {
accounts.push("60");
}
}
return uniqueStrings(accounts);
}
function inferDocumentTypesFromRecord(record, corpus) {
const items = [];
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
pushMany(items, ["bank_statement", "payment_order"]);
}
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
items.push("supplier_receipt");
}
if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
items.push("sales_document");
}
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
pushMany(items, ["invoice", "vat_document"]);
}
if (/корректировк|ручн|manual/i.test(corpus)) {
items.push("manual_operation");
}
if (/закрытие|регламент/i.test(corpus)) {
items.push("period_close_document");
}
if (/основн|амортиз|fixed_asset/i.test(corpus)) {
pushMany(items, ["fixed_asset_card", "depreciation_document"]);
}
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
items.push("deferred_expense_document");
}
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
items.push("settlement_document");
}
return uniqueStrings(items);
}
function inferDomainsFromRecord(corpus, documentTypes, record) {
const domains = [];
if (documentTypes.some((item) => item === "bank_statement" || item === "payment_order")) {
pushMany(domains, ["bank", "settlements"]);
}
if (documentTypes.some((item) => item === "supplier_receipt")) {
pushMany(domains, ["suppliers", "settlements"]);
}
if (documentTypes.some((item) => item === "sales_document")) {
pushMany(domains, ["customers", "settlements"]);
}
if (documentTypes.some((item) => item === "invoice" || item === "vat_document")) {
pushMany(domains, ["vat"]);
}
if (documentTypes.some((item) => item === "fixed_asset_card" || item === "depreciation_document")) {
pushMany(domains, ["fixed_assets"]);
}
if (documentTypes.some((item) => item === "deferred_expense_document")) {
pushMany(domains, ["deferred_expense", "period_close"]);
}
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(corpus);
const hasSettlementDocAnchor = documentTypes.some((item) => item === "bank_statement" || item === "payment_order" || item === "supplier_receipt" || item === "sales_document");
const hasSettlementDomainAnchor = domains.includes("bank") || domains.includes("suppliers") || domains.includes("customers") || domains.includes("supplier_payments");
if (findCounterpartyLinks(record).length > 0 && (hasSettlementLexicalAnchor || hasSettlementDocAnchor || hasSettlementDomainAnchor)) {
domains.push("settlements");
}
return uniqueStrings(domains);
}
function inferEntityTypes(record) {
const entities = [];
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
entities.push("document");
}
if (record.source_entity.startsWith("AccumulationRegister_")) {
entities.push("posting");
}
if (findCounterpartyLinks(record).length > 0) {
entities.push("counterparty");
}
const corpus = collectTextFromRecord(record);
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) {
entities.push("contract");
}
if (/основн|fixed_asset|инвентар/i.test(corpus)) {
entities.push("fixed_asset");
}
if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
entities.push("tax_entry");
}
return uniqueStrings(entities);
}
function inferRelationPatterns(record, corpus) {
const patterns = [];
const hasDocLinks = record.links.some((item) => item.target_entity === "Document");
const hasCounterparty = findCounterpartyLinks(record).length > 0;
if (hasDocLinks) {
patterns.push("document_to_posting");
}
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
patterns.push("payment_to_settlement");
}
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
patterns.push("statement_to_document");
}
if (/основн|fixed_asset|амортиз/i.test(corpus)) {
patterns.push("asset_card_to_depreciation");
}
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
patterns.push("deferred_expense_to_writeoff");
}
if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
patterns.push("invoice_to_vat");
}
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) {
patterns.push("contract_to_documents");
}
if (/склад|товар|материал|receipt/i.test(corpus)) {
patterns.push("receipt_to_stock_movement");
}
return uniqueStrings(patterns);
}
function inferLifecycleMarkers(record) {
const markers = ["created"];
if (record.attributes.Recorder && String(record.attributes.Recorder).trim()) {
markers.push("posted");
}
const unknownLinks = Number(record.unknown_link_count ?? 0);
const zeroGuidValues = countZeroGuidValues(record);
if (unknownLinks > 0 || zeroGuidValues > 0) {
markers.push("partially_linked");
}
const period = extractDate(record);
if (period && /-30T|-31T/.test(period)) {
markers.push("period_boundary");
}
if (record.links.length === 0) {
markers.push("no_continuation");
}
return uniqueStrings(markers);
}
function inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers) {
const anomalies = [];
const hasDocLinks = record.links.some((item) => item.target_entity === "Document");
const hasCounterparty = findCounterpartyLinks(record).length > 0;
const unknownLinks = Number(record.unknown_link_count ?? 0);
const zeroGuidValues = countZeroGuidValues(record);
if (!hasCounterparty || !hasDocLinks || unknownLinks > 0) {
anomalies.push("missing_link");
}
if (lifecycleMarkers.includes("partially_linked") || lifecycleMarkers.includes("no_continuation")) {
anomalies.push("broken_lifecycle");
}
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
anomalies.push("posting_mismatch");
}
if (/ручн|manual|корректировк/.test(corpus)) {
anomalies.push("manual_intervention_suspicion");
}
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
anomalies.push("closure_risk");
}
if (countNavigationLinks(record) >= 3 && (unknownLinks > 0 || zeroGuidValues > 0)) {
anomalies.push("repeated_anomaly");
}
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
anomalies.push("silent_orphan");
}
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|СЃСѓРјРј|РёСРѕРіРѕ|amount/i.test(key));
if (!hasAmountSignal && anomalies.length > 0) {
anomalies.push("amount_independent_risk");
}
const domains = inferDomainsFromRecord(corpus, inferDocumentTypesFromRecord(record, corpus), record);
if (domains.includes("bank") && domains.includes("vat")) {
anomalies.push("cross_domain_inconsistency");
}
return uniqueStrings(anomalies);
}
function inferRecordSignals(record) {
const corpus = collectTextFromRecord(record);
const accountContext = inferAccountsFromRecord(record, corpus);
const documentTypes = inferDocumentTypesFromRecord(record, corpus);
const entityTypes = inferEntityTypes(record);
const relationPatterns = inferRelationPatterns(record, corpus);
const lifecycleMarkers = inferLifecycleMarkers(record);
const anomalyPatterns = inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers);
const domains = inferDomainsFromRecord(corpus, documentTypes, record);
return {
account_context: accountContext,
domain_scope: domains,
document_types: documentTypes,
entity_types: entityTypes,
relation_patterns: relationPatterns,
anomaly_patterns: anomalyPatterns,
lifecycle_markers: lifecycleMarkers
};
}
function intersects(left, right) {
if (left.length === 0) {
return true;
}
const rightSet = new Set(right);
return left.some((item) => rightSet.has(item));
}
function evaluateExcludedInterpretations(profile, signals, record) {
const reasons = [];
const interpretationSet = new Set(profile.excluded_interpretations);
if (interpretationSet.has("simple_payment_delay")) {
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 без структурного дефекта.");
}
}
if (interpretationSet.has("amount_only_anomaly")) {
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 аномалия без структурных признаков.");
}
}
return {
excluded: reasons.length > 0,
reasons
};
}
function evaluateGraphTraversalForRecord(profile, signals) {
const graphDomains = toGraphDomainScope(signals.domain_scope, signals.account_context);
const runtimeSignals = inferGraphRuntimeSignals(signals);
if (!profile.graph_traversal.runtime_enabled || !profile.graph_traversal.eligible) {
return {
domain_match: false,
relation_match: false,
signal_match: false,
lifecycle_match: false,
runtime_signals: runtimeSignals,
graph_domains: graphDomains,
score: 0
};
}
const domainMatch = intersects(profile.graph_traversal.target_domains, graphDomains);
const relationMatch = intersects(profile.graph_traversal.target_relations, signals.relation_patterns);
const signalMatch = intersects(profile.graph_traversal.target_signals, [...signals.anomaly_patterns, ...runtimeSignals]);
const lifecycleMatch = intersects(profile.graph_traversal.target_lifecycle_markers, signals.lifecycle_markers);
const score = (domainMatch ? 3 : 0) +
(relationMatch ? 3 : 0) +
(signalMatch ? 2 : 0) +
(lifecycleMatch ? 1 : 0) +
Math.min(2, runtimeSignals.length);
return {
domain_match: domainMatch,
relation_match: relationMatch,
signal_match: signalMatch,
lifecycle_match: lifecycleMatch,
runtime_signals: runtimeSignals,
graph_domains: graphDomains,
score
};
}
function evaluateRecordAgainstProfile(record, profile) {
const signals = inferRecordSignals(record);
const accountMatch = intersects(profile.account_scope, signals.account_context);
const domainMatch = intersects(profile.domain_scope, signals.domain_scope);
const documentMatch = intersects(profile.document_types, signals.document_types);
const entityMatch = intersects(profile.entity_types, signals.entity_types);
const relationMatch = intersects(profile.relation_patterns, signals.relation_patterns);
const anomalyMatch = intersects(profile.anomaly_patterns, signals.anomaly_patterns);
const lifecycleMatch = intersects(profile.lifecycle_stage_filters, signals.lifecycle_markers);
const excluded = evaluateExcludedInterpretations(profile, signals, record);
const graphTraversal = evaluateGraphTraversalForRecord(profile, signals);
const matchReasons = [];
if (accountMatch && profile.account_scope.length > 0) {
matchReasons.push("Совпал account_scope.");
}
if (domainMatch && profile.domain_scope.length > 0) {
matchReasons.push("Совпал domain_scope.");
}
if (documentMatch && profile.document_types.length > 0) {
matchReasons.push("Совпал document_types.");
}
if (relationMatch && profile.relation_patterns.length > 0) {
matchReasons.push("Совпали relation_patterns.");
}
if (anomalyMatch && profile.anomaly_patterns.length > 0) {
matchReasons.push("Совпали anomaly_patterns.");
}
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
matchReasons.push("Совпал lifecycle_stage_filters.");
}
if (graphTraversal.domain_match) {
matchReasons.push("Graph traversal domain matched.");
}
if (graphTraversal.relation_match) {
matchReasons.push("Graph traversal relation matched.");
}
if (graphTraversal.signal_match) {
matchReasons.push("Graph traversal signal matched.");
}
if (graphTraversal.runtime_signals.length > 0) {
matchReasons.push(`Graph runtime signals: ${graphTraversal.runtime_signals.join(", ")}.`);
}
const matchScore = (accountMatch ? 3 : 0) +
(domainMatch ? 3 : 0) +
(documentMatch ? 2 : 0) +
(entityMatch ? 1 : 0) +
(relationMatch ? 3 : 0) +
(anomalyMatch ? 2 : 0) +
(lifecycleMatch ? 1 : 0) +
Math.min(2, signals.anomaly_patterns.length) +
graphTraversal.score;
return {
signals,
account_match: accountMatch,
domain_match: domainMatch,
document_match: documentMatch,
entity_match: entityMatch,
relation_match: relationMatch,
anomaly_match: anomalyMatch,
lifecycle_match: lifecycleMatch,
graph_domain_match: graphTraversal.domain_match,
graph_relation_match: graphTraversal.relation_match,
graph_signal_match: graphTraversal.signal_match,
graph_lifecycle_match: graphTraversal.lifecycle_match,
graph_runtime_signals: graphTraversal.runtime_signals,
graph_domain_scope: graphTraversal.graph_domains,
graph_traversal_score: graphTraversal.score,
excluded_by_interpretation: excluded.excluded,
match_score: matchScore,
match_reasons: matchReasons,
excluded_reasons: excluded.reasons
};
}
function shouldIncludeSemanticCandidate(candidate, profile) {
if (candidate.excluded_by_interpretation) {
return false;
}
if (!candidate.account_match && profile.account_scope.length > 0) {
return false;
}
if (!candidate.domain_match && profile.domain_scope.length > 0) {
return false;
}
const softAxes = [
profile.document_types.length > 0,
profile.entity_types.length > 0,
profile.relation_patterns.length > 0,
profile.anomaly_patterns.length > 0
].filter(Boolean).length;
const softHits = [candidate.document_match, candidate.entity_match, candidate.relation_match, candidate.anomaly_match].filter(Boolean).length;
const requiredSoftHits = softAxes > 0 ? 1 : 0;
const baseIncluded = softHits >= requiredSoftHits;
if (!baseIncluded) {
return false;
}
if (profile.graph_traversal.runtime_enabled && profile.graph_traversal.eligible) {
const graphHits = [
candidate.graph_domain_match,
candidate.graph_relation_match,
candidate.graph_signal_match,
candidate.graph_lifecycle_match
].filter(Boolean).length;
if (graphHits === 0 && candidate.graph_runtime_signals.length === 0 && candidate.match_score < 8) {
return false;
}
}
return true;
}
function semanticNarrowCandidates(records, profile) {
const evaluated = records.map((record) => ({
record,
evaluation: evaluateRecordAgainstProfile(record, profile)
}));
let narrowed = evaluated.filter((item) => shouldIncludeSemanticCandidate(item.evaluation, profile));
if (narrowed.length > 0) {
if (narrowed.length === records.length && records.length > 1) {
const keepCount = Math.max(2, Math.floor(records.length * 0.85));
return narrowed
.sort((left, right) => right.evaluation.match_score - left.evaluation.match_score)
.slice(0, keepCount);
}
return narrowed;
}
narrowed = evaluated
.filter((item) => !item.evaluation.excluded_by_interpretation &&
(item.evaluation.account_match || item.evaluation.domain_match || item.evaluation.relation_match))
.sort((left, right) => right.evaluation.match_score - left.evaluation.match_score)
.slice(0, Math.max(10, Math.floor(records.length * 0.35)));
if (narrowed.length > 0) {
return narrowed;
}
return evaluated
.filter((item) => !item.evaluation.excluded_by_interpretation)
.sort((left, right) => right.evaluation.match_score - left.evaluation.match_score)
.slice(0, Math.max(8, Math.floor(records.length * 0.2)));
}
function toSnapshotRecords(payload) {
if (!payload || typeof payload !== "object") {
return [];
}
const data = payload;
if (!Array.isArray(data.records)) {
return [];
}
const records = [];
for (const item of data.records) {
if (!item || typeof item !== "object") {
continue;
}
const source = item;
if (typeof source.source_entity !== "string" || typeof source.source_id !== "string") {
continue;
}
const links = Array.isArray(source.links)
? source.links
.map((link) => {
if (!link || typeof link !== "object")
return null;
const value = link;
return {
relation: String(value.relation ?? ""),
target_entity: String(value.target_entity ?? ""),
target_id: String(value.target_id ?? ""),
source_field: String(value.source_field ?? "")
};
})
.filter((link) => link !== null)
: [];
records.push({
problem_flags: Array.isArray(source.problem_flags) ? source.problem_flags.map((item) => String(item)) : [],
unknown_link_count: typeof source.unknown_link_count === "number" ? source.unknown_link_count : 0,
source_entity: source.source_entity,
source_id: source.source_id,
display_name: typeof source.display_name === "string" ? source.display_name : source.source_id,
attributes: source.attributes && typeof source.attributes === "object" && !Array.isArray(source.attributes)
? source.attributes
: {},
links
});
}
return records;
}
class AssistantDataLayer {
rootDir;
cache = null;
constructor(rootDir = config_1.ARCH_EXPORT_2020_DIR) {
this.rootDir = rootDir;
}
executeRoute(route, fragmentText) {
const data = this.ensureData();
if (!data) {
return {
status: "error",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Snapshot data files could not be loaded."],
errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
};
}
let result = null;
if (route === "hybrid_store_plus_live") {
result = this.executeHybrid(fragmentText, data);
}
else if (route === "store_feature_risk") {
result = this.executeRisk(fragmentText, data);
}
else if (route === "batch_refresh_then_store") {
result = this.executeBatch(fragmentText, data);
}
else if (route === "store_canonical") {
result = this.executeCanonical(fragmentText, data);
}
else if (route === "live_mcp_drilldown") {
result = this.executeDrilldown(fragmentText, data);
}
if (!result) {
return {
status: "error",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Route is not implemented in current data executor."],
errors: [`Маршрут ${route} не поддержан в текущем исполнителе.`]
};
}
return enforceBroadQueryGuards(route, fragmentText, result);
}
async executeRouteRuntime(route, fragmentText) {
const base = this.executeRoute(route, fragmentText);
if (!config_1.FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
return base;
}
if (route !== "hybrid_store_plus_live" && route !== "live_mcp_drilldown") {
return base;
}
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
return this.mergeWithLiveOverlay(base, liveOverlay);
}
cloneRawResult(base) {
return {
...base,
items: [...base.items],
summary: { ...(base.summary ?? {}) },
evidence: [...base.evidence],
why_included: [...base.why_included],
selection_reason: [...base.selection_reason],
risk_factors: [...base.risk_factors],
business_interpretation: [...base.business_interpretation],
limitations: [...base.limitations],
errors: [...base.errors]
};
}
mergeWithLiveOverlay(base, liveOverlay) {
const merged = this.cloneRawResult(base);
merged.summary = {
...(merged.summary ?? {}),
live_mcp: liveOverlay.summary
};
for (const line of liveOverlay.selection_reason) {
pushUniqueLine(merged.selection_reason, line);
}
for (const line of liveOverlay.limitations) {
pushUniqueLine(merged.limitations, line);
}
for (const error of liveOverlay.errors) {
pushUniqueLine(merged.errors, error);
}
if (liveOverlay.status === "ok" && liveOverlay.items.length > 0) {
if (merged.items.length === 0 || merged.status === "empty") {
merged.items = [...liveOverlay.items];
merged.status = "ok";
merged.result_type = "list";
pushUniqueLine(merged.why_included, "Добавлены live-сигналы из 1С через MCP.");
pushUniqueLine(merged.business_interpretation, "Snapshot не дал опорные записи, поэтому добавлены живые строки движений 1С для первичной проверки.");
if (merged.confidence === "low") {
merged.confidence = "medium";
}
}
else {
pushUniqueLine(merged.why_included, "Live MCP использован как дополнительное доказательство к snapshot-выдаче.");
}
merged.evidence = [...merged.evidence, ...liveOverlay.evidence].slice(0, 16);
}
return merged;
}
async fetchLiveMcpOverlay(route, fragmentText) {
const endpoint = this.buildMcpUrl("/api/execute_query");
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
const accountScope = explicitAccountScope.length > 0
? explicitAccountScope
: livePlan.claim_type === "prove_rbp_tail_state"
? ["97", "20", "25", "26", "44"]
: [];
const callExecutions = [];
const collectedRows = [];
const errors = [];
let fetchedRowsTotal = 0;
let matchedRowsTotal = 0;
for (const call of livePlan.calls) {
const callAccountScope = Array.isArray(call.account_scope_override) && call.account_scope_override.length > 0
? call.account_scope_override
: accountScope;
try {
const payload = await this.fetchJsonWithTimeout(endpoint, {
query: call.query,
limit: config_1.ASSISTANT_MCP_LIVE_LIMIT
});
const parsed = this.parseExecuteQueryPayload(payload);
if (parsed.error) {
errors.push(parsed.error);
callExecutions.push({
call_id: call.call_id,
purpose: call.purpose,
required_for_claim: call.required_for_claim,
status: "error",
fetched_rows: 0,
matched_rows: 0,
returned_rows: 0,
error: parsed.error
});
continue;
}
const matchedRows = this.filterLiveRowsByAccountScope(parsed.rows, callAccountScope);
const rowsForAnswer = callAccountScope.length > 0 ? matchedRows : parsed.rows;
fetchedRowsTotal += parsed.rows.length;
matchedRowsTotal += matchedRows.length;
for (const row of rowsForAnswer) {
collectedRows.push({
...row,
__live_call_id: call.call_id,
__live_call_purpose: call.purpose,
__claim_type: livePlan.claim_type,
__query_subject: livePlan.query_subject,
__account_scope_applied: callAccountScope
});
}
callExecutions.push({
call_id: call.call_id,
purpose: call.purpose,
required_for_claim: call.required_for_claim,
status: rowsForAnswer.length > 0 ? "ok" : "empty",
fetched_rows: parsed.rows.length,
matched_rows: matchedRows.length,
returned_rows: rowsForAnswer.length,
error: null
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(errorMessage);
callExecutions.push({
call_id: call.call_id,
purpose: call.purpose,
required_for_claim: call.required_for_claim,
status: "error",
fetched_rows: 0,
matched_rows: 0,
returned_rows: 0,
error: errorMessage
});
}
}
const items = this.toLiveOverlayItems(collectedRows.slice(0, 16), route);
const evidence = items.slice(0, 12).map((item) => ({
source_entity: item.source_entity,
source_id: item.source_id,
source_namespace: "assistant_derived",
period: item.period,
account_debit: item.account_debit,
account_credit: item.account_credit,
source_layer: item.source_layer,
document_context: item.document_context,
relation_pattern_hits: item.relation_pattern_hits,
lifecycle_markers: item.lifecycle_markers,
live_call_id: item.live_call_id,
live_call_purpose: item.live_call_purpose,
claim_type: item.claim_type
}));
const executedRequiredCalls = callExecutions
.filter((item) => item.required_for_claim && item.status !== "error")
.map((item) => item.call_id);
const missingLiveCalls = livePlan.required_live_calls.filter((callId) => !executedRequiredCalls.includes(callId));
const liveRouteExecutionRate = livePlan.required_live_calls.length > 0
? Number((executedRequiredCalls.length / livePlan.required_live_calls.length).toFixed(4))
: 1;
const routeGapReason = missingLiveCalls.length > 0
? "required_live_calls_not_executed"
: livePlan.claim_type && matchedRowsTotal <= 0
? "claim_live_calls_executed_but_zero_matches"
: livePlan.route_gap_reason;
const selectionReason = [
livePlan.claim_type
? `Claim-bound live path selected for ${livePlan.claim_type}.`
: `Live MCP probe: ${fetchedRowsTotal} rows fetched from 1C register.`,
accountScope.length > 0
? `Account scope filter (${accountScope.join(", ")}) matched ${matchedRowsTotal} rows.`
: "Account scope filter was not applied."
];
const limitations = [
"Live probe использует ограниченный выборочный read-only запрос к 1С."
];
if (missingLiveCalls.length > 0) {
limitations.push(`Required live calls were not executed: ${missingLiveCalls.join(", ")}.`);
}
if (items.length === 0) {
limitations.push("Live probe не вернул строк, релевантных текущему запросу.");
}
if (errors.length > 0) {
limitations.push("Часть live вызовов завершилась ошибкой; включен ограниченный fallback.");
}
const status = items.length > 0 ? "ok" : callExecutions.every((item) => item.status === "error") ? "error" : "empty";
return {
status,
items,
evidence,
summary: {
enabled: true,
status,
route,
channel: config_1.ASSISTANT_MCP_CHANNEL,
proxy: config_1.ASSISTANT_MCP_PROXY_URL,
source_profile: livePlan.claim_type ? "claim_bound_rbp_live_path" : "generic_live_probe",
claim_type: livePlan.claim_type,
query_subject: livePlan.query_subject,
account_scope: accountScope,
fetched_rows: fetchedRowsTotal,
matched_rows: matchedRowsTotal,
returned_rows: items.length,
required_live_calls: livePlan.required_live_calls,
executed_live_calls: callExecutions,
missing_live_calls: missingLiveCalls,
live_route_execution_rate: liveRouteExecutionRate,
route_gap_reason: routeGapReason
},
selection_reason: selectionReason,
limitations,
errors
};
}
async fetchJsonWithTimeout(url, body) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), Math.max(200, config_1.ASSISTANT_MCP_TIMEOUT_MS));
try {
const response = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json; charset=utf-8"
},
body: JSON.stringify(body),
signal: controller.signal
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(`MCP HTTP ${response.status}: ${responseText.slice(0, 300)}`);
}
if (!responseText.trim()) {
return {};
}
return JSON.parse(responseText);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`MCP fetch failed: ${message}`);
}
finally {
clearTimeout(timeout);
}
}
parseExecuteQueryPayload(payload) {
if (!payload || typeof payload !== "object") {
return {
rows: [],
error: "MCP payload is empty or malformed."
};
}
const source = payload;
if (source.success !== true) {
const errorMessage = valueAsString(source.error).trim();
return {
rows: [],
error: errorMessage || "MCP execute_query returned success=false."
};
}
const data = source.data;
if (Array.isArray(data)) {
return {
rows: data
.map((item) => (item && typeof item === "object" ? item : null))
.filter((item) => item !== null),
error: null
};
}
if (typeof data === "string") {
return {
rows: this.parseRowsFromTextTable(data),
error: null
};
}
if (data && typeof data === "object" && Array.isArray(data.rows)) {
const rows = (data.rows ?? [])
.map((item) => (item && typeof item === "object" ? item : null))
.filter((item) => item !== null);
return {
rows,
error: null
};
}
return {
rows: [],
error: null
};
}
parseRowsFromTextTable(source) {
const normalized = String(source ?? "").replace(/\r/g, "").trim();
if (!normalized) {
return [];
}
const headerMatch = normalized.match(/\{([^}]*)\}:/);
if (!headerMatch) {
return [];
}
const header = headerMatch[1] ?? "";
const columns = header
.split(",")
.map((item) => item.replace(/^"+|"+$/g, "").trim())
.filter((item) => item.length > 0);
const body = normalized.slice((headerMatch.index ?? 0) + headerMatch[0].length).trim();
if (!body) {
return [];
}
const rows = [];
const lines = body
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
for (const line of lines) {
const values = this.parseTextRowValues(line);
if (values.length === 0) {
continue;
}
const row = {};
for (let index = 0; index < columns.length; index += 1) {
const key = columns[index] ?? `column_${index + 1}`;
const raw = values[index] ?? "";
const numeric = parseFiniteNumber(raw);
const looksNumeric = /^-?\d+(?:[.,]\d+)?$/.test(raw);
row[key] = numeric !== null && looksNumeric ? numeric : raw;
}
if (values[0])
row.Period = values[0];
if (values[1])
row.Registrator = values[1];
if (values[2])
row.AccountDt = values[2];
if (values[3])
row.AccountKt = values[3];
if (values[4])
row.Amount = parseFiniteNumber(values[4]) ?? values[4];
rows.push(row);
}
return rows;
}
parseTextRowValues(line) {
const values = [];
const matcher = /"([^"]*)"|([^,]+)/g;
let match = null;
while ((match = matcher.exec(line)) !== null) {
const raw = match[1] !== undefined ? match[1] : match[2];
const value = String(raw ?? "").trim();
if (value.length > 0) {
values.push(value);
}
}
return values;
}
filterLiveRowsByAccountScope(rows, accountScope) {
if (accountScope.length === 0) {
return rows;
}
const matchers = accountScope.map((account) => new RegExp(`\\b${escapeRegExp(account)}(?:\\.\\d{2})?\\b`, "i"));
return rows.filter((row) => {
const searchable = Object.values(row).map(valueAsString).join(" ");
return matchers.some((matcher) => matcher.test(searchable));
});
}
toLiveOverlayItems(rows, route) {
return rows.map((row, index) => {
const periodRaw = valueAsString(row.Период ?? row.period ?? row.Period).trim();
const registrator = valueAsString(row.Регистратор ?? row.registrator ?? row.Registrator).trim();
const debit = valueAsString(row.СчетДт ?? row.account_dt ?? row.AccountDt).trim();
const credit = valueAsString(row.СчетКт ?? row.account_kt ?? row.AccountKt).trim();
const amount = parseFiniteNumber(row.Сумма ?? row.amount ?? row.Amount);
const baseId = `${route}-mcp-${index + 1}`;
const sourceId = periodRaw ? `${baseId}-${periodRaw}` : baseId;
const displayName = registrator || `Live movement row #${index + 1}`;
const accountContext = uniqueStrings([debit, credit].filter((item) => item.length > 0));
const callId = valueAsString(row.__live_call_id ?? "").trim();
const callPurpose = valueAsString(row.__live_call_purpose ?? "").trim();
const claimType = valueAsString(row.__claim_type ?? "").trim() || null;
const querySubject = valueAsString(row.__query_subject ?? "").trim() || null;
const registratorLower = registrator.toLowerCase();
const hasRbpByDocument = /(?:рбп|deferred|списани[ея]\s+рбп)/i.test(registratorLower);
const hasAccount97 = accountContext.some((item) => /^97(?:\.|$)/.test(item));
const hasCloseDoc = /(?:закрыти[ея]\s+месяц|period\s*close|month\s*close|close\s+operation)/i.test(registratorLower) ||
callId.includes("month_close");
const relationPatternHits = uniqueStrings([
"document_to_posting",
hasRbpByDocument || hasAccount97 ? "deferred_expense_to_writeoff" : "",
hasCloseDoc ? "close_operation" : "",
callId.includes("residual") ? "residuals_zero_or_explained" : ""
]);
const documentContext = uniqueStrings([
hasRbpByDocument || hasAccount97 ? "deferred_expense_document" : "",
hasCloseDoc ? "period_close_document" : "",
"posting"
]);
const graphDomainScope = uniqueStrings([
hasRbpByDocument || hasAccount97 ? "deferred_expense" : "",
hasCloseDoc ? "period_close" : ""
]);
const lifecycleMarkers = uniqueStrings([
callId.includes("residual") ? "period_boundary" : "",
callId.includes("residual") ? "tail_state_observed" : "",
hasCloseDoc ? "close_operation" : ""
]);
return {
source_entity: "MCPLiveMovement",
source_id: sourceId,
display_name: displayName,
period: periodRaw || null,
account_debit: debit || null,
account_credit: credit || null,
account_context: accountContext,
document_context: documentContext,
relation_pattern_hits: relationPatternHits,
graph_domain_scope: graphDomainScope,
lifecycle_markers: lifecycleMarkers,
source_namespace: "assistant_derived",
live_call_id: callId || null,
live_call_purpose: callPurpose || null,
claim_type: claimType,
query_subject: querySubject,
amount,
source_layer: "mcp_live_probe",
route
};
});
}
buildMcpUrl(endpoint) {
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
const separator = normalizedEndpoint.includes("?") ? "&" : "?";
return `${config_1.ASSISTANT_MCP_PROXY_URL}${normalizedEndpoint}${separator}channel=${encodeURIComponent(config_1.ASSISTANT_MCP_CHANNEL)}`;
}
ensureData() {
if (this.cache) {
return this.cache;
}
try {
const keyFields = this.readRecords("09_samples_key_fields_Recorder_Ref_Supplier_Buyer_Responsible.json");
const problemCases = this.readRecords("03_snapshot_fragment_problem_cases.json");
const journals = this.readRecords("07_samples_DocumentJournals.json");
const ndsRegisters = this.readRecords("08_samples_NDS_registers.json");
const docs = [
...this.readRecords("04_samples_SpisanieSRaschetnogoScheta.json"),
...this.readRecords("05_samples_RealizaciyaTovarovUslug.json"),
...this.readRecords("06_samples_PostuplenieTovarovUslug.json")
];
this.cache = {
keyFields,
problemCases,
journals,
ndsRegisters,
docs
};
return this.cache;
}
catch {
return null;
}
}
readRecords(fileName) {
const fullPath = path_1.default.resolve(this.rootDir, fileName);
if (!fs_1.default.existsSync(fullPath)) {
return [];
}
const raw = fs_1.default.readFileSync(fullPath, "utf-8");
const parsed = JSON.parse(raw);
return toSnapshotRecords(parsed);
}
executeHybrid(fragmentText, data) {
const guidFilter = extractGuids(fragmentText);
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const resolvedDomain = resolveP0DomainCard(fragmentText, semanticProfile);
const domainCard = resolvedDomain?.card ?? null;
const fallbackSources = ["keyFields", "journals", "docs"];
const sourceScope = domainCard
? uniqueStrings([...domainCard.allowed_evidence_sources.risk, ...domainCard.allowed_evidence_sources.canonical])
: fallbackSources;
const sourcePool = collectSourceRecords(data, sourceScope);
const strictForbidden = domainCard ? shouldUseStrictForbiddenDomainGate(domainCard, semanticProfile, fragmentText) : false;
let sourceGate = domainCard
? applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: sourcePool.map((item) => ({
...item,
signals: inferRecordSignals(item.record),
purity: {
allowed: true,
account_match: true,
domain_match: true,
entity_match: true,
edge_match: true,
forbidden_domains: [],
cross_domain_overlap: []
}
})),
rejected_total: 0,
rejected_forbidden: 0
};
let strictForbiddenFallbackUsed = false;
if (domainCard && strictForbidden && sourceGate.accepted.length === 0 && sourcePool.length > 0) {
sourceGate = applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: false });
strictForbiddenFallbackUsed = true;
}
let settlementSourceRecoveryUsed = false;
if (domainCard?.id === "settlements_60_62" && sourceGate.accepted.length === 0 && sourcePool.length > 0) {
const recovered = sourcePool
.map((item) => ({
...item,
signals: inferRecordSignals(item.record),
purity: {
allowed: true,
account_match: true,
domain_match: true,
entity_match: true,
edge_match: true,
forbidden_domains: [],
cross_domain_overlap: []
}
}))
.filter((item) => hasSettlementRecoverySignal(item.signals));
if (recovered.length > 0) {
sourceGate = {
accepted: recovered,
rejected_total: Math.max(0, sourcePool.length - recovered.length),
rejected_forbidden: sourceGate.rejected_forbidden
};
settlementSourceRecoveryUsed = true;
}
}
const sourceRecords = sourceGate.accepted.map((item) => item.record);
let narrowedCandidates = guidFilter.length > 0
? sourceRecords
.filter((record) => guidFilter.some((guid) => hasGuidMatch(record, guid)))
.map((record) => ({
record,
evaluation: evaluateRecordAgainstProfile(record, semanticProfile)
}))
: semanticNarrowCandidates(sourceRecords, semanticProfile);
let settlementNarrowingRecoveryUsed = false;
if (domainCard?.id === "settlements_60_62" &&
guidFilter.length === 0 &&
narrowedCandidates.length === 0 &&
sourceRecords.length > 0 &&
isSettlementSymptomQuery(fragmentText, semanticProfile)) {
const recovered = sourceRecords
.map((record) => ({
record,
evaluation: evaluateRecordAgainstProfile(record, semanticProfile)
}))
.filter((item) => hasSettlementRecoverySignal(item.evaluation.signals));
if (recovered.length > 0) {
narrowedCandidates = recovered;
settlementNarrowingRecoveryUsed = true;
}
}
const filtered = narrowedCandidates.map((item) => item.record);
const semanticNarrowingApplied = guidFilter.length === 0;
const graphTraversalRuntime = summarizeGraphTraversalRuntime(narrowedCandidates, semanticProfile);
const puritySummary = {
enabled: Boolean(domainCard),
domain_card_id: domainCard?.id ?? null,
domain_card_title: domainCard?.title ?? null,
source_scope: sourceScope,
source_pool_records: sourcePool.length,
source_selection_allowed: sourceGate.accepted.length,
source_selection_rejected: sourceGate.rejected_total,
source_selection_rejected_forbidden: sourceGate.rejected_forbidden,
ranking_checked: narrowedCandidates.length,
ranking_allowed: narrowedCandidates.length,
ranking_rejected: 0,
promotion_checked: 0,
promotion_allowed: 0,
promotion_rejected: 0,
top1_pure: true,
top3_pure: true,
strict_forbidden_mode: strictForbidden,
strict_forbidden_fallback: strictForbiddenFallbackUsed,
settlement_source_recovery: settlementSourceRecoveryUsed,
settlement_narrowing_recovery: settlementNarrowingRecoveryUsed
};
const groups = new Map();
for (const candidate of narrowedCandidates) {
const { record, evaluation } = candidate;
const cpLinks = findCounterpartyLinks(record);
if (cpLinks.length === 0) {
continue;
}
const docLinks = record.links.filter((link) => link.target_entity === "Document");
for (const link of cpLinks) {
const key = link.target_id || `unknown:${record.source_id}`;
let group = groups.get(key);
if (!group) {
group = {
counterparty_id: key,
operations_count: 0,
document_ids: new Set(),
relations: new Map(),
samples: [],
account_context: new Set(),
document_context: new Set(),
relation_pattern_hits: new Set(),
risk_factors: new Set(),
lifecycle_gaps: new Set(),
graph_runtime_signals: new Set(),
graph_domain_scope: new Set(),
selection_reasons: new Set(),
total_match_score: 0,
total_graph_traversal_score: 0,
graph_match_hits: 0
};
groups.set(key, group);
}
group.operations_count += 1;
group.total_match_score += evaluation.match_score;
group.total_graph_traversal_score += evaluation.graph_traversal_score;
if (evaluation.graph_traversal_score > 0) {
group.graph_match_hits += 1;
}
for (const relation of [link.relation, ...docLinks.map((item) => item.relation)]) {
group.relations.set(relation, (group.relations.get(relation) ?? 0) + 1);
}
for (const account of evaluation.signals.account_context) {
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedAccountContext(account)) {
continue;
}
if (semanticProfile.account_scope.length === 0 || semanticProfile.account_scope.includes(account)) {
group.account_context.add(account);
}
}
for (const item of evaluation.signals.document_types) {
if (domainCard?.id === "settlements_60_62" &&
!["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document", "manual_operation"].includes(item)) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedDocumentContext(item)) {
continue;
}
group.document_context.add(item);
}
for (const item of evaluation.signals.relation_patterns) {
if (domainCard?.id === "settlements_60_62" &&
!["payment_to_settlement", "statement_to_document", "contract_to_documents", "document_to_posting"].includes(item)) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedRelationPattern(item)) {
continue;
}
group.relation_pattern_hits.add(item);
}
for (const item of evaluation.signals.anomaly_patterns) {
group.risk_factors.add(item);
}
for (const item of evaluation.signals.lifecycle_markers) {
if (item === "partially_linked" || item === "no_continuation" || item === "period_boundary") {
group.lifecycle_gaps.add(item);
}
}
for (const item of evaluation.graph_runtime_signals) {
group.graph_runtime_signals.add(item);
}
for (const domain of evaluation.graph_domain_scope) {
if (domainCard?.id === "settlements_60_62" &&
!["bank_settlement", "customer_settlement"].includes(domain)) {
continue;
}
if (domainCard?.id === "vat_document_register_book" && !isVatAllowedGraphDomain(domain)) {
continue;
}
group.graph_domain_scope.add(domain);
}
for (const reason of evaluation.match_reasons.slice(0, 4)) {
group.selection_reasons.add(reason);
}
for (const docLink of docLinks) {
if (docLink.target_id) {
group.document_ids.add(docLink.target_id);
}
}
if (group.samples.length < 3) {
const unknownLinks = Number(record.unknown_link_count ?? 0);
const sampleAccountContext = domainCard?.id === "settlements_60_62"
? evaluation.signals.account_context.filter((item) => ["51", "60", "62", "76"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.account_context.filter((item) => isVatAllowedAccountContext(item))
: evaluation.signals.account_context;
const sampleDocumentContext = domainCard?.id === "settlements_60_62"
? evaluation.signals.document_types.filter((item) => ["bank_statement", "payment_order", "settlement_document", "supplier_receipt", "sales_document", "manual_operation"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.document_types.filter((item) => isVatAllowedDocumentContext(item))
: evaluation.signals.document_types;
const sampleRelationPatterns = domainCard?.id === "settlements_60_62"
? evaluation.signals.relation_patterns.filter((item) => ["payment_to_settlement", "statement_to_document", "contract_to_documents", "document_to_posting"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.signals.relation_patterns.filter((item) => isVatAllowedRelationPattern(item))
: evaluation.signals.relation_patterns;
const sampleGraphDomainScope = domainCard?.id === "settlements_60_62"
? evaluation.graph_domain_scope.filter((item) => ["bank_settlement", "customer_settlement"].includes(item))
: domainCard?.id === "vat_document_register_book"
? evaluation.graph_domain_scope.filter((item) => isVatAllowedGraphDomain(item))
: evaluation.graph_domain_scope;
group.samples.push({
source_entity: record.source_entity,
source_id: record.source_id,
period: extractDate(record),
recorder: record.attributes.Recorder ?? null,
account_context: sampleAccountContext,
document_context: sampleDocumentContext,
relation_patterns: sampleRelationPatterns,
anomaly_patterns: evaluation.signals.anomaly_patterns,
lifecycle_markers: evaluation.signals.lifecycle_markers,
graph_runtime_signals: evaluation.graph_runtime_signals,
graph_domain_scope: sampleGraphDomainScope,
graph_traversal_score: evaluation.graph_traversal_score,
missing_links: unknownLinks
});
}
}
}
const items = Array.from(groups.values())
.map((group) => ({
entity_type: "counterparty",
entity_id: group.counterparty_id,
label: group.counterparty_id,
counterparty_id: group.counterparty_id,
operations_count: group.operations_count,
document_refs_count: group.document_ids.size,
account_context: Array.from(group.account_context),
document_context: Array.from(group.document_context),
relation_pattern_hits: Array.from(group.relation_pattern_hits),
risk_factors: Array.from(group.risk_factors),
lifecycle_gaps: Array.from(group.lifecycle_gaps),
graph_runtime_signals: Array.from(group.graph_runtime_signals),
graph_domain_scope: Array.from(group.graph_domain_scope),
graph_match_hits: group.graph_match_hits,
graph_traversal_score: Number((group.total_graph_traversal_score / Math.max(1, group.operations_count)).toFixed(2)),
selection_reason: Array.from(group.selection_reasons).slice(0, 6),
ranking_score: Number((group.operations_count +
group.risk_factors.size * 2 +
group.relation_pattern_hits.size * 1.5 +
group.lifecycle_gaps.size * 1.25 +
group.total_match_score / Math.max(1, group.operations_count) +
(semanticProfile.graph_traversal.runtime_enabled && semanticProfile.graph_traversal.eligible
? group.graph_runtime_signals.size * 1.75 +
group.graph_match_hits * 0.5 +
group.total_graph_traversal_score / Math.max(1, group.operations_count)
: 0)).toFixed(2)),
confidence: group.risk_factors.size >= 2 || group.relation_pattern_hits.size >= 2
? "high"
: group.risk_factors.size >= 1
? "medium"
: "low",
business_interpretation: group.risk_factors.size > 0
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
: "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
relation_types: Array.from(group.relations.entries())
.sort((left, right) => right[1] - left[1])
.map((item) => item[0]),
samples: group.samples,
evidence_pack: group.samples.slice(0, 2)
}))
.sort((left, right) => {
const scoreDiff = Number(right.ranking_score) - Number(left.ranking_score);
if (scoreDiff !== 0) {
return scoreDiff;
}
return Number(right.operations_count) - Number(left.operations_count);
})
.slice(0, 8)
.map((item, index) => ({
...item,
rank: index + 1
}));
puritySummary.promotion_checked = items.length;
puritySummary.promotion_allowed = items.length;
puritySummary.promotion_rejected = 0;
puritySummary.top1_pure = true;
puritySummary.top3_pure = true;
if (items.length === 0) {
return {
status: "empty",
result_type: "chain",
items: [],
summary: {
source_records: sourceRecords.length,
filtered_records_after_narrowing: filtered.length,
checked_records: filtered.length,
matched_counterparties: 0,
semantic_narrowing_applied: semanticNarrowingApplied,
guid_mode: guidFilter.length > 0,
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary,
graph_runtime_enabled: semanticProfile.graph_traversal.runtime_enabled,
graph_eligible: semanticProfile.graph_traversal.eligible,
graph_traversal_applied: graphTraversalRuntime.traversal_applied,
graph_traversal: graphTraversalRuntime
},
evidence: [],
why_included: [],
selection_reason: [
"Поиск строился по 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}).`,
`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 или уточните период/контрагента."
],
confidence: "medium",
limitations: [
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: []
};
}
const evidence = items.flatMap((item) => item.samples.slice(0, 1).map((sample) => ({
counterparty_id: item.counterparty_id,
source_id: sample.source_id,
source_entity: sample.source_entity,
period: sample.period,
account_context: item.account_context,
relation_pattern_hits: item.relation_pattern_hits,
risk_factors: item.risk_factors,
graph_runtime_signals: item.graph_runtime_signals,
graph_domain_scope: item.graph_domain_scope
})));
const aggregatedRiskFactors = uniqueStrings(items.flatMap((item) => (Array.isArray(item.risk_factors) ? item.risk_factors : [])));
return {
status: "ok",
result_type: "chain",
items,
summary: {
source_records: sourceRecords.length,
filtered_records_after_narrowing: filtered.length,
checked_records: filtered.length,
matched_counterparties: items.length,
route_focus: "cross_entity_chain",
semantic_narrowing_applied: semanticNarrowingApplied,
guid_mode: guidFilter.length > 0,
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary,
graph_runtime_enabled: semanticProfile.graph_traversal.runtime_enabled,
graph_eligible: semanticProfile.graph_traversal.eligible,
graph_traversal_applied: graphTraversalRuntime.traversal_applied,
graph_traversal: graphTraversalRuntime
},
evidence: evidence.slice(0, 12),
why_included: [
`Семантическое сужение выполнено по профилю ${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} записей.`,
`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(", ")}.`,
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"}.`
],
risk_factors: aggregatedRiskFactors.length > 0
? aggregatedRiskFactors
: ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
business_interpretation: [
"Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
"Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
],
confidence: "high",
limitations: [
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: []
};
}
executeRisk(fragmentText, data) {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const profileRiskFactors = semanticProfile.anomaly_patterns;
const resolvedDomain = resolveP0DomainCard(fragmentText, semanticProfile);
const domainCard = resolvedDomain?.card ?? null;
const fallbackSources = ["problemCases", "ndsRegisters"];
const sourceScope = domainCard ? domainCard.allowed_evidence_sources.risk : fallbackSources;
const sourcePool = collectSourceRecords(data, sourceScope);
const strictForbidden = Boolean(domainCard);
let sourceGate = domainCard
? applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: sourcePool.map((item) => ({
...item,
signals: inferRecordSignals(item.record),
purity: {
allowed: true,
account_match: true,
domain_match: true,
entity_match: true,
edge_match: true,
forbidden_domains: [],
cross_domain_overlap: []
}
})),
rejected_total: 0,
rejected_forbidden: 0
};
let sourceStrictFallbackUsed = false;
if (domainCard && strictForbidden && sourceGate.accepted.length === 0 && sourcePool.length > 0) {
sourceGate = applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: false });
sourceStrictFallbackUsed = true;
}
const scored = sourceGate.accepted
.map((candidate) => {
const record = candidate.record;
const reasons = [];
let score = 0;
const unknownLinks = Number(record.unknown_link_count ?? 0);
const zeroGuidValues = countZeroGuidValues(record);
const navigationLinks = countNavigationLinks(record);
const cpLinks = findCounterpartyLinks(record).length;
if (unknownLinks > 0) {
score += 3;
reasons.push(`Есть связи с неопределенной сущностью (${unknownLinks}).`);
}
if (zeroGuidValues > 0) {
score += Math.min(3, 1 + zeroGuidValues);
reasons.push(`Найдены нулевые GUID в ключевых полях (${zeroGuidValues}).`);
}
if (navigationLinks > 0) {
score += 1;
reasons.push("Есть навигационные ссылки, требующие сверки связей.");
}
if (cpLinks === 0) {
score += 1;
reasons.push("Нет явной связи с контрагентом.");
}
const flags = Array.isArray(record.problem_flags) ? record.problem_flags : [];
if (flags.length > 0) {
score += 1;
}
return {
candidate,
source_entity: record.source_entity,
source_id: record.source_id,
period: extractDate(record),
risk_score: score,
reasons,
unknown_link_count: unknownLinks,
zero_guid_values: zeroGuidValues,
navigation_links: navigationLinks
};
})
.filter((item) => item.risk_score >= 2)
.sort((left, right) => right.risk_score - left.risk_score);
let rankingGate = domainCard
? enforceDomainPurityRanking(scored.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: scored.map((item) => item.candidate),
rejected_total: 0
};
let rankingStrictFallbackUsed = false;
if (domainCard && strictForbidden && rankingGate.accepted.length === 0 && scored.length > 0) {
rankingGate = enforceDomainPurityRanking(scored.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: false });
rankingStrictFallbackUsed = true;
}
const rankingAcceptedIds = new Set(rankingGate.accepted.map((item) => `${item.record.source_entity}:${item.record.source_id}`));
const rankedFiltered = scored.filter((item) => rankingAcceptedIds.has(`${item.source_entity}:${item.source_id}`));
const topRanked = rankedFiltered.slice(0, 15);
let promotionGate = domainCard
? enforceDomainPurityPromotion(topRanked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: topRanked.map((item) => item.candidate),
rejected_total: 0
};
let promotionStrictFallbackUsed = false;
if (domainCard && strictForbidden && promotionGate.accepted.length === 0 && topRanked.length > 0) {
promotionGate = enforceDomainPurityPromotion(topRanked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: false });
promotionStrictFallbackUsed = true;
}
const promotionAcceptedIds = new Set(promotionGate.accepted.map((item) => `${item.record.source_entity}:${item.record.source_id}`));
const promotedRanked = topRanked.filter((item) => promotionAcceptedIds.has(`${item.source_entity}:${item.source_id}`));
const items = promotedRanked.map(({ candidate: _candidate, ...payload }) => payload);
const puritySummary = {
enabled: Boolean(domainCard),
domain_card_id: domainCard?.id ?? null,
domain_card_title: domainCard?.title ?? null,
source_scope: sourceScope,
source_pool_records: sourcePool.length,
source_selection_allowed: sourceGate.accepted.length,
source_selection_rejected: sourceGate.rejected_total,
source_selection_rejected_forbidden: sourceGate.rejected_forbidden,
ranking_checked: scored.length,
ranking_allowed: rankedFiltered.length,
ranking_rejected: rankingGate.rejected_total,
promotion_checked: topRanked.length,
promotion_allowed: promotedRanked.length,
promotion_rejected: promotionGate.rejected_total,
top1_pure: domainCard ? topOnePurityHolds(promotedRanked.map((item) => item.candidate)) : true,
top3_pure: domainCard ? topThreePurityHolds(promotedRanked.map((item) => item.candidate)) : true,
strict_forbidden_mode: strictForbidden,
strict_forbidden_fallback_source: sourceStrictFallbackUsed,
strict_forbidden_fallback_ranking: rankingStrictFallbackUsed,
strict_forbidden_fallback_promotion: promotionStrictFallbackUsed
};
if (items.length === 0) {
return {
status: "empty",
result_type: "list",
items: [],
summary: {
checked_records: sourcePool.length,
risky_records: 0,
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary
},
evidence: [],
why_included: [],
selection_reason: [
"Risk scoring executed with technical anomaly heuristics.",
domainCard ? `P0 domain guardrail applied: ${domainCard.id}.` : "P0 domain guardrail was not activated."
],
risk_factors: profileRiskFactors,
business_interpretation: ["По текущему срезу явные риск-признаки не обнаружены."],
confidence: "medium",
limitations: [
"Оценка основана на snapshot-данных и эвристическом risk score.",
domainCard ? "Domain purity guardrail может отфильтровать записи вне целевого домена." : "Domain purity guardrail не активирован."
],
errors: []
};
}
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",
items,
summary: {
checked_records: sourcePool.length,
risky_records: items.length,
average_risk_score: Number(averageScore.toFixed(2)),
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary
},
evidence: items.slice(0, 10).map((item) => ({
source_entity: item.source_entity,
source_id: item.source_id,
risk_score: item.risk_score
})),
why_included: [
"В ответ включены записи с risk_score >= 2.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
],
selection_reason: [
"score растет при unknown links, zero GUID, навигационных ссылках и отсутствии явного контрагента.",
`Semantic profile subject: ${semanticProfile.query_subject}.`,
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
],
risk_factors: normalizedRiskFactors,
business_interpretation: ["Эти записи требуют первичной бухгалтерской проверки как потенциальные аномалии."],
confidence: "high",
limitations: [
"Риск-факторы определяются эвристикой, а не полным набором бизнес-правил 1С.",
domainCard ? "Часть нерелевантных записей исключена domain purity фильтром." : "Domain purity guardrail не применялся."
],
errors: []
};
}
executeBatch(fragmentText, data) {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const resolvedDomain = resolveP0DomainCard(fragmentText, semanticProfile);
const domainCard = resolvedDomain?.card ?? null;
const fallbackSources = ["problemCases", "keyFields", "docs"];
const sourceScope = domainCard
? uniqueStrings([...domainCard.allowed_evidence_sources.risk, ...domainCard.allowed_evidence_sources.canonical])
: fallbackSources;
const sourcePool = collectSourceRecords(data, sourceScope);
const strictForbidden = Boolean(domainCard);
let sourceGate = domainCard
? applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: sourcePool.map((item) => ({
...item,
signals: inferRecordSignals(item.record),
purity: {
allowed: true,
account_match: true,
domain_match: true,
entity_match: true,
edge_match: true,
forbidden_domains: [],
cross_domain_overlap: []
}
})),
rejected_total: 0,
rejected_forbidden: 0
};
let sourceStrictFallbackUsed = false;
if (domainCard && strictForbidden && sourceGate.accepted.length === 0 && sourcePool.length > 0) {
sourceGate = applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: false });
sourceStrictFallbackUsed = true;
}
const source = sourceGate.accepted.map((item) => item.record);
const byEntity = new Map();
for (const record of source) {
byEntity.set(record.source_entity, (byEntity.get(record.source_entity) ?? 0) + 1);
}
const items = Array.from(byEntity.entries())
.sort((left, right) => right[1] - left[1])
.slice(0, 10)
.map(([entity, count], index) => ({
rank: index + 1,
entity,
records_count: count
}));
const puritySummary = {
enabled: Boolean(domainCard),
domain_card_id: domainCard?.id ?? null,
domain_card_title: domainCard?.title ?? null,
source_scope: sourceScope,
source_pool_records: sourcePool.length,
source_selection_allowed: sourceGate.accepted.length,
source_selection_rejected: sourceGate.rejected_total,
source_selection_rejected_forbidden: sourceGate.rejected_forbidden,
top1_pure: domainCard ? topOnePurityHolds(sourceGate.accepted) : true,
top3_pure: domainCard ? topThreePurityHolds(sourceGate.accepted) : true,
strict_forbidden_mode: strictForbidden,
strict_forbidden_fallback_source: sourceStrictFallbackUsed
};
return {
status: items.length > 0 ? "ok" : "empty",
result_type: "ranking",
items,
summary: {
checked_records: sourcePool.length,
ranked_entities: items.length,
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary
},
evidence: items.slice(0, 5).map((item) => ({
entity: item.entity,
records_count: item.records_count
})),
why_included: items.length > 0
? [
"Показаны сущности с максимальным количеством записей.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
]
: [],
selection_reason: [
"Ранжирование выполнено по records_count по убыванию.",
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
],
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: [
"Ранжирование по объему не всегда эквивалентно бизнес-риску.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
],
errors: []
};
}
executeCanonical(fragmentText, data) {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const resolvedDomain = resolveP0DomainCard(fragmentText, semanticProfile);
const domainCard = resolvedDomain?.card ?? null;
const fallbackSources = semanticProfile.domain_scope.includes("vat") || semanticProfile.domain_scope.includes("taxes")
? ["ndsRegisters", "keyFields"]
: ["docs"];
const sourceScope = domainCard ? domainCard.allowed_evidence_sources.canonical : fallbackSources;
const sourcePool = collectSourceRecords(data, sourceScope);
const strictForbidden = Boolean(domainCard);
let sourceGate = domainCard
? applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: sourcePool.map((item) => ({
...item,
signals: inferRecordSignals(item.record),
purity: {
allowed: true,
account_match: true,
domain_match: true,
entity_match: true,
edge_match: true,
forbidden_domains: [],
cross_domain_overlap: []
}
})),
rejected_total: 0,
rejected_forbidden: 0
};
let sourceStrictFallbackUsed = false;
if (domainCard && strictForbidden && sourceGate.accepted.length === 0 && sourcePool.length > 0) {
sourceGate = applyDomainPuritySourceGate(sourcePool, domainCard, semanticProfile, { strict_forbidden: false });
sourceStrictFallbackUsed = true;
}
const ranked = sourceGate.accepted
.map((candidate) => {
const record = candidate.record;
const period = extractDate(record);
return {
candidate,
source_entity: record.source_entity,
source_id: record.source_id,
display_name: record.display_name,
period,
counterparty_id: findCounterpartyLinks(record)[0]?.target_id ?? null,
recorder: record.attributes.Recorder ?? null,
sort_key: parseDateCandidate(period) ?? 0
};
})
.sort((left, right) => right.sort_key - left.sort_key);
let rankingGate = domainCard
? enforceDomainPurityRanking(ranked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: ranked.map((item) => item.candidate),
rejected_total: 0
};
let rankingStrictFallbackUsed = false;
if (domainCard && strictForbidden && rankingGate.accepted.length === 0 && ranked.length > 0) {
rankingGate = enforceDomainPurityRanking(ranked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: false });
rankingStrictFallbackUsed = true;
}
const rankingAcceptedIds = new Set(rankingGate.accepted.map((item) => `${item.record.source_entity}:${item.record.source_id}`));
const rankedFiltered = ranked.filter((item) => rankingAcceptedIds.has(`${item.source_entity}:${item.source_id}`));
const topRanked = rankedFiltered.slice(0, 12);
let promotionGate = domainCard
? enforceDomainPurityPromotion(topRanked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: strictForbidden })
: {
accepted: topRanked.map((item) => item.candidate),
rejected_total: 0
};
let promotionStrictFallbackUsed = false;
if (domainCard && strictForbidden && promotionGate.accepted.length === 0 && topRanked.length > 0) {
promotionGate = enforceDomainPurityPromotion(topRanked.map((item) => item.candidate), domainCard, semanticProfile, { strict_forbidden: false });
promotionStrictFallbackUsed = true;
}
const promotionAcceptedIds = new Set(promotionGate.accepted.map((item) => `${item.record.source_entity}:${item.record.source_id}`));
const promotedRanked = topRanked.filter((item) => promotionAcceptedIds.has(`${item.source_entity}:${item.source_id}`));
const items = promotedRanked.map(({ candidate: _candidate, sort_key: _ignored, ...record }) => record);
const puritySummary = {
enabled: Boolean(domainCard),
domain_card_id: domainCard?.id ?? null,
domain_card_title: domainCard?.title ?? null,
source_scope: sourceScope,
source_pool_records: sourcePool.length,
source_selection_allowed: sourceGate.accepted.length,
source_selection_rejected: sourceGate.rejected_total,
source_selection_rejected_forbidden: sourceGate.rejected_forbidden,
ranking_checked: ranked.length,
ranking_allowed: rankedFiltered.length,
ranking_rejected: rankingGate.rejected_total,
promotion_checked: topRanked.length,
promotion_allowed: promotedRanked.length,
promotion_rejected: promotionGate.rejected_total,
top1_pure: domainCard ? topOnePurityHolds(promotedRanked.map((item) => item.candidate)) : true,
top3_pure: domainCard ? topThreePurityHolds(promotedRanked.map((item) => item.candidate)) : true,
strict_forbidden_mode: strictForbidden,
strict_forbidden_fallback_source: sourceStrictFallbackUsed,
strict_forbidden_fallback_ranking: rankingStrictFallbackUsed,
strict_forbidden_fallback_promotion: promotionStrictFallbackUsed
};
return {
status: items.length > 0 ? "ok" : "empty",
result_type: "list",
items,
summary: {
checked_records: sourcePool.length,
returned_records: items.length,
query_subject: semanticProfile.query_subject,
semantic_profile: semanticProfile,
ranking_basis: semanticProfile.ranking_basis,
domain_purity_guard: puritySummary
},
evidence: items.slice(0, 6).map((item) => ({
source_entity: item.source_entity,
source_id: item.source_id,
period: item.period
})),
why_included: items.length > 0
? [
"Показаны последние по дате записи канонического документного слоя.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
]
: [],
selection_reason: [
"Отбор по максимальной дате документа в пределах snapshot.",
`Semantic profile subject: ${semanticProfile.query_subject}.`,
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
],
risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
confidence: "high",
limitations: [
"Это read-only snapshot, а не онлайн-состояние 1С.",
domainCard ? "Canonical output ограничен доменным runtime-контрактом." : "Domain purity guardrail не применялся."
],
errors: []
};
}
executeDrilldown(fragmentText, data) {
const guidFilter = extractGuids(fragmentText);
const all = [...data.keyFields, ...data.problemCases, ...data.docs, ...data.journals, ...data.ndsRegisters];
const anchors = extractBusinessAnchorsFromText(fragmentText);
if (guidFilter.length === 0 && !anchors.sufficient) {
return {
status: "empty",
result_type: "object",
items: [],
summary: {
reason: "guid_not_provided",
business_anchor_trace_available: false,
anchor_score: {
document_numbers: anchors.document_numbers.length,
date_tokens: anchors.date_tokens.length,
amount_values: anchors.amount_values.length,
account_scope: anchors.account_scope.length
}
},
evidence: [],
why_included: [],
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
risk_factors: [],
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
confidence: "low",
limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
errors: []
};
}
if (guidFilter.length === 0) {
const matches = all
.map((record) => {
const scored = scoreRecordForBusinessAnchorTrace(record, anchors);
return {
record,
...scored
};
})
.filter((item) => item.score >= 4 && item.matched_categories.length >= 2)
.sort((left, right) => {
const scoreDiff = right.score - left.score;
if (scoreDiff !== 0) {
return scoreDiff;
}
return String(right.record.source_id).localeCompare(String(left.record.source_id));
})
.slice(0, 20)
.map((item) => ({
source_entity: item.record.source_entity,
source_id: item.record.source_id,
display_name: item.record.display_name,
period: extractDate(item.record),
links_count: item.record.links.length,
trace_score: item.score,
matched_categories: item.matched_categories
}));
return {
status: matches.length > 0 ? "ok" : "empty",
result_type: "object",
items: matches,
summary: {
reason: "business_anchor_trace",
matched_records: matches.length,
anchor_trace: {
document_numbers: anchors.document_numbers,
date_tokens: anchors.date_tokens,
amount_values: anchors.amount_values,
account_scope: anchors.account_scope,
period_keys: anchors.period_keys
}
},
evidence: matches.slice(0, 10),
why_included: matches.length > 0
? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
: [],
selection_reason: [
"GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
],
risk_factors: [],
business_interpretation: [
"Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
],
confidence: matches.length > 0 ? "medium" : "low",
limitations: [
"Поиск ограничен локальным snapshot-пакетом.",
"Без GUID совпадение построено по business anchors и может требовать ручной проверки."
],
errors: []
};
}
const matches = all
.filter((record) => guidFilter.some((guid) => hasGuidMatch(record, guid)))
.slice(0, 20)
.map((record) => ({
source_entity: record.source_entity,
source_id: record.source_id,
display_name: record.display_name,
period: extractDate(record),
links_count: record.links.length
}));
return {
status: matches.length > 0 ? "ok" : "empty",
result_type: "object",
items: matches,
summary: {
query_guids: guidFilter,
matched_records: matches.length
},
evidence: matches.slice(0, 10),
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
risk_factors: [],
business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
confidence: matches.length > 0 ? "high" : "medium",
limitations: ["Поиск ограничен локальным snapshot-пакетом."],
errors: []
};
}
}
exports.AssistantDataLayer = AssistantDataLayer;