NODEDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts

4405 lines
182 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1,
FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1,
FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1
} from "../config";
import type {
AddressCapabilityLayer,
AddressCapabilityRouteMode,
AddressShadowRouteStatus,
AddressAsOfDateBasis,
AddressEvidenceStrength,
AddressExecutionResult,
AddressFilterSet,
AddressIntent,
AddressLlmSemanticHints,
AddressLimitedReasonCategory,
AddressMatchFailureStage,
AddressMcpCallStatus,
AddressQueryShapeDetection,
AddressResultMode,
AddressResponseType,
AddressRuntimeReadiness,
AddressSemanticFrame
} from "../types/addressQuery";
import {
buildAddressRecipePlan,
selectAddressRecipe,
type AddressRecipeExecutionPlan
} from "./addressRecipeCatalog";
import {
executeAddressMcpMetadata,
executeAddressMcpQuery,
type AddressMcpMetadataRowsResult
} from "./addressMcpClient";
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
import {
composeFactualReply,
inferReplyType,
type ComposeReplySemantics,
type VatDirectSourceProbeItem,
type VatDirectSourceProbeSummary
} from "./address_runtime/composeStage";
import {
isCapabilityRouteBlocked,
resolveAddressCapabilityRouteDecision,
resolveShadowRouteIntent
} from "./addressCapabilityPolicy";
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
import {
mergeKnownOrganizations,
normalizeOrganizationScopeSearchText,
normalizeOrganizationScopeValue,
resolveOrganizationSelectionFromMessage
} from "./assistantOrganizationMatcher";
interface NormalizedAddressRow {
period: string | null;
registrator: string;
account_dt: string | null;
account_kt: string | null;
amount: number | null;
analytics: string[];
quantity?: number | null;
item?: string | null;
warehouse?: string | null;
organization?: string | null;
}
interface AddressTryHandleOptions {
followupContext?: AddressFollowupContext | null;
analysisDateHint?: string | null;
llmSemanticHints?: AddressLlmSemanticHints | null;
activeOrganization?: string | null;
knownOrganizations?: string[];
}
interface AddressCapabilityAudit {
capabilityId: string;
layer: AddressCapabilityLayer;
routeMode: AddressCapabilityRouteMode;
enabled: boolean;
reason: string;
}
interface AddressShadowRouteAudit {
intent: AddressIntent | null;
selectedRecipe: string | null;
status: AddressShadowRouteStatus;
}
interface AddressRouteExpectationAuditState {
status: AddressRouteExpectationAudit["status"];
reason: string;
expectedSelectedRecipes: string[];
expectedRequestedResultModes: AddressResultMode[];
expectedResultModes: AddressResultMode[];
}
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const;
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 = 4;
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления"] as const;
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур"] as const;
const VAT_METADATA_PROBE_CONCURRENCY = 2;
const VAT_METADATA_PROBE_STAGGER_MS = 90;
const VAT_METADATA_PROBE_TIMEOUT_MS = 1_200;
const VAT_METADATA_PROBE_RETRY_TIMEOUT_MS = 1_800;
const VAT_METADATA_PROBE_RETRY_DELAY_MS = 140;
const VAT_OBJECT_PROBE_CONCURRENCY = 1;
const VAT_OBJECT_PROBE_STAGGER_MS = 120;
const VAT_OBJECT_PROBE_TIMEOUT_MS = 1_500;
const VAT_OBJECT_PROBE_ABORT_RETRY_TIMEOUT_MS = 2_200;
const VAT_OBJECT_PROBE_ABORT_RETRY_DELAY_MS = 180;
const VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS = 1_500;
const PARTY_ANCHOR_STOPWORDS = new Set([
"ооо",
"ао",
"зао",
"ип",
"llc",
"ltd",
"company",
"компания",
"контрагент",
"counterparty",
"по",
"by"
]);
const LOW_QUALITY_PARTY_ANCHOR_TOKENS = new Set([
"что",
"чо",
"были",
"был",
"была",
"было",
"ли",
"какие",
"какой",
"покажи",
"показать",
"выведи",
"списания",
"списание",
"поступления",
"поступление",
"доки",
"документ",
"документы",
"документов",
"банковские",
"операции",
"платежи",
"платеж",
"платежи",
"плс",
"please"
]);
const ACCOUNT_ALIAS_MAP: Record<string, string[]> = {
"51": ["расчетный счет", "расчетные счета", "bank account"],
"52": ["валютный счет", "валютные счета", "currency account"],
"60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"],
"62": ["покупатель", "покупателями", "расчеты с покупателями"],
"76": ["прочие расчеты", "прочими дебиторами и кредиторами"]
};
interface VatMetadataObject {
fullName: string;
synonym: string | null;
objectType: "document" | "register";
}
const VAT_FALLBACK_METADATA_OBJECTS: VatMetadataObject[] = [
{
fullName: "РегистрНакопления.НДСЗаписиКнигиПродаж",
synonym: "НДС Продажи",
objectType: "register"
},
{
fullName: "РегистрНакопления.НДСЗаписиКнигиПокупок",
synonym: "НДС Покупки",
objectType: "register"
},
{
fullName: "РегистрНакопления.НДСПредъявленный",
synonym: "НДС предъявленный",
objectType: "register"
},
{
fullName: "РегистрНакопления.НДСВключенныйВСтоимость",
synonym: "НДС, включенный в стоимость",
objectType: "register"
}
];
const COUNTERPARTY_CATALOG_LOOKUP_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Регистратор,
"" КАК СчетДт,
"" КАК СчетКт,
0 КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Контрагенты.Ссылка) КАК Контрагент
ИЗ
Справочник.Контрагенты КАК Контрагенты
`;
interface CounterpartyCatalogResolution {
tried: boolean;
resolvedValue: string | null;
confidence: "high" | "medium" | "low" | null;
ambiguityCount: number;
}
let counterpartyCatalogCache: { names: string[]; loadedAt: number } | null = null;
function parseFiniteNumber(value: unknown): number | null {
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: unknown): string | null {
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: unknown): string {
if (value === null || value === undefined) {
return "";
}
return String(value);
}
function normalizeIsoDateForQuery(value: unknown): string | null {
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: string): string | null {
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: string): boolean {
const text = String(userMessage ?? "")
.toLowerCase()
.replace(/ё/g, "е");
if (!text.trim()) {
return false;
}
return /(?:в\s+налогов|почему|из\s+чего|источн|декларац|книга\s+продаж|книга\s+покупок|вычет|восстанов)/iu.test(text);
}
function detectVatMetadataObjectType(fullName: string): VatMetadataObject["objectType"] | null {
const normalized = String(fullName ?? "").trim();
if (!normalized) {
return null;
}
if (normalized.startsWith("Документ.")) {
return "document";
}
if (normalized.startsWith("РегистрНакопления.") || normalized.startsWith("РегистрСведений.")) {
return "register";
}
return null;
}
function firstNonEmptyString(...values: unknown[]): string | null {
for (const value of values) {
const normalized = valueAsString(value).trim();
if (normalized) {
return normalized;
}
}
return null;
}
function firstFiniteNumber(...values: unknown[]): number | null {
for (const value of values) {
const parsed = parseFiniteNumber(value);
if (parsed !== null) {
return parsed;
}
}
return null;
}
function extractVatMetadataObjects(rows: Array<Record<string, unknown>>): VatMetadataObject[] {
const out: VatMetadataObject[] = [];
const seen = new Set<string>();
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 isVatMetadataObject(item: VatMetadataObject): boolean {
const source = `${item.fullName} ${item.synonym ?? ""}`.toLowerCase().replace(/ё/g, "е");
if (source.includes("ндфл")) {
return false;
}
return /(?:ндс|книгапокуп|книгапродаж|счет[\s-]?фактур)/iu.test(source);
}
function isAbortErrorMessage(error: string | null | undefined): boolean {
const normalized = String(error ?? "").toLowerCase();
if (!normalized) {
return false;
}
return normalized.includes("aborted") || normalized.includes("abort");
}
async function mapWithConcurrency<T, R>(
items: T[],
concurrency: number,
worker: (item: T, index: number) => Promise<R>
): Promise<R[]> {
if (items.length === 0) {
return [];
}
const boundedConcurrency = Math.max(1, Math.min(Math.trunc(concurrency), items.length));
const results = new Array<R>(items.length);
let nextIndex = 0;
const runners = Array.from({ length: boundedConcurrency }, async () => {
while (true) {
const currentIndex = nextIndex;
nextIndex += 1;
if (currentIndex >= items.length) {
break;
}
results[currentIndex] = await worker(items[currentIndex], currentIndex);
}
});
await Promise.all(runners);
return results;
}
function sleepMs(ms: number): Promise<void> {
const delayMs = Number.isFinite(ms) ? Math.max(0, Math.trunc(ms)) : 0;
if (delayMs <= 0) {
return Promise.resolve();
}
return new Promise((resolve) => setTimeout(resolve, delayMs));
}
async function executeVatMetadataProbeRequest(request: {
meta_type: string;
name_mask: string;
limit: number;
}): Promise<AddressMcpMetadataRowsResult> {
const firstAttempt = await executeAddressMcpMetadata({
...request,
timeout_ms: VAT_METADATA_PROBE_TIMEOUT_MS
});
if (!firstAttempt.error || !isAbortErrorMessage(firstAttempt.error)) {
return firstAttempt;
}
await sleepMs(VAT_METADATA_PROBE_RETRY_DELAY_MS);
const retryLimit = Math.max(20, Math.min(request.limit, Math.trunc(request.limit / 2)));
const retryAttempt = await executeAddressMcpMetadata({
...request,
limit: retryLimit,
timeout_ms: VAT_METADATA_PROBE_RETRY_TIMEOUT_MS
});
if (!retryAttempt.error) {
return retryAttempt;
}
return {
...retryAttempt,
error: `${firstAttempt.error}; retry: ${retryAttempt.error}`
};
}
function scoreVatMetadataObject(item: VatMetadataObject): number {
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 vatMetadataObjectPriority(item: VatMetadataObject): number {
const idx = VAT_FALLBACK_METADATA_OBJECTS.findIndex((fallback) => fallback.fullName === item.fullName);
return idx >= 0 ? idx : VAT_FALLBACK_METADATA_OBJECTS.length + 1;
}
type VatObjectProbeMode = "latest" | "exists";
function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string, mode: VatObjectProbeMode = "latest"): string {
const orderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Движения.Период УБЫВ" : "";
if (object.objectType === "document") {
const documentOrderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Док.Дата УБЫВ" : "";
return `
ВЫБРАТЬ ПЕРВЫЕ 1
Док.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Док.Ссылка) КАК Регистратор,
"" КАК СчетДт,
"" КАК СчетКт,
0 КАК Сумма
ИЗ
${object.fullName} КАК Док
ГДЕ
Док.Дата <= ${asOfExpr}
${documentOrderClause}
`.trim().replace(/\n{3,}/g, "\n\n");
}
return `
ВЫБРАТЬ ПЕРВЫЕ 1
Движения.Период КАК Период,
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
"" КАК СчетДт,
"" КАК СчетКт,
0 КАК Сумма
ИЗ
${object.fullName} КАК Движения
ГДЕ
Движения.Период <= ${asOfExpr}
${orderClause}
`.trim().replace(/\n{3,}/g, "\n\n");
}
async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDirectSourceProbeSummary> {
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: Array<{ meta_type: string; name_mask: string; limit: number }> = 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 mapWithConcurrency(
metadataRequests,
VAT_METADATA_PROBE_CONCURRENCY,
async (request, index) => {
if (index > 0 && VAT_METADATA_PROBE_STAGGER_MS > 0) {
await sleepMs(index * VAT_METADATA_PROBE_STAGGER_MS);
}
return executeVatMetadataProbeRequest(request);
}
);
const metadataOutcomes = metadataResponses.map((response, index) => ({
request: metadataRequests[index],
response
}));
const successfulMetadataByType = new Map<string, number>();
const metadataErrors: string[] = [];
const metadataObjectsBuffer: VatMetadataObject[] = [];
for (const { request, response } of metadataOutcomes) {
if (response.error) {
continue;
}
const currentSuccessCount = successfulMetadataByType.get(request.meta_type) ?? 0;
successfulMetadataByType.set(request.meta_type, currentSuccessCount + 1);
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
}
for (const { request, response } of metadataOutcomes) {
if (response.error) {
if (isAbortErrorMessage(response.error) && (successfulMetadataByType.get(request.meta_type) ?? 0) > 0) {
continue;
}
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
}
}
const deduplicatedObjects = new Map<string, VatMetadataObject>();
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())
.filter((item) => isVatMetadataObject(item))
.sort(
(a, b) =>
vatMetadataObjectPriority(a) - vatMetadataObjectPriority(b) ||
scoreVatMetadataObject(b) - scoreVatMetadataObject(a) ||
a.fullName.localeCompare(b.fullName, "ru")
);
const mergedMetadataObjectsMap = new Map<string, VatMetadataObject>();
for (const item of discoveredMetadataObjects) {
mergedMetadataObjectsMap.set(item.fullName, item);
}
for (const fallbackObject of VAT_FALLBACK_METADATA_OBJECTS) {
if (!mergedMetadataObjectsMap.has(fallbackObject.fullName)) {
mergedMetadataObjectsMap.set(fallbackObject.fullName, fallbackObject);
}
}
const metadataObjects = Array.from(mergedMetadataObjectsMap.values())
.sort(
(a, b) =>
vatMetadataObjectPriority(a) - vatMetadataObjectPriority(b) ||
scoreVatMetadataObject(b) - scoreVatMetadataObject(a) ||
a.fullName.localeCompare(b.fullName, "ru")
)
.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
const probeRows = await mapWithConcurrency(
metadataObjects,
VAT_OBJECT_PROBE_CONCURRENCY,
async (object, index): Promise<VatDirectSourceProbeItem> => {
if (index > 0 && VAT_OBJECT_PROBE_STAGGER_MS > 0) {
await sleepMs(index * VAT_OBJECT_PROBE_STAGGER_MS);
}
let probeResult = await executeAddressMcpQuery({
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
limit: 1,
timeout_ms: VAT_OBJECT_PROBE_TIMEOUT_MS
});
let fallbackUsed = false;
if (probeResult.error) {
let latestError: string | null = probeResult.error;
if (isAbortErrorMessage(probeResult.error)) {
await sleepMs(VAT_OBJECT_PROBE_ABORT_RETRY_DELAY_MS);
const retryLatestResult = await executeAddressMcpQuery({
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
limit: 1,
timeout_ms: VAT_OBJECT_PROBE_ABORT_RETRY_TIMEOUT_MS
});
if (!retryLatestResult.error) {
probeResult = retryLatestResult;
latestError = null;
} else {
probeResult = retryLatestResult;
latestError = `${latestError}; retry_latest: ${retryLatestResult.error}`;
}
}
if (probeResult.error) {
const fallbackResult = await executeAddressMcpQuery({
query: buildVatObjectProbeQuery(object, asOfExpr, "exists"),
limit: 1,
timeout_ms: VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS
});
if (!fallbackResult.error) {
probeResult = fallbackResult;
fallbackUsed = true;
} else {
return {
fullName: object.fullName,
synonym: object.synonym,
objectType: object.objectType,
status: "error",
rowsFetched: probeResult.fetched_rows,
error: `${latestError ?? probeResult.error}; fallback: ${fallbackResult.error}`
};
}
}
}
const firstRow = probeResult.raw_rows[0] ?? null;
const lastPeriod =
firstRow !== null
? valueAsString(
(firstRow as Record<string, unknown>).Период ?? (firstRow as Record<string, unknown>).period
).trim() || null
: null;
const sampleRegistrator =
firstRow !== null
? valueAsString(
(firstRow as Record<string, unknown>).Регистратор ??
(firstRow as Record<string, unknown>).registrator ??
(firstRow as Record<string, unknown>).Registrator
).trim() || null
: null;
return {
fullName: object.fullName,
synonym: object.synonym,
objectType: object.objectType,
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
rowsFetched: probeResult.fetched_rows,
lastPeriod: fallbackUsed ? null : lastPeriod,
sampleRegistrator
};
}
);
const hasProbeAttempts = probeRows.length > 0;
const hasNonErrorProbeResult = probeRows.some((item) => item.status !== "error");
const status: VatDirectSourceProbeSummary["status"] =
hasProbeAttempts && hasNonErrorProbeResult
? "ok"
: metadataResponses.every((item) => item.error) && !hasProbeAttempts
? "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: string): string {
const map: Record<string, string> = {
а: "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: string): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9]+/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
function tokenizeAnchor(value: string): string[] {
return normalizeSearchText(value)
.split(" ")
.map((token) => token.trim())
.filter((token) => token.length >= 2 && !PARTY_ANCHOR_STOPWORDS.has(token));
}
function tokenizeSearchableText(value: string): string[] {
return normalizeSearchText(value)
.split(" ")
.map((token) => token.trim())
.filter(Boolean);
}
function anchorTokenVariants(token: string): string[] {
const source = String(token ?? "").trim().toLowerCase();
if (!source) {
return [];
}
const variants = new Set<string>([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: string, anchor: string): boolean {
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 normalizeInventoryItemAnchorSignature(value: string): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[\s,.;:!?()\[\]{}'"`«»]/g, "")
.replace(/(?:[xх×*\/._-]+)/giu, "x");
}
function matchesRelaxedInventoryItemAnchorText(searchable: string, anchor: string): boolean {
if (matchesAnchorText(searchable, anchor)) {
return true;
}
const searchableNormalized = normalizeSearchText(searchable);
const searchableLatin = transliterateCyrillicToLatin(searchableNormalized);
const searchableTokens = tokenizeSearchableText(searchable);
const anchorSignature = normalizeInventoryItemAnchorSignature(anchor);
const searchableSignature = normalizeInventoryItemAnchorSignature(searchable);
if (anchorSignature && searchableSignature.includes(anchorSignature)) {
return true;
}
const relaxedTokens = tokenizeAnchor(anchor).filter((token) => {
if (/^\d+$/u.test(token)) {
return false;
}
return !/^\d+(?:[xх×*\/._-]\d+)+$/iu.test(token);
});
if (relaxedTokens.length === 0) {
return false;
}
return relaxedTokens.every((token) => {
const variants = anchorTokenVariants(token);
return variants.some((variant) => {
const tokenLatin = transliterateCyrillicToLatin(variant);
if (searchableNormalized.includes(variant) || searchableLatin.includes(tokenLatin)) {
return true;
}
return searchableTokens.some((candidate) => {
const candidateLatin = transliterateCyrillicToLatin(candidate);
return (
candidate.startsWith(variant) ||
variant.startsWith(candidate) ||
candidateLatin.startsWith(tokenLatin) ||
tokenLatin.startsWith(candidateLatin)
);
});
});
});
}
function isLikelyLowQualityPartyAnchor(value: string | null | undefined): boolean {
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: string): string {
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: string): string[] {
const result: string[] = [];
const matcher = /\b(\d{2})(?:[.,](\d{1,2}))?\b/g;
let hit: RegExpExecArray | null = 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: string, candidateToken: string): boolean {
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: string): string | null {
const normalized = normalizeAccountToken(value);
const match = normalized.match(/^(\d{2})/);
return match ? match[1] : null;
}
function uniqueStrings(values: string[]): string[] {
return Array.from(
new Set(
values
.map((item) => item.trim())
.filter((item) => item.length > 0)
)
);
}
function normalizeCounterpartyName(value: string): string {
return normalizeSearchText(String(value ?? ""))
.replace(/\s+/g, " ")
.trim();
}
function extractCounterpartyCatalogNames(rows: Array<Record<string, unknown>>): string[] {
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: string, anchor: string): number | null {
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: AddressIntent, filters: AddressFilterSet): boolean {
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 === "open_contracts_confirmed_as_of_date" ||
intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_receivables_counterparties"
);
}
async function resolveCounterpartyViaCatalog(anchorRaw: string): Promise<CounterpartyCatalogResolution> {
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: string[] = cacheFresh ? [...counterpartyCatalogCache!.names] : [];
if (!cacheFresh) {
const mcp = await 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): item is { name: string; score: number } => 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: Record<string, unknown>): string[] {
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",
"Договор",
"Item",
"item",
"Номенклатура",
"НоменклатураПредставление",
"Warehouse",
"warehouse",
"Склад",
"СкладПредставление",
"Quantity",
"quantity",
"Количество",
"Organization",
"Организация",
"ОрганизацияПредставление",
"organization",
"organization_name"
];
const collected: string[] = [];
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("warehouse") ||
lowerKey.includes("склад") ||
lowerKey.includes("item") ||
lowerKey.includes("номенклат") ||
lowerKey.includes("organization") ||
lowerKey.includes("организац")
) {
const value = valueAsString(rawValue).trim();
if (value) {
collected.push(value);
}
}
}
return uniqueStrings(collected);
}
function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddressRow[] {
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 quantity = firstFiniteNumber(row.Количество, row.quantity, row.Quantity);
const item = firstNonEmptyString(
row.Номенклатура,
row.Item,
row.item,
row.НоменклатураПредставление,
row.SubcontoDt1,
row.SubcontoDt2,
row.SubcontoDt3,
row.SubcontoKt1,
row.SubcontoKt2,
row.SubcontoKt3,
row.СубконтоДт1,
row.СубконтоДт2,
row.СубконтоДт3,
row.СубконтоКт1,
row.СубконтоКт2,
row.СубконтоКт3
);
const warehouse = firstNonEmptyString(row.Склад, row.Warehouse, row.warehouse, row.СкладПредставление);
const organization = firstNonEmptyString(
row.Организация,
row.Organization,
row.organization,
row.organization_name,
row.ОрганизацияПредставление
);
const analytics = collectAnalyticsStrings(row);
return {
period,
registrator,
account_dt: accountDt,
account_kt: accountKt,
amount,
analytics,
quantity,
item,
warehouse,
organization
};
})
.filter((item) => Boolean(item.period || item.registrator));
}
function rowSearchableText(row: NormalizedAddressRow): string {
return [row.registrator, row.item ?? "", row.warehouse ?? "", row.account_dt ?? "", row.account_kt ?? "", ...row.analytics]
.join(" ")
.toLowerCase();
}
function rowMatchesAnyAccount(row: NormalizedAddressRow, accountScope: string[]): boolean {
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: NormalizedAddressRow[], accountScope: string[]): NormalizedAddressRow[] {
if (accountScope.length === 0) {
return rows;
}
return rows.filter((row) => rowMatchesAnyAccount(row, accountScope));
}
interface AnchorFilterResult {
rows: NormalizedAddressRow[];
mismatchReason: string | null;
}
function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilterSet): AnchorFilterResult {
let filtered = [...rows];
let mismatchReason: string | null = 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.item && String(filters.item).trim()) {
const needle = String(filters.item);
const before = filtered.length;
filtered = filtered.filter((row) => matchesRelaxedInventoryItemAnchorText(rowSearchableText(row), needle));
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
mismatchReason = "item_anchor_not_matched_in_materialized_rows";
}
}
if (filters.warehouse && String(filters.warehouse).trim()) {
const needle = String(filters.warehouse);
const before = filtered.length;
filtered = filtered.filter((row) => matchesAnchorText(rowSearchableText(row), needle));
if (before > 0 && filtered.length === 0 && mismatchReason === null) {
mismatchReason = "warehouse_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: AddressIntent, rows: NormalizedAddressRow[]): NormalizedAddressRow[] {
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: string | null | undefined): number | null {
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: AddressIntent): boolean {
return (
intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract"
);
}
function sameNormalizedOrganizationScope(left: string | null | undefined, right: string | null | undefined): boolean {
return normalizeOrganizationScopeSearchText(left ?? "") === normalizeOrganizationScopeSearchText(right ?? "");
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
semanticFrame: AddressSemanticFrame | null;
warnings: string[];
baseReasons: string[];
activeOrganization?: string | null;
knownOrganizations?: string[];
}): string | null {
const activeOrganization = normalizeOrganizationScopeValue(input.activeOrganization ?? null);
const candidateOrganizations = mergeKnownOrganizations([
...(Array.isArray(input.knownOrganizations) ? input.knownOrganizations : []),
activeOrganization
]);
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
if (
!input.filters.organization &&
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
activeOrganization
) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_from_active_scope")) {
input.warnings.push("organization_from_active_scope");
}
if (!input.baseReasons.includes("organization_from_active_scope")) {
input.baseReasons.push("organization_from_active_scope");
}
}
if (
resolvedOrganizationFromMessage &&
(!input.filters.organization || input.semanticFrame?.anchor_kind === "organization") &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, resolvedOrganizationFromMessage)
) {
input.filters.organization = resolvedOrganizationFromMessage;
if (!input.warnings.includes("organization_grounded_from_scope_candidates")) {
input.warnings.push("organization_grounded_from_scope_candidates");
}
if (!input.baseReasons.includes("organization_grounded_from_scope_candidates")) {
input.baseReasons.push("organization_grounded_from_scope_candidates");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
}
return resolvedOrganizationFromMessage;
}
function isHeuristicCandidatesIntent(intent: AddressIntent): boolean {
return (
intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" ||
intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract"
);
}
function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
return (
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date" ||
intent === "vat_liability_confirmed_for_tax_period"
);
}
function resolveAsOfDateBasis(filters: AddressFilterSet, semanticFrame?: AddressSemanticFrame | null): AddressAsOfDateBasis | null {
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
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: {
intent: AddressIntent;
selectedRecipe: string | null;
responseType: AddressResponseType;
rowsMatched: number;
}): AddressEvidenceStrength | undefined {
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: AddressIntent,
filters: AddressFilterSet,
semanticFrame?: AddressSemanticFrame | null
): AddressResultMode | undefined {
if (isConfirmedBalanceIntent(intent)) {
return "confirmed_balance";
}
if (intent === "list_open_contracts") {
return "heuristic_candidates";
}
if (isHeuristicCandidatesIntent(intent)) {
const asOfDateBasis = resolveAsOfDateBasis(filters, semanticFrame);
if (
asOfDateBasis === "explicit_as_of_date" ||
asOfDateBasis === "period_end" ||
asOfDateBasis === "period_range" ||
asOfDateBasis === "implicit_current_snapshot"
) {
return "confirmed_balance";
}
return "heuristic_candidates";
}
return undefined;
}
function deriveAddressResultSemantics(input: {
intent: AddressIntent;
selectedRecipe: string | null;
filters: AddressFilterSet;
semanticFrame?: AddressSemanticFrame | null;
responseType: AddressResponseType;
rowsMatched: number;
}): {
requested_result_mode?: AddressResultMode;
result_mode?: AddressResultMode;
evidence_strength?: AddressEvidenceStrength;
balance_confirmed?: boolean;
as_of_date_basis?: AddressAsOfDateBasis | null;
} {
const asOfDateBasis = resolveAsOfDateBasis(input.filters, input.semanticFrame);
const requestedResultMode = resolveRequestedResultMode(input.intent, input.filters, input.semanticFrame);
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 {};
}
type AddressResultSemantics = ReturnType<typeof deriveAddressResultSemantics>;
function mergeAddressResultSemantics(
base: AddressResultSemantics,
override: ComposeReplySemantics | undefined
): AddressResultSemantics {
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: string[],
requestedResultMode: AddressResultMode | undefined,
semantics: ComposeReplySemantics | undefined,
baseResultMode?: AddressResultMode
): string[] {
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: AddressIntent): AddressCapabilityAudit {
const decision = 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: {
intent: AddressIntent;
requestedResultMode: AddressResultMode | undefined;
filters: AddressFilterSet;
}): AddressShadowRouteAudit {
const shadowIntent = resolveShadowRouteIntent(input.intent, input.requestedResultMode);
if (!shadowIntent) {
return {
intent: null,
selectedRecipe: null,
status: "skipped"
};
}
const shadowRecipeSelection = 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: {
intent: AddressIntent;
selectedRecipe: string | null;
requestedResultMode?: AddressResultMode;
resultMode?: AddressResultMode;
}): AddressRouteExpectationAuditState {
if (!FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1) {
return {
status: "not_found",
reason: "route_expectation_audit_disabled",
expectedSelectedRecipes: [],
expectedRequestedResultModes: [],
expectedResultModes: []
};
}
const audit = 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: AddressRecipeExecutionPlan,
intent: AddressIntent
): AddressRecipeExecutionPlan {
if (intent === "list_open_contracts" && plan.recipe.recipe_id === "address_open_items_by_party_or_contract_v1") {
return plan;
}
const strictScopeIntents: AddressIntent[] = [
"list_receivables_counterparties",
"list_open_contracts",
"open_items_by_counterparty_or_contract"
];
const shouldEnforceStrictScope = strictScopeIntents.includes(intent);
if (!shouldEnforceStrictScope || plan.account_scope_mode === "strict") {
return plan;
}
return {
...plan,
account_scope_mode: "strict"
};
}
function resolveExecutionFiltersForConfirmedBalance(
filters: AddressFilterSet,
analysisDate: string | null
): {
executionFilters: AddressFilterSet;
asOfDerived: string | null;
} {
const explicitAsOf = normalizeAnalysisDateHint(filters.as_of_date);
const periodTo = normalizeAnalysisDateHint(filters.period_to);
const derivedAsOf = explicitAsOf ?? periodTo ?? analysisDate ?? null;
const executionFilters: AddressFilterSet = {
...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: string | null, filters: AddressFilterSet): string | null {
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: string | null | undefined): boolean {
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: string): string | null {
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: NormalizedAddressRow[],
intent: AddressIntent,
referenceDate: string | null
): { rows: NormalizedAddressRow[]; droppedCount: number } {
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: NormalizedAddressRow[] = [];
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: AddressFilterSet): boolean {
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: AddressIntent, filters: AddressFilterSet): boolean {
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" ||
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
}
function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
return (
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
}
function shouldClearAsOfDateForHistoryRecovery(intent: AddressIntent): boolean {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item";
}
function invertSort(sort: AddressFilterSet["sort"]): AddressFilterSet["sort"] {
return sort === "period_asc" ? "period_desc" : "period_asc";
}
function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
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: AddressIntent): boolean {
return (
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract"
);
}
function toIsoDatePrefix(value: string | null): string | null {
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: NormalizedAddressRow[]): { period_from: string | null; period_to: string | null } {
const dates = rows
.map((row) => toIsoDatePrefix(row.period))
.filter((item): item is string => 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: AddressFilterSet,
observed: { period_from: string | null; period_to: string | null }
): string {
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 injectNoticeAfterLeadLine(text: string, notice: string): string {
const normalizedText = typeof text === "string" ? text : "";
const normalizedNotice = typeof notice === "string" ? notice.trim() : "";
if (!normalizedText.trim()) {
return normalizedNotice;
}
if (!normalizedNotice) {
return normalizedText;
}
const lines = normalizedText.split("\n");
if (lines.length <= 1) {
return `${lines[0]}\n${normalizedNotice}`;
}
return [lines[0], normalizedNotice, ...lines.slice(1)].join("\n");
}
function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCategory): AddressRuntimeReadiness {
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: string): string {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements: Array<[RegExp, string]> = [
[/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: string): string {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements: Array<[RegExp, string]> = [
[/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();
}
interface RowStageDiagnostics {
rawRowKeysSample: string[];
materializationDropReason:
| "none"
| "dropped_by_account_scope_filter"
| "missing_period_and_registrator_fields"
| "missing_period_field"
| "missing_registrator_field"
| "unknown_row_shape";
}
interface AccountScopeAuditDebug {
accountTokenRaw: string | null;
accountTokenNormalized: string | null;
accountScopeFieldsChecked: string[];
accountScopeMatchStrategy: "account_code_regex_plus_alias_map_v1";
accountScopeDropReason:
| "not_applicable"
| "no_account_scope_requested"
| "no_rows_after_scope_filter"
| "rows_remaining_after_scope_filter";
}
function rowHasNonEmptyField(row: Record<string, unknown>, keys: string[]): boolean {
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
}
function deriveRowStageDiagnostics(
rawRows: Array<Record<string, unknown>>,
rowsAfterAccountScope: number,
rowsMaterialized: number
): RowStageDiagnostics {
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: AddressIntent): boolean {
return intent === "account_balance_snapshot" || intent === "documents_forming_balance";
}
function buildDefaultAccountScopeAudit(filters: AddressFilterSet): AccountScopeAuditDebug {
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: {
intent: AddressIntent;
filters: AddressFilterSet;
accountScope: string[];
rowsBeforeScope: number;
rowsAfterScope: number;
}): AccountScopeAuditDebug {
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: {
skipped?: boolean;
errored?: boolean;
rawRowsReceived: number;
rowsMaterialized: number;
rowsAnchorMatched: number;
rowsMatched: number;
}): AddressMcpCallStatus {
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: AddressMcpCallStatus
): "skipped" | "error" | "no_raw_rows" | "raw_rows_received_but_not_materialized" | "materialized_but_not_matched" | "matched_non_empty" {
if (status === "materialized_but_not_anchor_matched" || status === "materialized_but_filtered_out_by_recipe") {
return "materialized_but_not_matched";
}
return status;
}
function pickDeterministicVariant(seed: string, variants: string[]): string {
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: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function isWeakOfferAnchorValue(value: string): boolean {
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: string): string {
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 === "item") {
return "товар";
}
if (anchor === "warehouse") {
return "склад";
}
if (anchor === "period" || anchor === "period_from" || anchor === "period_to" || anchor === "as_of_date") {
return "период/дата";
}
return anchor.replace(/_/gu, " ");
}
function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
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: string[] = [];
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: AddressFilterSet): string {
const seedParts: string[] = [];
const keys: Array<keyof AddressFilterSet> = [
"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: {
category: AddressLimitedReasonCategory;
shape: AddressQueryShapeDetection;
intent: AddressIntent;
filters: AddressFilterSet;
missingRequiredFilters: string[];
reason: string;
nextStep?: string;
}): string[] {
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: string[] = [];
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 === "inventory_on_hand_as_of_date") {
offers.push("показать подтвержденный срез товаров на складах на дату по остатку счета 41.01");
} else if (input.intent === "inventory_purchase_provenance_for_item") {
offers.push("показать подтвержденные закупочные движения по товару на 41.01 с датами и документами");
} else if (input.intent === "inventory_purchase_documents_for_item") {
offers.push("показать документы поступления по товару на 41.01");
} else if (input.intent === "inventory_sale_trace_for_item") {
offers.push("показать подтвержденные движения выбытия товара со счета 41.01");
} else if (input.intent === "inventory_purchase_to_sale_chain") {
offers.push("показать документальную цепочку по товару: поступление на 41.01 и последующее выбытие");
} else if (input.intent === "open_contracts_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр договоров с открытыми взаиморасчетами на дату по 60/62/76");
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
} else if (input.intent === "vat_liability_confirmed_for_tax_period") {
offers.push("показать подтвержденный расчет НДС к уплате за налоговый период по книгам продаж/покупок");
} 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: {
intent: AddressIntent;
shape: AddressQueryShapeDetection;
}): string | null {
const byIntent: Partial<Record<AddressIntent, string>> = {
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: "Сигнал запроса: нужен список незакрытых договоров на дату.",
open_contracts_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный список договоров с открытыми взаиморасчетами на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
inventory_on_hand_as_of_date: "Сигнал запроса: нужен подтвержденный срез товаров на складе на дату.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
vat_liability_confirmed_for_tax_period: "Сигнал запроса: нужен подтвержденный расчет НДС к уплате за налоговый период."
};
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
DOCUMENT_LIST: "Сигнал запроса: список документов/операций.",
OBJECT_LOOKUP: "Сигнал запроса: поиск конкретных объектов.",
VERIFY_FACTUAL: "Сигнал запроса: проверка фактического состояния по данным.",
COMPOUND_FACTUAL_QUERY: "Сигнал запроса: комбинированная проверка взаимосвязанных фактов."
};
return byIntent[input.intent] ?? byShape[input.shape.shape] ?? null;
}
function hasAggregateLimitedSignal(input: {
shape: AddressQueryShapeDetection;
intent: AddressIntent;
reason: string;
}): boolean {
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: {
category: AddressLimitedReasonCategory;
reason: string;
nextStep?: string;
shape: AddressQueryShapeDetection;
intent: AddressIntent;
filters: AddressFilterSet;
missingRequiredFilters: string[];
}): string {
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: {
mode: { mode: "address_query" | "deep_analysis" | "unsupported"; confidence: "high" | "medium" | "low"; reasons: string[] };
shape: AddressQueryShapeDetection;
intent: { intent: AddressIntent; confidence: "high" | "medium" | "low"; reasons: string[] };
filters: AddressFilterSet;
missingRequiredFilters: string[];
selectedRecipe: string | null;
accountScopeMode?: "strict" | "preferred";
accountScopeFallbackApplied?: boolean;
accountScopeAudit?: AccountScopeAuditDebug;
anchor?: AnchorResolutionDebug;
matchFailureStage?: AddressMatchFailureStage;
matchFailureReason?: string | null;
mcpCallStatus: AddressMcpCallStatus;
rowsFetched: number;
rawRowsReceived?: number;
rowsAfterAccountScope?: number;
rowsAfterRecipeFilter?: number;
rowsMaterialized?: number;
rowsMatched: number;
rawRowKeysSample?: string[];
materializationDropReason?:
| "none"
| "dropped_by_account_scope_filter"
| "missing_period_and_registrator_fields"
| "missing_period_field"
| "missing_registrator_field"
| "unknown_row_shape";
limitations: string[];
reasons: string[];
reasonText: string;
nextStep?: string;
category: AddressLimitedReasonCategory;
capabilityAudit?: AddressCapabilityAudit;
shadowRouteAudit?: AddressShadowRouteAudit;
routeExpectationAudit?: AddressRouteExpectationAuditState;
semanticFrame?: AddressSemanticFrame | null;
}): AddressExecutionResult {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
const resultSemantics = deriveAddressResultSemantics({
intent: input.intent.intent,
selectedRecipe: input.selectedRecipe,
filters: input.filters,
semanticFrame: input.semanticFrame,
responseType: "LIMITED_WITH_REASON",
rowsMatched: input.rowsMatched
});
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters, input.semanticFrame);
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(
input.reasons,
requestedResultMode,
undefined,
resultSemantics.result_mode
);
const exactLimitedReason =
input.intent.intent === "inventory_on_hand_as_of_date"
? "exact_inventory_mode_limited_response"
: 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"
: input.intent.intent === "vat_liability_confirmed_for_tax_period"
? "exact_vat_tax_period_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,
semantic_frame: input.semanticFrame ?? null,
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
}
};
}
export class AddressQueryService {
public async tryHandle(userMessage: string, options: AddressTryHandleOptions = {}): Promise<AddressExecutionResult | null> {
if (!FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
return null;
}
const followupContext = options.followupContext ?? null;
const decompose = runAddressDecomposeStage(userMessage, followupContext, options.llmSemanticHints ?? null);
if (!decompose) {
return null;
}
const { mode, shape, intent, filters } = decompose;
const semanticFrame = filters.semantic_frame ?? null;
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 resolvedOrganizationFromMessage = applyPreExecutionOrganizationScopeGrounding({
userMessage,
filters: filters.extracted_filters,
semanticFrame,
warnings: filters.warnings,
baseReasons,
activeOrganization: options.activeOrganization ?? null,
knownOrganizations: options.knownOrganizations ?? []
});
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame);
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 confirmedBalanceInventoryIntent =
intent.intent === "inventory_on_hand_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 inventoryConfirmedExecution = confirmedBalanceInventoryIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
: null;
let executionFilters =
inventoryConfirmedExecution?.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");
}
}
if (
inventoryConfirmedExecution?.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_inventory_on_hand")) {
filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
}
if (!baseReasons.includes("as_of_date_derived_for_inventory_on_hand")) {
baseReasons.push("as_of_date_derived_for_inventory_on_hand");
}
}
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({
intent: intent.intent,
requestedResultMode,
filters: executionFilters
});
if (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,
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped"
],
semanticFrame,
capabilityAudit,
shadowRouteAudit
});
}
const composeOptionsFromFilters = (
filterSet: AddressFilterSet,
options: {
vatDirectSourceProbe?: VatDirectSourceProbeSummary | null;
emphasizeNumbers?: boolean;
useRubCurrency?: boolean;
} = {}
) => ({
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 = 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 = 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 === "inventory_on_hand_as_of_date" &&
!baseReasons.includes("confirmed_balance_exact_inventory_intent")
) {
baseReasons.push("confirmed_balance_exact_inventory_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 (
intent.intent === "vat_liability_confirmed_for_tax_period" &&
!baseReasons.includes("confirmed_balance_exact_vat_tax_period_intent")
) {
baseReasons.push("confirmed_balance_exact_vat_tax_period_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,
semanticFrame,
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],
semanticFrame,
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],
semanticFrame,
capabilityAudit,
shadowRouteAudit
});
}
if (!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,
semanticFrame,
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 = 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: AddressIntent = intent.intent;
let routeExpectationIntent: AddressIntent = intent.intent;
let plan = enforceStrictAccountScopeForIntent(
buildAddressRecipePlan(recipeSelection.selected_recipe, executionFilters),
intent.intent
);
let mcp = await 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" ||
plan.recipe.recipe_id === "address_open_contracts_candidates_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 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 = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
const fallbackPlan = enforceStrictAccountScopeForIntent(
buildAddressRecipePlan(fallbackSelection.selected_recipe, executionFilters),
intent.intent
);
const fallbackMcp = await 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],
semanticFrame,
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 = refineAnchorFromRows(anchor, normalizedRows);
let filtersForMatching: AddressFilterSet =
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
});
let anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
let filterByAnchors = anchorFilter.rows;
let filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
let filteredRowsFutureGuard = applyFutureDatedRowsGuard(
filteredRowsBeforeFutureGuard,
intent.intent,
futureGuardReferenceDate
);
let filteredRows = filteredRowsFutureGuard.rows;
let organizationWarehouseRecoveryApplied = false;
if (
filteredRows.length === 0 &&
anchorFilter.mismatchReason === "warehouse_anchor_not_matched_in_materialized_rows" &&
resolvedOrganizationFromMessage
) {
filters.extracted_filters = {
...filters.extracted_filters,
organization: resolvedOrganizationFromMessage
};
delete filters.extracted_filters.warehouse;
executionFilters = {
...executionFilters,
organization: resolvedOrganizationFromMessage
};
delete executionFilters.warehouse;
filtersForMatching = {
...filtersForMatching,
organization: resolvedOrganizationFromMessage
};
delete filtersForMatching.warehouse;
anchor = {
...anchor,
anchor_type: "organization",
anchor_value_raw: anchor.anchor_value_raw,
anchor_value_resolved: resolvedOrganizationFromMessage,
resolver_confidence: "medium"
};
if (semanticFrame) {
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
if (!filters.warnings.includes("warehouse_anchor_regrounded_to_organization_scope")) {
filters.warnings.push("warehouse_anchor_regrounded_to_organization_scope");
}
if (!baseReasons.includes("warehouse_anchor_regrounded_to_organization_scope")) {
baseReasons.push("warehouse_anchor_regrounded_to_organization_scope");
}
anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
filterByAnchors = anchorFilter.rows;
filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
filteredRowsFutureGuard = applyFutureDatedRowsGuard(
filteredRowsBeforeFutureGuard,
intent.intent,
futureGuardReferenceDate
);
filteredRows = filteredRowsFutureGuard.rows;
organizationWarehouseRecoveryApplied = filteredRows.length > 0;
}
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: AddressMatchFailureStage =
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 (organizationWarehouseRecoveryApplied) {
if (!baseReasons.includes("organization_scope_live_grounding_recovered_rows")) {
baseReasons.push("organization_scope_live_grounding_recovered_rows");
}
}
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 = 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: 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,
semanticFrame,
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: AddressFilterSet = {
...executionFilters,
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
};
const expandedSelection = selectAddressRecipe(intent.intent, expandedLimitFilters);
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
const expandedPlan = buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters);
if (expandedPlan.limit > currentLimit) {
const expandedMcp = await 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 = resolvePrimaryAnchor(intent.intent, expandedLimitFilters);
expandedAnchor = refineAnchorFromRows(expandedAnchor, expandedNormalizedRows);
const expandedFiltersForMatching: AddressFilterSet =
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 = 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: 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,
semanticFrame,
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: AddressFilterSet = { ...filters.extracted_filters };
const broadenedAdjustments: string[] = [];
delete autoBroadenedFilters.period_from;
delete autoBroadenedFilters.period_to;
if (stageStatus === "no_raw_rows" && shouldClearAsOfDateForHistoryRecovery(intent.intent) && toNonEmptyFilterValue(autoBroadenedFilters.as_of_date)) {
delete autoBroadenedFilters.as_of_date;
broadenedAdjustments.push("as_of_date_cleared_for_history_recovery");
}
if (shouldBoostAutoBroadenedLimit(intent.intent)) {
autoBroadenedFilters.limit = Math.max(
ADDRESS_ANCHOR_RECOVERY_LIMIT,
typeof autoBroadenedFilters.limit === "number" && Number.isFinite(autoBroadenedFilters.limit)
? Math.max(1, Math.trunc(autoBroadenedFilters.limit))
: 0
);
}
const broadenedSelection = selectAddressRecipe(intent.intent, autoBroadenedFilters);
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
const broadenedPlan = buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters);
const broadenedMcp = await 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 = resolvePrimaryAnchor(intent.intent, autoBroadenedFilters);
broadenedAnchor = refineAnchorFromRows(broadenedAnchor, broadenedNormalizedRows);
const broadenedFiltersForMatching: AddressFilterSet =
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 = composeFactualReply(
intent.intent,
broadenedFilteredRows,
composeOptionsFromFilters(autoBroadenedFilters)
);
const broadenedLimitations = [
...filters.warnings,
...broadenedAdjustments,
"period_window_auto_broadened_to_available_data"
];
const broadenedReasons = [...baseReasons, ...broadenedAdjustments, "period_window_auto_broadened_to_available_data"];
const broadenedResultSemantics = mergeAddressResultSemantics(
deriveAddressResultSemantics({
intent: intent.intent,
selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
filters: filters.extracted_filters,
semanticFrame,
responseType: broadenedFactual.responseType,
rowsMatched: broadenedFilteredRows.length
}),
broadenedFactual.semantics
);
const broadenedRouteExpectationAudit = buildRouteExpectationAudit({
intent: routeExpectationIntent,
selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
requestedResultMode,
resultMode: broadenedResultSemantics.result_mode
});
return {
handled: true,
reply_text: injectNoticeAfterLeadLine(broadenedFactual.text, broadenedPrefix),
reply_type: 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,
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: broadenedRouteExpectationAudit.status,
route_expectation_reason: broadenedRouteExpectationAudit.reason,
route_expectation_expected_selected_recipes: broadenedRouteExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes:
broadenedRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: broadenedRouteExpectationAudit.expectedResultModes,
semantic_frame: semanticFrame,
...broadenedResultSemantics,
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: AddressFilterSet = {
...filters.extracted_filters,
sort: invertSort(filters.extracted_filters.sort),
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
};
const historicalSelection = selectAddressRecipe(intent.intent, historicalFilters);
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
const historicalPlan = buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters);
const historicalMcp = await 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 = resolvePrimaryAnchor(intent.intent, historicalFilters);
historicalAnchor = refineAnchorFromRows(historicalAnchor, historicalNormalizedRows);
const historicalFiltersForMatching: AddressFilterSet =
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 = 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: 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,
semanticFrame,
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 = 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: 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,
semanticFrame,
responseType: fallbackFactual.responseType,
rowsMatched: documentBankFallbackRows.length
}),
fallbackFactual.semantics
),
limitations: fallbackLimitations,
reasons: withConfirmedBalanceFallbackReason(
fallbackReasons,
requestedResultMode,
fallbackFactual.semantics
)
}
};
}
}
const allowConfirmedAsOfZeroSnapshot =
filteredRows.length === 0 &&
(composeIntent === "vat_payable_confirmed_as_of_date" ||
composeIntent === "open_contracts_confirmed_as_of_date" ||
composeIntent === "payables_confirmed_as_of_date" ||
composeIntent === "receivables_confirmed_as_of_date") &&
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
!toNonEmptyFilterValue(filters.extracted_filters.counterparty) &&
!toNonEmptyFilterValue(filters.extracted_filters.contract) &&
!toNonEmptyFilterValue(filters.extracted_filters.document_ref);
if (filteredRows.length === 0 && !allowConfirmedAsOfZeroSnapshot) {
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: AddressLimitedReasonCategory = isFollowupAnchorCarryover
? "empty_match"
: anchorMismatchByCounterparty || anchorMismatchByContract
? "missing_anchor"
: !isLowQualityPartyAnchor
? "empty_match"
: "missing_anchor";
const category: AddressLimitedReasonCategory = 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,
semanticFrame,
capabilityAudit,
shadowRouteAudit
});
}
const vatProbeRequired =
composeIntent === "vat_payable_confirmed_as_of_date" ||
composeIntent === "vat_liability_confirmed_for_tax_period" ||
(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 === "vat_liability_confirmed_for_tax_period" ||
composeIntent === "payables_confirmed_as_of_date" ||
composeIntent === "receivables_confirmed_as_of_date";
const shouldUseRubCurrency =
composeIntent === "vat_payable_forecast" || composeIntent === "vat_liability_confirmed_for_tax_period";
const factual = 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,
semanticFrame,
responseType: factual.responseType,
rowsMatched: filteredRows.length
}),
factual.semantics
);
const finalRouteExpectationAudit = buildRouteExpectationAudit({
intent: routeExpectationIntent,
selectedRecipe: effectiveRecipeId,
requestedResultMode,
resultMode: factualResultSemantics.result_mode
});
if (finalRouteExpectationAudit.status === "mismatch" && 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}`],
semanticFrame,
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") ||
(intent.intent === "vat_liability_confirmed_for_tax_period" &&
composeIntent === "vat_liability_confirmed_for_tax_period");
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
const exactModeName =
intent.intent === "payables_confirmed_as_of_date"
? "payables"
: intent.intent === "receivables_confirmed_as_of_date"
? "receivables"
: intent.intent === "vat_liability_confirmed_for_tax_period"
? "vat_tax_period"
: "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"
: intent.intent === "vat_liability_confirmed_for_tax_period"
? "specify tax period boundaries and ensure purchase/sales VAT books are available via MCP"
: "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`],
semanticFrame,
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: 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,
semantic_frame: semanticFrame,
...factualResultSemantics,
limitations: factualLimitations,
reasons: withConfirmedBalanceFallbackReason(
reasonsWithRouteExpectation,
requestedResultMode,
factual.semantics,
factualResultSemantics.result_mode
)
}
};
}
}