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