3079 lines
160 KiB
JavaScript
3079 lines
160 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.AddressQueryService = void 0;
|
||
const config_1 = require("../config");
|
||
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
||
const addressMcpClient_1 = require("./addressMcpClient");
|
||
const decomposeStage_1 = require("./address_runtime/decomposeStage");
|
||
const resolveStage_1 = require("./address_runtime/resolveStage");
|
||
const composeStage_1 = require("./address_runtime/composeStage");
|
||
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
|
||
const addressRouteExpectations_1 = require("./addressRouteExpectations");
|
||
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
|
||
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
|
||
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||
const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||
const VAT_METADATA_PROBE_LIMIT = 100;
|
||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
|
||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"];
|
||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"];
|
||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||
"ооо",
|
||
"ао",
|
||
"зао",
|
||
"ип",
|
||
"llc",
|
||
"ltd",
|
||
"company",
|
||
"компания",
|
||
"контрагент",
|
||
"counterparty",
|
||
"по",
|
||
"by"
|
||
]);
|
||
const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([
|
||
"что",
|
||
"чо",
|
||
"были",
|
||
"был",
|
||
"была",
|
||
"было",
|
||
"ли",
|
||
"какие",
|
||
"какой",
|
||
"покажи",
|
||
"показать",
|
||
"выведи",
|
||
"списания",
|
||
"списание",
|
||
"поступления",
|
||
"поступление",
|
||
"доки",
|
||
"документ",
|
||
"документы",
|
||
"документов",
|
||
"банковские",
|
||
"операции",
|
||
"платежи",
|
||
"платеж",
|
||
"платежи",
|
||
"плс",
|
||
"please"
|
||
]);
|
||
const ACCOUNT_ALIAS_MAP = {
|
||
"51": ["расчетный счет", "расчетные счета", "bank account"],
|
||
"52": ["валютный счет", "валютные счета", "currency account"],
|
||
"60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"],
|
||
"62": ["покупатель", "покупателями", "расчеты с покупателями"],
|
||
"76": ["прочие расчеты", "прочими дебиторами и кредиторами"]
|
||
};
|
||
const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = `
|
||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Регистратор,
|
||
"" КАК СчетДт,
|
||
"" КАК СчетКт,
|
||
0 КАК Сумма,
|
||
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Контрагент
|
||
ИЗ
|
||
Справочник.Контрагенты КАК Контрагенты
|
||
`;
|
||
let counterpartyCatalogCache = null;
|
||
function parseFiniteNumber(value) {
|
||
if (typeof value === "number" && Number.isFinite(value)) {
|
||
return value;
|
||
}
|
||
if (typeof value === "string") {
|
||
const parsed = Number(value.replace(",", ".").trim());
|
||
if (Number.isFinite(parsed)) {
|
||
return parsed;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function normalizeAnalysisDateHint(value) {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return null;
|
||
}
|
||
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
|
||
if (!isoPrefix) {
|
||
return null;
|
||
}
|
||
const year = Number(isoPrefix[1]);
|
||
const month = Number(isoPrefix[2]);
|
||
const day = Number(isoPrefix[3]);
|
||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||
if (candidate.getUTCFullYear() !== year ||
|
||
candidate.getUTCMonth() + 1 !== month ||
|
||
candidate.getUTCDate() !== day) {
|
||
return null;
|
||
}
|
||
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
|
||
}
|
||
function valueAsString(value) {
|
||
if (value === null || value === undefined) {
|
||
return "";
|
||
}
|
||
return String(value);
|
||
}
|
||
function normalizeIsoDateForQuery(value) {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return null;
|
||
}
|
||
const match = trimmed.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 candidate = new Date(Date.UTC(year, month - 1, day));
|
||
if (candidate.getUTCFullYear() !== year ||
|
||
candidate.getUTCMonth() + 1 !== month ||
|
||
candidate.getUTCDate() !== day) {
|
||
return null;
|
||
}
|
||
return `${match[1]}-${match[2]}-${match[3]}`;
|
||
}
|
||
function toDateTimeExprForQuery(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]);
|
||
const day = Number(match[3]);
|
||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, 23, 59, 59)`;
|
||
}
|
||
function shouldProbeVatSourcesForForecast(userMessage) {
|
||
const text = String(userMessage ?? "")
|
||
.toLowerCase()
|
||
.replace(/ё/g, "е");
|
||
if (!text.trim()) {
|
||
return false;
|
||
}
|
||
return /(?:в\s+налогов|почему|из\s+чего|источн|декларац|книга\s+продаж|книга\s+покупок|вычет|восстанов)/iu.test(text);
|
||
}
|
||
function detectVatMetadataObjectType(fullName) {
|
||
const normalized = String(fullName ?? "").trim();
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
if (normalized.startsWith("Документ.")) {
|
||
return "document";
|
||
}
|
||
if (normalized.startsWith("РегистрНакопления.") || normalized.startsWith("РегистрСведений.")) {
|
||
return "register";
|
||
}
|
||
return null;
|
||
}
|
||
function extractVatMetadataObjects(rows) {
|
||
const out = [];
|
||
const seen = new Set();
|
||
for (const row of rows) {
|
||
const fullName = valueAsString(row.ПолноеИмя ?? row.full_name ?? row.FullName ?? row.Имя ?? row.name ?? row.Name).trim() || null;
|
||
if (!fullName) {
|
||
continue;
|
||
}
|
||
const objectType = detectVatMetadataObjectType(fullName);
|
||
if (!objectType) {
|
||
continue;
|
||
}
|
||
if (seen.has(fullName)) {
|
||
continue;
|
||
}
|
||
seen.add(fullName);
|
||
const synonym = valueAsString(row.Синоним ?? row.synonym ?? row.Synonym ?? row.Представление ?? row.presentation).trim() || null;
|
||
out.push({
|
||
fullName,
|
||
synonym,
|
||
objectType
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
function scoreVatMetadataObject(item) {
|
||
const fullName = item.fullName.toLowerCase();
|
||
const synonym = String(item.synonym ?? "").toLowerCase();
|
||
let score = item.objectType === "register" ? 120 : 80;
|
||
if (fullName.includes("книгипродаж") || synonym.includes("продаж")) {
|
||
score += 60;
|
||
}
|
||
if (fullName.includes("книгипокупок") || synonym.includes("покуп")) {
|
||
score += 60;
|
||
}
|
||
if (fullName.includes("начислен") || synonym.includes("начислен")) {
|
||
score += 40;
|
||
}
|
||
if (fullName.includes("предъявлен") || synonym.includes("предъявлен")) {
|
||
score += 40;
|
||
}
|
||
if (fullName.includes("оплатындс") || synonym.includes("в бюджет")) {
|
||
score += 35;
|
||
}
|
||
if (fullName.includes("декларац")) {
|
||
score -= 25;
|
||
}
|
||
if (fullName.includes("пояснен")) {
|
||
score -= 25;
|
||
}
|
||
return score;
|
||
}
|
||
function buildVatObjectProbeQuery(object, asOfExpr) {
|
||
if (object.objectType === "document") {
|
||
return `
|
||
ВЫБРАТЬ ПЕРВЫЕ 1
|
||
Док.Дата КАК Период,
|
||
ПРЕДСТАВЛЕНИЕ(Док.Ссылка) КАК Регистратор,
|
||
"" КАК СчетДт,
|
||
"" КАК СчетКт,
|
||
0 КАК Сумма
|
||
ИЗ
|
||
${object.fullName} КАК Док
|
||
ГДЕ
|
||
Док.Дата <= ${asOfExpr}
|
||
УПОРЯДОЧИТЬ ПО
|
||
Док.Дата УБЫВ
|
||
`.trim();
|
||
}
|
||
return `
|
||
ВЫБРАТЬ ПЕРВЫЕ 1
|
||
Движения.Период КАК Период,
|
||
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
|
||
"" КАК СчетДт,
|
||
"" КАК СчетКт,
|
||
0 КАК Сумма
|
||
ИЗ
|
||
${object.fullName} КАК Движения
|
||
ГДЕ
|
||
Движения.Период <= ${asOfExpr}
|
||
УПОРЯДОЧИТЬ ПО
|
||
Движения.Период УБЫВ
|
||
`.trim();
|
||
}
|
||
async function probeVatDirectSources(filters) {
|
||
const asOfDate = normalizeIsoDateForQuery(filters.as_of_date) ??
|
||
normalizeIsoDateForQuery(filters.period_to) ??
|
||
normalizeIsoDateForQuery(filters.period_from);
|
||
if (!asOfDate) {
|
||
return {
|
||
status: "skipped",
|
||
objectsTotal: 0,
|
||
documentsTotal: 0,
|
||
registersTotal: 0,
|
||
probedSources: [],
|
||
errors: ["as_of_date_not_resolved_for_vat_probe"]
|
||
};
|
||
}
|
||
const asOfExpr = toDateTimeExprForQuery(asOfDate);
|
||
if (!asOfExpr) {
|
||
return {
|
||
status: "skipped",
|
||
objectsTotal: 0,
|
||
documentsTotal: 0,
|
||
registersTotal: 0,
|
||
probedSources: [],
|
||
errors: ["as_of_expr_not_resolved_for_vat_probe"]
|
||
};
|
||
}
|
||
const metadataRequests = VAT_METADATA_PROBE_TYPES.flatMap((metaType) => VAT_METADATA_PROBE_MASKS.map((nameMask) => ({
|
||
meta_type: metaType,
|
||
name_mask: nameMask,
|
||
limit: VAT_METADATA_PROBE_LIMIT
|
||
})));
|
||
const metadataResponses = await Promise.all(metadataRequests.map((request) => (0, addressMcpClient_1.executeAddressMcpMetadata)(request)));
|
||
const metadataErrors = [];
|
||
const metadataObjectsBuffer = [];
|
||
for (const [index, response] of metadataResponses.entries()) {
|
||
const request = metadataRequests[index];
|
||
if (response.error) {
|
||
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
||
continue;
|
||
}
|
||
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
||
}
|
||
const deduplicatedObjects = new Map();
|
||
for (const item of metadataObjectsBuffer) {
|
||
const existing = deduplicatedObjects.get(item.fullName);
|
||
if (!existing) {
|
||
deduplicatedObjects.set(item.fullName, item);
|
||
continue;
|
||
}
|
||
if (!existing.synonym && item.synonym) {
|
||
deduplicatedObjects.set(item.fullName, {
|
||
...existing,
|
||
synonym: item.synonym
|
||
});
|
||
}
|
||
}
|
||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values()).sort((a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru"));
|
||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
||
const probeRows = [];
|
||
for (const object of metadataObjects) {
|
||
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
|
||
const probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: probeQuery,
|
||
limit: 1
|
||
});
|
||
if (probeResult.error) {
|
||
probeRows.push({
|
||
fullName: object.fullName,
|
||
synonym: object.synonym,
|
||
objectType: object.objectType,
|
||
status: "error",
|
||
rowsFetched: probeResult.fetched_rows,
|
||
error: probeResult.error
|
||
});
|
||
continue;
|
||
}
|
||
const firstRow = probeResult.raw_rows[0] ?? null;
|
||
const lastPeriod = firstRow !== null
|
||
? valueAsString(firstRow.Период ?? firstRow.period).trim() ||
|
||
null
|
||
: null;
|
||
const sampleRegistrator = firstRow !== null
|
||
? valueAsString(firstRow.Регистратор ??
|
||
firstRow.registrator ??
|
||
firstRow.Registrator).trim() || null
|
||
: null;
|
||
probeRows.push({
|
||
fullName: object.fullName,
|
||
synonym: object.synonym,
|
||
objectType: object.objectType,
|
||
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
||
rowsFetched: probeResult.fetched_rows,
|
||
lastPeriod,
|
||
sampleRegistrator
|
||
});
|
||
}
|
||
const status = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
||
const allErrors = [
|
||
...metadataErrors,
|
||
...probeRows
|
||
.filter((item) => item.status === "error")
|
||
.map((item) => `${item.fullName}: ${valueAsString(item.error).slice(0, 120)}`)
|
||
];
|
||
return {
|
||
status,
|
||
objectsTotal: discoveredMetadataObjects.length,
|
||
documentsTotal: discoveredMetadataObjects.filter((item) => item.objectType === "document").length,
|
||
registersTotal: discoveredMetadataObjects.filter((item) => item.objectType === "register").length,
|
||
probedSources: probeRows,
|
||
errors: allErrors
|
||
};
|
||
}
|
||
function transliterateCyrillicToLatin(value) {
|
||
const map = {
|
||
а: "a",
|
||
б: "b",
|
||
в: "v",
|
||
г: "g",
|
||
д: "d",
|
||
е: "e",
|
||
ё: "e",
|
||
ж: "zh",
|
||
з: "z",
|
||
и: "i",
|
||
й: "y",
|
||
к: "k",
|
||
л: "l",
|
||
м: "m",
|
||
н: "n",
|
||
о: "o",
|
||
п: "p",
|
||
р: "r",
|
||
с: "s",
|
||
т: "t",
|
||
у: "u",
|
||
ф: "f",
|
||
х: "h",
|
||
ц: "ts",
|
||
ч: "ch",
|
||
ш: "sh",
|
||
щ: "sch",
|
||
ъ: "",
|
||
ы: "y",
|
||
ь: "",
|
||
э: "e",
|
||
ю: "yu",
|
||
я: "ya"
|
||
};
|
||
let out = "";
|
||
for (const char of String(value ?? "").toLowerCase()) {
|
||
out += map[char] ?? char;
|
||
}
|
||
return out;
|
||
}
|
||
function normalizeSearchText(value) {
|
||
return String(value ?? "")
|
||
.toLowerCase()
|
||
.replace(/ё/g, "е")
|
||
.replace(/[^a-zа-я0-9]+/gi, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
function tokenizeAnchor(value) {
|
||
return normalizeSearchText(value)
|
||
.split(" ")
|
||
.map((token) => token.trim())
|
||
.filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token));
|
||
}
|
||
function anchorTokenVariants(token) {
|
||
const source = String(token ?? "").trim().toLowerCase();
|
||
if (!source) {
|
||
return [];
|
||
}
|
||
const variants = new Set([source]);
|
||
if (/^[а-яё]+$/iu.test(source) && source.length >= 4) {
|
||
const withoutEnding = source.replace(/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой|е|у|ы|а|я|и|ю)$/iu, "");
|
||
if (withoutEnding.length >= 3) {
|
||
variants.add(withoutEnding);
|
||
}
|
||
const withoutTrailingVowel = source.replace(/[аеёиоуыэюя]$/iu, "");
|
||
if (withoutTrailingVowel.length >= 3) {
|
||
variants.add(withoutTrailingVowel);
|
||
}
|
||
}
|
||
return Array.from(variants);
|
||
}
|
||
function matchesAnchorText(searchable, anchor) {
|
||
const searchableNormalized = normalizeSearchText(searchable);
|
||
const searchableLatin = transliterateCyrillicToLatin(searchableNormalized);
|
||
const tokens = tokenizeAnchor(anchor);
|
||
if (tokens.length === 0) {
|
||
const direct = normalizeSearchText(anchor);
|
||
if (!direct) {
|
||
return false;
|
||
}
|
||
return searchableNormalized.includes(direct) || searchableLatin.includes(transliterateCyrillicToLatin(direct));
|
||
}
|
||
return tokens.every((token) => {
|
||
const variants = anchorTokenVariants(token);
|
||
return variants.some((variant) => {
|
||
const tokenLatin = transliterateCyrillicToLatin(variant);
|
||
return searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin);
|
||
});
|
||
});
|
||
}
|
||
function isLikelyLowQualityPartyAnchor(value) {
|
||
const normalized = normalizeSearchText(String(value ?? ""));
|
||
if (!normalized) {
|
||
return true;
|
||
}
|
||
const tokens = normalized.split(" ").filter(Boolean);
|
||
if (tokens.length === 0) {
|
||
return true;
|
||
}
|
||
const meaningfulTokens = tokens.filter((token) => {
|
||
if (token.length < 2) {
|
||
return false;
|
||
}
|
||
if (PARTY_ANCHOR_STOPWORDS.has(token) || LOW_QUALITY_PARTY_ANCHOR_TOKENS.has(token)) {
|
||
return false;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
return meaningfulTokens.length === 0;
|
||
}
|
||
function normalizeAccountToken(value) {
|
||
const source = String(value ?? "").trim().replace(",", ".");
|
||
const match = source.match(/(\d{2})(?:\.(\d{1,2}))?/);
|
||
if (!match) {
|
||
return source.toLowerCase();
|
||
}
|
||
const base = match[1];
|
||
if (!match[2]) {
|
||
return base;
|
||
}
|
||
const sub = String(Number(match[2]));
|
||
return `${base}.${sub}`;
|
||
}
|
||
function extractAccountTokens(searchable) {
|
||
const result = [];
|
||
const matcher = /\b(\d{2})(?:[.,](\d{1,2}))?\b/g;
|
||
let hit = null;
|
||
while ((hit = matcher.exec(searchable)) !== null) {
|
||
const base = hit[1];
|
||
const sub = hit[2] ? String(Number(hit[2])) : null;
|
||
result.push(sub ? `${base}.${sub}` : base);
|
||
}
|
||
return uniqueStrings(result);
|
||
}
|
||
function accountTokenMatches(requestedToken, candidateToken) {
|
||
const requested = normalizeAccountToken(requestedToken);
|
||
const candidate = normalizeAccountToken(candidateToken);
|
||
if (requested === candidate) {
|
||
return true;
|
||
}
|
||
if (!requested.includes(".")) {
|
||
return candidate.startsWith(`${requested}.`) || candidate === requested;
|
||
}
|
||
return false;
|
||
}
|
||
function baseAccountCode(value) {
|
||
const normalized = normalizeAccountToken(value);
|
||
const match = normalized.match(/^(\d{2})/);
|
||
return match ? match[1] : null;
|
||
}
|
||
function uniqueStrings(values) {
|
||
return Array.from(new Set(values
|
||
.map((item) => item.trim())
|
||
.filter((item) => item.length > 0)));
|
||
}
|
||
function normalizeCounterpartyName(value) {
|
||
return normalizeSearchText(String(value ?? ""))
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
function extractCounterpartyCatalogNames(rows) {
|
||
return uniqueStrings(rows
|
||
.map((row) => {
|
||
const direct = valueAsString(row.Контрагент ?? row.counterparty ?? row.Counterparty).trim() ||
|
||
valueAsString(row.Регистратор ?? row.registrator ?? row.Registrator).trim();
|
||
return direct;
|
||
})
|
||
.map((value) => value.trim())
|
||
.filter((value) => value.length >= 2));
|
||
}
|
||
function scoreCounterpartyCandidate(name, anchor) {
|
||
if (!matchesAnchorText(name, anchor)) {
|
||
return null;
|
||
}
|
||
const normalizedName = normalizeCounterpartyName(name);
|
||
const normalizedAnchor = normalizeCounterpartyName(anchor);
|
||
if (!normalizedName || !normalizedAnchor) {
|
||
return null;
|
||
}
|
||
let score = 0;
|
||
if (normalizedName === normalizedAnchor) {
|
||
score += 10_000;
|
||
}
|
||
else if (normalizedName.includes(normalizedAnchor)) {
|
||
score += 5_000;
|
||
}
|
||
else if (normalizedAnchor.includes(normalizedName) && normalizedName.length >= 4) {
|
||
score += 2_000;
|
||
}
|
||
const anchorTokens = tokenizeAnchor(anchor);
|
||
for (const token of anchorTokens) {
|
||
const variants = anchorTokenVariants(token);
|
||
let tokenScore = 0;
|
||
for (const variant of variants) {
|
||
if (normalizedName.includes(variant)) {
|
||
tokenScore = Math.max(tokenScore, Math.max(2, variant.length) * 20);
|
||
}
|
||
}
|
||
if (tokenScore === 0) {
|
||
return null;
|
||
}
|
||
score += tokenScore;
|
||
}
|
||
const lengthPenalty = Math.abs(normalizedName.length - normalizedAnchor.length);
|
||
score -= lengthPenalty;
|
||
return score;
|
||
}
|
||
function shouldAttemptCounterpartyCatalogResolution(intent, filters) {
|
||
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
|
||
if (!counterparty || isLikelyLowQualityPartyAnchor(counterparty)) {
|
||
return false;
|
||
}
|
||
return (intent === "customer_revenue_and_payments" ||
|
||
intent === "supplier_payouts_profile" ||
|
||
intent === "contract_usage_and_value" ||
|
||
intent === "list_contracts_by_counterparty" ||
|
||
intent === "list_documents_by_counterparty" ||
|
||
intent === "bank_operations_by_counterparty" ||
|
||
intent === "open_items_by_counterparty_or_contract" ||
|
||
intent === "list_payables_counterparties" ||
|
||
intent === "payables_confirmed_as_of_date" ||
|
||
intent === "receivables_confirmed_as_of_date" ||
|
||
intent === "list_receivables_counterparties");
|
||
}
|
||
async function resolveCounterpartyViaCatalog(anchorRaw) {
|
||
const requested = String(anchorRaw ?? "").trim();
|
||
if (!requested || isLikelyLowQualityPartyAnchor(requested)) {
|
||
return {
|
||
tried: false,
|
||
resolvedValue: null,
|
||
confidence: null,
|
||
ambiguityCount: 0
|
||
};
|
||
}
|
||
const now = Date.now();
|
||
const cacheFresh = counterpartyCatalogCache !== null && now - counterpartyCatalogCache.loadedAt <= COUNTERPARTY_CATALOG_CACHE_TTL_MS;
|
||
let names = cacheFresh ? [...counterpartyCatalogCache.names] : [];
|
||
if (!cacheFresh) {
|
||
const mcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(COUNTERPARTY_CATALOG_LOOKUP_LIMIT)),
|
||
limit: COUNTERPARTY_CATALOG_LOOKUP_LIMIT
|
||
});
|
||
if (!mcp.error) {
|
||
names = extractCounterpartyCatalogNames(mcp.raw_rows);
|
||
if (names.length > 0) {
|
||
counterpartyCatalogCache = {
|
||
names: [...names],
|
||
loadedAt: now
|
||
};
|
||
}
|
||
}
|
||
else if (counterpartyCatalogCache && counterpartyCatalogCache.names.length > 0) {
|
||
names = [...counterpartyCatalogCache.names];
|
||
}
|
||
else {
|
||
return {
|
||
tried: true,
|
||
resolvedValue: null,
|
||
confidence: null,
|
||
ambiguityCount: 0
|
||
};
|
||
}
|
||
}
|
||
if (names.length === 0) {
|
||
return {
|
||
tried: true,
|
||
resolvedValue: null,
|
||
confidence: null,
|
||
ambiguityCount: 0
|
||
};
|
||
}
|
||
const scored = names
|
||
.map((name) => {
|
||
const score = scoreCounterpartyCandidate(name, requested);
|
||
return score === null ? null : { name, score };
|
||
})
|
||
.filter((item) => Boolean(item))
|
||
.sort((a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name, "ru"));
|
||
if (scored.length === 0) {
|
||
return {
|
||
tried: true,
|
||
resolvedValue: null,
|
||
confidence: null,
|
||
ambiguityCount: 0
|
||
};
|
||
}
|
||
const topScore = scored[0].score;
|
||
const topCandidates = scored.filter((item) => item.score === topScore);
|
||
const bestCandidate = topCandidates[0];
|
||
const normalizedRequested = normalizeCounterpartyName(requested);
|
||
const normalizedBest = normalizeCounterpartyName(bestCandidate.name);
|
||
const isExact = normalizedBest === normalizedRequested;
|
||
const isStrongContains = normalizedBest.includes(normalizedRequested);
|
||
if (topCandidates.length > 1 && !isExact && !isStrongContains) {
|
||
return {
|
||
tried: true,
|
||
resolvedValue: null,
|
||
confidence: "low",
|
||
ambiguityCount: topCandidates.length - 1
|
||
};
|
||
}
|
||
return {
|
||
tried: true,
|
||
resolvedValue: bestCandidate.name,
|
||
confidence: isExact ? "high" : isStrongContains ? "medium" : topCandidates.length === 1 ? "medium" : "low",
|
||
ambiguityCount: topCandidates.length - 1
|
||
};
|
||
}
|
||
function collectAnalyticsStrings(row) {
|
||
const fixedKeys = [
|
||
"СубконтоДт1",
|
||
"СубконтоДт2",
|
||
"СубконтоДт3",
|
||
"СубконтоКт1",
|
||
"СубконтоКт2",
|
||
"СубконтоКт3",
|
||
"SubcontoDt1",
|
||
"SubcontoDt2",
|
||
"SubcontoDt3",
|
||
"SubcontoKt1",
|
||
"SubcontoKt2",
|
||
"SubcontoKt3",
|
||
"subconto_dt1",
|
||
"subconto_dt2",
|
||
"subconto_dt3",
|
||
"subconto_kt1",
|
||
"subconto_kt2",
|
||
"subconto_kt3",
|
||
"Counterparty",
|
||
"Контрагент",
|
||
"Contract",
|
||
"Договор",
|
||
"Organization",
|
||
"Организация",
|
||
"ОрганизацияПредставление",
|
||
"organization",
|
||
"organization_name"
|
||
];
|
||
const collected = [];
|
||
for (const key of fixedKeys) {
|
||
const value = valueAsString(row[key]).trim();
|
||
if (value) {
|
||
collected.push(value);
|
||
}
|
||
}
|
||
for (const [key, rawValue] of Object.entries(row)) {
|
||
const lowerKey = key.toLowerCase();
|
||
if (lowerKey.includes("subconto") ||
|
||
lowerKey.includes("субконто") ||
|
||
lowerKey.includes("контраг") ||
|
||
lowerKey.includes("договор") ||
|
||
lowerKey.includes("organization") ||
|
||
lowerKey.includes("организац")) {
|
||
const value = valueAsString(rawValue).trim();
|
||
if (value) {
|
||
collected.push(value);
|
||
}
|
||
}
|
||
}
|
||
return uniqueStrings(collected);
|
||
}
|
||
function toNormalizedRows(rows) {
|
||
return rows
|
||
.map((row) => {
|
||
const period = valueAsString(row.Период ?? row.period ?? row.Period).trim() || null;
|
||
const registrator = valueAsString(row.Регистратор ?? row.registrator ?? row.Registrator).trim() ||
|
||
valueAsString(row.document ?? row.Recorder).trim() ||
|
||
"(без названия)";
|
||
const accountDt = valueAsString(row.СчетДт ?? row.account_dt ?? row.AccountDt).trim() || null;
|
||
const accountKt = valueAsString(row.СчетКт ?? row.account_kt ?? row.AccountKt).trim() || null;
|
||
const amount = parseFiniteNumber(row.Сумма ?? row.amount ?? row.Amount);
|
||
const analytics = collectAnalyticsStrings(row);
|
||
return {
|
||
period,
|
||
registrator,
|
||
account_dt: accountDt,
|
||
account_kt: accountKt,
|
||
amount,
|
||
analytics
|
||
};
|
||
})
|
||
.filter((item) => Boolean(item.period || item.registrator));
|
||
}
|
||
function rowSearchableText(row) {
|
||
return [row.registrator, row.account_dt ?? "", row.account_kt ?? "", ...row.analytics].join(" ").toLowerCase();
|
||
}
|
||
function rowMatchesAnyAccount(row, accountScope) {
|
||
if (accountScope.length === 0) {
|
||
return true;
|
||
}
|
||
const searchable = [row.account_dt ?? "", row.account_kt ?? "", row.registrator, ...row.analytics].join(" ");
|
||
const extractedTokens = extractAccountTokens(searchable);
|
||
const normalizedSearch = normalizeSearchText(searchable);
|
||
const translitSearch = transliterateCyrillicToLatin(normalizedSearch);
|
||
return accountScope.some((account) => {
|
||
const normalizedRequested = normalizeAccountToken(String(account ?? "").trim());
|
||
if (!normalizedRequested) {
|
||
return false;
|
||
}
|
||
if (extractedTokens.some((candidate) => accountTokenMatches(normalizedRequested, candidate))) {
|
||
return true;
|
||
}
|
||
const base = baseAccountCode(normalizedRequested);
|
||
if (!base) {
|
||
return false;
|
||
}
|
||
const aliases = ACCOUNT_ALIAS_MAP[base] ?? [];
|
||
return aliases.some((alias) => {
|
||
const normalizedAlias = normalizeSearchText(alias);
|
||
const aliasLatin = transliterateCyrillicToLatin(normalizedAlias);
|
||
return normalizedSearch.includes(normalizedAlias) || translitSearch.includes(aliasLatin);
|
||
});
|
||
});
|
||
}
|
||
function applyAccountScopeFilter(rows, accountScope) {
|
||
if (accountScope.length === 0) {
|
||
return rows;
|
||
}
|
||
return rows.filter((row) => rowMatchesAnyAccount(row, accountScope));
|
||
}
|
||
function applyAddressFilters(rows, filters) {
|
||
let filtered = [...rows];
|
||
let mismatchReason = null;
|
||
if (filters.account && String(filters.account).trim()) {
|
||
const scopedAccount = String(filters.account).trim();
|
||
const before = filtered.length;
|
||
filtered = filtered.filter((row) => rowMatchesAnyAccount(row, [scopedAccount]));
|
||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||
mismatchReason = "account_anchor_not_matched_in_materialized_rows";
|
||
}
|
||
}
|
||
if (filters.counterparty && String(filters.counterparty).trim()) {
|
||
const needle = String(filters.counterparty);
|
||
const before = filtered.length;
|
||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||
mismatchReason = "counterparty_anchor_not_matched_in_materialized_rows";
|
||
}
|
||
}
|
||
if (filters.contract && String(filters.contract).trim()) {
|
||
const needle = String(filters.contract);
|
||
const before = filtered.length;
|
||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||
mismatchReason = "contract_anchor_not_matched_in_materialized_rows";
|
||
}
|
||
}
|
||
if (filters.organization && String(filters.organization).trim()) {
|
||
const needle = String(filters.organization);
|
||
const before = filtered.length;
|
||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||
mismatchReason = "organization_anchor_not_matched_in_materialized_rows";
|
||
}
|
||
}
|
||
if (filters.document_ref && String(filters.document_ref).trim()) {
|
||
const needle = String(filters.document_ref);
|
||
const before = filtered.length;
|
||
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
|
||
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
|
||
mismatchReason = "document_ref_anchor_not_matched_in_materialized_rows";
|
||
}
|
||
}
|
||
return {
|
||
rows: filtered,
|
||
mismatchReason
|
||
};
|
||
}
|
||
function applyIntentSpecificFilter(intent, rows) {
|
||
if (intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract") {
|
||
const bankDocPattern = /(?:списаниесрасчетногосчета|поступлениенарасчетныйсчет|списание с расчетного счета|поступление на расчетный счет|bank|payment|wire|statement)/i;
|
||
return rows.filter((row) => bankDocPattern.test(row.registrator.toLowerCase()));
|
||
}
|
||
if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") {
|
||
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
|
||
const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||
return matched.length > 0 ? matched : rows;
|
||
}
|
||
if (intent === "documents_forming_balance") {
|
||
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|списаниесрасчетногосчета|поступлениенарасчетныйсчет|invoice|document|sale|purchase)/i;
|
||
const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||
return matched.length > 0 ? matched : rows;
|
||
}
|
||
return rows;
|
||
}
|
||
function parseIsoDateUtcTimestamp(value) {
|
||
const source = String(value ?? "").trim();
|
||
const match = source.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;
|
||
}
|
||
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
||
return null;
|
||
}
|
||
return Date.UTC(year, month - 1, day);
|
||
}
|
||
function isCounterpartyRiskIntent(intent) {
|
||
return (intent === "list_receivables_counterparties" ||
|
||
intent === "list_payables_counterparties" ||
|
||
intent === "payables_confirmed_as_of_date" ||
|
||
intent === "receivables_confirmed_as_of_date" ||
|
||
intent === "list_open_contracts" ||
|
||
intent === "open_items_by_counterparty_or_contract");
|
||
}
|
||
function isHeuristicCandidatesIntent(intent) {
|
||
return (intent === "list_receivables_counterparties" ||
|
||
intent === "list_payables_counterparties" ||
|
||
intent === "list_open_contracts" ||
|
||
intent === "open_items_by_counterparty_or_contract");
|
||
}
|
||
function isConfirmedBalanceIntent(intent) {
|
||
return (intent === "account_balance_snapshot" ||
|
||
intent === "documents_forming_balance" ||
|
||
intent === "payables_confirmed_as_of_date" ||
|
||
intent === "receivables_confirmed_as_of_date" ||
|
||
intent === "vat_payable_confirmed_as_of_date");
|
||
}
|
||
function resolveAsOfDateBasis(filters) {
|
||
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
||
if (asOfDate) {
|
||
return "explicit_as_of_date";
|
||
}
|
||
const periodFrom = normalizeAnalysisDateHint(filters.period_from);
|
||
const periodTo = normalizeAnalysisDateHint(filters.period_to);
|
||
if (periodFrom && periodTo) {
|
||
return "period_range";
|
||
}
|
||
if (!periodFrom && periodTo) {
|
||
return "period_end";
|
||
}
|
||
if (periodFrom) {
|
||
return "period_range";
|
||
}
|
||
return null;
|
||
}
|
||
function deriveAddressEvidenceStrength(input) {
|
||
if (isHeuristicCandidatesIntent(input.intent)) {
|
||
if (input.rowsMatched <= 0 || input.responseType === "LIMITED_WITH_REASON") {
|
||
return "weak";
|
||
}
|
||
if (input.selectedRecipe === "address_open_items_by_party_or_contract_v1") {
|
||
return "medium";
|
||
}
|
||
return "weak";
|
||
}
|
||
if (isConfirmedBalanceIntent(input.intent)) {
|
||
if (input.rowsMatched > 0) {
|
||
return "strong";
|
||
}
|
||
return input.responseType === "LIMITED_WITH_REASON" ? "weak" : "medium";
|
||
}
|
||
return undefined;
|
||
}
|
||
function resolveRequestedResultMode(intent, filters) {
|
||
if (isConfirmedBalanceIntent(intent)) {
|
||
return "confirmed_balance";
|
||
}
|
||
if (isHeuristicCandidatesIntent(intent)) {
|
||
const asOfDateBasis = resolveAsOfDateBasis(filters);
|
||
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") {
|
||
return "confirmed_balance";
|
||
}
|
||
return "heuristic_candidates";
|
||
}
|
||
return undefined;
|
||
}
|
||
function deriveAddressResultSemantics(input) {
|
||
const asOfDateBasis = resolveAsOfDateBasis(input.filters);
|
||
const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters);
|
||
if (isHeuristicCandidatesIntent(input.intent)) {
|
||
return {
|
||
requested_result_mode: requestedResultMode,
|
||
result_mode: "heuristic_candidates",
|
||
evidence_strength: deriveAddressEvidenceStrength(input),
|
||
balance_confirmed: false,
|
||
as_of_date_basis: asOfDateBasis
|
||
};
|
||
}
|
||
if (isConfirmedBalanceIntent(input.intent)) {
|
||
const balanceConfirmed = input.responseType !== "LIMITED_WITH_REASON";
|
||
return {
|
||
requested_result_mode: requestedResultMode,
|
||
result_mode: "confirmed_balance",
|
||
evidence_strength: deriveAddressEvidenceStrength(input),
|
||
balance_confirmed: balanceConfirmed,
|
||
as_of_date_basis: asOfDateBasis ?? "period_end"
|
||
};
|
||
}
|
||
if (requestedResultMode) {
|
||
return {
|
||
requested_result_mode: requestedResultMode
|
||
};
|
||
}
|
||
return {};
|
||
}
|
||
function mergeAddressResultSemantics(base, override) {
|
||
if (!override) {
|
||
return base;
|
||
}
|
||
return {
|
||
...base,
|
||
...(override.result_mode ? { result_mode: override.result_mode } : {}),
|
||
...(override.evidence_strength ? { evidence_strength: override.evidence_strength } : {}),
|
||
...(typeof override.balance_confirmed === "boolean" ? { balance_confirmed: override.balance_confirmed } : {})
|
||
};
|
||
}
|
||
function withConfirmedBalanceFallbackReason(reasons, requestedResultMode, semantics, baseResultMode) {
|
||
if (requestedResultMode !== "confirmed_balance") {
|
||
return reasons;
|
||
}
|
||
const effectiveResultMode = semantics?.result_mode ?? baseResultMode;
|
||
if (effectiveResultMode !== "heuristic_candidates") {
|
||
return reasons;
|
||
}
|
||
if (reasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
||
return reasons;
|
||
}
|
||
return [...reasons, "confirmed_balance_unavailable_fallback_to_heuristic_candidates"];
|
||
}
|
||
function buildCapabilityAudit(intent) {
|
||
const decision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent);
|
||
return {
|
||
capabilityId: decision.capability_id,
|
||
layer: decision.capability_layer,
|
||
routeMode: decision.capability_route_mode,
|
||
enabled: decision.capability_route_enabled,
|
||
reason: decision.capability_route_reason
|
||
};
|
||
}
|
||
function buildShadowRouteAudit(input) {
|
||
const shadowIntent = (0, addressCapabilityPolicy_1.resolveShadowRouteIntent)(input.intent, input.requestedResultMode);
|
||
if (!shadowIntent) {
|
||
return {
|
||
intent: null,
|
||
selectedRecipe: null,
|
||
status: "skipped"
|
||
};
|
||
}
|
||
const shadowRecipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(shadowIntent, input.filters);
|
||
if (!shadowRecipeSelection.selected_recipe) {
|
||
return {
|
||
intent: shadowIntent,
|
||
selectedRecipe: null,
|
||
status: "unavailable"
|
||
};
|
||
}
|
||
return {
|
||
intent: shadowIntent,
|
||
selectedRecipe: shadowRecipeSelection.selected_recipe.recipe_id,
|
||
status: "planned"
|
||
};
|
||
}
|
||
function buildRouteExpectationAudit(input) {
|
||
if (!config_1.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1) {
|
||
return {
|
||
status: "not_found",
|
||
reason: "route_expectation_audit_disabled",
|
||
expectedSelectedRecipes: [],
|
||
expectedRequestedResultModes: [],
|
||
expectedResultModes: []
|
||
};
|
||
}
|
||
const audit = (0, addressRouteExpectations_1.evaluateAddressRouteExpectation)({
|
||
intent: input.intent,
|
||
selectedRecipe: input.selectedRecipe,
|
||
requestedResultMode: input.requestedResultMode,
|
||
resultMode: input.resultMode
|
||
});
|
||
return {
|
||
status: audit.status,
|
||
reason: audit.reason,
|
||
expectedSelectedRecipes: audit.expected_selected_recipes,
|
||
expectedRequestedResultModes: audit.expected_requested_result_modes,
|
||
expectedResultModes: audit.expected_result_modes
|
||
};
|
||
}
|
||
function enforceStrictAccountScopeForIntent(plan, intent) {
|
||
if (intent !== "list_receivables_counterparties" || plan.account_scope_mode === "strict") {
|
||
return plan;
|
||
}
|
||
return {
|
||
...plan,
|
||
account_scope_mode: "strict"
|
||
};
|
||
}
|
||
function resolveExecutionFiltersForConfirmedBalance(filters, analysisDate) {
|
||
const explicitAsOf = normalizeAnalysisDateHint(filters.as_of_date);
|
||
const periodTo = normalizeAnalysisDateHint(filters.period_to);
|
||
const derivedAsOf = explicitAsOf ?? periodTo ?? analysisDate ?? null;
|
||
const executionFilters = {
|
||
...filters
|
||
};
|
||
if (derivedAsOf) {
|
||
executionFilters.as_of_date = derivedAsOf;
|
||
}
|
||
delete executionFilters.period_from;
|
||
delete executionFilters.period_to;
|
||
const limit = typeof executionFilters.limit === "number" && Number.isFinite(executionFilters.limit)
|
||
? Math.max(1, Math.trunc(executionFilters.limit))
|
||
: null;
|
||
if (limit === null || limit < ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT) {
|
||
executionFilters.limit = Math.max(ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT, limit ?? 0);
|
||
}
|
||
return {
|
||
executionFilters,
|
||
asOfDerived: derivedAsOf
|
||
};
|
||
}
|
||
function resolveFutureGuardReferenceDate(analysisDate, filters) {
|
||
if (analysisDate) {
|
||
return analysisDate;
|
||
}
|
||
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
||
if (asOfDate) {
|
||
return asOfDate;
|
||
}
|
||
const periodTo = normalizeAnalysisDateHint(filters.period_to);
|
||
if (periodTo) {
|
||
return periodTo;
|
||
}
|
||
return null;
|
||
}
|
||
function isMissingSubcontoFieldError(errorText) {
|
||
const normalized = String(errorText ?? "")
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const ruMissingField = "\u043f\u043e\u043b\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e";
|
||
const hasMissingFieldSignal = normalized.includes(ruMissingField) || normalized.includes("field not found");
|
||
if (!hasMissingFieldSignal) {
|
||
return false;
|
||
}
|
||
const hasAnySubcontoSignal = /(?:\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e(?:\u0434\u0442|\u043a\u0442)?\d*|subconto(?:_)?(?:dt|kt)?\d*)/iu.test(normalized) ||
|
||
normalized.includes("subcontodt") ||
|
||
normalized.includes("subcontokt");
|
||
return hasAnySubcontoSignal;
|
||
}
|
||
function buildCompositeSubcontoFallbackQuery(queryText) {
|
||
const source = String(queryText ?? "");
|
||
if (!source.trim()) {
|
||
return null;
|
||
}
|
||
const dt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт1\s*\)\s+КАК\s+СубконтоДт1\s*,?/iu;
|
||
const dt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт2\s*\)\s+КАК\s+СубконтоДт2\s*,?/iu;
|
||
const dt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт3\s*\)\s+КАК\s+СубконтоДт3\s*,?/iu;
|
||
const kt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт1\s*\)\s+КАК\s+СубконтоКт1\s*,?/iu;
|
||
const kt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт2\s*\)\s+КАК\s+СубконтоКт2\s*,?/iu;
|
||
const kt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт3\s*\)\s+КАК\s+СубконтоКт3\s*,?/iu;
|
||
const lines = source.split(/\r?\n/);
|
||
let replaced = false;
|
||
const rewrittenLines = lines.map((line) => {
|
||
const indent = line.match(/^\s*/)?.[0] ?? "";
|
||
if (dt1Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт) КАК СубконтоДт1,`;
|
||
}
|
||
if (dt2Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}"" КАК СубконтоДт2,`;
|
||
}
|
||
if (dt3Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}"" КАК СубконтоДт3,`;
|
||
}
|
||
if (kt1Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт) КАК СубконтоКт1,`;
|
||
}
|
||
if (kt2Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}"" КАК СубконтоКт2,`;
|
||
}
|
||
if (kt3Pattern.test(line)) {
|
||
replaced = true;
|
||
return `${indent}"" КАК СубконтоКт3,`;
|
||
}
|
||
return line;
|
||
});
|
||
if (!replaced) {
|
||
return null;
|
||
}
|
||
return rewrittenLines.join("\n");
|
||
}
|
||
function applyFutureDatedRowsGuard(rows, intent, referenceDate) {
|
||
if (!isCounterpartyRiskIntent(intent) || rows.length === 0) {
|
||
return {
|
||
rows,
|
||
droppedCount: 0
|
||
};
|
||
}
|
||
const referenceTs = (() => {
|
||
const explicitTs = parseIsoDateUtcTimestamp(referenceDate);
|
||
if (explicitTs !== null) {
|
||
return explicitTs;
|
||
}
|
||
const now = new Date();
|
||
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||
})();
|
||
const guardTailMs = 31 * 24 * 60 * 60 * 1000;
|
||
const latestAllowedTs = referenceTs + guardTailMs;
|
||
const keptRows = [];
|
||
let droppedCount = 0;
|
||
for (const row of rows) {
|
||
const rowTs = parseIsoDateUtcTimestamp(row.period);
|
||
if (rowTs !== null && rowTs > latestAllowedTs) {
|
||
droppedCount += 1;
|
||
continue;
|
||
}
|
||
keptRows.push(row);
|
||
}
|
||
return {
|
||
rows: keptRows,
|
||
droppedCount
|
||
};
|
||
}
|
||
function hasExplicitPeriodWindow(filters) {
|
||
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
|
||
}
|
||
function canAutoBroadenPeriodWindow(intent, filters) {
|
||
if (!hasExplicitPeriodWindow(filters)) {
|
||
return false;
|
||
}
|
||
return (intent === "list_documents_by_counterparty" ||
|
||
intent === "bank_operations_by_counterparty" ||
|
||
intent === "list_documents_by_contract" ||
|
||
intent === "bank_operations_by_contract");
|
||
}
|
||
function invertSort(sort) {
|
||
return sort === "period_asc" ? "period_desc" : "period_asc";
|
||
}
|
||
function isAnchorRecoveryIntent(intent) {
|
||
return (intent === "list_documents_by_counterparty" ||
|
||
intent === "bank_operations_by_counterparty" ||
|
||
intent === "list_contracts_by_counterparty" ||
|
||
intent === "list_documents_by_contract" ||
|
||
intent === "bank_operations_by_contract" ||
|
||
intent === "list_payables_counterparties" ||
|
||
intent === "list_receivables_counterparties" ||
|
||
intent === "open_items_by_counterparty_or_contract" ||
|
||
intent === "list_open_contracts");
|
||
}
|
||
function isDocumentOrBankAnchorIntent(intent) {
|
||
return (intent === "list_documents_by_counterparty" ||
|
||
intent === "bank_operations_by_counterparty" ||
|
||
intent === "list_documents_by_contract" ||
|
||
intent === "bank_operations_by_contract");
|
||
}
|
||
function toIsoDatePrefix(value) {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
const normalized = String(value).trim();
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/);
|
||
if (match) {
|
||
return match[1];
|
||
}
|
||
return null;
|
||
}
|
||
function deriveObservedPeriodWindow(rows) {
|
||
const dates = rows
|
||
.map((row) => toIsoDatePrefix(row.period))
|
||
.filter((item) => Boolean(item))
|
||
.sort();
|
||
if (dates.length === 0) {
|
||
return {
|
||
period_from: null,
|
||
period_to: null
|
||
};
|
||
}
|
||
return {
|
||
period_from: dates[0],
|
||
period_to: dates[dates.length - 1]
|
||
};
|
||
}
|
||
function composeAutoBroadenedPeriodPrefix(requested, observed) {
|
||
const requestedFrom = typeof requested.period_from === "string" ? requested.period_from : null;
|
||
const requestedTo = typeof requested.period_to === "string" ? requested.period_to : null;
|
||
if (requestedFrom && requestedTo && observed.period_from && observed.period_to) {
|
||
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные ${observed.period_from}..${observed.period_to}.`;
|
||
}
|
||
if (requestedFrom && requestedTo) {
|
||
return `По окну ${requestedFrom}..${requestedTo} строк не найдено; показаны ближайшие доступные данные по этому якорю.`;
|
||
}
|
||
return "По заданному периоду строк не найдено; показаны ближайшие доступные данные по этому якорю.";
|
||
}
|
||
function runtimeReadinessForLimitedCategory(category) {
|
||
if (category === "empty_match" || category === "missing_anchor") {
|
||
return "LIVE_QUERYABLE_WITH_LIMITS";
|
||
}
|
||
if (category === "recipe_visibility_gap") {
|
||
return "REQUIRES_SPECIALIZED_RECIPE";
|
||
}
|
||
if (category === "unsupported") {
|
||
return "DEEP_ONLY";
|
||
}
|
||
return "UNKNOWN";
|
||
}
|
||
function normalizeLimitedReason(reason) {
|
||
let normalized = String(reason ?? "").trim();
|
||
if (!normalized) {
|
||
return "не хватает подтвержденных данных для уверенного вывода";
|
||
}
|
||
const replacements = [
|
||
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||
[/address\s*v1/giu, "текущий адресный режим"],
|
||
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
|
||
[/live\s+recipe/giu, "текущий сценарий выборки"],
|
||
[/materialized\s+live-строках/giu, "доступном срезе данных"],
|
||
[/live-выборке/giu, "выборке данных"],
|
||
[/live-данных/giu, "данных"],
|
||
[/deep-analysis/giu, "режим расширенной проверки"],
|
||
[/\blookup\b/giu, "поиск"],
|
||
[/\bintent\b/giu, "сценария"],
|
||
[/\brecipe\b/giu, "шаблон выборки"],
|
||
[/\byakor\b/giu, "ориентир"],
|
||
[/\banchor\b/giu, "ориентир"],
|
||
[/\s+/gu, " "]
|
||
];
|
||
for (const [pattern, value] of replacements) {
|
||
normalized = normalized.replace(pattern, value);
|
||
}
|
||
return normalized.trim();
|
||
}
|
||
function normalizeLimitedNextStep(nextStep) {
|
||
let normalized = String(nextStep ?? "").trim();
|
||
if (!normalized) {
|
||
return "";
|
||
}
|
||
const replacements = [
|
||
[/address_query\s*v?1/giu, "текущий адресный режим"],
|
||
[/deep-analysis/giu, "режим расширенной проверки"],
|
||
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
|
||
[/\bintent\b/giu, "сценарий"],
|
||
[/\blookup\b/giu, "поиск"],
|
||
[/\s+/gu, " "]
|
||
];
|
||
for (const [pattern, value] of replacements) {
|
||
normalized = normalized.replace(pattern, value);
|
||
}
|
||
return normalized.trim();
|
||
}
|
||
function rowHasNonEmptyField(row, keys) {
|
||
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
|
||
}
|
||
function deriveRowStageDiagnostics(rawRows, rowsAfterAccountScope, rowsMaterialized) {
|
||
if (rawRows.length === 0 || rowsMaterialized > 0) {
|
||
return {
|
||
rawRowKeysSample: rawRows.length > 0 ? Object.keys(rawRows[0] ?? {}).slice(0, 20) : [],
|
||
materializationDropReason: "none"
|
||
};
|
||
}
|
||
if (rawRows.length > 0 && rowsAfterAccountScope === 0) {
|
||
return {
|
||
rawRowKeysSample: Object.keys(rawRows[0] ?? {}).slice(0, 20),
|
||
materializationDropReason: "dropped_by_account_scope_filter"
|
||
};
|
||
}
|
||
const rawRowKeysSample = Object.keys(rawRows[0] ?? {}).slice(0, 20);
|
||
const hasPeriodField = rawRows.some((row) => rowHasNonEmptyField(row, ["Период", "period", "Period"]));
|
||
const hasRegistratorField = rawRows.some((row) => rowHasNonEmptyField(row, ["Регистратор", "registrator", "Registrator", "document", "Recorder"]));
|
||
if (!hasPeriodField && !hasRegistratorField) {
|
||
return { rawRowKeysSample, materializationDropReason: "missing_period_and_registrator_fields" };
|
||
}
|
||
if (!hasPeriodField) {
|
||
return { rawRowKeysSample, materializationDropReason: "missing_period_field" };
|
||
}
|
||
if (!hasRegistratorField) {
|
||
return { rawRowKeysSample, materializationDropReason: "missing_registrator_field" };
|
||
}
|
||
return { rawRowKeysSample, materializationDropReason: "unknown_row_shape" };
|
||
}
|
||
function isAccountIntent(intent) {
|
||
return intent === "account_balance_snapshot" || intent === "documents_forming_balance";
|
||
}
|
||
function buildDefaultAccountScopeAudit(filters) {
|
||
const tokenRaw = typeof filters.account === "string" && filters.account.trim().length > 0 ? filters.account.trim() : null;
|
||
return {
|
||
accountTokenRaw: tokenRaw,
|
||
accountTokenNormalized: tokenRaw ? normalizeAccountToken(tokenRaw) : null,
|
||
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
|
||
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
|
||
accountScopeDropReason: "not_applicable"
|
||
};
|
||
}
|
||
function buildAccountScopeAudit(input) {
|
||
const tokenRaw = typeof input.filters.account === "string" && input.filters.account.trim().length > 0 ? input.filters.account.trim() : null;
|
||
const tokenNormalized = tokenRaw ? normalizeAccountToken(tokenRaw) : null;
|
||
if (!isAccountIntent(input.intent)) {
|
||
return {
|
||
accountTokenRaw: tokenRaw,
|
||
accountTokenNormalized: tokenNormalized,
|
||
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
|
||
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
|
||
accountScopeDropReason: "not_applicable"
|
||
};
|
||
}
|
||
if (input.accountScope.length === 0) {
|
||
return {
|
||
accountTokenRaw: tokenRaw,
|
||
accountTokenNormalized: tokenNormalized,
|
||
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
|
||
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
|
||
accountScopeDropReason: "no_account_scope_requested"
|
||
};
|
||
}
|
||
return {
|
||
accountTokenRaw: tokenRaw,
|
||
accountTokenNormalized: tokenNormalized,
|
||
accountScopeFieldsChecked: [...ACCOUNT_SCOPE_FIELDS_CHECKED],
|
||
accountScopeMatchStrategy: ACCOUNT_SCOPE_MATCH_STRATEGY,
|
||
accountScopeDropReason: input.rowsBeforeScope > 0 && input.rowsAfterScope === 0 ? "no_rows_after_scope_filter" : "rows_remaining_after_scope_filter"
|
||
};
|
||
}
|
||
function deriveMcpStageStatus(input) {
|
||
if (input.skipped) {
|
||
return "skipped";
|
||
}
|
||
if (input.errored) {
|
||
return "error";
|
||
}
|
||
if (input.rawRowsReceived === 0) {
|
||
return "no_raw_rows";
|
||
}
|
||
if (input.rowsMaterialized === 0) {
|
||
return "raw_rows_received_but_not_materialized";
|
||
}
|
||
if (input.rowsAnchorMatched === 0) {
|
||
return "materialized_but_not_anchor_matched";
|
||
}
|
||
if (input.rowsMatched === 0) {
|
||
return "materialized_but_filtered_out_by_recipe";
|
||
}
|
||
return "matched_non_empty";
|
||
}
|
||
function toLegacyMcpStatus(status) {
|
||
if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") {
|
||
return "materialized_but_not_matched";
|
||
}
|
||
return status;
|
||
}
|
||
function pickDeterministicVariant(seed, variants) {
|
||
if (variants.length === 0) {
|
||
return "";
|
||
}
|
||
let score = 0;
|
||
for (const char of String(seed ?? "")) {
|
||
score = (score + char.charCodeAt(0)) % 104_729;
|
||
}
|
||
return variants[score % variants.length];
|
||
}
|
||
function toNonEmptyFilterValue(value) {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const normalized = value.trim();
|
||
return normalized.length > 0 ? normalized : null;
|
||
}
|
||
function isWeakOfferAnchorValue(value) {
|
||
const normalized = String(value ?? "")
|
||
.toLowerCase()
|
||
.replace(/\s+/gu, " ")
|
||
.trim();
|
||
if (!normalized) {
|
||
return true;
|
||
}
|
||
if (normalized.length < 3) {
|
||
return true;
|
||
}
|
||
if (/^\d+$/u.test(normalized)) {
|
||
return true;
|
||
}
|
||
return /^(?:контрагент(?:ы|а|у|ом)?|договор(?:ы|а|у|ом)?|контракт(?:ы|а|у|ом)?|документ(?:ы|а|у|ом)?|оплат(?:а|ы|у|ой|ам)?|плат[её]ж(?:и|а|у|ом)?|операц(?:ия|ии|ий|ию|иями)?|период|данные|база|компания|организация)$/iu.test(normalized);
|
||
}
|
||
function normalizeMissingAnchorLabel(anchor) {
|
||
if (anchor === "counterparty_or_contract") {
|
||
return "контрагент или договор";
|
||
}
|
||
if (anchor === "counterparty") {
|
||
return "контрагент";
|
||
}
|
||
if (anchor === "contract") {
|
||
return "договор";
|
||
}
|
||
if (anchor === "account") {
|
||
return "счет";
|
||
}
|
||
if (anchor === "document_ref") {
|
||
return "документ";
|
||
}
|
||
if (anchor === "organization") {
|
||
return "организация";
|
||
}
|
||
if (anchor === "period" || anchor === "period_from" || anchor === "period_to" || anchor === "as_of_date") {
|
||
return "период/дата";
|
||
}
|
||
return anchor.replace(/_/gu, " ");
|
||
}
|
||
function buildLimitedScopeLine(filters) {
|
||
const organization = toNonEmptyFilterValue(filters.organization);
|
||
const asOfDate = toNonEmptyFilterValue(filters.as_of_date);
|
||
const periodFrom = toNonEmptyFilterValue(filters.period_from);
|
||
const periodTo = toNonEmptyFilterValue(filters.period_to);
|
||
const scopeParts = [];
|
||
if (organization) {
|
||
scopeParts.push(`организация ${organization}`);
|
||
}
|
||
if (asOfDate) {
|
||
scopeParts.push(`срез на ${asOfDate}`);
|
||
}
|
||
else if (periodFrom || periodTo) {
|
||
scopeParts.push(`период ${periodFrom ?? "..."}..${periodTo ?? "..."}`);
|
||
}
|
||
if (scopeParts.length === 0) {
|
||
return null;
|
||
}
|
||
return `Контекст запроса: ${scopeParts.join(", ")}.`;
|
||
}
|
||
function buildLimitedVariantSeedFingerprint(filters) {
|
||
const seedParts = [];
|
||
const keys = [
|
||
"organization",
|
||
"counterparty",
|
||
"contract",
|
||
"account",
|
||
"document_ref",
|
||
"as_of_date",
|
||
"period_from",
|
||
"period_to"
|
||
];
|
||
for (const key of keys) {
|
||
const raw = filters[key];
|
||
const value = typeof raw === "string" ? raw.trim() : "";
|
||
if (!value) {
|
||
continue;
|
||
}
|
||
seedParts.push(`${key}:${value.toLowerCase()}`);
|
||
}
|
||
return seedParts.length > 0 ? seedParts.join("|") : "no_filter_seed";
|
||
}
|
||
function buildLimitedOffers(input) {
|
||
const counterpartyRaw = toNonEmptyFilterValue(input.filters.counterparty);
|
||
const contractRaw = toNonEmptyFilterValue(input.filters.contract);
|
||
const counterparty = counterpartyRaw && !isWeakOfferAnchorValue(counterpartyRaw) ? counterpartyRaw : null;
|
||
const contract = contractRaw && !isWeakOfferAnchorValue(contractRaw) ? contractRaw : null;
|
||
const account = toNonEmptyFilterValue(input.filters.account);
|
||
const offers = [];
|
||
if (input.category === "missing_anchor") {
|
||
const missingAnchors = Array.from(new Set((Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : [])
|
||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||
.filter((item) => item.length > 0)));
|
||
if (missingAnchors.length > 0) {
|
||
offers.push(`уточнить ориентир: ${missingAnchors.join(", ")}`);
|
||
}
|
||
if (missingAnchors.includes("контрагент или договор")) {
|
||
offers.push("пример: «покажи документы по договору <номер> за 2020 год»");
|
||
}
|
||
}
|
||
if (input.intent === "list_receivables_counterparties") {
|
||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||
}
|
||
else if (input.intent === "receivables_confirmed_as_of_date") {
|
||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
||
}
|
||
else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||
}
|
||
else if (input.intent === "payables_confirmed_as_of_date") {
|
||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||
}
|
||
else if (input.intent === "list_payables_counterparties") {
|
||
offers.push("показать контрагентов с максимальными хвостами кредиторки по 60/76");
|
||
}
|
||
else if (input.intent === "open_items_by_counterparty_or_contract" || input.intent === "list_open_contracts") {
|
||
offers.push("показать незакрытые договоры и хвосты взаиморасчетов на дату");
|
||
}
|
||
if (counterparty) {
|
||
offers.push(`показать документы и платежи по контрагенту ${counterparty}`);
|
||
}
|
||
else if (contract) {
|
||
offers.push(`показать документы и платежи по договору ${contract}`);
|
||
}
|
||
else {
|
||
offers.push("показать документы/платежи по контрагенту или договору");
|
||
}
|
||
if (account) {
|
||
offers.push(`проверить остаток и документы, формирующие остаток по счету ${account}`);
|
||
}
|
||
else {
|
||
offers.push("показать незакрытые договоры или хвосты на дату");
|
||
}
|
||
const aggregateIntentSignal = input.shape.shape === "AGGREGATE_LOOKUP" ||
|
||
/(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin)/iu.test(String(input.reason ?? ""));
|
||
if (input.category === "unsupported" && aggregateIntentSignal) {
|
||
offers.unshift("собрать фактическую базу по периоду, после чего посчитать метрику в расширенном анализе");
|
||
}
|
||
const nextStep = normalizeLimitedNextStep(input.nextStep ?? "");
|
||
if (nextStep) {
|
||
offers.push(nextStep);
|
||
}
|
||
return Array.from(new Set(offers)).slice(0, 3);
|
||
}
|
||
function buildLimitedIntentSignalLine(input) {
|
||
const byIntent = {
|
||
list_documents_by_counterparty: "Сигнал запроса: нужен срез документов/платежей по контрагенту.",
|
||
list_documents_by_contract: "Сигнал запроса: нужен срез документов/платежей по договору.",
|
||
bank_operations_by_counterparty: "Сигнал запроса: нужен срез банковских операций по контрагенту.",
|
||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
||
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату."
|
||
};
|
||
const byShape = {
|
||
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
||
DOCUMENT_LIST: "Сигнал запроса: список документов/операций.",
|
||
OBJECT_LOOKUP: "Сигнал запроса: поиск конкретных объектов.",
|
||
VERIFY_FACTUAL: "Сигнал запроса: проверка фактического состояния по данным.",
|
||
COMPOUND_FACTUAL_QUERY: "Сигнал запроса: комбинированная проверка взаимосвязанных фактов."
|
||
};
|
||
return byIntent[input.intent] ?? byShape[input.shape.shape] ?? null;
|
||
}
|
||
function hasAggregateLimitedSignal(input) {
|
||
if (input.shape.shape === "AGGREGATE_LOOKUP") {
|
||
return true;
|
||
}
|
||
if (input.intent === "counterparty_population_and_roles" ||
|
||
input.intent === "counterparty_activity_lifecycle" ||
|
||
input.intent === "contract_usage_overview" ||
|
||
input.intent === "supplier_payouts_profile" ||
|
||
input.intent === "customer_revenue_and_payments" ||
|
||
input.intent === "contract_usage_and_value") {
|
||
return true;
|
||
}
|
||
return /(?:оборот|выруч|доход|прибыл|марж|рентабел|тренд|динам|самый|топ|ranking|revenue|profit|margin|year)/iu.test(String(input.reason ?? ""));
|
||
}
|
||
function composeLimitedReply(input) {
|
||
const reason = normalizeLimitedReason(input.reason);
|
||
const filterSeed = buildLimitedVariantSeedFingerprint(input.filters);
|
||
const missingSeed = Array.from(new Set(input.missingRequiredFilters.map((item) => String(item ?? "").trim())))
|
||
.filter((item) => item.length > 0)
|
||
.sort()
|
||
.join(",");
|
||
const headingSeed = `${input.category}|${input.shape.shape}|${input.intent}|${reason}|${filterSeed}|${missingSeed}`;
|
||
const aggregateLimitedSignal = hasAggregateLimitedSignal({
|
||
shape: input.shape,
|
||
intent: input.intent,
|
||
reason: input.reason
|
||
});
|
||
const missingAnchorLabels = Array.from(new Set((Array.isArray(input.missingRequiredFilters) ? input.missingRequiredFilters : [])
|
||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||
.filter((item) => item.length > 0)));
|
||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||
const heading = input.category === "empty_match"
|
||
? pickDeterministicVariant(headingSeed, [
|
||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||
"В текущем срезе данных по этому запросу совпадения не найдены.",
|
||
"По заданным фильтрам в текущем срезе совпадений пока нет."
|
||
])
|
||
: input.category === "missing_anchor"
|
||
? pickDeterministicVariant(headingSeed, [
|
||
"Чтобы ответ был точным, нужно чуть сильнее заякорить запрос.",
|
||
"Запрос понятен, но для надежного ответа не хватает опорного ориентира.",
|
||
"Вопрос по смыслу ясен, но пока не хватает конкретной опоры для выборки."
|
||
])
|
||
: input.category === "recipe_visibility_gap"
|
||
? pickDeterministicVariant(headingSeed, [
|
||
"Запрос понятен, но текущий сценарий выборки не дает нужной детализации.",
|
||
"Смысл запроса ясен, но в этом контуре не хватает глубины выборки.",
|
||
"Сценарий запроса корректный, но текущая витрина не дает нужной детализации."
|
||
])
|
||
: input.category === "unsupported"
|
||
? pickDeterministicVariant(headingSeed, [
|
||
"Сейчас не дам прямой адресный ответ, чтобы не ошибиться в выводах.",
|
||
"В текущем адресном контуре этот запрос лучше не закрывать «в лоб» — риск неверной трактовки высок.",
|
||
"Для такого формата запроса нужен более широкий аналитический контур, иначе ответ будет ненадежным."
|
||
])
|
||
: "Не удалось завершить проверку в адресном режиме.";
|
||
const reasonSeed = `${headingSeed}|reason`;
|
||
const reasonLine = input.category === "unsupported"
|
||
? aggregateLimitedSignal
|
||
? pickDeterministicVariant(reasonSeed, [
|
||
"Это агрегатный/сравнительный вопрос: без расширенного анализа здесь легко дать ложную метрику.",
|
||
"Запрос про сводную аналитику или ранжирование, поэтому в address-контуре ответ сейчас будет ненадежным.",
|
||
"Нужна расширенная аналитическая обработка: адресный режим в этом кейсе не гарантирует корректный расчет."
|
||
])
|
||
: pickDeterministicVariant(reasonSeed, [
|
||
"Сценарий пока не закрыт текущими адресными маршрутами без потери точности.",
|
||
"Для этого запроса пока нет надежного ответа в текущем адресном режиме.",
|
||
"Надежный ответ здесь требует более широкого анализа, чем текущий адресный контур."
|
||
])
|
||
: input.category === "missing_anchor"
|
||
? pickDeterministicVariant(reasonSeed, [
|
||
`Нужно чуть точнее заякорить запрос: не хватает конкретного ориентира (${missingAnchorPhrase}).`,
|
||
`Для точного ответа нужен хотя бы один явный ориентир: ${missingAnchorPhrase}.`,
|
||
`Смысл запроса понятен, но без уточнения (${missingAnchorPhrase}) риск ошибки слишком высокий.`
|
||
])
|
||
: input.category === "recipe_visibility_gap"
|
||
? "Для уверенного ответа нужен более специализированный сценарий выборки."
|
||
: `${reason}.`;
|
||
const lines = [heading, reasonLine];
|
||
const signalLine = buildLimitedIntentSignalLine({
|
||
intent: input.intent,
|
||
shape: input.shape
|
||
});
|
||
if (signalLine && !(input.category === "unsupported" && aggregateLimitedSignal)) {
|
||
lines.push(signalLine);
|
||
}
|
||
const scopeLine = buildLimitedScopeLine(input.filters);
|
||
if (scopeLine) {
|
||
lines.push(scopeLine);
|
||
}
|
||
const offers = buildLimitedOffers({
|
||
category: input.category,
|
||
shape: input.shape,
|
||
intent: input.intent,
|
||
filters: input.filters,
|
||
missingRequiredFilters: input.missingRequiredFilters,
|
||
reason: input.reason,
|
||
nextStep: input.nextStep
|
||
});
|
||
if (offers.length > 0) {
|
||
lines.push(`Что могу сделать сейчас: ${offers.join("; ")}.`);
|
||
}
|
||
return lines.join("\n\n");
|
||
}
|
||
function buildLimitedExecutionResult(input) {
|
||
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
|
||
const resultSemantics = deriveAddressResultSemantics({
|
||
intent: input.intent.intent,
|
||
selectedRecipe: input.selectedRecipe,
|
||
filters: input.filters,
|
||
responseType: "LIMITED_WITH_REASON",
|
||
rowsMatched: input.rowsMatched
|
||
});
|
||
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
|
||
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
|
||
const exactLimitedReason = input.intent.intent === "payables_confirmed_as_of_date"
|
||
? "exact_payables_mode_limited_response"
|
||
: input.intent.intent === "receivables_confirmed_as_of_date"
|
||
? "exact_receivables_mode_limited_response"
|
||
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
||
? "exact_vat_payable_mode_limited_response"
|
||
: null;
|
||
const reasons = exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
||
? [...reasonsWithConfirmedFallback, exactLimitedReason]
|
||
: reasonsWithConfirmedFallback;
|
||
const routeExpectationAudit = input.routeExpectationAudit ??
|
||
buildRouteExpectationAudit({
|
||
intent: input.intent.intent,
|
||
selectedRecipe: input.selectedRecipe,
|
||
requestedResultMode: requestedResultMode,
|
||
resultMode: resultSemantics.result_mode
|
||
});
|
||
return {
|
||
handled: true,
|
||
reply_text: composeLimitedReply({
|
||
category: input.category,
|
||
reason: input.reasonText,
|
||
nextStep: input.nextStep,
|
||
shape: input.shape,
|
||
intent: input.intent.intent,
|
||
filters: input.filters,
|
||
missingRequiredFilters: input.missingRequiredFilters
|
||
}),
|
||
reply_type: "partial_coverage",
|
||
response_type: "LIMITED_WITH_REASON",
|
||
debug: {
|
||
detected_mode: input.mode.mode,
|
||
detected_mode_confidence: input.mode.confidence,
|
||
query_shape: input.shape.shape,
|
||
query_shape_confidence: input.shape.confidence,
|
||
detected_intent: input.intent.intent,
|
||
detected_intent_confidence: input.intent.confidence,
|
||
extracted_filters: input.filters,
|
||
missing_required_filters: input.missingRequiredFilters,
|
||
selected_recipe: input.selectedRecipe,
|
||
mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus),
|
||
account_scope_mode: input.accountScopeMode ?? "strict",
|
||
account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false,
|
||
anchor_type: input.anchor?.anchor_type ?? null,
|
||
anchor_value_raw: input.anchor?.anchor_value_raw ?? null,
|
||
anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null,
|
||
resolver_confidence: input.anchor?.resolver_confidence ?? null,
|
||
ambiguity_count: input.anchor?.ambiguity_count ?? 0,
|
||
match_failure_stage: input.matchFailureStage ?? "none",
|
||
match_failure_reason: input.matchFailureReason ?? null,
|
||
mcp_call_status: input.mcpCallStatus,
|
||
rows_fetched: input.rowsFetched,
|
||
raw_rows_received: input.rawRowsReceived ?? input.rowsFetched,
|
||
rows_after_account_scope: input.rowsAfterAccountScope ?? 0,
|
||
rows_after_recipe_filter: input.rowsAfterRecipeFilter ?? 0,
|
||
rows_materialized: input.rowsMaterialized ?? 0,
|
||
rows_matched: input.rowsMatched,
|
||
raw_row_keys_sample: input.rawRowKeysSample ?? [],
|
||
materialization_drop_reason: input.materializationDropReason ?? "none",
|
||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
|
||
limited_reason_category: input.category,
|
||
response_type: "LIMITED_WITH_REASON",
|
||
capability_id: input.capabilityAudit?.capabilityId ?? null,
|
||
capability_layer: input.capabilityAudit?.layer ?? null,
|
||
capability_route_mode: input.capabilityAudit?.routeMode ?? null,
|
||
capability_route_enabled: input.capabilityAudit?.enabled ?? true,
|
||
capability_route_reason: input.capabilityAudit?.reason ?? null,
|
||
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
||
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
||
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
||
route_expectation_status: routeExpectationAudit.status,
|
||
route_expectation_reason: routeExpectationAudit.reason,
|
||
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
|
||
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
|
||
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
|
||
...resultSemantics,
|
||
limitations: input.limitations,
|
||
reasons
|
||
}
|
||
};
|
||
}
|
||
class AddressQueryService {
|
||
async tryHandle(userMessage, options = {}) {
|
||
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
||
return null;
|
||
}
|
||
const followupContext = options.followupContext ?? null;
|
||
const decompose = (0, decomposeStage_1.runAddressDecomposeStage)(userMessage, followupContext);
|
||
if (!decompose) {
|
||
return null;
|
||
}
|
||
const { mode, shape, intent, filters } = decompose;
|
||
const baseReasons = [...decompose.baseReasons];
|
||
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||
if (analysisDate) {
|
||
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
|
||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
|
||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
|
||
if (!hasTemporalFilter) {
|
||
filters.extracted_filters = {
|
||
...filters.extracted_filters,
|
||
as_of_date: analysisDate
|
||
};
|
||
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
|
||
baseReasons.push("as_of_date_from_analysis_context");
|
||
}
|
||
}
|
||
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters);
|
||
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
|
||
requestedResultMode === "confirmed_balance";
|
||
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
||
const confirmedBalanceVatPayableIntent = intent.intent === "vat_payable_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
|
||
const payablesConfirmedExecution = confirmedBalancePayablesIntent
|
||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||
: null;
|
||
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
|
||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||
: null;
|
||
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
|
||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||
: null;
|
||
const executionFilters = payablesConfirmedExecution?.executionFilters ??
|
||
receivablesConfirmedExecution?.executionFilters ??
|
||
vatPayableConfirmedExecution?.executionFilters ??
|
||
filters.extracted_filters;
|
||
if (payablesConfirmedExecution?.asOfDerived &&
|
||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
|
||
filters.warnings.push("as_of_date_derived_for_confirmed_payables");
|
||
}
|
||
if (!baseReasons.includes("as_of_date_derived_for_confirmed_payables")) {
|
||
baseReasons.push("as_of_date_derived_for_confirmed_payables");
|
||
}
|
||
}
|
||
if (receivablesConfirmedExecution?.asOfDerived &&
|
||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
|
||
filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
|
||
}
|
||
if (!baseReasons.includes("as_of_date_derived_for_confirmed_receivables")) {
|
||
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
|
||
}
|
||
}
|
||
if (vatPayableConfirmedExecution?.asOfDerived &&
|
||
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
|
||
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
||
}
|
||
if (!baseReasons.includes("as_of_date_derived_for_confirmed_vat_payable")) {
|
||
baseReasons.push("as_of_date_derived_for_confirmed_vat_payable");
|
||
}
|
||
}
|
||
const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
|
||
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
||
const shadowRouteAudit = buildShadowRouteAudit({
|
||
intent: intent.intent,
|
||
requestedResultMode,
|
||
filters: executionFilters
|
||
});
|
||
if ((0, addressCapabilityPolicy_1.isCapabilityRouteBlocked)(capabilityDecision)) {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: executionFilters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: null,
|
||
mcpCallStatus: "skipped",
|
||
rowsFetched: 0,
|
||
rowsMatched: 0,
|
||
category: "unsupported",
|
||
reasonText: "маршрут capability временно отключен feature-флагом",
|
||
nextStep: "включите capability route или используйте соседний поддерживаемый сценарий",
|
||
limitations: ["capability_route_disabled_by_flag"],
|
||
reasons: [
|
||
...baseReasons,
|
||
config_1.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped"
|
||
],
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
const composeOptionsFromFilters = (filterSet, options = {}) => ({
|
||
userMessage,
|
||
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
|
||
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
|
||
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
|
||
requestedResultMode,
|
||
vatDirectSourceProbe: options.vatDirectSourceProbe ?? undefined,
|
||
emphasizeNumbers: options.emphasizeNumbers ?? undefined,
|
||
useRubCurrency: options.useRubCurrency ?? undefined
|
||
});
|
||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
||
const debtLifecycleReceivablesScenario = intent.intent === "list_receivables_counterparties" &&
|
||
Array.isArray(intent.reasons) &&
|
||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected");
|
||
const debtLifecyclePayablesScenario = intent.intent === "list_payables_counterparties" &&
|
||
Array.isArray(intent.reasons) &&
|
||
(intent.reasons.includes("payables_debt_lifecycle_signal_detected") ||
|
||
intent.reasons.includes("supplier_tail_risk_signal_detected") ||
|
||
intent.reasons.includes("payables_signal_detected"));
|
||
const preferConfirmedBalanceForPayablesLifecycle = debtLifecyclePayablesScenario && requestedResultMode === "confirmed_balance";
|
||
const recipeIntent = debtLifecycleReceivablesScenario
|
||
? "open_items_by_counterparty_or_contract"
|
||
: debtLifecyclePayablesScenario && !preferConfirmedBalanceForPayablesLifecycle
|
||
? "open_items_by_counterparty_or_contract"
|
||
: intent.intent;
|
||
const recipeSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, executionFilters);
|
||
if (debtLifecycleReceivablesScenario && recipeIntent !== intent.intent) {
|
||
baseReasons.push("recipe_override_to_open_items_for_receivables_debt_lifecycle");
|
||
}
|
||
if (debtLifecyclePayablesScenario && recipeIntent !== intent.intent) {
|
||
baseReasons.push("recipe_override_to_open_items_for_payables_debt_lifecycle");
|
||
}
|
||
if (preferConfirmedBalanceForPayablesLifecycle && !baseReasons.includes("confirmed_balance_attempt_for_payables_debt_lifecycle")) {
|
||
baseReasons.push("confirmed_balance_attempt_for_payables_debt_lifecycle");
|
||
}
|
||
if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) {
|
||
baseReasons.push("confirmed_balance_exact_payables_intent");
|
||
}
|
||
if (intent.intent === "receivables_confirmed_as_of_date" &&
|
||
!baseReasons.includes("confirmed_balance_exact_receivables_intent")) {
|
||
baseReasons.push("confirmed_balance_exact_receivables_intent");
|
||
}
|
||
if (intent.intent === "vat_payable_confirmed_as_of_date" &&
|
||
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")) {
|
||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
||
}
|
||
if (requestedResultMode === "confirmed_balance" &&
|
||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
||
baseReasons.push("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||
}
|
||
if (intent.intent === "unknown") {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: filters.missing_required_filters,
|
||
selectedRecipe: null,
|
||
anchor,
|
||
mcpCallStatus: "skipped",
|
||
rowsFetched: 0,
|
||
rowsMatched: 0,
|
||
category: "unsupported",
|
||
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
|
||
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
|
||
limitations: ["intent_not_supported_in_v1"],
|
||
reasons: baseReasons,
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
if (recipeSelection.selected_recipe === null) {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: recipeSelection.missing_required_filters,
|
||
selectedRecipe: null,
|
||
anchor,
|
||
mcpCallStatus: "skipped",
|
||
rowsFetched: 0,
|
||
rowsMatched: 0,
|
||
category: "recipe_visibility_gap",
|
||
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
|
||
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
|
||
limitations: ["recipe_not_available"],
|
||
reasons: [...baseReasons, ...recipeSelection.selection_reason],
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
if (recipeSelection.missing_required_filters.length > 0) {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: recipeSelection.missing_required_filters,
|
||
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
|
||
anchor,
|
||
mcpCallStatus: "skipped",
|
||
rowsFetched: 0,
|
||
rowsMatched: 0,
|
||
category: "missing_anchor",
|
||
reasonText: "не хватает обязательных фильтров",
|
||
nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`,
|
||
limitations: ["missing_required_filters"],
|
||
reasons: [...baseReasons, ...recipeSelection.selection_reason],
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
if (!config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1) {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
|
||
anchor,
|
||
mcpCallStatus: "skipped",
|
||
rowsFetched: 0,
|
||
rowsMatched: 0,
|
||
category: "execution_error",
|
||
reasonText: "live address lane выключен feature-флагом",
|
||
nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1",
|
||
limitations: ["address_live_lane_disabled"],
|
||
reasons: baseReasons,
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
const rawCounterpartyAnchor = typeof filters.extracted_filters.counterparty === "string" ? filters.extracted_filters.counterparty.trim() : "";
|
||
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
|
||
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
|
||
if (catalogResolution.resolvedValue) {
|
||
if (normalizeCounterpartyName(rawCounterpartyAnchor) !== normalizeCounterpartyName(catalogResolution.resolvedValue)) {
|
||
filters.warnings.push("counterparty_anchor_resolved_via_catalog_lookup");
|
||
}
|
||
}
|
||
else if (catalogResolution.tried) {
|
||
filters.warnings.push(catalogResolution.ambiguityCount > 0
|
||
? "counterparty_anchor_catalog_lookup_ambiguous"
|
||
: "counterparty_anchor_catalog_lookup_no_match");
|
||
}
|
||
anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
|
||
if (anchor.anchor_type === "counterparty") {
|
||
anchor = {
|
||
...anchor,
|
||
anchor_value_raw: rawCounterpartyAnchor || anchor.anchor_value_raw
|
||
};
|
||
if (catalogResolution.resolvedValue) {
|
||
anchor = {
|
||
...anchor,
|
||
anchor_value_resolved: catalogResolution.resolvedValue,
|
||
resolver_confidence: catalogResolution.confidence ?? anchor.resolver_confidence,
|
||
ambiguity_count: Math.max(anchor.ambiguity_count, catalogResolution.ambiguityCount)
|
||
};
|
||
}
|
||
else if (catalogResolution.ambiguityCount > 0) {
|
||
anchor = {
|
||
...anchor,
|
||
resolver_confidence: "low",
|
||
ambiguity_count: Math.max(anchor.ambiguity_count, catalogResolution.ambiguityCount)
|
||
};
|
||
}
|
||
}
|
||
}
|
||
let effectiveRecipeId = recipeSelection.selected_recipe.recipe_id;
|
||
let composeIntent = intent.intent;
|
||
let routeExpectationIntent = intent.intent;
|
||
let plan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(recipeSelection.selected_recipe, executionFilters), intent.intent);
|
||
let mcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: plan.query,
|
||
limit: plan.limit
|
||
});
|
||
const missingSubcontoFallbackEligible = plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||
plan.recipe.recipe_id === "address_movements_payables_v1" ||
|
||
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1" ||
|
||
plan.recipe.recipe_id === "address_receivables_confirmed_as_of_date_v1";
|
||
const missingSubcontoErrorDetected = Boolean(mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error));
|
||
if (missingSubcontoErrorDetected) {
|
||
let missingSubcontoResolvedByComposite = false;
|
||
const compositeSubcontoQuery = buildCompositeSubcontoFallbackQuery(plan.query);
|
||
if (compositeSubcontoQuery) {
|
||
const compositeMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: compositeSubcontoQuery,
|
||
limit: plan.limit
|
||
});
|
||
if (!compositeMcp.error) {
|
||
plan = {
|
||
...plan,
|
||
query: compositeSubcontoQuery
|
||
};
|
||
mcp = compositeMcp;
|
||
missingSubcontoResolvedByComposite = true;
|
||
if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto")) {
|
||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto");
|
||
}
|
||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
|
||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
|
||
}
|
||
}
|
||
else if (intent.intent === "receivables_confirmed_as_of_date") {
|
||
if (!baseReasons.includes("confirmed_receivables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
|
||
baseReasons.push("confirmed_receivables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
|
||
}
|
||
}
|
||
}
|
||
else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed")) {
|
||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
|
||
}
|
||
}
|
||
else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable")) {
|
||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable");
|
||
}
|
||
if (!missingSubcontoResolvedByComposite) {
|
||
const fallbackSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("open_items_by_counterparty_or_contract", executionFilters);
|
||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||
const fallbackPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(fallbackSelection.selected_recipe, executionFilters), intent.intent);
|
||
const fallbackMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: fallbackPlan.query,
|
||
limit: fallbackPlan.limit
|
||
});
|
||
if (!fallbackMcp.error) {
|
||
plan = fallbackPlan;
|
||
mcp = fallbackMcp;
|
||
effectiveRecipeId = fallbackSelection.selected_recipe.recipe_id;
|
||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||
}
|
||
if (!baseReasons.includes("fallback_recipe_switched_to_open_items")) {
|
||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||
}
|
||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||
composeIntent = "list_payables_counterparties";
|
||
routeExpectationIntent = "list_payables_counterparties";
|
||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) {
|
||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
|
||
}
|
||
}
|
||
else if (intent.intent === "receivables_confirmed_as_of_date") {
|
||
composeIntent = "list_receivables_counterparties";
|
||
routeExpectationIntent = "list_receivables_counterparties";
|
||
if (!baseReasons.includes("confirmed_receivables_exact_mode_missing_subconto_fallback_to_open_items")) {
|
||
baseReasons.push("confirmed_receivables_exact_mode_missing_subconto_fallback_to_open_items");
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (mcp.error &&
|
||
missingSubcontoFallbackEligible &&
|
||
isMissingSubcontoFieldError(mcp.error) &&
|
||
!baseReasons.includes("confirmed_exact_mode_missing_subconto_no_heuristic_fallback")) {
|
||
baseReasons.push("confirmed_exact_mode_missing_subconto_no_heuristic_fallback");
|
||
}
|
||
if (mcp.error) {
|
||
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: effectiveRecipeId,
|
||
accountScopeMode: plan.account_scope_mode,
|
||
anchor,
|
||
mcpCallStatus: deriveMcpStageStatus({
|
||
errored: true,
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsMaterialized: 0,
|
||
rowsAnchorMatched: 0,
|
||
rowsMatched: 0
|
||
}),
|
||
accountScopeAudit: errorScopeAudit,
|
||
rowsFetched: mcp.fetched_rows,
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsAfterAccountScope: mcp.rows.length,
|
||
rowsAfterRecipeFilter: 0,
|
||
rowsMaterialized: 0,
|
||
rowsMatched: mcp.matched_rows,
|
||
rawRowKeysSample: [],
|
||
materializationDropReason: "none",
|
||
category: "execution_error",
|
||
reasonText: "live MCP вызов завершился ошибкой",
|
||
nextStep: mcp.error,
|
||
limitations: ["mcp_call_failed"],
|
||
reasons: [...baseReasons, mcp.error],
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
const normalizedRawRows = toNormalizedRows(mcp.raw_rows);
|
||
const scopedRows = applyAccountScopeFilter(normalizedRawRows, plan.account_scope);
|
||
const accountScopeFallbackApplied = plan.account_scope_mode === "preferred" &&
|
||
plan.account_scope.length > 0 &&
|
||
normalizedRawRows.length > 0 &&
|
||
scopedRows.length === 0;
|
||
const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows;
|
||
anchor = (0, resolveStage_1.refineAnchorFromRows)(anchor, normalizedRows);
|
||
const filtersForMatching = anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved
|
||
? { ...executionFilters, counterparty: anchor.anchor_value_resolved }
|
||
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
|
||
? { ...executionFilters, contract: anchor.anchor_value_resolved }
|
||
: executionFilters;
|
||
const accountScopeAudit = buildAccountScopeAudit({
|
||
intent: intent.intent,
|
||
filters: filtersForMatching,
|
||
accountScope: plan.account_scope,
|
||
rowsBeforeScope: normalizedRawRows.length,
|
||
rowsAfterScope: normalizedRows.length
|
||
});
|
||
const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
|
||
const filterByAnchors = anchorFilter.rows;
|
||
const filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
|
||
const filteredRowsFutureGuard = applyFutureDatedRowsGuard(filteredRowsBeforeFutureGuard, intent.intent, futureGuardReferenceDate);
|
||
const filteredRows = filteredRowsFutureGuard.rows;
|
||
if (filteredRowsFutureGuard.droppedCount > 0) {
|
||
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
|
||
filters.warnings.push("future_rows_excluded_from_response");
|
||
}
|
||
if (!baseReasons.includes("future_rows_excluded_from_response")) {
|
||
baseReasons.push("future_rows_excluded_from_response");
|
||
}
|
||
}
|
||
const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length);
|
||
const stageStatus = deriveMcpStageStatus({
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsMaterialized: normalizedRows.length,
|
||
rowsAnchorMatched: filterByAnchors.length,
|
||
rowsMatched: filteredRows.length
|
||
});
|
||
const matchFailureStage = stageStatus === "materialized_but_not_anchor_matched"
|
||
? "materialized_but_not_anchor_matched"
|
||
: stageStatus === "materialized_but_filtered_out_by_recipe"
|
||
? "materialized_but_filtered_out_by_recipe"
|
||
: "none";
|
||
const matchFailureReason = matchFailureStage === "materialized_but_not_anchor_matched"
|
||
? anchorFilter.mismatchReason ?? "anchor_not_matched_after_materialization"
|
||
: matchFailureStage === "materialized_but_filtered_out_by_recipe"
|
||
? "rows_filtered_out_by_intent_recipe_after_anchor_match"
|
||
: null;
|
||
if (filteredRows.length === 0 && intent.intent === "list_documents_by_contract" && filterByAnchors.length > 0) {
|
||
const recoveredBankRows = applyIntentSpecificFilter("bank_operations_by_contract", filterByAnchors);
|
||
const recoveredRows = recoveredBankRows.length > 0 ? recoveredBankRows : filterByAnchors;
|
||
if (recoveredRows.length > 0) {
|
||
const factual = (0, composeStage_1.composeFactualReply)(intent.intent, recoveredRows, composeOptionsFromFilters(executionFilters));
|
||
const recoveryReason = recoveredBankRows.length > 0
|
||
? "contract_docs_recovered_via_bank_fallback"
|
||
: "contract_docs_recovered_via_anchor_rows";
|
||
const replyPrefix = recoveredBankRows.length > 0
|
||
? "Документный фильтр в live дал пустой набор; показываю связанные банковские операции по договору."
|
||
: "Документный фильтр в live дал пустой набор; показываю найденные строки по договорному якорю.";
|
||
return {
|
||
handled: true,
|
||
reply_text: `${replyPrefix}\n${factual.text}`,
|
||
reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
|
||
response_type: factual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: effectiveRecipeId,
|
||
mcp_call_status_legacy: toLegacyMcpStatus("matched_non_empty"),
|
||
account_scope_mode: plan.account_scope_mode,
|
||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||
anchor_type: anchor.anchor_type,
|
||
anchor_value_raw: anchor.anchor_value_raw,
|
||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||
resolver_confidence: anchor.resolver_confidence,
|
||
ambiguity_count: anchor.ambiguity_count,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: "matched_non_empty",
|
||
rows_fetched: mcp.fetched_rows,
|
||
raw_rows_received: mcp.raw_rows.length,
|
||
rows_after_account_scope: normalizedRows.length,
|
||
rows_after_recipe_filter: filterByAnchors.length,
|
||
rows_materialized: normalizedRows.length,
|
||
rows_matched: recoveredRows.length,
|
||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: factual.responseType,
|
||
...mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: intent.intent,
|
||
selectedRecipe: effectiveRecipeId,
|
||
filters: filters.extracted_filters,
|
||
responseType: factual.responseType,
|
||
rowsMatched: recoveredRows.length
|
||
}), factual.semantics),
|
||
limitations: [...filters.warnings, recoveryReason],
|
||
reasons: withConfirmedBalanceFallbackReason([...baseReasons, recoveryReason], requestedResultMode, factual.semantics)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
if (filteredRows.length === 0 &&
|
||
isAnchorRecoveryIntent(intent.intent) &&
|
||
(stageStatus === "materialized_but_not_anchor_matched" ||
|
||
stageStatus === "materialized_but_filtered_out_by_recipe" ||
|
||
stageStatus === "raw_rows_received_but_not_materialized")) {
|
||
const currentLimit = typeof executionFilters.limit === "number" && Number.isFinite(executionFilters.limit)
|
||
? Math.max(1, Math.trunc(executionFilters.limit))
|
||
: plan.limit;
|
||
if (currentLimit < ADDRESS_ANCHOR_RECOVERY_LIMIT) {
|
||
const expandedLimitFilters = {
|
||
...executionFilters,
|
||
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
|
||
};
|
||
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters);
|
||
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
|
||
const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters);
|
||
if (expandedPlan.limit > currentLimit) {
|
||
const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: expandedPlan.query,
|
||
limit: expandedPlan.limit
|
||
});
|
||
if (!expandedMcp.error) {
|
||
const expandedRawRows = toNormalizedRows(expandedMcp.raw_rows);
|
||
const expandedScopedRows = applyAccountScopeFilter(expandedRawRows, expandedPlan.account_scope);
|
||
const expandedAccountScopeFallbackApplied = expandedPlan.account_scope_mode === "preferred" &&
|
||
expandedPlan.account_scope.length > 0 &&
|
||
expandedRawRows.length > 0 &&
|
||
expandedScopedRows.length === 0;
|
||
const expandedNormalizedRows = expandedAccountScopeFallbackApplied ? expandedRawRows : expandedScopedRows;
|
||
let expandedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, expandedLimitFilters);
|
||
expandedAnchor = (0, resolveStage_1.refineAnchorFromRows)(expandedAnchor, expandedNormalizedRows);
|
||
const expandedFiltersForMatching = expandedAnchor.anchor_type === "counterparty" && expandedAnchor.anchor_value_resolved
|
||
? { ...expandedLimitFilters, counterparty: expandedAnchor.anchor_value_resolved }
|
||
: expandedAnchor.anchor_type === "contract" && expandedAnchor.anchor_value_resolved
|
||
? { ...expandedLimitFilters, contract: expandedAnchor.anchor_value_resolved }
|
||
: expandedLimitFilters;
|
||
const expandedAccountScopeAudit = buildAccountScopeAudit({
|
||
intent: intent.intent,
|
||
filters: expandedFiltersForMatching,
|
||
accountScope: expandedPlan.account_scope,
|
||
rowsBeforeScope: expandedRawRows.length,
|
||
rowsAfterScope: expandedNormalizedRows.length
|
||
});
|
||
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
|
||
const expandedRowsByAnchor = expandedAnchorFilter.rows;
|
||
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
|
||
const expandedFutureGuard = applyFutureDatedRowsGuard(expandedFilteredRowsBeforeFutureGuard, intent.intent, resolveFutureGuardReferenceDate(analysisDate, expandedLimitFilters));
|
||
const expandedFilteredRows = expandedFutureGuard.rows;
|
||
if (expandedFutureGuard.droppedCount > 0) {
|
||
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
|
||
filters.warnings.push("future_rows_excluded_from_response");
|
||
}
|
||
if (!baseReasons.includes("future_rows_excluded_from_response")) {
|
||
baseReasons.push("future_rows_excluded_from_response");
|
||
}
|
||
}
|
||
if (expandedFilteredRows.length > 0) {
|
||
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
|
||
const expandedStageStatus = deriveMcpStageStatus({
|
||
rawRowsReceived: expandedMcp.raw_rows.length,
|
||
rowsMaterialized: expandedNormalizedRows.length,
|
||
rowsAnchorMatched: expandedRowsByAnchor.length,
|
||
rowsMatched: expandedFilteredRows.length
|
||
});
|
||
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, composeOptionsFromFilters(expandedLimitFilters));
|
||
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
|
||
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
|
||
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
|
||
return {
|
||
handled: true,
|
||
reply_text: `${expandedPrefix}\n${expandedFactual.text}`,
|
||
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
|
||
response_type: expandedFactual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: expandedSelection.selected_recipe.recipe_id,
|
||
mcp_call_status_legacy: toLegacyMcpStatus(expandedStageStatus),
|
||
account_scope_mode: expandedPlan.account_scope_mode,
|
||
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
|
||
anchor_type: expandedAnchor.anchor_type,
|
||
anchor_value_raw: expandedAnchor.anchor_value_raw,
|
||
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
|
||
resolver_confidence: expandedAnchor.resolver_confidence,
|
||
ambiguity_count: expandedAnchor.ambiguity_count,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: expandedStageStatus,
|
||
rows_fetched: expandedMcp.fetched_rows,
|
||
raw_rows_received: expandedMcp.raw_rows.length,
|
||
rows_after_account_scope: expandedNormalizedRows.length,
|
||
rows_after_recipe_filter: expandedRowsByAnchor.length,
|
||
rows_materialized: expandedNormalizedRows.length,
|
||
rows_matched: expandedFilteredRows.length,
|
||
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
|
||
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: expandedFactual.responseType,
|
||
...mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: intent.intent,
|
||
selectedRecipe: expandedSelection.selected_recipe.recipe_id,
|
||
filters: filters.extracted_filters,
|
||
responseType: expandedFactual.responseType,
|
||
rowsMatched: expandedFilteredRows.length
|
||
}), expandedFactual.semantics),
|
||
limitations: expandedLimitations,
|
||
reasons: withConfirmedBalanceFallbackReason(expandedReasons, requestedResultMode, expandedFactual.semantics)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (filteredRows.length === 0 && canAutoBroadenPeriodWindow(intent.intent, filters.extracted_filters)) {
|
||
const autoBroadenedFilters = { ...filters.extracted_filters };
|
||
delete autoBroadenedFilters.period_from;
|
||
delete autoBroadenedFilters.period_to;
|
||
const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters);
|
||
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
|
||
const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters);
|
||
const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: broadenedPlan.query,
|
||
limit: broadenedPlan.limit
|
||
});
|
||
if (!broadenedMcp.error) {
|
||
const broadenedRawRows = toNormalizedRows(broadenedMcp.raw_rows);
|
||
const broadenedScopedRows = applyAccountScopeFilter(broadenedRawRows, broadenedPlan.account_scope);
|
||
const broadenedAccountScopeFallbackApplied = broadenedPlan.account_scope_mode === "preferred" &&
|
||
broadenedPlan.account_scope.length > 0 &&
|
||
broadenedRawRows.length > 0 &&
|
||
broadenedScopedRows.length === 0;
|
||
const broadenedNormalizedRows = broadenedAccountScopeFallbackApplied ? broadenedRawRows : broadenedScopedRows;
|
||
let broadenedAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, autoBroadenedFilters);
|
||
broadenedAnchor = (0, resolveStage_1.refineAnchorFromRows)(broadenedAnchor, broadenedNormalizedRows);
|
||
const broadenedFiltersForMatching = broadenedAnchor.anchor_type === "counterparty" && broadenedAnchor.anchor_value_resolved
|
||
? { ...autoBroadenedFilters, counterparty: broadenedAnchor.anchor_value_resolved }
|
||
: broadenedAnchor.anchor_type === "contract" && broadenedAnchor.anchor_value_resolved
|
||
? { ...autoBroadenedFilters, contract: broadenedAnchor.anchor_value_resolved }
|
||
: autoBroadenedFilters;
|
||
const broadenedAccountScopeAudit = buildAccountScopeAudit({
|
||
intent: intent.intent,
|
||
filters: broadenedFiltersForMatching,
|
||
accountScope: broadenedPlan.account_scope,
|
||
rowsBeforeScope: broadenedRawRows.length,
|
||
rowsAfterScope: broadenedNormalizedRows.length
|
||
});
|
||
const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching);
|
||
const broadenedRowsByAnchor = broadenedAnchorFilter.rows;
|
||
const broadenedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor);
|
||
const broadenedFutureGuard = applyFutureDatedRowsGuard(broadenedFilteredRowsBeforeFutureGuard, intent.intent, resolveFutureGuardReferenceDate(analysisDate, autoBroadenedFilters));
|
||
const broadenedFilteredRows = broadenedFutureGuard.rows;
|
||
if (broadenedFutureGuard.droppedCount > 0) {
|
||
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
|
||
filters.warnings.push("future_rows_excluded_from_response");
|
||
}
|
||
if (!baseReasons.includes("future_rows_excluded_from_response")) {
|
||
baseReasons.push("future_rows_excluded_from_response");
|
||
}
|
||
}
|
||
if (broadenedFilteredRows.length > 0) {
|
||
const broadenedRowDiagnostics = deriveRowStageDiagnostics(broadenedMcp.raw_rows, broadenedNormalizedRows.length, broadenedNormalizedRows.length);
|
||
const broadenedStageStatus = deriveMcpStageStatus({
|
||
rawRowsReceived: broadenedMcp.raw_rows.length,
|
||
rowsMaterialized: broadenedNormalizedRows.length,
|
||
rowsAnchorMatched: broadenedRowsByAnchor.length,
|
||
rowsMatched: broadenedFilteredRows.length
|
||
});
|
||
const observedWindow = deriveObservedPeriodWindow(broadenedFilteredRows);
|
||
const broadenedPrefix = composeAutoBroadenedPeriodPrefix(filters.extracted_filters, observedWindow);
|
||
const broadenedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, broadenedFilteredRows, composeOptionsFromFilters(autoBroadenedFilters));
|
||
const broadenedLimitations = [...filters.warnings, "period_window_auto_broadened_to_available_data"];
|
||
const broadenedReasons = [...baseReasons, "period_window_auto_broadened_to_available_data"];
|
||
return {
|
||
handled: true,
|
||
reply_text: `${broadenedPrefix}\n${broadenedFactual.text}`,
|
||
reply_type: (0, composeStage_1.inferReplyType)(broadenedFactual.responseType),
|
||
response_type: broadenedFactual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: broadenedSelection.selected_recipe.recipe_id,
|
||
mcp_call_status_legacy: toLegacyMcpStatus(broadenedStageStatus),
|
||
account_scope_mode: broadenedPlan.account_scope_mode,
|
||
account_scope_fallback_applied: broadenedAccountScopeFallbackApplied,
|
||
anchor_type: broadenedAnchor.anchor_type,
|
||
anchor_value_raw: broadenedAnchor.anchor_value_raw,
|
||
anchor_value_resolved: broadenedAnchor.anchor_value_resolved,
|
||
resolver_confidence: broadenedAnchor.resolver_confidence,
|
||
ambiguity_count: broadenedAnchor.ambiguity_count,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: broadenedStageStatus,
|
||
rows_fetched: broadenedMcp.fetched_rows,
|
||
raw_rows_received: broadenedMcp.raw_rows.length,
|
||
rows_after_account_scope: broadenedNormalizedRows.length,
|
||
rows_after_recipe_filter: broadenedRowsByAnchor.length,
|
||
rows_materialized: broadenedNormalizedRows.length,
|
||
rows_matched: broadenedFilteredRows.length,
|
||
raw_row_keys_sample: broadenedRowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: broadenedRowDiagnostics.materializationDropReason,
|
||
account_token_raw: broadenedAccountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: broadenedAccountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: broadenedAccountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: broadenedAccountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: broadenedAccountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: broadenedFactual.responseType,
|
||
...mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: intent.intent,
|
||
selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
|
||
filters: filters.extracted_filters,
|
||
responseType: broadenedFactual.responseType,
|
||
rowsMatched: broadenedFilteredRows.length
|
||
}), broadenedFactual.semantics),
|
||
limitations: broadenedLimitations,
|
||
reasons: withConfirmedBalanceFallbackReason(broadenedReasons, requestedResultMode, broadenedFactual.semantics)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (filteredRows.length === 0 &&
|
||
isDocumentOrBankAnchorIntent(intent.intent) &&
|
||
!hasExplicitPeriodWindow(filters.extracted_filters) &&
|
||
(anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract")) {
|
||
const currentLimit = typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit)
|
||
? Math.max(1, Math.trunc(filters.extracted_filters.limit))
|
||
: plan.limit;
|
||
const historicalFilters = {
|
||
...filters.extracted_filters,
|
||
sort: invertSort(filters.extracted_filters.sort),
|
||
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
|
||
};
|
||
const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, historicalFilters);
|
||
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
|
||
const historicalPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters);
|
||
const historicalMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: historicalPlan.query,
|
||
limit: historicalPlan.limit
|
||
});
|
||
if (!historicalMcp.error) {
|
||
const historicalRawRows = toNormalizedRows(historicalMcp.raw_rows);
|
||
const historicalScopedRows = applyAccountScopeFilter(historicalRawRows, historicalPlan.account_scope);
|
||
const historicalAccountScopeFallbackApplied = historicalPlan.account_scope_mode === "preferred" &&
|
||
historicalPlan.account_scope.length > 0 &&
|
||
historicalRawRows.length > 0 &&
|
||
historicalScopedRows.length === 0;
|
||
const historicalNormalizedRows = historicalAccountScopeFallbackApplied ? historicalRawRows : historicalScopedRows;
|
||
let historicalAnchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, historicalFilters);
|
||
historicalAnchor = (0, resolveStage_1.refineAnchorFromRows)(historicalAnchor, historicalNormalizedRows);
|
||
const historicalFiltersForMatching = historicalAnchor.anchor_type === "counterparty" && historicalAnchor.anchor_value_resolved
|
||
? { ...historicalFilters, counterparty: historicalAnchor.anchor_value_resolved }
|
||
: historicalAnchor.anchor_type === "contract" && historicalAnchor.anchor_value_resolved
|
||
? { ...historicalFilters, contract: historicalAnchor.anchor_value_resolved }
|
||
: historicalFilters;
|
||
const historicalAccountScopeAudit = buildAccountScopeAudit({
|
||
intent: intent.intent,
|
||
filters: historicalFiltersForMatching,
|
||
accountScope: historicalPlan.account_scope,
|
||
rowsBeforeScope: historicalRawRows.length,
|
||
rowsAfterScope: historicalNormalizedRows.length
|
||
});
|
||
const historicalAnchorFilter = applyAddressFilters(historicalNormalizedRows, historicalFiltersForMatching);
|
||
const historicalRowsByAnchor = historicalAnchorFilter.rows;
|
||
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor);
|
||
const historicalFutureGuard = applyFutureDatedRowsGuard(historicalFilteredRowsBeforeFutureGuard, intent.intent, resolveFutureGuardReferenceDate(analysisDate, historicalFilters));
|
||
const historicalFilteredRows = historicalFutureGuard.rows;
|
||
if (historicalFutureGuard.droppedCount > 0) {
|
||
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
|
||
filters.warnings.push("future_rows_excluded_from_response");
|
||
}
|
||
if (!baseReasons.includes("future_rows_excluded_from_response")) {
|
||
baseReasons.push("future_rows_excluded_from_response");
|
||
}
|
||
}
|
||
if (historicalFilteredRows.length > 0) {
|
||
const historicalRowDiagnostics = deriveRowStageDiagnostics(historicalMcp.raw_rows, historicalNormalizedRows.length, historicalNormalizedRows.length);
|
||
const historicalStageStatus = deriveMcpStageStatus({
|
||
rawRowsReceived: historicalMcp.raw_rows.length,
|
||
rowsMaterialized: historicalNormalizedRows.length,
|
||
rowsAnchorMatched: historicalRowsByAnchor.length,
|
||
rowsMatched: historicalFilteredRows.length
|
||
});
|
||
const historicalFactual = (0, composeStage_1.composeFactualReply)(intent.intent, historicalFilteredRows, composeOptionsFromFilters(historicalFilters));
|
||
const historicalPrefix = "Найдены данные в историческом срезе базы по вашему запросу.";
|
||
const historicalSuggestion = intent.intent === "list_documents_by_counterparty"
|
||
? "\nЕсли нужно, могу дополнительно показать платежи и договоры по этому контрагенту."
|
||
: "";
|
||
const historicalLimitations = [...filters.warnings, "historical_window_sort_recovery_applied"];
|
||
const historicalReasons = [...baseReasons, "historical_window_sort_recovery_applied"];
|
||
return {
|
||
handled: true,
|
||
reply_text: `${historicalPrefix}\n${historicalFactual.text}${historicalSuggestion}`,
|
||
reply_type: (0, composeStage_1.inferReplyType)(historicalFactual.responseType),
|
||
response_type: historicalFactual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: historicalSelection.selected_recipe.recipe_id,
|
||
mcp_call_status_legacy: toLegacyMcpStatus(historicalStageStatus),
|
||
account_scope_mode: historicalPlan.account_scope_mode,
|
||
account_scope_fallback_applied: historicalAccountScopeFallbackApplied,
|
||
anchor_type: historicalAnchor.anchor_type,
|
||
anchor_value_raw: historicalAnchor.anchor_value_raw,
|
||
anchor_value_resolved: historicalAnchor.anchor_value_resolved,
|
||
resolver_confidence: historicalAnchor.resolver_confidence,
|
||
ambiguity_count: historicalAnchor.ambiguity_count,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: historicalStageStatus,
|
||
rows_fetched: historicalMcp.fetched_rows,
|
||
raw_rows_received: historicalMcp.raw_rows.length,
|
||
rows_after_account_scope: historicalNormalizedRows.length,
|
||
rows_after_recipe_filter: historicalRowsByAnchor.length,
|
||
rows_materialized: historicalNormalizedRows.length,
|
||
rows_matched: historicalFilteredRows.length,
|
||
raw_row_keys_sample: historicalRowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: historicalRowDiagnostics.materializationDropReason,
|
||
account_token_raw: historicalAccountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: historicalAccountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: historicalAccountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: historicalAccountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: historicalAccountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: historicalFactual.responseType,
|
||
...mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: intent.intent,
|
||
selectedRecipe: historicalSelection.selected_recipe.recipe_id,
|
||
filters: filters.extracted_filters,
|
||
responseType: historicalFactual.responseType,
|
||
rowsMatched: historicalFilteredRows.length
|
||
}), historicalFactual.semantics),
|
||
limitations: historicalLimitations,
|
||
reasons: withConfirmedBalanceFallbackReason(historicalReasons, requestedResultMode, historicalFactual.semantics)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (filteredRows.length === 0 &&
|
||
isDocumentOrBankAnchorIntent(intent.intent) &&
|
||
normalizedRows.length > 0 &&
|
||
filterByAnchors.length > 0 &&
|
||
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) {
|
||
const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||
if (documentBankFallbackRows.length > 0) {
|
||
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows, composeOptionsFromFilters(executionFilters));
|
||
const fallbackPrefix = "По вашему запросу показываю найденные документы и операции в доступном срезе базы.";
|
||
const fallbackSuggestion = intent.intent === "list_documents_by_counterparty"
|
||
? "\nЕсли нужно, могу дополнительно сузить период или показать только платежи."
|
||
: "";
|
||
const fallbackLimitations = [...filters.warnings, "anchor_not_matched_fallback_rows"];
|
||
const fallbackReasons = [...baseReasons, "anchor_not_matched_fallback_rows"];
|
||
return {
|
||
handled: true,
|
||
reply_text: `${fallbackPrefix}\n${fallbackFactual.text}${fallbackSuggestion}`,
|
||
reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType),
|
||
response_type: fallbackFactual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: effectiveRecipeId,
|
||
mcp_call_status_legacy: "matched_non_empty",
|
||
account_scope_mode: plan.account_scope_mode,
|
||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||
anchor_type: anchor.anchor_type,
|
||
anchor_value_raw: anchor.anchor_value_raw,
|
||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||
resolver_confidence: anchor.resolver_confidence,
|
||
ambiguity_count: anchor.ambiguity_count,
|
||
match_failure_stage: matchFailureStage,
|
||
match_failure_reason: matchFailureReason,
|
||
mcp_call_status: "matched_non_empty",
|
||
rows_fetched: mcp.fetched_rows,
|
||
raw_rows_received: mcp.raw_rows.length,
|
||
rows_after_account_scope: normalizedRows.length,
|
||
rows_after_recipe_filter: filterByAnchors.length,
|
||
rows_materialized: normalizedRows.length,
|
||
rows_matched: documentBankFallbackRows.length,
|
||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: fallbackFactual.responseType,
|
||
...mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: intent.intent,
|
||
selectedRecipe: effectiveRecipeId,
|
||
filters: filters.extracted_filters,
|
||
responseType: fallbackFactual.responseType,
|
||
rowsMatched: documentBankFallbackRows.length
|
||
}), fallbackFactual.semantics),
|
||
limitations: fallbackLimitations,
|
||
reasons: withConfirmedBalanceFallbackReason(fallbackReasons, requestedResultMode, fallbackFactual.semantics)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
if (filteredRows.length === 0) {
|
||
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
||
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
||
const isVisibilityGapCandidate = hadBaseRows &&
|
||
hadAnchorMatchedRows &&
|
||
(intent.intent === "list_documents_by_counterparty" ||
|
||
intent.intent === "bank_operations_by_counterparty" ||
|
||
intent.intent === "list_documents_by_contract" ||
|
||
intent.intent === "bank_operations_by_contract");
|
||
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
|
||
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
|
||
const isFollowupAnchorCarryover = Array.isArray(filters.warnings) &&
|
||
(filters.warnings.includes("counterparty_from_followup_context") ||
|
||
filters.warnings.includes("contract_from_followup_context"));
|
||
const anchorMismatchByCounterparty = isAnchorMismatch && String(matchFailureReason ?? "").includes("counterparty_anchor_not_matched");
|
||
const anchorMismatchByContract = isAnchorMismatch && String(matchFailureReason ?? "").includes("contract_anchor_not_matched");
|
||
const isLowQualityPartyAnchor = (anchor.anchor_type === "counterparty" || anchor.anchor_type === "contract") &&
|
||
isLikelyLowQualityPartyAnchor(anchor.anchor_value_raw);
|
||
const requestedPeriodFrom = typeof filters.extracted_filters.period_from === "string" ? filters.extracted_filters.period_from : null;
|
||
const requestedPeriodTo = typeof filters.extracted_filters.period_to === "string" ? filters.extracted_filters.period_to : null;
|
||
const requestedPeriodHint = requestedPeriodFrom && requestedPeriodTo ? ` (период ${requestedPeriodFrom}..${requestedPeriodTo} сохранен)` : "";
|
||
const anchorMismatchCategory = isFollowupAnchorCarryover
|
||
? "empty_match"
|
||
: anchorMismatchByCounterparty || anchorMismatchByContract
|
||
? "missing_anchor"
|
||
: !isLowQualityPartyAnchor
|
||
? "empty_match"
|
||
: "missing_anchor";
|
||
const category = isAnchorMismatch
|
||
? anchorMismatchCategory
|
||
: isRecipeFilteredOut
|
||
? "recipe_visibility_gap"
|
||
: isVisibilityGapCandidate
|
||
? "recipe_visibility_gap"
|
||
: "empty_match";
|
||
const reasonText = isAnchorMismatch
|
||
? anchorMismatchByCounterparty
|
||
? "контрагент по указанному имени/алиасу не найден в materialized live-строках"
|
||
: anchorMismatchByContract
|
||
? "договор по указанному номеру/названию не найден в materialized live-строках"
|
||
: anchorMismatchCategory === "missing_anchor"
|
||
? "якорь контрагента/договора не найден в materialized live-строках"
|
||
: "по указанному якорю и фильтрам в live-выборке нет строк"
|
||
: isRecipeFilteredOut
|
||
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
|
||
: isVisibilityGapCandidate
|
||
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
|
||
: "по выбранным фильтрам в live-выборке нет строк";
|
||
const nextStep = isAnchorMismatch
|
||
? anchorMismatchByCounterparty
|
||
? `уточните точное имя контрагента или добавьте ИНН${requestedPeriodHint}`
|
||
: anchorMismatchByContract
|
||
? `уточните номер/наименование договора${requestedPeriodHint}`
|
||
: anchorMismatchCategory === "missing_anchor"
|
||
? "уточните контрагента точным именем или добавьте ИНН/договор"
|
||
: "уточните период или снимите часть фильтров"
|
||
: isRecipeFilteredOut
|
||
? "сузьте период, уточните контрагента или документный тип"
|
||
: isVisibilityGapCandidate
|
||
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
|
||
: "уточните период, контрагента, договор или снимите часть фильтров";
|
||
const limitations = isAnchorMismatch
|
||
? [
|
||
anchorMismatchByCounterparty
|
||
? "counterparty_anchor_not_matched_after_materialization"
|
||
: anchorMismatchByContract
|
||
? "contract_anchor_not_matched_after_materialization"
|
||
: anchorMismatchCategory === "missing_anchor"
|
||
? "anchor_not_matched_after_materialization"
|
||
: "no_rows_for_anchor_after_materialization"
|
||
]
|
||
: isRecipeFilteredOut
|
||
? ["rows_filtered_out_by_recipe_after_anchor_match"]
|
||
: [
|
||
isVisibilityGapCandidate
|
||
? "document_or_bank_visibility_gap_after_base_filter"
|
||
: "no_rows_after_recipe_and_scope_filter"
|
||
];
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: effectiveRecipeId,
|
||
accountScopeMode: plan.account_scope_mode,
|
||
accountScopeFallbackApplied,
|
||
accountScopeAudit,
|
||
anchor,
|
||
matchFailureStage,
|
||
matchFailureReason,
|
||
mcpCallStatus: stageStatus,
|
||
rowsFetched: mcp.fetched_rows,
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsAfterAccountScope: normalizedRows.length,
|
||
rowsAfterRecipeFilter: filterByAnchors.length,
|
||
rowsMaterialized: normalizedRows.length,
|
||
rowsMatched: 0,
|
||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||
category,
|
||
reasonText,
|
||
nextStep,
|
||
limitations,
|
||
reasons: baseReasons,
|
||
capabilityAudit,
|
||
shadowRouteAudit
|
||
});
|
||
}
|
||
const vatProbeRequired = composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
||
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
||
const shouldEmphasizeNumbers = composeIntent === "vat_payable_forecast" ||
|
||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||
composeIntent === "payables_confirmed_as_of_date" ||
|
||
composeIntent === "receivables_confirmed_as_of_date";
|
||
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast";
|
||
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters, {
|
||
vatDirectSourceProbe,
|
||
emphasizeNumbers: shouldEmphasizeNumbers,
|
||
useRubCurrency: shouldUseRubCurrency
|
||
}));
|
||
const vatProbeLimitations = vatProbeRequired && vatDirectSourceProbe
|
||
? vatDirectSourceProbe.status === "error"
|
||
? ["vat_source_probe_error"]
|
||
: vatDirectSourceProbe.status === "skipped"
|
||
? ["vat_source_probe_skipped"]
|
||
: vatDirectSourceProbe.errors.length > 0
|
||
? ["vat_source_probe_partial_errors"]
|
||
: []
|
||
: [];
|
||
const factualLimitations = [...filters.warnings, ...vatProbeLimitations];
|
||
const factualResultSemantics = mergeAddressResultSemantics(deriveAddressResultSemantics({
|
||
intent: composeIntent,
|
||
selectedRecipe: effectiveRecipeId,
|
||
filters: filters.extracted_filters,
|
||
responseType: factual.responseType,
|
||
rowsMatched: filteredRows.length
|
||
}), factual.semantics);
|
||
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||
intent: routeExpectationIntent,
|
||
selectedRecipe: effectiveRecipeId,
|
||
requestedResultMode,
|
||
resultMode: factualResultSemantics.result_mode
|
||
});
|
||
if (finalRouteExpectationAudit.status === "mismatch" && config_1.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1) {
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: effectiveRecipeId,
|
||
accountScopeMode: plan.account_scope_mode,
|
||
accountScopeFallbackApplied,
|
||
accountScopeAudit,
|
||
anchor,
|
||
matchFailureStage,
|
||
matchFailureReason,
|
||
mcpCallStatus: stageStatus,
|
||
rowsFetched: mcp.fetched_rows,
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsAfterAccountScope: normalizedRows.length,
|
||
rowsAfterRecipeFilter: filterByAnchors.length,
|
||
rowsMaterialized: normalizedRows.length,
|
||
rowsMatched: filteredRows.length,
|
||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||
category: "recipe_visibility_gap",
|
||
reasonText: "маршрут не прошел baseline route expectation contract",
|
||
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
|
||
limitations: ["route_expectation_mismatch_guard_blocked"],
|
||
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
|
||
capabilityAudit,
|
||
shadowRouteAudit,
|
||
routeExpectationAudit: finalRouteExpectationAudit
|
||
});
|
||
}
|
||
const exactConfirmedIntent = (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
||
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date") ||
|
||
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date");
|
||
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
||
const exactModeName = intent.intent === "payables_confirmed_as_of_date"
|
||
? "payables"
|
||
: intent.intent === "receivables_confirmed_as_of_date"
|
||
? "receivables"
|
||
: "vat_payable";
|
||
return buildLimitedExecutionResult({
|
||
mode,
|
||
shape,
|
||
intent,
|
||
filters: filters.extracted_filters,
|
||
missingRequiredFilters: [],
|
||
selectedRecipe: effectiveRecipeId,
|
||
accountScopeMode: plan.account_scope_mode,
|
||
accountScopeFallbackApplied,
|
||
accountScopeAudit,
|
||
anchor,
|
||
matchFailureStage,
|
||
matchFailureReason,
|
||
mcpCallStatus: stageStatus,
|
||
rowsFetched: mcp.fetched_rows,
|
||
rawRowsReceived: mcp.raw_rows.length,
|
||
rowsAfterAccountScope: normalizedRows.length,
|
||
rowsAfterRecipeFilter: filterByAnchors.length,
|
||
rowsMaterialized: normalizedRows.length,
|
||
rowsMatched: filteredRows.length,
|
||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||
category: "recipe_visibility_gap",
|
||
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
|
||
nextStep: intent.intent === "vat_payable_confirmed_as_of_date"
|
||
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
||
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||
capabilityAudit,
|
||
shadowRouteAudit,
|
||
routeExpectationAudit: finalRouteExpectationAudit
|
||
});
|
||
}
|
||
const reasonsWithRouteExpectation = finalRouteExpectationAudit.status === "mismatch"
|
||
? [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`]
|
||
: baseReasons;
|
||
return {
|
||
handled: true,
|
||
reply_text: factual.text,
|
||
reply_type: (0, composeStage_1.inferReplyType)(factual.responseType),
|
||
response_type: factual.responseType,
|
||
debug: {
|
||
detected_mode: mode.mode,
|
||
detected_mode_confidence: mode.confidence,
|
||
query_shape: shape.shape,
|
||
query_shape_confidence: shape.confidence,
|
||
detected_intent: intent.intent,
|
||
detected_intent_confidence: intent.confidence,
|
||
extracted_filters: filters.extracted_filters,
|
||
missing_required_filters: [],
|
||
selected_recipe: effectiveRecipeId,
|
||
mcp_call_status_legacy: toLegacyMcpStatus(stageStatus),
|
||
account_scope_mode: plan.account_scope_mode,
|
||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||
anchor_type: anchor.anchor_type,
|
||
anchor_value_raw: anchor.anchor_value_raw,
|
||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||
resolver_confidence: anchor.resolver_confidence,
|
||
ambiguity_count: anchor.ambiguity_count,
|
||
match_failure_stage: "none",
|
||
match_failure_reason: null,
|
||
mcp_call_status: stageStatus,
|
||
rows_fetched: mcp.fetched_rows,
|
||
raw_rows_received: mcp.raw_rows.length,
|
||
rows_after_account_scope: normalizedRows.length,
|
||
rows_after_recipe_filter: filterByAnchors.length,
|
||
rows_materialized: normalizedRows.length,
|
||
rows_matched: filteredRows.length,
|
||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||
limited_reason_category: null,
|
||
response_type: factual.responseType,
|
||
capability_id: capabilityAudit.capabilityId,
|
||
capability_layer: capabilityAudit.layer,
|
||
capability_route_mode: capabilityAudit.routeMode,
|
||
capability_route_enabled: capabilityAudit.enabled,
|
||
capability_route_reason: capabilityAudit.reason,
|
||
shadow_route_intent: shadowRouteAudit.intent,
|
||
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
|
||
shadow_route_status: shadowRouteAudit.status,
|
||
route_expectation_status: finalRouteExpectationAudit.status,
|
||
route_expectation_reason: finalRouteExpectationAudit.reason,
|
||
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
|
||
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
||
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
||
...factualResultSemantics,
|
||
limitations: factualLimitations,
|
||
reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)
|
||
}
|
||
};
|
||
}
|
||
}
|
||
exports.AddressQueryService = AddressQueryService;
|