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