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

1455 lines
73 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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;