1455 lines
73 KiB
JavaScript
1455 lines
73 KiB
JavaScript
"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 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
|
||
}
|
||
};
|
||
function pushUniqueLine(target, line) {
|
||
if (!target.includes(line)) {
|
||
target.push(line);
|
||
}
|
||
}
|
||
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;
|
||
let anchorScore = 0;
|
||
if (hasGuidAnchor)
|
||
anchorScore += 3;
|
||
if (hasAccountAnchor)
|
||
anchorScore += 2;
|
||
if (hasPeriodAnchor)
|
||
anchorScore += 1;
|
||
if (hasEntityAnchor)
|
||
anchorScore += 1;
|
||
if (hasExactObjectAnchor)
|
||
anchorScore += 1;
|
||
const weakAnchors = anchorScore <= 1;
|
||
const strongFocus = hasGuidAnchor || (hasAccountAnchor && hasPeriodAnchor) || 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;
|
||
}
|
||
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;
|
||
}
|
||
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"]
|
||
},
|
||
"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"]
|
||
}
|
||
};
|
||
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 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();
|
||
}
|
||
function extractAccountScopeFromText(text) {
|
||
const matches = text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [];
|
||
const normalized = matches.map((item) => item.split(".")[0]);
|
||
return uniqueStrings(normalized);
|
||
}
|
||
function inferPeriodScope(fragmentText) {
|
||
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 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"]);
|
||
}
|
||
if (/постав|supplier|vendor|60\b/i.test(lower)) {
|
||
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|62\b/i.test(lower)) {
|
||
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|книга покупок|книга продаж|счет.?фактур|19\b|68\b/i.test(lower)) {
|
||
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+сред|fixed asset|amort|амортиз|01\b|02\b|08\b/i.test(lower)) {
|
||
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 (/рбп|расходы будущих периодов|deferred|writeoff|97\b/i.test(lower)) {
|
||
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 (/живут отдельно|не связ|без связи|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 (/закрыт|закрытие|период|month close|closure/i.test(lower)) {
|
||
pushMany(domainScope, ["period_close"]);
|
||
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
||
pushMany(documentTypes, ["period_close_document"]);
|
||
}
|
||
if (/не в платеже|not payment/i.test(lower)) {
|
||
pushMany(excludedInterpretations, ["simple_payment_delay"]);
|
||
}
|
||
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|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);
|
||
return {
|
||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies),
|
||
account_scope: uniqueStrings(accountScope),
|
||
subaccount_scope: [],
|
||
domain_scope: dedupedDomains,
|
||
document_types: uniqueStrings(documentTypes),
|
||
entity_types: uniqueStrings(entityTypes),
|
||
period_scope: inferPeriodScope(lower),
|
||
relation_patterns: uniqueStrings(relationPatterns),
|
||
lifecycle_stage_filters: ["created", "posted", "closed", "reconciled"],
|
||
anomaly_patterns: dedupedAnomalies,
|
||
ranking_basis: uniqueStrings(rankingBasis),
|
||
excluded_interpretations: uniqueStrings(excludedInterpretations),
|
||
explanation_focus: uniqueStrings(explanationFocus)
|
||
};
|
||
}
|
||
function inferAccountsFromRecord(record, corpus) {
|
||
const accounts = [];
|
||
const accountTokens = corpus.match(/\b\d{2}(?:\.\d{2})?\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 (/ндс|счетфактур|книгипокупок|книгипродаж/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");
|
||
}
|
||
if (findCounterpartyLinks(record).length > 0) {
|
||
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 (/ндс|книгипокупок|книгипродаж/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 (/ндс|счетфактур|книгипокупок|книгипродаж/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 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 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.");
|
||
}
|
||
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);
|
||
return {
|
||
signals,
|
||
account_match: accountMatch,
|
||
domain_match: domainMatch,
|
||
document_match: documentMatch,
|
||
entity_match: entityMatch,
|
||
relation_match: relationMatch,
|
||
anomaly_match: anomalyMatch,
|
||
lifecycle_match: lifecycleMatch,
|
||
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;
|
||
return softHits >= requiredSoftHits;
|
||
}
|
||
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) {
|
||
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);
|
||
}
|
||
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 sourceRecords = [...data.keyFields, ...data.journals, ...data.docs];
|
||
const narrowedCandidates = guidFilter.length > 0
|
||
? sourceRecords
|
||
.filter((record) => guidFilter.some((guid) => hasGuidMatch(record, guid)))
|
||
.map((record) => ({
|
||
record,
|
||
evaluation: evaluateRecordAgainstProfile(record, semanticProfile)
|
||
}))
|
||
: semanticNarrowCandidates(sourceRecords, semanticProfile);
|
||
const filtered = narrowedCandidates.map((item) => item.record);
|
||
const semanticNarrowingApplied = guidFilter.length === 0;
|
||
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(),
|
||
selection_reasons: new Set(),
|
||
total_match_score: 0
|
||
};
|
||
groups.set(key, group);
|
||
}
|
||
group.operations_count += 1;
|
||
group.total_match_score += evaluation.match_score;
|
||
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 (semanticProfile.account_scope.length === 0 || semanticProfile.account_scope.includes(account)) {
|
||
group.account_context.add(account);
|
||
}
|
||
}
|
||
for (const item of evaluation.signals.document_types) {
|
||
group.document_context.add(item);
|
||
}
|
||
for (const item of evaluation.signals.relation_patterns) {
|
||
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 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);
|
||
group.samples.push({
|
||
source_entity: record.source_entity,
|
||
source_id: record.source_id,
|
||
period: extractDate(record),
|
||
recorder: record.attributes.Recorder ?? null,
|
||
account_context: evaluation.signals.account_context,
|
||
document_context: evaluation.signals.document_types,
|
||
relation_patterns: evaluation.signals.relation_patterns,
|
||
anomaly_patterns: evaluation.signals.anomaly_patterns,
|
||
lifecycle_markers: evaluation.signals.lifecycle_markers,
|
||
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),
|
||
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)).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
|
||
}));
|
||
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
|
||
},
|
||
evidence: [],
|
||
why_included: [],
|
||
selection_reason: [
|
||
"Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
|
||
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
|
||
guidFilter.length > 0
|
||
? "GUID-фильтрация включена."
|
||
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`
|
||
],
|
||
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РЎ."
|
||
],
|
||
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
|
||
})));
|
||
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
|
||
},
|
||
evidence: evidence.slice(0, 12),
|
||
why_included: [
|
||
`Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
|
||
semanticProfile.account_scope.length > 0
|
||
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
|
||
: "Счета не были заданы явно, использованы domain/document/relation ограничения.",
|
||
`После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`
|
||
],
|
||
selection_reason: [
|
||
"Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
|
||
"GUID-mode отключен: full scan без ограничителей не использовался.",
|
||
`Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`
|
||
],
|
||
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РЎ."
|
||
],
|
||
errors: []
|
||
};
|
||
}
|
||
executeRisk(fragmentText, data) {
|
||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||
const profileRiskFactors = semanticProfile.anomaly_patterns;
|
||
const records = [...data.problemCases, ...data.ndsRegisters];
|
||
const scored = records
|
||
.map((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 {
|
||
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);
|
||
const items = scored.slice(0, 15);
|
||
if (items.length === 0) {
|
||
return {
|
||
status: "empty",
|
||
result_type: "list",
|
||
items: [],
|
||
summary: {
|
||
checked_records: records.length,
|
||
risky_records: 0,
|
||
query_subject: semanticProfile.query_subject,
|
||
semantic_profile: semanticProfile,
|
||
ranking_basis: semanticProfile.ranking_basis
|
||
},
|
||
evidence: [],
|
||
why_included: [],
|
||
selection_reason: ["Риск-оценка выполнялась по техническим признакам, но записи выше порога не найдены."],
|
||
risk_factors: profileRiskFactors,
|
||
business_interpretation: ["По текущему срезу явные риск-признаки не обнаружены."],
|
||
confidence: "medium",
|
||
limitations: ["Оценка основана на snapshot-данных и эвристическом risk score."],
|
||
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: records.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
|
||
},
|
||
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."],
|
||
selection_reason: [
|
||
"score растет при unknown links, zero GUID, навигационных ссылках и отсутствии явного контрагента.",
|
||
`Semantic profile subject: ${semanticProfile.query_subject}.`
|
||
],
|
||
risk_factors: normalizedRiskFactors,
|
||
business_interpretation: ["Рти записи требуют первичной бухгалтерской проверки как потенциальные аномалии."],
|
||
confidence: "high",
|
||
limitations: ["Риск-факторы определяются эвристикой, а не полным набором бизнес-правил 1С."],
|
||
errors: []
|
||
};
|
||
}
|
||
executeBatch(fragmentText, data) {
|
||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||
const source = [...data.problemCases, ...data.keyFields, ...data.docs];
|
||
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
|
||
}));
|
||
return {
|
||
status: items.length > 0 ? "ok" : "empty",
|
||
result_type: "ranking",
|
||
items,
|
||
summary: {
|
||
checked_records: source.length,
|
||
ranked_entities: items.length,
|
||
query_subject: semanticProfile.query_subject,
|
||
semantic_profile: semanticProfile,
|
||
ranking_basis: semanticProfile.ranking_basis
|
||
},
|
||
evidence: items.slice(0, 5).map((item) => ({
|
||
entity: item.entity,
|
||
records_count: item.records_count
|
||
})),
|
||
why_included: items.length > 0 ? ["Показаны сущности с максимальным количеством записей."] : [],
|
||
selection_reason: ["Ранжирование выполнено по records_count по убыванию."],
|
||
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: ["Ранжирование по объему не всегда эквивалентно бизнес-риску."],
|
||
errors: []
|
||
};
|
||
}
|
||
executeCanonical(fragmentText, data) {
|
||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||
const useVatSource = semanticProfile.domain_scope.includes("vat") || semanticProfile.domain_scope.includes("taxes");
|
||
const sourceRecords = useVatSource ? [...data.ndsRegisters, ...data.keyFields] : data.docs;
|
||
const items = sourceRecords
|
||
.map((record) => {
|
||
const period = extractDate(record);
|
||
return {
|
||
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)
|
||
.slice(0, 12)
|
||
.map(({ sort_key: _ignored, ...record }) => record);
|
||
return {
|
||
status: items.length > 0 ? "ok" : "empty",
|
||
result_type: "list",
|
||
items,
|
||
summary: {
|
||
checked_records: sourceRecords.length,
|
||
returned_records: items.length,
|
||
query_subject: semanticProfile.query_subject,
|
||
semantic_profile: semanticProfile,
|
||
ranking_basis: semanticProfile.ranking_basis
|
||
},
|
||
evidence: items.slice(0, 6).map((item) => ({
|
||
source_entity: item.source_entity,
|
||
source_id: item.source_id,
|
||
period: item.period
|
||
})),
|
||
why_included: items.length > 0 ? ["Показаны последние по дате записи канонического документного слоя."] : [],
|
||
selection_reason: [
|
||
"Отбор по максимальной дате документа в пределах snapshot.",
|
||
`Semantic profile subject: ${semanticProfile.query_subject}.`
|
||
],
|
||
risk_factors: semanticProfile.anomaly_patterns,
|
||
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
|
||
confidence: "high",
|
||
limitations: ["Рто read-only snapshot, Р° РЅРµ онлайн-состояние 1РЎ."],
|
||
errors: []
|
||
};
|
||
}
|
||
executeDrilldown(fragmentText, data) {
|
||
const guidFilter = extractGuids(fragmentText);
|
||
if (guidFilter.length === 0) {
|
||
return {
|
||
status: "empty",
|
||
result_type: "object",
|
||
items: [],
|
||
summary: {
|
||
reason: "guid_not_provided"
|
||
},
|
||
evidence: [],
|
||
why_included: [],
|
||
selection_reason: ["Для drilldown требуется GUID в тексте запроса."],
|
||
risk_factors: [],
|
||
business_interpretation: ["Без GUID точечный drilldown невозможен."],
|
||
confidence: "low",
|
||
limitations: ["Добавьте GUID документа/объекта для source-of-record проверки."],
|
||
errors: []
|
||
};
|
||
}
|
||
const all = [...data.keyFields, ...data.problemCases, ...data.docs, ...data.journals, ...data.ndsRegisters];
|
||
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;
|